blob: 5f4cf6dc521c212ffb85f37dfa772bbaaf65afe2 [file] [log] [blame]
// 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 "kudu/clock/test/mini_chronyd.h"
#include <unistd.h>
#include <algorithm>
#include <cerrno>
#include <csignal>
#include <iterator>
#include <memory>
#include <ostream>
#include <string>
#include <vector>
#include <glog/logging.h>
#include "kudu/gutil/strings/numbers.h"
#include "kudu/gutil/strings/split.h"
#include "kudu/gutil/strings/stringpiece.h"
#include "kudu/gutil/strings/strip.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/util/env.h"
#include "kudu/util/errno.h"
#include "kudu/util/monotime.h"
#include "kudu/util/net/net_util.h"
#include "kudu/util/path_util.h"
#include "kudu/util/slice.h"
#include "kudu/util/status.h"
#include "kudu/util/stopwatch.h"
#include "kudu/util/subprocess.h"
#include "kudu/util/test_util.h"
#include "kudu/util/user.h"
using std::string;
using std::unique_ptr;
using std::vector;
using strings::SkipEmpty;
using strings::Split;
using strings::Substitute;
namespace kudu {
namespace clock {
string MiniChronydServerOptions::ToString() const {
return Substitute(
"{address: $0,"
" port: $1,"
" minpoll: $2,"
" maxpoll: $3,"
" iburst: $4,"
" burst: $5,"
" offset: $6}",
address, port, minpoll, maxpoll, iburst, burst, offset);
}
string MiniChronydOptions::ToString() const {
string servers_str = "[";
for (const auto& s : servers) {
servers_str += " " + s.ToString() + ",";
}
servers_str += "]";
return Substitute(
"{index: $0,"
" data_root: $1,"
" bindcmdaddress: $2,"
" bindaddress: $3,"
" port: $4,"
" pidfile: $5,"
" local: $6,"
" local_stratum: $7,"
" servers: $8}",
index, data_root, bindcmdaddress, bindaddress, port, pidfile,
local, local_stratum, servers_str);
}
// Check that the specified servers are seen as good enough synchronisation
// source by the reference NTP client (chronyd itself).
Status MiniChronyd::CheckNtpSource(const vector<HostPort>& servers,
int timeout_sec) {
// The configuration template for chronyd to make it print the offset of the
// system clock from the NTP time of the specified servers. The NTP client
// has to latch on the specified NTP servers (i.e. deem them to be a reliable
// NTP clock source) before printing the offset. In case of successful
// latching, the offset is printed and chronyd exits with status 0. Otherwise,
// if the set of servers doesn't appear to be a reliable NTP clock source or
// other error happens, chronyd exits with non-zero status and prints warning
// "No suitable source for synchronisation".
//
// The shorter polling time intervals allows for faster querying of NTP
// servers, and using 'initial burst' mode allows for quicker exchange of NTP
// packets. Also, it's desirable to use the same NTP protocol version that is
// used by the Kudu built-in NTP client. Some more details:
// * 'minpoll' parameter is set to the smallest possible that is supported
// by chrony (-6, i.e. 1/64 second)
// * 'maxpoll' parameter is set to be at least 2 times longer interval
// than 'minpoll' with some margin to accumulate at least 4 samples
// with current setting of 'minpoll' interval
// * 'iburst' option allows to send first 4 NTP packets in burst.
// * 'version' is set to 3 to enforce using NTP v3 since the current
// implementation of the Kudu built-in NTP client uses NTPv3 (chrony
// supports NTP v4 and would use newer version of protocol otherwise)
static const string kConfigTemplate =
"server $0 port $1 maxpoll -1 minpoll -6 iburst version 3\n";
if (servers.empty()) {
return Status::InvalidArgument("empty set of NTP server endpoints");
}
string cfg;
for (const auto& hp : servers) {
cfg += Substitute(kConfigTemplate, hp.host(), hp.port());
}
string chronyd_bin;
RETURN_NOT_OK(MiniChronyd::GetChronydPath(&chronyd_bin));
const vector<string> cmd_and_args = {
chronyd_bin,
"-Q", // client-only mode without setting time
"-f", "/dev/stdin", // read config file from stdin
"-t", std::to_string(timeout_sec), // timeout for clock synchronisation
};
string out_stdout;
string out_stderr;
RETURN_NOT_OK_PREPEND(
Subprocess::Call(cmd_and_args, cfg, &out_stdout, &out_stderr),
Substitute("failed measure clock offset from reference NTP servers: "
"stdout{$0} stderr{$1}",
out_stdout, out_stderr));
return Status::OK();
}
MiniChronyd::MiniChronyd(MiniChronydOptions options)
: options_(std::move(options)) {
if (options_.data_root.empty()) {
options_.data_root = JoinPathSegments(
GetTestDataDirectory(), Substitute("chrony.$0", options_.index));
}
if (options_.pidfile.empty()) {
options_.pidfile = JoinPathSegments(options_.data_root, "chronyd.pid");
}
}
MiniChronyd::~MiniChronyd() {
if (process_) {
WARN_NOT_OK(Stop(), "unable to stop chronyd");
}
if (!cmd_socket_dir_.empty()) {
// Remove the directory created for the command Unix domain socket. The
// directory should be empty if chronyd exited normally: it cleans up after
// itself on a clean exit.
WARN_NOT_OK(Env::Default()->DeleteDir(cmd_socket_dir_),
"unable to remove chronyd command socket directory");
}
}
const MiniChronydOptions& MiniChronyd::options() const {
return options_;
}
pid_t MiniChronyd::pid() const {
CHECK(process_) << "must start chronyd process first";
return process_->pid();
}
HostPort MiniChronyd::address() const {
CHECK(process_) << "must start chronyd process first";
// If the test NTP server is bound to the wildcard IP address,
// use the loopback IP address to communicate with the server.
return HostPort(options_.bindaddress == kWildcardIpAddr ? kLoopbackIpAddr
: options_.bindaddress,
options_.port);
}
Status MiniChronyd::Start() {
SCOPED_LOG_SLOW_EXECUTION(WARNING, 100, "starting chronyd");
CHECK(!process_);
VLOG(1) << "starting chronyd: " << options_.ToString();
if (!Env::Default()->FileExists(options_.data_root)) {
RETURN_NOT_OK(Env::Default()->CreateDir(options_.data_root));
// The chronyd's implementation puts strict requirements on the ownership
// of the directories where the runtime data is stored. In some environments
// (e.g., macOS), the group owner of the newly created directory might be
// different from the user account's GID.
RETURN_NOT_OK(CorrectOwnership(options_.data_root));
}
VLOG(1) << "creating chronyd configuration file";
RETURN_NOT_OK(CreateConf());
// Start the chronyd in server-only mode, not detaching from terminal
// since the Subprocess needs to have the process running in foreground
// to be able to control it.
string server_bin;
RETURN_NOT_OK(GetChronydPath(&server_bin));
string username;
RETURN_NOT_OK(GetLoggedInUser(&username));
process_.reset(new Subprocess({
server_bin,
"-f", config_file_path(),
"-x", // do not drive the system clock (server-only mode)
"-d", // do not daemonize; print logs into standard out
}));
RETURN_NOT_OK(process_->Start());
static const auto kTimeout = MonoDelta::FromSeconds(1);
const auto deadline = MonoTime::Now() + kTimeout;
for (auto i = 0; ; ++i) {
auto s = GetServerStats(nullptr);
if (s.ok()) {
break;
}
if (deadline < MonoTime::Now()) {
return Status::TimedOut(Substitute("failed to contact chronyd in $0",
kTimeout.ToString()));
}
SleepFor(MonoDelta::FromMilliseconds(i * 2));
}
return Status::OK();
}
Status MiniChronyd::Stop() {
if (!process_) {
return Status::OK();
}
VLOG(1) << "stopping chronyd";
unique_ptr<Subprocess> proc = std::move(process_);
return proc->KillAndWait(SIGTERM);
}
Status MiniChronyd::Pause() {
if (!process_) {
return Status::IllegalState("chronyd is not running");
}
return process_->Kill(SIGSTOP);
}
Status MiniChronyd::Resume() {
if (!process_) {
return Status::IllegalState("chronyd is not running");
}
return process_->Kill(SIGCONT);
}
Status MiniChronyd::GetServerStats(ServerStats* stats) const {
static const string kNtpPacketsKey = "NTP packets received";
static const string kCmdPacketsKey = "Command packets received";
string out;
RETURN_NOT_OK(RunChronyCmd({ "serverstats" }, &out));
if (stats) {
ServerStats result;
bool ntp_packets_key_found = false;
bool cmd_packets_key_found = false;
for (StringPiece sp : Split(out, "\n", SkipEmpty())) {
vector<string> kv = Split(sp, ":", SkipEmpty());
if (kv.size() != 2) {
return Status::Corruption(
Substitute("'$0': unexpected line in serverstats", sp));
}
for (auto& str : kv) {
StripWhiteSpace(&str);
}
if (kv[0] == kNtpPacketsKey) {
int64_t val;
if (!safe_strto64(kv[1], &val)) {
return Status::Corruption(
Substitute("$0: unexpected value for '$1' in serverstats",
kv[1], kNtpPacketsKey));
}
result.ntp_packets_received = val;
ntp_packets_key_found = true;
} else if (kv[0] == kCmdPacketsKey) {
int64_t val;
if (!safe_strto64(kv[1], &val)) {
return Status::Corruption(
Substitute("$0: unexpected value for '$1' in serverstats",
kv[1], kCmdPacketsKey));
}
result.cmd_packets_received = val;
cmd_packets_key_found = true;
}
}
if (!(ntp_packets_key_found && cmd_packets_key_found)) {
return Status::Corruption("'$0': unexpected serverstats output", out);
}
*stats = result;
}
return Status::OK();
}
Status MiniChronyd::SetTime(time_t time) {
char buf[kFastToBufferSize];
char* time_to_set = FastTimeToBuffer(time, buf);
string out;
string err;
// The first command is 'manual reset' to remove samples accumulated prior
// to the run of this 'settime' command. Those are not needed and could
// confuse chrony when SetTime() is called next time with a close enough
// timestamp (in the chrony's source, see functions handle_settime() from
// cmdmon.c and MNL_AcceptTimestamp() from manual.c).
// The second command is 'settime <time>' which sets the time as requested.
auto s = RunChronyCmd(
{ "manual reset", Substitute("settime $0", time_to_set) }, &out, &err);
if (!s.ok()) {
s = s.CloneAndAppend(Substitute("stdout: $0 stderr: $1", out, err));
}
return s;
}
// Find absolute path to chronyc (chrony's CLI tool),
// storing the result path in 'path' output parameter.
Status MiniChronyd::GetChronycPath(string* path) {
return GetPath("chronyc", path);
}
// Find absolute path to chronyd (chrony NTP implementation),
// storing the result path in 'path' output parameter.
Status MiniChronyd::GetChronydPath(string* path) {
return GetPath("chronyd", path);
}
Status MiniChronyd::GetPath(const string& path_suffix, string* abs_path) {
Env* env = Env::Default();
string exe;
RETURN_NOT_OK(env->GetExecutablePath(&exe));
auto path = Substitute("$0/$1", DirName(exe), path_suffix);
string result;
RETURN_NOT_OK(env->Canonicalize(path, &result));
if (env->FileExists(result)) {
*abs_path = std::move(result);
return Status::OK();
}
return Status::NotFound(Substitute("$0: no such file", result));
}
Status MiniChronyd::CorrectOwnership(const string& path) {
const uid_t uid = getuid();
const gid_t gid = getgid();
if (chown(path.c_str(), uid, gid) == -1) {
int err = errno;
return Status::IOError(Substitute("chown($0, $1, $2)", path, uid, gid),
ErrnoToString(err), err);
}
return Status::OK();
}
string MiniChronyd::config_file_path() const {
return JoinPathSegments(options_.data_root, "chrony.conf");
}
// Creates a chronyd.conf file according to the provided options. The multitude
// of overriden parameters is because it's necessary to run multiple chronyd
// instances on the same node, so all the 'defaults' should be customized
// to avoid conflicts.
Status MiniChronyd::CreateConf() {
static const string kFileTemplateCommon = R"(
# Override the default user chronyd NTP server starts because the compiled-in
# default 'root' is not suitable when running chronyd in the context of the Kudu
# testing framework. It's also be possible to override this parameter in the
# chronyd's command line, but doing so in the configuration file is cleaner.
user $0
# The IP address to bind the NTP server socket to. By default, chronyd tries
# to bind to all available IP addresses, which is not desirable in case of a
# shared environment where Kudu tests are usually run.
bindaddress $1
# In this case, it's an absolute path to Unix domain socket file that is used
# by the chronyc CLI tool to send commands to chronyd server.
bindcmdaddress $2
# Override the default NTP port 123 since it's necessary to (1) run multiple
# chronyd servers at the same IP address and (2) specify non-privileged port,
# so chronyd is able to bind to the port even if the chronyd process is not run
# with super-user privileges.
port $3
# Absolute path where to store the PID of chronyd process once it's started.
pidfile $4
# The daemon is controlled only via Unix domain socket (see 'bindcmdaddress'),
# no INET network control port is needed. The command control via INET addresses
# is very limited: no need for that if the control via Unix domain socket
# is already enabled.
cmdport 0
# NTP clients from all addresses are allowed to access the NTP server that is
# serving requests as specified by the 'bindaddress' and 'port' directives.
allow all
# Allow setting the time manually using the cronyc CLI utility.
manual
)";
static const string kFileTemplateLocal = R"(
# Use the local clock as the clock source (usually it's a high precision
# oscillator or a NTP server), and report the stratum as configured.
local stratum $0
)";
static const string kFileTemplateServers = R"(
# The set of NTP servers to synchronize with.
$0
)";
if (options_.bindcmdaddress.empty()) {
// The path to Unix domain socket file cannot be longer than ~100 bytes,
// so it's necessary to create a directory with shorter absolute path.
// TODO(aserbin): use some synthetic mount point instead?
string dir = JoinPathSegments("/tmp", Substitute("$0.$1",
Env::Default()->NowMicros(), getpid()));
const auto s = Env::Default()->CreateDir(dir);
if (!s.ok() && !s.IsAlreadyPresent()) {
return s;
}
RETURN_NOT_OK(CorrectOwnership(dir));
options_.bindcmdaddress = Substitute("$0/chronyd.$1.sock",
dir, options_.index);
// Set cmd_socket_dir_ so we can cleanup the unix domain sockets.
cmd_socket_dir_ = std::move(dir);
}
string username;
RETURN_NOT_OK(GetLoggedInUser(&username));
auto contents = Substitute(kFileTemplateCommon,
username,
options_.bindaddress,
options_.bindcmdaddress,
options_.port,
options_.pidfile);
if (options_.local) {
contents += Substitute(kFileTemplateLocal, options_.local_stratum);
}
if (!options_.servers.empty()) {
string servers_str;
for (const auto& server : options_.servers) {
auto str = Substitute(
"server $0 port $1 minpoll $2 maxpoll $3 offset $4",
server.address, server.port, server.minpoll, server.maxpoll, server.offset);
if (server.iburst) {
str += " iburst";
}
if (server.burst) {
str += " burst";
}
str += "\n";
servers_str += str;
}
contents += Substitute(kFileTemplateServers, servers_str);
}
return WriteStringToFile(Env::Default(), contents, config_file_path());
}
Status MiniChronyd::RunChronyCmd(const vector<string>& args,
string* out_stdout,
string* out_stderr) const {
string chronyc_bin;
RETURN_NOT_OK(GetChronycPath(&chronyc_bin));
// Use '-m' switch to allow for multiple commands to be sent at once: each
// argument is interpreted as a separate command.
// Use '-n' switch to avoid resolving IP addresses into DNS names in case if
// chronyc outputs anything related to to IP addresses/hostnames (e.g.,
// list of source NTP servers or list of NTP clients server by chronyd, etc.).
// Since non-default command address is used for chronyd (the 'bindcmdaddress'
// option in 'chrony.conf'), it's necessary to specify the address using the
// '-h' switch (it's a path of the Unix domain socket in case of MiniChronyd).
// See 'man chronyc' for more details on the switches used for 'chronyc'
// invocation.
vector<string> cmd_and_args = {
chronyc_bin, "-n", "-m", "-h", cmdaddress(), };
std::copy(args.begin(), args.end(), std::back_inserter(cmd_and_args));
return Subprocess::Call(cmd_and_args, "", out_stdout, out_stderr);
}
} // namespace clock
} // namespace kudu