2017-04-10 18:54:12 +02:00
|
|
|
package runtime
|
2015-12-29 14:05:47 +01:00
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
|
|
|
"os"
|
|
|
|
"sync"
|
|
|
|
"time"
|
2016-03-12 03:23:12 +01:00
|
|
|
|
2019-01-17 13:26:16 +01:00
|
|
|
"github.com/bdlm/log"
|
|
|
|
|
2017-03-03 16:19:35 +01:00
|
|
|
"github.com/FreifunkBremen/yanic/data"
|
2018-01-07 21:00:56 +01:00
|
|
|
"github.com/FreifunkBremen/yanic/lib/jsontime"
|
2015-12-29 14:05:47 +01:00
|
|
|
)
|
|
|
|
|
2016-03-07 01:37:38 +01:00
|
|
|
// Nodes struct: cache DB of Node's structs
|
2015-12-29 14:05:47 +01:00
|
|
|
type Nodes struct {
|
2024-07-20 00:31:16 +02:00
|
|
|
List map[string]*Node `json:"nodes"` // the current nodemap, indexed by node ID
|
|
|
|
ifaceToNodeID map[string]string // mapping from MAC address to NodeID
|
|
|
|
ifaceToLinkType map[string]LinkType // mapping from MAC address to LinkType
|
|
|
|
ifaceToLinkProtocol map[string]LinkProtocol // mapping from MAC address to LinkProtocol
|
|
|
|
config *NodesConfig
|
2016-06-16 18:50:43 +02:00
|
|
|
sync.RWMutex
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
|
|
|
|
2016-03-20 18:30:44 +01:00
|
|
|
// NewNodes create Nodes structs
|
2018-01-07 21:00:56 +01:00
|
|
|
func NewNodes(config *NodesConfig) *Nodes {
|
2015-12-29 14:05:47 +01:00
|
|
|
nodes := &Nodes{
|
2024-07-20 00:31:16 +02:00
|
|
|
List: make(map[string]*Node),
|
|
|
|
ifaceToNodeID: make(map[string]string),
|
|
|
|
ifaceToLinkType: make(map[string]LinkType),
|
|
|
|
ifaceToLinkProtocol: make(map[string]LinkProtocol),
|
|
|
|
config: config,
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
|
|
|
|
2018-01-07 21:00:56 +01:00
|
|
|
if config.StatePath != "" {
|
2016-03-20 18:30:44 +01:00
|
|
|
nodes.load()
|
|
|
|
}
|
2017-01-29 20:46:06 +01:00
|
|
|
|
2015-12-29 14:05:47 +01:00
|
|
|
return nodes
|
|
|
|
}
|
|
|
|
|
2017-01-29 22:14:40 +01:00
|
|
|
// Start all services to manage Nodes
|
2016-11-20 18:45:18 +01:00
|
|
|
func (nodes *Nodes) Start() {
|
|
|
|
go nodes.worker()
|
|
|
|
}
|
|
|
|
|
2017-05-20 14:46:29 +02:00
|
|
|
func (nodes *Nodes) AddNode(node *Node) {
|
|
|
|
nodeinfo := node.Nodeinfo
|
|
|
|
if nodeinfo == nil || nodeinfo.NodeID == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
nodes.Lock()
|
|
|
|
defer nodes.Unlock()
|
|
|
|
nodes.List[nodeinfo.NodeID] = node
|
2024-07-20 00:31:16 +02:00
|
|
|
nodes.readIfaces(nodeinfo, node.Neighbours, false)
|
2017-05-20 14:46:29 +02:00
|
|
|
}
|
|
|
|
|
2016-03-20 16:25:33 +01:00
|
|
|
// Update a Node
|
2016-07-14 01:19:03 +02:00
|
|
|
func (nodes *Nodes) Update(nodeID string, res *data.ResponseData) *Node {
|
2016-03-20 17:10:39 +01:00
|
|
|
now := jsontime.Now()
|
2015-12-29 14:05:47 +01:00
|
|
|
|
|
|
|
nodes.Lock()
|
2022-03-28 03:56:00 +02:00
|
|
|
node := nodes.List[nodeID]
|
2015-12-29 14:05:47 +01:00
|
|
|
|
|
|
|
if node == nil {
|
|
|
|
node = &Node{
|
2016-03-20 16:25:33 +01:00
|
|
|
Firstseen: now,
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
2016-02-25 21:24:54 +01:00
|
|
|
nodes.List[nodeID] = node
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
2019-01-24 02:56:13 +01:00
|
|
|
if res.Nodeinfo != nil {
|
2024-07-20 00:31:16 +02:00
|
|
|
nodes.readIfaces(res.Nodeinfo, res.Neighbours, true)
|
2017-05-20 14:46:29 +02:00
|
|
|
}
|
2015-12-29 14:05:47 +01:00
|
|
|
nodes.Unlock()
|
|
|
|
|
2017-09-27 13:55:02 +02:00
|
|
|
// Update wireless statistics
|
|
|
|
if statistics := res.Statistics; statistics != nil {
|
2016-07-14 01:19:03 +02:00
|
|
|
// Update channel utilization if previous statistics are present
|
2017-09-27 13:55:02 +02:00
|
|
|
if node.Statistics != nil && node.Statistics.Wireless != nil && statistics.Wireless != nil {
|
|
|
|
statistics.Wireless.SetUtilization(node.Statistics.Wireless)
|
2016-07-14 01:19:03 +02:00
|
|
|
}
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
2016-07-14 01:19:03 +02:00
|
|
|
|
2017-09-27 13:55:02 +02:00
|
|
|
// Update fields
|
|
|
|
node.Lastseen = now
|
|
|
|
node.Online = true
|
|
|
|
node.Neighbours = res.Neighbours
|
2019-01-24 02:56:13 +01:00
|
|
|
node.Nodeinfo = res.Nodeinfo
|
2017-09-27 13:55:02 +02:00
|
|
|
node.Statistics = res.Statistics
|
2019-11-17 10:44:11 +01:00
|
|
|
node.CustomFields = res.CustomFields
|
2017-09-27 13:55:02 +02:00
|
|
|
|
2016-07-14 01:19:03 +02:00
|
|
|
return node
|
2016-05-29 21:41:58 +02:00
|
|
|
}
|
2016-06-16 18:50:43 +02:00
|
|
|
|
2017-01-29 22:14:40 +01:00
|
|
|
// Select selects a list of nodes to be returned
|
|
|
|
func (nodes *Nodes) Select(f func(*Node) bool) []*Node {
|
|
|
|
nodes.RLock()
|
|
|
|
defer nodes.RUnlock()
|
|
|
|
|
|
|
|
result := make([]*Node, 0, len(nodes.List))
|
|
|
|
for _, node := range nodes.List {
|
|
|
|
if f(node) {
|
|
|
|
result = append(result, node)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2017-12-05 23:17:49 +01:00
|
|
|
func (nodes *Nodes) GetNodeIDbyAddress(addr string) string {
|
|
|
|
return nodes.ifaceToNodeID[addr]
|
2017-05-20 14:46:29 +02:00
|
|
|
}
|
|
|
|
|
2017-09-27 13:55:02 +02:00
|
|
|
// NodeLinks returns a list of links to known neighbours
|
|
|
|
func (nodes *Nodes) NodeLinks(node *Node) (result []Link) {
|
|
|
|
// Store link data
|
|
|
|
neighbours := node.Neighbours
|
2024-07-20 00:31:16 +02:00
|
|
|
if neighbours == nil || neighbours.NodeID == "" || !node.Online {
|
2017-09-27 13:55:02 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for sourceMAC, batadv := range neighbours.Batadv {
|
|
|
|
for neighbourMAC, link := range batadv.Neighbours {
|
|
|
|
if neighbourID := nodes.ifaceToNodeID[neighbourMAC]; neighbourID != "" {
|
2021-04-03 05:11:33 +02:00
|
|
|
neighbour := nodes.List[neighbourID]
|
2021-03-26 10:18:29 +01:00
|
|
|
|
|
|
|
link := Link{
|
2017-12-05 23:17:49 +01:00
|
|
|
SourceID: neighbours.NodeID,
|
|
|
|
SourceAddress: sourceMAC,
|
|
|
|
TargetID: neighbourID,
|
|
|
|
TargetAddress: neighbourMAC,
|
2023-09-18 19:56:18 +02:00
|
|
|
TQ: float32(link.TQ) / 255.0,
|
2021-03-26 10:18:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
if neighbour.Nodeinfo != nil {
|
2021-04-03 05:11:33 +02:00
|
|
|
link.TargetHostname = neighbour.Nodeinfo.Hostname
|
2021-03-26 10:18:29 +01:00
|
|
|
}
|
|
|
|
if node.Nodeinfo != nil {
|
2021-04-03 05:11:33 +02:00
|
|
|
link.SourceHostname = node.Nodeinfo.Hostname
|
2021-03-26 10:18:29 +01:00
|
|
|
}
|
2024-07-20 00:31:16 +02:00
|
|
|
if lt, ok := nodes.ifaceToLinkType[sourceMAC]; ok && lt != OtherLinkType {
|
|
|
|
link.Type = lt
|
|
|
|
} else if lt, ok := nodes.ifaceToLinkType[neighbourMAC]; ok {
|
|
|
|
link.Type = lt
|
|
|
|
}
|
2021-03-26 10:18:29 +01:00
|
|
|
|
|
|
|
result = append(result, link)
|
2017-12-05 23:17:49 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, iface := range neighbours.Babel {
|
|
|
|
for neighbourIP, link := range iface.Neighbours {
|
|
|
|
if neighbourID := nodes.ifaceToNodeID[neighbourIP]; neighbourID != "" {
|
2024-07-20 00:31:16 +02:00
|
|
|
link := Link{
|
2017-12-05 23:17:49 +01:00
|
|
|
SourceID: neighbours.NodeID,
|
|
|
|
SourceAddress: iface.LinkLocalAddress,
|
|
|
|
TargetID: neighbourID,
|
|
|
|
TargetAddress: neighbourIP,
|
|
|
|
TQ: 1.0 - (float32(link.Cost) / 65535.0),
|
2024-07-20 00:31:16 +02:00
|
|
|
}
|
|
|
|
if lt, ok := nodes.ifaceToLinkType[iface.LinkLocalAddress]; ok && lt != OtherLinkType {
|
|
|
|
link.Type = lt
|
|
|
|
} else if lt, ok := nodes.ifaceToLinkType[neighbourIP]; ok {
|
|
|
|
link.Type = lt
|
|
|
|
}
|
|
|
|
result = append(result, link)
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-07-20 00:31:16 +02:00
|
|
|
for sourceMAC, neighmacs := range neighbours.LLDP {
|
|
|
|
for _, neighbourMAC := range neighmacs {
|
|
|
|
if neighbourID := nodes.ifaceToNodeID[neighbourMAC]; neighbourID != "" {
|
|
|
|
link := Link{
|
2022-07-09 20:20:48 +02:00
|
|
|
SourceID: neighbours.NodeID,
|
2024-07-20 00:31:16 +02:00
|
|
|
SourceAddress: sourceMAC,
|
2022-07-09 20:20:48 +02:00
|
|
|
TargetID: neighbourID,
|
2024-07-20 00:31:16 +02:00
|
|
|
TargetAddress: neighbourMAC,
|
2022-07-09 20:20:48 +02:00
|
|
|
// TODO maybe change LLDP for link quality / 100M or 1GE
|
|
|
|
TQ: 1.0,
|
2024-07-20 00:31:16 +02:00
|
|
|
}
|
|
|
|
if lt, ok := nodes.ifaceToLinkType[sourceMAC]; ok && lt != OtherLinkType {
|
|
|
|
link.Type = lt
|
|
|
|
} else if lt, ok := nodes.ifaceToLinkType[neighbourMAC]; ok {
|
|
|
|
link.Type = lt
|
|
|
|
}
|
|
|
|
result = append(result, link)
|
2022-07-09 20:20:48 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-09-27 13:55:02 +02:00
|
|
|
return result
|
|
|
|
}
|
|
|
|
|
2016-03-20 18:30:44 +01:00
|
|
|
// Periodically saves the cached DB to json file
|
|
|
|
func (nodes *Nodes) worker() {
|
2018-01-07 21:00:56 +01:00
|
|
|
c := time.Tick(nodes.config.SaveInterval.Duration)
|
2015-12-29 14:05:47 +01:00
|
|
|
|
|
|
|
for range c {
|
2016-10-08 10:50:41 +02:00
|
|
|
nodes.expire()
|
|
|
|
nodes.save()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Expires nodes and set nodes offline
|
|
|
|
func (nodes *Nodes) expire() {
|
2017-01-29 20:46:06 +01:00
|
|
|
now := jsontime.Now()
|
2016-10-08 10:50:41 +02:00
|
|
|
|
2017-01-29 21:04:10 +01:00
|
|
|
// Nodes last seen before expireAfter will be removed
|
2018-01-07 21:00:56 +01:00
|
|
|
prunePeriod := nodes.config.PruneAfter.Duration
|
2017-01-29 21:04:10 +01:00
|
|
|
if prunePeriod == 0 {
|
|
|
|
prunePeriod = time.Hour * 24 * 7 // our default
|
2016-10-08 10:50:41 +02:00
|
|
|
}
|
2017-01-29 21:04:10 +01:00
|
|
|
pruneAfter := now.Add(-prunePeriod)
|
2016-10-08 10:50:41 +02:00
|
|
|
|
2017-01-29 21:04:10 +01:00
|
|
|
// Nodes last seen within OfflineAfter are changed to 'offline'
|
2018-01-07 21:00:56 +01:00
|
|
|
offlineAfter := now.Add(-nodes.config.OfflineAfter.Duration)
|
2016-10-08 10:50:41 +02:00
|
|
|
|
|
|
|
// Locking foo
|
|
|
|
nodes.Lock()
|
|
|
|
defer nodes.Unlock()
|
|
|
|
|
|
|
|
for id, node := range nodes.List {
|
2017-01-29 21:04:10 +01:00
|
|
|
if node.Lastseen.Before(pruneAfter) {
|
2016-10-08 10:50:41 +02:00
|
|
|
// expire
|
|
|
|
delete(nodes.List, id)
|
2017-01-29 21:04:10 +01:00
|
|
|
} else if node.Lastseen.Before(offlineAfter) {
|
2016-10-08 10:50:41 +02:00
|
|
|
// set to offline
|
2017-04-10 18:54:12 +02:00
|
|
|
node.Online = false
|
2016-05-11 21:30:54 +02:00
|
|
|
}
|
2016-10-08 10:50:41 +02:00
|
|
|
}
|
|
|
|
}
|
2016-03-20 18:30:44 +01:00
|
|
|
|
2024-07-20 00:31:16 +02:00
|
|
|
func updateIface[K string | LinkProtocol | LinkType](class string, addr string, dataMap map[string]K, value K, warning bool) {
|
|
|
|
if oldValue := dataMap[addr]; oldValue != value {
|
|
|
|
var empty K
|
|
|
|
if oldValue != empty && warning {
|
|
|
|
log.Warnf("override %s from %s to %s on %s", class, oldValue, value, addr)
|
|
|
|
}
|
|
|
|
dataMap[addr] = value
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-27 13:55:02 +02:00
|
|
|
// adds the nodes interface addresses to the internal map
|
2024-07-20 00:31:16 +02:00
|
|
|
func (nodes *Nodes) readIfaces(nodeinfo *data.Nodeinfo, neighbours *data.Neighbours, warning bool) {
|
2017-09-27 13:55:02 +02:00
|
|
|
nodeID := nodeinfo.NodeID
|
|
|
|
network := nodeinfo.Network
|
|
|
|
|
|
|
|
if nodeID == "" {
|
2019-01-17 13:26:16 +01:00
|
|
|
log.Warn("nodeID missing in nodeinfo")
|
2017-09-27 13:55:02 +02:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
addresses := []string{network.Mac}
|
|
|
|
|
2017-12-05 23:17:49 +01:00
|
|
|
for _, iface := range network.Mesh {
|
2024-07-20 00:31:16 +02:00
|
|
|
for _, addr := range iface.Interfaces.Wireless {
|
|
|
|
updateIface("interface-type", addr, nodes.ifaceToLinkType, WirelessLinkType, warning)
|
|
|
|
}
|
|
|
|
for _, addr := range iface.Interfaces.Tunnel {
|
|
|
|
updateIface("interface-type", addr, nodes.ifaceToLinkType, TunnelLinkType, warning)
|
|
|
|
}
|
|
|
|
for _, addr := range iface.Interfaces.Other {
|
|
|
|
updateIface("interface-type", addr, nodes.ifaceToLinkType, OtherLinkType, warning)
|
|
|
|
}
|
2017-12-05 23:17:49 +01:00
|
|
|
addresses = append(addresses, iface.Addresses()...)
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
|
|
|
|
2017-12-05 23:17:49 +01:00
|
|
|
for _, addr := range addresses {
|
|
|
|
if addr == "" {
|
2018-01-21 20:39:36 +01:00
|
|
|
continue
|
|
|
|
}
|
2024-07-20 00:31:16 +02:00
|
|
|
updateIface("nodeID", addr, nodes.ifaceToNodeID, nodeID, warning)
|
|
|
|
}
|
|
|
|
|
|
|
|
if neighbours == nil || neighbours.NodeID == "" {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
for sourceMAC, batadv := range neighbours.Batadv {
|
|
|
|
updateIface("mesh-protocol", sourceMAC, nodes.ifaceToLinkProtocol, BatadvLinkProtocol, warning)
|
|
|
|
for neighbourMAC := range batadv.Neighbours {
|
|
|
|
updateIface("mesh-protocol", neighbourMAC, nodes.ifaceToLinkProtocol, BatadvLinkProtocol, warning)
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
|
|
|
}
|
2024-07-20 00:31:16 +02:00
|
|
|
for _, iface := range neighbours.Babel {
|
|
|
|
updateIface("mesh-protocol", iface.LinkLocalAddress, nodes.ifaceToLinkProtocol, BabelLinkProtocol, warning)
|
|
|
|
for neighbourIP := range iface.Neighbours {
|
|
|
|
updateIface("mesh-protocol", neighbourIP, nodes.ifaceToLinkProtocol, BabelLinkProtocol, warning)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for portmac, neighmacs := range neighbours.LLDP {
|
|
|
|
updateIface("mesh-protocol", portmac, nodes.ifaceToLinkProtocol, LLDPLinkProtocol, warning)
|
|
|
|
for _, neighmac := range neighmacs {
|
|
|
|
updateIface("mesh-protocol", neighmac, nodes.ifaceToLinkProtocol, LLDPLinkProtocol, warning)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
|
|
|
|
2016-11-20 18:30:32 +01:00
|
|
|
func (nodes *Nodes) load() {
|
2018-01-07 21:00:56 +01:00
|
|
|
path := nodes.config.StatePath
|
2016-11-20 18:30:32 +01:00
|
|
|
|
2017-01-20 14:38:13 +01:00
|
|
|
if f, err := os.Open(path); err == nil { // transform data to legacy meshviewer
|
2017-01-20 22:27:44 +01:00
|
|
|
if err = json.NewDecoder(f).Decode(nodes); err == nil {
|
2019-01-17 13:26:16 +01:00
|
|
|
log.Infof("loaded %d nodes", len(nodes.List))
|
2017-09-27 13:55:02 +02:00
|
|
|
|
2017-05-20 14:46:29 +02:00
|
|
|
nodes.Lock()
|
2017-09-27 13:55:02 +02:00
|
|
|
for _, node := range nodes.List {
|
|
|
|
if node.Nodeinfo != nil {
|
2024-07-20 00:31:16 +02:00
|
|
|
nodes.readIfaces(node.Nodeinfo, node.Neighbours, false)
|
2017-09-27 13:55:02 +02:00
|
|
|
}
|
|
|
|
}
|
2017-05-20 14:46:29 +02:00
|
|
|
nodes.Unlock()
|
2017-09-27 13:55:02 +02:00
|
|
|
|
2016-11-20 18:30:32 +01:00
|
|
|
} else {
|
2019-01-17 13:26:16 +01:00
|
|
|
log.Errorf("failed to unmarshal nodes: %s", err)
|
2016-11-20 18:30:32 +01:00
|
|
|
}
|
|
|
|
} else {
|
2019-01-17 13:26:16 +01:00
|
|
|
log.Errorf("failed to load cached nodes: %s", err)
|
2016-11-20 18:30:32 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-08 10:50:41 +02:00
|
|
|
func (nodes *Nodes) save() {
|
|
|
|
// Locking foo
|
|
|
|
nodes.RLock()
|
|
|
|
defer nodes.RUnlock()
|
|
|
|
|
|
|
|
// serialize nodes
|
2018-01-07 21:00:56 +01:00
|
|
|
SaveJSON(nodes, nodes.config.StatePath)
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
|
|
|
|
2017-04-10 18:54:12 +02:00
|
|
|
// SaveJSON to path
|
|
|
|
func SaveJSON(input interface{}, outputFile string) {
|
2016-11-20 18:30:32 +01:00
|
|
|
tmpFile := outputFile + ".tmp"
|
|
|
|
|
|
|
|
f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
2016-02-25 21:06:37 +01:00
|
|
|
if err != nil {
|
2016-02-19 11:30:42 +01:00
|
|
|
log.Panic(err)
|
2016-02-19 11:13:30 +01:00
|
|
|
}
|
2015-12-29 14:05:47 +01:00
|
|
|
|
2016-11-20 18:30:32 +01:00
|
|
|
err = json.NewEncoder(f).Encode(input)
|
|
|
|
if err != nil {
|
2016-02-19 11:30:42 +01:00
|
|
|
log.Panic(err)
|
|
|
|
}
|
2016-03-20 18:30:44 +01:00
|
|
|
|
2016-11-20 18:30:32 +01:00
|
|
|
f.Close()
|
2016-03-20 18:30:44 +01:00
|
|
|
if err := os.Rename(tmpFile, outputFile); err != nil {
|
2016-02-19 11:30:42 +01:00
|
|
|
log.Panic(err)
|
|
|
|
}
|
2015-12-29 14:05:47 +01:00
|
|
|
}
|
[TASK] add output raw-jsonl
PR at github: #199
This output takes the respondd response as sent by the node and includes
it in a Line-Delimited JSON (JSONL) document. In this format each line
can be interpreted as separate JSON element, which is useful for json
streaming. The first line is json object holding the timestamp and
version of the file. Then there follows one line for each node, each
containing a json object.
An example output looks like this:
{"version":1,"updated_at":"2021-03-27T21:58:48+0100","format":"raw-nodes-jsonl"}
{"firstseen": ..., "lastseen": ..., "online":true, "statistics": {...}, "nodeinfo": {...}, "neighbours":null, "custom_fields":null}
{"firstseen": ..., "lastseen": ..., "online":true, "statistics": {...}, "nodeinfo": {...}, "neighbours":null, "custom_fields":null}
{"firstseen": ..., "lastseen": ..., "online":true, "statistics": {...}, "nodeinfo": {...}, "neighbours":null, "custom_fields":null}
{"firstseen": ..., "lastseen": ..., "online":true, "statistics": {...}, "nodeinfo": {...}, "neighbours":null, "custom_fields":null}
...
Signed-off-by: Leonardo Mörlein <git@irrelefant.net>
2021-03-29 16:12:26 +02:00
|
|
|
|
|
|
|
// Save a slice of json objects as line-encoded JSON (JSONL) to a path.
|
|
|
|
func SaveJSONL(input []interface{}, outputFile string) {
|
|
|
|
tmpFile := outputFile + ".tmp"
|
|
|
|
|
|
|
|
f, err := os.OpenFile(tmpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, element := range input {
|
|
|
|
err = json.NewEncoder(f).Encode(element)
|
|
|
|
if err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
f.Close()
|
|
|
|
if err := os.Rename(tmpFile, outputFile); err != nil {
|
|
|
|
log.Panic(err)
|
|
|
|
}
|
|
|
|
}
|