luci-base: break circular luci.config <> luci.model.uci dependency
[project/luci.git] / modules / luci-base / luasrc / model / uci.lua
index 8ac8277..461ba9d 100644 (file)
 -- Licensed to the public under the Apache License 2.0.
 
 local os    = require "os"
-local uci   = require "uci"
 local util  = require "luci.util"
 local table = require "table"
 
 
 local setmetatable, rawget, rawset = setmetatable, rawget, rawset
-local require, getmetatable = require, getmetatable
+local require, getmetatable, assert = require, getmetatable, assert
 local error, pairs, ipairs = error, pairs, ipairs
 local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack
 
---- LuCI UCI model library.
 -- The typical workflow for UCI is:  Get a cursor instance from the
 -- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.),
 -- save the changes to the staging area via Cursor.save and finally
 -- Cursor.commit the data to the actual config files.
 -- LuCI then needs to Cursor.apply the changes so deamons etc. are
 -- reloaded.
--- @cstyle     instance
 module "luci.model.uci"
 
---- Create a new UCI-Cursor.
--- @class function
--- @name cursor
--- @return     UCI-Cursor
-cursor = uci.cursor
+local ERRSTR = {
+       "Invalid command",
+       "Invalid argument",
+       "Method not found",
+       "Entry not found",
+       "No data",
+       "Permission denied",
+       "Timeout",
+       "Not supported",
+       "Unknown error",
+       "Connection failed"
+}
+
+local session_id = nil
+
+local function call(cmd, args)
+       if type(args) == "table" and session_id then
+               args.ubus_rpc_session = session_id
+       end
+       return util.ubus("uci", cmd, args)
+end
 
-APIVERSION = uci.APIVERSION
 
---- Create a new Cursor initialized to the state directory.
--- @return UCI cursor
+function cursor()
+       return _M
+end
+
 function cursor_state()
-       return cursor(nil, "/var/state")
+       return _M
 end
 
+function substate(self)
+       return self
+end
 
-inst = cursor()
-inst_state = cursor_state()
 
-local Cursor = getmetatable(inst)
+function get_confdir(self)
+       return "/etc/config"
+end
 
---- Applies UCI configuration changes
--- @param configlist           List of UCI configurations
--- @param command                      Don't apply only return the command
-function Cursor.apply(self, configlist, command)
-       configlist = self:_affected(configlist)
-       if command then
-               return { "/sbin/luci-reload", unpack(configlist) }
-       else
-               return os.execute("/sbin/luci-reload %s >/dev/null 2>&1"
-                       % table.concat(configlist, " "))
-       end
+function get_savedir(self)
+       return "/tmp/.uci"
 end
 
+function get_session_id(self)
+       return session_id
+end
 
---- Delete all sections of a given type that match certain criteria.
--- @param config               UCI config
--- @param type                 UCI section type
--- @param comparator   Function that will be called for each section and
--- returns a boolean whether to delete the current section (optional)
-function Cursor.delete_all(self, config, stype, comparator)
-       local del = {}
+function set_confdir(self, directory)
+       return false
+end
 
-       if type(comparator) == "table" then
-               local tbl = comparator
-               comparator = function(section)
-                       for k, v in pairs(tbl) do
-                               if section[k] ~= v then
-                                       return false
+function set_savedir(self, directory)
+       return false
+end
+
+function set_session_id(self, id)
+       session_id = id
+       return true
+end
+
+
+function load(self, config)
+       return true
+end
+
+function save(self, config)
+       return true
+end
+
+function unload(self, config)
+       return true
+end
+
+
+function changes(self, config)
+       local rv = call("changes", { config = config })
+       local res = {}
+
+       if type(rv) == "table" and type(rv.changes) == "table" then
+               local package, changes
+               for package, changes in pairs(rv.changes) do
+                       res[package] = {}
+
+                       local _, change
+                       for _, change in ipairs(changes) do
+                               local operation, section, option, value = unpack(change)
+                               if option and value and operation ~= "add" then
+                                       res[package][section] = res[package][section] or { }
+
+                                       if operation == "list-add" then
+                                               local v = res[package][section][option]
+                                               if type(v) == "table" then
+                                                       v[#v+1] = value or ""
+                                               elseif v ~= nil then
+                                                       res[package][section][option] = { v, value }
+                                               else
+                                                       res[package][section][option] = { value }
+                                               end
+                                       else
+                                               res[package][section][option] = value or ""
+                                       end
+                               else
+                                       res[package][section] = res[package][section] or {}
+                                       res[package][section][".type"] = option or ""
                                end
                        end
-                       return true
                end
        end
 
-       local function helper (section)
+       return res
+end
+
+
+function revert(self, config)
+       local _, err = call("revert", { config = config })
+       return (err == nil), ERRSTR[err]
+end
+
+function commit(self, config)
+       local _, err = call("commit", { config = config })
+       return (err == nil), ERRSTR[err]
+end
+
+function apply(self, rollback)
+       local _, err
 
-               if not comparator or comparator(section) then
-                       del[#del+1] = section[".name"]
+       if rollback then
+               local conf = require "luci.config"
+               local timeout = tonumber(conf and conf.apply and conf.apply.rollback or "") or 0
+
+               _, err = call("apply", {
+                       timeout  = (timeout > 30) and timeout or 30,
+                       rollback = true
+               })
+
+               if not err then
+                       util.ubus("session", "set", {
+                               ubus_rpc_session = session_id,
+                               values = { rollback = os.time() + timeout }
+                       })
+               end
+       else
+               _, err = call("changes", {})
+
+               if not err then
+                       if type(_) == "table" and type(_.changes) == "table" then
+                               local k, v
+                               for k, v in pairs(_.changes) do
+                                       _, err = call("commit", { config = k })
+                                       if err then
+                                               break
+                                       end
+                               end
+                       end
+               end
+
+               if not err then
+                       _, err = call("apply", { rollback = false })
                end
        end
 
-       self:foreach(config, stype, helper)
+       return (err == nil), ERRSTR[err]
+end
 
-       for i, j in ipairs(del) do
-               self:delete(config, j)
+function confirm(self)
+       local _, err = call("confirm", {})
+       if not err then
+               util.ubus("session", "set", {
+                       ubus_rpc_session = session_id,
+                       values = { rollback = 0 }
+               })
        end
+       return (err == nil), ERRSTR[err]
 end
 
---- Create a new section and initialize it with data.
--- @param config       UCI config
--- @param type         UCI section type
--- @param name         UCI section name (optional)
--- @param values       Table of key - value pairs to initialize the section with
--- @return                     Name of created section
-function Cursor.section(self, config, type, name, values)
-       local stat = true
-       if name then
-               stat = self:set(config, name, type)
-       else
-               name = self:add(config, type)
-               stat = name and true
+function rollback(self)
+       local _, err = call("rollback", {})
+       if not err then
+               util.ubus("session", "set", {
+                       ubus_rpc_session = session_id,
+                       values = { rollback = 0 }
+               })
        end
+       return (err == nil), ERRSTR[err]
+end
 
-       if stat and values then
-               stat = self:tset(config, name, values)
+function rollback_pending(self)
+       local deadline, err = util.ubus("session", "get", {
+               ubus_rpc_session = session_id,
+               keys = { "rollback" }
+       })
+
+       if type(deadline) == "table" and
+          type(deadline.values) == "table" and
+          type(deadline.values.rollback) == "number" and
+          deadline.values.rollback > os.time()
+       then
+               return true, deadline.values.rollback - os.time()
        end
 
-       return stat and name
+       return false, ERRSTR[err]
 end
 
---- Updated the data of a section using data from a table.
--- @param config       UCI config
--- @param section      UCI section name (optional)
--- @param values       Table of key - value pairs to update the section with
-function Cursor.tset(self, config, section, values)
-       local stat = true
-       for k, v in pairs(values) do
-               if k:sub(1, 1) ~= "." then
-                       stat = stat and self:set(config, section, k, v)
+
+function foreach(self, config, stype, callback)
+       if type(callback) == "function" then
+               local rv, err = call("get", {
+                       config = config,
+                       type   = stype
+               })
+
+               if type(rv) == "table" and type(rv.values) == "table" then
+                       local sections = { }
+                       local res = false
+                       local index = 1
+
+                       local _, section
+                       for _, section in pairs(rv.values) do
+                               section[".index"] = section[".index"] or index
+                               sections[index] = section
+                               index = index + 1
+                       end
+
+                       table.sort(sections, function(a, b)
+                               return a[".index"] < b[".index"]
+                       end)
+
+                       for _, section in ipairs(sections) do
+                               local continue = callback(section)
+                               res = true
+                               if continue == false then
+                                       break
+                               end
+                       end
+                       return res
+               else
+                       return false, ERRSTR[err] or "No data"
                end
+       else
+               return false, "Invalid argument"
        end
-       return stat
 end
 
---- Get a boolean option and return it's value as true or false.
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option
--- @return                     Boolean
-function Cursor.get_bool(self, ...)
-       local val = self:get(...)
-       return ( val == "1" or val == "true" or val == "yes" or val == "on" )
+local function _get(self, operation, config, section, option)
+       if section == nil then
+               return nil
+       elseif type(option) == "string" and option:byte(1) ~= 46 then
+               local rv, err = call(operation, {
+                       config  = config,
+                       section = section,
+                       option  = option
+               })
+
+               if type(rv) == "table" then
+                       return rv.value or nil
+               elseif err then
+                       return false, ERRSTR[err]
+               else
+                       return nil
+               end
+       elseif option == nil then
+               local values = self:get_all(config, section)
+               if values then
+                       return values[".type"], values[".name"]
+               else
+                       return nil
+               end
+       else
+               return false, "Invalid argument"
+       end
 end
 
---- Get an option or list and return values as table.
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option
--- @return                     UCI value
-function Cursor.get_list(self, config, section, option)
-       if config and section and option then
-               local val = self:get(config, section, option)
-               return ( type(val) == "table" and val or { val } )
+function get(self, ...)
+       return _get(self, "get", ...)
+end
+
+function get_state(self, ...)
+       return _get(self, "state", ...)
+end
+
+function get_all(self, config, section)
+       local rv, err = call("get", {
+               config  = config,
+               section = section
+       })
+
+       if type(rv) == "table" and type(rv.values) == "table" then
+               return rv.values
+       elseif err then
+               return false, ERRSTR[err]
+       else
+               return nil
        end
-       return nil
-end
-
---- Get the given option from the first section with the given type.
--- @param config       UCI config
--- @param type         UCI section type
--- @param option       UCI option (optional)
--- @param default      Default value (optional)
--- @return                     UCI value
-function Cursor.get_first(self, conf, stype, opt, def)
-       local rv = def
-
-       self:foreach(conf, stype,
-               function(s)
-                       local val = not opt and s['.name'] or s[opt]
-
-                       if type(def) == "number" then
-                               val = tonumber(val)
-                       elseif type(def) == "boolean" then
-                               val = (val == "1" or val == "true" or
-                                      val == "yes" or val == "on")
-                       end
+end
 
-                       if val ~= nil then
-                               rv = val
-                               return false
-                       end
-               end)
+function get_bool(self, ...)
+       local val = self:get(...)
+       return (val == "1" or val == "true" or val == "yes" or val == "on")
+end
+
+function get_first(self, config, stype, option, default)
+       local rv = default
+
+       self:foreach(config, stype, function(s)
+               local val = not option and s[".name"] or s[option]
+
+               if type(default) == "number" then
+                       val = tonumber(val)
+               elseif type(default) == "boolean" then
+                       val = (val == "1" or val == "true" or
+                              val == "yes" or val == "on")
+               end
+
+               if val ~= nil then
+                       rv = val
+                       return false
+               end
+       end)
 
        return rv
 end
 
---- Set given values as list.
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option
--- @param value                UCI value
--- @return                     Boolean whether operation succeeded
-function Cursor.set_list(self, config, section, option, value)
+function get_list(self, config, section, option)
        if config and section and option then
-               return self:set(
-                       config, section, option,
-                       ( type(value) == "table" and value or { value } )
-               )
+               local val = self:get(config, section, option)
+               return (type(val) == "table" and val or { val })
        end
-       return false
+       return { }
 end
 
--- Return a list of initscripts affected by configuration changes.
-function Cursor._affected(self, configlist)
-       configlist = type(configlist) == "table" and configlist or {configlist}
 
-       local c = cursor()
-       c:load("ucitrack")
+function section(self, config, stype, name, values)
+       local rv, err = call("add", {
+               config = config,
+               type   = stype,
+               name   = name,
+               values = values
+       })
 
-       -- Resolve dependencies
-       local reloadlist = {}
+       if type(rv) == "table" then
+               return rv.section
+       elseif err then
+               return false, ERRSTR[err]
+       else
+               return nil
+       end
+end
 
-       local function _resolve_deps(name)
-               local reload = {name}
-               local deps = {}
 
-               c:foreach("ucitrack", name,
-                       function(section)
-                               if section.affects then
-                                       for i, aff in ipairs(section.affects) do
-                                               deps[#deps+1] = aff
-                                       end
-                               end
-                       end)
+function add(self, config, stype)
+       return self:section(config, stype)
+end
 
-               for i, dep in ipairs(deps) do
-                       for j, add in ipairs(_resolve_deps(dep)) do
-                               reload[#reload+1] = add
-                       end
-               end
+function set(self, config, section, option, value)
+       if value == nil then
+               local sname, err = self:section(config, option, section)
+               return (not not sname), err
+       else
+               local _, err = call("set", {
+                       config  = config,
+                       section = section,
+                       values  = { [option] = value }
+               })
+               return (err == nil), ERRSTR[err]
+       end
+end
 
-               return reload
+function set_list(self, config, section, option, value)
+       if section == nil or option == nil then
+               return false
+       elseif value == nil or (type(value) == "table" and #value == 0) then
+               return self:delete(config, section, option)
+       elseif type(value) == "table" then
+               return self:set(config, section, option, value)
+       else
+               return self:set(config, section, option, { value })
        end
+end
+
+function tset(self, config, section, values)
+       local _, err = call("set", {
+               config  = config,
+               section = section,
+               values  = values
+       })
+       return (err == nil), ERRSTR[err]
+end
+
+function reorder(self, config, section, index)
+       local sections
 
-       -- Collect initscripts
-       for j, config in ipairs(configlist) do
-               for i, e in ipairs(_resolve_deps(config)) do
-                       if not util.contains(reloadlist, e) then
-                               reloadlist[#reloadlist+1] = e
+       if type(section) == "string" and type(index) == "number" then
+               local pos = 0
+
+               sections = { }
+
+               self:foreach(config, nil, function(s)
+                       if pos == index then
+                               pos = pos + 1
                        end
-               end
+
+                       if s[".name"] ~= section then
+                               pos = pos + 1
+                               sections[pos] = s[".name"]
+                       else
+                               sections[index + 1] = section
+                       end
+               end)
+       elseif type(section) == "table" then
+               sections = section
+       else
+               return false, "Invalid argument"
        end
 
-       return reloadlist
-end
+       local _, err = call("order", {
+               config   = config,
+               sections = sections
+       })
 
---- Create a sub-state of this cursor. The sub-state is tied to the parent
--- curser, means it the parent unloads or loads configs, the sub state will
--- do so as well.
--- @return                     UCI state cursor tied to the parent cursor
-function Cursor.substate(self)
-       Cursor._substates = Cursor._substates or { }
-       Cursor._substates[self] = Cursor._substates[self] or cursor_state()
-       return Cursor._substates[self]
+       return (err == nil), ERRSTR[err]
 end
 
-local _load = Cursor.load
-function Cursor.load(self, ...)
-       if Cursor._substates and Cursor._substates[self] then
-               _load(Cursor._substates[self], ...)
-       end
-       return _load(self, ...)
+
+function delete(self, config, section, option)
+       local _, err = call("delete", {
+               config  = config,
+               section = section,
+               option  = option
+       })
+       return (err == nil), ERRSTR[err]
 end
 
-local _unload = Cursor.unload
-function Cursor.unload(self, ...)
-       if Cursor._substates and Cursor._substates[self] then
-               _unload(Cursor._substates[self], ...)
+function delete_all(self, config, stype, comparator)
+       local _, err
+       if type(comparator) == "table" then
+               _, err = call("delete", {
+                       config = config,
+                       type   = stype,
+                       match  = comparator
+               })
+       elseif type(comparator) == "function" then
+               local rv = call("get", {
+                       config = config,
+                       type   = stype
+               })
+
+               if type(rv) == "table" and type(rv.values) == "table" then
+                       local sname, section
+                       for sname, section in pairs(rv.values) do
+                               if comparator(section) then
+                                       _, err = call("delete", {
+                                               config  = config,
+                                               section = sname
+                                       })
+                               end
+                       end
+               end
+       elseif comparator == nil then
+               _, err = call("delete", {
+                       config  = config,
+                       type    = stype
+               })
+       else
+               return false, "Invalid argument"
        end
-       return _unload(self, ...)
-end
-
-
---- Add an anonymous section.
--- @class function
--- @name Cursor.add
--- @param config       UCI config
--- @param type         UCI section type
--- @return                     Name of created section
-
---- Get a table of saved but uncommitted changes.
--- @class function
--- @name Cursor.changes
--- @param config       UCI config
--- @return                     Table of changes
--- @see Cursor.save
-
---- Commit saved changes.
--- @class function
--- @name Cursor.commit
--- @param config       UCI config
--- @return                     Boolean whether operation succeeded
--- @see Cursor.revert
--- @see Cursor.save
-
---- Deletes a section or an option.
--- @class function
--- @name Cursor.delete
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option (optional)
--- @return                     Boolean whether operation succeeded
-
---- Call a function for every section of a certain type.
--- @class function
--- @name Cursor.foreach
--- @param config       UCI config
--- @param type         UCI section type
--- @param callback     Function to be called
--- @return                     Boolean whether operation succeeded
-
---- Get a section type or an option
--- @class function
--- @name Cursor.get
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option (optional)
--- @return                     UCI value
-
---- Get all sections of a config or all values of a section.
--- @class function
--- @name Cursor.get_all
--- @param config       UCI config
--- @param section      UCI section name (optional)
--- @return                     Table of UCI sections or table of UCI values
-
---- Manually load a config.
--- @class function
--- @name Cursor.load
--- @param config       UCI config
--- @return                     Boolean whether operation succeeded
--- @see Cursor.save
--- @see Cursor.unload
-
---- Revert saved but uncommitted changes.
--- @class function
--- @name Cursor.revert
--- @param config       UCI config
--- @return                     Boolean whether operation succeeded
--- @see Cursor.commit
--- @see Cursor.save
-
---- Saves changes made to a config to make them committable.
--- @class function
--- @name Cursor.save
--- @param config       UCI config
--- @return                     Boolean whether operation succeeded
--- @see Cursor.load
--- @see Cursor.unload
-
---- Set a value or create a named section.
--- @class function
--- @name Cursor.set
--- @param config       UCI config
--- @param section      UCI section name
--- @param option       UCI option or UCI section type
--- @param value                UCI value or nil if you want to create a section
--- @return                     Boolean whether operation succeeded
-
---- Get the configuration directory.
--- @class function
--- @name Cursor.get_confdir
--- @return                     Configuration directory
-
---- Get the directory for uncomitted changes.
--- @class function
--- @name Cursor.get_savedir
--- @return                     Save directory
-
---- Set the configuration directory.
--- @class function
--- @name Cursor.set_confdir
--- @param directory    UCI configuration directory
--- @return                     Boolean whether operation succeeded
-
---- Set the directory for uncommited changes.
--- @class function
--- @name Cursor.set_savedir
--- @param directory    UCI changes directory
--- @return                     Boolean whether operation succeeded
-
---- Discard changes made to a config.
--- @class function
--- @name Cursor.unload
--- @param config       UCI config
--- @return                     Boolean whether operation succeeded
--- @see Cursor.load
--- @see Cursor.save
+
+       return (err == nil), ERRSTR[err]
+end