Compare commits
No commits in common. "e7ed6cb2fdbcf277a71c8f7e5301bac0a95ddcc6" and "fb02d525d22b132de95cda3cb6d7fce3cb3a4c28" have entirely different histories.
e7ed6cb2fd
...
fb02d525d2
165
addr.go
165
addr.go
@ -1,165 +0,0 @@
|
|||||||
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
162
admin.go
@ -1,162 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"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
5002
assets/static/css/bootstrap-grid.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.min.css
vendored
7
assets/static/css/bootstrap-grid.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
5001
assets/static/css/bootstrap-grid.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
7
assets/static/css/bootstrap-grid.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
426
assets/static/css/bootstrap-reboot.css
vendored
426
assets/static/css/bootstrap-reboot.css
vendored
@ -1,426 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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 */
|
|
File diff suppressed because one or more lines are too long
8
assets/static/css/bootstrap-reboot.min.css
vendored
8
assets/static/css/bootstrap-reboot.min.css
vendored
@ -1,8 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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 */
|
|
File diff suppressed because one or more lines are too long
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
423
assets/static/css/bootstrap-reboot.rtl.css
vendored
@ -1,423 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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 */
|
|
File diff suppressed because one or more lines are too long
@ -1,8 +0,0 @@
|
|||||||
/*!
|
|
||||||
* 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 */
|
|
File diff suppressed because one or more lines are too long
4752
assets/static/css/bootstrap-utilities.css
vendored
4752
assets/static/css/bootstrap-utilities.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
4743
assets/static/css/bootstrap-utilities.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10837
assets/static/css/bootstrap.css
vendored
10837
assets/static/css/bootstrap.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.min.css
vendored
7
assets/static/css/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
10813
assets/static/css/bootstrap.rtl.css
vendored
10813
assets/static/css/bootstrap.rtl.css
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/css/bootstrap.rtl.min.css
vendored
7
assets/static/css/bootstrap.rtl.min.css
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
6748
assets/static/js/bootstrap.bundle.js
vendored
6748
assets/static/js/bootstrap.bundle.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.bundle.min.js
vendored
7
assets/static/js/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4967
assets/static/js/bootstrap.esm.js
vendored
4967
assets/static/js/bootstrap.esm.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.esm.min.js
vendored
7
assets/static/js/bootstrap.esm.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5016
assets/static/js/bootstrap.js
vendored
5016
assets/static/js/bootstrap.js
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
7
assets/static/js/bootstrap.min.js
vendored
7
assets/static/js/bootstrap.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,29 +0,0 @@
|
|||||||
<!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,18 +3,22 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFile = flag.String("config", "", "config file")
|
appFlag = flag.String("app", "", "run specific app")
|
||||||
isDaemon = flag.Bool("daemon", false, "run as daemon")
|
cfgFile = flag.String("config", "config.json", "config file")
|
||||||
debug = flag.Bool("debug", false, "log debug messages")
|
schedFlag = flag.String("schedule", "", "specific schedule")
|
||||||
quiet = flag.Bool("quiet", false, "remove most log messages")
|
slowFlag = flag.Bool("slow", false, "slow process")
|
||||||
logFile = flag.String("logfile", "", "log file")
|
testFlag = flag.Bool("test", false, "test run")
|
||||||
cfg *Config
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -22,50 +26,70 @@ func main() {
|
|||||||
|
|
||||||
fmt.Printf("backup (%s)\n", version)
|
fmt.Printf("backup (%s)\n", version)
|
||||||
|
|
||||||
if *debug {
|
email = new(Email)
|
||||||
log.SetLevel(log.DebugLevel)
|
email.startTime = time.Now()
|
||||||
}
|
email.items = make([]string, 0)
|
||||||
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()
|
||||||
log.SetReportCaller(true)
|
if err != nil {
|
||||||
|
log.Printf("Cannot load config (%s)", err)
|
||||||
if *cfgFile != "" {
|
os.Exit(1)
|
||||||
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 *isDaemon {
|
if *testMailFlag {
|
||||||
if cfg.Admin == nil {
|
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "test backup email topic", "test backup email body", cfg.Email.ToEmail)
|
||||||
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)
|
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
|
||||||
}
|
}
|
||||||
|
567
box.go
567
box.go
@ -1,222 +1,423 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"bytes"
|
||||||
"regexp"
|
"encoding/csv"
|
||||||
"sync"
|
"fmt"
|
||||||
|
"log"
|
||||||
"github.com/silenceper/pool"
|
"strings"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/crypto/ssh"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Box struct {
|
type Box struct {
|
||||||
name string
|
Addr string `json:"addr"`
|
||||||
addr string
|
User string `json:"user"`
|
||||||
user string
|
Key string `json:"key"`
|
||||||
key string
|
Name string `json:"-"`
|
||||||
zfs *BoxZfs
|
ssh *SSHConfig
|
||||||
sshPool pool.Pool
|
zfs *ZFSConfig
|
||||||
created bool
|
online bool
|
||||||
online bool
|
|
||||||
allowDirectConnect bool
|
|
||||||
mx sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BoxSshPool struct {
|
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
||||||
signer ssh.Signer
|
if *debugFlag {
|
||||||
config *ssh.ClientConfig
|
log.Printf("Box.ZFSTakeSnapshot : %s : Taking snapshot on %s for %s", b.Name, path, schedule)
|
||||||
client *ssh.Client
|
|
||||||
logged bool
|
|
||||||
mx sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p, err := NewSshPool(name, addr, user, key)
|
|
||||||
if err != nil {
|
|
||||||
log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
b.zfs.box = b
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
if !b.online {
|
||||||
return nil
|
err = fmt.Errorf("box offline")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := b.zfs.Close(); err != nil {
|
err = b.SnapshotInitialize()
|
||||||
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Close", "error": err}).Errorf("")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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 != nil {
|
||||||
log.WithFields(log.Fields{"name": b.name, "error": err, "call": "SshPool.Get"}).Errorf("")
|
return
|
||||||
return "", err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer b.sshPool.Put(v)
|
b.zfs.M.Lock()
|
||||||
s := v.(*Ssh)
|
defer b.zfs.M.Unlock()
|
||||||
|
|
||||||
return s.Exec(cmd)
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
func TransferZfs(from, to Addr) error {
|
func (b *Box) ZFSGetLastSnapshot(path string) (last Snapshot, err error) {
|
||||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
|
if *debugFlag {
|
||||||
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
|
log.Printf("Box.ZFSGetLastSnapshot : %s : Start %s (%d snapshots)", b.Name, path, len(b.zfs.SnapshotList))
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
if !b.online {
|
||||||
err error
|
err = fmt.Errorf("box offline")
|
||||||
fromSnapshots, toSnapshots []*ZfsSnapshot
|
return
|
||||||
directTransfer bool
|
}
|
||||||
)
|
|
||||||
|
|
||||||
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
|
err = b.SnapshotInitialize()
|
||||||
directTransfer = true
|
if err != nil {
|
||||||
|
return last, err
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.M.Lock()
|
||||||
|
defer b.zfs.M.Unlock()
|
||||||
|
|
||||||
|
for _, v := range b.zfs.SnapshotList {
|
||||||
|
if v.Path() == path {
|
||||||
|
last = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(string(last)) == 0 {
|
||||||
|
err = fmt.Errorf("no snapshot")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) {
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !b.online {
|
||||||
|
err = fmt.Errorf("box offline")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = b.SnapshotInitialize()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.ZFSGetNextSnapshot(src)
|
||||||
|
if err != nil {
|
||||||
|
if err.Error() == "no snapshot" {
|
||||||
|
is = true
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
directTransfer = false
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if fromSnapshots, err = from.ValidSnapshots(); err != nil {
|
if !b.online {
|
||||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
|
err = fmt.Errorf("box offline")
|
||||||
return err
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(fromSnapshots) == 0 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if toSnapshots, err = to.ValidSnapshots(); err != nil {
|
if *debugFlag {
|
||||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": to, "error": err}).Errorf("")
|
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)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(toSnapshots) == 0 {
|
for _, rec := range csvData {
|
||||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
|
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
|
||||||
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 {
|
|
||||||
//handle indirect transfer
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fromFromSnapshotId := -1
|
if *debugFlag {
|
||||||
lastToSnapshot := toSnapshots[len(toSnapshots)-1]
|
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if fromFromSnapshotId == -1 {
|
b.zfs.SnapshotInitialized = true
|
||||||
err := errors.New("zfs snapshot unsync")
|
b.zfs.SnapshotAdded = false
|
||||||
log.WithFields(log.Fields{"from": from, "to": to, "error": err}).Errorf("")
|
b.zfs.SnapshotDeleted = false
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
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
|
||||||
|
} else {
|
||||||
|
base = base + `/` + d
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rec := range csvData {
|
||||||
|
b.zfs.ZFSMap[rec[0]] = rec[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
b.zfs.ZFSInitialized = true
|
||||||
|
b.zfs.ZFSAdded = false
|
||||||
|
b.zfs.ZFSDeleted = false
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
432
config.go
432
config.go
@ -1,287 +1,227 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
"bytes"
|
||||||
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"regexp"
|
"regexp"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
"golang.org/x/crypto/ssh"
|
||||||
"github.com/tailscale/hujson"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ScheduleDuration map[string]string `json:"schedule"`
|
Zfsnap map[string]string `json:"zfsnap"`
|
||||||
Box map[string]BoxConfig `json:"box"`
|
Box map[string]*Box `json:"box"`
|
||||||
Email EmailConfig `json:"email"`
|
Email EmailConfig `json:"email"`
|
||||||
Apps []AppConfig `json:"apps"`
|
Apps []AppConfig `json:"apps"`
|
||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
Admin *AdminConfig `json:"admin"`
|
Now time.Time `json:"-"`
|
||||||
Debug bool `json:"debug"`
|
|
||||||
box map[string]*Box `json:"-"`
|
|
||||||
apps map[string]*App `json:"-"`
|
|
||||||
timezone *time.Location `json:"-"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
//Load config from file
|
||||||
cfgMx sync.Mutex
|
func (c *Config) Load() error {
|
||||||
cfgRun bool
|
if *debugFlag {
|
||||||
)
|
log.Printf("SSHConfig.Load : Start")
|
||||||
|
}
|
||||||
//go:embed assets/backup.sample.json
|
b, err := ioutil.ReadFile(*cfgFile)
|
||||||
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 err != nil {
|
||||||
log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
|
if *debugFlag {
|
||||||
return nil, err
|
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoadConfigByte(b)
|
err = json.Unmarshal(b, &c)
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 err != nil {
|
||||||
log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
|
if *debugFlag {
|
||||||
return nil, err
|
log.Printf("Config.Load : json.Unmarshal : %s", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.Unmarshal(b, c); err != nil {
|
if *debugFlag {
|
||||||
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
|
log.Printf("Config.Load :\r\n%v", cfg)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
c.timezone, err = time.LoadLocation(c.Timezone)
|
l, err := time.LoadLocation(cfg.Timezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
|
if *debugFlag {
|
||||||
return nil, err
|
log.Printf("Config.Load : time.LoadLocation : %s", err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Email.Active {
|
if len(cfg.Email.SmtpHost) == 0 {
|
||||||
if len(c.Email.SmtpHost) == 0 {
|
if *debugFlag {
|
||||||
err := fmt.Errorf("no smtp")
|
log.Printf("Config.Load : 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range c.ScheduleDuration {
|
if len(cfg.Email.FromEmail) == 0 {
|
||||||
switch k {
|
if *debugFlag {
|
||||||
case "hourly":
|
log.Printf("Config.Load : no email from")
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
c.box = make(map[string]*Box)
|
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)
|
||||||
|
|
||||||
for k, v := range c.Box {
|
for k, v := range c.Box {
|
||||||
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
|
v.Name = k
|
||||||
log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
|
v.online = false
|
||||||
return nil, err
|
v.zfs = NewZFSConfig()
|
||||||
} else {
|
s := &SSHConfig{
|
||||||
if _, ok := c.box[k]; ok {
|
logged: false,
|
||||||
err := errors.New("already exists")
|
name: k,
|
||||||
log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
c.box[k] = b
|
|
||||||
}
|
}
|
||||||
}
|
v.ssh = s
|
||||||
|
keyRaw, err := ioutil.ReadFile(v.Key)
|
||||||
c.apps = make(map[string]*App)
|
if err != nil {
|
||||||
for _, v := range c.Apps {
|
if *debugFlag {
|
||||||
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
|
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
c.apps[v.Name] = a
|
return err
|
||||||
for k := range a.schedule {
|
}
|
||||||
if dur, ok := c.ScheduleDuration[k]; ok {
|
|
||||||
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
|
key, err := ssh.ParseRawPrivateKey(keyRaw)
|
||||||
if !re.MatchString(dur) {
|
if err != nil {
|
||||||
err := errors.New("incorrect schedule duration")
|
if *debugFlag {
|
||||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
|
||||||
return nil, err
|
}
|
||||||
}
|
return err
|
||||||
} else {
|
}
|
||||||
err := errors.New("undefined schedule duration")
|
|
||||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
s.signer, err = ssh.NewSignerFromKey(key)
|
||||||
return nil, err
|
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)
|
||||||
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, app := range c.Apps {
|
||||||
|
for _, src := range app.Sources {
|
||||||
|
if !src.Valid() {
|
||||||
|
return fmt.Errorf("Source not valid : %s", string(src))
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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 c, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run config
|
//Close config
|
||||||
func (c *Config) Run() {
|
func (c *Config) Close() error {
|
||||||
log.WithFields(log.Fields{}).Debugf("starting")
|
return nil
|
||||||
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
13
const.go
@ -1,13 +0,0 @@
|
|||||||
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,101 +2,61 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"log"
|
||||||
"net/smtp"
|
"net/smtp"
|
||||||
"sort"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Email struct {
|
type Email struct {
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
items []string
|
items []string
|
||||||
mx sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
Active bool `json:"active"`
|
|
||||||
SmtpHost string `json:"smtp"`
|
SmtpHost string `json:"smtp"`
|
||||||
FromEmail string `json:"email_from"`
|
FromEmail string `json:"email_from"`
|
||||||
ToEmail []string `json:"email_to"`
|
ToEmail []string `json:"email_to"`
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func SendMail(addr, from, subject, body string, to []string) error {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("starting")
|
if *debugFlag {
|
||||||
defer log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("done")
|
log.Printf("SendMail : Start")
|
||||||
|
}
|
||||||
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
|
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
|
||||||
|
|
||||||
c, err := smtp.Dial(addr)
|
c, err := smtp.Dial(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "smtp.Dial", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : %s : smtp.Dial (%s)", addr, err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
defer c.Close()
|
defer c.Close()
|
||||||
|
|
||||||
if err = c.Mail(r.Replace(from)); err != nil {
|
if err = c.Mail(r.Replace(from)); err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Mail", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : %s : client.Mail (%s)", from, err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range to {
|
for i := range to {
|
||||||
to[i] = r.Replace(to[i])
|
to[i] = r.Replace(to[i])
|
||||||
if err = c.Rcpt(to[i]); err != nil {
|
if err = c.Rcpt(to[i]); err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Rcpt", "attr": to[i], "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : %s : client.Rcpt (%s)", to[i], err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w, err := c.Data()
|
w, err := c.Data()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : client.Data (%s)", err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,21 +68,29 @@ func SendMail(addr, from, subject, body string, to []string) error {
|
|||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
||||||
|
|
||||||
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail :\r\n%s", msg)
|
||||||
|
}
|
||||||
|
|
||||||
_, err = w.Write([]byte(msg))
|
_, err = w.Write([]byte(msg))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : writer.Write (%s)", err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = w.Close()
|
err = w.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
|
log.Printf("SendMail : writer.Close (%s)", err)
|
||||||
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = c.Quit(); err != nil {
|
err = c.Quit()
|
||||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Quit", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
return err
|
log.Printf("SendMail : client.Quit (%s)", err)
|
||||||
}
|
}
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
9
go.mod
9
go.mod
@ -3,11 +3,6 @@ module git.siteop.biz/shoopea/backup
|
|||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // 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,143 +1,12 @@
|
|||||||
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 h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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-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-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-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-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 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
|
||||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
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.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-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
67
http.go
@ -1,67 +0,0 @@
|
|||||||
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 +1,20 @@
|
|||||||
package main
|
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,123 +1,20 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "strings"
|
||||||
"errors"
|
|
||||||
"regexp"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
type Snapshot string
|
||||||
)
|
|
||||||
|
|
||||||
func SnapshotName(schedule string, now time.Time) string {
|
func (s Snapshot) Path() string {
|
||||||
log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("starting")
|
s2 := strings.Split(string(s), `@`)
|
||||||
log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("done")
|
return s2[0]
|
||||||
|
|
||||||
return schedule + "-" + now.Format(zfsSnapshotDatePattern) + "--" + cfg.ScheduleDuration[schedule]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ZfsSnapshot) Valid() bool {
|
func (s Snapshot) Name() string {
|
||||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
s2 := strings.Split(string(s), `@`)
|
||||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
return s2[1]
|
||||||
|
|
||||||
re := regexp.MustCompile(zfsSnapshotPattern)
|
|
||||||
return re.MatchString(s.name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ZfsSnapshot) Schedule() (string, error) {
|
func (s Snapshot) Append(path string) Snapshot {
|
||||||
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
|
s2 := strings.Split(string(s), `@`)
|
||||||
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
|
return Snapshot(s2[0] + "/" + path + "@" + s2[1])
|
||||||
|
|
||||||
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,170 +1,47 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"io"
|
"bytes"
|
||||||
"os"
|
"log"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/silenceper/pool"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
const SshDialTimeout = time.Duration(10 * time.Second)
|
type SSHConfig struct {
|
||||||
const SshInactivityTimeout = time.Duration(time.Minute)
|
signer ssh.Signer
|
||||||
|
config *ssh.ClientConfig
|
||||||
type Ssh struct {
|
client *ssh.Client
|
||||||
name string
|
logged bool
|
||||||
signer ssh.Signer
|
name string
|
||||||
config *ssh.ClientConfig
|
snapshot []Snapshot
|
||||||
client *ssh.Client
|
|
||||||
session *ssh.Session
|
|
||||||
in io.WriteCloser
|
|
||||||
out io.Reader
|
|
||||||
err io.Reader
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSsh(name, addr, user, key string) (*Ssh, error) {
|
func (s *SSHConfig) exec(cmd string) (b *bytes.Buffer, err error) {
|
||||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
|
if *debugFlag {
|
||||||
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
|
log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd)
|
||||||
|
|
||||||
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()
|
session, err := s.client.NewSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "client.NewSession", "error": err}).Errorf("")
|
if *debugFlag {
|
||||||
return err
|
log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err)
|
||||||
}
|
}
|
||||||
s.session = session
|
return
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.in, err = s.session.StdinPipe(); err != nil {
|
var buf bytes.Buffer
|
||||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdinPipe", "error": err}).Errorf("")
|
b = &buf
|
||||||
s.session.Close()
|
session.Stdout = b
|
||||||
return err
|
|
||||||
|
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.out, err = s.session.StdoutPipe(); err != nil {
|
session.Close()
|
||||||
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdoutPipe", "error": err}).Errorf("")
|
|
||||||
s.session.Close()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.err, err = s.session.StderrPipe(); err != nil {
|
return
|
||||||
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
76
user.go
@ -1,76 +0,0 @@
|
|||||||
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
41
utils.go
@ -1,41 +0,0 @@
|
|||||||
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,7 +1,6 @@
|
|||||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||||
package main
|
package main
|
||||||
var githash = "1a1713e"
|
var githash = "7f9cf49"
|
||||||
var branch = "v2"
|
var buildstamp = "2022-10-08_03:14:52"
|
||||||
var buildstamp = "2023-08-21_12:35:47"
|
var commits = "54"
|
||||||
var commits = "83"
|
var version = "7f9cf49-b54 - 2022-10-08_03:14:52"
|
||||||
var version = "1a1713e-b83 - 2023-08-21_12:35:47"
|
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
# Get the version.
|
# Get the version.
|
||||||
githash=`git rev-parse --short HEAD`
|
githash=`git rev-parse --short HEAD`
|
||||||
branch=`git rev-parse --abbrev-ref HEAD`
|
|
||||||
buildstamp=`date -u '+%Y-%m-%d_%H:%M:%S'`
|
buildstamp=`date -u '+%Y-%m-%d_%H:%M:%S'`
|
||||||
commits=`git rev-list --count $branch`
|
commits=`git rev-list --count master`
|
||||||
# Write out the package.
|
# Write out the package.
|
||||||
cat << EOF > version.go
|
cat << EOF > version.go
|
||||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||||
package main
|
package main
|
||||||
var githash = "$githash"
|
var githash = "$githash"
|
||||||
var branch = "$branch"
|
|
||||||
var buildstamp = "$buildstamp"
|
var buildstamp = "$buildstamp"
|
||||||
var commits = "$commits"
|
var commits = "$commits"
|
||||||
var version = "$githash-b$commits - $buildstamp"
|
var version = "$githash-b$commits - $buildstamp"
|
||||||
|
344
zfs.go
344
zfs.go
@ -1,329 +1,27 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "sync"
|
||||||
"bytes"
|
|
||||||
"encoding/csv"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
type ZFSConfig struct {
|
||||||
)
|
SnapshotAdded bool
|
||||||
|
SnapshotDeleted bool
|
||||||
type BoxZfs struct {
|
SnapshotInitialized bool
|
||||||
filesystems map[string]*ZfsFs
|
SnapshotList []Snapshot
|
||||||
box *Box
|
ZFSAdded bool
|
||||||
online bool
|
ZFSDeleted bool
|
||||||
mx sync.Mutex
|
ZFSInitialized bool
|
||||||
|
ZFSMap map[string]string
|
||||||
|
M sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type ZfsFs struct {
|
func NewZFSConfig() (z *ZFSConfig) {
|
||||||
path string
|
z = &ZFSConfig{
|
||||||
managed bool
|
SnapshotAdded: false,
|
||||||
backedUp bool
|
SnapshotDeleted: false,
|
||||||
zfs *BoxZfs
|
SnapshotInitialized: false,
|
||||||
snapshots map[string]*ZfsSnapshot
|
ZFSAdded: false,
|
||||||
srcApps []*App
|
ZFSDeleted: false,
|
||||||
destApps []*App
|
ZFSInitialized: false,
|
||||||
mx sync.Mutex
|
}
|
||||||
}
|
return
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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…
x
Reference in New Issue
Block a user