[Info] AVM-Firmwarevariablen abfragen/setzen mit einem einzigen Aufruf und ohne ctlmgr_ctl

PeterPawn

IPPF-Urgestein
Mitglied seit
10 Mai 2006
Beiträge
13,765
Punkte für Reaktionen
1,261
Punkte
113
Da ich ab und an mal Bezug darauf nehme und dann immer auf ein Freetz-Ticket verweisen muß, will ich mal etwas zum Lua-Variableninterface schreiben und wie man von der Shell aus darauf zugreifen kann. Sollte ich einen anderen Beitrag dazu übersehen haben, ist entweder der Vortrag des Redners Glück oder man kann vergleichen. Wenn es am Ende zwei nahezu identische Beiträge wären, lösche ich meinen aber auch wieder ... aber ich habe bisher nichts dazu gefunden.

Man kann das Lua-Variableninterface mit dem Utility /bin/luavar benutzen, das irgendwann mal von AVM in die Firmware integriert wurde. Die Verwendung dieses Programms durch AVM selbst habe ich bisher nur bei den DOCSIS-Modellen gesehen, wo damit in einigen Fällen Daten zu den SIP-Telefonnummern vom SNMP-Stack in die "normale" voip.cfg übertragen werden.

So wie es von AVM auch in den Lua-Files für das GUI praktiziert wird, kann man über das Interface auch komplette "Listen" von Einträgen abfragen, sogar mit der Vorgabe der "Grenzen" für eine Art Paging.

Für die reine Abfrage von Werten kann man (als funktionsfähiges Beispiel) das folgende Script verwenden:
EDIT: Das Script wurde noch etwas geändert, um einerseits einen ordentlichen Exit-Code zu setzen und andererseits Fehlernachrichten nicht mehr nach 'stdout', sondern wie es sich gehört nach 'stderr' auszugeben.
Rich (BBCode):
#! /bin/luavar

lineno = 0;
rc = 0;

while true do
    local line = io.read("*line");
    if (line == nil) then
        if (lineno == 0) then
            io.stderr:write("Missing requests\n");
            os.exit(2);
        end
        break;
    end
    lineno = lineno + 1;
    local varname, query = string.match(line, "(.*)=(.*)");
    if (varname ~= nil and query ~= nil) then
        if (string.find(query, "%(.*%)") == nil) then
            local single_res = box.query(query);
            if (single_res == nil) then
                print(varname.."=\"***no result***\"");
            else
                print(varname.."=\""..single_res.."\"");
            end
        else   
            local columns = {};
            local first, last = string.find(query, "%(.*%)");
            local list = string.sub(query, first + 1, last - 1);
            for col in string.gmatch(list, "[^,]+") do
                table.insert(columns, col);
            end
            if (string.find(query, "listwindow%(.*%)") ~= nil) then
                table.remove(columns, 1);
                table.remove(columns, 1);
            end
            local result = box.multiquery(query);
            local index = 0;
            for i, entry in ipairs(result) do
                for j, value in ipairs(entry) do
                    j = j - 1;
                    if (j > 0) then
                        colname = columns[j];
                    else
                        colname = "index";
                    end
                    print(varname.."_"..i.."_"..colname.."=\""..value.."\"");
                end
                index = i;
            end
            print(varname.."_count="..index);
        end
    else
        io.stderr:write("Malformed request at line ",lineno,"\n");
        rc = 1;
-- die folgende Zeile aktivieren (-- am Beginn entfernen), um bei falscher Eingabe die Verarbeitung abzubrechen
--        break;
    end
end

os.exit(rc);
Abspeichern (z.B. als "queries.lua") und "ausführbar" machen ... es ist kein Shell-Script, somit macht ein Aufruf mit "sh" auch keinen Sinn. Wenn schon, dann müßte es eine Zeile "luavar queries.lua" sein ... dank "shebang" in der ersten Zeile muß man sich bei einer ausführbaren Datei darum aber nicht kümmern.

Über STDIN erwartet das Script Zeilen in der Form
Rich (BBCode):
variable=query
Solange dabei query eine einfache Abfrage eines Wertes ist, wird dieser als eine Zeile
Rich (BBCode):
variable="value"
ausgegeben, wenn die Abfrage keiner bekannten Variablen entspricht, wird als Wert "***no result***" verwendet.

Spannender wird es dann, wenn es um die Abfrage einer Liste geht. Diese hat ja in der AVM-Firmware z.B. das Format
Rich (BBCode):
sip:settings/sip/list(ID,displayname)
und in der Eingabedatei wird daraus dann eine Zeile:
Rich (BBCode):
SIP=sip:settings/sip/list(ID,displayname)
Nach der Abfrage der Variablen wird dann eine Liste in der folgenden Form ausgegeben:
Rich (BBCode):
SIP_count=m
SIP_n_index=single_name
SIP_n_ID=value
SIP_n_displayname=value
Für jeden Eintrag in der Liste wird also anhand seiner Position ein Index n gebildet und die einzelnen Felder der Abfrage werden dann als Zuweisungen zu einer passenden Variablen ausgegeben. Der spezielle "Feldname" index wird für die Ausgabe des Wertes benutzt, den man benötigt, um für diesen einen Eintrag eine gezielte Abfrage auszuführen. Wenn also die Abfrage von
Rich (BBCode):
SIP=sip:settings/sip/list(ID,displayname)
ein Ergebnis der Form
Rich (BBCode):
SIP_1_index="sip0"
SIP_1_ID="0"
SIP_1_displayname="80"
SIP_2_index="sip1"
SIP_2_ID="1"
SIP_2_displayname="82"
SIP_3_index="sip2"
SIP_3_ID="18"
SIP_3_displayname="620"
SIP_count=3
liefert, dann kann man für die zweite Nummer (sip1, in der voip.cfg als ua2 abgelegt) weitere Angaben gezielt mit einer Zeile
Rich (BBCode):
REGSRVR=sip:settings/sip1/registrar
ermitteln.

Zurück zur Liste ... der zusätzliche Eintrag "variable_count" in der Ausgabe einer Liste (er wird immer am Ende der Liste angefügt, auch wenn er oben als erster Eintrag aufgeführt ist) enthält die Anzahl der Zeilen in der "Ergebnistabelle". Wenn man also mit einem weiteren Script über die Ausgabe iterieren will, hat man mit dieser Zeile den maximalen Wert seines Schleifenzählers.

Beispiele für abfragbare Daten findet man in der AVM-Firmware (insb. in den Lua-Files für das GUI) in Massen ... es gibt aber m.W. nur eine einzige Stelle bisher, wo von den "Paging"-Fähigkeiten der Schnittstelle auch Gebrauch gemacht wird.

Bei einer Abfrage der Form
Rich (BBCode):
variable=section:settings/listwindow(x,y,field_1,...,field_n)
wird die Menge der ausgegebenen Daten bereits von der "box.multiquery"-Funktion begrenzt. Es werden nur noch y Einträge ausgegeben, beginnend mit dem x-ten Eintrag (nullbasiert).

Zum Abschluß noch ein komplettes Beispiel für mögliche Abfragen, allerdings aus vollkommen verschiedenen Firmware-"Bereichen" und somit bitte wirklich nur als Beispiel zu verstehen. Eine Eingabedatei der Form
Rich (BBCode):
HOST=box:settings/hostname
SSID1=wlan:settings/ssid
SSID2=wlan:settings/ssid_scnd
FORWARDS=forwardrules:settings/rule/list(activated,description,protocol,port,fwip,fwport,endport)
SIPS=sip:settings/sip/list(ID,displayname)
NUMBERS=telcfg:settings/VoipExtension/listwindow(2,2,Name,enabled) <=== eingeschränkte Ergebnismenge
DEVICES=ctlusb:settings/device/count
PHYS=usbdevices:settings/physmedium/list(name,vendor,serial,fw_version,conntype,capacity,status,usbspeed,model)
PHYSCNT=usbdevices:settings/physmediumcnt
VOLS=usbdevices:settings/logvol/list(name,status,enable,phyref,filesystem,capacity,usedspace,readonly)
VOLSCNT=usbdevices:settings/logvolcnt
PARTS=ctlusb:settings/storage-part/count
SIP1=sip:settings/sip1/active <=== ungültiger "Feldname"
SIP1=sip:settings/sip1/activated <=== gültiger Name für einzelnen Wert
HOSTNAME=box:settings/Hostname <=== ungültiger Variablenname
- als "abfragen" gespeichert - wird angenommen. Dann sähe ein Aufruf z.B. so aus (gesetzt den Fall, der Dateiname von oben wurde verwendet und das Script unter /var/media/ftp/queries.lua abgelegt):
Rich (BBCode):
cat abfragen | /var/media/ftp/queries.lua
und ein Ergebnis könnte so aussehen (auch wenn ich von einem Portforwarding für einen SSH-Server auf Port 22 dringend abraten würde):
Rich (BBCode):
HOST="FB7490"
SSID1="WLAN24"
SSID2="WLAN5"
FORWARDS_1_index="rule0"
FORWARDS_1_activated="1"
FORWARDS_1_description="SSH-Server"
FORWARDS_1_protocol="TCP"
FORWARDS_1_port="22"
FORWARDS_1_fwip="192.168.178.2"
FORWARDS_1_fwport="22"
FORWARDS_1_endport="22"
FORWARDS_count=1
SIPS_1_index="sip0"
SIPS_1_ID="0"
SIPS_1_displayname="80"
SIPS_2_index="sip1"
SIPS_2_ID="1"
SIPS_2_displayname="82"
SIPS_3_index="sip2"
SIPS_3_ID="18"
SIPS_3_displayname="620"
SIPS_count=3
NUMBERS_1_index="VoipExtension2"
NUMBERS_1_Name="3CXPhone"
NUMBERS_1_enabled="1"
NUMBERS_2_index="VoipExtension3"
NUMBERS_2_Name=""
NUMBERS_2_enabled="0"
NUMBERS_count=2
DEVICES="1"
PHYS_1_index="physmedium0"
PHYS_1_name="sys"
PHYS_1_vendor="WDC WD16"
PHYS_1_serial="0"
PHYS_1_fw_version="01.0"
PHYS_1_conntype="USB 2.0"
PHYS_1_capacity="155414663168"
PHYS_1_status="Online"
PHYS_1_usbspeed="480"
PHYS_1_model="00BEVE-11UYT0"
PHYS_count=1
PHYSCNT="1"
VOLS_1_index="logvol0"
VOLS_1_name="data"
VOLS_1_status="Online"
VOLS_1_enable="1"
VOLS_1_phyref="1"
VOLS_1_filesystem="ext3"
VOLS_1_capacity="121594040320"
VOLS_1_usedspace="19144179712"
VOLS_1_readonly="0"
VOLS_2_index="logvol1"
VOLS_2_name="system"
VOLS_2_status="Online"
VOLS_2_enable="1"
VOLS_2_phyref="1"
VOLS_2_filesystem="ext3"
VOLS_2_capacity="33820622848"
VOLS_2_usedspace="990674944"
VOLS_2_readonly="0"
VOLS_count=2
VOLSCNT="2"
PARTS="2"
SIP1="***no result***"
SIP1="1"
HOSTNAME="non-emu"
Die letzten Zeilen oben zeigen zwei unterschiedliche Ausgaben für ungültige Variablennamen.
Wenn die AVM-Firmware für einen Wert "nil" zurückgibt (also "nichts"), dann wird vom Script "***no result***" eingesetzt.
Bei bestimmten "Sektionen" kann es aber schon passieren, daß von der Firmware für ungültige Namen der Wert "non-emu" zurückgegeben wird, wie man es von ctlmgr_ctl auch kennt.

Und noch ein weiterer Punkt wird oben deutlich: Für die eindeutige Benennung der Variablen bei "Einzelabruf" (oben in den Zeilen mit "SIP1=..." gezeigt) ist der Aufrufer selbst verantwortlich. Wenn da doppelte Namen verwendet werden, juckt das das Script in keinster Weise, da die Eingabedatei Zeile für Zeile abgearbeitet wird. Die Probleme kommen dann u.U. erst beim Parsen der Ergebnisse ...

Wie man das dann ziemlich leicht wieder in Shell-Variable umwandelt (eval) und es im weiteren Verlauf dann auseinandernimmt und passend wieder zusammensetzt, überlasse ich der Phantasie des Lesers. Ich wollte nur zeigen, daß es jenseits des "üblichen" ctlmgr_ctl noch andere Wege gibt, an AVM-Firmwareeinstellungen zu gelangen und dabei unter anderem auch auf Werte zuzugreifen, die ansonsten über das ctlmgr-Interface nicht ohne weiteres abzufragen sind.

Man kann über das Lua-Interface auch Variablen setzen, dazu komme ich gleich. Der weitaus häufigere Fall dürfte die Abfrage von Einstellungen sein, vielleicht hilft es ja jemandem weiter. Einiges von den Einstellungen läßt sich auch Stück für Stück über einzelne Abfragen per ctlmgr_ctl ermitteln, bei mir macht sich aber z.B. der Unterschied zwischen der einzelnen Abfrage der Einstellungen für >20 VPN-Verbindungen (jeweils 10 Einstellungen) und der "kompakten Listenabfrage" (so auch im AVM-GUI zu finden)
Rich (BBCode):
VPN=vpn:settings/connection/list(activated,name,deletable,editable,state,remote_ip,src,dst,connected_since,settings)
schon bemerkbar. Anstelle von 200 Aufrufen für ctlmgr_ctl bleibt eben nur noch ein einzelner Scriptaufruf übrig, das Parsen der Ergebnisse sieht am Ende ziemlich identisch aus in beiden Varianten.

Zum Setzen von Einstellungen kann man das folgende Script verwenden:
Rich (BBCode):
#! /bin/luavar

lineno = 0;
rc = 0;
cmtable = {};

while true do
    local line = io.read("*line");
    if (line == nil) then
        if (lineno == 0) then
            io.stderr:write("Missing set statements\n");
            os.exit(2);
        else
            break;
        end
    end
    lineno = lineno + 1;
    local varname, value = string.match(line, "(.*)=(.*)");
    if (varname ~= nil and value ~= nil) then
        table.insert(cmtable, { ["name"] = varname, ["value"] = value } );
    else
        io.stderr:write("Malformed set statement at line ",lineno,"\n");
        os.exit(127);
    end
end

err = 0;
message = "";
err, message = box.set_config(cmtable);
if (err == 0) then
    io.stderr:write("OK\n");
    rc = 0;
else
    if (string.len(message) > 0) then   
        io.stderr:write(message,"\n");
    else
        io.stderr:write("error\n");
    end
    rc = 1;
end

os.exit(rc);
Bei mir heißt dieses Script ab jetzt "set.lua". Die Anwendung erfolgt analog zu "queries.lua", jedoch werden Zeilen der Form
Rich (BBCode):
varname=value
erwartet. Es können also auch mehrere Einstellungen auf einmal gesetzt werden, bei einigen Firmware-Einstellungen ist das sogar erforderlich, da ansonsten ein Fehler auftritt, wenn am Ende ein "Set" aus Einstellungen (z.B. für einen DynDNS-Account) unvollständig ist. Auch hier kann wieder ein Blick in die Lua-Dateien des AVM-GUI die richtigen Variablennamen offenbaren.

Sollte beim Setzen von Einstellungen ein Fehler auftreten und eine Fehlermeldung dazu vom Variableninterface zurückgegeben werden, wird diese auf stderr ausgegeben und ein Exit-Code von 1 gesetzt. Ob und wie weit dann Zuweisungen teilweise ausgeführt wurden, hängt nach meinen Tests von der Reihenfolge der Anweisungen ab ... ich würde die Vermutung wagen, daß beim ersten Fehler abgebrochen wird.

Auch hier mal ein Beispiel, wir setzen eine neue statische IPv4-Route:
Rich (BBCode):
active=1
ipaddr=192.168.100.0
netmask=255.255.255.0
gateway=192.168.178.2
eval $(echo "newId=route:settings/route/newid" | queries.lua)
echo -e "route:settings/$newId/activated=$active\nroute:settings/$newId/ipaddr=$ipaddr\nroute:settings/$newId/netmask=$netmask\nroute:settings/$newId/gateway=$gateway" | set.lua
echo "rc = $?" 1>&2
Wer lieber ein Script-File hätte, bei dem man für das Setzen einzelner Einstellungen diese direkt als Parameter angeben kann und keine "Eingabedatei" für stdin benötigt, der nimmt dann dieses Wrapper-File als "setvar.sh":
Rich (BBCode):
#! /bin/sh
name=$1
shift
echo "$name=$*" | set.lua
exit $?
Es ist mir jedenfalls nicht gelungen, über das AVM-Utility "/bin/luavar" irgendwie an die auf der Kommandozeile angegebenen Parameter zu gelangen.
 
Zuletzt bearbeitet:
  • Like
Reaktionen: FischersFreetz

MaxMuster

IPPF-Promi
Mitglied seit
1 Feb 2005
Beiträge
6,932
Punkte für Reaktionen
3
Punkte
38
Klasse, klar zusammen gefasst und auch gleich mit funktionsfähigem Skript zur sofortigen Nutzung!
Dafür ein fettes DANKE !
 

koyaanisqatsi

IPPF-Urgestein
Mitglied seit
24 Jan 2013
Beiträge
13,339
Punkte für Reaktionen
457
Punkte
83
Moin

Auch von mir ein Dankeschön dafür.
Ich denke damit lassen sich HTTP CGIs nochmal beschleunigen.
Denn ctlmgr_ctl Abfragen sind doch deutlich langsamer.

EDIT: Grad als CGI getestet, kein Vergleich, keine Wartezeit, astrein. ;)
Code:
#!/bin/sh
echo 'content-type: text/html
'
cat hd_st
echo "<title>LUA Variablen</title>"
cat hd_en
echo "<body>"
cat bd_mn
echo "<pre>$(cat abfragen | ../scripts/query.lua)</pre>
</body>
</html>
"

EDIT2: Thema bewerten geht nicht bei mir, die Fehlermeldung...
Code:
Ein erforderliches Feld <em>ipaddress</em> fehlt oder enthält eine ungültige Angabe.
...liegt das an meiner IPv6 Adresse?
 
Zuletzt bearbeitet:

PeterPawn

IPPF-Urgestein
Mitglied seit
10 Mai 2006
Beiträge
13,765
Punkte für Reaktionen
1,261
Punkte
113
So, der Vollständigkeit halber habe ich auch noch ein Script-File zum Setzen von Einstellungen über "box.set_config" erstellt und in #1 eingefügt. Im Zuge dessen habe ich noch ein paar Änderungen an "queries.lua" vorgenommen, um den Einsatz in anderen Umgebungen zu erleichtern. So wird jetzt das Mischen der normalen Ausgabe und eventueller Fehlermeldungen vermieden (das erleichtert ein 'eval' oder 'source') und ein Exit-Code gesetzt, wenn ein Fehler aufgetreten ist.
 
Zuletzt bearbeitet:

FischersFreetz

Neuer User
Mitglied seit
22 Feb 2019
Beiträge
169
Punkte für Reaktionen
24
Punkte
18
Das Abfragen und (Er)setzen via /bin/luavar ist u.U. sehr praktisch.
Gibt es damit auch eine Möglichkeit, neu gesetzte Firmware-Variblen wieder zu löschen?
(Ich würde gern die Blacklist der Fritzbox dynamisch ersetzen können.)
 

PeterPawn

IPPF-Urgestein
Mitglied seit
10 Mai 2006
Beiträge
13,765
Punkte für Reaktionen
1,261
Punkte
113
Bei Arrays gab es irgendeine Aktion (aus dem Gedächtnis), mit der man einen Eintrag entfernen konnte ... das läuft dann meist über irgendeine delete-Property für das Array-Element (adressiert über den Namen der Liste und den Index des Eintrags), die man auf 1 setzen muß. Wie das genau geht, kannst Du Dir (immer noch aus dem Gedächtnis) z.B. bei der Benutzerverwaltung ansehen (da gibt es definitiv eine Funktion, um einen Benutzer zu entfernen) ... ob das bei der Blacklist genauso funktioniert, mußt Du halt selbst in den entsprechenden GUI-Files von AVM (das ist ja vermutlich auch alles Lua) recherchieren.
 

FischersFreetz

Neuer User
Mitglied seit
22 Feb 2019
Beiträge
169
Punkte für Reaktionen
24
Punkte
18
Ich habe mir die Problematik mal angesehen und konnte - dank deiner Impulse - für mich (etwas) Licht ins Dunkle bringen. Das Löschen von AVM-Firmwarevariablen unterscheidet sich vom Setzen:
Rich (BBCode):
### Setzen eines neuen Blacklist-Eintrages
eval $(echo "newId=parental_control:settings/blacklist0/url/newid" | ./queries.lua)
echo "parental_control:settings/blacklist0/$newId/url=zuloeschen.de" | ./set.lua
intern hauptsächlich nur durch zwei Veränderungen (durch einen command-Befehl und dem Parameter "delete"):
Rich (BBCode):
### Löschen des oben angelegten Blacklist-Eintrages
echo "parental_control:command/blacklist0/$newId=delete" | ./set.lua
Und wie man sieht, lässt sich so sogar dein set.lua-Script - natürlich mit den veränderten Eingabewerten - zum Löschen von AVM-Firmwarevariablen verwenden.

Nebenbei, am Rande... Beim Durchdringen der Materie fiel mir auf, dass die Zeilen 28 bis 37 des queries.lua -Scripts (für mich) keinen Sinn ergeben bzw. bei der Ausführung keine Bedeutung haben.
Rich (BBCode):
...
            local columns = {};
            local first, last = string.find(query, "%(.*%)");
            local list = string.sub(query, first + 1, last - 1);
            for col in string.gmatch(list, "[^,]+") do
                table.insert(columns, col);
            end
            if (string.find(query, "listwindow%(.*%)") ~= nil) then
                table.remove(columns, 1);
                table.remove(columns, 1);
            end
...
Und nochmals Danke für deine "Fritzbox-systemrelevanten" Beiträge, mit denen ich schon so einige Probleme effizient lösen konnte.
 
Zuletzt bearbeitet:

PeterPawn

IPPF-Urgestein
Mitglied seit
10 Mai 2006
Beiträge
13,765
Punkte für Reaktionen
1,261
Punkte
113
bei der Ausführung keine Bedeutung haben.
Das stammt noch aus einer Zeit, wo aus dem box.multiquery tatsächlich nur eine Tabelle kam (bzw. ein Array von Tabellen, wenn es mehr als einen Eintrag gab) und kein verkapptes Objekt, wo man auch noch den Namen der "Pseudo-Properties" ermitteln (und ausgeben) kann.

Wenn Du nachvollziehen willst, warum ich das damals dort so und nicht anders gemacht habe, kannst Du einen Blick in die /usr/www/avm/query.lua bis zur Version 06.60 (inkl.) werfen - das parse_rows() arbeitet dort im Prinzip genauso.

EDIT: Ich habe das gerade noch einmal getestet ... wenn ich mich nicht vertan habe, arbeitet das /bin/luavar immer noch genauso, während unter dem ctlmgr in den GUI-Seiten das box-Objekt wohl anders implementiert wurde und dort tatsächlich die Ergebnisse dieser Datenabfragen als assoziatives Array (also mit alphanumerischen Keys, die dann beim Iterieren über pairs() auch wieder als solche angeboten werden) vorliegen. Ich gehe mal davon aus, daß das /bin/luavar selbst für das Erzeugen der Table aus den Ergebnissen der Abfrage beim ctlmgr zuständig ist und daß da nichts geändert wurde (egal, wie die query.lua auch aussehen mag). Ich kriege jedenfalls beim Iterieren über einen Eintrag in einem Listen-Array auch nur numerische Indizes angeboten ... das Ergebnis ist bei pairs() und ipairs() genau dasselbe.
 
Zuletzt bearbeitet:
3CX

Neueste Beiträge

Statistik des Forums

Themen
237,879
Beiträge
2,102,243
Mitglieder
360,375
Neuestes Mitglied
TECHNIKERMEYER

Erhalten Sie 3CX für 1 Jahr kostenlos!

Gehostet, in Ihrer privaten Cloud oder on-Premise! Ganz ohne Haken. Geben Sie Ihren Namen und Ihre E-Mail an und los geht´s:

3CX
Für diese E-Mail-Adresse besteht bereits ein 3CX-Konto. Sie werden zum Kundenportal weitergeleitet, wo Sie sich anmelden oder Ihr Passwort zurücksetzen können, falls Sie dieses vergessen haben.
oder via