2c5d3f462a6a6d47d933be8897653bb3c3d991a2
[gnucomo.git] / src / gcm_input / message.cpp
1
2 /**************************************************************************
3 **  (c) Copyright 2002, Andromeda Technology & Automation
4 ** This is free software; you can redistribute it and/or modify it under the
5 ** terms of the GNU General Public License, see the file COPYING.
6 ***************************************************************************
7 ** MODULE INFORMATION *
8 ***********************
9 **      FILE NAME      : message.cpp
10 **      SYSTEM NAME    : Gnucomo - Gnu Computer Monitoring
11 **      VERSION NUMBER : $Revision: 1.12 $
12 **
13 **  DESCRIPTION      :  Implementation of the message handling classes
14 **
15 **  EXPORTED OBJECTS : 
16 **  LOCAL    OBJECTS : 
17 **  MODULES  USED    :
18 ***************************************************************************
19 **  ADMINISTRATIVE INFORMATION *
20 ********************************
21 **      ORIGINAL AUTHOR : Arjen Baart - arjen@andromeda.nl
22 **      CREATION DATE   : Sep 16, 2002
23 **      LAST UPDATE     : Jul 24, 2003
24 **      MODIFICATIONS   : 
25 **************************************************************************/
26
27 /*****************************
28    $Log: message.cpp,v $
29    Revision 1.12  2003-08-11 16:56:16  arjen
30    Different kinds of log files are parsed by a collection of objects
31    of different classes, derived from the base class line_cooker
32    Depending on the message content or the message_type element in
33    XML, one of these objects is selected.
34
35    Logrunner is integrated with gcm_input. Although its functionality
36    is still limited, a connection between logrunner and gcm_input
37    is beginning to form.
38
39    Revision 1.11  2003/08/05 08:15:00  arjen
40    Debug output to the log stream instead of cerr.
41    Fixed namespace problems in XPath searches of the DOM.
42    Moved string utility functions to a separate file.
43
44    Revision 1.10  2003/04/29 09:16:44  arjen
45    Read XML input,
46    Only cooked log entries for now.
47
48    Revision 1.9  2003/03/29 09:04:10  arjen
49    Extract the hostname out of the 'From:' or 'Message-Id:' line
50    of an email header.
51
52    Revision 1.8  2003/03/16 09:42:40  arjen
53    Read IRIX system logs.
54
55    Revision 1.7  2003/02/21 08:08:05  arjen
56    Gcm_input also detects packages that are removed from the system.
57    Determining the version number of a package in a RPM
58    list is improved. Only the last one or two parts of the string that
59    begin with a '-' and a number are considered the version.
60
61    Revision 1.6  2003/02/05 09:37:51  arjen
62    Create notifications when a new package is discovered
63    in a 'rpm -qa' list or when the version of a package is changed.
64
65    Revision 1.4  2002/12/06 22:26:28  arjen
66    Set the value of log.processed to FALSE when inserting a
67    new log entry into the database
68    When a syslog entry arrives from last year, gcm_input subtracts one from the
69    year of arrival to create the year of the log entry.
70    Read output from "rpm -qa" and enter packages in the parameter table.
71
72    Revision 1.3  2002/11/09 08:04:27  arjen
73    Added a reference to the GPL
74
75    Revision 1.2  2002/11/04 10:13:36  arjen
76    Use proper namespace for iostream classes
77
78    Revision 1.1  2002/10/05 10:25:49  arjen
79    Creation of gcm_input and a first approach to a web interface
80
81 *****************************/
82
83 static const char *RCSID = "$Id: message.cpp,v 1.12 2003-08-11 16:56:16 arjen Exp $";
84
85 #include <algorithm>
86 #include <libxml/xpath.h>
87 #include <libxml/debugXML.h>
88 #include "message.h"
89
90 //#define DEBUG
91
92 extern bool verbose;   /*  Defined in the main application */
93 extern bool testmode;
94 extern bool incremental;
95 extern std::ostream *log;
96
97 /*   Utility functions   */
98
99 extern String SQL_Escape(String s);
100
101 /*=========================================================================
102 **  NAME           : operator >>
103 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
104 **  PARAMETERS     : 
105 **  RETURN VALUE   : True if input was available.
106 **
107 **  DESCRIPTION    : Input operator. Read the next line from the message.
108 **
109 **  VARS USED      :
110 **  VARS CHANGED   :
111 **  FUNCTIONS USED :
112 **  SEE ALSO       :
113 **  LAST MODIFIED  : Nov 04, 2002
114 **=========================================================================
115 */
116
117 bool operator >> (message_buffer &b, String &s)
118 {
119    bool   input_ok = false;
120
121    if (b.next_line == b.buffer.end())
122    {
123       String   l;
124
125       if (*(b.input) >> l)
126       {
127          b.buffer.push_back(l);
128
129          //   next_line keeps pointing to the end.
130
131          s = l;
132          input_ok = true;
133       }
134    }
135    else
136    {
137       s = *(b.next_line);
138       b.next_line++;
139       input_ok = true;
140    }
141    return input_ok;
142 }
143
144 client_message::client_message(std::istream *in, gnucomo_database db)
145 {
146    input.from(in);
147    database = db;
148
149    hostname = "";
150    mail_header   = false;
151    gpg_encrypted = false;
152    classification = UNKNOWN;
153    xmlDom         = NULL;
154    certainty      = 0.0;
155 }
156
157 static const String syslog_date_re("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
158 static const String mail_date_re("[[:alpha:]]{3}, [ 123]?[0-9] [[:alpha:]]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [+-][0-9]{4}");
159 static const String unix_date_re("[[:alpha:]]{3} [[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}");
160 static const String email_address_re("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
161
162 static const regex re_syslog(syslog_date_re + " [[:alnum:]]+ [[:alpha:]]+.*:.+");
163 static const regex re_syslog_irix(syslog_date_re + " [0-7][A-T]:[[:alnum:]]+ [[:alpha:]]+.*:.+");
164 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
165 static const regex re_dump("^ *DUMP: Date of this level");
166 static const regex re_accesslog("(GET|POST) .+ HTTP");
167 static const regex re_errorlog("^\\[" + unix_date_re + "\\] \\[(error|notice)\\] .+");
168 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
169
170 static const regex re_uxmail_from("^From [^ \t]+[ ]+" + unix_date_re);
171 static const regex re_mail_From("^From:[[:blank:]]+");
172 static const regex re_mail_Date("^Date:[[:blank:]]+" + mail_date_re);
173 static const regex re_mail_MsId("^Message-Id:[[:blank:]]+");
174 static const regex re_email_address("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
175 static const regex re_email_user("[[:alnum:]_.-]+@");
176 static const regex re_xml_header("xml .*\?>$");
177
178 /*=========================================================================
179 **  NAME           : readXMLinput
180 **  SYNOPSIS       : int readXMLinput(String first_line)
181 **  PARAMETERS     : 
182 **  RETURN VALUE   : Parse the XML input and extract the header information
183 **
184 **  DESCRIPTION    : 
185 **
186 **  VARS USED      :
187 **  VARS CHANGED   :
188 **  FUNCTIONS USED :
189 **  SEE ALSO       :
190 **  LAST MODIFIED  : Jul 24, 2003
191 **=========================================================================
192 */
193
194 int client_message::readXMLinput(String first_line)
195 {
196    xmlParserCtxtPtr ctxt;
197    String           line;
198    xmlNodePtr       root, item;
199    xmlNsPtr         namespaces[1];
200
201    xmlXPathObjectPtr res;
202    xmlXPathContextPtr pathcontext;
203
204
205    ctxt = xmlCreatePushParserCtxt(NULL, NULL, first_line, ~first_line, NULL);
206    while (input >> line)
207    {
208       xmlParseChunk(ctxt, line, ~line, 0);
209    }
210    xmlParseChunk(ctxt, "", 0, 1);
211    xmlDom = ctxt->myDoc;
212    xmlFreeParserCtxt(ctxt);
213
214    root = xmlDocGetRootElement(xmlDom);
215    namespaces[0] = root->ns;
216
217    //TODO Ought to check  root->name and  root->ns->href 
218
219    pathcontext = xmlXPathNewContext(xmlDom);
220    pathcontext->node = xmlDocGetRootElement(xmlDom);
221    pathcontext->namespaces = namespaces;
222    pathcontext->nsNr       = 1;
223
224 #ifdef DEBUG
225    xmlDebugDumpNodeList(stdout, pathcontext->node, 0);
226 #endif
227
228    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:messagetype/text()", pathcontext);
229    if (res->nodesetval != NULL)
230    {
231 #ifdef DEBUG
232       xmlDebugDumpNodeList(stdout, *res->nodesetval->nodeTab, 0);
233 #endif
234       item = *res->nodesetval->nodeTab;
235
236       //  Select a line cooker based on the message type.
237
238 #ifdef DEBUG
239       std::cout << "Looking for a line cooker for " << item->content << "\n";
240 #endif
241       list<line_cooker *>::iterator lci = kitchen.begin();
242       pan = 0;
243       while (pan == 0 && lci != kitchen.end())
244       {
245          pan = *lci;
246          if (pan->message_type() != (const char *)(item->content))
247          {
248             pan = 0;
249          }
250          lci++;
251       }
252       if (pan == 0)
253       {
254          *log << "Can not find a line cooker for message type " << item->content << "\n";
255       }
256    }
257    else
258    {
259       *log << "Message type not found in XML header.\n";
260    }
261
262    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:hostname/text()", pathcontext);
263    if (res->nodesetval != NULL)
264    {
265 #ifdef DEBUG
266       xmlDebugDumpNodeList(stdout, *res->nodesetval->nodeTab, 0);
267 #endif
268       item = *res->nodesetval->nodeTab;
269       hostname = (const char *)item->content;
270    }
271    else
272    {
273       *log << "Hostname not found in XML header.\n";
274    }
275
276    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:service/text()", pathcontext);
277    if (res->nodesetval != NULL)
278    {
279       item = *res->nodesetval->nodeTab;
280       service = (const char *)item->content;
281    }
282    res = xmlXPathEval((const xmlChar *)"gcmt:header/gcmt:time/text()", pathcontext);
283    if (res->nodesetval != NULL)
284    {
285       item = *res->nodesetval->nodeTab;
286       arrival = String((char *)item->content);
287
288    }
289    //xmlDebugDumpNodeList(stdout, *res->nodesetval->nodeTab, 0);
290
291 }
292
293 /*=========================================================================
294 **  NAME           : classify
295 **  SYNOPSIS       : double classify(String host, date arriv_d, hour arriv_t, String serv)
296 **  PARAMETERS     : 
297 **  RETURN VALUE   : The certainty with which the message is classified.
298 **
299 **  DESCRIPTION    : 
300 **
301 **  VARS USED      :
302 **  VARS CHANGED   :
303 **  FUNCTIONS USED :
304 **  SEE ALSO       :
305 **  LAST MODIFIED  : Aug 11, 2003
306 **=========================================================================
307 */
308
309 double client_message::classify(String host, UTC arriv, String serv)
310 {
311    String    line;
312
313    hostname    = host;
314    arrival     = arriv;
315    service     = serv;
316
317    /*  First, check if the message has a mail header. */
318
319    if (input >> line && line == re_uxmail_from)
320    {
321       String    from_address;
322
323       mail_header = true;
324
325       /*  Scan ahead for the hostname and date of arrival.  */
326
327       while (input >> line && line != "")
328       {
329          if (line == re_mail_From)
330          {
331             from_address = line(re_email_address);
332             from_address(re_email_user) = "";            //  Remove the user part;
333             if (from_address != "" && ~hostname < ~from_address)
334             {
335                *log << "Detected hostname " << from_address << "\n";
336                hostname = from_address;
337             }
338          }
339          if (line == re_mail_MsId)
340          {
341             from_address = line(re_email_address);
342             from_address(re_email_user) = "";            //  Remove the user part;
343             if (from_address != "" && ~hostname < ~from_address)
344             {
345                *log << "Detected hostname " << from_address << "\n";
346                hostname = from_address;
347             }
348          }
349          if (line == re_mail_Date)
350          {
351             arrival = UTC(line(regex(mail_date_re)));
352          }
353       }
354    }
355    else
356    {
357       //  Push the first line back, we need to read it again.
358       --input;
359
360    }
361
362    pan = 0;
363
364    /*
365     *  Now that we have the mail header out of the way, try to figure
366     *  out what the content of the message is.
367     */
368
369
370    while (input >> line && certainty < 0.9)
371    {
372       if (verbose)
373       {
374          *log << "  testing: " << line << "\n";
375       }
376
377       if (line == re_xml_header)
378       {
379          certainty = 1.0;
380          classification = XML;
381          if (verbose)
382          {
383             *log << "XML input detected.\n";
384          }
385          readXMLinput(line);
386       }
387       else if (line == re_PGP)
388       {
389          certainty = 1.0;
390          gpg_encrypted = true;
391          *log << "The message is PGP/GnuPG encrypted.\n";
392       }
393       else if (line == re_dump)
394       {
395           certainty = 1.0;
396           if (verbose)
397           {
398              *log << "DUMP output detected.\n";
399           }
400       }
401       else if (line == re_rpm)
402       {
403           certainty = 1.0;
404           classification = RPMLIST;
405           service = "";
406           if (verbose)
407           {
408              *log << "RPM package list detected.\n";
409           }
410       }
411       else
412       {
413          //  Scan the list of line cookers if there is anything familiar.
414
415          list<line_cooker *>::iterator lci = kitchen.begin();
416          pan = 0;
417          while (pan == 0 && lci != kitchen.end())
418          {
419             pan = *lci;
420             if (!pan->check_pattern(line))
421             {
422                pan = 0;
423             }
424             lci++;
425          }
426          if (pan != 0)
427          {
428             certainty = 1.0;
429             classification = COOKER_OBJECT;
430             if (verbose)
431             {
432                *log << "Detected message type " << pan->message_type() << "\n";
433             }
434          }
435       }
436    }
437    input.rewind();
438
439    if (hostname == "")
440    {
441       *log <<  "Can not determine the hostname where the message came from.\n";
442       certainty = 0.0;
443    }
444    else if (!arrival.proper())
445    {
446       *log << "Arrival time is not knwon.\n";
447       certainty = 0.0;
448    }
449    else
450    {
451       certainty = 1.0;
452    }
453
454    return certainty;
455 }
456
457 /*=========================================================================
458 **  NAME           : enterXML
459 **  SYNOPSIS       : int enterXML()
460 **  PARAMETERS     : 
461 **  RETURN VALUE   : None
462 **
463 **  DESCRIPTION    : Analyze the DOM tree from the XML input.
464 **                   The DOM tree was previously parsed by readXMLinput().
465 **
466 **  VARS USED      :
467 **  VARS CHANGED   :
468 **  FUNCTIONS USED :
469 **  SEE ALSO       :
470 **  LAST MODIFIED  : Jul 24, 2003
471 **=========================================================================
472 */
473
474 void client_message::enterXML()
475 {
476    xmlXPathObjectPtr res;
477    xmlXPathContextPtr pathcontext;
478    xmlNsPtr           namespaces[1];
479
480    /*  Try to find the host in the database */
481
482    String objectid;
483
484    objectid = database.find_host(hostname);
485    if (objectid == "")
486    {
487       *log << "Please define the host " << hostname << " in the database.\n";
488       return;
489    }
490    if (verbose)
491    {
492       *log << "Object id for " << hostname << " is " << objectid << "\n";
493    }
494
495    pathcontext = xmlXPathNewContext(xmlDom);
496    pathcontext->node = xmlDocGetRootElement(xmlDom);
497    namespaces[0] = pathcontext->node->ns;
498    pathcontext->namespaces = namespaces;
499    pathcontext->nsNr       = 1;
500
501    res = xmlXPathEval((const xmlChar *)"gcmt:data/node()", pathcontext);
502
503    if (res->nodesetval != NULL)
504    {
505       //  Find the first child element of the <data> element.
506
507       xmlNodePtr  node = *res->nodesetval->nodeTab;
508       while (node->type != XML_ELEMENT_NODE)
509       {
510          node = node->next;
511       }
512       if (strcmp((char *)node->name, "log") == 0)
513       {
514          //  Each child contains a log entry, raw or cooked.
515
516          node = node->children;
517          while (node != NULL)
518          {
519             if (node->type == XML_ELEMENT_NODE)
520             {
521                xmlNodePtr  item;
522                String      log_hostname;
523                UTC         log_date;
524                String      raw("");;
525                String      log_service;
526
527                if (strcmp((char *)node->name, "raw") == 0)
528                {
529                   item = node->children;
530                   if (pan == 0)
531                   {
532                      *log << "Can not cook this type of <raw> log element.\n";
533                   }
534                   else
535                   {
536                      raw = String((const char *)item->content);
537                      if (pan->cook_this(raw, arrival))
538                      {
539                         log_hostname = pan->hostname();
540                         if (log_hostname == "")
541                         {
542                            log_hostname = hostname;
543                         }
544                         log_service = pan->service();
545                         log_date    = pan->timestamp();
546                      }
547                      else
548                      {
549                         *log << "Log line " << raw << " does not match.\n";
550                         raw = "";
551                      }
552                   }
553                }
554                else if (strcmp((char *)node->name, "cooked") == 0)
555                {
556                   //  Find the parts of the log entry
557
558                   if (verbose)
559                   {
560                      *log << "Analyzing cooked element.\n";
561                   }
562                   pathcontext->node = node;
563
564                   res = xmlXPathEval((const xmlChar *)"hostname/text()", pathcontext);
565                   if (res->nodesetval != NULL)
566                   {
567                      item = *res->nodesetval->nodeTab;
568                      log_hostname = (const char *)item->content;
569                      if (log_hostname != hostname(0, ~log_hostname))
570                      {
571                         *log << "Hostname " << log_hostname << " does not match.\n";
572                         log_hostname = "";
573                      }
574                   }
575                   else
576                   {
577                      log_hostname = hostname;
578                   }
579
580                   res = xmlXPathEval((const xmlChar *)"service/text()", pathcontext);
581                   if (res->nodesetval != NULL)
582                   {
583                      item = *res->nodesetval->nodeTab;
584                      log_service = (const char *)item->content;
585                   }
586                   else
587                   {
588                      log_service = service;
589                   }
590
591                   res = xmlXPathEval((const xmlChar *)"timestamp/text()", pathcontext);
592                   if (res->nodesetval != NULL)
593                   {
594                      item = *res->nodesetval->nodeTab;
595                      log_date = String((const char *)item->content);
596                   }
597                   else
598                   {
599                      *log << "<timestamp> missing from cooked log element.\n";
600                   }
601
602                   res = xmlXPathEval((const xmlChar *)"raw/text()", pathcontext);
603                   if (res->nodesetval != NULL)
604                   {
605                      item = *res->nodesetval->nodeTab;
606                      raw = String((const char *)item->content);
607                   }
608                   else
609                   {
610                      *log << "<raw> missing from cooked log element.\n";
611                   }
612
613                }
614
615                //   Insert a new log record into the database.
616                if (raw != "" && log_hostname != "" && log_date.proper())
617                {
618                   String insertion("insert into log (objectid, servicecode,"
619                         " object_timestamp, timestamp, rawdata, processed) values (");
620
621                   /*   Insert a new record into the log table   */
622
623                   insertion += "'" + objectid + "',";
624                   insertion += "'" + log_service + "',";
625                   insertion += "'" + log_date.format("%Y-%m-%d %T") + "',";
626                   insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
627                   insertion += "'" + SQL_Escape(raw) + "',FALSE";
628                   insertion += ")";
629
630                   if (testmode)
631                   {
632                      *log << insertion << "\n";
633                   }
634                   else
635                   {
636                      database.Query(insertion);
637                   }
638
639                   if (verbose)
640                   {
641                      *log << "\n\n";
642                   }
643                }
644  
645             }
646             node = node->next;
647          }
648       }
649       else
650       {
651          *log << "Data element " << node->name << " is not supported.\n";
652       }
653    }
654    else
655    {
656       *log << "Data node not found.\n";
657    }
658 }
659
660 /*=========================================================================
661 **  NAME           : enter
662 **  SYNOPSIS       : int enter()
663 **  PARAMETERS     : 
664 **  RETURN VALUE   : The number of lines successfully parsed from the input
665 **
666 **  DESCRIPTION    : 
667 **
668 **  VARS USED      :
669 **  VARS CHANGED   :
670 **  FUNCTIONS USED :
671 **  SEE ALSO       :
672 **  LAST MODIFIED  : Jul 24, 2003
673 **=========================================================================
674 */
675
676 int client_message::enter()
677 {
678    if (classification == XML)
679    {
680       enterXML();
681       return 1;
682    }
683
684    long   nr_lines = 0;
685    String line;
686    String qry;
687
688    String change_notification("");
689    String create_notification("");
690    bool initial_entry = false;
691
692    std::list<String>  packages;
693
694
695    /*  Double-check the classification of the message */
696
697    if (classification == UNKNOWN || certainty < 0.9 || gpg_encrypted)
698    {
699       return 0;
700    }
701
702    if (mail_header)
703    {
704       //  Skip the mail header.
705
706       while (input >> line && line != "");
707    }
708
709    /*  Try to find the host in the database */
710
711    String objectid;
712
713    objectid = database.find_host(hostname);
714    if (objectid == "")
715    {
716       *log << "Please define the host " << hostname << " in the database.\n";
717       return 0;
718    }
719    if (verbose)
720    {
721       *log << "Object id for " << hostname << " is " << objectid << "\n";
722    }
723
724    if (classification == RPMLIST)
725    {
726
727       int n_packages;
728
729       /*   Read all packages, so we will know which ones are  */
730       /*   missing at the end.                                */
731
732       qry = "select name from parameter where objectid='";
733       qry += objectid + "' and class='package'";
734       n_packages = database.Query(qry);
735       initial_entry = n_packages == 0;
736
737 #ifdef DEBUG
738       *log << n_packages << " packages in database.\n";
739 #endif
740       for (int t = 0; t < n_packages; t++)
741       {
742          packages.push_back(database.Field(t, "name"));
743       }
744 #ifdef DEBUG
745       *log << "Package list built: " << packages.size() << ".\n";
746 #endif
747    }
748
749    /*  Scan the input line by line, entring records into the database */
750
751    String rest;   //  Rest of the line to be parsed
752    regex  re_any(".*");
753
754    while (input >> line)
755    {
756       if (verbose)
757       {
758          *log << line << "\n";
759       }
760
761
762       /*  Check each line if it contains valid information */
763
764       const regex *check;
765
766       switch (classification)
767       {
768       case RPMLIST:
769             check = &re_rpm;
770             break;
771       case COOKER_OBJECT:
772             check = &re_any;
773             break;
774       }
775
776       if (line == *check)
777       {
778          date   log_date;
779          hour   log_time;
780          int    i;
781
782          String insertion("insert into log (objectid, servicecode,"
783                            " object_timestamp, timestamp, rawdata, processed) values (");
784          String datestring;
785
786          switch (classification)
787          {
788          case COOKER_OBJECT:
789 #ifdef DEBUG
790             std::cerr << "\ncooker check: " << pan->check_pattern(line) << "\n";
791 #endif
792             pan->cook_this(line, arrival);
793
794             if (pan->hostname() == hostname(0,~pan->hostname()))
795             {
796
797 #ifdef DEBUG
798                std::cerr << " Information from cooker:\n";
799                std::cerr << "     timestamp = " << pan->timestamp() << "\n";
800                std::cerr << "     hostname  = " << pan->hostname() << "\n";
801                std::cerr << "     service   = " << pan->service() << "\n";
802 #endif
803                /*   Insert a new record into the log table   */
804
805                insertion += "'" + objectid + "',";
806                insertion += "'" + pan->service() + "',";
807                insertion += "'" + pan->timestamp().format("%Y-%m-%d %T") + "',";
808                insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
809                insertion += "'" + SQL_Escape(line) + "',FALSE";
810                insertion += ")";
811
812                if (testmode)
813                {
814                   *log << insertion << "\n";
815                }
816                else
817                {
818                   database.Query(insertion);
819                }
820
821                if (verbose)
822                {
823                   *log << "\n\n";
824                }
825
826                nr_lines++;
827             }
828             else
829             {
830                *log << "   Hostname " << pan->hostname() << " does not match.\n";
831             }
832             break;
833
834
835          case RPMLIST:
836             //  Scan a list of packages and versions from "rpm -a".
837             //  A similar listing can be created on IRIX 6.5 by using the
838             //  command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v '^[-=]' \
839             //            |grep -v Version-Description".
840             //
841             //  We have to separate the package name and the version.
842             //  The separation is marked by a '-', followed by a digit.
843             //  However, there may be other sequences of '-'digit in the package name,
844             //  do we have to scan ahead until there is at most one such sequence
845             //  left in the version string. The '-'digit seqeunce inside the
846             //  version usually separates the version and the release number.
847
848             int  version_start, next_version_start;
849
850             i = line.index('-');
851             version_start = i;
852             next_version_start = i;
853
854             while (i < ~line - 1)
855             {
856                while (i < ~line - 1 && !(line[i] == '-' && isdigit(line[i + 1])))
857                {
858                   i++;
859                }
860                if (i < ~line - 1)
861                {
862                   version_start = next_version_start;
863                   next_version_start = i;
864                }
865                i++;
866             }
867             
868             if (!isdigit(line[version_start + 1]))
869             {
870                version_start = next_version_start;
871             }
872             String package(line(0,version_start));
873             String version(line(version_start + 1, ~line));
874             String paramid;
875             String remark;
876             String insert_h;
877
878             if (verbose)
879             {
880                *log << "Package is " << package;
881                *log << ", version is " << version << "\n";
882             }
883
884             //  Construct a qry to check the package's existance
885
886             qry = "select paramid from parameter where objectid='";
887             qry += objectid + "' and class='package' and name='";
888             qry += package + "'";
889
890             if (database.Query(qry) == 1)
891             {
892                std::list<String>::iterator  lp;
893
894                lp = find(packages.begin(), packages.end(), package);
895                if (lp != packages.end())
896                {
897                   packages.erase(lp);
898                }
899                else
900                {
901                   *log <<  "Could NOT find " << package << " in list.\n";
902                }
903
904                paramid = database.Field(0, "paramid");
905                qry = "select value from property where paramid='";
906                qry += paramid + "' and name='version'";
907                if (database.Query(qry) == 0)
908                {
909                   *log << "Database corruption: Package " << package;
910                   *log << " does not have a 'version' property.\n";
911                }
912                else if (database.Field(0, "value") != version)
913                {
914                   if (verbose)
915                   {
916                      *log << "  Parameter " << package << " has different version\n";
917                   }
918                   insertion = "update property set value='";
919                   insertion += version + "' where paramid='";
920                   insertion += paramid + "' and name='version'";
921
922                   insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
923                   insert_h += " values ('";
924                   insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'MODIFIED', 'version', '";
925                   insert_h += version + "')";
926
927                   database.Query(insertion);
928                   database.Query(insert_h);
929
930                   if (change_notification == "")
931                   {
932                      remark = "Gnucomo detected a different version for package parameter(s) ";
933                      change_notification = database.new_notification(objectid, "property modified", remark);
934                   }
935
936                   if (change_notification != "")
937                   {
938                      insertion = "insert into parameter_notification (notificationid, paramid) values ('";
939                      insertion += change_notification + "', '";
940                      insertion += paramid + "')";
941
942                      database.Query(insertion);
943                   }
944                   else
945                   {
946                      *log << "gcm_input ERROR: Cannot create 'property modified' notification.\n";
947                   }
948                }
949                else
950                {
951                   if (verbose)
952                   {
953                      *log << "   Parameter " << package << " has not changed.\n";
954                   }
955                }
956             }
957             else
958             {
959
960                if (verbose)
961                {
962                   *log << "  Parameter " << package << " does not exist.\n";
963                }
964                //  Create a new package parameter, including version property and history record
965
966                insertion = "insert into parameter (objectid, name, class, description) values ('";
967                insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
968                if (testmode)
969                {
970                   paramid = "0";
971                   *log << insertion << "\n";
972                }
973                else
974                {
975                   database.Query(insertion);
976                   qry = "select paramid from parameter where objectid='";
977                   qry += objectid + "' and class='package' and name='";
978                   qry += package + "'";
979                   database.Query(qry);
980                   paramid = database.Field(0, "paramid");
981                }
982
983                insertion = "insert into property (paramid, name, value, type) values ('";
984                insertion += paramid + "', 'version', '";
985                insertion += version + "', 'STATIC')";
986                insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
987                insert_h += " values ('";
988                insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'CREATED', 'version', '";
989                insert_h += version + "')";
990
991                if (testmode)
992                {
993                   *log << insertion << "\n" << insert_h << "\n";
994                }
995                else
996                {
997                   database.Query(insertion);
998                   database.Query(insert_h);
999                   if (!initial_entry)
1000                   {
1001                      if (create_notification == "")
1002                      {
1003                         remark = "Gnucomo detected new parameter(s) of class package";
1004                         create_notification = database.new_notification(objectid, "parameter created", remark);
1005                      }
1006                      if (create_notification != "")
1007                      {
1008                         insertion = "insert into parameter_notification (notificationid, paramid) values ('";
1009                         insertion += create_notification + "', '";
1010                         insertion += paramid + "')";
1011
1012                         database.Query(insertion);
1013                      }
1014                      else
1015                      {
1016                         *log << "gcm_input ERROR: Cannot create 'parameter created' notification.\n";
1017                      }
1018                   }
1019                }
1020             }
1021
1022             if (verbose)
1023             {
1024                *log << "\n";
1025             }
1026
1027             nr_lines++;
1028             break;
1029
1030          }
1031       }
1032       else
1033       {
1034          *log << "gcm_input WARNING: Not a valid line: " << line << "\n";
1035       }
1036    }
1037
1038    if (classification == RPMLIST && !incremental)
1039    {
1040       std::list<String>::iterator  lp;
1041       String     remove_notification("");
1042
1043       /*
1044        *     If there are any packages left in the list, they seem to have
1045        *     disappeared from the system.
1046        */
1047
1048       for (lp = packages.begin(); lp != packages.end(); lp++)
1049       {
1050          String paramid;
1051          String remark;
1052          String insert;
1053
1054          //  Construct a qry to check the package's existance
1055
1056          qry = "select paramid from parameter where objectid='";
1057          qry += objectid + "' and class='package' and name='";
1058          qry += *lp + "'";
1059
1060          if (database.Query(qry) == 1)
1061          {
1062             paramid = database.Field(0, "paramid");
1063             qry ="select change_nature from history where paramid='";
1064             qry += paramid + "' order by modified desc";
1065             if (database.Query(qry) <= 0)
1066             {
1067                *log << "Database ERROR: no history record for parameter " << *lp << ".\n";
1068             }
1069             else if (database.Field(0, "change_nature") != "REMOVED")
1070             {
1071                if (verbose)
1072                {
1073                  *log << "Removing parameter " << *lp << ".\n";
1074                }
1075
1076                insert = "insert into history (paramid, modified, change_nature)";
1077                insert += " values ('";
1078                insert += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'REMOVED')";
1079
1080                database.Query(insert);
1081
1082                if (remove_notification == "")
1083                {
1084                   remark = "Gnucomo detected that package(s) have disappeared ";
1085                   remove_notification = database.new_notification(objectid, "parameter removed", remark);
1086                }
1087
1088                if (remove_notification != "")
1089                {
1090                   insert = "insert into parameter_notification (notificationid, paramid) values ('";
1091                   insert += remove_notification + "', '";
1092                   insert += paramid + "')";
1093
1094                   database.Query(insert);
1095                }
1096                else
1097                {
1098                   *log << "gcm_input ERROR: Cannot create 'parameter removed' notification.\n";
1099                }
1100             }
1101          }
1102       }
1103    }
1104
1105    if (verbose)
1106    {
1107       *log << nr_lines << " lines parsed from the log file.\n";
1108    }
1109    return nr_lines;
1110 }