From 7d9c43916b0600ac4879dfe9793eab807a83ab2b Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Mon, 10 Mar 2014 22:45:18 +0200 Subject: rename ACF2 to Alpine Configurator (aconf) --- aconf/persistence/backends/augeas.lua | 273 ++++++++++++++++++++++++++++++++ aconf/persistence/backends/files.lua | 107 +++++++++++++ aconf/persistence/backends/json.lua | 79 +++++++++ aconf/persistence/backends/null.lua | 10 ++ aconf/persistence/backends/service.lua | 32 ++++ aconf/persistence/backends/volatile.lua | 46 ++++++ aconf/persistence/defer.lua | 47 ++++++ aconf/persistence/init.lua | 113 +++++++++++++ aconf/persistence/util.lua | 27 ++++ 9 files changed, 734 insertions(+) create mode 100644 aconf/persistence/backends/augeas.lua create mode 100644 aconf/persistence/backends/files.lua create mode 100644 aconf/persistence/backends/json.lua create mode 100644 aconf/persistence/backends/null.lua create mode 100644 aconf/persistence/backends/service.lua create mode 100644 aconf/persistence/backends/volatile.lua create mode 100644 aconf/persistence/defer.lua create mode 100644 aconf/persistence/init.lua create mode 100644 aconf/persistence/util.lua (limited to 'aconf/persistence') 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 diff --git a/aconf/persistence/defer.lua b/aconf/persistence/defer.lua new file mode 100644 index 0000000..5db5a21 --- /dev/null +++ b/aconf/persistence/defer.lua @@ -0,0 +1,47 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local object = require('aconf.object') +local super = object.super + +local pth = require('aconf.path') + + +local DeferringCommitter = object.class( + require('aconf.transaction.base').Transaction +) + +function DeferringCommitter:init(backend) + super(self, DeferringCommitter):init(backend) + self.defer_paths = {} + self.committed = true +end + +function DeferringCommitter:defer(path) self.defer_paths[path] = true end + +function DeferringCommitter:_set_multiple(mods) + super(self, DeferringCommitter):_set_multiple(mods) + + if not self.committed then return end + self.committed = false + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + while path > '/' do + if self.defer_paths[path] then return end + path = pth.parent(path) + end + end + + self:commit() +end + +function DeferringCommitter:commit() + super(self, DeferringCommitter):commit() + self.committed = true +end + + +return DeferringCommitter(require('aconf.persistence')) diff --git a/aconf/persistence/init.lua b/aconf/persistence/init.lua new file mode 100644 index 0000000..991776a --- /dev/null +++ b/aconf/persistence/init.lua @@ -0,0 +1,113 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local loadmods = require('aconf.loader') +local topology = require('aconf.model.root').topology +local object = require('aconf.object') +local pth = require('aconf.path') + +local util = require('aconf.util') +local contains = util.contains +local setdefault = util.setdefault + +local stringy = require('stringy') + + +local DataStore = object.class( + require('aconf.transaction.base').TransactionBackend +) + +function DataStore:init() + object.super(self, DataStore):init() + self.backends = util.map( + function(m) return m() end, + loadmods('persistence/backends') + ) + self.triggers = {pre={}, post={}} +end + +function DataStore:trigger(phase, path, func) + local funcs = setdefault(self.triggers[phase], path, {}) + if not contains(funcs, func) then table.insert(funcs, func) end +end + +function DataStore:split_path(path) + local comps = pth.split(path) + local backend = self.backends[comps[1]] + assert(backend) + table.remove(comps, 1) + return backend, comps +end + +function DataStore:get(path) + local backend, comps = self:split_path(path) + local top = topology(path) + + local res = backend:get(comps, top) + + if top then + local t = top.type + if t and res ~= nil then + local atype = type(res) + + if t == 'table' then assert(atype == 'table') + + else + assert(atype ~= 'table') + + if t == 'string' then res = tostring(res) + elseif t == 'number' then res = tonumber(res) + + elseif t == 'boolean' then + if atype == 'string' then res = res:lower() end + if res == 1 or contains({'1', 't', 'true', 'y', 'yes'}, res) then + res = true + elseif res == 0 or contains( + {'0', 'f', 'false', 'n', 'no'}, res + ) then + res = false + else res = res and true or false end + + elseif contains({'binary', 'reference'}, t) then + assert(atype == 'string') + + else assert(false) end + end + end + end + + return util.copy(res), self.mod_time[path] or 0 +end + +function DataStore:_set_multiple(mods) + local bms = {} + local trigger = {} + + for _, mod in ipairs(mods) do + local path, value = unpack(mod) + + local tp = path + while not trigger[tp] do + trigger[tp] = true + tp = pth.parent(tp) + end + + local backend, comps = self:split_path(path) + table.insert(setdefault(bms, backend, {}), {comps, value}) + end + + local function exec_triggers(phase) + for path, _ in pairs(trigger) do + for _, func in ipairs(self.triggers[phase][path] or {}) do func() end + end + end + + exec_triggers('pre') + for backend, bm in pairs(bms) do backend:set(bm) end + exec_triggers('post') +end + + +return DataStore() diff --git a/aconf/persistence/util.lua b/aconf/persistence/util.lua new file mode 100644 index 0000000..d233b1d --- /dev/null +++ b/aconf/persistence/util.lua @@ -0,0 +1,27 @@ +--[[ +Copyright (c) 2012-2014 Kaarle Ritvanen +See LICENSE file for license details +--]] + +local M = {} + +function M.open_file(path, mode) + local file = io.open(path, mode) + if not file then error('Cannot open file: '..path) end + return file +end + +function M.read_file(path) + local file = M.open_file(path) + local data = file:read('*all') + file:close() + return data +end + +function M.tostring(value) + -- TODO make values configurable per address + if type(value) == 'boolean' then return value and 'yes' or 'no' end + return tostring(value) +end + +return M -- cgit v1.2.3