Merge pull request #6727 from Icinga/feature/cluster-config-sync-stage

Improve cluster config sync
This commit is contained in:
Michael Friedrich 2019-06-19 17:37:30 +02:00 committed by GitHub
commit 0b85928a30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 954 additions and 232 deletions

View File

@ -96,7 +96,33 @@ feature with the `cipher_list` attribute.
In case that one of these ciphers is marked as insecure in the future,
please let us know with an issue on GitHub.
### HA-aware Features <a id="upgrading-to-2-11-ha-aware-features"></a>
### Cluster <a id="upgrading-to-2-11-cluster"></a>
#### Config Sync <a id="upgrading-to-2-11-cluster-config-sync"></a>
2.11 overhauls the cluster config sync in many ways. This includes the
following under the hood:
- Synced configuration files are not immediately put into production, but left inside a stage.
- Unsuccessful config validation never puts the config into production, additional logging and API states are available.
- Zone directories which are not configured in zones.conf, are not included anymore on secondary master/satellites/clients.
- Synced config change calculation use checksums instead of timestamps to trigger validation/reload. This is more safe, and the usage of timestamps is now deprecated.
- Don't allow parallel cluster syncs to avoid race conditions with overridden files.
- Deleted directories and files are now purged, previous versions had a bug.
Whenever a newer child endpoint receives a configuration update without
checksums, it will log a warning.
```
Received configuration update without checksums from parent endpoint satellite1. This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+
```
This is a gentle reminder to upgrade the master and satellites first,
prior to installing new clients/agents.
Technical details are available in the [technical concepts](19-technical-concepts.md#technical-concepts-cluster-config-sync) chapter.
#### HA-aware Features <a id="upgrading-to-2-11-cluster-ha-aware-features"></a>
v2.11 introduces additional HA functionality similar to the DB IDO feature.
This enables the feature being active only on one endpoint while the other
@ -182,7 +208,7 @@ constant in [constants.conf](04-configuring-icinga-2.md#constants-conf) instead.
### REST API <a id="upgrading-to-2-11-api"></a>
#### Actions <a id="upgrading-to-2-11-api-config-packages"></a>
#### Actions <a id="upgrading-to-2-11-api-actions"></a>
The [schedule-downtime](12-icinga2-api.md#icinga2-api-actions-schedule-downtime-host-all-services)
action supports the `all_services` parameter for Host types. Defaults to false.

View File

@ -813,6 +813,188 @@ Icinga 2 v2.9+ adds more performance metrics for these values:
* `sum_bytes_sent_per_second` and `sum_bytes_received_per_second`
### Config Sync <a id="technical-concepts-cluster-config-sync"></a>
The visible feature for the user is to put configuration files in `/etc/icinga2/zones.d/<zonename>`
and have them synced automatically to all involved zones and endpoints.
This not only includes host and service objects being checked
in a satellite zone, but also additional config objects such as
commands, groups, timeperiods and also templates.
Additional thoughts and complexity added:
- Putting files into zone directory names removes the burden to set the `zone` attribute on each object in this directory. This is done automatically by the config compiler.
- Inclusion of `zones.d` happens automatically, the user shouldn't be bothered about this.
- Before the REST API was created, only static configuration files in `/etc/icinga2/zones.d` existed. With the addition of config packages, additional `zones.d` targets must be registered (e.g. used by the Director)
- Only one config master is allowed. This one identifies itself with configuration files in `/etc/icinga2/zones.d`. This is not necessarily the zone master seen in the debug logs, that one is important for message routing internally.
- Objects and templates which cannot be bound into a specific zone (e.g. hosts in the satellite zone) must be made available "globally".
- Users must be able to deny the synchronisation of specific zones, e.g. for security reasons.
#### Config Sync: Config Master <a id="technical-concepts-cluster-config-sync-config-master"></a>
All zones must be configured and included in the `zones.conf` config file beforehand.
The zone names are the identifier for the directories underneath the `/etc/icinga2/zones.d`
directory. If a zone is not configured, it will not be included in the config sync - keep this
in mind for troubleshooting.
When the config master starts, the content of `/etc/icinga2/zones.d` is automatically
included. There's no need for an additional entry in `icinga2.conf` like `conf.d`.
You can verify this by running the config validation on debug level:
```
icinga2 daemon -C -x debug | grep 'zones.d'
[2019-06-19 15:16:19 +0200] notice/ConfigCompiler: Compiling config file: /etc/icinga2/zones.d/global-templates/commands.conf
```
Once the config validation succeeds, the startup routine for the daemon
copies the files into the "production" directory in `/var/lib/icinga2/api/zones`.
This directory is used for all endpoints where Icinga stores the received configuration.
With the exception of the config master retrieving this from `/etc/icinga2/zones.d` instead.
These operations are logged for better visibility.
```
[2019-06-19 15:26:38 +0200] information/ApiListener: Copying 1 zone configuration files for zone 'global-templates' to '/var/lib/icinga2/api/zones/global-templates'.
[2019-06-19 15:26:38 +0200] information/ApiListener: Updating configuration file: /var/lib/icinga2/api/zones/global-templates//_etc/commands.conf
```
The master is finished at this point. Depending on the cluster configuration,
the next iteration is a connected endpoint after successful TLS handshake and certificate
authentication.
It calls `SendConfigUpdate(client)` which sends the [config::Update](19-technical-concepts.md#technical-concepts-json-rpc-messages-config-update)
JSON-RPC message including all required zones and their configuration file content.
#### Config Sync: Receive Config <a id="technical-concepts-cluster-config-sync-receive-config"></a>
The secondary master endpoint and endpoints in a child zone will be connected to the config
master. The endpoint receives the [config::Update](19-technical-concepts.md#technical-concepts-json-rpc-messages-config-update)
JSON-RPC message and processes the content in `ConfigUpdateHandler()`. This method checks
whether config should be accepted. In addition to that, it locks a local mutex to avoid race conditions
with multiple syncs in parallel.
After that, the received configuration content is analysed.
> **Note**
>
> The cluster design allows that satellite endpoints may connect to the secondary master first.
> There is no immediate need to always connect to the config master first, especially since
> the satellite endpoints don't know that.
>
> The secondary master not only stores the master zone config files, but also all child zones.
> This is also the case for any HA enabled zone with more than one endpoint.
2.11 puts the received configuration files into a staging directory in
`/var/lib/icinga2/api/zones-stage`. Previous versions directly wrote the
files into production which could have led to broken configuration on the
next manual restart.
```
[2019-06-19 16:08:29 +0200] information/ApiListener: New client connection for identity 'master1' to [127.0.0.1]:5665
[2019-06-19 16:08:30 +0200] information/ApiListener: Applying config update from endpoint 'master1' of zone 'master'.
[2019-06-19 16:08:30 +0200] information/ApiListener: Received configuration for zone 'agent' from endpoint 'master1'. Comparing the checksums.
[2019-06-19 16:08:30 +0200] information/ApiListener: Stage: Updating received configuration file '/var/lib/icinga2/api/zones-stage/agent//_etc/host.conf' for zone 'agent'.
[2019-06-19 16:08:30 +0200] information/ApiListener: Applying configuration file update for path '/var/lib/icinga2/api/zones-stage/agent' (176 Bytes).
[2019-06-19 16:08:30 +0200] information/ApiListener: Received configuration for zone 'master' from endpoint 'master1'. Comparing the checksums.
[2019-06-19 16:08:30 +0200] information/ApiListener: Applying configuration file update for path '/var/lib/icinga2/api/zones-stage/master' (17 Bytes).
[2019-06-19 16:08:30 +0200] information/ApiListener: Received configuration from endpoint 'master1' is different to production, triggering validation and reload.
```
It then validates the received configuration in its own config stage. There is
an parameter override in place which disables the automatic inclusion of the production
config in `/var/lib/icinga2/api/zones`.
Once completed, the reload is triggered. This follows the same configurable timeout
as with the global reload.
```
[2019-06-19 16:52:26 +0200] information/ApiListener: Config validation for stage '/var/lib/icinga2/api/zones-stage/' was OK, replacing into '/var/lib/icinga2/api/zones/' and triggering reload.
[2019-06-19 16:52:27 +0200] information/Application: Got reload command: Started new instance with PID '19945' (timeout is 300s).
[2019-06-19 16:52:28 +0200] information/Application: Reload requested, letting new process take over.
```
Whenever the staged configuration validation fails, Icinga logs this including a reference
to the startup log file which includes additional errors.
```
[2019-06-19 15:45:27 +0200] critical/ApiListener: Config validation failed for staged cluster config sync in '/var/lib/icinga2/api/zones-stage/'. Aborting. Logs: '/var/lib/icinga2/api/zones-stage//startup.log'
```
#### Config Sync: Changes and Reload <a id="technical-concepts-cluster-config-sync-changes-reload"></a>
Whenever a new configuration is received, it is validated and upon success, the
daemon automatically reloads. While the daemon continues with checks, the reload
cannot hand over open TCP connections. That being said, reloading the daemon everytime
a configuration is synchronized would lead into many not connected endpoints.
Therefore the cluster config sync checks whether the configuration files actually
changed, and will only trigger a reload when such a change happened.
2.11 calculates a checksum from each file content and compares this to the
production configuration. Previous versions used additional metadata with timestamps from
files which sometimes led to problems with asynchronous dates.
> **Note**
>
> For compatibility reasons, the timestamp metadata algorithm is still intact, e.g.
> when the client is 2.11 already, but the parent endpoint is still on 2.10.
Icinga logs a warning when this happens.
```
Received configuration update without checksums from parent endpoint satellite1. This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+
```
The debug log provides more details on the actual checksums and checks. Future output
may change, use this solely for troubleshooting and debugging whenever the cluster
config sync fails.
```
[2019-06-19 16:13:16 +0200] information/ApiListener: Received configuration for zone 'agent' from endpoint 'master1'. Comparing the checksums.
[2019-06-19 16:13:16 +0200] debug/ApiListener: Checking for config change between stage and production. Old (3): '{"/.checksums":"7ede1276a9a32019c1412a52779804a976e163943e268ec4066e6b6ec4d15d73","/.timestamp":"ec4354b0eca455f7c2ca386fddf5b9ea810d826d402b3b6ac56ba63b55c2892c","/_etc/host.conf":"35d4823684d83a5ab0ca853c9a3aa8e592adfca66210762cdf2e54339ccf0a44"}' vs. new (3): '{"/.checksums":"84a586435d732327e2152e7c9b6d85a340cc917b89ae30972042f3dc344ea7cf","/.timestamp":"0fd6facf35e49ab1b2a161872fa7ad794564eba08624373d99d31c32a7a4c7d3","/_etc/host.conf":"0d62075e89be14088de1979644b40f33a8f185fcb4bb6ff1f7da2f63c7723fcb"}'.
[2019-06-19 16:13:16 +0200] debug/ApiListener: Checking /_etc/host.conf for checksum: 35d4823684d83a5ab0ca853c9a3aa8e592adfca66210762cdf2e54339ccf0a44
[2019-06-19 16:13:16 +0200] debug/ApiListener: Path '/_etc/host.conf' doesn't match old checksum '0d62075e89be14088de1979644b40f33a8f185fcb4bb6ff1f7da2f63c7723fcb' with new checksum '35d4823684d83a5ab0ca853c9a3aa8e592adfca66210762cdf2e54339ccf0a44'.
```
#### Config Sync: Trust <a id="technical-concepts-cluster-config-sync-trust"></a>
The config sync follows the "top down" approach, where the master endpoint in the master
zone is allowed to synchronize configuration to the child zone, e.g. the satellite zone.
Endpoints in the same zone, e.g. a secondary master, receive configuration for the same
zone and all child zones.
Endpoints in the satellite zone trust the parent zone, and will accept the pushed
configuration via JSON-RPC cluster messages. By default, this is disabled and must
be enabled with the `accept_config` attribute in the ApiListener feature (manually or with CLI
helpers).
The satellite zone will not only accept zone configuration for its own zone, but also
all configured child zones. That is why it is important to configure the zone hierarchy
on the satellite as well.
Child zones are not allowed to sync configuration up to the parent zone. Each Icinga instance
evaluates this in startup and knows on endpoint connect which config zones need to be synced.
Global zones have a special trust relationship: They are synced to all child zones, be it
a satellite zone or client zone. Since checkable objects such as a Host or a Service object
must have only one endpoint as authority, they cannot be put into a global zone (denied by
the config compiler).
Apply rules and templates are allowed, since they are evaluated in the endpoint which received
the synced configuration. Keep in mind that there may be differences on the master and the satellite
when e.g. hostgroup membership is used for assign where expressions, but the groups are only
available on the master.
## TLS Network IO <a id="technical-concepts-tls-network-io"></a>
### TLS Connection Handling <a id="technical-concepts-tls-network-io-connection-handling"></a>

View File

@ -100,6 +100,10 @@ static void IncludePackage(const String& packagePath, bool& success)
bool DaemonUtility::ValidateConfigFiles(const std::vector<std::string>& configs, const String& objectsFile)
{
bool success;
Namespace::Ptr systemNS = ScriptGlobal::Get("System");
VERIFY(systemNS);
if (!objectsFile.IsEmpty())
ConfigCompilerContext::GetInstance()->OpenObjectsFile(objectsFile);
@ -122,12 +126,15 @@ bool DaemonUtility::ValidateConfigFiles(const std::vector<std::string>& configs,
* unfortunately moving it there is somewhat non-trivial. */
success = true;
String zonesEtcDir = Configuration::ZonesDir;
if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir))
Utility::Glob(zonesEtcDir + "/*", std::bind(&IncludeZoneDirRecursive, _1, "_etc", std::ref(success)), GlobDirectory);
/* Only load zone directory if we're not in staging validation. */
if (!systemNS->Contains("ZonesStageVarDir")) {
String zonesEtcDir = Configuration::ZonesDir;
if (!zonesEtcDir.IsEmpty() && Utility::PathExists(zonesEtcDir))
Utility::Glob(zonesEtcDir + "/*", std::bind(&IncludeZoneDirRecursive, _1, "_etc", std::ref(success)), GlobDirectory);
if (!success)
return false;
if (!success)
return false;
}
/* Load package config files - they may contain additional zones which
* are authoritative on this node and are checked in HasZoneConfigAuthority(). */
@ -138,17 +145,24 @@ bool DaemonUtility::ValidateConfigFiles(const std::vector<std::string>& configs,
if (!success)
return false;
/* Load cluster synchronized configuration files */
/* Load cluster synchronized configuration files. This can be overridden for staged sync validations. */
String zonesVarDir = Configuration::DataDir + "/api/zones";
/* Cluster config sync stage validation needs this. */
if (systemNS->Contains("ZonesStageVarDir")) {
zonesVarDir = systemNS->Get("ZonesStageVarDir");
Log(LogNotice, "DaemonUtility")
<< "Overriding zones var directory with '" << zonesVarDir << "' for cluster config sync staging.";
}
if (Utility::PathExists(zonesVarDir))
Utility::Glob(zonesVarDir + "/*", std::bind(&IncludeNonLocalZone, _1, "_cluster", std::ref(success)), GlobDirectory);
if (!success)
return false;
Namespace::Ptr systemNS = ScriptGlobal::Get("System");
VERIFY(systemNS);
/* This is initialized inside the IcingaApplication class. */
Value vAppType;
VERIFY(systemNS->Get("ApplicationType", &vAppType));

View File

@ -8,6 +8,7 @@
#include "icinga/icingaapplication.hpp"
#include "icinga/clusterevents.hpp"
#include "icinga/checkable.hpp"
#include "remote/apilistener.hpp"
#include "base/application.hpp"
#include "base/objectlock.hpp"
#include "base/utility.hpp"
@ -157,6 +158,20 @@ void IcingaCheckTask::ScriptFunc(const Checkable::Ptr& checkable, const CheckRes
cr->SetState(ServiceWarning);
}
/* Indicate a warning when the last synced config caused a stage validation error. */
ApiListener::Ptr listener = ApiListener::GetInstance();
if (listener) {
Dictionary::Ptr validationResult = listener->GetLastFailedZonesStageValidation();
if (validationResult) {
output += "; Last zone sync stage validation failed at "
+ Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", validationResult->Get("ts"));
cr->SetState(ServiceWarning);
}
}
/* Extract the version number of the running Icinga2 instance.
* We assume that appVersion will allways be something like 'v2.10.1-8-gaebe6da' and we want to extract '2.10.1'.
*/

View File

@ -59,6 +59,7 @@ set_target_properties (
#install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api\")")
install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/log\")")
install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones\")")
install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/api/zones-stage\")")
install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certs\")")
install(CODE "file(MAKE_DIRECTORY \"\$ENV{DESTDIR}${ICINGA2_FULL_DATADIR}/certificate-requests\")")

View File

@ -3,9 +3,12 @@
#include "remote/apilistener.hpp"
#include "remote/apifunction.hpp"
#include "config/configcompiler.hpp"
#include "base/tlsutility.hpp"
#include "base/json.hpp"
#include "base/configtype.hpp"
#include "base/logger.hpp"
#include "base/convert.hpp"
#include "base/application.hpp"
#include "base/exception.hpp"
#include "base/utility.hpp"
#include <fstream>
@ -17,233 +20,200 @@ REGISTER_APIFUNCTION(Update, config, &ApiListener::ConfigUpdateHandler);
boost::mutex ApiListener::m_ConfigSyncStageLock;
void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
{
CONTEXT("Creating config update for file '" + file + "'");
Log(LogNotice, "ApiListener")
<< "Creating config update for file '" << file << "'.";
std::ifstream fp(file.CStr(), std::ifstream::binary);
if (!fp)
return;
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
Dictionary::Ptr update;
if (Utility::Match("*.conf", file))
update = config.UpdateV1;
else
update = config.UpdateV2;
update->Set(file.SubStr(path.GetLength()), content);
}
Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
{
Dictionary::Ptr result = new Dictionary();
if (config.UpdateV1)
config.UpdateV1->CopyTo(result);
if (config.UpdateV2)
config.UpdateV2->CopyTo(result);
return result;
}
ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
{
ConfigDirInformation config;
config.UpdateV1 = new Dictionary();
config.UpdateV2 = new Dictionary();
Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile);
return config;
}
bool ApiListener::UpdateConfigDir(const ConfigDirInformation& oldConfigInfo, const ConfigDirInformation& newConfigInfo, const String& configDir, bool authoritative)
{
bool configChange = false;
Dictionary::Ptr oldConfig = MergeConfigUpdate(oldConfigInfo);
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
double oldTimestamp;
if (!oldConfig->Contains("/.timestamp"))
oldTimestamp = 0;
else
oldTimestamp = oldConfig->Get("/.timestamp");
double newTimestamp;
if (!newConfig->Contains("/.timestamp"))
newTimestamp = Utility::GetTime();
else
newTimestamp = newConfig->Get("/.timestamp");
/* skip update if our configuration files are more recent */
if (oldTimestamp >= newTimestamp) {
Log(LogNotice, "ApiListener")
<< "Our configuration is more recent than the received configuration update."
<< " Ignoring configuration file update for path '" << configDir << "'. Current timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
<< std::fixed << std::setprecision(6) << oldTimestamp
<< ") >= received timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
<< newTimestamp << ").";
return false;
}
size_t numBytes = 0;
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair& kv : newConfig) {
if (oldConfig->Get(kv.first) != kv.second) {
if (!Utility::Match("*/.timestamp", kv.first))
configChange = true;
String path = configDir + "/" + kv.first;
Log(LogInformation, "ApiListener")
<< "Updating configuration file: " << path;
/* Sync string content only. */
String content = kv.second;
/* Generate a directory tree (zones/1/2/3 might not exist yet). */
Utility::MkDirP(Utility::DirName(path), 0755);
std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fp << content;
fp.close();
numBytes += content.GetLength();
}
}
}
Log(LogInformation, "ApiListener")
<< "Applying configuration file update for path '" << configDir << "' (" << numBytes << " Bytes). Received timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
<< std::fixed << std::setprecision(6) << newTimestamp
<< "), Current timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", oldTimestamp) << "' ("
<< oldTimestamp << ").";
ObjectLock xlock(oldConfig);
for (const Dictionary::Pair& kv : oldConfig) {
if (!newConfig->Contains(kv.first)) {
configChange = true;
String path = configDir + "/" + kv.first;
(void) unlink(path.CStr());
}
}
String tsPath = configDir + "/.timestamp";
if (!Utility::PathExists(tsPath)) {
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << newTimestamp;
fp.close();
}
if (authoritative) {
String authPath = configDir + "/.authoritative";
if (!Utility::PathExists(authPath)) {
std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp.close();
}
}
return configChange;
}
void ApiListener::SyncZoneDir(const Zone::Ptr& zone) const
{
ConfigDirInformation newConfigInfo;
newConfigInfo.UpdateV1 = new Dictionary();
newConfigInfo.UpdateV2 = new Dictionary();
for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zone->GetName())) {
ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
{
ObjectLock olock(newConfigPart.UpdateV1);
for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
newConfigInfo.UpdateV1->Set("/" + zf.Tag + kv.first, kv.second);
}
}
{
ObjectLock olock(newConfigPart.UpdateV2);
for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
newConfigInfo.UpdateV2->Set("/" + zf.Tag + kv.first, kv.second);
}
}
}
int sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
if (sumUpdates == 0)
return;
String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
Log(LogInformation, "ApiListener")
<< "Copying " << sumUpdates << " zone configuration files for zone '" << zone->GetName() << "' to '" << oldDir << "'.";
Utility::MkDirP(oldDir, 0700);
ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, true);
}
void ApiListener::SyncZoneDirs() const
/**
* Entrypoint for updating all authoritative configs from /etc/zones.d, packages, etc.
* into var/lib/icinga2/api/zones
*/
void ApiListener::SyncLocalZoneDirs() const
{
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
try {
SyncZoneDir(zone);
SyncLocalZoneDir(zone);
} catch (const std::exception&) {
continue;
}
}
}
/**
* Sync a zone directory where we have an authoritative copy (zones.d, packages, etc.)
*
* This function collects the registered zone config dirs from
* the config compiler and reads the file content into the config
* information structure.
*
* Returns early when there are no updates.
*
* @param zone Pointer to the zone object being synced.
*/
void ApiListener::SyncLocalZoneDir(const Zone::Ptr& zone) const
{
if (!zone)
return;
ConfigDirInformation newConfigInfo;
newConfigInfo.UpdateV1 = new Dictionary();
newConfigInfo.UpdateV2 = new Dictionary();
newConfigInfo.Checksums = new Dictionary();
String zoneName = zone->GetName();
// Load registered zone paths, e.g. '_etc', '_api' and user packages.
for (const ZoneFragment& zf : ConfigCompiler::GetZoneDirs(zoneName)) {
ConfigDirInformation newConfigPart = LoadConfigDir(zf.Path);
// Config files '*.conf'.
{
ObjectLock olock(newConfigPart.UpdateV1);
for (const Dictionary::Pair& kv : newConfigPart.UpdateV1) {
String path = "/" + zf.Tag + kv.first;
newConfigInfo.UpdateV1->Set(path, kv.second);
newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
}
}
// Meta files.
{
ObjectLock olock(newConfigPart.UpdateV2);
for (const Dictionary::Pair& kv : newConfigPart.UpdateV2) {
String path = "/" + zf.Tag + kv.first;
newConfigInfo.UpdateV2->Set(path, kv.second);
newConfigInfo.Checksums->Set(path, GetChecksum(kv.second));
}
}
}
size_t sumUpdates = newConfigInfo.UpdateV1->GetLength() + newConfigInfo.UpdateV2->GetLength();
// Return early if there are no updates.
if (sumUpdates == 0)
return;
String productionZonesDir = GetApiZonesDir() + zoneName;
Log(LogInformation, "ApiListener")
<< "Copying " << sumUpdates << " zone configuration files for zone '" << zoneName << "' to '" << productionZonesDir << "'.";
// Purge files to allow deletion via zones.d.
if (Utility::PathExists(productionZonesDir))
Utility::RemoveDirRecursive(productionZonesDir);
Utility::MkDirP(productionZonesDir, 0700);
// Copy content and add additional meta data.
size_t numBytes = 0;
/* Note: We cannot simply copy directories here.
*
* Zone directories are registered from everywhere and we already
* have read their content into memory with LoadConfigDir().
*/
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair& kv : newConfig) {
String dst = productionZonesDir + "/" + kv.first;
Utility::MkDirP(Utility::DirName(dst), 0755);
Log(LogInformation, "ApiListener")
<< "Updating configuration file: " << dst;
String content = kv.second;
std::ofstream fp(dst.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fp << content;
fp.close();
numBytes += content.GetLength();
}
}
// Additional metadata.
String tsPath = productionZonesDir + "/.timestamp";
if (!Utility::PathExists(tsPath)) {
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << Utility::GetTime();
fp.close();
}
String authPath = productionZonesDir + "/.authoritative";
if (!Utility::PathExists(authPath)) {
std::ofstream fp(authPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp.close();
}
// Checksums.
String checksumsPath = productionZonesDir + "/.checksums";
if (Utility::PathExists(checksumsPath))
Utility::Remove(checksumsPath);
std::ofstream fp(checksumsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << JsonEncode(newConfigInfo.Checksums);
fp.close();
Log(LogNotice, "ApiListener")
<< "Updated meta data for cluster config sync. Checksum: '" << checksumsPath
<< "', timestamp: '" << tsPath << "', auth: '" << authPath << "'.";
}
/**
* Entrypoint for sending a file based config update to a cluster client.
* This includes security checks for zone relations.
* Loads the zone config files where this client belongs to
* and sends the 'config::Update' JSON-RPC message.
*
* @param aclient Connected JSON-RPC client.
*/
void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
{
Endpoint::Ptr endpoint = aclient->GetEndpoint();
ASSERT(endpoint);
Zone::Ptr azone = endpoint->GetZone();
Zone::Ptr lzone = Zone::GetLocalZone();
Zone::Ptr clientZone = endpoint->GetZone();
Zone::Ptr localZone = Zone::GetLocalZone();
/* don't try to send config updates to our master */
if (!azone->IsChildOf(lzone))
// Don't send config updates to parent zones
if (!clientZone->IsChildOf(localZone))
return;
Dictionary::Ptr configUpdateV1 = new Dictionary();
Dictionary::Ptr configUpdateV2 = new Dictionary();
Dictionary::Ptr configUpdateChecksums = new Dictionary(); // new since 2.11
String zonesDir = Configuration::DataDir + "/api/zones";
String zonesDir = GetApiZonesDir();
for (const Zone::Ptr& zone : ConfigType::GetObjectsByType<Zone>()) {
String zoneDir = zonesDir + "/" + zone->GetName();
String zoneName = zone->GetName();
String zoneDir = zonesDir + zoneName;
if (!zone->IsChildOf(azone) && !zone->IsGlobal())
// Only sync child and global zones.
if (!zone->IsChildOf(clientZone) && !zone->IsGlobal())
continue;
// Zone was configured, but there's no configuration directory.
if (!Utility::PathExists(zoneDir))
continue;
Log(LogInformation, "ApiListener")
<< "Syncing configuration files for " << (zone->IsGlobal() ? "global " : "")
<< "zone '" << zone->GetName() << "' to endpoint '" << endpoint->GetName() << "'.";
<< "zone '" << zoneName << "' to endpoint '" << endpoint->GetName() << "'.";
ConfigDirInformation config = LoadConfigDir(zonesDir + "/" + zone->GetName());
configUpdateV1->Set(zone->GetName(), config.UpdateV1);
configUpdateV2->Set(zone->GetName(), config.UpdateV2);
ConfigDirInformation config = LoadConfigDir(zoneDir);
configUpdateV1->Set(zoneName, config.UpdateV1);
configUpdateV2->Set(zoneName, config.UpdateV2);
configUpdateChecksums->Set(zoneName, config.Checksums); // new since 2.11
}
Dictionary::Ptr message = new Dictionary({
@ -251,15 +221,29 @@ void ApiListener::SendConfigUpdate(const JsonRpcConnection::Ptr& aclient)
{ "method", "config::Update" },
{ "params", new Dictionary({
{ "update", configUpdateV1 },
{ "update_v2", configUpdateV2 }
{ "update_v2", configUpdateV2 }, // Since 2.4.2.
{ "checksums", configUpdateChecksums } // Since 2.11.0.
}) }
});
aclient->SendMessage(message);
}
/**
* Registered handler when a new config::Update message is received.
*
* Checks destination and permissions first, locks the transaction and analyses the update.
* The newly received configuration is not copied to production immediately,
* but into the staging directory first.
* Last, the async validation and restart is triggered.
*
* @param origin Where this message came from.
* @param params Message parameters including the config updates.
* @returns Empty, required by the interface.
*/
Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const Dictionary::Ptr& params)
{
// Verify permissions and trust relationship.
if (!origin->FromClient->GetEndpoint() || (origin->FromZone && !Zone::GetLocalZone()->IsChildOf(origin->FromZone)))
return Empty;
@ -281,52 +265,526 @@ Value ApiListener::ConfigUpdateHandler(const MessageOrigin::Ptr& origin, const D
*/
boost::mutex::scoped_lock lock(m_ConfigSyncStageLock);
Log(LogInformation, "ApiListener")
<< "Applying config update from endpoint '" << origin->FromClient->GetEndpoint()->GetName()
<< "' of zone '" << GetFromZoneName(origin->FromZone) << "'.";
String apiZonesStageDir = GetApiZonesStageDir();
String fromEndpointName = origin->FromClient->GetEndpoint()->GetName();
String fromZoneName = GetFromZoneName(origin->FromZone);
Log(LogInformation, "ApiListener")
<< "Applying config update from endpoint '" << fromEndpointName
<< "' of zone '" << fromZoneName << "'.";
// Config files.
Dictionary::Ptr updateV1 = params->Get("update");
// Meta data files: .timestamp, etc.
Dictionary::Ptr updateV2 = params->Get("update_v2");
// New since 2.11.0.
Dictionary::Ptr checksums;
if (params->Contains("checksums"))
checksums = params->Get("checksums");
bool configChange = false;
// Keep track of the relative config paths for later validation and copying. TODO: Find a better algorithm.
std::vector<String> relativePaths;
/*
* We can and must safely purge the staging directory, as the difference is taken between
* runtime production config and newly received configuration.
* This is needed to not mix deleted/changed content between received and stage
* config.
*/
if (Utility::PathExists(apiZonesStageDir))
Utility::RemoveDirRecursive(apiZonesStageDir);
Utility::MkDirP(apiZonesStageDir, 0700);
// Analyse and process the update.
ObjectLock olock(updateV1);
for (const Dictionary::Pair& kv : updateV1) {
Zone::Ptr zone = Zone::GetByName(kv.first);
// Check for the configured zones.
String zoneName = kv.first;
Zone::Ptr zone = Zone::GetByName(zoneName);
if (!zone) {
Log(LogWarning, "ApiListener")
<< "Ignoring config update for unknown zone '" << kv.first << "'.";
<< "Ignoring config update from endpoint '" << fromEndpointName
<< "' for unknown zone '" << zoneName << "'.";
continue;
}
if (ConfigCompiler::HasZoneConfigAuthority(kv.first)) {
Log(LogWarning, "ApiListener")
<< "Ignoring config update for zone '" << kv.first << "' because we have an authoritative version of the zone's config.";
// Ignore updates where we have an authoritive copy in etc/zones.d, packages, etc.
if (ConfigCompiler::HasZoneConfigAuthority(zoneName)) {
Log(LogInformation, "ApiListener")
<< "Ignoring config update from endpoint '" << fromEndpointName
<< "' for zone '" << zoneName << "' because we have an authoritative version of the zone's config.";
continue;
}
String oldDir = Configuration::DataDir + "/api/zones/" + zone->GetName();
// Put the received configuration into our stage directory.
String productionConfigZoneDir = GetApiZonesDir() + zoneName;
String stageConfigZoneDir = GetApiZonesStageDir() + zoneName;
Utility::MkDirP(oldDir, 0700);
Utility::MkDirP(productionConfigZoneDir, 0700);
Utility::MkDirP(stageConfigZoneDir, 0700);
// Merge the config information.
ConfigDirInformation newConfigInfo;
newConfigInfo.UpdateV1 = kv.second;
// Load metadata.
if (updateV2)
newConfigInfo.UpdateV2 = updateV2->Get(kv.first);
Dictionary::Ptr newConfig = kv.second;
ConfigDirInformation oldConfigInfo = LoadConfigDir(oldDir);
// Load checksums. New since 2.11.
if (checksums)
newConfigInfo.Checksums = checksums->Get(kv.first);
if (UpdateConfigDir(oldConfigInfo, newConfigInfo, oldDir, false))
configChange = true;
// Load the current production config details.
ConfigDirInformation productionConfigInfo = LoadConfigDir(productionConfigZoneDir);
// Merge updateV1 and updateV2
Dictionary::Ptr productionConfig = MergeConfigUpdate(productionConfigInfo);
Dictionary::Ptr newConfig = MergeConfigUpdate(newConfigInfo);
/* If we have received 'checksums' via cluster message, go for it.
* Otherwise do the old timestamp dance for versions < 2.11.
*/
if (checksums) {
Log(LogInformation, "ApiListener")
<< "Received configuration for zone '" << zoneName << "' from endpoint '"
<< fromEndpointName << "'. Comparing the checksums.";
// TODO: Do this earlier in hello-handshakes?
if (CheckConfigChange(productionConfigInfo, newConfigInfo))
configChange = true;
} else {
/* Fallback to timestamp handling when the parent endpoint didn't send checks.
* This can happen when the satellite is 2.11 and the master is 2.10.
*
* TODO: Deprecate and remove this behaviour in 2.13+.
*/
Log(LogWarning, "ApiListener")
<< "Received configuration update without checksums from parent endpoint "
<< fromEndpointName << ". This behaviour is deprecated. Please upgrade the parent endpoint to 2.11+";
double productionTimestamp;
if (!productionConfig->Contains("/.timestamp"))
productionTimestamp = 0;
else
productionTimestamp = productionConfig->Get("/.timestamp");
double newTimestamp;
if (!newConfig->Contains("/.timestamp"))
newTimestamp = Utility::GetTime();
else
newTimestamp = newConfig->Get("/.timestamp");
// Skip update if our configuration files are more recent.
if (productionTimestamp >= newTimestamp) {
Log(LogInformation, "ApiListener")
<< "Our configuration is more recent than the received configuration update."
<< " Ignoring configuration file update for path '" << stageConfigZoneDir << "'. Current timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", productionTimestamp) << "' ("
<< std::fixed << std::setprecision(6) << productionTimestamp
<< ") >= received timestamp '"
<< Utility::FormatDateTime("%Y-%m-%d %H:%M:%S %z", newTimestamp) << "' ("
<< newTimestamp << ").";
} else {
configChange = true;
}
// Keep another hack when there's a timestamp file missing.
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair &kv : newConfig) {
// This is super expensive with a string content comparison.
if (productionConfig->Get(kv.first) != kv.second) {
if (!Utility::Match("*/.timestamp", kv.first))
configChange = true;
}
}
}
// Update the .timestamp file.
String tsPath = stageConfigZoneDir + "/.timestamp";
if (!Utility::PathExists(tsPath)) {
std::ofstream fp(tsPath.CStr(), std::ofstream::out | std::ostream::trunc);
fp << std::fixed << newTimestamp;
fp.close();
}
}
// Dump the received configuration for this zone into the stage directory.
size_t numBytes = 0;
{
ObjectLock olock(newConfig);
for (const Dictionary::Pair& kv : newConfig) {
/* Store the relative config file path for later validation and activation.
* IMPORTANT: Store this prior to any filters.
* */
relativePaths.push_back(zoneName + "/" + kv.first);
// Ignore same config content. This is an expensive comparison.
if (productionConfig->Get(kv.first) == kv.second)
continue;
String path = stageConfigZoneDir + "/" + kv.first;
if (Utility::Match("*.conf", path)) {
Log(LogInformation, "ApiListener")
<< "Stage: Updating received configuration file '" << path << "' for zone '" << zoneName << "'.";
}
// Sync string content only.
String content = kv.second;
// Generate a directory tree (zones/1/2/3 might not exist yet).
Utility::MkDirP(Utility::DirName(path), 0755);
// Write the content to file.
std::ofstream fp(path.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fp << content;
fp.close();
numBytes += content.GetLength();
}
}
Log(LogInformation, "ApiListener")
<< "Applying configuration file update for path '" << stageConfigZoneDir << "' ("
<< numBytes << " Bytes).";
// If the update removes a path, delete it on disk and signal a config change.
{
ObjectLock xlock(productionConfig);
for (const Dictionary::Pair& kv : productionConfig) {
if (!newConfig->Contains(kv.first)) {
configChange = true;
String path = stageConfigZoneDir + "/" + kv.first;
Utility::Remove(path);
}
}
}
}
/*
* We have processed all configuration files and stored them in the staging directory.
*
* We need to store them locally for later analysis. A config change means
* that we will validate the configuration in a separate process sandbox,
* and only copy the configuration to production when everything is ok.
*
* A successful validation also triggers the final restart.
*/
if (configChange) {
Log(LogInformation, "ApiListener", "Restarting after configuration change.");
Application::RequestRestart();
Log(LogInformation, "ApiListener")
<< "Received configuration from endpoint '" << fromEndpointName
<< "' is different to production, triggering validation and reload.";
AsyncTryActivateZonesStage(relativePaths);
} else {
Log(LogInformation, "ApiListener")
<< "Received configuration from endpoint '" << fromEndpointName
<< "' is equal to production, not triggering reload.";
}
return Empty;
}
/**
* Callback for stage config validation.
* When validation was successful, the configuration is copied from
* stage to production and a restart is triggered.
* On failure, there's no restart and this is logged.
*
* @param pr Result of the validation process.
* @param relativePaths Collected paths including the zone name, which are copied from stage to current directories.
*/
void ApiListener::TryActivateZonesStageCallback(const ProcessResult& pr,
const std::vector<String>& relativePaths)
{
String apiZonesDir = GetApiZonesDir();
String apiZonesStageDir = GetApiZonesStageDir();
String logFile = apiZonesStageDir + "/startup.log";
std::ofstream fpLog(logFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fpLog << pr.Output;
fpLog.close();
String statusFile = apiZonesStageDir + "/status";
std::ofstream fpStatus(statusFile.CStr(), std::ofstream::out | std::ostream::binary | std::ostream::trunc);
fpStatus << pr.ExitStatus;
fpStatus.close();
// Validation went fine, copy stage and reload.
if (pr.ExitStatus == 0) {
Log(LogInformation, "ApiListener")
<< "Config validation for stage '" << apiZonesStageDir << "' was OK, replacing into '" << apiZonesDir << "' and triggering reload.";
// Purge production before copying stage.
if (Utility::PathExists(apiZonesDir))
Utility::RemoveDirRecursive(apiZonesDir);
Utility::MkDirP(apiZonesDir, 0700);
// Copy all synced configuration files from stage to production.
for (const String& path : relativePaths) {
if (!Utility::PathExists(path))
continue;
Log(LogInformation, "ApiListener")
<< "Copying file '" << path << "' from config sync staging to production zones directory.";
String stagePath = apiZonesStageDir + path;
String currentPath = apiZonesDir + path;
Utility::MkDirP(Utility::DirName(currentPath), 0700);
Utility::CopyFile(stagePath, currentPath);
}
// Clear any failed deployment before
ApiListener::Ptr listener = ApiListener::GetInstance();
if (listener)
listener->ClearLastFailedZonesStageValidation();
Application::RequestRestart();
// All good, return early.
return;
}
// Error case.
Log(LogCritical, "ApiListener")
<< "Config validation failed for staged cluster config sync in '" << apiZonesStageDir
<< "'. Aborting. Logs: '" << logFile << "'";
ApiListener::Ptr listener = ApiListener::GetInstance();
if (listener)
listener->UpdateLastFailedZonesStageValidation(pr.Output);
}
/**
* Spawns a new validation process and waits for its output.
* Sets 'System.ZonesStageVarDir' to override the config validation zone dirs with our current stage.
*
* @param relativePaths Required for later file operations in the callback. Provides the zone name plus path in a list.
*/
void ApiListener::AsyncTryActivateZonesStage(const std::vector<String>& relativePaths)
{
VERIFY(Application::GetArgC() >= 1);
/* Inherit parent process args. */
Array::Ptr args = new Array({
Application::GetExePath(Application::GetArgV()[0]),
});
for (int i = 1; i < Application::GetArgC(); i++) {
String argV = Application::GetArgV()[i];
if (argV == "-d" || argV == "--daemonize")
continue;
args->Add(argV);
}
args->Add("--validate");
// Set the ZonesStageDir. This creates our own local chroot without any additional automated zone includes.
args->Add("--define");
args->Add("System.ZonesStageVarDir=" + GetApiZonesStageDir());
Process::Ptr process = new Process(Process::PrepareCommand(args));
process->SetTimeout(Application::GetReloadTimeout());
process->Run(std::bind(&TryActivateZonesStageCallback, _1, relativePaths));
}
/**
* Update the structure from the last failed validation output.
* Uses the current timestamp.
*
* @param log The process output from the config validation.
*/
void ApiListener::UpdateLastFailedZonesStageValidation(const String& log)
{
Dictionary::Ptr lastFailedZonesStageValidation = new Dictionary({
{ "log", log },
{ "ts", Utility::GetTime() }
});
SetLastFailedZonesStageValidation(lastFailedZonesStageValidation);
}
/**
* Clear the structure for the last failed reload.
*
*/
void ApiListener::ClearLastFailedZonesStageValidation()
{
SetLastFailedZonesStageValidation(Dictionary::Ptr());
}
/**
* Generate a config checksum.
*
* @param content String content used for generating the checksum.
* @returns The checksum as string.
*/
String ApiListener::GetChecksum(const String& content)
{
return SHA256(content);
}
bool ApiListener::CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig)
{
Dictionary::Ptr oldChecksums = oldConfig.Checksums;
Dictionary::Ptr newChecksums = newConfig.Checksums;
// TODO: Figure out whether normal users need this for debugging.
Log(LogDebug, "ApiListener")
<< "Checking for config change between stage and production. Old (" << oldChecksums->GetLength() << "): '"
<< JsonEncode(oldChecksums)
<< "' vs. new (" << newChecksums->GetLength() << "): '"
<< JsonEncode(newChecksums) << "'.";
// Different length means that either one or the other side added or removed something. */
if (oldChecksums->GetLength() != newChecksums->GetLength())
return true;
// Both dictionaries have an equal size.
ObjectLock olock(oldChecksums);
for (const Dictionary::Pair& kv : oldChecksums) {
String path = kv.first;
String oldChecksum = kv.second;
// TODO: Figure out if config changes only apply to '.conf'. Leaving this open for other config files.
//if (!Utility::Match("*.conf", path))
// continue;
/* Ignore internal files, especially .timestamp and .checksums.
*
* If we don't, this results in "always change" restart loops.
*/
if (Utility::Match("/.*", path))
continue;
Log(LogDebug, "ApiListener")
<< "Checking " << path << " for checksum: " << oldChecksum;
// Check whether our key exists in the new checksums, and they have an equal value.
String newChecksum = newChecksums->Get(path);
if (newChecksums->Get(path) != kv.second) {
Log(LogDebug, "ApiListener")
<< "Path '" << path << "' doesn't match old checksum '"
<< newChecksum << "' with new checksum '" << oldChecksum << "'.";
return true;
}
}
return false;
}
/**
* Load the given config dir and read their file content into the config structure.
*
* @param dir Path to the config directory.
* @returns ConfigDirInformation structure.
*/
ConfigDirInformation ApiListener::LoadConfigDir(const String& dir)
{
ConfigDirInformation config;
config.UpdateV1 = new Dictionary();
config.UpdateV2 = new Dictionary();
config.Checksums = new Dictionary();
Utility::GlobRecursive(dir, "*", std::bind(&ApiListener::ConfigGlobHandler, std::ref(config), dir, _1), GlobFile);
return config;
}
/**
* Read the given file and store it in the config information structure.
* Callback function for Glob().
*
* @param config Reference to the config information object.
* @param path File path.
* @param file Full file name.
*/
void ApiListener::ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file)
{
// Avoid loading the authoritative marker for syncs at all cost.
if (Utility::BaseName(file) == ".authoritative")
return;
CONTEXT("Creating config update for file '" + file + "'");
Log(LogNotice, "ApiListener")
<< "Creating config update for file '" << file << "'.";
std::ifstream fp(file.CStr(), std::ifstream::binary);
if (!fp)
return;
String content((std::istreambuf_iterator<char>(fp)), std::istreambuf_iterator<char>());
Dictionary::Ptr update;
String relativePath = file.SubStr(path.GetLength());
/*
* 'update' messages contain conf files. 'update_v2' syncs everything else (.timestamp).
*
* **Keep this intact to stay compatible with older clients.**
*/
if (Utility::Match("*.conf", file))
update = config.UpdateV1;
else
update = config.UpdateV2;
update->Set(relativePath, content);
/* Calculate a checksum for each file (and a global one later).
*
* IMPORTANT: Ignore the .authoritative file above, this must not be synced.
* */
config.Checksums->Set(relativePath, GetChecksum(content));
}
/**
* Compatibility helper for merging config update v1 and v2 into a global result.
*
* @param config Config information structure.
* @returns Dictionary which holds the merged information.
*/
Dictionary::Ptr ApiListener::MergeConfigUpdate(const ConfigDirInformation& config)
{
Dictionary::Ptr result = new Dictionary();
if (config.UpdateV1)
config.UpdateV1->CopyTo(result);
if (config.UpdateV2)
config.UpdateV2->CopyTo(result);
return result;
}

View File

@ -59,6 +59,16 @@ String ApiListener::GetApiDir()
return Configuration::DataDir + "/api/";
}
String ApiListener::GetApiZonesDir()
{
return GetApiDir() + "zones/";
}
String ApiListener::GetApiZonesStageDir()
{
return GetApiDir() + "zones-stage/";
}
String ApiListener::GetCertsDir()
{
return Configuration::DataDir + "/certs/";
@ -232,7 +242,7 @@ void ApiListener::Start(bool runtimeCreated)
Log(LogInformation, "ApiListener")
<< "'" << GetName() << "' started.";
SyncZoneDirs();
SyncLocalZoneDirs();
ObjectImpl<ApiListener>::Start(runtimeCreated);

View File

@ -9,6 +9,7 @@
#include "remote/endpoint.hpp"
#include "remote/messageorigin.hpp"
#include "base/configobject.hpp"
#include "base/process.hpp"
#include "base/timer.hpp"
#include "base/workqueue.hpp"
#include "base/tcpsocket.hpp"
@ -31,6 +32,7 @@ struct ConfigDirInformation
{
Dictionary::Ptr UpdateV1;
Dictionary::Ptr UpdateV2;
Dictionary::Ptr Checksums;
};
/**
@ -47,6 +49,8 @@ public:
ApiListener();
static String GetApiDir();
static String GetApiZonesDir();
static String GetApiZonesStageDir();
static String GetCertsDir();
static String GetCaDir();
static String GetCertificateRequestsDir();
@ -167,16 +171,26 @@ private:
/* filesync */
static boost::mutex m_ConfigSyncStageLock;
static ConfigDirInformation LoadConfigDir(const String& dir);
static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config);
static bool UpdateConfigDir(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig, const String& configDir, bool authoritative);
void SyncLocalZoneDirs() const;
void SyncLocalZoneDir(const Zone::Ptr& zone) const;
void SyncZoneDirs() const;
void SyncZoneDir(const Zone::Ptr& zone) const;
static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file);
void SendConfigUpdate(const JsonRpcConnection::Ptr& aclient);
static Dictionary::Ptr MergeConfigUpdate(const ConfigDirInformation& config);
static ConfigDirInformation LoadConfigDir(const String& dir);
static void ConfigGlobHandler(ConfigDirInformation& config, const String& path, const String& file);
static void TryActivateZonesStageCallback(const ProcessResult& pr,
const std::vector<String>& relativePaths);
static void AsyncTryActivateZonesStage(const std::vector<String>& relativePaths);
static String GetChecksum(const String& content);
static bool CheckConfigChange(const ConfigDirInformation& oldConfig, const ConfigDirInformation& newConfig);
void UpdateLastFailedZonesStageValidation(const String& log);
void ClearLastFailedZonesStageValidation();
/* configsync */
void UpdateConfigObject(const ConfigObject::Ptr& object, const MessageOrigin::Ptr& origin,
const JsonRpcConnection::Ptr& client = nullptr);

View File

@ -54,6 +54,8 @@ class ApiListener : ConfigObject
[state, no_user_modify] Timestamp log_message_timestamp;
[no_user_modify] String identity;
[state, no_user_modify] Dictionary::Ptr last_failed_zones_stage_validation;
};
}