Skip to content

Commit 6ef18a3

Browse files
author
Nikita Savchenko
committed
add local and remote mode for patroni attack
Signed-off-by: Nikita Savchenko <[email protected]>
1 parent 6a2f8c5 commit 6ef18a3

File tree

9 files changed

+299
-124
lines changed

9 files changed

+299
-124
lines changed

cmd/attack/attack.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ func NewAttackCommand() *cobra.Command {
4242
NewHTTPAttackCommand(&uid),
4343
NewVMAttackCommand(&uid),
4444
NewUserDefinedCommand(&uid),
45+
NewPatroniAttackCommand(&uid),
4546
)
4647

4748
return cmd

cmd/attack/patroni.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,37 +48,39 @@ func NewPatroniAttackCommand(uid *string) *cobra.Command {
4848

4949
cmd.PersistentFlags().StringVarP(&options.User, "user", "u", "patroni", "patroni cluster user")
5050
cmd.PersistentFlags().StringVar(&options.Password, "password", "p", "patroni cluster password")
51+
cmd.PersistentFlags().StringVarP(&options.Address, "address", "a", "", "patroni cluster address, any of available hosts")
52+
cmd.PersistentFlags().BoolVarP(&options.LocalMode, "local-mode", "l", false, "execute patronictl on host with chaosd. User with privileges required.")
53+
cmd.PersistentFlags().BoolVarP(&options.RemoteMode, "remote-mode", "r", false, "execute patroni command by REST API")
5154

5255
return cmd
5356
}
5457

5558
func NewPatroniSwitchoverCommand(dep fx.Option, options *core.PatroniCommand) *cobra.Command {
5659
cmd := &cobra.Command{
5760
Use: "switchover",
58-
Short: "exec switchover, default without another attack. Warning! Command is not recover!",
61+
Short: "exec switchover command. Warning! Command is not recover!",
5962
Run: func(*cobra.Command, []string) {
6063
options.Action = core.SwitchoverAction
6164
utils.FxNewAppWithoutLog(dep, fx.Invoke(PatroniAttackF)).Run()
6265
},
6366
}
64-
cmd.Flags().StringVarP(&options.Address, "address", "a", "", "patroni cluster address, any of available hosts")
6567
cmd.Flags().StringVarP(&options.Candidate, "candidate", "c", "", "switchover candidate, default random unit for replicas")
66-
cmd.Flags().StringVarP(&options.Scheduled_at, "scheduled_at", "d", fmt.Sprintln(time.Now().Add(time.Second*60).Format(time.RFC3339)), "scheduled switchover, default now()+1 minute")
68+
cmd.Flags().StringVarP(&options.Scheduled_at, "scheduled_at", "d", fmt.Sprint(time.Now().Add(time.Second*60).Format(time.RFC3339)), `scheduled switchover,
69+
default now()+1 minute by remote mode`)
6770

6871
return cmd
6972
}
7073

7174
func NewPatroniFailoverCommand(dep fx.Option, options *core.PatroniCommand) *cobra.Command {
7275
cmd := &cobra.Command{
7376
Use: "failover",
74-
Short: "exec failover, default without another attack",
77+
Short: "Exec failover command. Warning! Command is not recover!",
7578
Run: func(*cobra.Command, []string) {
7679
options.Action = core.FailoverAction
7780
utils.FxNewAppWithoutLog(dep, fx.Invoke(PatroniAttackF)).Run()
7881
},
7982
}
8083

81-
cmd.Flags().StringVarP(&options.Address, "address", "a", "", "patroni cluster address, any of available hosts")
8284
cmd.Flags().StringVarP(&options.Candidate, "leader", "c", "", "failover new leader, default random unit for replicas")
8385
return cmd
8486
}

pkg/core/experiment.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ func GetAttackByKind(kind string) *AttackConfig {
129129
attackConfig = &VMOption{}
130130
case UserDefinedAttack:
131131
attackConfig = &UserDefinedOption{}
132+
case PatroniAttack:
133+
attackConfig = &PatroniCommand{}
132134
default:
133135
return nil
134136
}

pkg/core/patroni.go

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,23 +35,33 @@ type PatroniCommand struct {
3535
User string `json:"user,omitempty"`
3636
Password string `json:"password,omitempty"`
3737
Scheduled_at string `json:"scheduled_at,omitempty"`
38+
LocalMode bool `json:"local_mode,omitempty"`
39+
RemoteMode bool `json:"remote_mode,omitempty"`
3840
RecoverCmd string `json:"recoverCmd,omitempty"`
3941
}
4042

4143
func (p *PatroniCommand) Validate() error {
4244
if err := p.CommonAttackConfig.Validate(); err != nil {
4345
return err
4446
}
45-
if len(p.Address) == 0 {
46-
return errors.New("address not provided")
47-
}
4847

49-
if len(p.User) == 0 {
50-
return errors.New("patroni user not provided")
48+
if !p.RemoteMode && !p.LocalMode {
49+
return errors.New("local or remote mode required")
5150
}
5251

53-
if len(p.Password) == 0 {
54-
return errors.New("patroni password not provided")
52+
if p.RemoteMode {
53+
54+
if len(p.Address) == 0 {
55+
return errors.New("address not provided")
56+
}
57+
58+
if len(p.User) == 0 {
59+
return errors.New("patroni user not provided")
60+
}
61+
62+
if len(p.Password) == 0 {
63+
return errors.New("patroni password not provided")
64+
}
5565
}
5666

5767
return nil

pkg/server/chaosd/patroni.go

Lines changed: 108 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,18 @@
1414
package chaosd
1515

1616
import (
17-
"bytes"
1817
"encoding/json"
1918
"fmt"
20-
"io"
2119
"math/rand"
2220
"net/http"
21+
"os/exec"
22+
"strings"
2323

24-
"github.com/chaos-mesh/chaosd/pkg/core"
25-
"github.com/chaos-mesh/chaosd/pkg/server/utils"
2624
"github.com/pingcap/errors"
2725
"github.com/pingcap/log"
26+
27+
"github.com/chaos-mesh/chaosd/pkg/core"
28+
"github.com/chaos-mesh/chaosd/pkg/server/utils"
2829
)
2930

3031
type patroniAttack struct{}
@@ -34,96 +35,157 @@ var PatroniAttack AttackType = patroniAttack{}
3435
func (patroniAttack) Attack(options core.AttackConfig, _ Environment) error {
3536
attack := options.(*core.PatroniCommand)
3637

37-
candidate := attack.Candidate
38-
39-
leader := attack.Leader
38+
var responce []byte
4039

41-
var scheduled_at string
40+
var address string
4241

43-
var url string
42+
var err error
4443

4544
values := make(map[string]string)
4645

47-
patroniInfo, err := utils.GetPatroniInfo(attack.Address)
46+
if attack.RemoteMode {
47+
address = attack.Address
48+
} else if attack.LocalMode {
49+
address, err = utils.GetLocalHostname()
50+
if err != nil {
51+
return errors.WithStack(err)
52+
}
53+
}
54+
55+
patroniInfo, err := utils.GetPatroniInfo(address)
4856
if err != nil {
4957
err = errors.Errorf("failed to get patroni info for %v: %v", options.String(), err)
5058
return errors.WithStack(err)
5159
}
5260

53-
if len(patroniInfo.Replicas) == 0 {
54-
err = errors.Errorf("failed to get available replicas. Please, check your cluster")
61+
if len(patroniInfo.Replicas) == 0 && len(patroniInfo.SyncStandby) == 0 {
62+
err = errors.Errorf("failed to get available candidates. Please, check your cluster")
63+
return errors.WithStack(err)
64+
}
65+
66+
sync_mode_check, err := isSynchronousClusterMode(address, attack.User, attack.Password)
67+
if err != nil {
68+
err = errors.Errorf("failed to check cluster synchronous mode for %v: %v", options.String(), err)
5569
return errors.WithStack(err)
5670
}
5771

58-
if candidate == "" {
59-
candidate = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
72+
if attack.Candidate == "" {
73+
if sync_mode_check {
74+
values["candidate"] = patroniInfo.SyncStandby[rand.Intn(len(patroniInfo.SyncStandby))]
75+
} else {
76+
values["candidate"] = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
77+
}
78+
6079
}
6180

62-
if leader == "" {
63-
leader = patroniInfo.Master
81+
if attack.Leader == "" {
82+
values["leader"] = patroniInfo.Master
6483
}
6584

66-
switch options.String() {
67-
case "switchover":
85+
values["scheduled_at"] = attack.Scheduled_at
6886

69-
scheduled_at = attack.Scheduled_at
87+
cmd := options.String()
7088

71-
values = map[string]string{"leader": leader, "scheduled_at": scheduled_at}
89+
switch cmd {
90+
case "switchover":
7291

73-
log.Info(fmt.Sprintf("Switchover will be done from %v to another available replica in %v", patroniInfo.Master, scheduled_at))
92+
log.Info(fmt.Sprintf("Switchover will be done from %v to %v in %v", values["leader"], values["candidate"], values["scheduled_at"]))
7493

7594
case "failover":
7695

77-
values = map[string]string{"candidate": candidate}
96+
log.Info(fmt.Sprintf("Failover will be done from %v to %v", values["leader"], values["candidate"]))
7897

79-
log.Info(fmt.Sprintf("Failover will be done from %v to %v", patroniInfo.Master, candidate))
98+
}
8099

100+
if attack.RemoteMode {
101+
responce, err = execPatroniAttackByRemoteMode(address, attack.User, attack.Password, cmd, values)
102+
if err != nil {
103+
return err
104+
}
105+
} else if attack.LocalMode {
106+
responce, err = execPatroniAttackByLocalMode(cmd, values)
107+
if err != nil {
108+
return err
109+
}
81110
}
82111

83-
patroniAddr := attack.Address
112+
if attack.RemoteMode {
113+
log.S().Infof("Execute %v successfully: %v", cmd, string(responce))
114+
}
84115

85-
cmd := options.String()
116+
if attack.LocalMode {
117+
log.S().Infof("Execute %v successfully", cmd)
118+
fmt.Println(string(responce))
119+
}
120+
121+
return nil
122+
}
123+
124+
func execPatroniAttackByRemoteMode(patroniAddr string, user string, password string, cmd string, values map[string]string) ([]byte, error) {
86125

87126
data, err := json.Marshal(values)
88127
if err != nil {
89128
err = errors.Errorf("failed to marshal data: %v", values)
90-
return errors.WithStack(err)
129+
return nil, errors.WithStack(err)
91130
}
92131

93-
url = fmt.Sprintf("http://%v:8008/%v", patroniAddr, cmd)
132+
buf, err := utils.MakeHTTPRequest(http.MethodPost, patroniAddr, 8008, cmd, data, user, password)
133+
if err != nil {
134+
return nil, errors.WithStack(err)
135+
}
94136

95-
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
137+
return buf, nil
138+
}
139+
140+
func execPatroniAttackByLocalMode(cmd string, values map[string]string) ([]byte, error) {
141+
var cmdTemplate string
142+
143+
if cmd == "failover" {
144+
cmdTemplate = fmt.Sprintf("patronictl %v --master %v --candidate %v --force", cmd, values["leader"], values["candidate"])
145+
} else if cmd == "switchover" {
146+
cmdTemplate = fmt.Sprintf("patronictl %v --master %v --candidate %v --scheduled %v --force", cmd, values["leader"], values["candidate"], values["scheduled_at"])
147+
}
148+
149+
execCmd := exec.Command("bash", "-c", cmdTemplate)
150+
output, err := execCmd.CombinedOutput()
96151
if err != nil {
97-
err = errors.Errorf("failed to %v: %v", cmd, err)
98-
return errors.WithStack(err)
152+
log.S().Errorf(fmt.Sprintf("failed to %v: %v", cmdTemplate, string(output)))
153+
return nil, err
99154
}
100155

101-
request.Header.Set("Content-Type", "application/json")
102-
request.SetBasicAuth(attack.User, attack.Password)
156+
if strings.Contains(string(output), "failed") {
157+
err = errors.New(string(output))
158+
return nil, err
159+
}
103160

104-
client := &http.Client{}
105-
resp, error := client.Do(request)
106-
if error != nil {
107-
err = errors.Errorf("failed to %v: %v", cmd, err)
108-
return errors.WithStack(err)
161+
return output, nil
162+
}
163+
164+
func isSynchronousClusterMode(patroniAddr string, user string, password string) (bool, error) {
165+
166+
buf, err := utils.MakeHTTPRequest(http.MethodGet, patroniAddr, 8008, "config", []byte{}, user, password)
167+
if err != nil {
168+
return false, err
109169
}
110170

111-
defer resp.Body.Close()
171+
patroni_responce := make(map[string]interface{})
112172

113-
buf, err := io.ReadAll(resp.Body)
173+
err = json.Unmarshal(buf, &patroni_responce)
114174
if err != nil {
115-
err = errors.Errorf("failed to read %v responce: %v", cmd, err)
116-
return errors.WithStack(err)
175+
return false, fmt.Errorf("bad request %v %v", err.Error(), http.StatusBadRequest)
117176
}
118177

119-
if resp.StatusCode != 200 && resp.StatusCode != 202 {
120-
err = errors.Errorf("failed to %v: status code %v, responce %v", cmd, resp.StatusCode, string(buf))
121-
return errors.WithStack(err)
178+
mode_check, ok := patroni_responce["synchronous_mode"].(bool)
179+
if !ok {
180+
return false, fmt.Errorf("failed to cast synchronous_mode field from patroni responce")
122181
}
123182

124-
log.S().Infof("Execute %v successfully: %v", cmd, string(buf))
183+
if mode_check {
184+
return true, nil
185+
}
186+
187+
return false, nil
125188

126-
return nil
127189
}
128190

129191
func (patroniAttack) Recover(exp core.Experiment, _ Environment) error {

pkg/server/chaosd/recover.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ func (s *Server) RecoverAttack(uid string) error {
7979
attackType = VMAttack
8080
case core.UserDefinedAttack:
8181
attackType = UserDefinedAttack
82+
case core.PatroniAttack:
83+
attackType = PatroniAttack
8284
default:
8385
return perr.Errorf("chaos experiment kind %s not found", exp.Kind)
8486
}

0 commit comments

Comments
 (0)