]> Cypherpunks repositories - dsc.git/commitdiff
Initial commit
authorSergey Matveev <stargrave@stargrave.org>
Tue, 12 Aug 2025 14:38:58 +0000 (17:38 +0300)
committerSergey Matveev <stargrave@stargrave.org>
Thu, 14 Aug 2025 07:45:31 +0000 (10:45 +0300)
23 files changed:
dsc [new file with mode: 0755]
schema/net/*/addr/*/check [new file with mode: 0755]
schema/net/*/addr/*/descr [new file with mode: 0644]
schema/net/*/addr/*/prefixlen/check [new file with mode: 0755]
schema/net/*/addr/*/prefixlen/descr [new file with mode: 0644]
schema/net/*/addr/*/prefixlen/title [new file with mode: 0644]
schema/net/*/addr/*/title [new file with mode: 0644]
schema/net/*/check [new file with mode: 0755]
schema/net/*/mtu/check [new file with mode: 0755]
schema/net/*/mtu/descr [new file with mode: 0644]
schema/net/*/mtu/title [new file with mode: 0644]
schema/net/*/title [new file with mode: 0644]
schema/srv/*/autostart/check [new file with mode: 0755]
schema/srv/*/autostart/title [new file with mode: 0644]
schema/srv/*/check [new file with mode: 0755]
schema/srv/*/title [new file with mode: 0644]
schema/sys/hostname/check [new file with mode: 0755]
schema/sys/hostname/title [new file with mode: 0644]
schema/sys/note/check [new file with mode: 0755]
schema/sys/note/title [new file with mode: 0644]
schema/ui/password/check [new file with mode: 0755]
schema/ui/password/descr [new file with mode: 0644]
schema/ui/password/title [new file with mode: 0644]

diff --git a/dsc b/dsc
new file mode 100755 (executable)
index 0000000..5d2e860
--- /dev/null
+++ b/dsc
@@ -0,0 +1,279 @@
+#!/usr/bin/env tclsh
+# dsc -- damn small configuration manager
+# Copyright (C) 2025 Sergey Matveev <stargrave@stargrave.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, version 3 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+if {$argc == 0} {
+    puts -nonewline stderr {Usage:
+    dsc list [-v] [prefix] -- list all available options
+    dsc add opt       -- add /*/ section
+    dsc del opt       -- delete /*/ section
+    dsc del ""        -- delete the whole configuration, useful for import
+    dsc has opt       -- check if specified /*/ option exists
+    dsc set opt value -- set option's value
+    dsc set opt ""    -- unset value, make it default one
+    dsc get opt       -- get option's value, maybe default one
+    dsc get opt/*     -- list /*/ sections
+    dsc diff [prefix] -- show the difference between committed and current
+    dsc revert opt    -- revert opt's configuration
+    dsc commit        -- commit (save) configuration
+    dsc export >file.txtar -- export whole configuration
+    dsc import <file.txtar -- import it
+
+Environmental variables:
+    $DSC_SCHEMA    -- path to the schema definition
+    $DSC_CURRENT   -- path to current/unsaved configuration state
+    $DSC_COMMITTED -- path to committed/saved state
+
+There are two kinds of options:
+    * array/list ones, which are identified with /*/ in "list"'s
+      output. "add", "del", "get opt/*" commands apply
+    * ordinary scalar ones, which can be "set", "get opt"
+}
+    exit 1
+}
+
+if {[catch {set Schema $env(DSC_SCHEMA)}]} {set Schema schema}
+if {[catch {set Curr $env(DSC_CURRENT)}]} {set Curr current}
+if {[catch {set Comm $env(DSC_COMMITTED)}]} {set Comm committed}
+
+proc walk {root typ} {
+    set rv [list]
+    set dirs [glob -directory $root -types $typ -tails -nocomplain -- *]
+    foreach s [lsort $dirs] {
+        lappend rv $root/$s
+        lappend rv {*}[walk $root/$s $typ]
+    }
+    return $rv
+}
+
+proc fileread {fn} {
+    set fh [open $fn]
+    set v [read $fh]
+    close $fh
+    return $v
+}
+
+proc find-checker {opt} {
+    global Schema
+    set path .
+    set opt [string trimright $opt /]
+    set err "can not find checker for $opt"
+    foreach e [split $opt /] {
+        if {[file exists $Schema/$path/$e]} {
+            set path $path/$e
+            continue
+        }
+        if {[file exists $Schema/$path/*]} {
+            set path $path/*
+            continue
+        }
+        puts stderr $err
+        exit 1
+    }
+    set path [string range $path 2 end]
+    if {! [file exists $Schema/$path/check]} {
+        puts stderr $err
+        exit 1
+    }
+    return $path
+}
+
+proc run-checker {opt v} {
+    global Schema
+    set fh [open "| $Schema/[find-checker $opt]/check $opt" r+]
+    puts $fh $v
+    close $fh w
+    set v [read $fh]
+    if {[catch {close $fh}]} {
+        puts -nonewline stderr $v
+        exit 1
+    }
+    return $v
+}
+
+proc assure-exists {opt} {
+    global Curr
+    if {! [file exists $Curr/$opt]} {
+        puts stderr "not found"
+        exit 1
+    }
+}
+
+set opt [lindex $argv 1]
+switch [lindex $argv 0] {
+    list {
+        set verbose n
+        set prefix [lindex $argv 1]
+        if {$argc > 1 && [lindex $argv 1] == "-v"} {
+            set verbose y
+            set prefix [lindex $argv 2]
+        }
+        foreach opt [walk $Schema d] {
+            if {! [file exists $opt/title]} {continue}
+            set name [string range $opt [expr {[string length $Schema] + 1}] end]
+            if {$prefix != ""} {
+                if {[string range $name 0 [string length $prefix]-1] != $prefix} {
+                    continue
+                }
+            }
+            set v [fileread $opt/title]
+            puts -nonewline "$name\t$v"
+            if {$verbose} {
+                if {[file exists $opt/descr]} {
+                    set lines [split [fileread $opt/descr] "\n"]
+                    set lines [lrange $lines 0 end-1]
+                    foreach line $lines {
+                        puts "\t$line"
+                    }
+                }
+            }
+        }
+    }
+    add {
+        set dir [file dirname $opt]
+        set tail [run-checker $opt [file tail $opt]]
+        set tail [string trimright $tail "\n"]
+        file mkdir $Curr/$dir/$tail
+        puts $dir/$tail
+    }
+    del {
+        if {$opt != ""} {
+            assure-exists $opt
+        }
+        file delete -force $Curr/$opt
+    }
+    set {
+        if {[llength $argv] == 2} {
+            set v [read -nonewline stdin]
+        } else {
+            set v [lindex $argv 2]
+        }
+        if {$v == ""} {
+            file delete $Curr/$opt
+            exit
+        }
+        set v [run-checker $opt $v]
+        file mkdir "$Curr/[file dirname $opt]"
+        set fh [open $Curr/$opt w]
+        puts -nonewline $fh $v
+        close $fh
+    }
+    has {
+        assure-exists $opt
+    }
+    get-checker {
+        puts [find-checker $opt]
+    }
+    get {
+        if {[file tail $opt] == "*"} {
+            set opt [file dirname $opt]
+            assure-exists $opt
+            set dirs [glob -directory $Curr/$opt -types d -tails -nocomplain -- *]
+            foreach dir [lsort $dirs] {
+                puts $dir
+            }
+            exit
+        }
+        if {[file exists $Curr/$opt]} {
+            puts -nonewline [fileread $Curr/$opt]
+            exit
+        }
+        puts -nonewline [run-checker $opt ""]
+    }
+    diff {
+        set fh [file tempfile dirsComm]
+        foreach fn [walk $Comm/$opt d] {
+            puts $fh [string range $fn [string length $Comm]+1 end]
+        }
+        close $fh
+        set fh [file tempfile dirsCurr]
+        foreach fn [walk $Curr/$opt d] {
+            puts $fh [string range $fn [string length $Curr]+1 end]
+        }
+        close $fh
+        set fh [open "| diff -u -L dirs/committed -L dirs/current
+            $dirsComm $dirsCurr" r]
+        puts -nonewline [read $fh]
+        catch {close $fh}
+        file delete $dirsComm
+        file delete $dirsCurr
+        set fh [open "| diff -urN $Comm/$opt $Curr/$opt" r]
+        puts -nonewline [read $fh]
+        catch {close $fh}
+    }
+    revert {
+        catch {file delete -force $Curr/$opt}
+        catch {file copy $Comm/$opt $Curr/$opt}
+    }
+    commit {
+        file delete -force $Comm.bak
+        set tmp $Comm.[expr {int(rand() * 1000000)}]
+        file copy $Curr $tmp
+        file rename $Comm $Comm.bak
+        file rename $tmp $Comm
+        file delete -force $Comm.bak
+    }
+    export {
+        set dirs [walk $Comm d]
+        puts "-- .dirs --"
+        foreach fn $dirs {
+            puts [string range $fn [string length $Comm]+1 end]
+        }
+        foreach dir $dirs {
+            foreach fn [walk $dir f] {
+                puts "-- [string range $fn [string length $Comm]+1 end] --"
+                puts -nonewline [fileread $fn]
+            }
+        }
+    }
+    import {
+        set fn ""
+        set lines [list]
+        proc filewrite {fn v} {
+            global Curr
+            if {[llength $v] == 0} {
+                return
+            }
+            if {$fn == ".dirs"} {
+                foreach dir $v {
+                    file mkdir $Curr/$dir
+                }
+                return
+            }
+            file mkdir [file dirname $Curr/$fn]
+            set fh [open $Curr/$fn w]
+            puts $fh [join $v "\n"]
+            close $fh
+        }
+        while {[gets stdin line] >= 0} {
+            if {
+                [string length $line] > 6 &&
+                [string range $line 0 2] == "-- " &&
+                [string range $line end-2 end] == " --"
+            } {
+                if {$fn != ""} {
+                    filewrite $fn $lines
+                    set lines [list]
+                }
+                set fn [string range $line 3 end-3]
+            } else {
+                lappend lines $line
+            }
+        }
+        if {$fn != ""} {
+            filewrite $fn $lines
+        }
+    }
+}
diff --git a/schema/net/*/addr/*/check b/schema/net/*/addr/*/check
new file mode 100755 (executable)
index 0000000..172909c
--- /dev/null
@@ -0,0 +1,19 @@
+#!/usr/bin/env tclsh8.6
+
+set addr [read -nonewline stdin]
+package require ip
+set version [::ip::version $addr]
+if {$version == -1} {
+    puts "invalid format"
+    exit 1
+}
+if {[::ip::mask $addr] != ""} {
+    puts "prefixlen must be empty"
+    exit 1
+}
+if {$version == 4} {
+    set addr [::ip::normalize $addr]
+} else {
+    set addr [::ip::contract $addr]
+}
+puts $addr
diff --git a/schema/net/*/addr/*/descr b/schema/net/*/addr/*/descr
new file mode 100644 (file)
index 0000000..fd3e48c
--- /dev/null
@@ -0,0 +1,2 @@
+No CIDR notation, just a pure address.
+Set prefix length through net/*/addr/*/prefixlen.
diff --git a/schema/net/*/addr/*/prefixlen/check b/schema/net/*/addr/*/prefixlen/check
new file mode 100755 (executable)
index 0000000..e673657
--- /dev/null
@@ -0,0 +1,23 @@
+#!/usr/bin/env tclsh8.6
+
+set addr [file tail [file dirname [lindex $argv 0]]]
+set maxlen 128
+set deflen 64
+package require ip
+if {[::ip::version $addr] == 4} {
+    set maxlen 32
+    set deflen 24
+}
+set prefixlen [read -nonewline stdin]
+if {$prefixlen == ""} {
+    set prefixlen $deflen
+}
+if {! [string is integer -strict $prefixlen]} {
+    puts "invalid integer"
+    exit 1
+}
+if {$prefixlen <= 0 || $prefixlen > $maxlen} {
+    puts "not in range"
+    exit 1
+}
+puts $prefixlen
diff --git a/schema/net/*/addr/*/prefixlen/descr b/schema/net/*/addr/*/prefixlen/descr
new file mode 100644 (file)
index 0000000..ea80de7
--- /dev/null
@@ -0,0 +1 @@
+If not set, then defaults to /64 (or /24 for IPv4).
diff --git a/schema/net/*/addr/*/prefixlen/title b/schema/net/*/addr/*/prefixlen/title
new file mode 100644 (file)
index 0000000..3c0d1c5
--- /dev/null
@@ -0,0 +1 @@
+Prefix length
diff --git a/schema/net/*/addr/*/title b/schema/net/*/addr/*/title
new file mode 100644 (file)
index 0000000..7e93a2e
--- /dev/null
@@ -0,0 +1 @@
+Network address
diff --git a/schema/net/*/check b/schema/net/*/check
new file mode 100755 (executable)
index 0000000..0d492a1
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env tclsh8.6
+
+set n [read -nonewline stdin]
+if {! [regexp {^[a-z0-9]+$} $n]} {
+    puts {does not match ^[a-z0-9]+$}
+    exit 1
+}
+puts $n
diff --git a/schema/net/*/mtu/check b/schema/net/*/mtu/check
new file mode 100755 (executable)
index 0000000..69c3899
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/env tclsh8.6
+
+set i [read -nonewline stdin]
+if {$i == ""} {
+    set i 1500
+}
+if {! [string is integer -strict $i]} {
+    puts "invalid integer"
+    exit 1
+}
+if {$i <= 0 || $i > 65535} {
+    puts "not in (0..65536) range"
+    exit 1
+}
+puts $i
diff --git a/schema/net/*/mtu/descr b/schema/net/*/mtu/descr
new file mode 100644 (file)
index 0000000..00b5788
--- /dev/null
@@ -0,0 +1 @@
+If not set, then defaults to 1500.
diff --git a/schema/net/*/mtu/title b/schema/net/*/mtu/title
new file mode 100644 (file)
index 0000000..9e88021
--- /dev/null
@@ -0,0 +1 @@
+Maximum transmission unit
diff --git a/schema/net/*/title b/schema/net/*/title
new file mode 100644 (file)
index 0000000..e8d980d
--- /dev/null
@@ -0,0 +1 @@
+Network interface name
diff --git a/schema/srv/*/autostart/check b/schema/srv/*/autostart/check
new file mode 100755 (executable)
index 0000000..71c4051
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env tclsh8.6
+
+set n [read -nonewline stdin]
+if {$n == ""} {
+    set n no
+}
+if {! [string is boolean $n]} {
+    puts {is not boolean}
+    exit 1
+}
+puts $n
diff --git a/schema/srv/*/autostart/title b/schema/srv/*/autostart/title
new file mode 100644 (file)
index 0000000..2bdb90a
--- /dev/null
@@ -0,0 +1 @@
+Is autostart enabled?
diff --git a/schema/srv/*/check b/schema/srv/*/check
new file mode 100755 (executable)
index 0000000..0f8cef8
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/env tclsh8.6
+
+set n [read -nonewline stdin]
+if {! [regexp {^[.A-Za-z0-9_-]+$} $n]} {
+    puts {does not match ^[.A-Za-z0-9_-]+$}
+    exit 1
+}
+puts $n
diff --git a/schema/srv/*/title b/schema/srv/*/title
new file mode 100644 (file)
index 0000000..0cceb55
--- /dev/null
@@ -0,0 +1 @@
+Service/daemon name
diff --git a/schema/sys/hostname/check b/schema/sys/hostname/check
new file mode 100755 (executable)
index 0000000..a90bf84
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env tclsh8.6
+
+set n [read -nonewline stdin]
+if {$n == ""} {
+    set n unknown
+}
+if {! [regexp {^[a-z0-9]+$} $n]} {
+    puts {does not match ^[a-z0-9]+$}
+    exit 1
+}
+puts $n
diff --git a/schema/sys/hostname/title b/schema/sys/hostname/title
new file mode 100644 (file)
index 0000000..96010e8
--- /dev/null
@@ -0,0 +1 @@
+Hostname
diff --git a/schema/sys/note/check b/schema/sys/note/check
new file mode 100755 (executable)
index 0000000..7481f32
--- /dev/null
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exec cat
diff --git a/schema/sys/note/title b/schema/sys/note/title
new file mode 100644 (file)
index 0000000..3f7558b
--- /dev/null
@@ -0,0 +1 @@
+Arbitrary note
diff --git a/schema/ui/password/check b/schema/ui/password/check
new file mode 100755 (executable)
index 0000000..f396097
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/env tclsh8.6
+
+set passwd [read -nonewline stdin]
+if {$passwd == ""} {
+    set passwd admin
+}
+if {! [regexp {^[0-9a-z-]+$} $passwd]} {
+    puts {does not match ^[0-9a-z-]+$}
+    exit 1
+}
+puts $passwd
diff --git a/schema/ui/password/descr b/schema/ui/password/descr
new file mode 100644 (file)
index 0000000..f3a29c6
--- /dev/null
@@ -0,0 +1 @@
+Password for the admin user accessing WebUI.
diff --git a/schema/ui/password/title b/schema/ui/password/title
new file mode 100644 (file)
index 0000000..22c409e
--- /dev/null
@@ -0,0 +1 @@
+WebUI's admin password