v2 #2

Merged
shoopea merged 29 commits from v2 into master 2023-08-21 20:03:11 +02:00
65 changed files with 61074 additions and 1444 deletions

165
addr.go Normal file
View File

@ -0,0 +1,165 @@
package main
import (
"errors"
"fmt"
"regexp"
"strings"
log "github.com/sirupsen/logrus"
)
type Addr string
var (
reBox = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]+$`)
rePath = regexp.MustCompile(`^(/){0,1}[a-zA-Z0-9\-_\.]+(/[a-zA-Z0-9\-_\.]+)+$`)
)
func (a Addr) Box() string {
s := strings.Split(string(a), `:`)
box := s[0]
if reBox.MatchString(box) {
return box
} else {
return ""
}
}
func (a Addr) Path() string {
s := strings.Split(string(a), `:`)
path := s[1]
if rePath.MatchString(path) {
return path
} else {
return ""
}
}
func (a Addr) Append(path string) Addr {
newPath := a.Path() + path
if rePath.MatchString(newPath) {
return Addr(a.Box() + ":" + newPath)
} else {
return ""
}
}
func (a Addr) BoxExec(cmd string) (string, error) {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
err := errors.New("box doesn't exist")
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
return "", err
} else {
return b.Exec(cmd)
}
}
func (a Addr) Exec() (string, error) {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
return a.BoxExec(a.Path())
}
func (a Addr) ValidSnapshots() ([]*ZfsSnapshot, error) {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
err := errors.New("box doesn't exist")
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
return nil, err
} else {
if fs, ok := b.zfs.filesystems[a.Path()]; ok {
return fs.ValidSnapshots(), nil
} else {
err := errors.New("path doesn't exist")
log.WithFields(log.Fields{"addr": a, "error": err}).Errorf("")
return nil, err
}
}
}
func (a Addr) SetManaged(val bool) error {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
err := errors.New("box doesn't exist")
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
return err
} else if fs, ok := b.zfs.filesystems[a.Path()]; !ok {
err := errors.New("path doesn't exist")
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
return err
} else {
fs.mx.Lock()
defer fs.mx.Unlock()
if fs.managed != val {
var cmd string
if val {
cmd = fmt.Sprintf("zfs set %s=+ %s", zfsManagedPropertyName, a.Path())
} else {
cmd = fmt.Sprintf("zfs set %s=- %s", zfsManagedPropertyName, a.Path())
}
if _, err := b.Exec(cmd); err != nil {
log.WithFields(log.Fields{"addr": a, "call": "Exec", "attr": cmd, "error": err}).Errorf("")
return err
}
}
fs.managed = val
return nil
}
}
func (a Addr) SetBackedUp(val bool) error {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
err := errors.New("box doesn't exist")
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
return err
} else if fs, ok := b.zfs.filesystems[a.Path()]; !ok {
err := errors.New("path doesn't exist")
log.WithFields(log.Fields{"addr": a, "path": a.Path(), "error": err}).Errorf("")
return err
} else {
fs.mx.Lock()
defer fs.mx.Unlock()
fs.backedUp = val
return nil
}
}
func (a Addr) Mkdir() error {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
err := errors.New("box doesn't exist")
log.WithFields(log.Fields{"addr": a, "box": a.Box(), "error": err}).Errorf("")
return err
} else {
return b.zfs.Mkdir(a.Path())
}
}
func (a Addr) String() string {
return string(a)
}
func (a Addr) Online() bool {
log.WithFields(log.Fields{"addr": a}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": a}).Debugf("done")
if b, ok := cfg.box[a.Box()]; !ok {
return false
} else {
return b.online
}
}

162
admin.go Normal file
View File

@ -0,0 +1,162 @@
package main
import (
"context"
"embed"
"html/template"
"io/fs"
"net/http"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/robfig/cron/v3"
"github.com/sethvargo/go-password/password"
log "github.com/sirupsen/logrus"
)
type AdminConfig struct {
Users []*User `json:"users"`
Secrets *SecretsConfig `json:"secrets"`
Addr string `json:"addr"`
}
type SecretsConfig struct {
PasswordPepper string `json:"password_pepper"`
ContextKey string `json:"context_key"`
ContextExpiration int `json:"context_expiration"`
ScryptN int `json:"scrypt_n"`
ScryptR int `json:"scrypt_r"`
ScryptP int `json:"scrypt_p"`
}
//go:embed assets
var assets embed.FS
func NewAdmin() *AdminConfig {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
a := &AdminConfig{
Addr: "0.0.0.0:8080",
Secrets: NewSecrets(),
Users: make([]*User, 0),
}
return a
}
func NewSecrets() *SecretsConfig {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
pepper, _ := password.Generate(20, 5, 0, false, false)
ctx, _ := password.Generate(20, 5, 0, false, false)
return &SecretsConfig{
PasswordPepper: pepper,
ContextKey: ctx,
ContextExpiration: 3600,
ScryptN: 32768,
ScryptR: 8,
ScryptP: 1,
}
}
func (a *AdminConfig) NewAdminUser() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
p, _ := password.Generate(20, 5, 0, false, false)
u, _ := NewUser("admin", p)
a.Users = append(a.Users, u)
log.WithFields(log.Fields{}).Warnf("Admin user password : %s", p)
return
}
func (a *AdminConfig) Run() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
// Create context that listens for the interrupt signal from the OS.
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
if !*debug {
gin.SetMode(gin.ReleaseMode)
}
r := gin.Default()
if t, err := template.ParseFS(assets, "assets/templates/*.html"); err != nil {
log.WithFields(log.Fields{"call": "template.ParseFS", "error": err}).Errorf("")
return
} else {
r.SetHTMLTemplate(t)
}
r.GET("/", HttpAnyIndex)
r.POST("/", HttpAnyIndex)
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.GET("/run", func(c *gin.Context) {
cfg.Run()
c.JSON(http.StatusOK, gin.H{
"message": "done",
})
})
fsys, _ := fs.Sub(assets, "assets/static")
r.StaticFS("/assets", http.FS(fsys))
protected := r.Group("p", HttpAuth())
protected.GET("test", HttpAnyHome)
unprotected := r.Group("u", HttpNoAuth())
unprotected.GET("signin", HttpAnySignIn)
unprotected.POST("signin", HttpAnySignIn)
srv := &http.Server{
Addr: a.Addr,
Handler: r,
}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.WithFields(log.Fields{"call": "http.ListenAndServe", "attr": a.Addr, "error": err}).Errorf("")
}
}()
c := cron.New(cron.WithLocation(time.UTC))
if _, err := c.AddFunc("0 * * * *", func() { cfg.Run() }); err != nil {
log.WithFields(log.Fields{"call": "cron.AddFunc", "error": err}).Errorf("")
}
c.Start()
log.WithFields(log.Fields{"call": "cron.Start"}).Debugf("cron started")
// Listen for the interrupt signal.
<-ctx.Done()
// Restore default behavior on the interrupt signal and notify user of shutdown.
stop()
log.WithFields(log.Fields{"call": "stop"}).Warnf("shutting down")
// The context is used to inform the server it has 5 seconds to finish
// the request it is currently handling
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.WithFields(log.Fields{"call": "http.Shutdown", "error": err}).Errorf("shutting down")
}
}

1151
app.go

File diff suppressed because it is too large Load Diff

18
assets/backup.sample.json Normal file
View File

@ -0,0 +1,18 @@
{
"schedule":{
"hourly":"25h",
"daily":"1m",
"weekly":"3m",
"monthly":"13m"
},
"box":{},
"email":{
"active":false
},
"apps":[],
"timezone":"Etc/UTC",
"admin":{
"addr":":8080"
},
"debug":true
}

5002
assets/static/css/bootstrap-grid.css vendored Normal file

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

5001
assets/static/css/bootstrap-grid.rtl.css vendored Normal file

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

426
assets/static/css/bootstrap-reboot.css vendored Normal file
View File

@ -0,0 +1,426 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-left: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-left: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr /* rtl:ignore */;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: left;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: left;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: left;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
/* rtl:raw:
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.min.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,423 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/
*,
*::before,
*::after {
box-sizing: border-box;
}
@media (prefers-reduced-motion: no-preference) {
:root {
scroll-behavior: smooth;
}
}
body {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #212529;
background-color: #fff;
-webkit-text-size-adjust: 100%;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
hr {
margin: 1rem 0;
color: inherit;
background-color: currentColor;
border: 0;
opacity: 0.25;
}
hr:not([size]) {
height: 1px;
}
h6, h5, h4, h3, h2, h1 {
margin-top: 0;
margin-bottom: 0.5rem;
font-weight: 500;
line-height: 1.2;
}
h1 {
font-size: calc(1.375rem + 1.5vw);
}
@media (min-width: 1200px) {
h1 {
font-size: 2.5rem;
}
}
h2 {
font-size: calc(1.325rem + 0.9vw);
}
@media (min-width: 1200px) {
h2 {
font-size: 2rem;
}
}
h3 {
font-size: calc(1.3rem + 0.6vw);
}
@media (min-width: 1200px) {
h3 {
font-size: 1.75rem;
}
}
h4 {
font-size: calc(1.275rem + 0.3vw);
}
@media (min-width: 1200px) {
h4 {
font-size: 1.5rem;
}
}
h5 {
font-size: 1.25rem;
}
h6 {
font-size: 1rem;
}
p {
margin-top: 0;
margin-bottom: 1rem;
}
abbr[title],
abbr[data-bs-original-title] {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
cursor: help;
-webkit-text-decoration-skip-ink: none;
text-decoration-skip-ink: none;
}
address {
margin-bottom: 1rem;
font-style: normal;
line-height: inherit;
}
ol,
ul {
padding-right: 2rem;
}
ol,
ul,
dl {
margin-top: 0;
margin-bottom: 1rem;
}
ol ol,
ul ul,
ol ul,
ul ol {
margin-bottom: 0;
}
dt {
font-weight: 700;
}
dd {
margin-bottom: 0.5rem;
margin-right: 0;
}
blockquote {
margin: 0 0 1rem;
}
b,
strong {
font-weight: bolder;
}
small {
font-size: 0.875em;
}
mark {
padding: 0.2em;
background-color: #fcf8e3;
}
sub,
sup {
position: relative;
font-size: 0.75em;
line-height: 0;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
a {
color: #0d6efd;
text-decoration: underline;
}
a:hover {
color: #0a58ca;
}
a:not([href]):not([class]), a:not([href]):not([class]):hover {
color: inherit;
text-decoration: none;
}
pre,
code,
kbd,
samp {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 1em;
direction: ltr ;
unicode-bidi: bidi-override;
}
pre {
display: block;
margin-top: 0;
margin-bottom: 1rem;
overflow: auto;
font-size: 0.875em;
}
pre code {
font-size: inherit;
color: inherit;
word-break: normal;
}
code {
font-size: 0.875em;
color: #d63384;
word-wrap: break-word;
}
a > code {
color: inherit;
}
kbd {
padding: 0.2rem 0.4rem;
font-size: 0.875em;
color: #fff;
background-color: #212529;
border-radius: 0.2rem;
}
kbd kbd {
padding: 0;
font-size: 1em;
font-weight: 700;
}
figure {
margin: 0 0 1rem;
}
img,
svg {
vertical-align: middle;
}
table {
caption-side: bottom;
border-collapse: collapse;
}
caption {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
color: #6c757d;
text-align: right;
}
th {
text-align: inherit;
text-align: -webkit-match-parent;
}
thead,
tbody,
tfoot,
tr,
td,
th {
border-color: inherit;
border-style: solid;
border-width: 0;
}
label {
display: inline-block;
}
button {
border-radius: 0;
}
button:focus:not(:focus-visible) {
outline: 0;
}
input,
button,
select,
optgroup,
textarea {
margin: 0;
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
button,
select {
text-transform: none;
}
[role=button] {
cursor: pointer;
}
select {
word-wrap: normal;
}
select:disabled {
opacity: 1;
}
[list]::-webkit-calendar-picker-indicator {
display: none;
}
button,
[type=button],
[type=reset],
[type=submit] {
-webkit-appearance: button;
}
button:not(:disabled),
[type=button]:not(:disabled),
[type=reset]:not(:disabled),
[type=submit]:not(:disabled) {
cursor: pointer;
}
::-moz-focus-inner {
padding: 0;
border-style: none;
}
textarea {
resize: vertical;
}
fieldset {
min-width: 0;
padding: 0;
margin: 0;
border: 0;
}
legend {
float: right;
width: 100%;
padding: 0;
margin-bottom: 0.5rem;
font-size: calc(1.275rem + 0.3vw);
line-height: inherit;
}
@media (min-width: 1200px) {
legend {
font-size: 1.5rem;
}
}
legend + * {
clear: right;
}
::-webkit-datetime-edit-fields-wrapper,
::-webkit-datetime-edit-text,
::-webkit-datetime-edit-minute,
::-webkit-datetime-edit-hour-field,
::-webkit-datetime-edit-day-field,
::-webkit-datetime-edit-month-field,
::-webkit-datetime-edit-year-field {
padding: 0;
}
::-webkit-inner-spin-button {
height: auto;
}
[type=search] {
outline-offset: -2px;
-webkit-appearance: textfield;
}
[type="tel"],
[type="url"],
[type="email"],
[type="number"] {
direction: ltr;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-color-swatch-wrapper {
padding: 0;
}
::file-selector-button {
font: inherit;
}
::-webkit-file-upload-button {
font: inherit;
-webkit-appearance: button;
}
output {
display: inline-block;
}
iframe {
border: 0;
}
summary {
display: list-item;
cursor: pointer;
}
progress {
vertical-align: baseline;
}
[hidden] {
display: none !important;
}
/*# sourceMappingURL=bootstrap-reboot.rtl.css.map */

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,8 @@
/*!
* Bootstrap Reboot v5.0.2 (https://getbootstrap.com/)
* Copyright 2011-2021 The Bootstrap Authors
* Copyright 2011-2021 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
* Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
*/*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
/*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */

File diff suppressed because one or more lines are too long

4752
assets/static/css/bootstrap-utilities.css vendored Normal file

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

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 Normal file

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 Normal file

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 Normal file

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

6748
assets/static/js/bootstrap.bundle.js vendored Normal file

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

4967
assets/static/js/bootstrap.esm.js vendored Normal file

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 Normal file

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 Normal file

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 Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,29 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<title>Login :: zBackup</title>
<!-- Bootstrap core CSS -->
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
</head>
<body class="text-center">
<div class="container">
<form class="form-signin" method="POST" action="/u/submit">
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
<div class="form-group">
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
</div>
<div class="form-group">
<input type="password" class="form-control" id="pwd" placeholder="Password" name="password" required>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
<p class="mt-5 mb-3 text-muted">&copy; 2023</p>
</form>
<a href="/u/recover">Lost password</a>
</div>
</body>
</html>

122
backup.go
View File

@ -3,22 +3,18 @@ package main
import ( import (
"flag" "flag"
"fmt" "fmt"
"log"
"os" "os"
"time"
log "github.com/sirupsen/logrus"
) )
var ( var (
appFlag = flag.String("app", "", "run specific app") cfgFile = flag.String("config", "", "config file")
cfgFile = flag.String("config", "config.json", "config file") isDaemon = flag.Bool("daemon", false, "run as daemon")
schedFlag = flag.String("schedule", "", "specific schedule") debug = flag.Bool("debug", false, "log debug messages")
slowFlag = flag.Bool("slow", false, "slow process") quiet = flag.Bool("quiet", false, "remove most log messages")
testFlag = flag.Bool("test", false, "test run") logFile = flag.String("logfile", "", "log file")
debugFlag = flag.Bool("debug", false, "debug") cfg *Config
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() {
@ -26,70 +22,50 @@ func main() {
fmt.Printf("backup (%s)\n", version) fmt.Printf("backup (%s)\n", version)
email = new(Email) if *debug {
email.startTime = time.Now() log.SetLevel(log.DebugLevel)
email.items = make([]string, 0) }
if *quiet {
err := cfg.Load() log.SetLevel(log.WarnLevel)
if err != nil { }
log.Printf("Cannot load config (%s)", err) if *logFile != "" {
os.Exit(1) 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()
} }
if *testMailFlag { }
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "test backup email topic", "test backup email body", cfg.Email.ToEmail) log.SetReportCaller(true)
if *cfgFile != "" {
if c, err := LoadConfigFile(*cfgFile); err != nil {
log.Printf("Cannot load config (%s)", err)
os.Exit(1)
} else {
cfg = c
}
} else if c, err := LoadConfigFile("backup.json"); err == nil {
cfg = c
} else {
cfg, _ = LoadConfigByte(sampleCfg)
}
if *isDaemon {
if cfg.Admin == nil {
cfg.Admin = NewAdmin()
}
if cfg.Admin.Secrets == nil {
cfg.Admin.Secrets = NewSecrets()
}
if len(cfg.Admin.Users) == 0 {
cfg.Admin.NewAdminUser()
}
cfg.Admin.Run()
} else {
cfg.Run()
os.Exit(0) 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
} }

557
box.go
View File

@ -1,423 +1,222 @@
package main package main
import ( import (
"bytes" "errors"
"encoding/csv" "regexp"
"fmt" "sync"
"log"
"strings" "github.com/silenceper/pool"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
) )
type Box struct { type Box struct {
Addr string `json:"addr"` name string
User string `json:"user"` addr string
Key string `json:"key"` user string
Name string `json:"-"` key string
ssh *SSHConfig zfs *BoxZfs
zfs *ZFSConfig sshPool pool.Pool
created bool
online bool online bool
allowDirectConnect bool
mx sync.Mutex
} }
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) { type BoxSshPool struct {
if *debugFlag { signer ssh.Signer
log.Printf("Box.ZFSTakeSnapshot : %s : Taking snapshot on %s for %s", b.Name, path, schedule) config *ssh.ClientConfig
} client *ssh.Client
logged bool
if !b.online { mx sync.Mutex
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
_, err = b.ssh.exec("zfs snapshot " + path + "@" + name)
if err != nil {
return
}
b.zfs.SnapshotAdded = true
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(path+"@"+name))
return
} }
func (b *Box) ZFSGetLastSnapshot(path string) (last Snapshot, err error) { func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err error) {
if *debugFlag { log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
log.Printf("Box.ZFSGetLastSnapshot : %s : Start %s (%d snapshots)", b.Name, path, len(b.zfs.SnapshotList)) defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
re := regexp.MustCompile(boxNamePattern)
if !re.MatchString(name) {
err := errors.New("invalid name")
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
return nil, err
} }
if !b.online { p, err := NewSshPool(name, addr, user, key)
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil { if err != nil {
return last, err log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
return nil, err
} }
b.zfs.M.Lock() b = &Box{
defer b.zfs.M.Unlock() name: name,
addr: addr,
user: user,
key: key,
zfs: &BoxZfs{
online: false,
},
sshPool: p,
online: false,
created: true,
allowDirectConnect: true, //FIXME use direct
}
for _, v := range b.zfs.SnapshotList { b.zfs.box = b
if v.Path() == path {
last = v return b, nil
}
}
if len(string(last)) == 0 {
err = fmt.Errorf("no snapshot")
}
return
} }
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) { func (b *Box) Open() error {
if *debugFlag { log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src)) defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
}
if !b.online { b.mx.Lock()
err = fmt.Errorf("box offline") defer b.mx.Unlock()
return
}
err = b.SnapshotInitialize() if b.online {
if err != nil {
return
}
_, err = b.ZFSGetNextSnapshot(src)
if err != nil {
if err.Error() == "no snapshot" {
is = true
err = nil
}
} else {
is = false
}
return
}
func (b *Box) ZFSGetFirstSnapshot(path string) (first Snapshot, err error) {
if *debugFlag {
log.Printf("Box.ZFSGetFirstSnapshot : %s : Start %s", b.Name, path)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
for _, v := range b.zfs.SnapshotList {
if v.Path() == path {
first = v
return
}
}
err = fmt.Errorf("no snapshot")
return
}
func (b *Box) ZFSGetNextSnapshot(src Snapshot) (next Snapshot, err error) {
if *debugFlag {
log.Printf("Box.ZFSGetNextSnapshot : %s : Start %s", b.Name, string(src))
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
for id, v := range b.zfs.SnapshotList {
if v == src {
if len(b.zfs.SnapshotList) > id+1 {
next = b.zfs.SnapshotList[id+1]
if next.Path() == src.Path() {
return
} else {
err = fmt.Errorf("no snapshot")
return
}
} else {
err = fmt.Errorf("no snapshot")
return
}
}
}
err = fmt.Errorf("no snapshot")
return
}
func (b *Box) ZFSUpdateSnapshotList() (err error) {
if *debugFlag {
log.Printf("Box.ZFSUpdateSnapshotList : %s : Start", b.Name)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
b.zfs.M.Lock()
if b.zfs.SnapshotDeleted || b.zfs.SnapshotAdded {
b.zfs.SnapshotInitialized = false
}
b.zfs.M.Unlock()
err = b.SnapshotInitialize()
return
}
func (b *Box) ZFSGetSnapshotList() (snaps []Snapshot, err error) {
if *debugFlag {
log.Printf("Box.ZFSGetSnapshotList : %s : Start", b.Name)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
snaps = b.zfs.SnapshotList
return
}
func (b *Box) SnapshotInitialize() (err error) {
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
if b.zfs.SnapshotInitialized {
return nil return nil
} }
if *debugFlag { hostname, err := b.Exec("hostname")
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 err != nil {
if *debugFlag { log.WithFields(log.Fields{"name": b.name, "call": "Exec", "attr": "hostname", "error": err}).Errorf("")
log.Printf("Box.SnapshotInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
}
return err return err
} }
for _, rec := range csvData { log.WithFields(log.Fields{"name": b.name}).Debugf("hostname : %s", hostname)
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
}
if *debugFlag { b.online = true
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
}
b.zfs.SnapshotInitialized = true if err := b.zfs.Open(); err != nil {
b.zfs.SnapshotAdded = false log.WithFields(log.Fields{"name": b.name, "call": "zfs.Open", "error": err}).Errorf("")
b.zfs.SnapshotDeleted = false
return nil
}
func (b *Box) ZFSUpdateList() (err error) {
if *debugFlag {
log.Printf("Box.ZFSUpdateList : %s : Start", b.Name)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
b.zfs.M.Lock()
if b.zfs.ZFSDeleted || b.zfs.ZFSAdded {
b.zfs.ZFSInitialized = false
}
b.zfs.M.Unlock()
err = b.ZFSInitialize()
return
}
func (b *Box) ZFSIsZFS(path string) bool {
if *debugFlag {
log.Printf("Box.ZFSIsZFS : %s : Start %s", b.Name, path)
}
if !b.online {
return false
}
err := b.ZFSInitialize()
if err != nil {
return false
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
if _, ok := b.zfs.ZFSMap[path]; ok {
return true
}
return false
}
func (b *Box) ZFSCreateZFS(path string) (err error) {
if *debugFlag {
log.Printf("Box.ZFSCreateZFS : %s : Start %s", b.Name, path)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.ZFSInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
p := strings.Split(path, `/`)
var base string
for _, d := range p {
if base == "" {
base = d
} 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 return err
} }
for _, rec := range csvData { return nil
b.zfs.ZFSMap[rec[0]] = rec[1] }
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 {
return nil
} }
b.zfs.ZFSInitialized = true if err := b.zfs.Close(); err != nil {
b.zfs.ZFSAdded = false log.WithFields(log.Fields{"name": b.name, "call": "zfs.Close", "error": err}).Errorf("")
b.zfs.ZFSDeleted = false return err
}
b.online = false
return nil return nil
} }
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) { func (b *Box) Exec(cmd string) (r string, err error) {
if !b.online { log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("starting")
err = fmt.Errorf("box offline") defer log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("done")
return
if !b.created {
err := errors.New("box not initialized")
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
return "", err
} }
buf, err = b.ssh.exec(cmd) v, err := b.sshPool.Get()
return if err != nil {
log.WithFields(log.Fields{"name": b.name, "error": err, "call": "SshPool.Get"}).Errorf("")
return "", err
}
defer b.sshPool.Put(v)
s := v.(*Ssh)
return s.Exec(cmd)
} }
func (b *Box) Host() string { func TransferZfs(from, to Addr) error {
s := strings.Split(string(b.Addr), `:`) log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
return s[0] defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
var (
err error
fromSnapshots, toSnapshots []*ZfsSnapshot
directTransfer bool
)
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
directTransfer = true
} else {
directTransfer = false
}
if fromSnapshots, err = from.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
return err
}
if len(fromSnapshots) == 0 {
return nil
}
if toSnapshots, err = to.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": to, "error": err}).Errorf("")
return err
}
if len(toSnapshots) == 0 {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
if directTransfer {
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
return err
}
newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}
toSnapshots = append(toSnapshots, newToSnapshot)
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot)
} else {
//handle indirect transfer
}
}
fromFromSnapshotId := -1
lastToSnapshot := toSnapshots[len(toSnapshots)-1]
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("searching last snapshot %s", lastToSnapshot.String())
for id, v := range fromSnapshots {
if v.name == lastToSnapshot.name {
fromFromSnapshotId = id
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("found %s", v.String())
break
}
}
if fromFromSnapshotId == -1 {
err := errors.New("zfs snapshot unsync")
log.WithFields(log.Fields{"from": from, "to": to, "error": err}).Errorf("")
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
} }

412
config.go
View File

@ -1,227 +1,287 @@
package main package main
import ( import (
"bytes" _ "embed"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io/ioutil" "os"
"log"
"regexp" "regexp"
"sync"
"time" "time"
"golang.org/x/crypto/ssh" log "github.com/sirupsen/logrus"
"github.com/tailscale/hujson"
) )
type Config struct { type Config struct {
Zfsnap map[string]string `json:"zfsnap"` ScheduleDuration map[string]string `json:"schedule"`
Box map[string]*Box `json:"box"` Box map[string]BoxConfig `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"`
Now time.Time `json:"-"` Admin *AdminConfig `json:"admin"`
Debug bool `json:"debug"`
box map[string]*Box `json:"-"`
apps map[string]*App `json:"-"`
timezone *time.Location `json:"-"`
} }
//Load config from file var (
func (c *Config) Load() error { cfgMx sync.Mutex
if *debugFlag { cfgRun bool
log.Printf("SSHConfig.Load : Start") )
}
b, err := ioutil.ReadFile(*cfgFile) //go:embed assets/backup.sample.json
var sampleCfg []byte
type BoxConfig struct {
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
AllowDirectConnect bool `json:"allow_direct_connect"`
}
type AppConfig struct {
Name string `json:"name"`
Schedule []string `json:"schedule"`
Sources []string `json:"src"`
Destinations []string `json:"dest"`
Before map[string]string `json:"before"`
After map[string]string `json:"after"`
Active bool `json:"active"`
}
// Load config from file
func LoadConfigFile(path string) (*Config, error) {
log.WithFields(log.Fields{"path": path}).Debugf("starting")
defer log.WithFields(log.Fields{"path": path}).Debugf("done")
b, err := os.ReadFile(path)
if err != nil { if err != nil {
if *debugFlag { log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err) return nil, err
}
return err
} }
err = json.Unmarshal(b, &c) return LoadConfigByte(b)
}
// Load config from string
func LoadConfigByte(conf []byte) (*Config, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
c := &Config{}
if err := json.Unmarshal(sampleCfg, c); err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
return nil, err
}
b, err := hujson.Standardize(conf)
if err != nil { if err != nil {
if *debugFlag { log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
log.Printf("Config.Load : json.Unmarshal : %s", err) return nil, err
}
return err
} }
if *debugFlag { if err := json.Unmarshal(b, c); err != nil {
log.Printf("Config.Load :\r\n%v", cfg) log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
return nil, err
} }
l, err := time.LoadLocation(cfg.Timezone) c.timezone, err = time.LoadLocation(c.Timezone)
if err != nil { if err != nil {
if *debugFlag { log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
log.Printf("Config.Load : time.LoadLocation : %s", err) return nil, err
}
return err
} }
if len(cfg.Email.SmtpHost) == 0 { if c.Email.Active {
if *debugFlag { if len(c.Email.SmtpHost) == 0 {
log.Printf("Config.Load : no smtp") err := fmt.Errorf("no smtp")
} log.WithFields(log.Fields{"error": err}).Errorf("")
return fmt.Errorf("no smtp") return nil, err
} }
if len(cfg.Email.FromEmail) == 0 { if len(c.Email.FromEmail) == 0 {
if *debugFlag { err := fmt.Errorf("no email from")
log.Printf("Config.Load : no email from") log.WithFields(log.Fields{"error": err}).Errorf("")
} return nil, err
return fmt.Errorf("no email from")
} }
if len(cfg.Email.ToEmail) == 0 { if len(c.Email.ToEmail) == 0 {
if *debugFlag { err := fmt.Errorf("no email to")
log.Printf("Config.Load : no email to") log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
} }
return fmt.Errorf("no email to")
} }
c.Now = time.Now().In(l) for k, v := range c.ScheduleDuration {
switch k {
case "hourly":
case "daily":
case "weekly":
case "monthly":
case "yearly":
if _, err := Expiration(time.Now(), v); err != nil {
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
return nil, err
}
default:
err := errors.New("invalid schedule")
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
return nil, err
}
}
c.box = make(map[string]*Box)
for k, v := range c.Box { for k, v := range c.Box {
v.Name = k if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
v.online = false log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
v.zfs = NewZFSConfig() return nil, err
s := &SSHConfig{ } else {
logged: false, if _, ok := c.box[k]; ok {
name: k, err := errors.New("already exists")
log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
return nil, err
} }
v.ssh = s c.box[k] = b
keyRaw, err := ioutil.ReadFile(v.Key)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
} }
return err
} }
key, err := ssh.ParseRawPrivateKey(keyRaw) 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 : ssh.ParseRawPrivateKey(%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
} }
return err c.apps[v.Name] = a
} for k := range a.schedule {
if dur, ok := c.ScheduleDuration[k]; ok {
s.signer, err = ssh.NewSignerFromKey(key) re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
if err != nil { if !re.MatchString(dur) {
if *debugFlag { err := errors.New("incorrect schedule duration")
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err) log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
} return nil, 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 { } else {
v.online = true err := errors.New("undefined schedule duration")
session, err := s.client.NewSession() log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
if err != nil { return nil, err
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 nil return c, nil
} }
//Close config // Run config
func (c *Config) Close() error { func (c *Config) Run() {
return nil log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if cfgRun {
return
}
cfgMx.Lock()
defer cfgMx.Unlock()
cfgRun = true
defer func() { cfgRun = false }()
e := NewEmail(time.Now())
var wg sync.WaitGroup
// Setup boxes
for _, b := range c.box {
wg.Add(1)
go func(box *Box) {
defer wg.Done()
if err := box.Open(); err != nil {
log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("")
e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name))
return
}
}(b)
}
wg.Wait()
// Run each app
for _, a := range cfg.apps {
wg.Add(1)
go func(app *App) {
if sched, err := app.Run(e.startTime); err != nil {
e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
} else if *debug {
if sched != "" {
e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched))
} else {
e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name))
}
}
wg.Done()
}(a)
}
wg.Wait()
// Cleanup
for _, a := range cfg.apps {
for _, src := range a.sources {
if b, ok := c.box[src.Box()]; ok {
if fs, ok := b.zfs.filesystems[src.Path()]; ok {
fs.srcApps = append(fs.srcApps, a)
}
}
for _, dest := range a.destinations {
if b, ok := c.box[dest.Box()]; ok {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
fs.destApps = append(fs.destApps, a)
} else {
e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String()))
}
}
}
}
}
for _, b := range cfg.box {
if b.online {
for _, fs := range b.zfs.filesystems {
if len(fs.srcApps) > 0 && !fs.backedUp {
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up")
e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path))
}
if len(fs.destApps) == 0 && !fs.backedUp && fs.managed {
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed")
e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path))
}
}
}
}
// Stop
for _, b := range c.box {
if err := b.Close(); err != nil {
log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("")
}
}
if len(e.items) > 0 {
if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil {
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
}
}
return
} }

13
const.go Normal file
View File

@ -0,0 +1,13 @@
package main
const (
boxNamePattern = `[a-zA-Z0-9\-_\.]`
zfsManagedPropertyName = "biz.siteop:managed"
zfsSnapshotPattern = `^(?P<Schedule>hourly|daily|weekly|monthly|yearly|adhoc)\-(?P<Timestamp>[0-9]{4}-[0-9]{2}-[0-9]{2}_[0-9]{2}.[0-9]{2}.[0-9]{2})\-\-(?P<Expiration>forever|([0-9]+(h|d|m|y))+)$`
zfsSnapshotDatePattern = "2006-01-02_15.04.05"
serverAddr = ":8080"
serverUsername = "admin"
serverPassword = "8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918" //admin
)

View File

@ -2,61 +2,101 @@ package main
import ( import (
"encoding/base64" "encoding/base64"
"log" "fmt"
"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 SendMail(addr, from, subject, body string, to []string) error { func NewEmail(now time.Time) *Email {
if *debugFlag { log.WithFields(log.Fields{"now": now}).Debugf("starting")
log.Printf("SendMail : Start") defer log.WithFields(log.Fields{"now": now}).Debugf("done")
return &Email{startTime: now, items: make([]string, 0)}
}
func (e *Email) AddItem(item string) {
log.WithFields(log.Fields{"item": item}).Debugf("starting")
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
e.items = append(e.items, item)
}
func (e *Email) Send(addr, from string, to []string) error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if len(e.items) == 0 {
return nil
} }
sort.Slice(e.items, func(i, j int) bool {
return e.items[i] < e.items[j]
})
body := e.items[0]
for _, item := range e.items[1:] {
body = body + "\r\n" + item
}
subject := fmt.Sprintf("Autobackup report (%s)", e.startTime)
if err := SendMail(addr, from, subject, body, to); err != nil {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "SendMail", "error": err}).Errorf("")
return err
}
return nil
}
func SendMail(addr, from, subject, body string, to []string) error {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("done")
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "") 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 {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "smtp.Dial", "error": err}).Errorf("")
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 {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Mail", "error": err}).Errorf("")
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 {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Rcpt", "attr": to[i], "error": err}).Errorf("")
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 {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
log.Printf("SendMail : client.Data (%s)", err)
}
return err return err
} }
@ -68,29 +108,21 @@ 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 {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
log.Printf("SendMail : writer.Write (%s)", err)
}
return err return err
} }
err = w.Close() err = w.Close()
if err != nil { if err != nil {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
log.Printf("SendMail : writer.Close (%s)", err)
}
return err return err
} }
err = c.Quit() if err = c.Quit(); err != nil {
if *debugFlag { log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Quit", "error": err}).Errorf("")
log.Printf("SendMail : client.Quit (%s)", err)
}
return err return err
}
return nil
} }

9
go.mod
View File

@ -3,6 +3,11 @@ module git.siteop.biz/shoopea/backup
go 1.16 go 1.16
require ( require (
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b github.com/gin-gonic/gin v1.9.1 // indirect
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sethvargo/go-password v0.2.0 // indirect
github.com/silenceper/pool v1.0.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
golang.org/x/crypto v0.9.0
) )

131
go.sum
View File

@ -1,12 +1,143 @@
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU=
github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI=
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0= golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b 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 Normal file
View File

@ -0,0 +1,67 @@
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func HttpAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// Search signed-in userID
userID := 0
if userID == 0 {
// Return 404 and abort handlers chain.
c.String(http.StatusNotFound, "404 page not found")
c.AbortWithStatus(http.StatusNotFound)
return
}
}
}
func HttpNoAuth() gin.HandlerFunc {
return func(c *gin.Context) {
return
}
}
func HttpAnySignIn(c *gin.Context) {
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
SetCSRFToken(c)
warning, _ := c.Cookie("warning")
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
c.HTML(http.StatusOK, "page-signin.html", gin.H{
"Error": warning,
})
}
return
}
func HttpAnyIndex(c *gin.Context) {
if GetWebSessionUserID(c) > 0 {
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
} else {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
}
return
}
func HttpAnyHome(c *gin.Context) {
if GetWebSessionUserID(c) == 0 {
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
} else {
SetCSRFToken(c)
c.HTML(http.StatusOK, "page-home.html", gin.H{})
}
return
}
func GetWebSessionUserID(c *gin.Context) int64 {
return 0
}
func SetCSRFToken(c *gin.Context) {
return
}

View File

@ -1,20 +1 @@
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
}

View File

@ -1,20 +1,123 @@
package main package main
import "strings" import (
"errors"
"regexp"
"time"
type Snapshot string log "github.com/sirupsen/logrus"
)
func (s Snapshot) Path() string { func SnapshotName(schedule string, now time.Time) string {
s2 := strings.Split(string(s), `@`) log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("starting")
return s2[0] log.WithFields(log.Fields{"schedule": schedule, "now": now}).Debugf("done")
return schedule + "-" + now.Format(zfsSnapshotDatePattern) + "--" + cfg.ScheduleDuration[schedule]
} }
func (s Snapshot) Name() string { func (s *ZfsSnapshot) Valid() bool {
s2 := strings.Split(string(s), `@`) log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
return s2[1] defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
re := regexp.MustCompile(zfsSnapshotPattern)
return re.MatchString(s.name)
} }
func (s Snapshot) Append(path string) Snapshot { func (s *ZfsSnapshot) Schedule() (string, error) {
s2 := strings.Split(string(s), `@`) log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
return Snapshot(s2[0] + "/" + path + "@" + s2[1]) defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
if !s.Valid() {
err := errors.New("invalid name")
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
return "", err
}
re := regexp.MustCompile(zfsSnapshotPattern)
return re.ReplaceAllString(s.name, "${Schedule}"), nil
}
func (s *ZfsSnapshot) Expiration() (time.Time, error) {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
if !s.Valid() {
err := errors.New("invalid name")
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
return time.Now(), err
}
re := regexp.MustCompile(zfsSnapshotPattern)
expirationString := re.ReplaceAllString(s.name, "${Expiration}")
timestampTime, err := s.Timestamp()
if err != nil {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Timestamp", "error": err}).Errorf("")
return time.Now(), err
}
return Expiration(timestampTime, expirationString)
}
func (s *ZfsSnapshot) Timestamp() (time.Time, error) {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
t := time.Now()
if !s.Valid() {
err := errors.New("invalid name")
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
return t, err
}
re := regexp.MustCompile(zfsSnapshotPattern)
timestampString := re.ReplaceAllString(s.name, "${Timestamp}")
timestampTime, err := time.ParseInLocation(zfsSnapshotDatePattern, timestampString, cfg.timezone)
if err != nil {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "time.Parse", "attr": timestampString, "error": err}).Errorf("")
return t, err
}
return timestampTime, nil
}
func (s *ZfsSnapshot) Expired(now time.Time) (bool, error) {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
if !s.Valid() {
err := errors.New("invalid name")
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Valid", "error": err}).Errorf("")
return false, err
}
expirationTime, err := s.Expiration()
if err != nil {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name, "call": "Timestamp", "error": err}).Errorf("")
return false, err
}
if now.After(expirationTime) {
return true, nil
} else {
return false, nil
}
}
func (s *ZfsSnapshot) String() string {
return s.fs.path + "@" + s.name
}
func (s *ZfsSnapshot) Delete() error {
log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": s.fs.zfs.box.name, "fs": s.fs.path, "name": s.name}).Debugf("done")
return s.fs.DelSnapshot(s.name)
} }

171
ssh.go
View File

@ -1,47 +1,170 @@
package main package main
import ( import (
"bytes" "io"
"log" "os"
"time"
"github.com/silenceper/pool"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh"
) )
type SSHConfig struct { const SshDialTimeout = time.Duration(10 * time.Second)
const SshInactivityTimeout = time.Duration(time.Minute)
type Ssh struct {
name string
signer ssh.Signer signer ssh.Signer
config *ssh.ClientConfig config *ssh.ClientConfig
client *ssh.Client client *ssh.Client
logged bool session *ssh.Session
name string in io.WriteCloser
snapshot []Snapshot out io.Reader
err io.Reader
} }
func (s *SSHConfig) exec(cmd string) (b *bytes.Buffer, err error) { func NewSsh(name, addr, user, key string) (*Ssh, error) {
if *debugFlag { log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
log.Printf("SSHConfig.exec : %s : Start %s", s.name, cmd) defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
s := &Ssh{
name: name,
} }
k, err := os.ReadFile(key)
if err != nil {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "os.ReadFile", "error": err}).Errorf("")
return s, err
}
parsedKey, err := ssh.ParseRawPrivateKey(k)
if err != nil {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.ParseRawPrivateKey", "error": err}).Errorf("")
return s, err
}
s.signer, err = ssh.NewSignerFromKey(parsedKey)
if err != nil {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.NewSignerFromKey", "error": err}).Errorf("")
return s, err
}
s.config = &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(s.signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: SshDialTimeout,
}
s.client, err = ssh.Dial("tcp", addr, s.config)
if err != nil {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key, "call": "ssh.Dial", "error": err}).Errorf("")
return s, err
}
return s, nil
}
func (s *Ssh) Close() error {
log.WithFields(log.Fields{"name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": s.name}).Debugf("done")
return s.client.Close()
}
func NewSshPool(name, addr, user, key string) (pool.Pool, error) {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
//factory Specify the method to create the connection
factory := func() (interface{}, error) { return NewSsh(name, addr, user, key) }
// close Specify the method to close the connection
close := func(v interface{}) error { return v.(*Ssh).Close() }
// Create a connection pool: Initialize the number of connections to 0, the maximum idle connection is 2, and the maximum concurrent connection is 25
poolConfig := &pool.Config{
InitialCap: 0,
MaxIdle: 2,
MaxCap: 25,
Factory: factory,
Close: close,
//Ping: ping,
//The maximum idle time of the connection, the connection exceeding this time will be closed, which can avoid the problem of automatic failure when connecting to EOF when idle
IdleTimeout: SshInactivityTimeout,
}
return pool.NewChannelPool(poolConfig)
}
func (s *Ssh) Exec(cmd string) (string, error) {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("starting")
defer log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("done")
if err := s.ExecPipe(cmd); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "ssh.ExecPipe", "error": err}).Errorf("")
return "", err
}
defer s.session.Close()
if err := s.session.Wait(); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("")
return "", err
}
buf, err := io.ReadAll(s.out)
if err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "io.ReadAll", "error": err}).Errorf("")
return "", err
}
return string(buf), nil
}
func (s *Ssh) ExecPipe(cmd string) error {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("starting")
defer log.WithFields(log.Fields{"name": s.name, "cmd": cmd}).Debugf("done")
session, err := s.client.NewSession() session, err := s.client.NewSession()
if err != nil { if err != nil {
if *debugFlag { log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "client.NewSession", "error": err}).Errorf("")
log.Printf("SSHConfig.exec : %s : client().NewSession(%s) : %s", s.name, cmd, err) return err
} }
return s.session = session
if s.session.Setenv("TZ", cfg.Timezone); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Setenv", "error": err}).Errorf("")
return err
} }
var buf bytes.Buffer if s.in, err = s.session.StdinPipe(); err != nil {
b = &buf log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdinPipe", "error": err}).Errorf("")
session.Stdout = b s.session.Close()
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
} }
session.Close() if s.out, err = s.session.StdoutPipe(); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StdoutPipe", "error": err}).Errorf("")
s.session.Close()
return err
}
return if s.err, err = s.session.StderrPipe(); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.StderrPipe", "error": err}).Errorf("")
s.session.Close()
return err
}
if err = s.session.Start(cmd); err != nil {
log.WithFields(log.Fields{"name": s.name, "cmd": cmd, "call": "session.Start", "error": err}).Errorf("")
s.session.Close()
return err
}
return nil
} }

76
user.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/scrypt"
)
type User struct {
Username string `json:"username"`
Salt string `json:"salt"`
Passwd string `json:"passwd"`
}
func NewUser(name, passwd string) (*User, error) {
log.WithFields(log.Fields{"name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name}).Debugf("done")
for _, v := range cfg.Admin.Users {
if v.Username == name {
err := errors.New("user already exists")
log.WithFields(log.Fields{"name": name, "error": err}).Errorf("")
return nil, err
}
}
u := &User{
Username: name,
}
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
log.WithFields(log.Fields{"name": name, "call": "rand.Read", "error": err}).Errorf("")
return nil, err
}
u.Salt = hex.EncodeToString(salt)
if pass, err := u.HashPassword(passwd); err != nil {
log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("")
return nil, err
} else {
u.Passwd = hex.EncodeToString(pass)
}
return u, nil
}
func (u *User) HashPassword(passwd string) ([]byte, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
//peppering the pass
hash := hmac.New(sha256.New, []byte(cfg.Admin.Secrets.PasswordPepper))
hash.Write([]byte(passwd))
hashPass := hash.Sum(nil)
//salting the hash
salt := make([]byte, 32)
if _, err := hex.Decode(salt, []byte(u.Salt)); err != nil {
log.WithFields(log.Fields{"call": "hex.Decode", "error": err}).Errorf("")
return nil, err
}
if h, err := scrypt.Key(hashPass, salt, cfg.Admin.Secrets.ScryptN, cfg.Admin.Secrets.ScryptR, cfg.Admin.Secrets.ScryptP, 32); err != nil {
log.WithFields(log.Fields{"call": "scrypt.Key", "error": err}).Errorf("")
return h, err
} else {
return h, nil
}
}

41
utils.go Normal file
View File

@ -0,0 +1,41 @@
package main
import (
"errors"
"regexp"
"strconv"
"time"
log "github.com/sirupsen/logrus"
)
func Expiration(now time.Time, deadline string) (time.Time, error) {
log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("starting")
defer log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("done")
if deadline == "forever" {
return time.Unix(1<<63-1, 0), nil
}
reExpiration := regexp.MustCompile(`([0-9]+)([a-z]+)`)
for _, v := range reExpiration.FindAllStringSubmatch(deadline, -1) {
log.WithFields(log.Fields{"now": now, "deadline": deadline}).Debugf("duration[%d] : %v", len(v), v)
count, _ := strconv.Atoi(v[1])
switch v[2] {
case "y":
now = now.AddDate(count, 0, 0)
case "m":
now = now.AddDate(0, count, 0)
case "d":
now = now.AddDate(0, 0, count)
case "h":
now = now.Add(time.Duration(time.Duration(count) * time.Hour))
default:
err := errors.New("invalid duration")
log.WithFields(log.Fields{"now": now, "deadline": deadline, "attr": v[2], "error": err}).Errorf("")
return time.Now(), err
}
}
return now, nil
}

View File

@ -1,6 +1,7 @@
// Code generated by version.sh (@generated) DO NOT EDIT. // Code generated by version.sh (@generated) DO NOT EDIT.
package main package main
var githash = "7f9cf49" var githash = "1a1713e"
var buildstamp = "2022-10-08_03:14:52" var branch = "v2"
var commits = "54" var buildstamp = "2023-08-21_12:35:47"
var version = "7f9cf49-b54 - 2022-10-08_03:14:52" var commits = "83"
var version = "1a1713e-b83 - 2023-08-21_12:35:47"

View File

@ -1,12 +1,14 @@
# 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 master` commits=`git rev-list --count $branch`
# 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"

342
zfs.go
View File

@ -1,27 +1,329 @@
package main package main
import "sync" import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"regexp"
"sort"
"strings"
"sync"
type ZFSConfig struct { log "github.com/sirupsen/logrus"
SnapshotAdded bool )
SnapshotDeleted bool
SnapshotInitialized bool type BoxZfs struct {
SnapshotList []Snapshot filesystems map[string]*ZfsFs
ZFSAdded bool box *Box
ZFSDeleted bool online bool
ZFSInitialized bool mx sync.Mutex
ZFSMap map[string]string
M sync.Mutex
} }
func NewZFSConfig() (z *ZFSConfig) { type ZfsFs struct {
z = &ZFSConfig{ path string
SnapshotAdded: false, managed bool
SnapshotDeleted: false, backedUp bool
SnapshotInitialized: false, zfs *BoxZfs
ZFSAdded: false, snapshots map[string]*ZfsSnapshot
ZFSDeleted: false, srcApps []*App
ZFSInitialized: false, destApps []*App
mx sync.Mutex
}
type ZfsSnapshot struct {
name string
fs *ZfsFs
}
func (z *BoxZfs) Open() error {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
z.mx.Lock()
defer z.mx.Unlock()
if z.online {
return nil
} }
return
z.filesystems = make(map[string]*ZfsFs)
zfsList, err := z.box.Exec("zfs list -H -t filesystem -o name,mountpoint")
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t filesystem -o name,mountpoint", "error": err}).Errorf("")
return err
}
csvReader := csv.NewReader(bytes.NewBufferString(zfsList))
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 2
csvData, err := csvReader.ReadAll()
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
log.WithFields(log.Fields{"name": z.box.name, "zfs-name": rec[0], "zfs-mount": rec[1]}).Debugf("zfs list -t filesystem")
if rec[1] != "legacy" {
fs := &ZfsFs{
path: rec[0],
zfs: z,
snapshots: make(map[string]*ZfsSnapshot),
}
z.filesystems[rec[0]] = fs
log.WithFields(log.Fields{"name": z.box.name, "fs": rec[0]}).Infof("new filesystem")
}
}
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll"}).Infof("")
zfsList, err = z.box.Exec("zfs list -H -t snapshot -o name")
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs list -H -t snapshot -o name", "error": err}).Errorf("")
return err
}
csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 1
csvData, err = csvReader.ReadAll()
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
log.WithFields(log.Fields{"name": z.box.name, "zfs-snapshot": rec[0]}).Debugf("zfs list -t snapshot")
s := strings.Split(rec[0], `@`)
if fs, ok := z.filesystems[s[0]]; ok {
snap := &ZfsSnapshot{
name: s[1],
fs: fs,
}
fs.snapshots[s[1]] = snap
log.WithFields(log.Fields{"name": z.box.name, "fs": s[0], "snapshot": s[1]}).Infof("new snapshot")
}
}
zfsList, err = z.box.Exec("zfs get -H -o name,value " + zfsManagedPropertyName)
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "attr": "zfs get -H -o name,value,source " + zfsManagedPropertyName, "error": err}).Errorf("")
return err
}
csvReader = csv.NewReader(bytes.NewBufferString(zfsList))
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 2
csvData, err = csvReader.ReadAll()
if err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "csvReader.ReadAll", "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Debugf("zfs get " + zfsManagedPropertyName)
if fs, ok := z.filesystems[rec[0]]; ok {
if rec[1] == "+" {
fs.managed = true
log.WithFields(log.Fields{"name": z.box.name, "zfs-fs": rec[0], "zfs-value": rec[1]}).Infof("managed fs")
}
}
}
z.online = true
return nil
}
func (z *BoxZfs) Close() error {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
z.mx.Lock()
defer z.mx.Unlock()
for _, fs := range z.filesystems {
fs.mx.Lock()
defer fs.mx.Unlock()
}
z.online = false
return nil
}
func (z *BoxZfs) Mkdir(path string) error {
log.WithFields(log.Fields{"name": z.box.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": z.box.name}).Debugf("done")
if !z.online {
err := errors.New("zfs offline")
log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
return err
}
z.mx.Lock()
defer z.mx.Unlock()
b := z.box
if !b.online {
err := errors.New("box offline")
log.WithFields(log.Fields{"name": z.box.name, "error": err}).Errorf("")
return err
}
if _, ok := z.filesystems[path]; ok {
return nil
}
if _, err := b.Exec(fmt.Sprintf("zfs create -p %s", path)); err != nil {
log.WithFields(log.Fields{"name": z.box.name, "call": "Exec", "error": err}).Errorf("")
return err
}
newPath := ""
for _, p := range strings.Split(path, "/") {
if newPath == "" {
newPath = p
} else {
newPath = newPath + "/" + p
}
if _, ok := z.filesystems[newPath]; !ok {
fs := &ZfsFs{
path: newPath,
managed: false,
zfs: z,
snapshots: make(map[string]*ZfsSnapshot),
srcApps: make([]*App, 0),
destApps: make([]*App, 0),
}
z.filesystems[newPath] = fs
}
}
return nil
}
func (fs *ZfsFs) TakeSnapshot(name string) (*ZfsSnapshot, error) {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")
if !fs.zfs.online {
err := errors.New("zfs offline")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return nil, err
}
re := regexp.MustCompile(`^[a-zA-Z0-9\-\._]{1,255}$`)
if !re.MatchString(name) {
err := errors.New("unsupported name")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return nil, err
}
fs.mx.Lock()
defer fs.mx.Unlock()
if _, ok := fs.snapshots[name]; ok {
err := errors.New("already exists")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return nil, err
}
if _, err := fs.zfs.box.Exec("zfs snapshot " + fs.path + "@" + name); err != nil {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return nil, err
}
s := &ZfsSnapshot{
name: name,
fs: fs,
}
fs.snapshots[name] = s
return s, nil
}
func (fs *ZfsFs) DelSnapshot(name string) error {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("done")
if !fs.zfs.online {
err := errors.New("zfs offline")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return err
}
fs.mx.Lock()
defer fs.mx.Unlock()
if _, ok := fs.snapshots[name]; !ok {
err := errors.New("doesn't exist")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return err
}
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name}).Debugf("zfs destroy " + fs.path + "@" + name)
if _, err := fs.zfs.box.Exec("zfs destroy " + fs.path + "@" + name); err != nil {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": name, "error": err}).Errorf("")
return err
}
delete(fs.snapshots, name)
return nil
}
func (fs *ZfsFs) AddSnapshot(s *ZfsSnapshot) error {
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("starting")
defer log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name}).Debugf("done")
if !fs.zfs.online {
err := errors.New("zfs offline")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
return err
}
fs.mx.Lock()
defer fs.mx.Unlock()
if _, ok := fs.snapshots[s.name]; ok {
err := errors.New("already exist")
log.WithFields(log.Fields{"box": fs.zfs.box.name, "fs": fs.path, "name": s.name, "error": err}).Errorf("")
return err
}
fs.snapshots[s.name] = s
return nil
}
func (fs *ZfsFs) ValidSnapshots() []*ZfsSnapshot {
tab := make([]*ZfsSnapshot, 0)
for _, s := range fs.snapshots {
if s.Valid() {
tab = append(tab, s)
}
}
sort.Slice(tab, func(i, j int) bool {
ti, _ := tab[i].Timestamp()
tj, _ := tab[j].Timestamp()
return ti.Before(tj)
})
return tab
} }