Skip to content

Commit 6d0273a

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

File tree

5 files changed

+140
-60
lines changed

5 files changed

+140
-60
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: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ 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

@@ -46,12 +48,18 @@ func (p *PatroniCommand) Validate() error {
4648
return errors.New("address not provided")
4749
}
4850

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

53-
if len(p.Password) == 0 {
54-
return errors.New("patroni password not provided")
55+
if p.RemoteMode {
56+
if len(p.User) == 0 {
57+
return errors.New("patroni user not provided")
58+
}
59+
60+
if len(p.Password) == 0 {
61+
return errors.New("patroni password not provided")
62+
}
5563
}
5664

5765
return nil

pkg/server/chaosd/patroni.go

Lines changed: 81 additions & 45 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"
2019
"io"
2120
"math/rand"
22-
"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,19 @@ 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
40-
41-
var scheduled_at string
42-
43-
var url string
39+
var address string
4440

4541
values := make(map[string]string)
4642

47-
patroniInfo, err := utils.GetPatroniInfo(attack.Address)
43+
if attack.RemoteMode {
44+
address = attack.Address
45+
} else if attack.LocalMode {
46+
address = "localhost"
47+
}
48+
49+
patroniInfo, err := utils.GetPatroniInfo(address)
4850
if err != nil {
4951
err = errors.Errorf("failed to get patroni info for %v: %v", options.String(), err)
5052
return errors.WithStack(err)
@@ -55,75 +57,109 @@ func (patroniAttack) Attack(options core.AttackConfig, _ Environment) error {
5557
return errors.WithStack(err)
5658
}
5759

58-
if candidate == "" {
59-
candidate = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
60+
if attack.Candidate == "" {
61+
values["candidate"] = patroniInfo.Replicas[rand.Intn(len(patroniInfo.Replicas))]
6062
}
6163

62-
if leader == "" {
63-
leader = patroniInfo.Master
64+
if attack.Leader == "" {
65+
values["leader"] = patroniInfo.Master
6466
}
6567

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

69-
scheduled_at = attack.Scheduled_at
70+
cmd := options.String()
7071

71-
values = map[string]string{"leader": leader, "scheduled_at": scheduled_at}
72+
switch cmd {
73+
case "switchover":
7274

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

7577
case "failover":
7678

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

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

83+
if attack.RemoteMode {
84+
responce, err = execPatroniAttackByRemoteMode(attack, cmd, values)
85+
if err != nil {
86+
return err
87+
}
88+
} else if attack.LocalMode {
89+
responce, err = execPatroniAttackByLocalMode(attack, cmd, values)
90+
if err != nil {
91+
return err
92+
}
8193
}
8294

83-
patroniAddr := attack.Address
95+
if attack.RemoteMode {
96+
log.S().Infof("Execute %v successfully: %v", cmd, string(responce))
97+
}
8498

85-
cmd := options.String()
99+
if attack.LocalMode {
100+
log.S().Infof("Execute %v successfully", cmd)
101+
fmt.Println(string(responce))
102+
}
103+
104+
return nil
105+
}
106+
107+
func execPatroniAttackByRemoteMode(attack *core.PatroniCommand, cmd string, values map[string]string) ([]byte, error) {
108+
patroniAddr := attack.Address
86109

87110
data, err := json.Marshal(values)
88111
if err != nil {
89112
err = errors.Errorf("failed to marshal data: %v", values)
90-
return errors.WithStack(err)
113+
return nil, errors.WithStack(err)
91114
}
92115

93-
url = fmt.Sprintf("http://%v:8008/%v", patroniAddr, cmd)
94-
95-
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
116+
resp, err := utils.MakePostHTTPRequest(patroniAddr, 8008, cmd, data, attack.User, attack.Password)
96117
if err != nil {
97-
err = errors.Errorf("failed to %v: %v", cmd, err)
98-
return errors.WithStack(err)
118+
return nil, errors.WithStack(err)
99119
}
100120

101-
request.Header.Set("Content-Type", "application/json")
102-
request.SetBasicAuth(attack.User, attack.Password)
103-
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)
121+
if resp.StatusCode != 200 && resp.StatusCode != 202 {
122+
//to simplify diagnostics
123+
buf, err := io.ReadAll(resp.Body)
124+
if err != nil {
125+
err = errors.Errorf("failed to read from %s responce: status code %v, responce %v, error %v", cmd, resp.StatusCode, resp.Body, err)
126+
return nil, err
127+
}
128+
err = errors.Errorf("failed to exec %v request: status code %v, responce %v", cmd, resp.StatusCode, buf)
129+
return nil, errors.WithStack(err)
109130
}
110131

111-
defer resp.Body.Close()
112-
113132
buf, err := io.ReadAll(resp.Body)
114133
if err != nil {
115-
err = errors.Errorf("failed to read %v responce: %v", cmd, err)
116-
return errors.WithStack(err)
134+
err = errors.Errorf("failed to read %v from %s responce: %v", resp.Body, cmd, err)
135+
return nil, errors.WithStack(err)
117136
}
118137

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)
138+
return buf, nil
139+
}
140+
141+
func execPatroniAttackByLocalMode(attack *core.PatroniCommand, cmd string, values map[string]string) ([]byte, error) {
142+
var cmdTemplate string
143+
144+
if cmd == "failover" {
145+
cmdTemplate = fmt.Sprintf("patronictl %v --master %v --candidate %v --force", cmd, values["leader"], values["candidate"])
146+
} else if cmd == "switchover" {
147+
cmdTemplate = fmt.Sprintf("patronictl %v --master %v --candidate %v --scheduled %v --force", cmd, values["leader"], values["candidate"], values["scheduled_at"])
148+
}
149+
150+
execCmd := exec.Command("bash", "-c", cmdTemplate)
151+
output, err := execCmd.CombinedOutput()
152+
if err != nil {
153+
log.S().Errorf(fmt.Sprintf("failed to %v: %v", cmdTemplate, string(output)))
154+
return nil, err
122155
}
123156

124-
log.S().Infof("Execute %v successfully: %v", cmd, string(buf))
157+
if strings.Contains(string(output), "failed") {
158+
err = errors.New(string(output))
159+
return nil, err
160+
}
125161

126-
return nil
162+
return output, nil
127163
}
128164

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

pkg/server/utils/status.go renamed to pkg/server/utils/http_requests.go

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@
1010
// distributed under the License is distributed on an "AS IS" BASIS,
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
13+
1314
package utils
1415

1516
import (
17+
"bytes"
1618
"fmt"
1719
"io"
1820
"net/http"
@@ -23,9 +25,10 @@ import (
2325
)
2426

2527
type PatroniInfo struct {
26-
Master string
27-
Replicas []string
28-
Status []string
28+
Master string
29+
Replicas []string
30+
SyncStandby []string
31+
Status []string
2932
}
3033

3134
func GetPatroniInfo(address string) (PatroniInfo, error) {
@@ -50,17 +53,47 @@ func GetPatroniInfo(address string) (PatroniInfo, error) {
5053
members := gjson.Get(data, "members")
5154

5255
for _, member := range members.Array() {
53-
if member.Get("role").Str == "leader" {
56+
switch member.Get("role").Str {
57+
case "leader":
5458
patroniInfo.Master = member.Get("name").Str
5559
patroniInfo.Status = append(patroniInfo.Status, member.Get("state").Str)
56-
} else if member.Get("role").Str == "replica" || member.Get("role").Str == "sync_standby" {
60+
case "replica":
5761
patroniInfo.Replicas = append(patroniInfo.Replicas, member.Get("name").Str)
5862
patroniInfo.Status = append(patroniInfo.Status, member.Get("state").Str)
63+
case "sync_standby":
64+
patroniInfo.SyncStandby = append(patroniInfo.SyncStandby, member.Get("name").Str)
65+
patroniInfo.Status = append(patroniInfo.Status, member.Get("state").Str)
66+
5967
}
6068
}
6169

62-
log.Info(fmt.Sprintf("patroni info: master %v, replicas %v, statuses %v\n", patroniInfo.Master, patroniInfo.Replicas, patroniInfo.Status))
70+
log.Info(fmt.Sprintf("patroni info: master %v, replicas %v, sync_standy %s, statuses %v\n", patroniInfo.Master, patroniInfo.Replicas,
71+
patroniInfo.SyncStandby, patroniInfo.Status))
6372

6473
return patroniInfo, nil
6574

6675
}
76+
77+
func MakePostHTTPRequest(address string, port int64, path string, body []byte, user string, password string) (*http.Response, error) {
78+
url := fmt.Sprintf("http://%v:%v/%v", address, port, path)
79+
80+
request, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
81+
if err != nil {
82+
err = errors.Errorf("failed to request %v: %v", url, err)
83+
return nil, errors.WithStack(err)
84+
}
85+
86+
request.Header.Set("Content-Type", "application/json")
87+
request.SetBasicAuth(user, password)
88+
89+
client := &http.Client{}
90+
resp, error := client.Do(request)
91+
if error != nil {
92+
err = errors.Errorf("failed to exec request %v: %v", url, err)
93+
return nil, errors.WithStack(err)
94+
}
95+
96+
defer resp.Body.Close()
97+
98+
return resp, nil
99+
}

0 commit comments

Comments
 (0)