Add location grouping with cola layout
This commit is contained in:
BIN
.config.yaml.swp
Normal file
BIN
.config.yaml.swp
Normal file
Binary file not shown.
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
iface := flag.String("i", "", "interface to use")
|
iface := flag.String("i", "", "interface to use")
|
||||||
|
configFile := flag.String("config", "", "path to YAML config file")
|
||||||
noARP := flag.Bool("no-arp", false, "disable ARP discovery")
|
noARP := flag.Bool("no-arp", false, "disable ARP discovery")
|
||||||
noLLDP := flag.Bool("no-lldp", false, "disable LLDP discovery")
|
noLLDP := flag.Bool("no-lldp", false, "disable LLDP discovery")
|
||||||
noSNMP := flag.Bool("no-snmp", false, "disable SNMP discovery")
|
noSNMP := flag.Bool("no-snmp", false, "disable SNMP discovery")
|
||||||
@@ -35,6 +36,7 @@ func main() {
|
|||||||
|
|
||||||
t := tendrils.New()
|
t := tendrils.New()
|
||||||
t.Interface = *iface
|
t.Interface = *iface
|
||||||
|
t.ConfigFile = *configFile
|
||||||
t.DisableARP = *noARP
|
t.DisableARP = *noARP
|
||||||
t.DisableLLDP = *noLLDP
|
t.DisableLLDP = *noLLDP
|
||||||
t.DisableSNMP = *noSNMP
|
t.DisableSNMP = *noSNMP
|
||||||
|
|||||||
21
config.example.yaml
Normal file
21
config.example.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
locations:
|
||||||
|
stage:
|
||||||
|
children:
|
||||||
|
upstage:
|
||||||
|
nodes:
|
||||||
|
- lighting-1
|
||||||
|
downstage:
|
||||||
|
nodes:
|
||||||
|
- lighting-2
|
||||||
|
house:
|
||||||
|
children:
|
||||||
|
house_left:
|
||||||
|
nodes:
|
||||||
|
- audio
|
||||||
|
house_right:
|
||||||
|
nodes:
|
||||||
|
- video
|
||||||
|
booth:
|
||||||
|
nodes:
|
||||||
|
- qlab
|
||||||
|
- satellite-1
|
||||||
39
config.go
Normal file
39
config.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package tendrils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Locations map[string]*Location `yaml:"locations" json:"locations"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Nodes []string `yaml:"nodes,omitempty" json:"nodes,omitempty"`
|
||||||
|
Children map[string]*Location `yaml:"children,omitempty" json:"children,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoadConfig(path string) (*Config, error) {
|
||||||
|
if path == "" {
|
||||||
|
return &Config{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := yaml.Unmarshal(data, &cfg); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ToJSON() ([]byte, error) {
|
||||||
|
return json.Marshal(c)
|
||||||
|
}
|
||||||
11
config.yaml
Normal file
11
config.yaml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
locations:
|
||||||
|
stage:
|
||||||
|
children:
|
||||||
|
upstage:
|
||||||
|
nodes:
|
||||||
|
- lighting-2
|
||||||
|
downstage:
|
||||||
|
children:
|
||||||
|
rack-lighting-1:
|
||||||
|
nodes:
|
||||||
|
- lighting-1
|
||||||
2
go.mod
2
go.mod
@@ -8,10 +8,12 @@ require (
|
|||||||
github.com/gosnmp/gosnmp v1.42.1
|
github.com/gosnmp/gosnmp v1.42.1
|
||||||
github.com/miekg/dns v1.1.72
|
github.com/miekg/dns v1.1.72
|
||||||
go.jetify.com/typeid v1.3.0
|
go.jetify.com/typeid v1.3.0
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||||
|
github.com/kr/text v0.2.0 // indirect
|
||||||
golang.org/x/mod v0.31.0 // indirect
|
golang.org/x/mod v0.31.0 // indirect
|
||||||
golang.org/x/net v0.48.0 // indirect
|
golang.org/x/net v0.48.0 // indirect
|
||||||
golang.org/x/sync v0.19.0 // indirect
|
golang.org/x/sync v0.19.0 // indirect
|
||||||
|
|||||||
10
go.sum
10
go.sum
@@ -1,3 +1,4 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=
|
||||||
@@ -10,10 +11,16 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
|
|||||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/gosnmp/gosnmp v1.42.1 h1:MEJxhpC5v1coL3tFRix08PYmky9nyb1TLRRgJAmXm8A=
|
github.com/gosnmp/gosnmp v1.42.1 h1:MEJxhpC5v1coL3tFRix08PYmky9nyb1TLRRgJAmXm8A=
|
||||||
github.com/gosnmp/gosnmp v1.42.1/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo=
|
github.com/gosnmp/gosnmp v1.42.1/go.mod h1:CxVS6bXqmWZlafUj9pZUnQX5e4fAltqPcijxWpCitDo=
|
||||||
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||||
|
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
go.jetify.com/typeid v1.3.0 h1:fuWV7oxO4mSsgpxwhaVpFXgt0IfjogR29p+XAjDCVKY=
|
go.jetify.com/typeid v1.3.0 h1:fuWV7oxO4mSsgpxwhaVpFXgt0IfjogR29p+XAjDCVKY=
|
||||||
@@ -40,6 +47,9 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK
|
|||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
10
http.go
10
http.go
@@ -24,6 +24,7 @@ func (t *Tendrils) startHTTPServer() {
|
|||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/api/status", t.handleAPIStatus)
|
mux.HandleFunc("/api/status", t.handleAPIStatus)
|
||||||
|
mux.HandleFunc("/api/config", t.handleAPIConfig)
|
||||||
mux.Handle("/", http.FileServer(http.Dir("static")))
|
mux.Handle("/", http.FileServer(http.Dir("static")))
|
||||||
|
|
||||||
log.Printf("[http] listening on %s", t.HTTPPort)
|
log.Printf("[http] listening on %s", t.HTTPPort)
|
||||||
@@ -40,6 +41,15 @@ func (t *Tendrils) handleAPIStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(status)
|
json.NewEncoder(w).Encode(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *Tendrils) handleAPIConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
if t.config == nil {
|
||||||
|
json.NewEncoder(w).Encode(&Config{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(t.config)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tendrils) GetStatus() *StatusResponse {
|
func (t *Tendrils) GetStatus() *StatusResponse {
|
||||||
return &StatusResponse{
|
return &StatusResponse{
|
||||||
Nodes: t.getNodes(),
|
Nodes: t.getNodes(),
|
||||||
|
|||||||
4
static/cola.min.js
vendored
Normal file
4
static/cola.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
8
static/cytoscape-cola.min.js
vendored
Normal file
8
static/cytoscape-cola.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -47,7 +47,10 @@
|
|||||||
<div id="cy"></div>
|
<div id="cy"></div>
|
||||||
|
|
||||||
<script src="cytoscape.min.js"></script>
|
<script src="cytoscape.min.js"></script>
|
||||||
|
<script src="cola.min.js"></script>
|
||||||
|
<script src="cytoscape-cola.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
cytoscape.use(cytoscapeCola);
|
||||||
let cy;
|
let cy;
|
||||||
|
|
||||||
function getLabel(node) {
|
function getLabel(node) {
|
||||||
@@ -55,31 +58,73 @@
|
|||||||
return '??';
|
return '??';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodeIdentifiers(node) {
|
||||||
|
const ids = [];
|
||||||
|
if (node.names) {
|
||||||
|
node.names.forEach(n => ids.push(n.toLowerCase()));
|
||||||
|
}
|
||||||
|
if (node.interfaces) {
|
||||||
|
node.interfaces.forEach(iface => {
|
||||||
|
if (iface.mac) ids.push(iface.mac.toLowerCase());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
function isSwitch(node) {
|
function isSwitch(node) {
|
||||||
return !!(node.poe_budget);
|
return !!(node.poe_budget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLocationIndex(locations, parentId, nodeToLocation, locationMeta) {
|
||||||
|
if (!locations) return;
|
||||||
|
for (const [name, loc] of Object.entries(locations)) {
|
||||||
|
const locId = 'loc_' + name.replace(/[^a-zA-Z0-9]/g, '_');
|
||||||
|
locationMeta.set(locId, { name, parentId });
|
||||||
|
|
||||||
|
if (loc.nodes) {
|
||||||
|
loc.nodes.forEach(nodeRef => {
|
||||||
|
nodeToLocation.set(nodeRef.toLowerCase(), locId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loc.children) {
|
||||||
|
buildLocationIndex(loc.children, locId, nodeToLocation, locationMeta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLocationChain(locId, locationMeta) {
|
||||||
|
const chain = [];
|
||||||
|
let current = locId;
|
||||||
|
while (current) {
|
||||||
|
chain.push(current);
|
||||||
|
const meta = locationMeta.get(current);
|
||||||
|
current = meta ? meta.parentId : null;
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
function doLayout() {
|
function doLayout() {
|
||||||
cy.layout({
|
cy.layout({
|
||||||
name: 'cose',
|
name: 'cola',
|
||||||
animate: false,
|
animate: false,
|
||||||
padding: 50,
|
padding: 50,
|
||||||
nodeDimensionsIncludeLabels: true,
|
nodeDimensionsIncludeLabels: true,
|
||||||
avoidOverlap: true,
|
avoidOverlap: true,
|
||||||
avoidOverlapPadding: 20,
|
nodeSpacing: 40,
|
||||||
nodeRepulsion: 100000,
|
edgeLength: 200,
|
||||||
idealEdgeLength: 200,
|
|
||||||
edgeElasticity: 100,
|
|
||||||
gravity: 0.1,
|
|
||||||
numIter: 2000,
|
|
||||||
fit: true,
|
fit: true,
|
||||||
randomize: true
|
randomize: true
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const resp = await fetch('/api/status');
|
const [statusResp, configResp] = await Promise.all([
|
||||||
const data = await resp.json();
|
fetch('/api/status'),
|
||||||
|
fetch('/api/config')
|
||||||
|
]);
|
||||||
|
const data = await statusResp.json();
|
||||||
|
const config = await configResp.json();
|
||||||
|
|
||||||
const nodes = data.nodes || [];
|
const nodes = data.nodes || [];
|
||||||
const links = data.links || [];
|
const links = data.links || [];
|
||||||
@@ -91,20 +136,61 @@
|
|||||||
const idMap = new Map();
|
const idMap = new Map();
|
||||||
const switchIds = new Set();
|
const switchIds = new Set();
|
||||||
|
|
||||||
|
const nodeToLocation = new Map();
|
||||||
|
const locationMeta = new Map();
|
||||||
|
buildLocationIndex(config.locations, null, nodeToLocation, locationMeta);
|
||||||
|
|
||||||
nodes.forEach((n, i) => {
|
nodes.forEach((n, i) => {
|
||||||
const id = 'n' + i;
|
const id = 'n' + i;
|
||||||
idMap.set(n.typeid, id);
|
idMap.set(n.typeid, id);
|
||||||
if (isSwitch(n)) switchIds.add(id);
|
if (isSwitch(n)) switchIds.add(id);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const usedLocations = new Set();
|
||||||
|
const nodeParents = new Map();
|
||||||
|
|
||||||
|
nodes.forEach((n, i) => {
|
||||||
|
const id = 'n' + i;
|
||||||
|
const identifiers = getNodeIdentifiers(n);
|
||||||
|
for (const ident of identifiers) {
|
||||||
|
if (nodeToLocation.has(ident)) {
|
||||||
|
const locId = nodeToLocation.get(ident);
|
||||||
|
nodeParents.set(id, locId);
|
||||||
|
getLocationChain(locId, locationMeta).forEach(l => usedLocations.add(l));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedLocations = Array.from(usedLocations).sort((a, b) => {
|
||||||
|
const chainA = getLocationChain(a, locationMeta).length;
|
||||||
|
const chainB = getLocationChain(b, locationMeta).length;
|
||||||
|
return chainA - chainB;
|
||||||
|
});
|
||||||
|
|
||||||
|
sortedLocations.forEach(locId => {
|
||||||
|
const meta = locationMeta.get(locId);
|
||||||
|
elements.push({
|
||||||
|
data: {
|
||||||
|
id: locId,
|
||||||
|
label: meta.name,
|
||||||
|
parent: meta.parentId && usedLocations.has(meta.parentId) ? meta.parentId : null,
|
||||||
|
isLocation: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
nodes.forEach((n, i) => {
|
nodes.forEach((n, i) => {
|
||||||
const id = 'n' + i;
|
const id = 'n' + i;
|
||||||
const sw = switchIds.has(id);
|
const sw = switchIds.has(id);
|
||||||
|
const parent = nodeParents.get(id) || null;
|
||||||
|
|
||||||
elements.push({
|
elements.push({
|
||||||
data: {
|
data: {
|
||||||
id: id,
|
id: id,
|
||||||
label: getLabel(n),
|
label: getLabel(n),
|
||||||
isSwitch: sw
|
isSwitch: sw,
|
||||||
|
parent: parent
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -158,6 +244,35 @@
|
|||||||
'height': 50
|
'height': 50
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
selector: 'node[?isLocation]',
|
||||||
|
style: {
|
||||||
|
'background-color': '#333',
|
||||||
|
'background-opacity': 0.8,
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#666',
|
||||||
|
'text-valign': 'top',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'text-margin-y': 10,
|
||||||
|
'font-size': 16,
|
||||||
|
'font-weight': 'bold',
|
||||||
|
'color': '#fff',
|
||||||
|
'padding': 30,
|
||||||
|
'shape': 'round-rectangle'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ':parent',
|
||||||
|
style: {
|
||||||
|
'background-opacity': 0.5,
|
||||||
|
'border-width': 2,
|
||||||
|
'border-color': '#666',
|
||||||
|
'text-valign': 'top',
|
||||||
|
'text-halign': 'center',
|
||||||
|
'text-margin-y': 10,
|
||||||
|
'padding': 30
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
selector: 'edge',
|
selector: 'edge',
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ type Tendrils struct {
|
|||||||
nodes *Nodes
|
nodes *Nodes
|
||||||
artnet *ArtNetNodes
|
artnet *ArtNetNodes
|
||||||
danteFlows *DanteFlows
|
danteFlows *DanteFlows
|
||||||
|
config *Config
|
||||||
|
|
||||||
Interface string
|
Interface string
|
||||||
|
ConfigFile string
|
||||||
DisableARP bool
|
DisableARP bool
|
||||||
DisableLLDP bool
|
DisableLLDP bool
|
||||||
DisableSNMP bool
|
DisableSNMP bool
|
||||||
@@ -83,6 +85,13 @@ func (t *Tendrils) Run() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
cfg, err := LoadConfig(t.ConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] failed to load config: %v", err)
|
||||||
|
} else {
|
||||||
|
t.config = cfg
|
||||||
|
}
|
||||||
|
|
||||||
t.populateLocalAddresses()
|
t.populateLocalAddresses()
|
||||||
t.startHTTPServer()
|
t.startHTTPServer()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user