Compare commits

..

59 Commits

Author SHA1 Message Date
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
shoopea
fb02d525d2 update email encoding 2022-10-08 11:15:09 +08:00
shoopea
7f9cf4908e bump 2022-10-08 10:58:17 +08:00
shoopea
a7ee0bfeb7 update email and stop on error 2022-10-08 10:49:28 +08:00
shoopea
f36e7da824 change box addr for zfs send 2022-09-07 22:21:03 +08:00
shoopea
8a6fced9c8 version bump 2022-06-19 12:33:37 +08:00
shoopea
fcc9fa3f5c fix fields not exported in json 2022-06-19 12:33:24 +08:00
shoopea
55d1c2330d debug 2022-06-19 12:31:36 +08:00
shoopea
f2509f0ca6 config test 2022-06-19 12:27:25 +08:00
shoopea
a5b2d51cfb more debug 2022-06-19 12:20:27 +08:00
shoopea
f3abe27055 more debug 2022-06-19 12:18:34 +08:00
shoopea
db052048b6 more debug 2022-06-19 12:13:52 +08:00
shoopea
493715555d test email 2022-06-19 12:12:00 +08:00
shoopea
86ddd52642 debug email 2022-06-19 11:59:17 +08:00
shoopea
7c309b850b only send with online boxes 2022-06-17 22:11:20 +08:00
shoopea
9594fd7970 refresh snapshot only on online boxes 2022-06-17 22:07:37 +08:00
shoopea
23d7d90d20 cleanup only when source is up 2022-06-17 22:03:12 +08:00
shoopea
28bae81236 typo 2022-06-17 21:57:32 +08:00
shoopea
93169ffb23 missing recv -F 2022-06-17 21:54:58 +08:00
shoopea
324710825d force zfs recv when sending incrementally too 2022-06-17 21:52:39 +08:00
shoopea
4d24ded4d7 do not try to backup app when one source offline 2022-06-17 21:44:25 +08:00
shoopea
e62627e406 test email and clean first snapshots 2022-06-17 20:54:14 +08:00
shoopea
41d56eb90a go straight to last snapshot 2022-05-21 23:22:39 +08:00
shoopea
2ae1737081 fix 2022-04-16 21:30:47 +08:00
shoopea
ed261141f5 fix 2022-04-16 21:11:05 +08:00
shoopea
cddb2223e9 add timeout 2022-04-16 21:09:06 +08:00
shoopea
fd62711571 allow offline box 2022-04-16 20:57:45 +08:00
shoopea
762c538607 up version 2022-04-16 15:16:34 +08:00
shoopea
0083ab6907 path creation fix 2022-04-16 15:16:23 +08:00
shoopea
b511eecec3 fix mutex 2021-11-14 18:19:37 +08:00
shoopea
d2e5b34b64 update debug 2021-11-14 18:14:57 +08:00
66 changed files with 61174 additions and 1249 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
config.json
backup

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")
}
}

1106
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>

View File

@@ -3,18 +3,18 @@ package main
import (
"flag"
"fmt"
"log"
"os"
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")
cfg Config
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() {
@@ -22,50 +22,50 @@ func main() {
fmt.Printf("backup (%s)\n", version)
err := cfg.Load()
if err != nil {
log.Printf("Cannot load config (%s)", err)
os.Exit(1)
if *debug {
log.SetLevel(log.DebugLevel)
}
err = RunBackup(*appFlag)
if err != nil {
log.Printf("Cannot run schedule (%s)", err)
os.Exit(1)
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()
}
//RunBackup run all backup targets where schedule is registered
func RunBackup(app string) 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)
}
return err
}
}
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 {
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
}
}
}
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)
}
return nil
}

473
box.go
View File

@@ -1,353 +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)
}
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
}
err = b.SnapshotInitialize()
p, err := NewSshPool(name, addr, user, key)
if err != nil {
return
log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
return nil, err
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
b = &Box{
name: name,
addr: addr,
user: user,
key: key,
zfs: &BoxZfs{
online: false,
},
sshPool: p,
online: false,
created: true,
allowDirectConnect: true, //FIXME use direct
}
for _, v := range b.zfs.SnapshotList {
if v.Path() == path {
last = v
}
}
if len(string(last)) == 0 {
err = fmt.Errorf("no snapshot")
}
return
b.zfs.box = b
return b, nil
}
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) {
if *debugFlag {
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src))
func (b *Box) Open() error {
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
b.mx.Lock()
defer b.mx.Unlock()
if b.online {
return nil
}
err = b.SnapshotInitialize()
hostname, err := b.Exec("hostname")
if err != nil {
return
log.WithFields(log.Fields{"name": b.name, "call": "Exec", "attr": "hostname", "error": err}).Errorf("")
return err
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
log.WithFields(log.Fields{"name": b.name}).Debugf("hostname : %s", hostname)
if *debugFlag {
log.Printf("SSHConfig.isLastSnapshot : %s : Start %s", b.Name, string(src))
b.online = true
if err := b.zfs.Open(); err != nil {
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Open", "error": err}).Errorf("")
return err
}
_, err = b.ZFSGetNextSnapshot(src)
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
}
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) Exec(cmd string) (r string, err error) {
log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("starting")
defer log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("done")
if !b.created {
err := errors.New("box not initialized")
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
return "", err
}
v, err := b.sshPool.Get()
if err != nil {
if err.Error() == "no snapshot" {
is = true
err = nil
}
log.WithFields(log.Fields{"name": b.name, "error": err, "call": "SshPool.Get"}).Errorf("")
return "", err
}
defer b.sshPool.Put(v)
s := v.(*Ssh)
return s.Exec(cmd)
}
func TransferZfs(from, to Addr) error {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
var (
err error
fromSnapshots, toSnapshots []*ZfsSnapshot
directTransfer bool
)
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
directTransfer = true
} else {
is = false
}
return
}
func (b *Box) ZFSGetFirstSnapshot(path string) (first Snapshot, err error) {
if *debugFlag {
log.Printf("SSHConfig.getFirstSnapshot : %s : Start %s", b.Name, path)
directTransfer = false
}
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))
}
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)
}
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)
}
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) {
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
if b.zfs.SnapshotInitialized {
return nil
}
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
}
b.zfs.SnapshotList = make([]Snapshot, 0)
var buf *bytes.Buffer
buf, err = b.SSHExec("zfs list -H -t snapshot -o name")
csvReader := csv.NewReader(buf)
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 1
csvData, err := csvReader.ReadAll()
if err != nil {
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
}
if fromSnapshots, err = from.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
if len(fromSnapshots) == 0 {
return nil
}
if *debugFlag {
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
if toSnapshots, err = to.ValidSnapshots(); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": to, "error": err}).Errorf("")
return err
}
b.zfs.SnapshotInitialized = true
b.zfs.SnapshotAdded = false
b.zfs.SnapshotDeleted = false
return nil
}
func (b *Box) ZFSUpdateList() (err error) {
if *debugFlag {
log.Printf("Box.ZFSUpdateList : %s : Start", b.Name)
}
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)
}
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)
}
err = b.ZFSInitialize()
if err != nil {
return
}
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
p := strings.Split(path, `/`)
var base string
for _, d := range p {
if base == "" {
base = d
if len(toSnapshots) == 0 {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
if directTransfer {
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
return err
}
newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}
toSnapshots = append(toSnapshots, newToSnapshot)
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot)
} else {
base = base + `/` + d
//handle indirect transfer
}
if _, ok := b.zfs.ZFSMap[base]; !ok {
if *debugFlag {
log.Printf("Box.ZFSCreateZFS : Creating %s:%s", b.Name, base)
}
}
_, err = b.SSHExec("zfs create -o mountpoint=none " + base)
if err != nil {
if *debugFlag {
log.Printf("Box.ZFSCreateZFS : %s : SSHExec : %s", b.Name, err)
}
return
}
b.zfs.ZFSMap[base] = "none"
b.zfs.ZFSAdded = true
fromFromSnapshotId := -1
lastToSnapshot := toSnapshots[len(toSnapshots)-1]
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("searching last snapshot %s", lastToSnapshot.String())
for id, v := range fromSnapshots {
if v.name == lastToSnapshot.name {
fromFromSnapshotId = id
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("found %s", v.String())
break
}
}
return
}
func (b *Box) ZFSInitialize() (err error) {
b.zfs.M.Lock()
defer b.zfs.M.Unlock()
if b.zfs.ZFSInitialized {
return nil
}
if *debugFlag {
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
}
b.zfs.ZFSMap = make(map[string]string)
var buf *bytes.Buffer
buf, err = b.SSHExec("zfs list -H -o name,mountpoint")
csvReader := csv.NewReader(buf)
csvReader.Comma = '\t'
csvReader.FieldsPerRecord = 2
csvData, err := csvReader.ReadAll()
if err != nil {
if *debugFlag {
log.Printf("Box.ZFSInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
}
if fromFromSnapshotId == -1 {
err := errors.New("zfs snapshot unsync")
log.WithFields(log.Fields{"from": from, "to": to, "error": err}).Errorf("")
return err
}
for _, rec := range csvData {
b.zfs.ZFSMap[rec[0]] = rec[1]
}
if fromFromSnapshotId < len(fromSnapshots)-1 {
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("transfering from %s to %s", fromSnapshots[fromFromSnapshotId].name, fromSnapshots[len(fromSnapshots)-1].name)
if directTransfer {
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send -I " + fromSnapshots[fromFromSnapshotId].String() + " " + fromSnapshots[len(fromSnapshots)-1].String() + " | zfs recv -F " + to.Path()); err != nil {
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
return err
}
} else {
// handle indirect transfer
}
b.zfs.ZFSInitialized = true
b.zfs.ZFSAdded = false
b.zfs.ZFSDeleted = false
for _, v := range fromSnapshots[fromFromSnapshotId+1:] {
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(&ZfsSnapshot{name: v.name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]})
}
}
return nil
}
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) {
buf, err = b.ssh.exec(cmd)
return
}

395
config.go
View File

@@ -1,182 +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"`
Apps []AppConfig `json:apps`
Timezone string `json:"timezone"`
Now time.Time `json:"-"`
ScheduleDuration map[string]string `json:"schedule"`
Box map[string]BoxConfig `json:"box"`
Email EmailConfig `json:"email"`
Apps []AppConfig `json:"apps"`
Timezone string `json:"timezone"`
Admin *AdminConfig `json:"admin"`
Debug bool `json:"debug"`
box map[string]*Box `json:"-"`
apps map[string]*App `json:"-"`
timezone *time.Location `json:"-"`
}
//Load config from file
func (c *Config) Load() error {
if *debugFlag {
log.Printf("SSHConfig.Load : Start")
}
b, err := ioutil.ReadFile(*cfgFile)
var (
cfgMx sync.Mutex
cfgRun bool
)
//go:embed assets/backup.sample.json
var sampleCfg []byte
type BoxConfig struct {
Addr string `json:"addr"`
User string `json:"user"`
Key string `json:"key"`
AllowDirectConnect bool `json:"allow_direct_connect"`
}
type AppConfig struct {
Name string `json:"name"`
Schedule []string `json:"schedule"`
Sources []string `json:"src"`
Destinations []string `json:"dest"`
Before map[string]string `json:"before"`
After map[string]string `json:"after"`
Active bool `json:"active"`
}
// Load config from file
func LoadConfigFile(path string) (*Config, error) {
log.WithFields(log.Fields{"path": path}).Debugf("starting")
defer log.WithFields(log.Fields{"path": path}).Debugf("done")
b, err := os.ReadFile(path)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
}
return err
log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
return nil, err
}
err = json.Unmarshal(b, &c)
return LoadConfigByte(b)
}
// Load config from string
func LoadConfigByte(conf []byte) (*Config, error) {
log.WithFields(log.Fields{}).Debugf("starting")
defer log.WithFields(log.Fields{}).Debugf("done")
c := &Config{}
if err := json.Unmarshal(sampleCfg, c); err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
return nil, err
}
b, err := hujson.Standardize(conf)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : json.Unmarshal : %s", err)
}
return err
log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
return nil, err
}
l, err := time.LoadLocation(cfg.Timezone)
if err := json.Unmarshal(b, c); err != nil {
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
return nil, err
}
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
}
c.Now = time.Now().In(l)
if c.Email.Active {
if len(c.Email.SmtpHost) == 0 {
err := fmt.Errorf("no smtp")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.FromEmail) == 0 {
err := fmt.Errorf("no email from")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
if len(c.Email.ToEmail) == 0 {
err := fmt.Errorf("no email to")
log.WithFields(log.Fields{"error": err}).Errorf("")
return nil, err
}
}
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.zfs = NewZFSConfig()
s := &SSHConfig{
logged: false,
name: k,
}
v.ssh = s
keyRaw, err := ioutil.ReadFile(v.Key)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
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
}
return err
c.box[k] = b
}
key, err := ssh.ParseRawPrivateKey(keyRaw)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
}
return err
}
s.signer, err = ssh.NewSignerFromKey(key)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err)
}
return err
}
s.config = &ssh.ClientConfig{
User: v.User,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(s.signer),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}
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)
}
return err
}
session, err := s.client.NewSession()
if err != nil {
if *debugFlag {
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
}
return err
}
var b bytes.Buffer
session.Stdout = &b
err = session.Run("TZ=\"" + cfg.Timezone + "\" zfsnap --version")
if err != nil {
if *debugFlag {
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
}
return err
}
if *debugFlag {
log.Printf("Config.Load : logged into %s : %s", k, b.String())
}
session.Close()
s.logged = true
}
for _, app := range c.Apps {
for _, src := range app.Sources {
if !src.Valid() {
return fmt.Errorf("Source not valid : %s", string(src))
c.apps = make(map[string]*App)
for _, v := range c.Apps {
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
return nil, err
} else {
if _, ok := c.apps[v.Name]; ok {
err := errors.New("app already exists")
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
return nil, err
}
if _, ok := cfg.Box[src.Box()]; !ok {
return fmt.Errorf("No box defined for source : %s", string(src))
}
}
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))
}
}
for val, before := range app.Before {
_, err = regexp.Compile(val)
if err != nil {
if *debugFlag {
log.Printf("Config.Load : invalid regex : %s", val)
c.apps[v.Name] = a
for k := range a.schedule {
if dur, ok := c.ScheduleDuration[k]; ok {
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
if !re.MatchString(dur) {
err := errors.New("incorrect schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
} else {
err := errors.New("undefined schedule duration")
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
return nil, err
}
return err
}
if !before.Valid() {
return fmt.Errorf("Before not valid : %s", string(before))
}
if _, ok := cfg.Box[before.Box()]; !ok {
return fmt.Errorf("No box defined for before : %s", string(before))
}
}
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))
}
}
}
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
)

128
email.go Normal file
View File

@@ -0,0 +1,128 @@
package main
import (
"encoding/base64"
"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 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 {
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 {
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 {
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 {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
return err
}
msg := "Date: " + time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700") + "\r\n" +
"To: " + strings.Join(to, ",") + "\r\n" +
"From: " + from + "\r\n" +
"Subject: " + subject + "\r\n" +
"Content-Type: text/plain; charset=\"UTF-8\"\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
_, err = w.Write([]byte(msg))
if err != nil {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
return err
}
err = w.Close()
if err != nil {
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
return 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
}

10
go.mod
View File

@@ -2,4 +2,12 @@ module git.siteop.biz/shoopea/backup
go 1.16
require golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
require (
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
)

137
go.sum
View File

@@ -1,10 +1,143 @@
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg=
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-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
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)
}

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

76
user.go Normal file
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 = "d895b93"
var buildstamp = "2021-11-14_10:09:45"
var commits = "23"
var version = "d895b93-b23 - 2021-11-14_10:09:45"
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
}