diff --git a/cfgmgr/Makefile.am b/cfgmgr/Makefile.am index a8cbddb4e7..b5a0086c9a 100644 --- a/cfgmgr/Makefile.am +++ b/cfgmgr/Makefile.am @@ -5,7 +5,7 @@ LIBNL_LIBS = -lnl-genl-3 -lnl-route-3 -lnl-3 SAIMETA_LIBS = -lsaimeta -lsaimetadata -lzmq COMMON_LIBS = -lswsscommon -bin_PROGRAMS = vlanmgrd teammgrd portmgrd intfmgrd buffermgrd vrfmgrd nbrmgrd vxlanmgrd sflowmgrd natmgrd coppmgrd tunnelmgrd macsecmgrd fabricmgrd +bin_PROGRAMS = vlanmgrd teammgrd portmgrd intfmgrd buffermgrd vrfmgrd nbrmgrd vxlanmgrd sflowmgrd natmgrd coppmgrd tunnelmgrd macsecmgrd fabricmgrd timerangemgrd scheduledconfigmgrd cfgmgrdir = $(datadir)/swss @@ -96,6 +96,16 @@ tunnelmgrd_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CF tunnelmgrd_CPPFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) tunnelmgrd_LDADD = $(LDFLAGS_ASAN) $(COMMON_LIBS) $(SAIMETA_LIBS) +timerangemgrd_SOURCES = timerangemgrd.cpp timerangemgr.cpp $(COMMON_ORCH_SOURCE) shellcmd.h +timerangemgrd_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) +timerangemgrd_CPPFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) +timerangemgrd_LDADD = $(LDFLAGS_ASAN) $(COMMON_LIBS) $(SAIMETA_LIBS) + +scheduledconfigmgrd_SOURCES = scheduledconfigmgrd.cpp scheduledconfigmgr.cpp $(COMMON_ORCH_SOURCE) shellcmd.h +scheduledconfigmgrd_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) +scheduledconfigmgrd_CPPFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) +scheduledconfigmgrd_LDADD = $(LDFLAGS_ASAN) $(COMMON_LIBS) $(SAIMETA_LIBS) + macsecmgrd_SOURCES = macsecmgrd.cpp macsecmgr.cpp $(COMMON_ORCH_SOURCE) shellcmd.h macsecmgrd_CFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) macsecmgrd_CPPFLAGS = $(DBGFLAGS) $(AM_CFLAGS) $(CFLAGS_COMMON) $(CFLAGS_SAI) $(CFLAGS_ASAN) @@ -115,6 +125,8 @@ sflowmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp natmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp coppmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp tunnelmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp +timerangemgrd_SOURCES += ../gcovpreload/gcovpreload.cpp +scheduledconfigmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp macsecmgrd_SOURCES += ../gcovpreload/gcovpreload.cpp endif @@ -133,5 +145,7 @@ coppmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp tunnelmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp macsecmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp fabricmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp +timerangemgrd_SOURCES += $(top_srcdir)/lib/asan.cpp +scheduledconfigmgrd_SOURCES += $(top_srcdir)/lib/asan.cpp endif diff --git a/cfgmgr/croncpp.h b/cfgmgr/croncpp.h new file mode 100644 index 0000000000..f0a898524f --- /dev/null +++ b/cfgmgr/croncpp.h @@ -0,0 +1,917 @@ +/* File taken from https://github.com/mariusbancila/croncpp */ +/* +MIT License + +Copyright (c) 2018 Marius Bancila + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if __cplusplus > 201402L +#include +#define CRONCPP_IS_CPP17 +#endif + +namespace cron +{ +#ifdef CRONCPP_IS_CPP17 +#define CRONCPP_STRING_VIEW std::string_view +#define CRONCPP_STRING_VIEW_NPOS std::string_view::npos +#define CRONCPP_CONSTEXPTR constexpr +#else +#define CRONCPP_STRING_VIEW std::string const & +#define CRONCPP_STRING_VIEW_NPOS std::string::npos +#define CRONCPP_CONSTEXPTR +#endif + + using cron_int = uint8_t; + + constexpr std::time_t INVALID_TIME = static_cast(-1); + + constexpr size_t INVALID_INDEX = static_cast(-1); + + class cronexpr; + + namespace detail + { + enum class cron_field + { + minute, + hour_of_day, + day_of_week, + day_of_month, + month, + year + }; + + template + static bool find_next(cronexpr const &cex, + std::tm &date, + size_t const dot); + } + + struct bad_cronexpr : public std::runtime_error + { + public: + explicit bad_cronexpr(CRONCPP_STRING_VIEW message) : std::runtime_error(message.data()) + { + } + }; + + struct cron_standard_traits + { + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 0; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 6; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +#else + static std::vector &DAYS() + { + static std::vector days = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + return days; + } + + static std::vector &MONTHS() + { + static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + return months; + } +#endif + }; + + struct cron_oracle_traits + { + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 0; + static const cron_int CRON_MAX_MONTHS = 11; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +#else + + static std::vector &DAYS() + { + static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + return days; + } + + static std::vector &MONTHS() + { + static std::vector months = {"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + return months; + } +#endif + }; + + struct cron_quartz_traits + { + static const cron_int CRON_MIN_MINUTES = 0; + static const cron_int CRON_MAX_MINUTES = 59; + + static const cron_int CRON_MIN_HOURS = 0; + static const cron_int CRON_MAX_HOURS = 23; + + static const cron_int CRON_MIN_DAYS_OF_WEEK = 1; + static const cron_int CRON_MAX_DAYS_OF_WEEK = 7; + + static const cron_int CRON_MIN_DAYS_OF_MONTH = 1; + static const cron_int CRON_MAX_DAYS_OF_MONTH = 31; + + static const cron_int CRON_MIN_MONTHS = 1; + static const cron_int CRON_MAX_MONTHS = 12; + + static const cron_int CRON_MAX_YEARS_DIFF = 4; + +#ifdef CRONCPP_IS_CPP17 + static const inline std::vector DAYS = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + static const inline std::vector MONTHS = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; +#else + static std::vector &DAYS() + { + static std::vector days = {"NIL", "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"}; + return days; + } + + static std::vector &MONTHS() + { + static std::vector months = {"NIL", "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"}; + return months; + } +#endif + }; + + class cronexpr; + + template + static cronexpr make_cron(CRONCPP_STRING_VIEW expr); + + class cronexpr + { + std::bitset<60> minutes; + std::bitset<24> hours; + std::bitset<7> days_of_week; + std::bitset<31> days_of_month; + std::bitset<12> months; + std::string expr; + + friend bool operator==(cronexpr const &e1, cronexpr const &e2); + friend bool operator!=(cronexpr const &e1, cronexpr const &e2); + + template + friend bool detail::find_next(cronexpr const &cex, + std::tm &date, + size_t const dot); + + friend std::string to_cronstr(cronexpr const &cex); + friend std::string to_string(cronexpr const &cex); + + template + friend cronexpr make_cron(CRONCPP_STRING_VIEW expr); + }; + + inline bool operator==(cronexpr const &e1, cronexpr const &e2) + { + return e1.minutes == e2.minutes && + e1.hours == e2.hours && + e1.days_of_week == e2.days_of_week && + e1.days_of_month == e2.days_of_month && + e1.months == e2.months; + } + + inline bool operator!=(cronexpr const &e1, cronexpr const &e2) + { + return !(e1 == e2); + } + + inline std::string to_string(cronexpr const &cex) + { + return cex.minutes.to_string() + " " + + cex.hours.to_string() + " " + + cex.days_of_month.to_string() + " " + + cex.months.to_string() + " " + + cex.days_of_week.to_string(); + } + + inline std::string to_cronstr(cronexpr const &cex) + { + return cex.expr; + } + + namespace utils + { + inline std::time_t tm_to_time(std::tm &date) + { + return std::mktime(&date); + } + + inline std::tm *time_to_tm(std::time_t const *date, std::tm *const out) + { +#ifdef _WIN32 + errno_t err = localtime_s(out, date); + return 0 == err ? out : nullptr; +#else + return localtime_r(date, out); +#endif + } + + inline std::tm to_tm(CRONCPP_STRING_VIEW time) + { + std::tm result; +#if __cplusplus > 201103L + std::istringstream str(time.data()); + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + + str >> std::get_time(&result, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) + throw std::runtime_error("Parsing date failed!"); +#else + int year = 1900; + int month = 1; + int day = 1; + int hour = 0; + int minute = 0; + int second = 0; + sscanf(time.data(), "%d-%d-%d %d:%d:%d", &year, &month, &day, &hour, &minute, &second); + result.tm_year = year - 1900; + result.tm_mon = month - 1; + result.tm_mday = day; + result.tm_hour = hour; + result.tm_min = minute; + result.tm_sec = second; +#endif + result.tm_isdst = -1; // DST info not available + + return result; + } + + inline std::string to_string(std::tm const &tm) + { +#if __cplusplus > 201103L + std::ostringstream str; + str.imbue(std::locale(setlocale(LC_ALL, nullptr))); + str << std::put_time(&tm, "%Y-%m-%d %H:%M:%S"); + if (str.fail()) + throw std::runtime_error("Writing date failed!"); + + return str.str(); +#else + char buff[70] = {0}; + strftime(buff, sizeof(buff), "%Y-%m-%d %H:%M:%S", &tm); + return std::string(buff); +#endif + } + + inline std::string to_upper(std::string text) + { + std::transform(std::begin(text), std::end(text), + std::begin(text), [](char const c) + { return static_cast(std::toupper(c)); }); + + return text; + } + + static std::vector split(CRONCPP_STRING_VIEW text, char const delimiter) + { + std::vector tokens; + std::string token; + std::istringstream tokenStream(text.data()); + while (std::getline(tokenStream, token, delimiter)) + { + tokens.push_back(token); + } + return tokens; + } + + CRONCPP_CONSTEXPTR inline bool contains(CRONCPP_STRING_VIEW text, char const ch) noexcept + { + return CRONCPP_STRING_VIEW_NPOS != text.find_first_of(ch); + } + } + + namespace detail + { + + inline cron_int to_cron_int(CRONCPP_STRING_VIEW text) + { + try + { + return static_cast(std::stoul(text.data())); + } + catch (std::exception const &ex) + { + throw bad_cronexpr(ex.what()); + } + } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-function" + static std::string replace_ordinals( + std::string text, + std::vector const &replacement) + { + for (size_t i = 0; i < replacement.size(); ++i) + { + auto pos = text.find(replacement[i]); + if (std::string::npos != pos) + text.replace(pos, 3, std::to_string(i)); + } + + return text; + } +#pragma GCC diagnostic pop + + static std::pair make_range( + CRONCPP_STRING_VIEW field, + cron_int const minval, + cron_int const maxval) + { + cron_int first = 0; + cron_int last = 0; + if (field.size() == 1 && field[0] == '*') + { + first = minval; + last = maxval; + } + else if (!utils::contains(field, '-')) + { + first = to_cron_int(field); + last = first; + } + else + { + auto parts = utils::split(field, '-'); + if (parts.size() != 2) + throw bad_cronexpr("Specified range requires two fields"); + + first = to_cron_int(parts[0]); + last = to_cron_int(parts[1]); + } + + if (first > maxval || last > maxval) + { + throw bad_cronexpr("Specified range exceeds maximum"); + } + if (first < minval || last < minval) + { + throw bad_cronexpr("Specified range is less than minimum"); + } + if (first > last) + { + throw bad_cronexpr("Specified range start exceeds range end"); + } + + return {first, last}; + } + + template + static void set_cron_field( + CRONCPP_STRING_VIEW value, + std::bitset &target, + cron_int const minval, + cron_int const maxval) + { + if (value.length() > 0 && value[value.length() - 1] == ',') + throw bad_cronexpr("Value cannot end with comma"); + + auto fields = utils::split(value, ','); + if (fields.empty()) + throw bad_cronexpr("Expression parsing error"); + + for (auto const &field : fields) + { + if (!utils::contains(field, '/')) + { +#ifdef CRONCPP_IS_CPP17 + auto [first, last] = detail::make_range(field, minval, maxval); +#else + auto range = detail::make_range(field, minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + for (cron_int i = first - minval; i <= last - minval; ++i) + { + target.set(i); + } + } + else + { + auto parts = utils::split(field, '/'); + if (parts.size() != 2) + throw bad_cronexpr("Incrementer must have two fields"); + +#ifdef CRONCPP_IS_CPP17 + auto [first, last] = detail::make_range(parts[0], minval, maxval); +#else + auto range = detail::make_range(parts[0], minval, maxval); + auto first = range.first; + auto last = range.second; +#endif + + if (!utils::contains(parts[0], '-')) + { + last = maxval; + } + + auto delta = detail::to_cron_int(parts[1]); + if (delta <= 0) + throw bad_cronexpr("Incrementer must be a positive value"); + + for (cron_int i = first - minval; i <= last - minval; i += delta) + { + target.set(i); + } + } + } + } + + template + static void set_cron_days_of_week( + std::string value, + std::bitset<7> &target) + { + auto days = utils::to_upper(value); + auto days_replaced = detail::replace_ordinals( + days, +#ifdef CRONCPP_IS_CPP17 + Traits::DAYS +#else + Traits::DAYS() +#endif + ); + + if (days_replaced.size() == 1 && days_replaced[0] == '?') + days_replaced[0] = '*'; + + set_cron_field( + days_replaced, + target, + Traits::CRON_MIN_DAYS_OF_WEEK, + Traits::CRON_MAX_DAYS_OF_WEEK); + } + + template + static void set_cron_days_of_month( + std::string value, + std::bitset<31> &target) + { + if (value.size() == 1 && value[0] == '?') + value[0] = '*'; + + set_cron_field( + value, + target, + Traits::CRON_MIN_DAYS_OF_MONTH, + Traits::CRON_MAX_DAYS_OF_MONTH); + } + + template + static void set_cron_month( + std::string value, + std::bitset<12> &target) + { + auto month = utils::to_upper(value); + auto month_replaced = detail::replace_ordinals( + month, +#ifdef CRONCPP_IS_CPP17 + Traits::MONTHS +#else + Traits::MONTHS() +#endif + ); + + set_cron_field( + month_replaced, + target, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS); + } + + template + inline size_t next_set_bit( + std::bitset const &target, + size_t /*minimum*/, + size_t /*maximum*/, + size_t offset) + { + for (auto i = offset; i < N; ++i) + { + if (target.test(i)) + return i; + } + + return INVALID_INDEX; + } + + inline void add_to_field( + std::tm &date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::minute: + date.tm_min += val; + break; + case cron_field::hour_of_day: + date.tm_hour += val; + break; + case cron_field::day_of_week: + case cron_field::day_of_month: + date.tm_mday += val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon += val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year += val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void set_field( + std::tm &date, + cron_field const field, + int const val) + { + switch (field) + { + case cron_field::minute: + date.tm_min = val; + break; + case cron_field::hour_of_day: + date.tm_hour = val; + break; + case cron_field::day_of_week: + date.tm_wday = val; + break; + case cron_field::day_of_month: + date.tm_mday = val; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = val; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = val; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_field( + std::tm &date, + cron_field const field) + { + switch (field) + { + case cron_field::minute: + date.tm_min = 0; + break; + case cron_field::hour_of_day: + date.tm_hour = 0; + break; + case cron_field::day_of_week: + date.tm_wday = 0; + break; + case cron_field::day_of_month: + date.tm_mday = 1; + date.tm_isdst = -1; + break; + case cron_field::month: + date.tm_mon = 0; + date.tm_isdst = -1; + break; + case cron_field::year: + date.tm_year = 0; + break; + } + + if (INVALID_TIME == utils::tm_to_time(date)) + throw bad_cronexpr("Invalid time expression"); + } + + inline void reset_all_fields( + std::tm &date, + std::bitset<6> const &marked_fields) + { + for (size_t i = 0; i < marked_fields.size(); ++i) + { + if (marked_fields.test(i)) + reset_field(date, static_cast(i)); + } + } + + inline void mark_field( + std::bitset<6> &orders, + cron_field const field) + { + if (!orders.test(static_cast(field))) + orders.set(static_cast(field)); + } + + template + static size_t find_next( + std::bitset const &target, + std::tm &date, + unsigned int const minimum, + unsigned int const maximum, + unsigned int const value, + cron_field const field, + cron_field const next_field, + std::bitset<6> const &marked_fields) + { + auto next_value = next_set_bit(target, minimum, maximum, value); + if (INVALID_INDEX == next_value) + { + add_to_field(date, next_field, 1); + reset_field(date, field); + next_value = next_set_bit(target, minimum, maximum, 0); + } + + if (INVALID_INDEX == next_value || next_value != value) + { + set_field(date, field, static_cast(next_value)); + reset_all_fields(date, marked_fields); + } + + return next_value; + } + + template + static size_t find_next_day( + std::tm &date, + std::bitset<31> const &days_of_month, + size_t day_of_month, + std::bitset<7> const &days_of_week, + size_t day_of_week, + std::bitset<6> const &marked_fields) + { + unsigned int count = 0; + unsigned int maximum = 366; + while ( + (!days_of_month.test(day_of_month - Traits::CRON_MIN_DAYS_OF_MONTH) || + !days_of_week.test(day_of_week - Traits::CRON_MIN_DAYS_OF_WEEK)) && + count++ < maximum) + { + add_to_field(date, cron_field::day_of_month, 1); + + day_of_month = date.tm_mday; + day_of_week = date.tm_wday; + + reset_all_fields(date, marked_fields); + } + + return day_of_month; + } + + template + static bool find_next(cronexpr const &cex, + std::tm &date, + size_t const dot) + { + bool res = true; + + std::bitset<6> marked_fields{0}; + std::bitset<6> empty_list{0}; + + unsigned int minute = date.tm_min; + auto update_minute = find_next( + cex.minutes, + date, + Traits::CRON_MIN_MINUTES, + Traits::CRON_MAX_MINUTES, + minute, + cron_field::minute, + cron_field::hour_of_day, + marked_fields); + if (minute == update_minute) + { + mark_field(marked_fields, cron_field::minute); + } + else + { + res = find_next(cex, date, dot); + if (!res) + return res; + } + + unsigned int hour = date.tm_hour; + auto updated_hour = find_next( + cex.hours, + date, + Traits::CRON_MIN_HOURS, + Traits::CRON_MAX_HOURS, + hour, + cron_field::hour_of_day, + cron_field::day_of_week, + marked_fields); + if (hour == updated_hour) + { + mark_field(marked_fields, cron_field::hour_of_day); + } + else + { + res = find_next(cex, date, dot); + if (!res) + return res; + } + + unsigned int day_of_week = date.tm_wday; + unsigned int day_of_month = date.tm_mday; + auto updated_day_of_month = find_next_day( + date, + cex.days_of_month, + day_of_month, + cex.days_of_week, + day_of_week, + marked_fields); + if (day_of_month == updated_day_of_month) + { + mark_field(marked_fields, cron_field::day_of_month); + } + else + { + res = find_next(cex, date, dot); + if (!res) + return res; + } + + unsigned int month = date.tm_mon; + auto updated_month = find_next( + cex.months, + date, + Traits::CRON_MIN_MONTHS, + Traits::CRON_MAX_MONTHS, + month, + cron_field::month, + cron_field::year, + marked_fields); + if (month != updated_month) + { + if (date.tm_year - dot > Traits::CRON_MAX_YEARS_DIFF) + return false; + + res = find_next(cex, date, dot); + if (!res) + return res; + } + + return res; + } + } + + template + static cronexpr make_cron(CRONCPP_STRING_VIEW expr) + { + cronexpr cex; + + if (expr.empty()) + throw bad_cronexpr("Invalid empty cron expression"); + + auto fields = utils::split(expr, ' '); + fields.erase( + std::remove_if(std::begin(fields), std::end(fields), + [](CRONCPP_STRING_VIEW s) + { return s.empty(); }), + std::end(fields)); + if (fields.size() != 5) + throw bad_cronexpr("cron expression must have five fields"); + + detail::set_cron_field(fields[0], cex.minutes, Traits::CRON_MIN_MINUTES, Traits::CRON_MAX_MINUTES); + detail::set_cron_field(fields[1], cex.hours, Traits::CRON_MIN_HOURS, Traits::CRON_MAX_HOURS); + + detail::set_cron_days_of_week(fields[4], cex.days_of_week); + + detail::set_cron_days_of_month(fields[2], cex.days_of_month); + + detail::set_cron_month(fields[3], cex.months); + + cex.expr = expr; + + return cex; + } + + template + static std::tm cron_next(cronexpr const &cex, std::tm date) + { + time_t original = utils::tm_to_time(date); + if (INVALID_TIME == original) + return {}; + + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + + time_t calculated = utils::tm_to_time(date); + if (INVALID_TIME == calculated) + return {}; + + if (calculated == original) + { + add_to_field(date, detail::cron_field::minute, 1); + if (!detail::find_next(cex, date, date.tm_year)) + return {}; + } + + return date; + } + + template + static std::time_t cron_next(cronexpr const &cex, std::time_t const &date) + { + std::tm val; + std::tm *dt = utils::time_to_tm(&date, &val); + if (dt == nullptr) + return INVALID_TIME; + + time_t original = utils::tm_to_time(*dt); + if (INVALID_TIME == original) + return INVALID_TIME; + + if (!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + + time_t calculated = utils::tm_to_time(*dt); + if (INVALID_TIME == calculated) + return calculated; + + if (calculated == original) + { + add_to_field(*dt, detail::cron_field::minute, 1); + if (!detail::find_next(cex, *dt, dt->tm_year)) + return INVALID_TIME; + } + + return utils::tm_to_time(*dt); + } + + template + static std::chrono::system_clock::time_point cron_next(cronexpr const &cex, std::chrono::system_clock::time_point const &time_point) + { + return std::chrono::system_clock::from_time_t(cron_next(cex, std::chrono::system_clock::to_time_t(time_point))); + } +} diff --git a/cfgmgr/scheduledconfigmgr.cpp b/cfgmgr/scheduledconfigmgr.cpp new file mode 100644 index 0000000000..6b755866c8 --- /dev/null +++ b/cfgmgr/scheduledconfigmgr.cpp @@ -0,0 +1,619 @@ +#include +#include +#include "select.h" +#include "schema.h" +#include +#include +#include "dbconnector.h" +#include "scheduledconfigmgr.h" +#include "json.h" +#include +#include "tokenize.h" + +using namespace std; +using namespace swss; +using json = nlohmann::json; + +ScheduledConfigMgr::ScheduledConfigMgr(vector &connectors, DBConnector *appDb) : Orch(connectors) +{ + m_appDb = appDb; +} + +string ScheduledConfigMgr::findTimeRangeByConfiguration(string scheduledConfigurationName) { + for (const auto& pair : scheduledConfigurations) { + const std::string& timeRangeName = pair.first; + const ConfigData& configDataMap = pair.second; + + // Check if the scheduledConfigurationName exists in the inner ConfigData map + if (configDataMap.find(scheduledConfigurationName) != configDataMap.end()) { + return timeRangeName; + } + } + return ""; // Return an empty string if not found +} + +DBConnector* ScheduledConfigMgr::getDBConnector(const string &tableName){ + + Consumer* tableConsumer = static_cast(getExecutor(tableName)); + if (!tableConsumer) + { + SWSS_LOG_ERROR("Failed to get consumer for %s", tableName.c_str()); + return nullptr; + } + DBConnector* tableDBConnector = const_cast(tableConsumer->getDbConnector()); + if (!tableDBConnector) { + SWSS_LOG_ERROR("Failed to get DB connector for %s", tableName.c_str()); + return nullptr; + } + return tableDBConnector; +} + +string join(const json &jsonArray, const string &delimiter) +{ + // Join the elements of the JSON array into a single string + string result; + + for (auto it = jsonArray.begin(); it != jsonArray.end(); ++it) + { + if (!result.empty()) + { + result += delimiter; + } + // Convert each element to a string, handling non-string types appropriately + if (it->is_string()) + { + result += it->get(); + } + else + { + // For non-string types, use dump() to serialize + // dump() serializes the json object to a string representation + // Since dump() includes quotes around strings, we remove them for consistency + string element = it->dump(); + // Optionally trim quotes if desired, especially if consistency with direct strings is needed + if (element.front() == '"' && element.back() == '"') + { + element = element.substr(1, element.length() - 2); + } + result += element; + } + } + + return result; +} + +vector convertJsonToFieldValues(const json &jsonObj) +{ + vector fieldValues{}; + for (const auto &item : jsonObj.items()) + { + string key = item.key(); + if (item.value().is_primitive()) + { + // Removing quotes from primitive types to match the expected format + string value = item.value().dump(); + if (value.front() == '"' && value.back() == '"') + { + value = value.substr(1, value.length() - 2); + } + fieldValues.emplace_back(key, value); + } + else if (item.value().is_array()) + { + string arrayValues = join(item.value(), ","); + fieldValues.emplace_back(key, arrayValues); + } + } + return fieldValues; +} + +bool ScheduledConfigMgr::isTimeRangeActive(const string &timeRangeName) +{ + SWSS_LOG_ENTER(); + shared_ptr statusPtr{}; + string key = ""; + DBConnector* timeRangeStatusDBConnector = getDBConnector(STATE_TIME_RANGE_STATUS_TABLE_NAME); + if (!timeRangeStatusDBConnector) + return false; + + key = STATE_TIME_RANGE_STATUS_TABLE_NAME + SonicDBConfig::getSeparator(timeRangeStatusDBConnector) + timeRangeName; + statusPtr = timeRangeStatusDBConnector->hget(key, "status"); + if (!statusPtr){ + SWSS_LOG_ERROR("Failed to get time range status for %s", timeRangeName.c_str()); + return false; + } + + return *statusPtr=="active"; +} + +bool ScheduledConfigMgr::applyTableConfiguration(const std::string &tableName, const json &tableKeyFields) +{ + SWSS_LOG_ENTER(); + + // Create a Table object for the given tableName + ProducerStateTable tableObj(m_appDb, tableName); + + + // Extract the key and fieldValues from the JSON object + for (auto it = tableKeyFields.begin(); it != tableKeyFields.end(); ++it) { + + // Extract the key and value from the iterator + const string& key = it.key(); + const json& fieldValuesJson = it.value(); + + // Here, we convert the JSON tableDetails into the vector format. + vector fieldValues = convertJsonToFieldValues(fieldValuesJson); + if (fieldValues.empty()) + { + SWSS_LOG_ERROR("Failed to convert JSON to FieldValueTuple for table: %s", tableName.c_str()); + return false; + } + + // Create a Table object and set the field values + tableObj.set(key, fieldValues); + } + return true; +} + +bool ScheduledConfigMgr::removeTableConfiguration(const string &tableName, const string &key) +{ + SWSS_LOG_ENTER(); + + // Create a Table object for the given tableName + ProducerStateTable tableObj(m_appDb, tableName); + + // Create a Table object and set the field values + tableObj.del(key); + return true; +} + +task_process_status ScheduledConfigMgr::applyConfiguration(const string &configName, const json &configJson) +{ + SWSS_LOG_ENTER(); + + for (const auto &tableEntry : configJson.items()) + { + string tableName = tableEntry.key(); // Table name + const json &tableKeyFields = tableEntry.value(); // Table details + + if (!applyTableConfiguration(tableName, tableKeyFields)) + { + SWSS_LOG_ERROR("Failed to apply configuration %s for table: %s", configName.c_str(), tableName.c_str()); + return task_process_status::task_failed; + } + } + + return task_process_status::task_success; +} + +task_process_status ScheduledConfigMgr::removeConfiguration(const string &configName, const json &configJson) +{ + SWSS_LOG_ENTER(); + std::string tableName; + std::string key; + + for (const auto &tableEntry : configJson.items()) { + tableName = tableEntry.key(); // e.g., "ACL_TABLE_TABLE" or "ACL_TABLE_TABLE:ACL_TABLE_NAME" + const json &innerObject = tableEntry.value(); + + // Check if the outer key already contains the entire table name and key + size_t pos = tableName.find(':'); + if (pos != std::string::npos) { + key = tableName.substr(pos + 1); // Extract the key part after ':' + tableName = tableName.substr(0, pos); // Extract the table name part before ':' + } else if (innerObject.is_object()) { + // Iterate through the inner object to get the key + for (const auto &innerEntry : innerObject.items()) { + key = innerEntry.key(); // e.g., "ACL_TABLE_NAME" + break; // We only need the first key for this function + } + } else { + SWSS_LOG_ERROR("Expected JSON object for key: %s", tableName.c_str()); + return task_process_status::task_failed; + } + + // Call removeTableConfiguration with the parsed tableName and key + if (!removeTableConfiguration(tableName, key)) { + SWSS_LOG_ERROR("Failed to remove configuration %s for table: %s with key: %s", configName.c_str(), tableName.c_str(), key.c_str()); + return task_process_status::task_failed; + } + } + + return task_process_status::task_success; +} + +// TODO - Implement this function +bool ScheduledConfigMgr::validateConfiguration(const json &configJson) +{ + SWSS_LOG_ENTER(); + return true; +} + +task_process_status ScheduledConfigMgr::doProcessScheduledConfiguration(string timeRangeName, string scheduledConfigName, string configuration) +{ + SWSS_LOG_ENTER(); + SWSS_LOG_INFO("Processing scheduled configuration %s for time range %s", scheduledConfigName.c_str(), timeRangeName.c_str()); + task_process_status task_status = task_process_status::task_success; + + // Validate timeRangeName, scheduledConfigName, and configuration + if (timeRangeName.empty() || scheduledConfigName.empty() || configuration.empty()) + { + SWSS_LOG_ERROR("Invalid arguments for scheduled configuration: timeRangeName, scheduledConfigName, or configuration is empty"); + return task_process_status::task_invalid_entry; + } + + try + { + // Parse the configuration string into a JSON object for validation + // Assuming the configuration is in a JSON string format + SWSS_LOG_DEBUG("===JSON CONFIGURATION STRING BEFORE PROCESS==="); + SWSS_LOG_DEBUG("%s", configuration.c_str()); + + // Simple replacement of single quotes with double quotes + // Necessary for json to succesfully parse the data + replace(configuration.begin(), configuration.end(), '\'', '\"'); + + SWSS_LOG_DEBUG("===JSON CONFIGURATION STRING AFTER PROCESS==="); + SWSS_LOG_DEBUG("%s", configuration.c_str()); + + json configJson = json::parse(configuration); + + if (!validateConfiguration(configJson)) + { + SWSS_LOG_ERROR("Configuration validation failed for %s", scheduledConfigName.c_str()); + return task_process_status::task_failed; + } + + // Verify time range does not exist in the scheduledConfigurations hashmap + if (scheduledConfigurations[timeRangeName].find(scheduledConfigName) != scheduledConfigurations[timeRangeName].end()) + { + SWSS_LOG_ERROR("Scheduled configuration %s already exists for time range %s", scheduledConfigName.c_str(), timeRangeName.c_str()); + return task_process_status::task_failed; + } + + // Add the configuration to the scheduledConfigurations hashmap + scheduledConfigurations[timeRangeName][scheduledConfigName] = configJson; + SWSS_LOG_INFO("Successfully added %s to time range %s ", scheduledConfigName.c_str(), timeRangeName.c_str()); + + // Apply the configuration if the time range currently is active + if (isTimeRangeActive(timeRangeName)) + { + if (task_process_status::task_success != applyConfiguration(scheduledConfigName, configJson)) + { + SWSS_LOG_ERROR("Could not apply configuration for time range %s, configName: %s", timeRangeName.c_str(), scheduledConfigName.c_str()); + return task_process_status::task_need_retry; + } + // Add the configuration to the scheduledConfigurationStatus hashmap with status true + scheduledConfigurationStatus[scheduledConfigName] = true; + SWSS_LOG_INFO("Applied configuration for time range %s, configName: %s", timeRangeName.c_str(), scheduledConfigName.c_str()); + } else { + // Add the configuration to the scheduledConfigurationStatus hashmap with status false + scheduledConfigurationStatus[scheduledConfigName] = false; + } + } + catch (const json::exception &e) + { + SWSS_LOG_ERROR("JSON parsing error: %s", e.what()); + task_status = task_process_status::task_failed; + } + catch (const std::exception &e) + { + SWSS_LOG_ERROR("Error processing scheduled configuration: %s", e.what()); + task_status = task_process_status::task_failed; + } + return task_status; +} + +task_process_status ScheduledConfigMgr::doProcessTimeRangeStatus(string timeRangeName, string status) +{ + SWSS_LOG_ENTER(); + SWSS_LOG_INFO("Processing time range status for time range %s", timeRangeName.c_str()); + task_process_status task_status = task_process_status::task_success; + + // Validate timeRangeName and status + if (timeRangeName.empty() || status.empty()) + { + SWSS_LOG_ERROR("Invalid arguments for time range status: timeRangeName or status is empty"); + return task_process_status::task_invalid_entry; + } + + try + { + // Check if the time range exists in the scheduledConfigurations hashmap + if (scheduledConfigurations.find(timeRangeName) == scheduledConfigurations.end()) + { + SWSS_LOG_INFO("Time range %s is being created in the local db", timeRangeName.c_str()); + // Create the time range in the local db with default value + scheduledConfigurations[timeRangeName]; + + SWSS_LOG_INFO("Adding unbound configurations for time range %s", timeRangeName.c_str()); + if (unboundConfigurations.find(timeRangeName) != unboundConfigurations.end()) + { + for (const auto &configData : unboundConfigurations[timeRangeName]) + { + SWSS_LOG_NOTICE("Binding configuration %s to time range %s", configData.first.c_str(), timeRangeName.c_str()); + scheduledConfigurations[timeRangeName].insert(configData); + } + unboundConfigurations.erase(timeRangeName); + } + + SWSS_LOG_INFO("Time range %s created in local db, will retry to decide what to do next", timeRangeName.c_str()); + return task_process_status::task_need_retry; + } + + // If the time range exists, apply the configuration based on the status + if (status == "active"){ + task_status = enableTimeRange(timeRangeName); + } + else if (status == "inactive") + { + task_status = disableTimeRange(timeRangeName); + } + else + { + SWSS_LOG_ERROR("Invalid status for time range %s: %s", timeRangeName.c_str(), status.c_str()); + task_status = task_process_status::task_failed; + } + } + catch (const json::exception &e) + { + SWSS_LOG_ERROR("JSON parsing error: %s", e.what()); + task_status = task_process_status::task_failed; + } + catch (const std::exception &e) + { + SWSS_LOG_ERROR("Error processing time range status: %s", e.what()); + task_status = task_process_status::task_failed; + } + + return task_status; +} + +task_process_status ScheduledConfigMgr::enableTimeRange(const string &timeRangeName) +{ + SWSS_LOG_ENTER(); + + string configName{}; + json configJson{}; + + // Check if there are any configurations for the time range + if (scheduledConfigurations[timeRangeName].empty()) + { + SWSS_LOG_INFO("No configuration found for time range %s", timeRangeName.c_str()); + return task_process_status::task_success; + } + + // Apply the configuration + // scheduledConfigurations[timeRangeName].first is the configName + // scheduledConfigurations[timeRangeName].second is the configuration JSON + SWSS_LOG_INFO("Applying configurations for time range %s", timeRangeName.c_str()); + + for (const auto &configData : scheduledConfigurations[timeRangeName]) + { + configName = configData.first; + configJson = configData.second; + if (task_process_status::task_success != applyConfiguration(configName, configJson)) + { + SWSS_LOG_ERROR("Could not apply configuration for time range %s, configName: %s", timeRangeName.c_str(), configName.c_str()); + return task_process_status::task_need_retry; + } + scheduledConfigurationStatus[configName] = true; + SWSS_LOG_INFO("Applied configuration for time range %s, configName: %s", timeRangeName.c_str(), configName.c_str()); + } + return task_process_status::task_success; +} + +task_process_status ScheduledConfigMgr::disableTimeRange(const string &timeRangeName) +{ + SWSS_LOG_ENTER(); + + string configName{}; + json configJson{}; + + // Check if there are any configurations for the time range + if (scheduledConfigurations[timeRangeName].empty()) + { + SWSS_LOG_INFO("No configuration found for time range %s", timeRangeName.c_str()); + return task_process_status::task_success; + } + + // Remove the configuration + // scheduledConfigurations[timeRangeName].first is the configName + // scheduledConfigurations[timeRangeName].second is the configuration JSON + SWSS_LOG_INFO("Removing configurations for time range %s", timeRangeName.c_str()); + + for (const auto &configData : scheduledConfigurations[timeRangeName]) + { + configName = configData.first; + configJson = configData.second; + if (task_process_status::task_success != removeConfiguration(configName, configJson)) + { + SWSS_LOG_ERROR("Could not remove configuration for time range %s, configName: %s", timeRangeName.c_str(), configName.c_str()); + return task_process_status::task_need_retry; + } + scheduledConfigurationStatus[configName] = false; + SWSS_LOG_INFO("Removed configuration for time range %s, configName: %s", timeRangeName.c_str(), configName.c_str()); + } + return task_process_status::task_success; +} + +void ScheduledConfigMgr::doTimeRangeTask(Consumer &consumer) +{ + SWSS_LOG_ENTER(); + auto it = consumer.m_toSync.begin(); + while (it != consumer.m_toSync.end()) + { + string timeRangeName = ""; + string status = ""; + + KeyOpFieldsValuesTuple t = it->second; + + string keySeparator = CONFIGDB_KEY_SEPARATOR; + vector keys = tokenize(kfvKey(t), keySeparator[0]); + timeRangeName = keys[0]; + + string op = kfvOp(t); + task_process_status task_status = task_process_status::task_success; + if (op == SET_COMMAND) + { + for (const auto &i : kfvFieldsValues(t)) + { + if (fvField(i) == "status") + { + status = fvValue(i); + } + else + { + SWSS_LOG_ERROR("%s has unknown field %s", STATE_TIME_RANGE_STATUS_TABLE_NAME, fvField(i).c_str()); + task_status = task_process_status::task_invalid_entry; + break; + } + } + + if (task_status == task_process_status::task_success) + { + task_status = doProcessTimeRangeStatus(timeRangeName, status); + } + } else if (op == DEL_COMMAND) + { + // Disable, and then remove the time range + if (scheduledConfigurations.find(timeRangeName) != scheduledConfigurations.end()) + { + if (task_process_status::task_success != disableTimeRange(timeRangeName)) + { + SWSS_LOG_ERROR("Could not disable time range %s", timeRangeName.c_str()); + task_status = task_process_status::task_need_retry; + } + SWSS_LOG_INFO("Disabled time range %s", timeRangeName.c_str()); + } + // Save configurations for future creation of time range + unboundConfigurations[timeRangeName] = scheduledConfigurations[timeRangeName]; + + // Remove time range + scheduledConfigurations.erase(timeRangeName); + } + switch (task_status) + { + case task_process_status::task_failed: + SWSS_LOG_ERROR("Failed to process table update"); + return; + case task_process_status::task_need_retry: + SWSS_LOG_INFO("Unable to process table update. Will retry..."); + ++it; + break; + case task_process_status::task_invalid_entry: + SWSS_LOG_ERROR("Failed to process invalid entry, drop it"); + it = consumer.m_toSync.erase(it); + break; + default: + it = consumer.m_toSync.erase(it); + break; + } + } +} + +void ScheduledConfigMgr::doScheduledConfigurationTask(Consumer &consumer) +{ + SWSS_LOG_ENTER(); + auto it = consumer.m_toSync.begin(); + while (it != consumer.m_toSync.end()) + { + string timeRangeName = ""; + string configType = ""; + string configuration = ""; + + KeyOpFieldsValuesTuple t = it->second; + + string keySeparator = CONFIGDB_KEY_SEPARATOR; + vector keys = tokenize(kfvKey(t), keySeparator[0]); + string scheduledConfigurationName(keys[0]); + + string op = kfvOp(t); + task_process_status task_status = task_process_status::task_success; + if (op == SET_COMMAND) + { + for (const auto &i : kfvFieldsValues(t)) + { + if (fvField(i) == "time_range") + { + timeRangeName = fvValue(i); + } + else if (fvField(i) == "config_type") + { + configType = fvValue(i); + } + else if (fvField(i) == "configuration") + { + configuration = fvValue(i); + } + else + { + SWSS_LOG_ERROR("%s has unknown field %s", CFG_SCHEDULED_CONFIGURATION_TABLE_NAME, fvField(i).c_str()); + task_status = task_process_status::task_invalid_entry; + } + } + if (task_status == task_process_status::task_success) + { + task_status = doProcessScheduledConfiguration(timeRangeName, scheduledConfigurationName, configuration); + } + } else if (op == DEL_COMMAND) + { + if (scheduledConfigurationStatus.find(scheduledConfigurationName) != scheduledConfigurationStatus.end()){ + if (scheduledConfigurationStatus[scheduledConfigurationName]){ + // Get scheduled configuration time range name + timeRangeName = findTimeRangeByConfiguration(scheduledConfigurationName); + + // Remove the configuration + if (scheduledConfigurations.find(timeRangeName) != scheduledConfigurations.end()) + { + if (task_process_status::task_success != removeConfiguration(scheduledConfigurationName, scheduledConfigurations[timeRangeName][scheduledConfigurationName])) + { + SWSS_LOG_ERROR("Could not remove configuration for time range %s, configName: %s", timeRangeName.c_str(), scheduledConfigurationName.c_str()); + task_status = task_process_status::task_need_retry; + } + scheduledConfigurationStatus.erase(scheduledConfigurationName); + scheduledConfigurations[timeRangeName].erase(scheduledConfigurationName); + SWSS_LOG_INFO("Removed configuration for time range %s, configName: %s", timeRangeName.c_str(), scheduledConfigurationName.c_str()); + } + } + } else { + SWSS_LOG_ERROR("Scheduled configuration %s does not exist", scheduledConfigurationName.c_str()); + task_status = task_process_status::task_failed; + } + } + + switch (task_status) + { + case task_process_status::task_failed: + SWSS_LOG_ERROR("Failed to process table update"); + it = consumer.m_toSync.erase(it); + return; + case task_process_status::task_need_retry: + SWSS_LOG_INFO("Unable to process table update. Will retry..."); + ++it; + break; + case task_process_status::task_invalid_entry: + SWSS_LOG_ERROR("Failed to process invalid entry, drop it"); + it = consumer.m_toSync.erase(it); + break; + default: + it = consumer.m_toSync.erase(it); + break; + } + } +} + +void ScheduledConfigMgr::doTask(Consumer &consumer) +{ + SWSS_LOG_ENTER(); + + string table_name = consumer.getTableName(); + if (table_name == CFG_SCHEDULED_CONFIGURATION_TABLE_NAME) + { + doScheduledConfigurationTask(consumer); + } + else if (table_name == STATE_TIME_RANGE_STATUS_TABLE_NAME) + { + doTimeRangeTask(consumer); + } +} \ No newline at end of file diff --git a/cfgmgr/scheduledconfigmgr.h b/cfgmgr/scheduledconfigmgr.h new file mode 100644 index 0000000000..3e8974dad5 --- /dev/null +++ b/cfgmgr/scheduledconfigmgr.h @@ -0,0 +1,61 @@ +#ifndef __SCHEDULEDCONFIGMGR_H__ +#define __SCHEDULEDCONFIGMGR_H__ + +#include "dbconnector.h" +#include "producerstatetable.h" +#include "orch.h" +#include +#include +#include +#include + +using json = nlohmann::json; + +// Define a type alias for the configuration data structure +using ConfigStatus = std::unordered_map; // Maps configName to config status boolean +using ConfigData = std::unordered_map; // Maps configName to JSON object +using TimeRangeConfigMap = std::unordered_map; // Maps time range names to map of config data + +namespace swss { + +class ScheduledConfigMgr : public Orch +{ +public: + ScheduledConfigMgr(std::vector& connectors, DBConnector* appDb); + using Orch::doTask; + +private: + DBConnector *m_appDb; + TimeRangeConfigMap scheduledConfigurations, unboundConfigurations; + ConfigStatus scheduledConfigurationStatus; + + // Helper Functions + DBConnector* getDBConnector(const std::string &tableName); + std::string findTimeRangeByConfiguration(std::string scheduledConfigurationName); + + // Validation Functions + bool validateConfiguration(const json &configJson); + bool isTimeRangeActive(const std::string &timeRangeName); + + // Configuration Functions + task_process_status enableTimeRange(const std::string &timeRangeName); + task_process_status applyConfiguration(const std::string &configName, const json &configJson); + bool applyTableConfiguration(const std::string &tableName, const json &tableKeyFields); + + task_process_status disableTimeRange(const std::string &timeRangeName); + task_process_status removeConfiguration(const std::string &configName, const json &configJson); + bool removeTableConfiguration(const std::string &tableName, const std::string &key); + + // Task Processing Functions + task_process_status doProcessScheduledConfiguration(std::string timeRangeName, std::string configType, std::string configuration); + task_process_status doProcessTimeRangeStatus(std::string timeRangeName, std::string status); + + // High-level Task Functions + void doTimeRangeTask(Consumer &consumer); + void doScheduledConfigurationTask(Consumer &consumer); + void doTask(Consumer &consumer); +}; + +} + +#endif /* __SCHEDULEDCONFIGMGR_H__ */ \ No newline at end of file diff --git a/cfgmgr/scheduledconfigmgrd.cpp b/cfgmgr/scheduledconfigmgrd.cpp new file mode 100644 index 0000000000..0059a85740 --- /dev/null +++ b/cfgmgr/scheduledconfigmgrd.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include "dbconnector.h" +#include "select.h" +#include "exec.h" +#include "schema.h" +#include "scheduledconfigmgr.h" +#include +#include + +using namespace std; +using namespace swss; + +/* SELECT() function timeout retry time, in millisecond */ +#define SELECT_TIMEOUT 1000 + +int main(int argc, char **argv) +{ + Logger::linkToDbNative("scheduledconfigmgrd"); + SWSS_LOG_ENTER(); + + SWSS_LOG_NOTICE("--- Starting scheduledconfigmgrd ---"); + + try + { + std::vector cfgOrchList; + + // Create DB connectors + DBConnector cfgDb("CONFIG_DB", 0); + DBConnector stateDb("STATE_DB", 0); + DBConnector appDb("APPL_DB", 0); + + // Create table connectors that ScheduledConfigMgr will subscribe to + TableConnector cfgDbScheduledConfigurations(&cfgDb, CFG_SCHEDULED_CONFIGURATION_TABLE_NAME); + TableConnector stateDbTimeRangeStatusTable(&stateDb, STATE_TIME_RANGE_STATUS_TABLE_NAME); + vector connectors = {cfgDbScheduledConfigurations, stateDbTimeRangeStatusTable}; + + cfgOrchList.emplace_back(new ScheduledConfigMgr(connectors, &appDb)); + + auto scheduledconfigmgr = cfgOrchList[0]; + + swss::Select s; + for (Orch *o : cfgOrchList) + { + s.addSelectables(o->getSelectables()); + } + + SWSS_LOG_NOTICE("starting main loop"); + while (true) + { + Selectable *sel; + int ret; + + ret = s.select(&sel, SELECT_TIMEOUT); + if (ret == Select::ERROR) + { + SWSS_LOG_NOTICE("Error: %s!", strerror(errno)); + continue; + } + if (ret == Select::TIMEOUT) + { + scheduledconfigmgr->doTask(); + continue; + } + + auto *c = (Executor *)sel; + c->execute(); + } + } + catch(const std::exception &e) + { + SWSS_LOG_ERROR("Runtime error: %s", e.what()); + } + return -1; +} diff --git a/cfgmgr/timerangemgr.cpp b/cfgmgr/timerangemgr.cpp new file mode 100644 index 0000000000..dd25e191bc --- /dev/null +++ b/cfgmgr/timerangemgr.cpp @@ -0,0 +1,282 @@ +#include +#include +#include +#include +#include "logger.h" +#include "dbconnector.h" +#include "timer.h" +#include "timerangemgr.h" +#include "tokenize.h" +#include + +using namespace std; +using namespace swss; +using namespace cron; + +TimeRangeMgr::TimeRangeMgr(DBConnector *cfgDb, DBConnector *stateDb, const vector &tableNames) : Orch(cfgDb, tableNames), + m_stateTimeRangeStatusTable(stateDb, STATE_TIME_RANGE_STATUS_TABLE_NAME) +{ +} + + +bool TimeRangeMgr::isTimeInRange(const cronexpr& startExpr, const cronexpr& endExpr, const tm& currentTM, const string& startYear = "", const string& endYear = "") { + time_t currentTime = mktime(const_cast(¤tTM)); // Convert currentTM to time_t + + // Call the other isTimeInRange function with the time_t version of current time + return this->isTimeInRange(startExpr, endExpr, currentTime, startYear, endYear); +} + +bool TimeRangeMgr::isTimeInRange(const cronexpr& startExpr, const cronexpr& endExpr, const time_t& currentTime, const string& startYear = "", const string& endYear = "") { + + // Check if the current year is within the start and end year range + bool startYearExists = (startYear != ""); + bool endYearExists = (endYear != ""); + + if (startYearExists || endYearExists) + { + // Get the current year + tm currentTM = *localtime(¤tTime); + int currentYear = currentTM.tm_year + 1900; // tm_year is years since 1900 + + // Check if the current year is within the start and end year range + if (startYearExists && currentYear < stoi(startYear)) + { + return false; + } + if (endYearExists && currentYear > stoi(endYear)) + { + return false; + } + } + + // Find the next occurrence of the start time after the current time + time_t nextStartTime = cron_next(startExpr, currentTime); + + // Find the next occurrence of the end time after the current time + time_t nextEndTime = cron_next(endExpr, currentTime); + + // Check if we are currently in the time range + return (nextStartTime > nextEndTime); +} + +task_process_status TimeRangeMgr::writeCrontabFile(const string &fileName, const string &schedule, const string &command) +{ + string cronFileName = CRON_FILES_PATH_PREFIX_STR + fileName; + ofstream crontabFile{cronFileName}; + + if (crontabFile.fail()) + { + SWSS_LOG_ERROR("Failed to create crontab file for %s", fileName.c_str()); + return task_process_status::task_need_retry; + } + crontabFile << schedule << " "; + crontabFile << CRON_USERNAME_STR << " "; + crontabFile << command; + crontabFile << endl; + crontabFile.close(); + + SWSS_LOG_DEBUG("Crontab file for %s has been created", fileName.c_str()); + return task_process_status::task_success; +} + +// TODO add rollback mechanism +task_process_status TimeRangeMgr::createCronjobs(const string &taskName, const string &startTime, const string &endTime, const string &startYear = "", const string &endYear = "") +{ + string enableCrontabName = taskName + "-enable"; + string disableCrontabName = taskName + "-disable"; + + // Create year check string + string yearCheck = ""; + if (startYear != "") + { + yearCheck = "[ $(date +\"%Y\") -ge " + startYear + " ]"; + } + if (endYear != "") + { + if (startYear != "") + { + yearCheck += " && "; + } + yearCheck += "[ $(date +\"%Y\") -le " + endYear + " ]"; + } + + // Create command for enabling the task + string command_enabled = string("/usr/bin/redis-cli -n ") + to_string(STATE_DB) + " HSET '" + STATE_TIME_RANGE_STATUS_TABLE_NAME + "|" + taskName + "' '" + TIME_RANGE_STATUS_STR + "' '" + TIME_RANGE_ACTIVE_STR + "'"; + command_enabled = yearCheck + " && " + command_enabled; + + // Create command for disabling the task + string command_disabled = string("/usr/bin/redis-cli -n ") + to_string(STATE_DB) + " HSET '" + STATE_TIME_RANGE_STATUS_TABLE_NAME + "|" + taskName + "' '" + TIME_RANGE_STATUS_STR + "' '" + TIME_RANGE_INACTIVE_STR + "'"; + command_disabled = yearCheck + " && " + command_disabled; + + // Service file for enabling the task + if (writeCrontabFile(enableCrontabName, startTime, command_enabled) != task_process_status::task_success) + { + return task_process_status::task_need_retry; + } + + // Service file for disabling the task + if (writeCrontabFile(disableCrontabName, endTime, command_disabled) != task_process_status::task_success) + { + return task_process_status::task_need_retry; + } + + SWSS_LOG_INFO("Succesfully created crontab files for %s", taskName.c_str()); + + return task_process_status::task_success; +} + +task_process_status TimeRangeMgr::doTimeRangeTaskDelete(const string &rangeName) +{ + SWSS_LOG_ENTER(); + string enableCrontabName = rangeName + "-enable"; + string disableCrontabName = rangeName + "-disable"; + + // Delete the crontab files for the time range + if (remove((CRON_FILES_PATH_PREFIX_STR + enableCrontabName).c_str()) != 0) + { + SWSS_LOG_ERROR("Failed to delete crontab file for %s", enableCrontabName.c_str()); + return task_process_status::task_need_retry; + } + if (remove((CRON_FILES_PATH_PREFIX_STR + disableCrontabName).c_str()) != 0) + { + SWSS_LOG_ERROR("Failed to delete crontab file for %s", disableCrontabName.c_str()); + return task_process_status::task_need_retry; + } + + // Delete the time range status entry from the state db + m_stateTimeRangeStatusTable.del(rangeName); + + return task_process_status::task_success; +} + +task_process_status TimeRangeMgr::doTimeRangeTask(const string &rangeName, const vector &fieldValues) +{ + SWSS_LOG_ENTER(); + string start = ""; + string end = ""; + string start_year = ""; + string end_year = ""; + + for (const auto &i : fieldValues) + { + if (fvField(i) == "start") + { + start = fvValue(i); + } + else if (fvField(i) == "end") + { + end = fvValue(i); + } + else if (fvField(i) == "start_year") + { + start_year = fvValue(i); + } + else if (fvField(i) == "end_year") + { + end_year = fvValue(i); + } + else + { + SWSS_LOG_ERROR("Time range %s has unknown field %s", rangeName.c_str(), fvField(i).c_str()); + return task_process_status::task_invalid_entry; + } + } + + if (start == "" || end == "") + { + SWSS_LOG_ERROR("Time range %s is missing start or end time", rangeName.c_str()); + return task_process_status::task_invalid_entry; + } + + // Create cron files for time range and enable them + // TODO sanitize inputs + if (task_process_status::task_need_retry == createCronjobs(rangeName, start, end, start_year, end_year)) + { + return task_process_status::task_need_retry; + } + + // Check if time range should be active by default + string time_range_default_status = TIME_RANGE_INACTIVE_STR; + try + { + // croncpp.h uses nonstandard "seconds" field. Add "0 " to the beginning of the cron expression. + // This is a workaround to avoid using seconds field. + // TODO To make croncpp more efficient for standard cron use, remove seconds field from croncpp.h + // auto startSeconds = string("0 ") + start; + // auto endSeconds = string("0 ") + end; + auto startExpr = make_cron(start); + auto endExpr = make_cron(end); + + time_t currentTime = time(nullptr); + + if (isTimeInRange(startExpr, endExpr, currentTime, start_year, end_year)) + { + SWSS_LOG_INFO("Time range %s is active", rangeName.c_str()); + time_range_default_status = TIME_RANGE_ACTIVE_STR; + } + } catch (bad_cronexpr const & ex) + { + SWSS_LOG_WARN("%s", ex.what()); + } + + // Prepare state table field-values + vector stateTableFieldValues; + string key = rangeName; + stateTableFieldValues.emplace_back(FieldValueTuple(TIME_RANGE_STATUS_STR, time_range_default_status)); + + // Add time range status to range status table in state db + m_stateTimeRangeStatusTable.set(key, stateTableFieldValues); + + return task_process_status::task_success; +} + +void TimeRangeMgr::doTask(Consumer &consumer) +{ + SWSS_LOG_ENTER(); + + string table_name = consumer.getTableName(); + + auto it = consumer.m_toSync.begin(); + while (it != consumer.m_toSync.end()) + { + KeyOpFieldsValuesTuple t = it->second; + + string keySeparator = CONFIGDB_KEY_SEPARATOR; + vector keys = tokenize(kfvKey(t), keySeparator[0]); + string rangeName(keys[0]); + + string op = kfvOp(t); + task_process_status task_status = task_process_status::task_success; + if (op == SET_COMMAND) + { + if (table_name == CFG_TIME_RANGE_TABLE_NAME) + { + task_status = doTimeRangeTask(rangeName, kfvFieldsValues(t)); + } + } + else if (op == DEL_COMMAND) + { + if (table_name == CFG_TIME_RANGE_TABLE_NAME) + { + task_status = doTimeRangeTaskDelete(rangeName); + } + } + switch (task_status) + { + case task_process_status::task_failed: + SWSS_LOG_ERROR("Failed to process table update"); + return; + case task_process_status::task_need_retry: + SWSS_LOG_INFO("Unable to process table update. Will retry..."); + ++it; + break; + case task_process_status::task_invalid_entry: + SWSS_LOG_ERROR("Failed to process invalid entry, drop it"); + it = consumer.m_toSync.erase(it); + break; + default: + it = consumer.m_toSync.erase(it); + break; + } + } +} \ No newline at end of file diff --git a/cfgmgr/timerangemgr.h b/cfgmgr/timerangemgr.h new file mode 100644 index 0000000000..a97c166fb7 --- /dev/null +++ b/cfgmgr/timerangemgr.h @@ -0,0 +1,47 @@ +#ifndef __TIMERANGEMGR__ +#define __TIMERANGEMGR__ + +#include "dbconnector.h" +#include "producerstatetable.h" +#include "orch.h" + +#include +#include +#include +#include +#include "croncpp.h" + + +namespace swss { + +#define TIME_RANGE_ACTIVE_STR "active" +#define TIME_RANGE_INACTIVE_STR "inactive" +#define TIME_RANGE_STATUS_STR "status" + + +#define CRON_FILES_PATH_PREFIX_STR "/etc/cron.d/" +#define CRON_USERNAME_STR "root" + + +class TimeRangeMgr : public Orch +{ +public: + TimeRangeMgr(DBConnector *cfgDb, DBConnector *stateDb, const std::vector &tableNames); + using Orch::doTask; + +private: + Table m_stateTimeRangeStatusTable; + + task_process_status writeCrontabFile(const std::string& fileName, const std::string& schedule, const std::string& command); + task_process_status createCronjobs(const std::string& rangeName, const std::string& start, const std::string& end, const std::string& startYear, const std::string& endYear); + bool isTimeInRange(const cron::cronexpr& startExpr, const cron::cronexpr& endExpr, const std::tm& currentTM, const std::string& startYear, const std::string& endYear); + bool isTimeInRange(const cron::cronexpr& startExpr, const cron::cronexpr& endExpr, const std::time_t& currentTime, const std::string& startYear, const std::string& endYear); + + void doTask(Consumer &consumer); + task_process_status doTimeRangeTask(const std::string& rangeName, const std::vector& fieldValues); + task_process_status doTimeRangeTaskDelete(const std::string &rangeName); +}; + +} + +#endif /* __TIMERANGEMGR__ */ diff --git a/cfgmgr/timerangemgrd.cpp b/cfgmgr/timerangemgrd.cpp new file mode 100644 index 0000000000..95e07772b6 --- /dev/null +++ b/cfgmgr/timerangemgrd.cpp @@ -0,0 +1,72 @@ +#include +#include +#include +#include "dbconnector.h" +#include "select.h" +#include "exec.h" +#include "schema.h" +#include "timerangemgr.h" +#include +#include + +using namespace std; +using namespace swss; + +/* SELECT() function timeout retry time, in millisecond */ +#define SELECT_TIMEOUT 1000 + +int main(int argc, char **argv) +{ + Logger::linkToDbNative("timerangemgrd"); + SWSS_LOG_ENTER(); + + SWSS_LOG_NOTICE("--- Starting timerangemgrd ---"); + + try + { + std::vector cfgOrchList; + + DBConnector cfgDb("CONFIG_DB", 0); + DBConnector stateDb("STATE_DB", 0); + + vector cfg_buffer_tables = { + CFG_TIME_RANGE_TABLE_NAME, + }; + cfgOrchList.emplace_back(new TimeRangeMgr(&cfgDb, &stateDb, cfg_buffer_tables)); + + auto timerangemgr = cfgOrchList[0]; + + swss::Select s; + for (Orch *o : cfgOrchList) + { + s.addSelectables(o->getSelectables()); + } + + SWSS_LOG_NOTICE("starting main loop"); + while (true) + { + Selectable *sel; + int ret; + + ret = s.select(&sel, SELECT_TIMEOUT); + if (ret == Select::ERROR) + { + SWSS_LOG_NOTICE("Error: %s!", strerror(errno)); + continue; + } + if (ret == Select::TIMEOUT) + { + timerangemgr->doTask(); + continue; + } + + auto *c = (Executor *)sel; + c->execute(); + } + } + catch(const std::exception &e) + { + SWSS_LOG_ERROR("Runtime error: %s", e.what()); + } + return -1; +}