Skip to content

Commit 2eef5ba

Browse files
author
Nikita Savchenko
committed
add local and remote mode for patroni attack
1 parent 3461e37 commit 2eef5ba

File tree

7 files changed

+278
-120
lines changed

7 files changed

+278
-120
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/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: 104 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
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

2424
"github.com/chaos-mesh/chaosd/pkg/core"
2525
"github.com/chaos-mesh/chaosd/pkg/server/utils"
@@ -34,17 +34,24 @@ var PatroniAttack AttackType = patroniAttack{}
3434
func (patroniAttack) Attack(options core.AttackConfig, _ Environment) error {
3535
attack := options.(*core.PatroniCommand)
3636

37-
candidate := attack.Candidate
37+
var responce []byte
3838

39-
leader := attack.Leader
39+
var address string
4040

41-
var scheduled_at string
42-
43-
var url string
41+
var err error
4442

4543
values := make(map[string]string)
4644

47-
patroniInfo, err := utils.GetPatroniInfo(attack.Address)
45+
if attack.RemoteMode {
46+
address = attack.Address
47+
} else if attack.LocalMode {
48+
address, err = utils.GetLocalHostname()
49+
if err != nil {
50+
return errors.WithStack(err)
51+
}
52+
}
53+
54+
patroniInfo, err := utils.GetPatroniInfo(address)
4855
if err != nil {
4956
err = errors.Errorf("failed to get patroni info for %v: %v", options.String(), err)
5057
return errors.WithStack(err)
@@ -55,75 +62,130 @@ func (patroniAttack) Attack(options core.AttackConfig, _ Environment) error {
5562
return errors.WithStack(err)
5663
}
5764

58-
if candidate == "" {
59-
candidate = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
65+
sync_mode_check, err := isSynchronousClusterMode(attack.Address, attack.User, attack.Password)
66+
if err != nil {
67+
err = errors.Errorf("failed to check cluster synchronous mode for %v: %v", options.String(), err)
68+
return errors.WithStack(err)
6069
}
6170

62-
if leader == "" {
63-
leader = patroniInfo.Master
71+
if attack.Candidate == "" {
72+
if sync_mode_check {
73+
values["candidate"] = patroniInfo.SyncStandby[rand.Intn(len(patroniInfo.SyncStandby))]
74+
} else {
75+
values["candidate"] = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
76+
}
77+
6478
}
6579

66-
switch options.String() {
67-
case "switchover":
80+
if attack.Leader == "" {
81+
values["leader"] = patroniInfo.Master
82+
}
6883

69-
scheduled_at = attack.Scheduled_at
84+
values["scheduled_at"] = attack.Scheduled_at
7085

71-
values = map[string]string{"leader": leader, "scheduled_at": scheduled_at}
86+
cmd := options.String()
87+
88+
switch cmd {
89+
case "switchover":
7290

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

7593
case "failover":
7694

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

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

99+
if attack.RemoteMode {
100+
responce, err = execPatroniAttackByRemoteMode(attack, cmd, values)
101+
if err != nil {
102+
return err
103+
}
104+
} else if attack.LocalMode {
105+
responce, err = execPatroniAttackByLocalMode(attack, cmd, values)
106+
if err != nil {
107+
return err
108+
}
81109
}
82110

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

85-
cmd := options.String()
115+
if attack.LocalMode {
116+
log.S().Infof("Execute %v successfully", cmd)
117+
fmt.Println(string(responce))
118+
}
119+
120+
return nil
121+
}
122+
123+
func execPatroniAttackByRemoteMode(attack *core.PatroniCommand, cmd string, values map[string]string) ([]byte, error) {
124+
patroniAddr := attack.Address
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, attack.User, attack.Password)
133+
if err != nil {
134+
return nil, errors.WithStack(err)
135+
}
136+
137+
return buf, nil
138+
}
139+
140+
func execPatroniAttackByLocalMode(attack *core.PatroniCommand, 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+
}
94148

95-
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
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
154+
}
155+
156+
if strings.Contains(string(output), "failed") {
157+
err = errors.New(string(output))
158+
return nil, err
99159
}
100160

101-
request.Header.Set("Content-Type", "application/json")
102-
request.SetBasicAuth(attack.User, attack.Password)
161+
return output, nil
162+
}
103163

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)
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 {

0 commit comments

Comments
 (0)