| /** @file |
| |
| Parse the records.config configuration file. |
| |
| @section license License |
| |
| Licensed to the Apache Software Foundation (ASF) under one |
| or more contributor license agreements. See the NOTICE file |
| distributed with this work for additional information |
| regarding copyright ownership. The ASF licenses this file |
| to you under the Apache License, Version 2.0 (the |
| "License"); you may not use this file except in compliance |
| with the License. You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| #include "tscore/ink_platform.h" |
| #include "tscore/ink_memory.h" |
| |
| #include "tscore/TextBuffer.h" |
| #include "tscore/Tokenizer.h" |
| #include "tscore/ink_defs.h" |
| #include "tscore/ink_string.h" |
| #include "tscore/runroot.h" |
| |
| #include "P_RecFile.h" |
| #include "P_RecUtils.h" |
| #include "P_RecMessage.h" |
| #include "P_RecCore.h" |
| #include "tscore/Layout.h" |
| |
| #include "records/RecYAMLDefs.h" |
| #include "records/RecYAMLDecoder.h" |
| |
| const char *g_rec_config_fpath = nullptr; |
| std::unordered_set<std::string> g_rec_config_contents_ht; |
| ink_mutex g_rec_config_lock; |
| |
| //------------------------------------------------------------------------- |
| // RecConfigFileInit |
| //------------------------------------------------------------------------- |
| void |
| RecConfigFileInit() |
| { |
| ink_mutex_init(&g_rec_config_lock); |
| } |
| |
| //------------------------------------------------------------------------- |
| // RecFileImport_Xmalloc |
| //------------------------------------------------------------------------- |
| static int |
| RecFileImport_Xmalloc(const char *file, char **file_buf, int *file_size) |
| { |
| int err = REC_ERR_FAIL; |
| RecHandle h_file; |
| int bytes_read; |
| |
| if (file && file_buf && file_size) { |
| *file_buf = nullptr; |
| *file_size = 0; |
| if ((h_file = RecFileOpenR(file)) != REC_HANDLE_INVALID) { |
| *file_size = RecFileGetSize(h_file); |
| *file_buf = static_cast<char *>(ats_malloc(*file_size + 1)); |
| if (RecFileRead(h_file, *file_buf, *file_size, &bytes_read) != REC_ERR_FAIL && bytes_read == *file_size) { |
| (*file_buf)[*file_size] = '\0'; |
| err = REC_ERR_OKAY; |
| } else { |
| ats_free(*file_buf); |
| *file_buf = nullptr; |
| *file_size = 0; |
| } |
| RecFileClose(h_file); |
| } |
| } |
| |
| return err; |
| } |
| |
| //------------------------------------------------------------------------- |
| // Records whose paths are managed by runroot. When runroot is active the |
| // value from records.yaml is replaced with the resolved Layout path. |
| //------------------------------------------------------------------------- |
| static constexpr std::pair<std::string_view, std::string Layout::*> runroot_records[] = { |
| {"proxy.config.bin_path", &Layout::bindir }, |
| {"proxy.config.local_state_dir", &Layout::runtimedir}, |
| {"proxy.config.log.logfile_dir", &Layout::logdir }, |
| {"proxy.config.plugin.plugin_dir", &Layout::libexecdir}, |
| }; |
| |
| //------------------------------------------------------------------------- |
| // RecConfigOverrideFromEnvironment |
| //------------------------------------------------------------------------- |
| std::pair<std::string, RecConfigOverrideSource> |
| RecConfigOverrideFromEnvironment(const char *name, const char *value) |
| { |
| ats_scoped_str envname(ats_strdup(name)); |
| const char *envval = nullptr; |
| |
| // Munge foo.bar.config into FOO_BAR_CONFIG. |
| for (char *c = envname; *c != '\0'; ++c) { |
| switch (*c) { |
| case '.': |
| *c = '_'; |
| break; |
| default: |
| *c = ParseRules::ink_toupper(*c); |
| break; |
| } |
| } |
| |
| envval = getenv(envname.get()); |
| if (envval) { |
| return {envval, RecConfigOverrideSource::ENV}; |
| } |
| |
| if (!get_runroot().empty()) { |
| for (auto const &[rec_name, member] : runroot_records) { |
| if (rec_name == name) { |
| return {Layout::get()->*member, RecConfigOverrideSource::RUNROOT}; |
| } |
| } |
| } |
| |
| return {value ? value : "", RecConfigOverrideSource::NONE}; |
| } |
| |
| //------------------------------------------------------------------------- |
| // RecParseConfigFile |
| //------------------------------------------------------------------------- |
| int |
| RecConfigFileParse(const char *path, RecConfigEntryCallback handler) |
| { |
| char *fbuf; |
| int fsize; |
| |
| const char *line; |
| int line_num; |
| |
| char *rec_type_str, *name_str, *data_type_str, *data_str; |
| RecT rec_type; |
| RecDataT data_type; |
| |
| Tokenizer line_tok("\r\n"); |
| tok_iter_state line_tok_state; |
| |
| RecDebug(DL_Note, "Reading '%s'", path); |
| |
| // watch out, we're altering our g_rec_config_xxx structures |
| ink_mutex_acquire(&g_rec_config_lock); |
| |
| if (RecFileImport_Xmalloc(path, &fbuf, &fsize) == REC_ERR_FAIL) { |
| RecLog(DL_Warning, "Could not import '%s'", path); |
| ink_mutex_release(&g_rec_config_lock); |
| return REC_ERR_FAIL; |
| } |
| |
| line_tok.Initialize(fbuf, SHARE_TOKS | ALLOW_EMPTY_TOKS); |
| line = line_tok.iterFirst(&line_tok_state); |
| line_num = 1; |
| while (line) { |
| char *lc = ats_strdup(line); |
| char *lt = lc; |
| char *ln; |
| |
| while (isspace(*lt)) { |
| lt++; |
| } |
| rec_type_str = strtok_r(lt, " \t", &ln); |
| |
| // check for blank lines and comments |
| if ((!rec_type_str) || (rec_type_str && (*rec_type_str == '#'))) { |
| goto L_done; |
| } |
| |
| name_str = strtok_r(nullptr, " \t", &ln); |
| data_type_str = strtok_r(nullptr, " \t", &ln); |
| |
| // extract the string data (a little bit tricker since it can have spaces) |
| if (ln) { |
| // 'ln' will point to either the next token or a bunch of spaces |
| // if the user didn't supply a value (e.g. 'STRING '). First |
| // scan past all of the spaces. If we hit a '\0', then we |
| // know we didn't have a valid value. If not, set 'data_str' to |
| // the start of the token and scan until we find the end. Once |
| // the end is found, back-peddle to remove any trailing spaces. |
| while (isspace(*ln)) { |
| ln++; |
| } |
| if (*ln == '\0') { |
| data_str = nullptr; |
| } else { |
| data_str = ln; |
| while (*ln != '\0') { |
| ln++; |
| } |
| ln--; |
| while (isspace(*ln) && (ln > data_str)) { |
| ln--; |
| } |
| ln++; |
| *ln = '\0'; |
| } |
| } else { |
| data_str = nullptr; |
| } |
| |
| // check for errors |
| if (!(rec_type_str && name_str && data_type_str && data_str)) { |
| RecLog(DL_Warning, "Could not parse line at '%s:%d' -- skipping line: '%s'", path, line_num, line); |
| goto L_done; |
| } |
| |
| // record type |
| if (strcmp(rec_type_str, "CONFIG") == 0) { |
| rec_type = RECT_CONFIG; |
| } else if (strcmp(rec_type_str, "PROCESS") == 0) { |
| rec_type = RECT_PROCESS; |
| } else if (strcmp(rec_type_str, "NODE") == 0) { |
| rec_type = RECT_NODE; |
| } else if (strcmp(rec_type_str, "LOCAL") == 0) { |
| rec_type = RECT_LOCAL; |
| } else { |
| RecLog(DL_Warning, "Unknown record type '%s' at '%s:%d' -- skipping line", rec_type_str, path, line_num); |
| goto L_done; |
| } |
| |
| // data_type |
| if (strcmp(data_type_str, "INT") == 0) { |
| data_type = RECD_INT; |
| } else if (strcmp(data_type_str, "FLOAT") == 0) { |
| data_type = RECD_FLOAT; |
| } else if (strcmp(data_type_str, "STRING") == 0) { |
| data_type = RECD_STRING; |
| } else if (strcmp(data_type_str, "COUNTER") == 0) { |
| data_type = RECD_COUNTER; |
| } else { |
| RecLog(DL_Warning, "Unknown data type '%s' at '%s:%d' -- skipping line", data_type_str, path, line_num); |
| goto L_done; |
| } |
| |
| // OK, we parsed the record, send it to the handler ... |
| { |
| auto [value_str, override_source] = RecConfigOverrideFromEnvironment(name_str, data_str); |
| if (override_source != RecConfigOverrideSource::NONE) { |
| RecDebug(DL_Debug, "'%s' overridden with '%s' by %s", name_str, value_str.c_str(), |
| RecConfigOverrideSourceName(override_source)); |
| } |
| handler(rec_type, data_type, name_str, value_str.c_str(), |
| override_source == RecConfigOverrideSource::NONE ? REC_SOURCE_EXPLICIT : REC_SOURCE_ENV); |
| } |
| |
| // update our g_rec_config_contents_xxx |
| g_rec_config_contents_ht.emplace(name_str); |
| |
| L_done: |
| line = line_tok.iterNext(&line_tok_state); |
| line_num++; |
| ats_free(lc); |
| } |
| |
| ink_mutex_release(&g_rec_config_lock); |
| ats_free(fbuf); |
| |
| return REC_ERR_OKAY; |
| } |
| |
| swoc::Errata |
| RecYAMLConfigFileParse(const char *path, RecYAMLNodeHandler handler) |
| { |
| swoc::Errata status; |
| try { |
| auto nodes = YAML::LoadAllFromFile(path); |
| // We will parse each doc from the stream, subsequently config node will overwrite |
| // the value for previously loaded records. This would work similar to the old |
| // records.config which a later CONFIG variable would overwrite a previous one. |
| for (auto const &root : nodes) { |
| if (auto &&ret = ParseRecordsFromYAML(root, handler); !ret.empty()) { |
| status.note(ret); |
| } |
| } |
| } catch (std::exception const &ex) { |
| status.note(ERRATA_ERROR, "Error parsing {}. {}", path, ex.what()); |
| } |
| |
| return status; |
| } |