deal with case that have no IP from DNS nor from Cache
[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         HostAnalyzer.set_resolved_ip_count(1);   // must have been 0 --> failed
209              // ping would create failed assumption (nPings > nIPs)
210         ping_done_handler(false);
211         HostAnalyzer.set_resolved_ip_count(0);   // set back
212     }
213     else
214     {
215         uint32_t ttl = ip.get_ttl().get_updated_value();
216         std::string expiry;
217         if (ttl == 0)
218             expiry = "out of date!";
219         else
220         {
221             boost::posix_time::ptime now =
222                                   boost::posix_time::second_clock::local_time();
223             expiry = boost::posix_time::to_simple_string(now + seconds(ttl));
224         }
225
226         GlobalLogger.info() << LogPrefix << "pinging IP " << ip.get_ip()
227             << " with TTL " << ttl << "s (" << expiry << ")";
228         Ping->ping( ip.get_ip(),
229                     DestinationPort,
230                     boost::bind(&PingScheduler::ping_done_handler, this, _1) );
231     }
232 }
233
234
235 //------------------------------------------------------------------------------
236 // Post Processing of Ping result
237 //------------------------------------------------------------------------------
238
239 /**
240  * @brief called when Ping::ping is done; calls functions to update
241  *   statistics, ping interval and elapsed time;
242  *   schedules a call to ping, thereby closing the loop
243  */
244 void PingScheduler::ping_done_handler( const bool ping_success )
245 {
246     GlobalLogger.info() << LogPrefix << "Ping done with success = "
247                                      << ping_success;
248
249     // post-processing
250     // You must call these 3 methods exactly in this order
251     // TODO Fix this method, once it has a semantic dependency with the
252     // update_ping_statistics method, because it depends on the PingAnalyzer
253     // statistics to update the exceeded_ping_failed_limit
254     HostAnalyzer.update_ping_statistics( ping_success );
255     update_ping_interval();
256     update_ping_elapsed_time();
257
258     if (ping_success)
259     {   // reset ContinueOnOutdatedIps
260         ContinueOnOutdatedIps = false;
261         update_log_prefix();
262     }
263
264     // get next protocol, possibly start resolving IPs
265     update_ping_protocol();
266
267     // schedule next ping
268     (void) NextPingTimer.expires_from_now( seconds( PingIntervalInSec ) );
269     NextPingTimer.async_wait( bind( &PingScheduler::ping, this,
270                                          boost::asio::placeholders::error ) );
271 }
272
273 void PingScheduler::update_ping_interval()
274 {
275     // have to ping more often?
276     if ( HostAnalyzer.exceeded_ping_failed_limit() )
277     {
278         PingIntervalInSec.speed_up();
279
280         GlobalLogger.debug() << LogPrefix << "- Speeding up ping interval to: "
281                              << PingIntervalInSec << "s";
282     }
283     else
284     {
285         PingIntervalInSec.back_to_original();
286
287         GlobalLogger.debug() << LogPrefix << "- Stick to the original ping "
288                              << "interval: " << PingIntervalInSec << "s";
289     }
290 }
291
292 void PingScheduler::update_ping_elapsed_time()
293 {
294     ptime now = microsec_clock::universal_time();
295     time_resolution_traits_adapted64_impl::int_type elapsed_time_in_sec =
296             (now - TimeSentLastPing).total_seconds();
297     GlobalLogger.debug() << LogPrefix << "- Time elapsed since last ping: "
298                                       << elapsed_time_in_sec << "s";
299
300     TimeSentLastPing = microsec_clock::universal_time();
301 }
302
303
304 //------------------------------------------------------------------------------
305 // Ping Protocol Rotation
306 //------------------------------------------------------------------------------
307
308 void PingScheduler::init_ping_protocol()
309 {
310     ProtocolIter = Protocols.end();
311     get_next_ping_protocol();
312 }
313
314 void PingScheduler::update_ping_protocol()
315 {
316     if ( can_change_ping_protocol() )
317     {
318         get_next_ping_protocol();
319     }
320 }
321
322 void PingScheduler::get_next_ping_protocol()
323 {
324     if (Ping)
325     {
326         Ping->stop_pinging();
327         Ping.reset();
328     }
329
330     GlobalLogger.debug() << LogPrefix
331         << "------------------------------------------------------------------";
332     ++ProtocolIter;
333     if (ProtocolIter == Protocols.end())
334         ProtocolIter = Protocols.begin();
335     PingProtocol ping_protocol = *ProtocolIter;
336     // --> ProtocolIter still points to currently used protocol which is
337     //     required in dns_resolve_callback
338
339     Ping = PingerFactory::createPinger(ping_protocol, IoService,
340                                        NetworkInterfaceName, PingReplyTimeout);
341
342     update_dns_resolver( ping_protocol );
343
344 }
345
346 bool PingScheduler::can_change_ping_protocol() const
347 {
348     // TODO can_change_ping_protocol() and get_next_ping_protocol() may be
349     // implemented in a Algorithm class that can be exchanged in this class to
350     // provide an algorithm neutral class
351     return true;
352 }
353
354 //------------------------------------------------------------------------------
355 // DNS host name resolution
356 //------------------------------------------------------------------------------
357
358 // show "!" after host name if running on outdated IPs
359 void PingScheduler::update_log_prefix()
360 {
361     std::stringstream temp;
362     temp << "Sched(" << DestinationAddress;
363     if (ContinueOnOutdatedIps)
364         temp << "!";
365     temp << "): ";
366     LogPrefix = temp.str();
367 }
368
369 void PingScheduler::update_dns_resolver( PingProtocol current_protocol )
370 {
371     if (Resolver && Resolver->is_resolving())
372         cancel_resolve(false);
373
374     if (ContinueOnOutdatedIps)
375     {
376         ContinueOnOutdatedIps = false;
377         update_log_prefix();
378     }
379
380     // DNS master caches created resolvers and resolved IPs, so this will
381     // probably just return an existing resolver with already resolved IPs for
382     // requested protocol ( ICMP/TCP is ignored, only IPv4/v6 is important)
383     Resolver = DnsMaster::get_instance()->get_resolver_for(DestinationAddress,
384                                                            current_protocol);
385
386     // get number of up-to-date IPs
387     // TODO should check here, if they will be up to date in PingIntervalInSec
388     bool check_up_to_date = true;
389     int ip_count = Resolver->get_resolved_ip_count(check_up_to_date);
390     if (ip_count > 0)
391     {
392         GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
393             << ip_count << " (IPs may be outdated=" << !check_up_to_date << ")";
394         HostAnalyzer.set_resolved_ip_count( ip_count );
395
396         if (Resolver->is_resolving())
397             GlobalLogger.warning() << LogPrefix << "have up to date IPs but "
398                 << "resolver seems to be resolving all the same... "
399                 << "Start pinging anyway!";
400         ping_when_ready();
401     }
402     else
403     {
404         GlobalLogger.info() << LogPrefix
405                             << "No up-to-date IPs --> start resolve";
406         start_resolving_ping_address();
407         // set resolved_ip_count will be called in resolve callback
408     }
409 }
410
411 void PingScheduler::start_resolving_ping_address()
412 {
413     Resolver->async_resolve( boost::bind(&PingScheduler::dns_resolve_callback,
414                                           this, _1, _2) );
415 }
416
417 void PingScheduler::dns_resolve_callback(const bool was_success,
418                                          const int recursion_count)
419 {
420     GlobalLogger.info() << LogPrefix << "dns resolution finished "
421                         << "with success = " << was_success << " "
422                         << "after " << recursion_count << " recursions";
423
424     if ( was_success )
425     {
426         // trust that a successfull DNS resolve means we have an IP with TTL>0
427         int ip_count = Resolver->get_resolved_ip_count(!ContinueOnOutdatedIps);
428         if (ip_count == 0)
429         {   // this will create trouble in HostAnalyzer
430             GlobalLogger.warning() << LogPrefix
431                 << "Should not have reached this case: resolve was "
432                 << "successfull but still have no IPs (up-to-date="
433                 << !ContinueOnOutdatedIps << ")!";
434             if (DnsMaster::get_instance()->get_resolved_ip_ttl_threshold() > 0)
435                 GlobalLogger.warning() << LogPrefix << "This probably happened "
436                     << "because you specified a TTL threshold > 0 but resolving"
437                     << " had no effect on TTLs since external cache is only "
438                     << "updated when TTL=0 is reached.";
439         }
440         else
441         {
442             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
443                 << ip_count << " (IPs may be outdated="
444                 << ContinueOnOutdatedIps << ") --> could ping now";
445             HostAnalyzer.set_resolved_ip_count( ip_count );
446         }
447         ping_when_ready();
448     }
449     else
450     {   // host name resolution failed; try again bypassing first outdated CNAME
451         // or using cached IP
452         std::string skip_host = Resolver->get_skip_cname();
453
454         if (skip_host.empty())
455         {   // try to continue with cached IPs
456             int ip_count = Resolver->get_resolved_ip_count(false);
457
458             if (ip_count == 0)
459                 GlobalLogger.notice() << LogPrefix << "DNS failed "
460                     << "and have no cached IPs either --> cannot ping";
461                 // ping_when_ready will deal with this case
462             else
463             {
464                 ContinueOnOutdatedIps = true;
465                 update_log_prefix();
466
467                 GlobalLogger.notice() << LogPrefix << "DNS failed, "
468                     << "try anyway with cached data";
469             }
470
471             GlobalLogger.info() << LogPrefix << "Set resolved_ip_count to "
472                 << ip_count << " (IPs may be outdated=" << true << ")";
473             HostAnalyzer.set_resolved_ip_count( ip_count );
474
475             ping_when_ready();
476         }
477         else
478         {   // have CNAME to continue
479             ContinueOnOutdatedIps = true;
480             update_log_prefix();
481             GlobalLogger.notice() << LogPrefix << "DNS failed, "
482                 << "try again skipping a CNAME and resolving "
483                 << skip_host << " directly";
484
485             cancel_resolve(false);
486
487             // now create new resolver
488             Resolver = DnsMaster::get_instance()
489                                    ->get_resolver_for(skip_host, *ProtocolIter);
490             start_resolving_ping_address();
491         }
492     }
493 }
494
495 /**
496  * cancel resolver if force_cancel or if it is not resolving DestinationAddress
497  *
498  * Resolvers have a life on their own: they are cached by DnsMaster so never go
499  *   out of scope and even after calling callbacks, there might still be a
500  *   longterm timer active to re-try resolving.
501  * We want to cancel that long-term timer only if the Resolver is not for our
502  *   real, original DestinationAddress but a CNAME, which can happen when trying
503  *   to skip cnames and working on out-dated IPs
504  */
505 void PingScheduler::cancel_resolve(const bool force_cancel)
506 {
507     if (force_cancel)
508     {
509         GlobalLogger.info() << "Cancelling resolver (forced)";
510         Resolver->cancel_resolve();
511     }
512     else if ( Resolver->get_hostname() == DestinationAddress )
513         GlobalLogger.info() << LogPrefix
514                             << "Leave original resolver active in background";
515     else
516     {
517         GlobalLogger.info() << LogPrefix << "Cancel resolver for "
518             << Resolver->get_hostname() << " since is not the original "
519             << DestinationAddress;
520         Resolver->cancel_resolve();
521     }
522 }
523