aboutsummaryrefslogtreecommitdiffstats
path: root/configutil
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /configutil
Publish
Diffstat (limited to 'configutil')
-rw-r--r--configutil/.gitignore2
-rw-r--r--configutil/CMakeLists.txt22
-rw-r--r--configutil/OWNERS1
-rw-r--r--configutil/src/.gitignore4
-rw-r--r--configutil/src/apps/configstatus/.gitignore4
-rw-r--r--configutil/src/apps/configstatus/CMakeLists.txt9
-rw-r--r--configutil/src/apps/configstatus/main.cpp104
-rw-r--r--configutil/src/apps/modelinspect/.gitignore4
-rw-r--r--configutil/src/apps/modelinspect/CMakeLists.txt9
-rw-r--r--configutil/src/apps/modelinspect/main.cpp131
-rw-r--r--configutil/src/lib/.gitignore2
-rw-r--r--configutil/src/lib/CMakeLists.txt9
-rw-r--r--configutil/src/lib/configstatus.cpp237
-rw-r--r--configutil/src/lib/configstatus.h31
-rw-r--r--configutil/src/lib/modelinspect.cpp366
-rw-r--r--configutil/src/lib/modelinspect.h52
-rw-r--r--configutil/src/lib/tags.cpp39
-rw-r--r--configutil/src/lib/tags.h12
-rw-r--r--configutil/src/testlist.txt3
-rw-r--r--configutil/src/tests/config_status/.gitignore4
-rw-r--r--configutil/src/tests/config_status/CMakeLists.txt8
-rw-r--r--configutil/src/tests/config_status/config_status_test.cpp109
-rw-r--r--configutil/src/tests/model_inspect/.gitignore4
-rw-r--r--configutil/src/tests/model_inspect/CMakeLists.txt8
-rw-r--r--configutil/src/tests/model_inspect/FILES1
-rw-r--r--configutil/src/tests/model_inspect/model.cfg159
-rw-r--r--configutil/src/tests/model_inspect/model_inspect_test.cpp267
-rw-r--r--configutil/src/tests/tags/.gitignore4
-rw-r--r--configutil/src/tests/tags/CMakeLists.txt8
-rw-r--r--configutil/src/tests/tags/FILES1
-rw-r--r--configutil/src/tests/tags/tags_test.cpp20
-rw-r--r--configutil/testrun/.gitignore21
32 files changed, 1655 insertions, 0 deletions
diff --git a/configutil/.gitignore b/configutil/.gitignore
new file mode 100644
index 00000000000..a9b20e8992d
--- /dev/null
+++ b/configutil/.gitignore
@@ -0,0 +1,2 @@
+Makefile
+Testing
diff --git a/configutil/CMakeLists.txt b/configutil/CMakeLists.txt
new file mode 100644
index 00000000000..081e74ac46f
--- /dev/null
+++ b/configutil/CMakeLists.txt
@@ -0,0 +1,22 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_define_module(
+ DEPENDS
+ vespadefaults
+ fastos
+ config_cloudconfig
+ vbench
+ vespalib
+ fastlib_fast
+
+ LIBS
+ src/lib
+
+ APPS
+ src/apps/configstatus
+ src/apps/modelinspect
+
+ TESTS
+ src/tests/config_status
+ src/tests/model_inspect
+ src/tests/tags
+ )
diff --git a/configutil/OWNERS b/configutil/OWNERS
new file mode 100644
index 00000000000..e0a00db5f4f
--- /dev/null
+++ b/configutil/OWNERS
@@ -0,0 +1 @@
+musum
diff --git a/configutil/src/.gitignore b/configutil/src/.gitignore
new file mode 100644
index 00000000000..f7e40c87b26
--- /dev/null
+++ b/configutil/src/.gitignore
@@ -0,0 +1,4 @@
+/Makefile.ini
+/config_command.sh
+/configutil.mak
+/project.dsw
diff --git a/configutil/src/apps/configstatus/.gitignore b/configutil/src/apps/configstatus/.gitignore
new file mode 100644
index 00000000000..5d04dfe43bd
--- /dev/null
+++ b/configutil/src/apps/configstatus/.gitignore
@@ -0,0 +1,4 @@
+/.depend
+/Makefile
+/vespa-config-status
+vespa-config-status-bin
diff --git a/configutil/src/apps/configstatus/CMakeLists.txt b/configutil/src/apps/configstatus/CMakeLists.txt
new file mode 100644
index 00000000000..58bea4e0d70
--- /dev/null
+++ b/configutil/src/apps/configstatus/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(configutil_vespa-config-status_app
+ SOURCES
+ main.cpp
+ OUTPUT_NAME vespa-config-status-bin
+ INSTALL bin
+ DEPENDS
+ configutil_util
+)
diff --git a/configutil/src/apps/configstatus/main.cpp b/configutil/src/apps/configstatus/main.cpp
new file mode 100644
index 00000000000..b3fb611ffa4
--- /dev/null
+++ b/configutil/src/apps/configstatus/main.cpp
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/defaults.h>
+#include <vespa/log/log.h>
+LOG_SETUP("vespa-config-status");
+#include <iostream>
+#include <lib/configstatus.h>
+
+class Application : public FastOS_Application
+{
+ ConfigStatus::Flags _flags;
+ vespalib::string _cfgId;
+ vespalib::string _specString;
+ int parseOpts();
+ vespalib::string getSources();
+public:
+ void usage(void);
+ int Main(void);
+
+ Application() : _flags(), _cfgId("admin/model"), _specString("") {}
+};
+
+int
+Application::parseOpts()
+{
+ char c = '?';
+ const char *optArg = NULL;
+ int optInd = 0;
+ while ((c = GetOpt("c:s:vC:", optArg, optInd)) != -1) {
+ switch (c) {
+ case 'v':
+ _flags.verbose = true;
+ break;
+ case 'C':
+ _cfgId = optArg;
+ break;
+ case 'c':
+ _specString = optArg;
+ break;
+ case 'h':
+ usage();
+ exit(0);
+ default:
+ usage();
+ exit(1);
+ }
+ }
+ if (_specString.empty()) {
+ _specString = getSources();
+ }
+ return optInd;
+}
+
+
+void
+Application::usage(void)
+{
+ std::cerr <<
+ "vespa-config-status version 1.0" << std::endl <<
+ "Usage: " << _argv[0] << " [options] " << std::endl <<
+ "options: [-v] for verbose" << std::endl <<
+ " [-c host] or [-c host:port] to specify config server" << std::endl <<
+ std::endl;
+}
+
+int
+Application::Main(void)
+{
+ parseOpts();
+
+ config::ServerSpec spec(_specString);
+ config::ConfigUri uri = config::ConfigUri::createFromSpec(_cfgId, spec);
+ ConfigStatus status(_flags, uri);
+
+ return status.action();
+}
+
+vespalib::string
+Application::getSources(void)
+{
+ vespalib::string cmd = vespa::Defaults::vespaHome();
+ cmd.append("libexec/vespa/vespa-config.pl -configsources");
+ FILE* fp = popen(cmd.c_str(), "r");
+ if (fp == 0) {
+ std::cerr << "Failed to run " << cmd << " ("
+ << errno << "): " << strerror(errno) << "\n";
+ return "";
+ }
+ vespalib::asciistream specs;
+ char data[500];
+ while (fgets(data, 500, fp) != 0) {
+ specs << &data[0] << "\n";
+ }
+ pclose(fp);
+ return specs.str();
+}
+
+int
+main(int argc, char **argv)
+{
+ Application app;
+ return app.Entry(argc, argv);
+}
diff --git a/configutil/src/apps/modelinspect/.gitignore b/configutil/src/apps/modelinspect/.gitignore
new file mode 100644
index 00000000000..89d2d6112b6
--- /dev/null
+++ b/configutil/src/apps/modelinspect/.gitignore
@@ -0,0 +1,4 @@
+/.depend
+/Makefile
+/vespa-model-inspect
+vespa-model-inspect-bin
diff --git a/configutil/src/apps/modelinspect/CMakeLists.txt b/configutil/src/apps/modelinspect/CMakeLists.txt
new file mode 100644
index 00000000000..be65aa67701
--- /dev/null
+++ b/configutil/src/apps/modelinspect/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(configutil_vespa-model-inspect_app
+ SOURCES
+ main.cpp
+ OUTPUT_NAME vespa-model-inspect-bin
+ INSTALL bin
+ DEPENDS
+ configutil_util
+)
diff --git a/configutil/src/apps/modelinspect/main.cpp b/configutil/src/apps/modelinspect/main.cpp
new file mode 100644
index 00000000000..f5a1e28e218
--- /dev/null
+++ b/configutil/src/apps/modelinspect/main.cpp
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/defaults.h>
+#include <vespa/log/log.h>
+LOG_SETUP("vespa-model-inspect");
+#include <iostream>
+#include <lib/modelinspect.h>
+#include <vespa/vespalib/text/stringtokenizer.h>
+#include <vespa/vespalib/stllike/asciistream.h>
+
+class Application : public FastOS_Application
+{
+ ModelInspect::Flags _flags;
+ vespalib::string _cfgId;
+ vespalib::string _specString;
+ int parseOpts();
+ vespalib::string getSources();
+public:
+ void usage(void);
+ int Main(void);
+
+ Application() : _flags(), _cfgId("admin/model"), _specString("") {}
+};
+
+int
+Application::parseOpts()
+{
+ char c = '?';
+ const char *optArg = NULL;
+ int optInd = 0;
+ while ((c = GetOpt("hvut:c:C:", optArg, optInd)) != -1) {
+ switch (c) {
+ case 'v':
+ _flags.verbose = true;
+ break;
+ case 'u':
+ _flags.makeuri = true;
+ break;
+ case 't':
+ _flags.tagFilter.push_back(optArg);
+ _flags.tagfilt = true;
+ break;
+ case 'C':
+ _cfgId = optArg;
+ break;
+ case 'c':
+ _specString = optArg;
+ break;
+ case 'h':
+ return _argc;
+ default:
+ usage();
+ exit(1);
+ }
+ }
+ if (_specString.empty()) {
+ _specString = getSources();
+ }
+ return optInd;
+}
+
+vespalib::string
+Application::getSources(void)
+{
+ vespalib::string cmd = vespa::Defaults::vespaHome();
+ cmd.append("libexec/vespa/vespa-config.pl -configsources");
+ FILE* fp = popen(cmd.c_str(), "r");
+ if (fp == 0) {
+ std::cerr << "Failed to run " << cmd << " ("
+ << errno << "): " << strerror(errno) << "\n";
+ return "";
+ }
+ vespalib::asciistream specs;
+ char data[500];
+ while (fgets(data, 500, fp) != 0) {
+ specs << &data[0] << "\n";
+ }
+ pclose(fp);
+ return specs.str();
+}
+
+void
+Application::usage(void)
+{
+ std::cerr <<
+ "vespa-model-inspect version 2.0" << std::endl <<
+ "Usage: " << _argv[0] << " [options] <command> <options>" << std::endl <<
+ "options: [-u] for URLs, [-v] for verbose" << std::endl <<
+ " [-c host] or [-c host:port] to specify server" << std::endl <<
+ " [-t tag] to filter on a port tag" << std::endl <<
+ "Where command is:" << std::endl <<
+ " hosts - show all hosts" << std::endl <<
+ " services - show all services" << std::endl <<
+ " clusters - show all cluster names" << std::endl <<
+ " configids - show all config IDs" << std::endl <<
+ " filter:ports - list ports matching filter options" << std::endl <<
+ " host <hostname> - show services on a given host" << std::endl <<
+ " service [cluster:]<servicetype>" <<
+ " - show all instances of a given servicetype" << std::endl <<
+ " cluster <clustername>" <<
+ " - show all services associated with the cluster" << std::endl <<
+ " configid <configid>" <<
+ " - show service using configid" << std::endl <<
+ " get-index-of <servicetype> <host>" <<
+ " - show all indexes for instances of the servicetype on the host" << std::endl <<
+ std::endl;
+}
+
+int
+Application::Main(void)
+{
+ int cnt = parseOpts();
+ if (_argc == cnt) {
+ usage();
+ return 0;
+ }
+
+ config::ServerSpec spec(_specString);
+ config::ConfigUri uri = config::ConfigUri::createFromSpec(_cfgId, spec);
+ ModelInspect model(_flags, uri, std::cout);
+ return model.action(_argc - cnt, &_argv[cnt]);
+}
+
+int
+main(int argc, char** argv)
+{
+ vespa::Defaults::bootstrap(argv[0]);
+ Application app;
+ return app.Entry(argc, argv);
+}
diff --git a/configutil/src/lib/.gitignore b/configutil/src/lib/.gitignore
new file mode 100644
index 00000000000..7e7c0fe7fae
--- /dev/null
+++ b/configutil/src/lib/.gitignore
@@ -0,0 +1,2 @@
+/.depend
+/Makefile
diff --git a/configutil/src/lib/CMakeLists.txt b/configutil/src/lib/CMakeLists.txt
new file mode 100644
index 00000000000..290957bf9f3
--- /dev/null
+++ b/configutil/src/lib/CMakeLists.txt
@@ -0,0 +1,9 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_library(configutil_util STATIC
+ SOURCES
+ tags.cpp
+ modelinspect.cpp
+ configstatus.cpp
+ DEPENDS
+ configdefinitions
+)
diff --git a/configutil/src/lib/configstatus.cpp b/configutil/src/lib/configstatus.cpp
new file mode 100644
index 00000000000..f889c436a97
--- /dev/null
+++ b/configutil/src/lib/configstatus.cpp
@@ -0,0 +1,237 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <vespa/log/log.h>
+LOG_SETUP("vespa-config-status");
+#include <iostream>
+
+#include <vespa/fnet/frt/frt.h>
+#include <vespa/vespalib/data/slime/slime.h>
+#include <vbench/http/http_result_handler.h>
+#include <vbench/http/server_spec.h>
+#include <vbench/http/http_client.h>
+
+#include <lib/tags.h>
+
+#include "configstatus.h"
+
+using configdefinitions::tagsContain;
+
+struct ComponentTraverser : public vespalib::slime::ObjectTraverser
+{
+ const std::string _configId;
+ std::string _component;
+ enum {
+ ROOT,
+ COMPONENT
+ } _state;
+ std::map<std::string, int64_t> &_generations;
+
+ ComponentTraverser(std::string configId,
+ std::map<std::string, int64_t> &generations)
+ : _configId(configId), _state(ROOT), _generations(generations)
+ {}
+
+ void object(const vespalib::slime::Inspector &inspector) {
+ inspector.traverse(*this);
+ }
+
+ static void collect(const std::string configId, const vespalib::Slime &slime,
+ std::map<std::string, int64_t> &generations) {
+ ComponentTraverser traverser(configId, generations);
+ slime.get()["config"].traverse(traverser);
+ }
+
+ virtual void
+ field(const vespalib::slime::Memory &symbol_name, const vespalib::slime::Inspector &inspector) {
+ switch (_state) {
+ case ROOT:
+ _component = symbol_name.make_string();
+ _state = COMPONENT;
+ inspector.traverse(*this);
+ _state = ROOT;
+ break;
+ case COMPONENT:
+ const std::string key = symbol_name.make_string();
+ int64_t value;
+ if (key == "generation") {
+ if (inspector.type().getId() == vespalib::slime::DOUBLE::ID) {
+ value = (int64_t) inspector.asDouble();
+ _generations[_component] = value;
+ } else if (inspector.type().getId() == vespalib::slime::LONG::ID) {
+ value = inspector.asLong();
+ _generations[_component] = value;
+ } else {
+ value = 0;
+ std::cerr << _configId << ":" << _component <<
+ "Generation has wrong type" << std::endl;
+ }
+ }
+
+ break;
+ }
+ }
+};
+
+class MyHttpHandler : public vbench::HttpResultHandler {
+private:
+ std::string _json;
+ std::string _error;
+ std::string _configId;
+
+public:
+
+ MyHttpHandler(std::string configId)
+ : _json(), _error(), _configId(configId)
+ {}
+
+ virtual void handleHeader(const vbench::string &name, const vbench::string &value) {
+ (void) name;
+ (void) value;
+ }
+
+ virtual void handleContent(const vbench::Memory &data) {
+ _json += std::string(data.data, data.size);
+ }
+
+ virtual void handleFailure(const vbench::string &reason) {
+ std::cerr << _configId << ": Failed to fetch json: " << reason << std::endl;
+ _error = reason;
+ }
+
+ bool failed() {
+ return(_error.size() > 0);
+ }
+
+ std::string getJson() {
+ return _json;
+ }
+};
+
+ConfigStatus::ConfigStatus(Flags flags, const config::ConfigUri uri)
+ : _cfg(), _flags(flags), _generation(0)
+{
+ if (_flags.verbose) {
+ std::cerr << "Subscribing to model config with config id " <<
+ uri.getConfigId() << std::endl;
+ }
+ try {
+ config::ConfigSubscriber subscriber(uri.getContext());
+ config::ConfigHandle<cloud::config::ModelConfig>::UP handle =
+ subscriber.subscribe<cloud::config::ModelConfig>(uri.getConfigId());
+ subscriber.nextConfig(0);
+ _cfg = handle->getConfig();
+ _generation = subscriber.getGeneration();
+ } catch(config::ConfigRuntimeException &e) {
+ std::cerr << e.getMessage() << std::endl;
+ }
+
+ if (_cfg.get() == NULL) {
+ std::cerr << "FATAL ERROR: failed to get model configuration." << std::endl;
+ exit(1);
+ }
+}
+
+int
+ConfigStatus::action()
+{
+ bool allUpToDate = true;
+
+ for (size_t i = 0; i < _cfg->hosts.size(); i++) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+
+ for (size_t j = 0; j < hconf.services.size(); j++) {
+ const cloud::config::ModelConfig::Hosts::Services &svc = hconf.services[j];
+
+ if (svc.type == "configserver") {
+ continue;
+ }
+
+ for (size_t k = 0; k < svc.ports.size(); k++) {
+ std::string tags = svc.ports[k].tags;
+ if (tagsContain(tags, "http") &&
+ tagsContain(tags, "state")) {
+ bool upToDate = checkServiceGeneration(svc.configid, hconf.name,
+ svc.ports[k].number,
+ "/state/v1/config");
+
+ if (!upToDate) {
+ if (svc.type == "searchnode" ||
+ svc.type == "filedistributorservice" ||
+ svc.type == "topleveldispatch")
+ {
+ std::cerr << "[generation not up-to-date ignored]" << std::endl;
+ } else {
+ allUpToDate = false;
+ }
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ return allUpToDate ? 0 : 1;
+}
+
+bool
+ConfigStatus::fetch_json(std::string configId, std::string host, int port,
+ std::string path, std::string &data)
+{
+ MyHttpHandler myHandler(configId);
+ bool ok = vbench::HttpClient::fetch(vbench::ServerSpec(host, port), path, myHandler);
+
+ if (ok) {
+ data = myHandler.getJson();
+ return true;
+ } else {
+ return false;
+ }
+}
+
+bool
+ConfigStatus::checkServiceGeneration(std::string configId, std::string host, int port, std::string path)
+{
+ std::string data;
+ vespalib::Slime slime;
+
+ if (!fetch_json(configId, host, port, path, data)) {
+ return false;
+ }
+
+ size_t size = vespalib::slime::JsonFormat::decode(data, slime);
+
+ if (size == 0) {
+ std::cerr << configId << ": JSON parsing failed" << std::endl;
+ return false;
+ }
+
+ vespalib::slime::SimpleBuffer buf;
+ vespalib::slime::JsonFormat::encode(slime, buf, false);
+
+ if (slime.get()["config"].valid()) {
+ std::map<std::string, int64_t> generations;
+ ComponentTraverser::collect(configId, slime, generations);
+ bool upToDate = true;
+
+ std::map<std::string, int64_t>::iterator iter;
+ for (iter = generations.begin(); iter != generations.end(); iter++) {
+ if (iter->second != _generation) {
+ std::cout << configId << ":" << iter->first << " has generation " <<
+ iter->second << " not " << _generation << std::endl;
+ upToDate = false;
+ } else {
+ if (_flags.verbose) {
+ std::cout << configId << ":" << iter->first <<
+ " has the latest generation " << iter->second << std::endl;
+ }
+ }
+ }
+
+ return upToDate;
+ } else {
+ std::cerr << configId << ": No valid config object" << std::endl;
+
+ return false;
+ }
+}
diff --git a/configutil/src/lib/configstatus.h b/configutil/src/lib/configstatus.h
new file mode 100644
index 00000000000..41e3d2fe782
--- /dev/null
+++ b/configutil/src/lib/configstatus.h
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/config-model.h>
+#include <vespa/vespalib/stllike/string.h>
+#include <vespa/config/config.h>
+
+class ConfigStatus
+{
+public:
+ struct Flags {
+ bool verbose;
+ Flags()
+ : verbose(false)
+ {}
+ };
+
+ ConfigStatus(Flags flags, const config::ConfigUri uri);
+ int action();
+
+private:
+ std::unique_ptr<cloud::config::ModelConfig> _cfg;
+ Flags _flags;
+ int64_t _generation;
+
+ bool fetch_json(std::string configId, std::string host, int port, std::string path,
+ std::string &data);
+ bool checkServiceGeneration(std::string configId, std::string host, int port,
+ std::string path);
+};
+
diff --git a/configutil/src/lib/modelinspect.cpp b/configutil/src/lib/modelinspect.cpp
new file mode 100644
index 00000000000..69fd2b804df
--- /dev/null
+++ b/configutil/src/lib/modelinspect.cpp
@@ -0,0 +1,366 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+#include <ctype.h>
+#include <vespa/log/log.h>
+LOG_SETUP("vespa-model-inspect");
+#include <iostream>
+#include "modelinspect.h"
+#include <set>
+#include <lib/tags.h>
+
+using configdefinitions::tagsContain;
+using configdefinitions::upcase;
+
+ModelInspect::ModelInspect(Flags flags, const config::ConfigUri uri, std::ostream &out)
+ : _cfg(), _flags(flags), _out(out)
+{
+ if (_flags.verbose) {
+ std::cerr << "subscribing to model config with configid " << uri.getConfigId() << "\n";
+ }
+
+ try {
+ _cfg = config::ConfigGetter<cloud::config::ModelConfig>::getConfig(uri.getConfigId(),
+ uri.getContext());
+ } catch(config::ConfigRuntimeException &e) {
+ std::cerr << e.getMessage() << "\n";
+ }
+ if (_cfg.get() != NULL) {
+ if (_flags.verbose) std::cerr << "success!\n";
+ } else {
+ std::cerr << "FATAL ERROR: failed to get model configuration.\n";
+ exit(1);
+ }
+}
+
+ModelInspect::~ModelInspect()
+{
+}
+
+void
+ModelInspect::printPort(const vespalib::string &host, int port,
+ const vespalib::string &tags)
+{
+ if (_flags.tagfilt) {
+ for (size_t i = 0; i < _flags.tagFilter.size(); ++i) {
+ if (! tagsContain(tags, _flags.tagFilter[i])) {
+ return;
+ }
+ }
+ }
+ if (_flags.makeuri && tagsContain(tags, "HTTP")) {
+ _out << " http://" << host << ":" << port << "/";
+ } else {
+ _out << " tcp/" << host << ":" << port;
+ }
+ if (_flags.tagfilt) {
+ _out << "\n";
+ } else {
+ vespalib::string upper = upcase(tags);
+ _out << " (" << upper << ")\n";
+ }
+}
+
+void
+ModelInspect::printService(const cloud::config::ModelConfig::Hosts::Services &svc,
+ const vespalib::string &host)
+{
+ if (!_flags.tagfilt) {
+ _out << svc.name << " @ " << host << " : " << svc.clustertype << std::endl;
+ _out << svc.configid << std::endl;
+ }
+ for (size_t i = 0; i < svc.ports.size(); ++i) {
+ printPort(host, svc.ports[i].number, svc.ports[i].tags);
+ }
+}
+
+int
+ModelInspect::action(int cnt, char **argv)
+{
+ const vespalib::string cmd = *argv++;
+ if (cnt == 1) {
+ if (cmd == "yamldump") {
+ yamlDump();
+ return 0;
+ }
+ if (cmd == "hosts") {
+ listHosts();
+ return 0;
+ }
+ if (cmd == "services") {
+ listServices();
+ return 0;
+ }
+ if (cmd == "clusters") {
+ listClusters();
+ return 0;
+ }
+ if (cmd == "configids") {
+ listConfigIds();
+ return 0;
+ }
+ if (cmd == "filter:hosts") {
+ if (!_flags.tagfilt) {
+ std::cerr << "filter needs some filter options" << std::endl;
+ return 1;
+ }
+ std::cerr << "not implemented" << std::endl;
+ return 1;
+ }
+ if (cmd == "filter:ports") {
+ if (!_flags.tagfilt) {
+ std::cerr << "filter needs some filter options" << std::endl;
+ return 1;
+ }
+ return listAllPorts();
+ }
+ }
+ if (cnt == 2) {
+ vespalib::string arg = *argv++;
+ if (cmd == "host") {
+ return listHost(arg);
+ }
+ if (cmd == "cluster") {
+ return listCluster(arg);
+ }
+ if (cmd == "service") {
+ size_t colon = arg.find(':');
+ if (colon != vespalib::string::npos) {
+ return listService(arg.substr(0, colon),
+ arg.substr(colon + 1));
+ } else {
+ return listService(arg);
+ }
+ }
+ if (cmd == "configid") {
+ return listConfigId(arg);
+ }
+ }
+ if (cnt == 3) {
+ vespalib::string arg1 = *argv++;
+ vespalib::string arg2 = *argv++;
+ if (cmd == "get-index-of") {
+ return getIndexOf(arg1, arg2);
+ }
+ };
+ std::cerr << "bad args '" << cmd << "' (got " << cnt << " arguments)" << std::endl;
+ return 1;
+}
+
+void
+ModelInspect::dumpService(const cloud::config::ModelConfig::Hosts::Services &svc,
+ const vespalib::string &host)
+{
+ _out << "- servicename: " << svc.name << "\n";
+ _out << " servicetype: " << svc.type << "\n";
+ _out << " clustertype: " << svc.clustertype << "\n";
+ _out << " clustername: " << svc.clustername << "\n";
+ _out << " index: " << svc.index << "\n";
+ _out << " hostname: " << host << "\n";
+ _out << " config-id: " << svc.configid << "\n";
+
+ if (svc.ports.size() > 0) {
+ _out << " ports: \n";
+ for (size_t i = 0; i < svc.ports.size(); ++i) {
+ _out << " - " << svc.ports[i].number << "\n";
+ }
+ }
+}
+
+void
+ModelInspect::yamlDump()
+{
+ _out << "--- \n";
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ dumpService(hconf.services[j], hconf.name);
+ }
+ }
+}
+
+void
+ModelInspect::listHosts()
+{
+ std::vector<vespalib::string> hosts;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ hosts.push_back(hconf.name);
+ }
+ std::sort(hosts.begin(), hosts.end());
+ for (size_t i = 0; i < hosts.size(); ++i) {
+ _out << hosts[i] << std::endl;
+ }
+}
+
+void
+ModelInspect::listServices()
+{
+ typedef std::set<vespalib::string> Set;
+ Set services;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ services.insert(hconf.services[j].type);
+ }
+ }
+ for (Set::const_iterator it = services.begin(); it != services.end(); ++it) {
+ _out << (*it) << std::endl;
+ }
+}
+
+void
+ModelInspect::listClusters()
+{
+ typedef std::set<vespalib::string> Set;
+ Set clusters;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ clusters.insert(hconf.services[j].clustername);
+ }
+ }
+ for (Set::const_iterator it = clusters.begin(); it != clusters.end(); ++it) {
+ _out << (*it) << std::endl;
+ }
+}
+
+void
+ModelInspect::listConfigIds()
+{
+ std::vector<vespalib::string> configids;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ configids.push_back(hconf.services[j].configid);
+ }
+ }
+ std::sort(configids.begin(), configids.end());
+ for (size_t i = 0; i < configids.size(); ++i) {
+ _out << configids[i] << std::endl;
+ }
+}
+
+int
+ModelInspect::listHost(const vespalib::string host)
+{
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ if (host == hconf.name) {
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ printService(hconf.services[j], host);
+ }
+ return 0;
+ }
+ }
+ std::cerr << "no config found for host '" << host << "'\n";
+ return 1;
+}
+
+int
+ModelInspect::listCluster(const vespalib::string cluster)
+{
+ bool found = false;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ if (cluster == hconf.services[j].clustername) {
+ found = true;
+ printService(hconf.services[j], hconf.name);
+ }
+ }
+ }
+ if (found) return 0;
+ std::cerr << "no config found for cluster '" << cluster << "'\n";
+ return 1;
+}
+
+int
+ModelInspect::listAllPorts()
+{
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ printService(hconf.services[j], hconf.name);
+ }
+ }
+ return 0;
+}
+
+int
+ModelInspect::listService(const vespalib::string svctype)
+{
+ bool found = false;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ if (svctype == hconf.services[j].type) {
+ found = true;
+ printService(hconf.services[j], hconf.name);
+ }
+ }
+ }
+ if (found) return 0;
+ std::cerr << "no services found with type '" << svctype << "'\n";
+ return 1;
+}
+
+
+int
+ModelInspect::listService(const vespalib::string cluster,
+ const vespalib::string svctype)
+{
+ bool found = false;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ if (cluster == hconf.services[j].clustername &&
+ svctype == hconf.services[j].type)
+ {
+ found = true;
+ printService(hconf.services[j], hconf.name);
+ }
+ }
+ }
+ if (found) return 0;
+ std::cerr << "no services found with type '" << svctype << "' in cluster '" << cluster << "'\n";
+ return 1;
+}
+
+int
+ModelInspect::listConfigId(const vespalib::string configid)
+{
+ bool found = false;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ if (configid == hconf.services[j].configid) {
+ found = true;
+ printService(hconf.services[j], hconf.name);
+ }
+ }
+ }
+ if (found) return 0;
+ std::cerr << "no services found with configid '" << configid << "'\n";
+ return 1;
+}
+
+int
+ModelInspect::getIndexOf(const vespalib::string service, const vespalib::string host)
+{
+ bool found = false;
+ for (size_t i = 0; i < _cfg->hosts.size(); ++i) {
+ const cloud::config::ModelConfig::Hosts &hconf = _cfg->hosts[i];
+ if (host == hconf.name) {
+ for (size_t j = 0; j < hconf.services.size(); ++j) {
+ if (service == hconf.services[j].type) {
+ found = true;
+ _out << hconf.services[j].index << std::endl;
+ }
+ }
+ }
+ }
+ if (found) return 0;
+ std::cerr << "no service of type '" << service << "' found for host '" << host << "'\n";
+ return 1;
+}
diff --git a/configutil/src/lib/modelinspect.h b/configutil/src/lib/modelinspect.h
new file mode 100644
index 00000000000..b50ea04e197
--- /dev/null
+++ b/configutil/src/lib/modelinspect.h
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/config-model.h>
+#include <vespa/vespalib/stllike/string.h>
+#include <vespa/config/config.h>
+
+class ModelInspect
+
+{
+public:
+ struct Flags {
+ bool verbose;
+ bool makeuri;
+ bool tagfilt;
+ std::vector<vespalib::string> tagFilter;
+ Flags()
+ : verbose(false), makeuri(false), tagfilt(false),
+ tagFilter()
+ {}
+ };
+
+ ModelInspect(Flags flags, const config::ConfigUri uri, std::ostream &out);
+ virtual ~ModelInspect();
+
+ int action(int cnt, char *argv[]);
+
+ virtual void yamlDump();
+ virtual void listHosts();
+ virtual void listServices();
+ virtual void listClusters();
+ virtual void listConfigIds();
+ virtual int listHost(const vespalib::string host);
+ virtual int listCluster(const vespalib::string cluster);
+ virtual int listAllPorts();
+ virtual int listService(const vespalib::string svctype);
+ virtual int listService(const vespalib::string cluster,
+ const vespalib::string svctype);
+ virtual int listConfigId(const vespalib::string configid);
+ virtual int getIndexOf(const vespalib::string service, const vespalib::string host);
+
+private:
+ std::unique_ptr<cloud::config::ModelConfig> _cfg;
+ Flags _flags;
+ std::ostream &_out;
+
+ void printService(const cloud::config::ModelConfig::Hosts::Services &svc,
+ const vespalib::string &host);
+ void printPort(const vespalib::string &host, int port,
+ const vespalib::string &tags);
+ void dumpService(const cloud::config::ModelConfig::Hosts::Services &svc,
+ const vespalib::string &host);
+
+};
diff --git a/configutil/src/lib/tags.cpp b/configutil/src/lib/tags.cpp
new file mode 100644
index 00000000000..50b01c80ba2
--- /dev/null
+++ b/configutil/src/lib/tags.cpp
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+
+#include <vespa/fastos/fastos.h>
+
+#include <vespa/vespalib/stllike/string.h>
+
+#include "tags.h"
+
+namespace configdefinitions {
+
+vespalib::string upcase(const vespalib::string &orig)
+{
+ vespalib::string upper(orig);
+ for (size_t i = 0; i < orig.size(); ++i) {
+ int l = (unsigned char)orig[i];
+ upper[i] = (unsigned char)toupper(l);
+ }
+ return upper;
+}
+
+bool tagsContain(const vespalib::string &tags, const vespalib::string &tag)
+{
+ vespalib::string allupper = upcase(tags);
+ vespalib::string tagupper = upcase(tag);
+
+ for (;;) {
+ size_t pos = allupper.rfind(' ');
+ if (pos == vespalib::string::npos) {
+ break;
+ }
+ if (allupper.substr(pos+1) == tagupper) {
+ return true;
+ }
+ allupper.resize(pos);
+ }
+ return (allupper == tagupper);
+}
+
+}
diff --git a/configutil/src/lib/tags.h b/configutil/src/lib/tags.h
new file mode 100644
index 00000000000..11764fe7af1
--- /dev/null
+++ b/configutil/src/lib/tags.h
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#pragma once
+
+#include <vespa/vespalib/stllike/string.h>
+
+namespace configdefinitions {
+
+vespalib::string upcase(const vespalib::string &orig);
+bool tagsContain(const vespalib::string &tags, const vespalib::string &tag);
+
+}
+
diff --git a/configutil/src/testlist.txt b/configutil/src/testlist.txt
new file mode 100644
index 00000000000..79108b8c618
--- /dev/null
+++ b/configutil/src/testlist.txt
@@ -0,0 +1,3 @@
+tests/config_status
+tests/model_inspect
+tests/tags
diff --git a/configutil/src/tests/config_status/.gitignore b/configutil/src/tests/config_status/.gitignore
new file mode 100644
index 00000000000..921ed89e386
--- /dev/null
+++ b/configutil/src/tests/config_status/.gitignore
@@ -0,0 +1,4 @@
+/.depend
+/Makefile
+/config_status_test
+configutil_config_status_test_app
diff --git a/configutil/src/tests/config_status/CMakeLists.txt b/configutil/src/tests/config_status/CMakeLists.txt
new file mode 100644
index 00000000000..2eda052bfd8
--- /dev/null
+++ b/configutil/src/tests/config_status/CMakeLists.txt
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(configutil_config_status_test_app
+ SOURCES
+ config_status_test.cpp
+ DEPENDS
+ configutil_util
+)
+vespa_add_test(NAME configutil_config_status_test_app COMMAND configutil_config_status_test_app)
diff --git a/configutil/src/tests/config_status/config_status_test.cpp b/configutil/src/tests/config_status/config_status_test.cpp
new file mode 100644
index 00000000000..af3f343a0b7
--- /dev/null
+++ b/configutil/src/tests/config_status/config_status_test.cpp
@@ -0,0 +1,109 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/vespalib/testkit/testapp.h>
+#include <lib/configstatus.h>
+#include <iostream>
+#include <vespa/fastlib/net/httpserver.h>
+#include <vespa/config-model.h>
+#include <vespa/config/config.h>
+#include <vespa/config/subscription/sourcespec.h>
+#include <vespa/vespalib/stllike/string.h>
+
+using namespace config;
+
+class HTTPStatus : private Fast_HTTPServer {
+private:
+ std::string _reply;
+ bool _fail;
+
+ virtual void onGetRequest(const string &, const string &,
+ Fast_HTTPConnection &conn) {
+ if (_fail) {
+ conn.Output(conn.GetHTTPVersion().c_str());
+ conn.Output(" 500 Error\r\n");
+ conn.Output("Connection: close\r\n");
+ conn.Output("\r\n");
+ } else {
+ conn.Output(conn.GetHTTPVersion().c_str());
+ conn.Output(" 200 OK\r\n");
+ conn.Output("Content-Type: application/json\r\n\r\n");
+ conn.Output(_reply.c_str());
+ }
+ };
+
+
+public:
+ HTTPStatus(std::string reply)
+ : Fast_HTTPServer(0), _reply(reply), _fail(false)
+ {
+ Start();
+ };
+ HTTPStatus(bool fail)
+ : Fast_HTTPServer(0), _reply(""), _fail(fail)
+ {
+ Start();
+ };
+
+ int getListenPort() { return Fast_HTTPServer::getListenPort(); }
+
+ ~HTTPStatus() {
+ Stop();
+ };
+};
+
+class Status {
+public:
+ ConfigStatus::Flags flags;
+ std::unique_ptr<ConfigStatus> status;
+
+ Status(int httpport) : flags() {
+ flags.verbose = true;
+ ConfigSet set;
+ ConfigContext::SP ctx(new ConfigContext(set));
+ cloud::config::ModelConfigBuilder builder;
+
+ cloud::config::ModelConfigBuilder::Hosts::Services::Ports port;
+ port.number = httpport;
+ port.tags = "http state";
+
+ cloud::config::ModelConfigBuilder::Hosts::Services service;
+ service.name = "qrserver";
+ service.type = "qrserver";
+ service.configid = "qrserver/cluster.default";
+ service.clustertype = "qrserver";
+ service.clustername = "default";
+ service.ports.push_back(port);
+
+ cloud::config::ModelConfigBuilder::Hosts host;
+ host.services.push_back(service);
+ host.name = "localhost";
+
+ builder.hosts.push_back(host);
+
+ set.addBuilder("admin/model", &builder);
+ config::ConfigUri uri("admin/model", ctx);
+ std::unique_ptr<ConfigStatus> s(new ConfigStatus(flags, uri));
+ status = std::move(s);
+ };
+
+ ~Status() {
+ };
+};
+
+TEST_FF("all ok", HTTPStatus(std::string("{\"config\": { \"all\": { \"generation\": 1 } }}")), Status(f1.getListenPort())) {
+ ASSERT_EQUAL(0, f2.status->action());
+}
+
+TEST_FF("generation too old", HTTPStatus(std::string("{\"config\": { \"all\": { \"generation\": 0 } }}")), Status(f1.getListenPort())) {
+ ASSERT_EQUAL(1, f2.status->action());
+}
+
+TEST_FF("bad json", HTTPStatus(std::string("{")), Status(f1.getListenPort())) {
+ ASSERT_EQUAL(1, f2.status->action());
+}
+
+TEST_FF("http failure", HTTPStatus(true), Status(f1.getListenPort())) {
+ ASSERT_EQUAL(1, f2.status->action());
+}
+
+TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/configutil/src/tests/model_inspect/.gitignore b/configutil/src/tests/model_inspect/.gitignore
new file mode 100644
index 00000000000..083a3fd3de8
--- /dev/null
+++ b/configutil/src/tests/model_inspect/.gitignore
@@ -0,0 +1,4 @@
+/.depend
+/Makefile
+/model_inspect_test
+configutil_model_inspect_test_app
diff --git a/configutil/src/tests/model_inspect/CMakeLists.txt b/configutil/src/tests/model_inspect/CMakeLists.txt
new file mode 100644
index 00000000000..a2890b5ae23
--- /dev/null
+++ b/configutil/src/tests/model_inspect/CMakeLists.txt
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(configutil_model_inspect_test_app
+ SOURCES
+ model_inspect_test.cpp
+ DEPENDS
+ configutil_util
+)
+vespa_add_test(NAME configutil_model_inspect_test_app COMMAND configutil_model_inspect_test_app)
diff --git a/configutil/src/tests/model_inspect/FILES b/configutil/src/tests/model_inspect/FILES
new file mode 100644
index 00000000000..fb9a8362cdd
--- /dev/null
+++ b/configutil/src/tests/model_inspect/FILES
@@ -0,0 +1 @@
+tags_test.cpp
diff --git a/configutil/src/tests/model_inspect/model.cfg b/configutil/src/tests/model_inspect/model.cfg
new file mode 100644
index 00000000000..76377e75825
--- /dev/null
+++ b/configutil/src/tests/model_inspect/model.cfg
@@ -0,0 +1,159 @@
+hosts[0].name "example.yahoo.com"
+hosts[0].services[0].name "logd"
+hosts[0].services[0].type "logd"
+hosts[0].services[0].configid "hosts/example.yahoo.com/logd"
+hosts[0].services[0].clustertype "hosts"
+hosts[0].services[0].clustername "admin"
+hosts[0].services[0].index 999999
+hosts[0].services[2].name "configproxy"
+hosts[0].services[2].type "configproxy"
+hosts[0].services[2].configid "hosts/example.yahoo.com/configproxy"
+hosts[0].services[2].clustertype "hosts"
+hosts[0].services[2].clustername "admin"
+hosts[0].services[2].index 999999
+hosts[0].services[2].ports[0].number 19090
+hosts[0].services[2].ports[0].tags "admin rpc status client rpc"
+hosts[0].services[3].name "config-sentinel"
+hosts[0].services[3].type "config-sentinel"
+hosts[0].services[3].configid "hosts/example.yahoo.com/sentinel"
+hosts[0].services[3].clustertype "hosts"
+hosts[0].services[3].clustername "admin"
+hosts[0].services[3].index 999999
+hosts[0].services[3].ports[0].number 19097
+hosts[0].services[3].ports[0].tags "notyet rpc"
+hosts[0].services[3].ports[1].number 19098
+hosts[0].services[3].ports[1].tags "interactive telnet"
+hosts[0].services[4].name "logserver"
+hosts[0].services[4].type "logserver"
+hosts[0].services[4].configid "admin/logserver"
+hosts[0].services[4].clustertype "admin"
+hosts[0].services[4].clustername "admin"
+hosts[0].services[4].index 999999
+hosts[0].services[4].ports[0].number 19080
+hosts[0].services[4].ports[0].tags "unused"
+hosts[0].services[4].ports[1].number 19081
+hosts[0].services[4].ports[1].tags "logtp"
+hosts[0].services[4].ports[2].number 19082
+hosts[0].services[4].ports[2].tags "unused"
+hosts[0].services[4].ports[3].number 19083
+hosts[0].services[4].ports[3].tags "telnet logtp"
+hosts[0].services[5].name "configserver"
+hosts[0].services[5].type "configserver"
+hosts[0].services[5].configid "admin/configservers/configserver.0"
+hosts[0].services[5].clustertype "admin"
+hosts[0].services[5].clustername "admin"
+hosts[0].services[5].index 0
+hosts[0].services[5].ports[0].number 19070
+hosts[0].services[5].ports[0].tags "config rpc"
+hosts[0].services[5].ports[1].number 19071
+hosts[0].services[5].ports[1].tags "config http"
+hosts[0].services[6].name "adminserver"
+hosts[0].services[6].type "adminserver"
+hosts[0].services[6].configid "admin/adminserver"
+hosts[0].services[6].clustertype "admin"
+hosts[0].services[6].clustername "admin"
+hosts[0].services[6].index 999999
+hosts[0].services[7].name "slobrok"
+hosts[0].services[7].type "slobrok"
+hosts[0].services[7].configid "admin/slobrok.0"
+hosts[0].services[7].clustertype "slobrok"
+hosts[0].services[7].clustername "admin"
+hosts[0].services[7].index 0
+hosts[0].services[7].ports[0].number 19099
+hosts[0].services[7].ports[0].tags "status admin rpc"
+hosts[0].services[8].name "qrserver"
+hosts[0].services[8].type "qrserver"
+hosts[0].services[8].configid "search/qrsclusters/default/qrserver.0"
+hosts[0].services[8].clustertype ""
+hosts[0].services[8].clustername "default"
+hosts[0].services[8].index 0
+hosts[0].services[8].ports[0].number 4080
+hosts[0].services[8].ports[0].tags "state external query http"
+hosts[0].services[8].ports[1].number 4081
+hosts[0].services[8].ports[1].tags "external status http"
+hosts[0].services[8].ports[2].number 4082
+hosts[0].services[8].ports[2].tags "messaging rpc"
+hosts[0].services[8].ports[3].number 4083
+hosts[0].services[8].ports[3].tags "external fileserver http"
+hosts[0].services[8].ports[4].number 4084
+hosts[0].services[8].ports[4].tags "admin rpc"
+hosts[0].services[8].ports[5].number 4085
+hosts[0].services[8].ports[5].tags "rmiregistry rmi"
+hosts[0].services[8].ports[6].number 4086
+hosts[0].services[8].ports[6].tags "jmx rmi"
+hosts[0].services[9].name "searchnode"
+hosts[0].services[9].type "searchnode"
+hosts[0].services[9].configid "search/cluster.music/c0/r0"
+hosts[0].services[9].clustertype "search"
+hosts[0].services[9].clustername "music"
+hosts[0].services[9].index 0
+hosts[0].services[9].ports[0].number 19100
+hosts[0].services[9].ports[0].tags "status admin rtc rpc"
+hosts[0].services[9].ports[1].number 19101
+hosts[0].services[9].ports[1].tags "fs4"
+hosts[0].services[9].ports[2].number 19102
+hosts[0].services[9].ports[2].tags "metrics xml http"
+hosts[0].services[9].ports[3].number 19103
+hosts[0].services[9].ports[3].tags "persistence-provider rpc"
+hosts[0].services[9].ports[4].number 19104
+hosts[0].services[9].ports[4].tags "health json http"
+hosts[0].services[10].name "transactionlogserver"
+hosts[0].services[10].type "transactionlogserver"
+hosts[0].services[10].configid "search/cluster.music/c0/r0/transactionlogserver"
+hosts[0].services[10].clustertype "search"
+hosts[0].services[10].clustername "music"
+hosts[0].services[10].index 999999
+hosts[0].services[10].ports[0].number 19105
+hosts[0].services[10].ports[0].tags "tls"
+hosts[0].services[11].name "clustercontroller"
+hosts[0].services[11].type "clustercontroller"
+hosts[0].services[11].configid "search/cluster.music/rtx/0"
+hosts[0].services[11].clustertype "search"
+hosts[0].services[11].clustername "music"
+hosts[0].services[11].index 0
+hosts[0].services[11].ports[0].number 19106
+hosts[0].services[11].ports[0].tags "status admin rpc rtx"
+hosts[0].services[11].ports[1].number 19107
+hosts[0].services[11].ports[1].tags "unused"
+hosts[0].services[12].name "topleveldispatch"
+hosts[0].services[12].type "topleveldispatch"
+hosts[0].services[12].configid "search/cluster.music/tlds/tld.0"
+hosts[0].services[12].clustertype "search"
+hosts[0].services[12].clustername "music"
+hosts[0].services[12].index 0
+hosts[0].services[12].ports[0].number 19108
+hosts[0].services[12].ports[0].tags "admin rpc"
+hosts[0].services[12].ports[1].number 19109
+hosts[0].services[12].ports[1].tags "fs4"
+hosts[0].services[12].ports[2].number 19110
+hosts[0].services[12].ports[2].tags "health json http"
+hosts[0].services[13].name "docprocservice"
+hosts[0].services[13].type "docprocservice"
+hosts[0].services[13].configid "docproc/cluster.music.indexing/0"
+hosts[0].services[13].clustertype ""
+hosts[0].services[13].clustername "music.indexing"
+hosts[0].services[13].index 0
+hosts[0].services[13].ports[0].number 19111
+hosts[0].services[13].ports[0].tags "state external query http"
+hosts[0].services[13].ports[1].number 19112
+hosts[0].services[13].ports[1].tags "external status http"
+hosts[0].services[13].ports[2].number 19113
+hosts[0].services[13].ports[2].tags "messaging rpc"
+hosts[0].services[13].ports[3].number 19114
+hosts[0].services[13].ports[3].tags "external fileserver http"
+hosts[0].services[13].ports[4].number 19115
+hosts[0].services[13].ports[4].tags "admin rpc"
+hosts[0].services[13].ports[5].number 19116
+hosts[0].services[13].ports[5].tags "rmiregistry rmi"
+hosts[0].services[13].ports[6].number 19117
+hosts[0].services[13].ports[6].tags "jmx rmi"
+hosts[0].services[14].name "filedistributorservice"
+hosts[0].services[14].type "filedistributorservice"
+hosts[0].services[14].configid "filedistribution/example.yahoo.com"
+hosts[0].services[14].clustertype "filedistribution"
+hosts[0].services[14].clustername "admin"
+hosts[0].services[14].index 999999
+hosts[0].services[14].ports[0].number 19118
+hosts[0].services[14].ports[0].tags "rpc"
+hosts[0].services[14].ports[1].number 19119
+hosts[0].services[14].ports[1].tags "torrent"
diff --git a/configutil/src/tests/model_inspect/model_inspect_test.cpp b/configutil/src/tests/model_inspect/model_inspect_test.cpp
new file mode 100644
index 00000000000..283d1f0c4f2
--- /dev/null
+++ b/configutil/src/tests/model_inspect/model_inspect_test.cpp
@@ -0,0 +1,267 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/fastos/fastos.h>
+#include <vespa/vespalib/testkit/testapp.h>
+#include <lib/modelinspect.h>
+#include <iostream>
+
+class Model {
+public:
+ config::ConfigUri uri;
+ ModelInspect::Flags flags;
+ std::stringstream stream;
+ ModelInspect model;
+
+ Model() : uri("file:model.cfg"), flags(), stream(),
+ model(flags, uri, stream) {
+ };
+
+ Model(ModelInspect::Flags _flags)
+ : uri("file:model.cfg"), flags(_flags), stream(),
+ model(flags, uri, stream) {
+ };
+
+ void contains(std::string needle) {
+ ASSERT_TRUE(stream.str().find(needle) != std::string::npos);
+ };
+
+ void missing(std::string needle) {
+ ASSERT_TRUE(stream.str().find(needle) == std::string::npos);
+ };
+
+ ~Model() {
+ };
+};
+
+class MakeUriFlags : public ModelInspect::Flags {
+public:
+ MakeUriFlags() : ModelInspect::Flags() {
+ makeuri = true;
+ }
+};
+
+class TagFilterFlags : public ModelInspect::Flags {
+public:
+ TagFilterFlags(vespalib::string tag) : ModelInspect::Flags() {
+ tagfilt = true;
+ tagFilter.push_back(tag);
+ }
+};
+
+class ModelDummy : public ModelInspect {
+public:
+ bool _yamlDump,
+ _listHosts,
+ _listServices,
+ _listCluster,
+ _listConfigIds,
+ _listHost,
+ _listClusters,
+ _listAllPorts,
+ _listService,
+ _listService2,
+ _listConfigId,
+ _getIndexOf;
+ ModelDummy(std::stringstream &stream)
+ : ModelInspect(ModelInspect::Flags(),
+ config::ConfigUri("file:model.cfg"),
+ stream) {
+ _yamlDump =
+ _listHosts =
+ _listServices =
+ _listCluster =
+ _listConfigIds =
+ _listHost =
+ _listClusters =
+ _listAllPorts =
+ _listService =
+ _listService2 =
+ _listConfigId =
+ _getIndexOf = false;
+ };
+
+ void yamlDump() { _yamlDump = true; };
+ void listHosts() { _listHosts = true; };
+ void listServices() { _listServices = true; };
+ void listClusters() { _listClusters = true; };
+ void listConfigIds() { _listConfigIds = true; };
+ int listHost(const vespalib::string) { _listHost = true; return 0; };
+ int listAllPorts() { _listAllPorts = true; return 0; };
+ int listCluster(const vespalib::string) { _listCluster = true; return 0; };
+ int listService(const vespalib::string) { _listService = true; return 0; };
+ int listService(const vespalib::string, const vespalib::string) { _listService2 = true; return 0; };
+ int listConfigId(const vespalib::string) { _listConfigId = true; return 0; };
+ int getIndexOf(const vespalib::string, const vespalib::string) { _getIndexOf = true; return 0; };
+
+ ~ModelDummy() {};
+};
+
+
+TEST_F("yamlDump", Model) {
+ ModelInspect & inspect = f1.model;
+ inspect.yamlDump();
+
+ // Make sure that we got at least some yaml data out
+ // TODO: Parse yaml
+ f1.contains("- servicename: logd");
+}
+
+TEST_F("listHosts", Model) {
+ f1.model.listHosts();
+ f1.contains("example.yahoo.com");
+}
+
+TEST_F("listServices", Model) {
+ f1.model.listServices();
+ f1.contains("logd");
+ f1.contains("qrserver");
+}
+
+TEST_F("listClusters", Model) {
+ f1.model.listClusters();
+ f1.contains("admin");
+ f1.contains("default");
+ f1.contains("music");
+}
+
+TEST_F("listConfigIds", Model) {
+ f1.model.listConfigIds();
+ f1.contains("search/qrsclusters/default/qrserver.0");
+ f1.contains("admin/configservers/configserver.0");
+}
+
+TEST_F("listHost", Model) {
+ ASSERT_EQUAL(0, f1.model.listHost("example.yahoo.com"));
+ f1.contains("search/qrsclusters/default/qrserver.0");
+ ASSERT_EQUAL(1, f1.model.listHost("nothere"));
+}
+
+TEST_F("listCluster", Model) {
+ ASSERT_EQUAL(0, f1.model.listCluster("default"));
+ f1.contains("search/qrsclusters/default/qrserver.0");
+
+ ASSERT_EQUAL(1, f1.model.listCluster("nothere"));
+}
+
+TEST_F("listAllPorts", Model) {
+ f1.model.listAllPorts();
+ f1.contains("example.yahoo.com:19080");
+}
+
+TEST_F("listService", Model) {
+ ASSERT_EQUAL(0, f1.model.listService("qrserver"));
+ f1.contains("search/qrsclusters/default/qrserver.0");
+ ASSERT_EQUAL(1, f1.model.listService("nothere"));
+}
+
+TEST_F("listService with cluster", Model) {
+ ASSERT_EQUAL(0, f1.model.listService("default", "qrserver"));
+ f1.contains("search/qrsclusters/default/qrserver.0");
+ ASSERT_EQUAL(1, f1.model.listService("notacluster", "qrserver"));
+}
+
+TEST_F("listConfigId", Model) {
+ ASSERT_EQUAL(0, f1.model.listConfigId("hosts/example.yahoo.com/logd"));
+ f1.contains("logd @ example.yahoo.com : hosts");
+ ASSERT_EQUAL(1, f1.model.listConfigId("nothere"));
+}
+
+TEST_F("getIndexOf", Model) {
+ ASSERT_EQUAL(0, f1.model.getIndexOf("logd", "example.yahoo.com"));
+ ASSERT_EQUAL(1, f1.model.getIndexOf("nothere", "example.yahoo.com"));
+ ASSERT_EQUAL(1, f1.model.getIndexOf("nothere", "nothere"));
+}
+
+TEST_FF("tag filter match", TagFilterFlags("http"), Model(f1)) {
+ f2.model.listService("qrserver");
+ f2.contains("tcp/example.yahoo.com:4080");
+}
+
+TEST_FF("tag filter no match", TagFilterFlags("nothing"), Model(f1)) {
+ f2.model.listService("qrserver");
+ f2.missing("tcp/example.yahoo.com:4080");
+}
+
+TEST_FF("makeuri", MakeUriFlags(), Model(f1)) {
+ f2.model.listService("qrserver");
+ f2.contains("http://example.yahoo.com:4080");
+}
+
+TEST_FF("action no command", std::stringstream, ModelDummy(f1)) {
+ char *argv[] = { strdup("notacommand"), NULL, NULL };
+ ASSERT_EQUAL(1, f2.action(1, argv));
+ ASSERT_EQUAL(1, f2.action(2, argv));
+ ASSERT_EQUAL(1, f2.action(3, argv));
+ free(argv[0]);
+}
+
+TEST_FF("action", std::stringstream, ModelDummy(f1)) {
+ {
+ char *arg[] = { strdup("yamldump") };
+ ASSERT_EQUAL(0, f2.action(1, arg));
+ ASSERT_TRUE(f2._yamlDump);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("hosts") };
+ ASSERT_EQUAL(0, f2.action(1, arg));
+ ASSERT_TRUE(f2._listHosts);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("services") };
+ ASSERT_EQUAL(0, f2.action(1, arg));
+ ASSERT_TRUE(f2._listServices);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("clusters") };
+ ASSERT_EQUAL(0, f2.action(1, arg));
+ ASSERT_TRUE(f2._listClusters);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("configids") };
+ ASSERT_EQUAL(0, f2.action(1, arg));
+ ASSERT_TRUE(f2._listConfigIds);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("host"), NULL };
+ ASSERT_EQUAL(0, f2.action(2, arg));
+ ASSERT_TRUE(f2._listHost);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("cluster"), NULL };
+ ASSERT_EQUAL(0, f2.action(2, arg));
+ ASSERT_TRUE(f2._listCluster);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("service"), NULL };
+ ASSERT_EQUAL(0, f2.action(2, arg));
+ ASSERT_TRUE(f2._listService);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("configid"), NULL };
+ ASSERT_EQUAL(0, f2.action(2, arg));
+ ASSERT_TRUE(f2._listConfigId);
+ free(arg[0]);
+ }
+ {
+ char *arg[] = { strdup("service"), strdup("a:b") };
+ ASSERT_EQUAL(0, f2.action(2, arg));
+ ASSERT_TRUE(f2._listService2);
+ free(arg[0]);
+ free(arg[1]);
+ }
+ {
+ char *arg[] = { strdup("get-index-of"), NULL, NULL };
+ ASSERT_EQUAL(0, f2.action(3, arg));
+ ASSERT_TRUE(f2._getIndexOf);
+ free(arg[0]);
+ }
+}
+
+TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/configutil/src/tests/tags/.gitignore b/configutil/src/tests/tags/.gitignore
new file mode 100644
index 00000000000..6d21ec0c34d
--- /dev/null
+++ b/configutil/src/tests/tags/.gitignore
@@ -0,0 +1,4 @@
+/.depend
+/Makefile
+/tags_test
+configutil_tags_test_app
diff --git a/configutil/src/tests/tags/CMakeLists.txt b/configutil/src/tests/tags/CMakeLists.txt
new file mode 100644
index 00000000000..5e43bee2ecc
--- /dev/null
+++ b/configutil/src/tests/tags/CMakeLists.txt
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+vespa_add_executable(configutil_tags_test_app
+ SOURCES
+ tags_test.cpp
+ DEPENDS
+ configutil_util
+)
+vespa_add_test(NAME configutil_tags_test_app COMMAND configutil_tags_test_app)
diff --git a/configutil/src/tests/tags/FILES b/configutil/src/tests/tags/FILES
new file mode 100644
index 00000000000..fb9a8362cdd
--- /dev/null
+++ b/configutil/src/tests/tags/FILES
@@ -0,0 +1 @@
+tags_test.cpp
diff --git a/configutil/src/tests/tags/tags_test.cpp b/configutil/src/tests/tags/tags_test.cpp
new file mode 100644
index 00000000000..61c23490a8a
--- /dev/null
+++ b/configutil/src/tests/tags/tags_test.cpp
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+#include <vespa/vespalib/testkit/testapp.h>
+#include <lib/tags.h>
+
+using namespace configdefinitions;
+
+TEST("upcase") {
+ EXPECT_EQUAL(vespalib::string("A"), upcase(vespalib::string("a")));
+ EXPECT_EQUAL(vespalib::string("A"), upcase(vespalib::string("A")));
+}
+
+TEST("tagsContain") {
+ EXPECT_TRUE(tagsContain("a b c", "a"));
+ EXPECT_TRUE(tagsContain("a b c", "b"));
+ EXPECT_TRUE(tagsContain("a b c", "c"));
+
+ EXPECT_FALSE(tagsContain("a b c", "d"));
+}
+
+TEST_MAIN() { TEST_RUN_ALL(); }
diff --git a/configutil/testrun/.gitignore b/configutil/testrun/.gitignore
new file mode 100644
index 00000000000..6a7cd8e3a6e
--- /dev/null
+++ b/configutil/testrun/.gitignore
@@ -0,0 +1,21 @@
+/test-report.html
+/test-report.html.bottom
+/test-report.html.entry
+/test-report.html.summary
+/test-report.html.top
+/test.1.config_status.log.file.txt
+/test.2.model_inspect.files.html
+/test.2.model_inspect.log.file.txt
+/test.3.tags.file.tags_test.cpp.txt
+/test.3.tags.files.html
+/test.3.tags.log.file.txt
+/tmp.config_status-time
+/tmp.config_status.log-control
+/tmp.end-time
+/tmp.model_inspect-time
+/tmp.model_inspect.log-control
+/tmp.start-time
+/tmp.tags-time
+/tmp.tags.log-control
+/test.*.*.result
+/Makefile