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