Reject log entries that are found to be invalid.
[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.14 $
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.14  2003-09-01 06:57:14  arjen
30    Reject log entries that are found to be invalid.
31
32    Revision 1.13  2003/08/16 15:28:45  arjen
33    Fixed a namespace problem
34
35    Revision 1.12  2003/08/11 16:56:16  arjen
36    Different kinds of log files are parsed by a collection of objects
37    of different classes, derived from the base class line_cooker
38    Depending on the message content or the message_type element in
39    XML, one of these objects is selected.
40
41    Logrunner is integrated with gcm_input. Although its functionality
42    is still limited, a connection between logrunner and gcm_input
43    is beginning to form.
44
45    Revision 1.11  2003/08/05 08:15:00  arjen
46    Debug output to the log stream instead of cerr.
47    Fixed namespace problems in XPath searches of the DOM.
48    Moved string utility functions to a separate file.
49
50    Revision 1.10  2003/04/29 09:16:44  arjen
51    Read XML input,
52    Only cooked log entries for now.
53
54    Revision 1.9  2003/03/29 09:04:10  arjen
55    Extract the hostname out of the 'From:' or 'Message-Id:' line
56    of an email header.
57
58    Revision 1.8  2003/03/16 09:42:40  arjen
59    Read IRIX system logs.
60
61    Revision 1.7  2003/02/21 08:08:05  arjen
62    Gcm_input also detects packages that are removed from the system.
63    Determining the version number of a package in a RPM
64    list is improved. Only the last one or two parts of the string that
65    begin with a '-' and a number are considered the version.
66
67    Revision 1.6  2003/02/05 09:37:51  arjen
68    Create notifications when a new package is discovered
69    in a 'rpm -qa' list or when the version of a package is changed.
70
71    Revision 1.4  2002/12/06 22:26:28  arjen
72    Set the value of log.processed to FALSE when inserting a
73    new log entry into the database
74    When a syslog entry arrives from last year, gcm_input subtracts one from the
75    year of arrival to create the year of the log entry.
76    Read output from "rpm -qa" and enter packages in the parameter table.
77
78    Revision 1.3  2002/11/09 08:04:27  arjen
79    Added a reference to the GPL
80
81    Revision 1.2  2002/11/04 10:13:36  arjen
82    Use proper namespace for iostream classes
83
84    Revision 1.1  2002/10/05 10:25:49  arjen
85    Creation of gcm_input and a first approach to a web interface
86
87 *****************************/
88
89 static const char *RCSID = "$Id: message.cpp,v 1.14 2003-09-01 06:57:14 arjen Exp $";
90
91 #include <algorithm>
92 #include <libxml/xpath.h>
93 #include <libxml/debugXML.h>
94 #include "message.h"
95
96 //#define DEBUG
97
98 extern bool verbose;   /*  Defined in the main application */
99 extern bool testmode;
100 extern bool incremental;
101 extern std::ostream *log;
102
103 /*   Utility functions   */
104
105 extern String SQL_Escape(String s);
106
107 /*=========================================================================
108 **  NAME           : operator >>
109 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
110 **  PARAMETERS     : 
111 **  RETURN VALUE   : True if input was available.
112 **
113 **  DESCRIPTION    : Input operator. Read the next line from the message.
114 **
115 **  VARS USED      :
116 **  VARS CHANGED   :
117 **  FUNCTIONS USED :
118 **  SEE ALSO       :
119 **  LAST MODIFIED  : Nov 04, 2002
120 **=========================================================================
121 */
122
123 bool operator >> (message_buffer &b, String &s)
124 {
125    bool   input_ok = false;
126
127    if (b.next_line == b.buffer.end())
128    {
129       String   l;
130
131       if (*(b.input) >> l)
132       {
133          b.buffer.push_back(l);
134
135          //   next_line keeps pointing to the end.
136
137          s = l;
138          input_ok = true;
139       }
140    }
141    else
142    {
143       s = *(b.next_line);
144       b.next_line++;
145       input_ok = true;
146    }
147    return input_ok;
148 }
149
150 client_message::client_message(std::istream *in, gnucomo_database db)
151 {
152    input.from(in);
153    database = db;
154
155    hostname = "";
156    mail_header   = false;
157    gpg_encrypted = false;
158    classification = UNKNOWN;
159    xmlDom         = NULL;
160    certainty      = 0.0;
161 }
162
163 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}");
164 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}");
165
166 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
167 static const regex re_dump("^ *DUMP: Date of this level");
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       std::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          std::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 known.\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             if (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             }
833             else
834             {
835                *log << "gcm_input WARNING: Not a valid line: " << line << "\n";
836             }
837             break;
838
839
840          case RPMLIST:
841             //  Scan a list of packages and versions from "rpm -a".
842             //  A similar listing can be created on IRIX 6.5 by using the
843             //  command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v '^[-=]' \
844             //            |grep -v Version-Description".
845             //
846             //  We have to separate the package name and the version.
847             //  The separation is marked by a '-', followed by a digit.
848             //  However, there may be other sequences of '-'digit in the package name,
849             //  do we have to scan ahead until there is at most one such sequence
850             //  left in the version string. The '-'digit seqeunce inside the
851             //  version usually separates the version and the release number.
852
853             int  version_start, next_version_start;
854
855             i = line.index('-');
856             version_start = i;
857             next_version_start = i;
858
859             while (i < ~line - 1)
860             {
861                while (i < ~line - 1 && !(line[i] == '-' && isdigit(line[i + 1])))
862                {
863                   i++;
864                }
865                if (i < ~line - 1)
866                {
867                   version_start = next_version_start;
868                   next_version_start = i;
869                }
870                i++;
871             }
872             
873             if (!isdigit(line[version_start + 1]))
874             {
875                version_start = next_version_start;
876             }
877             String package(line(0,version_start));
878             String version(line(version_start + 1, ~line));
879             String paramid;
880             String remark;
881             String insert_h;
882
883             if (verbose)
884             {
885                *log << "Package is " << package;
886                *log << ", version is " << version << "\n";
887             }
888
889             //  Construct a qry to check the package's existance
890
891             qry = "select paramid from parameter where objectid='";
892             qry += objectid + "' and class='package' and name='";
893             qry += package + "'";
894
895             if (database.Query(qry) == 1)
896             {
897                std::list<String>::iterator  lp;
898
899                lp = find(packages.begin(), packages.end(), package);
900                if (lp != packages.end())
901                {
902                   packages.erase(lp);
903                }
904                else
905                {
906                   *log <<  "Could NOT find " << package << " in list.\n";
907                }
908
909                paramid = database.Field(0, "paramid");
910                qry = "select value from property where paramid='";
911                qry += paramid + "' and name='version'";
912                if (database.Query(qry) == 0)
913                {
914                   *log << "Database corruption: Package " << package;
915                   *log << " does not have a 'version' property.\n";
916                }
917                else if (database.Field(0, "value") != version)
918                {
919                   if (verbose)
920                   {
921                      *log << "  Parameter " << package << " has different version\n";
922                   }
923                   insertion = "update property set value='";
924                   insertion += version + "' where paramid='";
925                   insertion += paramid + "' and name='version'";
926
927                   insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
928                   insert_h += " values ('";
929                   insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'MODIFIED', 'version', '";
930                   insert_h += version + "')";
931
932                   database.Query(insertion);
933                   database.Query(insert_h);
934
935                   if (change_notification == "")
936                   {
937                      remark = "Gnucomo detected a different version for package parameter(s) ";
938                      change_notification = database.new_notification(objectid, "property modified", remark);
939                   }
940
941                   if (change_notification != "")
942                   {
943                      insertion = "insert into parameter_notification (notificationid, paramid) values ('";
944                      insertion += change_notification + "', '";
945                      insertion += paramid + "')";
946
947                      database.Query(insertion);
948                   }
949                   else
950                   {
951                      *log << "gcm_input ERROR: Cannot create 'property modified' notification.\n";
952                   }
953                }
954                else
955                {
956                   if (verbose)
957                   {
958                      *log << "   Parameter " << package << " has not changed.\n";
959                   }
960                }
961             }
962             else
963             {
964
965                if (verbose)
966                {
967                   *log << "  Parameter " << package << " does not exist.\n";
968                }
969                //  Create a new package parameter, including version property and history record
970
971                insertion = "insert into parameter (objectid, name, class, description) values ('";
972                insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
973                if (testmode)
974                {
975                   paramid = "0";
976                   *log << insertion << "\n";
977                }
978                else
979                {
980                   database.Query(insertion);
981                   qry = "select paramid from parameter where objectid='";
982                   qry += objectid + "' and class='package' and name='";
983                   qry += package + "'";
984                   database.Query(qry);
985                   paramid = database.Field(0, "paramid");
986                }
987
988                insertion = "insert into property (paramid, name, value, type) values ('";
989                insertion += paramid + "', 'version', '";
990                insertion += version + "', 'STATIC')";
991                insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
992                insert_h += " values ('";
993                insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'CREATED', 'version', '";
994                insert_h += version + "')";
995
996                if (testmode)
997                {
998                   *log << insertion << "\n" << insert_h << "\n";
999                }
1000                else
1001                {
1002                   database.Query(insertion);
1003                   database.Query(insert_h);
1004                   if (!initial_entry)
1005                   {
1006                      if (create_notification == "")
1007                      {
1008                         remark = "Gnucomo detected new parameter(s) of class package";
1009                         create_notification = database.new_notification(objectid, "parameter created", remark);
1010                      }
1011                      if (create_notification != "")
1012                      {
1013                         insertion = "insert into parameter_notification (notificationid, paramid) values ('";
1014                         insertion += create_notification + "', '";
1015                         insertion += paramid + "')";
1016
1017                         database.Query(insertion);
1018                      }
1019                      else
1020                      {
1021                         *log << "gcm_input ERROR: Cannot create 'parameter created' notification.\n";
1022                      }
1023                   }
1024                }
1025             }
1026
1027             if (verbose)
1028             {
1029                *log << "\n";
1030             }
1031
1032             nr_lines++;
1033             break;
1034
1035          }
1036       }
1037       else
1038       {
1039          *log << "gcm_input WARNING: Not a valid line: " << line << "\n";
1040       }
1041    }
1042
1043    if (classification == RPMLIST && !incremental)
1044    {
1045       std::list<String>::iterator  lp;
1046       String     remove_notification("");
1047
1048       /*
1049        *     If there are any packages left in the list, they seem to have
1050        *     disappeared from the system.
1051        */
1052
1053       for (lp = packages.begin(); lp != packages.end(); lp++)
1054       {
1055          String paramid;
1056          String remark;
1057          String insert;
1058
1059          //  Construct a qry to check the package's existance
1060
1061          qry = "select paramid from parameter where objectid='";
1062          qry += objectid + "' and class='package' and name='";
1063          qry += *lp + "'";
1064
1065          if (database.Query(qry) == 1)
1066          {
1067             paramid = database.Field(0, "paramid");
1068             qry ="select change_nature from history where paramid='";
1069             qry += paramid + "' order by modified desc";
1070             if (database.Query(qry) <= 0)
1071             {
1072                *log << "Database ERROR: no history record for parameter " << *lp << ".\n";
1073             }
1074             else if (database.Field(0, "change_nature") != "REMOVED")
1075             {
1076                if (verbose)
1077                {
1078                  *log << "Removing parameter " << *lp << ".\n";
1079                }
1080
1081                insert = "insert into history (paramid, modified, change_nature)";
1082                insert += " values ('";
1083                insert += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'REMOVED')";
1084
1085                database.Query(insert);
1086
1087                if (remove_notification == "")
1088                {
1089                   remark = "Gnucomo detected that package(s) have disappeared ";
1090                   remove_notification = database.new_notification(objectid, "parameter removed", remark);
1091                }
1092
1093                if (remove_notification != "")
1094                {
1095                   insert = "insert into parameter_notification (notificationid, paramid) values ('";
1096                   insert += remove_notification + "', '";
1097                   insert += paramid + "')";
1098
1099                   database.Query(insert);
1100                }
1101                else
1102                {
1103                   *log << "gcm_input ERROR: Cannot create 'parameter removed' notification.\n";
1104                }
1105             }
1106          }
1107       }
1108    }
1109
1110    if (verbose)
1111    {
1112       *log << nr_lines << " lines parsed from the log file.\n";
1113    }
1114    return nr_lines;
1115 }