blob: 37ce9f37a496d55be30dece27525de400c137d36 [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 <cstdio>
#include <cstdlib>
#include <fstream> // IWYU pragma: keep
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <unordered_map>
#include <utility>
#include <vector>
#include <gflags/gflags.h>
#include <glog/logging.h>
#include <google/protobuf/message.h>
#include <google/protobuf/stubs/status.h>
#include <google/protobuf/stubs/stringpiece.h>
#include <google/protobuf/util/json_util.h>
#include <rapidjson/document.h>
#include <rapidjson/encodings.h>
#include <rapidjson/error/en.h>
#include <rapidjson/error/error.h>
#include <rapidjson/filereadstream.h>
#include <rapidjson/reader.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>
#include "kudu/gutil/macros.h"
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/gutil/walltime.h"
#include "kudu/tools/tool_action.h"
#include "kudu/tools/tool_action_common.h"
#include "kudu/util/env.h"
#include "kudu/util/flag_tags.h"
#include "kudu/util/flag_validators.h"
#include "kudu/util/path_util.h"
#include "kudu/util/pb_util.h"
#include "kudu/util/scoped_cleanup.h"
#include "kudu/util/slice.h"
#include "kudu/util/status.h"
#include "kudu/util/subprocess.h"
using google::protobuf::util::JsonParseOptions;
using google::protobuf::util::JsonStringToMessage;
using std::cout;
using std::string;
using std::unique_ptr;
using std::vector;
DEFINE_bool(oneline, false, "Print each protobuf on a single line");
TAG_FLAG(oneline, stable);
DEFINE_bool(json, false, "Print protobufs in JSON format");
TAG_FLAG(json, stable);
DEFINE_bool(json_pretty, false, "Print or edit protobufs in JSON pretty format");
TAG_FLAG(json_pretty, evolving);
DEFINE_bool(debug, false, "Print extra debugging information about each protobuf");
TAG_FLAG(debug, stable);
DEFINE_bool(backup, true, "Write a backup file");
bool ValidatePBCFlags() {
int count = 0;
if (FLAGS_debug) count++;
if (FLAGS_oneline) count++;
if (FLAGS_json) count++;
if (FLAGS_json_pretty) count++;
if (count > 1) {
LOG(ERROR) << "only one of --debug, --oneline, --json or --json_pretty "
"can be provided at most";
return false;
}
return true;
}
GROUP_FLAG_VALIDATOR(validate_pbc_flags, ValidatePBCFlags);
namespace kudu {
using pb_util::ReadablePBContainerFile;
using strings::Substitute;
namespace tools {
namespace {
const char* const kPathArg = "path";
bool IsFileEncrypted(Env* env, const std::string& fname) {
if (!env->IsEncryptionEnabled()) {
return false;
}
RandomAccessFileOptions opts;
opts.is_sensitive = true;
unique_ptr<RandomAccessFile> reader;
return env->NewRandomAccessFile(opts, fname, &reader).ok();
}
Status DumpPBContainerFile(const RunnerContext& context) {
const string& path = FindOrDie(context.required_args, kPathArg);
auto format = ReadablePBContainerFile::Format::DEFAULT;
if (FLAGS_json) {
format = ReadablePBContainerFile::Format::JSON;
} else if (FLAGS_json_pretty) {
format = ReadablePBContainerFile::Format::JSON_PRETTY;
} else if (FLAGS_oneline) {
format = ReadablePBContainerFile::Format::ONELINE;
} else if (FLAGS_debug) {
format = ReadablePBContainerFile::Format::DEBUG;
}
RETURN_NOT_OK(SetServerKey());
Env* env = Env::Default();
unique_ptr<RandomAccessFile> reader;
RandomAccessFileOptions opts;
opts.is_sensitive = IsFileEncrypted(env, path);
RETURN_NOT_OK(env->NewRandomAccessFile(opts, path, &reader));
ReadablePBContainerFile pb_reader(std::move(reader));
RETURN_NOT_OK(pb_reader.Open());
RETURN_NOT_OK(pb_reader.Dump(&std::cout, format));
return Status::OK();
}
// Run the user's configured editor on 'path'.
Status RunEditor(const string& path) {
const char* editor = getenv("EDITOR");
if (!editor) {
editor = "vi";
}
Subprocess editor_proc({editor, path});
editor_proc.ShareParentStdin();
editor_proc.ShareParentStdout();
editor_proc.ShareParentStderr();
RETURN_NOT_OK_PREPEND(editor_proc.Start(), "couldn't start editor");
int ret = 0;
RETURN_NOT_OK_PREPEND(editor_proc.Wait(&ret), "edit failed");
if (ret != 0) {
return Status::Aborted("editor returned non-zero exit code");
}
return Status::OK();
}
Status EditFile(const RunnerContext& context) {
RETURN_NOT_OK(SetServerKey());
Env* env = Env::Default();
const string& path = FindOrDie(context.required_args, kPathArg);
const string& dir = DirName(path);
// Open the original file.
unique_ptr<RandomAccessFile> reader;
RandomAccessFileOptions reader_opts;
reader_opts.is_sensitive = IsFileEncrypted(env, path);
RETURN_NOT_OK(env->NewRandomAccessFile(reader_opts, path, &reader));
ReadablePBContainerFile pb_reader(std::move(reader));
RETURN_NOT_OK(pb_reader.Open());
// Make a new RWFile where we'll write the changed PBC file.
// Do this up front so that we fail early if the user doesn't have appropriate permissions.
const string tmp_out_path = path + ".new";
unique_ptr<RWFile> out_rwfile;
RWFileOptions out_rwfile_opts;
out_rwfile_opts.is_sensitive = IsFileEncrypted(env, path);
RETURN_NOT_OK_PREPEND(env->NewRWFile(out_rwfile_opts, tmp_out_path, &out_rwfile),
"couldn't open output PBC file");
auto delete_tmp_output = MakeScopedCleanup([&]() {
WARN_NOT_OK(env->DeleteFile(tmp_out_path),
"Could not delete file " + tmp_out_path);
});
// Also make a tmp file where we'll write the PBC in JSON format for
// easy editing. Encryption needs to be disabled for the tmp file.
unique_ptr<WritableFile> tmp_json_file;
string tmp_json_path;
WritableFileOptions tmp_json_opts;
tmp_json_opts.is_sensitive = false;
const string tmp_template = Substitute("pbc-edit$0.XXXXXX", kTmpInfix);
RETURN_NOT_OK_PREPEND(env->NewTempWritableFile(tmp_json_opts,
JoinPathSegments(dir, tmp_template),
&tmp_json_path, &tmp_json_file),
"couldn't create temporary file");
auto delete_tmp_json = MakeScopedCleanup([&]() {
WARN_NOT_OK(env->DeleteFile(tmp_json_path),
"Could not delete file " + tmp_json_path);
});
// Dump the contents in JSON to the temporary file.
{
// It is quite difficult to get a C++ ostream pointed at a temporary file,
// so we just dump to a string and then write it to a file.
std::ostringstream stream;
ReadablePBContainerFile::Format format =
FLAGS_json_pretty ? ReadablePBContainerFile::Format::JSON_PRETTY :
ReadablePBContainerFile::Format::JSON;
RETURN_NOT_OK(pb_reader.Dump(&stream, format));
RETURN_NOT_OK_PREPEND(tmp_json_file->Append(stream.str()), "couldn't write to temporary file");
RETURN_NOT_OK_PREPEND(tmp_json_file->Close(), "couldn't close temporary file");
}
// Open the temporary file in the editor for the user to edit, and load the content
// back into a list of lines.
RETURN_NOT_OK(RunEditor(tmp_json_path));
{
const google::protobuf::Message* prototype;
RETURN_NOT_OK_PREPEND(pb_reader.GetPrototype(&prototype),
"couldn't load message prototype from file");
pb_util::WritablePBContainerFile pb_writer(std::shared_ptr<RWFile>(out_rwfile.release()));
RETURN_NOT_OK_PREPEND(pb_writer.CreateNew(*prototype), "couldn't init PBC writer");
// Parse the edited file.
unique_ptr<google::protobuf::Message> m(prototype->New());
FILE* fp = nullptr;
POINTER_RETRY_ON_EINTR(fp, fopen(tmp_json_path.c_str(), "r"));
if (fp == nullptr) {
return Status::IOError(Substitute("open file ($0) failed", tmp_json_path));
}
SCOPED_CLEANUP({ fclose(fp); });
char read_buffer[65536];
rapidjson::FileReadStream in_stream(fp, read_buffer, sizeof(read_buffer));
JsonParseOptions opts;
opts.case_insensitive_enum_parsing = true;
do {
// The file may contains multiple JSON objects, parse one object once.
rapidjson::Document document;
document.ParseStream<rapidjson::kParseStopWhenDoneFlag>(in_stream);
if (document.HasParseError()) {
auto code = document.GetParseError();
if (code != rapidjson::kParseErrorDocumentEmpty) {
return Status::Corruption("JSON text is corrupt",
rapidjson::GetParseError_En(code));
}
// No more JSON object left.
break;
}
// Convert one JSON object to protobuf object once.
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
document.Accept(writer);
m->Clear();
auto str = buffer.GetString();
const auto& google_status = JsonStringToMessage(str, m.get(), opts);
if (!google_status.ok()) {
return Status::InvalidArgument(
Substitute("Unable to parse JSON text: $0", str),
google_status.error_message().ToString());
}
// Append the protobuf object to writer.
RETURN_NOT_OK_PREPEND(pb_writer.Append(*m), "unable to append PB to output");
} while (true);
RETURN_NOT_OK_PREPEND(pb_writer.Sync(), "failed to sync output");
RETURN_NOT_OK_PREPEND(pb_writer.Close(), "failed to close output");
}
// We successfully wrote the new file.
if (FLAGS_backup) {
// Move the old file to a backup location.
string backup_path = Substitute("$0.bak.$1", path, GetCurrentTimeMicros());
RETURN_NOT_OK_PREPEND(env->RenameFile(path, backup_path),
"couldn't back up original file");
LOG(INFO) << "Moved original file to " << backup_path;
}
// Move the new file to the final location.
RETURN_NOT_OK_PREPEND(env->RenameFile(tmp_out_path, path),
"couldn't move new file into place");
delete_tmp_output.cancel();
WARN_NOT_OK(env->SyncDir(dir), "couldn't sync directory");
return Status::OK();
}
} // anonymous namespace
unique_ptr<Mode> BuildPbcMode() {
unique_ptr<Action> dump =
ActionBuilder("dump", &DumpPBContainerFile)
.Description("Dump a PBC (protobuf container) file")
.AddRequiredParameter({kPathArg, "path to PBC file"})
.AddOptionalParameter("debug")
.AddOptionalParameter("oneline")
.AddOptionalParameter("json")
.AddOptionalParameter("json_pretty")
.Build();
unique_ptr<Action> edit =
ActionBuilder("edit", &EditFile)
.Description("Edit a PBC (protobuf container) file")
.AddRequiredParameter({kPathArg, "path to PBC file"})
.AddOptionalParameter("backup")
.AddOptionalParameter("json")
.AddOptionalParameter("json_pretty")
.Build();
return ModeBuilder("pbc")
.Description("Operate on PBC (protobuf container) files")
.AddAction(std::move(dump))
.AddAction(std::move(edit))
.Build();
}
} // namespace tools
} // namespace kudu