blob: 84df5a9099d21708965d399865614764afeda52e [file] [log] [blame]
/*
* Portions copyright 2019-present Open Networking Foundation
* Original copyright 2019-present Ciena Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package commands
import (
"context"
flags "github.com/jessevdk/go-flags"
corderrors "github.com/opencord/cordctl/internal/pkg/error"
"time"
)
const (
DEFAULT_BACKUP_FORMAT = "table{{ .Status }}\t{{ .Checksum }}\t{{ .Chunks }}\t{{ .Bytes }}"
)
type BackupOutput struct {
Status string `json:"status"`
Checksum string `json:"checksum"`
Chunks int `json:"chunks"`
Bytes int `json:"bytes"`
}
type BackupCreate struct {
OutputOptions
ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Chunk size for streaming transfer"`
Args struct {
LocalFileName string
} `positional-args:"yes" required:"yes"`
}
type BackupRestore struct {
OutputOptions
ChunkSize int `short:"h" long:"chunksize" default:"65536" description:"Chunk size for streaming transfer"`
Args struct {
LocalFileName string
} `positional-args:"yes" required:"yes"`
CreateURIFunc func() (string, string) // allow override of CreateURIFunc for easy unit testing
}
type BackupOpts struct {
Create BackupCreate `command:"create"`
Restore BackupRestore `command:"restore"`
}
var backupOpts = BackupOpts{}
func RegisterBackupCommands(parser *flags.Parser) {
parser.AddCommand("backup", "backup management commands", "Commands to create backups and restore backups", &backupOpts)
}
func (options *BackupCreate) Execute(args []string) error {
conn, descriptor, err := InitClient(INIT_DEFAULT)
if err != nil {
return err
}
defer conn.Close()
ctx := context.Background() // TODO: Implement a sync timeout
// We might close and reopen the connection befor we do the DownloadFile,
// so make sure we've downloaded the service descriptor.
_, err = descriptor.FindSymbol("xos.filetransfer")
if err != nil {
return err
}
local_name := options.Args.LocalFileName
// STEP 1: Create backup operation
backupop := make(map[string]interface{})
backupop["operation"] = "create"
err = CreateModel(conn, descriptor, "BackupOperation", backupop)
if err != nil {
return err
}
conditional_printf(!options.Quiet, "Created backup-create operation id=%d uuid=%s\n", backupop["id"], backupop["uuid"])
conditional_printf(!options.Quiet, "Waiting for sync ")
// STEP 2: Wait for the operation to complete
flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | Ternary_uint32(options.Quiet, GM_QUIET, 0)
conn, completed_backupop, err := GetModelWithRetry(ctx, conn, descriptor, "BackupOperation", backupop["id"].(int32), flags)
if err != nil {
return err
}
defer conn.Close()
status := completed_backupop.GetFieldByName("status").(string)
conditional_printf(!options.Quiet, "\nStatus: %s\n", status)
// we've failed. leave.
if status != "created" {
return corderrors.NewInternalError("BackupOp status is %s", status)
}
// STEP 3: Retrieve URI
backupfile_id := completed_backupop.GetFieldByName("file_id").(int32)
if backupfile_id == 0 {
return corderrors.NewInternalError("BackupOp.file_id is not set")
}
completed_backupfile, err := GetModel(ctx, conn, descriptor, "BackupFile", backupfile_id)
if err != nil {
return err
}
uri := completed_backupfile.GetFieldByName("uri").(string)
conditional_printf(!options.Quiet, "URI %s\n", uri)
// STEP 4: Download the file
conditional_printf(!options.Quiet, "Downloading %s\n", local_name)
h, err := DownloadFile(conn, descriptor, uri, local_name)
if err != nil {
return err
}
// STEP 5: Verify checksum
if completed_backupfile.GetFieldByName("checksum").(string) != h.GetChecksum() {
return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
Actual: h.GetChecksum(),
Expected: completed_backupfile.GetFieldByName("checksum").(string)})
}
// STEP 6: Show results
data := make([]BackupOutput, 1)
data[0].Chunks = h.chunks
data[0].Bytes = h.bytes
data[0].Status = h.status
data[0].Checksum = h.GetChecksum()
FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
return nil
}
// Create a file:/// URI to use for storing the file in the core
func CreateDynamicURI() (string, string) {
remote_name := "cordctl-restore-" + time.Now().Format("20060102T150405Z")
uri := "file:///var/run/xos/backup/local/" + remote_name
return remote_name, uri
}
func (options *BackupRestore) Execute(args []string) error {
conn, descriptor, err := InitClient(INIT_DEFAULT)
if err != nil {
return err
}
defer conn.Close()
ctx := context.Background() // TODO: Implement a sync timeout
local_name := options.Args.LocalFileName
var remote_name, uri string
if options.CreateURIFunc != nil {
remote_name, uri = options.CreateURIFunc()
} else {
remote_name, uri = CreateDynamicURI()
}
// STEP 1: Upload the file
h, upload_result, err := UploadFile(conn, descriptor, local_name, uri, options.ChunkSize)
if err != nil {
return err
}
upload_status := GetEnumValue(upload_result, "status")
if upload_status != "SUCCESS" {
return corderrors.NewInternalError("Upload status was %s", upload_status)
}
// STEP 2: Verify checksum
if upload_result.GetFieldByName("checksum").(string) != h.GetChecksum() {
return corderrors.WithStackTrace(&corderrors.ChecksumMismatchError{
Expected: h.GetChecksum(),
Actual: upload_result.GetFieldByName("checksum").(string)})
}
// STEP 2: Create a BackupFile object
backupfile := make(map[string]interface{})
backupfile["name"] = remote_name
backupfile["uri"] = uri
backupfile["checksum"] = h.GetChecksum()
err = CreateModel(conn, descriptor, "BackupFile", backupfile)
if err != nil {
return err
}
conditional_printf(!options.Quiet, "Created backup file %d\n", backupfile["id"])
// STEP 3: Create a BackupOperation object
backupop := make(map[string]interface{})
backupop["operation"] = "restore"
backupop["file_id"] = backupfile["id"]
err = CreateModel(conn, descriptor, "BackupOperation", backupop)
if err != nil {
return err
}
conditional_printf(!options.Quiet, "Created backup-restore operation id=%d uuid=%s\n", backupop["id"], backupop["uuid"])
conditional_printf(!options.Quiet, "Waiting for completion ")
// STEP 4: Wait for completion
flags := GM_UNTIL_ENACTED | GM_UNTIL_FOUND | GM_UNTIL_STATUS | Ternary_uint32(options.Quiet, GM_QUIET, 0)
queries := map[string]string{"uuid": backupop["uuid"].(string)}
conn, completed_backupop, err := FindModelWithRetry(ctx, conn, descriptor, "BackupOperation", queries, flags)
if err != nil {
return err
}
defer conn.Close()
conditional_printf(!options.Quiet, "\n")
// STEP 5: Show results
data := make([]BackupOutput, 1)
data[0].Checksum = upload_result.GetFieldByName("checksum").(string)
data[0].Chunks = int(upload_result.GetFieldByName("chunks_received").(int32))
data[0].Bytes = int(upload_result.GetFieldByName("bytes_received").(int32))
if completed_backupop.GetFieldByName("status") == "restored" {
data[0].Status = "SUCCESS"
} else {
data[0].Status = "FAILURE"
}
FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
return nil
}