2017-02-10 01:58:20 +00:00
|
|
|
package agent
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2018-01-16 21:35:32 +00:00
|
|
|
"fmt"
|
2017-02-10 01:58:20 +00:00
|
|
|
"net/http"
|
|
|
|
"net/http/httptest"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
2017-12-18 21:16:23 +00:00
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/testutil/retry"
|
|
|
|
"github.com/hashicorp/nomad/api"
|
2017-02-10 01:58:20 +00:00
|
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
2018-01-16 21:35:32 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2018-09-28 04:27:38 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2017-02-10 01:58:20 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
func TestHTTP_OperatorRaftConfiguration(t *testing.T) {
|
2017-07-20 05:42:15 +00:00
|
|
|
t.Parallel()
|
2017-07-20 05:14:36 +00:00
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
2017-02-10 01:58:20 +00:00
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, err := http.NewRequest("GET", "/v1/operator/raft/configuration", body)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorRaftConfiguration(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 200 {
|
|
|
|
t.Fatalf("bad code: %d", resp.Code)
|
|
|
|
}
|
|
|
|
out, ok := obj.(structs.RaftConfigurationResponse)
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("unexpected: %T", obj)
|
|
|
|
}
|
|
|
|
if len(out.Servers) != 1 ||
|
|
|
|
!out.Servers[0].Leader ||
|
|
|
|
!out.Servers[0].Voter {
|
|
|
|
t.Fatalf("bad: %v", out)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestHTTP_OperatorRaftPeer(t *testing.T) {
|
2018-01-16 21:35:32 +00:00
|
|
|
assert := assert.New(t)
|
2017-07-20 05:42:15 +00:00
|
|
|
t.Parallel()
|
2017-07-20 05:14:36 +00:00
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
2017-02-10 01:58:20 +00:00
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, err := http.NewRequest("DELETE", "/v1/operator/raft/peer?address=nope", body)
|
2018-01-16 21:35:32 +00:00
|
|
|
assert.Nil(err)
|
|
|
|
|
|
|
|
// If we get this error, it proves we sent the address all the
|
|
|
|
// way through.
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
_, err = s.Server.OperatorRaftPeer(resp, req)
|
|
|
|
if err == nil || !strings.Contains(err.Error(),
|
|
|
|
"address \"nope\" was not found in the Raft configuration") {
|
2017-02-10 01:58:20 +00:00
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-01-16 21:35:32 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, err := http.NewRequest("DELETE", "/v1/operator/raft/peer?id=nope", body)
|
|
|
|
assert.Nil(err)
|
2017-02-10 01:58:20 +00:00
|
|
|
|
|
|
|
// If we get this error, it proves we sent the address all the
|
|
|
|
// way through.
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
_, err = s.Server.OperatorRaftPeer(resp, req)
|
|
|
|
if err == nil || !strings.Contains(err.Error(),
|
2018-01-16 21:35:32 +00:00
|
|
|
"id \"nope\" was not found in the Raft configuration") {
|
2017-02-10 01:58:20 +00:00
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
2017-12-18 21:16:23 +00:00
|
|
|
|
|
|
|
func TestOperator_AutopilotGetConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, _ := http.NewRequest("GET", "/v1/operator/autopilot/configuration", body)
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorAutopilotConfiguration(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 200 {
|
|
|
|
t.Fatalf("bad code: %d", resp.Code)
|
|
|
|
}
|
|
|
|
out, ok := obj.(api.AutopilotConfiguration)
|
|
|
|
if !ok {
|
|
|
|
t.Fatalf("unexpected: %T", obj)
|
|
|
|
}
|
|
|
|
if !out.CleanupDeadServers {
|
|
|
|
t.Fatalf("bad: %#v", out)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_AutopilotSetConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`))
|
|
|
|
req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body)
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
if _, err := s.Server.OperatorAutopilotConfiguration(resp, req); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 200 {
|
2018-01-30 03:53:34 +00:00
|
|
|
t.Fatalf("bad code: %d, %q", resp.Code, resp.Body.String())
|
2017-12-18 21:16:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.GenericRequest{
|
|
|
|
QueryOptions: structs.QueryOptions{
|
|
|
|
Region: s.Config.Region,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-01-30 03:53:34 +00:00
|
|
|
var reply structs.AutopilotConfig
|
2017-12-18 21:16:23 +00:00
|
|
|
if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if reply.CleanupDeadServers {
|
|
|
|
t.Fatalf("bad: %#v", reply)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_AutopilotCASConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer([]byte(`{"CleanupDeadServers": false}`))
|
|
|
|
req, _ := http.NewRequest("PUT", "/v1/operator/autopilot/configuration", body)
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
if _, err := s.Server.OperatorAutopilotConfiguration(resp, req); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 200 {
|
|
|
|
t.Fatalf("bad code: %d", resp.Code)
|
|
|
|
}
|
|
|
|
|
|
|
|
args := structs.GenericRequest{
|
|
|
|
QueryOptions: structs.QueryOptions{
|
|
|
|
Region: s.Config.Region,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-01-30 03:53:34 +00:00
|
|
|
var reply structs.AutopilotConfig
|
2017-12-18 21:16:23 +00:00
|
|
|
if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if reply.CleanupDeadServers {
|
|
|
|
t.Fatalf("bad: %#v", reply)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a CAS request, bad index
|
|
|
|
{
|
|
|
|
buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`))
|
|
|
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex-1), buf)
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorAutopilotConfiguration(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if res := obj.(bool); res {
|
|
|
|
t.Fatalf("should NOT work")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create a CAS request, good index
|
|
|
|
{
|
|
|
|
buf := bytes.NewBuffer([]byte(`{"CleanupDeadServers": true}`))
|
|
|
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/autopilot/configuration?cas=%d", reply.ModifyIndex), buf)
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorAutopilotConfiguration(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if res := obj.(bool); !res {
|
|
|
|
t.Fatalf("should work")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the update
|
|
|
|
if err := s.RPC("Operator.AutopilotGetConfiguration", &args, &reply); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if !reply.CleanupDeadServers {
|
|
|
|
t.Fatalf("bad: %#v", reply)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_ServerHealth(t *testing.T) {
|
|
|
|
httpTest(t, func(c *Config) {
|
|
|
|
c.Server.RaftProtocol = 3
|
|
|
|
}, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body)
|
|
|
|
retry.Run(t, func(r *retry.R) {
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorServerHealth(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 200 {
|
|
|
|
r.Fatalf("bad code: %d", resp.Code)
|
|
|
|
}
|
|
|
|
out, ok := obj.(*api.OperatorHealthReply)
|
|
|
|
if !ok {
|
|
|
|
r.Fatalf("unexpected: %T", obj)
|
|
|
|
}
|
|
|
|
if len(out.Servers) != 1 ||
|
|
|
|
!out.Servers[0].Healthy ||
|
|
|
|
out.Servers[0].Name != s.server.LocalMember().Name ||
|
|
|
|
out.Servers[0].SerfStatus != "alive" ||
|
|
|
|
out.FailureTolerance != 0 {
|
|
|
|
r.Fatalf("bad: %v, %q", out, s.server.LocalMember().Name)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_ServerHealth_Unhealthy(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, func(c *Config) {
|
|
|
|
c.Server.RaftProtocol = 3
|
|
|
|
c.Autopilot.LastContactThreshold = -1 * time.Second
|
|
|
|
}, func(s *TestAgent) {
|
|
|
|
body := bytes.NewBuffer(nil)
|
|
|
|
req, _ := http.NewRequest("GET", "/v1/operator/autopilot/health", body)
|
|
|
|
retry.Run(t, func(r *retry.R) {
|
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorServerHealth(resp, req)
|
|
|
|
if err != nil {
|
|
|
|
r.Fatalf("err: %v", err)
|
|
|
|
}
|
|
|
|
if resp.Code != 429 {
|
|
|
|
r.Fatalf("bad code: %d, %v", resp.Code, obj.(*api.OperatorHealthReply))
|
|
|
|
}
|
|
|
|
out, ok := obj.(*api.OperatorHealthReply)
|
|
|
|
if !ok {
|
|
|
|
r.Fatalf("unexpected: %T", obj)
|
|
|
|
}
|
|
|
|
if len(out.Servers) != 1 ||
|
|
|
|
out.Healthy ||
|
|
|
|
out.Servers[0].Name != s.server.LocalMember().Name {
|
|
|
|
r.Fatalf("bad: %#v", out.Servers)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
2018-09-28 04:27:38 +00:00
|
|
|
|
|
|
|
func TestOperator_SchedulerGetConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
require := require.New(t)
|
|
|
|
body := bytes.NewBuffer(nil)
|
2018-11-12 21:57:45 +00:00
|
|
|
req, _ := http.NewRequest("GET", "/v1/operator/scheduler/configuration", body)
|
2018-09-28 04:27:38 +00:00
|
|
|
resp := httptest.NewRecorder()
|
|
|
|
obj, err := s.Server.OperatorSchedulerConfiguration(resp, req)
|
|
|
|
require.Nil(err)
|
|
|
|
require.Equal(200, resp.Code)
|
2018-11-11 01:53:47 +00:00
|
|
|
out, ok := obj.(structs.SchedulerConfigurationResponse)
|
2018-09-28 04:27:38 +00:00
|
|
|
require.True(ok)
|
2018-10-29 18:10:43 +00:00
|
|
|
require.True(out.SchedulerConfig.PreemptionConfig.SystemSchedulerEnabled)
|
2019-05-03 19:06:12 +00:00
|
|
|
require.True(out.SchedulerConfig.PreemptionConfig.BatchSchedulerEnabled)
|
|
|
|
require.True(out.SchedulerConfig.PreemptionConfig.ServiceSchedulerEnabled)
|
2018-09-28 04:27:38 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_SchedulerSetConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
require := require.New(t)
|
2018-10-01 14:26:52 +00:00
|
|
|
body := bytes.NewBuffer([]byte(`{"PreemptionConfig": {
|
2019-04-29 23:48:07 +00:00
|
|
|
"SystemSchedulerEnabled": true,
|
2019-05-03 19:06:12 +00:00
|
|
|
"ServiceSchedulerEnabled": true
|
2018-10-01 14:26:52 +00:00
|
|
|
}}`))
|
2018-11-12 21:57:45 +00:00
|
|
|
req, _ := http.NewRequest("PUT", "/v1/operator/scheduler/configuration", body)
|
2018-09-28 04:27:38 +00:00
|
|
|
resp := httptest.NewRecorder()
|
2018-11-10 16:31:10 +00:00
|
|
|
setResp, err := s.Server.OperatorSchedulerConfiguration(resp, req)
|
2018-09-28 04:27:38 +00:00
|
|
|
require.Nil(err)
|
|
|
|
require.Equal(200, resp.Code)
|
2018-11-10 16:31:10 +00:00
|
|
|
schedSetResp, ok := setResp.(structs.SchedulerSetConfigurationResponse)
|
|
|
|
require.True(ok)
|
|
|
|
require.NotZero(schedSetResp.Index)
|
2018-09-28 04:27:38 +00:00
|
|
|
|
|
|
|
args := structs.GenericRequest{
|
|
|
|
QueryOptions: structs.QueryOptions{
|
|
|
|
Region: s.Config.Region,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-10-30 16:10:46 +00:00
|
|
|
var reply structs.SchedulerConfigurationResponse
|
2018-09-28 04:27:38 +00:00
|
|
|
err = s.RPC("Operator.SchedulerGetConfiguration", &args, &reply)
|
|
|
|
require.Nil(err)
|
2018-10-30 16:10:46 +00:00
|
|
|
require.True(reply.SchedulerConfig.PreemptionConfig.SystemSchedulerEnabled)
|
2019-05-03 19:06:12 +00:00
|
|
|
require.True(reply.SchedulerConfig.PreemptionConfig.ServiceSchedulerEnabled)
|
2018-09-28 04:27:38 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestOperator_SchedulerCASConfiguration(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
httpTest(t, nil, func(s *TestAgent) {
|
|
|
|
require := require.New(t)
|
2018-10-01 14:26:52 +00:00
|
|
|
body := bytes.NewBuffer([]byte(`{"PreemptionConfig": {
|
2019-04-29 23:48:07 +00:00
|
|
|
"SystemSchedulerEnabled": true,
|
2019-05-03 19:06:12 +00:00
|
|
|
"BatchSchedulerEnabled":true
|
2018-10-01 14:26:52 +00:00
|
|
|
}}`))
|
2018-11-12 21:57:45 +00:00
|
|
|
req, _ := http.NewRequest("PUT", "/v1/operator/scheduler/configuration", body)
|
2018-09-28 04:27:38 +00:00
|
|
|
resp := httptest.NewRecorder()
|
2018-11-10 16:31:10 +00:00
|
|
|
setResp, err := s.Server.OperatorSchedulerConfiguration(resp, req)
|
2018-09-28 04:27:38 +00:00
|
|
|
require.Nil(err)
|
|
|
|
require.Equal(200, resp.Code)
|
2018-11-10 16:31:10 +00:00
|
|
|
schedSetResp, ok := setResp.(structs.SchedulerSetConfigurationResponse)
|
|
|
|
require.True(ok)
|
|
|
|
require.NotZero(schedSetResp.Index)
|
2018-09-28 04:27:38 +00:00
|
|
|
|
|
|
|
args := structs.GenericRequest{
|
|
|
|
QueryOptions: structs.QueryOptions{
|
|
|
|
Region: s.Config.Region,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
2018-10-30 16:10:46 +00:00
|
|
|
var reply structs.SchedulerConfigurationResponse
|
2018-09-28 04:27:38 +00:00
|
|
|
if err := s.RPC("Operator.SchedulerGetConfiguration", &args, &reply); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-10-30 16:10:46 +00:00
|
|
|
require.True(reply.SchedulerConfig.PreemptionConfig.SystemSchedulerEnabled)
|
2019-05-03 19:06:12 +00:00
|
|
|
require.True(reply.SchedulerConfig.PreemptionConfig.BatchSchedulerEnabled)
|
2018-09-28 04:27:38 +00:00
|
|
|
|
|
|
|
// Create a CAS request, bad index
|
|
|
|
{
|
2018-10-01 14:26:52 +00:00
|
|
|
buf := bytes.NewBuffer([]byte(`{"PreemptionConfig": {
|
2019-04-29 23:48:07 +00:00
|
|
|
"SystemSchedulerEnabled": false,
|
2019-05-03 19:06:12 +00:00
|
|
|
"BatchSchedulerEnabled":true
|
2018-10-01 14:26:52 +00:00
|
|
|
}}`))
|
2018-11-12 21:57:45 +00:00
|
|
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/scheduler/configuration?cas=%d", reply.QueryMeta.Index-1), buf)
|
2018-09-28 04:27:38 +00:00
|
|
|
resp := httptest.NewRecorder()
|
2018-11-10 16:31:10 +00:00
|
|
|
setResp, err := s.Server.OperatorSchedulerConfiguration(resp, req)
|
2018-09-28 04:27:38 +00:00
|
|
|
require.Nil(err)
|
2018-11-10 16:31:10 +00:00
|
|
|
// Verify that the response has Updated=false
|
|
|
|
schedSetResp, ok := setResp.(structs.SchedulerSetConfigurationResponse)
|
|
|
|
require.True(ok)
|
|
|
|
require.NotZero(schedSetResp.Index)
|
|
|
|
require.False(schedSetResp.Updated)
|
2018-09-28 04:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Create a CAS request, good index
|
|
|
|
{
|
2018-10-01 14:26:52 +00:00
|
|
|
buf := bytes.NewBuffer([]byte(`{"PreemptionConfig": {
|
2019-04-29 23:48:07 +00:00
|
|
|
"SystemSchedulerEnabled": false,
|
2019-05-03 19:06:12 +00:00
|
|
|
"BatchSchedulerEnabled":false
|
2018-10-01 14:26:52 +00:00
|
|
|
}}`))
|
2018-11-12 21:57:45 +00:00
|
|
|
req, _ := http.NewRequest("PUT", fmt.Sprintf("/v1/operator/scheduler/configuration?cas=%d", reply.QueryMeta.Index), buf)
|
2018-09-28 04:27:38 +00:00
|
|
|
resp := httptest.NewRecorder()
|
2018-11-10 16:31:10 +00:00
|
|
|
setResp, err := s.Server.OperatorSchedulerConfiguration(resp, req)
|
2018-09-28 04:27:38 +00:00
|
|
|
require.Nil(err)
|
2018-11-10 16:31:10 +00:00
|
|
|
// Verify that the response has Updated=true
|
|
|
|
schedSetResp, ok := setResp.(structs.SchedulerSetConfigurationResponse)
|
|
|
|
require.True(ok)
|
|
|
|
require.NotZero(schedSetResp.Index)
|
|
|
|
require.True(schedSetResp.Updated)
|
2018-09-28 04:27:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the update
|
|
|
|
if err := s.RPC("Operator.SchedulerGetConfiguration", &args, &reply); err != nil {
|
|
|
|
t.Fatalf("err: %v", err)
|
|
|
|
}
|
2018-10-30 16:10:46 +00:00
|
|
|
require.False(reply.SchedulerConfig.PreemptionConfig.SystemSchedulerEnabled)
|
2019-05-03 19:06:12 +00:00
|
|
|
require.False(reply.SchedulerConfig.PreemptionConfig.BatchSchedulerEnabled)
|
2018-09-28 04:27:38 +00:00
|
|
|
})
|
|
|
|
}
|