diff --git a/.air.toml b/.air.toml new file mode 100644 index 0000000..81a9bf9 --- /dev/null +++ b/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + bin = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["tmp", "vendor", "testdata", "assets/node_modules"] + exclude_file = [] + exclude_regex = ["_test.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html", "gohtml", "css"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/assets/package.json b/assets/package.json new file mode 100644 index 0000000..8e0d1d6 --- /dev/null +++ b/assets/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "tailwindcss": "^3.4.1" + } +} diff --git a/assets/src/style.css b/assets/src/style.css new file mode 100644 index 0000000..a90f074 --- /dev/null +++ b/assets/src/style.css @@ -0,0 +1,4 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js new file mode 100644 index 0000000..e4f9c53 --- /dev/null +++ b/assets/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["../view/**/*.gohtml"], + theme: { + extend: {}, + }, + plugins: [], +} + diff --git a/go.mod b/go.mod index 0cfea1d..0513d77 100644 --- a/go.mod +++ b/go.mod @@ -4,4 +4,7 @@ go 1.22.1 require libvirt.org/go/libvirt v1.10001.0 -require gopkg.in/ffmt.v1 v1.5.6 // indirect +require ( + github.com/go-chi/chi/v5 v5.0.12 // indirect + gopkg.in/ffmt.v1 v1.5.6 // indirect +) diff --git a/go.sum b/go.sum index 239d34a..eae4159 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= gopkg.in/ffmt.v1 v1.5.6 h1:4Bu3riZp5sAIXW2T/18JM9BkwJLodurXFR0f7PXp+cw= gopkg.in/ffmt.v1 v1.5.6/go.mod h1:LssvGOZFiBGoBcobkTqnyh+uN1VzIRoibW+c0JI/Ha4= libvirt.org/go/libvirt v1.10001.0 h1:lEVDNE7xfzmZXiDEGIS8NvJSuaz11OjRXw+ufbQEtPY= diff --git a/main.go b/main.go index 67eec9c..4116b5d 100644 --- a/main.go +++ b/main.go @@ -2,24 +2,63 @@ package main import ( "log" + "net/http" - "git.staur.ca/stobbsm/clustvirt/lib/host" - ffmt "gopkg.in/ffmt.v1" + "git.staur.ca/stobbsm/clustvirt/view" + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" ) +const DEBUG bool = true + func main() { log.Println("Starting clustvirt, the libvirt cluster manager") - venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca") - if err != nil { - log.Fatal(err) - } - defer venus.Close() - ffmt.P(venus) - lm, err := venus.GetGuestByName("logan-minecraft") - if err != nil { - log.Fatal(err) - } - defer lm.Close() - //ffmt.P(lm) + //venus, err := host.ConnectHost(host.URI_QEMU_SSH_SYSTEM, "venus.staur.ca") + //if err != nil { + // log.Fatal(err) + //} + //defer venus.Close() + //lm, err := venus.GetGuestByName("logan-minecraft") + //if err != nil { + // log.Fatal(err) + //} + //defer lm.Close() + + // Start webserver and serve homepage + + fs := http.StripPrefix("/static/", http.FileServer(http.Dir("public"))) + + r := chi.NewRouter() + r.Use(middleware.Logger) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if DEBUG { + w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Add("Pragma", "no-cache") + w.Header().Add("Expire", "0") + } + log.Println(w.Write([]byte("Nothing on / yet"))) + }) + r.Get("/static/*", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if DEBUG { + w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Add("Pragma", "no-cache") + w.Header().Add("Expire", "0") + } + fs.ServeHTTP(w, r) + }) + r.Get("/home", func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + if DEBUG { + w.Header().Add("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Add("Pragma", "no-cache") + w.Header().Add("Expire", "0") + } + w.Header().Add("Content", "text/html") + log.Println(view.ViewHome.Render(w, nil)) + }) + + log.Println(http.ListenAndServe(":3000", r)) } diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..0535b7c --- /dev/null +++ b/public/css/style.css @@ -0,0 +1,580 @@ +/* +! tailwindcss v3.4.1 | MIT License | https://tailwindcss.com +*/ + +/* +1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) +2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) +*/ + +*, +::before, +::after { + box-sizing: border-box; + /* 1 */ + border-width: 0; + /* 2 */ + border-style: solid; + /* 2 */ + border-color: #e5e7eb; + /* 2 */ +} + +::before, +::after { + --tw-content: ''; +} + +/* +1. Use a consistent sensible line-height in all browsers. +2. Prevent adjustments of font size after orientation changes in iOS. +3. Use a more readable tab size. +4. Use the user's configured `sans` font-family by default. +5. Use the user's configured `sans` font-feature-settings by default. +6. Use the user's configured `sans` font-variation-settings by default. +7. Disable tap highlights on iOS +*/ + +html, +:host { + line-height: 1.5; + /* 1 */ + -webkit-text-size-adjust: 100%; + /* 2 */ + -moz-tab-size: 4; + /* 3 */ + -o-tab-size: 4; + tab-size: 4; + /* 3 */ + font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + /* 4 */ + font-feature-settings: normal; + /* 5 */ + font-variation-settings: normal; + /* 6 */ + -webkit-tap-highlight-color: transparent; + /* 7 */ +} + +/* +1. Remove the margin in all browsers. +2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. +*/ + +body { + margin: 0; + /* 1 */ + line-height: inherit; + /* 2 */ +} + +/* +1. Add the correct height in Firefox. +2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) +3. Ensure horizontal rules are visible by default. +*/ + +hr { + height: 0; + /* 1 */ + color: inherit; + /* 2 */ + border-top-width: 1px; + /* 3 */ +} + +/* +Add the correct text decoration in Chrome, Edge, and Safari. +*/ + +abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; +} + +/* +Remove the default font size and weight for headings. +*/ + +h1, +h2, +h3, +h4, +h5, +h6 { + font-size: inherit; + font-weight: inherit; +} + +/* +Reset links to optimize for opt-in styling instead of opt-out. +*/ + +a { + color: inherit; + text-decoration: inherit; +} + +/* +Add the correct font weight in Edge and Safari. +*/ + +b, +strong { + font-weight: bolder; +} + +/* +1. Use the user's configured `mono` font-family by default. +2. Use the user's configured `mono` font-feature-settings by default. +3. Use the user's configured `mono` font-variation-settings by default. +4. Correct the odd `em` font sizing in all browsers. +*/ + +code, +kbd, +samp, +pre { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; + /* 1 */ + font-feature-settings: normal; + /* 2 */ + font-variation-settings: normal; + /* 3 */ + font-size: 1em; + /* 4 */ +} + +/* +Add the correct font size in all browsers. +*/ + +small { + font-size: 80%; +} + +/* +Prevent `sub` and `sup` elements from affecting the line height in all browsers. +*/ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* +1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) +2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) +3. Remove gaps between table borders by default. +*/ + +table { + text-indent: 0; + /* 1 */ + border-color: inherit; + /* 2 */ + border-collapse: collapse; + /* 3 */ +} + +/* +1. Change the font styles in all browsers. +2. Remove the margin in Firefox and Safari. +3. Remove default padding in all browsers. +*/ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + /* 1 */ + font-feature-settings: inherit; + /* 1 */ + font-variation-settings: inherit; + /* 1 */ + font-size: 100%; + /* 1 */ + font-weight: inherit; + /* 1 */ + line-height: inherit; + /* 1 */ + color: inherit; + /* 1 */ + margin: 0; + /* 2 */ + padding: 0; + /* 3 */ +} + +/* +Remove the inheritance of text transform in Edge and Firefox. +*/ + +button, +select { + text-transform: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Remove default button styles. +*/ + +button, +[type='button'], +[type='reset'], +[type='submit'] { + -webkit-appearance: button; + /* 1 */ + background-color: transparent; + /* 2 */ + background-image: none; + /* 2 */ +} + +/* +Use the modern Firefox focus style for all focusable elements. +*/ + +:-moz-focusring { + outline: auto; +} + +/* +Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) +*/ + +:-moz-ui-invalid { + box-shadow: none; +} + +/* +Add the correct vertical alignment in Chrome and Firefox. +*/ + +progress { + vertical-align: baseline; +} + +/* +Correct the cursor style of increment and decrement buttons in Safari. +*/ + +::-webkit-inner-spin-button, +::-webkit-outer-spin-button { + height: auto; +} + +/* +1. Correct the odd appearance in Chrome and Safari. +2. Correct the outline style in Safari. +*/ + +[type='search'] { + -webkit-appearance: textfield; + /* 1 */ + outline-offset: -2px; + /* 2 */ +} + +/* +Remove the inner padding in Chrome and Safari on macOS. +*/ + +::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* +1. Correct the inability to style clickable types in iOS and Safari. +2. Change font properties to `inherit` in Safari. +*/ + +::-webkit-file-upload-button { + -webkit-appearance: button; + /* 1 */ + font: inherit; + /* 2 */ +} + +/* +Add the correct display in Chrome and Safari. +*/ + +summary { + display: list-item; +} + +/* +Removes the default spacing and border for appropriate elements. +*/ + +blockquote, +dl, +dd, +h1, +h2, +h3, +h4, +h5, +h6, +hr, +figure, +p, +pre { + margin: 0; +} + +fieldset { + margin: 0; + padding: 0; +} + +legend { + padding: 0; +} + +ol, +ul, +menu { + list-style: none; + margin: 0; + padding: 0; +} + +/* +Reset default styling for dialogs. +*/ + +dialog { + padding: 0; +} + +/* +Prevent resizing textareas horizontally by default. +*/ + +textarea { + resize: vertical; +} + +/* +1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) +2. Set the default placeholder color to the user's configured gray 400 color. +*/ + +input::-moz-placeholder, textarea::-moz-placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +input::placeholder, +textarea::placeholder { + opacity: 1; + /* 1 */ + color: #9ca3af; + /* 2 */ +} + +/* +Set the default cursor for buttons. +*/ + +button, +[role="button"] { + cursor: pointer; +} + +/* +Make sure disabled buttons don't get the pointer cursor. +*/ + +:disabled { + cursor: default; +} + +/* +1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) +2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) + This can trigger a poorly considered lint error in some tools but is included by design. +*/ + +img, +svg, +video, +canvas, +audio, +iframe, +embed, +object { + display: block; + /* 1 */ + vertical-align: middle; + /* 2 */ +} + +/* +Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) +*/ + +img, +video { + max-width: 100%; + height: auto; +} + +/* Make elements with the HTML hidden attribute stay hidden by default */ + +[hidden] { + display: none; +} + +*, ::before, ::after { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +::backdrop { + --tw-border-spacing-x: 0; + --tw-border-spacing-y: 0; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-rotate: 0; + --tw-skew-x: 0; + --tw-skew-y: 0; + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-pan-x: ; + --tw-pan-y: ; + --tw-pinch-zoom: ; + --tw-scroll-snap-strictness: proximity; + --tw-gradient-from-position: ; + --tw-gradient-via-position: ; + --tw-gradient-to-position: ; + --tw-ordinal: ; + --tw-slashed-zero: ; + --tw-numeric-figure: ; + --tw-numeric-spacing: ; + --tw-numeric-fraction: ; + --tw-ring-inset: ; + --tw-ring-offset-width: 0px; + --tw-ring-offset-color: #fff; + --tw-ring-color: rgb(59 130 246 / 0.5); + --tw-ring-offset-shadow: 0 0 #0000; + --tw-ring-shadow: 0 0 #0000; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + --tw-blur: ; + --tw-brightness: ; + --tw-contrast: ; + --tw-grayscale: ; + --tw-hue-rotate: ; + --tw-invert: ; + --tw-saturate: ; + --tw-sepia: ; + --tw-drop-shadow: ; + --tw-backdrop-blur: ; + --tw-backdrop-brightness: ; + --tw-backdrop-contrast: ; + --tw-backdrop-grayscale: ; + --tw-backdrop-hue-rotate: ; + --tw-backdrop-invert: ; + --tw-backdrop-opacity: ; + --tw-backdrop-saturate: ; + --tw-backdrop-sepia: ; +} + +.container { + width: 100%; +} + +@media (min-width: 640px) { + .container { + max-width: 640px; + } +} + +@media (min-width: 768px) { + .container { + max-width: 768px; + } +} + +@media (min-width: 1024px) { + .container { + max-width: 1024px; + } +} + +@media (min-width: 1280px) { + .container { + max-width: 1280px; + } +} + +@media (min-width: 1536px) { + .container { + max-width: 1536px; + } +} + diff --git a/view/_footer.gohtml b/view/_footer.gohtml new file mode 100644 index 0000000..e21410f --- /dev/null +++ b/view/_footer.gohtml @@ -0,0 +1,6 @@ +{{ define "footer" }} +
This will contain all the graphs, storage pool lists, vm lists, etc for the host selected.
+{{ end }} diff --git a/view/pages.go b/view/pages.go new file mode 100644 index 0000000..f18ee03 --- /dev/null +++ b/view/pages.go @@ -0,0 +1,43 @@ +package view + +import ( + "log" + "os" +) + +// Regular pages are defined as views here, like the homepage + +// Major components of each page. +const ( + index = `view/_index.gohtml` + header = `view/_header.gohtml` + footer = `view/_footer.gohtml` +) + +// These constitute the static parts of the site that don't need to change, loaded as a template for rendering +const ( + home = `view/static/home.gohtml` +) + +func init() { + if fi, err := os.Stat(index); err != nil { + log.Fatal(fi.Name(), fi.IsDir(), err) + } + if fi, err := os.Stat(header); err != nil { + log.Fatal(fi.Name(), fi.IsDir(), err) + } + if fi, err := os.Stat(footer); err != nil { + log.Fatal(fi.Name(), fi.IsDir(), err) + } + if fi, err := os.Stat(home); err != nil { + log.Fatal(fi.Name(), fi.IsDir(), err) + }} + +var ViewHome *View + +func init() { + log.Println("Initializing homepage") + var err error + ViewHome, err = NewFromFile(home) + log.Println(err) +} diff --git a/view/static/home.gohtml b/view/static/home.gohtml new file mode 100644 index 0000000..e1dd37d --- /dev/null +++ b/view/static/home.gohtml @@ -0,0 +1,87 @@ +{{ define "content" }} ++ Clustvirt (work in progress name) aims to be the agnostic cluster controller for libvirtd. + The server component is used to display both the WebUI and run the REST API used to control one to many + libvirtd hosts to manage virual machines, LXC containers (through libvirtd), gather information about + each host, and monitor each host. +
++ The aims of this project are: +
+ What this project does not, but may someday do (future goals): +
+ What this project will NEVER do, even if asked really nicely: +
+ Why does this even exist? +
I recently created a post on reddit announcing that I was building this, + and while the majority of responses were supportive, even offering features that may enhance what I originally + set out to do, many responded with "Why do we need another one??"
+Besides the list above about why this exists, I wanted to clarify a few things those individuals did not seeem to + get: This is not a rebuild of Proxmox, Cloudstack, VMWare, Harvester or any of the other "Hyper-converged + Infrastructer Operating System" offerings out there. This will not take over your base operating system machine, just + act as a cluster manager and interface to access the existing libvirtd instances on those machines, nor will it + prescribe a set of requirements that make it hard to move your own infrastructure around.
+At the heart of this project is that I hate the enshitifiation of Open Source that has been going on, where its + just another way to make money and control the eco system. RedHat tried to do it by locking down their source code, + Proxmox does it by making sure anything you do on Proxmox is tied to Proxmox (no offense to Proxmox), and even + Hashicorp, who I loved so dearly, changed from a pure Open Source licensing model to one that protects the business + over the community.
+I will not let that happen here
+This project will seek to use the Unix philosophy, of building off of existing standards, combining tools, and + having one tool do one job well. This does not mean there will be one application for each aspect of the job, but + that this application stack will manage Libvirtd well, and have individual and configurable paths to manage each + sub aspect of the libvirt stack. This stack will not create a Ceph cluster for you, it leaves you to do that. It + will not even talk to a ceph cluster. It will, however, let you add that cluster via configuration options to define + it as a storage pool that libvirt can use.
+If you want something that will allow you to use a single interface to create all sub aspects that can be used by + libvirt (managing all firewall rules, creating a ceph cluster, etc.), use something like Proxmox which includes + that builtin functionality. This isn't the stack for you.
+{{ end }} diff --git a/view/view.go b/view/view.go new file mode 100644 index 0000000..7b3023c --- /dev/null +++ b/view/view.go @@ -0,0 +1,55 @@ +// Package view handles WebUI generation for clustvirt. The methods and utilties in this module control what is viewed, +// templates that are loaded, and building those templates. Caching is not considered beyond what is done +// automattically by go (if anything). +package view + +import ( + "html/template" + "io" + "log" + "os" +) + +// View is responsible for assembling a group of templates, providing +// methods to add data and compose pages in a common way. +type View struct { + content string + template *template.Template +} + +var basetemplate *template.Template + +// New returns a new instance of the View, expecting the content to be the actual +// content as a template defining "content", in string format. +func New(content string) *View { + if basetemplate == nil { + log.Println("Initializing base template") + basetemplate = template.Must(template.New("").ParseFiles(index, header, footer)) + } + log.Println("Cloning base template") + v := &View{template: template.Must(basetemplate.Clone())} + v.parse(content) + + return v +} + +// NewFromFile loads a template from a file +func NewFromFile(file string) (*View, error) { + b, err := os.ReadFile(file) + return New(string(b)), err +} + +func (v *View) parse(tmpl string) error { + log.Println("Parsing template contents") + if _, err := v.template.Parse(tmpl); err != nil { + return err + } + log.Println("Template parsed") + return nil +} + +// Render returns the executed template with data +func (v *View) Render(w io.Writer, data any) error { + log.Println("Excuting template") + return v.template.ExecuteTemplate(w, "_index", data) +}