Create notifications when a new package is discovered
[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.6 $
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     : Jan 31, 2003
24 **      MODIFICATIONS   : 
25 **************************************************************************/
26
27 /*****************************
28    $Log: message.cpp,v $
29    Revision 1.6  2003-02-05 09:37:51  arjen
30    Create notifications when a new package is discovered
31    in a 'rpm -qa' list or when the version of a package is changed.
32
33    Revision 1.4  2002/12/06 22:26:28  arjen
34    Set the value of log.processed to FALSE when inserting a
35    new log entry into the database
36    When a syslog entry arrives from last year, gcm_input subtracts one from the
37    year of arrival to create the year of the log entry.
38    Read output from "rpm -qa" and enter packages in the parameter table.
39
40    Revision 1.3  2002/11/09 08:04:27  arjen
41    Added a reference to the GPL
42
43    Revision 1.2  2002/11/04 10:13:36  arjen
44    Use proper namespace for iostream classes
45
46    Revision 1.1  2002/10/05 10:25:49  arjen
47    Creation of gcm_input and a first approach to a web interface
48
49 *****************************/
50
51 static const char *RCSID = "$Id: message.cpp,v 1.6 2003-02-05 09:37:51 arjen Exp $";
52
53 #include "message.h"
54
55 extern bool verbose;   /*  Defined in the main application */
56 extern bool testmode;
57
58 /*   Utility functions   */
59
60 String SQL_Escape(String s);
61
62 /*=========================================================================
63 **  NAME           : operator >>
64 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
65 **  PARAMETERS     : 
66 **  RETURN VALUE   : True if input was available.
67 **
68 **  DESCRIPTION    : Input operator. Read the next line from the message.
69 **
70 **  VARS USED      :
71 **  VARS CHANGED   :
72 **  FUNCTIONS USED :
73 **  SEE ALSO       :
74 **  LAST MODIFIED  : Nov 04, 2002
75 **=========================================================================
76 */
77
78 bool operator >> (message_buffer &b, String &s)
79 {
80    bool   input_ok = false;
81
82    if (b.next_line == b.buffer.end())
83    {
84       String   l;
85
86       if (*(b.input) >> l)
87       {
88          b.buffer.push_back(l);
89
90          //   next_line keeps pointing to the end.
91  
92          s = l;
93          input_ok = true;
94       }
95    }
96    else
97    {
98       s = *(b.next_line);
99       b.next_line++;
100       input_ok = true;
101    }
102    return input_ok;
103 }
104
105 client_message::client_message(std::istream *in, gnucomo_database db)
106 {
107    input.from(in);
108    database = db;
109
110    hostname = "";
111    mail_header   = false;
112    gpg_encrypted = false;
113    classification = UNKNOWN;
114    certainty      = 0.0;
115 }
116
117 static const String syslog_date_re("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
118 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}");
119 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}");
120
121 static const regex re_syslog(syslog_date_re + " [[:alnum:]]+ [[:alpha:]]+.*:.+");
122 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
123 static const regex re_dump("^ *DUMP: Date of this level");
124 static const regex re_accesslog("(GET|POST) .+ HTTP");
125 static const regex re_errorlog("^\\[" + unix_date_re + "\\] \\[(error|notice)\\] .+");
126 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
127
128 static const regex re_syslog_date("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
129 static const regex re_uxmail_from("^From - " + unix_date_re);
130 static const regex re_mail_From("^From:[[:blank:]]+");
131 static const regex re_mail_Date("^Date:[[:blank:]]+" + mail_date_re);
132 static const regex re_email_address("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
133 static const regex re_email_user("[[:alnum:]_.-]+@");
134
135 /*=========================================================================
136 **  NAME           : classify
137 **  SYNOPSIS       : double classify(String host, date arriv_d, hour arriv_t, String serv)
138 **  PARAMETERS     : 
139 **  RETURN VALUE   : The certainty with which the message is classified.
140 **
141 **  DESCRIPTION    : 
142 **
143 **  VARS USED      :
144 **  VARS CHANGED   :
145 **  FUNCTIONS USED :
146 **  SEE ALSO       :
147 **  LAST MODIFIED  : Nov 16, 2002
148 **=========================================================================
149 */
150
151 double client_message::classify(String host, UTC arriv, String serv)
152 {
153    String    line;
154
155    hostname    = host;
156    arrival     = arriv;
157    service     = serv;
158
159    /*  First, check if the message has a mail header. */
160
161    if (input >> line && line == re_uxmail_from)
162    {
163       String    from_address;
164
165       mail_header = true;
166
167       /*  Scan ahead for the hostname and date of arrival.  */
168
169       while (input >> line && line != "")
170       {
171          if (line == re_mail_From)
172          {
173             from_address = line(re_email_address);
174             from_address(re_email_user) = "";            //  Remove the user part;
175             hostname = from_address;
176          }
177          if (line == re_mail_Date)
178          {
179             arrival = UTC(line(regex(mail_date_re)));
180          }
181       }
182    }
183    else
184    {
185       //  Push the first line back, we need to read it again.
186       --input;
187    }
188
189    /*
190     *  Now that we have the mail header out of the way, try to figure
191     *  out what the content of the message is.
192     */
193
194
195    while (input >> line && certainty < 0.9)
196    {
197       std::cout << "  testing: " << line << "\n";
198       if (line == re_syslog)
199       {
200          certainty = 1.0;
201          classification = SYSLOG;
202          if (verbose)
203          {
204             std::cout << "Syslog detected.\n";
205          }
206       }
207       else if (line == re_PGP)
208       {
209          certainty = 1.0;
210          gpg_encrypted = true;
211          std::cerr << "The message is PGP/GnuPG encrypted.\n";
212       }
213       else if (line == re_dump)
214       {
215           certainty = 1.0;
216           if (verbose)
217           {
218              std::cout << "DUMP output detected.\n";
219           }
220       }
221       else if (line == re_accesslog)
222       {
223           certainty = 1.0;
224           classification = ACCESSLOG;
225           service = "httpd";
226           if (verbose)
227           {
228              std::cout << "HTTP access log detected.\n";
229           }
230       }
231       else if (line == re_errorlog)
232       {
233           certainty = 1.0;
234           classification = ERRORLOG;
235           service = "httpd";
236           if (verbose)
237           {
238              std::cout << "HTTP error log detected.\n";
239           }
240       }
241       else if (line == re_rpm)
242       {
243           certainty = 1.0;
244           classification = RPMLIST;
245           service = "";
246           if (verbose)
247           {
248              std::cout << "RPM package list detected.\n";
249           }
250       }
251    }
252    input.rewind();
253
254    if (hostname == "")
255    {
256       std::cerr <<  "Can not determine the hostname where the message came from.\n";
257       certainty = 0.0;
258    }
259    else if (!arrival.proper())
260    {
261       std::cerr << "Arrival time is not knwon.\n";
262       certainty = 0.0;
263    }
264    else
265    {
266       certainty = 1.0;
267    }
268
269    return certainty;
270 }
271
272 /*=========================================================================
273 **  NAME           : enter
274 **  SYNOPSIS       : int enter()
275 **  PARAMETERS     : 
276 **  RETURN VALUE   : The number of lines successfully parsed from the input
277 **
278 **  DESCRIPTION    : 
279 **
280 **  VARS USED      :
281 **  VARS CHANGED   :
282 **  FUNCTIONS USED :
283 **  SEE ALSO       :
284 **  LAST MODIFIED  : Jan 31, 2003
285 **=========================================================================
286 */
287
288 int client_message::enter()
289 {
290    long   nr_lines = 0;
291    String line;
292
293    String change_notification("");
294    String create_notification("");
295
296
297    /*  Double-check the classification of the message */
298
299    if (classification == UNKNOWN || certainty < 0.9 || gpg_encrypted)
300    {
301       return 0;
302    }
303
304    if (mail_header)
305    {
306       //  Skip the mail header.
307  
308       while (input >> line && line != "");
309    }
310
311    /*  Try to find the host in the database */
312
313    String objectid;
314
315    objectid = database.find_host(hostname);
316    if (objectid == "")
317    {
318       std::cerr << "Please define the host " << hostname << " in the database.\n";
319       return 0;
320    }
321    if (verbose)
322    {
323       std::cout << "Object id for " << hostname << " is " << objectid << "\n";
324    }
325
326    /*  Scan the input line by line, entring records into the database */
327
328    String rest;   //  Rest of the line to be parsed
329
330    while (input >> line)
331    {
332       if (verbose)
333       {
334          std::cout << line << "\n";
335       }
336
337
338       /*  Check each line if it contains valid information */
339
340       const regex *check;
341
342       switch (classification)
343       {
344       case SYSLOG:
345             check = &re_syslog;
346             break;
347       case ACCESSLOG:
348             check = &re_accesslog;
349             break;
350       case ERRORLOG:
351             check = &re_errorlog;
352             break;
353       case RPMLIST:
354             check = &re_rpm;
355             break;
356       }
357
358       if (line == *check)
359       {
360          date   log_date;
361          hour   log_time;
362          int    i;
363
364          String insertion("insert into log (objectid, servicecode,"
365                            " object_timestamp, timestamp, rawdata, processed) values (");
366          String datestring;
367
368          switch (classification)
369          {
370          case SYSLOG:
371             log_date = line;
372             log_time = line;
373             if (log_date.Year() < 0 || log_date.Year() > 2500)
374             {
375                //  The year is not in the log file. Assume the year of arrival,
376                //  unless this puts the log entry at a later date than the arrival date.
377                //  This happens e.g. when a log entry from December arrives in Januari.
378
379                log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year());
380                if (log_date > date(arrival))
381                {
382                   log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year() - 1);
383                }
384             }
385
386             if (verbose)
387             {
388                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
389             }
390             rest = line << 16;
391             i = rest.index(' ');
392             if (rest(0,i) == hostname(0,i))
393             {
394                rest <<= i + 1;
395                if (verbose)
396                {
397                   std::cout << "   Hostname matches.\n";
398                   std::cout << "   rest = " << rest << "\n";
399                }
400                for (i = 0; isalpha(rest[i]) && i < ~rest; i++);
401                if (verbose)
402                {
403                   std::cout << "   Service name = " << rest(0,i) << "\n";
404                }
405
406                /*   Insert a new record into the log table   */
407
408                insertion += "'" + objectid + "',";
409                insertion += "'" + rest(0,i) + "',";
410                insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
411                insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
412                insertion += "'" + SQL_Escape(line) + "',FALSE";
413                insertion += ")";
414             
415                if (testmode)
416                {
417                   std::cout << insertion << "\n";
418                }
419                else
420                {
421                   database.Query(insertion);
422                }
423
424                if (verbose)
425                {
426                   std::cout << "\n\n";
427                }
428
429                nr_lines++;
430             }
431             else
432             {
433                std::cerr << "   Hostname " << rest(0,i) << " does not match.\n";
434             }
435             break;
436
437          case ACCESSLOG:
438             datestring = line(regex("\\[.+\\]"));
439             datestring <<= 1;
440             datestring >>= 1;
441             datestring[datestring.index(':')] = ' ';
442             log_date = datestring;
443             log_time = datestring;
444             if (verbose)
445             {
446                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
447             }
448             insertion += "'" + objectid + "',";
449             insertion += "'" + service + "',";
450             insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
451             insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
452             insertion += "'" + SQL_Escape(line) + "',FALSE";
453             insertion += ")";
454             
455             if (testmode)
456             {
457                std::cout << insertion << "\n";
458             }
459             else
460             {
461                database.Query(insertion);
462             }
463
464             if (verbose)
465             {
466                std::cout << "\n\n";
467             }
468
469             nr_lines++;
470             break;
471
472          case ERRORLOG:
473             datestring = line(regex("\\[.+\\]"));
474             datestring <<= 1;
475             datestring >>= 1;
476             log_date = datestring;
477             log_time = datestring;
478             if (verbose)
479             {
480                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
481             }
482             insertion += "'" + objectid + "',";
483             insertion += "'" + service + "',";
484             insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
485             insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
486             insertion += "'" + SQL_Escape(line) + "',FALSE";
487             insertion += ")";
488             
489             if (testmode)
490             {
491                std::cout << insertion << "\n";
492             }
493             else
494             {
495                database.Query(insertion);
496             }
497
498             if (verbose)
499             {
500                std::cout << "\n\n";
501             }
502
503             nr_lines++;
504             break;
505
506          case RPMLIST:
507             //  Scan a list of packages and versions from "rpm -a".
508             //  A similar listing can be created on IRIX 6.5 by using the
509             //  command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v '^[-=]' \
510             //            |grep -v Version-Description".
511             //
512             //  We have to separate the package name and the version.
513             //  The separation is marked by a '-', followed by a digit.
514  
515             String qry = "select count(paramid) from parameter where objectid='";
516             qry += objectid + "' and class='package'";
517
518             database.Query(qry);
519             long n_packages = String(database.Field(0, "count"));
520
521             bool initial_entry = n_packages == 0;
522
523             i = line.index('-');
524             while (!(line[i] == '-' && isdigit(line[i + 1])))
525             {
526                i++;
527             }
528             String package(line(0,i));
529             String version(line(i+1, ~line));
530             String paramid;
531             String remark;
532             String insert_h;
533
534             if (verbose)
535             {
536                std::cout << "Package is " << package;
537                std::cout << ", version is " << version << "\n";
538             }
539
540             //  Construct a qry to check the package's existance
541
542             qry = "select paramid from parameter where objectid='";
543             qry += objectid + "' and class='package' and name='";
544             qry += package + "'";
545
546             if (database.Query(qry) == 1)
547             {
548                paramid = database.Field(0, "paramid");
549                qry = "select value from property where paramid='";
550                qry += paramid + "' and name='version'";
551                if (database.Query(qry) == 0)
552                {
553                   std::cerr << "Database corruption: Package " << package;
554                   std::cerr << " does not have a 'version' property.\n";
555                }
556                else if (database.Field(0, "value") != version)
557                {
558                   if (verbose)
559                   {
560                      std::cout << "  Parameter " << package << " has different version\n";
561                   }
562                   insertion = "update property set value='";
563                   insertion += version + "' where paramid='";
564                   insertion += paramid + "' and name='version'";
565
566                   insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
567                   insert_h += " values ('";
568                   insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'MODIFIED', 'version', '";
569                   insert_h += version + "')";
570
571                   database.Query(insertion);
572                   database.Query(insert_h);
573
574                   if (change_notification == "")
575                   {
576                      remark = "Gnucomo detected a different version for package parameter(s) ";
577                      change_notification = database.new_notification(objectid, "property modified", remark);
578                   }
579
580                   insertion = "insert into parameter_notification (notificationid, paramid) values ('";
581                   insertion += change_notification + "', '";
582                   insertion += paramid + "')";
583
584                   database.Query(insertion);
585                }
586                else
587                {
588                   if (verbose)
589                   {
590                      std::cout << "   Parameter " << package << " has not changed.\n";
591                   }
592                }
593             }
594             else
595             {
596
597                if (verbose)
598                {
599                   std::cout << "  Parameter " << package << " does not exist.\n";
600                }
601                //  Create a new package parameter, including version property and history record
602
603                insertion = "insert into parameter (objectid, name, class, description) values ('";
604                insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
605                if (testmode)
606                {
607                   paramid = "0";
608                   std::cout << insertion << "\n";
609                }
610                else
611                {
612                   database.Query(insertion);
613                   qry = "select paramid from parameter where objectid='";
614                   qry += objectid + "' and class='package' and name='";
615                   qry += package + "'";
616                   database.Query(qry);
617                   paramid = database.Field(0, "paramid");
618                }
619
620                insertion = "insert into property (paramid, name, value, type) values ('";
621                insertion += paramid + "', 'version', '";
622                insertion += version + "', 'STATIC')";
623                insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
624                insert_h += " values ('";
625                insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'CREATED', 'version', '";
626                insert_h += version + "')";
627
628                if (testmode)
629                {
630                   std::cout << insertion << "\n" << insert_h << "\n";
631                }
632                else
633                {
634                   database.Query(insertion);
635                   database.Query(insert_h);
636                   if (!initial_entry)
637                   {
638                      if (create_notification == "")
639                      {
640                         remark = "Gnucomo detected new parameter(s) of class package";
641                         create_notification = database.new_notification(objectid, "parameter created", remark);
642                      }
643                      insertion = "insert into parameter_notification (notificationid, paramid) values ('";
644                      insertion += create_notification + "', '";
645                      insertion += paramid + "')";
646
647                      database.Query(insertion);
648                   }
649                }
650             }
651
652             if (verbose)
653             {
654                std::cout << "\n";
655             }
656
657             nr_lines++;
658             break;
659
660          }
661       }
662       else
663       {
664          std::cerr << "gcm_input WARNING: Not a valid line: " << line << "\n";
665       }
666    }
667
668    if (verbose)
669    {
670       std::cout << nr_lines << " lines parsed from the log file.\n";
671    }
672    return nr_lines;
673 }
674
675 /*=========================================================================
676 **  NAME           : SQL_Escape
677 **  SYNOPSIS       : String SQL_Escape(String)
678 **  PARAMETERS     : 
679 **  RETURN VALUE   : 
680 **
681 **  DESCRIPTION    : Insert backslashes before single quotes.
682 **
683 **  VARS USED      :
684 **  VARS CHANGED   :
685 **  FUNCTIONS USED :
686 **  SEE ALSO       :
687 **  LAST MODIFIED  : 
688 **=========================================================================
689 */
690
691 String SQL_Escape(String s)
692 {
693    int i;
694
695    for (i = 0; i < ~s; i++)
696    {
697       if (s[i] == '\'')
698       {
699          s(i,0) = "\\";
700          i++;
701       }
702    }
703
704    return s;
705 }