diff --git a/pkg/apis/compute/dbinstance.go b/pkg/apis/compute/dbinstance.go index e956a6421e..a4bab3ca99 100644 --- a/pkg/apis/compute/dbinstance.go +++ b/pkg/apis/compute/dbinstance.go @@ -160,6 +160,9 @@ type DBInstanceCreateInput struct { // 从备份中创建新实例 DBInstancebackupId string `json:"dbinstancebackup_id"` + + // 多可用区部署 + MultiAZ bool `json:"multi_az"` } type SDBInstanceChangeConfigInput struct { diff --git a/pkg/apis/compute/dbinstance_const.go b/pkg/apis/compute/dbinstance_const.go index e20ab29db7..3071fcf411 100644 --- a/pkg/apis/compute/dbinstance_const.go +++ b/pkg/apis/compute/dbinstance_const.go @@ -103,6 +103,7 @@ const ( DBINSTANCE_TYPE_ORACLE = "Oracle" DBINSTANCE_TYPE_PPAS = "PPAS" DBINSTANCE_TYPE_PERCONA = "Percona" + DBINSTANCE_TYPE_AURORA = "Aurora" //阿里云实例类型 ALIYUN_DBINSTANCE_CATEGORY_BASIC = "basic" //基础版 @@ -128,6 +129,18 @@ const ( // Azure AZURE_DBINSTANCE_CATEGORY_BASIC = "basic" + // Aws + // SQLServer + AWS_DBINSTANCE_CATEGORY_ENTERPRISE_EDITION = "Enterprise Edition" + AWS_DBINSTANCE_CATEGORY_EXPRESS_EDITION = "Express Edition" + AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION = "Standard Edition" + AWS_DBINSTANCE_CATEGORY_WEB_EDITION = "Web Edition" + // Oracle + AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION_TWO = "Standard Edition Two" + + AWS_DBINSTANCE_CATEGORY_GENERAL_PURPOSE = "General Purpose" + AWS_DBINSTANCE_CATEGORY_MEMORY_OPTIMIZED = "Memory Optimized" + //阿里云存储类型 ALIYUN_DBINSTANCE_STORAGE_TYPE_LOCAL_SSD = "local_ssd" //本地盘SSD盘 ALIYUN_DBINSTANCE_STORAGE_TYPE_CLOUD_ESSD = "cloud_essd" //ESSD云盘 diff --git a/pkg/apis/const.go b/pkg/apis/const.go index 0beca5fb00..89b1203bbc 100644 --- a/pkg/apis/const.go +++ b/pkg/apis/const.go @@ -42,6 +42,7 @@ const ( STATUS_UPDATE_TAGS = "update_tags" STATUS_UPDATE_TAGS_FAILED = "update_tags_fail" + STATUS_SYNC_STATUS = "sync_status" STATUS_DELETING = "deleting" STATUS_DELETE_FAILED = "delete_failed" STATUS_UNKNOWN = "unknown" diff --git a/pkg/cloudprovider/dbinstance.go b/pkg/cloudprovider/dbinstance.go index 82a2a989f5..1677374063 100644 --- a/pkg/cloudprovider/dbinstance.go +++ b/pkg/cloudprovider/dbinstance.go @@ -67,6 +67,8 @@ type SManagedDBInstanceCreateConfig struct { Username string ProjectId string + MultiAz bool + BillingCycle *billing.SBillingCycle Tags map[string]string diff --git a/pkg/compute/models/cloudsync.go b/pkg/compute/models/cloudsync.go index 545c9cd8da..11346380a1 100644 --- a/pkg/compute/models/cloudsync.go +++ b/pkg/compute/models/cloudsync.go @@ -1071,7 +1071,7 @@ func syncDBInstanceSkus(ctx context.Context, userCred mcclient.TokenCredential, syncResults.Add(DBInstanceSkuManager, result) msg := result.Result() - log.Infof("SyncDBInstanceSkus for region %s result: %s", localRegion.Name, msg) + log.Infof("sync rds sku for region %s result: %s", localRegion.Name, msg) if result.IsError() { return } diff --git a/pkg/compute/models/dbinstance_skus.go b/pkg/compute/models/dbinstance_skus.go index 3fe5076094..8bab735bf8 100644 --- a/pkg/compute/models/dbinstance_skus.go +++ b/pkg/compute/models/dbinstance_skus.go @@ -79,12 +79,13 @@ type SDBInstanceSku struct { Category string `width:"32" index:"true" nullable:"false" list:"user" create:"optional"` Engine string `width:"16" index:"true" charset:"ascii" nullable:"false" list:"user" create:"required"` - EngineVersion string `width:"16" index:"true" charset:"ascii" nullable:"false" list:"user" create:"required"` + EngineVersion string `width:"64" index:"true" charset:"ascii" nullable:"false" list:"user" create:"required"` - Zone1 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` - Zone2 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` - Zone3 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` - ZoneId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` + Zone1 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` + Zone2 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` + Zone3 string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` + ZoneId string `width:"128" charset:"ascii" nullable:"false" list:"user" create:"admin_optional" update:"admin"` + MultiAZ tristate.TriState `default:"false" list:"user" create:"optional"` } func (manager *SDBInstanceSkuManager) fetchDBInstanceSkus(provider string, region *SCloudregion) ([]SDBInstanceSku, error) { @@ -543,6 +544,7 @@ func (sku *SDBInstanceSku) syncWithCloudSku(ctx context.Context, userCred mcclie sku.Status = isku.Status sku.TPS = isku.TPS sku.QPS = isku.QPS + sku.MultiAZ = isku.MultiAZ sku.MaxConnections = isku.MaxConnections return nil }) @@ -569,7 +571,7 @@ func SyncRegionDBInstanceSkus(ctx context.Context, userCred mcclient.TokenCreden return } if cnt > 0 { - log.Debugf("SyncDBInstanceSkus synced skus, skip...") + log.Debugf("sync rds sku for %s synced skus, skip...", regionId) return } } @@ -599,7 +601,7 @@ func SyncRegionDBInstanceSkus(ctx context.Context, userCred mcclient.TokenCreden } result := DBInstanceSkuManager.SyncDBInstanceSkus(ctx, userCred, ®ion, meta) msg := result.Result() - notes := fmt.Sprintf("SyncDBInstanceSkus for region %s result: %s", region.Name, msg) + notes := fmt.Sprintf("sync rds sku for region %s result: %s", region.Name, msg) log.Infof(notes) } diff --git a/pkg/compute/models/dbinstances.go b/pkg/compute/models/dbinstances.go index 1b29af4b89..0f645ba785 100644 --- a/pkg/compute/models/dbinstances.go +++ b/pkg/compute/models/dbinstances.go @@ -111,7 +111,7 @@ type SDBInstance struct { Engine string `width:"16" charset:"ascii" nullable:"false" list:"user" create:"required"` // 引擎版本 // example: 5.7 - EngineVersion string `width:"16" charset:"ascii" nullable:"false" list:"user" create:"required"` + EngineVersion string `width:"64" charset:"ascii" nullable:"false" list:"user" create:"required"` // 套餐名称 // example: mysql.x4.large.2c InstanceType string `width:"64" charset:"utf8" nullable:"true" list:"user" create:"optional"` @@ -124,9 +124,9 @@ type SDBInstance struct { VpcId string `width:"36" charset:"ascii" nullable:"false" list:"user" create:"optional"` // 外部连接地址 - ConnectionStr string `width:"256" charset:"ascii" nullable:"false" list:"user" create:"optional"` + ConnectionStr string `width:"256" charset:"ascii" nullable:"true" list:"user" create:"optional"` // 内部连接地址 - InternalConnectionStr string `width:"256" charset:"ascii" nullable:"false" list:"user" create:"optional"` + InternalConnectionStr string `width:"256" charset:"ascii" nullable:"true" list:"user" create:"optional"` // 可用区1 Zone1 string `width:"36" charset:"ascii" nullable:"false" create:"optional" list:"user"` @@ -275,17 +275,13 @@ func (man *SDBInstanceManager) QueryDistinctExtraField(q *sqlchemy.SQuery, field return q, httperrors.ErrNotFound } -func (manager *SDBInstanceManager) BatchCreateValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data *jsonutils.JSONDict) (*jsonutils.JSONDict, error) { - input := api.DBInstanceCreateInput{} - err := data.Unmarshal(&input) - if err != nil { - return nil, errors.Wrapf(err, "data.Unmarshal") - } +func (manager *SDBInstanceManager) BatchCreateValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.DBInstanceCreateInput) (api.DBInstanceCreateInput, error) { + var err error input, err = manager.ValidateCreateData(ctx, userCred, ownerId, query, input) if err != nil { - return nil, errors.Wrapf(err, "ValidateCreateData") + return input, errors.Wrapf(err, "ValidateCreateData") } - return input.JSON(input), nil + return input, nil } func (man *SDBInstanceManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.DBInstanceCreateInput) (api.DBInstanceCreateInput, error) { @@ -497,18 +493,12 @@ func (self *SDBInstance) PostCreate(ctx context.Context, userCred mcclient.Token ids = append(ids, secgroupId) } } - resetPassword := true - if input.ResetPassword != nil && !*input.ResetPassword { - resetPassword = false - } - self.StartDBInstanceCreateTask(ctx, userCred, resetPassword, input.Password, "") + self.StartDBInstanceCreateTask(ctx, userCred, jsonutils.Marshal(input)) } -func (self *SDBInstance) StartDBInstanceCreateTask(ctx context.Context, userCred mcclient.TokenCredential, resetPassword bool, password, parentTaskId string) error { - params := jsonutils.NewDict() - params.Add(jsonutils.NewString(password), "password") - params.Add(jsonutils.NewBool(resetPassword), "reset_password") - task, err := taskman.TaskManager.NewTask(ctx, "DBInstanceCreateTask", self, userCred, params, parentTaskId, "", nil) +func (self *SDBInstance) StartDBInstanceCreateTask(ctx context.Context, userCred mcclient.TokenCredential, data jsonutils.JSONObject) error { + params := data.(*jsonutils.JSONDict) + task, err := taskman.TaskManager.NewTask(ctx, "DBInstanceCreateTask", self, userCred, params, "", "", nil) if err != nil { return errors.Wrapf(err, "NewTask") } @@ -785,6 +775,9 @@ func (self *SDBInstance) GetMasterInstance() (*SDBInstance, error) { } func (self *SDBInstance) GetIDBInstance() (cloudprovider.ICloudDBInstance, error) { + if len(self.ExternalId) == 0 { + return nil, errors.Wrapf(cloudprovider.ErrNotFound, "empty external id") + } iregion, err := self.GetIRegion() if err != nil { return nil, errors.Wrap(err, "self.GetIRegion") @@ -899,18 +892,15 @@ func (self *SDBInstance) PerformReboot(ctx context.Context, userCred mcclient.To //同步RDS实例状态 func (self *SDBInstance) PerformSyncstatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - var openTask = true - count, err := taskman.TaskManager.QueryTasksOfObject(self, time.Now().Add(-3*time.Minute), &openTask).CountWithError() - if err != nil { - return nil, err - } - if count > 0 { - return nil, httperrors.NewBadRequestError("DBInstance has %d task active, can't sync status", count) - } - - return nil, StartResourceSyncStatusTask(ctx, userCred, self, "DBInstanceSyncStatusTask", "") + return self.PerformSync(ctx, userCred, query, data) } +// 同步RDS状态(弃用) +func (self *SDBInstance) PerformSyncStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { + return self.PerformSync(ctx, userCred, query, data) +} + +// 同步RDS信息 func (self *SDBInstance) PerformSync(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { var openTask = true count, err := taskman.TaskManager.QueryTasksOfObject(self, time.Now().Add(-3*time.Minute), &openTask).CountWithError() @@ -921,20 +911,7 @@ func (self *SDBInstance) PerformSync(ctx context.Context, userCred mcclient.Toke return nil, httperrors.NewBadRequestError("DBInstance has %d task active, can't sync status", count) } - return nil, self.StartDBInstanceSyncTask(ctx, userCred, jsonutils.NewDict(), "") -} - -func (self *SDBInstance) PerformSyncStatus(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { - var openTask = true - count, err := taskman.TaskManager.QueryTasksOfObject(self, time.Now().Add(-3*time.Minute), &openTask).CountWithError() - if err != nil { - return nil, err - } - if count > 0 { - return nil, httperrors.NewBadRequestError("DBInstance has %d task active, can't sync status", count) - } - - return nil, StartResourceSyncStatusTask(ctx, userCred, self, "DBInstanceSyncStatusTask", "") + return nil, self.StartDBInstanceSyncTask(ctx, userCred, "") } func (self *SDBInstance) PerformRenew(ctx context.Context, userCred mcclient.TokenCredential, query jsonutils.JSONObject, data jsonutils.JSONObject) (jsonutils.JSONObject, error) { @@ -1136,14 +1113,8 @@ func (self *SDBInstance) StartDBInstanceRebootTask(ctx context.Context, userCred return nil } -func (self *SDBInstance) StartDBInstanceSyncTask(ctx context.Context, userCred mcclient.TokenCredential, data *jsonutils.JSONDict, parentTaskId string) error { - self.SetStatus(userCred, api.DBINSTANCE_SYNC_CONFIG, "") - task, err := taskman.TaskManager.NewTask(ctx, "DBInstanceSyncTask", self, userCred, data, parentTaskId, "", nil) - if err != nil { - return err - } - task.ScheduleRun(nil) - return nil +func (self *SDBInstance) StartDBInstanceSyncTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error { + return StartResourceSyncStatusTask(ctx, userCred, self, "DBInstanceSyncTask", parentTaskId) } func (manager *SDBInstanceManager) getDBInstancesByProviderId(providerId string) ([]SDBInstance, error) { @@ -1600,36 +1571,36 @@ func (self *SDBInstance) SyncAllWithCloudDBInstance(ctx context.Context, userCre return nil } -func (self *SDBInstance) SyncWithCloudDBInstance(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, extInstance cloudprovider.ICloudDBInstance) error { - diff, err := db.UpdateWithLock(ctx, self, func() error { - self.ExternalId = extInstance.GetGlobalId() - self.Engine = extInstance.GetEngine() - self.EngineVersion = extInstance.GetEngineVersion() - self.InstanceType = extInstance.GetInstanceType() - self.VcpuCount = extInstance.GetVcpuCount() - self.VmemSizeMb = extInstance.GetVmemSizeMB() - self.DiskSizeGB = extInstance.GetDiskSizeGB() - self.StorageType = extInstance.GetStorageType() - self.Category = extInstance.GetCategory() - self.Status = extInstance.GetStatus() - self.Port = extInstance.GetPort() +func (self *SDBInstance) SyncWithCloudDBInstance(ctx context.Context, userCred mcclient.TokenCredential, provider *SCloudprovider, ext cloudprovider.ICloudDBInstance) error { + diff, err := db.Update(self, func() error { + self.ExternalId = ext.GetGlobalId() + self.Engine = ext.GetEngine() + self.EngineVersion = ext.GetEngineVersion() + self.InstanceType = ext.GetInstanceType() + self.VcpuCount = ext.GetVcpuCount() + self.VmemSizeMb = ext.GetVmemSizeMB() + self.DiskSizeGB = ext.GetDiskSizeGB() + self.StorageType = ext.GetStorageType() + self.Category = ext.GetCategory() + self.Status = ext.GetStatus() + self.Port = ext.GetPort() - self.ConnectionStr = extInstance.GetConnectionStr() - self.InternalConnectionStr = extInstance.GetInternalConnectionStr() + self.ConnectionStr = ext.GetConnectionStr() + self.InternalConnectionStr = ext.GetInternalConnectionStr() - self.MaintainTime = extInstance.GetMaintainTime() - self.SetZoneIds(extInstance) + self.MaintainTime = ext.GetMaintainTime() + self.SetZoneIds(ext) - if createdAt := extInstance.GetCreatedAt(); !createdAt.IsZero() { + if createdAt := ext.GetCreatedAt(); !createdAt.IsZero() { self.CreatedAt = createdAt } - if expiredAt := extInstance.GetExpiredAt(); !expiredAt.IsZero() { + if expiredAt := ext.GetExpiredAt(); !expiredAt.IsZero() { self.ExpiredAt = expiredAt } if len(self.VpcId) == 0 { - if vpcId := extInstance.GetIVpcId(); len(vpcId) > 0 { + if vpcId := ext.GetIVpcId(); len(vpcId) > 0 { vpc, err := db.FetchByExternalIdAndManagerId(VpcManager, vpcId, func(q *sqlchemy.SQuery) *sqlchemy.SQuery { return q.Equals("manager_id", provider.Id) }) @@ -1658,12 +1629,12 @@ func (self *SDBInstance) SyncWithCloudDBInstance(ctx context.Context, userCred m return errors.Wrap(err, "SyncWithCloudDBInstance.GetProviderFactory") } - if factory.IsSupportPrepaidResources() && !extInstance.GetExpiredAt().IsZero() { - self.BillingType = extInstance.GetBillingType() - if expired := extInstance.GetExpiredAt(); !expired.IsZero() { + if factory.IsSupportPrepaidResources() && !ext.GetExpiredAt().IsZero() { + self.BillingType = ext.GetBillingType() + if expired := ext.GetExpiredAt(); !expired.IsZero() { self.ExpiredAt = expired } - self.AutoRenew = extInstance.IsAutoRenew() + self.AutoRenew = ext.IsAutoRenew() } return nil @@ -1671,7 +1642,7 @@ func (self *SDBInstance) SyncWithCloudDBInstance(ctx context.Context, userCred m if err != nil { return err } - syncVirtualResourceMetadata(ctx, userCred, self, extInstance) + syncVirtualResourceMetadata(ctx, userCred, self, ext) db.OpsLog.LogSyncUpdate(self, diff, userCred) if len(diff) > 0 { notifyclient.EventNotify(ctx, userCred, notifyclient.SEventNotifyParam{ diff --git a/pkg/compute/models/resource_syncstatus.go b/pkg/compute/models/resource_syncstatus.go index 2be5b5e05f..d4172872e3 100644 --- a/pkg/compute/models/resource_syncstatus.go +++ b/pkg/compute/models/resource_syncstatus.go @@ -19,15 +19,12 @@ import ( "yunion.io/x/jsonutils" + "yunion.io/x/onecloud/pkg/apis" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" "yunion.io/x/onecloud/pkg/mcclient" ) -const ( - SYNC_STATUS = "sync_status" -) - type IStatusBase interface { SetStatus(userCred mcclient.TokenCredential, status string, reason string) error GetStatus() string @@ -45,7 +42,7 @@ func StartResourceSyncStatusTask(ctx context.Context, userCred mcclient.TokenCre if err != nil { return err } - obj.SetStatus(userCred, SYNC_STATUS, "perform_syncstatus") + obj.SetStatus(userCred, apis.STATUS_SYNC_STATUS, "perform_syncstatus") task.ScheduleRun(nil) return nil } diff --git a/pkg/compute/regiondrivers/aws.go b/pkg/compute/regiondrivers/aws.go index 9e0d524663..daa93a149e 100644 --- a/pkg/compute/regiondrivers/aws.go +++ b/pkg/compute/regiondrivers/aws.go @@ -42,6 +42,7 @@ import ( "yunion.io/x/onecloud/pkg/util/pinyinutils" "yunion.io/x/onecloud/pkg/util/rand" "yunion.io/x/onecloud/pkg/util/rbacutils" + "yunion.io/x/onecloud/pkg/util/seclib2" ) type SAwsRegionDriver struct { @@ -98,6 +99,67 @@ func networkCheck(network *models.SNetwork) error { return nil } +func (self *SAwsRegionDriver) IsSupportedDBInstance() bool { + return true +} + +func (self *SAwsRegionDriver) GetRdsSupportSecgroupCount() int { + return 1 +} + +func (self *SAwsRegionDriver) ValidateCreateDBInstanceData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, input api.DBInstanceCreateInput, skus []models.SDBInstanceSku, network *models.SNetwork) (api.DBInstanceCreateInput, error) { + if len(input.Password) > 0 { + for _, s := range input.Password { + if s == '/' || s == '"' || s == '@' || s == '\'' { + return input, httperrors.NewInputParameterError("aws rds not support password character %s", string(s)) + } + } + } + if len(input.Password) == 0 { + for _, s := range seclib2.RandomPassword2(100) { + if s == '/' || s == '"' || s == '@' || s == '\'' { + continue + } + input.Password += string(s) + if len(input.Password) >= 20 { + break + } + } + } + return input, nil +} + +func (self *SAwsRegionDriver) ValidateCreateDBInstanceBackupData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceBackupCreateInput) (api.DBInstanceBackupCreateInput, error) { + return input, nil +} + +func (self *SAwsRegionDriver) ValidateCreateDBInstanceDatabaseData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceDatabaseCreateInput) (api.DBInstanceDatabaseCreateInput, error) { + return input, httperrors.NewNotSupportedError("aws not support create rds database") +} + +func (self *SAwsRegionDriver) ValidateCreateDBInstanceAccountData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, instance *models.SDBInstance, input api.DBInstanceAccountCreateInput) (api.DBInstanceAccountCreateInput, error) { + return input, httperrors.NewNotSupportedError("aws not support create rds account") +} + +func (self *SAwsRegionDriver) InitDBInstanceUser(ctx context.Context, instance *models.SDBInstance, task taskman.ITask, desc *cloudprovider.SManagedDBInstanceCreateConfig) error { + user := "admin" + if desc.Engine == api.DBINSTANCE_TYPE_POSTGRESQL || desc.Category == api.DBINSTANCE_TYPE_POSTGRESQL { + user = "postgres" + } + + account := models.SDBInstanceAccount{} + account.DBInstanceId = instance.Id + account.Name = user + account.Status = api.DBINSTANCE_USER_AVAILABLE + account.SetModelManager(models.DBInstanceAccountManager, &account) + err := models.DBInstanceAccountManager.TableSpec().Insert(ctx, &account) + if err != nil { + return err + } + + return account.SetPassword(desc.Password) +} + func validateAwsLbNetwork(ownerId mcclient.IIdentityProvider, data *jsonutils.JSONDict, requiredMin int) (*jsonutils.JSONDict, error) { var networkIds []string if ns, err := data.GetString("network"); err != nil { diff --git a/pkg/compute/regiondrivers/managedvirtual.go b/pkg/compute/regiondrivers/managedvirtual.go index 078e958325..21fce1a392 100644 --- a/pkg/compute/regiondrivers/managedvirtual.go +++ b/pkg/compute/regiondrivers/managedvirtual.go @@ -1580,10 +1580,10 @@ func (self *SManagedVirtualizationRegionDriver) RequestCreateDBInstance(ctx cont return nil, errors.Wrap(err, "dbinstance.GetVpc()") } - params := task.GetParams() - passwd, _ := params.GetString("password") - if len(passwd) == 0 && jsonutils.QueryBoolean(params, "reset_password", true) { - passwd = seclib2.RandomPassword2(12) + input := api.DBInstanceCreateInput{} + task.GetParams().Unmarshal(&input) + if len(input.Password) == 0 && jsonutils.QueryBoolean(task.GetParams(), "reset_password", true) { + input.Password = seclib2.RandomPassword2(12) } desc := cloudprovider.SManagedDBInstanceCreateConfig{ Name: dbinstance.Name, @@ -1597,7 +1597,8 @@ func (self *SManagedVirtualizationRegionDriver) RequestCreateDBInstance(ctx cont EngineVersion: dbinstance.EngineVersion, Category: dbinstance.Category, Port: dbinstance.Port, - Password: passwd, + Password: input.Password, + MultiAz: input.MultiAZ, } desc.Tags, _ = dbinstance.GetAllUserMetadata() diff --git a/pkg/compute/tasks/dbinstance_change_config.go b/pkg/compute/tasks/dbinstance_change_config.go index 6c4f232c12..40462ea740 100644 --- a/pkg/compute/tasks/dbinstance_change_config.go +++ b/pkg/compute/tasks/dbinstance_change_config.go @@ -80,7 +80,7 @@ func (self *DBInstanceChangeConfigTask) OnDBInstanceChangeConfigComplete(ctx con Action: notifyclient.ActionChangeConfig, }) self.SetStage("OnSyncDBInstanceStatusComplete", nil) - models.StartResourceSyncStatusTask(ctx, self.UserCred, rds, "DBInstanceSyncStatusTask", self.GetTaskId()) + rds.StartDBInstanceSyncTask(ctx, self.UserCred, self.GetTaskId()) } func (self *DBInstanceChangeConfigTask) OnDBInstanceChangeConfigCompleteFailed(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_create_task.go b/pkg/compute/tasks/dbinstance_create_task.go index 4e7162ad85..1dadf3bc7a 100644 --- a/pkg/compute/tasks/dbinstance_create_task.go +++ b/pkg/compute/tasks/dbinstance_create_task.go @@ -76,7 +76,7 @@ func (self *DBInstanceCreateTask) OnCreateDBInstanceComplete(ctx context.Context rds := obj.(*models.SDBInstance) logclient.AddActionLogWithStartable(self, rds, logclient.ACT_CREATE, nil, self.UserCred, true) self.SetStage("OnSyncDBInstanceStatusComplete", nil) - models.StartResourceSyncStatusTask(ctx, self.UserCred, rds, "DBInstanceSyncStatusTask", self.GetTaskId()) + rds.StartDBInstanceSyncTask(ctx, self.UserCred, self.GetTaskId()) } func (self *DBInstanceCreateTask) OnCreateDBInstanceCompleteFailed(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_delete_task.go b/pkg/compute/tasks/dbinstance_delete_task.go index 9d6c86902b..f392cbcf1a 100644 --- a/pkg/compute/tasks/dbinstance_delete_task.go +++ b/pkg/compute/tasks/dbinstance_delete_task.go @@ -16,6 +16,7 @@ package tasks import ( "context" + "time" "yunion.io/x/jsonutils" "yunion.io/x/pkg/errors" @@ -71,6 +72,10 @@ func (self *DBInstanceDeleteTask) DeleteDBInstance(ctx context.Context, rds *mod self.taskFailed(ctx, rds, err) return } + err = cloudprovider.WaitDeleted(irds, time.Second*10, time.Minute*50) + if err != nil { + self.taskFailed(ctx, rds, errors.Wrapf(err, "WaitDeleted")) + } } self.DeleteDBInstanceComplete(ctx, rds) diff --git a/pkg/compute/tasks/dbinstance_remote_update_task.go b/pkg/compute/tasks/dbinstance_remote_update_task.go index 2aa6fc13c4..f9bae8ab99 100644 --- a/pkg/compute/tasks/dbinstance_remote_update_task.go +++ b/pkg/compute/tasks/dbinstance_remote_update_task.go @@ -58,7 +58,7 @@ func (self *DBInstanceRemoteUpdateTask) OnInit(ctx context.Context, obj db.IStan func (self *DBInstanceRemoteUpdateTask) OnRemoteUpdateComplete(ctx context.Context, rds *models.SDBInstance, data jsonutils.JSONObject) { self.SetStage("OnSyncStatusComplete", nil) - models.StartResourceSyncStatusTask(ctx, self.UserCred, rds, "DBInstanceSyncStatusTask", self.GetTaskId()) + rds.StartDBInstanceSyncTask(ctx, self.UserCred, self.GetTaskId()) } func (self *DBInstanceRemoteUpdateTask) OnRemoteUpdateCompleteFailed(ctx context.Context, rds *models.SDBInstance, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_renew_task.go b/pkg/compute/tasks/dbinstance_renew_task.go index 6178f4fc93..4ecb145ba6 100644 --- a/pkg/compute/tasks/dbinstance_renew_task.go +++ b/pkg/compute/tasks/dbinstance_renew_task.go @@ -88,7 +88,7 @@ func (self *DBInstanceRenewTask) OnInit(ctx context.Context, obj db.IStandaloneM logclient.AddActionLogWithStartable(self, rds, logclient.ACT_RENEW, map[string]string{"duration": duration}, self.UserCred, true) self.SetStage("OnSyncstatusComplete", nil) - models.StartResourceSyncStatusTask(ctx, self.UserCred, rds, "DBInstanceSyncStatusTask", self.GetTaskId()) + rds.StartDBInstanceSyncTask(ctx, self.UserCred, self.GetTaskId()) } func (self *DBInstanceRenewTask) OnSyncstatusComplete(ctx context.Context, rds *models.SDBInstance, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_set_auto_renew_task.go b/pkg/compute/tasks/dbinstance_set_auto_renew_task.go index 8617c0adc8..03bfe54f92 100644 --- a/pkg/compute/tasks/dbinstance_set_auto_renew_task.go +++ b/pkg/compute/tasks/dbinstance_set_auto_renew_task.go @@ -59,7 +59,7 @@ func (self *DBInstanceSetAutoRenewTask) OnInit(ctx context.Context, obj db.IStan return } self.SetStage("OnDBInstanceSyncComplete", nil) - rds.StartDBInstanceSyncTask(ctx, self.GetUserCred(), nil, "") + rds.StartDBInstanceSyncTask(ctx, self.GetUserCred(), self.GetTaskId()) } func (self *DBInstanceSetAutoRenewTask) OnDBInstanceSyncComplete(ctx context.Context, rds *models.SDBInstance, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_sync_secgroups_task.go b/pkg/compute/tasks/dbinstance_sync_secgroups_task.go index 975dc57dc4..bf3d14e47d 100644 --- a/pkg/compute/tasks/dbinstance_sync_secgroups_task.go +++ b/pkg/compute/tasks/dbinstance_sync_secgroups_task.go @@ -65,7 +65,7 @@ func (self *DBInstanceSyncSecgroupsTask) OnSyncSecurityGroupsCompleteFailed(ctx func (self *DBInstanceSyncSecgroupsTask) OnSyncSecurityGroupsComplete(ctx context.Context, rds *models.SDBInstance, data jsonutils.JSONObject) { self.SetStage("OnSyncComplete", nil) - rds.StartDBInstanceSyncTask(ctx, self.GetUserCred(), nil, self.GetTaskId()) + rds.StartDBInstanceSyncTask(ctx, self.GetUserCred(), self.GetTaskId()) } func (self *DBInstanceSyncSecgroupsTask) OnSyncCompleteFailed(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { diff --git a/pkg/compute/tasks/dbinstance_sync_status_task.go b/pkg/compute/tasks/dbinstance_sync_status_task.go deleted file mode 100644 index 597bb38bed..0000000000 --- a/pkg/compute/tasks/dbinstance_sync_status_task.go +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright 2019 Yunion -// -// 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 tasks - -import ( - "context" - - "yunion.io/x/jsonutils" - "yunion.io/x/pkg/errors" - - api "yunion.io/x/onecloud/pkg/apis/compute" - "yunion.io/x/onecloud/pkg/cloudcommon/db" - "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" - "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" - "yunion.io/x/onecloud/pkg/compute/models" - "yunion.io/x/onecloud/pkg/util/logclient" -) - -type DBInstanceSyncStatusTask struct { - taskman.STask -} - -func init() { - taskman.RegisterTask(DBInstanceSyncStatusTask{}) -} - -func (self *DBInstanceSyncStatusTask) taskFailed(ctx context.Context, dbinstance *models.SDBInstance, err error) { - dbinstance.SetStatus(self.UserCred, api.DBINSTANCE_UNKNOWN, err.Error()) - db.OpsLog.LogEvent(dbinstance, db.ACT_SYNC_STATUS, err, self.GetUserCred()) - logclient.AddActionLogWithStartable(self, dbinstance, logclient.ACT_SYNC_STATUS, err, self.UserCred, false) - notifyclient.EventNotify(ctx, self.GetUserCred(), notifyclient.SEventNotifyParam{ - Obj: dbinstance, - Action: notifyclient.ActionSyncStatus, - IsFail: true, - }) - self.SetStageFailed(ctx, jsonutils.NewString(err.Error())) -} - -func (self *DBInstanceSyncStatusTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { - dbinstance := obj.(*models.SDBInstance) - self.SyncStatusDBInstance(ctx, dbinstance) -} - -func (self *DBInstanceSyncStatusTask) SyncStatusDBInstance(ctx context.Context, dbinstance *models.SDBInstance) { - idbinstance, err := dbinstance.GetIDBInstance() - if err != nil { - self.taskFailed(ctx, dbinstance, errors.Wrapf(err, "dbinstance.GetIDBInstance")) - return - } - dbinstance.SyncAllWithCloudDBInstance(ctx, self.UserCred, dbinstance.GetCloudprovider(), idbinstance) - self.SetStageComplete(ctx, nil) -} diff --git a/pkg/compute/tasks/dbinstance_sync_task.go b/pkg/compute/tasks/dbinstance_sync_task.go index 9ce54ff253..5f344b0e78 100644 --- a/pkg/compute/tasks/dbinstance_sync_task.go +++ b/pkg/compute/tasks/dbinstance_sync_task.go @@ -23,6 +23,7 @@ import ( api "yunion.io/x/onecloud/pkg/apis/compute" "yunion.io/x/onecloud/pkg/cloudcommon/db" "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman" + "yunion.io/x/onecloud/pkg/cloudcommon/notifyclient" "yunion.io/x/onecloud/pkg/compute/models" "yunion.io/x/onecloud/pkg/util/logclient" ) @@ -35,27 +36,32 @@ func init() { taskman.RegisterTask(DBInstanceSyncTask{}) } -func (self *DBInstanceSyncTask) taskFailed(ctx context.Context, dbinstance *models.SDBInstance, err error) { - dbinstance.SetStatus(self.UserCred, api.DBINSTANCE_UNKNOWN, err.Error()) - db.OpsLog.LogEvent(dbinstance, db.ACT_SYNC_CONF, err, self.GetUserCred()) - logclient.AddActionLogWithStartable(self, dbinstance, logclient.ACT_SYNC_CONF, err, self.UserCred, false) +func (self *DBInstanceSyncTask) taskFailed(ctx context.Context, rds *models.SDBInstance, err error) { + rds.SetStatus(self.UserCred, api.DBINSTANCE_UNKNOWN, err.Error()) + db.OpsLog.LogEvent(rds, db.ACT_SYNC_STATUS, err, self.GetUserCred()) + logclient.AddActionLogWithStartable(self, rds, logclient.ACT_SYNC_CONF, err, self.UserCred, false) + notifyclient.EventNotify(ctx, self.GetUserCred(), notifyclient.SEventNotifyParam{ + Obj: rds, + Action: notifyclient.ActionSyncStatus, + IsFail: true, + }) self.SetStageFailed(ctx, jsonutils.NewString(err.Error())) } func (self *DBInstanceSyncTask) OnInit(ctx context.Context, obj db.IStandaloneModel, data jsonutils.JSONObject) { - dbinstance := obj.(*models.SDBInstance) - self.SyncDBInstance(ctx, dbinstance) + rds := obj.(*models.SDBInstance) + self.SyncDBInstance(ctx, rds) } -func (self *DBInstanceSyncTask) SyncDBInstance(ctx context.Context, dbinstance *models.SDBInstance) { - idbinstance, err := dbinstance.GetIDBInstance() +func (self *DBInstanceSyncTask) SyncDBInstance(ctx context.Context, rds *models.SDBInstance) { + irds, err := rds.GetIDBInstance() if err != nil { - self.taskFailed(ctx, dbinstance, errors.Wrapf(err, "dbinstance.GetIDBInstance")) + self.taskFailed(ctx, rds, errors.Wrapf(err, "rds.GetIDBInstance")) return } - err = dbinstance.SyncAllWithCloudDBInstance(ctx, self.UserCred, dbinstance.GetCloudprovider(), idbinstance) + err = rds.SyncAllWithCloudDBInstance(ctx, self.UserCred, rds.GetCloudprovider(), irds) if err != nil { - self.taskFailed(ctx, dbinstance, errors.Wrapf(err, "dbinstance.GetIDBInstance")) + self.taskFailed(ctx, rds, errors.Wrapf(err, "rds.GetIDBInstance")) return } self.SetStageComplete(ctx, nil) diff --git a/pkg/mcclient/options/compute/dbinstances.go b/pkg/mcclient/options/compute/dbinstances.go index f403ac9ce1..bf0c8f2beb 100644 --- a/pkg/mcclient/options/compute/dbinstances.go +++ b/pkg/mcclient/options/compute/dbinstances.go @@ -43,6 +43,7 @@ type DBInstanceCreateOptions struct { AllowDelete *bool `help:"not lock dbinstance" ` Tags []string `help:"Tags info,prefix with 'user:', eg: user:project=default" json:"-"` DBInstancebackupId string `help:"create dbinstance from backup" json:"dbinstancebackup_id"` + MultiAz bool `help:"deploy rds with multi az"` } func (opts *DBInstanceCreateOptions) Params() (jsonutils.JSONObject, error) { diff --git a/pkg/multicloud/aws/aws.go b/pkg/multicloud/aws/aws.go index 261e8f4719..51fa3a32bb 100644 --- a/pkg/multicloud/aws/aws.go +++ b/pkg/multicloud/aws/aws.go @@ -505,7 +505,7 @@ func (self *SAwsClient) GetCapabilities() []string { cloudprovider.CLOUD_CAPABILITY_NETWORK, cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, - cloudprovider.CLOUD_CAPABILITY_RDS + cloudprovider.READ_ONLY_SUFFIX, + cloudprovider.CLOUD_CAPABILITY_RDS, cloudprovider.CLOUD_CAPABILITY_CACHE + cloudprovider.READ_ONLY_SUFFIX, cloudprovider.CLOUD_CAPABILITY_NAT + cloudprovider.READ_ONLY_SUFFIX, cloudprovider.CLOUD_CAPABILITY_EVENT, diff --git a/pkg/multicloud/aws/aws_request.go b/pkg/multicloud/aws/aws_request.go index 9f3a21c5ca..71c86ff949 100644 --- a/pkg/multicloud/aws/aws_request.go +++ b/pkg/multicloud/aws/aws_request.go @@ -29,7 +29,7 @@ import ( "github.com/aws/aws-sdk-go/aws/request" v4 "github.com/aws/aws-sdk-go/aws/signer/v4" "github.com/aws/aws-sdk-go/private/protocol/query" - "github.com/aws/aws-sdk-go/private/protocol/xml/xmlutil" + xj "github.com/basgys/goxml2json" "yunion.io/x/jsonutils" "yunion.io/x/log" @@ -142,37 +142,48 @@ func Build(r *request.Request) { var UnmarshalErrorHandler = request.NamedHandler{Name: "awssdk.ec2query.UnmarshalError", Fn: UnmarshalError} +type sAwsError struct { + Errors struct { + Type string + Code string + Message string + } `json:"Error"` + RequestID string +} + +func (self sAwsError) Error() string { + return jsonutils.Marshal(self).String() +} + func UnmarshalError(r *request.Request) { defer r.HTTPResponse.Body.Close() - respErr := &struct { - XMLName xml.Name `xml:"Response"` - Code string `xml:"Errors>Error>Code"` - Message string `xml:"Errors>Error>Message"` - RequestID string `xml:"RequestID"` - }{} - - err := xmlutil.UnmarshalXMLError(&respErr, r.HTTPResponse.Body) + result, err := xj.Convert(r.HTTPResponse.Body) if err != nil { - r.Error = awserr.NewRequestFailure( - awserr.New(request.ErrCodeSerialization, - "failed to unmarshal error message", err), - r.HTTPResponse.StatusCode, - r.RequestID, - ) + r.Error = errors.Wrapf(err, "xj.Convert") return } - if strings.Contains(respErr.Code, "NotFound") { + obj, err := jsonutils.Parse([]byte(result.String())) + if err != nil { + r.Error = errors.Wrapf(err, "jsonutils.Parse") + return + } + + respErr := &sAwsError{} + err = obj.Unmarshal(respErr, "ErrorResponse") + if err != nil { + r.Error = errors.Wrapf(err, "obj.Unmarshal") + return + } + + if strings.Contains(respErr.Errors.Code, "NotFound") { r.Error = errors.Wrapf(cloudprovider.ErrNotFound, jsonutils.Marshal(respErr).String()) return } - r.Error = awserr.NewRequestFailure( - awserr.New(respErr.Code, respErr.Message, nil), - r.HTTPResponse.StatusCode, - respErr.RequestID, - ) + r.Error = respErr + return } func (self *SAwsClient) request(regionId, serviceName, serviceId, apiVersion string, apiName string, params map[string]string, retval interface{}, assumeRole bool) error { diff --git a/pkg/multicloud/aws/dbinstance.go b/pkg/multicloud/aws/dbinstance.go index 532eb4dda6..b321749b5a 100644 --- a/pkg/multicloud/aws/dbinstance.go +++ b/pkg/multicloud/aws/dbinstance.go @@ -15,7 +15,9 @@ package aws import ( + "context" "fmt" + "strings" "time" "yunion.io/x/jsonutils" @@ -119,6 +121,7 @@ type SDBInstance struct { type SDBInstances struct { DBInstances []SDBInstance `xml:"DBInstances>DBInstance"` + Marker string `xml:"Marker"` } func (rds *SDBInstance) GetName() string { @@ -133,14 +136,17 @@ func (rds *SDBInstance) GetGlobalId() string { return rds.GetId() } +// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/accessing-monitoring.html#Overview.DBInstance.Status func (rds *SDBInstance) GetStatus() string { switch rds.DBInstanceStatus { - case "creating": + case "creating", "backing-up": return api.DBINSTANCE_DEPLOYING case "available": return api.DBINSTANCE_RUNNING case "deleting": return api.DBINSTANCE_DELETING + case "rebooting": + return api.DBINSTANCE_REBOOTING default: log.Errorf("Unknown db instance status: %s", rds.DBInstanceStatus) return api.DBINSTANCE_UNKNOWN @@ -159,11 +165,57 @@ func (rds *SDBInstance) GetCreatedAt() time.Time { return rds.InstanceCreateTime } +func (rds *SDBInstance) Reboot() error { + return rds.region.RebootDBInstance(rds.DBInstanceIdentifier) +} + +func (self *SDBInstance) GetCategory() string { + switch self.Engine { + case "aurora", "aurora-mysql": + return api.DBINSTANCE_TYPE_MYSQL + case "aurora-postgresql": + return api.DBINSTANCE_TYPE_POSTGRESQL + case "oracle-ee", "sqlserver-ee": + return api.AWS_DBINSTANCE_CATEGORY_ENTERPRISE_EDITION + case "oracle-se2": + return api.AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION_TWO + case "sqlserver-se": + return api.AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION + case "sqlserver-ex": + return api.AWS_DBINSTANCE_CATEGORY_EXPRESS_EDITION + case "sqlserver-web": + return api.AWS_DBINSTANCE_CATEGORY_WEB_EDITION + default: + if strings.HasPrefix(self.DBInstanceClass, "db.r") || strings.HasPrefix(self.DBInstanceClass, "db.x") || strings.HasPrefix(self.DBInstanceClass, "db.d") { + return api.AWS_DBINSTANCE_CATEGORY_MEMORY_OPTIMIZED + } + return api.AWS_DBINSTANCE_CATEGORY_GENERAL_PURPOSE + } +} + func (rds *SDBInstance) GetStorageType() string { return rds.StorageType } func (rds *SDBInstance) GetEngine() string { + if strings.Contains(rds.Engine, "aurora") { + return api.DBINSTANCE_TYPE_AURORA + } + if strings.Contains(rds.Engine, "oracle") { + return api.DBINSTANCE_TYPE_ORACLE + } + if strings.Contains(rds.Engine, "sqlserver") { + return api.DBINSTANCE_TYPE_SQLSERVER + } + for k, v := range map[string]string{ + "mariadb": api.DBINSTANCE_TYPE_MARIADB, + "mysql": api.DBINSTANCE_TYPE_MYSQL, + "postgres": api.DBINSTANCE_TYPE_POSTGRESQL, + } { + if rds.Engine == k { + return v + } + } return rds.Engine } @@ -175,6 +227,20 @@ func (rds *SDBInstance) GetInstanceType() string { return rds.DBInstanceClass } +func (rds *SDBInstance) ChangeConfig(ctx context.Context, config *cloudprovider.SManagedDBInstanceChangeConfig) error { + params := map[string]string{ + "DBInstanceIdentifier": rds.DBInstanceIdentifier, + "ApplyImmediately": "true", + } + if config.DiskSizeGB > 0 && rds.GetEngine() != api.DBINSTANCE_TYPE_AURORA { + params["AllocatedStorage"] = fmt.Sprintf("%d", config.DiskSizeGB) + } + if len(config.InstanceType) > 0 { + params["DBInstanceClass"] = config.InstanceType + } + return rds.region.rdsRequest("ModifyDBInstance", params, nil) +} + func (rds *SDBInstance) GetVcpuCount() int { if spec, ok := DBInstanceSpecs[rds.DBInstanceClass]; ok { return spec.VcpuCount @@ -210,11 +276,12 @@ func (rds *SDBInstance) Refresh() error { if err != nil { return err } + rds.AwsTags = instance.AwsTags return jsonutils.Update(rds, instance) } func (region *SRegion) GetDBInstance(instanceId string) (*SDBInstance, error) { - instances, err := region.GetDBInstances(instanceId) + instances, _, err := region.GetDBInstances(instanceId, "") if err != nil { return nil, errors.Wrap(err, "GetDBInstances") } @@ -278,6 +345,22 @@ func (rds *SDBInstance) GetConnectionStr() string { return "" } +func (rds *SDBInstance) OpenPublicConnection() error { + params := map[string]string{ + "DBInstanceIdentifier": rds.DBInstanceIdentifier, + "PubliclyAccessible": "true", + } + return rds.region.rdsRequest("ModifyDBInstance", params, nil) +} + +func (rds *SDBInstance) ClosePublicConnection() error { + params := map[string]string{ + "DBInstanceIdentifier": rds.DBInstanceIdentifier, + "PubliclyAccessible": "false", + } + return rds.region.rdsRequest("ModifyDBInstance", params, nil) +} + func (rds *SDBInstance) GetIDBInstanceParameters() ([]cloudprovider.ICloudDBInstanceParameter, error) { parameters, err := rds.region.GetDBInstanceParameters(rds.DBParameterGroups[0].DBParameterGroupName) if err != nil { @@ -300,7 +383,37 @@ func (rds *SDBInstance) GetIDBInstanceDatabases() ([]cloudprovider.ICloudDBInsta return idatabases, nil } -func (region *SRegion) GetDBInstances(instanceId string) ([]SDBInstance, error) { +func (rds *SDBInstance) GetIDBInstanceBackups() ([]cloudprovider.ICloudDBInstanceBackup, error) { + backups, err := rds.region.GetDBInstanceSnapshots(rds.DBInstanceIdentifier, "") + if err != nil { + return nil, err + } + ret := []cloudprovider.ICloudDBInstanceBackup{} + for i := range backups { + backups[i].region = rds.region + ret = append(ret, &backups[i]) + } + return ret, nil +} + +func (rds *SDBInstance) CreateIBackup(conf *cloudprovider.SDBInstanceBackupCreateConfig) (string, error) { + params := map[string]string{ + "DBInstanceIdentifier": rds.DBInstanceIdentifier, + "DBSnapshotIdentifier": conf.Name, + } + ret := struct { + DBSnapshot SDBInstanceSnapshot `xml:"DBSnapshot"` + }{} + err := rds.region.rdsRequest("CreateDBSnapshot", params, &ret) + if err != nil { + return "", err + } + ret.DBSnapshot.region = rds.region + cloudprovider.WaitStatus(&ret.DBSnapshot, api.DBINSTANCE_BACKUP_READY, time.Second*10, time.Hour*2) + return ret.DBSnapshot.GetGlobalId(), nil +} + +func (region *SRegion) GetDBInstances(instanceId, marker string) ([]SDBInstance, string, error) { instances := SDBInstances{} params := map[string]string{} idx := 1 @@ -308,28 +421,43 @@ func (region *SRegion) GetDBInstances(instanceId string) ([]SDBInstance, error) params[fmt.Sprintf("Filters.Filter.%d.Name", idx)] = "dbi-resource-id" params[fmt.Sprintf("Filters.Filter.%d.Values.Value.1", idx)] = instanceId } + + if len(marker) > 0 { + params["Marker"] = marker + } + err := region.rdsRequest("DescribeDBInstances", params, &instances) if err != nil { - return nil, errors.Wrap(err, "DescribeDBInstances") + return nil, "", errors.Wrap(err, "DescribeDBInstances") } - return instances.DBInstances, nil + return instances.DBInstances, instances.Marker, nil } func (region *SRegion) GetIDBInstances() ([]cloudprovider.ICloudDBInstance, error) { - instances, err := region.GetDBInstances("") + idbinstances := []cloudprovider.ICloudDBInstance{} + instances, marker, err := region.GetDBInstances("", "") if err != nil { return nil, errors.Wrap(err, "GetDBInstances") } - idbinstances := []cloudprovider.ICloudDBInstance{} for i := 0; i < len(instances); i++ { instances[i].region = region idbinstances = append(idbinstances, &instances[i]) } + for len(marker) > 0 { + instances, marker, err = region.GetDBInstances("", marker) + if err != nil { + return nil, errors.Wrap(err, "GetDBInstances") + } + for i := 0; i < len(instances); i++ { + instances[i].region = region + idbinstances = append(idbinstances, &instances[i]) + } + } return idbinstances, nil } func (self *SRegion) GetIDBInstanceById(id string) (cloudprovider.ICloudDBInstance, error) { - instances, err := self.GetDBInstances(id) + instances, _, err := self.GetDBInstances(id, "") if err != nil { return nil, errors.Wrap(err, "GetDBInstances") } @@ -345,3 +473,183 @@ func (self *SRegion) GetIDBInstanceById(id string) (cloudprovider.ICloudDBInstan instances[0].region = self return &instances[0], nil } + +func (self *SRegion) CreateIDBInstance(desc *cloudprovider.SManagedDBInstanceCreateConfig) (cloudprovider.ICloudDBInstance, error) { + params := map[string]string{ + "DBInstanceClass": desc.InstanceType, + "DBInstanceIdentifier": desc.Name, + "EngineVersion": desc.EngineVersion, + "MasterUsername": "admin", + } + if len(desc.Password) > 0 { + params["MasterUserPassword"] = desc.Password + } + + if desc.Engine != api.DBINSTANCE_TYPE_AURORA { + params["StorageType"] = desc.StorageType + params["AllocatedStorage"] = fmt.Sprintf("%d", desc.DiskSizeGB) + for i, sec := range desc.SecgroupIds { + params[fmt.Sprintf("VpcSecurityGroupIds.VpcSecurityGroupId.%d", i+1)] = sec + } + } + if desc.MultiAz { + params["MultiAZ"] = "true" + } + if desc.StorageType == api.STORAGE_IO1_SSD { + params["Iops"] = "3000" + } + switch desc.Engine { + case api.DBINSTANCE_TYPE_MYSQL: + params["Engine"] = "mysql" + case api.DBINSTANCE_TYPE_POSTGRESQL: + params["Engine"] = "postgres" + params["MasterUsername"] = "postgres" + case api.DBINSTANCE_TYPE_MARIADB: + params["Engine"] = "mariadb" + case api.DBINSTANCE_TYPE_SQLSERVER: + params["LicenseModel"] = "license-included" + switch desc.Category { + case api.AWS_DBINSTANCE_CATEGORY_ENTERPRISE_EDITION: + params["Engine"] = "sqlserver-ee" + case api.AWS_DBINSTANCE_CATEGORY_EXPRESS_EDITION: + params["Engine"] = "sqlserver-ex" + params["MultiAZ"] = "false" + case api.AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION: + params["Engine"] = "sqlserver-se" + case api.AWS_DBINSTANCE_CATEGORY_WEB_EDITION: + params["Engine"] = "sqlserver-web" + params["MultiAZ"] = "false" + default: + return nil, fmt.Errorf("invalid category %s for engine %s", desc.Category, desc.Engine) + } + case api.DBINSTANCE_TYPE_AURORA: + delete(params, "MultiAZ") + switch desc.Category { + case api.DBINSTANCE_TYPE_MYSQL: + params["Engine"] = "aurora" + if !strings.HasPrefix(desc.EngineVersion, "5.6") { + params["Engine"] = "aurora-mysql" + } + case api.DBINSTANCE_TYPE_POSTGRESQL: + params["Engine"] = "aurora-postgresql" + params["MasterUsername"] = "postgres" + default: + return nil, fmt.Errorf("invalid category %s for engine %s", desc.Category, desc.Engine) + } + case api.DBINSTANCE_TYPE_ORACLE: + params["LicenseModel"] = "bring-your-own-license" + switch desc.Category { + case api.AWS_DBINSTANCE_CATEGORY_ENTERPRISE_EDITION: + params["Engine"] = "oracle-ee" + case api.AWS_DBINSTANCE_CATEGORY_STANDARD_EDITION_TWO: + params["Engine"] = "oracle-se2" + default: + return nil, fmt.Errorf("invalid category %s for engine %s", desc.Category, desc.Engine) + } + } + i := 1 + for k, v := range desc.Tags { + params[fmt.Sprintf("Tags.Tag.%d.Key", i)] = k + params[fmt.Sprintf("Tags.Tag.%d.Value", i)] = v + i++ + } + result := struct { + DBInstance SDBInstance `xml:"DBInstance"` + }{} + result.DBInstance.region = self + return &result.DBInstance, self.rdsRequest("CreateDBInstance", params, &result) +} + +func (self *SDBInstance) Delete() error { + params := map[string]string{ + "DBInstanceIdentifier": self.DBInstanceIdentifier, + "SkipFinalSnapshot": "true", + } + return self.region.rdsRequest("DeleteDBInstance", params, nil) +} + +func (self *SRegion) RebootDBInstance(id string) error { + params := map[string]string{ + "DBInstanceIdentifier": id, + } + return self.rdsRequest("RebootDBInstance", params, nil) +} + +func (self *SDBInstance) SetTags(tags map[string]string, replace bool) error { + oldTags, err := self.region.ListRdsResourceTags(self.DBInstanceArn) + if err != nil { + return errors.Wrapf(err, "ListRdsResourceTags") + } + added, removed := map[string]string{}, map[string]string{} + for k, v := range tags { + oldValue, ok := oldTags[k] + if !ok { + added[k] = v + } else if oldValue != v { + removed[k] = oldValue + added[k] = v + } + } + if replace { + for k, v := range oldTags { + newValue, ok := tags[k] + if !ok { + removed[k] = v + } else if v != newValue { + added[k] = newValue + removed[k] = v + } + } + } + if len(removed) > 0 { + err = self.region.RemoveRdsTagsFromResource(self.DBInstanceArn, removed) + if err != nil { + return errors.Wrapf(err, "RemoveRdsTagsFromResource %s", removed) + } + } + if len(added) > 0 { + return self.region.AddRdsTagsToResource(self.DBInstanceArn, added) + } + return nil +} + +func (self *SRegion) ListRdsResourceTags(arn string) (map[string]string, error) { + params := map[string]string{ + "ResourceName": arn, + } + tags := multicloud.AwsTags{} + err := self.rdsRequest("ListTagsForResource", params, &tags) + if err != nil { + return nil, errors.Wrapf(err, "ListTagsForResource") + } + return tags.GetTags() +} + +func (self *SRegion) AddRdsTagsToResource(arn string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + params := map[string]string{ + "ResourceName": arn, + } + i := 1 + for k, v := range tags { + params[fmt.Sprintf("Tags.member.%d.Key", i)] = k + params[fmt.Sprintf("Tags.member.%d.Value", i)] = v + } + return self.rdsRequest("AddTagsToResource", params, nil) +} + +func (self *SRegion) RemoveRdsTagsFromResource(arn string, tags map[string]string) error { + if len(tags) == 0 { + return nil + } + params := map[string]string{ + "ResourceName": arn, + } + i := 1 + for k := range tags { + params[fmt.Sprintf("TagKeys.member.%d", i)] = k + } + return self.rdsRequest("RemoveTagsFromResource", params, nil) +} diff --git a/pkg/multicloud/aws/dbinstance_const.go b/pkg/multicloud/aws/dbinstance_const.go index f57dc3edfb..dfdcd80997 100644 --- a/pkg/multicloud/aws/dbinstance_const.go +++ b/pkg/multicloud/aws/dbinstance_const.go @@ -20,6 +20,11 @@ type SDBInstanceSpec struct { } var DBInstanceSpecs = map[string]SDBInstanceSpec{ + "db.m1.small": {VcpuCount: 1, VmemSizeMb: 1.75 * 1024.0}, + "db.m1.medium": {VcpuCount: 1, VmemSizeMb: 3.75 * 1024.0}, + "db.m1.large": {VcpuCount: 2, VmemSizeMb: 7.5 * 1024}, + "db.m1.xlarge": {VcpuCount: 4, VmemSizeMb: 15 * 1024}, + "db.t3.micro": {VcpuCount: 2, VmemSizeMb: 1 * 1024}, "db.t3.small": {VcpuCount: 2, VmemSizeMb: 2 * 1024}, "db.t3.medium": {VcpuCount: 2, VmemSizeMb: 4 * 1024}, @@ -34,6 +39,13 @@ var DBInstanceSpecs = map[string]SDBInstanceSpec{ "db.t2.xlarge": {VcpuCount: 4, VmemSizeMb: 4 * 1024}, "db.t2.2xlarge": {VcpuCount: 8, VmemSizeMb: 8 * 1024}, + "db.t4g.micro": {VcpuCount: 2, VmemSizeMb: 1 * 1024}, + "db.t4g.small": {VcpuCount: 2, VmemSizeMb: 2 * 1024}, + "db.t4g.medium": {VcpuCount: 2, VmemSizeMb: 4 * 1024}, + "db.t4g.large": {VcpuCount: 2, VmemSizeMb: 8 * 1024}, + "db.t4g.xlarge": {VcpuCount: 4, VmemSizeMb: 16 * 1024}, + "db.t4g.2xlarge": {VcpuCount: 8, VmemSizeMb: 32 * 1024}, + "db.m6g.large": {VcpuCount: 2, VmemSizeMb: 8 * 1024}, "db.m6g.xlarge": {VcpuCount: 4, VmemSizeMb: 16 * 1024}, "db.m6g.2xlarge": {VcpuCount: 8, VmemSizeMb: 32 * 1024}, @@ -42,14 +54,14 @@ var DBInstanceSpecs = map[string]SDBInstanceSpec{ "db.m6g.12xlarge": {VcpuCount: 48, VmemSizeMb: 192 * 1024}, "db.m6g.16xlarge": {VcpuCount: 64, VmemSizeMb: 256 * 1024}, - "db.m5.large": {VcpuCount: 2, VmemSizeMb: 8 * 1024}, - "db.m5.xlarge": {VcpuCount: 4, VmemSizeMb: 16 * 1024}, - "db.m5.2xlarge": {VcpuCount: 8, VmemSizeMb: 32 * 1024}, - "db.m5.4xlarge": {VcpuCount: 16, VmemSizeMb: 64 * 1024}, - "db.m5.8xlarge": {VcpuCount: 32, VmemSizeMb: 128 * 1024}, - "db.m5.12xlarge4": {VcpuCount: 48, VmemSizeMb: 192 * 1024}, - "db.m5.16xlarge2": {VcpuCount: 64, VmemSizeMb: 256 * 1024}, - "db.m5.24xlarge8": {VcpuCount: 96, VmemSizeMb: 384 * 1024}, + "db.m5.large": {VcpuCount: 2, VmemSizeMb: 8 * 1024}, + "db.m5.xlarge": {VcpuCount: 4, VmemSizeMb: 16 * 1024}, + "db.m5.2xlarge": {VcpuCount: 8, VmemSizeMb: 32 * 1024}, + "db.m5.4xlarge": {VcpuCount: 16, VmemSizeMb: 64 * 1024}, + "db.m5.8xlarge": {VcpuCount: 32, VmemSizeMb: 128 * 1024}, + "db.m5.12xlarge": {VcpuCount: 48, VmemSizeMb: 192 * 1024}, + "db.m5.16xlarge": {VcpuCount: 64, VmemSizeMb: 256 * 1024}, + "db.m5.24xlarge": {VcpuCount: 96, VmemSizeMb: 384 * 1024}, "db.m5d.large": {VcpuCount: 2, VmemSizeMb: 8 * 1024}, "db.m5d.xlarge": {VcpuCount: 4, VmemSizeMb: 16 * 1024}, @@ -68,6 +80,16 @@ var DBInstanceSpecs = map[string]SDBInstanceSpec{ "db.r6g.12xlarge": {VcpuCount: 48, VmemSizeMb: 384 * 1024}, "db.r6g.16xlarge": {VcpuCount: 64, VmemSizeMb: 512 * 1024}, + "db.r6gd.medium": {VcpuCount: 1, VmemSizeMb: 8 * 1024}, + "db.r6gd.large": {VcpuCount: 2, VmemSizeMb: 16 * 1024}, + "db.r6gd.xlarge": {VcpuCount: 4, VmemSizeMb: 32 * 1024}, + "db.r6gd.2xlarge": {VcpuCount: 8, VmemSizeMb: 64 * 1024}, + "db.r6gd.4xlarge": {VcpuCount: 16, VmemSizeMb: 128 * 1024}, + "db.r6gd.8xlarge": {VcpuCount: 32, VmemSizeMb: 256 * 1024}, + "db.r6gd.12xlarge": {VcpuCount: 48, VmemSizeMb: 383 * 1024}, + "db.r6gd.16xlarge": {VcpuCount: 64, VmemSizeMb: 512 * 1024}, + "db.r6gd.metal": {VcpuCount: 64, VmemSizeMb: 512 * 1024}, + "db.r5.large": {VcpuCount: 2, VmemSizeMb: 16 * 1024}, "db.r5.xlarge": {VcpuCount: 4, VmemSizeMb: 32 * 1024}, "db.r5.2xlarge": {VcpuCount: 8, VmemSizeMb: 64 * 1024}, @@ -149,4 +171,13 @@ var DBInstanceSpecs = map[string]SDBInstanceSpec{ "db.r4.4xlarge": {VcpuCount: 16, VmemSizeMb: 122 * 1024}, "db.r4.8xlarge": {VcpuCount: 32, VmemSizeMb: 244 * 1024}, "db.r4.16xlarge": {VcpuCount: 64, VmemSizeMb: 488 * 1024}, + + "db.x2g.medium": {VcpuCount: 1, VmemSizeMb: 16 * 1024}, + "db.x2g.large": {VcpuCount: 2, VmemSizeMb: 32 * 1024}, + "db.x2g.xlarge": {VcpuCount: 4, VmemSizeMb: 64 * 1024}, + "db.x2g.2xlarge": {VcpuCount: 8, VmemSizeMb: 128 * 1024}, + "db.x2g.4xlarge": {VcpuCount: 16, VmemSizeMb: 256 * 1024}, + "db.x2g.8xlarge": {VcpuCount: 32, VmemSizeMb: 512 * 1024}, + "db.x2g.12xlarge": {VcpuCount: 48, VmemSizeMb: 768 * 1024}, + "db.x2g.16xlarge": {VcpuCount: 64, VmemSizeMb: 1024 * 1024}, } diff --git a/pkg/multicloud/aws/dbinstance_snapshot.go b/pkg/multicloud/aws/dbinstance_snapshot.go index 6709fa798d..bb73818deb 100644 --- a/pkg/multicloud/aws/dbinstance_snapshot.go +++ b/pkg/multicloud/aws/dbinstance_snapshot.go @@ -17,6 +17,7 @@ package aws import ( "time" + "yunion.io/x/jsonutils" "yunion.io/x/log" "yunion.io/x/pkg/errors" @@ -55,6 +56,7 @@ type SDBInstanceSnapshot struct { type SDBInstanceSnapshots struct { Snapshots []SDBInstanceSnapshot `xml:"DBSnapshots>DBSnapshot"` + Marker string `xml:"Marker"` } func (snapshot *SDBInstanceSnapshot) GetId() string { @@ -104,6 +106,14 @@ func (snapshot *SDBInstanceSnapshot) GetStatus() string { } } +func (self *SDBInstanceSnapshot) Refresh() error { + snap, err := self.region.GetRdsSnapshot(self.DBSnapshotIdentifier) + if err != nil { + return err + } + return jsonutils.Update(self, snap) +} + func (snapshot *SDBInstanceSnapshot) GetBackupSizeMb() int { return snapshot.AllocatedStorage * 1024 } @@ -116,23 +126,62 @@ func (snapshot *SDBInstanceSnapshot) GetDBInstanceId() string { return snapshot.DbiResourceId } -func (region *SRegion) GetDBInstanceSnapshots(instanceId string) ([]SDBInstanceSnapshot, error) { +func (self *SDBInstanceSnapshot) Delete() error { + return self.region.DeleteRdsSnapshot(self.DBSnapshotIdentifier) +} + +func (self *SRegion) DeleteRdsSnapshot(id string) error { + params := map[string]string{ + "DBSnapshotIdentifier": id, + } + return self.rdsRequest("DeleteDBSnapshot", params, nil) +} + +func (region *SRegion) GetDBInstanceSnapshots(instanceId, backupId string) ([]SDBInstanceSnapshot, error) { params := map[string]string{} if len(instanceId) > 0 { - params["DbiResourceId"] = instanceId + params["DBInstanceIdentifier"] = instanceId } + if len(backupId) > 0 { + params["DBSnapshotIdentifier"] = backupId + } + ret, marker := []SDBInstanceSnapshot{}, "" + for { + snapshots := SDBInstanceSnapshots{} + params["Marker"] = marker + err := region.rdsRequest("DescribeDBSnapshots", params, &snapshots) + if err != nil { + return nil, errors.Wrap(err, "DescribeDBSnapshots") + } + ret = append(ret, snapshots.Snapshots...) + if len(snapshots.Marker) == 0 { + break + } + marker = snapshots.Marker + } + return ret, nil +} - snapshots := SDBInstanceSnapshots{} - - err := region.rdsRequest("DescribeDBSnapshots", params, &snapshots) +func (self *SRegion) GetRdsSnapshot(id string) (*SDBInstanceSnapshot, error) { + if len(id) == 0 { + return nil, cloudprovider.ErrNotFound + } + backups, err := self.GetDBInstanceSnapshots("", id) if err != nil { - return nil, errors.Wrap(err, "DescribeDBSnapshots") + return nil, err } - return snapshots.Snapshots, nil + if len(backups) == 1 { + backups[0].region = self + return &backups[0], nil + } + if len(backups) == 0 { + return nil, cloudprovider.ErrNotFound + } + return nil, cloudprovider.ErrDuplicateId } func (region *SRegion) GetIDBInstanceBackups() ([]cloudprovider.ICloudDBInstanceBackup, error) { - snapshots, err := region.GetDBInstanceSnapshots("") + snapshots, err := region.GetDBInstanceSnapshots("", "") if err != nil { return nil, errors.Wrap(err, "GetDBInstanceSnapshots") } diff --git a/pkg/multicloud/aws/shell/dbinstance.go b/pkg/multicloud/aws/shell/dbinstance.go index 2db5b307ed..98e9523782 100644 --- a/pkg/multicloud/aws/shell/dbinstance.go +++ b/pkg/multicloud/aws/shell/dbinstance.go @@ -22,15 +22,14 @@ import ( func init() { type DBInstanceListOptions struct { Id string - Offset int - Limit int + Marker string } shellutils.R(&DBInstanceListOptions{}, "dbinstance-list", "List rds intances", func(cli *aws.SRegion, args *DBInstanceListOptions) error { - instances, err := cli.GetDBInstances(args.Id) + instances, _, err := cli.GetDBInstances(args.Id, args.Marker) if err != nil { return err } - printList(instances, 0, args.Offset, args.Limit, []string{}) + printList(instances, 0, 0, 0, []string{}) return nil }) @@ -47,4 +46,13 @@ func init() { return nil }) + shellutils.R(&DBInstanceIdOptions{}, "dbinstance-tags-list", "Show rds intance tags", func(cli *aws.SRegion, args *DBInstanceIdOptions) error { + instance, err := cli.ListRdsResourceTags(args.ID) + if err != nil { + return err + } + printObject(instance) + return nil + }) + } diff --git a/pkg/multicloud/aws/shell/dbinstance_snapshot.go b/pkg/multicloud/aws/shell/dbinstance_snapshot.go index 7fa9410f4f..8d63382999 100644 --- a/pkg/multicloud/aws/shell/dbinstance_snapshot.go +++ b/pkg/multicloud/aws/shell/dbinstance_snapshot.go @@ -22,10 +22,11 @@ import ( func init() { type DBInstanceSnapshotListOptions struct { DBInstanceId string + Id string } shellutils.R(&DBInstanceSnapshotListOptions{}, "dbinstance-snapshot-list", "List rds intance snapshots", func(cli *aws.SRegion, args *DBInstanceSnapshotListOptions) error { - snapshots, err := cli.GetDBInstanceSnapshots(args.DBInstanceId) + snapshots, err := cli.GetDBInstanceSnapshots(args.DBInstanceId, args.Id) if err != nil { return err } diff --git a/pkg/multicloud/tag_base.go b/pkg/multicloud/tag_base.go index c3ba0a879c..6a3915e48e 100644 --- a/pkg/multicloud/tag_base.go +++ b/pkg/multicloud/tag_base.go @@ -233,8 +233,15 @@ type SAwsTag struct { Value string `xml:"value"` } +type SAwsRdsTag struct { + Key string `xml:"Key"` + Value string `xml:"Value"` +} + type AwsTags struct { TagSet []SAwsTag `xml:"tagSet>item"` + // rds + TagList []SAwsRdsTag `xml:"TagList>Tag"` } func (self AwsTags) GetName() string { @@ -254,6 +261,12 @@ func (self *AwsTags) GetTags() (map[string]string, error) { } ret[tag.Key] = tag.Value } + for _, tag := range self.TagList { + if strings.ToLower(tag.Key) == "name" || strings.ToLower(tag.Key) == "description" { + continue + } + ret[tag.Key] = tag.Value + } return ret, nil }