sub: init
This commit is contained in:
commit
81e0b20d2d
|
@ -0,0 +1,29 @@
|
|||
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||
|
||||
subv4
|
|
@ -0,0 +1,16 @@
|
|||
FROM golang AS builder
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.* ./
|
||||
RUN go mod download -x && go mod verify
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -tags -netgo -o sub
|
||||
|
||||
FROM scratch
|
||||
|
||||
COPY --from=builder /src/sub /sub
|
||||
|
||||
CMD ["/sub"]
|
|
@ -0,0 +1,382 @@
|
|||
/* Classless.css v1.0
|
||||
|
||||
Table of Contents:
|
||||
1. Theme Settings
|
||||
2. Reset
|
||||
3. Base Style
|
||||
4. Extras (remove unwanted)
|
||||
5. Classes (remove unwanted)
|
||||
*/
|
||||
|
||||
/* 1. Theme Settings ––––––––––––––––––––-–––––––––––––– */
|
||||
|
||||
|
||||
:root, html[data-theme='light'] {
|
||||
--rem: 12pt;
|
||||
--width: 50rem;
|
||||
--navpos: absolute; /* fixed | absolute */
|
||||
--font-p: 1em/1.7 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif;
|
||||
--font-h: .9em/1.5 'Open Sans', 'DejaVu Sans', FreeSans, Helvetica, sans-serif;
|
||||
--font-c: .9em/1.4 'DejaVu Sans Mono', monospace;
|
||||
--border: 1px solid var(--cmed);
|
||||
--ornament: "‹‹‹ ›››";
|
||||
/* foreground | background color */
|
||||
--cfg: #433; --cbg: #fff;
|
||||
--cdark: #888; --clight: #f5f6f7;
|
||||
--cmed: #d1d1d1;
|
||||
--clink: #07c;
|
||||
--cemph: #088; --cemphbg: #0881;
|
||||
}
|
||||
|
||||
|
||||
/* 2. Reset –––––––––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* reset block elements */
|
||||
* { box-sizing: border-box; border-spacing: 0; margin: 0; padding: 0;}
|
||||
header, footer, figure, table, video, details, blockquote,
|
||||
ul, ol, dl, fieldset, pre, pre > code, caption {
|
||||
display: block;
|
||||
margin: 0.5rem 0rem 1rem;
|
||||
width: 100%;
|
||||
overflow: auto hidden;
|
||||
text-align: left;
|
||||
}
|
||||
video, summary, input, select { outline:none; }
|
||||
|
||||
/* reset clickable things (FF Bug: select:hover prevents usage) */
|
||||
a, button, select, summary { color: var(--clink); cursor: pointer; }
|
||||
|
||||
|
||||
/* 3. Base Style ––––––––––––––––––––––––––––––––––––––– */
|
||||
html { font-size: var(--rem); background: var(--cbg); }
|
||||
body {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
max-width: var(--width);
|
||||
font: var(--font-p);
|
||||
color: var(--cfg);
|
||||
padding: 3.0rem 0.6rem 0;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body > footer { margin: 10rem 0rem 0rem; font-size: 90%; }
|
||||
p { margin: .6em 0; }
|
||||
|
||||
/* links */
|
||||
a[href]{ text-decoration: underline solid var(--cmed); text-underline-position: under; }
|
||||
a[href^="#"] {text-decoration: none; }
|
||||
a:hover, button:not([disabled]):hover, summary:hover {
|
||||
filter: brightness(92%); color: var(--cemph); border-color: var(--cemph);
|
||||
}
|
||||
|
||||
/* lists */
|
||||
ul, ol, dl { margin: 1rem 0; padding: 0 0 0 2em; }
|
||||
li:not(:last-child), dd:not(:last-child) { margin-bottom: 0.5rem; }
|
||||
dt { font-weight: bold; }
|
||||
|
||||
/* headings */
|
||||
h1, h2, h3, h4, h5 { margin: 1.5em 0 .5rem; font: var(--font-h); line-height: 1.2em; clear: both; }
|
||||
h1+h2, h2+h3, h3+h4, h4+h5 { margin-top: .5em; padding-top: 0; } /* non-clashing headings */
|
||||
h1 { font-size: 2.2em; font-weight: 300; }
|
||||
h2 { font-size: 2.0em; font-weight: 300; font-variant: small-caps; }
|
||||
h3 { font-size: 1.5em; font-weight: 400; }
|
||||
h4 { font-size: 1.1em; font-weight: 700; }
|
||||
h5 { font-size: 1.2em; font-weight: 400; color: var(--cfg); }
|
||||
h6 { font-size: 1.0em; font-weight: 700; font-style: italic; display: inline; }
|
||||
h6 + p { display: inline; }
|
||||
|
||||
/* tables */
|
||||
td, th {
|
||||
padding: 0.5em 0.8em;
|
||||
text-align: right;
|
||||
border-bottom: 0.1rem solid var(--cmed);
|
||||
white-space: nowrap;
|
||||
font-size: 95%;
|
||||
}
|
||||
thead th[colspan] { padding: .2em 0.8em; text-align: center; }
|
||||
thead tr:not(:only-child) td { padding: .2em 0.8em; }
|
||||
thead+tbody tr:first-child td { border-top: 0.1rem solid var(--cdark); }
|
||||
td:first-child, th:first-child { text-align: left; }
|
||||
tr:hover{ background-color: var(--clight); }
|
||||
table img { display: block; }
|
||||
|
||||
/* figures */
|
||||
img, svg { max-width: 100%; vertical-align: text-top; object-fit: cover; }
|
||||
p>img:not(:only-child) { float: right; margin: 0 0 .5em .5em; }
|
||||
figure > img { display: inline-block; width: auto; }
|
||||
figure > img:only-of-type, figure > svg:only-of-type { max-width: 100%; display: block; margin: 0 auto 0.4em; }
|
||||
figcaption, caption { font: var(--font-h); color: var(--cdark); width: 100%; }
|
||||
figcaption > *:first-child, caption > *:first-child { display: inline-block; margin: 0; }
|
||||
figure > *:not(:last-child) { margin-bottom: 0.4rem; }
|
||||
|
||||
/* code */
|
||||
pre > code {
|
||||
margin: 0;
|
||||
position: relative;
|
||||
padding: 0.8em;
|
||||
border-left: .4rem solid var(--cemph);
|
||||
}
|
||||
code, kbd, samp {
|
||||
padding: 0.2em;
|
||||
font: var(--font-c);
|
||||
background: var(--clight);
|
||||
border-radius: 4px;
|
||||
}
|
||||
kbd { border: 1px solid var(--cmed); }
|
||||
|
||||
/* misc */
|
||||
blockquote { border-left: 0.4rem solid var(--cmed); padding: 0 0 0 1rem; }
|
||||
time{ color: var(--cdark); }
|
||||
hr { border: 0; border-top: 0.1rem solid var(--cmed); }
|
||||
nav { width: 100%; background-color: var(--clight); }
|
||||
::selection, mark { background: var(--clink); color: var(--cbg); }
|
||||
|
||||
|
||||
/* 4. Extra Style –––––––––––––––––––––––––––––––––––––– */
|
||||
|
||||
/* Auto Numbering: figure/tables/headings/cite */
|
||||
article { counter-reset: h2 0 h3 0 tab 0 fig 0 lst 0 ref 0 eq 0; }
|
||||
article figure figcaption:before {
|
||||
color: var(--cemph);
|
||||
counter-increment: fig;
|
||||
content: "Figure " counter(fig) ": ";
|
||||
}
|
||||
|
||||
/* subfigures */
|
||||
figure { counter-reset: subfig 0 }
|
||||
article figure figure { counter-reset: none; }
|
||||
article figure > figure { display: inline-grid; width: auto; }
|
||||
figure > figure:not(:last-of-type) { padding-right: 1rem; }
|
||||
article figure figure figcaption:before {
|
||||
counter-increment: subfig 1;
|
||||
content: counter(subfig, lower-alpha) ": ";
|
||||
}
|
||||
|
||||
/* listings */
|
||||
article figure pre + figcaption:before {
|
||||
counter-increment: lst 1;
|
||||
content: "Listing " counter(lst) ": ";
|
||||
}
|
||||
|
||||
/* tables */
|
||||
figure > table:only-of-type { display: table; margin: 0.5em auto !important; width: fit-content; }
|
||||
article figure > table caption { display: table-caption; caption-side: bottom; }
|
||||
article figure > table + figcaption:before,
|
||||
article table caption:before {
|
||||
color: var(--cemph);
|
||||
counter-increment: tab 1;
|
||||
content: "Table " counter(tab) ": ";
|
||||
}
|
||||
|
||||
/* headings */
|
||||
article h2, h3 { position: relative; }
|
||||
article h2:before,
|
||||
article h3:before {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
font-size: 0.6em;
|
||||
text-align: right;
|
||||
vertical-align: baseline;
|
||||
left: -1rem;
|
||||
width: 2.5em;
|
||||
margin-left: -2.5em;
|
||||
}
|
||||
article h1 { counter-set: h2; }
|
||||
article h2:before { counter-increment: h2; content: counter(h2) ". "; counter-set: h3; }
|
||||
article h3:before { counter-increment: h3; content: counter(h2) "." counter(h3) ". ";}
|
||||
@media (max-width: 60rem) { h2:before, h3:before { display: none; } }
|
||||
|
||||
/* tooltip + citation */
|
||||
article p>cite:before {
|
||||
padding: 0 .5em 0 0;
|
||||
counter-increment: ref; content: " [" counter(ref) "] ";
|
||||
vertical-align: super; font-size: .6em;
|
||||
}
|
||||
article p>cite > *:only-child { display: none; }
|
||||
article p>cite:hover > *:only-child,
|
||||
[data-tooltip]:hover:before {
|
||||
display: inline-block; z-index: 40;
|
||||
white-space: pre-wrap;
|
||||
position: absolute; left: 1rem; right: 1rem;
|
||||
padding: 1em 2em;
|
||||
text-align: center;
|
||||
transform:translateY( calc(-100%) );
|
||||
content: attr(data-tooltip);
|
||||
color: var(--cbg);
|
||||
background-color: var(--cemph);
|
||||
box-shadow: 0 2px 10px 0 black;
|
||||
}
|
||||
[data-tooltip], article p>cite:before {
|
||||
color: var(--clink);
|
||||
border: .8rem solid transparent; margin: -.8rem;
|
||||
}
|
||||
abbr[title], [data-tooltip] { cursor: help; }
|
||||
|
||||
/* navbar */
|
||||
nav+* { margin-top: 3rem; }
|
||||
body>nav, header nav {
|
||||
position: var(--navpos);
|
||||
top: 0; left: 0; right: 0;
|
||||
z-index: 41;
|
||||
box-shadow: 0vw -50vw 0 50vw var(--clight), 0 calc(-50vw + 2px) 4px 50vw var(--cdark);
|
||||
}
|
||||
nav ul { list-style-type: none; }
|
||||
nav ul:first-child { margin: 0; padding: 0; overflow: visible; }
|
||||
nav ul:first-child > li {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0.8rem .6rem;
|
||||
}
|
||||
nav ul > li > ul {
|
||||
display: none;
|
||||
width: auto;
|
||||
position: absolute;
|
||||
margin: 0.5rem 0;
|
||||
padding: 1rem 2rem;
|
||||
background-color: var(--clight);
|
||||
border: var(--border);
|
||||
border-radius: 4px;
|
||||
z-index: 42;
|
||||
}
|
||||
nav ul > li > ul > li { white-space: nowrap; }
|
||||
nav ul > li:hover > ul { display: block; }
|
||||
@media (max-width: 40rem) {
|
||||
nav ul:first-child > li:first-child:after { content: " \25BE"; }
|
||||
nav ul:first-child > li:not(:first-child):not(.sticky) { display: none; }
|
||||
nav ul:first-child:hover > li:not(:first-child):not(.sticky) { display: block; float: none !important; }
|
||||
}
|
||||
|
||||
/* details/cards */
|
||||
summary>* { display: inline; }
|
||||
.card, details {
|
||||
display: block;
|
||||
margin: 0.5rem 0rem 1rem;
|
||||
padding: 0 .6rem;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card, details[open] { outline: 1px solid var(--cmed); }
|
||||
.card>img:first-child { margin: -3px -.6rem; max-width: calc(100% + 1.2rem); }
|
||||
summary:hover, details[open] summary, .card>p:first-child {
|
||||
box-shadow: inset 0 0 0 2em var(--clight), 0 -.8rem 0 .8rem var(--clight);
|
||||
}
|
||||
.hint { --cmed: var(--cemph); --clight: var(--cemphbg); background-color: var(--clight); }
|
||||
.warn { --cmed: #c11; --clight: #e221; background-color: var(--clight); }
|
||||
|
||||
/* big first letter */
|
||||
article > section:first-of-type > h2:first-of-type + p:first-letter,
|
||||
article > h2:first-of-type + p:first-letter, .lettrine {
|
||||
float: left;
|
||||
font-size: 3.5em;
|
||||
padding: 0.1em 0.1em 0 0;
|
||||
line-height: 0.68em;
|
||||
color: var(--cemph);
|
||||
}
|
||||
|
||||
/* ornaments */
|
||||
section:after {
|
||||
display: block;
|
||||
margin: 1em 0;
|
||||
color: var(--cmed);
|
||||
text-align: center;
|
||||
font-size: 1.5em;
|
||||
content: var(--ornament);
|
||||
}
|
||||
|
||||
/* side menu (aside is not intended for use in a paragraph!) */
|
||||
main aside {
|
||||
position: absolute;
|
||||
width: 8rem; right: -8.6rem;
|
||||
font-size: 0.8em; line-height: 1.4em;
|
||||
}
|
||||
@media (max-width: 70rem) { main aside { display: none; } }
|
||||
|
||||
/* forms and inputs */
|
||||
textarea, input:not([type=range]), button, select {
|
||||
font: var(--font-h);
|
||||
border-radius: 4px;
|
||||
border: 1.5px solid var(--cmed);
|
||||
padding: 0.4em 0.8em;
|
||||
color: var(--cfg);
|
||||
background-color: var(--clight);
|
||||
}
|
||||
fieldset select, input:not([type=checkbox]):not([type=radio]) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
button, select {
|
||||
font-weight: bold;
|
||||
margin: .5em;
|
||||
border: 1.5px solid var(--clink);
|
||||
}
|
||||
button { padding: 0.4em 1em; font-size: 85%; letter-spacing: 0.1em; }
|
||||
button[disabled]{ color: var(--cdark); border-color: var(--cmed); }
|
||||
fieldset { border-radius: 4px; border: var(--border); padding: .5em 1em;}
|
||||
textarea:hover, input:not([type=checkbox]):not([type*='ra']):hover, select:hover{
|
||||
border: 1.5px solid var(--cemph);
|
||||
}
|
||||
textarea:focus, input:not([type=checkbox]):not([type*='ra']):focus{
|
||||
border: 1.5px solid var(--clink);
|
||||
box-shadow: 0 0 5px var(--clink);
|
||||
}
|
||||
p>button { padding: 0 .5em; margin: 0 .5em; }
|
||||
p>select { padding: 0; margin: 0 .5em; }
|
||||
|
||||
|
||||
/* 5. Bootstrap-compatible classes ––––––––––––––––––––– */
|
||||
|
||||
/* grid */
|
||||
.row { display: flex; margin: 0.5rem -0.6rem; align-items: stretch; }
|
||||
.row [class*="col"] { padding: 0 0.6rem; }
|
||||
.row .col { flex: 1 1 100%; }
|
||||
.row .col-2 { flex: 0 0 16.66%; max-width: 16.66%;}
|
||||
.row .col-3 { flex: 0 0 25%; max-width: 25%;}
|
||||
.row .col-4 { flex: 0 0 33.33%; max-width: 33.33%; }
|
||||
.row .col-5 { flex: 0 0 41.66%; max-width: 41.66%; }
|
||||
.row .col-6 { flex: 0 0 50%; max-width: 50%; }
|
||||
@media (max-width: 40rem) { .row { flex-direction: column; } }
|
||||
|
||||
/* align */
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.float-left { float: left !important; }
|
||||
.float-right { float: right !important; }
|
||||
.clearfix { clear: both; }
|
||||
|
||||
/* colors */
|
||||
.text-black { color: #000; }
|
||||
.text-white { color: #fff; }
|
||||
.text-primary { color: var(--cemph); }
|
||||
.text-secondary{ color: var(--cdark); }
|
||||
.bg-white { background-color: #fff; }
|
||||
.bg-light { background-color: var(--clight); }
|
||||
.bg-primary { background-color: var(--cemph); }
|
||||
.bg-secondary{ background-color: var(--cmed); }
|
||||
|
||||
/* margins */
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
.m-0 { margin: 0 !important; }
|
||||
.m-1, .mx-1, .mr-1 { margin-right: 1.0rem !important; }
|
||||
.m-1, .mx-1, .ml-1 { margin-left: 1.0rem !important; }
|
||||
.m-1, .my-1, .mt-1 { margin-top: 1.0rem !important; }
|
||||
.m-1, .my-1, .mb-1 { margin-bottom: 1.0rem !important; }
|
||||
|
||||
/* pading */
|
||||
.p-0 { padding: 0 !important; }
|
||||
.p-1, .px-1, .pr-1 { padding-right: 1.0rem !important; }
|
||||
.p-1, .px-1, .pl-1 { padding-left: 1.0rem !important; }
|
||||
.p-1, .py-1, .pt-1 { padding-top: 1.0rem !important; }
|
||||
.p-1, .py-1, .pb-1 { padding-bottom: 1.0rem !important; }
|
||||
|
||||
/* be print-friendly */
|
||||
@media print {
|
||||
@page { margin: 1.5cm 2cm; }
|
||||
html {font-size: 9pt!important; }
|
||||
body { max-width: 27cm; }
|
||||
p { orphans: 2; widows: 2; }
|
||||
caption, figcaption { page-break-before: avoid; }
|
||||
h2, h3, h4, h5 { page-break-after: avoid;}
|
||||
.noprint, body>nav, section:after { display: none; }
|
||||
.row { flex-direction: row; }
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
#control {
|
||||
display: flex;
|
||||
}
|
||||
#control button {
|
||||
flex:auto;
|
||||
}
|
||||
#used-grouped td:first-child ,
|
||||
#used td:first-child {
|
||||
text-align: right
|
||||
}
|
||||
#used-grouped td:first-child:after ,
|
||||
#used td:first-child:after {
|
||||
content: "/"
|
||||
}
|
||||
#used-grouped td ,
|
||||
#used td {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
#used-grouped {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
justify-content: space-evenly;
|
||||
gap: 3rem;
|
||||
}
|
||||
#used-grouped > div {
|
||||
flex: 0 1 auto;
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Subnetzvergabe</title>
|
||||
<link rel="stylesheet" href="/css/classless.css">
|
||||
<link rel="stylesheet" href="/css/subv4.css">
|
||||
<script src="/js/htmx.min.js"></script>
|
||||
<script src="/js/idiomorph-ext.min.js"></script>
|
||||
</head>
|
||||
<body hx-ext="morph">
|
||||
<h1>Subnetzvergabe</h1>
|
||||
|
||||
<div id="control">
|
||||
<button hx-get="/api/alloc/32" hx-swap="none">/32</button>
|
||||
<button hx-get="/api/alloc/31" hx-swap="none">/31</button>
|
||||
<button hx-get="/api/alloc/30" hx-swap="none">/30</button>
|
||||
<button hx-get="/api/alloc/29" hx-swap="none">/29</button>
|
||||
<button hx-get="/api/alloc/28" hx-swap="none">/28</button>
|
||||
<button hx-get="/api/alloc/27" hx-swap="none">/27</button>
|
||||
<button hx-get="/api/alloc/26" hx-swap="none">/26</button>
|
||||
<button hx-get="/api/alloc/25" hx-swap="none">/25</button>
|
||||
<button hx-get="/api/alloc/24" hx-swap="none">/24</button>
|
||||
</div>
|
||||
|
||||
{{ block "update" .Update }}<div id="updatemsg" class="card {{ .Type }}" hx-swap-oob="morph">{{ with .Content }}{{ . }}{{ end }}</div>{{ end }}
|
||||
|
||||
{{ block "used" .Api }}
|
||||
<div id="state" hx-swap-oob="morph">
|
||||
|
||||
<h2>grouped</h2>
|
||||
<div id="used-grouped" hx-swap-oob="morph">
|
||||
{{- range .UsedGrouped }}
|
||||
{{ $first := index . 0 }}
|
||||
{{ $bits := $first.Bits }}
|
||||
<div>
|
||||
<h3>/{{ $bits }}</h3>
|
||||
<table id="group-{{ $bits }}">
|
||||
{{ range . }}
|
||||
<tr hx-get="/api/dealloc/{{.}}" hx-swap="none"><td>{{.Addr}}</td><td>{{.Bits}}</td></tr>{{ end }}
|
||||
</table>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<h2>all</h2>
|
||||
<div id="used" hx-swap-oob="morph">
|
||||
<table>
|
||||
{{- range .Used }}
|
||||
<tr hx-get="/api/dealloc/{{.}}" hx-swap="none"><td>{{.Addr}}</td><td>{{.Bits}}</td></tr>{{ end }}
|
||||
</table>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,468 @@
|
|||
package ipalloc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
"slices"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Append(Operation) error
|
||||
Operations() (Scanner, error)
|
||||
}
|
||||
|
||||
type Scanner interface {
|
||||
Next() bool
|
||||
Operation() Operation
|
||||
Err() error
|
||||
}
|
||||
|
||||
type Operation interface {
|
||||
Prefix() netip.Prefix
|
||||
}
|
||||
|
||||
type op struct {
|
||||
prefix netip.Prefix
|
||||
}
|
||||
|
||||
func (o op) Prefix() netip.Prefix {
|
||||
return o.prefix
|
||||
}
|
||||
|
||||
type OpProvision struct{ op }
|
||||
|
||||
type OpAllocation struct{ op }
|
||||
|
||||
type OpDeallocation struct{ op }
|
||||
|
||||
type Memstore struct {
|
||||
ops []Operation
|
||||
}
|
||||
|
||||
func (m *Memstore) Append(op Operation) error {
|
||||
m.ops = append(m.ops, op)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Memstore) Operations() (Scanner, error) {
|
||||
return &memscanner{opQueue: slices.Clip(m.ops)}, nil
|
||||
}
|
||||
|
||||
type memscanner struct {
|
||||
cur Operation
|
||||
opQueue []Operation
|
||||
}
|
||||
|
||||
func (m *memscanner) Next() bool {
|
||||
if len(m.opQueue) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
m.opQueue, m.cur = m.opQueue[1:], m.opQueue[0]
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *memscanner) Operation() Operation {
|
||||
return m.cur
|
||||
}
|
||||
|
||||
func (m *memscanner) Err() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type Filestore struct {
|
||||
filepath string
|
||||
}
|
||||
|
||||
func (fs *Filestore) Append(op Operation) error {
|
||||
f, err := os.OpenFile(fs.filepath, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = fmt.Fprintln(f, op)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *Filestore) Operations() (Scanner, error) {
|
||||
f, err := os.OpenFile(fs.filepath, os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = syscall.Flock(int(f.Fd()), syscall.LOCK_SH); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newFileScanner(f), nil
|
||||
}
|
||||
|
||||
type fileScanner struct {
|
||||
err error
|
||||
cur Operation
|
||||
f *os.File
|
||||
s bufio.Scanner
|
||||
}
|
||||
|
||||
func newFileScanner(f *os.File) *fileScanner {
|
||||
return &fileScanner{
|
||||
err: nil,
|
||||
cur: nil,
|
||||
f: f,
|
||||
s: *bufio.NewScanner(f),
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fileScanner) Next() bool {
|
||||
if f.f == nil {
|
||||
return false
|
||||
}
|
||||
if !f.s.Scan() {
|
||||
f.err = f.s.Err()
|
||||
if err := f.f.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
log.Println(f.s.Text())
|
||||
f.cur = nil // TODO: parse
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *fileScanner) Operation() Operation {
|
||||
return f.cur
|
||||
}
|
||||
|
||||
func (f *fileScanner) Err() error {
|
||||
return f.err
|
||||
}
|
||||
|
||||
type tree struct {
|
||||
prefix netip.Prefix
|
||||
lo *tree
|
||||
hi *tree
|
||||
parent *tree
|
||||
}
|
||||
|
||||
func (t *tree) Prefix() netip.Prefix {
|
||||
return t.prefix
|
||||
}
|
||||
|
||||
func (t *tree) Split() {
|
||||
lo, hi := splitPrefix(t.prefix)
|
||||
|
||||
t.lo = &tree{prefix: lo, parent: t}
|
||||
t.hi = &tree{prefix: hi, parent: t}
|
||||
}
|
||||
|
||||
func (t *tree) BestMatch(bits int) *tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if t.Leaf() {
|
||||
// already taken
|
||||
return nil
|
||||
}
|
||||
//log.Println("testing", t.prefix)
|
||||
if t.prefix.Bits() >= bits {
|
||||
//log.Println("too long", t.prefix)
|
||||
return nil
|
||||
}
|
||||
|
||||
var best *tree
|
||||
if !t.Full() {
|
||||
best = t
|
||||
}
|
||||
|
||||
best = better(best, t.lo.BestMatch(bits))
|
||||
best = better(best, t.hi.BestMatch(bits))
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
func better(t1, t2 *tree) *tree {
|
||||
switch {
|
||||
case t1 == nil:
|
||||
return t2
|
||||
case t2 == nil:
|
||||
return t1
|
||||
case t1.prefix.Bits() >= t2.prefix.Bits():
|
||||
return t1
|
||||
default:
|
||||
return t2
|
||||
}
|
||||
}
|
||||
|
||||
func (t *tree) Alloc(bits int) *tree {
|
||||
//log.Println("Searching space for", bits)
|
||||
best := t.BestMatch(bits)
|
||||
if best == nil {
|
||||
best = t
|
||||
}
|
||||
if best.Full() {
|
||||
return nil
|
||||
}
|
||||
|
||||
for best.prefix.Bits() < bits {
|
||||
lo, hi := splitPrefix(best.prefix)
|
||||
//log.Println(lo, hi)
|
||||
switch {
|
||||
case best.lo == nil:
|
||||
best.lo = &tree{prefix: lo, parent: best}
|
||||
best = best.lo
|
||||
case best.hi == nil:
|
||||
best.hi = &tree{prefix: hi, parent: best}
|
||||
best = best.hi
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
//log.Println("choosing", best.prefix)
|
||||
}
|
||||
|
||||
return best
|
||||
}
|
||||
|
||||
func (t *tree) Insert(p netip.Prefix) *tree {
|
||||
var ret *tree
|
||||
|
||||
for t.prefix.Bits() < p.Bits() {
|
||||
lo, hi := splitPrefix(t.prefix)
|
||||
//log.Println(lo, hi)
|
||||
switch {
|
||||
case lo.Overlaps(p):
|
||||
if t.lo != nil {
|
||||
t = t.lo
|
||||
} else {
|
||||
t.lo = &tree{prefix: lo, parent: t}
|
||||
t = t.lo
|
||||
ret = t
|
||||
}
|
||||
case hi.Overlaps(p):
|
||||
if t.hi != nil {
|
||||
t = t.hi
|
||||
} else {
|
||||
t.hi = &tree{prefix: hi, parent: t}
|
||||
t = t.hi
|
||||
ret = t
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
//log.Println("splitting to", best.prefix)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *tree) FindBest(p netip.Prefix) *tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
if !t.prefix.Overlaps(p) {
|
||||
return nil
|
||||
}
|
||||
if best := t.lo.FindBest(p); best != nil {
|
||||
return best
|
||||
}
|
||||
if best := t.hi.FindBest(p); best != nil {
|
||||
return best
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
type ErrNotFound struct{}
|
||||
|
||||
func (e ErrNotFound) Error() string {
|
||||
return "prefix not found"
|
||||
}
|
||||
|
||||
type ErrNotEmpty struct{}
|
||||
|
||||
func (e ErrNotEmpty) Error() string {
|
||||
return "prefix not empty"
|
||||
}
|
||||
|
||||
func (t *tree) Dealloc(p netip.Prefix) error {
|
||||
match := t.Find(p)
|
||||
if match == nil {
|
||||
return ErrNotFound{}
|
||||
}
|
||||
if !match.Leaf() {
|
||||
return ErrNotEmpty{}
|
||||
}
|
||||
|
||||
for match != nil {
|
||||
if match.Leaf() && match.parent != nil {
|
||||
if match.parent.lo == match {
|
||||
match.parent.lo = nil
|
||||
}
|
||||
if match.parent.hi == match {
|
||||
match.parent.hi = nil
|
||||
}
|
||||
}
|
||||
|
||||
match = match.parent
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (t *tree) Find(p netip.Prefix) *tree {
|
||||
if t == nil {
|
||||
return nil
|
||||
}
|
||||
//log.Println("searching for", p, "in", t.prefix)
|
||||
if t.prefix == p {
|
||||
return t
|
||||
}
|
||||
if !t.prefix.Overlaps(p) {
|
||||
//log.Println(p, "does not overlap", t.prefix)
|
||||
return nil
|
||||
}
|
||||
l := t.lo.Find(p)
|
||||
if l != nil {
|
||||
return l
|
||||
}
|
||||
return t.hi.Find(p)
|
||||
}
|
||||
|
||||
func (t *tree) Leaf() bool {
|
||||
return t != nil && t.lo == nil && t.hi == nil
|
||||
}
|
||||
func (t *tree) Full() bool {
|
||||
return t != nil && t.lo != nil && t.hi != nil
|
||||
}
|
||||
|
||||
func (t *tree) Walk(f func(*tree) bool) {
|
||||
if t == nil {
|
||||
return
|
||||
}
|
||||
t.lo.Walk(f)
|
||||
if !f(t) {
|
||||
return
|
||||
}
|
||||
t.hi.Walk(f)
|
||||
}
|
||||
|
||||
type DB struct {
|
||||
sync.Mutex
|
||||
provisions *tree
|
||||
}
|
||||
|
||||
func NewDB(prefix string) *DB {
|
||||
return &DB{provisions: &tree{prefix: netip.MustParsePrefix(prefix)}}
|
||||
}
|
||||
|
||||
func (db *DB) Used() []netip.Prefix {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
var ret []netip.Prefix
|
||||
db.provisions.Walk(func(t *tree) bool {
|
||||
if t.Leaf() && t.parent != nil {
|
||||
ret = append(ret, t.prefix)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
func (db *DB) UsedGrouped() [][]netip.Prefix {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
bins := make(map[int][]netip.Prefix)
|
||||
db.provisions.Walk(func(t *tree) bool {
|
||||
if t.Leaf() && t.parent != nil {
|
||||
p := t.prefix
|
||||
bins[p.Bits()] = append(bins[p.Bits()], p)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
var ret [][]netip.Prefix
|
||||
for _, bin := range bins {
|
||||
ret = append(ret, bin)
|
||||
}
|
||||
|
||||
slices.SortFunc(ret, func(ps1, ps2 []netip.Prefix) int {
|
||||
return ps2[0].Bits() - ps1[0].Bits()
|
||||
})
|
||||
|
||||
return ret
|
||||
}
|
||||
func (db *DB) All() []*tree {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
var ret []*tree
|
||||
db.provisions.Walk(func(t *tree) bool {
|
||||
ret = append(ret, t)
|
||||
return true
|
||||
})
|
||||
return ret
|
||||
}
|
||||
func (db *DB) Alloc(bits int) (*tree, error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
t := db.provisions.Alloc(bits)
|
||||
if t == nil {
|
||||
return nil, fmt.Errorf("Allocation not possible for size %d", bits)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
func (db *DB) Insert(prefix netip.Prefix) error {
|
||||
return nil
|
||||
}
|
||||
func (db *DB) Dealloc(prefix netip.Prefix) error {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
return db.provisions.Dealloc(prefix)
|
||||
}
|
||||
func (db *DB) Free() ([]netip.Prefix, error) {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
return nil, nil
|
||||
}
|
||||
func (db *DB) Find(p netip.Prefix) bool {
|
||||
db.Lock()
|
||||
defer db.Unlock()
|
||||
p = p.Masked()
|
||||
return db.provisions.Find(p) != nil
|
||||
}
|
||||
|
||||
//func (db *DB) Provision(p netip.Prefix) error {
|
||||
// db.Lock()
|
||||
// defer db.Unlock()
|
||||
// p = p.Masked()
|
||||
// if db.provisions.Provision(p) == nil {
|
||||
// return fmt.Errorf("Overlapping provisions: %s", p)
|
||||
// }
|
||||
// return nil
|
||||
//}
|
||||
|
||||
func splitPrefix(p netip.Prefix) (netip.Prefix, netip.Prefix) {
|
||||
if p.IsSingleIP() {
|
||||
log.Fatal("can't split single ip prefix", p)
|
||||
}
|
||||
p = p.Masked()
|
||||
|
||||
bs := p.Addr().AsSlice()
|
||||
off := p.Bits() / 8
|
||||
bit := p.Bits() % 8
|
||||
bs[off] |= 0x80 >> bit
|
||||
hiaddr, ok := netip.AddrFromSlice(bs)
|
||||
if !ok {
|
||||
log.Fatal("can't use slice as addr", bs)
|
||||
}
|
||||
|
||||
lo := netip.PrefixFrom(p.Addr(), p.Bits()+1)
|
||||
hi := netip.PrefixFrom(hiaddr, p.Bits()+1)
|
||||
|
||||
return lo, hi
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package ipalloc
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/netip"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSplitPrefix(t *testing.T) {
|
||||
tests := []struct{ in, lo, hi string }{
|
||||
{"0.0.0.0/0", "0.0.0.0/1", "128.0.0.0/1"},
|
||||
{"0.0.0.0/29", "0.0.0.0/30", "0.0.0.4/30"},
|
||||
{"0.0.0.0/31", "0.0.0.0/32", "0.0.0.1/32"},
|
||||
{"0.0.0.1/0", "0.0.0.0/1", "128.0.0.0/1"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
lo, hi := splitPrefix(netip.MustParsePrefix(test.in))
|
||||
if lo != netip.MustParsePrefix(test.lo) || hi != netip.MustParsePrefix(test.hi) {
|
||||
t.Fatal(test.in, lo, hi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTreeAlloc(t *testing.T) {
|
||||
tests := []struct {
|
||||
bits int
|
||||
best string
|
||||
}{
|
||||
{32, "0.0.0.0/32"},
|
||||
{32, "0.0.0.1/32"},
|
||||
{1, "128.0.0.0/1"},
|
||||
{2, "64.0.0.0/2"},
|
||||
{31, "0.0.0.2/31"},
|
||||
{32, "0.0.0.4/32"},
|
||||
}
|
||||
root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")}
|
||||
|
||||
for _, test := range tests {
|
||||
best := root.Alloc(test.bits)
|
||||
if best == nil {
|
||||
t.Fatal(root, test.bits)
|
||||
}
|
||||
if best.prefix != netip.MustParsePrefix(test.best) {
|
||||
t.Fatal(root, test.bits, best.prefix.String())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestTreeDealloc(t *testing.T) {
|
||||
tests := []struct {
|
||||
bits int
|
||||
best string
|
||||
}{
|
||||
{32, "0.0.0.0/32"},
|
||||
{32, "0.0.0.1/32"},
|
||||
{1, "128.0.0.0/1"},
|
||||
{2, "64.0.0.0/2"},
|
||||
{31, "0.0.0.2/31"},
|
||||
{32, "0.0.0.4/32"},
|
||||
}
|
||||
root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")}
|
||||
|
||||
for _, test := range tests {
|
||||
best := root.Alloc(test.bits)
|
||||
if best == nil {
|
||||
t.Fatal(root, test.bits)
|
||||
}
|
||||
if best.prefix != netip.MustParsePrefix(test.best) {
|
||||
t.Fatal(root, test.bits, best.prefix.String())
|
||||
}
|
||||
}
|
||||
|
||||
deallocs := []string{
|
||||
"0.0.0.0/32",
|
||||
"0.0.0.1/32",
|
||||
"128.0.0.0/1",
|
||||
"64.0.0.0/2",
|
||||
"0.0.0.2/31",
|
||||
"0.0.0.4/32",
|
||||
}
|
||||
|
||||
for _, test := range deallocs {
|
||||
if err := root.Dealloc(netip.MustParsePrefix(test)); err != nil {
|
||||
t.Fatal(test, err)
|
||||
}
|
||||
}
|
||||
|
||||
if !root.Leaf() {
|
||||
root.Walk(func(t *tree) bool {
|
||||
log.Println(t.prefix, t.Leaf())
|
||||
return true
|
||||
})
|
||||
t.Fatalf("root not empty: %#v", root)
|
||||
}
|
||||
|
||||
first := root.Alloc(2).prefix
|
||||
root.Alloc(2)
|
||||
root.Dealloc(first)
|
||||
if first != root.Alloc(2).prefix {
|
||||
root.Walk(func(t *tree) bool {
|
||||
log.Println(t.prefix, t.Leaf())
|
||||
return true
|
||||
})
|
||||
t.Fatal(first)
|
||||
}
|
||||
}
|
||||
func TestFindBest(t *testing.T) {
|
||||
tests := []struct {
|
||||
bits int
|
||||
best string
|
||||
}{
|
||||
{32, "0.0.0.0/32"},
|
||||
{32, "0.0.0.1/32"},
|
||||
{1, "128.0.0.0/1"},
|
||||
{2, "64.0.0.0/2"},
|
||||
{31, "0.0.0.2/31"},
|
||||
{32, "0.0.0.4/32"},
|
||||
}
|
||||
root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")}
|
||||
|
||||
for _, test := range tests {
|
||||
best := root.Alloc(test.bits)
|
||||
if best == nil {
|
||||
t.Fatal(root, test.bits)
|
||||
}
|
||||
if best.prefix != netip.MustParsePrefix(test.best) {
|
||||
t.Fatal(root, test.bits, best.prefix.String())
|
||||
}
|
||||
}
|
||||
|
||||
best := [][2]string{
|
||||
{"128.0.0.0/32", "128.0.0.0/1"},
|
||||
{"128.0.0.1/32", "128.0.0.0/1"},
|
||||
}
|
||||
|
||||
for _, test := range best {
|
||||
best := root.FindBest(netip.MustParsePrefix(test[0]))
|
||||
want := netip.MustParsePrefix(test[1])
|
||||
|
||||
if best.prefix != want {
|
||||
t.Fatal(best, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
tests := []string{
|
||||
"0.0.0.1/32",
|
||||
"0.0.0.0/32",
|
||||
"128.0.0.0/1",
|
||||
"64.0.0.0/2",
|
||||
"0.0.0.4/32",
|
||||
"0.0.0.2/31",
|
||||
}
|
||||
root := &tree{prefix: netip.MustParsePrefix("0.0.0.0/0")}
|
||||
|
||||
for _, test := range tests {
|
||||
p := netip.MustParsePrefix(test)
|
||||
insert := root.Insert(p)
|
||||
if insert == nil {
|
||||
t.Fatal(root, test)
|
||||
}
|
||||
match := root.Find(p)
|
||||
if match.prefix != p {
|
||||
t.Fatal("unable to find", p, "in", root)
|
||||
}
|
||||
}
|
||||
for _, test := range tests {
|
||||
p := netip.MustParsePrefix(test)
|
||||
insert := root.Insert(p)
|
||||
if insert != nil {
|
||||
t.Fatal(root, test)
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,157 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"subv4/ipalloc"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
mux *http.ServeMux
|
||||
ipdb *ipalloc.DB
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
func (s *server) setup() {
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("/js/", s.static())
|
||||
s.mux.HandleFunc("/css/", s.static())
|
||||
s.mux.HandleFunc("/", s.template)
|
||||
s.mux.HandleFunc("/api/prefix/", s.apiPrefix)
|
||||
s.mux.HandleFunc("/api/alloc/", s.apiAlloc)
|
||||
s.mux.HandleFunc("/api/dealloc/", s.apiDealloc)
|
||||
|
||||
s.mux.HandleFunc("/api/used/", s.apiUsed)
|
||||
s.mux.HandleFunc("/api/print/", s.apiPrint)
|
||||
//s.mux.HandleFunc("/api/provision/", s.apiProvision)
|
||||
|
||||
s.ipdb = ipalloc.NewDB("10.83.46.0/23")
|
||||
s.tmpl = template.Must(template.ParseFS(webcontent, "index.html"))
|
||||
}
|
||||
|
||||
func (s *server) apiPrefix(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
prefixString := strings.TrimPrefix(r.URL.Path, "/api/prefix/")
|
||||
prefix, err := netip.ParsePrefix(prefixString)
|
||||
fmt.Fprintln(w, prefix, err)
|
||||
case "POST":
|
||||
}
|
||||
}
|
||||
|
||||
type PageContent struct {
|
||||
Api *ipalloc.DB
|
||||
Update UpdateMessage
|
||||
}
|
||||
type UpdateMessage struct {
|
||||
Type string
|
||||
Content any
|
||||
}
|
||||
|
||||
func (s *server) template(w http.ResponseWriter, r *http.Request) {
|
||||
err := s.tmpl.Execute(w, PageContent{Api: s.ipdb})
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
func (s *server) apiUsed(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
for _, used := range s.ipdb.Used() {
|
||||
fmt.Fprintln(w, used)
|
||||
}
|
||||
}
|
||||
func (s *server) apiPrint(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
for _, p := range s.ipdb.All() {
|
||||
fmt.Fprintln(w, p.Prefix(), p.Leaf())
|
||||
}
|
||||
}
|
||||
func (s *server) apiAlloc(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var updmsg UpdateMessage
|
||||
|
||||
bitsString := strings.TrimPrefix(r.URL.Path, "/api/alloc/")
|
||||
bits, err := strconv.Atoi(bitsString)
|
||||
if err != nil {
|
||||
updmsg.Type = "warn"
|
||||
updmsg.Content = err
|
||||
if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
alloc, err := s.ipdb.Alloc(bits)
|
||||
|
||||
if err != nil {
|
||||
updmsg.Type = "warn"
|
||||
updmsg.Content = err
|
||||
} else {
|
||||
updmsg.Type = "hint"
|
||||
updmsg.Content = fmt.Sprintf("Added %s", alloc.Prefix())
|
||||
}
|
||||
if err := s.tmpl.ExecuteTemplate(w, "update", updmsg); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if err := s.tmpl.ExecuteTemplate(w, "used", s.ipdb); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
func (s *server) apiDealloc(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
var updmsg UpdateMessage
|
||||
|
||||
prefixString := strings.TrimPrefix(r.URL.Path, "/api/dealloc/")
|
||||
prefix, err := netip.ParsePrefix(prefixString)
|
||||
if err != nil {
|
||||
updmsg.Type = "warn"
|
||||
updmsg.Content = err
|
||||
s.tmpl.ExecuteTemplate(w, "update", updmsg)
|
||||
return
|
||||
}
|
||||
|
||||
err = s.ipdb.Dealloc(prefix)
|
||||
|
||||
if err != nil {
|
||||
updmsg.Type = "warn"
|
||||
updmsg.Content = err
|
||||
} else {
|
||||
updmsg.Type = "hint"
|
||||
updmsg.Content = fmt.Sprintf("Removed %s", prefix)
|
||||
}
|
||||
s.tmpl.ExecuteTemplate(w, "update", updmsg)
|
||||
s.tmpl.ExecuteTemplate(w, "used", s.ipdb)
|
||||
}
|
||||
|
||||
//go:embed js css *.html
|
||||
var webcontent embed.FS
|
||||
|
||||
func (s *server) static() http.HandlerFunc {
|
||||
fs := http.FS(webcontent)
|
||||
fileserver := http.FileServer(fs)
|
||||
return fileserver.ServeHTTP
|
||||
}
|
||||
|
||||
func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
log.Println(r.URL)
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s server
|
||||
s.setup()
|
||||
http.ListenAndServe("[::1]:8080", &s)
|
||||
}
|
Loading…
Reference in New Issue