2020-04-24 21:35:03 +02:00
package snapshot
import (
"encoding/json"
"errors"
2024-02-08 15:52:00 +01:00
"flag"
2020-04-24 21:35:03 +02:00
"fmt"
2022-08-21 23:13:44 +02:00
"io"
2020-04-24 21:35:03 +02:00
"net/http"
"net/url"
2022-05-04 21:12:03 +02:00
"regexp"
"strings"
"sync/atomic"
"time"
2020-04-24 21:35:03 +02:00
2024-02-08 15:52:00 +01:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/httputils"
2020-04-24 21:35:03 +02:00
"github.com/VictoriaMetrics/VictoriaMetrics/lib/logger"
)
2022-05-04 21:12:03 +02:00
var snapshotNameRegexp = regexp . MustCompile ( ` ^[0-9] { 14}-[0-9A-Fa-f]+$ ` )
2024-02-08 15:52:00 +01:00
var (
tlsInsecureSkipVerify = flag . Bool ( "snapshot.tlsInsecureSkipVerify" , false , "Whether to skip tls verification when connecting to -snapshotCreateURL" )
tlsCertFile = flag . String ( "snapshot.tlsCertFile" , "" , "Optional path to client-side TLS certificate file to use when connecting to -snapshotCreateURL" )
tlsKeyFile = flag . String ( "snapshot.tlsKeyFile" , "" , "Optional path to client-side TLS certificate key to use when connecting to -snapshotCreateURL" )
tlsCAFile = flag . String ( "snapshot.tlsCAFile" , "" , ` Optional path to TLS CA file to use for verifying connections to -snapshotCreateURL. By default, system CA is used ` )
tlsServerName = flag . String ( "snapshot.tlsServerName" , "" , ` Optional TLS server name to use for connections to -snapshotCreateURL. By default, the server name from -snapshotCreateURL is used ` )
)
2020-04-24 21:35:03 +02:00
type snapshot struct {
Status string ` json:"status" `
Snapshot string ` json:"snapshot" `
Msg string ` json:"msg" `
}
2022-05-04 21:12:03 +02:00
// Create creates a snapshot via the provided api endpoint and returns the snapshot name
2020-04-24 21:35:03 +02:00
func Create ( createSnapshotURL string ) ( string , error ) {
2020-11-23 16:09:59 +01:00
logger . Infof ( "Creating snapshot" )
2020-04-24 21:35:03 +02:00
u , err := url . Parse ( createSnapshotURL )
if err != nil {
return "" , err
}
2024-02-08 15:52:00 +01:00
// create Transport
tr , err := httputils . Transport ( createSnapshotURL , * tlsCertFile , * tlsKeyFile , * tlsCAFile , * tlsServerName , * tlsInsecureSkipVerify )
if err != nil {
return "" , err
}
hc := & http . Client { Transport : tr }
resp , err := hc . Get ( u . String ( ) )
2020-04-24 21:35:03 +02:00
if err != nil {
return "" , err
}
2022-08-21 23:13:44 +02:00
body , err := io . ReadAll ( resp . Body )
2020-04-24 21:35:03 +02:00
if err != nil {
return "" , err
}
2020-11-30 13:49:07 +01:00
if resp . StatusCode != http . StatusOK {
2023-01-18 20:35:21 +01:00
return "" , fmt . Errorf ( "unexpected status code returned from %q: %d; expecting %d; response body: %q" , u . Redacted ( ) , resp . StatusCode , http . StatusOK , body )
2020-11-30 13:49:07 +01:00
}
2020-04-24 21:35:03 +02:00
snap := snapshot { }
err = json . Unmarshal ( body , & snap )
if err != nil {
2023-01-18 20:35:21 +01:00
return "" , fmt . Errorf ( "cannot parse JSON response from %q: %w; response body: %q" , u . Redacted ( ) , err , body )
2020-04-24 21:35:03 +02:00
}
if snap . Status == "ok" {
logger . Infof ( "Snapshot %s created" , snap . Snapshot )
return snap . Snapshot , nil
2023-11-02 21:37:49 +01:00
}
if snap . Status == "error" {
2020-04-24 21:35:03 +02:00
return "" , errors . New ( snap . Msg )
}
2023-11-02 21:37:49 +01:00
return "" , fmt . Errorf ( "Unkown status: %v" , snap . Status )
2020-04-24 21:35:03 +02:00
}
2022-05-04 21:12:03 +02:00
// Delete deletes a snapshot via the provided api endpoint
2020-04-24 21:35:03 +02:00
func Delete ( deleteSnapshotURL string , snapshotName string ) error {
logger . Infof ( "Deleting snapshot %s" , snapshotName )
formData := url . Values {
"snapshot" : { snapshotName } ,
}
u , err := url . Parse ( deleteSnapshotURL )
if err != nil {
return err
}
2024-02-08 15:52:00 +01:00
// create Transport
tr , err := httputils . Transport ( deleteSnapshotURL , * tlsCertFile , * tlsKeyFile , * tlsCAFile , * tlsServerName , * tlsInsecureSkipVerify )
if err != nil {
return err
}
hc := & http . Client { Transport : tr }
resp , err := hc . PostForm ( u . String ( ) , formData )
2020-04-24 21:35:03 +02:00
if err != nil {
return err
}
2022-08-21 23:13:44 +02:00
body , err := io . ReadAll ( resp . Body )
2020-04-24 21:35:03 +02:00
if err != nil {
return err
}
2020-11-30 13:49:07 +01:00
if resp . StatusCode != http . StatusOK {
2023-01-18 20:35:21 +01:00
return fmt . Errorf ( "unexpected status code returned from %q: %d; expecting %d; response body: %q" , u . Redacted ( ) , resp . StatusCode , http . StatusOK , body )
2020-11-30 13:49:07 +01:00
}
2020-04-24 21:35:03 +02:00
snap := snapshot { }
err = json . Unmarshal ( body , & snap )
if err != nil {
2023-01-18 20:35:21 +01:00
return fmt . Errorf ( "cannot parse JSON response from %q: %w; response body: %q" , u . Redacted ( ) , err , body )
2020-04-24 21:35:03 +02:00
}
if snap . Status == "ok" {
logger . Infof ( "Snapshot %s deleted" , snapshotName )
return nil
2023-11-02 21:37:49 +01:00
}
if snap . Status == "error" {
2020-04-24 21:35:03 +02:00
return errors . New ( snap . Msg )
}
2023-11-02 21:37:49 +01:00
return fmt . Errorf ( "Unkown status: %v" , snap . Status )
2020-04-24 21:35:03 +02:00
}
2022-05-04 21:12:03 +02:00
// Validate validates the snapshotName
func Validate ( snapshotName string ) error {
_ , err := Time ( snapshotName )
return err
}
// Time returns snapshot creation time from the given snapshotName
func Time ( snapshotName string ) ( time . Time , error ) {
if ! snapshotNameRegexp . MatchString ( snapshotName ) {
return time . Time { } , fmt . Errorf ( "unexpected snapshot name=%q; it must match %q regexp" , snapshotName , snapshotNameRegexp . String ( ) )
}
n := strings . IndexByte ( snapshotName , '-' )
if n < 0 {
logger . Panicf ( "BUG: cannot find `-` in snapshotName=%q" , snapshotName )
}
s := snapshotName [ : n ]
t , err := time . Parse ( "20060102150405" , s )
if err != nil {
return time . Time { } , fmt . Errorf ( "unexpected timestamp=%q in snapshot name: %w; it must match YYYYMMDDhhmmss pattern" , s , err )
}
return t , nil
}
// NewName returns new name for new snapshot
func NewName ( ) string {
return fmt . Sprintf ( "%s-%08X" , time . Now ( ) . UTC ( ) . Format ( "20060102150405" ) , nextSnapshotIdx ( ) )
}
func nextSnapshotIdx ( ) uint64 {
return atomic . AddUint64 ( & snapshotIdx , 1 )
}
var snapshotIdx = uint64 ( time . Now ( ) . UnixNano ( ) )