Backups feature

pull/32/merge
Paulo Truta 2023-05-29 21:52:27 +02:00
parent f7823b9e20
commit a66ff40e65
5 changed files with 315 additions and 7 deletions

View File

@ -21,9 +21,6 @@ build-arm64:
build-armhf:
GOOS=linux GOARCH=arm RELEASE=prod make build
build-amd64:
GOOS=linux GOARCH=amd64 RELEASE=prod make build
build:
@echo "Building ${GOOS}-${GOARCH}"
GOOS=${GOOS} GOARCH=${GOARCH} go build \
@ -52,7 +49,7 @@ install-cloud: build-cloud
@echo "To start edgeboxctl run: systemctl start edgeboxctl"
install-prod: build-prod
-sudo systemctl stop edgeboxctl
sudo systemctl stop edgeboxctl
sudo rm -rf /usr/local/bin/edgeboxctl /lib/systemd/system/edgeboxctl.service
sudo cp ./bin/edgeboxctl /usr/local/bin/edgeboxctl
sudo cp ./edgeboxctl.service /lib/systemd/system/edgeboxctl.service

View File

@ -0,0 +1,28 @@
package backups
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/edgebox-iot/edgeboxctl/internal/diagnostics"
"github.com/edgebox-iot/edgeboxctl/internal/utils"
"github.com/shirou/gopsutil/disk"
)
// Repository : Struct representing the backup repository of a device in the system
type Repository struct {
ID string `json:"id"`
FileCount int64 `json:"file_count"`
Size string `json:"size"`
Snapshots []Snapshot `json:"snapshots"`
Status string `json:"status"`
UsageStat UsageStat `json:"usage_stat"`
}
// Snapshot : Struct representing a single snapshot in the backup repository
type Snapshot struct {
ID string `json:"id"`
time string `json:"time"`
}

View File

@ -130,6 +130,22 @@ func GetServiceStatus(serviceID string) string {
return utils.Exec(wsPath, "systemctl", cmdargs)
}
func CreateBackupsPasswordFile(password string) {
// Create a password file for backups
backupPasswordFile := utils.GetPath(utils.BackupPasswordFileLocation)
backupPasswordFileDir := filepath.Dir(backupPasswordFile)
if _, err := os.Stat(backupPasswordFileDir); os.IsNotExist(err) {
os.MkdirAll(backupPasswordFileDir, 0755)
}
// Write the password to the file, overriting an existing file
err := ioutil.WriteFile(backupPasswordFile, []byte(password), 0644)
if err != nil {
panic(err)
}
}
// CreateTunnel: Creates a tunnel via cloudflared, needs to be authenticated first
func CreateTunnel(configDestination string) {
fmt.Println("Creating Tunnel for Edgebox.")

View File

@ -66,6 +66,14 @@ type taskEnablePublicDashboardArgs struct {
InternetURL string `json:"internet_url"`
}
type taskSetupBackupsArgs struct {
Service string `json:"service"`
AccessKeyID string `json:"access_key_id"`
SecretAccessKey string `json:"secret_access_key"`
RepositoryName string `json:"repository_name"`
RepositoryPassword string `json:"repository_password"`
}
const STATUS_CREATED int = 0
const STATUS_EXECUTING int = 1
@ -124,6 +132,7 @@ func ExecuteTask(task Task) Task {
formatedDatetime := utils.GetSQLiteFormattedDateTime(time.Now())
fmt.Println("Changing task status to executing: " + task.Task)
_, err = statement.Exec(STATUS_EXECUTING, formatedDatetime, strconv.Itoa(task.ID)) // Execute SQL Statement
if err != nil {
log.Fatal(err.Error())
@ -135,6 +144,34 @@ func ExecuteTask(task Task) Task {
log.Println("Task: " + task.Task)
log.Println("Args: " + task.Args.String)
switch task.Task {
case "setup_backups":
log.Println("Setting up Backups Destination...")
var args taskSetupBackupsArgs
err := json.Unmarshal([]byte(task.Args.String), &args)
if err != nil {
log.Println("Error reading arguments of setup_backups task: %s", err)
} else {
taskResult := taskSetupBackups(args)
taskResultBool := true
// Check if returned taskResult string contains "error"
if strings.Contains(taskResult, "error") {
taskResultBool = false
}
task.Result = sql.NullString{String: taskResult, Valid: taskResultBool}
}
case "start_backup":
log.Println("Backing up Edgebox...")
taskResult := taskBackup()
taskResultBool := true
// Check if returned taskResult string contains "error"
if strings.Contains(taskResult, "error") {
taskResultBool = false
}
task.Result = sql.NullString{String: taskResult, Valid: taskResultBool}
case "setup_tunnel":
log.Println("Setting up Cloudflare Tunnel...")
@ -272,11 +309,13 @@ func ExecuteTask(task Task) Task {
formatedDatetime = utils.GetSQLiteFormattedDateTime(time.Now())
if task.Result.Valid {
fmt.Println("Task Result: " + task.Result.String)
_, err = statement.Exec(STATUS_FINISHED, task.Result.String, formatedDatetime, strconv.Itoa(task.ID)) // Execute SQL Statement with result info
if err != nil {
log.Fatal(err.Error())
}
} else {
fmt.Println("Error executing task with result: " + task.Result.String)
_, err = statement.Exec(STATUS_ERROR, "Error", formatedDatetime, strconv.Itoa(task.ID)) // Execute SQL Statement with Error info
if err != nil {
log.Fatal(err.Error())
@ -338,6 +377,8 @@ func ExecuteSchedules(tick int) {
if tick%30 == 0 {
// Executing every 30 ticks
log.Println(taskGetEdgeApps())
// RESET SOME VARIABLES HERE IF NEEDED, SINCE SYSTEM IS UNBLOCKED
utils.WriteOption("BACKUP_IS_WORKING", "0")
}
if tick%60 == 0 {
@ -345,10 +386,223 @@ func ExecuteSchedules(tick int) {
log.Println("System IP is: " + ip)
}
if tick%3600 == 0 {
// Executing every 3600 ticks (1 hour)
backup := taskAutoBackup()
log.Println("Auto Backup: " + backup)
}
// Just add a schedule here if you need a custom one (every "tick hour", every "tick day", etc...)
}
func taskSetupBackups(args taskSetupBackupsArgs) string {
fmt.Println("Executing taskSetupBackups" + args.Service)
// ...
service_url := ""
key_id_name := "AWS_ACCESS_KEY_ID"
key_secret_name := "AWS_SECRET_ACCESS_KEY"
repo_location := "/home/system/components/apps/"
service_found := false
switch args.Service {
case "s3":
service_url = "s3.amazonaws.com/"
service_found = true
case "b2":
service_url = ""
key_id_name = "B2_ACCOUNT_ID"
key_secret_name = "B2_ACCOUNT_KEY"
service_found = true
case "wasabi":
service_found = true
service_url = "s3.wasabisys.com/"
}
if !service_found {
fmt.Println("Service not found")
return "{\"status\": \"error\", \"message\": \"Service not found\"}"
}
fmt.Println("Creating env vars for authentication with backup service")
os.Setenv(key_id_name, args.AccessKeyID)
os.Setenv(key_secret_name, args.SecretAccessKey)
fmt.Println("Creating restic password file")
system.CreateBackupsPasswordFile(args.RepositoryPassword)
fmt.Println("Initializing restic repository")
utils.WriteOption("BACKUP_IS_WORKING", "1")
cmdArgs := []string{"-r", args.Service + ":" + service_url + args.RepositoryName + ":" + repo_location, "init", "--password-file", utils.GetPath(utils.BackupPasswordFileLocation), "--verbose=3"}
result := utils.ExecAndStream(repo_location, "restic", cmdArgs)
utils.WriteOption("BACKUP_IS_WORKING", "0")
// See if result contains the substring "Fatal:"
if strings.Contains(result, "Fatal:") {
fmt.Println("Error initializing restic repository")
utils.WriteOption("BACKUP_STATUS", "error")
utils.WriteOption("BACKUP_ERROR_MESSAGE", result)
return "{\"status\": \"error\", \"message\": \"" + result + "\"}"
}
// Save options to database
utils.WriteOption("BACKUP_STATUS", "initiated")
utils.WriteOption("BACKUP_SERVICE", args.Service)
utils.WriteOption("BACKUP_SERVICE_URL", service_url)
utils.WriteOption("BACKUP_REPOSITORY_NAME", args.RepositoryName)
utils.WriteOption("BACKUP_REPOSITORY_PASSWORD", args.RepositoryPassword)
utils.WriteOption("BACKUP_REPOSITORY_ACCESS_KEY_ID", args.AccessKeyID)
utils.WriteOption("BACKUP_REPOSITORY_SECRET_ACCESS_KEY", args.SecretAccessKey)
utils.WriteOption("BACKUP_REPOSITORY_LOCATION", repo_location)
// Populate Stats right away
taskGetBackupStatus()
return "{\"status\": \"ok\"}"
}
func taskRemoveBackups() string {
fmt.Println("Executing taskRemoveBackups")
// ... This deletes the restic repository
// cmdArgs := []string{"-r", "s3:https://s3.amazonaws.com/edgebox-backups:/home/system/components/apps/", "forget", "latest", "--password-file", utils.GetPath(utils.BackupPasswordFileLocation), "--verbose=3"}
utils.WriteOption("BACKUP_STATUS", "")
utils.WriteOption("BACKUP_IS_WORKING", "0")
return "{\"status\": \"ok\"}"
}
func taskBackup() string {
fmt.Println("Executing taskBackup")
// Load Backup Options
backup_service := utils.ReadOption("BACKUP_SERVICE")
backup_service_url := utils.ReadOption("BACKUP_SERVICE_URL")
backup_repository_name := utils.ReadOption("BACKUP_REPOSITORY_NAME")
// backup_repository_password := utils.ReadOption("BACKUP_REPOSITORY_PASSWORD")
backup_repository_access_key_id := utils.ReadOption("BACKUP_REPOSITORY_ACCESS_KEY_ID")
backup_repository_secret_access_key := utils.ReadOption("BACKUP_REPOSITORY_SECRET_ACCESS_KEY")
backup_repository_location := utils.ReadOption("BACKUP_REPOSITORY_LOCATION")
key_id_name := "AWS_ACCESS_KEY_ID"
key_secret_name := "AWS_SECRET_ACCESS_KEY"
service_found := false
switch backup_service {
case "s3":
service_found = true
case "b2":
key_id_name = "B2_ACCOUNT_ID"
key_secret_name = "B2_ACCOUNT_KEY"
service_found = true
case "wasabi":
service_found = true
}
if !service_found {
fmt.Println("Service not found")
return "{\"status\": \"error\", \"message\": \"Backup Service not found\"}"
}
fmt.Println("Creating env vars for authentication with backup service")
fmt.Println(key_id_name)
os.Setenv(key_id_name, backup_repository_access_key_id)
fmt.Println(key_secret_name)
os.Setenv(key_secret_name, backup_repository_secret_access_key)
utils.WriteOption("BACKUP_IS_WORKING", "1")
// ... This backs up the restic repository
cmdArgs := []string{"-r", backup_service + ":" + backup_service_url + backup_repository_name + ":" + backup_repository_location, "backup", backup_repository_location, "--password-file", utils.GetPath(utils.BackupPasswordFileLocation), "--verbose=3"}
result := utils.ExecAndStream(backup_repository_location, "restic", cmdArgs)
utils.WriteOption("BACKUP_IS_WORKING", "0")
// Write as Unix timestamp
utils.WriteOption("BACKUP_LAST_RUN", strconv.FormatInt(time.Now().Unix(), 10))
// See if result contains the substring "Fatal:"
if strings.Contains(result, "Fatal:") {
fmt.Println("Error backing up")
utils.WriteOption("BACKUP_STATUS", "error")
utils.WriteOption("BACKUP_ERROR_MESSAGE", result)
return "{\"status\": \"error\", \"message\": \"" + result + "\"}"
}
utils.WriteOption("BACKUP_STATUS", "working")
taskGetBackupStatus()
return "{\"status\": \"ok\"}"
}
func taskAutoBackup() string {
fmt.Println("Executing taskAutoBackup")
// Get Backup Status
backup_status := utils.ReadOption("BACKUP_STATUS")
// We only backup is the status is "working"
if backup_status == "working" {
return taskBackup()
} else {
fmt.Println("Backup status is not working... skipping")
return "{\"status\": \"skipped\"}"
}
}
func taskGetBackupStatus() string {
fmt.Println("Executing taskGetBackupStatus")
// Load Backup Options
backup_service := utils.ReadOption("BACKUP_SERVICE")
backup_service_url := utils.ReadOption("BACKUP_SERVICE_URL")
backup_repository_name := utils.ReadOption("BACKUP_REPOSITORY_NAME")
// backup_repository_password := utils.ReadOption("BACKUP_REPOSITORY_PASSWORD")
backup_repository_access_key_id := utils.ReadOption("BACKUP_REPOSITORY_ACCESS_KEY_ID")
backup_repository_secret_access_key := utils.ReadOption("BACKUP_REPOSITORY_SECRET_ACCESS_KEY")
backup_repository_location := utils.ReadOption("BACKUP_REPOSITORY_LOCATION")
key_id_name := "AWS_ACCESS_KEY_ID"
key_secret_name := "AWS_SECRET_ACCESS_KEY"
service_found := false
switch backup_service {
case "s3":
service_found = true
case "b2":
key_id_name = "B2_ACCOUNT_ID"
key_secret_name = "B2_ACCOUNT_KEY"
service_found = true
case "wasabi":
service_found = true
}
if !service_found {
fmt.Println("Service not found")
return "{\"status\": \"error\", \"message\": \"Backup Service not found\"}"
}
fmt.Println("Creating env vars for authentication with backup service")
os.Setenv(key_id_name, backup_repository_access_key_id)
os.Setenv(key_secret_name, backup_repository_secret_access_key)
// ... This gets the restic repository status
cmdArgs := []string{"-r", backup_service + ":" + backup_service_url + backup_repository_name + ":" + backup_repository_location, "stats", "--password-file", utils.GetPath(utils.BackupPasswordFileLocation), "--verbose=3"}
utils.WriteOption("BACKUP_STATS", utils.ExecAndStream(backup_repository_location, "restic", cmdArgs))
return "{\"status\": \"ok\"}"
}
func taskSetupTunnel(args taskSetupTunnelArgs) string {
fmt.Println("Executing taskSetupTunnel")
wsPath := utils.GetPath(utils.WsPath)

View File

@ -16,7 +16,7 @@ import (
)
// ExecAndStream : Runs a terminal command, but streams progress instead of outputting. Ideal for long lived process that need to be logged.
func ExecAndStream(path string, command string, args []string) {
func ExecAndStream(path string, command string, args []string) string {
cmd := exec.Command(command, args...)
@ -27,13 +27,17 @@ func ExecAndStream(path string, command string, args []string) {
err := cmd.Run()
outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())
returnVal := outStr
if err != nil {
fmt.Printf("cmd.Run() failed with %s\n", err)
returnVal = errStr
}
outStr, errStr := string(stdoutBuf.Bytes()), string(stderrBuf.Bytes())
fmt.Printf("\nout:\n%s\nerr:\n%s\n", outStr, errStr)
return returnVal
}
// Exec : Runs a terminal Command, catches and logs errors, returns the result.
@ -100,6 +104,7 @@ func GetSQLiteFormattedDateTime(t time.Time) string {
return formatedDatetime
}
const BackupPasswordFileLocation string = "backupPasswordFileLocation"
const CloudEnvFileLocation string = "cloudEnvFileLocation"
const ApiEnvFileLocation string = "apiEnvFileLocation"
const ApiPath string = "apiPath"
@ -159,6 +164,14 @@ func GetPath(pathKey string) string {
targetPath = "/home/system/components/ws/"
}
case BackupPasswordFileLocation:
if env["BACKUP_PASSWORD_FILE_LOCATION"] != "" {
targetPath = env["BACKUP_PASSWORD_FILE_LOCATION"]
} else {
targetPath = "/home/system/components/backups/pw.txt"
}
default:
log.Printf("path_key %s nonexistant in GetPath().\n", pathKey)
@ -207,7 +220,7 @@ func ReadOption(optionKey string) string {
err = db.QueryRow("SELECT value FROM option WHERE name = ?", optionKey).Scan(&optionValue)
if err != nil {
log.Fatal(err.Error())
log.Println(err.Error())
}
db.Close()