Skip to content
Snippets Groups Projects
Commit 49094a39 authored by Dmitry E. Oboukhov's avatar Dmitry E. Oboukhov
Browse files

console can connect to remote admin port. closes #538

 + Add tarantoolctl enter instance_name command
parent 01d45477
No related branches found
No related tags found
No related merge requests found
......@@ -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)
......
......@@ -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
})
......
......@@ -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
}
......
......@@ -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;
}
TAP version 13
1..25
1..26
ok - console.listen started
ok - Handshake
ok - connect to console
ok - eval
ok - state.socker:peer().host
......
......@@ -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
......
......@@ -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'''
...
......@@ -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')
......@@ -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
......
......@@ -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')
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment