Merge pull request 'v2' (#2) from v2 into master
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Vendored
+5002
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+5001
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
@@ -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
+8
@@ -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
+423
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+4743
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
Vendored
+10837
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+10813
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+6748
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+4967
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
+5016
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Vendored
+7
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
<meta name="description" content="">
|
||||
<title>Login :: zBackup</title>
|
||||
|
||||
<!-- Bootstrap core CSS -->
|
||||
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
|
||||
</head>
|
||||
<body class="text-center">
|
||||
<div class="container">
|
||||
<form class="form-signin" method="POST" action="/u/submit">
|
||||
<h1 class="h3 mb-3 font-weight-normal">Sign in</h1>
|
||||
{{if (ne .Error "")}}<div class="alert alert-danger">{{.Error}}</div>{{end}}
|
||||
<div class="form-group">
|
||||
<input type="username" class="form-control" id="username" placeholder="Username" name="username" required autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input type="password" class="form-control" id="pwd" placeholder="Password" name="password" required>
|
||||
</div>
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
|
||||
<p class="mt-5 mb-3 text-muted">© 2023</p>
|
||||
</form>
|
||||
<a href="/u/recover">Lost password</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,22 +3,18 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var (
|
||||
appFlag = flag.String("app", "", "run specific app")
|
||||
cfgFile = flag.String("config", "config.json", "config file")
|
||||
schedFlag = flag.String("schedule", "", "specific schedule")
|
||||
slowFlag = flag.Bool("slow", false, "slow process")
|
||||
testFlag = flag.Bool("test", false, "test run")
|
||||
debugFlag = flag.Bool("debug", false, "debug")
|
||||
testMailFlag = flag.Bool("test-mail", false, "test email setup")
|
||||
stopOnErrorFlag = flag.Bool("stop-on-error", false, "stop processing on error")
|
||||
cfg Config
|
||||
email *Email
|
||||
cfgFile = flag.String("config", "", "config file")
|
||||
isDaemon = flag.Bool("daemon", false, "run as daemon")
|
||||
debug = flag.Bool("debug", false, "log debug messages")
|
||||
quiet = flag.Bool("quiet", false, "remove most log messages")
|
||||
logFile = flag.String("logfile", "", "log file")
|
||||
cfg *Config
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -26,70 +22,50 @@ func main() {
|
||||
|
||||
fmt.Printf("backup (%s)\n", version)
|
||||
|
||||
email = new(Email)
|
||||
email.startTime = time.Now()
|
||||
email.items = make([]string, 0)
|
||||
if *debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
}
|
||||
if *quiet {
|
||||
log.SetLevel(log.WarnLevel)
|
||||
}
|
||||
if *logFile != "" {
|
||||
if f, err := os.OpenFile(*logFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644); err != nil {
|
||||
log.Printf("Cannot open logfile (%s)", err)
|
||||
} else {
|
||||
log.SetOutput(f)
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
err := cfg.Load()
|
||||
if err != nil {
|
||||
log.Printf("Cannot load config (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.SetReportCaller(true)
|
||||
|
||||
if *cfgFile != "" {
|
||||
if c, err := LoadConfigFile(*cfgFile); err != nil {
|
||||
log.Printf("Cannot load config (%s)", err)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
cfg = c
|
||||
}
|
||||
} else if c, err := LoadConfigFile("backup.json"); err == nil {
|
||||
cfg = c
|
||||
} else {
|
||||
cfg, _ = LoadConfigByte(sampleCfg)
|
||||
}
|
||||
|
||||
if *testMailFlag {
|
||||
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "test backup email topic", "test backup email body", cfg.Email.ToEmail)
|
||||
if *isDaemon {
|
||||
if cfg.Admin == nil {
|
||||
cfg.Admin = NewAdmin()
|
||||
}
|
||||
if cfg.Admin.Secrets == nil {
|
||||
cfg.Admin.Secrets = NewSecrets()
|
||||
}
|
||||
if len(cfg.Admin.Users) == 0 {
|
||||
cfg.Admin.NewAdminUser()
|
||||
}
|
||||
cfg.Admin.Run()
|
||||
} else {
|
||||
cfg.Run()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
err = RunBackup(*appFlag, *stopOnErrorFlag)
|
||||
if err != nil {
|
||||
log.Printf("Cannot run schedule (%s)", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(email.items) > 0 {
|
||||
body := " - " + email.items[0]
|
||||
for _, v := range email.items[1:] {
|
||||
body = body + "\r\n" + " - " + v
|
||||
}
|
||||
SendMail(cfg.Email.SmtpHost, cfg.Email.FromEmail, "Autobackup report", body, cfg.Email.ToEmail)
|
||||
log.Printf("Sending summary email\r\n%v", email.items)
|
||||
}
|
||||
}
|
||||
|
||||
//RunBackup run all backup targets where schedule is registered
|
||||
func RunBackup(app string, stopOnError bool) error {
|
||||
if app == "" {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Start")
|
||||
}
|
||||
for _, a := range cfg.Apps {
|
||||
err := a.RunAppBackup()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Error running %s", a.Name)
|
||||
}
|
||||
if stopOnError {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Start %s", app)
|
||||
}
|
||||
for _, a := range cfg.Apps {
|
||||
if a.Name == app {
|
||||
err := a.RunAppBackup()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("RunBackup() : Error running %s", a.Name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,423 +1,222 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"errors"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/silenceper/pool"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type Box struct {
|
||||
Addr string `json:"addr"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"-"`
|
||||
ssh *SSHConfig
|
||||
zfs *ZFSConfig
|
||||
online bool
|
||||
name string
|
||||
addr string
|
||||
user string
|
||||
key string
|
||||
zfs *BoxZfs
|
||||
sshPool pool.Pool
|
||||
created bool
|
||||
online bool
|
||||
allowDirectConnect bool
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Box) ZFSTakeSnapshot(schedule, path string) (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSTakeSnapshot : %s : Taking snapshot on %s for %s", b.Name, path, schedule)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
timestamp := cfg.Now.Format("2006-01-02_15.04.05")
|
||||
name := fmt.Sprintf("%s-%s--%s", schedule, timestamp, cfg.Zfsnap[schedule])
|
||||
_, err = b.ssh.exec("zfs snapshot " + path + "@" + name)
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.SnapshotAdded = true
|
||||
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(path+"@"+name))
|
||||
|
||||
return
|
||||
type BoxSshPool struct {
|
||||
signer ssh.Signer
|
||||
config *ssh.ClientConfig
|
||||
client *ssh.Client
|
||||
logged bool
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetLastSnapshot(path string) (last Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetLastSnapshot : %s : Start %s (%d snapshots)", b.Name, path, len(b.zfs.SnapshotList))
|
||||
func (c *Config) NewBox(name, addr, user, key string, direct bool) (b *Box, err error) {
|
||||
log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": name, "addr": addr, "user": user, "key": key}).Debugf("done")
|
||||
|
||||
re := regexp.MustCompile(boxNamePattern)
|
||||
if !re.MatchString(name) {
|
||||
err := errors.New("invalid name")
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
p, err := NewSshPool(name, addr, user, key)
|
||||
if err != nil {
|
||||
return last, err
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "NewSshPool", "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
b = &Box{
|
||||
name: name,
|
||||
addr: addr,
|
||||
user: user,
|
||||
key: key,
|
||||
zfs: &BoxZfs{
|
||||
online: false,
|
||||
},
|
||||
sshPool: p,
|
||||
online: false,
|
||||
created: true,
|
||||
allowDirectConnect: true, //FIXME use direct
|
||||
}
|
||||
|
||||
for _, v := range b.zfs.SnapshotList {
|
||||
if v.Path() == path {
|
||||
last = v
|
||||
}
|
||||
}
|
||||
if len(string(last)) == 0 {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
}
|
||||
return
|
||||
b.zfs.box = b
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func (b *Box) ZFSIsLastSnapshot(src Snapshot) (is bool, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSIsLastSnapshot : %s : Start %s", b.Name, string(src))
|
||||
func (b *Box) Open() error {
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
|
||||
|
||||
b.mx.Lock()
|
||||
defer b.mx.Unlock()
|
||||
|
||||
if b.online {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostname, err := b.Exec("hostname")
|
||||
if err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "Exec", "attr": "hostname", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("hostname : %s", hostname)
|
||||
|
||||
b.online = true
|
||||
|
||||
if err := b.zfs.Open(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Open", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) Close() error {
|
||||
log.WithFields(log.Fields{"name": b.name}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name}).Debugf("done")
|
||||
|
||||
b.mx.Lock()
|
||||
defer b.mx.Unlock()
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
if err := b.zfs.Close(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "zfs.Close", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = b.ZFSGetNextSnapshot(src)
|
||||
b.online = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) Exec(cmd string) (r string, err error) {
|
||||
log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"name": b.name, "cmd": cmd}).Debugf("done")
|
||||
|
||||
if !b.created {
|
||||
err := errors.New("box not initialized")
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
v, err := b.sshPool.Get()
|
||||
if err != nil {
|
||||
if err.Error() == "no snapshot" {
|
||||
is = true
|
||||
err = nil
|
||||
}
|
||||
log.WithFields(log.Fields{"name": b.name, "error": err, "call": "SshPool.Get"}).Errorf("")
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer b.sshPool.Put(v)
|
||||
s := v.(*Ssh)
|
||||
|
||||
return s.Exec(cmd)
|
||||
}
|
||||
|
||||
func TransferZfs(from, to Addr) error {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"from": from, "to": to}).Debugf("done")
|
||||
|
||||
var (
|
||||
err error
|
||||
fromSnapshots, toSnapshots []*ZfsSnapshot
|
||||
directTransfer bool
|
||||
)
|
||||
|
||||
if cfg.box[from.Box()].allowDirectConnect && cfg.box[to.Box()].allowDirectConnect {
|
||||
directTransfer = true
|
||||
} else {
|
||||
is = false
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetFirstSnapshot(path string) (first Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetFirstSnapshot : %s : Start %s", b.Name, path)
|
||||
directTransfer = false
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
for _, v := range b.zfs.SnapshotList {
|
||||
if v.Path() == path {
|
||||
first = v
|
||||
return
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetNextSnapshot(src Snapshot) (next Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetNextSnapshot : %s : Start %s", b.Name, string(src))
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
for id, v := range b.zfs.SnapshotList {
|
||||
if v == src {
|
||||
if len(b.zfs.SnapshotList) > id+1 {
|
||||
next = b.zfs.SnapshotList[id+1]
|
||||
if next.Path() == src.Path() {
|
||||
return
|
||||
} else {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
err = fmt.Errorf("no snapshot")
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSUpdateSnapshotList() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSUpdateSnapshotList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
if b.zfs.SnapshotDeleted || b.zfs.SnapshotAdded {
|
||||
b.zfs.SnapshotInitialized = false
|
||||
}
|
||||
b.zfs.M.Unlock()
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSGetSnapshotList() (snaps []Snapshot, err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSGetSnapshotList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.SnapshotInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
snaps = b.zfs.SnapshotList
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) SnapshotInitialize() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
if b.zfs.SnapshotInitialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
b.zfs.SnapshotList = make([]Snapshot, 0)
|
||||
|
||||
var buf *bytes.Buffer
|
||||
buf, err = b.SSHExec("zfs list -H -t snapshot -o name")
|
||||
|
||||
csvReader := csv.NewReader(buf)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 1
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
|
||||
}
|
||||
if fromSnapshots, err = from.ValidSnapshots(); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": from, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
b.zfs.SnapshotList = append(b.zfs.SnapshotList, Snapshot(rec[0]))
|
||||
if len(fromSnapshots) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.SnapshotInitialize : %s : read %d zfs snapshots", b.Name, len(b.zfs.SnapshotList))
|
||||
if toSnapshots, err = to.ValidSnapshots(); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "ValidSnapshots", "attr": to, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
b.zfs.SnapshotInitialized = true
|
||||
b.zfs.SnapshotAdded = false
|
||||
b.zfs.SnapshotDeleted = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) ZFSUpdateList() (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSUpdateList : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
if b.zfs.ZFSDeleted || b.zfs.ZFSAdded {
|
||||
b.zfs.ZFSInitialized = false
|
||||
}
|
||||
b.zfs.M.Unlock()
|
||||
|
||||
err = b.ZFSInitialize()
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSIsZFS(path string) bool {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSIsZFS : %s : Start %s", b.Name, path)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
return false
|
||||
}
|
||||
|
||||
err := b.ZFSInitialize()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
if _, ok := b.zfs.ZFSMap[path]; ok {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
|
||||
}
|
||||
|
||||
func (b *Box) ZFSCreateZFS(path string) (err error) {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : %s : Start %s", b.Name, path)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
err = b.ZFSInitialize()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
|
||||
p := strings.Split(path, `/`)
|
||||
var base string
|
||||
for _, d := range p {
|
||||
if base == "" {
|
||||
base = d
|
||||
if len(toSnapshots) == 0 {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("initiating destination")
|
||||
if directTransfer {
|
||||
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send " + fromSnapshots[0].String() + " | zfs recv -F " + to.Path()); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
newToSnapshot := &ZfsSnapshot{name: fromSnapshots[0].name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]}
|
||||
toSnapshots = append(toSnapshots, newToSnapshot)
|
||||
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(newToSnapshot)
|
||||
} else {
|
||||
base = base + `/` + d
|
||||
//handle indirect transfer
|
||||
}
|
||||
if _, ok := b.zfs.ZFSMap[base]; !ok {
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : Creating %s:%s", b.Name, base)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = b.SSHExec("zfs create -o mountpoint=none " + base)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSCreateZFS : %s : SSHExec : %s", b.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.zfs.ZFSMap[base] = "none"
|
||||
b.zfs.ZFSAdded = true
|
||||
fromFromSnapshotId := -1
|
||||
lastToSnapshot := toSnapshots[len(toSnapshots)-1]
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("searching last snapshot %s", lastToSnapshot.String())
|
||||
for id, v := range fromSnapshots {
|
||||
if v.name == lastToSnapshot.name {
|
||||
fromFromSnapshotId = id
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("found %s", v.String())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) ZFSInitialize() (err error) {
|
||||
b.zfs.M.Lock()
|
||||
defer b.zfs.M.Unlock()
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
if b.zfs.ZFSInitialized {
|
||||
return nil
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : Start", b.Name)
|
||||
}
|
||||
|
||||
b.zfs.ZFSMap = make(map[string]string)
|
||||
|
||||
var buf *bytes.Buffer
|
||||
buf, err = b.SSHExec("zfs list -H -o name,mountpoint")
|
||||
|
||||
csvReader := csv.NewReader(buf)
|
||||
csvReader.Comma = '\t'
|
||||
csvReader.FieldsPerRecord = 2
|
||||
|
||||
csvData, err := csvReader.ReadAll()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Box.ZFSInitialize : %s : csvReader.ReadAll() : %s", b.Name, err)
|
||||
}
|
||||
if fromFromSnapshotId == -1 {
|
||||
err := errors.New("zfs snapshot unsync")
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for _, rec := range csvData {
|
||||
b.zfs.ZFSMap[rec[0]] = rec[1]
|
||||
}
|
||||
if fromFromSnapshotId < len(fromSnapshots)-1 {
|
||||
log.WithFields(log.Fields{"from": from, "to": to}).Debugf("transfering from %s to %s", fromSnapshots[fromFromSnapshotId].name, fromSnapshots[len(fromSnapshots)-1].name)
|
||||
if directTransfer {
|
||||
if _, err := to.BoxExec("ssh " + from.Box() + " zfs send -I " + fromSnapshots[fromFromSnapshotId].String() + " " + fromSnapshots[len(fromSnapshots)-1].String() + " | zfs recv -F " + to.Path()); err != nil {
|
||||
log.WithFields(log.Fields{"from": from, "to": to, "call": "BoxExec", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// handle indirect transfer
|
||||
}
|
||||
|
||||
b.zfs.ZFSInitialized = true
|
||||
b.zfs.ZFSAdded = false
|
||||
b.zfs.ZFSDeleted = false
|
||||
for _, v := range fromSnapshots[fromFromSnapshotId+1:] {
|
||||
cfg.box[to.Box()].zfs.filesystems[to.Path()].AddSnapshot(&ZfsSnapshot{name: v.name, fs: cfg.box[to.Box()].zfs.filesystems[to.Path()]})
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Box) SSHExec(cmd string) (buf *bytes.Buffer, err error) {
|
||||
if !b.online {
|
||||
err = fmt.Errorf("box offline")
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = b.ssh.exec(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *Box) Host() string {
|
||||
s := strings.Split(string(b.Addr), `:`)
|
||||
return s[0]
|
||||
}
|
||||
|
||||
@@ -1,227 +1,287 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tailscale/hujson"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Zfsnap map[string]string `json:"zfsnap"`
|
||||
Box map[string]*Box `json:"box"`
|
||||
Email EmailConfig `json:"email"`
|
||||
Apps []AppConfig `json:"apps"`
|
||||
Timezone string `json:"timezone"`
|
||||
Now time.Time `json:"-"`
|
||||
ScheduleDuration map[string]string `json:"schedule"`
|
||||
Box map[string]BoxConfig `json:"box"`
|
||||
Email EmailConfig `json:"email"`
|
||||
Apps []AppConfig `json:"apps"`
|
||||
Timezone string `json:"timezone"`
|
||||
Admin *AdminConfig `json:"admin"`
|
||||
Debug bool `json:"debug"`
|
||||
box map[string]*Box `json:"-"`
|
||||
apps map[string]*App `json:"-"`
|
||||
timezone *time.Location `json:"-"`
|
||||
}
|
||||
|
||||
//Load config from file
|
||||
func (c *Config) Load() error {
|
||||
if *debugFlag {
|
||||
log.Printf("SSHConfig.Load : Start")
|
||||
}
|
||||
b, err := ioutil.ReadFile(*cfgFile)
|
||||
var (
|
||||
cfgMx sync.Mutex
|
||||
cfgRun bool
|
||||
)
|
||||
|
||||
//go:embed assets/backup.sample.json
|
||||
var sampleCfg []byte
|
||||
|
||||
type BoxConfig struct {
|
||||
Addr string `json:"addr"`
|
||||
User string `json:"user"`
|
||||
Key string `json:"key"`
|
||||
AllowDirectConnect bool `json:"allow_direct_connect"`
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
Name string `json:"name"`
|
||||
Schedule []string `json:"schedule"`
|
||||
Sources []string `json:"src"`
|
||||
Destinations []string `json:"dest"`
|
||||
Before map[string]string `json:"before"`
|
||||
After map[string]string `json:"after"`
|
||||
Active bool `json:"active"`
|
||||
}
|
||||
|
||||
// Load config from file
|
||||
func LoadConfigFile(path string) (*Config, error) {
|
||||
log.WithFields(log.Fields{"path": path}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"path": path}).Debugf("done")
|
||||
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", *cfgFile, err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"path": path, "error": err, "call": "os.ReadFile"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(b, &c)
|
||||
return LoadConfigByte(b)
|
||||
|
||||
}
|
||||
|
||||
// Load config from string
|
||||
func LoadConfigByte(conf []byte) (*Config, error) {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
c := &Config{}
|
||||
if err := json.Unmarshal(sampleCfg, c); err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal", "attr": "sampleCfg"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b, err := hujson.Standardize(conf)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : json.Unmarshal : %s", err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"error": err, "call": "hujson.Standardize"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load :\r\n%v", cfg)
|
||||
if err := json.Unmarshal(b, c); err != nil {
|
||||
log.WithFields(log.Fields{"error": err, "call": "json.Unmarshal"}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l, err := time.LoadLocation(cfg.Timezone)
|
||||
c.timezone, err = time.LoadLocation(c.Timezone)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : time.LoadLocation : %s", err)
|
||||
}
|
||||
return err
|
||||
log.WithFields(log.Fields{"error": err, "call": "time.LoadLocation", "attr": cfg.Timezone}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(cfg.Email.SmtpHost) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no smtp")
|
||||
if c.Email.Active {
|
||||
if len(c.Email.SmtpHost) == 0 {
|
||||
err := fmt.Errorf("no smtp")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.FromEmail) == 0 {
|
||||
err := fmt.Errorf("no email from")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(c.Email.ToEmail) == 0 {
|
||||
err := fmt.Errorf("no email to")
|
||||
log.WithFields(log.Fields{"error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Errorf("no smtp")
|
||||
}
|
||||
|
||||
if len(cfg.Email.FromEmail) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no email from")
|
||||
for k, v := range c.ScheduleDuration {
|
||||
switch k {
|
||||
case "hourly":
|
||||
case "daily":
|
||||
case "weekly":
|
||||
case "monthly":
|
||||
case "yearly":
|
||||
if _, err := Expiration(time.Now(), v); err != nil {
|
||||
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
err := errors.New("invalid schedule")
|
||||
log.WithFields(log.Fields{"schedule": k, "deadline": v, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return fmt.Errorf("no email from")
|
||||
}
|
||||
|
||||
if len(cfg.Email.ToEmail) == 0 {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : no email to")
|
||||
}
|
||||
return fmt.Errorf("no email to")
|
||||
}
|
||||
|
||||
c.Now = time.Now().In(l)
|
||||
|
||||
c.box = make(map[string]*Box)
|
||||
for k, v := range c.Box {
|
||||
v.Name = k
|
||||
v.online = false
|
||||
v.zfs = NewZFSConfig()
|
||||
s := &SSHConfig{
|
||||
logged: false,
|
||||
name: k,
|
||||
}
|
||||
v.ssh = s
|
||||
keyRaw, err := ioutil.ReadFile(v.Key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ioutil.ReadFile(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
key, err := ssh.ParseRawPrivateKey(keyRaw)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.ParseRawPrivateKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.signer, err = ssh.NewSignerFromKey(key)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.NewSignerFromKey(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
s.config = &ssh.ClientConfig{
|
||||
User: v.User,
|
||||
Auth: []ssh.AuthMethod{
|
||||
ssh.PublicKeys(s.signer),
|
||||
},
|
||||
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
s.client, err = ssh.Dial("tcp", v.Addr, s.config)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : ssh.Dial(%s) : %s", k, err)
|
||||
}
|
||||
if b, err := c.NewBox(k, v.Addr, v.User, v.Key, v.AllowDirectConnect); err != nil {
|
||||
log.WithFields(log.Fields{"call": "NewBox", "attr": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
v.online = true
|
||||
session, err := s.client.NewSession()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
if _, ok := c.box[k]; ok {
|
||||
err := errors.New("already exists")
|
||||
log.WithFields(log.Fields{"attr": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
session.Stdout = &b
|
||||
|
||||
err = session.Run("TZ=\"" + cfg.Timezone + "\" zfsnap --version")
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : client.NewSession(%s) : %s", k, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : logged into %s : %s", k, b.String())
|
||||
}
|
||||
session.Close()
|
||||
s.logged = true
|
||||
c.box[k] = b
|
||||
}
|
||||
}
|
||||
|
||||
for _, app := range c.Apps {
|
||||
for _, src := range app.Sources {
|
||||
if !src.Valid() {
|
||||
return fmt.Errorf("Source not valid : %s", string(src))
|
||||
c.apps = make(map[string]*App)
|
||||
for _, v := range c.Apps {
|
||||
if a, err := c.NewApp(v.Name, v.Sources, v.Destinations, v.Schedule, v.Before, v.After); err != nil {
|
||||
log.WithFields(log.Fields{"call": "NewApp", "attr": v.Name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
} else {
|
||||
if _, ok := c.apps[v.Name]; ok {
|
||||
err := errors.New("app already exists")
|
||||
log.WithFields(log.Fields{"app": v.Name, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := cfg.Box[src.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for source : %s", string(src))
|
||||
}
|
||||
if !cfg.Box[src.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("Source box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
var allOffline bool = true
|
||||
for _, dest := range app.Destinations {
|
||||
if !dest.Valid() {
|
||||
return fmt.Errorf("Destination not valid : %s", string(dest))
|
||||
}
|
||||
if _, ok := cfg.Box[dest.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for destination : %s", string(dest))
|
||||
}
|
||||
if cfg.Box[dest.Box()].online {
|
||||
allOffline = false
|
||||
}
|
||||
}
|
||||
if allOffline {
|
||||
email.items = append(email.items, fmt.Sprintf("No online destination box for app : %s", app.Name))
|
||||
}
|
||||
|
||||
for val, before := range app.Before {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
c.apps[v.Name] = a
|
||||
for k := range a.schedule {
|
||||
if dur, ok := c.ScheduleDuration[k]; ok {
|
||||
re := regexp.MustCompile(`^forever|([0-9]+(h|d|m|y))+$`)
|
||||
if !re.MatchString(dur) {
|
||||
err := errors.New("incorrect schedule duration")
|
||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err := errors.New("undefined schedule duration")
|
||||
log.WithFields(log.Fields{"app": v.Name, "schedule": k, "error": err}).Errorf("")
|
||||
return nil, err
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !before.Valid() {
|
||||
return fmt.Errorf("Before not valid : %s", string(before))
|
||||
}
|
||||
if _, ok := cfg.Box[before.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for before : %s", string(before))
|
||||
}
|
||||
if !cfg.Box[before.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("Before box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
for val, after := range app.After {
|
||||
_, err = regexp.Compile(val)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("Config.Load : invalid regex : %s", val)
|
||||
}
|
||||
return err
|
||||
}
|
||||
if !after.Valid() {
|
||||
return fmt.Errorf("After not valid : %s", string(after))
|
||||
}
|
||||
if _, ok := cfg.Box[after.Box()]; !ok {
|
||||
return fmt.Errorf("No box defined for after : %s", string(after))
|
||||
}
|
||||
if !cfg.Box[after.Box()].online {
|
||||
email.items = append(email.items, fmt.Sprintf("After box offline for app : %s", app.Name))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return c, nil
|
||||
}
|
||||
|
||||
//Close config
|
||||
func (c *Config) Close() error {
|
||||
return nil
|
||||
// Run config
|
||||
func (c *Config) Run() {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if cfgRun {
|
||||
return
|
||||
}
|
||||
|
||||
cfgMx.Lock()
|
||||
defer cfgMx.Unlock()
|
||||
|
||||
cfgRun = true
|
||||
defer func() { cfgRun = false }()
|
||||
|
||||
e := NewEmail(time.Now())
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Setup boxes
|
||||
for _, b := range c.box {
|
||||
wg.Add(1)
|
||||
go func(box *Box) {
|
||||
defer wg.Done()
|
||||
if err := box.Open(); err != nil {
|
||||
log.WithFields(log.Fields{"name": box.name, "call": "Open", "error": err}).Errorf("")
|
||||
e.AddItem(fmt.Sprintf(" - Box : %s is down", box.name))
|
||||
return
|
||||
}
|
||||
}(b)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Run each app
|
||||
for _, a := range cfg.apps {
|
||||
wg.Add(1)
|
||||
go func(app *App) {
|
||||
if sched, err := app.Run(e.startTime); err != nil {
|
||||
e.AddItem(fmt.Sprintf(" - App : Error running %s (%s)", app.name, err))
|
||||
} else if *debug {
|
||||
if sched != "" {
|
||||
e.AddItem(fmt.Sprintf(" - App : Success backing up %s (%s)", app.name, sched))
|
||||
} else {
|
||||
e.AddItem(fmt.Sprintf(" - App : No backup for %s", app.name))
|
||||
}
|
||||
}
|
||||
wg.Done()
|
||||
}(a)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Cleanup
|
||||
for _, a := range cfg.apps {
|
||||
for _, src := range a.sources {
|
||||
if b, ok := c.box[src.Box()]; ok {
|
||||
if fs, ok := b.zfs.filesystems[src.Path()]; ok {
|
||||
fs.srcApps = append(fs.srcApps, a)
|
||||
}
|
||||
}
|
||||
for _, dest := range a.destinations {
|
||||
if b, ok := c.box[dest.Box()]; ok {
|
||||
dest2 := dest.Append("/" + src.Box() + "/" + src.Path())
|
||||
if fs, ok := b.zfs.filesystems[dest2.Path()]; ok {
|
||||
fs.destApps = append(fs.destApps, a)
|
||||
} else {
|
||||
e.AddItem(fmt.Sprintf(" - Dest : No folder (%s)", dest2.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, b := range cfg.box {
|
||||
if b.online {
|
||||
for _, fs := range b.zfs.filesystems {
|
||||
if len(fs.srcApps) > 0 && !fs.backedUp {
|
||||
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("not backed up")
|
||||
e.AddItem(fmt.Sprintf(" - Src : Folder not backed up (%s)", b.name+":"+fs.path))
|
||||
}
|
||||
if len(fs.destApps) == 0 && !fs.backedUp && fs.managed {
|
||||
log.WithFields(log.Fields{"box": b.name, "fs": fs.path}).Warnf("managed")
|
||||
e.AddItem(fmt.Sprintf(" - Dest : Folder managed (%s)", b.name+":"+fs.path))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop
|
||||
for _, b := range c.box {
|
||||
if err := b.Close(); err != nil {
|
||||
log.WithFields(log.Fields{"name": b.name, "call": "Close", "error": err}).Errorf("")
|
||||
}
|
||||
}
|
||||
|
||||
if len(e.items) > 0 {
|
||||
if err := e.Send(cfg.Email.SmtpHost, cfg.Email.FromEmail, cfg.Email.ToEmail); err != nil {
|
||||
log.WithFields(log.Fields{"call": "email.Send", "error": err}).Errorf("")
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -2,61 +2,101 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"log"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
startTime time.Time
|
||||
items []string
|
||||
mx sync.Mutex
|
||||
}
|
||||
|
||||
type EmailConfig struct {
|
||||
Active bool `json:"active"`
|
||||
SmtpHost string `json:"smtp"`
|
||||
FromEmail string `json:"email_from"`
|
||||
ToEmail []string `json:"email_to"`
|
||||
}
|
||||
|
||||
func SendMail(addr, from, subject, body string, to []string) error {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : Start")
|
||||
func NewEmail(now time.Time) *Email {
|
||||
log.WithFields(log.Fields{"now": now}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"now": now}).Debugf("done")
|
||||
|
||||
return &Email{startTime: now, items: make([]string, 0)}
|
||||
}
|
||||
|
||||
func (e *Email) AddItem(item string) {
|
||||
log.WithFields(log.Fields{"item": item}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"item": item}).Debugf("done")
|
||||
|
||||
e.items = append(e.items, item)
|
||||
}
|
||||
|
||||
func (e *Email) Send(addr, from string, to []string) error {
|
||||
log.WithFields(log.Fields{}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{}).Debugf("done")
|
||||
|
||||
if len(e.items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Slice(e.items, func(i, j int) bool {
|
||||
return e.items[i] < e.items[j]
|
||||
})
|
||||
|
||||
body := e.items[0]
|
||||
for _, item := range e.items[1:] {
|
||||
body = body + "\r\n" + item
|
||||
}
|
||||
|
||||
subject := fmt.Sprintf("Autobackup report (%s)", e.startTime)
|
||||
|
||||
if err := SendMail(addr, from, subject, body, to); err != nil {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "SendMail", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func SendMail(addr, from, subject, body string, to []string) error {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("starting")
|
||||
defer log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject}).Debugf("done")
|
||||
|
||||
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
|
||||
|
||||
c, err := smtp.Dial(addr)
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : smtp.Dial (%s)", addr, err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "smtp.Dial", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
defer c.Close()
|
||||
|
||||
if err = c.Mail(r.Replace(from)); err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : client.Mail (%s)", from, err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Mail", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range to {
|
||||
to[i] = r.Replace(to[i])
|
||||
if err = c.Rcpt(to[i]); err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : %s : client.Rcpt (%s)", to[i], err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Rcpt", "attr": to[i], "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
w, err := c.Data()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : client.Data (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Date", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -68,29 +108,21 @@ func SendMail(addr, from, subject, body string, to []string) error {
|
||||
"Content-Transfer-Encoding: base64\r\n" +
|
||||
"\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
||||
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail :\r\n%s", msg)
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(msg))
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : writer.Write (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Write", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : writer.Close (%s)", err)
|
||||
}
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "writer.Close", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
|
||||
err = c.Quit()
|
||||
if *debugFlag {
|
||||
log.Printf("SendMail : client.Quit (%s)", err)
|
||||
if err = c.Quit(); err != nil {
|
||||
log.WithFields(log.Fields{"addr": addr, "from": from, "subject": subject, "call": "client.Quit", "error": err}).Errorf("")
|
||||
return err
|
||||
}
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,11 @@ module git.siteop.biz/shoopea/backup
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
|
||||
github.com/gin-gonic/gin v1.9.1 // indirect
|
||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||
github.com/sethvargo/go-password v0.2.0 // indirect
|
||||
github.com/silenceper/pool v1.0.0 // indirect
|
||||
github.com/sirupsen/logrus v1.9.3 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
|
||||
golang.org/x/crypto v0.9.0
|
||||
)
|
||||
|
||||
@@ -1,12 +1,143 @@
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/sethvargo/go-password v0.2.0 h1:BTDl4CC/gjf/axHMaDQtw507ogrXLci6XRiLc7i/UHI=
|
||||
github.com/sethvargo/go-password v0.2.0/go.mod h1:Ym4Mr9JXLBycr02MFuVQ/0JHidNetSgbzutTr3zsYXE=
|
||||
github.com/silenceper/pool v1.0.0 h1:JTCaA+U6hJAA0P8nCx+JfsRCHMwLTfatsm5QXelffmU=
|
||||
github.com/silenceper/pool v1.0.0/go.mod h1:3DN13bqAbq86Lmzf6iUXWEPIWFPOSYVfaoceFvilKKI=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
|
||||
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
|
||||
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
|
||||
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
|
||||
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
|
||||
golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func HttpAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Search signed-in userID
|
||||
userID := 0
|
||||
if userID == 0 {
|
||||
// Return 404 and abort handlers chain.
|
||||
c.String(http.StatusNotFound, "404 page not found")
|
||||
c.AbortWithStatus(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func HttpNoAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func HttpAnySignIn(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
warning, _ := c.Cookie("warning")
|
||||
c.SetCookie("warning", "", -1, "/", cfg.Admin.Addr, false, true)
|
||||
c.HTML(http.StatusOK, "page-signin.html", gin.H{
|
||||
"Error": warning,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HttpAnyIndex(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) > 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/p/home")
|
||||
} else {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func HttpAnyHome(c *gin.Context) {
|
||||
if GetWebSessionUserID(c) == 0 {
|
||||
c.Redirect(http.StatusTemporaryRedirect, "/u/signin")
|
||||
} else {
|
||||
SetCSRFToken(c)
|
||||
c.HTML(http.StatusOK, "page-home.html", gin.H{})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func GetWebSessionUserID(c *gin.Context) int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func SetCSRFToken(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
-19
@@ -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
|
||||
}
|
||||
|
||||
+114
-11
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
// Code generated by version.sh (@generated) DO NOT EDIT.
|
||||
package main
|
||||
var githash = "7f9cf49"
|
||||
var buildstamp = "2022-10-08_03:14:52"
|
||||
var commits = "54"
|
||||
var version = "7f9cf49-b54 - 2022-10-08_03:14:52"
|
||||
var githash = "1a1713e"
|
||||
var branch = "v2"
|
||||
var buildstamp = "2023-08-21_12:35:47"
|
||||
var commits = "83"
|
||||
var version = "1a1713e-b83 - 2023-08-21_12:35:47"
|
||||
|
||||
+3
-1
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user