summaryrefslogtreecommitdiffstats
path: root/aconf/persistence/backends
diff options
context:
space:
mode:
authorKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-10 22:45:18 +0200
committerKaarle Ritvanen <kaarle.ritvanen@datakunkku.fi>2014-03-24 01:18:13 +0200
commit7d9c43916b0600ac4879dfe9793eab807a83ab2b (patch)
treeec54ed64c9a557b6ea4ad88d31138a02d3e0cd04 /aconf/persistence/backends
parentcb6c243dc356ef1d46d7ddb96e6ea6ae007c6cca (diff)
downloadaconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.bz2
aconf-7d9c43916b0600ac4879dfe9793eab807a83ab2b.tar.xz
rename ACF2 to Alpine Configurator (aconf)
Diffstat (limited to 'aconf/persistence/backends')
-rw-r--r--aconf/persistence/backends/augeas.lua273
-rw-r--r--aconf/persistence/backends/files.lua107
-rw-r--r--aconf/persistence/backends/json.lua79
-rw-r--r--aconf/persistence/backends/null.lua10
-rw-r--r--aconf/persistence/backends/service.lua32
-rw-r--r--aconf/persistence/backends/volatile.lua46
6 files changed, 547 insertions, 0 deletions
diff --git a/aconf/persistence/backends/augeas.lua b/aconf/persistence/backends/augeas.lua
new file mode 100644
index 0000000..a448e65
--- /dev/null
+++ b/aconf/persistence/backends/augeas.lua
@@ -0,0 +1,273 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local topology = require('aconf.model.root').topology
+local class = require('aconf.object').class
+local pth = require('aconf.path')
+local tostr = require('aconf.persistence.util').tostring
+
+local util = require('aconf.util')
+local copy = util.copy
+
+
+local stringy = require('stringy')
+
+
+local function array_join(tbl, value)
+ local res = copy(tbl)
+ table.insert(res, value)
+ return res
+end
+
+local function array_without_last(tbl)
+ local res = copy(tbl)
+ res[#res] = nil
+ return res
+end
+
+
+local function basename(path)
+ local res, pred = path:match('^.*/([#%w._-]+)([^/]*)$')
+ assert(res)
+ assert(res ~= '#')
+ assert(pred == '' or pred:match('^%[.+%]$'))
+ return res
+end
+
+local function append_pred(path, pred) return path..'['..pred..']' end
+
+
+local function key_mode(mode) return mode and stringy.startswith(mode, '@') end
+
+local function key(mode)
+ assert(key_mode(mode))
+ return mode == '@' and '.' or mode:sub(2, -1)
+end
+
+local function append_key_pred(path, mode, value)
+ return append_pred(path, key(mode).." = '"..value.."'")
+end
+
+
+local function conv_path(path)
+ local res = '/files'
+ if #path == 0 then return res, nil, {} end
+
+ path = copy(path)
+ local mode
+ local keys = {}
+
+ repeat
+ local comp = path[1]
+
+ if comp == '#' or key_mode(comp) then
+ assert(not mode)
+ mode = comp
+ elseif not mode then
+ res = res..'/'..comp
+ keys = {}
+ else
+ if mode == '#' then
+ assert(type(comp) == 'number')
+ res = append_pred(res, comp)
+ else
+ assert(type(comp) == 'string' and comp:match('^[%w %_-%.]+$'))
+ res = append_key_pred(res, mode, comp)
+ table.insert(keys, key(mode))
+ end
+ mode = nil
+ end
+
+ table.remove(path, 1)
+ until #path == 0
+
+ return res, mode, keys
+end
+
+
+local function aug_top(path)
+ path = copy(path)
+ table.insert(path, 1, 'augeas')
+ return topology(path)
+end
+
+
+local backend = class()
+
+function backend:init() self.aug = require('augeas').init() end
+
+function backend:get(path, top)
+ local apath, mode, keys = conv_path(path)
+ local existence = top == true
+ if existence or not top then top = aug_top(path) end
+
+ local tpe = top and top.type
+ local leaf = tpe ~= 'table' and not mode
+
+ local matches = self.aug:match(apath..(not leaf and not mode and '/*' or ''))
+
+ if #matches == 0 and not mode then return end
+
+ if mode and #path > 1 and not self:get(
+ array_without_last(array_without_last(path)), true
+ ) then
+ return
+ end
+
+ if not tpe and not mode then
+ if #matches > 1 then
+ leaf = false
+ mode = '#'
+ else
+ local children = self.aug:match(apath..'/*')
+ if #children > 0 then
+ leaf = false
+ matches = children
+ end
+ end
+ end
+
+ if leaf then
+ assert(#matches == 1)
+ return self.aug:get(apath)
+ end
+
+ if existence then return true end
+
+ local names = {}
+
+ for i, child in ipairs(matches) do
+ local name
+ if not mode then
+ name = basename(child)
+ name = tonumber(name) or name
+ if util.contains(keys, name) then name = nil end
+ elseif mode == '#' then name = i
+ else
+ name = self.aug:get(child..(mode == '@' and '' or '/'..key(mode)))
+ end
+
+ if name and self:get(array_join(path, name), true) then
+ names[name] = true
+ end
+ end
+
+ return util.keys(names)
+end
+
+function backend:set(mods)
+ local gc = {}
+
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+
+ local function insert(path, new)
+ local apath, mode, keys = conv_path(path)
+ if mode then path[#path] = nil end
+ if #path == 0 then return apath, keys end
+ local name = path[#path]
+
+ local parent = array_without_last(path)
+ local ppath, pmode = conv_path(parent)
+
+ if pmode then
+ gc[pth.join(unpack(array_without_last(parent)))] = false
+ end
+
+ if pmode == '#' then
+ local count = #self.aug:match(ppath)
+ while count < name do
+ insert(parent, true)
+ count = count + 1
+ end
+ return apath, keys
+ end
+
+ local matches = self.aug:match(apath)
+ local count = #matches
+
+ if count > 0 and not new then return apath, keys end
+
+ if key_mode(pmode) then
+ apath = pmode == '@' and append_key_pred(
+ ppath, '@', ''
+ ) or append_pred(ppath, 'count('..key(pmode)..') = 0')
+
+ matches = self.aug:match(apath)
+ assert(#matches < 2)
+ apath = matches[1] or insert(parent, true)
+
+ local key = key(pmode)
+ self.aug:set(apath..'/'..key, name)
+
+ return apath, keys
+ end
+
+ if #matches == 0 then
+ matches = self.aug:match(ppath..'/*')
+
+ local function order(path)
+ local top = aug_top(path)
+ return top and top.order
+ end
+ local ord = order(path)
+
+ for _, sibling in ipairs(matches) do
+ local sord = order(array_join(parent, basename(sibling)))
+ if sord and sord > ord then
+ self.aug:insert(sibling, name, true)
+ return apath, keys
+ end
+ end
+ end
+
+ if #matches == 0 then
+ if new then self.aug:set(apath, nil) end
+ return apath, keys
+ end
+
+ self.aug:insert(matches[#matches], name)
+ return append_pred(apath, count + 1), keys
+ end
+
+ local apath, keys = insert(path)
+ local is_table = type(value) == 'table'
+
+ if not (is_table or util.contains(keys, '.')) then
+ self.aug:set(apath, value ~= nil and tostr(value) or nil)
+ end
+
+ util.setdefault(gc, pth.join(unpack(path)), true)
+ end
+
+ for path, _ in pairs(gc) do
+ local p = pth.split(path)
+ while #p > 0 do
+ local value = self:get(p)
+
+ if (
+ type(value) == 'string' and value ~= ''
+ ) or (
+ type(value) == 'table' and #value > 0
+ ) then
+ break
+ end
+
+ if gc[pth.join(unpack(p))] ~= false then self.aug:rm(conv_path(p)) end
+ p[#p] = nil
+ end
+ end
+
+ if self.aug:save() ~= 0 then
+ print('Augeas save failed')
+ for _, ep in ipairs(self.aug:match('/augeas//error')) do
+ print(ep, self.aug:get(ep))
+ end
+ assert(false)
+ end
+end
+
+
+return backend
diff --git a/aconf/persistence/backends/files.lua b/aconf/persistence/backends/files.lua
new file mode 100644
index 0000000..e19763d
--- /dev/null
+++ b/aconf/persistence/backends/files.lua
@@ -0,0 +1,107 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local topology = require('aconf.model.root').topology
+local pth = require('aconf.path')
+local util = require('aconf.persistence.util')
+local copy = require('aconf.util').copy
+
+local posix = require('posix')
+local stringy = require('stringy')
+
+
+local function get_scope(top)
+ if not top or top.type ~= 'reference' or not pth.is_unique(top.scope) then
+ return
+ end
+
+ return stringy.startswith(
+ top.scope, '/files/'
+ ) and top.scope:sub(7, -1) or nil
+end
+
+
+local backend = require('aconf.object').class()
+
+-- TODO cache expiration
+function backend:init() self.cache = {} end
+
+function backend:get(path, top)
+ local name = pth.join('/', unpack(path))
+
+ if not self.cache[name] then
+ local t = posix.stat(name, 'type')
+ if not t then return end
+
+ if t == 'regular' then
+ self.cache[name] = util.read_file(name)
+
+ elseif t == 'link' then
+ -- TODO handle relative symlinks
+ local target = posix.readlink(name)
+ assert(target)
+
+ local scope = get_scope(top)
+ assert(scope)
+ scope = scope..'/'
+
+ local slen = scope:len()
+ assert(target:sub(1, slen) == scope)
+ return target:sub(slen + 1, -1)
+
+ elseif t == 'directory' then
+ local res = {}
+ for _, fname in ipairs(posix.dir(name)) do
+ if not ({['.']=true, ['..']=true})[fname] then
+ table.insert(res, pth.name(fname))
+ end
+ end
+ return res
+
+ else error('Unsupported file type: '..name) end
+ end
+
+ return self.cache[name]
+end
+
+function backend:set(mods)
+ for _, mod in pairs(mods) do
+ local path, value = unpack(mod)
+ local name = pth.join('/', unpack(path))
+
+ if value == nil then
+ print('DEL', name)
+
+ local t = posix.stat(name, 'type')
+ if t == 'directory' then
+ assert(posix.rmdir(name))
+ elseif t then assert(os.remove(name)) end
+
+ self.cache[name] = nil
+
+ elseif type(value) == 'table' then
+ assert(posix.mkdir(name))
+
+ else
+ local scope = get_scope(topology('/files'..name))
+
+ if scope then
+ -- TODO use relative symlink
+ os.remove(name)
+ assert(posix.link(pth.to_absolute(value, scope), name, true))
+
+ else
+ local file = util.open_file(name, 'w')
+ file:write(util.tostring(value))
+ file:close()
+
+ self.cache[name] = value
+ end
+ end
+ end
+end
+
+
+return backend
diff --git a/aconf/persistence/backends/json.lua b/aconf/persistence/backends/json.lua
new file mode 100644
index 0000000..607315a
--- /dev/null
+++ b/aconf/persistence/backends/json.lua
@@ -0,0 +1,79 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local pth = require('aconf.path')
+local Cache = require('aconf.persistence.backends.volatile')
+local util = require('aconf.persistence.util')
+local copy = require('aconf.util').copy
+
+local json = require('cjson')
+local posix = require('posix')
+
+
+local backend = require('aconf.object').class()
+
+function backend:init()
+ -- TODO cache expiration
+ self.cache = {}
+ self.dirty = {}
+end
+
+function backend:split_path(path)
+ local fpath = copy(path)
+ local jpath = {}
+ local res
+
+ while #fpath > 0 do
+ local fp = pth.join('/', unpack(fpath))
+ if self.cache[fp] then return fp, jpath end
+ table.insert(jpath, 1, fpath[#fpath])
+ table.remove(fpath)
+ end
+
+ fpath = '/'
+
+ while true do
+ fpath = pth.join(fpath, jpath[1])
+ table.remove(jpath, 1)
+
+ local t = posix.stat(fpath, 'type')
+ if t == 'link' then t = posix.stat(posix.readlink(fpath), 'type') end
+ if not t or not ({directory=true, regular=true})[t] then
+ error('File or directory does not exist: '..fpath)
+ end
+
+ if t == 'regular' then return fpath, jpath end
+
+ assert(#jpath > 0)
+ end
+end
+
+function backend:get(path, top)
+ local fpath, jpath = self:split_path(path)
+ if not self.cache[fpath] then
+ self.cache[fpath] = Cache(json.decode(util.read_file(fpath)))
+ end
+ return self.cache[fpath]:get(jpath, top)
+end
+
+function backend:set(mods)
+ local dirty = {}
+
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+ local fpath, jpath = self:split_path(path)
+ self.cache[fpath]:_set(jpath, value)
+ dirty[fpath] = true
+ end
+
+ for path, _ in pairs(dirty) do
+ local file = util.open_file(path, 'w')
+ file:write(json.encode(self.cache[path]:_get{}))
+ file:close()
+ end
+end
+
+
+return backend
diff --git a/aconf/persistence/backends/null.lua b/aconf/persistence/backends/null.lua
new file mode 100644
index 0000000..770e5e8
--- /dev/null
+++ b/aconf/persistence/backends/null.lua
@@ -0,0 +1,10 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local backend = require('aconf.object').class()
+function backend:get(path, top) if #path == 0 then return {} end end
+function backend:set(mods) end
+
+return backend
diff --git a/aconf/persistence/backends/service.lua b/aconf/persistence/backends/service.lua
new file mode 100644
index 0000000..4569ce8
--- /dev/null
+++ b/aconf/persistence/backends/service.lua
@@ -0,0 +1,32 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local rc = require('rc')
+local stringy = require('stringy')
+
+
+local backend = require('aconf.object').class()
+
+function backend:get(path, top)
+ if #path == 1 then return {'enabled', 'status'} end
+ assert(#path == 2)
+ local status = rc.service_status(path[1])
+ if path[2] == 'status' then return status end
+ if path[2] == 'enabled' then return stringy.startswith(status, 'start') end
+end
+
+function backend:set(mods)
+ for _, mod in ipairs(mods) do
+ local path, value = unpack(mod)
+ assert(#path == 2 and path[2] == 'enabled')
+
+ local name = path[1]
+ if value then rc.service_add(name)
+ else rc.service_delete(name) end
+ os.execute('rc-service '..name..' '..(value and 'start' or 'stop'))
+ end
+end
+
+return backend
diff --git a/aconf/persistence/backends/volatile.lua b/aconf/persistence/backends/volatile.lua
new file mode 100644
index 0000000..7016a9b
--- /dev/null
+++ b/aconf/persistence/backends/volatile.lua
@@ -0,0 +1,46 @@
+--[[
+Copyright (c) 2012-2014 Kaarle Ritvanen
+See LICENSE file for license details
+--]]
+
+local util = require('aconf.util')
+
+
+local backend = require('aconf.object').class()
+
+function backend:init(data) self.data = data or {} end
+
+function backend:_get(path)
+ local res = self.data
+ for _, comp in ipairs(path) do
+ if res == nil then return end
+ assert(type(res) == 'table')
+ res = res[comp]
+ end
+ return res
+end
+
+function backend:get(path, top)
+ local res = self:_get(path)
+ return type(res) == 'table' and util.keys(res) or res
+end
+
+function backend:_set(path, value)
+ if type(value) == 'table' then value = {} end
+
+ if #path == 0 then self.data = value
+
+ else
+ local comps = util.copy(path)
+ local name = comps[#comps]
+ table.remove(comps)
+ self:_get(comps)[name] = value
+ end
+end
+
+function backend:set(mods)
+ for _, mod in ipairs(mods) do self:_set(unpack(mod)) end
+end
+
+
+return backend