diff --git a/cli/events.go b/cli/events.go new file mode 100644 index 0000000000..553ab0f1ca --- /dev/null +++ b/cli/events.go @@ -0,0 +1,270 @@ +// Copyright (c) 2014,2015,2016,2017 Docker, Inc. +// Copyright (c) 2018 Huawei Corporation. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package main + +import ( + "encoding/json" + "fmt" + "os" + "sync" + "time" + + vc "github.com/kata-containers/runtime/virtcontainers" + + "github.com/sirupsen/logrus" + "github.com/urfave/cli" +) + +type event struct { + Type string `json:"type"` + ID string `json:"id"` + Data interface{} `json:"data,omitempty"` +} + +// stats is the runc specific stats structure for stability when encoding and decoding stats. +type stats struct { + CPU cpu `json:"cpu"` + Memory memory `json:"memory"` + Pids pids `json:"pids"` + Blkio blkio `json:"blkio"` + Hugetlb map[string]hugetlb `json:"hugetlb"` + IntelRdt intelRdt `json:"intel_rdt"` +} + +type hugetlb struct { + Usage uint64 `json:"usage,omitempty"` + Max uint64 `json:"max,omitempty"` + Failcnt uint64 `json:"failcnt"` +} + +type blkioEntry struct { + Major uint64 `json:"major,omitempty"` + Minor uint64 `json:"minor,omitempty"` + Op string `json:"op,omitempty"` + Value uint64 `json:"value,omitempty"` +} + +type blkio struct { + IoServiceBytesRecursive []blkioEntry `json:"ioServiceBytesRecursive,omitempty"` + IoServicedRecursive []blkioEntry `json:"ioServicedRecursive,omitempty"` + IoQueuedRecursive []blkioEntry `json:"ioQueueRecursive,omitempty"` + IoServiceTimeRecursive []blkioEntry `json:"ioServiceTimeRecursive,omitempty"` + IoWaitTimeRecursive []blkioEntry `json:"ioWaitTimeRecursive,omitempty"` + IoMergedRecursive []blkioEntry `json:"ioMergedRecursive,omitempty"` + IoTimeRecursive []blkioEntry `json:"ioTimeRecursive,omitempty"` + SectorsRecursive []blkioEntry `json:"sectorsRecursive,omitempty"` +} + +type pids struct { + Current uint64 `json:"current,omitempty"` + Limit uint64 `json:"limit,omitempty"` +} + +type throttling struct { + Periods uint64 `json:"periods,omitempty"` + ThrottledPeriods uint64 `json:"throttledPeriods,omitempty"` + ThrottledTime uint64 `json:"throttledTime,omitempty"` +} + +type cpuUsage struct { + // Units: nanoseconds. + Total uint64 `json:"total,omitempty"` + Percpu []uint64 `json:"percpu,omitempty"` + Kernel uint64 `json:"kernel"` + User uint64 `json:"user"` +} + +type cpu struct { + Usage cpuUsage `json:"usage,omitempty"` + Throttling throttling `json:"throttling,omitempty"` +} + +type memoryEntry struct { + Limit uint64 `json:"limit"` + Usage uint64 `json:"usage,omitempty"` + Max uint64 `json:"max,omitempty"` + Failcnt uint64 `json:"failcnt"` +} + +type memory struct { + Cache uint64 `json:"cache,omitempty"` + Usage memoryEntry `json:"usage,omitempty"` + Swap memoryEntry `json:"swap,omitempty"` + Kernel memoryEntry `json:"kernel,omitempty"` + KernelTCP memoryEntry `json:"kernelTCP,omitempty"` + Raw map[string]uint64 `json:"raw,omitempty"` +} + +type l3CacheInfo struct { + CbmMask string `json:"cbm_mask,omitempty"` + MinCbmBits uint64 `json:"min_cbm_bits,omitempty"` + NumClosids uint64 `json:"num_closids,omitempty"` +} + +type intelRdt struct { + // The read-only L3 cache information + L3CacheInfo *l3CacheInfo `json:"l3_cache_info,omitempty"` + + // The read-only L3 cache schema in root + L3CacheSchemaRoot string `json:"l3_cache_schema_root,omitempty"` + + // The L3 cache schema in 'container_id' group + L3CacheSchema string `json:"l3_cache_schema,omitempty"` +} + +var eventsCLICommand = cli.Command{ + Name: "events", + Usage: "display container events such as OOM notifications, cpu, memory, and IO usage statistics", + ArgsUsage: ` + +Where "" is the name for the instance of the container.`, + Description: `The events command displays information about the container. By default the +information is displayed once every 5 seconds.`, + Flags: []cli.Flag{ + cli.DurationFlag{ + Name: "interval", + Value: 5 * time.Second, + Usage: "set the stats collection interval", + }, + cli.BoolFlag{ + Name: "stats", + Usage: "display the container's stats then exit", + }, + }, + Action: func(context *cli.Context) error { + containerID := context.Args().First() + if containerID == "" { + return fmt.Errorf("container id cannot be empty") + } + + duration := context.Duration("interval") + if duration <= 0 { + return fmt.Errorf("duration interval must be greater than 0") + } + + status, sandboxID, err := getExistingContainerInfo(containerID) + if err != nil { + return err + } + + if status.State.State == vc.StateStopped { + return fmt.Errorf("container with id %s is not running", status.ID) + } + + var ( + events = make(chan *event, 1024) + group = &sync.WaitGroup{} + ) + group.Add(1) + + go func() { + defer group.Done() + enc := json.NewEncoder(os.Stdout) + for e := range events { + if err := enc.Encode(e); err != nil { + logrus.Error(err) + } + } + }() + + if context.Bool("stats") { + s, err := vci.StatsContainer(sandboxID, containerID) + if err != nil { + return err + } + events <- &event{Type: "stats", ID: status.ID, Data: convertVirtcontainerStats(&s)} + close(events) + group.Wait() + return nil + } + + go func() { + for range time.Tick(context.Duration("interval")) { + s, err := vci.StatsContainer(sandboxID, containerID) + if err != nil { + logrus.Error(err) + continue + } + events <- &event{Type: "stats", ID: status.ID, Data: convertVirtcontainerStats(&s)} + } + }() + + group.Wait() + return nil + }, +} + +func convertVirtcontainerStats(containerStats *vc.ContainerStats) *stats { + cg := containerStats.CgroupStats + if cg == nil { + return nil + } + var s stats + s.Pids.Current = cg.PidsStats.Current + s.Pids.Limit = cg.PidsStats.Limit + + s.CPU.Usage.Kernel = cg.CPUStats.CPUUsage.UsageInKernelmode + s.CPU.Usage.User = cg.CPUStats.CPUUsage.UsageInUsermode + s.CPU.Usage.Total = cg.CPUStats.CPUUsage.TotalUsage + s.CPU.Usage.Percpu = cg.CPUStats.CPUUsage.PercpuUsage + s.CPU.Throttling.Periods = cg.CPUStats.ThrottlingData.Periods + s.CPU.Throttling.ThrottledPeriods = cg.CPUStats.ThrottlingData.ThrottledPeriods + s.CPU.Throttling.ThrottledTime = cg.CPUStats.ThrottlingData.ThrottledTime + + s.Memory.Cache = cg.MemoryStats.Cache + s.Memory.Kernel = convertMemoryEntry(cg.MemoryStats.KernelUsage) + s.Memory.KernelTCP = convertMemoryEntry(cg.MemoryStats.KernelTCPUsage) + s.Memory.Swap = convertMemoryEntry(cg.MemoryStats.SwapUsage) + s.Memory.Usage = convertMemoryEntry(cg.MemoryStats.Usage) + s.Memory.Raw = cg.MemoryStats.Stats + + s.Blkio.IoServiceBytesRecursive = convertBlkioEntry(cg.BlkioStats.IoServiceBytesRecursive) + s.Blkio.IoServicedRecursive = convertBlkioEntry(cg.BlkioStats.IoServicedRecursive) + s.Blkio.IoQueuedRecursive = convertBlkioEntry(cg.BlkioStats.IoQueuedRecursive) + s.Blkio.IoServiceTimeRecursive = convertBlkioEntry(cg.BlkioStats.IoServiceTimeRecursive) + s.Blkio.IoWaitTimeRecursive = convertBlkioEntry(cg.BlkioStats.IoWaitTimeRecursive) + s.Blkio.IoMergedRecursive = convertBlkioEntry(cg.BlkioStats.IoMergedRecursive) + s.Blkio.IoTimeRecursive = convertBlkioEntry(cg.BlkioStats.IoTimeRecursive) + s.Blkio.SectorsRecursive = convertBlkioEntry(cg.BlkioStats.SectorsRecursive) + + s.Hugetlb = make(map[string]hugetlb) + for k, v := range cg.HugetlbStats { + s.Hugetlb[k] = convertHugtlb(v) + } + + return &s +} + +func convertHugtlb(c vc.HugetlbStats) hugetlb { + return hugetlb{ + Usage: c.Usage, + Max: c.MaxUsage, + Failcnt: c.Failcnt, + } +} + +func convertMemoryEntry(c vc.MemoryData) memoryEntry { + return memoryEntry{ + Limit: c.Limit, + Usage: c.Usage, + Max: c.MaxUsage, + Failcnt: c.Failcnt, + } +} + +func convertBlkioEntry(c []vc.BlkioStatEntry) []blkioEntry { + var out []blkioEntry + for _, e := range c { + out = append(out, blkioEntry{ + Major: e.Major, + Minor: e.Minor, + Op: e.Op, + Value: e.Value, + }) + } + return out +} diff --git a/cli/events_test.go b/cli/events_test.go new file mode 100644 index 0000000000..315b268606 --- /dev/null +++ b/cli/events_test.go @@ -0,0 +1,141 @@ +// Copyright (c) 2018 Huawei Corporation. +// +// SPDX-License-Identifier: Apache-2.0 +// + +package main + +import ( + "flag" + "os" + "testing" + "time" + + vc "github.com/kata-containers/runtime/virtcontainers" + vcAnnotations "github.com/kata-containers/runtime/virtcontainers/pkg/annotations" + "github.com/kata-containers/runtime/virtcontainers/pkg/vcmock" + "github.com/stretchr/testify/assert" + "github.com/urfave/cli" +) + +func TestEventsCliAction(t *testing.T) { + assert := assert.New(t) + + // get Action function + actionFunc, ok := eventsCLICommand.Action.(func(ctx *cli.Context) error) + flagSet := flag.NewFlagSet("events", flag.ContinueOnError) + + // create a new fake context + ctx := cli.NewContext(&cli.App{}, flagSet, nil) + assert.True(ok) + + err := actionFunc(ctx) + assert.Error(err, "Missing container ID") +} + +func TestEventsCLIFailure(t *testing.T) { + assert := assert.New(t) + + flagSet := flag.NewFlagSet("events", flag.ContinueOnError) + ctx := cli.NewContext(&cli.App{}, flagSet, nil) + + actionFunc, ok := eventsCLICommand.Action.(func(ctx *cli.Context) error) + assert.True(ok) + + // missing container ID + err := actionFunc(ctx) + assert.Error(err) + + // interval is negative + flagSet.Parse([]string{testContainerID}) + flagSet.Duration("interval", (-1)*time.Second, "") + ctx = cli.NewContext(&cli.App{}, flagSet, nil) + err = actionFunc(ctx) + assert.Error(err) + + // interval is zero + flagSet = flag.NewFlagSet("events", flag.ContinueOnError) + flagSet.Parse([]string{testContainerID}) + flagSet.Duration("interval", 0*time.Second, "") + ctx = cli.NewContext(&cli.App{}, flagSet, nil) + err = actionFunc(ctx) + assert.Error(err) + + // not running + sandbox := &vcmock.Sandbox{ + MockID: testContainerID, + } + + sandbox.MockContainers = []*vcmock.Container{ + { + MockID: sandbox.ID(), + MockSandbox: sandbox, + }, + } + + testingImpl.StatusContainerFunc = func(sandboxID, containerID string) (vc.ContainerStatus, error) { + return vc.ContainerStatus{ + ID: sandbox.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + }, nil + } + + defer func() { + testingImpl.StatusContainerFunc = nil + }() + err = actionFunc(ctx) + assert.Error(err) +} + +func TestEventsCLISuccessful(t *testing.T) { + assert := assert.New(t) + + sandbox := &vcmock.Sandbox{ + MockID: testContainerID, + } + + sandbox.MockContainers = []*vcmock.Container{ + { + MockID: sandbox.ID(), + MockSandbox: sandbox, + }, + } + + testingImpl.StatusContainerFunc = func(sandboxID, containerID string) (vc.ContainerStatus, error) { + return vc.ContainerStatus{ + ID: sandbox.ID(), + Annotations: map[string]string{ + vcAnnotations.ContainerTypeKey: string(vc.PodContainer), + }, + State: vc.State{ + State: vc.StateRunning, + }, + }, nil + } + + testingImpl.StatsContainerFunc = func(sandboxID, containerID string) (vc.ContainerStats, error) { + return vc.ContainerStats{}, nil + } + + defer func() { + testingImpl.StatusContainerFunc = nil + testingImpl.StatsContainerFunc = nil + }() + + path, err := createTempContainerIDMapping(sandbox.ID(), sandbox.ID()) + assert.NoError(err) + defer os.RemoveAll(path) + + actionFunc, ok := eventsCLICommand.Action.(func(ctx *cli.Context) error) + assert.True(ok) + + flagSet := flag.NewFlagSet("events", flag.ContinueOnError) + flagSet.Parse([]string{testContainerID}) + flagSet.Duration("interval", 5*time.Second, "") + flagSet.Bool("stats", true, "") + ctx := cli.NewContext(&cli.App{}, flagSet, nil) + err = actionFunc(ctx) + assert.NoError(err) +} diff --git a/cli/main.go b/cli/main.go index b84d6fff19..e5b7f61e25 100644 --- a/cli/main.go +++ b/cli/main.go @@ -117,6 +117,7 @@ var runtimeCommands = []cli.Command{ startCLICommand, stateCLICommand, updateCLICommand, + eventsCLICommand, versionCLICommand, // Kata Containers specific extensions diff --git a/virtcontainers/agent.go b/virtcontainers/agent.go index a787ade7b0..e1e0b406cd 100644 --- a/virtcontainers/agent.go +++ b/virtcontainers/agent.go @@ -194,4 +194,7 @@ type agent interface { // This function should be called after hot adding vCPUs or Memory. // cpus specifies the number of CPUs that were added and the agent should online onlineCPUMem(cpus uint32) error + + // statsContainer will tell the agent to get stats from a container related to a Sandbox + statsContainer(sandbox *Sandbox, c Container) (*ContainerStats, error) } diff --git a/virtcontainers/api.go b/virtcontainers/api.go index 5cfb5a23f1..1d439d8cf0 100644 --- a/virtcontainers/api.go +++ b/virtcontainers/api.go @@ -628,3 +628,28 @@ func UpdateContainer(sandboxID, containerID string, resources specs.LinuxResourc return s.UpdateContainer(containerID, resources) } + +// StatsContainer is the virtcontainers container stats entry point. +// StatsContainer returns a detailed container stats. +func StatsContainer(sandboxID, containerID string) (ContainerStats, error) { + if sandboxID == "" { + return ContainerStats{}, errNeedSandboxID + } + + if containerID == "" { + return ContainerStats{}, errNeedContainerID + } + lockFile, err := rLockSandbox(sandboxID) + if err != nil { + return ContainerStats{}, err + } + + defer unlockSandbox(lockFile) + + s, err := fetchSandbox(sandboxID) + if err != nil { + return ContainerStats{}, err + } + + return s.StatsContainer(containerID) +} diff --git a/virtcontainers/api_test.go b/virtcontainers/api_test.go index 0a009f53e7..502302ab9c 100644 --- a/virtcontainers/api_test.go +++ b/virtcontainers/api_test.go @@ -1934,6 +1934,74 @@ func TestStatusContainerFailing(t *testing.T) { } } +func TestStatsContainerFailing(t *testing.T) { + cleanUp() + + contID := "100" + config := newTestSandboxConfigNoop() + + p, err := CreateSandbox(config) + if p == nil || err != nil { + t.Fatal(err) + } + + pImpl, ok := p.(*Sandbox) + assert.True(t, ok) + + os.RemoveAll(pImpl.configPath) + globalSandboxList.removeSandbox(p.ID()) + + _, err = StatsContainer(p.ID(), contID) + if err == nil { + t.Fatal() + } +} + +func TestStatsContainer(t *testing.T) { + cleanUp() + + assert := assert.New(t) + contID := "100" + + _, err := StatsContainer("", "") + assert.Error(err) + + _, err = StatsContainer("abc", "") + assert.Error(err) + + _, err = StatsContainer("abc", "abc") + assert.Error(err) + + config := newTestSandboxConfigNoop() + p, err := CreateSandbox(config) + assert.NoError(err) + assert.NotNil(p) + + p, err = StartSandbox(p.ID()) + if p == nil || err != nil { + t.Fatal(err) + } + + pImpl, ok := p.(*Sandbox) + assert.True(ok) + defer os.RemoveAll(pImpl.configPath) + + contConfig := newTestContainerConfigNoop(contID) + _, c, err := CreateContainer(p.ID(), contConfig) + assert.NoError(err) + assert.NotNil(c) + + _, err = StatsContainer(pImpl.id, "xyz") + assert.Error(err) + + _, err = StatsContainer("xyz", contID) + assert.Error(err) + + stats, err := StatsContainer(pImpl.id, contID) + assert.NoError(err) + assert.Equal(stats, ContainerStats{}) +} + func TestProcessListContainer(t *testing.T) { cleanUp() diff --git a/virtcontainers/container.go b/virtcontainers/container.go index f96cc61d37..bbab96fcbb 100644 --- a/virtcontainers/container.go +++ b/virtcontainers/container.go @@ -1,5 +1,6 @@ +// +build linux // Copyright (c) 2016 Intel Corporation -// +// Copyright (c) 2014,2015,2016,2017 Docker, Inc. // SPDX-License-Identifier: Apache-2.0 // @@ -55,6 +56,119 @@ type ContainerStatus struct { Annotations map[string]string } +// ThrottlingData gather the date related to container cpu throttling. +type ThrottlingData struct { + // Number of periods with throttling active + Periods uint64 `json:"periods,omitempty"` + // Number of periods when the container hit its throttling limit. + ThrottledPeriods uint64 `json:"throttled_periods,omitempty"` + // Aggregate time the container was throttled for in nanoseconds. + ThrottledTime uint64 `json:"throttled_time,omitempty"` +} + +// CPUUsage denotes the usage of a CPU. +// All CPU stats are aggregate since container inception. +type CPUUsage struct { + // Total CPU time consumed. + // Units: nanoseconds. + TotalUsage uint64 `json:"total_usage,omitempty"` + // Total CPU time consumed per core. + // Units: nanoseconds. + PercpuUsage []uint64 `json:"percpu_usage,omitempty"` + // Time spent by tasks of the cgroup in kernel mode. + // Units: nanoseconds. + UsageInKernelmode uint64 `json:"usage_in_kernelmode"` + // Time spent by tasks of the cgroup in user mode. + // Units: nanoseconds. + UsageInUsermode uint64 `json:"usage_in_usermode"` +} + +// CPUStats describes the cpu stats +type CPUStats struct { + CPUUsage CPUUsage `json:"cpu_usage,omitempty"` + ThrottlingData ThrottlingData `json:"throttling_data,omitempty"` +} + +// MemoryData gather the data related to memory +type MemoryData struct { + Usage uint64 `json:"usage,omitempty"` + MaxUsage uint64 `json:"max_usage,omitempty"` + Failcnt uint64 `json:"failcnt"` + Limit uint64 `json:"limit"` +} + +// MemoryStats describes the memory stats +type MemoryStats struct { + // memory used for cache + Cache uint64 `json:"cache,omitempty"` + // usage of memory + Usage MemoryData `json:"usage,omitempty"` + // usage of memory swap + SwapUsage MemoryData `json:"swap_usage,omitempty"` + // usage of kernel memory + KernelUsage MemoryData `json:"kernel_usage,omitempty"` + // usage of kernel TCP memory + KernelTCPUsage MemoryData `json:"kernel_tcp_usage,omitempty"` + // if true, memory usage is accounted for throughout a hierarchy of cgroups. + UseHierarchy bool `json:"use_hierarchy"` + + Stats map[string]uint64 `json:"stats,omitempty"` +} + +// PidsStats describes the pids stats +type PidsStats struct { + // number of pids in the cgroup + Current uint64 `json:"current,omitempty"` + // active pids hard limit + Limit uint64 `json:"limit,omitempty"` +} + +// BlkioStatEntry gather date related to a block device +type BlkioStatEntry struct { + Major uint64 `json:"major,omitempty"` + Minor uint64 `json:"minor,omitempty"` + Op string `json:"op,omitempty"` + Value uint64 `json:"value,omitempty"` +} + +// BlkioStats describes block io stats +type BlkioStats struct { + // number of bytes tranferred to and from the block device + IoServiceBytesRecursive []BlkioStatEntry `json:"io_service_bytes_recursive,omitempty"` + IoServicedRecursive []BlkioStatEntry `json:"io_serviced_recursive,omitempty"` + IoQueuedRecursive []BlkioStatEntry `json:"io_queue_recursive,omitempty"` + IoServiceTimeRecursive []BlkioStatEntry `json:"io_service_time_recursive,omitempty"` + IoWaitTimeRecursive []BlkioStatEntry `json:"io_wait_time_recursive,omitempty"` + IoMergedRecursive []BlkioStatEntry `json:"io_merged_recursive,omitempty"` + IoTimeRecursive []BlkioStatEntry `json:"io_time_recursive,omitempty"` + SectorsRecursive []BlkioStatEntry `json:"sectors_recursive,omitempty"` +} + +// HugetlbStats describes hugetable memory stats +type HugetlbStats struct { + // current res_counter usage for hugetlb + Usage uint64 `json:"usage,omitempty"` + // maximum usage ever recorded. + MaxUsage uint64 `json:"max_usage,omitempty"` + // number of times hugetlb usage allocation failure. + Failcnt uint64 `json:"failcnt"` +} + +// CgroupStats describes all cgroup subsystem stats +type CgroupStats struct { + CPUStats CPUStats `json:"cpu_stats,omitempty"` + MemoryStats MemoryStats `json:"memory_stats,omitempty"` + PidsStats PidsStats `json:"pids_stats,omitempty"` + BlkioStats BlkioStats `json:"blkio_stats,omitempty"` + // the map is in the format "size of hugepage: stats of the hugepage" + HugetlbStats map[string]HugetlbStats `json:"hugetlb_stats,omitempty"` +} + +// ContainerStats describes a container stats. +type ContainerStats struct { + CgroupStats *CgroupStats +} + // ContainerResources describes container resources type ContainerResources struct { // CPUQuota specifies the total amount of time in microseconds @@ -788,6 +902,13 @@ func (c *Container) processList(options ProcessListOptions) (ProcessList, error) return c.sandbox.agent.processListContainer(c.sandbox, *c, options) } +func (c *Container) stats() (*ContainerStats, error) { + if err := c.checkSandboxRunning("stats"); err != nil { + return nil, err + } + return c.sandbox.agent.statsContainer(c.sandbox, *c) +} + func (c *Container) update(resources specs.LinuxResources) error { if err := c.checkSandboxRunning("update"); err != nil { return err diff --git a/virtcontainers/hyperstart_agent.go b/virtcontainers/hyperstart_agent.go index 648acd506e..c62417c0b3 100644 --- a/virtcontainers/hyperstart_agent.go +++ b/virtcontainers/hyperstart_agent.go @@ -609,6 +609,11 @@ func (h *hyper) processListContainer(sandbox *Sandbox, c Container, options Proc return h.processListOneContainer(sandbox.id, c.id, options) } +// statsContainer is the hyperstart agent Container stats implementation. It does nothing. +func (h *hyper) statsContainer(sandbox *Sandbox, c Container) (*ContainerStats, error) { + return &ContainerStats{}, nil +} + func (h *hyper) updateContainer(sandbox *Sandbox, c Container, resources specs.LinuxResources) error { // hyperstart-agent does not support update return nil diff --git a/virtcontainers/implementation.go b/virtcontainers/implementation.go index dba32c5063..81ed6db32f 100644 --- a/virtcontainers/implementation.go +++ b/virtcontainers/implementation.go @@ -106,6 +106,11 @@ func (impl *VCImpl) StatusContainer(sandboxID, containerID string) (ContainerSta return StatusContainer(sandboxID, containerID) } +// StatsContainer implements the VC function of the same name. +func (impl *VCImpl) StatsContainer(sandboxID, containerID string) (ContainerStats, error) { + return StatsContainer(sandboxID, containerID) +} + // KillContainer implements the VC function of the same name. func (impl *VCImpl) KillContainer(sandboxID, containerID string, signal syscall.Signal, all bool) error { return KillContainer(sandboxID, containerID, signal, all) diff --git a/virtcontainers/interfaces.go b/virtcontainers/interfaces.go index c86736232f..f61fd1de07 100644 --- a/virtcontainers/interfaces.go +++ b/virtcontainers/interfaces.go @@ -34,6 +34,7 @@ type VC interface { KillContainer(sandboxID, containerID string, signal syscall.Signal, all bool) error StartContainer(sandboxID, containerID string) (VCContainer, error) StatusContainer(sandboxID, containerID string) (ContainerStatus, error) + StatsContainer(sandboxID, containerID string) (ContainerStats, error) StopContainer(sandboxID, containerID string) (VCContainer, error) ProcessListContainer(sandboxID, containerID string, options ProcessListOptions) (ProcessList, error) UpdateContainer(sandboxID, containerID string, resources specs.LinuxResources) error @@ -59,6 +60,7 @@ type VCSandbox interface { DeleteContainer(contID string) (VCContainer, error) StartContainer(containerID string) (VCContainer, error) StatusContainer(containerID string) (ContainerStatus, error) + StatsContainer(containerID string) (ContainerStats, error) EnterContainer(containerID string, cmd Cmd) (VCContainer, *Process, error) UpdateContainer(containerID string, resources specs.LinuxResources) error WaitProcess(containerID, processID string) (int32, error) diff --git a/virtcontainers/kata_agent.go b/virtcontainers/kata_agent.go index 4deb2c6f3d..70440a2493 100644 --- a/virtcontainers/kata_agent.go +++ b/virtcontainers/kata_agent.go @@ -941,6 +941,38 @@ func (k *kataAgent) onlineCPUMem(cpus uint32) error { return err } +func (k *kataAgent) statsContainer(sandbox *Sandbox, c Container) (*ContainerStats, error) { + req := &grpc.StatsContainerRequest{ + ContainerId: c.id, + } + + returnStats, err := k.sendReq(req) + + if err != nil { + return nil, err + } + + stats, ok := returnStats.(*grpc.StatsContainerResponse) + if !ok { + return nil, fmt.Errorf("irregular response container stats") + } + + data, err := json.Marshal(stats.CgroupStats) + if err != nil { + return nil, err + } + + var cgroupStats CgroupStats + err = json.Unmarshal(data, &cgroupStats) + if err != nil { + return nil, err + } + containerStats := &ContainerStats{ + CgroupStats: &cgroupStats, + } + return containerStats, nil +} + func (k *kataAgent) connect() error { if k.client != nil { return nil @@ -1069,6 +1101,9 @@ func (k *kataAgent) installReqFunc(c *kataclient.AgentClient) { k.reqHandlers["grpc.CloseStdinRequest"] = func(ctx context.Context, req interface{}, opts ...golangGrpc.CallOption) (interface{}, error) { return k.client.CloseStdin(ctx, req.(*grpc.CloseStdinRequest), opts...) } + k.reqHandlers["grpc.StatsContainerRequest"] = func(ctx context.Context, req interface{}, opts ...golangGrpc.CallOption) (interface{}, error) { + return k.client.StatsContainer(ctx, req.(*grpc.StatsContainerRequest), opts...) + } } func (k *kataAgent) sendReq(request interface{}) (interface{}, error) { diff --git a/virtcontainers/kata_agent_test.go b/virtcontainers/kata_agent_test.go index df87ffa73f..41ad8f15d9 100644 --- a/virtcontainers/kata_agent_test.go +++ b/virtcontainers/kata_agent_test.go @@ -199,6 +199,10 @@ func (p *gRPCProxy) OnlineCPUMem(ctx context.Context, req *pb.OnlineCPUMemReques return emptyResp, nil } +func (p *gRPCProxy) StatsContainer(ctx context.Context, req *pb.StatsContainerRequest) (*pb.StatsContainerResponse, error) { + return &pb.StatsContainerResponse{}, nil +} + func (p *gRPCProxy) Check(ctx context.Context, req *pb.CheckRequest) (*pb.HealthCheckResponse, error) { return &pb.HealthCheckResponse{}, nil } @@ -226,6 +230,7 @@ var reqList = []interface{}{ &pb.SignalProcessRequest{}, &pb.CheckRequest{}, &pb.WaitProcessRequest{}, + &pb.StatsContainerRequest{}, } func TestKataAgentSendReq(t *testing.T) { diff --git a/virtcontainers/noop_agent.go b/virtcontainers/noop_agent.go index e437e62c5b..07356f02e9 100644 --- a/virtcontainers/noop_agent.go +++ b/virtcontainers/noop_agent.go @@ -91,6 +91,11 @@ func (n *noopAgent) check() error { return nil } +// statsContainer is the Noop agent Container stats implementation. It does nothing. +func (n *noopAgent) statsContainer(sandbox *Sandbox, c Container) (*ContainerStats, error) { + return &ContainerStats{}, nil +} + // waitProcess is the Noop agent process waiter. It does nothing. func (n *noopAgent) waitProcess(c *Container, processID string) (int32, error) { return 0, nil diff --git a/virtcontainers/noop_agent_test.go b/virtcontainers/noop_agent_test.go index 703c05576a..cd158cbf10 100644 --- a/virtcontainers/noop_agent_test.go +++ b/virtcontainers/noop_agent_test.go @@ -117,3 +117,16 @@ func TestNoopAgentStopContainer(t *testing.T) { t.Fatal(err) } } + +func TestNoopAgentStatsContainer(t *testing.T) { + n := &noopAgent{} + sandbox, container, err := testCreateNoopContainer() + if err != nil { + t.Fatal(err) + } + defer cleanUp() + _, err = n.statsContainer(sandbox, *container) + if err != nil { + t.Fatal(err) + } +} diff --git a/virtcontainers/pkg/vcmock/mock.go b/virtcontainers/pkg/vcmock/mock.go index df9e0170f0..bda162e381 100644 --- a/virtcontainers/pkg/vcmock/mock.go +++ b/virtcontainers/pkg/vcmock/mock.go @@ -179,6 +179,15 @@ func (m *VCMock) StatusContainer(sandboxID, containerID string) (vc.ContainerSta return vc.ContainerStatus{}, fmt.Errorf("%s: %s (%+v): sandboxID: %v, containerID: %v", mockErrorPrefix, getSelf(), m, sandboxID, containerID) } +// StatsContainer implements the VC function of the same name. +func (m *VCMock) StatsContainer(sandboxID, containerID string) (vc.ContainerStats, error) { + if m.StatsContainerFunc != nil { + return m.StatsContainerFunc(sandboxID, containerID) + } + + return vc.ContainerStats{}, fmt.Errorf("%s: %s (%+v): sandboxID: %v, containerID: %v", mockErrorPrefix, getSelf(), m, sandboxID, containerID) +} + // KillContainer implements the VC function of the same name. func (m *VCMock) KillContainer(sandboxID, containerID string, signal syscall.Signal, all bool) error { if m.KillContainerFunc != nil { diff --git a/virtcontainers/pkg/vcmock/mock_test.go b/virtcontainers/pkg/vcmock/mock_test.go index e966b328e7..77537432fe 100644 --- a/virtcontainers/pkg/vcmock/mock_test.go +++ b/virtcontainers/pkg/vcmock/mock_test.go @@ -510,6 +510,33 @@ func TestVCMockStatusContainer(t *testing.T) { assert.True(IsMockError(err)) } +func TestVCMockStatsContainer(t *testing.T) { + assert := assert.New(t) + + m := &VCMock{} + assert.Nil(m.StatsContainerFunc) + + _, err := m.StatsContainer(testSandboxID, testContainerID) + + assert.Error(err) + assert.True(IsMockError(err)) + + m.StatsContainerFunc = func(sandboxID, containerID string) (vc.ContainerStats, error) { + return vc.ContainerStats{}, nil + } + + stats, err := m.StatsContainer(testSandboxID, testContainerID) + assert.NoError(err) + assert.Equal(stats, vc.ContainerStats{}) + + // reset + m.StatsContainerFunc = nil + + _, err = m.StatsContainer(testSandboxID, testContainerID) + assert.Error(err) + assert.True(IsMockError(err)) +} + func TestVCMockStopContainer(t *testing.T) { assert := assert.New(t) diff --git a/virtcontainers/pkg/vcmock/sandbox.go b/virtcontainers/pkg/vcmock/sandbox.go index 25a44c94f6..270958f744 100644 --- a/virtcontainers/pkg/vcmock/sandbox.go +++ b/virtcontainers/pkg/vcmock/sandbox.go @@ -94,6 +94,11 @@ func (p *Sandbox) StatusContainer(contID string) (vc.ContainerStatus, error) { return vc.ContainerStatus{}, nil } +// StatsContainer implements the VCSandbox function of the same name. +func (p *Sandbox) StatsContainer(contID string) (vc.ContainerStats, error) { + return vc.ContainerStats{}, nil +} + // Status implements the VCSandbox function of the same name. func (p *Sandbox) Status() vc.SandboxStatus { return vc.SandboxStatus{} diff --git a/virtcontainers/pkg/vcmock/types.go b/virtcontainers/pkg/vcmock/types.go index 36a6eb38ec..73226c50f8 100644 --- a/virtcontainers/pkg/vcmock/types.go +++ b/virtcontainers/pkg/vcmock/types.go @@ -37,16 +37,17 @@ type Container struct { type VCMock struct { SetLoggerFunc func(logger logrus.FieldLogger) - CreateSandboxFunc func(sandboxConfig vc.SandboxConfig) (vc.VCSandbox, error) - DeleteSandboxFunc func(sandboxID string) (vc.VCSandbox, error) - ListSandboxFunc func() ([]vc.SandboxStatus, error) - FetchSandboxFunc func(sandboxID string) (vc.VCSandbox, error) - PauseSandboxFunc func(sandboxID string) (vc.VCSandbox, error) - ResumeSandboxFunc func(sandboxID string) (vc.VCSandbox, error) - RunSandboxFunc func(sandboxConfig vc.SandboxConfig) (vc.VCSandbox, error) - StartSandboxFunc func(sandboxID string) (vc.VCSandbox, error) - StatusSandboxFunc func(sandboxID string) (vc.SandboxStatus, error) - StopSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + CreateSandboxFunc func(sandboxConfig vc.SandboxConfig) (vc.VCSandbox, error) + DeleteSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + ListSandboxFunc func() ([]vc.SandboxStatus, error) + FetchSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + PauseSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + ResumeSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + RunSandboxFunc func(sandboxConfig vc.SandboxConfig) (vc.VCSandbox, error) + StartSandboxFunc func(sandboxID string) (vc.VCSandbox, error) + StatusSandboxFunc func(sandboxID string) (vc.SandboxStatus, error) + StatsContainerFunc func(sandboxID, containerID string) (vc.ContainerStats, error) + StopSandboxFunc func(sandboxID string) (vc.VCSandbox, error) CreateContainerFunc func(sandboxID string, containerConfig vc.ContainerConfig) (vc.VCSandbox, vc.VCContainer, error) DeleteContainerFunc func(sandboxID, containerID string) (vc.VCContainer, error) diff --git a/virtcontainers/sandbox.go b/virtcontainers/sandbox.go index 823f4bbe4b..556dddb805 100644 --- a/virtcontainers/sandbox.go +++ b/virtcontainers/sandbox.go @@ -1101,6 +1101,21 @@ func (s *Sandbox) UpdateContainer(containerID string, resources specs.LinuxResou return c.update(resources) } +// StatsContainer return the stats of a running container +func (s *Sandbox) StatsContainer(containerID string) (ContainerStats, error) { + // Fetch the container. + c, err := s.findContainer(containerID) + if err != nil { + return ContainerStats{}, err + } + + stats, err := c.stats() + if err != nil { + return ContainerStats{}, err + } + return *stats, nil +} + // createContainers registers all containers to the proxy, create the // containers in the guest and starts one shim per container. func (s *Sandbox) createContainers() error {