周玉环 před 1 dnem
rodič
revize
ebd4ae27c3

+ 53 - 14
backend/cmd/climc/shell/compute/containers.go

@@ -19,6 +19,7 @@ import (
 	"io/ioutil"
 	"os"
 	"os/exec"
+	"path"
 	"strings"
 
 	"github.com/ghodss/yaml"
@@ -31,6 +32,7 @@ import (
 	"yunion.io/x/onecloud/pkg/mcclient"
 	modules "yunion.io/x/onecloud/pkg/mcclient/modules/compute"
 	options "yunion.io/x/onecloud/pkg/mcclient/options/compute"
+	"yunion.io/x/onecloud/pkg/util/fileutils2"
 	"yunion.io/x/onecloud/pkg/util/pod/stream/cp"
 )
 
@@ -137,15 +139,18 @@ func init() {
 		return nil
 	})
 
-	R(new(options.ContainerCopyOptions), "container-cp", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyOptions) error {
-		parts := strings.Split(opts.CONTAINER_ID_FILE, ":")
+	copyToContainer := func(s *mcclient.ClientSession, src, dst string, rawFile bool) error {
+		parts := strings.Split(dst, ":")
 		if len(parts) != 2 {
-			return fmt.Errorf("invalid container id: %s", opts.CONTAINER_ID_FILE)
+			return fmt.Errorf("invalid container id: %s", dst)
 		}
-		if opts.RawFile {
-			fr, err := os.Open(opts.SRC_FILE)
+		if rawFile {
+			if fileutils2.IsDir(src) {
+				return fmt.Errorf("source path cannot be a directory when raw file is true: %s", src)
+			}
+			fr, err := os.Open(src)
 			if err != nil {
-				return errors.Wrapf(err, "open file: %v", opts.SRC_FILE)
+				return errors.Wrapf(err, "open file: %v", src)
 			}
 			defer fr.Close()
 			if err := modules.Containers.CopyTo(s, parts[0], parts[1], fr); err != nil {
@@ -153,22 +158,24 @@ func init() {
 			}
 			return nil
 		} else {
-			return cp.NewCopy().CopyToContainer(s, opts.SRC_FILE, cp.ContainerFileOpt{
+			return cp.NewCopy().CopyToContainer(s, src, cp.ContainerFileOpt{
 				ContainerId: parts[0],
 				File:        parts[1],
 			})
 		}
-	})
-
-	R(new(options.ContainerCopyOptions), "container-cp-from", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyOptions) error {
-		parts := strings.Split(opts.SRC_FILE, ":")
+	}
+	copyFromContainer := func(s *mcclient.ClientSession, src, dst string, rawFile bool) error {
+		parts := strings.Split(src, ":")
 		if len(parts) != 2 {
-			return fmt.Errorf("invalid container id: %s", opts.CONTAINER_ID_FILE)
+			return fmt.Errorf("invalid container id: %s", src)
 		}
 		ctrId := parts[0]
 		ctrFile := parts[1]
-		destFile := opts.CONTAINER_ID_FILE
-		if opts.RawFile {
+		destFile := dst
+		if fileutils2.IsDir(destFile) {
+			destFile = path.Join(destFile, path.Base(ctrFile))
+		}
+		if rawFile {
 			fw, err := os.Create(destFile)
 			if err != nil {
 				return errors.Wrapf(err, "open file: %v", destFile)
@@ -184,5 +191,37 @@ func init() {
 				File:        ctrFile,
 			}, destFile)
 		}
+	}
+
+	R(new(options.ContainerCopyOptions), "container-cp", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyOptions) error {
+		if strings.Contains(opts.SRC, ":") && !strings.Contains(opts.DST, ":") {
+			// copy from container to host
+			err := copyFromContainer(s, opts.SRC, opts.DST, opts.RawFile)
+			if err != nil {
+				return errors.Wrapf(err, "copy from container to host")
+			}
+			return nil
+		} else if !strings.Contains(opts.SRC, ":") && strings.Contains(opts.DST, ":") {
+			// copy from host to container
+			err := copyToContainer(s, opts.SRC, opts.DST, opts.RawFile)
+			if err != nil {
+				return errors.Wrapf(err, "copy to container")
+			}
+			return nil
+		} else if strings.Contains(opts.SRC, ":") && strings.Contains(opts.DST, ":") {
+			// copy from container to container
+			return fmt.Errorf("copy from container to container is not supported")
+		} else {
+			return fmt.Errorf("copy between local files is not supported")
+		}
+
+	})
+
+	R(new(options.ContainerCopyFromOptions), "container-cp-from", "Container copy", func(s *mcclient.ClientSession, opts *options.ContainerCopyFromOptions) error {
+		err := copyFromContainer(s, opts.CONTAINER_ID_PATH, opts.DST_PATH, opts.RawFile)
+		if err != nil {
+			return errors.Wrapf(err, "copy from container to host")
+		}
+		return nil
 	})
 }

+ 2 - 0
backend/cmd/climc/shell/notify/topic.go

@@ -27,4 +27,6 @@ func init() {
 	cmd.Update(new(options.TopicUpdateOptions))
 	cmd.Show(new(options.TopicOptions))
 	cmd.Delete(new(options.TopicOptions))
+	cmd.Perform("add-actions", new(options.TopicAddActionInput))
+	cmd.Perform("add-resources", new(options.TopicAddResourcesInput))
 }

+ 8 - 0
backend/pkg/apis/notify/topic.go

@@ -94,3 +94,11 @@ type STopicCreateInput struct {
 	Actions           []string         `json:"actions"`
 	WebconsoleDisable bool             `json:"webconsole_disable"`
 }
+
+type TopicAddActionInput struct {
+	Actions []string `json:"actions"`
+}
+
+type TopicAddResourcesInput struct {
+	Resources []string `json:"resources"`
+}

+ 5 - 0
backend/pkg/compute/tasks/disk/disk_batch_create_task.go

@@ -138,6 +138,11 @@ func (task *DiskBatchCreateTask) SaveScheduleResult(ctx context.Context, obj uti
 		task.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
 		db.OpsLog.LogEvent(disk, db.ACT_ALLOCATE_FAIL, err, task.UserCred)
 		notifyclient.NotifySystemErrorWithCtx(ctx, disk.Id, disk.Name, api.DISK_ALLOC_FAILED, err.Error())
+		notifyclient.EventNotify(ctx, task.UserCred, notifyclient.SEventNotifyParam{
+			Obj:    disk,
+			Action: notifyclient.ActionCreate,
+			IsFail: true,
+		})
 	}
 
 	data := utils.GetBatchParamsAtIndex(task, index)

+ 6 - 0
backend/pkg/compute/tasks/disk/disk_change_storage_type_task.go

@@ -24,6 +24,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"
 )
@@ -82,5 +83,10 @@ func (self *DiskChangeStorageTypeTask) OnInit(ctx context.Context, obj db.IStand
 func (self *DiskChangeStorageTypeTask) taskFail(ctx context.Context, disk *models.SDisk, err error) {
 	disk.SetStatus(ctx, self.GetUserCred(), api.DISK_MIGRATE_FAIL, "")
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_MIGRATE, err, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionMigrate,
+		IsFail: true,
+	})
 	self.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
 }

+ 10 - 0
backend/pkg/compute/tasks/disk/disk_create_task.go

@@ -102,6 +102,11 @@ func (self *DiskCreateTask) OnStorageCacheImageComplete(ctx context.Context, dis
 func (self *DiskCreateTask) OnStartAllocateFailed(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
 	disk.SetStatus(ctx, self.UserCred, api.DISK_ALLOC_FAILED, data.String())
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_ALLOCATE, data, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionCreate,
+		IsFail: true,
+	})
 	self.SetStageFailed(ctx, data)
 }
 
@@ -155,6 +160,11 @@ func (self *DiskCreateTask) OnDiskReadyFailed(ctx context.Context, disk *models.
 	}
 	disk.SetStatus(ctx, self.UserCred, status, data.String())
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_ALLOCATE, data, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionCreate,
+		IsFail: true,
+	})
 	self.SetStageFailed(ctx, data)
 }
 

+ 10 - 0
backend/pkg/compute/tasks/disk/disk_delete_task.go

@@ -231,6 +231,11 @@ func (self *DiskDeleteTask) OnGuestDiskDeleteCompleteFailed(ctx context.Context,
 	disk.SetStatus(ctx, self.GetUserCred(), api.DISK_DEALLOC_FAILED, reason.String())
 	self.SetStageFailed(ctx, reason)
 	db.OpsLog.LogEvent(disk, db.ACT_DELOCATE_FAIL, disk.GetShortDesc(ctx), self.GetUserCred())
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionDelete,
+		IsFail: true,
+	})
 	logclient.AddActionLogWithContext(ctx, disk, logclient.ACT_DELOCATE, reason, self.UserCred, false)
 }
 
@@ -276,5 +281,10 @@ func (self *StorageDeleteRbdDiskTask) DeleteDisk(ctx context.Context, storage *m
 func (self *StorageDeleteRbdDiskTask) OnDeleteDiskFailed(ctx context.Context, storage *models.SStorage, data jsonutils.JSONObject) {
 	deleteDisk, _ := data.GetString("delete_disk")
 	db.OpsLog.LogEvent(storage, db.ACT_DELETE_OBJECT, fmt.Sprintf("delete disk %s failed", deleteDisk), self.UserCred)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    storage,
+		Action: notifyclient.ActionDelete,
+		IsFail: true,
+	})
 	self.DeleteDisk(ctx, storage, self.Params)
 }

+ 5 - 0
backend/pkg/compute/tasks/disk/disk_reset_task.go

@@ -54,6 +54,11 @@ func (self *DiskResetTask) getSnapshot() (*models.SSnapshot, error) {
 func (self *DiskResetTask) TaskFailed(ctx context.Context, disk *models.SDisk, reason error) {
 	disk.SetStatus(ctx, self.UserCred, api.DISK_READY, "")
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_RESET_DISK, reason, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionReset,
+		IsFail: true,
+	})
 	snapshot, _ := self.getSnapshot()
 	if snapshot != nil {
 		logclient.AddActionLogWithStartable(self, snapshot, logclient.ACT_RESET_DISK, reason, self.UserCred, false)

+ 11 - 0
backend/pkg/compute/tasks/disk/disk_resize_task.go

@@ -25,6 +25,7 @@ import (
 	"yunion.io/x/onecloud/pkg/cloudcommon/db"
 	"yunion.io/x/onecloud/pkg/cloudcommon/db/quotas"
 	"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/mcclient"
 	"yunion.io/x/onecloud/pkg/util/logclient"
@@ -97,6 +98,11 @@ func (self *DiskResizeTask) OnStartResizeDiskFailed(ctx context.Context, disk *m
 	self.SetStageFailed(ctx, jsonutils.Marshal(reason))
 	db.OpsLog.LogEvent(disk, db.ACT_RESIZE_FAIL, reason, self.GetUserCred())
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_RESIZE, reason, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionResize,
+		IsFail: true,
+	})
 }
 
 func (self *DiskResizeTask) OnDiskResizeComplete(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
@@ -157,5 +163,10 @@ func (self *DiskResizeTask) OnDiskResizeCompleteFailed(ctx context.Context, disk
 	self.SetDiskReady(ctx, disk, self.GetUserCred(), data.String())
 	db.OpsLog.LogEvent(disk, db.ACT_RESIZE_FAIL, disk.GetShortDesc(ctx), self.UserCred)
 	logclient.AddActionLogWithStartable(self, disk, logclient.ACT_RESIZE, data, self.UserCred, false)
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionResize,
+		IsFail: true,
+	})
 	self.SetStageFailed(ctx, data)
 }

+ 6 - 0
backend/pkg/compute/tasks/disk/ha_disk_create_task.go

@@ -24,6 +24,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"
 )
 
@@ -83,6 +84,11 @@ func (self *HADiskCreateTask) OnDiskReady(
 
 func (self *HADiskCreateTask) OnBackupAllocateFailed(ctx context.Context, disk *models.SDisk, data jsonutils.JSONObject) {
 	disk.SetStatus(ctx, self.UserCred, api.DISK_BACKUP_ALLOC_FAILED, data.String())
+	notifyclient.EventNotify(ctx, self.UserCred, notifyclient.SEventNotifyParam{
+		Obj:    disk,
+		Action: notifyclient.ActionCreate,
+		IsFail: true,
+	})
 	self.SetStageFailed(ctx, data)
 }
 

+ 1 - 1
backend/pkg/compute/tasks/disk/dns_record_create_task.go → backend/pkg/compute/tasks/dnszone/dns_record_create_task.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package disk
+package dnszone
 
 import (
 	"context"

+ 1 - 1
backend/pkg/compute/tasks/disk/dns_record_delete_task.go → backend/pkg/compute/tasks/dnszone/dns_record_delete_task.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package disk
+package dnszone
 
 import (
 	"context"

+ 1 - 1
backend/pkg/compute/tasks/disk/dns_record_set_enabled_task.go → backend/pkg/compute/tasks/dnszone/dns_record_set_enabled_task.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package disk
+package dnszone
 
 import (
 	"context"

+ 1 - 1
backend/pkg/compute/tasks/disk/dns_record_update_task.go → backend/pkg/compute/tasks/dnszone/dns_record_update_task.go

@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package disk
+package dnszone
 
 import (
 	"context"

+ 5 - 0
backend/pkg/compute/tasks/utils/schedule.go

@@ -71,6 +71,11 @@ func (self *SSchedTask) OnScheduleFailCallback(ctx context.Context, obj ISchedul
 	obj.SetStatus(ctx, self.GetUserCred(), api.VM_SCHEDULE_FAILED, reason.String())
 	db.OpsLog.LogEvent(obj, db.ACT_ALLOCATE_FAIL, reason, self.GetUserCred())
 	logclient.AddActionLogWithStartable(self, obj, logclient.ACT_ALLOCATE, reason, self.GetUserCred(), false)
+	notifyclient.EventNotify(ctx, self.GetUserCred(), notifyclient.SEventNotifyParam{
+		Obj:    obj,
+		Action: notifyclient.ActionCreate,
+		IsFail: true,
+	})
 	notifyclient.NotifySystemErrorWithCtx(ctx, obj.GetId(), obj.GetName(), api.VM_SCHEDULE_FAILED, reason.String())
 }
 

+ 1 - 1
backend/pkg/mcclient/modules/compute/mod_containers.go

@@ -46,7 +46,7 @@ var (
 func init() {
 	Containers = ContainerManager{
 		modules.NewComputeManager("container", "containers",
-			[]string{"ID", "Name", "Guest_ID", "Status", "Started_At", "Last_Finished_At", "Restart_Count", "Spec"},
+			[]string{"ID", "Name", "Guest_ID", "Status", "Started_At", "Last_Finished_At", "Restart_Count"},
 			[]string{}),
 	}
 	modules.RegisterCompute(&Containers)

+ 12 - 6
backend/pkg/mcclient/options/compute/containers.go

@@ -55,7 +55,7 @@ type ContainerCreateCommonOptions struct {
 	WorkingDir        string   `help:"Current working directory of the command" json:"working_dir"`
 	Env               []string `help:"List of environment variable to set in the container and the format is: <key>=<value>"`
 	RootFs            string   `help:"Root filesystem of the container, e.g.: disk_index=<disk_number>,disk_id=<disk_id>"`
-	VolumeMount       []string `help:"Volume mount of the container and the format is: name=<val>,mount=<container_path>,readonly=<true_or_false>,case_insensitive_paths=p1,p2,disk_index=<disk_number>,disk_id=<disk_id>"`
+	VolumeMount       []string `help:"Volume mount of the container and the format is: name=<val>,mount_path=<container_path>,readonly=<true_or_false>,case_insensitive_paths=p1,p2,disk_index=<disk_number>,disk_id=<disk_id>"`
 	Device            []string `help:"Host device: <host_path>:<container_path>:<permissions>, e.g.: /dev/snd:/dev/snd:rwm"`
 	Privileged        bool     `help:"Privileged mode"`
 	Caps              string   `help:"Container capabilities, e.g.: SETPCAP,AUDIT_WRITE,SYS_CHROOT,CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID,SYSLOG,SYS_ADMIN,WAKE_ALARM,SYS_PTRACE,BLOCK_SUSPEND,MKNOD,KILL,SYS_RESOURCE,NET_RAW,NET_ADMIN,NET_BIND_SERVICE,SYS_NICE"`
@@ -240,7 +240,7 @@ func parseContainerVolumeMount(vmStr string) (*apis.ContainerVolumeMount, error)
 			}
 			gId64 := int64(gId)
 			vm.FsGroup = &gId64
-		case "mount_path":
+		case "mount_path", "mount":
 			vm.MountPath = val
 		case "host_path":
 			if vm.HostPath == nil {
@@ -473,7 +473,7 @@ func (o *ContainerLogOptions) ToAPIInput() (*computeapi.PodLogOptions, error) {
 	if len(o.Since) > 0 {
 		dur, err := time.ParseDuration(o.Since)
 		if err != nil {
-			return nil, errors.Wrapf(err, "parse duration %s", o.Since)
+			return nil, errors.Wrapf(err, "invalid time duration: %s, shoud like 300ms, 1.5h or 2h45m", o.Since)
 		}
 		sec := int64(dur.Round(time.Second).Seconds())
 		opt.SinceSeconds = &sec
@@ -569,7 +569,13 @@ func (o *ContainerRemoveVolumeMountPostOverlayOptions) Params() (jsonutils.JSONO
 }
 
 type ContainerCopyOptions struct {
-	SRC_FILE          string
-	CONTAINER_ID_FILE string
-	RawFile           bool
+	SRC     string `help:"Local path or file name, or cotnainer:path, e.g. /etc/hots or ctr-0:/etc/hosts"`
+	DST     string `help:"Local path or file name, or cotnainer:path, e.g. /etc/hots or ctr-0:/etc/hosts"`
+	RawFile bool   `help:"copy the file as raw data, if false, requires tar in executive path in container and host"`
+}
+
+type ContainerCopyFromOptions struct {
+	CONTAINER_ID_PATH string `help:"container id and the file path in the container, separated by ':', e.g. ctr-0:/etc/hosts"`
+	DST_PATH          string `help:"Local destination path or file name"`
+	RawFile           bool   `help:"copy the file as raw data, if false, requires tar in executive path in container and host"`
 }

+ 5 - 5
backend/pkg/mcclient/options/compute/server_pod.go

@@ -30,11 +30,11 @@ import (
 )
 
 type PodCreateOptions struct {
-	NAME string `help:"Name of server pod" json:"-"`
-	ServerCreateCommonConfig
-	MEM         string `help:"Memory size MB" metavar:"MEM" json:"-"`
-	VcpuCount   int    `help:"#CPU cores of VM server, default 1" default:"1" metavar:"<SERVER_CPU_COUNT>" json:"vcpu_count" token:"ncpu"`
-	AllowDelete *bool  `help:"Unlock server to allow deleting" json:"-"`
+	NAME                     string `help:"Name of server pod" json:"-"`
+	ServerCreateCommonConfig `"disk->nargs":"*"`
+	MEM                      string `help:"Memory size MB" metavar:"MEM" json:"-"`
+	VcpuCount                int    `help:"#CPU cores of VM server, default 1" default:"1" metavar:"<SERVER_CPU_COUNT>" json:"vcpu_count" token:"ncpu"`
+	AllowDelete              *bool  `help:"Unlock server to allow deleting" json:"-"`
 	//PortMapping []string `help:"Port mapping of the pod and the format is: host_port=8080,port=80,protocol=<tcp|udp>,host_port_range=<int>-<int>" short-token:"p"`
 	Arch             string `help:"image arch" choices:"aarch64|x86_64"`
 	AutoStart        bool   `help:"Auto start server after it is created"`

+ 18 - 0
backend/pkg/mcclient/options/notify/topic.go

@@ -118,3 +118,21 @@ type TopicCreateOptions struct {
 func (rl *TopicCreateOptions) Params() (jsonutils.JSONObject, error) {
 	return jsonutils.Marshal(rl), nil
 }
+
+type TopicAddActionInput struct {
+	TopicOptions
+	Actions []string `json:"actions"`
+}
+
+func (rl *TopicAddActionInput) Params() (jsonutils.JSONObject, error) {
+	return jsonutils.Marshal(rl), nil
+}
+
+type TopicAddResourcesInput struct {
+	TopicOptions
+	Resources []string `json:"resources"`
+}
+
+func (rl *TopicAddResourcesInput) Params() (jsonutils.JSONObject, error) {
+	return jsonutils.Marshal(rl), nil
+}

+ 28 - 1
backend/pkg/notify/models/topic.go

@@ -75,7 +75,7 @@ type STopic struct {
 	db.SEnabledStatusStandaloneResourceBase
 
 	Type        string               `width:"20" nullable:"false" create:"required" update:"user" list:"user"`
-	Results     tristate.TriState    `default:"true"`
+	Results     tristate.TriState    `default:"true" create:"optional" get:"user" list:"user"`
 	TitleCn     string               `length:"medium" nullable:"true" charset:"utf8" list:"user" update:"user" create:"optional"`
 	TitleEn     string               `length:"medium" nullable:"true" charset:"utf8" list:"user" update:"user" create:"optional"`
 	ContentCn   string               `length:"medium" nullable:"true" charset:"utf8" list:"user" update:"user" create:"optional"`
@@ -485,6 +485,7 @@ func initTopicElement(name string, t *STopic) {
 			api.TOPIC_RESOURCE_LOADBALANCER,
 			api.TOPIC_RESOURCE_DBINSTANCE,
 			api.TOPIC_RESOURCE_ELASTICCACHE,
+			api.TOPIC_RESOURCE_DISK,
 			api.TOPIC_RESOURCE_CLOUDPHONE,
 		)
 		t.addAction(
@@ -947,6 +948,32 @@ func (t *STopic) PreCheckPerformAction(
 	return nil
 }
 
+func (t *STopic) PerformAddActions(
+	ctx context.Context, userCred mcclient.TokenCredential,
+	query jsonutils.JSONObject, data api.TopicAddActionInput,
+) (jsonutils.JSONObject, error) {
+	for _, action := range data.Actions {
+		if len(action) == 0 {
+			continue
+		}
+		t.addAction(notify.SAction(action))
+	}
+	return nil, nil
+}
+
+func (t *STopic) PerformAddResources(
+	ctx context.Context, userCred mcclient.TokenCredential,
+	query jsonutils.JSONObject, data api.TopicAddResourcesInput,
+) (jsonutils.JSONObject, error) {
+	for _, resource := range data.Resources {
+		if len(resource) == 0 {
+			continue
+		}
+		t.addResources(resource)
+	}
+	return nil, nil
+}
+
 func init() {
 	converter = &sConverter{
 		resource2Value: &sync.Map{},