completed partial IPv6 compatibility in DNS; does retrieve and Cache IPv6 IPs
[pingcheck] / src / host / pingscheduler.cpp
1 /*
2 The software in this package is distributed under the GNU General
3 Public License version 2 (with a special exception described below).
4
5 A copy of GNU General Public License (GPL) is included in this distribution,
6 in the file COPYING.GPL.
7
8 As a special exception, if other files instantiate templates or use macros
9 or inline functions from this file, or you compile this file and link it
10 with other works to produce a work based on this file, this file
11 does not by itself cause the resulting work to be covered
12 by the GNU General Public License.
13
14 However the source code for this file must still be made available
15 in accordance with section (3) of the GNU General Public License.
16
17 This exception does not invalidate any other reasons why a work based
18 on this file might be covered by the GNU General Public License.
19 */
20 #include "host/pingscheduler.h"
21
22 #include <iostream>
23 #include <limits>
24
25 #include <boost/bind.hpp>
26 #include <boost/foreach.hpp>
27
28 #include <logfunc.hpp>
29
30 #include "boost_assert_handler.h"
31 #include "host/pingerfactory.h"
32 #include "dns/dnsmaster.h"
33 #include "icmp/icmppinger.h"
34 #include "link/linkstatus.h"
35
36 using namespace std;
37 using boost::asio::io_service;
38 using boost::bind;
39 using boost::date_time::time_resolution_traits_adapted64_impl;
40 using boost::posix_time::microsec_clock;
41 using boost::posix_time::ptime;
42 using boost::posix_time::seconds;
43 using boost::shared_ptr;
44 using I2n::Logger::GlobalLogger;
45
46 //-----------------------------------------------------------------------------
47 // PingScheduler
48 //-----------------------------------------------------------------------------
49
50 /**
51  * @brief Parameterized constructor.
52  *
53  * @param io_serv The one @c io_serv object that controls async processing
54  * @param network_interface The name of the network interface sending the pings.
55  * @param destination_address The remote address to ping.
56  * @param destination_port The remote port to ping.
57  * @param ping_protocol_list A list of protocols to use.
58  * @param ping_interval_in_sec Amount of time between each ping.
59  * @param ping_fail_percentage_limit Maximum amount of pings that can fail.
60  * @param link_analyzer The object to monitor the link status.
61  * @param first_delay Delay in seconds from start_pinging to first ping attempt
62  */
63 PingScheduler::PingScheduler(
64         const IoServiceItem io_serv,
65         const string &network_interface,
66         const string &destination_address,
67         const uint16_t destination_port,
68         const PingProtocolList &ping_protocol_list,
69         const long ping_interval_in_sec,
70         const int ping_fail_percentage_limit,
71         const int ping_reply_timeout,
72         LinkStatusItem link_analyzer,
73         const int first_delay
74
75 ) :
76     IoService( io_serv ),
77     NetworkInterfaceName( network_interface ),
78     DestinationAddress( destination_address ),
79     DestinationPort( destination_port ),
80     Protocols( ping_protocol_list ),
81     ProtocolIter(),
82     PingIntervalInSec( ping_interval_in_sec ),
83     FirstDelay( first_delay ),
84     NextPingTimer( *io_serv ),
85     TimeSentLastPing( microsec_clock::universal_time() ),
86     PingReplyTimeout( ping_reply_timeout ),
87     HostAnalyzer( destination_address, ping_fail_percentage_limit,
88                   link_analyzer ),
89     Resolver(),
90     Ping(),
91     WantToPing( false ),
92     LogPrefix(),
93     ContinueOnOutdatedIps( false )
94 {
95     BOOST_ASSERT( !network_interface.empty() );
96     BOOST_ASSERT( !destination_address.empty() );
97     BOOST_ASSERT( ( 0 < destination_port ) &&
98                   ( destination_port < numeric_limits<uint16_t>::max() ) );
99     BOOST_ASSERT( 0 < ping_interval_in_sec );
100     BOOST_ASSERT( (0 <= ping_fail_percentage_limit) &&
101                   ( ping_fail_percentage_limit <= 100) );
102
103     update_log_prefix();
104
105     init_ping_protocol();
106 }
107
108 /**
109  * @brief Destructor.
110  */
111 PingScheduler::~PingScheduler()
112 {
113 }
114
115 void PingScheduler::stop_pinging()
116 {
117     // stop pinger and resolver
118     GlobalLogger.debug() << LogPrefix << "scheduler: stop pinging";
119     Ping->stop_pinging();
120     cancel_resolve(true);
121
122     // now cancel the own timer in case that pinger cancelation called callback
123     GlobalLogger.debug() << LogPrefix << "scheduler: cancel timer";
124     NextPingTimer.cancel();
125 }
126
127 /**
128  * @brief Start into infinite loop of calls to ping
129  *
130  * Does not start yet but set NextPingTimer (possibly to 0), so action starts
131  *   when io_service is started
132  */
133 void PingScheduler::start_pinging()
134 {
135     if ( FirstDelay > 0 )
136         GlobalLogger.info() << LogPrefix << "Delaying first ping by "
137                                          << FirstDelay << "s";
138     else
139         GlobalLogger.info() << LogPrefix << "Schedule ping as soon as possible";
140
141     (void) NextPingTimer.expires_from_now( seconds( FirstDelay ) );
142     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
143                                           boost::asio::placeholders::error ) );
144 }
145
146
147 /**
148  * @brief call Ping::ping and schedule a call to ping_done_handler when finished
149  */
150 void PingScheduler::ping(const boost::system::error_code &error)
151 {
152     if ( error )
153     {   // get here, e.g. by NextPingTimer.cancel in stop_pinging
154         if ( error ==  boost::asio::error::operation_aborted )
155             GlobalLogger.error() << LogPrefix << "Timer for ping was cancelled!"
156                                  << " --> Stopping";
157         else
158             GlobalLogger.error() << LogPrefix << "Received error " << error
159                                  << " waiting for ping! Stopping";
160         return;
161     }
162
163     // ping as soon as dns is ready
164     WantToPing = true;
165     ping_when_ready();
166 }
167
168
169 void PingScheduler::ping_when_ready()
170 {
171     if ( !WantToPing )
172     {
173         GlobalLogger.info() << LogPrefix << "waiting for ping request "
174             << "(should take no more than than " << PingIntervalInSec << "s)";
175         return;
176     }
177     else if ( Resolver && Resolver->is_resolving() )
178     {
179         GlobalLogger.info() << LogPrefix << "waiting for DNS to finish";
180         return;
181     }
182     else if ( !Resolver )
183         // should not happen, but check anyway
184         GlobalLogger.warning() << LogPrefix << "Have no resolver!";
185
186     GlobalLogger.info() << LogPrefix << "start ping";
187     WantToPing = false;
188
189     // try to get an up-to-date IP (ContinueOnOutdatedIps may only be set
190     //   because a CNAME was out of date -- IPs may still be current)
191     HostAddress ip = Resolver->get_next_ip();
192
193     if ( !ip.is_valid() )
194     {   // this can happen in 2 cases: if ContinueOnOutdatedIps==true
195         // or when ip went out of date between resolve and now
196         // --> try to use outdated IP
197         GlobalLogger.info() << LogPrefix << "Checking for outdated IPs";
198         bool check_up_to_date = false;
199         ip = Resolver->get_next_ip(check_up_to_date);
200     }
201     if ( !ip.is_valid() )
202     {   // Do not even have an outdated IP!
203         // This happens if have no cached IPs and resolve failed
204         GlobalLogger.info() << LogPrefix << "Not even outdated IP to ping "
205             << "-- treat like a failed ping.";
206
207         // skip the ping and directly call ping_done_handler
208         ping_done_handler(false);
209     }
210     else
211     {
212         uint32_t ttl = ip.get_ttl().get_updated_value();
213         std::string expiry;
214         if (ttl == 0)
215             expiry = "out of date!";
216         else
217         {
218             boost::posix_time::ptime now =
219                                   boost::posix_time::second_clock::local_time();
220             expiry = boost::posix_time::to_simple_string(now + seconds(ttl));
221         }
222
223         GlobalLogger.info() << LogPrefix << "pinging IP " << ip.get_ip()
224             << " with TTL " << ttl << "s (" << expiry << ")";
225         Ping->ping( ip.get_ip(),
226                     DestinationPort,
227                     boost::bind(&PingScheduler::ping_done_handler, this, _1) );
228     }
229 }
230
231
232 //------------------------------------------------------------------------------
233 // Post Processing of Ping result
234 //------------------------------------------------------------------------------
235
236 /**
237  * @brief called when Ping::ping is done; calls functions to update
238  *   statistics, ping interval and elapsed time;
239  *   schedules a call to ping, thereby closing the loop
240  */
241 void PingScheduler::ping_done_handler( const bool ping_success )
242 {
243     GlobalLogger.info() << LogPrefix << "Ping done with success = "
244                                      << ping_success;
245
246     // post-processing
247     // You must call these 3 methods exactly in this order
248     // TODO Fix this method, once it has a semantic dependency with the
249     // update_ping_statistics method, because it depends on the PingAnalyzer
250     // statistics to update the exceeded_ping_failed_limit
251     HostAnalyzer.update_ping_statistics( ping_success );
252     update_ping_interval();
253     update_ping_elapsed_time();
254
255     if (ping_success)
256     {   // reset ContinueOnOutdatedIps
257         ContinueOnOutdatedIps = false;
258         update_log_prefix();
259     }
260
261     // get next protocol, possibly start resolving IPs
262     update_ping_protocol();
263
264     // schedule next ping
265     (void) NextPingTimer.expires_from_now( seconds( PingIntervalInSec ) );
266     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
267                                          boost::asio::placeholders::error ) );
268 }
269
270 void PingScheduler::update_ping_interval()
271 {
272     // have to ping more often?
273     if ( HostAnalyzer.exceeded_ping_failed_limit() )
274     {
275         PingIntervalInSec.speed_up();
276
277         GlobalLogger.debug() << LogPrefix << "- Speeding up ping interval to: "
278                              << PingIntervalInSec << "s";
279     }
280     else
281     {
282         PingIntervalInSec.back_to_original();
283
284         GlobalLogger.debug() << LogPrefix << "- Stick to the original ping "
285                              << "interval: " << PingIntervalInSec << "s";
286     }
287 }
288
289 void PingScheduler::update_ping_elapsed_time()
290 {
291     ptime now = microsec_clock::universal_time();
292     time_resolution_traits_adapted64_impl::int_type elapsed_time_in_sec =
293             (now - TimeSentLastPing).total_seconds();
294     GlobalLogger.debug() << LogPrefix << "- Time elapsed since last ping: "
295                                       << elapsed_time_in_sec << "s";
296
297     TimeSentLastPing = microsec_clock::universal_time();
298 }
299
300
301 //------------------------------------------------------------------------------
302 // Ping Protocol Rotation
303 //------------------------------------------------------------------------------
304
305 void PingScheduler::init_ping_protocol()
306 {
307     ProtocolIter = Protocols.end();
308     get_next_ping_protocol();
309 }
310
311 void PingScheduler::update_ping_protocol()
312 {
313     if ( can_change_ping_protocol() )
314     {
315         get_next_ping_protocol();
316     }
317 }
318
319 void PingScheduler::get_next_ping_protocol()
320 {
321     if (Ping)
322     {
323         Ping->stop_pinging();
324         Ping.reset();
325     }
326
327     GlobalLogger.debug() << LogPrefix
328         << "------------------------------------------------------------------";
329     ++ProtocolIter;
330     if (ProtocolIter == Protocols.end())
331         ProtocolIter = Protocols.begin();
332     PingProtocol ping_protocol = *ProtocolIter;
333     // --> ProtocolIter still points to currently used protocol which is
334     //     required in dns_resolve_callback
335
336     Ping = PingerFactory::createPinger(ping_protocol, IoService,
337                                        NetworkInterfaceName, PingReplyTimeout);
338
339     update_dns_resolver( ping_protocol );
340
341 }
342
343 bool PingScheduler::can_change_ping_protocol() const
344 {
345     // TODO can_change_ping_protocol() and get_next_ping_protocol() may be
346     // implemented in a Algorithm class that can be exchanged in this class to
347     // provide an algorithm neutral class
348     return true;
349 }
350
351 //------------------------------------------------------------------------------
352 // DNS host name resolution
353 //------------------------------------------------------------------------------
354
355 // show "!" after host name if running on outdated IPs
356 void PingScheduler::update_log_prefix()
357 {
358     std::stringstream temp;
359     temp << "Sched(" << DestinationAddress;
360     if (ContinueOnOutdatedIps)
361         temp << "!";
362     temp << "): ";
363     LogPrefix = temp.str();
364 }
365
366 void PingScheduler::update_dns_resolver( PingProtocol current_protocol )
367 {
368     if (Resolver && Resolver->is_resolving())
369         cancel_resolve(false);
370
371     if (ContinueOnOutdatedIps)
372     {
373         ContinueOnOutdatedIps = false;
374         update_log_prefix();
375     }
376
377     // DNS master caches created resolvers and resolved IPs, so this will
378     // probably just return an existing resolver with already resolved IPs for
379     // requested protocol ( ICMP/TCP is ignored, only IPv4/v6 is important)
380     Resolver = DnsMaster::get_instance()->get_resolver_for(DestinationAddress,
381                                                            current_protocol);
382
383     // get number of up-to-date IPs
384     // TODO should check here, if they will be up to date in PingIntervalInSec
385     bool check_up_to_date = true;
386     int ip_count = Resolver->get_resolved_ip_count(check_up_to_date);
387     if (ip_count > 0)
388     {
389         GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
390             << ip_count << " (IPs may be outdated=" << !check_up_to_date << ")";
391         HostAnalyzer.set_resolved_ip_count( ip_count );
392
393         if (Resolver->is_resolving())
394             GlobalLogger.warning() << LogPrefix << "have up to date IPs but "
395                 << "resolver seems to be resolving all the same... "
396                 << "Start pinging anyway!";
397         ping_when_ready();
398     }
399     else
400     {
401         GlobalLogger.info() << LogPrefix
402                             << "No up-to-date IPs --> start resolve";
403         start_resolving_ping_address();
404         // set resolved_ip_count will be called in resolve callback
405     }
406 }
407
408 void PingScheduler::start_resolving_ping_address()
409 {
410     Resolver->async_resolve( boost::bind(&PingScheduler::dns_resolve_callback,
411                                           this, _1, _2) );
412 }
413
414 void PingScheduler::dns_resolve_callback(const bool was_success,
415                                          const int recursion_count)
416 {
417     GlobalLogger.info() << LogPrefix << "dns resolution finished "
418                         << "with success = " << was_success << " "
419                         << "after " << recursion_count << " recursions";
420
421     if ( was_success )
422     {
423         // trust that a successfull DNS resolve means we have an IP with TTL>0
424         int ip_count = Resolver->get_resolved_ip_count(!ContinueOnOutdatedIps);
425         if (ip_count == 0)
426         {   // this will create trouble in HostAnalyzer
427             GlobalLogger.warning() << LogPrefix
428                 << "Should not have reached this case: resolve was "
429                 << "successfull but still have no IPs (up-to-date="
430                 << !ContinueOnOutdatedIps << ")!";
431             if (DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() > 0)
432                 GlobalLogger.warning() << LogPrefix << "This probably happened "
433                     << "because you specified a TTL threshold > 0 but resolving"
434                     << " had no effect on TTLs since external cache is only "
435                     << "updated when TTL=0 is reached.";
436         }
437         else
438         {
439             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
440                 << ip_count << " (IPs may be outdated="
441                 << ContinueOnOutdatedIps << ") --> could ping now";
442             HostAnalyzer.set_resolved_ip_count( ip_count );
443         }
444         ping_when_ready();
445     }
446     else
447     {   // host name resolution failed; try again bypassing first outdated CNAME
448         // or using cached IP
449         ContinueOnOutdatedIps = true;
450         update_log_prefix();
451
452         std::string skip_host = Resolver->get_skip_cname();
453
454         if (skip_host.empty())
455         {   // continue with IP
456             GlobalLogger.notice() << LogPrefix << "DNS failed, "
457                 << "try anyway with cached data";
458
459             int ip_count = Resolver->get_resolved_ip_count(false);
460             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
461                 << ip_count << " (IPs may be outdated=" << true << ")";
462             HostAnalyzer.set_resolved_ip_count( ip_count );
463
464             ping_when_ready();
465         }
466         else
467         {   // have CNAME to continue
468             GlobalLogger.notice() << LogPrefix << "DNS failed, "
469                 << "try again skipping a CNAME and resolving "
470                 << skip_host << " directly";
471
472             cancel_resolve(false);
473
474             // now create new resolver
475             Resolver = DnsMaster::get_instance()
476                                    ->get_resolver_for(skip_host, *ProtocolIter);
477             start_resolving_ping_address();
478         }
479     }
480 }
481
482 /**
483  * cancel resolver if force_cancel or if it is not resolving DestinationAddress
484  *
485  * Resolvers have a life on their own: they are cached by DnsMaster so never go
486  *   out of scope and even after calling callbacks, there might still be a
487  *   longterm timer active to re-try resolving.
488  * We want to cancel that long-term timer only if the Resolver is not for our
489  *   real, original DestinationAddress but a CNAME, which can happen when trying
490  *   to skip cnames and working on out-dated IPs
491  */
492 void PingScheduler::cancel_resolve(const bool force_cancel)
493 {
494     if (force_cancel)
495     {
496         GlobalLogger.info() << "Cancelling resolver (forced)";
497         Resolver->cancel_resolve();
498     }
499     else if ( Resolver->get_hostname() == DestinationAddress )
500         GlobalLogger.info() << LogPrefix
501                             << "Leave original resolver active in background";
502     else
503     {
504         GlobalLogger.info() << LogPrefix << "Cancel resolver for "
505             << Resolver->get_hostname() << " since is not the original "
506             << DestinationAddress;
507         Resolver->cancel_resolve();
508     }
509 }
510