tarantoolctl 14.99 KiB
#!/usr/bin/env tarantool
--[[
=head1 NAME
tarantoolctl - an utility to control tarantool instances
=head1 SYNOPSIS
vim /etc/tarantool/instances.enabled/my_instance.lua
tarantoolctl start my_instance
tarantoolctl stop my_instance
tarantoolctl logrotate my_instance
=head1 DESCRIPTION
The script is read C</etc/sysconfig/tarantool> or C</etc/default/tarantool>.
The file contains common default instances options:
$ cat /etc/default/tarantool
-- Options for Tarantool
default_cfg = {
-- will become pid_file .. instance .. '.pid'
pid_file = "/var/run/tarantool",
-- will become wal_dir/instance/
wal_dir = "/var/lib/tarantool",
-- snap_dir/instance/
snap_dir = "/var/lib/tarantool",
-- sophia_dir/instance/
sophia_dir = "/var/lib/tarantool/sophia",
-- logger/instance .. '.log'
logger = "/var/log/tarantool",
username = "tarantool",
}
instance_dir = "/etc/tarantool/instances.enabled"
The file defines C<instance_dir> where user can place his
applications (instances).
Each instance can be controlled by C<tarantoolctl>:
=head2 Starting instance
tarantoolctl start instance_name
=head2 Stopping instance
tarantoolctl stop instance_name
=head2 Logrotate instance's log
tarantoolctl logrotate instance_name
=head2 Enter instance admin console
tarantoolctl enter instance_name
=head2 status
tarantoolctl status instance_name
Check if instance is up.
If pid file exists and control socket exists and control socket is alive
returns code C<0>.
Return code != 0 in other cases. Can complain in log (stderr) if pid file
exists and socket doesn't, etc.
=head2 separate instances control
If You use SysV init, You can use symlink from
C<tarantoolctl> to C</etc/init.d/instance_name[.lua]>.
C<tarantoolctl> detects if it is started by symlink and uses
instance_name as C<`basename $0 .lua`>.
=head1 COPYRIGHT
Copyright (C) 2010-2013 Tarantool AUTHORS:
please see AUTHORS file.
=cut
]]
local fio = require 'fio'
local log = require 'log'
local errno = require 'errno'
local yaml = require 'yaml'
local console = require 'console'
local socket = require 'socket'
local ffi = require 'ffi'
local os = require 'os'
local fiber = require 'fiber'
local digest = require 'digest'
local urilib = require 'uri'
ffi.cdef[[
int kill(int pid, int sig);
struct passwd {
char *pw_name; /* username */
char *pw_passwd; /* user password */
int pw_uid; /* user ID */
int pw_gid; /* group ID */
char *pw_gecos; /* user information */
char *pw_dir; /* home directory */
char *pw_shell; /* shell program */
};
struct group{
char *gr_name;
char *gr_passwd;
int gr_gid;
char **gr_mem;
};
struct passwd *getpwnam(const char *name);
struct group *getgrgid(int gid);
]]
local available_commands = {
'start',
'stop',
'logrotate',
'status',
'enter',
'restart',
'reload',
'eval'
}
--
-- true if we're running in HOME directory of a user
--
local usermode
--
-- a file with system-wide settings
--
local default_file
--
-- instance name
--
local instance_name
--
--
--
local instance_path
--
-- console socket
--
local console_sock
--
-- print usage and exit
--
local function usage()
log.error("Usage: %s {%s} instance_name",
arg[0], table.concat(available_commands, '|'))
log.error("Config file: %s", default_file)
os.exit(1)
end
--
-- shift argv to remove 'tarantoolctl' from arg[0]
--
local function shift_argv(arg, argno, argcount)
for i = argno, 128 do
arg[i] = arg[i + argcount]
if arg[i] == nil then
break
end
end
end
--
-- Check if the requested action is among
-- supported ones, and return it in this case
-- Otherwise print help and exit.
--
local function check_cmd(cmd)
for _, vcmd in pairs(available_commands) do
if cmd == vcmd then
return cmd
end
end
usage()
end
local function check_user_level()
-- local dir configuration
local pwd = os.getenv('PWD')
if pwd ~= nil then
local local_dir = pwd .. '/.tarantoolctl'
if fio.stat(local_dir) then
usermode = true
return local_dir
end
end
-- home dir configuration
if os.getenv('HOME') then
local c = os.getenv('HOME') .. '/.config/tarantool/tarantool'
if fio.stat(c) then
usermode = true
return c
end
end
return nil
end
--
-- Find if we're running under a user, and this
-- user has a default file in his home directory.
-- If present, use it. Otherwise assume a system-
-- wide default. If it's missing, it's OK as well.
--
local function find_default_file()
--
-- try to find local dir or user config
--
local user_level = check_user_level()
if user_level ~= nil then
return user_level
end
--
-- no user-level defaults, use a system-wide one
--
usermode = false
local config_list = {
'/etc/sysconfig/tarantool',
'/etc/default/tarantool',
'/usr/local/etc/tarantool/default/tarantool',
}
for _, c in pairs(config_list) do
if fio.stat(c) then
return c
end
end
-- It's OK if there is no default file -
-- load_default_file() will assume some defaults
return nil
end
--
-- System-wide default file may be missing, this is OK,
-- we'll assume built-in defaults
--
local group_name
function load_default_file(default_file)
if default_file then
dofile(default_file)
end
if default_cfg == nil then
default_cfg = {}
end
local d = default_cfg
d.pid_file = d.pid_file and d.pid_file or "/var/run/tarantool"
d.wal_dir = d.wal_dir and d.wal_dir or "/var/lib/tarantool"
d.snap_dir = d.snap_dir and d.snap_dir or "/var/lib/tarantool"
d.logger = d.logger and d.logger or "/var/log/tarantool"
d.sophia_dir = d.sophia_dir and d.sophia_dir or "/var/lib/tarantool"
d.pid_file = fio.pathjoin(d.pid_file, instance_name .. '.pid')
d.wal_dir = fio.pathjoin(d.wal_dir, instance_name)
d.snap_dir = fio.pathjoin(d.snap_dir, instance_name)
d.sophia_dir = fio.pathjoin(d.sophia_dir, instance_name)
d.logger = fio.pathjoin(d.logger, instance_name .. '.log')
if not usermode then
-- change user name only if not running locally
d.username = d.username and d.username or "tarantool"
--
-- instance_dir must be set in the defaults file,
-- but don't try to set it to the global instance dir
-- if the user-local defaults file is in use
--
if not instance_dir then
instance_dir = '/etc/tarantool/instances.enabled'
end
-- get user data
local user_data = ffi.C.getpwnam(ffi.cast('const char*', d.username))
if user_data == nil then
log.error('Unknown user: %s', d.username)
os.exit(-1)
end
-- get group data
local group = ffi.C.getgrgid(user_data.pw_gid)
if group == nil then
log.error('Group lookup by gid failed: %d',
user_data.pw_gid)
os.exit(-1)
end
group_name = ffi.string(group.gr_name)
end
if instance_dir == nil then
log.error('Instance directory (instance_dir) is not set in %s', default_file)
os.exit(-1)
end
if not fio.stat(instance_dir) then
log.error('Instance directory %s does not exist', instance_dir)
os.exit(-1)
end
end
--
-- In case there is no explicit instance name,
-- check whether arg[0] is a symlink. In that
-- case, the name of the symlink is the instance
-- name.
--
local function find_instance_name(arg0, arg2)
if arg2 ~= nil then
return fio.basename(arg2, '.lua')
end
local istat = fio.lstat(arg0)
if istat == nil then
log.error("Can't stat %s: %s", arg0, errno.strerror())
os.exit(1)
end
if not istat:is_link() then
usage()
end
return fio.basename(arg0, '.lua')
end
local function mkdir(dirname)
log.info("mkdir %s", dirname)
if not fio.mkdir(dirname, tonumber('0755', 8)) then
log.error("Can't mkdir %s: %s", dirname, errno.strerror())
os.exit(-1)
end
if not usermode and not fio.chown(dirname, default_cfg.username, group_name) then
log.error("Can't chown(%s, %s, %s): %s",
default_cfg.username, group_name, dirname, errno.strerror())
end
end
local function read_file(filename)
local file = fio.open(filename, {'O_RDONLY'})
local buf = {}
local i = 1
while true do
buf[i] = file:read(1024)
if buf[i] == '' then
break
end
i = i + 1
end
return table.concat(buf)
end
function mk_default_dirs(cfg)
-- create pid_dir
local pid_dir = fio.dirname(cfg.pid_file)
if fio.stat(pid_dir) == nil then
mkdir(pid_dir)
end
-- create wal_dir
if fio.stat(cfg.wal_dir) == nil then
mkdir(cfg.wal_dir)
end
-- create snap_dir
if fio.stat(cfg.snap_dir) == nil then
mkdir(cfg.snap_dir)
end
-- create sophia_dir
if fio.stat(cfg.sophia_dir) == nil then
mkdir(cfg.sophia_dir)
end
-- create log_dir
local log_dir = fio.dirname(cfg.logger)
if log_dir:find('|') == nil and fio.stat(log_dir) == nil then
mkdir(log_dir)
end
end
local orig_cfg = box.cfg
local function wrapper_cfg(cfg)
for i, v in pairs(default_cfg) do
if cfg[i] == nil then
cfg[i] = v
end
end
--
-- force these startup options
--
cfg.pid_file = default_cfg.pid_file
cfg.username = (os.getenv('USER') == default_cfg.username and default_cfg.username) or nil
if cfg.background == nil then
cfg.background = true
end
mk_default_dirs(cfg)
local res = orig_cfg(cfg)
require('fiber').name(instance_name)
log.info('Run console at %s', console_sock)
console.listen(console_sock)
return res
end
function stop()
log.info("Stopping instance...")
local pid_file = default_cfg.pid_file
if fio.stat(pid_file) == nil then
log.error("Process is not running (pid: %s)", pid_file)
return 0
end
local f = fio.open(pid_file, 'O_RDONLY')
if f == nil then
log.error("Can't read pid file %s: %s", pid_file, errno.strerror())
return -1
end
local str = f:read(64)
f:close()
local pid = tonumber(str)
if pid == nil or pid <= 0 then
log.error("Broken pid file %s", pid_file)
fio.unlink(pid_file)
return -1
end
if ffi.C.kill(pid, 15) < 0 then
log.error("Can't kill process %d: %s", pid, errno.strerror())
fio.unlink(pid_file)
return -1
end
return 0
end
function start()
log.info("Starting instance...")
box.cfg = wrapper_cfg
require('title').update{
script_name = instance_path,
__defer_update = true
}
local success, data = pcall(dofile, instance_path)
-- if load fails - show last 10 lines of the log file
if not success then
print('Start failed: ' .. data)
if fio.stat(default_cfg.logger) then
os.execute('tail -n 10 ' .. default_cfg.logger)
end
end
end
default_file = find_default_file()
instance_name = find_instance_name(arg[0], arg[2])
local cmd = check_cmd(arg[1])
load_default_file(default_file)
--
-- Pass the rest of command line arguments to the
-- instsance
--
shift_argv(arg, 0, 2)
instance_path = fio.pathjoin(instance_dir, instance_name .. '.lua')
if not fio.stat(instance_path) then
log.error('Instance %s is not found in %s', instance_name, instance_dir)
os.exit(-1)
end
log.info('Found %s.lua in %s', instance_name, instance_dir)
-- create a path to the control socket (admin console)
console_sock = fio.pathjoin(fio.dirname(default_cfg.pid_file),
instance_name .. '.control')
if cmd == 'start' then
start()
elseif cmd == 'stop' then
os.exit(stop())
elseif cmd == 'restart' then
stop()
fiber.sleep(1)
start()
elseif cmd == 'logrotate' then
if fio.stat(console_sock) == nil then
-- process is not running, do nothing
os.exit(0)
end
local s = socket.tcp_connect('unix/', console_sock)
if s == nil then
-- socket is not opened, do nothing
os.exit(0)
end
s:write[[
require('log'):rotate()
require('log').info("Rotate log file")
]]
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)
elseif cmd == 'status' then
local pid_file = default_cfg.pid_file
if fio.stat(pid_file) == nil then
if errno() == errno.ENOENT then
print(instance_name .. ' is stopped (pid file does not exist)')
os.exit(1)
end
log.error("Cant access pidfile %s: %s", pid_file, errno.strerror())
end
if fio.stat(console_sock) == nil then
if errno() == errno.ENOENT then
log.warn("pid file exists, but the control socket (%s) doesn't",
console_sock)
os.exit(2)
end
end
local s = socket.tcp_connect('unix/', console_sock)
if s == nil then
if errno() ~= errno.EACCES then
log.warn("Can't access control socket %s: %s", console_sock,
errno.strerror())
os.exit(3)
else
os.exit(0)
end
end
s:close()
print(instance_name .. ' is running (pid:' .. default_cfg.pid_file .. ')')
os.exit(0)
elseif cmd == 'reload' or cmd == 'eval' then
local filename = arg[1]
if filename == nil then
log.error("Usage: tarantoolctl eval instance_name file.lua")
os.exit(1)
end
if fio.stat(filename) == nil then
if errno() == errno.ENOENT then
print(filename .. ': file not found')
os.exit(1)
end
end
content = digest.base64_encode(read_file(filename))
if fio.stat(console_sock) == nil then
log.warn("pid file exists, but the control socket (%s) doesn't",
console_sock)
os.exit(2)
end
local u = urilib.parse(console_sock)
local remote = require('net.box'):new(u.host, u.service,
{ user = u.login, password = u.password })
local code = string.format(
'loadstring(require("digest").base64_decode([[%s]]))()',
content
)
remote:console(code)
os.exit(0)
else
log.error("Unknown command '%s'", cmd)
os.exit(-1)
end
-- vim: syntax=lua