Currently httpserver-api module exposes both read and write (destructive) HTTP API operations. In most production deployment scenarios it is not desirable to let anyone be able to poweroff OSv instance or delete/modify any files on its filesystem. And the only way to disallow these is to setup SSL client certificate.
So this patch adds new module httpserver-readonly-api that allows to build a version of httpserver-api with read operations only (HTTP GET). It achieves it by passing -DREADONLY during compilation of api/*.cc files to make corresponding preprocessor directive disable relevant destructive parts of the API code. Here is a command to build HTML5 cli with readonly httpserver api: ./scripts/build image=httpserver-readonly-api.none,httpserver-html5-cli.fg Signed-off-by: Waldemar Kozaczuk <jwkozac...@gmail.com> --- modules/httpserver-api/Makefile | 10 +- modules/httpserver-api/api/api.cc | 2 + modules/httpserver-api/api/app.cc | 4 + modules/httpserver-api/api/env.cc | 3 +- modules/httpserver-api/api/file.cc | 6 + modules/httpserver-api/api/os.cc | 6 + modules/httpserver-api/api/trace.cc | 8 + modules/httpserver-api/tests/basetest.py | 11 +- .../httpserver-api/tests/readonly-api/testenv.py | 19 ++ .../httpserver-api/tests/readonly-api/testfile.py | 54 ++++ .../httpserver-api/tests/readonly-api/testfs.py | 18 ++ .../tests/readonly-api/testjolokia.py | 61 +++++ .../httpserver-api/tests/readonly-api/testjvm.py | 48 ++++ .../tests/readonly-api/testnetwork.py | 30 +++ .../httpserver-api/tests/readonly-api/testos.py | 52 ++++ .../httpserver-api/tests/readonly-api/testtrace.py | 39 +++ .../tests/testhttpserver-readonly-api.py | 39 +++ modules/httpserver-readonly-api/.gitignore | 4 + .../Makefile | 54 ++-- .../api-doc/listings/api.json | 40 +++ .../api-doc/listings/app.json | 38 +++ .../api-doc/listings/env.json | 57 ++++ .../api-doc/listings/file.json | 187 +++++++++++++ .../api-doc/listings/fs.json | 98 +++++++ .../api-doc/listings/hardware.json | 73 ++++++ .../api-doc/listings/network.json | 266 +++++++++++++++++++ .../api-doc/listings/os.json | 289 +++++++++++++++++++++ .../api-doc/listings/trace.json | 151 +++++++++++ modules/httpserver-readonly-api/module.py | 33 +++ 29 files changed, 1664 insertions(+), 36 deletions(-) create mode 100755 modules/httpserver-api/tests/readonly-api/testenv.py create mode 100755 modules/httpserver-api/tests/readonly-api/testfile.py create mode 100755 modules/httpserver-api/tests/readonly-api/testfs.py create mode 100644 modules/httpserver-api/tests/readonly-api/testjolokia.py create mode 100755 modules/httpserver-api/tests/readonly-api/testjvm.py create mode 100755 modules/httpserver-api/tests/readonly-api/testnetwork.py create mode 100755 modules/httpserver-api/tests/readonly-api/testos.py create mode 100644 modules/httpserver-api/tests/readonly-api/testtrace.py create mode 100755 modules/httpserver-api/tests/testhttpserver-readonly-api.py create mode 100644 modules/httpserver-readonly-api/.gitignore copy modules/{httpserver-api => httpserver-readonly-api}/Makefile (72%) create mode 100644 modules/httpserver-readonly-api/api-doc/listings/api.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/app.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/env.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/file.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/fs.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/hardware.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/network.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/os.json create mode 100644 modules/httpserver-readonly-api/api-doc/listings/trace.json create mode 100644 modules/httpserver-readonly-api/module.py diff --git a/modules/httpserver-api/Makefile b/modules/httpserver-api/Makefile index 3737b94..9c9b971 100644 --- a/modules/httpserver-api/Makefile +++ b/modules/httpserver-api/Makefile @@ -23,14 +23,15 @@ miscbase = $(src)/external/$(ARCH)/misc.bin libs-dir = $(miscbase)/usr/lib64 boost-libs := -lboost_system -lboost_filesystem -lboost_regex +STUB_CPP_FILES := $(wildcard stub/*.cc) +STUB_FILES := $(addprefix obj/,$(STUB_CPP_FILES:.cc=.o)) + # the build target executable: TARGET = httpserver-api JSON_FILES := $(wildcard api-doc/listings/*.json) JSON_CC_FILES := $(subst .json,.json.cc,$(subst api-doc/listings/,autogen/,$(JSON_FILES))) CPP_FILES := $(JSON_CC_FILES) $(wildcard *.cc) $(wildcard json/*.cc) -STUB_CPP_FILES := $(wildcard stub/*.cc) OBJ_FILES := $(addprefix obj/,$(CPP_FILES:.cc=.o)) -STUB_FILES := $(addprefix obj/,$(STUB_CPP_FILES:.cc=.o)) DYN_LIBS = -lpthread -ldl -L../libtools -ltools $(boost-libs) -lyaml-cpp @@ -66,10 +67,7 @@ api_%: libhttpserver-api_%.so stub-lib: $(STUB_FILES) $(call quiet, $(CXX) $(CXXFLAGS) -shared -o $(TARGET)-stub.so $^, LINK $@) -$(TARGET): $(OBJ_FILES) $(STUB_FILES) - $(call quiet, $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS), LINK $@) - -lib$(TARGET).so: $(OBJ_FILES) +lib$(TARGET).so: $(OBJ_FILES) #$(STUB_FILES) $(call quiet, $(CXX) $(CXXFLAGS) -shared $(STATIC_LIBS) -o $@ $^ $(DYN_LIBS), LINK $@) $(call very-quiet, ldd $@ | grep boost | sed 's/ *[^ ] *\(.*\) => \(.*\) .*/\/usr\/lib\/\1: \2/' > _usr_$(TARGET).manifest) diff --git a/modules/httpserver-api/api/api.cc b/modules/httpserver-api/api/api.cc index a1339e6..cfeeedb 100644 --- a/modules/httpserver-api/api/api.cc +++ b/modules/httpserver-api/api/api.cc @@ -249,10 +249,12 @@ void init(routes& routes) api_json_init_path("Advanced API options"); api_batch.set_handler(new api_param_handler(routes)); +#if !defined(READONLY) stop_api.set_handler([](const_req req){ global_server::stop(); return ""; }); +#endif } diff --git a/modules/httpserver-api/api/app.cc b/modules/httpserver-api/api/app.cc index 41dfb71..72c71d3 100644 --- a/modules/httpserver-api/api/app.cc +++ b/modules/httpserver-api/api/app.cc @@ -23,6 +23,7 @@ using namespace std; using namespace json; using namespace app_json; +#if !defined(READONLY) static std::string exec_app(const std::string& cmnd_line, bool new_program) { bool ok; auto new_commands = osv::parse_command_line(cmnd_line, ok); @@ -48,6 +49,7 @@ static std::string exec_app(const std::string& cmnd_line, bool new_program) { } return app_ids; } +#endif extern "C" void httpserver_plugin_register_routes(httpserver::routes* routes) { httpserver::api::app::init(*routes); @@ -57,11 +59,13 @@ void init(routes& routes) { app_json_init_path("app API"); +#if !defined(READONLY) run_app.set_handler([](const_req req) { string command = req.get_query_param("command"); bool new_program = str2bool(req.get_query_param("new_program")); return exec_app(command, new_program); }); +#endif finished_app.set_handler([](const_req req) { std::string tid_str = req.get_query_param("tid"); diff --git a/modules/httpserver-api/api/env.cc b/modules/httpserver-api/api/env.cc index ed46d16..ea64200 100644 --- a/modules/httpserver-api/api/env.cc +++ b/modules/httpserver-api/api/env.cc @@ -45,6 +45,7 @@ void init(routes& routes) return res; }); +#if !defined(READONLY) setEnv.set_handler([](const_req req) { string param = req.param.at("var").substr(1); if (setenv(param.c_str(), @@ -61,7 +62,7 @@ void init(routes& routes) } return ""; }); - +#endif } } diff --git a/modules/httpserver-api/api/file.cc b/modules/httpserver-api/api/file.cc index 6b12b16..54972f3 100644 --- a/modules/httpserver-api/api/file.cc +++ b/modules/httpserver-api/api/file.cc @@ -98,6 +98,7 @@ static string file_name(const string& path) return path.substr(found + 1); } +#if !defined(READONLY) /** * Generate a temporary file name in a target directory * according to a file name @@ -152,6 +153,7 @@ static void copy(const std::string& from, const std::string& to) + e.code().message()); } } +#endif class get_file_handler : public file_interaction_handler { virtual void handle(const std::string& path, parameters* params, @@ -318,6 +320,7 @@ class get_file_handler : public file_interaction_handler { } }; +#if !defined(READONLY) class del_file_handler : public handler_base { virtual void handle(const std::string& path, parameters* params, const http::server::request& req, http::server::reply& rep) @@ -442,6 +445,7 @@ class put_file_handler : public handler_base { set_headers(rep, "json"); } }; +#endif extern "C" void httpserver_plugin_register_routes(httpserver::routes* routes) { httpserver::api::file::init(*routes); @@ -451,9 +455,11 @@ void init(routes& routes) { file_json_init_path("file API"); getFile.set_handler(new get_file_handler()); +#if !defined(READONLY) delFile.set_handler(new del_file_handler()); putFile.set_handler(new put_file_handler()); upload.set_handler(new post_file_handler()); +#endif } } diff --git a/modules/httpserver-api/api/os.cc b/modules/httpserver-api/api/os.cc index 358a86c..0e042db 100644 --- a/modules/httpserver-api/api/os.cc +++ b/modules/httpserver-api/api/os.cc @@ -83,6 +83,7 @@ void init(routes& routes) return memory::get_balloon_size(); }); +#if !defined(READONLY) os_shutdown.set_handler([](const_req req) { osv::shutdown(); return ""; @@ -97,6 +98,7 @@ void init(routes& routes) osv::reboot(); return ""; }); +#endif os_dmesg.set_handler([](const_req req) { return debug_buffer; @@ -109,11 +111,13 @@ void init(routes& routes) return json_return_type(hostname); }); +#if !defined(READONLY) os_set_hostname.set_handler([](const_req req) { string hostname = req.get_query_param("name"); sethostname(hostname.c_str(), hostname.size()); return ""; }); +#endif os_threads.set_handler([](const_req req) { using namespace std::chrono; @@ -143,6 +147,7 @@ void init(routes& routes) return osv::getcmdline(); }); +#if !defined(READONLY) os_set_cmdline.set_handler([](const_req req) { string newcmd = req.get_query_param("cmdline"); @@ -157,6 +162,7 @@ void init(routes& routes) return osv::getcmdline(); }); +#endif } diff --git a/modules/httpserver-api/api/trace.cc b/modules/httpserver-api/api/trace.cc index f871e65..211ed61 100644 --- a/modules/httpserver-api/api/trace.cc +++ b/modules/httpserver-api/api/trace.cc @@ -59,6 +59,7 @@ void httpserver::api::trace::init(routes & routes) } return res; }); +#if !defined(READONLY) trace_json::setTraceEventStatus.set_handler([](const_req req) { std::vector<jstrace_event_info> res; const auto enabled = str2bool(req.get_query_param("enabled")); @@ -68,11 +69,13 @@ void httpserver::api::trace::init(routes & routes) } return res; }); +#endif trace_json::getSingleTraceEventStatus.set_handler([](const_req req) { const auto eventid = req.param.at("eventid").substr(1); const auto e = ::trace::get_event_info(eventid); return jstrace_event_info(e); }); +#if !defined(READONLY) trace_json::setSingleTraceEventStatus.set_handler([](const_req req) { const auto eventid = req.param.at("eventid").substr(1); const auto enabled = str2bool(req.get_query_param("enabled")); @@ -98,6 +101,7 @@ void httpserver::api::trace::init(routes & routes) prof::start_sampler(config); return "Sampler started successfully"; }); +#endif class create_trace_dump_file { public: @@ -145,6 +149,7 @@ void httpserver::api::trace::init(routes & routes) trace_json::getTraceBuffers.set_handler(new create_trace_dump()); +#if !defined(READONLY) trace_json::setCountEvent.set_handler([](const_req req) { const auto eventid = req.param.at("eventid").substr(1); const auto enabled = str2bool(req.get_query_param("enabled")); @@ -165,6 +170,7 @@ void httpserver::api::trace::init(routes & routes) } return ""; }); +#endif trace_json::getCounts.set_handler([](const_req req) { httpserver::json::TraceCounts ret; ret.time_ms = std::chrono::duration_cast<std::chrono::milliseconds> @@ -177,9 +183,11 @@ void httpserver::api::trace::init(routes & routes) } return ret; }); +#if !defined(READONLY) trace_json::deleteCounts.set_handler([](const_req req) { counters.clear(); return ""; }); +#endif } diff --git a/modules/httpserver-api/tests/basetest.py b/modules/httpserver-api/tests/basetest.py index 4d57191..0f4570d 100755 --- a/modules/httpserver-api/tests/basetest.py +++ b/modules/httpserver-api/tests/basetest.py @@ -81,9 +81,9 @@ class Basetest(unittest.TestCase): path = self.path_by_nick(api_definition, nickname) self.assertRegexpMatches(self.curl(path), expr) - def assertHttpError(self, url, code=404): + def assertHttpError(self, url, code=404, method='GET', data=None): try: - self.curl(url) + self.curl(url, method, data) except HttpError as e: if e.code != code: raise Exception('Expected error code %d but got %d' % (code, e.code)) @@ -151,6 +151,13 @@ class Basetest(unittest.TestCase): time.sleep(1) @classmethod + def hard_shutdown(cls): + child_pid = subprocess.call(['pgrep', "-P", str(cls.os_process.pid)]) + subprocess.call(['kill', '-9', str(child_pid)]) + cls.os_process.kill() + cls.os_process.wait() + + @classmethod def start_image(cls): if cls.config.check_jvm: jvm_plugin_api_listings_path = \ diff --git a/modules/httpserver-api/tests/readonly-api/testenv.py b/modules/httpserver-api/tests/readonly-api/testenv.py new file mode 100755 index 0000000..a15dcc8 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testenv.py @@ -0,0 +1,19 @@ +#!/usr/bin/env python +import basetest + +class testenv(basetest.Basetest): + def test_env(self): + param = "test-param" + get_path = self.path_by_nick(self.env_api, "list_env") + param + set_path = get_path + "?val=TEST" + self.assertHttpError(set_path, 404, method='POST') + + lst = self.curl(self.path_by_nick(self.env_api, "list_env")) + if not "OSV_CPUS=4" in lst: + raise Exception('environment variable OSV_CPUS not found in list') + + self.assertHttpError(get_path, 404, method="DELETE") + + @classmethod + def setUpClass(cls): + cls.env_api = cls.get_json_api("env.json") diff --git a/modules/httpserver-api/tests/readonly-api/testfile.py b/modules/httpserver-api/tests/readonly-api/testfile.py new file mode 100755 index 0000000..7aeec52 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testfile.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python +import os +import urllib +import basetest +import subprocess + +class testfile(basetest.Basetest): + def build_curl_cmd(self, args): + if not self._client.is_ssl(): + return 'curl ' + args + return 'curl --cacert %s --cert %s --key %s %s' % ( + self.get_ca_cert_path(), self.get_client_cert_path(), self.get_client_key_path(), args) + + def build_wget_cmd(self, args): + if not self._client.is_ssl(): + return 'wget ' + args + return 'wget --ca-certificate=%s --certificate=%s --private-key=%s %s' % ( + self.get_ca_cert_path(), self.get_client_cert_path(), self.get_client_key_path(), args) + + def test_list_file_cmd(self): + path = "/file" + lst = self.curl(path + "/etc?op=LISTSTATUS") + hosts = next((item for item in lst if item["pathSuffix"] == "hosts"), None) + self.assertEqual(hosts["owner"], "osv") + + def test_list_astrik_file_cmd(self): + path = "/file" + lst = self.curl(path + "/et%3F/hos*?op=LISTSTATUS") + hosts = next((item for item in lst if item["pathSuffix"] == "hosts"), None) + self.assertEqual(hosts["owner"], "osv") + + def test_file_status_cmd(self): + path = "/file" + hosts = self.curl(path + "/etc/hosts?op=GETFILESTATUS") + self.assertEqual(hosts["type"], "FILE") + self.assert_between("accessTime", 1300000000, 2000000000, hosts["accessTime"]) + self.assertEqual(hosts["blockSize"], 512) + self.assertEqual(hosts["group"], "osv") + self.assert_between("length", 20, 40, hosts["length"]) + self.assert_between("modificationTime", 1300000000, 2000000000, hosts["modificationTime"]) + self.assertEqual(hosts["owner"], "osv") + self.assertEqual(hosts["pathSuffix"], "hosts") + self.assertEqual(hosts["permission"], "664") + self.assertEqual(hosts["replication"], 1) + + def test_put_file_cmd(self): + path = "/file" + self.assertHttpError(path + "/etc/hosts?op=COPY&destination="+urllib.quote("/etc/hosts1"), 404, method='PUT') + self.assertHttpError(path + "/etc/hosts1?op=RENAME&destination="+urllib.quote("/etc/hosts2"), 404, method='PUT') + self.assertHttpError(path + "/etc/hosts2?op=DELETE", 404, method='DELETE') + + @classmethod + def setUpClass(cls): + cls.file_api = cls.get_json_api("file.json") diff --git a/modules/httpserver-api/tests/readonly-api/testfs.py b/modules/httpserver-api/tests/readonly-api/testfs.py new file mode 100755 index 0000000..dadb973 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testfs.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +import basetest + +class testfs(basetest.Basetest): + def test_getfs(self): + get_path = "/fs/df/" + ls = self.curl(get_path) + val = ls[0] + self.assertEqual(val["mount"], "/") + self.assertGreaterEqual(val["ffree"], 20000000) + self.assertGreaterEqual(val["ftotal"], 20000000) + self.assertGreaterEqual(val["bfree"], 20000000) + self.assertGreaterEqual(val["btotal"], 20000000) + self.assertEqual(val["filesystem"], "/dev/vblk0.1") + + @classmethod + def setUpClass(cls): + cls.fs_api = cls.get_json_api("fs.json") diff --git a/modules/httpserver-api/tests/readonly-api/testjolokia.py b/modules/httpserver-api/tests/readonly-api/testjolokia.py new file mode 100644 index 0000000..a994027 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testjolokia.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python +import basetest +import json + +class testjolokia(basetest.Basetest): + def setUp(self): + self.path = '/jolokia' + + def check_response(self, res): + self.assert_key_in('timestamp', res) + self.assert_key_in('status', res) + + def test_version(self): + res = self.curl(self.path) + self.check_response(res) + self.assert_key_in('request', res) + self.assertEqual('version', res['request']['type']) + self.assertEqual('OSv Jolokia Bridge', res['value']['config']['agentId']) + + def test_get_read_simple(self): + res = self.curl(self.path + '/read/java.lang:type=Memory/HeapMemoryUsage') + self.check_response(res) + value = res['value'] + for k in ['max', 'committed', 'init', 'used']: + self.assert_key_in(k, value) + + def test_post_read_simple(self): + body = { + 'type' : 'read', + 'mbean' : 'java.lang:type=Memory', + 'attribute' : 'HeapMemoryUsage', + } + res = self.curl(self.path, method='POST', data=json.dumps(body)) + self.check_response(res) + value = res['value'] + for k in ['max', 'committed', 'init', 'used']: + self.assert_key_in(k, value) + + def test_get_write_simple(self): + res = self.curl(self.path + '/write/java.lang:type=Memory/Verbose/true') + try: + self.check_response(res) + res = self.curl(self.path + '/read/java.lang:type=Memory/Verbose') + self.assertTrue(res['value']) + finally: + res = self.curl(self.path + '/write/java.lang:type=Memory/Verbose/false') + + def test_post_write_simple(self): + body = { + 'type' : 'write', + 'mbean' : 'java.lang:type=Memory', + 'attribute' : 'Verbose', + 'value' : 'true' + } + res = self.curl(self.path, method='POST', data=json.dumps(body)) + try: + self.check_response(res) + res = self.curl(self.path + '/read/java.lang:type=Memory/Verbose') + self.assertTrue(res['value']) + finally: + res = self.curl(self.path + '/write/java.lang:type=Memory/Verbose/false') diff --git a/modules/httpserver-api/tests/readonly-api/testjvm.py b/modules/httpserver-api/tests/readonly-api/testjvm.py new file mode 100755 index 0000000..a3b9ac1 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testjvm.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +import time +import basetest +import urllib + +class testjvm(basetest.Basetest): + def test_jvm_version(self): + self.validate_path_regex(self.jvm_api, "getJavaVersion", r"^1\.[78]\.\d+_?\d*$") + + def test_gc_info(self): + gc = self.curl(self.path_by_nick(self.jvm_api, "getGCinfo")) + self.assertGreaterEqual(len(gc), 2) + self.assert_key_in("count", gc[0]) + self.assert_key_in("name", gc[0]) + self.assert_key_in("time", gc[0]) + + def test_force_gc(self): + gc = self.curl(self.path_by_nick(self.jvm_api, "getGCinfo")) + time.sleep(1) + self.curl(self.path_by_nick(self.jvm_api, "forceGC"), method='POST') + gc1 = self.curl(self.path_by_nick(self.jvm_api, "getGCinfo")) + self.assertGreaterEqual(len(gc), 2) + self.assertGreater(gc1[0]["count"], gc[0]["count"]) + self.assertGreater(gc1[0]["time"], gc[0]["time"]) + + def test_get_mbeans(self): + gc = self.curl(self.path_by_nick(self.jvm_api, "getMbeanList")) + self.assertGreaterEqual(len(gc), 10) + self.assertGreaterEqual(gc.index("java.lang:type=Memory"), 0, + "Memory mbean is missing") + + def test_get_mbean(self): + mbean = self.curl(self.path_by_nick(self.jvm_api, "getMbeanList") + + urllib.quote("java.lang:name=PS Old Gen,type=MemoryPool")) + self.assertGreaterEqual(len(mbean), 15) + self.assert_key_in("type", mbean[0]) + self.assert_key_in("name", mbean[0]) + self.assert_key_in("value", mbean[0]) + + def test_set_mbean(self): + path = self.path_by_nick(self.jvm_api, "getMbeanList") + urllib.quote("java.lang:name=PS Old Gen,type=MemoryPool") + mbean = self.curl(path) + usage = next((item for item in mbean if item["name"] == "UsageThreshold"), None) + self.assertTrue(usage != None) + self.curl(path + "/UsageThreshold?value=" + str(usage["value"] + 1), method='POST') + mbean1 = self.curl(path) + usage1 = next((item for item in mbean1 if item["name"] == "UsageThreshold"), None) + self.assertEqual(usage["value"] + 1, usage1["value"]) diff --git a/modules/httpserver-api/tests/readonly-api/testnetwork.py b/modules/httpserver-api/tests/readonly-api/testnetwork.py new file mode 100755 index 0000000..d8fa95f --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testnetwork.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +import time +import basetest + +class testnetwork(basetest.Basetest): + def test_ifconfig(self): + path = self.path_by_nick(self.network_api, "listIfconfig") + intf = self.curl(path + 'lo0') + self.assert_key_in("data", intf) + self.assert_key_in("config", intf) + conf = intf["config"] + self.assert_key_in("phys_addr", conf) + self.assert_key_in("mask", conf) + self.assertEqual(conf["addr"], "127.0.0.1") + lst = self.curl(path) + self.assertNotEqual(lst, []) + + def test_get_routes(self): + path = self.path_by_nick(self.network_api, "getRoute") + routes = self.curl(path) + route = routes[0] + self.assert_key_in("destination", route) + self.assert_key_in("gateway", route) + self.assert_key_in("flags", route) + self.assert_key_in("netif", route) + self.assert_key_in("ipv6", route) + + @classmethod + def setUpClass(cls): + cls.network_api = cls.get_json_api("network.json") diff --git a/modules/httpserver-api/tests/readonly-api/testos.py b/modules/httpserver-api/tests/readonly-api/testos.py new file mode 100755 index 0000000..0208de7 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testos.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +import time +import basetest + +class testos(basetest.Basetest): + def test_os_version(self): + path = self.path_by_nick(self.os_api, "os_version") + self.assertRegexpMatches(self.curl(path), r"v0\.\d+(-rc\d+)?(-\d+-[0-9a-z]+)?" , path) + + def test_vendor(self): + self.validate_path(self.os_api, "os_vendor", "Cloudius Systems") + + def test_os_uptime(self): + path = self.path_by_nick(self.os_api, "os_uptime") + up_time = self.curl(path) + time.sleep(2) + self.assert_between(path, up_time + 1, up_time + 3, self.curl(path)) + + def test_os_date(self): + path = self.path_by_nick(self.os_api, "os_date") + val = self.curl(path).encode('ascii', 'ignore') + self.assertRegexpMatches(val, "...\\s+...\\s+\\d+\\s+\\d\\d:\\d\\d:\\d\\d\\s+UTC\\s+20..", path) + + def test_os_total_memory(self): + path = self.path_by_nick(self.os_api, "os_memory_total") + val = self.curl(path) + self.assertGreater(val, 1024 * 1024 * 256, msg="Memory should be greater than 256Mb") + + def test_os_free_memory(self): + path = self.path_by_nick(self.os_api, "os_memory_free") + val = self.curl(path) + self.assertGreater(val, 1024 * 1024 * 256, msg="Free memory should be greater than 256Mb") + + def test_os_threads(self): + path = self.path_by_nick(self.os_api, "os_threads") + val = self.curl(path) + self.assert_key_in("time_ms", val) + ctime = val["time_ms"] + idle_thread = next((item for item in val["list"] if item["name"] == "idle1"), None) + idle = idle_thread["cpu_ms"] + id = idle_thread["id"] + cpu = idle_thread["cpu"] + self.assertEqual(cpu,1) + time.sleep(2) + val = self.curl(path) + self.assert_key_in("time_ms", val) + self.assert_between(path, ctime + 1000, ctime + 3000, val["time_ms"]) + idle_thread = next((item for item in val["list"] if item["name"] == "idle1"), None) + idle1 = idle_thread["cpu_ms"] + self.assert_between(path + " idle thread cputime was" + str(idle)+ + " new time=" + str(idle1), idle + 1000, idle + 3000, idle1) + self.assertEqual(id, idle_thread["id"]) diff --git a/modules/httpserver-api/tests/readonly-api/testtrace.py b/modules/httpserver-api/tests/readonly-api/testtrace.py new file mode 100644 index 0000000..11a6496 --- /dev/null +++ b/modules/httpserver-api/tests/readonly-api/testtrace.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +import basetest + +class testtrace(basetest.Basetest): + def setUp(self): + self.path = "/trace" + + def check_status_list(self, list): + for s in list: + self.assert_key_in("backtrace", s) + self.assert_key_in("enabled", s) + self.assert_key_in("name", s) + self.assert_key_in("id", s) + + def test_get_status(self): + status = self.curl(self.path + "/status") + self.assertGreaterEqual(status, 0) + self.check_status_list(status) + + def test_set_status(self): + for bt in [True, False]: + for en in [True, False]: + self.assertHttpError(self.path + '/status?enabled=' + str(en) + '&backtrace=' + str(bt), 404, method='POST') + + def test_get_single_eventinfo(self): + status = self.curl(self.path + "/status") + for s in status: + s2 = self.curl(self.path + '/event/' + s['id']) + self.assertEqual(s2, s) + + def test_set_single_eventinfo(self): + status = self.curl(self.path + "/status") + for s in status: + for bt in [True, False]: + for en in [True, False]: + self.assertHttpError(self.path + '/event/' + s['id'] + '?enabled=' + str(en) + '&backtrace=' + str(bt), 404, method='POST') + + def test_get_trace_dump(self): + self.assertHttpError(self.path + '/status?enabled=true&backtrace=true', 404, method='POST') diff --git a/modules/httpserver-api/tests/testhttpserver-readonly-api.py b/modules/httpserver-api/tests/testhttpserver-readonly-api.py new file mode 100755 index 0000000..a769399 --- /dev/null +++ b/modules/httpserver-api/tests/testhttpserver-readonly-api.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python +import sys +import argparse +import os +import unittest +import basetest + +from osv import client + +parser = argparse.ArgumentParser(description="""Testing the read-only Httpserver""") + +module_base = os.path.join(os.path.realpath(os.path.dirname(__file__)), '..') +osv_base = os.path.join(module_base, '..', '..') + +parser.add_argument('--connect', help='Connect to an existing image', action='store_true') +parser.add_argument('--run_script', help='path to the run image script', default=os.path.join(osv_base, 'scripts', 'run.py')) +parser.add_argument('--cmd', help='the command to execute') +parser.add_argument('--use_sudo', help='Use sudo with -n option instead of port forwarding', action='store_true') +parser.add_argument('--jsondir', help='location of the json files', default=os.path.join(module_base, 'api-doc/listings/')) +client.Client.add_arguments(parser) + +class test_httpserver(basetest.Basetest): + @classmethod + def setUpClass(cls): + basetest.Basetest.start_image() + + @classmethod + def tearDownClass(cls): + basetest.Basetest.hard_shutdown() + +if __name__ == '__main__': + basetest.Basetest.set_config(parser) + basetest.Basetest.config.check_jvm = True + basetest.Basetest.start_image() + del sys.argv[1:] + api_tests = unittest.TestLoader().discover(os.path.join(module_base, 'tests', 'readonly-api'), pattern='*.py') + test_suite = unittest.TestSuite((api_tests)) + unittest.TextTestRunner(verbosity=2).run(test_suite) + basetest.Basetest.hard_shutdown() diff --git a/modules/httpserver-readonly-api/.gitignore b/modules/httpserver-readonly-api/.gitignore new file mode 100644 index 0000000..8d8365a --- /dev/null +++ b/modules/httpserver-readonly-api/.gitignore @@ -0,0 +1,4 @@ +*.so +autogen/* +obj/* +*.manifest diff --git a/modules/httpserver-api/Makefile b/modules/httpserver-readonly-api/Makefile similarity index 72% copy from modules/httpserver-api/Makefile copy to modules/httpserver-readonly-api/Makefile index 3737b94..a5da8d6 100644 --- a/modules/httpserver-api/Makefile +++ b/modules/httpserver-readonly-api/Makefile @@ -1,11 +1,11 @@ -INCLUDES = -I. -I../../include -I../../arch/$(ARCH) -I../.. \ +INCLUDES = -I. -I../httpserver-api -I../../include -I../../arch/$(ARCH) -I../.. \ -I../../build/$(mode)/gen/include # compiler flags: # -g adds debugging information to the executable file # -Wall turns on most, but not all, compiler warnings autodepend = -MD -MT $@ -MP -CXXFLAGS = -g -Wall -std=c++11 -fPIC $(INCLUDES) -O2 $(autodepend) +CXXFLAGS = -g -Wall -std=c++11 -fPIC $(INCLUDES) -O2 $(autodepend) -DREADONLY src = $(shell readlink -f ../..) ifndef ARCH @@ -23,14 +23,15 @@ miscbase = $(src)/external/$(ARCH)/misc.bin libs-dir = $(miscbase)/usr/lib64 boost-libs := -lboost_system -lboost_filesystem -lboost_regex +STUB_CPP_FILES := $(notdir $(wildcard ../httpserver-api/stub/*.cc)) +STUB_FILES := $(addprefix obj/stub/,$(STUB_CPP_FILES:.cc=.o)) + # the build target executable: TARGET = httpserver-api JSON_FILES := $(wildcard api-doc/listings/*.json) JSON_CC_FILES := $(subst .json,.json.cc,$(subst api-doc/listings/,autogen/,$(JSON_FILES))) -CPP_FILES := $(JSON_CC_FILES) $(wildcard *.cc) $(wildcard json/*.cc) -STUB_CPP_FILES := $(wildcard stub/*.cc) +CPP_FILES := $(notdir $(wildcard ../httpserver-api/*.cc)) $(addprefix json/,$(notdir $(wildcard ../httpserver-api/json/*.cc))) OBJ_FILES := $(addprefix obj/,$(CPP_FILES:.cc=.o)) -STUB_FILES := $(addprefix obj/,$(STUB_CPP_FILES:.cc=.o)) DYN_LIBS = -lpthread -ldl -L../libtools -ltools $(boost-libs) -lyaml-cpp @@ -66,9 +67,6 @@ api_%: libhttpserver-api_%.so stub-lib: $(STUB_FILES) $(call quiet, $(CXX) $(CXXFLAGS) -shared -o $(TARGET)-stub.so $^, LINK $@) -$(TARGET): $(OBJ_FILES) $(STUB_FILES) - $(call quiet, $(CXX) $(CXXFLAGS) -o $@ $^ $(LIBS), LINK $@) - lib$(TARGET).so: $(OBJ_FILES) $(call quiet, $(CXX) $(CXXFLAGS) -shared $(STATIC_LIBS) -o $@ $^ $(DYN_LIBS), LINK $@) $(call very-quiet, ldd $@ | grep boost | sed 's/ *[^ ] *\(.*\) => \(.*\) .*/\/usr\/lib\/\1: \2/' > _usr_$(TARGET).manifest) @@ -80,9 +78,9 @@ ifneq ($(MAKECMDGOALS),clean) -include $(DEPS) endif -autogen/%.cc: api-doc/listings/% json2code.py +autogen/%.cc: api-doc/listings/% ../httpserver-api/json2code.py $(call very-quiet, mkdir -p autogen) - $(call quiet,./json2code.py -f $< -ns json, GEN $@) + $(call quiet,../httpserver-api/json2code.py -outdir autogen -f $< -ns json, GEN $@) init_obj = \ $(call very-quiet, mkdir -p obj/stub) \ @@ -90,7 +88,20 @@ init_obj = \ $(call very-quiet, mkdir -p obj/api) \ $(call very-quiet, mkdir -p obj/autogen) -obj/%.o: %.cc +obj/autogen/%.o: autogen/%.cc + $(call init_obj) + $(call quiet, $(CXX) $(CXXFLAGS) -c -MMD -o $@ $<, CXX $@) + +obj/api/%.o: ../httpserver-api/api/%.cc + $(call init_obj) + $(call quiet, $(CXX) $(CXXFLAGS) -c -MMD -o $@ $<, CXX $@) + +obj/json/%.o: ../httpserver-api/json/%.cc + $(call init_obj) + $(call quiet, $(CXX) $(CXXFLAGS) -c -MMD -o $@ $<, CXX $@) + +$(OBJ_FILES): obj/%.o: ../httpserver-api/%.cc +# echo $(OBJ_FILES) $(call init_obj) $(call quiet, $(CXX) $(CXXFLAGS) -c -MMD -o $@ $<, CXX $@) @@ -101,23 +112,12 @@ clean: $(call very-quiet, $(RM) -rf autogen) $(call very-quiet, $(RM) -f *usr*.manifest) -check: check-http check-ssl - -check-http: - # Test plain HTTP - cd $(src) && \ - make image=httpserver-api,jetty && \ - PYTHONPATH=$(src)/scripts modules/httpserver-api/tests/testhttpserver-api.py - -check-ssl: - # Test SSL +check: + # Test plain readonly HTTP cd $(src) && \ - make image=httpserver-api.fg_ssl,certs,jetty && \ - PYTHONPATH=$(src)/scripts modules/httpserver-api/tests/testhttpserver-api.py \ - --cert modules/certs/build/client.pem \ - --key modules/certs/build/client.key \ - --cacert modules/certs/build/cacert.pem + make image=httpserver-readonly-api.fg,jetty && \ + PYTHONPATH=$(src)/scripts modules/httpserver-api/tests/testhttpserver-readonly-api.py -.PHONY: check check-http check-ssl +.PHONY: check .SECONDARY: diff --git a/modules/httpserver-readonly-api/api-doc/listings/api.json b/modules/httpserver-readonly-api/api-doc/listings/api.json new file mode 100644 index 0000000..ec18bd9 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/api.json @@ -0,0 +1,40 @@ +{ + "apiVersion":"0.0.1", + "swaggerVersion":"1.2", + "basePath":"{{Protocol}}://{{Host}}", + "resourcePath":"/api", + "produces":[ + "application/json" + ], + "apis":[ + { + "path":"/api/batch", + "operations":[ + { + "method":"POST", + "summary":"Batch process API calls", + "notes":"Perform batch API calls in a single command. Commands are performed sequentially and independently. Each command has its own response code", + "type":"string", + "errorResponses":[ + { + "code":400, + "reason":"Bad request" + } + ], + "nickname":"api_batch", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"batch", + "paramType":"form", + "type":"string", + "required":true + } + ] + } + ] + } + ] +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/app.json b/modules/httpserver-readonly-api/api-doc/listings/app.json new file mode 100644 index 0000000..c4f0d1f --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/app.json @@ -0,0 +1,38 @@ +{ + "apiVersion": "0.0.1", + "swaggerVersion": "1.2", + "basePath": "http://{{Host}}", + "resourcePath": "/app", + "produces": [ + "application/json", + "application/xml" + ], + "apis": [ + { + "path": "/app/finished", + "operations": [ + { + "method": "GET", + "summary": "Check if application terminated", + "notes": "Check if application specified by thread ID tid finished. Invalid tid is reported as finished.", + "type": "string", + "nickname": "finished_app", + "produces": [ + "application/json", + "application/xml" + ], + "parameters": [ + { + "name": "tid", + "description": "application main thread ID", + "required": true, + "allowMultiple":false, + "type":"string", + "paramType":"query" + } + ] + } + ] + } + ] +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/env.json b/modules/httpserver-readonly-api/api-doc/listings/env.json new file mode 100644 index 0000000..29ce438 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/env.json @@ -0,0 +1,57 @@ +{ + "apiVersion":"0.0.1", + "swaggerVersion":"1.2", + "basePath":"{{Protocol}}://{{Host}}", + "resourcePath":"/env", + "produces":[ + "application/json" + ], + "apis":[ + { + "path":"/env/{var}", + "operations":[ + { + "method":"GET", + "summary":"Get an environment variable", + "notes":"return the environment variable value", + "type":"string", + "errorResponses":[ + { + "code":400, + "reason":"Variable not found" + } + ], + "nickname":"getEnv", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"var", + "description":"name of the environment variable", + "required":true, + "allowMultiple":true, + "type":"string", + "paramType":"path" + } + ] + } + ] + }, + { + "path":"/env/", + "operations":[ + { + "method":"GET", + "summary":"Returns a list of all environment variables in the system.", + "type":"array", + "items": {"type": "string"}, + "nickname":"list_env", + "produces":[ + "application/json" + ] + } + ] + } + ] +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/file.json b/modules/httpserver-readonly-api/api-doc/listings/file.json new file mode 100644 index 0000000..7ab8a0d --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/file.json @@ -0,0 +1,187 @@ +{ + "apiVersion":"0.0.1", + "swaggerVersion":"1.2", + "basePath":"{{Protocol}}://{{Host}}", + "resourcePath":"/file", + "produces":[ + "application/json" + ], + "apis":[ + { + "path":"/file/{path-par}", + "operations":[ + { + "method":"GET", + "summary":"Get File/Directory information", + "notes":"return File or Directory related information", + "type":"string", + "errorResponses":[ + { + "code":404, + "reason":"File not found" + }, + { + "code":400, + "reason":"Bad Request" + } + ], + "nickname":"getFile", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"path-par", + "description":"Full path of file or directory", + "required":true, + "allowMultiple":true, + "type":"string", + "paramType":"path" + }, + { + "name":"op", + "description":"The operation to perform", + "required":true, + "allowMultiple":false, + "type":"string", + "paramType":"query", + "enum":["GET", "LISTSTATUS", "GETFILESTATUS"] + }, + { + "name":"offset", + "description":"Offset in a file", + "required":false, + "allowMultiple":false, + "type":"long", + "paramType":"query" + }, + { + "name":"length", + "description":"The number of bytes to be processed.", + "required":false, + "allowMultiple":false, + "type":"long", + "paramType":"query" + } + ] + } + ] + } + ], + "models":{ + "ContentSummary":{ + "id": "ContentSummary", + "properties":{ + "directoryCount":{ + "description":"The number of directories.", + "type":"int", + "required":true + }, + "fileCount":{ + "description":"The number of files.", + "type":"int", + "required":true + }, + "length":{ + "description":"The number of bytes used by the content.", + "type":"int", + "required":true + }, + "quota":{ + "description":"The namespace quota of this directory.", + "type":"int", + "required":true + }, + "spaceConsumed":{ + "description":"The disk space consumed by the content.", + "type":"int", + "required":true + }, + "spaceQuota":{ + "description":"The disk space quota.", + "type":"int", + "required":true + } + } + }, + "FileChecksum":{ + "id": "FileChecksum", + "properties":{ + "algorithm":{ + "description":"The name of the checksum algorithm.", + "type":"string", + "required":true + }, + "bytes":{ + "description":"The byte sequence of the checksum in hexadecimal.", + "type":"string", + "required":true + }, + "length":{ + "description":"The length of the bytes (not the length of the string).", + "type":"int", + "required":true + } + } + }, + "FileStatusProperties":{ + "id": "FileStatusProperties", + "properties":{ + "accessTime":{ + "description":"The access time.", + "type":"int", + "required":true + }, + "blockSize":{ + "description":"The block size of a file.", + "type":"int", + "required":true + }, + "group":{ + "description":"The group owner.", + "type":"string", + "required":true + }, + "length":{ + "description":"The number of bytes in a file.", + "type":"long", + "required":true + }, + "modificationTime":{ + "description":"The modification time.", + "type":"int", + "required":true + }, + "owner":{ + "description":"The user who is the owner.", + "type":"string", + "required":true + }, + "pathSuffix":{ + "description":"The path suffix.", + "type":"string", + "required":true + }, + "permission":{ + "description":"The permission represented as a octal string.", + "type":"string", + "required":true + }, + "replication":{ + "description":"The number of replication of a file.", + "type":"int", + "required":true + }, + "symlink":{ + "description":"The link target of a symlink.", + "type":"string" + }, + "type":{ + "description":"The type of the path object.", + "type":"string", + "required":true + } + } + } + } +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/fs.json b/modules/httpserver-readonly-api/api-doc/listings/fs.json new file mode 100644 index 0000000..7fd2062 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/fs.json @@ -0,0 +1,98 @@ +{ + "apiVersion": "0.0.1", + "swaggerVersion": "1.2", + "basePath": "{{Protocol}}://{{Host}}", + "resourcePath": "/fs", + "produces": [ + "application/json" + ], + "apis": [ + { + "path": "/fs/df/{mount}", + "operations": [ + { + "method": "GET", + "summary": "report a file system usage", + "type": "array", + "items": { + "type": "DFStat" + }, + "errorResponses":[ + { + "code":404, + "reason":"File system not found" + } + ], + "nickname" : "getDFStats", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name":"mount", + "description":"mount point", + "required":false, + "allowMultiple":true, + "type":"string", + "paramType":"path" + } + ], + "deprecated": "false" + } + ] + }, + { + "path": "/fs/df/", + "operations": [ + { + "method": "GET", + "summary": "Return all file systems", + "type": "array", + "items": { + "$ref": "DFStat" + }, + "nickname" : "list_df_stats", + "produces": [ + "application/json" + ] + } + ] + } + ], + "models" : { + "DFStat": { + "description": "Information on one file system", + "id": "DFStat", + "properties": { + "filesystem" : { + "type": "string", + "description": "fs name" + }, + "mount" : { + "type": "string", + "description": "mount point" + }, + "btotal" : { + "type": "long", + "description": "total data blocks in file system" + }, + "bfree" : { + "type": "long", + "description": "free blocks in file system" + }, + "ftotal" : { + "type": "long", + "description": "total file nodes in file system" + }, + "ffree" : { + "type": "long", + "description": "free file nodes in file system" + }, + "blocksize" : { + "type": "long", + "description": "block size in bytes" + } + } + } + } +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/hardware.json b/modules/httpserver-readonly-api/api-doc/listings/hardware.json new file mode 100644 index 0000000..eaf2627 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/hardware.json @@ -0,0 +1,73 @@ +{ + "apiVersion": "0.0.1", + "swaggerVersion": "1.2", + "basePath": "{{Protocol}}://{{Host}}", + "resourcePath": "/hardware", + "produces": [ + "application/json" + ], + "apis": [ + { + "path": "/hardware/processor/flags", + "operations": [ + { + "method": "GET", + "summary": "list all present processor features", + "type": "string", + "nickname" : "processorFeatures", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/hardware/firmware/vendor", + "operations": [ + { + "method": "GET", + "summary": "Return the firmware vendor", + "type": "string", + "nickname" : "firmware_vendor", + "produces": [ + "application/json" + ] + } + ] + }, + { + "path": "/hardware/hypervisor", + "operations": [ + { + "method": "GET", + "summary": "Returns name of the hypervisor OSv is running on.", + "type": "string", + "nickname" : "hypervisor_name", + "produces": [ + "application/json" + ] + } + ] + }, + { + "path": "/hardware/processor/count", + "operations": [ + { + "method": "GET", + "summary": "list the current number of processors", + "type": "long", + "nickname" : "processorCount", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + } + ] +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/network.json b/modules/httpserver-readonly-api/api-doc/listings/network.json new file mode 100644 index 0000000..0d082f3 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/network.json @@ -0,0 +1,266 @@ +{ + "apiVersion":"0.0.1", + "swaggerVersion":"1.2", + "basePath":"{{Protocol}}://{{Host}}", + "resourcePath":"/network", + "produces":[ + "application/json" + ], + "apis":[ + { + "path":"/network/ifconfig/", + "operations":[ + { + "method":"GET", + "summary":"Get all interfaces", + "notes":"Get a list of all the interfaces configuration and data", + "type":"array", + "items": {"type": "Interface"}, + "nickname":"listIfconfig", + "produces":[ + "application/json" + ], + "parameters":[ + ] + } + ] + }, + { + "path":"/network/ifconfig/{intf}", + "operations":[ + { + "method":"GET", + "summary":"Get an interface", + "notes":"Get an interface configuration and data", + "type":"Interface", + "errorResponses":[ + { + "code":404, + "reason":"Interface not found" + } + ], + "nickname":"getIfconfig", + "produces":[ + "application/json" + ], + "parameters":[ + { + "name":"intf", + "description":"name of the interface", + "required":true, + "allowMultiple":false, + "type":"string", + "paramType":"path" + } + ] + } + ] + }, + { + "path":"/network/route/", + "operations":[ + { + "method":"GET", + "summary":"Get the ip route table", + "notes":"Get a list of the available routes", + "type":"Routes", + "items": {"type": "Route"}, + "nickname":"getRoute", + "produces":[ + "application/json" + ], + "parameters":[ + + ] + } + ] + } + ], + "models":{ + "Interface_config":{ + "id": "Interface_config", + "properties":{ + "name":{ + "type":"string" + }, + "addr":{ + "type":"string" + }, + "mask":{ + "type":"string" + }, + "broadcast":{ + "type":"string" + }, + "flags":{ + "type":"string" + }, + "mtu":{ + "type":"string" + }, + "phys_addr":{ + "type":"string" + } + } + }, + "Wakeup_stats":{ + "id": "Wakeup_stats", + "properties":{ + "packets_8":{ + "type":"long" + }, + "packets_16":{ + "type":"long" + }, + "packets_32":{ + "type":"long" + }, + "packets_64":{ + "type":"long" + }, + "packets_128":{ + "type":"long" + }, + "packets_256":{ + "type":"long" + } + } + }, + "Interface_data":{ + "id": "Interface_data", + "properties":{ + "ifi_type":{ + "type":"char" + }, + "ifi_physical":{ + "type":"char" + }, + "ifi_addrlen":{ + "type":"char" + }, + "ifi_hdrlen":{ + "type":"char" + }, + "ifi_link_state":{ + "type":"char" + }, + "ifi_spare_char1":{ + "type":"char" + }, + "ifi_spare_char2":{ + "type":"char" + }, + "ifi_datalen":{ + "type":"char" + }, + "ifi_mtu":{ + "type":"long" + }, + "ifi_metric":{ + "type":"long" + }, + "ifi_baudrate":{ + "type":"long" + }, + "ifi_ipackets":{ + "type":"long" + }, + "ifi_ierrors":{ + "type":"long" + }, + "ifi_opackets":{ + "type":"long" + }, + "ifi_oerrors":{ + "type":"long" + }, + "ifi_collisions":{ + "type":"long" + }, + "ifi_ibytes":{ + "type":"long" + }, + "ifi_obytes":{ + "type":"long" + }, + "ifi_imcasts":{ + "type":"long" + }, + "ifi_omcasts":{ + "type":"long" + }, + "ifi_iqdrops":{ + "type":"long" + }, + "ifi_noproto":{ + "type":"long" + }, + "ifi_hwassist":{ + "type":"long" + }, + "ifi_epoch":{ + "type":"long" + }, + "ifi_ibh_wakeups":{ + "type":"long" + }, + "ifi_oworker_kicks":{ + "type":"long" + }, + "ifi_oworker_wakeups":{ + "type":"long" + }, + "ifi_oworker_packets":{ + "type":"long" + }, + "ifi_okicks":{ + "type":"long" + }, + "ifi_oqueue_is_full":{ + "type":"long" + }, + "ifi_iwakeup_stats":{ + "type": "Wakeup_stats" + }, + "ifi_owakeup_stats":{ + "type": "Wakeup_stats" + } + } + }, + "Interface":{ + "id":"Interface", + "properties":{ + "config":{ + "type":"Interface_config" + }, + "data":{ + "type":"Interface_data" + }, + "time":{ + "type":"long", + "description":"Time when interface information was taken (microseconds since uptime)" + } + } + }, + "Route":{ + "id":"Route", + "properties":{ + "destination":{ + "type":"string" + }, + "gateway":{ + "type":"string" + }, + "flags":{ + "type":"string" + }, + "netif":{ + "type":"string" + }, + "ipv6":{ + "type":"boolean" + } + } + } + } +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/os.json b/modules/httpserver-readonly-api/api-doc/listings/os.json new file mode 100644 index 0000000..a3ecae8 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/os.json @@ -0,0 +1,289 @@ +{ + "apiVersion": "0.0.1", + "swaggerVersion": "1.2", + "basePath": "{{Protocol}}://{{Host}}", + "resourcePath": "/os", + "produces": [ + "application/json" + ], + "apis": [ + { + "path": "/os/name", + "operations": [ + { + "method": "GET", + "summary": "Returns name of the operating system", + "type": "string", + "nickname": "os_name", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/version", + "operations": [ + { + "method": "GET", + "summary": "Returns version of the operating system", + "type": "string", + "nickname": "os_version", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/vendor", + "operations": [ + { + "method": "GET", + "summary": "Returns the vendor of the operating system", + "type": "string", + "nickname": "os_vendor", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/uptime", + "operations": [ + { + "method": "GET", + "summary": "Returns the number of seconds since the system was booted", + "type": "long", + "nickname": "os_uptime", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/date", + "operations": [ + { + "method": "GET", + "summary": "Returns the current date and time", + "type": "string", + "nickname": "os_date", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/memory/total", + "operations": [ + { + "method": "GET", + "summary": "Returns total amount of memory usable by the system (in bytes)", + "type": "long", + "nickname": "os_memory_total", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/memory/free", + "operations": [ + { + "method": "GET", + "summary": "Returns the amount of free memory in the system (in bytes)", + "type": "long", + "nickname": "os_memory_free", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/memory/balloon", + "operations": [ + { + "method": "GET", + "summary": "Returns the JVM balloon size (in bytes)", + "type": "long", + "nickname": "os_memory_balloon", + "produces": [ + "application/json" + ], + "parameters": [ + ] + } + ] + }, + { + "path": "/os/dmesg", + "operations": [ + { + "method": "GET", + "summary": "Returns the operating system boot log", + "type": "string", + "nickname": "os_dmesg", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/hostname", + "operations": [ + { + "method": "GET", + "summary": "Returns the system host name", + "type": "string", + "nickname": "os_get_hostname", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/threads", + "operations": [ + { + "method": "GET", + "summary": "Returns a list of threads in the system", + "type": "Threads", + "nickname": "os_threads", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/os/cmdline", + "operations": [ + { + "method": "GET", + "summary": "Returns the current boot command line", + "notes": "This operation retrieves the current OSv command line. The current command line is either the command line used for the current boot, or the one written by POST.", + "type": "string", + "nickname": "os_get_cmdline", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + } + ], + "models": { + "Thread": { + "id": "Thread", + "description": "Information on one thread", + "properties": { + "id": { + "type": "long", + "description": "32-bit thread id" + }, + "cpu": { + "type": "long", + "description": "CPU the thread is running on" + }, + "cpu_ms": { + "type": "long", + "description": "Total CPU time used by the thread (in milliseconds)" + }, + "switches": { + "type": "long", + "description": "Number of times this thread was context-switched in" + }, + "migrations": { + "type": "long", + "description": "Number of times this thread was migrated between CPUs" + }, + "preemptions": { + "type": "long", + "description": "Number of times this thread was preempted (still runnable, but switched out)" + }, + "priority": { + "type": "float" + }, + "stack_size": { + "type": "long" + }, + "status": { + "type": "string", + "description": "thread status", + "enum": [ + "invalid", + "prestarted", + "unstarted", + "waiting", + "running", + "queued", + "waking", + "terminating", + "terminated" + ] + }, + "name": { + "type": "string", + "description": "Thread description (not necessarily unique)" + } + } + }, + "Threads": { + "id": "Threads", + "description": "List of threads", + "properties": { + "list": { + "type": "array", + "items": { + "type": "Thread" + }, + "description": "List of thread objects" + }, + "time_ms": { + "type": "long", + "description": "Time when thread list was taken (milliseconds since epoche)" + } + } + } + } +} diff --git a/modules/httpserver-readonly-api/api-doc/listings/trace.json b/modules/httpserver-readonly-api/api-doc/listings/trace.json new file mode 100644 index 0000000..926a9a5 --- /dev/null +++ b/modules/httpserver-readonly-api/api-doc/listings/trace.json @@ -0,0 +1,151 @@ +{ + "apiVersion": "0.0.1", + "swaggerVersion": "1.2", + "basePath": "{{Protocol}}://{{Host}}", + "resourcePath": "/trace", + "produces": [ + "application/json" + ], + "apis": [ + { + "path": "/trace/status", + "operations": [ + { + "method": "GET", + "summary": "All/matching trace event status", + "notes": "return info and status of the matching trace events", + "type": "array", + "items":{"type":"TraceEventInfo"}, + "nickname": "getTraceEventStatus", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "match", + "description": "Match regular expression to match event ids against", + "required": false, + "allowMultiple": false, + "type": "string", + "paramType": "query" + } + ], + "deprecated": "false" + } + ] + }, + { + "path": "/trace/event/{eventid}", + "operations": [ + { + "method": "GET", + "summary": "Trace event info", + "notes": "return the trace event state info", + "type": "TraceEventInfo", + "nickname": "getSingleTraceEventStatus", + "produces": [ + "application/json" + ], + "parameters": [ + { + "name": "eventid", + "description": "Event ID to query", + "required": true, + "allowMultiple": true, + "type": "string", + "paramType": "path" + } + ], + "deprecated": "false" + } + ] + }, + { + "path": "/trace/count", + "operations": [ + { + "method": "GET", + "summary": "Get enabled counts", + "notes": "Returned the counters for all previously-enabled tracepoint counters", + "type": "TraceCounts", + "nickname": "getCounts", + "produces": [ + "application/json" + ], + "parameters": [ + ], + "deprecated": "false" + } + ] + }, + { + "path": "/trace/buffers", + "operations": [ + { + "method": "GET", + "summary": "Retrieve the trace buffer contents and associated meta data", + "notes": "returns binary file in the OSv trace dump format", + "type": "string", + "nickname": "getTraceBuffers", + "produces": [ + "application/octect-stream" + ], + "deprecated": "false" + } + ] + } + ], + "models" : { + "TraceEventInfo" : { + "id": "TraceEventInfo", + "description" : "trace event info", + "properties": { + "name": { + "type": "string", + "description": "event name" + }, + "id": { + "type": "string", + "description": "trace event id" + }, + "enabled": { + "type": "boolean", + "description": "event enabled" + }, + "backtrace": { + "type": "boolean", + "description": "generate backtrace when sampled" + } + } + }, + "TraceCount" : { + "id": "TraceCount", + "description" : "Number of times event seen since count enabled", + "properties": { + "name": { + "type": "string", + "description": "event name" + }, + "count": { + "type": "long", + "description": "count" + } + } + }, + "TraceCounts": { + "id": "TraceCounts", + "description": "Counts of all counted events", + "properties": { + "list": { + "type": "array", + "items": {"type": "TraceCount"}, + "description": "List of counted events" + }, + "time_ms": { + "type": "long", + "description": "Time when counts were taken (milliseconds since boot)" + } + } + } + } +} diff --git a/modules/httpserver-readonly-api/module.py b/modules/httpserver-readonly-api/module.py new file mode 100644 index 0000000..0369e89 --- /dev/null +++ b/modules/httpserver-readonly-api/module.py @@ -0,0 +1,33 @@ +import os +from osv.modules.api import * +from osv.modules.filemap import FileMap +from osv.modules import api + +provides = ['httpserver-api'] + +_module = '${OSV_BASE}/modules/httpserver-readonly-api' + +_exe = '/libhttpserver-api.so' + +usr_files = FileMap() +usr_files.add(os.path.join(_module, 'libhttpserver-api.so')).to(_exe) +usr_files.add(os.path.join(_module, 'api-doc')).to('/usr/mgmt/api') + +api.require('openssl') +api.require('libtools') +api.require('libyaml') + +# only require next 3 modules if java (jre) is included in the list of modules +api.require_if_other_module_present('josvsym','java') +api.require_if_other_module_present('httpserver-jolokia-plugin','java') +api.require_if_other_module_present('httpserver-jvm-plugin','java') + +# httpserver will run regardless of an explicit command line +# passed with "run.py -e". +daemon = api.run_on_init(_exe + ' &!') + +fg = api.run(_exe) + +fg_ssl = api.run(_exe + ' --ssl') + +default = daemon -- 2.7.4 -- You received this message because you are subscribed to the Google Groups "OSv Development" group. To unsubscribe from this group and stop receiving emails from it, send an email to osv-dev+unsubscr...@googlegroups.com. For more options, visit https://groups.google.com/d/optout.