root/ff-luci/trunk/libs/uvl/luasrc/uvl.lua @ 2905

Revision 2905, 19.7 KB (checked in by jow, 5 years ago)

* luci/libs: change default scheme directory to /lib/uci/schema/ in uvl

  • Property svn:keywords set to Id
Line 
1--[[
2
3UCI Validation Layer - Main Library
4(c) 2008 Jo-Philipp Wich <xm@leipzig.freifunk.net>
5(c) 2008 Steven Barth <steven@midlink.org>
6
7Licensed under the Apache License, Version 2.0 (the "License");
8you may not use this file except in compliance with the License.
9You may obtain a copy of the License at
10
11        http://www.apache.org/licenses/LICENSE-2.0
12
13$Id$
14
15]]--
16
17
18--- UVL - UCI Validation Layer
19-- @class   module
20-- @cstyle  instance
21
22module( "luci.uvl", package.seeall )
23
24require("luci.fs")
25require("luci.util")
26require("luci.model.uci")
27require("luci.uvl.loghelper")
28require("luci.uvl.datatypes")
29require("luci.uvl.validation")
30require("luci.uvl.dependencies")
31
32
33TYPE_SECTION  = 0x01
34TYPE_VARIABLE = 0x02
35TYPE_ENUM     = 0x03
36
37--- Boolean; default true;
38-- treat sections found in config but not in scheme as error
39STRICT_UNKNOWN_SECTIONS    = true
40
41--- Boolean; default true;
42-- treat options found in config but not in scheme as error
43STRICT_UNKNOWN_OPTIONS     = true
44
45--- Boolean; default true;
46-- treat failed external validators as error
47STRICT_EXTERNAL_VALIDATORS = true
48
49--- Boolean; default true;
50-- treat list values stored as options like errors
51STRICT_LIST_TYPE           = true
52
53
54local default_schemedir = "/lib/uci/schema"
55
56local function _assert( condition, fmt, ... )
57    if not condition then
58        return assert( nil, string.format( fmt, ... ) )
59    else
60        return condition
61    end
62end
63
64
65--- Object constructor
66-- @class           function
67-- @name            UVL
68-- @param schemedir Path to the scheme directory (optional)
69-- @return          Instance object
70UVL = luci.util.class()
71
72function UVL.__init__( self, schemedir )
73    self.schemedir  = schemedir or default_schemedir
74    self.packages   = { }
75    self.beenthere  = { }
76    self.uci        = luci.model.uci
77    self.log        = luci.uvl.loghelper
78    self.datatypes  = luci.uvl.datatypes
79end
80
81
82--- Validate given configuration, section or option.
83-- @param config    Name of the configuration to validate
84-- @param section   Name of the section to validate (optional)
85-- @param option    Name of the option to validate (optional)
86-- @return          Boolean indicating whether the given config validates
87-- @return          String containing the reason for errors (if any)
88function UVL.validate( self, config, section, option )
89    if config and section and option then
90        return self:validate_option( config, section, option )
91    elseif config and section then
92        return self:validate_section( config, section )
93    elseif config then
94        return self:validate_config( config )
95    end
96end
97
98--- Validate given configuration.
99-- @param config    Name of the configuration to validate
100-- @return          Boolean indicating whether the given config validates
101-- @return          String containing the reason for errors (if any)
102function UVL.validate_config( self, config )
103
104    if not self.packages[config] then
105        local ok, err = pcall( self.read_scheme, self, config )
106        if not ok then
107            return false, self.log.scheme_error( config, err )
108        end
109    end
110
111    self.uci.load_config( config )
112    self.beenthere = { }
113
114    local co = self.uci.get_all( config )
115    local sc = { }
116
117    if not co then
118        return false, 'Unable to load configuration "' .. config .. '"'
119    end
120
121    local function _uci_foreach( type, func )
122        local ok, err
123        for k, v in pairs(co) do
124            if co[k]['.type'] == type then
125                sc[type] = sc[type] + 1
126                ok, err = func( k, v )
127                if not ok then
128                    err = self.log.config_error( config, err )
129                    break
130                end
131            end
132        end
133        return ok, err
134    end
135
136    for k, v in pairs( self.packages[config].sections ) do
137        sc[k] = 0
138        local ok, err = _uci_foreach( k,
139            function(s)
140                local sect = luci.uvl.section( self, co, k, config, s )
141                return self:_validate_section( sect )
142            end
143        )
144        if not ok then return false, err end
145    end
146
147    if STRICT_UNKNOWN_SECTIONS then
148        for k, v in pairs(co) do
149            if not self.beenthere[config..'.'..k] then
150                return false, self.log.config_error( config,
151                    "Section '" .. config .. '.' .. co[k]['.type'] ..
152                    "' not found in scheme" )
153            end
154        end
155    end
156
157    for _, k in ipairs(luci.util.keys(sc)) do
158        local s = self.packages[config].sections[k]
159
160        if s.required and sc[k] == 0 then
161            return false, self.log.config_error( config,
162                'Required section "' .. k .. '" not found in config' )
163        elseif s.unique and sc[k] > 1 then
164            return false, self.log.config_error( config,
165                'Unique section "' .. k .. '" occurs multiple times in config' )
166        end
167    end
168
169    return true, nil
170end
171
172--- Validate given config section.
173-- @param config    Name of the configuration to validate
174-- @param section   Name of the section to validate
175-- @return          Boolean indicating whether the given config validates
176-- @return          String containing the reason for errors (if any)
177function UVL.validate_section( self, config, section )
178
179    if not self.packages[config] then
180        local ok, err = pcall( self.read_scheme, self, config )
181        if not ok then
182            return false, self.log.scheme_error( config, err )
183        end
184    end
185
186    self.uci.load_config( config )
187    self.beenthere = { }
188
189    local co = self.uci.get_all( config )
190
191    if not co then
192        return false, 'Unable to load configuration "' .. config .. '"'
193    end
194
195    if co[section] then
196        return self:_validate_section( luci.uvl.section(
197            self, co, co[section]['.type'], config, section
198        ) )
199    else
200        return false, 'Section "' .. config .. '.' .. section ..
201            '" not found in config. Nothing to do.'
202    end
203end
204
205--- Validate given config option.
206-- @param config    Name of the configuration to validate
207-- @param section   Name of the section to validate
208-- @param option    Name of the option to validate
209-- @return          Boolean indicating whether the given config validates
210-- @return          String containing the reason for errors (if any)
211function UVL.validate_option( self, config, section, option )
212
213    if not self.packages[config] then
214        local ok, err = pcall( self.read_scheme, self, config )
215        if not ok then
216            return false, self.log.scheme_error( config, err )
217        end
218    end
219
220    self.uci.load_config( config )
221    self.beenthere = { }
222
223    local co = self.uci.get_all( config )
224
225    if not co then
226        return false, 'Unable to load configuration "' .. config .. '"'
227    end
228
229    if co[section] and co[section][option] then
230        return self:_validate_option( luci.uvl.option(
231            self, co, co[section]['.type'], config, section, option
232        ) )
233    else
234        return false, 'Option "' ..
235            config .. '.' .. section .. '.' .. option ..
236            '" not found in config. Nothing to do.'
237    end
238end
239
240
241function UVL._validate_section( self, section )
242
243    if section:values() then
244
245        for _, v in ipairs(section:variables()) do
246            local ok, err = self:_validate_option( v )
247
248            if not ok then
249                return ok, self.log.section_error( section, err )
250            end
251        end
252
253        local ok, err = luci.uvl.dependencies.check( self, section )
254
255        if not ok then
256            return false, err
257        end
258    else
259        return false, 'Option "' .. section:sid() .. '" not found in config'
260    end
261
262    if STRICT_UNKNOWN_OPTIONS and not section:section().dynamic then
263        for k, v in pairs(section:values()) do
264            if k:sub(1,1) ~= "." and not self.beenthere[
265                section:cid() .. '.' .. k
266            ] then
267                return false, "Option '" .. section:sid() .. '.' .. k ..
268                    "' not found in scheme"
269            end
270        end
271    end
272
273    return true, nil
274end
275
276function UVL._validate_option( self, option, nodeps )
277
278    local item = option:option()
279    local val  = option:value()
280
281    if not item and not ( option:section() and option:section().dynamic ) then
282        return false, 'Option "' .. option:cid() ..
283            '" not found in scheme'
284
285    elseif item then
286        if item.required and not val then
287            return false, 'Mandatory variable "' .. option:cid() ..
288                '" does not have a value'
289        end
290
291        if item.type == "enum" and val then
292            if not item.values or not item.values[val] then
293                return false, 'Value "' .. ( val or '<nil>' ) ..
294                    '" of given option "' .. option:cid() ..
295                    '" is not defined in enum { ' ..
296                        table.concat( luci.util.keys(item.values), ", " ) ..
297                    ' }'
298            end
299        elseif item.type == "list" and val then
300            if type(val) ~= "table" and STRICT_LIST_TYPE then
301                return false, 'Option "' .. option:cid() ..
302                    '" is defined as list but stored as plain value'
303            end
304        end
305
306        if item.datatype and val then
307            if self.datatypes[item.datatype] then
308                val = ( type(val) == "table" and val or { val } )
309                for i, v in ipairs(val) do
310                    if not self.datatypes[item.datatype]( v ) then
311                        return false, 'Value' .. ( #val>1 and ' #'..i or '' ) ..
312                            ' "' .. ( v or '<nil>' ) ..
313                            '" of given option "' .. option:cid() ..
314                            '" does not validate as datatype "' ..
315                            item.datatype .. '"'
316                    end
317                end
318            else
319                return false, 'Unknown datatype "' ..
320                    item.datatype .. '" encountered'
321            end
322        end
323
324        if not nodeps then
325            return luci.uvl.dependencies.check( self, option )
326        end
327
328        local ok, err = luci.uvl.validation.check( self, option )
329        if not ok and STRICT_EXTERNAL_VALIDATORS then
330            return false, self.log.validator_error( option, err )
331        end
332    end
333
334    return true, nil
335end
336
337--- Find all parts of given scheme and construct validation tree.
338-- This is normally done on demand, so you don't have to call this function
339-- by yourself.
340-- @param scheme    Name of the scheme to parse
341function UVL.read_scheme( self, scheme )
342    local schemes = { }
343    local files = luci.fs.glob(self.schemedir .. '/*/' .. scheme)
344
345    if files then
346        for i, file in ipairs( files ) do
347            _assert( luci.fs.access(file), "Can't access file '%s'", file )
348
349            self.uci.set_confdir( luci.fs.dirname(file) )
350            self.uci.load( luci.fs.basename(file) )
351
352            table.insert( schemes, self.uci.get_all( luci.fs.basename(file) ) )
353        end
354
355        return self:_read_scheme_parts( scheme, schemes )
356    else
357        error(
358            'Can not find scheme "' .. scheme ..
359            '" in "' .. self.schemedir .. '"'
360        )
361    end
362end
363
364-- Process all given parts and construct validation tree
365function UVL._read_scheme_parts( self, scheme, schemes )
366
367    -- helper function to construct identifiers for given elements
368    local function _id( c, t )
369        if c == TYPE_SECTION then
370            return string.format(
371                'section "%s.%s"',
372                    scheme, t.name or '?' )
373        elseif c == TYPE_VARIABLE then
374            return string.format(
375                'variable "%s.%s.%s"',
376                    scheme, t.section or '?.?', t.name or '?' )
377        elseif c == TYPE_ENUM then
378            return string.format(
379                'enum "%s.%s.%s"',
380                    scheme, t.variable or '?.?.?', t.value or '?' )
381        end
382    end
383
384    -- helper function to check for required fields
385    local function _req( c, t, r )
386        for i, v in ipairs(r) do
387            _assert( t[v], 'Missing required field "%s" in %s', v, _id(c, t) )
388        end
389    end
390
391    -- helper function to validate references
392    local function _ref( c, t )
393        local k
394        if c == TYPE_SECTION then
395            k = "package"
396        elseif c == TYPE_VARIABLE then
397            k = "section"
398        elseif c == TYPE_ENUM then
399            k = "variable"
400        end
401
402        local r = luci.util.split( t[k], "." )
403        r[1] = ( #r[1] > 0 and r[1] or scheme )
404
405        _assert( #r == c, 'Malformed %s reference in %s', k, _id(c, t) )
406
407        return r
408    end
409
410    -- helper function to read bools
411    local function _bool( v )
412        return ( v == "true" or v == "yes" or v == "on" or v == "1" )
413    end
414
415    -- Step 1: get all sections
416    for i, conf in ipairs( schemes ) do
417        for k, v in pairs( conf ) do
418            if v['.type'] == 'section' then
419
420                _req( TYPE_SECTION, v, { "name", "package" } )
421
422                local r = _ref( TYPE_SECTION, v )
423
424                self.packages[r[1]] =
425                    self.packages[r[1]] or {
426                        ["sections"]  = { };
427                        ["variables"] = { };
428                    }
429
430                local p = self.packages[r[1]]
431                      p.sections[v.name]  = p.sections[v.name]  or { }
432                      p.variables[v.name] = p.variables[v.name] or { }
433
434                local s = p.sections[v.name]
435
436                for k, v2 in pairs(v) do
437                    if k ~= "name" and k ~= "package" and k:sub(1,1) ~= "." then
438                        if k == "depends" then
439                            s["depends"] = _assert(
440                                self:_read_dependency( v2, s["depends"] ),
441                                'Section "%s" in scheme "%s" has malformed ' ..
442                                'dependency specification in "%s"',
443                                v.name or '<nil>', scheme or '<nil>', k
444                            )
445                        elseif k == "dynamic" or k == "unique" or k == "required" then
446                            s[k] = _bool(v2)
447                        else
448                            s[k] = v2
449                        end
450                    end
451                end
452
453                s.dynamic  = s.dynamic  or false
454                s.unique   = s.unique   or false
455                s.required = s.required or false
456            end
457        end
458    end
459
460    -- Step 2: get all variables
461    for i, conf in ipairs( schemes ) do
462        for k, v in pairs( conf ) do
463            if v['.type'] == "variable" then
464
465                _req( TYPE_VARIABLE, v, { "name", "section" } )
466
467                local r = _ref( TYPE_VARIABLE, v )
468
469                local p = _assert( self.packages[r[1]],
470                    'Variable "%s" in scheme "%s" references unknown package "%s"',
471                    v.name, scheme, r[1] )
472
473                local s = _assert( p.variables[r[2]],
474                    'Variable "%s" in scheme "%s" references unknown section "%s"',
475                    v.name, scheme, r[2] )
476
477                s[v.name] = s[v.name] or { }
478
479                local t = s[v.name]
480
481                for k, v2 in pairs(v) do
482                    if k ~= "name" and k ~= "section" and k:sub(1,1) ~= "." then
483                        if k == "depends" then
484                            t["depends"] = _assert(
485                                self:_read_dependency( v2, t["depends"] ),
486                                'Invalid reference "%s" in "%s.%s.%s"',
487                                v2, v.name, scheme, k
488                            )
489                        elseif k == "validator" then
490                            t["validators"] = _assert(
491                                self:_read_validator( v2, t["validators"] ),
492                                'Variable "%s" in scheme "%s" has malformed ' ..
493                                'validator specification in "%s"',
494                                v.name, scheme, k
495                            )
496                        elseif k == "required" then
497                            t[k] = _bool(v2)
498                        else
499                            t[k] = v2
500                        end
501                    end
502                end
503
504                t.type     = t.type     or "variable"
505                t.datatype = t.datatype or "string"
506                t.required = t.required or false
507            end
508        end
509    end
510
511    -- Step 3: get all enums
512    for i, conf in ipairs( schemes ) do
513        for k, v in pairs( conf ) do
514            if v['.type'] == "enum" then
515
516                _req( TYPE_ENUM, v, { "value", "variable" } )
517
518                local r = _ref( TYPE_ENUM, v )
519                local p = _assert( self.packages[r[1]],
520                    'Enum "%s" in scheme "%s" references unknown package "%s"',
521                    v.value, scheme, r[1] )
522
523                local s = _assert( p.variables[r[2]],
524                    'Enum "%s" in scheme "%s" references unknown section "%s"',
525                    v.value, scheme, r[2] )
526
527                local t = _assert( s[r[3]],
528                    'Enum "%s" in scheme "%s", section "%s" references ' ..
529                    'unknown variable "%s"',
530                    v.value, scheme, r[2], r[3] )
531
532                _assert( t.type == "enum",
533                    'Enum "%s" in scheme "%s", section "%s" references ' ..
534                    'variable "%s" with non enum type "%s"',
535                    v.value, scheme, r[2], r[3], t.type )
536
537                if not t.values then
538                    t.values = { [v.value] = v.title or v.value }
539                else
540                    t.values[v.value] = v.title or v.value
541                end
542
543                if v.default then
544                    _assert( not t.default,
545                        'Enum "%s" in scheme "%s", section "%s" redeclares ' ..
546                        'the default value of variable "%s"',
547                        v.value, scheme, r[2], v.variable )
548
549                    t.default = v.value
550                end
551            end
552        end
553    end
554
555    return self
556end
557
558-- Read a dependency specification
559function UVL._read_dependency( self, values, deps )
560    local expr = "%$?[a-zA-Z0-9_]+"
561    if values then
562        values = ( type(values) == "table" and values or { values } )
563        for _, value in ipairs(values) do
564            local parts     = luci.util.split( value, "%s*,%s*", nil, true )
565            local condition = { }
566            for i, val in ipairs(parts) do
567                local k, v = unpack(luci.util.split(val, "%s*=%s*", nil, true))
568
569                if k and (
570                    k:match("^"..expr.."%."..expr.."%."..expr.."$") or
571                    k:match("^"..expr.."%."..expr.."$") or
572                    k:match("^"..expr.."$")
573                ) then
574                    condition[k] = v or true
575                else
576                    return nil
577                end
578            end
579
580            if not deps then
581                deps = { condition }
582            else
583                table.insert( deps, condition )
584            end
585        end
586    end
587
588    return deps
589end
590
591-- Read a validator specification
592function UVL._read_validator( self, values, validators )
593    if values then
594        values = ( type(values) == "table" and values or { values } )
595        for _, value in ipairs(values) do
596            local validator
597
598            if value:match("^exec:") then
599                validator = value:gsub("^exec:","")
600            elseif value:match("^lua:") then
601                validator = self:_resolve_function( (value:gsub("^lua:","") ) )
602            end
603
604            if validator then
605                if not validators then
606                    validators = { validator }
607                else
608                    table.insert( validators, validator )
609                end
610            else
611                return nil
612            end
613        end
614
615        return validators
616    end
617end
618
619-- Resolve given path
620function UVL._resolve_function( self, value )
621    local path = luci.util.split(value, ".")
622
623    for i=1, #path-1 do
624        local stat, mod = pcall(require, table.concat(path, ".", 1, i))
625        if stat and mod then
626            for j=i+1, #path-1 do
627                if not type(mod) == "table" then
628                    break
629                end
630                mod = mod[path[j]]
631                if not mod then
632                    break
633                end
634            end
635            mod = type(mod) == "table" and mod[path[#path]] or nil
636            if type(mod) == "function" then
637                return mod
638            end
639        end
640    end
641end
642
643
644--- Object representation of a scheme/config section.
645-- @class   module
646-- @cstyle  instance
647-- @name    luci.uvl.section
648
649--- Section instance constructor.
650-- @class           function
651-- @name            section
652-- @param scheme    Scheme instance
653-- @param co        Configuration data
654-- @param st        Section type
655-- @param c         Configuration name
656-- @param s         Section name
657-- @return          Section instance
658section = luci.util.class()
659
660function section.__init__(self, scheme, co, st, c, s)
661    self.csection = co[s]
662    self.ssection = scheme.packages[c].sections[st]
663    self.cref     = { c, s }
664    self.sref     = { c, st }
665    self.scheme   = scheme
666    self.config   = co
667    self.type     = luci.uvl.TYPE_SECTION
668end
669
670--- Get the config path of this section.
671-- @return  String containing the identifier
672function section.cid(self)
673    return ( self.cref[1] or '?' ) .. '.' .. ( self.cref[2] or '?' )
674end
675
676--- Get the scheme path of this section.
677-- @return  String containing the identifier
678function section.sid(self)
679    return ( self.sref[1] or '?' ) .. '.' .. ( self.sref[2] or '?' )
680end
681
682--- Get all configuration values within this section.
683-- @return  Table containing the values
684function section.values(self)
685    return self.csection
686end
687
688--- Get the associated section information in scheme.
689-- @return  Table containing the scheme properties
690function section.section(self)
691    return self.ssection
692end
693
694--- Get all option objects associated with this section.
695-- @return  Table containing all associated luci.uvl.option instances
696function section.variables(self)
697    local v = { }
698    if self.scheme.packages[self.sref[1]].variables[self.sref[2]] then
699        for o, _ in pairs(
700            self.scheme.packages[self.sref[1]].variables[self.sref[2]]
701        ) do
702            table.insert( v, luci.uvl.option(
703                self.scheme, self.config, self.sref[2],
704                self.cref[1], self.cref[2], o
705            ) )
706        end
707    end
708    return v
709end
710
711
712--- Object representation of a scheme/config option.
713-- @class   module
714-- @cstyle  instance
715-- @name    luci.uvl.option
716
717--- Section instance constructor.
718-- @class           function
719-- @name            option
720-- @param scheme    Scheme instance
721-- @param co        Configuration data
722-- @param st        Section type
723-- @param c         Configuration name
724-- @param s         Section name
725-- @param o         Option name
726-- @return          Option instance
727option = luci.util.class()
728
729function option.__init__(self, scheme, co, st, c, s, o)
730    self.coption = co[s] and co[s][o] or nil
731    self.soption = scheme.packages[c].variables[st][o]
732    self.cref    = { c, s, o }
733    self.sref    = { c, st, o }
734    self.scheme  = scheme
735    self.config  = co
736    self.type    = luci.uvl.TYPE_OPTION
737end
738
739--- Get the config path of this option.
740-- @return  String containing the identifier
741function option.cid(self)
742    return ( self.cref[1] or '?' ) .. '.' ..
743           ( self.cref[2] or '?' ) .. '.' ..
744           ( self.cref[3] or '?' )
745end
746
747--- Get the scheme path of this option.
748-- @return  String containing the identifier
749function option.sid(self)
750    return ( self.sref[1] or '?' ) .. '.' ..
751           ( self.sref[2] or '?' ) .. '.' ..
752           ( self.sref[3] or '?' )
753end
754
755--- Get the value of this option.
756-- @return  The associated configuration value
757function option.value(self)
758    return self.coption
759end
760
761--- Get the associated option information in scheme.
762-- @return  Table containing the scheme properties
763function option.option(self)
764    return self.soption
765end
766
767--- Get the associated section information in scheme.
768-- @return  Table containing the scheme properties
769function option.section(self)
770    return self.scheme.packages[self.sref[1]].sections[self.sref[2]]
771end
Note: See TracBrowser for help on using the browser.