1 /* Icinga 2 | (c) 2012 Icinga GmbH | GPLv2+ */
2
3 #include "icinga/legacytimeperiod.hpp"
4 #include "base/function.hpp"
5 #include "base/convert.hpp"
6 #include "base/exception.hpp"
7 #include "base/objectlock.hpp"
8 #include "base/logger.hpp"
9 #include "base/debug.hpp"
10 #include "base/utility.hpp"
11
12 using namespace icinga;
13
14 REGISTER_FUNCTION_NONCONST(Internal, LegacyTimePeriod, &LegacyTimePeriod::ScriptFunc, "tp:begin:end");
15
16 /**
17 * Returns the same as mktime() but does not modify its argument and takes a const pointer.
18 *
19 * @param t struct tm to convert to time_t
20 * @return time_t representing the timestamp given by t
21 */
mktime_const(const tm * t)22 static time_t mktime_const(const tm *t) {
23 tm copy = *t;
24 return mktime(©);
25 }
26
IsInTimeRange(const tm * begin,const tm * end,int stride,const tm * reference)27 bool LegacyTimePeriod::IsInTimeRange(const tm *begin, const tm *end, int stride, const tm *reference)
28 {
29 time_t tsbegin, tsend, tsref;
30 tsbegin = mktime_const(begin);
31 tsend = mktime_const(end);
32 tsref = mktime_const(reference);
33
34 if (tsref < tsbegin || tsref > tsend)
35 return false;
36
37 int daynumber = (tsref - tsbegin) / (24 * 60 * 60);
38
39 if (stride > 1 && daynumber % stride > 0)
40 return false;
41
42 return true;
43 }
44
45 /**
46 * Update all day-related fields of reference (tm_year, tm_mon, tm_mday, tm_wday, tm_yday) to reference the n-th
47 * occurrence of a weekday (given by wday) in the month represented by the original value of reference.
48 *
49 * If n is negative, counting is done from the end of the month, so for example with wday=1 and n=-1, the result will be
50 * the last Monday in the month given by reference.
51 *
52 * @param wday Weekday (0 = Sunday, 1 = Monday, ..., 6 = Saturday, like tm_wday)
53 * @param n Search the n-th weekday (given by wday) in the month given by reference
54 * @param reference Input for the current month and output for the given day of that moth
55 */
FindNthWeekday(int wday,int n,tm * reference)56 void LegacyTimePeriod::FindNthWeekday(int wday, int n, tm *reference)
57 {
58 // Work on a copy to only update specific fields of reference (as documented).
59 tm t = *reference;
60
61 int dir, seen = 0;
62
63 if (n > 0) {
64 dir = 1;
65 } else {
66 n *= -1;
67 dir = -1;
68
69 /* Negative days are relative to the next month. */
70 t.tm_mon++;
71 }
72
73 ASSERT(n > 0);
74
75 t.tm_mday = 1;
76
77 for (;;) {
78 // Always operate on 00:00:00 with automatic DST detection, otherwise days could
79 // be skipped or counted twice if +-24 hours is not on the next or previous day.
80 t.tm_hour = 0;
81 t.tm_min = 0;
82 t.tm_sec = 0;
83 t.tm_isdst = -1;
84
85 mktime(&t);
86
87 if (t.tm_wday == wday) {
88 seen++;
89
90 if (seen == n)
91 break;
92 }
93
94 t.tm_mday += dir;
95 }
96
97 reference->tm_year = t.tm_year;
98 reference->tm_mon = t.tm_mon;
99 reference->tm_mday = t.tm_mday;
100 reference->tm_wday = t.tm_wday;
101 reference->tm_yday = t.tm_yday;
102 }
103
WeekdayFromString(const String & daydef)104 int LegacyTimePeriod::WeekdayFromString(const String& daydef)
105 {
106 if (daydef == "sunday")
107 return 0;
108 else if (daydef == "monday")
109 return 1;
110 else if (daydef == "tuesday")
111 return 2;
112 else if (daydef == "wednesday")
113 return 3;
114 else if (daydef == "thursday")
115 return 4;
116 else if (daydef == "friday")
117 return 5;
118 else if (daydef == "saturday")
119 return 6;
120 else
121 return -1;
122 }
123
MonthFromString(const String & monthdef)124 int LegacyTimePeriod::MonthFromString(const String& monthdef)
125 {
126 if (monthdef == "january")
127 return 0;
128 else if (monthdef == "february")
129 return 1;
130 else if (monthdef == "march")
131 return 2;
132 else if (monthdef == "april")
133 return 3;
134 else if (monthdef == "may")
135 return 4;
136 else if (monthdef == "june")
137 return 5;
138 else if (monthdef == "july")
139 return 6;
140 else if (monthdef == "august")
141 return 7;
142 else if (monthdef == "september")
143 return 8;
144 else if (monthdef == "october")
145 return 9;
146 else if (monthdef == "november")
147 return 10;
148 else if (monthdef == "december")
149 return 11;
150 else
151 return -1;
152 }
153
GetEndOfMonthDay(int year,int month)154 boost::gregorian::date LegacyTimePeriod::GetEndOfMonthDay(int year, int month)
155 {
156 boost::gregorian::date d(boost::gregorian::greg_year(year), boost::gregorian::greg_month(month), 1);
157
158 return d.end_of_month();
159 }
160
161 /**
162 * Finds the first day on or after the day given by reference and writes the beginning and end time of that day to
163 * the output parameters begin and end.
164 *
165 * @param timespec Day to find, for example "2021-10-20", "sunday", ...
166 * @param begin if != nullptr, set to 00:00:00 on that day
167 * @param end if != nullptr, set to 24:00:00 on that day (i.e. 00:00:00 of the next day)
168 * @param reference Time to begin the search at
169 */
ParseTimeSpec(const String & timespec,tm * begin,tm * end,const tm * reference)170 void LegacyTimePeriod::ParseTimeSpec(const String& timespec, tm *begin, tm *end, const tm *reference)
171 {
172 /* YYYY-MM-DD */
173 if (timespec.GetLength() == 10 && timespec[4] == '-' && timespec[7] == '-') {
174 int year = Convert::ToLong(timespec.SubStr(0, 4));
175 int month = Convert::ToLong(timespec.SubStr(5, 2));
176 int day = Convert::ToLong(timespec.SubStr(8, 2));
177
178 if (month < 1 || month > 12)
179 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
180 if (day < 1 || day > 31)
181 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid day in time specification: " + timespec));
182
183 if (begin) {
184 *begin = *reference;
185 begin->tm_year = year - 1900;
186 begin->tm_mon = month - 1;
187 begin->tm_mday = day;
188 begin->tm_hour = 0;
189 begin->tm_min = 0;
190 begin->tm_sec = 0;
191 begin->tm_isdst = -1;
192 }
193
194 if (end) {
195 *end = *reference;
196 end->tm_year = year - 1900;
197 end->tm_mon = month - 1;
198 end->tm_mday = day;
199 end->tm_hour = 24;
200 end->tm_min = 0;
201 end->tm_sec = 0;
202 end->tm_isdst = -1;
203 }
204
205 return;
206 }
207
208 std::vector<String> tokens = timespec.Split(" ");
209
210 int mon = -1;
211
212 if (tokens.size() > 1 && (tokens[0] == "day" || (mon = MonthFromString(tokens[0])) != -1)) {
213 if (mon == -1)
214 mon = reference->tm_mon;
215
216 int mday = Convert::ToLong(tokens[1]);
217
218 if (begin) {
219 *begin = *reference;
220 begin->tm_mon = mon;
221 begin->tm_mday = mday;
222 begin->tm_hour = 0;
223 begin->tm_min = 0;
224 begin->tm_sec = 0;
225 begin->tm_isdst = -1;
226
227 /* day -X: Negative days are relative to the next month. */
228 if (mday < 0) {
229 boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
230
231 //Depending on the number, we need to substract specific days (counting starts at 0).
232 d = d - boost::gregorian::days(mday * -1 - 1);
233
234 *begin = boost::gregorian::to_tm(d);
235 begin->tm_hour = 0;
236 begin->tm_min = 0;
237 begin->tm_sec = 0;
238 }
239 }
240
241 if (end) {
242 *end = *reference;
243 end->tm_mon = mon;
244 end->tm_mday = mday;
245 end->tm_hour = 24;
246 end->tm_min = 0;
247 end->tm_sec = 0;
248 end->tm_isdst = -1;
249
250 /* day -X: Negative days are relative to the next month. */
251 if (mday < 0) {
252 boost::gregorian::date d(GetEndOfMonthDay(reference->tm_year + 1900, mon + 1)); //TODO: Refactor this mess into full Boost.DateTime
253
254 //Depending on the number, we need to substract specific days (counting starts at 0).
255 d = d - boost::gregorian::days(mday * -1 - 1);
256
257 // End date is one day in the future, starting 00:00:00
258 d = d + boost::gregorian::days(1);
259
260 *end = boost::gregorian::to_tm(d);
261 end->tm_hour = 0;
262 end->tm_min = 0;
263 end->tm_sec = 0;
264 }
265 }
266
267 return;
268 }
269
270 int wday;
271
272 if (tokens.size() >= 1 && (wday = WeekdayFromString(tokens[0])) != -1) {
273 tm myref = *reference;
274 myref.tm_isdst = -1;
275
276 if (tokens.size() > 2) {
277 mon = MonthFromString(tokens[2]);
278
279 if (mon == -1)
280 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid month in time specification: " + timespec));
281
282 myref.tm_mon = mon;
283 }
284
285 int n = 0;
286
287 if (tokens.size() > 1)
288 n = Convert::ToLong(tokens[1]);
289
290 if (begin) {
291 *begin = myref;
292
293 if (tokens.size() > 1)
294 FindNthWeekday(wday, n, begin);
295 else
296 begin->tm_mday += (7 - begin->tm_wday + wday) % 7;
297
298 begin->tm_hour = 0;
299 begin->tm_min = 0;
300 begin->tm_sec = 0;
301 }
302
303 if (end) {
304 *end = myref;
305
306 if (tokens.size() > 1)
307 FindNthWeekday(wday, n, end);
308 else
309 end->tm_mday += (7 - end->tm_wday + wday) % 7;
310
311 end->tm_hour = 0;
312 end->tm_min = 0;
313 end->tm_sec = 0;
314 end->tm_mday++;
315 }
316
317 return;
318 }
319
320 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + timespec));
321 }
322
323 /**
324 * Parse a range of days.
325 *
326 * The input can have the following formats:
327 * begin
328 * begin - end
329 * begin / stride
330 * begin - end / stride
331 *
332 * @param timerange Text representation of a day range or a single day, for example "2021-10-20", "monday - friday", ...
333 * @param begin Output parameter set to 00:00:00 of the first day of the range
334 * @param end Output parameter set to 24:00:00 of the last day of the range (i.e. 00:00:00 of the day after)
335 * @param stride Output parameter for the stride (for every n-th day)
336 * @param reference Expand the range relative to this timestamp
337 */
ParseTimeRange(const String & timerange,tm * begin,tm * end,int * stride,const tm * reference)338 void LegacyTimePeriod::ParseTimeRange(const String& timerange, tm *begin, tm *end, int *stride, const tm *reference)
339 {
340 String def = timerange;
341
342 /* Figure out the stride. */
343 size_t pos = def.FindFirstOf('/');
344
345 if (pos != String::NPos) {
346 String strStride = def.SubStr(pos + 1).Trim();
347 *stride = Convert::ToLong(strStride);
348
349 /* Remove the stride parameter from the definition. */
350 def = def.SubStr(0, pos);
351 } else {
352 *stride = 1; /* User didn't specify anything, assume default. */
353 }
354
355 /* Figure out whether the user has specified two dates. */
356 pos = def.Find("- ");
357
358 if (pos != String::NPos) {
359 String first = def.SubStr(0, pos).Trim();
360
361 String second = def.SubStr(pos + 1).Trim();
362
363 ParseTimeSpec(first, begin, nullptr, reference);
364
365 /* If the second definition starts with a number we need
366 * to add the first word from the first definition, e.g.:
367 * day 1 - 15 --> "day 15" */
368 bool is_number = true;
369 size_t xpos = second.FindFirstOf(' ');
370 String fword = second.SubStr(0, xpos);
371
372 try {
373 Convert::ToLong(fword);
374 } catch (...) {
375 is_number = false;
376 }
377
378 if (is_number) {
379 xpos = first.FindFirstOf(' ');
380 ASSERT(xpos != String::NPos);
381 second = first.SubStr(0, xpos + 1) + second;
382 }
383
384 ParseTimeSpec(second, nullptr, end, reference);
385 } else {
386 ParseTimeSpec(def, begin, end, reference);
387 }
388 }
389
IsInDayDefinition(const String & daydef,const tm * reference)390 bool LegacyTimePeriod::IsInDayDefinition(const String& daydef, const tm *reference)
391 {
392 tm begin, end;
393 int stride;
394
395 ParseTimeRange(daydef, &begin, &end, &stride, reference);
396
397 Log(LogDebug, "LegacyTimePeriod")
398 << "ParseTimeRange: '" << daydef << "' => " << mktime(&begin)
399 << " -> " << mktime(&end) << ", stride: " << stride;
400
401 return IsInTimeRange(&begin, &end, stride, reference);
402 }
403
404 static inline
ProcessTimeRaw(const String & in,const tm * reference,tm * out)405 void ProcessTimeRaw(const String& in, const tm *reference, tm *out)
406 {
407 *out = *reference;
408
409 auto hd (in.Split(":"));
410
411 switch (hd.size()) {
412 case 2:
413 out->tm_sec = 0;
414 break;
415 case 3:
416 out->tm_sec = Convert::ToLong(hd[2]);
417 break;
418 default:
419 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid time specification: " + in));
420 }
421
422 out->tm_hour = Convert::ToLong(hd[0]);
423 out->tm_min = Convert::ToLong(hd[1]);
424 }
425
ProcessTimeRangeRaw(const String & timerange,const tm * reference,tm * begin,tm * end)426 void LegacyTimePeriod::ProcessTimeRangeRaw(const String& timerange, const tm *reference, tm *begin, tm *end)
427 {
428 std::vector<String> times = timerange.Split("-");
429
430 if (times.size() != 2)
431 BOOST_THROW_EXCEPTION(std::invalid_argument("Invalid timerange: " + timerange));
432
433 ProcessTimeRaw(times[0], reference, begin);
434 ProcessTimeRaw(times[1], reference, end);
435
436 if (begin->tm_hour * 3600 + begin->tm_min * 60 + begin->tm_sec >=
437 end->tm_hour * 3600 + end->tm_min * 60 + end->tm_sec)
438 end->tm_hour += 24;
439 }
440
ProcessTimeRange(const String & timestamp,const tm * reference)441 Dictionary::Ptr LegacyTimePeriod::ProcessTimeRange(const String& timestamp, const tm *reference)
442 {
443 tm begin, end;
444
445 ProcessTimeRangeRaw(timestamp, reference, &begin, &end);
446
447 return new Dictionary({
448 { "begin", (long)mktime(&begin) },
449 { "end", (long)mktime(&end) }
450 });
451 }
452
453 /**
454 * Takes a list of timeranges end expands them to concrete timestamp based on a reference time.
455 *
456 * @param timeranges String of comma separated time ranges, for example "10:00-12:00", "12:15:30-12:23:43,16:00-18:00"
457 * @param reference Starting point for searching the segments
458 * @param result For each range, a dict with keys "begin" and "end" is added
459 */
ProcessTimeRanges(const String & timeranges,const tm * reference,const Array::Ptr & result)460 void LegacyTimePeriod::ProcessTimeRanges(const String& timeranges, const tm *reference, const Array::Ptr& result)
461 {
462 std::vector<String> ranges = timeranges.Split(",");
463
464 for (const String& range : ranges) {
465 Dictionary::Ptr segment = ProcessTimeRange(range, reference);
466
467 if (segment->Get("begin") >= segment->Get("end"))
468 continue;
469
470 result->Add(segment);
471 }
472 }
473
FindRunningSegment(const String & daydef,const String & timeranges,const tm * reference)474 Dictionary::Ptr LegacyTimePeriod::FindRunningSegment(const String& daydef, const String& timeranges, const tm *reference)
475 {
476 tm begin, end, iter;
477 time_t tsend, tsiter, tsref;
478 int stride;
479
480 tsref = mktime_const(reference);
481
482 ParseTimeRange(daydef, &begin, &end, &stride, reference);
483
484 iter = begin;
485
486 tsend = mktime(&end);
487
488 do {
489 if (IsInTimeRange(&begin, &end, stride, &iter)) {
490 Array::Ptr segments = new Array();
491 ProcessTimeRanges(timeranges, &iter, segments);
492
493 Dictionary::Ptr bestSegment;
494 double bestEnd = 0.0;
495
496 ObjectLock olock(segments);
497 for (const Dictionary::Ptr& segment : segments) {
498 double begin = segment->Get("begin");
499 double end = segment->Get("end");
500
501 if (begin >= tsref || end < tsref)
502 continue;
503
504 if (!bestSegment || end > bestEnd) {
505 bestSegment = segment;
506 bestEnd = end;
507 }
508 }
509
510 if (bestSegment)
511 return bestSegment;
512 }
513
514 iter.tm_mday++;
515 iter.tm_hour = 0;
516 iter.tm_min = 0;
517 iter.tm_sec = 0;
518 tsiter = mktime(&iter);
519 } while (tsiter < tsend);
520
521 return nullptr;
522 }
523
FindNextSegment(const String & daydef,const String & timeranges,const tm * reference)524 Dictionary::Ptr LegacyTimePeriod::FindNextSegment(const String& daydef, const String& timeranges, const tm *reference)
525 {
526 tm begin, end, iter, ref;
527 time_t tsend, tsiter, tsref;
528 int stride;
529
530 for (int pass = 1; pass <= 2; pass++) {
531 if (pass == 1) {
532 ref = *reference;
533 } else {
534 ref = end;
535 ref.tm_mday++;
536 }
537
538 tsref = mktime(&ref);
539
540 ParseTimeRange(daydef, &begin, &end, &stride, &ref);
541
542 iter = begin;
543
544 tsend = mktime(&end);
545
546 do {
547 if (IsInTimeRange(&begin, &end, stride, &iter)) {
548 Array::Ptr segments = new Array();
549 ProcessTimeRanges(timeranges, &iter, segments);
550
551 Dictionary::Ptr bestSegment;
552 double bestBegin;
553
554 ObjectLock olock(segments);
555 for (const Dictionary::Ptr& segment : segments) {
556 double begin = segment->Get("begin");
557
558 if (begin < tsref)
559 continue;
560
561 if (!bestSegment || begin < bestBegin) {
562 bestSegment = segment;
563 bestBegin = begin;
564 }
565 }
566
567 if (bestSegment)
568 return bestSegment;
569 }
570
571 iter.tm_mday++;
572 iter.tm_hour = 0;
573 iter.tm_min = 0;
574 iter.tm_sec = 0;
575 tsiter = mktime(&iter);
576 } while (tsiter < tsend);
577 }
578
579 return nullptr;
580 }
581
ScriptFunc(const TimePeriod::Ptr & tp,double begin,double end)582 Array::Ptr LegacyTimePeriod::ScriptFunc(const TimePeriod::Ptr& tp, double begin, double end)
583 {
584 Array::Ptr segments = new Array();
585
586 Dictionary::Ptr ranges = tp->GetRanges();
587
588 if (ranges) {
589 for (int i = 0; i <= (end - begin) / (24 * 60 * 60); i++) {
590 time_t refts = begin + i * 24 * 60 * 60;
591 tm reference = Utility::LocalTime(refts);
592
593 #ifdef I2_DEBUG
594 Log(LogDebug, "LegacyTimePeriod")
595 << "Checking reference time " << refts;
596 #endif /* I2_DEBUG */
597
598 ObjectLock olock(ranges);
599 for (const Dictionary::Pair& kv : ranges) {
600 if (!IsInDayDefinition(kv.first, &reference)) {
601 #ifdef I2_DEBUG
602 Log(LogDebug, "LegacyTimePeriod")
603 << "Not in day definition '" << kv.first << "'.";
604 #endif /* I2_DEBUG */
605 continue;
606 }
607
608 #ifdef I2_DEBUG
609 Log(LogDebug, "LegacyTimePeriod")
610 << "In day definition '" << kv.first << "'.";
611 #endif /* I2_DEBUG */
612
613 ProcessTimeRanges(kv.second, &reference, segments);
614 }
615 }
616 }
617
618 Log(LogDebug, "LegacyTimePeriod")
619 << "Legacy timeperiod update returned " << segments->GetLength() << " segments.";
620
621 return segments;
622 }
623