add digital ocean & DNS state windows

lists digital ocean droplets
    create a new digital ocean droplet
    knows what needs to be done to get IPv4 and IPv6 to work
    update windows on Show()
    make a window for the state of DNS specific to the hostname

Signed-off-by: Jeff Carr <jcarr@wit.com>
This commit is contained in:
Jeff Carr 2023-12-29 01:36:10 -06:00
parent 8afc73da04
commit 1258be9bef
12 changed files with 574 additions and 32 deletions

1
.gitignore vendored
View File

@ -4,4 +4,5 @@ control-panel-dns
*.swp
/plugins/*
examples/control-panel-digitalocean/control-panel-digitalocean
examples/control-panel-cloudflare/control-panel-cloudflare

View File

@ -0,0 +1,49 @@
package digitalocean
import (
"context"
"fmt"
"golang.org/x/oauth2"
"github.com/digitalocean/godo"
)
// ListDroplets fetches and prints out the droplets along with their IPv4 and IPv6 addresses.
func ListDroplets(token string) error {
// OAuth token for authentication.
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
// OAuth2 client.
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
// DigitalOcean client.
client := godo.NewClient(oauthClient)
// Context.
ctx := context.TODO()
// List all droplets.
droplets, _, err := client.Droplets.List(ctx, &godo.ListOptions{})
if err != nil {
return err
}
// Iterate over droplets and print their details.
for _, droplet := range droplets {
fmt.Printf("Droplet: %s\n", droplet.Name)
for _, network := range droplet.Networks.V4 {
if network.Type == "public" {
fmt.Printf("IPv4: %s\n", network.IPAddress)
}
}
for _, network := range droplet.Networks.V6 {
if network.Type == "public" {
fmt.Printf("IPv6: %s\n", network.IPAddress)
}
}
fmt.Println("-------------------------")
}
return nil
}

31
digitalocean/listKeys.go Normal file
View File

@ -0,0 +1,31 @@
package digitalocean
import (
"context"
"fmt"
"golang.org/x/oauth2"
"github.com/digitalocean/godo"
)
func GetSSHKeyID(token, name string) (string, error) {
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
client := godo.NewClient(oauthClient)
// List all keys.
keys, _, err := client.Keys.List(context.Background(), &godo.ListOptions{})
if err != nil {
return "", err
}
// Find the key by name.
for _, key := range keys {
if key.Name == name {
return key.Fingerprint, nil
}
}
return "", fmt.Errorf("SSH Key not found")
}

View File

@ -0,0 +1,18 @@
package digitalocean
import (
"log"
"go.wit.com/gui"
)
func MakeWindow(n *gui.Node) *gui.Node {
log.Println("digitalocean MakeWindow() START")
win := n.NewWindow("DigitalOcean Control Panel")
// box := g1.NewGroup("data")
group := win.NewGroup("data")
log.Println("digitalocean MakeWindow() END", group)
return win
}

View File

@ -63,3 +63,46 @@ func dnsAAAAlookupDoH(domain string) ([]string, error) {
return ipv6Addresses, nil
}
// dnsLookupDoH performs a DNS lookup for AAAA records over HTTPS.
func lookupDoH(hostname string, rrType string) []string {
var values []string
// Construct the URL for a DNS query with Google's DNS-over-HTTPS API
url := fmt.Sprintf("https://dns.google/resolve?name=%s&type=%s", hostname, rrType)
log.Println("curl", url)
// Perform the HTTP GET request
resp, err := http.Get(url)
if err != nil {
log.Error(err, "error performing DNS-over-HTTPS request")
return nil
}
defer resp.Body.Close()
// Read and unmarshal the response body
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error(fmt.Errorf("error reading response: %w", err))
return nil
}
var data struct {
Answer []struct {
Data string `json:"data"`
} `json:"Answer"`
}
if err := json.Unmarshal(body, &data); err != nil {
log.Error(fmt.Errorf("error unmarshaling response: %w", err))
return nil
}
// Extract the IPv6 addresses
for _, answer := range data.Answer {
values = append(values, answer.Data)
}
return values
}

View File

@ -92,7 +92,7 @@ func NewDigStatusWindow(p *gui.Node) *digStatus {
ds.ready = false
ds.hidden = true
ds.window = p.NewWindow("DNS Lookup Status")
ds.window = p.NewWindow("DNS Resolver Status")
ds.window.Custom = func () {
ds.hidden = true
ds.window.Hide()

View File

@ -0,0 +1,25 @@
# export GO111MODULE="off"
run: build
./control-panel-digitalocean
build-release:
go get -v -u -x .
go build
./control-panel-digitalocean
build:
go get -v -x .
go build
update:
go get -v -u -x .
log:
reset
tail -f /tmp/witgui.* /tmp/guilogfile
gocui: build
./control-panel-digitalocean -gui gocui
quiet:
./control-panel-digitalocean >/tmp/witgui.log.stderr 2>&1

View File

@ -0,0 +1,124 @@
package main
import (
"context"
"fmt"
"os"
"golang.org/x/oauth2"
"go.wit.com/log"
"go.wit.com/gui"
"github.com/digitalocean/godo"
"go.wit.com/control-panel-dns/digitalocean"
)
var title string = "Digital Ocean Control Panel"
/*
// createDroplet creates a new droplet in the specified region with the given name.
func createDroplet(token, name, region, size, image string) (*godo.Droplet, error) {
// Create an OAuth2 token.
tokenSource := &oauth2.Token{
AccessToken: token,
}
// Create an OAuth2 client.
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
// Create a DigitalOcean client with the OAuth2 client.
client := godo.NewClient(oauthClient)
// Define the create request.
createRequest := &godo.DropletCreateRequest{
Name: name,
Region: region,
Size: size,
Image: godo.DropletCreateImage{
Slug: image,
},
}
// Create the droplet.
ctx := context.TODO()
newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
if err != nil {
return nil, err
}
return newDroplet, nil
}
*/
func main() {
// Your personal API token from DigitalOcean.
token := os.Getenv("DIGITALOCEAN_TOKEN")
if token == "" {
log.Fatal("Please set your DigitalOcean API token in the DIGITALOCEAN_TOKEN environment variable")
}
// List droplets and their IP addresses.
err := digitalocean.ListDroplets(token)
if err != nil {
log.Fatalf("Error listing droplets: %s\n", err)
}
// initialize a new GO GUI instance
myGui := gui.New().Default()
// draw the cloudflare control panel window
win := digitalocean.MakeWindow(myGui)
win.SetText(title)
// This is just a optional goroutine to watch that things are alive
gui.Watchdog()
gui.StandardExit()
os.Exit(0)
// Parameters for the droplet you wish to create.
name := "ipv6.wit.com"
region := "nyc1" // New York City region.
size := "s-1vcpu-1gb" // Size of the droplet.
image := "ubuntu-20-04-x64" // Image slug for Ubuntu 20.04 (LTS) x64.
// Create a new droplet.
droplet, err := createDropletNew(token, name, region, size, image)
if err != nil {
log.Fatalf("Something went wrong: %s\n", err)
}
fmt.Printf("Created droplet ID %d with name %s\n", droplet.ID, droplet.Name)
}
// createDroplet creates a new droplet in the specified region with the given name.
func createDropletNew(token, name, region, size, image string) (*godo.Droplet, error) {
// Create an OAuth2 token.
tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
// Create an OAuth2 client.
oauthClient := oauth2.NewClient(context.Background(), tokenSource)
// Create a DigitalOcean client with the OAuth2 client.
client := godo.NewClient(oauthClient)
// Define the create request.
createRequest := &godo.DropletCreateRequest{
Name: name,
Region: region,
Size: size,
Image: godo.DropletCreateImage{
Slug: image,
},
IPv6: true, // Enable IPv6
}
// Create the droplet.
ctx := context.TODO()
newDroplet, _, err := client.Droplets.Create(ctx, createRequest)
if err != nil {
return nil, err
}
return newDroplet, nil
}

39
gui.go
View File

@ -308,20 +308,38 @@ func dnsTab(title string) {
})
me.fix.Disable()
me.digStatusButton = me.mainStatus.NewButton("Show DNS Lookup Status", func () {
me.digStatusButton = me.mainStatus.NewButton("Resolver Status", func () {
if (me.digStatus == nil) {
log.Info("drawing the digStatus window START")
me.digStatus = NewDigStatusWindow(me.window)
me.digStatus = NewDigStatusWindow(myGui)
log.Info("drawing the digStatus window END")
me.digStatusButton.SetText("Hide DNS Lookup Status")
me.digStatus.Update()
return
}
if me.digStatus.hidden {
me.digStatusButton.SetText("Hide Resolver Status")
me.digStatus.Show()
me.digStatus.Update()
} else {
if me.digStatus.hidden {
me.digStatusButton.SetText("Hide DNS Lookup Status")
me.digStatus.Show()
} else {
me.digStatusButton.SetText("Show DNS Lookup Status")
me.digStatus.Hide()
}
me.digStatusButton.SetText("Resolver Status")
me.digStatus.Hide()
}
})
me.hostnameStatusButton = me.mainStatus.NewButton("Show hostname DNS Status", func () {
if (me.hostnameStatus == nil) {
me.hostnameStatus = NewHostnameStatusWindow(myGui)
me.hostnameStatusButton.SetText("Hide " + me.hostname + " DNS Status")
me.hostnameStatus.Update()
return
}
if me.hostnameStatus.hidden {
me.hostnameStatusButton.SetText("Hide " + me.hostname + " DNS Status")
me.hostnameStatus.Show()
me.hostnameStatus.Update()
} else {
me.hostnameStatusButton.SetText("Show " + me.hostname + " DNS Status")
me.hostnameStatus.Hide()
}
})
@ -344,7 +362,7 @@ func statusGrid(n *gui.Node) {
me.statusIPv6.Set("known")
gridP.NewLabel("hostname =")
me.hostnameStatus = gridP.NewLabel("invalid")
me.hostnameStatusOLD = gridP.NewLabel("invalid")
gridP.NewLabel("dns resolution")
me.DnsSpeed = gridP.NewLabel("unknown")
@ -383,6 +401,7 @@ func updateDNS() {
}
me.digStatus.Update()
me.hostnameStatus.Update()
// log.Println("digAAAA()")
aaaa = digAAAA(h)

View File

@ -1,29 +1,19 @@
// inspired from:
// https://github.com/mactsouk/opensource.com.git
// and
// https://coderwall.com/p/wohavg/creating-a-simple-tcp-server-in-go
// figures out if your hostname is valid
// then checks if your DNS is setup correctly
package main
import (
"log"
// "net"
"strings"
"go.wit.com/log"
"go.wit.com/shell"
"go.wit.com/control-panel-dns/cloudflare"
"github.com/miekg/dns"
// will try to get this hosts FQDN
"github.com/Showmax/go-fqdn"
)
// will try to get this hosts FQDN
import "github.com/Showmax/go-fqdn"
// this is the king of dns libraries
// import "github.com/miekg/dns"
func getHostname() {
var err error
var s string = "gui.Label == nil"
@ -59,15 +49,15 @@ func getHostname() {
test = hshort + "." + dn
if (me.hostname != test) {
debug(LogInfo, "me.hostname", me.hostname, "does not equal", test)
if (me.hostnameStatus.S != "BROKEN") {
if (me.hostnameStatusOLD.S != "BROKEN") {
debug(LogChange, "me.hostname", me.hostname, "does not equal", test)
me.changed = true
me.hostnameStatus.SetText("BROKEN")
me.hostnameStatusOLD.SetText("BROKEN")
}
} else {
if (me.hostnameStatus.S != "VALID") {
if (me.hostnameStatusOLD.S != "VALID") {
debug(LogChange, "me.hostname", me.hostname, "is valid")
me.hostnameStatus.SetText("VALID")
me.hostnameStatusOLD.SetText("VALID")
me.changed = true
}
// enable the cloudflare button if the provider is cloudflare
@ -140,7 +130,6 @@ func digAAAA(hostname string) []string {
log.Println("digAAAA() RUNNING dnsAAAAlookupDoH(domain)")
ipv6Addresses, _ = dnsAAAAlookupDoH(hostname)
log.Println("digAAAA() has ipv6Addresses =", strings.Join(ipv6Addresses, " "))
log.Printf("digAAAA() IPv6 Addresses for %s:\n", hostname)
for _, addr := range ipv6Addresses {
log.Println(addr)
}

240
hostnameStatus.go Normal file
View File

@ -0,0 +1,240 @@
/*
figures out if your hostname is valid
then checks if your DNS is setup correctly
*/
package main
import (
"os"
"fmt"
"time"
"reflect"
"strings"
"go.wit.com/log"
"go.wit.com/gui"
"go.wit.com/control-panel-dns/cloudflare"
)
type hostnameStatus struct {
ready bool
hidden bool
hostname string // my hostname. Example: "test.wit.com"
window *gui.Node
status *cloudflare.OneLiner
summary *cloudflare.OneLiner
speed *cloudflare.OneLiner
speedActual *cloudflare.OneLiner
dnsA *cloudflare.OneLiner
dnsAAAA *cloudflare.OneLiner
dnsAPI *cloudflare.OneLiner
statusIPv4 *cloudflare.OneLiner
statusIPv6 *cloudflare.OneLiner
// Details Group
currentIPv4 *cloudflare.OneLiner
currentIPv6 *cloudflare.OneLiner
}
func NewHostnameStatusWindow(p *gui.Node) *hostnameStatus {
var hs *hostnameStatus
hs = new(hostnameStatus)
hs.ready = false
hs.hidden = true
hs.hostname = me.hostname
hs.window = p.NewWindow( hs.hostname + " Status")
hs.window.Custom = func () {
hs.hidden = true
hs.window.Hide()
}
box := hs.window.NewBox("hBox", true)
group := box.NewGroup("Summary")
grid := group.NewGrid("LookupStatus", 2, 2)
hs.status = cloudflare.NewOneLiner(grid, "status").Set("unknown")
hs.statusIPv4 = cloudflare.NewOneLiner(grid, "IPv4").Set("unknown")
hs.statusIPv6 = cloudflare.NewOneLiner(grid, "IPv6").Set("unknown")
group.Pad()
grid.Pad()
group = box.NewGroup("Details")
grid = group.NewGrid("LookupDetails", 2, 2)
hs.currentIPv4 = cloudflare.NewOneLiner(grid, "Current IPv4")
hs.currentIPv6 = cloudflare.NewOneLiner(grid, "Current IPv6")
hs.dnsAPI = cloudflare.NewOneLiner(grid, "dns API provider").Set("unknown")
hs.dnsA = cloudflare.NewOneLiner(grid, "dns IPv4 resource records").Set("unknown")
hs.dnsAAAA = cloudflare.NewOneLiner(grid, "dns IPv6 resource records").Set("unknown")
hs.speed = cloudflare.NewOneLiner(grid, "speed").Set("unknown")
hs.speedActual = cloudflare.NewOneLiner(grid, "actual").Set("unknown")
group.Pad()
grid.Pad()
hs.hidden = false
hs.ready = true
return hs
}
func (hs *hostnameStatus) Update() {
log.Info("hostnameStatus() Update() START")
if hs == nil {
log.Error("hostnameStatus() Update() hs == nil")
return
}
duration := timeFunction(func () {
hs.updateStatus()
})
s := fmt.Sprint(duration)
hs.set(hs.speedActual, s)
if (duration > 500 * time.Millisecond ) {
hs.set(hs.speed, "SLOW")
} else if (duration > 100 * time.Millisecond ) {
hs.set(hs.speed, "OK")
} else {
hs.set(hs.speed, "FAST")
}
log.Info("hostnameStatus() Update() END")
}
// Returns true if the status is valid
func (hs *hostnameStatus) Ready() bool {
if hs == nil {return false}
return hs.ready
}
// Returns true if IPv4 is working
func (hs *hostnameStatus) IPv4() bool {
if ! hs.Ready() {return false}
if (hs.statusIPv4.Get() == "OK") {
return true
}
if (hs.statusIPv4.Get() == "GOOD") {
return true
}
return false
}
// Returns true if IPv6 is working
func (hs *hostnameStatus) IPv6() bool {
if ! hs.Ready() {return false}
if (hs.statusIPv6.Get() == "GOOD") {
return true
}
return false
}
func (hs *hostnameStatus) setIPv4(s string) {
hs.statusIPv4.Set(s)
if ! hs.Ready() {return}
}
func (hs *hostnameStatus) setIPv6(s string) {
hs.statusIPv6.Set(s)
if ! hs.Ready() {return}
}
func (hs *hostnameStatus) set(a any, s string) {
if ! hs.Ready() {return}
if hs.hidden {
return
}
if a == nil {
return
}
var n *gui.Node
if reflect.TypeOf(a) == reflect.TypeOf(n) {
n = a.(*gui.Node)
n.SetText(s)
return
}
var ol *cloudflare.OneLiner
if reflect.TypeOf(a) == reflect.TypeOf(ol) {
ol = a.(*cloudflare.OneLiner)
if ol == nil {
log.Println("ol = nil", reflect.TypeOf(a), "a =", a)
return
}
log.Println("SETTING ol:", ol)
ol.Set(s)
return
}
log.Error("unknown type TypeOf(a) =", reflect.TypeOf(a), "a =", a)
os.Exit(0)
}
func (hs *hostnameStatus) updateStatus() {
var s string
var vals []string
log.Info("updateStatus() START")
if ! hs.Ready() { return }
vals = lookupDoH(hs.hostname, "AAAA")
log.Println("IPv6 Addresses for ", hs.hostname, "=", vals)
if len(vals) == 0 {
s = "(none)"
hs.setIPv6("NEED VPN")
} else {
for _, addr := range vals {
log.Println(addr)
s += addr + " (DELETE)"
hs.setIPv6("NEEDS DELETE")
}
}
hs.set(hs.dnsAAAA, s)
vals = lookupDoH(hs.hostname, "A")
log.Println("IPv4 Addresses for ", hs.hostname, "=", vals)
s = strings.Join(vals, "\n")
if (s == "") {
s = "(none)"
hs.setIPv4("NEEDS CNAME")
}
hs.set(hs.dnsA, s)
vals = lookupDoH(hs.hostname, "CNAME")
s = strings.Join(vals, "\n")
if (s != "") {
hs.set(hs.dnsA, "CNAME " + s)
hs.setIPv4("GOOD")
}
hs.currentIPv4.Set(me.IPv4.S)
hs.currentIPv6.Set(me.IPv6.S)
if hs.IPv4() && hs.IPv4() {
hs.status.Set("GOOD")
} else {
hs.status.Set("BROKEN")
}
hs.dnsAPI.Set(me.DnsAPI.S)
}
func (hs *hostnameStatus) Show() {
log.Info("hostnameStatus.Show() window")
if hs.hidden {
hs.window.Show()
}
hs.hidden = false
}
func (hs *hostnameStatus) Hide() {
log.Info("hostnameStatus.Hide() window")
if ! hs.hidden {
hs.window.Hide()
}
hs.hidden = true
}

View File

@ -16,7 +16,7 @@ type Host struct {
hostname string // mirrors
domainname *gui.Node // kernel.org
hostshort *gui.Node // hostname -s
hostnameStatus *gui.Node // is the hostname configured correctly in the OS?
hostnameStatusOLD *gui.Node // is the hostname configured correctly in the OS?
// fqdn string // mirrors.kernel.org
// dnsTTL int `default:"3"` // Recheck DNS is working every TTL (in seconds)
@ -77,6 +77,9 @@ type Host struct {
digStatus *digStatus
statusIPv6 *cloudflare.OneLiner
digStatusButton *gui.Node
hostnameStatus *hostnameStatus
hostnameStatusButton *gui.Node
}
type IPtype struct {