Skip to content

Commit b6958e8

Browse files
author
Nikita Savchenko
committed
Add attack to Patroni PostgreSQL cluster (#229)
Signed-off-by: Nikita Savchenko <[email protected]>
1 parent 3a6efb7 commit b6958e8

File tree

7 files changed

+376
-0
lines changed

7 files changed

+376
-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: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
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+
if len(p.User) == 0 {
50+
return errors.New("patroni user not provided")
51+
}
52+
53+
if len(p.Password) == 0 {
54+
return errors.New("patroni password not provided")
55+
}
56+
57+
return nil
58+
}
59+
60+
func (p PatroniCommand) RecoverData() string {
61+
data, _ := json.Marshal(p)
62+
63+
return string(data)
64+
}
65+
66+
func NewPatroniCommand() *PatroniCommand {
67+
return &PatroniCommand{
68+
CommonAttackConfig: CommonAttackConfig{
69+
Kind: PatroniAttack,
70+
},
71+
}
72+
}

pkg/server/chaosd/patroni.go

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

0 commit comments

Comments
 (0)