diff --git a/cmd/attack/attack.go b/cmd/attack/attack.go index c303b10..ff9ff45 100644 --- a/cmd/attack/attack.go +++ b/cmd/attack/attack.go @@ -40,6 +40,7 @@ func NewAttackCommand() *cobra.Command { NewRedisAttackCommand(&uid), NewFileAttackCommand(&uid), NewVMAttackCommand(&uid), + NewUserDefinedCommand(&uid), ) return cmd diff --git a/cmd/attack/user_defined.go b/cmd/attack/user_defined.go new file mode 100644 index 0000000..015ed94 --- /dev/null +++ b/cmd/attack/user_defined.go @@ -0,0 +1,64 @@ +// Copyright 2022 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package attack + +import ( + "fmt" + + "github.com/spf13/cobra" + "go.uber.org/fx" + + "github.com/chaos-mesh/chaosd/cmd/server" + "github.com/chaos-mesh/chaosd/pkg/core" + "github.com/chaos-mesh/chaosd/pkg/server/chaosd" + "github.com/chaos-mesh/chaosd/pkg/utils" +) + +func NewUserDefinedCommand(uid *string) *cobra.Command { + options := core.NewUserDefinedOption() + dep := fx.Options( + server.Module, + fx.Provide(func() *core.UserDefinedOption { + options.UID = *uid + return options + }), + ) + + cmd := &cobra.Command{ + Use: "user-defined ", + Short: "user defined attack related commands", + Run: func(*cobra.Command, []string) { + options.CompleteDefaults() + utils.FxNewAppWithoutLog(dep, fx.Invoke(userDefinedAttackFunc)).Run() + }, + } + + cmd.Flags().StringVarP(&options.AttackCmd, "attack-cmd", "a", "", "the command to be executed when attack") + cmd.Flags().StringVarP(&options.RecoverCmd, "recover-cmd", "r", "", "the command to be executed when recover") + + return cmd +} + +func userDefinedAttackFunc(options *core.UserDefinedOption, chaos *chaosd.Server) { + if err := options.Validate(); err != nil { + utils.ExitWithError(utils.ExitBadArgs, err) + } + + uid, err := chaos.ExecuteAttack(chaosd.UserDefinedAttack, options, core.CommandMode) + if err != nil { + utils.ExitWithError(utils.ExitError, err) + } + + utils.NormalExit(fmt.Sprintf("Attack user defined comamnd successfully, uid: %s", uid)) +} diff --git a/pkg/core/experiment.go b/pkg/core/experiment.go index de2f9f4..13eda6f 100644 --- a/pkg/core/experiment.go +++ b/pkg/core/experiment.go @@ -31,17 +31,18 @@ const ( ) const ( - ProcessAttack = "process" - NetworkAttack = "network" - StressAttack = "stress" - DiskAttack = "disk" - ClockAttack = "clock" - HostAttack = "host" - JVMAttack = "jvm" - KafkaAttack = "kafka" - RedisAttack = "redis" - FileAttack = "file" - VMAttack = "vm" + ProcessAttack = "process" + NetworkAttack = "network" + StressAttack = "stress" + DiskAttack = "disk" + ClockAttack = "clock" + HostAttack = "host" + JVMAttack = "jvm" + KafkaAttack = "kafka" + RedisAttack = "redis" + FileAttack = "file" + VMAttack = "vm" + UserDefinedAttack = "userDefined" ) const ( @@ -119,6 +120,8 @@ func GetAttackByKind(kind string) *AttackConfig { attackConfig = &FileCommand{} case VMAttack: attackConfig = &VMOption{} + case UserDefinedAttack: + attackConfig = &UserDefinedOption{} default: return nil } diff --git a/pkg/core/user_defined.go b/pkg/core/user_defined.go new file mode 100644 index 0000000..16acb27 --- /dev/null +++ b/pkg/core/user_defined.go @@ -0,0 +1,58 @@ +// Copyright 2022 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package core + +import ( + "encoding/json" + + "github.com/pingcap/errors" +) + +var _ AttackConfig = &UserDefinedOption{} + +type UserDefinedOption struct { + CommonAttackConfig + + AttackCmd string `json:"attackCmd,omitempty"` + RecoverCmd string `json:"recoverCmd,omitempty"` +} + +func (u *UserDefinedOption) Validate() error { + if err := u.CommonAttackConfig.Validate(); err != nil { + return err + } + if len(u.AttackCmd) == 0 { + return errors.New("attack command not provided") + } + + if len(u.RecoverCmd) == 0 { + return errors.New("recover command not provided") + } + + return nil +} + +func (u *UserDefinedOption) RecoverData() string { + data, _ := json.Marshal(u) + + return string(data) +} + +func NewUserDefinedOption() *UserDefinedOption { + return &UserDefinedOption{ + CommonAttackConfig: CommonAttackConfig{ + Kind: UserDefinedAttack, + }, + } +} diff --git a/pkg/server/chaosd/recover.go b/pkg/server/chaosd/recover.go index 47053fb..9d1fe5f 100644 --- a/pkg/server/chaosd/recover.go +++ b/pkg/server/chaosd/recover.go @@ -73,6 +73,8 @@ func (s *Server) RecoverAttack(uid string) error { attackType = FileAttack case core.VMAttack: attackType = VMAttack + case core.UserDefinedAttack: + attackType = UserDefinedAttack default: return perr.Errorf("chaos experiment kind %s not found", exp.Kind) } diff --git a/pkg/server/chaosd/user_defined.go b/pkg/server/chaosd/user_defined.go new file mode 100644 index 0000000..8910715 --- /dev/null +++ b/pkg/server/chaosd/user_defined.go @@ -0,0 +1,64 @@ +// Copyright 2022 Chaos Mesh Authors. +// +// 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, +// See the License for the specific language governing permissions and +// limitations under the License. + +package chaosd + +import ( + "os/exec" + + "github.com/pingcap/log" + "github.com/pkg/errors" + "go.uber.org/zap" + + "github.com/chaos-mesh/chaosd/pkg/core" +) + +type userDefinedAttack struct{} + +var UserDefinedAttack AttackType = userDefinedAttack{} + +func (userDefinedAttack) Attack(options core.AttackConfig, _ Environment) error { + option := options.(*core.UserDefinedOption) + log.Info("attack command", zap.String("command", option.AttackCmd)) + + cmd := exec.Command("bash", "-c", option.AttackCmd) + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrap(err, string(output)) + } + if len(output) > 0 { + log.Info("attack command", zap.String("output", string(output))) + } + + return nil +} + +func (userDefinedAttack) Recover(exp core.Experiment, _ Environment) error { + config, err := exp.GetRequestCommand() + if err != nil { + return err + } + option := config.(*core.UserDefinedOption) + log.Info("recover command", zap.String("command", option.RecoverCmd)) + + cmd := exec.Command("bash", "-c", option.RecoverCmd) + output, err := cmd.CombinedOutput() + if err != nil { + return errors.Wrap(err, string(output)) + } + if len(output) > 0 { + log.Info("recover command", zap.String("command", option.RecoverCmd), zap.String("output", string(output))) + } + + return nil +} diff --git a/pkg/server/httpserver/server.go b/pkg/server/httpserver/server.go index 6ed7535..86cbe4b 100644 --- a/pkg/server/httpserver/server.go +++ b/pkg/server/httpserver/server.go @@ -95,6 +95,7 @@ func (s *httpServer) handler(engine *gin.Engine) { attack.POST("/kafka", s.createKafkaAttack) attack.POST("/vm", s.createVMAttack) attack.POST("/redis", s.createRedisAttack) + attack.POST("/user_defined", s.createUserDefinedAttack) attack.DELETE("/:uid", s.recoverAttack) } @@ -404,6 +405,38 @@ func (s *httpServer) createRedisAttack(c *gin.Context) { c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid)) } +// @Summary Create user defined attack. +// @Description Create user defined attack. +// @Tags attack +// @Produce json +// @Param request body core.RedisCommand true "Request body" +// @Success 200 {object} utils.Response +// @Failure 400 {object} utils.APIError +// @Failure 500 {object} utils.APIError +// @Router /api/attack/user_defined [post] +func (s *httpServer) createUserDefinedAttack(c *gin.Context) { + attack := core.NewUserDefinedOption() + if err := c.ShouldBindJSON(attack); err != nil { + c.AbortWithError(http.StatusBadRequest, utils.ErrInternalServer.WrapWithNoMessage(err)) + return + } + + attack.CompleteDefaults() + if err := attack.Validate(); err != nil { + err = core.ErrAttackConfigValidation.Wrap(err, "attack config validation failed") + handleError(c, err) + return + } + + uid, err := s.chaos.ExecuteAttack(chaosd.UserDefinedAttack, attack, core.ServerMode) + if err != nil { + handleError(c, err) + return + } + + c.JSON(http.StatusOK, utils.AttackSuccessResponse(uid)) +} + // @Summary Create recover attack. // @Description Create recover attack. // @Tags attack diff --git a/test/integration_test/user_defined/run.sh b/test/integration_test/user_defined/run.sh new file mode 100644 index 0000000..905737b --- /dev/null +++ b/test/integration_test/user_defined/run.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Copyright 2022 Chaos Mesh Authors. +# +# 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, +# See the License for the specific language governing permissions and +# limitations under the License. + +set -u + +cur=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +cd $cur + +bin_path=../../../bin + +# test user defined attack +${bin_path}/chaosd attack user-defined --attack-cmd "touch /tmp/chaos" --recover-cmd "rm /tmp/chaos" --uid 1234567890 + +if [ ! -e /tmp/chaos ]; then + echo "file not exist" + exit 1 +fi + +${bin_path}/chaosd recover 1234567890 + +if [ -e /tmp/chaos ]; then + echo "file still exist" + exit 1 +fi + +exit 0