From 49094a39da94eeb2c917440be612f60b188e60a2 Mon Sep 17 00:00:00 2001 From: "Dmitry E. Oboukhov" <unera@debian.org> Date: Tue, 30 Sep 2014 18:14:13 +0400 Subject: [PATCH] console can connect to remote admin port. closes #538 + Add tarantoolctl enter instance_name command --- extra/dist/dist.lua | 19 +++++ src/box/lua/load_cfg.lua | 6 +- src/lua/box_net_box.lua | 136 ++++++++++++++++++++++++++++------ src/lua/console.lua | 35 +++++++++ test/app/console.result | 3 +- test/app/console.test.lua | 4 +- test/box/box.net.box.result | 57 +++++++++++++- test/box/box.net.box.test.lua | 28 +++++++ test/box/cfg.result | 2 +- test/lib/admin_connection.py | 6 ++ 10 files changed, 269 insertions(+), 27 deletions(-) diff --git a/extra/dist/dist.lua b/extra/dist/dist.lua index 8a27a76e8c..b4d9e381ce 100755 --- a/extra/dist/dist.lua +++ b/extra/dist/dist.lua @@ -61,6 +61,9 @@ Each instance can be controlled by C<dist.lua>: dist.lua logrotate instance_name +=head2 Enter instance admin console + + dist.lua enter instance_name =head1 COPYRIGHT @@ -253,6 +256,22 @@ elseif cmd == 'logrotate' then s:read({ '[.][.][.]' }, 2) os.exit(0) + +elseif cmd == 'enter' then + if fio.stat(console_sock) == nil then + log.error("Can't connect to %s (socket not found)", console_sock) + os.exit(-1) + end + + log.info('Connecting to %s', console_sock) + + local cmd = string.format( + "require('console').connect('%s')", console_sock) + + console.on_start( function(self) self:eval(cmd) end ) + console.on_client_disconnect( function(self) self.running = false end ) + console.start() + os.exit(0) else log.error("Unknown command '%s'", cmd) os.exit(-1) diff --git a/src/box/lua/load_cfg.lua b/src/box/lua/load_cfg.lua index 47496a3e64..019e873dbd 100644 --- a/src/box/lua/load_cfg.lua +++ b/src/box/lua/load_cfg.lua @@ -163,11 +163,15 @@ local box = require('box') local box_configured = {} for k, v in pairs(box) do box_configured[k] = v - box[k] = nil + -- box.net.box uses box.error and box.internal + if k ~= 'error' and k ~= 'internal' then + box[k] = nil + end end setmetatable(box, { __index = function(table, index) + error(debug.traceback("Please call box.cfg{} first")) error("Please call box.cfg{} first") end }) diff --git a/src/lua/box_net_box.lua b/src/lua/box_net_box.lua index 9642ce907c..256cf11e02 100644 --- a/src/lua/box_net_box.lua +++ b/src/lua/box_net_box.lua @@ -42,6 +42,8 @@ local TIMEOUT_INFINITY = 500 * 365 * 86400 local sequence_mt = { __serialize = 'sequence'} local mapping_mt = { __serialize = 'mapping'} +local CONSOLE_FAKESYNC = 15121974 + local function request(header, body) -- hint msgpack to always encode header and body as a map @@ -399,7 +401,32 @@ local remote_methods = { call = function(self, proc_name, ...) proc_name = tostring(proc_name) - local res = self:_request('call', true, proc_name, {...}) + + if not self.console then + local res = self:_request('call', true, proc_name, {...}) + return res.body[DATA] + end + + local eval_str = proc_name .. '(' + for i = 1, select('#', ...) do + if i > 1 then + eval_str = eval_str .. ', ' + end + local arg = select(i, ...) + + if arg == nil then + eval_str = eval_str .. 'nil' + elseif type(arg) == 'number' then + eval_str = eval_str .. tostring(arg) + else + arg = tostring(arg) + arg = string.gsub(arg, '"', '\\"') + eval_str = eval_str .. '"' .. arg .. '"' + end + end + eval_str = eval_str .. ")\n" + + local res = self:_console_request(eval_str, true) return res.body[DATA] end, @@ -497,7 +524,49 @@ local remote_methods = { end end, + + _check_console_response = function(self) + while true do + local resp = string.match(self.rbuf, '.-\n[.][.][.]\r?\n') + if resp == nil then + break + end + self.rbuf = string.sub(self.rbuf, #resp + 1) + + local result = yaml.decode(resp) + if result ~= nil then + result = result[1] + end + + local hdr = { [SYNC] = CONSOLE_FAKESYNC, [TYPE] = 0 } + local body = {} + + if type(result) ~= 'table' then + result = { result } + end + + + if result.error ~= nil then + hdr[TYPE] = bit.bor(ERROR_TYPE, box.error.PROC_LUA) + body[ERROR] = result.error + else + body[DATA] = { result } + end + + if self.ch.sync[CONSOLE_FAKESYNC] ~= nil then + self.ch.sync[CONSOLE_FAKESYNC]:put({hdr = hdr, body = body }) + self.ch.sync[CONSOLE_FAKESYNC] = nil + else + log.warn("Unexpected console response: %s", resp) + end + end + end, + _check_response = function(self) + if self.console then + return self:_check_console_response(self) + end + while true do if #self.rbuf < 5 then break @@ -599,6 +668,7 @@ local remote_methods = { end end, + _connect_worker = function(self) fiber.name('net.box.connector') while true do @@ -630,24 +700,30 @@ local remote_methods = { elseif string.len(self.handshake) ~= 128 then self:_fatal("Can't read handshake") else - self.wbuf = '' self.rbuf = '' - local s, e = pcall(function() - self:_auth() - end) - if not s then - self:_fatal(e) - end - - xpcall(function() self:_load_schema() end, - function(e) - log.info("Can't load schema: %s", tostring(e)) - end) - - if self.state ~= 'error' and self.state ~= 'closed' then + if string.match(self.handshake, '^Tarantool console') then + log.info('Remote host is tarantool console') + self.console = true self:_switch_state('active') + else + self.console = false + local s, e = pcall(function() + self:_auth() + end) + if not s then + self:_fatal(e) + end + + xpcall(function() self:_load_schema() end, + function(e) + log.info("Can't load schema: %s", tostring(e)) + end) + + if self.state ~= 'error' and self.state ~= 'closed' then + self:_switch_state('active') + end end end end @@ -886,16 +962,20 @@ local remote_methods = { return self:_request_internal(name, raise, ...) end, - _request_internal = function(self, name, raise, ...) + _console_request = function(self, request_body, raise) + if raise == nil then + raise = true + end + return self:_request_raw(CONSOLE_FAKESYNC, request_body, raise) + end, + + _request_raw = function(self, sync, request, raise) local fid = fiber.id() if self.timeouts[fid] == nil then self.timeouts[fid] = TIMEOUT_INFINITY end - local sync = self.proto:sync() - local request = self.proto[name](sync, ...) - self.wbuf = self.wbuf .. request local wstate = self._to_wstate[self.state] @@ -931,8 +1011,11 @@ local remote_methods = { end if response.body[DATA] ~= nil then - for i, v in pairs(response.body[DATA]) do - response.body[DATA][i] = box.tuple.new(response.body[DATA][i]) + if rawget(box, 'tuple') ~= nil then + for i, v in pairs(response.body[DATA]) do + response.body[DATA][i] = + box.tuple.new(response.body[DATA][i]) + end end -- disable YAML flow output (useful for admin console) setmetatable(response.body[DATA], sequence_mt) @@ -941,6 +1024,14 @@ local remote_methods = { return response end, + _request_internal = function(self, name, raise, ...) + + local sync = self.proto:sync() + local request = self.proto[name](sync, ...) + return self:_request_raw(sync, request, raise) + + end, + -- private (low level) methods _select = function(self, spaceno, indexno, key, opts) local res = self:_request('select', true, spaceno, indexno, key, opts) @@ -994,7 +1085,8 @@ remote.self = { result[i] = box.tuple.new(v) end return result - end + end, + console = false } diff --git a/src/lua/console.lua b/src/lua/console.lua index fe04952f5b..04a5eea269 100644 --- a/src/lua/console.lua +++ b/src/lua/console.lua @@ -70,6 +70,9 @@ end -- local function remote_eval(self, line) if not line then + if type(self.on_client_disconnect) == 'function' then + self:on_client_disconnect() + end pcall(self.remote.close, self.remote) self.remote = nil self.eval = nil @@ -179,7 +182,12 @@ local repl_mt = { -- REPL = read-eval-print-loop -- local function repl(self) + fiber.self().storage.console = self + if type(self.on_start) == 'function' then + self:on_start() + end + while self.running do local command = self:read() local output = self:eval(command) @@ -188,6 +196,22 @@ local function repl(self) fiber.self().storage.console = nil end +local function on_start(foo) + if foo == nil or type(foo) == 'function' then + repl_mt.__index.on_start = foo + return + end + error('Wrong type of on_start hook: ' .. type(foo)) +end + +local function on_client_disconnect(foo) + if foo == nil or type(foo) == 'function' then + repl_mt.__index.on_client_disconnect = foo + return + end + error('Wrong type of on_client_disconnect hook: ' .. type(foo)) +end + -- -- Set delimiter -- @@ -239,6 +263,14 @@ local function connect(uri) -- connect to remote host local remote = require('net.box'):new(u.host, u.service, { user = u.login, password = u.password }) + + -- run disconnect trigger + if remote.state == 'closed' then + if type(self.on_client_disconnect) == 'function' then + self:on_client_disconnect() + end + end + -- check permissions remote:call('dostring', 'return true') -- override methods @@ -256,6 +288,7 @@ local function client_handler(client, peer) print = client_print; client = client; }, repl_mt) + state:print(string.format("%-63s\n%-63s\n", "Tarantool console port", "")) repl(state) log.info("console: client %s:%s disconnected", peer.host, peer.port) end @@ -292,4 +325,6 @@ return { delimiter = delimiter; connect = connect; listen = listen; + on_start = on_start; + on_client_disconnect = on_client_disconnect; } diff --git a/test/app/console.result b/test/app/console.result index c36eef7c72..40596ba7e4 100644 --- a/test/app/console.result +++ b/test/app/console.result @@ -1,6 +1,7 @@ TAP version 13 -1..25 +1..26 ok - console.listen started +ok - Handshake ok - connect to console ok - eval ok - state.socker:peer().host diff --git a/test/app/console.test.lua b/test/app/console.test.lua index 8ee97cbaf1..d78df26eac 100755 --- a/test/app/console.test.lua +++ b/test/app/console.test.lua @@ -22,12 +22,14 @@ local EOL = "\n%.%.%.\n" test = tap.test("console") -test:plan(25) +test:plan(26) -- Start console and connect to it local server = console.listen(CONSOLE_SOCKET) test:ok(server ~= nil, "console.listen started") local client = socket.tcp_connect("unix/", CONSOLE_SOCKET) +local handshake = client:read{chunk = 128} +test:ok(string.match(handshake, '^Tarantool console') ~= nil, 'Handshake') test:ok(client ~= nil, "connect to console") -- Execute some command diff --git a/test/box/box.net.box.result b/test/box/box.net.box.result index eff5cf4775..c4f8a3adf0 100644 --- a/test/box/box.net.box.result +++ b/test/box/box.net.box.result @@ -141,7 +141,7 @@ cn.space.net_box_test_space:insert{234, 1,2,3} ... cn.space.net_box_test_space.insert{234, 1,2,3} --- -- error: 'builtin/net.box.lua:226: Use space:method(...) instead space.method(...)' +- error: 'builtin/net.box.lua:228: Use space:method(...) instead space.method(...)' ... cn.space.net_box_test_space:replace{354, 1,2,3} --- @@ -518,3 +518,58 @@ remote.self:timeout(123).space.net_box_test_space:select{234} space:drop() --- ... +-- admin console tests +function console_test(...) return { ... } end +--- +... +function console_test_error(...) error(string.format(...)) end +--- +... +function console_unpack_test(...) return ... end +--- +... +ADMIN = require('uri').parse(os.getenv('ADMIN')) +--- +... +cn = remote:new(LISTEN.host, LISTEN.service) +--- +... +cnc = remote:new(ADMIN.host, ADMIN.service) +--- +... +cnc.console +--- +- true +... +cn:call('console_test', 1, 2, 3, 'string', nil) +--- +- - [1, 2, 3, 'string'] +... +cnc:call('console_test', 1, 2, 3, 'string', nil) +--- +- - [1, 2, 3, 'string'] +... +cn:call('console_test_error', 'error %d', 123) +--- +- error: '[string "function console_test_error(...) error(string..."]:1: error 123' +... +cnc:call('console_test_error', 'error %d', 123) +--- +- error: '[string "function console_test_error(...) error(string..."]:1: error 123' +... +cn:call('console_unpack_test', 1) +--- +- - [1] +... +cnc:call('console_unpack_test', 1) +--- +- - [1] +... +cn:call('123') +--- +- error: Procedure '123' is not defined +... +cnc:call('123') +--- +- error: '[string "123()"]:1: unexpected symbol near ''123''' +... diff --git a/test/box/box.net.box.test.lua b/test/box/box.net.box.test.lua index 6dc3448aaf..b3de500c8a 100644 --- a/test/box/box.net.box.test.lua +++ b/test/box/box.net.box.test.lua @@ -195,3 +195,31 @@ remote.self:timeout(123).space.net_box_test_space:select{234} -- cleanup database after tests space:drop() + +-- admin console tests +function console_test(...) return { ... } end +function console_test_error(...) error(string.format(...)) end +function console_unpack_test(...) return ... end + + +ADMIN = require('uri').parse(os.getenv('ADMIN')) + +cn = remote:new(LISTEN.host, LISTEN.service) +cnc = remote:new(ADMIN.host, ADMIN.service) +cnc.console + +cn:call('console_test', 1, 2, 3, 'string', nil) +cnc:call('console_test', 1, 2, 3, 'string', nil) + +cn:call('console_test_error', 'error %d', 123) +cnc:call('console_test_error', 'error %d', 123) + + +cn:call('console_unpack_test', 1) +cnc:call('console_unpack_test', 1) + + + + +cn:call('123') +cnc:call('123') diff --git a/test/box/cfg.result b/test/box/cfg.result index 60f359c31f..6255ecf21b 100644 --- a/test/box/cfg.result +++ b/test/box/cfg.result @@ -2,7 +2,7 @@ --# push filter 'admin: .*' to 'admin: <uri>' box.cfg.nosuchoption = 1 --- -- error: '[string "-- load_cfg.lua - internal file..."]:191: Attempt to modify a read-only +- error: '[string "-- load_cfg.lua - internal file..."]:195: Attempt to modify a read-only table' ... t = {} for k,v in pairs(box.cfg) do if type(v) ~= 'table' and type(v) ~= 'function' then table.insert(t, k..': '..tostring(v)) end end diff --git a/test/lib/admin_connection.py b/test/lib/admin_connection.py index 14d2e90802..544be0b950 100644 --- a/test/lib/admin_connection.py +++ b/test/lib/admin_connection.py @@ -56,3 +56,9 @@ class AdminConnection(TarantoolConnection): if not silent: sys.stdout.write(res.replace("\r\n", "\n")) return res + + def connect(self): + super(AdminConnection, self).connect() + handshake = self.socket.recv(128) + if not re.search(r'^Tarantool console', str(handshake)): + raise RuntimeError('Broken tarantool console handshake') -- GitLab