Compare commits

...

30 Commits

Author SHA1 Message Date
e7ed6cb2fd Merge pull request 'v2' (#2) from v2 into master
Reviewed-on: #2
2023-08-21 20:03:11 +02:00
shoopea
f616c4ccf9 start using ssh pipes 2023-08-21 14:37:47 +02:00
shoopea
1a1713eb14 update http 2023-08-20 16:57:53 +02:00
shoopea
b07a74543b tweak debug message for app success 2023-08-01 11:18:00 +02:00
shoopea
ab4bfd077b reset cron schedule to correct value 2023-08-01 10:09:37 +02:00
shoopea
9c152943cd send mail if debug on 2023-08-01 09:35:45 +02:00
shoopea
8ecde237b1 use proper schedule for cron 2023-08-01 01:22:59 +02:00
shoopea
54a16bd410 add cron start 2023-08-01 00:15:54 +02:00
shoopea
c72f903e31 add /run 2023-07-31 20:47:24 +02:00
shoopea
f6fdd83a23 set addr in admin 2023-07-31 18:19:28 +02:00
shoopea
ca3d8177bd test cron 2023-07-31 18:11:29 +02:00
shoopea
f51d7f4f51 wrong tag used for zfs managed detection 2023-07-31 11:23:50 +02:00
shoopea
da53fcf8b5 debug 2023-07-31 11:16:11 +02:00
shoopea
4a0f110663 debug 2023-07-31 11:10:41 +02:00
shoopea
f4487acb47 taking wrong box for cleanup 2023-07-31 10:46:02 +02:00
shoopea
88220d47e6 crash fix for offline boxes 2023-07-31 10:34:14 +02:00
shoopea
138786855f debug 2023-07-31 10:30:59 +02:00
shoopea
2afd699099 sort email output 2023-07-31 10:20:31 +02:00
shoopea
b4d27e0867 use managed attribute 2023-07-31 10:13:36 +02:00
shoopea
24a5e6676f fix crash for new app with no dest path existing 2023-07-01 22:34:04 +02:00
shoopea
e07c1c4f3f fix typo for schedule name 2023-07-01 14:10:35 +02:00
shoopea
db05789cfe remove sources invalid snapshots 2023-07-01 00:15:31 +02:00
shoopea
c372d43104 use hujson for config file 2023-06-30 22:04:22 +02:00
shoopea
aa8d20370b miscounted snapshots to add 2023-06-30 00:07:07 +02:00
shoopea
89ae6ea612 add quiet flag 2023-06-29 23:46:53 +02:00
shoopea
f94f9b9f71 transfer blocked by wrong comparison 2023-06-29 23:42:46 +02:00
shoopea
0e0e5db303 actually delete invalid snapshots 2023-06-29 23:25:34 +02:00
shoopea
3c785dec8b update sanity check when parsing config 2023-06-29 23:20:46 +02:00
shoopea
7e8a34a435 del invalid snapshots before/after running the app 2023-06-29 23:13:20 +02:00
shoopea
35e234533c revamp 2023-06-29 22:58:24 +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 (
"flag"
"fmt"
"log"
"os"
"time"
log "github.com/sirupsen/logrus"
)
var (
appFlag = flag.String("app", "", "run specific app")
cfgFile = flag.String("config", "config.json", "config file")
schedFlag = flag.String("schedule", "", "specific schedule")
slowFlag = flag.Bool("slow", false, "slow process")
testFlag = flag.Bool("test", false, "test run")
debugFlag = flag.Bool("debug", false, "debug")
testMailFlag = flag.Bool("test-mail", false, "test email setup")
stopOnErrorFlag = flag.Bool("stop-on-error", false, "stop processing on error")
cfg Config
email *Email
cfgFile = flag.String("config", "", "config file")
isDaemon = flag.Bool("daemon", false, "run as daemon")
debug = flag.Bool("debug", false, "log debug messages")
quiet = flag.Bool("quiet", false, "remove most log messages")
logFile = flag.String("logfile", "", "log file")
cfg *Config
)
func main() {
@ -26,70 +22,50 @@ func main() {
fmt.Printf("backup (%s)\n", version)
email = new(Email)
email.startTime = time.Now()
email.items = make([]string, 0)
err := cfg.Load()
if err != nil {
log.Printf("Cannot load config (%s)", err)
os.Exit(1)
if *debug {
log.SetLevel(log.DebugLevel)
}
if *quiet {
log.SetLevel(log.WarnLevel)
}
if *logFile != "" {
if f, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil {
log.Printf("Cannot open logfile (%s)", err)
} else {
log.SetOutput(f)
defer f.Close()
}
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)
}
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
import (
"bytes"
"encoding/csv"
"fmt"
"log"
"strings"
"errors"
"regexp"
"sync"
"github.com/silenceper/pool"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
)
type Box struct {
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
Name string `json:"-"`
ssh *SSHConfig
zfs *ZFSConfig
name string
addr string
user string
key string
zfs *BoxZfs
sshPool pool.Pool
created bool
online bool
allowDirectConnect bool
mx sync.Mutex
}
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
if *debugFlag {
log.Printf("Box.ZFSTakeSnapshot : %s : Taking snapshot on %s for %s", b.Name, path, schedule)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
_, err = b.ssh.exec("zfs snapshot " + path + "@" + name)
if err != nil {
return
}
b.zfs.SnapshotAdded = true
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(path+"@"+name))
return
type BoxSshPool struct {
signer ssh.Signer
config *ssh.ClientConfig
client *ssh.Client
logged bool
mx sync.Mutex
}
func (b *Box) ZFSGetLastSnapshot(path string) (last Snapshot, err error) {
if *debugFlag {
log.Printf("Box.ZFSGetLastSnapshot : %s : Start %s (%d snapshots)", b.Name, path, len(b.zfs.SnapshotList))
func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err error) {
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
re := regexp.MustCompile(boxNamePattern)
if !re.MatchString(name) {
err := errors.New("invalid name")
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
return nil, err
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.SnapshotInitialize()
p, err := NewSshPool(name, addr, user, key)
if err != nil {
return last, err
log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
return nil, err
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
b = &Box{
name: name,
addr: addr,
user: user,
key: key,
zfs: &BoxZfs{
online: false,
},
sshPool: p,
online: false,
created: true,
allowDirectConnect: true, //FIXME use direct
}
for _, v := range b.zfs.SnapshotList {
if v.Path() == path {
last = v
}
}
if len(string(last)) == 0 {
err = fmt.Errorf("no snapshot")
}
return
b.zfs.box = b
return b, nil
}
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) {
if *debugFlag {
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src))
}
func (b *Box) Open() error {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
if !b.online {
err = fmt.Errorf("box offline")
return
}
b.mx.Lock()
defer b.mx.Unlock()
err = b.SnapshotInitialize()
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 {
if b.online {
return nil
}
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
}
b.zfs.SnapshotList = make([]Snapshot, 0)
var buf *bytes.Buffer
buf, err = b.SSHExec("zfs list -H -t snapshot -o name")
csvReader := csv.NewReader(buf)
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 1
csvData, err := csvReader.ReadAll()
hostname, err := b.Exec("hostname")
if err != nil {
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
}
log.WithFields(log.Fields{"name": b.name, "call": "Exec", "attr": "hostname", "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
}
log.WithFields(log.Fields{"name": b.name}).Debugf("hostname : %s", hostname)
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
}
b.online = true
b.zfs.SnapshotInitialized = true
b.zfs.SnapshotAdded = false
b.zfs.SnapshotDeleted = false
return nil
}
func (b *Box) ZFSUpdateList() (err error) {
if *debugFlag {
log.Printf("Box.ZFSUpdateList : %s : Start", b.Name)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
b.zfs.M.Lock()
if b.zfs.ZFSDeleted || b.zfs.ZFSAdded {
b.zfs.ZFSInitialized = false
}
b.zfs.M.Unlock()
err = b.ZFSInitialize()
return
}
func (b *Box) ZFSIsZFS(path string) bool {
if *debugFlag {
log.Printf("Box.ZFSIsZFS : %s : Start %s", b.Name, path)
}
if !b.online {
return false
}
err := b.ZFSInitialize()
if err != nil {
return false
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
if _, ok := b.zfs.ZFSMap[path]; ok {
return true
}
return false
}
func (b *Box) ZFSCreateZFS(path string) (err error) {
if *debugFlag {
log.Printf("Box.ZFSCreateZFS : %s : Start %s", b.Name, path)
}
if !b.online {
err = fmt.Errorf("box offline")
return
}
err = b.ZFSInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
p := strings.Split(path, `/`)
var base string
for _, d := range p {
if base == "" {
base = d
} 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)
}
if err := b.zfs.Open(); err != nil {
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Open", "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
b.zfs.ZFSMap[rec[0]] = rec[1]
return nil
}
func (b *Box) Close() error {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
b.mx.Lock()
defer b.mx.Unlock()
if !b.online {
return nil
}
b.zfs.ZFSInitialized = true
b.zfs.ZFSAdded = false
b.zfs.ZFSDeleted = false
if err := b.zfs.Close(); err != nil {
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Close", "error": err}).Errorf("")
return err
}
b.online = false
return nil
}
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) {
if !b.online {
err = fmt.Errorf("box offline")
return
func (b *Box) Exec(cmd string) (r string, err error) {
log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("done")
if !b.created {
err := errors.New("box not initialized")
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
return "", err
}
buf, err = b.ssh.exec(cmd)
return
v, err := b.sshPool.Get()
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 {
s := strings.Split(string(b.Addr), `:`)
return s[0]
func TransferZfs(from, to Addr) error {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
var (
err error
fromSnapshots, toSnapshots []*ZfsSnapshot
directTransfer bool
)
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
directTransfer = true
} else {
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
import (
"bytes"
_ "embed"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"regexp"
"sync"
"time"
"golang.org/x/crypto/ssh"
log "github.com/sirupsen/logrus"
"github.com/tailscale/hujson"
)
type Config struct {
Zfsnap map[string]string `json:"zfsnap"`
Box map[string]*Box `json:"box"`
ScheduleDuration map[string]string `json:"schedule"`
Box map[string]BoxConfig `json:"box"`
Email EmailConfig `json:"email"`
Apps []AppConfig `json:"apps"`
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
func (c *Config) Load() error {
if *debugFlag {
log.Printf("SSHConfig.Load : Start")
}
b, err := ioutil.ReadFile(*cfgFile)
var (
cfgMx sync.Mutex
cfgRun bool
)
//go:embed assets/backup.sample.json
var sampleCfg []byte
type BoxConfig struct {
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
AllowDirectConnect bool `json:"allow_direct_connect"`
}
type AppConfig struct {
Name string `json:"name"`
Schedule []string `json:"schedule"`
Sources []string `json:"src"`
Destinations []string `json:"dest"`
Before map[string]string `json:"before"`
After map[string]string `json:"after"`
Active bool `json:"active"`
}
// Load config from file
func LoadConfigFile(path string) (*Config, error) {
log.WithFields(log.Fields{"path": path}).Debugf("starting")
defer log.WithFields(log.Fields{"path": path}).Debugf("done")
b, err := os.ReadFile(path)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
}
return err
log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
return nil, err
}
err = json.Unmarshal(b, &c)
return LoadConfigByte(b)
}
// Load config from string
func LoadConfigByte(conf []byte) (*Config, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
c := &Config{}
if err := json.Unmarshal(sampleCfg, c); err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
return nil, err
}
b, err := hujson.Standardize(conf)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : json.Unmarshal : %s", err)
}
return err
log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
return nil, err
}
if *debugFlag {
log.Printf("Config.Load :\r\n%v", cfg)
if err := json.Unmarshal(b, c); err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
return nil, err
}
l, err := time.LoadLocation(cfg.Timezone)
c.timezone, err = time.LoadLocation(c.Timezone)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : time.LoadLocation : %s", err)
}
return err
log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
return nil, err
}
if len(cfg.Email.SmtpHost) == 0 {
if *debugFlag {
log.Printf("Config.Load : no smtp")
}
return fmt.Errorf("no smtp")
if c.Email.Active {
if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(cfg.Email.FromEmail) == 0 {
if *debugFlag {
log.Printf("Config.Load : no email from")
}
return fmt.Errorf("no email from")
if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(cfg.Email.ToEmail) == 0 {
if *debugFlag {
log.Printf("Config.Load : no email to")
if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
return fmt.Errorf("no 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 {
v.Name = k
v.online = false
v.zfs = NewZFSConfig()
s := &SSHConfig{
logged: false,
name: k,
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
return nil, err
} else {
if _, ok := c.box[k]; ok {
err := errors.New("already exists")
log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
return nil, err
}
v.ssh = s
keyRaw, err := ioutil.ReadFile(v.Key)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
c.box[k] = b
}
return err
}
key, err := ssh.ParseRawPrivateKey(keyRaw)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
c.apps = make(map[string]*App)
for _, v := range c.Apps {
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
return nil, err
} else {
if _, ok := c.apps[v.Name]; ok {
err := errors.New("app already exists")
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
return nil, err
}
return err
}
s.signer, err = ssh.NewSignerFromKey(key)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err)
}
return err
}
s.config = &ssh.ClientConfig{
User: v.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(s.signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
Timeout: 5 * time.Second,
}
s.client, err = ssh.Dial("tcp", v.Addr, s.config)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ssh.Dial(%s) : %s", k, err)
c.apps[v.Name] = a
for k := range a.schedule {
if dur, ok := c.ScheduleDuration[k]; ok {
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
if !re.MatchString(dur) {
err := errors.New("incorrect schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
} else {
v.online = true
session, err := s.client.NewSession()
if err != nil {
if *debugFlag {
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
err := errors.New("undefined schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, 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
func (c *Config) Close() error {
return nil
// Run config
func (c *Config) Run() {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if cfgRun {
return
}
cfgMx.Lock()
defer cfgMx.Unlock()
cfgRun = true
defer func() { cfgRun = false }()
e := NewEmail(time.Now())
var wg sync.WaitGroup
// Setup boxes
for _, b := range c.box {
wg.Add(1)
go func(box *Box) {
defer wg.Done()
if err := box.Open(); err != nil {
log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("")
e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name))
return
}
}(b)
}
wg.Wait()
// Run each app
for _, a := range cfg.apps {
wg.Add(1)
go func(app *App) {
if sched, err := app.Run(e.startTime); err != nil {
e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
} else if *debug {
if sched != "" {
e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched))
} else {
e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name))
}
}
wg.Done()
}(a)
}
wg.Wait()
// Cleanup
for _, a := range cfg.apps {
for _, src := range a.sources {
if b, ok := c.box[src.Box()]; ok {
if fs, ok := b.zfs.filesystems[src.Path()]; ok {
fs.srcApps = append(fs.srcApps, a)
}
}
for _, dest := range a.destinations {
if b, ok := c.box[dest.Box()]; ok {
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
fs.destApps = append(fs.destApps, a)
} else {
e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String()))
}
}
}
}
}
for _, b := range cfg.box {
if b.online {
for _, fs := range b.zfs.filesystems {
if len(fs.srcApps) > 0 && !fs.backedUp {
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up")
e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path))
}
if len(fs.destApps) == 0 && !fs.backedUp && fs.managed {
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed")
e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path))
}
}
}
}
// Stop
for _, b := range c.box {
if err := b.Close(); err != nil {
log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("")
}
}
if len(e.items) > 0 {
if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil {
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
}
}
return
}

13
const.go Normal file
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 (
"encoding/base64"
"log"
"fmt"
"net/smtp"
"sort"
"strings"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
type Email struct {
startTime time.Time
items []string
mx sync.Mutex
}
type EmailConfig struct {
Active bool `json:"active"`
SmtpHost string `json:"smtp"`
FromEmail string `json:"email_from"`
ToEmail []string `json:"email_to"`
}
func SendMail(addr, from, subject, body string, to []string) error {
if *debugFlag {
log.Printf("SendMail : Start")
func NewEmail(now time.Time) *Email {
log.WithFields(log.Fields{"now": now}).Debugf("starting")
defer log.WithFields(log.Fields{"now": now}).Debugf("done")
return &Email{startTime: now, items: make([]string, 0)}
}
func (e *Email) AddItem(item string) {
log.WithFields(log.Fields{"item": item}).Debugf("starting")
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
e.items = append(e.items, item)
}
func (e *Email) Send(addr, from string, to []string) error {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
if len(e.items) == 0 {
return nil
}
sort.Slice(e.items, func(i, j int) bool {
return e.items[i] < e.items[j]
})
body := e.items[0]
for _, item := range e.items[1:] {
body = body + "\r\n" + item
}
subject := fmt.Sprintf("Autobackup report (%s)", e.startTime)
if err := SendMail(addr, from, subject, body, to); err != nil {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "SendMail", "error": err}).Errorf("")
return err
}
return nil
}
func SendMail(addr, from, subject, body string, to []string) error {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("starting")
defer log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("done")
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
c, err := smtp.Dial(addr)
if err != nil {
if *debugFlag {
log.Printf("SendMail : %s : smtp.Dial (%s)", addr, err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "smtp.Dial", "error": err}).Errorf("")
return err
}
defer c.Close()
if err = c.Mail(r.Replace(from)); err != nil {
if *debugFlag {
log.Printf("SendMail : %s : client.Mail (%s)", from, err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Mail", "error": err}).Errorf("")
return err
}
for i := range to {
to[i] = r.Replace(to[i])
if err = c.Rcpt(to[i]); err != nil {
if *debugFlag {
log.Printf("SendMail : %s : client.Rcpt (%s)", to[i], err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Rcpt", "attr": to[i], "error": err}).Errorf("")
return err
}
}
w, err := c.Data()
if err != nil {
if *debugFlag {
log.Printf("SendMail : client.Data (%s)", err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
return err
}
@ -68,29 +108,21 @@ func SendMail(addr, from, subject, body string, to []string) error {
"Content-Transfer-Encoding: base64\r\n" +
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
if *debugFlag {
log.Printf("SendMail :\r\n%s", msg)
}
_, err = w.Write([]byte(msg))
if err != nil {
if *debugFlag {
log.Printf("SendMail : writer.Write (%s)", err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
return err
}
err = w.Close()
if err != nil {
if *debugFlag {
log.Printf("SendMail : writer.Close (%s)", err)
}
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
return err
}
err = c.Quit()
if *debugFlag {
log.Printf("SendMail : client.Quit (%s)", err)
}
if err = c.Quit(); err != nil {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Quit", "error": err}).Errorf("")
return err
}
return nil
}

9
go.mod
View File

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

131
go.sum
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/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

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

171
ssh.go
View File

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

76
user.go Normal file
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.
package main
var githash = "7f9cf49"
var buildstamp = "2022-10-08_03:14:52"
var commits = "54"
var version = "7f9cf49-b54 - 2022-10-08_03:14:52"
var githash = "1a1713e"
var branch = "v2"
var buildstamp = "2023-08-21_12:35:47"
var commits = "83"
var version = "1a1713e-b83 - 2023-08-21_12:35:47"

View File

@ -1,12 +1,14 @@
# Get the version.
githash=`git rev-parse --short HEAD`
branch=`git rev-parse --abbrev-ref HEAD`
buildstamp=`date -u '+%Y-%m-%d_%H:%M:%S'`
commits=`git rev-list --count master`
commits=`git rev-list --count $branch`
# Write out the package.
cat << EOF > version.go
// Code generated by version.sh (@generated) DO NOT EDIT.
package main
var githash = "$githash"
var branch = "$branch"
var buildstamp = "$buildstamp"
var commits = "$commits"
var version = "$githash-b$commits - $buildstamp"

342
zfs.go
View File

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