Device management
This commit is contained in:
parent
5635c2af1a
commit
b9a5437909
6 changed files with 294 additions and 9 deletions
64
config.go
64
config.go
|
|
@ -3,7 +3,10 @@ package main
|
||||||
import (
|
import (
|
||||||
// Standard
|
// Standard
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|
@ -20,6 +23,8 @@ type Config struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
Devices []Device
|
Devices []Device
|
||||||
|
|
||||||
|
filename string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Device struct {
|
type Device struct {
|
||||||
|
|
@ -39,5 +44,64 @@ func readConfig() (config Config, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err = json.Unmarshal(configData, &config)
|
err = json.Unmarshal(configData, &config)
|
||||||
|
config.filename = flagConfig
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *Config) UpdateDevice(currentName string, deviceToUpdate Device) (dev Device, err error) {
|
||||||
|
|
||||||
|
i := slices.IndexFunc(cfg.Devices, func(d Device) bool {
|
||||||
|
return strings.TrimSpace(strings.ToLower(d.Name)) == strings.TrimSpace(strings.ToLower(currentName))
|
||||||
|
})
|
||||||
|
|
||||||
|
if i > -1 {
|
||||||
|
dev = cfg.Devices[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(deviceToUpdate.Name) == "" {
|
||||||
|
err = fmt.Errorf("Name can't be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(deviceToUpdate.Address) == "" {
|
||||||
|
err = fmt.Errorf("Address can't be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(deviceToUpdate.Username) == "" {
|
||||||
|
err = fmt.Errorf("Username can't be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if deviceToUpdate.Port < 1 || deviceToUpdate.Port > 65535 {
|
||||||
|
err = fmt.Errorf("Invalid port")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dev.Name = strings.TrimSpace(strings.ToLower(deviceToUpdate.Name))
|
||||||
|
dev.Address = strings.TrimSpace(strings.ToLower(deviceToUpdate.Address))
|
||||||
|
dev.Port = deviceToUpdate.Port
|
||||||
|
dev.Username = strings.TrimSpace(deviceToUpdate.Username)
|
||||||
|
|
||||||
|
// TODO - Should be configurable...
|
||||||
|
if dev.Timeout == 0 {
|
||||||
|
dev.Timeout = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device not found - create it!
|
||||||
|
if i == -1 {
|
||||||
|
if strings.TrimSpace(deviceToUpdate.Password) == "" {
|
||||||
|
err = fmt.Errorf("Password can't be empty")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dev.Password = strings.TrimSpace(deviceToUpdate.Password)
|
||||||
|
cfg.Devices = append(cfg.Devices, dev)
|
||||||
|
} else {
|
||||||
|
if deviceToUpdate.Password != "" {
|
||||||
|
dev.Password = strings.TrimSpace(deviceToUpdate.Password)
|
||||||
|
}
|
||||||
|
cfg.Devices[i] = dev
|
||||||
|
}
|
||||||
|
|
||||||
|
j, _ := json.Marshal(cfg)
|
||||||
|
err = os.WriteFile(cfg.filename, j, 0600)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,7 @@ func (dev *RouterosDevice) Init() { // {{{
|
||||||
|
|
||||||
// query sends a RouterOS REST API query and returns the unparsed body.
|
// query sends a RouterOS REST API query and returns the unparsed body.
|
||||||
func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{
|
func (dev RouterosDevice) query(method, path string, reqBody []byte) (body []byte, err error) { // {{{
|
||||||
|
logger.Debug("FOO", "port", dev.Port)
|
||||||
url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path)
|
url := fmt.Sprintf("https://%s:%d/rest%s", dev.Host, dev.Port, path)
|
||||||
logger.Info("URL", "method", method, "url", url)
|
logger.Info("URL", "method", method, "url", url)
|
||||||
|
|
||||||
|
|
@ -150,7 +151,7 @@ func (dev *RouterosDevice) UpdateDNSEntry(record DNSEntry) (entry DNSEntry, err
|
||||||
err = json.Unmarshal(body, &entry)
|
err = json.Unmarshal(body, &entry)
|
||||||
return
|
return
|
||||||
}// }}}
|
}// }}}
|
||||||
func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {
|
func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {// {{{
|
||||||
_, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{})
|
_, err = dev.query("DELETE", "/ip/dns/static/"+id, []byte{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rosError := struct{ Detail string }{}
|
rosError := struct{ Detail string }{}
|
||||||
|
|
@ -162,7 +163,7 @@ func (dev *RouterosDevice) DeleteDNSEntry(id string) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}// }}}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
// FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router
|
// FillPeerDetails retrieves RouterOS resource ID, allowed-address and comment from the router
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
--header-background: #acbb78;
|
--header-background: #e1eabe;
|
||||||
--header-border: 1px solid #869a41;
|
--header-border: 1px solid #869a41;
|
||||||
|
|
||||||
--line-color: #ccc;
|
--line-color: #ccc;
|
||||||
|
|
@ -120,12 +120,21 @@ button {
|
||||||
background-color: var(--header-background);
|
background-color: var(--header-background);
|
||||||
|
|
||||||
.device-select {
|
.device-select {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content min-content;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-right: var(--header-border);
|
border-right: var(--header-border);
|
||||||
|
|
||||||
.device-name {
|
.label {
|
||||||
|
grid-column: 1 / -1;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#search {
|
#search {
|
||||||
|
|
@ -465,3 +474,49 @@ button {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#device-dialog {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content min-content;
|
||||||
|
align-items: top;
|
||||||
|
grid-gap: 16px;
|
||||||
|
|
||||||
|
.devices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
|
||||||
|
.header {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device {
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--header-background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: min-content 1fr;
|
||||||
|
grid-gap: 8px 16px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
border: 1px solid #aaa;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
justify-self: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,9 +25,9 @@
|
||||||
borderopacity="1.0"
|
borderopacity="1.0"
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="1.1646825"
|
inkscape:zoom="5.6568542"
|
||||||
inkscape:cx="-71.693356"
|
inkscape:cx="-20.948038"
|
||||||
inkscape:cy="-55.809199"
|
inkscape:cy="9.0156115"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
showgrid="false"
|
showgrid="false"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -303,6 +303,10 @@ export class Application {
|
||||||
this.searchWidget.searchField.focus()
|
this.searchWidget.searchField.focus()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case 'd':
|
||||||
|
new DeviceDialog(this.devices).render()
|
||||||
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
handled = false
|
handled = false
|
||||||
}
|
}
|
||||||
|
|
@ -385,6 +389,21 @@ class Device {
|
||||||
name() {// {{{
|
name() {// {{{
|
||||||
return this.data.Name?.toLowerCase() || ''
|
return this.data.Name?.toLowerCase() || ''
|
||||||
}// }}}
|
}// }}}
|
||||||
|
address() {// {{{
|
||||||
|
return this.data.Address || ''
|
||||||
|
}// }}}
|
||||||
|
port() {// {{{
|
||||||
|
return parseInt(this.data.Port || 0)
|
||||||
|
}// }}}
|
||||||
|
username() {// {{{
|
||||||
|
return this.data.Username || ''
|
||||||
|
}// }}}
|
||||||
|
password() {// {{{
|
||||||
|
return this.data.Password || ''
|
||||||
|
}// }}}
|
||||||
|
name() {// {{{
|
||||||
|
return this.data.Name?.toLowerCase() || ''
|
||||||
|
}// }}}
|
||||||
async retrieveRecords() {// {{{
|
async retrieveRecords() {// {{{
|
||||||
const data = await fetch(`/device/${this.name()}/dns_records`)
|
const data = await fetch(`/device/${this.name()}/dns_records`)
|
||||||
const json = await data.json()
|
const json = await data.json()
|
||||||
|
|
@ -410,12 +429,14 @@ class DeviceSelectWidget {
|
||||||
this.div = document.createElement('div')
|
this.div = document.createElement('div')
|
||||||
this.div.classList.add('device-select')
|
this.div.classList.add('device-select')
|
||||||
this.div.innerHTML = `
|
this.div.innerHTML = `
|
||||||
<div class="device-name">Select device</div>
|
<div class="label">Select device</div>
|
||||||
<select></select>
|
<select></select>
|
||||||
|
<img src="/images/${_VERSION}/icon_device_edit.svg">
|
||||||
`
|
`
|
||||||
|
|
||||||
this.elDeviceSelect = this.div.querySelector('select')
|
this.elDeviceSelect = this.div.querySelector('select')
|
||||||
this.elDeviceSelect.addEventListener('change', () => this.notifyDeviceSelect())
|
this.elDeviceSelect.addEventListener('change', () => this.notifyDeviceSelect())
|
||||||
|
this.div.querySelector('img').addEventListener('click', ()=>new DeviceDialog(this.devices).render())
|
||||||
}
|
}
|
||||||
this.restockDeviceSelect()
|
this.restockDeviceSelect()
|
||||||
return this.div
|
return this.div
|
||||||
|
|
@ -976,3 +997,117 @@ class SettingsDialog {
|
||||||
this.dlg.close()
|
this.dlg.close()
|
||||||
}// }}}
|
}// }}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DeviceDialog {
|
||||||
|
constructor(devices) {// {{{
|
||||||
|
this.devices = devices
|
||||||
|
this.device = null
|
||||||
|
}// }}}
|
||||||
|
render() {// {{{
|
||||||
|
// Only one open at any one time.
|
||||||
|
if (document.getElementById('device-dialog'))
|
||||||
|
return
|
||||||
|
|
||||||
|
this.dlg = document.createElement('dialog')
|
||||||
|
this.dlg.id = 'device-dialog'
|
||||||
|
this.dlg.innerHTML = `
|
||||||
|
<div class="devices">
|
||||||
|
<div class="header">Devices</div>
|
||||||
|
</div>
|
||||||
|
<div class="fields">
|
||||||
|
<div>Name</div>
|
||||||
|
<input type="text" class="name">
|
||||||
|
|
||||||
|
<div>Address</div>
|
||||||
|
<input type="text" class="address">
|
||||||
|
|
||||||
|
<div>Port</div>
|
||||||
|
<input type="number" class="port">
|
||||||
|
|
||||||
|
<div>Username</div>
|
||||||
|
<input type="text" class="username">
|
||||||
|
|
||||||
|
<div>Password</div>
|
||||||
|
<input type="text" class="password">
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="delete">Delete</button>
|
||||||
|
<button class="update">Update</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
|
||||||
|
this.elDevices = this.dlg.querySelector('.devices')
|
||||||
|
this.elFields = this.dlg.querySelector('.fields')
|
||||||
|
this.elName = this.dlg.querySelector('.fields .name')
|
||||||
|
this.elAddress = this.dlg.querySelector('.fields .address')
|
||||||
|
this.elPort = this.dlg.querySelector('.fields .port')
|
||||||
|
this.elUsername = this.dlg.querySelector('.fields .username')
|
||||||
|
this.elPassword = this.dlg.querySelector('.fields .password')
|
||||||
|
|
||||||
|
const devices = Array.from(this.devices.values())
|
||||||
|
devices.sort((a, b)=>{
|
||||||
|
if (a.name() < b.name()) return -1
|
||||||
|
if (a.name() > b.name()) return 1
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
devices.forEach(dev=>{
|
||||||
|
const devEl = document.createElement('div')
|
||||||
|
devEl.classList.add('device')
|
||||||
|
devEl.innerText = dev.name()
|
||||||
|
devEl.addEventListener('click', ()=>this.editDevice(dev, devEl))
|
||||||
|
this.elDevices.appendChild(devEl)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.dlg.querySelector('.update').addEventListener('click', ()=>this.updateDevice())
|
||||||
|
this.dlg.addEventListener('close', ()=>this.dlg.remove())
|
||||||
|
|
||||||
|
document.body.appendChild(this.dlg)
|
||||||
|
this.dlg.showModal()
|
||||||
|
}// }}}
|
||||||
|
|
||||||
|
editDevice(dev, devEl) {// {{{
|
||||||
|
this.device = dev
|
||||||
|
|
||||||
|
this.elDevices.querySelectorAll('.device.selected').forEach(el=>el.classList.remove('selected'))
|
||||||
|
devEl.classList.add('selected')
|
||||||
|
|
||||||
|
this.elName.value = dev.name()
|
||||||
|
this.elAddress.value = dev.address()
|
||||||
|
this.elPort.value = dev.port()
|
||||||
|
this.elUsername.value = dev.username()
|
||||||
|
this.elPassword.value = dev.password()
|
||||||
|
}// }}}
|
||||||
|
async updateDevice() {// {{{
|
||||||
|
const req = {
|
||||||
|
CurrentName: this.device.name(),
|
||||||
|
Device: {
|
||||||
|
Name: this.elName.value,
|
||||||
|
Address: this.elAddress.value,
|
||||||
|
Port: parseInt(this.elPort.value),
|
||||||
|
Username: this.elUsername.value,
|
||||||
|
Password: this.elPassword.value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await fetch('/device', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(req),
|
||||||
|
})
|
||||||
|
const json = await data.json()
|
||||||
|
|
||||||
|
this.device.data.Name = json.Device.Name
|
||||||
|
this.device.data.Address = json.Device.Address
|
||||||
|
this.device.data.Port = json.Device.Port
|
||||||
|
this.device.data.Username = json.Device.Username
|
||||||
|
|
||||||
|
_mbus.dispatch('device_updated', { device: this.device })
|
||||||
|
this.dlg.close()
|
||||||
|
|
||||||
|
} catch(err) {
|
||||||
|
console.error(err)
|
||||||
|
alert(err)
|
||||||
|
}
|
||||||
|
}// }}}
|
||||||
|
}
|
||||||
|
|
|
||||||
32
webserver.go
32
webserver.go
|
|
@ -33,7 +33,8 @@ func registerWebserverHandlers() { // {{{
|
||||||
}
|
}
|
||||||
|
|
||||||
http.HandleFunc("/", rootHandler)
|
http.HandleFunc("/", rootHandler)
|
||||||
http.HandleFunc("/devices", actionDevices)
|
http.HandleFunc("GET /devices", actionDevices)
|
||||||
|
http.HandleFunc("POST /device", actionDeviceUpdate)
|
||||||
http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords)
|
http.HandleFunc("GET /device/{dev}/dns_records", actionDNSRecords)
|
||||||
http.HandleFunc("POST /device/{dev}/record", actionRecordSave)
|
http.HandleFunc("POST /device/{dev}/record", actionRecordSave)
|
||||||
http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete)
|
http.HandleFunc("DELETE /device/{dev}/record/{id}", actionRecordDelete)
|
||||||
|
|
@ -96,6 +97,35 @@ func actionDevices(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
|
||||||
w.Write(j)
|
w.Write(j)
|
||||||
} // }}}
|
} // }}}
|
||||||
|
func actionDeviceUpdate(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
|
var req struct {
|
||||||
|
CurrentName string
|
||||||
|
Device Device
|
||||||
|
}
|
||||||
|
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
err := json.Unmarshal(body, &req)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
device, err := config.UpdateDevice(req.CurrentName, req.Device)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
device.Password = "" // don't leak unnecessarily
|
||||||
|
|
||||||
|
j, _ := json.Marshal(struct {
|
||||||
|
OK bool
|
||||||
|
Device Device
|
||||||
|
}{
|
||||||
|
true,
|
||||||
|
device,
|
||||||
|
})
|
||||||
|
w.Write(j)
|
||||||
|
} // }}}
|
||||||
|
|
||||||
func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{
|
func actionDNSRecords(w http.ResponseWriter, r *http.Request) { // {{{
|
||||||
devname := r.PathValue("dev")
|
devname := r.PathValue("dev")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue