2014-04-23 19:57:06 +00:00
|
|
|
package agent
|
|
|
|
|
2014-04-28 21:52:30 +00:00
|
|
|
import (
|
Use fmt.Fprint/Fprintf/Fprintln
Used the following rewrite rules:
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c, d))) -> fmt.Fprintf(resp, a, b, c, d)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c))) -> fmt.Fprintf(resp, a, b, c)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b))) -> fmt.Fprintf(resp, a, b)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a))) -> fmt.Fprint(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a + "\n")) -> fmt.Fprintln(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a)) -> fmt.Fprint(resp, a)' *.go
2017-04-20 14:07:42 +00:00
|
|
|
"fmt"
|
2014-04-28 21:52:30 +00:00
|
|
|
"net/http"
|
2014-04-28 22:52:37 +00:00
|
|
|
"sort"
|
2014-04-28 21:52:30 +00:00
|
|
|
"strings"
|
2015-02-11 21:35:46 +00:00
|
|
|
|
2020-06-22 19:14:12 +00:00
|
|
|
"github.com/hashicorp/consul/agent/config"
|
2017-07-06 10:34:00 +00:00
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
2017-04-19 23:00:11 +00:00
|
|
|
"github.com/hashicorp/consul/api"
|
2014-04-28 21:52:30 +00:00
|
|
|
)
|
|
|
|
|
2018-09-07 17:06:55 +00:00
|
|
|
// metaExternalSource is the key name for the service instance meta that
|
|
|
|
// defines the external syncing source. This is used by the UI APIs below
|
|
|
|
// to extract this.
|
|
|
|
const metaExternalSource = "external-source"
|
|
|
|
|
2020-05-11 17:35:17 +00:00
|
|
|
type GatewayConfig struct {
|
2020-07-30 16:21:11 +00:00
|
|
|
AssociatedServiceCount int `json:",omitempty"`
|
|
|
|
Addresses []string `json:",omitempty"`
|
2020-06-22 19:14:12 +00:00
|
|
|
// internal to track uniqueness
|
|
|
|
addressesSet map[string]struct{}
|
2020-05-11 17:35:17 +00:00
|
|
|
}
|
|
|
|
|
2014-04-28 22:52:37 +00:00
|
|
|
// ServiceSummary is used to summarize a service
|
|
|
|
type ServiceSummary struct {
|
2020-07-29 15:10:06 +00:00
|
|
|
Kind structs.ServiceKind `json:",omitempty"`
|
|
|
|
Name string
|
|
|
|
Tags []string
|
|
|
|
Nodes []string
|
|
|
|
InstanceCount int
|
|
|
|
ChecksPassing int
|
|
|
|
ChecksWarning int
|
|
|
|
ChecksCritical int
|
|
|
|
ExternalSources []string
|
|
|
|
externalSourceSet map[string]struct{} // internal to track uniqueness
|
|
|
|
GatewayConfig GatewayConfig `json:",omitempty"`
|
|
|
|
ConnectedWithProxy bool
|
|
|
|
ConnectedWithGateway bool
|
2019-12-10 02:26:41 +00:00
|
|
|
|
|
|
|
structs.EnterpriseMeta
|
2014-04-28 22:52:37 +00:00
|
|
|
}
|
|
|
|
|
2014-04-28 21:52:30 +00:00
|
|
|
// UINodes is used to list the nodes in a given datacenter. We return a
|
2014-04-28 22:09:46 +00:00
|
|
|
// NodeDump which provides overview information for all the nodes
|
2014-04-28 21:52:30 +00:00
|
|
|
func (s *HTTPServer) UINodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2015-02-11 21:35:46 +00:00
|
|
|
// Parse arguments
|
|
|
|
args := structs.DCSpecificRequest{}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2019-12-10 02:26:41 +00:00
|
|
|
|
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-16 16:00:15 +00:00
|
|
|
s.parseFilter(req, &args.Filter)
|
2014-04-28 21:52:30 +00:00
|
|
|
|
2015-02-11 21:35:46 +00:00
|
|
|
// Make the RPC request
|
|
|
|
var out structs.IndexedNodeDump
|
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
|
|
RPC:
|
|
|
|
if err := s.agent.RPC("Internal.NodeDump", &args, &out); err != nil {
|
|
|
|
// Retry the request allowing stale data if no leader
|
|
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
|
|
args.AllowStale = true
|
|
|
|
goto RPC
|
|
|
|
}
|
2014-04-28 21:52:30 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
2015-11-15 05:05:37 +00:00
|
|
|
|
|
|
|
// Use empty list instead of nil
|
|
|
|
for _, info := range out.Dump {
|
|
|
|
if info.Services == nil {
|
|
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
|
|
}
|
|
|
|
if info.Checks == nil {
|
|
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if out.Dump == nil {
|
|
|
|
out.Dump = make(structs.NodeDump, 0)
|
|
|
|
}
|
2015-02-11 21:35:46 +00:00
|
|
|
return out.Dump, nil
|
2014-04-28 21:52:30 +00:00
|
|
|
}
|
|
|
|
|
2014-04-28 22:09:46 +00:00
|
|
|
// UINodeInfo is used to get info on a single node in a given datacenter. We return a
|
|
|
|
// NodeInfo which provides overview information for the node
|
|
|
|
func (s *HTTPServer) UINodeInfo(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2015-02-11 21:35:46 +00:00
|
|
|
// Parse arguments
|
|
|
|
args := structs.NodeSpecificRequest{}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2014-04-28 22:09:46 +00:00
|
|
|
|
2019-12-10 02:26:41 +00:00
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2014-04-28 22:09:46 +00:00
|
|
|
// Verify we have some DC, or use the default
|
2015-02-11 21:35:46 +00:00
|
|
|
args.Node = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/node/")
|
|
|
|
if args.Node == "" {
|
2017-08-23 19:19:11 +00:00
|
|
|
resp.WriteHeader(http.StatusBadRequest)
|
Use fmt.Fprint/Fprintf/Fprintln
Used the following rewrite rules:
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c, d))) -> fmt.Fprintf(resp, a, b, c, d)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b, c))) -> fmt.Fprintf(resp, a, b, c)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a, b))) -> fmt.Fprintf(resp, a, b)' *.go
gofmt -w -r 'resp.Write([]byte(fmt.Sprintf(a))) -> fmt.Fprint(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a + "\n")) -> fmt.Fprintln(resp, a)' *.go
gofmt -w -r 'resp.Write([]byte(a)) -> fmt.Fprint(resp, a)' *.go
2017-04-20 14:07:42 +00:00
|
|
|
fmt.Fprint(resp, "Missing node name")
|
2014-04-28 22:09:46 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2015-02-11 21:35:46 +00:00
|
|
|
// Make the RPC request
|
|
|
|
var out structs.IndexedNodeDump
|
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
|
|
RPC:
|
|
|
|
if err := s.agent.RPC("Internal.NodeInfo", &args, &out); err != nil {
|
|
|
|
// Retry the request allowing stale data if no leader
|
|
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
|
|
args.AllowStale = true
|
|
|
|
goto RPC
|
|
|
|
}
|
2014-04-28 22:09:46 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return only the first entry
|
2015-02-11 21:35:46 +00:00
|
|
|
if len(out.Dump) > 0 {
|
2015-11-15 05:05:37 +00:00
|
|
|
info := out.Dump[0]
|
|
|
|
if info.Services == nil {
|
|
|
|
info.Services = make([]*structs.NodeService, 0)
|
|
|
|
}
|
|
|
|
if info.Checks == nil {
|
|
|
|
info.Checks = make([]*structs.HealthCheck, 0)
|
|
|
|
}
|
|
|
|
return info, nil
|
2014-04-28 22:09:46 +00:00
|
|
|
}
|
2017-07-10 16:40:00 +00:00
|
|
|
|
2017-07-11 14:13:29 +00:00
|
|
|
resp.WriteHeader(http.StatusNotFound)
|
2014-04-28 22:09:46 +00:00
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
2014-04-28 22:52:37 +00:00
|
|
|
// UIServices is used to list the services in a given datacenter. We return a
|
|
|
|
// ServiceSummary which provides overview information for the service
|
|
|
|
func (s *HTTPServer) UIServices(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
2015-02-11 21:35:46 +00:00
|
|
|
// Parse arguments
|
2019-06-20 19:04:39 +00:00
|
|
|
args := structs.ServiceDumpRequest{}
|
2015-02-11 21:35:46 +00:00
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
2014-04-28 22:52:37 +00:00
|
|
|
|
2019-12-10 02:26:41 +00:00
|
|
|
if err := s.parseEntMeta(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2019-04-16 16:00:15 +00:00
|
|
|
s.parseFilter(req, &args.Filter)
|
|
|
|
|
2015-02-11 21:35:46 +00:00
|
|
|
// Make the RPC request
|
2020-07-30 16:21:11 +00:00
|
|
|
var out structs.IndexedNodesWithGateways
|
2015-02-11 21:35:46 +00:00
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
|
|
RPC:
|
2019-04-16 16:00:15 +00:00
|
|
|
if err := s.agent.RPC("Internal.ServiceDump", &args, &out); err != nil {
|
2015-02-11 21:35:46 +00:00
|
|
|
// Retry the request allowing stale data if no leader
|
|
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
|
|
args.AllowStale = true
|
|
|
|
goto RPC
|
|
|
|
}
|
2014-04-28 22:52:37 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generate the summary
|
2020-05-11 17:35:17 +00:00
|
|
|
// TODO (gateways) (freddy) Have Internal.ServiceDump return ServiceDump instead. Need to add bexpr filtering for type.
|
2020-07-30 16:21:11 +00:00
|
|
|
return summarizeServices(out.Nodes.ToServiceDump(), out.Gateways, s.agent.config), nil
|
2014-04-28 22:52:37 +00:00
|
|
|
}
|
|
|
|
|
2020-05-11 17:35:17 +00:00
|
|
|
// UIGatewayServices is used to query all the nodes for services associated with a gateway along with their gateway config
|
|
|
|
func (s *HTTPServer) UIGatewayServicesNodes(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
|
|
|
// Parse arguments
|
|
|
|
args := structs.ServiceSpecificRequest{}
|
|
|
|
if err := s.parseEntMetaNoWildcard(req, &args.EnterpriseMeta); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Pull out the service name
|
|
|
|
args.ServiceName = strings.TrimPrefix(req.URL.Path, "/v1/internal/ui/gateway-services-nodes/")
|
|
|
|
if args.ServiceName == "" {
|
|
|
|
resp.WriteHeader(http.StatusBadRequest)
|
|
|
|
fmt.Fprint(resp, "Missing gateway name")
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make the RPC request
|
|
|
|
var out structs.IndexedServiceDump
|
|
|
|
defer setMeta(resp, &out.QueryMeta)
|
|
|
|
RPC:
|
|
|
|
if err := s.agent.RPC("Internal.GatewayServiceDump", &args, &out); err != nil {
|
|
|
|
// Retry the request allowing stale data if no leader
|
|
|
|
if strings.Contains(err.Error(), structs.ErrNoLeader.Error()) && !args.AllowStale {
|
|
|
|
args.AllowStale = true
|
|
|
|
goto RPC
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
2020-06-22 19:14:12 +00:00
|
|
|
|
2020-07-30 16:21:11 +00:00
|
|
|
return summarizeServices(out.Dump, nil, s.agent.config), nil
|
2020-05-11 17:35:17 +00:00
|
|
|
}
|
|
|
|
|
2020-07-30 16:21:11 +00:00
|
|
|
func summarizeServices(dump structs.ServiceDump, gateways structs.GatewayServices, cfg *config.RuntimeConfig) []*ServiceSummary {
|
2014-04-28 22:52:37 +00:00
|
|
|
// Collect the summary information
|
2020-07-30 16:21:11 +00:00
|
|
|
var services []structs.ServiceName
|
|
|
|
summary := make(map[structs.ServiceName]*ServiceSummary)
|
2020-07-29 15:10:06 +00:00
|
|
|
|
2020-07-30 16:21:11 +00:00
|
|
|
linkedGateways := make(map[structs.ServiceName][]structs.ServiceName)
|
|
|
|
hasProxy := make(map[structs.ServiceName]bool)
|
2020-07-29 15:10:06 +00:00
|
|
|
|
2020-07-30 16:21:11 +00:00
|
|
|
getService := func(service structs.ServiceName) *ServiceSummary {
|
2014-04-28 22:52:37 +00:00
|
|
|
serv, ok := summary[service]
|
|
|
|
if !ok {
|
2019-12-10 02:26:41 +00:00
|
|
|
serv = &ServiceSummary{
|
2020-07-30 16:21:11 +00:00
|
|
|
Name: service.Name,
|
2019-12-10 02:26:41 +00:00
|
|
|
EnterpriseMeta: service.EnterpriseMeta,
|
2020-03-09 15:56:19 +00:00
|
|
|
// the other code will increment this unconditionally so we
|
|
|
|
// shouldn't initialize it to 1
|
|
|
|
InstanceCount: 0,
|
2019-12-10 02:26:41 +00:00
|
|
|
}
|
2014-04-28 22:52:37 +00:00
|
|
|
summary[service] = serv
|
|
|
|
services = append(services, service)
|
|
|
|
}
|
|
|
|
return serv
|
|
|
|
}
|
|
|
|
|
2020-07-30 16:21:11 +00:00
|
|
|
// Collect the list of services linked to each gateway up front
|
|
|
|
// THis also allows tracking whether a service name is associated with a gateway
|
|
|
|
gsCount := make(map[structs.ServiceName]int)
|
|
|
|
|
|
|
|
for _, gs := range gateways {
|
|
|
|
gsCount[gs.Gateway] += 1
|
|
|
|
linkedGateways[gs.Service] = append(linkedGateways[gs.Service], gs.Gateway)
|
|
|
|
}
|
|
|
|
|
2019-04-16 16:00:15 +00:00
|
|
|
for _, csn := range dump {
|
2020-05-11 17:35:17 +00:00
|
|
|
if csn.GatewayService != nil {
|
2020-06-22 19:14:12 +00:00
|
|
|
gwsvc := csn.GatewayService
|
2020-07-30 16:21:11 +00:00
|
|
|
sum := getService(gwsvc.Service)
|
2020-06-22 19:14:12 +00:00
|
|
|
modifySummaryForGatewayService(cfg, sum, gwsvc)
|
2020-05-11 17:35:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Will happen in cases where we only have the GatewayServices mapping
|
|
|
|
if csn.Service == nil {
|
|
|
|
continue
|
|
|
|
}
|
2020-07-30 16:21:11 +00:00
|
|
|
sid := structs.NewServiceName(csn.Service.Service, &csn.Service.EnterpriseMeta)
|
2020-05-11 17:35:17 +00:00
|
|
|
sum := getService(sid)
|
|
|
|
|
2019-04-16 16:00:15 +00:00
|
|
|
svc := csn.Service
|
|
|
|
sum.Nodes = append(sum.Nodes, csn.Node.Node)
|
|
|
|
sum.Kind = svc.Kind
|
2020-03-09 15:56:19 +00:00
|
|
|
sum.InstanceCount += 1
|
2020-03-27 14:57:46 +00:00
|
|
|
if svc.Kind == structs.ServiceKindConnectProxy {
|
2020-07-30 16:21:11 +00:00
|
|
|
hasProxy[structs.NewServiceName(svc.Proxy.DestinationServiceName, &svc.EnterpriseMeta)] = true
|
2020-03-27 14:57:46 +00:00
|
|
|
}
|
2019-04-16 16:00:15 +00:00
|
|
|
for _, tag := range svc.Tags {
|
|
|
|
found := false
|
|
|
|
for _, existing := range sum.Tags {
|
|
|
|
if existing == tag {
|
|
|
|
found = true
|
|
|
|
break
|
2018-09-06 19:19:05 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-16 16:00:15 +00:00
|
|
|
if !found {
|
|
|
|
sum.Tags = append(sum.Tags, tag)
|
|
|
|
}
|
2014-04-28 22:52:37 +00:00
|
|
|
}
|
2019-04-16 16:00:15 +00:00
|
|
|
|
|
|
|
// If there is an external source, add it to the list of external
|
|
|
|
// sources. We only want to add unique sources so there is extra
|
|
|
|
// accounting here with an unexported field to maintain the set
|
|
|
|
// of sources.
|
|
|
|
if len(svc.Meta) > 0 && svc.Meta[metaExternalSource] != "" {
|
|
|
|
source := svc.Meta[metaExternalSource]
|
|
|
|
if sum.externalSourceSet == nil {
|
|
|
|
sum.externalSourceSet = make(map[string]struct{})
|
2014-04-28 22:52:37 +00:00
|
|
|
}
|
2019-04-16 16:00:15 +00:00
|
|
|
if _, ok := sum.externalSourceSet[source]; !ok {
|
|
|
|
sum.externalSourceSet[source] = struct{}{}
|
|
|
|
sum.ExternalSources = append(sum.ExternalSources, source)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, check := range csn.Checks {
|
|
|
|
switch check.Status {
|
|
|
|
case api.HealthPassing:
|
|
|
|
sum.ChecksPassing++
|
|
|
|
case api.HealthWarning:
|
|
|
|
sum.ChecksWarning++
|
|
|
|
case api.HealthCritical:
|
|
|
|
sum.ChecksCritical++
|
2014-04-28 22:52:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Return the services in sorted order
|
2019-12-10 02:26:41 +00:00
|
|
|
sort.Slice(services, func(i, j int) bool {
|
|
|
|
return services[i].LessThan(&services[j])
|
|
|
|
})
|
2020-07-30 16:21:11 +00:00
|
|
|
|
2014-04-28 22:52:37 +00:00
|
|
|
output := make([]*ServiceSummary, len(summary))
|
|
|
|
for idx, service := range services {
|
|
|
|
sum := summary[service]
|
2020-07-29 15:10:06 +00:00
|
|
|
if hasProxy[service] {
|
|
|
|
sum.ConnectedWithProxy = true
|
|
|
|
}
|
2020-07-30 16:21:11 +00:00
|
|
|
|
|
|
|
// Verify that at least one of the gateways linked by config entry has an instance registered in the catalog
|
|
|
|
for _, gw := range linkedGateways[service] {
|
|
|
|
if s := summary[gw]; s != nil && s.InstanceCount > 0 {
|
|
|
|
sum.ConnectedWithGateway = true
|
|
|
|
}
|
2020-07-29 15:10:06 +00:00
|
|
|
}
|
2020-07-30 16:21:11 +00:00
|
|
|
sum.GatewayConfig.AssociatedServiceCount = gsCount[service]
|
|
|
|
|
|
|
|
// Sort the nodes and tags
|
2014-04-28 22:52:37 +00:00
|
|
|
sort.Strings(sum.Nodes)
|
2020-05-11 17:35:17 +00:00
|
|
|
sort.Strings(sum.Tags)
|
2014-04-28 22:52:37 +00:00
|
|
|
output[idx] = sum
|
|
|
|
}
|
|
|
|
return output
|
|
|
|
}
|
2020-06-22 19:14:12 +00:00
|
|
|
|
|
|
|
func modifySummaryForGatewayService(
|
|
|
|
cfg *config.RuntimeConfig,
|
|
|
|
sum *ServiceSummary,
|
|
|
|
gwsvc *structs.GatewayService,
|
|
|
|
) {
|
|
|
|
var dnsAddresses []string
|
|
|
|
for _, domain := range []string{cfg.DNSDomain, cfg.DNSAltDomain} {
|
|
|
|
// If the domain is empty, do not use it to construct a valid DNS
|
|
|
|
// address
|
|
|
|
if domain == "" {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
dnsAddresses = append(dnsAddresses, serviceIngressDNSName(
|
|
|
|
gwsvc.Service.Name,
|
|
|
|
cfg.Datacenter,
|
|
|
|
domain,
|
|
|
|
&gwsvc.Service.EnterpriseMeta,
|
|
|
|
))
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, addr := range gwsvc.Addresses(dnsAddresses) {
|
|
|
|
// check for duplicates, a service will have a ServiceInfo struct for
|
|
|
|
// every instance that is registered.
|
|
|
|
if _, ok := sum.GatewayConfig.addressesSet[addr]; !ok {
|
|
|
|
if sum.GatewayConfig.addressesSet == nil {
|
|
|
|
sum.GatewayConfig.addressesSet = make(map[string]struct{})
|
|
|
|
}
|
|
|
|
sum.GatewayConfig.addressesSet[addr] = struct{}{}
|
|
|
|
sum.GatewayConfig.Addresses = append(
|
|
|
|
sum.GatewayConfig.Addresses, addr,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|