Skip to content

Commit 98bb5db

Browse files
author
Nikita Savchenko
committed
Add attack to Patroni PostgreSQL cluster (#229)
Signed-off-by: Nikita Savchenko [email protected]
1 parent a9c0540 commit 98bb5db

File tree

7 files changed

+355
-0
lines changed

7 files changed

+355
-0
lines changed

cmd/attack/patroni.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright 2020 Chaos Mesh Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package attack
15+
16+
import (
17+
"fmt"
18+
"time"
19+
20+
"github.com/spf13/cobra"
21+
"go.uber.org/fx"
22+
23+
"github.com/chaos-mesh/chaosd/cmd/server"
24+
"github.com/chaos-mesh/chaosd/pkg/core"
25+
"github.com/chaos-mesh/chaosd/pkg/server/chaosd"
26+
"github.com/chaos-mesh/chaosd/pkg/utils"
27+
)
28+
29+
func NewPatroniAttackCommand(uid *string) *cobra.Command {
30+
options := core.NewPatroniCommand()
31+
dep := fx.Options(
32+
server.Module,
33+
fx.Provide(func() *core.PatroniCommand {
34+
options.UID = *uid
35+
return options
36+
}),
37+
)
38+
39+
cmd := &cobra.Command{
40+
Use: "patroni <subcommand>",
41+
Short: "Patroni attack related commands",
42+
}
43+
44+
cmd.AddCommand(
45+
NewPatroniSwitchoverCommand(dep, options),
46+
NewPatroniFailoverCommand(dep, options),
47+
)
48+
49+
cmd.PersistentFlags().StringVarP(&options.User, "user", "u", "patroni", "patroni cluster user")
50+
cmd.PersistentFlags().StringVar(&options.Password, "password", "p", "patroni cluster password")
51+
52+
return cmd
53+
}
54+
55+
func NewPatroniSwitchoverCommand(dep fx.Option, options *core.PatroniCommand) *cobra.Command {
56+
cmd := &cobra.Command{
57+
Use: "switchover",
58+
Short: "exec switchover, default without another attack. Warning! Command is not recover!",
59+
Run: func(*cobra.Command, []string) {
60+
options.Action = core.SwitchoverAction
61+
utils.FxNewAppWithoutLog(dep, fx.Invoke(PatroniAttackF)).Run()
62+
},
63+
}
64+
cmd.Flags().StringVarP(&options.Address, "address", "a", "", "patroni cluster address, any of available hosts")
65+
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")
67+
68+
return cmd
69+
}
70+
71+
func NewPatroniFailoverCommand(dep fx.Option, options *core.PatroniCommand) *cobra.Command {
72+
cmd := &cobra.Command{
73+
Use: "failover",
74+
Short: "exec failover, default without another attack",
75+
Run: func(*cobra.Command, []string) {
76+
options.Action = core.FailoverAction
77+
utils.FxNewAppWithoutLog(dep, fx.Invoke(PatroniAttackF)).Run()
78+
},
79+
}
80+
81+
cmd.Flags().StringVarP(&options.Address, "address", "a", "", "patroni cluster address, any of available hosts")
82+
cmd.Flags().StringVarP(&options.Candidate, "leader", "c", "", "failover new leader, default random unit for replicas")
83+
return cmd
84+
}
85+
86+
func PatroniAttackF(options *core.PatroniCommand, chaos *chaosd.Server) {
87+
if err := options.Validate(); err != nil {
88+
utils.ExitWithError(utils.ExitBadArgs, err)
89+
}
90+
91+
uid, err := chaos.ExecuteAttack(chaosd.PatroniAttack, options, core.CommandMode)
92+
if err != nil {
93+
utils.ExitWithError(utils.ExitError, err)
94+
}
95+
96+
utils.NormalExit(fmt.Sprintf("Attack %s successfully to patroni address %s, uid: %s", options.Action, options.Address, uid))
97+
}

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ require (
3232
github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe
3333
github.com/swaggo/gin-swagger v1.5.0
3434
github.com/swaggo/swag v1.8.3
35+
github.com/tidwall/gjson v1.14.4
3536
go.uber.org/fx v1.17.1
3637
go.uber.org/zap v1.21.0
3738
google.golang.org/grpc v1.40.0
@@ -122,6 +123,8 @@ require (
122123
github.com/romana/ipset v1.0.0 // indirect
123124
github.com/romana/rlog v0.0.0-20171115192701-f018bc92e7d7 // indirect
124125
github.com/sirupsen/logrus v1.8.1 // indirect
126+
github.com/tidwall/match v1.1.1 // indirect
127+
github.com/tidwall/pretty v1.2.0 // indirect
125128
github.com/tklauser/go-sysconf v0.3.10 // indirect
126129
github.com/tklauser/numcpus v0.4.0 // indirect
127130
github.com/ugorji/go/codec v1.2.7 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,12 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG
10291029
github.com/syndtr/goleveldb v1.0.1-0.20200815110645-5c35d600f0ca/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
10301030
github.com/syndtr/goleveldb v1.0.1-0.20210305035536-64b5b1c73954/go.mod h1:u2MKkTVTVJWe5D1rCvame8WqhBd88EuIwODJZ1VHCPM=
10311031
github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I=
1032+
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
1033+
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
1034+
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
1035+
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
1036+
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
1037+
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
10321038
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
10331039
github.com/tklauser/go-sysconf v0.3.10 h1:IJ1AZGZRWbY8T5Vfk04D9WOA5WSejdflXxP03OUqALw=
10341040
github.com/tklauser/go-sysconf v0.3.10/go.mod h1:C8XykCvCb+Gn0oNCWPIlcb0RuglQTYaQ2hGm7jmxEFk=

pkg/core/experiment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
FileAttack = "file"
4444
HTTPAttack = "http"
4545
VMAttack = "vm"
46+
PatroniAttack = "patroni"
4647
UserDefinedAttack = "userDefined"
4748
)
4849

pkg/core/patroni.go

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
// Copyright 2020 Chaos Mesh Authors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package core
15+
16+
import (
17+
"encoding/json"
18+
19+
"github.com/pingcap/errors"
20+
)
21+
22+
const (
23+
SwitchoverAction = "switchover"
24+
FailoverAction = "failover"
25+
)
26+
27+
var _ AttackConfig = &PatroniCommand{}
28+
29+
type PatroniCommand struct {
30+
CommonAttackConfig
31+
32+
Address string `json:"address,omitempty"`
33+
Candidate string `json:"candidate,omitempty"`
34+
Leader string `json:"leader,omitempty"`
35+
User string `json:"user,omitempty"`
36+
Password string `json:"password,omitempty"`
37+
Scheduled_at string `json:"scheduled_at,omitempty"`
38+
RecoverCmd string `json:"recoverCmd,omitempty"`
39+
}
40+
41+
func (p *PatroniCommand) Validate() error {
42+
if err := p.CommonAttackConfig.Validate(); err != nil {
43+
return err
44+
}
45+
if len(p.Address) == 0 {
46+
return errors.New("address not provided")
47+
}
48+
49+
// TODO: validate signal
50+
51+
return nil
52+
}
53+
54+
func (p PatroniCommand) RecoverData() string {
55+
data, _ := json.Marshal(p)
56+
57+
return string(data)
58+
}
59+
60+
func NewPatroniCommand() *PatroniCommand {
61+
return &PatroniCommand{
62+
CommonAttackConfig: CommonAttackConfig{
63+
Kind: PatroniAttack,
64+
},
65+
}
66+
}

pkg/server/chaosd/patroni.go

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package chaosd
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"math/rand"
9+
"net/http"
10+
11+
"github.com/chaos-mesh/chaosd/pkg/core"
12+
"github.com/chaos-mesh/chaosd/pkg/server/utils"
13+
"github.com/pingcap/errors"
14+
"github.com/pingcap/log"
15+
)
16+
17+
type patroniAttack struct{}
18+
19+
var PatroniAttack AttackType = patroniAttack{}
20+
21+
func (patroniAttack) Attack(options core.AttackConfig, _ Environment) error {
22+
attack := options.(*core.PatroniCommand)
23+
24+
candidate := attack.Candidate
25+
26+
leader := attack.Leader
27+
28+
var scheduled_at string
29+
30+
var url string
31+
32+
var availableReplicas []string
33+
34+
values := make(map[string]string)
35+
36+
patroniInfo, err := utils.GetPatroniInfo(attack.Address)
37+
if err != nil {
38+
err = errors.Errorf("failed to get patroni info for : %v", options.String(), err)
39+
return errors.WithStack(err)
40+
}
41+
42+
for _, replica := range patroniInfo.Replicas {
43+
if replica != attack.Address {
44+
availableReplicas = append(availableReplicas, replica)
45+
}
46+
}
47+
48+
if len(availableReplicas) == 0 {
49+
err = errors.Errorf("failed to get available replics. Please, choose another host")
50+
return errors.WithStack(err)
51+
}
52+
53+
if candidate == "" {
54+
55+
candidate = availableReplicas[rand.Intn(len(availableReplicas))]
56+
57+
}
58+
59+
if leader == "" {
60+
leader = patroniInfo.Master
61+
}
62+
63+
switch options.String() {
64+
case "switchover":
65+
66+
scheduled_at = attack.Scheduled_at
67+
68+
values = map[string]string{"leader": leader, "scheduled_at": scheduled_at}
69+
70+
log.Info(fmt.Sprintf("Switchover will be done from %v to another available replica in %v", patroniInfo.Master, scheduled_at))
71+
72+
case "failover":
73+
74+
values = map[string]string{"candidate": candidate}
75+
76+
log.Info(fmt.Sprintf("Failover will be done from %v to %v", patroniInfo.Master, candidate))
77+
78+
}
79+
80+
patroniAddr := attack.Address
81+
82+
cmd := options.String()
83+
84+
data, err := json.Marshal(values)
85+
if err != nil {
86+
err = errors.Errorf("failed to marshal data: %v", values)
87+
return errors.WithStack(err)
88+
}
89+
90+
url = fmt.Sprintf("http://%v:8008/%v", patroniAddr, cmd)
91+
92+
request, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
93+
if err != nil {
94+
err = errors.Errorf("failed to %v: %v", cmd, err)
95+
return errors.WithStack(err)
96+
}
97+
98+
request.Header.Set("Content-Type", "application/json")
99+
request.SetBasicAuth(attack.User, attack.Password)
100+
101+
client := &http.Client{}
102+
resp, error := client.Do(request)
103+
if error != nil {
104+
err = errors.Errorf("failed to %v: %v", cmd, err)
105+
return errors.WithStack(err)
106+
}
107+
108+
defer resp.Body.Close()
109+
110+
buf, err := io.ReadAll(resp.Body)
111+
if err != nil {
112+
err = errors.Errorf("failed to read %v responce: %v", cmd, err)
113+
return errors.WithStack(err)
114+
}
115+
116+
if resp.StatusCode != 200 && resp.StatusCode != 202 {
117+
err = errors.Errorf("failed to %v: status code %v, responce %v", cmd, resp.StatusCode, string(buf))
118+
return errors.WithStack(err)
119+
}
120+
121+
log.S().Infof("Execute %v successfully: %v", cmd, string(buf))
122+
123+
return nil
124+
}
125+
126+
func (patroniAttack) Recover(exp core.Experiment, _ Environment) error {
127+
return nil
128+
}

pkg/server/utils/status.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package utils
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"net/http"
7+
8+
"github.com/pingcap/log"
9+
"github.com/pkg/errors"
10+
"github.com/tidwall/gjson"
11+
)
12+
13+
type PatroniInfo struct {
14+
Master string
15+
Replicas []string
16+
Status []string
17+
}
18+
19+
func GetPatroniInfo(address string) (PatroniInfo, error) {
20+
res, err := http.Get(fmt.Sprintf("http://%v:8008/cluster", address))
21+
if err != nil {
22+
err = errors.Errorf("failed to get patroni status: %v", err)
23+
return PatroniInfo{}, errors.WithStack(err)
24+
}
25+
26+
defer res.Body.Close()
27+
28+
buf, err := io.ReadAll(res.Body)
29+
if err != nil {
30+
err = errors.Errorf("failed to read responce: %v", err)
31+
return PatroniInfo{}, errors.WithStack(err)
32+
}
33+
34+
data := string(buf)
35+
36+
patroniInfo := PatroniInfo{}
37+
38+
members := gjson.Get(data, "members")
39+
40+
for _, member := range members.Array() {
41+
if member.Get("role").Str == "leader" {
42+
patroniInfo.Master = member.Get("name").Str
43+
patroniInfo.Status = append(patroniInfo.Status, member.Get("state").Str)
44+
} else if member.Get("role").Str == "replica" || member.Get("role").Str == "sync_standby" {
45+
patroniInfo.Replicas = append(patroniInfo.Replicas, member.Get("name").Str)
46+
patroniInfo.Status = append(patroniInfo.Status, member.Get("state").Str)
47+
}
48+
}
49+
50+
log.Info(fmt.Sprintf("patroni info: master %v, replicas %v, statuses %v\n", patroniInfo.Master, patroniInfo.Replicas, patroniInfo.Status))
51+
52+
return patroniInfo, nil
53+
54+
}

0 commit comments

Comments
 (0)