Skip to content
Snippets Groups Projects
scripting.rst 12.29 KiB

Run-time reconfiguration

Knot Resolver offers several ways to modify its configuration at run-time:

  • Using control socket driven by an external system
  • Using Lua program embedded in Resolver's configuration file

Both ways can also be combined: For example the configuration file can contain a little Lua function which gathers statistics and returns them in JSON string. This can be used by an external system which uses control socket to call this user-defined function and to retrieve its results.

Control sockets

Control socket acts like "an interactive configuration file" so all actions available in configuration file can be executed interactively using the control socket. One possible use-case is reconfiguring the resolver instances from another program, e.g. a maintenance script.

Note

Each instance of Knot Resolver exposes its own control socket. Take that into account when scripting deployments with :ref:`systemd-multiple-instances`.

When Knot Resolver is started using Systemd (see section :ref:`quickstart-startup`) it creates a control socket in path /run/knot-resolver/control/$ID. Connection to the socket can be made from command line using e.g. socat:

$ socat - UNIX-CONNECT:/run/knot-resolver/control/1

When successfully connected to a socket, the command line should change to something like >. Then you can interact with kresd to see configuration or set a new one. There are some basic commands to start with.

> help()            -- shows help
> net.interfaces()  -- lists available interfaces
> net.list()        -- lists running network services

The direct output of commands sent over socket is captured and sent back, which gives you an immediate response on the outcome of your command. The commands and their output are also logged in contrl group, on debug level if successful or warning level if failed (see around :func:`log_level`).

Control sockets are also a way to enumerate and test running instances, the list of sockets corresponds to the list of processes, and you can test the process for liveliness by connecting to the UNIX socket.

Lua scripts

As it was mentioned in section :ref:`config-syntax`, Resolver's configuration file contains program in Lua programming language. This allows you to write dynamic rules and helps you to avoid repetitive templating that is unavoidable with static configuration. For example parts of configuration can depend on :func:`hostname` of the machine:

if hostname() == 'hidden' then
        net.listen(net.eth0, 5353)
else
        net.listen('127.0.0.1')
        net.listen(net.eth1.addr[1])
end

Another example would show how it is possible to bind to all interfaces, using iteration.

for name, addr_list in pairs(net.interfaces()) do
        net.listen(addr_list)
end

Tip

Some users observed a considerable, close to 100%, performance gain in Docker containers when they bound the daemon to a single interface:ip address pair. One may expand the aforementioned example with browsing available addresses as:

addrpref = env.EXPECTED_ADDR_PREFIX
for k, v in pairs(addr_list["addr"]) do
        if string.sub(v,1,string.len(addrpref)) == addrpref then
                net.listen(v)
...

You can also use third-party Lua libraries (available for example through LuaRocks) as on this example to download cache from parent, to avoid cold-cache start.

local http = require('socket.http')
local ltn12 = require('ltn12')

local cache_size = 100*MB
local cache_path = '/var/cache/knot-resolver'
cache.open(cache_size, 'lmdb://' .. cache_path)
if cache.count() == 0 then
        cache.close()
        -- download cache from parent
        http.request {
                url = 'http://parent/data.mdb',
                sink = ltn12.sink.file(io.open(cache_path .. '/data.mdb', 'w'))
        }
        -- reopen cache with 100M limit
        cache.open(cache_size, 'lmdb://' .. cache_path)
end

Helper functions

Following built-in functions are useful for scripting:

Asynchronous events

Lua language used in configuration file allows you to script actions upon various events, for example publish statistics each minute. Following example uses built-in function :func:`event.recurrent()` which calls user-supplied anonymous function:

local ffi = require('ffi')
modules.load('stats')

-- log statistics every second
local stat_id = event.recurrent(1 * second, function(evid)
     log_info(ffi.C.LOG_GRP_STATISTICS, table_print(stats.list()))
end)

-- stop printing statistics after first minute
event.after(1 * minute, function(evid)
     event.cancel(stat_id)
end)

Note that each scheduled event is identified by a number valid for the duration of the event, you may use it to cancel the event at any time.

To persist state between two invocations of a function Lua uses concept called closures. In the following example function speed_monitor() is a closure function, which provides persistent variable called previous.

local ffi = require('ffi')
modules.load('stats')

-- make a closure, encapsulating counter
function speed_monitor()
    local previous = stats.list()
    -- monitoring function
    return function(evid)
        local now = stats.list()
        local total_increment = now['answer.total'] - previous['answer.total']
        local slow_increment = now['answer.slow'] - previous['answer.slow']
        if slow_increment / total_increment > 0.05 then
            log_warn(ffi.C.LOG_GRP_STATISTICS, 'WARNING! More than 5 %% of queries was slow!')
        end
        previous = now  -- store current value in closure
     end
 end

 -- monitor every minute
 local monitor_id = event.recurrent(1 * minute, speed_monitor())

Another type of actionable event is activity on a file descriptor. This allows you to embed other event loops or monitor open files and then fire a callback when an activity is detected. This allows you to build persistent services like monitoring probes that cooperate well with the daemon internal operations. See :func:`event.socket()`.

Filesystem watchers are possible with :func:`worker.coroutine()` and cqueues, see the cqueues documentation for more information. Here is an simple example:

local notify = require('cqueues.notify')
local watcher = notify.opendir('/etc')
watcher:add('hosts')

-- Watch changes to /etc/hosts
worker.coroutine(function ()
  for flags, name in watcher:changes() do
    for flag in notify.flags(flags) do
      -- print information about the modified file
      print(name, notify[flag])
    end
  end
end)