Compare commits
30 Commits
fb02d525d2
...
e7ed6cb2fd
Author | SHA1 | Date | |
---|---|---|---|
e7ed6cb2fd | |||
|
f616c4ccf9 | ||
|
1a1713eb14 | ||
|
b07a74543b | ||
|
ab4bfd077b | ||
|
9c152943cd | ||
|
8ecde237b1 | ||
|
54a16bd410 | ||
|
c72f903e31 | ||
|
f6fdd83a23 | ||
|
ca3d8177bd | ||
|
f51d7f4f51 | ||
|
da53fcf8b5 | ||
|
4a0f110663 | ||
|
f4487acb47 | ||
|
88220d47e6 | ||
|
138786855f | ||
|
2afd699099 | ||
|
b4d27e0867 | ||
|
24a5e6676f | ||
|
e07c1c4f3f | ||
|
db05789cfe | ||
|
c372d43104 | ||
|
aa8d20370b | ||
|
89ae6ea612 | ||
|
f94f9b9f71 | ||
|
0e0e5db303 | ||
|
3c785dec8b | ||
|
7e8a34a435 | ||
|
35e234533c |
165
addr.go
Normal file
165
addr.go
Normal file
@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Addr string
|
||||
|
||||
var (
|
||||
reBox = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]+$`)
|
||||
rePath = regexp.MustCompile(`^(/){0,1}[a-zA-Z0-9\-_\.]+(/[a-zA-Z0-9\-_\.]+)+$`)
|
||||
)
|
||||
|
||||
func (a Addr) Box() string {
|
||||
s := strings.Split(string(a), `:`)
|
||||
box := s[0]
|
||||
if reBox.MatchString(box) {
|
||||
return box
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) Path() string {
|
||||
s := strings.Split(string(a), `:`)
|
||||
path := s[1]
|
||||
if rePath.MatchString(path) {
|
||||
return path
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) Append(path string) Addr {
|
||||
newPath := a.Path() + path
|
||||
if rePath.MatchString(newPath) {
|
||||
return Addr(a.Box() + ":" + newPath)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) BoxExec(cmd string) (string, error) {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
err := errors.New("box doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
|
||||
return "", err
|
||||
} else {
|
||||
return b.Exec(cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) Exec() (string, error) {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
return a.BoxExec(a.Path())
|
||||
}
|
||||
|
||||
func (a Addr) ValidSnapshots() ([]*ZfsSnapshot, error) {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
err := errors.New("box doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
if fs, ok := b.zfs.filesystems[a.Path()]; ok {
|
||||
return fs.ValidSnapshots(), nil
|
||||
} else {
|
||||
err := errors.New("path doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) SetManaged(val bool) error {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
err := errors.New("box doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
|
||||
return err
|
||||
} else if fs, ok := b.zfs.filesystems[a.Path()]; !ok {
|
||||
err := errors.New("path doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
|
||||
return err
|
||||
} else {
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
if fs.managed != val {
|
||||
var cmd string
|
||||
if val {
|
||||
cmd = fmt.Sprintf("zfs set %s=+ %s", zfsManagedPropertyName, a.Path())
|
||||
} else {
|
||||
cmd = fmt.Sprintf("zfs set %s=- %s", zfsManagedPropertyName, a.Path())
|
||||
}
|
||||
if _, err := b.Exec(cmd); err != nil {
|
||||
log.WithFields(log.Fields{"addr": a, "call": "Exec", "attr": cmd, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
}
|
||||
fs.managed = val
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) SetBackedUp(val bool) error {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
err := errors.New("box doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
|
||||
return err
|
||||
} else if fs, ok := b.zfs.filesystems[a.Path()]; !ok {
|
||||
err := errors.New("path doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
|
||||
return err
|
||||
} else {
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
fs.backedUp = val
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) Mkdir() error {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
err := errors.New("box doesn't exist")
|
||||
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
|
||||
return err
|
||||
} else {
|
||||
return b.zfs.Mkdir(a.Path())
|
||||
}
|
||||
}
|
||||
|
||||
func (a Addr) String() string {
|
||||
return string(a)
|
||||
}
|
||||
|
||||
func (a Addr) Online() bool {
|
||||
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
|
||||
|
||||
if b, ok := cfg.box[a.Box()]; !ok {
|
||||
return false
|
||||
} else {
|
||||
return b.online
|
||||
}
|
||||
}
|
162
admin.go
Normal file
162
admin.go
Normal file
@ -0,0 +1,162 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron/v3"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AdminConfig struct {
|
||||
Users []*User `json:"users"`
|
||||
Secrets *SecretsConfig `json:"secrets"`
|
||||
Addr string `json:"addr"`
|
||||
}
|
||||
|
||||
type SecretsConfig struct {
|
||||
PasswordPepper string `json:"password_pepper"`
|
||||
ContextKey string `json:"context_key"`
|
||||
ContextExpiration int `json:"context_expiration"`
|
||||
ScryptN int `json:"scrypt_n"`
|
||||
ScryptR int `json:"scrypt_r"`
|
||||
ScryptP int `json:"scrypt_p"`
|
||||
}
|
||||
|
||||
//go:embed assets
|
||||
var assets embed.FS
|
||||
|
||||
func NewAdmin() *AdminConfig {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
a := &AdminConfig{
|
||||
Addr: "0.0.0.0:8080",
|
||||
Secrets: NewSecrets(),
|
||||
Users: make([]*User, 0),
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func NewSecrets() *SecretsConfig {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
pepper, _ := password.Generate(20, 5, 0, false, false)
|
||||
ctx, _ := password.Generate(20, 5, 0, false, false)
|
||||
|
||||
return &SecretsConfig{
|
||||
PasswordPepper: pepper,
|
||||
ContextKey: ctx,
|
||||
ContextExpiration: 3600,
|
||||
ScryptN: 32768,
|
||||
ScryptR: 8,
|
||||
ScryptP: 1,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AdminConfig) NewAdminUser() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
p, _ := password.Generate(20, 5, 0, false, false)
|
||||
u, _ := NewUser("admin", p)
|
||||
|
||||
a.Users = append(a.Users, u)
|
||||
|
||||
log.WithFields(log.Fields{}).Warnf("Admin user password : %s", p)
|
||||
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
func (a *AdminConfig) Run() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
// Create context that listens for the interrupt signal from the OS.
|
||||
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
if !*debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
|
||||
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil {
|
||||
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("")
|
||||
return
|
||||
} else {
|
||||
r.SetHTMLTemplate(t)
|
||||
}
|
||||
|
||||
r.GET("/", HttpAnyIndex)
|
||||
r.POST("/", HttpAnyIndex)
|
||||
|
||||
r.GET("/ping", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "pong",
|
||||
})
|
||||
})
|
||||
|
||||
r.GET("/run", func(c *gin.Context) {
|
||||
cfg.Run()
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "done",
|
||||
})
|
||||
})
|
||||
|
||||
fsys, _ := fs.Sub(assets, "assets/static")
|
||||
r.StaticFS("/assets", http.FS(fsys))
|
||||
|
||||
protected := r.Group("p", HttpAuth())
|
||||
protected.GET("test", HttpAnyHome)
|
||||
|
||||
unprotected := r.Group("u", HttpNoAuth())
|
||||
unprotected.GET("signin", HttpAnySignIn)
|
||||
unprotected.POST("signin", HttpAnySignIn)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: a.Addr,
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.WithFields(log.Fields{"call": "http.ListenAndServe", "attr": a.Addr, "error": err}).Errorf("")
|
||||
}
|
||||
}()
|
||||
|
||||
c := cron.New(cron.WithLocation(time.UTC))
|
||||
if _, err := c.AddFunc("0 * * * *", func() { cfg.Run() }); err != nil {
|
||||
log.WithFields(log.Fields{"call": "cron.AddFunc", "error": err}).Errorf("")
|
||||
}
|
||||
c.Start()
|
||||
log.WithFields(log.Fields{"call": "cron.Start"}).Debugf("cron started")
|
||||
|
||||
// Listen for the interrupt signal.
|
||||
<-ctx.Done()
|
||||
|
||||
// Restore default behavior on the interrupt signal and notify user of shutdown.
|
||||
stop()
|
||||
log.WithFields(log.Fields{"call": "stop"}).Warnf("shutting down")
|
||||
|
||||
// The context is used to inform the server it has 5 seconds to finish
|
||||
// the request it is currently handling
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.WithFields(log.Fields{"call": "http.Shutdown", "error": err}).Errorf("shutting down")
|
||||
}
|
||||
|
||||
}
|
18
assets/backup.sample.json
Normal file
18
assets/backup.sample.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"schedule":{
|
||||
"hourly":"25h",
|
||||
"daily":"1m",
|
||||
"weekly":"3m",
|
||||
"monthly":"13m"
|
||||
},
|
||||
"box":{},
|
||||
"email":{
|
||||
"active":false
|
||||
},
|
||||
"apps":[],
|
||||
"timezone":"Etc/UTC",
|
||||
"admin":{
|
||||
"addr":":8080"
|
||||
},
|
||||
"debug":true
|
||||
}
|
5002
assets/static/css/bootstrap-grid.css
vendored
Normal file
5002
assets/static/css/bootstrap-grid.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap-grid.css.map
Normal file
1
assets/static/css/bootstrap-grid.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.min.css
vendored
Normal file
7
assets/static/css/bootstrap-grid.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap-grid.min.css.map
Normal file
1
assets/static/css/bootstrap-grid.min.css.map
Normal file
File diff suppressed because one or more lines are too long
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
Normal file
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap-grid.rtl.css.map
Normal file
1
assets/static/css/bootstrap-grid.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap-grid.rtl.min.css.map
Normal file
1
assets/static/css/bootstrap-grid.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
426
assets/static/css/bootstrap-reboot.css
vendored
Normal file
426
assets/static/css/bootstrap-reboot.css
vendored
Normal file
@ -0,0 +1,426 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr /* rtl:ignore */;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: left;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
/* rtl:raw:
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=bootstrap-reboot.css.map */
|
1
assets/static/css/bootstrap-reboot.css.map
Normal file
1
assets/static/css/bootstrap-reboot.css.map
Normal file
File diff suppressed because one or more lines are too long
8
assets/static/css/bootstrap-reboot.min.css
vendored
Normal file
8
assets/static/css/bootstrap-reboot.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.min.css.map */
|
1
assets/static/css/bootstrap-reboot.min.css.map
Normal file
1
assets/static/css/bootstrap-reboot.min.css.map
Normal file
File diff suppressed because one or more lines are too long
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
Normal file
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
Normal file
@ -0,0 +1,423 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:root {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
color: #212529;
|
||||
background-color: #fff;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 1rem 0;
|
||||
color: inherit;
|
||||
background-color: currentColor;
|
||||
border: 0;
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
hr:not([size]) {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
h6, h5, h4, h3, h2, h1 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: calc(1.375rem + 1.5vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: calc(1.325rem + 0.9vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: calc(1.3rem + 0.6vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h3 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
abbr[title],
|
||||
abbr[data-bs-original-title] {
|
||||
-webkit-text-decoration: underline dotted;
|
||||
text-decoration: underline dotted;
|
||||
cursor: help;
|
||||
-webkit-text-decoration-skip-ink: none;
|
||||
text-decoration-skip-ink: none;
|
||||
}
|
||||
|
||||
address {
|
||||
margin-bottom: 1rem;
|
||||
font-style: normal;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-right: 2rem;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul,
|
||||
dl {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol ol,
|
||||
ul ul,
|
||||
ol ul,
|
||||
ul ol {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
dt {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 0.5rem;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.2em;
|
||||
background-color: #fcf8e3;
|
||||
}
|
||||
|
||||
sub,
|
||||
sup {
|
||||
position: relative;
|
||||
font-size: 0.75em;
|
||||
line-height: 0;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
a:hover {
|
||||
color: #0a58ca;
|
||||
}
|
||||
|
||||
a:not([href]):not([class]), a:not([href]):not([class]):hover {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
pre,
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 1em;
|
||||
direction: ltr ;
|
||||
unicode-bidi: bidi-override;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
overflow: auto;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
pre code {
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
word-break: normal;
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 0.875em;
|
||||
color: #d63384;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
a > code {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
kbd {
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.875em;
|
||||
color: #fff;
|
||||
background-color: #212529;
|
||||
border-radius: 0.2rem;
|
||||
}
|
||||
kbd kbd {
|
||||
padding: 0;
|
||||
font-size: 1em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
figure {
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
img,
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
table {
|
||||
caption-side: bottom;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
caption {
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 0.5rem;
|
||||
color: #6c757d;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: inherit;
|
||||
text-align: -webkit-match-parent;
|
||||
}
|
||||
|
||||
thead,
|
||||
tbody,
|
||||
tfoot,
|
||||
tr,
|
||||
td,
|
||||
th {
|
||||
border-color: inherit;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
button:focus:not(:focus-visible) {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input,
|
||||
button,
|
||||
select,
|
||||
optgroup,
|
||||
textarea {
|
||||
margin: 0;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
[role=button] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
select {
|
||||
word-wrap: normal;
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
[list]::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
button,
|
||||
[type=button],
|
||||
[type=reset],
|
||||
[type=submit] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
button:not(:disabled),
|
||||
[type=button]:not(:disabled),
|
||||
[type=reset]:not(:disabled),
|
||||
[type=submit]:not(:disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
::-moz-focus-inner {
|
||||
padding: 0;
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
legend {
|
||||
float: right;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 0.5rem;
|
||||
font-size: calc(1.275rem + 0.3vw);
|
||||
line-height: inherit;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
legend {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
legend + * {
|
||||
clear: right;
|
||||
}
|
||||
|
||||
::-webkit-datetime-edit-fields-wrapper,
|
||||
::-webkit-datetime-edit-text,
|
||||
::-webkit-datetime-edit-minute,
|
||||
::-webkit-datetime-edit-hour-field,
|
||||
::-webkit-datetime-edit-day-field,
|
||||
::-webkit-datetime-edit-month-field,
|
||||
::-webkit-datetime-edit-year-field {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::-webkit-inner-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
[type=search] {
|
||||
outline-offset: -2px;
|
||||
-webkit-appearance: textfield;
|
||||
}
|
||||
|
||||
[type="tel"],
|
||||
[type="url"],
|
||||
[type="email"],
|
||||
[type="number"] {
|
||||
direction: ltr;
|
||||
}
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
::file-selector-button {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
font: inherit;
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
output {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
iframe {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */
|
1
assets/static/css/bootstrap-reboot.rtl.css.map
Normal file
1
assets/static/css/bootstrap-reboot.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
8
assets/static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
8
assets/static/css/bootstrap-reboot.rtl.min.css
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
|
||||
* Copyright 2011-2021 The Bootstrap Authors
|
||||
* Copyright 2011-2021 Twitter, Inc.
|
||||
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
|
||||
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
|
||||
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
|
||||
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
|
1
assets/static/css/bootstrap-reboot.rtl.min.css.map
Normal file
1
assets/static/css/bootstrap-reboot.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4752
assets/static/css/bootstrap-utilities.css
vendored
Normal file
4752
assets/static/css/bootstrap-utilities.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap-utilities.css.map
Normal file
1
assets/static/css/bootstrap-utilities.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-utilities.min.css
vendored
Normal file
7
assets/static/css/bootstrap-utilities.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap-utilities.min.css.map
Normal file
1
assets/static/css/bootstrap-utilities.min.css.map
Normal file
File diff suppressed because one or more lines are too long
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
Normal file
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap-utilities.rtl.css.map
Normal file
1
assets/static/css/bootstrap-utilities.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
7
assets/static/css/bootstrap-utilities.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap-utilities.rtl.min.css.map
Normal file
1
assets/static/css/bootstrap-utilities.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
10837
assets/static/css/bootstrap.css
vendored
Normal file
10837
assets/static/css/bootstrap.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap.css.map
Normal file
1
assets/static/css/bootstrap.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.min.css
vendored
Normal file
7
assets/static/css/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap.min.css.map
Normal file
1
assets/static/css/bootstrap.min.css.map
Normal file
File diff suppressed because one or more lines are too long
10813
assets/static/css/bootstrap.rtl.css
vendored
Normal file
10813
assets/static/css/bootstrap.rtl.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/css/bootstrap.rtl.css.map
Normal file
1
assets/static/css/bootstrap.rtl.css.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.rtl.min.css
vendored
Normal file
7
assets/static/css/bootstrap.rtl.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/css/bootstrap.rtl.min.css.map
Normal file
1
assets/static/css/bootstrap.rtl.min.css.map
Normal file
File diff suppressed because one or more lines are too long
6748
assets/static/js/bootstrap.bundle.js
vendored
Normal file
6748
assets/static/js/bootstrap.bundle.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/js/bootstrap.bundle.js.map
Normal file
1
assets/static/js/bootstrap.bundle.js.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.bundle.min.js
vendored
Normal file
7
assets/static/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/js/bootstrap.bundle.min.js.map
Normal file
1
assets/static/js/bootstrap.bundle.min.js.map
Normal file
File diff suppressed because one or more lines are too long
4967
assets/static/js/bootstrap.esm.js
vendored
Normal file
4967
assets/static/js/bootstrap.esm.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/js/bootstrap.esm.js.map
Normal file
1
assets/static/js/bootstrap.esm.js.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.esm.min.js
vendored
Normal file
7
assets/static/js/bootstrap.esm.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/js/bootstrap.esm.min.js.map
Normal file
1
assets/static/js/bootstrap.esm.min.js.map
Normal file
File diff suppressed because one or more lines are too long
5016
assets/static/js/bootstrap.js
vendored
Normal file
5016
assets/static/js/bootstrap.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
assets/static/js/bootstrap.js.map
Normal file
1
assets/static/js/bootstrap.js.map
Normal file
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.min.js
vendored
Normal file
7
assets/static/js/bootstrap.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
assets/static/js/bootstrap.min.js.map
Normal file
1
assets/static/js/bootstrap.min.js.map
Normal file
File diff suppressed because one or more lines are too long
29
assets/templates/page-signin.html
Normal file
29
assets/templates/page-signin.html
Normal file
@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<title>Login :: zBackup</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<form class="form-signin" method="POST" action="/u/submit">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
|
||||
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
<div class="form-group">
|
||||
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="pwd" placeholder="Password" name="password" required>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
|
||||
<p class="mt-5 mb-3 text-muted">© 2023</p>
|
||||
</form>
|
||||
<a href="/u/recover">Lost password</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
120
backup.go
120
backup.go
@ -3,22 +3,18 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
appFlag = flag.String("app", "", "run specific app")
|
||||
cfgFile = flag.String("config", "config.json", "config file")
|
||||
schedFlag = flag.String("schedule", "", "specific schedule")
|
||||
slowFlag = flag.Bool("slow", false, "slow process")
|
||||
testFlag = flag.Bool("test", false, "test run")
|
||||
debugFlag = flag.Bool("debug", false, "debug")
|
||||
testMailFlag = flag.Bool("test-mail", false, "test email setup")
|
||||
stopOnErrorFlag = flag.Bool("stop-on-error", false, "stop processing on error")
|
||||
cfg Config
|
||||
email *Email
|
||||
cfgFile = flag.String("config", "", "config file")
|
||||
isDaemon = flag.Bool("daemon", false, "run as daemon")
|
||||
debug = flag.Bool("debug", false, "log debug messages")
|
||||
quiet = flag.Bool("quiet", false, "remove most log messages")
|
||||
logFile = flag.String("logfile", "", "log file")
|
||||
cfg *Config
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -26,70 +22,50 @@ func main() {
|
||||
|
||||
fmt.Printf("backup (%s)\n", version)
|
||||
|
||||
email = new(Email)
|
||||
email.startTime = time.Now()
|
||||
email.items = make([]string, 0)
|
||||
if *debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
if *quiet {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
if *logFile != "" {
|
||||
if f, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil {
|
||||
log.Printf("Cannot open logfile (%s)", err)
|
||||
} else {
|
||||
log.SetOutput(f)
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
err := cfg.Load()
|
||||
if err != nil {
|
||||
log.Printf("Cannot load config (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.SetReportCaller(true)
|
||||
|
||||
if *cfgFile != "" {
|
||||
if c, err := LoadConfigFile(*cfgFile); err != nil {
|
||||
log.Printf("Cannot load config (%s)", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
cfg = c
|
||||
}
|
||||
} else if c, err := LoadConfigFile("backup.json"); err == nil {
|
||||
cfg = c
|
||||
} else {
|
||||
cfg, _ = LoadConfigByte(sampleCfg)
|
||||
}
|
||||
|
||||
if *testMailFlag {
|
||||
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "test backup email topic", "test backup email body", cfg.Email.ToEmail)
|
||||
if *isDaemon {
|
||||
if cfg.Admin == nil {
|
||||
cfg.Admin = NewAdmin()
|
||||
}
|
||||
if cfg.Admin.Secrets == nil {
|
||||
cfg.Admin.Secrets = NewSecrets()
|
||||
}
|
||||
if len(cfg.Admin.Users) == 0 {
|
||||
cfg.Admin.NewAdminUser()
|
||||
}
|
||||
cfg.Admin.Run()
|
||||
} else {
|
||||
cfg.Run()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err = RunBackup(*appFlag, *stopOnErrorFlag)
|
||||
if err != nil {
|
||||
log.Printf("Cannot run schedule (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(email.items) > 0 {
|
||||
body := " - " + email.items[0]
|
||||
for _, v := range email.items[1:] {
|
||||
body = body + "\r\n" + " - " + v
|
||||
}
|
||||
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "Autobackup report", body, cfg.Email.ToEmail)
|
||||
log.Printf("Sending summary email\r\n%v", email.items)
|
||||
}
|
||||
}
|
||||
|
||||
//RunBackup run all backup targets where schedule is registered
|
||||
func RunBackup(app string, stopOnError bool) error {
|
||||
if app == "" {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Start")
|
||||
}
|
||||
for _, a := range cfg.Apps {
|
||||
err := a.RunAppBackup()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Error running %s", a.Name)
|
||||
}
|
||||
if stopOnError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Start %s", app)
|
||||
}
|
||||
for _, a := range cfg.Apps {
|
||||
if a.Name == app {
|
||||
err := a.RunAppBackup()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Error running %s", a.Name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
545
box.go
545
box.go
@ -1,423 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"errors"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/silenceper/pool"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Box struct {
|
||||
Addr string `json:"addr"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"-"`
|
||||
ssh *SSHConfig
|
||||
zfs *ZFSConfig
|
||||
online bool
|
||||
name string
|
||||
addr string
|
||||
user string
|
||||
key string
|
||||
zfs *BoxZfs
|
||||
sshPool pool.Pool
|
||||
created bool
|
||||
online bool
|
||||
allowDirectConnect bool
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSTakeSnapshot : %s : Taking snapshot on %s for %s", b.Name, path, schedule)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
|
||||
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
|
||||
_, err = b.ssh.exec("zfs snapshot " + path + "@" + name)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.SnapshotAdded = true
|
||||
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(path+"@"+name))
|
||||
|
||||
return
|
||||
type BoxSshPool struct {
|
||||
signer ssh.Signer
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
logged bool
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetLastSnapshot(path string) (last Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetLastSnapshot : %s : Start %s (%d snapshots)", b.Name, path, len(b.zfs.SnapshotList))
|
||||
func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err error) {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
|
||||
|
||||
re := regexp.MustCompile(boxNamePattern)
|
||||
if !re.MatchString(name) {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
p, err := NewSshPool(name, addr, user, key)
|
||||
if err != nil {
|
||||
return last, err
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
b = &Box{
|
||||
name: name,
|
||||
addr: addr,
|
||||
user: user,
|
||||
key: key,
|
||||
zfs: &BoxZfs{
|
||||
online: false,
|
||||
},
|
||||
sshPool: p,
|
||||
online: false,
|
||||
created: true,
|
||||
allowDirectConnect: true, //FIXME use direct
|
||||
}
|
||||
|
||||
for _, v := range b.zfs.SnapshotList {
|
||||
if v.Path() == path {
|
||||
last = v
|
||||
}
|
||||
}
|
||||
if len(string(last)) == 0 {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
}
|
||||
return
|
||||
b.zfs.box = b
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src))
|
||||
func (b *Box) Open() error {
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
|
||||
|
||||
b.mx.Lock()
|
||||
defer b.mx.Unlock()
|
||||
|
||||
if b.online {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostname, err := b.Exec("hostname")
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "Exec", "attr": "hostname", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("hostname : %s", hostname)
|
||||
|
||||
b.online = true
|
||||
|
||||
if err := b.zfs.Open(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Open", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) Close() error {
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
|
||||
|
||||
b.mx.Lock()
|
||||
defer b.mx.Unlock()
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
if err := b.zfs.Close(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Close", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = b.ZFSGetNextSnapshot(src)
|
||||
b.online = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) Exec(cmd string) (r string, err error) {
|
||||
log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("done")
|
||||
|
||||
if !b.created {
|
||||
err := errors.New("box not initialized")
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
v, err := b.sshPool.Get()
|
||||
if err != nil {
|
||||
if err.Error() == "no snapshot" {
|
||||
is = true
|
||||
err = nil
|
||||
}
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err, "call": "SshPool.Get"}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer b.sshPool.Put(v)
|
||||
s := v.(*Ssh)
|
||||
|
||||
return s.Exec(cmd)
|
||||
}
|
||||
|
||||
func TransferZfs(from, to Addr) error {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
|
||||
|
||||
var (
|
||||
err error
|
||||
fromSnapshots, toSnapshots []*ZfsSnapshot
|
||||
directTransfer bool
|
||||
)
|
||||
|
||||
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
|
||||
directTransfer = true
|
||||
} else {
|
||||
is = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetFirstSnapshot(path string) (first Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetFirstSnapshot : %s : Start %s", b.Name, path)
|
||||
directTransfer = false
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
for _, v := range b.zfs.SnapshotList {
|
||||
if v.Path() == path {
|
||||
first = v
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetNextSnapshot(src Snapshot) (next Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetNextSnapshot : %s : Start %s", b.Name, string(src))
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
for id, v := range b.zfs.SnapshotList {
|
||||
if v == src {
|
||||
if len(b.zfs.SnapshotList) > id+1 {
|
||||
next = b.zfs.SnapshotList[id+1]
|
||||
if next.Path() == src.Path() {
|
||||
return
|
||||
} else {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSUpdateSnapshotList() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSUpdateSnapshotList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
if b.zfs.SnapshotDeleted || b.zfs.SnapshotAdded {
|
||||
b.zfs.SnapshotInitialized = false
|
||||
}
|
||||
b.zfs.M.Unlock()
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetSnapshotList() (snaps []Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetSnapshotList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
snaps = b.zfs.SnapshotList
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) SnapshotInitialize() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
if b.zfs.SnapshotInitialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
b.zfs.SnapshotList = make([]Snapshot, 0)
|
||||
|
||||
var buf *bytes.Buffer
|
||||
buf, err = b.SSHExec("zfs list -H -t snapshot -o name")
|
||||
|
||||
csvReader := csv.NewReader(buf)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 1
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
|
||||
}
|
||||
if fromSnapshots, err = from.ValidSnapshots(); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
|
||||
if len(fromSnapshots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
|
||||
if toSnapshots, err = to.ValidSnapshots(); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": to, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
b.zfs.SnapshotInitialized = true
|
||||
b.zfs.SnapshotAdded = false
|
||||
b.zfs.SnapshotDeleted = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) ZFSUpdateList() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSUpdateList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
if b.zfs.ZFSDeleted || b.zfs.ZFSAdded {
|
||||
b.zfs.ZFSInitialized = false
|
||||
}
|
||||
b.zfs.M.Unlock()
|
||||
|
||||
err = b.ZFSInitialize()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSIsZFS(path string) bool {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSIsZFS : %s : Start %s", b.Name, path)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
return false
|
||||
}
|
||||
|
||||
err := b.ZFSInitialize()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
if _, ok := b.zfs.ZFSMap[path]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (b *Box) ZFSCreateZFS(path string) (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : %s : Start %s", b.Name, path)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.ZFSInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
p := strings.Split(path, `/`)
|
||||
var base string
|
||||
for _, d := range p {
|
||||
if base == "" {
|
||||
base = d
|
||||
if len(toSnapshots) == 0 {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
|
||||
if directTransfer {
|
||||
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}
|
||||
toSnapshots = append(toSnapshots, newToSnapshot)
|
||||
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot)
|
||||
} else {
|
||||
base = base + `/` + d
|
||||
//handle indirect transfer
|
||||
}
|
||||
if _, ok := b.zfs.ZFSMap[base]; !ok {
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : Creating %s:%s", b.Name, base)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = b.SSHExec("zfs create -o mountpoint=none " + base)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : %s : SSHExec : %s", b.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.ZFSMap[base] = "none"
|
||||
b.zfs.ZFSAdded = true
|
||||
fromFromSnapshotId := -1
|
||||
lastToSnapshot := toSnapshots[len(toSnapshots)-1]
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("searching last snapshot %s", lastToSnapshot.String())
|
||||
for id, v := range fromSnapshots {
|
||||
if v.name == lastToSnapshot.name {
|
||||
fromFromSnapshotId = id
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("found %s", v.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSInitialize() (err error) {
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
if b.zfs.ZFSInitialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
b.zfs.ZFSMap = make(map[string]string)
|
||||
|
||||
var buf *bytes.Buffer
|
||||
buf, err = b.SSHExec("zfs list -H -o name,mountpoint")
|
||||
|
||||
csvReader := csv.NewReader(buf)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 2
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
|
||||
}
|
||||
if fromFromSnapshotId == -1 {
|
||||
err := errors.New("zfs snapshot unsync")
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
b.zfs.ZFSMap[rec[0]] = rec[1]
|
||||
}
|
||||
if fromFromSnapshotId < len(fromSnapshots)-1 {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("transfering from %s to %s", fromSnapshots[fromFromSnapshotId].name, fromSnapshots[len(fromSnapshots)-1].name)
|
||||
if directTransfer {
|
||||
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send -I " + fromSnapshots[fromFromSnapshotId].String() + " " + fromSnapshots[len(fromSnapshots)-1].String() + " | zfs recv -F " + to.Path()); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// handle indirect transfer
|
||||
}
|
||||
|
||||
b.zfs.ZFSInitialized = true
|
||||
b.zfs.ZFSAdded = false
|
||||
b.zfs.ZFSDeleted = false
|
||||
for _, v := range fromSnapshots[fromFromSnapshotId+1:] {
|
||||
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(&ZfsSnapshot{name: v.name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) {
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = b.ssh.exec(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) Host() string {
|
||||
s := strings.Split(string(b.Addr), `:`)
|
||||
return s[0]
|
||||
}
|
||||
|
424
config.go
424
config.go
@ -1,227 +1,287 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Zfsnap map[string]string `json:"zfsnap"`
|
||||
Box map[string]*Box `json:"box"`
|
||||
Email EmailConfig `json:"email"`
|
||||
Apps []AppConfig `json:"apps"`
|
||||
Timezone string `json:"timezone"`
|
||||
Now time.Time `json:"-"`
|
||||
ScheduleDuration map[string]string `json:"schedule"`
|
||||
Box map[string]BoxConfig `json:"box"`
|
||||
Email EmailConfig `json:"email"`
|
||||
Apps []AppConfig `json:"apps"`
|
||||
Timezone string `json:"timezone"`
|
||||
Admin *AdminConfig `json:"admin"`
|
||||
Debug bool `json:"debug"`
|
||||
box map[string]*Box `json:"-"`
|
||||
apps map[string]*App `json:"-"`
|
||||
timezone *time.Location `json:"-"`
|
||||
}
|
||||
|
||||
//Load config from file
|
||||
func (c *Config) Load() error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.Load : Start")
|
||||
}
|
||||
b, err := ioutil.ReadFile(*cfgFile)
|
||||
var (
|
||||
cfgMx sync.Mutex
|
||||
cfgRun bool
|
||||
)
|
||||
|
||||
//go:embed assets/backup.sample.json
|
||||
var sampleCfg []byte
|
||||
|
||||
type BoxConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
AllowDirectConnect bool `json:"allow_direct_connect"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Schedule []string `json:"schedule"`
|
||||
Sources []string `json:"src"`
|
||||
Destinations []string `json:"dest"`
|
||||
Before map[string]string `json:"before"`
|
||||
After map[string]string `json:"after"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// Load config from file
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
log.WithFields(log.Fields{"path": path}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"path": path}).Debugf("done")
|
||||
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &c)
|
||||
return LoadConfigByte(b)
|
||||
|
||||
}
|
||||
|
||||
// Load config from string
|
||||
func LoadConfigByte(conf []byte) (*Config, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
c := &Config{}
|
||||
if err := json.Unmarshal(sampleCfg, c); err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := hujson.Standardize(conf)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : json.Unmarshal : %s", err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load :\r\n%v", cfg)
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := time.LoadLocation(cfg.Timezone)
|
||||
c.timezone, err = time.LoadLocation(c.Timezone)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : time.LoadLocation : %s", err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cfg.Email.SmtpHost) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no smtp")
|
||||
if c.Email.Active {
|
||||
if len(c.Email.SmtpHost) == 0 {
|
||||
err := fmt.Errorf("no smtp")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.FromEmail) == 0 {
|
||||
err := fmt.Errorf("no email from")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.ToEmail) == 0 {
|
||||
err := fmt.Errorf("no email to")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Errorf("no smtp")
|
||||
}
|
||||
|
||||
if len(cfg.Email.FromEmail) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no email from")
|
||||
for k, v := range c.ScheduleDuration {
|
||||
switch k {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
case "weekly":
|
||||
case "monthly":
|
||||
case "yearly":
|
||||
if _, err := Expiration(time.Now(), v); err != nil {
|
||||
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
err := errors.New("invalid schedule")
|
||||
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Errorf("no email from")
|
||||
}
|
||||
|
||||
if len(cfg.Email.ToEmail) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no email to")
|
||||
}
|
||||
return fmt.Errorf("no email to")
|
||||
}
|
||||
|
||||
c.Now = time.Now().In(l)
|
||||
|
||||
c.box = make(map[string]*Box)
|
||||
for k, v := range c.Box {
|
||||
v.Name = k
|
||||
v.online = false
|
||||
v.zfs = NewZFSConfig()
|
||||
s := &SSHConfig{
|
||||
logged: false,
|
||||
name: k,
|
||||
}
|
||||
v.ssh = s
|
||||
keyRaw, err := ioutil.ReadFile(v.Key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ssh.ParseRawPrivateKey(keyRaw)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.signer, err = ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.config = &ssh.ClientConfig{
|
||||
User: v.User,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(s.signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
s.client, err = ssh.Dial("tcp", v.Addr, s.config)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.Dial(%s) : %s", k, err)
|
||||
}
|
||||
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
|
||||
log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
v.online = true
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
if _, ok := c.box[k]; ok {
|
||||
err := errors.New("already exists")
|
||||
log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
|
||||
err = session.Run("TZ=\"" + cfg.Timezone + "\" zfsnap --version")
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : logged into %s : %s", k, b.String())
|
||||
}
|
||||
session.Close()
|
||||
s.logged = true
|
||||
c.box[k] = b
|
||||
}
|
||||
}
|
||||
|
||||
for _, app := range c.Apps {
|
||||
for _, src := range app.Sources {
|
||||
if !src.Valid() {
|
||||
return fmt.Errorf("Source not valid : %s", string(src))
|
||||
c.apps = make(map[string]*App)
|
||||
for _, v := range c.Apps {
|
||||
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
|
||||
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
if _, ok := c.apps[v.Name]; ok {
|
||||
err := errors.New("app already exists")
|
||||
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := cfg.Box[src.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for source : %s", string(src))
|
||||
}
|
||||
if !cfg.Box[src.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("Source box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
var allOffline bool = true
|
||||
for _, dest := range app.Destinations {
|
||||
if !dest.Valid() {
|
||||
return fmt.Errorf("Destination not valid : %s", string(dest))
|
||||
}
|
||||
if _, ok := cfg.Box[dest.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for destination : %s", string(dest))
|
||||
}
|
||||
if cfg.Box[dest.Box()].online {
|
||||
allOffline = false
|
||||
}
|
||||
}
|
||||
if allOffline {
|
||||
email.items = append(email.items, fmt.Sprintf("No online destination box for app : %s", app.Name))
|
||||
}
|
||||
|
||||
for val, before := range app.Before {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
c.apps[v.Name] = a
|
||||
for k := range a.schedule {
|
||||
if dur, ok := c.ScheduleDuration[k]; ok {
|
||||
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
|
||||
if !re.MatchString(dur) {
|
||||
err := errors.New("incorrect schedule duration")
|
||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err := errors.New("undefined schedule duration")
|
||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !before.Valid() {
|
||||
return fmt.Errorf("Before not valid : %s", string(before))
|
||||
}
|
||||
if _, ok := cfg.Box[before.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for before : %s", string(before))
|
||||
}
|
||||
if !cfg.Box[before.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("Before box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
for val, after := range app.After {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !after.Valid() {
|
||||
return fmt.Errorf("After not valid : %s", string(after))
|
||||
}
|
||||
if _, ok := cfg.Box[after.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for after : %s", string(after))
|
||||
}
|
||||
if !cfg.Box[after.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("After box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
//Close config
|
||||
func (c *Config) Close() error {
|
||||
return nil
|
||||
// Run config
|
||||
func (c *Config) Run() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if cfgRun {
|
||||
return
|
||||
}
|
||||
|
||||
cfgMx.Lock()
|
||||
defer cfgMx.Unlock()
|
||||
|
||||
cfgRun = true
|
||||
defer func() { cfgRun = false }()
|
||||
|
||||
e := NewEmail(time.Now())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Setup boxes
|
||||
for _, b := range c.box {
|
||||
wg.Add(1)
|
||||
go func(box *Box) {
|
||||
defer wg.Done()
|
||||
if err := box.Open(); err != nil {
|
||||
log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("")
|
||||
e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name))
|
||||
return
|
||||
}
|
||||
}(b)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Run each app
|
||||
for _, a := range cfg.apps {
|
||||
wg.Add(1)
|
||||
go func(app *App) {
|
||||
if sched, err := app.Run(e.startTime); err != nil {
|
||||
e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
|
||||
} else if *debug {
|
||||
if sched != "" {
|
||||
e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched))
|
||||
} else {
|
||||
e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name))
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(a)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Cleanup
|
||||
for _, a := range cfg.apps {
|
||||
for _, src := range a.sources {
|
||||
if b, ok := c.box[src.Box()]; ok {
|
||||
if fs, ok := b.zfs.filesystems[src.Path()]; ok {
|
||||
fs.srcApps = append(fs.srcApps, a)
|
||||
}
|
||||
}
|
||||
for _, dest := range a.destinations {
|
||||
if b, ok := c.box[dest.Box()]; ok {
|
||||
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
|
||||
if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
|
||||
fs.destApps = append(fs.destApps, a)
|
||||
} else {
|
||||
e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range cfg.box {
|
||||
if b.online {
|
||||
for _, fs := range b.zfs.filesystems {
|
||||
if len(fs.srcApps) > 0 && !fs.backedUp {
|
||||
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up")
|
||||
e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path))
|
||||
}
|
||||
if len(fs.destApps) == 0 && !fs.backedUp && fs.managed {
|
||||
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed")
|
||||
e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop
|
||||
for _, b := range c.box {
|
||||
if err := b.Close(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("")
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.items) > 0 {
|
||||
if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil {
|
||||
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
13
const.go
Normal file
13
const.go
Normal file
@ -0,0 +1,13 @@
|
||||
package main
|
||||
|
||||
const (
|
||||
boxNamePattern = `[a-zA-Z0-9\-_\.]`
|
||||
|
||||
zfsManagedPropertyName = "biz.siteop:managed"
|
||||
zfsSnapshotPattern = `^(?P<Schedule>hourly|daily|weekly|monthly|yearly|adhoc)\-(?P<Timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})\-\-(?P<Expiration>forever|([0-9]+(h|d|m|y))+)$`
|
||||
zfsSnapshotDatePattern = "2006-01-02_15.04.05"
|
||||
|
||||
serverAddr = ":8080"
|
||||
serverUsername = "admin"
|
||||
serverPassword = "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" //admin
|
||||
)
|
92
email.go
92
email.go
@ -2,61 +2,101 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
startTime time.Time
|
||||
items []string
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Active bool `json:"active"`
|
||||
SmtpHost string `json:"smtp"`
|
||||
FromEmail string `json:"email_from"`
|
||||
ToEmail []string `json:"email_to"`
|
||||
}
|
||||
|
||||
func SendMail(addr, from, subject, body string, to []string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : Start")
|
||||
func NewEmail(now time.Time) *Email {
|
||||
log.WithFields(log.Fields{"now": now}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"now": now}).Debugf("done")
|
||||
|
||||
return &Email{startTime: now, items: make([]string, 0)}
|
||||
}
|
||||
|
||||
func (e *Email) AddItem(item string) {
|
||||
log.WithFields(log.Fields{"item": item}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
|
||||
|
||||
e.items = append(e.items, item)
|
||||
}
|
||||
|
||||
func (e *Email) Send(addr, from string, to []string) error {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if len(e.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(e.items, func(i, j int) bool {
|
||||
return e.items[i] < e.items[j]
|
||||
})
|
||||
|
||||
body := e.items[0]
|
||||
for _, item := range e.items[1:] {
|
||||
body = body + "\r\n" + item
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Autobackup report (%s)", e.startTime)
|
||||
|
||||
if err := SendMail(addr, from, subject, body, to); err != nil {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "SendMail", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func SendMail(addr, from, subject, body string, to []string) error {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("done")
|
||||
|
||||
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : smtp.Dial (%s)", addr, err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "smtp.Dial", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if err = c.Mail(r.Replace(from)); err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : client.Mail (%s)", from, err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Mail", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range to {
|
||||
to[i] = r.Replace(to[i])
|
||||
if err = c.Rcpt(to[i]); err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : client.Rcpt (%s)", to[i], err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Rcpt", "attr": to[i], "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : client.Data (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
@ -68,29 +108,21 @@ func SendMail(addr, from, subject, body string, to []string) error {
|
||||
"Content-Transfer-Encoding: base64\r\n" +
|
||||
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail :\r\n%s", msg)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(msg))
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : writer.Write (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : writer.Close (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.Quit()
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : client.Quit (%s)", err)
|
||||
if err = c.Quit(); err != nil {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Quit", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
9
go.mod
9
go.mod
@ -3,6 +3,11 @@ module git.siteop.biz/shoopea/backup
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sethvargo/go-password v0.2.0 // indirect
|
||||
github.com/silenceper/pool v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
|
||||
golang.org/x/crypto v0.9.0
|
||||
)
|
||||
|
131
go.sum
131
go.sum
@ -1,12 +1,143 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU=
|
||||
github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
67
http.go
Normal file
67
http.go
Normal file
@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func HttpAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Search signed-in userID
|
||||
userID := 0
|
||||
if userID == 0 {
|
||||
// Return 404 and abort handlers chain.
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HttpNoAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HttpAnySignIn(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
warning, _ := c.Cookie("warning")
|
||||
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
|
||||
c.HTML(http.StatusOK, "page-signin.html", gin.H{
|
||||
"Error": warning,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HttpAnyIndex(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HttpAnyHome(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) == 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
c.HTML(http.StatusOK, "page-home.html", gin.H{})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetWebSessionUserID(c *gin.Context) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func SetCSRFToken(c *gin.Context) {
|
||||
return
|
||||
}
|
19
location.go
19
location.go
@ -1,20 +1 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
type Location string
|
||||
|
||||
func (l Location) Box() string {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return s[0]
|
||||
}
|
||||
|
||||
func (l Location) Path() string {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return s[1]
|
||||
}
|
||||
|
||||
func (l Location) Valid() bool {
|
||||
s := strings.Split(string(l), `:`)
|
||||
return len(s) == 2
|
||||
}
|
||||
|
125
snapshot.go
125
snapshot.go
@ -1,20 +1,123 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
type Snapshot string
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func (s Snapshot) Path() string {
|
||||
s2 := strings.Split(string(s), `@`)
|
||||
return s2[0]
|
||||
func SnapshotName(schedule string, now time.Time) string {
|
||||
log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("starting")
|
||||
log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("done")
|
||||
|
||||
return schedule + "-" + now.Format(zfsSnapshotDatePattern) + "--" + cfg.ScheduleDuration[schedule]
|
||||
}
|
||||
|
||||
func (s Snapshot) Name() string {
|
||||
s2 := strings.Split(string(s), `@`)
|
||||
return s2[1]
|
||||
func (s *ZfsSnapshot) Valid() bool {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
re := regexp.MustCompile(zfsSnapshotPattern)
|
||||
return re.MatchString(s.name)
|
||||
}
|
||||
|
||||
func (s Snapshot) Append(path string) Snapshot {
|
||||
s2 := strings.Split(string(s), `@`)
|
||||
return Snapshot(s2[0] + "/" + path + "@" + s2[1])
|
||||
func (s *ZfsSnapshot) Schedule() (string, error) {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
if !s.Valid() {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(zfsSnapshotPattern)
|
||||
|
||||
return re.ReplaceAllString(s.name, "${Schedule}"), nil
|
||||
|
||||
}
|
||||
|
||||
func (s *ZfsSnapshot) Expiration() (time.Time, error) {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
if !s.Valid() {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(zfsSnapshotPattern)
|
||||
|
||||
expirationString := re.ReplaceAllString(s.name, "${Expiration}")
|
||||
|
||||
timestampTime, err := s.Timestamp()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Timestamp", "error": err}).Errorf("")
|
||||
return time.Now(), err
|
||||
}
|
||||
|
||||
return Expiration(timestampTime, expirationString)
|
||||
|
||||
}
|
||||
|
||||
func (s *ZfsSnapshot) Timestamp() (time.Time, error) {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
t := time.Now()
|
||||
|
||||
if !s.Valid() {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
|
||||
return t, err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(zfsSnapshotPattern)
|
||||
|
||||
timestampString := re.ReplaceAllString(s.name, "${Timestamp}")
|
||||
timestampTime, err := time.ParseInLocation(zfsSnapshotDatePattern, timestampString, cfg.timezone)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "time.Parse", "attr": timestampString, "error": err}).Errorf("")
|
||||
return t, err
|
||||
}
|
||||
|
||||
return timestampTime, nil
|
||||
}
|
||||
|
||||
func (s *ZfsSnapshot) Expired(now time.Time) (bool, error) {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
if !s.Valid() {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
|
||||
return false, err
|
||||
}
|
||||
|
||||
expirationTime, err := s.Expiration()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Timestamp", "error": err}).Errorf("")
|
||||
return false, err
|
||||
}
|
||||
|
||||
if now.After(expirationTime) {
|
||||
return true, nil
|
||||
} else {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *ZfsSnapshot) String() string {
|
||||
return s.fs.path + "@" + s.name
|
||||
}
|
||||
|
||||
func (s *ZfsSnapshot) Delete() error {
|
||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
return s.fs.DelSnapshot(s.name)
|
||||
}
|
||||
|
179
ssh.go
179
ssh.go
@ -1,47 +1,170 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/silenceper/pool"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type SSHConfig struct {
|
||||
signer ssh.Signer
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
logged bool
|
||||
name string
|
||||
snapshot []Snapshot
|
||||
const SshDialTimeout = time.Duration(10 * time.Second)
|
||||
const SshInactivityTimeout = time.Duration(time.Minute)
|
||||
|
||||
type Ssh struct {
|
||||
name string
|
||||
signer ssh.Signer
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
session *ssh.Session
|
||||
in io.WriteCloser
|
||||
out io.Reader
|
||||
err io.Reader
|
||||
}
|
||||
|
||||
func (s *SSHConfig) exec(cmd string) (b *bytes.Buffer, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd)
|
||||
func NewSsh(name, addr, user, key string) (*Ssh, error) {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
|
||||
|
||||
s := &Ssh{
|
||||
name: name,
|
||||
}
|
||||
|
||||
k, err := os.ReadFile(key)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "os.ReadFile", "error": err}).Errorf("")
|
||||
return s, err
|
||||
}
|
||||
|
||||
parsedKey, err := ssh.ParseRawPrivateKey(k)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.ParseRawPrivateKey", "error": err}).Errorf("")
|
||||
return s, err
|
||||
}
|
||||
|
||||
s.signer, err = ssh.NewSignerFromKey(parsedKey)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.NewSignerFromKey", "error": err}).Errorf("")
|
||||
return s, err
|
||||
}
|
||||
|
||||
s.config = &ssh.ClientConfig{
|
||||
User: user,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(s.signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: SshDialTimeout,
|
||||
}
|
||||
|
||||
s.client, err = ssh.Dial("tcp", addr, s.config)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.Dial", "error": err}).Errorf("")
|
||||
return s, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Ssh) Close() error {
|
||||
log.WithFields(log.Fields{"name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": s.name}).Debugf("done")
|
||||
|
||||
return s.client.Close()
|
||||
}
|
||||
|
||||
func NewSshPool(name, addr, user, key string) (pool.Pool, error) {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
|
||||
|
||||
//factory Specify the method to create the connection
|
||||
factory := func() (interface{}, error) { return NewSsh(name, addr, user, key) }
|
||||
|
||||
// close Specify the method to close the connection
|
||||
close := func(v interface{}) error { return v.(*Ssh).Close() }
|
||||
|
||||
// Create a connection pool: Initialize the number of connections to 0, the maximum idle connection is 2, and the maximum concurrent connection is 25
|
||||
poolConfig := &pool.Config{
|
||||
InitialCap: 0,
|
||||
MaxIdle: 2,
|
||||
MaxCap: 25,
|
||||
Factory: factory,
|
||||
Close: close,
|
||||
//Ping: ping,
|
||||
//The maximum idle time of the connection, the connection exceeding this time will be closed, which can avoid the problem of automatic failure when connecting to EOF when idle
|
||||
IdleTimeout: SshInactivityTimeout,
|
||||
}
|
||||
|
||||
return pool.NewChannelPool(poolConfig)
|
||||
}
|
||||
|
||||
func (s *Ssh) Exec(cmd string) (string, error) {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("done")
|
||||
|
||||
if err := s.ExecPipe(cmd); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "ssh.ExecPipe", "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
defer s.session.Close()
|
||||
|
||||
if err := s.session.Wait(); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(s.out)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "io.ReadAll", "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(buf), nil
|
||||
|
||||
}
|
||||
|
||||
func (s *Ssh) ExecPipe(cmd string) error {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("done")
|
||||
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err)
|
||||
}
|
||||
return
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "client.NewSession", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
s.session = session
|
||||
|
||||
if s.session.Setenv("TZ", cfg.Timezone); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
b = &buf
|
||||
session.Stdout = b
|
||||
|
||||
err = session.Run("TZ=\"" + cfg.Timezone + "\" " + cmd)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.exec : session(%s).Run(%s) : %s", s.name, cmd, err)
|
||||
}
|
||||
return
|
||||
if s.in, err = s.session.StdinPipe(); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdinPipe", "error": err}).Errorf("")
|
||||
s.session.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
session.Close()
|
||||
if s.out, err = s.session.StdoutPipe(); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdoutPipe", "error": err}).Errorf("")
|
||||
s.session.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return
|
||||
if s.err, err = s.session.StderrPipe(); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StderrPipe", "error": err}).Errorf("")
|
||||
s.session.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
if err = s.session.Start(cmd); err != nil {
|
||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Start", "error": err}).Errorf("")
|
||||
s.session.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
76
user.go
Normal file
76
user.go
Normal file
@ -0,0 +1,76 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/scrypt"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
Username string `json:"username"`
|
||||
Salt string `json:"salt"`
|
||||
Passwd string `json:"passwd"`
|
||||
}
|
||||
|
||||
func NewUser(name, passwd string) (*User, error) {
|
||||
log.WithFields(log.Fields{"name": name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
|
||||
|
||||
for _, v := range cfg.Admin.Users {
|
||||
if v.Username == name {
|
||||
err := errors.New("user already exists")
|
||||
log.WithFields(log.Fields{"name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
u := &User{
|
||||
Username: name,
|
||||
}
|
||||
|
||||
salt := make([]byte, 32)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
u.Salt = hex.EncodeToString(salt)
|
||||
|
||||
if pass, err := u.HashPassword(passwd); err != nil {
|
||||
log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
u.Passwd = hex.EncodeToString(pass)
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
func (u *User) HashPassword(passwd string) ([]byte, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
//peppering the pass
|
||||
hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper))
|
||||
hash.Write([]byte(passwd))
|
||||
hashPass := hash.Sum(nil)
|
||||
|
||||
//salting the hash
|
||||
salt := make([]byte, 32)
|
||||
if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil {
|
||||
log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil {
|
||||
log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("")
|
||||
return h, err
|
||||
} else {
|
||||
return h, nil
|
||||
}
|
||||
|
||||
}
|
41
utils.go
Normal file
41
utils.go
Normal file
@ -0,0 +1,41 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Expiration(now time.Time, deadline string) (time.Time, error) {
|
||||
log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("done")
|
||||
|
||||
if deadline == "forever" {
|
||||
return time.Unix(1<<63-1, 0), nil
|
||||
}
|
||||
|
||||
reExpiration := regexp.MustCompile(`([0-9]+)([a-z]+)`)
|
||||
for _, v := range reExpiration.FindAllStringSubmatch(deadline, -1) {
|
||||
log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("duration[%d] : %v", len(v), v)
|
||||
count, _ := strconv.Atoi(v[1])
|
||||
switch v[2] {
|
||||
case "y":
|
||||
now = now.AddDate(count, 0, 0)
|
||||
case "m":
|
||||
now = now.AddDate(0, count, 0)
|
||||
case "d":
|
||||
now = now.AddDate(0, 0, count)
|
||||
case "h":
|
||||
now = now.Add(time.Duration(time.Duration(count) * time.Hour))
|
||||
default:
|
||||
err := errors.New("invalid duration")
|
||||
log.WithFields(log.Fields{"now": now, "deadline": deadline, "attr": v[2], "error": err}).Errorf("")
|
||||
return time.Now(), err
|
||||
}
|
||||
}
|
||||
|
||||
return now, nil
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||
package main
|
||||
var githash = "7f9cf49"
|
||||
var buildstamp = "2022-10-08_03:14:52"
|
||||
var commits = "54"
|
||||
var version = "7f9cf49-b54 - 2022-10-08_03:14:52"
|
||||
var githash = "1a1713e"
|
||||
var branch = "v2"
|
||||
var buildstamp = "2023-08-21_12:35:47"
|
||||
var commits = "83"
|
||||
var version = "1a1713e-b83 - 2023-08-21_12:35:47"
|
||||
|
@ -1,12 +1,14 @@
|
||||
# Get the version.
|
||||
githash=`git rev-parse --short HEAD`
|
||||
branch=`git rev-parse --abbrev-ref HEAD`
|
||||
buildstamp=`date -u '+%Y-%m-%d_%H:%M:%S'`
|
||||
commits=`git rev-list --count master`
|
||||
commits=`git rev-list --count $branch`
|
||||
# Write out the package.
|
||||
cat << EOF > version.go
|
||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||
package main
|
||||
var githash = "$githash"
|
||||
var branch = "$branch"
|
||||
var buildstamp = "$buildstamp"
|
||||
var commits = "$commits"
|
||||
var version = "$githash-b$commits - $buildstamp"
|
||||
|
342
zfs.go
342
zfs.go
@ -1,27 +1,329 @@
|
||||
package main
|
||||
|
||||
import "sync"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
type ZFSConfig struct {
|
||||
SnapshotAdded bool
|
||||
SnapshotDeleted bool
|
||||
SnapshotInitialized bool
|
||||
SnapshotList []Snapshot
|
||||
ZFSAdded bool
|
||||
ZFSDeleted bool
|
||||
ZFSInitialized bool
|
||||
ZFSMap map[string]string
|
||||
M sync.Mutex
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BoxZfs struct {
|
||||
filesystems map[string]*ZfsFs
|
||||
box *Box
|
||||
online bool
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func NewZFSConfig() (z *ZFSConfig) {
|
||||
z = &ZFSConfig{
|
||||
SnapshotAdded: false,
|
||||
SnapshotDeleted: false,
|
||||
SnapshotInitialized: false,
|
||||
ZFSAdded: false,
|
||||
ZFSDeleted: false,
|
||||
ZFSInitialized: false,
|
||||
type ZfsFs struct {
|
||||
path string
|
||||
managed bool
|
||||
backedUp bool
|
||||
zfs *BoxZfs
|
||||
snapshots map[string]*ZfsSnapshot
|
||||
srcApps []*App
|
||||
destApps []*App
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
type ZfsSnapshot struct {
|
||||
name string
|
||||
fs *ZfsFs
|
||||
}
|
||||
|
||||
func (z *BoxZfs) Open() error {
|
||||
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
|
||||
|
||||
z.mx.Lock()
|
||||
defer z.mx.Unlock()
|
||||
|
||||
if z.online {
|
||||
return nil
|
||||
}
|
||||
return
|
||||
|
||||
z.filesystems = make(map[string]*ZfsFs)
|
||||
|
||||
zfsList, err := z.box.Exec("zfs list -H -t filesystem -o name,mountpoint")
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t filesystem -o name,mountpoint", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
csvReader := csv.NewReader(bytes.NewBufferString(zfsList))
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 2
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "zfs-name": rec[0], "zfs-mount": rec[1]}).Debugf("zfs list -t filesystem")
|
||||
if rec[1] != "legacy" {
|
||||
fs := &ZfsFs{
|
||||
path: rec[0],
|
||||
zfs: z,
|
||||
snapshots: make(map[string]*ZfsSnapshot),
|
||||
}
|
||||
z.filesystems[rec[0]] = fs
|
||||
log.WithFields(log.Fields{"name": z.box.name, "fs": rec[0]}).Infof("new filesystem")
|
||||
}
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll"}).Infof("")
|
||||
|
||||
zfsList, err = z.box.Exec("zfs list -H -t snapshot -o name")
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t snapshot -o name", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 1
|
||||
|
||||
csvData, err = csvReader.ReadAll()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "zfs-snapshot": rec[0]}).Debugf("zfs list -t snapshot")
|
||||
|
||||
s := strings.Split(rec[0], `@`)
|
||||
if fs, ok := z.filesystems[s[0]]; ok {
|
||||
snap := &ZfsSnapshot{
|
||||
name: s[1],
|
||||
fs: fs,
|
||||
}
|
||||
fs.snapshots[s[1]] = snap
|
||||
log.WithFields(log.Fields{"name": z.box.name, "fs": s[0], "snapshot": s[1]}).Infof("new snapshot")
|
||||
}
|
||||
}
|
||||
|
||||
zfsList, err = z.box.Exec("zfs get -H -o name,value " + zfsManagedPropertyName)
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs get -H -o name,value,source " + zfsManagedPropertyName, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 2
|
||||
|
||||
csvData, err = csvReader.ReadAll()
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Debugf("zfs get " + zfsManagedPropertyName)
|
||||
|
||||
if fs, ok := z.filesystems[rec[0]]; ok {
|
||||
if rec[1] == "+" {
|
||||
fs.managed = true
|
||||
log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Infof("managed fs")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
z.online = true
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *BoxZfs) Close() error {
|
||||
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
|
||||
|
||||
z.mx.Lock()
|
||||
defer z.mx.Unlock()
|
||||
|
||||
for _, fs := range z.filesystems {
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
}
|
||||
|
||||
z.online = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (z *BoxZfs) Mkdir(path string) error {
|
||||
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
|
||||
|
||||
if !z.online {
|
||||
err := errors.New("zfs offline")
|
||||
log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
z.mx.Lock()
|
||||
defer z.mx.Unlock()
|
||||
|
||||
b := z.box
|
||||
if !b.online {
|
||||
err := errors.New("box offline")
|
||||
log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := z.filesystems[path]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := b.Exec(fmt.Sprintf("zfs create -p %s", path)); err != nil {
|
||||
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
newPath := ""
|
||||
for _, p := range strings.Split(path, "/") {
|
||||
if newPath == "" {
|
||||
newPath = p
|
||||
} else {
|
||||
newPath = newPath + "/" + p
|
||||
}
|
||||
|
||||
if _, ok := z.filesystems[newPath]; !ok {
|
||||
fs := &ZfsFs{
|
||||
path: newPath,
|
||||
managed: false,
|
||||
zfs: z,
|
||||
snapshots: make(map[string]*ZfsSnapshot),
|
||||
srcApps: make([]*App, 0),
|
||||
destApps: make([]*App, 0),
|
||||
}
|
||||
z.filesystems[newPath] = fs
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *ZfsFs) TakeSnapshot(name string) (*ZfsSnapshot, error) {
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")
|
||||
|
||||
if !fs.zfs.online {
|
||||
err := errors.New("zfs offline")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`^[a-zA-Z0-9\-\._]{1,255}$`)
|
||||
if !re.MatchString(name) {
|
||||
err := errors.New("unsupported name")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
|
||||
if _, ok := fs.snapshots[name]; ok {
|
||||
err := errors.New("already exists")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := fs.zfs.box.Exec("zfs snapshot " + fs.path + "@" + name); err != nil {
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := &ZfsSnapshot{
|
||||
name: name,
|
||||
fs: fs,
|
||||
}
|
||||
|
||||
fs.snapshots[name] = s
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (fs *ZfsFs) DelSnapshot(name string) error {
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")
|
||||
|
||||
if !fs.zfs.online {
|
||||
err := errors.New("zfs offline")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
|
||||
if _, ok := fs.snapshots[name]; !ok {
|
||||
err := errors.New("doesn't exist")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("zfs destroy " + fs.path + "@" + name)
|
||||
|
||||
if _, err := fs.zfs.box.Exec("zfs destroy " + fs.path + "@" + name); err != nil {
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
delete(fs.snapshots, name)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error {
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("done")
|
||||
|
||||
if !fs.zfs.online {
|
||||
err := errors.New("zfs offline")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
fs.mx.Lock()
|
||||
defer fs.mx.Unlock()
|
||||
|
||||
if _, ok := fs.snapshots[s.name]; ok {
|
||||
err := errors.New("already exist")
|
||||
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
fs.snapshots[s.name] = s
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot {
|
||||
tab := make([]*ZfsSnapshot, 0)
|
||||
|
||||
for _, s := range fs.snapshots {
|
||||
if s.Valid() {
|
||||
tab = append(tab, s)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(tab, func(i, j int) bool {
|
||||
ti, _ := tab[i].Timestamp()
|
||||
tj, _ := tab[j].Timestamp()
|
||||
return ti.Before(tj)
|
||||
})
|
||||
|
||||
return tab
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user