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