Rainbond/api/handler/group/group_backup.go
2019-03-14 17:48:42 +08:00

462 lines
18 KiB
Go

// RAINBOND, Application Management Platform
// Copyright (C) 2014-2017 Goodrain Co., Ltd.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package group
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"time"
v1 "github.com/goodrain/rainbond/worker/appm/types/v1"
"github.com/coreos/etcd/clientv3"
"github.com/goodrain/rainbond/event"
"github.com/Sirupsen/logrus"
"github.com/goodrain/rainbond/worker/client"
mqclient "github.com/goodrain/rainbond/mq/client"
"github.com/jinzhu/gorm"
"github.com/pquerna/ffjson/ffjson"
"github.com/goodrain/rainbond/api/util"
"github.com/goodrain/rainbond/db"
dbmodel "github.com/goodrain/rainbond/db/model"
core_util "github.com/goodrain/rainbond/util"
)
//Backup GroupBackup
// swagger:parameters groupBackup
type Backup struct {
// in: path
// required: true
TenantName string `json:"tenant_name"`
Body struct {
EventID string `json:"event_id" validate:"event_id|required"`
GroupID string `json:"group_id" validate:"group_name|required"`
Metadata string `json:"metadata,omitempty" validate:"metadata|required"`
ServiceIDs []string `json:"service_ids" validate:"service_ids|required"`
Mode string `json:"mode" validate:"mode|required|in:full-online,full-offline"`
Version string `json:"version" validate:"version|required"`
SlugInfo struct {
Namespace string `json:"namespace"`
FTPHost string `json:"ftp_host"`
FTPPort string `json:"ftp_port"`
FTPUser string `json:"ftp_username"`
FTPPassword string `json:"ftp_password"`
} `json:"slug_info,omitempty"`
ImageInfo struct {
HubURL string `json:"hub_url"`
HubUser string `json:"hub_user"`
HubPassword string `json:"hub_password"`
Namespace string `json:"namespace"`
IsTrust bool `json:"is_trust,omitempty"`
} `json:"image_info,omitempty"`
SourceDir string `json:"source_dir"`
BackupID string `json:"backup_id,omitempty"`
}
}
//BackupHandle group app backup handle
type BackupHandle struct {
mqcli mqclient.MQClient
statusCli *client.AppRuntimeSyncClient
etcdCli *clientv3.Client
}
//CreateBackupHandle CreateBackupHandle
func CreateBackupHandle(MQClient mqclient.MQClient, statusCli *client.AppRuntimeSyncClient, etcdCli *clientv3.Client) *BackupHandle {
return &BackupHandle{mqcli: MQClient, statusCli: statusCli, etcdCli: etcdCli}
}
//NewBackup new backup task
func (h *BackupHandle) NewBackup(b Backup) (*dbmodel.AppBackup, *util.APIHandleError) {
logger := event.GetManager().GetLogger(b.Body.EventID)
var appBackup = dbmodel.AppBackup{
EventID: b.Body.EventID,
BackupID: core_util.NewUUID(),
GroupID: b.Body.GroupID,
Status: "starting",
Version: b.Body.Version,
BackupMode: b.Body.Mode,
}
//check last backup task whether complete or version whether exist
if db.GetManager().AppBackupDao().CheckHistory(b.Body.GroupID, b.Body.Version) {
return nil, util.CreateAPIHandleError(400, fmt.Errorf("last backup task do not complete or have restore backup or version is exist"))
}
//check all service exist
if alias, err := db.GetManager().TenantServiceDao().GetServiceAliasByIDs(b.Body.ServiceIDs); len(alias) != len(b.Body.ServiceIDs) || err != nil {
return nil, util.CreateAPIHandleError(400, fmt.Errorf("some services do not exist in need backup services"))
}
//make source dir
sourceDir := fmt.Sprintf("/grdata/groupbackup/%s_%s", b.Body.GroupID, b.Body.Version)
if err := os.MkdirAll(sourceDir, 0755); err != nil {
return nil, util.CreateAPIHandleError(500, fmt.Errorf("create backup dir error,%s", err))
}
b.Body.SourceDir = sourceDir
appBackup.SourceDir = sourceDir
//snapshot the app metadata of region and write
if err := h.snapshot(b.Body.ServiceIDs, sourceDir); err != nil {
os.RemoveAll(sourceDir)
if strings.HasPrefix(err.Error(), "Statefulset app must be closed") {
return nil, util.CreateAPIHandleError(401, fmt.Errorf("snapshot group apps error,%s", err))
}
return nil, util.CreateAPIHandleError(500, fmt.Errorf("snapshot group apps error,%s", err))
}
logger.Info(core_util.Translation("write region level metadata success"), map[string]string{"step": "back-api"})
//write console level metadata.
if err := ioutil.WriteFile(fmt.Sprintf("%s/console_apps_metadata.json", sourceDir), []byte(b.Body.Metadata), 0755); err != nil {
return nil, util.CreateAPIHandleError(500, fmt.Errorf("write metadata file error,%s", err))
}
logger.Info(core_util.Translation("write console level metadata success"), map[string]string{"step": "back-api"})
//save backup history
if err := db.GetManager().AppBackupDao().AddModel(&appBackup); err != nil {
return nil, util.CreateAPIHandleErrorFromDBError("create backup history", err)
}
//clear metadata
b.Body.Metadata = ""
b.Body.BackupID = appBackup.BackupID
err := h.mqcli.SendBuilderTopic(mqclient.TaskStruct{
TaskBody: b.Body,
TaskType: "backup_apps_new",
Topic: mqclient.BuilderTopic,
})
if err != nil {
logrus.Error("Failed to Enqueue MQ for BackupApp:", err)
return nil, util.CreateAPIHandleError(500, fmt.Errorf("build enqueue task error,%s", err))
}
logger.Info(core_util.Translation("Asynchronous tasks are sent successfully"), map[string]string{"step": "back-api"})
return &appBackup, nil
}
//GetBackup get one backup info
func (h *BackupHandle) GetBackup(backupID string) (*dbmodel.AppBackup, *util.APIHandleError) {
backup, err := db.GetManager().AppBackupDao().GetAppBackup(backupID)
if err != nil {
return nil, util.CreateAPIHandleErrorFromDBError("get backup history", err)
}
return backup, nil
}
//DeleteBackup delete backup
func (h *BackupHandle) DeleteBackup(backupID string) *util.APIHandleError {
backup, err := h.GetBackup(backupID)
if err != nil {
return err
}
//if status != success it could be deleted
//if status == success, backup mode must be offline could be deleted
if backup.Status != "success" || backup.BackupMode == "full-offline" {
backup.Deleted = true
if er := db.GetManager().AppBackupDao().UpdateModel(backup); er != nil {
return util.CreateAPIHandleErrorFromDBError("delete backup error", er)
}
return nil
}
return util.CreateAPIHandleErrorf(400, "backup success do not support delete.")
}
//GetBackupByGroupID get some backup info by group id
func (h *BackupHandle) GetBackupByGroupID(groupID string) ([]*dbmodel.AppBackup, *util.APIHandleError) {
backups, err := db.GetManager().AppBackupDao().GetAppBackups(groupID)
if err != nil {
return nil, util.CreateAPIHandleErrorFromDBError("get backup history", err)
}
return backups, nil
}
//RegionServiceSnapshot RegionServiceSnapshot
type RegionServiceSnapshot struct {
ServiceID string
Service *dbmodel.TenantServices
ServiceProbe []*dbmodel.TenantServiceProbe
LBMappingPort []*dbmodel.TenantServiceLBMappingPort
ServiceEnv []*dbmodel.TenantServiceEnvVar
ServiceLabel []*dbmodel.TenantServiceLable
ServiceMntRelation []*dbmodel.TenantServiceMountRelation
PluginRelation []*dbmodel.TenantServicePluginRelation
ServiceRelation []*dbmodel.TenantServiceRelation
ServiceStatus string
ServiceVolume []*dbmodel.TenantServiceVolume
ServicePort []*dbmodel.TenantServicesPort
Versions []*dbmodel.VersionInfo
PluginConfigs []*dbmodel.TenantPluginVersionDiscoverConfig
}
//snapshot
func (h *BackupHandle) snapshot(ids []string, sourceDir string) error {
var datas []RegionServiceSnapshot
for _, id := range ids {
service, err := db.GetManager().TenantServiceDao().GetServiceByID(id)
if err != nil {
return fmt.Errorf("Get service(%s) error %s", id, err.Error())
}
if dbmodel.ServiceKind(service.Kind) == dbmodel.ServiceKindThirdParty {
//TODO: support thirdpart service backup and restore
continue
}
var data = RegionServiceSnapshot{
ServiceID: id,
}
status := h.statusCli.GetStatus(id)
serviceType, err := db.GetManager().TenantServiceLabelDao().GetTenantServiceTypeLabel(id)
if err != nil {
return fmt.Errorf("Get service deploy type error,%s", err.Error())
}
if status != v1.CLOSED && serviceType != nil && serviceType.LabelValue == core_util.StatefulServiceType {
return fmt.Errorf("Statefulset app must be closed before backup,%s", err.Error())
}
data.ServiceStatus = status
data.Service = service
serviceProbes, err := db.GetManager().ServiceProbeDao().GetServiceProbes(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) probe error %s", id, err)
}
data.ServiceProbe = serviceProbes
lbmappingPorts, err := db.GetManager().TenantServiceLBMappingPortDao().GetTenantServiceLBMappingPortByService(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) lb mapping port error %s", id, err)
}
data.LBMappingPort = lbmappingPorts
serviceEnv, err := db.GetManager().TenantServiceEnvVarDao().GetServiceEnvs(id, nil)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) envs error %s", id, err)
}
data.ServiceEnv = serviceEnv
serviceLabels, err := db.GetManager().TenantServiceLabelDao().GetTenantServiceLabel(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) labels error %s", id, err)
}
data.ServiceLabel = serviceLabels
serviceMntRelations, err := db.GetManager().TenantServiceMountRelationDao().GetTenantServiceMountRelationsByService(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) mnt relations error %s", id, err)
}
data.ServiceMntRelation = serviceMntRelations
servicePlugins, err := db.GetManager().TenantServicePluginRelationDao().GetALLRelationByServiceID(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) plugins error %s", id, err)
}
data.PluginRelation = servicePlugins
serviceRelations, err := db.GetManager().TenantServiceRelationDao().GetTenantServiceRelations(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) relations error %s", id, err)
}
data.ServiceRelation = serviceRelations
serviceVolume, err := db.GetManager().TenantServiceVolumeDao().GetTenantServiceVolumesByServiceID(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) volume error %s", id, err)
}
data.ServiceVolume = serviceVolume
servicePorts, err := db.GetManager().TenantServicesPortDao().GetPortsByServiceID(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) ports error %s", id, err)
}
data.ServicePort = servicePorts
versions, err := db.GetManager().VersionInfoDao().GetVersionByServiceID(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) build versions error %s", id, err)
}
data.Versions = versions
pluginConfigs, err := db.GetManager().TenantPluginVersionConfigDao().GetPluginConfigs(id)
if err != nil && err.Error() != gorm.ErrRecordNotFound.Error() {
return fmt.Errorf("Get service(%s) plugin configs error %s", id, err)
}
data.PluginConfigs = pluginConfigs
datas = append(datas, data)
}
body, err := ffjson.Marshal(datas)
if err != nil {
return err
}
//write region level metadata.
if err := ioutil.WriteFile(fmt.Sprintf("%s/region_apps_metadata.json", sourceDir), body, 0755); err != nil {
return util.CreateAPIHandleError(500, fmt.Errorf("write region_apps_metadata file error,%s", err))
}
return nil
}
//BackupRestore BackupRestore
type BackupRestore struct {
BackupID string `json:"backup_id"`
Body struct {
SlugInfo struct {
Namespace string `json:"namespace"`
FTPHost string `json:"ftp_host"`
FTPPort string `json:"ftp_port"`
FTPUser string `json:"ftp_username"`
FTPPassword string `json:"ftp_password"`
} `json:"slug_info,omitempty"`
ImageInfo struct {
HubURL string `json:"hub_url"`
HubUser string `json:"hub_user"`
HubPassword string `json:"hub_password"`
Namespace string `json:"namespace"`
IsTrust bool `json:"is_trust,omitempty"`
} `json:"image_info,omitempty"`
EventID string `json:"event_id"`
//need restore target tenant id
TenantID string `json:"tenant_id"`
//RestoreMode(cdct) current datacenter and current tenant
//RestoreMode(cdot) current datacenter and other tenant
//RestoreMode(od) other datacenter
RestoreMode string `json:"restore_mode"`
}
}
//RestoreResult RestoreResult
type RestoreResult struct {
Status string `json:"status"`
Message string `json:"message"`
CreateTime time.Time `json:"create_time"`
ServiceChange map[string]*Info `json:"service_change"`
BackupID string `json:"backup_id"`
RestoreMode string `json:"restore_mode"`
EventID string `json:"event_id"`
RestoreID string `json:"restore_id"`
Metadata string `json:"metadata"`
CacheDir string `json:"cache_dir"`
}
//Info service cache info
type Info struct {
ServiceID string
ServiceAlias string
Status string
LBPorts map[int]int
}
//RestoreBackup restore a backup version
//all app could be closed before restore
func (h *BackupHandle) RestoreBackup(br BackupRestore) (*RestoreResult, *util.APIHandleError) {
logger := event.GetManager().GetLogger(br.Body.EventID)
backup, Aerr := h.GetBackup(br.BackupID)
if Aerr != nil {
return nil, Aerr
}
if backup.Status != "success" || backup.SourceDir == "" || backup.SourceType == "" {
return nil, util.CreateAPIHandleErrorf(500, "backup can not be restored")
}
var restoreID string
if br.Body.EventID != "" {
restoreID = br.Body.EventID
}
restoreID = core_util.NewUUID()
var dataMap = map[string]interface{}{
"slug_info": br.Body.SlugInfo,
"image_info": br.Body.ImageInfo,
"backup_id": backup.BackupID,
"tenant_id": br.Body.TenantID,
"restore_id": restoreID,
"restore_mode": br.Body.RestoreMode,
}
err := h.mqcli.SendBuilderTopic(mqclient.TaskStruct{
TaskBody: dataMap,
TaskType: "backup_apps_restore",
Topic: mqclient.BuilderTopic,
})
if err != nil {
logrus.Error("Failed to Enqueue MQ for BackupApp:", err)
return nil, util.CreateAPIHandleError(500, fmt.Errorf("build enqueue task error,%s", err))
}
logger.Info(core_util.Translation("Asynchronous tasks are sent successfully"), map[string]string{"step": "back-api"})
var rr = &RestoreResult{
Status: "starting",
BackupID: br.BackupID,
EventID: br.Body.EventID,
RestoreMode: br.Body.RestoreMode,
RestoreID: restoreID,
}
body, _ := ffjson.Marshal(rr)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
_, err = h.etcdCli.Put(ctx, "/rainbond/backup_restore/"+restoreID, string(body))
if err != nil {
logrus.Errorf("save backup restore history error.")
return nil, util.CreateAPIHandleError(500, err)
}
return rr, nil
}
//RestoreBackupResult RestoreBackupResult
func (h *BackupHandle) RestoreBackupResult(restoreID string) (*RestoreResult, *util.APIHandleError) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
res, err := h.etcdCli.Get(ctx, "/rainbond/backup_restore/"+restoreID)
if err != nil {
return nil, util.CreateAPIHandleError(500, err)
}
if res.Count == 0 {
return nil, util.CreateAPIHandleError(404, fmt.Errorf("restore result not exist "))
}
var rr RestoreResult
if err := ffjson.Unmarshal(res.Kvs[0].Value, &rr); err != nil {
return nil, util.CreateAPIHandleError(500, err)
}
if rr.Status == "success" {
//write console level metadata.
body, err := ioutil.ReadFile(fmt.Sprintf("%s/console_apps_metadata.json", rr.CacheDir))
if err != nil {
return nil, util.CreateAPIHandleError(500, fmt.Errorf("read metadata file error,%s", err))
}
rr.Metadata = string(body)
}
return &rr, nil
}
//BackupCopy BackupCopy
type BackupCopy struct {
Body struct {
EventID string `json:"event_id" validate:"event_id|required"`
GroupID string `json:"group_id" validate:"group_id|required"`
//Status in starting,failed,success,restore
Status string `json:"status" validate:"status|required"`
Version string `json:"version" validate:"version|required"`
SourceDir string `json:"source_dir" validate:"source_dir|required"`
SourceType string ` json:"source_type" validate:"source_type|required"`
BackupMode string `json:"backup_mode" validate:"backup_mode|required"`
BuckupSize int `json:"backup_size" validate:"backup_size|required"`
}
}
//BackupCopy BackupCopy
func (h *BackupHandle) BackupCopy(b BackupCopy) (*dbmodel.AppBackup, *util.APIHandleError) {
var ab dbmodel.AppBackup
ab.BackupID = core_util.NewUUID()
ab.EventID = b.Body.EventID
ab.GroupID = b.Body.GroupID
ab.Status = b.Body.Status
ab.Version = b.Body.Version
ab.SourceDir = b.Body.SourceDir
ab.SourceType = b.Body.SourceType
ab.BackupMode = b.Body.BackupMode
ab.BuckupSize = b.Body.BuckupSize
if err := db.GetManager().AppBackupDao().AddModel(&ab); err != nil {
return nil, util.CreateAPIHandleErrorFromDBError("copy backup", err)
}
return &ab, nil
}