mirror of
https://github.com/empayre/fleet.git
synced 2024-11-07 17:28:54 +00:00
704ddd424b
* Initial scaffolding of the host summary endpoint * inmem datastore implementation of GenerateHostStatusStatistics * HostSummary docstring * changing the url of the host summary endpoint * datastore tests for GenerateHostStatusStatistics * MySQL datastore implementation of GenerateHostStatusStatistics * <= and >= to catch exact time edge case * removing clock interface method * lowercase error wraps * removin superfluous whitespace * use updated_at * adding a seen_at column to the hosts table * moving the update of seen_time to the caller * using db.Get instead of db.Select
589 lines
15 KiB
Go
589 lines
15 KiB
Go
package datastore
|
|
|
|
import (
|
|
"fmt"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/WatchBeam/clock"
|
|
"github.com/kolide/kolide-ose/server/kolide"
|
|
"github.com/kolide/kolide-ose/server/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var enrollTests = []struct {
|
|
uuid, hostname, platform string
|
|
nodeKeySize int
|
|
}{
|
|
0: {uuid: "6D14C88F-8ECF-48D5-9197-777647BF6B26",
|
|
hostname: "web.kolide.co",
|
|
platform: "linux",
|
|
nodeKeySize: 12,
|
|
},
|
|
1: {uuid: "B998C0EB-38CE-43B1-A743-FBD7A5C9513B",
|
|
hostname: "mail.kolide.co",
|
|
platform: "linux",
|
|
nodeKeySize: 10,
|
|
},
|
|
2: {uuid: "008F0688-5311-4C59-86EE-00C2D6FC3EC2",
|
|
hostname: "home.kolide.co",
|
|
platform: "darwin",
|
|
nodeKeySize: 25,
|
|
},
|
|
3: {uuid: "uuid123",
|
|
hostname: "fakehostname",
|
|
platform: "darwin",
|
|
nodeKeySize: 1,
|
|
},
|
|
}
|
|
|
|
func testSaveHosts(t *testing.T, ds kolide.Datastore) {
|
|
host, err := ds.NewHost(&kolide.Host{
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "1",
|
|
UUID: "1",
|
|
HostName: "foo.local",
|
|
})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
|
|
host.HostName = "bar.local"
|
|
err = ds.SaveHost(host)
|
|
require.Nil(t, err)
|
|
|
|
host, err = ds.Host(host.ID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, "bar.local", host.HostName)
|
|
|
|
host.NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
HostID: host.ID,
|
|
Interface: "en0",
|
|
IPAddress: "98.99.100.101",
|
|
},
|
|
&kolide.NetworkInterface{
|
|
HostID: host.ID,
|
|
Interface: "en1",
|
|
IPAddress: "98.99.100.102",
|
|
},
|
|
}
|
|
|
|
err = ds.SaveHost(host)
|
|
require.Nil(t, err)
|
|
|
|
host, err = ds.Host(host.ID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
require.Equal(t, 2, len(host.NetworkInterfaces))
|
|
primaryNicID := host.NetworkInterfaces[0].ID
|
|
host.PrimaryNetworkInterfaceID = &primaryNicID
|
|
err = ds.SaveHost(host)
|
|
require.Nil(t, err)
|
|
host, err = ds.Host(host.ID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
require.Equal(t, 2, len(host.NetworkInterfaces))
|
|
assert.Equal(t, primaryNicID, *host.PrimaryNetworkInterfaceID)
|
|
|
|
// remove primary nic, host primary nic should change
|
|
host.NetworkInterfaces = []*kolide.NetworkInterface{
|
|
host.NetworkInterfaces[1],
|
|
}
|
|
err = ds.SaveHost(host)
|
|
require.Nil(t, err)
|
|
host, err = ds.Host(host.ID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
assert.Equal(t, host.NetworkInterfaces[0].ID, *host.PrimaryNetworkInterfaceID)
|
|
assert.Equal(t, 1, len(host.NetworkInterfaces))
|
|
|
|
// remove all nics primary nic should be nil
|
|
host.NetworkInterfaces = []*kolide.NetworkInterface{}
|
|
err = ds.SaveHost(host)
|
|
require.Nil(t, err)
|
|
assert.Nil(t, host.PrimaryNetworkInterfaceID)
|
|
host, err = ds.Host(host.ID)
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
assert.Nil(t, host.PrimaryNetworkInterfaceID)
|
|
|
|
err = ds.DeleteHost(host.ID)
|
|
assert.Nil(t, err)
|
|
|
|
host, err = ds.Host(host.ID)
|
|
assert.NotNil(t, err)
|
|
assert.Nil(t, host)
|
|
}
|
|
|
|
func testDeleteHost(t *testing.T, ds kolide.Datastore) {
|
|
host, err := ds.NewHost(&kolide.Host{
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "1",
|
|
UUID: "1",
|
|
HostName: "foo.local",
|
|
})
|
|
require.Nil(t, err)
|
|
require.NotNil(t, host)
|
|
|
|
err = ds.DeleteHost(host.ID)
|
|
assert.Nil(t, err)
|
|
|
|
host, err = ds.Host(host.ID)
|
|
assert.NotNil(t, err)
|
|
}
|
|
|
|
func testListHost(t *testing.T, ds kolide.Datastore) {
|
|
hosts := []*kolide.Host{}
|
|
for i := 0; i < 10; i++ {
|
|
host, err := ds.NewHost(&kolide.Host{
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
OsqueryHostID: strconv.Itoa(i),
|
|
NodeKey: fmt.Sprintf("%d", i),
|
|
UUID: fmt.Sprintf("%d", i),
|
|
HostName: fmt.Sprintf("foo.local%d", i),
|
|
})
|
|
assert.Nil(t, err)
|
|
if err != nil {
|
|
return
|
|
}
|
|
hosts = append(hosts, host)
|
|
}
|
|
|
|
hosts[1].NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
Interface: "en0",
|
|
IPAddress: "99.100.101.102",
|
|
},
|
|
&kolide.NetworkInterface{
|
|
Interface: "en1",
|
|
IPAddress: "99.100.101.103",
|
|
},
|
|
}
|
|
|
|
err := ds.SaveHost(hosts[1])
|
|
require.Nil(t, err)
|
|
|
|
hosts[3].NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
Interface: "en2",
|
|
IPAddress: "99.100.101.104",
|
|
},
|
|
}
|
|
err = ds.SaveHost(hosts[3])
|
|
require.Nil(t, err)
|
|
|
|
hosts2, err := ds.ListHosts(kolide.ListOptions{})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, len(hosts), len(hosts2))
|
|
|
|
require.Equal(t, 2, len(hosts2[1].NetworkInterfaces))
|
|
require.Equal(t, 0, len(hosts2[2].NetworkInterfaces))
|
|
require.Equal(t, 1, len(hosts2[3].NetworkInterfaces))
|
|
assert.Equal(t, "en1", hosts2[1].NetworkInterfaces[1].Interface)
|
|
assert.Equal(t, "en2", hosts2[3].NetworkInterfaces[0].Interface)
|
|
|
|
err = ds.DeleteHost(hosts[0].ID)
|
|
require.Nil(t, err)
|
|
hosts2, err = ds.ListHosts(kolide.ListOptions{})
|
|
require.Nil(t, err)
|
|
assert.Equal(t, len(hosts)-1, len(hosts2))
|
|
|
|
hosts, err = ds.ListHosts(kolide.ListOptions{})
|
|
require.Nil(t, err)
|
|
require.Equal(t, len(hosts2), len(hosts))
|
|
hosts[0].NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
IPAddress: "98.99.100.101",
|
|
Interface: "en0",
|
|
},
|
|
&kolide.NetworkInterface{
|
|
IPAddress: "98.99.100.102",
|
|
Interface: "en1",
|
|
},
|
|
}
|
|
|
|
err = ds.SaveHost(hosts[0])
|
|
require.Nil(t, err)
|
|
hosts2, err = ds.ListHosts(kolide.ListOptions{})
|
|
require.Nil(t, err)
|
|
require.Equal(t, hosts[0].ID, hosts2[0].ID)
|
|
assert.Equal(t, len(hosts[0].NetworkInterfaces), len(hosts2[0].NetworkInterfaces))
|
|
assert.Equal(t, 0, len(hosts2[1].NetworkInterfaces))
|
|
assert.Equal(t, hosts[0].ID, hosts2[0].NetworkInterfaces[0].HostID)
|
|
}
|
|
|
|
func testEnrollHost(t *testing.T, ds kolide.Datastore) {
|
|
var hosts []*kolide.Host
|
|
for _, tt := range enrollTests {
|
|
h, err := ds.EnrollHost(tt.uuid, tt.nodeKeySize)
|
|
require.Nil(t, err)
|
|
|
|
hosts = append(hosts, h)
|
|
assert.Equal(t, tt.uuid, h.OsqueryHostID)
|
|
assert.NotEmpty(t, h.NodeKey)
|
|
}
|
|
|
|
}
|
|
|
|
func testAuthenticateHost(t *testing.T, ds kolide.Datastore) {
|
|
for _, tt := range enrollTests {
|
|
h, err := ds.EnrollHost(tt.uuid, tt.nodeKeySize)
|
|
require.Nil(t, err)
|
|
|
|
returned, err := ds.AuthenticateHost(h.NodeKey)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, h.NodeKey, returned.NodeKey)
|
|
}
|
|
|
|
_, err := ds.AuthenticateHost("7B1A9DC9-B042-489F-8D5A-EEC2412C95AA")
|
|
assert.NotNil(t, err)
|
|
|
|
_, err = ds.AuthenticateHost("")
|
|
assert.NotNil(t, err)
|
|
}
|
|
|
|
func testSearchHosts(t *testing.T, ds kolide.Datastore) {
|
|
_, err := ds.NewHost(&kolide.Host{
|
|
OsqueryHostID: "1234",
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "1",
|
|
UUID: "1",
|
|
HostName: "foo.local",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
h2, err := ds.NewHost(&kolide.Host{
|
|
OsqueryHostID: "5679",
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "2",
|
|
UUID: "2",
|
|
HostName: "bar.local",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
h3, err := ds.NewHost(&kolide.Host{
|
|
OsqueryHostID: "99999",
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "3",
|
|
UUID: "3",
|
|
HostName: "foo-bar.local",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
// We once threw errors when the search query was empty. Verify that we
|
|
// don't error.
|
|
_, err = ds.SearchHosts("")
|
|
require.Nil(t, err)
|
|
|
|
hosts, err := ds.SearchHosts("foo")
|
|
assert.Nil(t, err)
|
|
assert.Len(t, hosts, 2)
|
|
|
|
host, err := ds.SearchHosts("foo", h3.ID)
|
|
require.Nil(t, err)
|
|
require.Len(t, host, 1)
|
|
assert.Equal(t, "foo.local", host[0].HostName)
|
|
|
|
host, err = ds.SearchHosts("foo", h3.ID, h2.ID)
|
|
require.Nil(t, err)
|
|
require.Len(t, host, 1)
|
|
assert.Equal(t, "foo.local", host[0].HostName)
|
|
|
|
none, err := ds.SearchHosts("xxx")
|
|
assert.Nil(t, err)
|
|
assert.Len(t, none, 0)
|
|
|
|
// check to make sure search on ip address works
|
|
h2.NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
Interface: "en0",
|
|
IPAddress: "99.100.101.102",
|
|
},
|
|
&kolide.NetworkInterface{
|
|
Interface: "en1",
|
|
IPAddress: "99.100.101.103",
|
|
},
|
|
}
|
|
err = ds.SaveHost(h2)
|
|
require.Nil(t, err)
|
|
|
|
hits, err := ds.SearchHosts("99.100.101")
|
|
require.Nil(t, err)
|
|
require.Equal(t, 1, len(hits))
|
|
assert.Equal(t, 2, len(hits[0].NetworkInterfaces))
|
|
|
|
hits, err = ds.SearchHosts("99.100.111")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 0, len(hits))
|
|
|
|
h3.NetworkInterfaces = []*kolide.NetworkInterface{
|
|
&kolide.NetworkInterface{
|
|
Interface: "en3",
|
|
IPAddress: "99.100.101.104",
|
|
},
|
|
}
|
|
err = ds.SaveHost(h3)
|
|
require.Nil(t, err)
|
|
hits, err = ds.SearchHosts("99.100.101")
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 2, len(hits))
|
|
hits, err = ds.SearchHosts("99.100.101", h3.ID)
|
|
require.Nil(t, err)
|
|
assert.Equal(t, 1, len(hits))
|
|
}
|
|
|
|
func testSearchHostsLimit(t *testing.T, ds kolide.Datastore) {
|
|
for i := 0; i < 15; i++ {
|
|
_, err := ds.NewHost(&kolide.Host{
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
OsqueryHostID: fmt.Sprintf("host%d", i),
|
|
NodeKey: fmt.Sprintf("%d", i),
|
|
UUID: fmt.Sprintf("%d", i),
|
|
HostName: fmt.Sprintf("foo.%d.local", i),
|
|
})
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
hosts, err := ds.SearchHosts("foo")
|
|
require.Nil(t, err)
|
|
assert.Len(t, hosts, 10)
|
|
}
|
|
|
|
func testDistributedQueriesForHost(t *testing.T, ds kolide.Datastore) {
|
|
user := test.NewUser(t, ds, "Zach", "zwass", "zwass@kolide.co", true)
|
|
|
|
h1, err := ds.NewHost(&kolide.Host{
|
|
OsqueryHostID: "1",
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "1",
|
|
UUID: "1",
|
|
HostName: "foo.local",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
h2, err := ds.NewHost(&kolide.Host{
|
|
OsqueryHostID: "2",
|
|
DetailUpdateTime: time.Now(),
|
|
SeenTime: time.Now(),
|
|
NodeKey: "2",
|
|
UUID: "2",
|
|
HostName: "bar.local",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
// All should have no queries
|
|
var queries map[uint]string
|
|
queries, err = ds.DistributedQueriesForHost(h1)
|
|
require.Nil(t, err)
|
|
assert.Empty(t, queries)
|
|
queries, err = ds.DistributedQueriesForHost(h2)
|
|
require.Nil(t, err)
|
|
assert.Empty(t, queries)
|
|
|
|
// Create a label
|
|
l1, err := ds.NewLabel(&kolide.Label{
|
|
Name: "label foo",
|
|
Query: "query1",
|
|
})
|
|
require.Nil(t, err)
|
|
l1ID := fmt.Sprintf("%d", l1.ID)
|
|
|
|
// Add hosts to label
|
|
for _, h := range []*kolide.Host{h1, h2} {
|
|
err = ds.RecordLabelQueryExecutions(h, map[string]bool{l1ID: true}, time.Now())
|
|
require.Nil(t, err)
|
|
}
|
|
|
|
// Create a query
|
|
q1 := &kolide.Query{
|
|
Name: "bar",
|
|
Query: "select * from bar",
|
|
AuthorID: user.ID,
|
|
}
|
|
q1, err = ds.NewQuery(q1)
|
|
require.Nil(t, err)
|
|
|
|
// Create a query campaign
|
|
c1 := &kolide.DistributedQueryCampaign{
|
|
QueryID: q1.ID,
|
|
Status: kolide.QueryRunning,
|
|
}
|
|
c1, err = ds.NewDistributedQueryCampaign(c1)
|
|
require.Nil(t, err)
|
|
|
|
// Add a target to the campaign
|
|
target := &kolide.DistributedQueryCampaignTarget{
|
|
Type: kolide.TargetLabel,
|
|
DistributedQueryCampaignID: c1.ID,
|
|
TargetID: l1.ID,
|
|
}
|
|
target, err = ds.NewDistributedQueryCampaignTarget(target)
|
|
require.Nil(t, err)
|
|
|
|
// All should have the query now
|
|
queries, err = ds.DistributedQueriesForHost(h1)
|
|
require.Nil(t, err)
|
|
assert.Len(t, queries, 1)
|
|
assert.Equal(t, "select * from bar", queries[c1.ID])
|
|
queries, err = ds.DistributedQueriesForHost(h2)
|
|
require.Nil(t, err)
|
|
assert.Len(t, queries, 1)
|
|
assert.Equal(t, "select * from bar", queries[c1.ID])
|
|
|
|
// Record an execution
|
|
exec := &kolide.DistributedQueryExecution{
|
|
HostID: h1.ID,
|
|
DistributedQueryCampaignID: c1.ID,
|
|
Status: kolide.ExecutionSucceeded,
|
|
}
|
|
_, err = ds.NewDistributedQueryExecution(exec)
|
|
require.Nil(t, err)
|
|
|
|
// Add another query/campaign
|
|
q2 := &kolide.Query{
|
|
Name: "foo",
|
|
Query: "select * from foo",
|
|
AuthorID: user.ID,
|
|
}
|
|
q2, err = ds.NewQuery(q2)
|
|
require.Nil(t, err)
|
|
|
|
c2 := &kolide.DistributedQueryCampaign{
|
|
QueryID: q2.ID,
|
|
Status: kolide.QueryRunning,
|
|
}
|
|
c2, err = ds.NewDistributedQueryCampaign(c2)
|
|
require.Nil(t, err)
|
|
|
|
// This one targets only h1
|
|
target = &kolide.DistributedQueryCampaignTarget{
|
|
Type: kolide.TargetHost,
|
|
DistributedQueryCampaignID: c2.ID,
|
|
TargetID: h1.ID,
|
|
}
|
|
_, err = ds.NewDistributedQueryCampaignTarget(target)
|
|
require.Nil(t, err)
|
|
|
|
// Check for correct queries
|
|
queries, err = ds.DistributedQueriesForHost(h1)
|
|
require.Nil(t, err)
|
|
assert.Len(t, queries, 1)
|
|
assert.Equal(t, "select * from foo", queries[c2.ID])
|
|
queries, err = ds.DistributedQueriesForHost(h2)
|
|
require.Nil(t, err)
|
|
assert.Len(t, queries, 1)
|
|
assert.Equal(t, "select * from bar", queries[c1.ID])
|
|
|
|
// End both of the campaigns
|
|
c1.Status = kolide.QueryComplete
|
|
require.Nil(t, ds.SaveDistributedQueryCampaign(c1))
|
|
c2.Status = kolide.QueryComplete
|
|
require.Nil(t, ds.SaveDistributedQueryCampaign(c2))
|
|
|
|
// Now no queries should be returned
|
|
queries, err = ds.DistributedQueriesForHost(h1)
|
|
require.Nil(t, err)
|
|
assert.Empty(t, queries)
|
|
queries, err = ds.DistributedQueriesForHost(h2)
|
|
require.Nil(t, err)
|
|
assert.Empty(t, queries)
|
|
}
|
|
|
|
func testGenerateHostStatusStatistics(t *testing.T, ds kolide.Datastore) {
|
|
mockClock := clock.NewMockClock()
|
|
|
|
// Online
|
|
_, err := ds.NewHost(&kolide.Host{
|
|
ID: 1,
|
|
OsqueryHostID: "1",
|
|
UUID: "1",
|
|
NodeKey: "1",
|
|
DetailUpdateTime: mockClock.Now(),
|
|
SeenTime: mockClock.Now(),
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
// Online
|
|
_, err = ds.NewHost(&kolide.Host{
|
|
ID: 2,
|
|
OsqueryHostID: "2",
|
|
UUID: "2",
|
|
NodeKey: "2",
|
|
DetailUpdateTime: mockClock.Now().Add(-1 * time.Minute),
|
|
SeenTime: mockClock.Now().Add(-1 * time.Minute),
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
// Offline
|
|
_, err = ds.NewHost(&kolide.Host{
|
|
ID: 3,
|
|
OsqueryHostID: "3",
|
|
UUID: "3",
|
|
NodeKey: "3",
|
|
DetailUpdateTime: mockClock.Now().Add(-1 * time.Hour),
|
|
SeenTime: mockClock.Now().Add(-1 * time.Hour),
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
// MIA
|
|
_, err = ds.NewHost(&kolide.Host{
|
|
ID: 4,
|
|
OsqueryHostID: "4",
|
|
UUID: "4",
|
|
NodeKey: "4",
|
|
DetailUpdateTime: mockClock.Now().Add(-35 * (24 * time.Hour)),
|
|
SeenTime: mockClock.Now().Add(-35 * (24 * time.Hour)),
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
online, offline, mia, err := ds.GenerateHostStatusStatistics(mockClock.Now())
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, uint(2), online)
|
|
assert.Equal(t, uint(1), offline)
|
|
assert.Equal(t, uint(1), mia)
|
|
}
|
|
|
|
func testMarkHostSeen(t *testing.T, ds kolide.Datastore) {
|
|
mockClock := clock.NewMockClock()
|
|
|
|
anHourAgo := mockClock.Now().Add(-1 * time.Hour).UTC()
|
|
aDayAgo := mockClock.Now().Add(-24 * time.Hour).UTC()
|
|
|
|
h1, err := ds.NewHost(&kolide.Host{
|
|
ID: 1,
|
|
OsqueryHostID: "1",
|
|
UUID: "1",
|
|
NodeKey: "1",
|
|
DetailUpdateTime: aDayAgo,
|
|
SeenTime: aDayAgo,
|
|
})
|
|
assert.Nil(t, err)
|
|
|
|
{
|
|
h1Verify, err := ds.Host(1)
|
|
assert.Nil(t, err)
|
|
require.NotNil(t, h1Verify)
|
|
assert.WithinDuration(t, aDayAgo, h1Verify.SeenTime, time.Second)
|
|
}
|
|
|
|
err = ds.MarkHostSeen(h1, anHourAgo)
|
|
assert.Nil(t, err)
|
|
|
|
{
|
|
h1Verify, err := ds.Host(1)
|
|
assert.Nil(t, err)
|
|
require.NotNil(t, h1Verify)
|
|
assert.WithinDuration(t, anHourAgo, h1Verify.SeenTime, time.Second)
|
|
}
|
|
}
|