mirror of
https://github.com/openrport/openrport.git
synced 2025-10-26 11:27:11 +00:00
Additional os fields (#251)
* Additional os fields for clients * Returned back vault * Fixed windows version * Fixing api failures * Minor documentation adjustments * Added client filtering test in client repo * Added tests for virtual info mapping, small code review fixes * Started documentation * Added documentation and refactored extraction of filters from get params * Unittest fixing * Fixing unittest 2 * api docs: fix os fields descriptions Co-authored-by: Mykola Terelia <mykola.terelia@gmail.com>
This commit is contained in:
parent
7aa480343e
commit
19a803a844
44
api-doc.yml
44
api-doc.yml
@ -328,6 +328,14 @@ paths:
|
||||
description: "Sort option `-<field>`(desc) or `<field>`(asc). `<field>` can be one of `'id', 'name', 'os', 'hostname', 'version'`. For example, `&sort=-name` or `&sort=hostname`, etc"
|
||||
required: false
|
||||
type: "string"
|
||||
- name: "filter"
|
||||
in: "query"
|
||||
description: "Filter option `filter[<field>]` or `filter[<field>,<field>] for or conditions`.\n
|
||||
`<field>` can be one of `'os_full_name', 'os_version', 'os_virtualization_system', 'os_virtualization_role',\n
|
||||
'cpu_family', 'cpu_model', 'cpu_model_name', 'num_cpus', 'timezone'`. For example, `&filter[os_full_name]=Ubuntu 20.04` or `filter[os_full_name]=Ubuntu 20.04,Ubuntu 18.04`, etc.\n
|
||||
Multiple filters are possible. You can also use wildcards for partial matches e.g. `filter[os_full_name]=Ubuntu*` will list all clients whose os_full_name starts with 'Ubuntu'."
|
||||
required: false
|
||||
type: "string"
|
||||
summary: "List all active and disconnected client connections. By default sorted by ID in asc order"
|
||||
description: ""
|
||||
produces:
|
||||
@ -1857,7 +1865,13 @@ definitions:
|
||||
description: "client name"
|
||||
os:
|
||||
type: "string"
|
||||
description: "client OS description"
|
||||
description: "long description of client OS"
|
||||
os_full_name:
|
||||
type: "string"
|
||||
description: "short description of client OS (e.g. Microsoft Windows Server 2016 Standard)"
|
||||
os_version:
|
||||
type: "string"
|
||||
description: "version info about client's OS e.g. 10.0.14393 Build 14393"
|
||||
os_arch:
|
||||
type: "string"
|
||||
description: "client cpu architecture (ex: 386, amd64)"
|
||||
@ -1867,9 +1881,33 @@ definitions:
|
||||
os_kernel:
|
||||
type: "string"
|
||||
description: "client OS kernel (ex: linux, windows)"
|
||||
os_virtualization_system:
|
||||
type: "string"
|
||||
description: "info about the VM where client is running e.g. KVM, LXC, HyperV, VMWare, Xen"
|
||||
os_virtualization_role:
|
||||
type: "string"
|
||||
description: "role of the client in the running VM e.g. host or guest"
|
||||
hostname:
|
||||
type: "string"
|
||||
description: "client hostname"
|
||||
cpu_family:
|
||||
type: "string"
|
||||
description: "client's processor family info"
|
||||
cpu_model:
|
||||
type: "string"
|
||||
description: "client's processor model info, e.g. 85"
|
||||
cpu_model_name:
|
||||
type: "string"
|
||||
description: "human readable name of the client's processor model, e.g. Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz"
|
||||
num_cpus:
|
||||
type: "integer"
|
||||
description: "Number of cpu cores in the client's machine"
|
||||
mem_total:
|
||||
type: "number"
|
||||
description: "Total memory in bytes"
|
||||
timezone:
|
||||
type: "string"
|
||||
description: "Client's timezone e.g. PDT (UTC-07:00)"
|
||||
ipv4:
|
||||
type: "array"
|
||||
items:
|
||||
@ -2353,7 +2391,7 @@ definitions:
|
||||
script:
|
||||
type: "string"
|
||||
description: "text of the script"
|
||||
sudo:
|
||||
is_sudo:
|
||||
type: "boolean"
|
||||
description: "if true, this script will be executed as a sudo user"
|
||||
ScriptInput:
|
||||
@ -2371,6 +2409,6 @@ definitions:
|
||||
cwd:
|
||||
type: "string"
|
||||
description: "current working directory, where the script should be executed"
|
||||
sudo:
|
||||
is_sudo:
|
||||
type: "boolean"
|
||||
description: "if true, this script will be executed as a sudo user"
|
||||
|
||||
@ -23,6 +23,8 @@ import (
|
||||
"github.com/cloudradar-monitoring/rport/share/comm"
|
||||
)
|
||||
|
||||
const UnknownValue = "unknown"
|
||||
|
||||
//Client represents a client instance
|
||||
type Client struct {
|
||||
*chshare.Logger
|
||||
@ -42,13 +44,14 @@ type Client struct {
|
||||
|
||||
//NewClient creates a new client instance
|
||||
func NewClient(config *Config) *Client {
|
||||
cmdExec := NewCmdExecutor()
|
||||
client := &Client{
|
||||
Logger: chshare.NewLogger("client", config.Logging.LogOutput, config.Logging.LogLevel),
|
||||
config: config,
|
||||
running: true,
|
||||
runningc: make(chan error, 1),
|
||||
cmdExec: NewCmdExecutor(),
|
||||
systemInfo: NewSystemInfo(),
|
||||
cmdExec: cmdExec,
|
||||
systemInfo: NewSystemInfo(cmdExec),
|
||||
}
|
||||
|
||||
client.sshConfig = &ssh.ClientConfig{
|
||||
@ -368,47 +371,106 @@ func (c *Client) connectionRequest(ctx context.Context) *chshare.ConnectionReque
|
||||
defer cancel()
|
||||
|
||||
connReq := &chshare.ConnectionRequest{
|
||||
Version: chshare.BuildVersion,
|
||||
ID: c.config.Client.ID,
|
||||
Name: c.config.Client.Name,
|
||||
OSArch: c.systemInfo.GoArch(),
|
||||
Tags: c.config.Client.Tags,
|
||||
Remotes: c.config.Client.remotes,
|
||||
ID: c.config.Client.ID,
|
||||
Name: c.config.Client.Name,
|
||||
Tags: c.config.Client.Tags,
|
||||
Remotes: c.config.Client.remotes,
|
||||
OS: UnknownValue,
|
||||
OSArch: c.systemInfo.GoArch(),
|
||||
OSKernel: UnknownValue,
|
||||
OSFamily: UnknownValue,
|
||||
OSVersion: UnknownValue,
|
||||
OSVirtualizationRole: UnknownValue,
|
||||
OSVirtualizationSystem: UnknownValue,
|
||||
Version: chshare.BuildVersion,
|
||||
Hostname: UnknownValue,
|
||||
CPUFamily: UnknownValue,
|
||||
CPUModel: UnknownValue,
|
||||
CPUModelName: UnknownValue,
|
||||
}
|
||||
|
||||
info, err := c.systemInfo.HostInfo(ctx)
|
||||
if err != nil {
|
||||
c.Logger.Errorf("Could not get os information: %v", err)
|
||||
connReq.OSKernel = "unknown"
|
||||
connReq.OSFamily = "unknown"
|
||||
} else {
|
||||
connReq.OSKernel = info.OS
|
||||
connReq.OSFamily = info.PlatformFamily
|
||||
}
|
||||
|
||||
connReq.OS, err = c.getOS(ctx, info)
|
||||
os, err := c.getOS(ctx, info)
|
||||
if err != nil {
|
||||
connReq.OS = "unknown"
|
||||
c.Logger.Errorf("Could not get os name: %v", err)
|
||||
} else {
|
||||
connReq.OS = os
|
||||
}
|
||||
|
||||
connReq.OSFullName = c.getOSFullName(info)
|
||||
if info != nil && info.PlatformVersion != "" {
|
||||
connReq.OSVersion = info.PlatformVersion
|
||||
}
|
||||
|
||||
oSVirtualizationSystem, oSVirtualizationRole, err := c.systemInfo.VirtualizationInfo(ctx, info)
|
||||
if err != nil {
|
||||
c.Logger.Errorf("Could not get OS Virtualization Info: %v", err)
|
||||
} else {
|
||||
connReq.OSVirtualizationSystem = oSVirtualizationSystem
|
||||
connReq.OSVirtualizationRole = oSVirtualizationRole
|
||||
}
|
||||
|
||||
connReq.IPv4, connReq.IPv6, err = c.localIPAddresses()
|
||||
if err != nil {
|
||||
c.Logger.Errorf("Could not get local ips: %v", err)
|
||||
}
|
||||
connReq.Hostname, err = c.systemInfo.Hostname()
|
||||
|
||||
hostname, err := c.systemInfo.Hostname()
|
||||
if err != nil {
|
||||
connReq.Hostname = "unknown"
|
||||
c.Logger.Errorf("Could not get hostname: %v", err)
|
||||
} else {
|
||||
connReq.Hostname = hostname
|
||||
}
|
||||
|
||||
cpuInfo, err := c.systemInfo.CPUInfo(ctx)
|
||||
|
||||
if err != nil {
|
||||
c.Logger.Errorf("Could not get cpu information: %v", err)
|
||||
}
|
||||
|
||||
if len(cpuInfo.CPUs) > 0 {
|
||||
connReq.CPUFamily = cpuInfo.CPUs[0].Family
|
||||
connReq.CPUModel = cpuInfo.CPUs[0].Model
|
||||
connReq.CPUModelName = cpuInfo.CPUs[0].ModelName
|
||||
}
|
||||
connReq.NumCPUs = cpuInfo.NumCores
|
||||
|
||||
memoryInfo, err := c.systemInfo.MemoryStats(ctx)
|
||||
if err != nil {
|
||||
c.Logger.Errorf("Could not get memory information: %v", err)
|
||||
} else if memoryInfo != nil {
|
||||
connReq.MemoryTotal = memoryInfo.Total
|
||||
}
|
||||
|
||||
connReq.Timezone = c.getTimezone()
|
||||
|
||||
return connReq
|
||||
}
|
||||
|
||||
func (c *Client) getOS(ctx context.Context, info *host.InfoStat) (string, error) {
|
||||
if info == nil {
|
||||
return "unknown", nil
|
||||
return UnknownValue, nil
|
||||
} else if info.OS == "windows" {
|
||||
return info.Platform + " " + info.PlatformVersion + " " + info.PlatformFamily, nil
|
||||
}
|
||||
return c.systemInfo.Uname(ctx)
|
||||
}
|
||||
|
||||
func (c *Client) getOSFullName(infoStat *host.InfoStat) string {
|
||||
if infoStat == nil {
|
||||
return UnknownValue
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s %s", strings.Title(strings.ToLower(infoStat.Platform)), infoStat.PlatformVersion)
|
||||
}
|
||||
|
||||
func (c *Client) getTimezone() string {
|
||||
return c.systemInfo.SystemTime().Format("MST (UTC-07:00)")
|
||||
}
|
||||
|
||||
@ -10,6 +10,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
|
||||
"github.com/shirou/gopsutil/host"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
@ -96,28 +99,57 @@ func TestConnectionRequest(t *testing.T) {
|
||||
{
|
||||
Name: "no errors",
|
||||
SystemInfo: &mockSystemInfo{
|
||||
ReturnHostname: "test-hostname",
|
||||
ReturnUname: "test-uname",
|
||||
ReturnUname: "test-uname",
|
||||
ReturnHostname: "test-hostname",
|
||||
ReturnHostnameError: nil,
|
||||
ReturnHostInfo: &host.InfoStat{
|
||||
OS: "test-os",
|
||||
PlatformFamily: "test-family",
|
||||
OS: "test-os",
|
||||
PlatformFamily: "test-family",
|
||||
Platform: "UBUNTU",
|
||||
PlatformVersion: "18.04",
|
||||
VirtualizationSystem: "KVM",
|
||||
VirtualizationRole: "guest",
|
||||
},
|
||||
ReturnInterfaceAddrs: interfaceAddrs,
|
||||
ReturnGoArch: "test-arch",
|
||||
ReturnCPUInfo: CPUInfo{
|
||||
CPUs: []cpu.InfoStat{
|
||||
{
|
||||
Family: "fam1",
|
||||
Model: "mod1",
|
||||
ModelName: "mod name 123",
|
||||
},
|
||||
},
|
||||
NumCores: 4,
|
||||
},
|
||||
ReturnMemoryStat: &mem.VirtualMemoryStat{
|
||||
Total: 100000,
|
||||
},
|
||||
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
|
||||
},
|
||||
ExpectedConnectionRequest: &chshare.ConnectionRequest{
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: "test-uname",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "test-os",
|
||||
Hostname: "test-hostname",
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
NumCPUs: 4,
|
||||
MemoryTotal: 100000,
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
OS: "test-uname",
|
||||
OSFullName: "Ubuntu 18.04",
|
||||
OSVersion: "18.04",
|
||||
OSVirtualizationSystem: "KVM",
|
||||
OSVirtualizationRole: "guest",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "test-os",
|
||||
Hostname: "test-hostname",
|
||||
CPUFamily: "fam1",
|
||||
CPUModel: "mod1",
|
||||
CPUModelName: "mod name 123",
|
||||
Timezone: "UTC (UTC+00:00)",
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
},
|
||||
}, {
|
||||
Name: "windows, no errors",
|
||||
@ -132,20 +164,38 @@ func TestConnectionRequest(t *testing.T) {
|
||||
},
|
||||
ReturnInterfaceAddrs: interfaceAddrs,
|
||||
ReturnGoArch: "test-arch",
|
||||
ReturnCPUInfo: CPUInfo{
|
||||
CPUs: []cpu.InfoStat{
|
||||
{
|
||||
Family: "cpufam1",
|
||||
Model: "cpumod1",
|
||||
ModelName: "cpumod_name1",
|
||||
},
|
||||
},
|
||||
NumCores: 2,
|
||||
},
|
||||
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
|
||||
},
|
||||
ExpectedConnectionRequest: &chshare.ConnectionRequest{
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: "test-platform 123 test-family",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "windows",
|
||||
Hostname: "test-hostname",
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: "test-platform 123 test-family",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "windows",
|
||||
Hostname: "test-hostname",
|
||||
OSFullName: "Test-Platform 123",
|
||||
OSVersion: "123",
|
||||
CPUFamily: "cpufam1",
|
||||
CPUModel: "cpumod1",
|
||||
CPUModelName: "cpumod_name1",
|
||||
Timezone: "UTC (UTC+00:00)",
|
||||
NumCPUs: 2,
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
},
|
||||
}, {
|
||||
Name: "all errors",
|
||||
@ -155,20 +205,28 @@ func TestConnectionRequest(t *testing.T) {
|
||||
ReturnHostInfoError: errors.New("test error"),
|
||||
ReturnInterfaceAddrsError: errors.New("test error"),
|
||||
ReturnGoArch: "test-arch",
|
||||
ReturnCPUInfoError: errors.New("test error"),
|
||||
ReturnMemoryError: errors.New("test error"),
|
||||
},
|
||||
ExpectedConnectionRequest: &chshare.ConnectionRequest{
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: "unknown",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "unknown",
|
||||
OSKernel: "unknown",
|
||||
Hostname: "unknown",
|
||||
IPv4: nil,
|
||||
IPv6: nil,
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: UnknownValue,
|
||||
OSArch: "test-arch",
|
||||
OSFamily: UnknownValue,
|
||||
OSKernel: UnknownValue,
|
||||
Hostname: UnknownValue,
|
||||
CPUFamily: UnknownValue,
|
||||
CPUModel: UnknownValue,
|
||||
CPUModelName: UnknownValue,
|
||||
OSFullName: UnknownValue,
|
||||
OSVersion: UnknownValue,
|
||||
Timezone: "UTC (UTC+00:00)",
|
||||
IPv4: nil,
|
||||
IPv6: nil,
|
||||
},
|
||||
}, {
|
||||
Name: "uname error",
|
||||
@ -183,20 +241,27 @@ func TestConnectionRequest(t *testing.T) {
|
||||
},
|
||||
ReturnInterfaceAddrs: interfaceAddrs,
|
||||
ReturnGoArch: "test-arch",
|
||||
ReturnSystemTime: time.Date(2001, 1, 1, 1, 0, 0, 0, time.UTC),
|
||||
},
|
||||
ExpectedConnectionRequest: &chshare.ConnectionRequest{
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: "unknown",
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "test-os",
|
||||
Hostname: "test-hostname",
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
Version: "0.0.0-src",
|
||||
ID: "test-client-id",
|
||||
Name: "test-name",
|
||||
OSVersion: "123",
|
||||
OSFullName: "Test-Platform 123",
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
Remotes: []*chshare.Remote{remote1, remote2},
|
||||
OS: UnknownValue,
|
||||
OSArch: "test-arch",
|
||||
OSFamily: "test-family",
|
||||
OSKernel: "test-os",
|
||||
Hostname: "test-hostname",
|
||||
Timezone: "UTC (UTC+00:00)",
|
||||
CPUFamily: UnknownValue,
|
||||
CPUModel: UnknownValue,
|
||||
CPUModelName: UnknownValue,
|
||||
IPv4: []string{"192.0.2.1", "192.0.2.2"},
|
||||
IPv6: []string{"2001:db8::1", "2001:db8::2"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -2,28 +2,45 @@ package chclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/cpu"
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
|
||||
"github.com/shirou/gopsutil/host"
|
||||
)
|
||||
|
||||
type CPUInfo struct {
|
||||
CPUs []cpu.InfoStat
|
||||
NumCores int
|
||||
}
|
||||
|
||||
type SystemInfo interface {
|
||||
Hostname() (string, error)
|
||||
HostInfo(context.Context) (*host.InfoStat, error)
|
||||
CPUInfo(ctx context.Context) (CPUInfo, error)
|
||||
MemoryStats(context.Context) (*mem.VirtualMemoryStat, error)
|
||||
Uname(context.Context) (string, error)
|
||||
InterfaceAddrs() ([]net.Addr, error)
|
||||
GoArch() string
|
||||
SystemTime() time.Time
|
||||
VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error)
|
||||
}
|
||||
|
||||
type realSystemInfo struct {
|
||||
cmdExec CmdExecutor
|
||||
}
|
||||
|
||||
func NewSystemInfo() SystemInfo {
|
||||
return &realSystemInfo{}
|
||||
func NewSystemInfo(cmdExec CmdExecutor) SystemInfo {
|
||||
return &realSystemInfo{
|
||||
cmdExec: cmdExec,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *realSystemInfo) Hostname() (string, error) {
|
||||
@ -48,3 +65,47 @@ func (s *realSystemInfo) InterfaceAddrs() ([]net.Addr, error) {
|
||||
}
|
||||
|
||||
func (s *realSystemInfo) GoArch() string { return runtime.GOARCH }
|
||||
|
||||
func (s *realSystemInfo) CPUInfo(ctx context.Context) (CPUInfo, error) {
|
||||
cpuInfo := CPUInfo{
|
||||
CPUs: []cpu.InfoStat{},
|
||||
}
|
||||
|
||||
errs := make([]string, 0, 2)
|
||||
cpuInfos, err1 := cpu.InfoWithContext(ctx)
|
||||
if err1 == nil {
|
||||
cpuInfo.CPUs = cpuInfos
|
||||
} else {
|
||||
errs = append(errs, err1.Error())
|
||||
}
|
||||
|
||||
cpuCount, err2 := cpu.CountsWithContext(ctx, true)
|
||||
if err2 == nil {
|
||||
cpuInfo.NumCores = cpuCount
|
||||
} else {
|
||||
errs = append(errs, err2.Error())
|
||||
}
|
||||
|
||||
return cpuInfo, errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
func (s *realSystemInfo) MemoryStats(ctx context.Context) (*mem.VirtualMemoryStat, error) {
|
||||
return mem.VirtualMemoryWithContext(ctx)
|
||||
}
|
||||
|
||||
func (s *realSystemInfo) SystemTime() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (s *realSystemInfo) VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error) {
|
||||
if infoStat != nil && infoStat.VirtualizationSystem != "" {
|
||||
return strings.ToUpper(infoStat.VirtualizationSystem), strings.ToLower(infoStat.VirtualizationRole), nil
|
||||
}
|
||||
|
||||
virtSystem, virtRole, err = s.virtualizationInfo(ctx)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return strings.ToUpper(virtSystem), strings.ToLower(virtRole), nil
|
||||
}
|
||||
|
||||
31
client/system_info_nix.go
Normal file
31
client/system_info_nix.go
Normal file
@ -0,0 +1,31 @@
|
||||
//+build !windows
|
||||
|
||||
package chclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
const devicesInfoPath = "/proc/bus/pci/devices"
|
||||
|
||||
func (s *realSystemInfo) virtualizationInfo(ctx context.Context) (virtSystem, virtRole string, err error) {
|
||||
_, err = os.Stat(devicesInfoPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return "", "", nil
|
||||
}
|
||||
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
fileContent, err := ioutil.ReadFile(devicesInfoPath)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
virtSystem, virtRole = getVirtInfoFromNixDevicesList(string(fileContent))
|
||||
|
||||
return virtSystem, virtRole, nil
|
||||
}
|
||||
@ -3,20 +3,29 @@ package chclient
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/shirou/gopsutil/mem"
|
||||
|
||||
"github.com/shirou/gopsutil/host"
|
||||
)
|
||||
|
||||
type mockSystemInfo struct {
|
||||
ReturnHostname string
|
||||
ReturnHostnameError error
|
||||
ReturnHostInfo *host.InfoStat
|
||||
ReturnHostInfoError error
|
||||
ReturnUname string
|
||||
ReturnUnameError error
|
||||
ReturnInterfaceAddrs []net.Addr
|
||||
ReturnInterfaceAddrsError error
|
||||
ReturnGoArch string
|
||||
ReturnHostname string
|
||||
ReturnHostnameError error
|
||||
ReturnHostInfo *host.InfoStat
|
||||
ReturnHostInfoError error
|
||||
ReturnCPUInfo CPUInfo
|
||||
ReturnCPUInfoError error
|
||||
ReturnMemoryStat *mem.VirtualMemoryStat
|
||||
ReturnMemoryError error
|
||||
ReturnUname string
|
||||
ReturnUnameError error
|
||||
ReturnInterfaceAddrs []net.Addr
|
||||
ReturnInterfaceAddrsError error
|
||||
ReturnGoArch string
|
||||
ReturnSystemTime time.Time
|
||||
ReturnVirtualizationInfoError error
|
||||
}
|
||||
|
||||
func (s *mockSystemInfo) Hostname() (string, error) {
|
||||
@ -38,3 +47,23 @@ func (s *mockSystemInfo) InterfaceAddrs() ([]net.Addr, error) {
|
||||
func (s *mockSystemInfo) GoArch() string {
|
||||
return s.ReturnGoArch
|
||||
}
|
||||
|
||||
func (s *mockSystemInfo) CPUInfo(ctx context.Context) (CPUInfo, error) {
|
||||
return s.ReturnCPUInfo, s.ReturnCPUInfoError
|
||||
}
|
||||
|
||||
func (s *mockSystemInfo) MemoryStats(ctx context.Context) (*mem.VirtualMemoryStat, error) {
|
||||
return s.ReturnMemoryStat, s.ReturnMemoryError
|
||||
}
|
||||
|
||||
func (s *mockSystemInfo) SystemTime() time.Time {
|
||||
return s.ReturnSystemTime
|
||||
}
|
||||
|
||||
func (s *mockSystemInfo) VirtualizationInfo(ctx context.Context, infoStat *host.InfoStat) (virtSystem, virtRole string, err error) {
|
||||
if infoStat == nil {
|
||||
return "", "", s.ReturnVirtualizationInfoError
|
||||
}
|
||||
|
||||
return infoStat.VirtualizationSystem, infoStat.VirtualizationRole, s.ReturnVirtualizationInfoError
|
||||
}
|
||||
|
||||
23
client/system_info_win.go
Normal file
23
client/system_info_win.go
Normal file
@ -0,0 +1,23 @@
|
||||
//+build windows
|
||||
|
||||
package chclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *realSystemInfo) virtualizationInfo(ctx context.Context) (virtSystem, virtRole string, err error) {
|
||||
cmd := s.cmdExec.New(ctx, "powerShell", "Get-Service", "", false)
|
||||
execRes, err := cmd.CombinedOutput()
|
||||
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
sysInfo := strings.TrimSpace(string(execRes))
|
||||
|
||||
virtSystem, virtRole = getVirtInfoFromPowershellServicesList(sysInfo)
|
||||
|
||||
return virtSystem, virtRole, nil
|
||||
}
|
||||
111
client/virt_info_provider.go
Normal file
111
client/virt_info_provider.go
Normal file
@ -0,0 +1,111 @@
|
||||
package chclient
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
VirtualSystemHyperV = "HyperV"
|
||||
VirtualSystemVMWare = "VMware"
|
||||
VirtualSystemKVM = "KVM"
|
||||
VirtualSystemXen = "Xen"
|
||||
VirtualSystemRoleGuest = "guest"
|
||||
VirtualSystemRoleHost = "host"
|
||||
)
|
||||
|
||||
/**
|
||||
This method interprets result of powerShell Get-Service output as following:
|
||||
Get-Service | findstr vmcompute detect if a machine is a HyperV host.
|
||||
This would result in "os_virtualization_system": "HyperV", "os_virtualization_role":"guest"
|
||||
Get-Service|findstr "Running.*vmicheartbeat" detects if a machine is a Hyper-V guest.
|
||||
Get-service|findstr "Running.*VMTools" detects if a machine is VMware guest.
|
||||
Get-service|findstr "Running.*QEMU-GA" detects if a machine is KVM guest.
|
||||
*/
|
||||
func getVirtInfoFromPowershellServicesList(rawServicesList string) (virtSystem, virtRole string) {
|
||||
if strings.Contains(rawServicesList, "vmcompute") {
|
||||
virtSystem = VirtualSystemHyperV
|
||||
virtRole = VirtualSystemRoleHost
|
||||
|
||||
return virtSystem, virtRole
|
||||
}
|
||||
|
||||
regexToVirtInfoMapping := map[string]struct {
|
||||
virtSystem string
|
||||
virtRole string
|
||||
}{
|
||||
`Running.*vmicheartbeat`: {
|
||||
virtSystem: VirtualSystemHyperV,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
`Running.*VMTools`: {
|
||||
virtSystem: VirtualSystemVMWare,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
`Running.*QEMU-GA`: {
|
||||
virtSystem: VirtualSystemKVM,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
}
|
||||
|
||||
for regexStr, virtInfo := range regexToVirtInfoMapping {
|
||||
rx := regexp.MustCompile(regexStr)
|
||||
if rx.MatchString(rawServicesList) {
|
||||
return virtInfo.virtSystem, virtInfo.virtRole
|
||||
}
|
||||
}
|
||||
|
||||
return UnknownValue, UnknownValue
|
||||
}
|
||||
|
||||
/**
|
||||
Parses the output of /proc/bus/pci/devices on nix systems as following:
|
||||
If grep -c hyperv_fb /proc/bus/pci/devices = 1 the Linux system is a HyperV guest.
|
||||
If grep -c vmwgfx /proc/bus/pci/devices = 1 the Linux system is a Vmware guest.
|
||||
If grep -c virtio-pci /proc/bus/pci/devices > 0 the Linux system is a KVM guest.
|
||||
If grep -c xen-platform-pci /proc/bus/pci/devices > 0 the Linux system is a Xen guest.
|
||||
*/
|
||||
func getVirtInfoFromNixDevicesList(rawDevicesList string) (virtSystem, virtRole string) {
|
||||
devicesInfoExpectations := []struct {
|
||||
expectedSubstr string
|
||||
expectedCount int
|
||||
virtSystem string
|
||||
virtRole string
|
||||
}{
|
||||
{
|
||||
expectedSubstr: "hyperv_fb",
|
||||
expectedCount: 1,
|
||||
virtSystem: VirtualSystemHyperV,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
{
|
||||
expectedSubstr: "vmwgfx",
|
||||
expectedCount: 1,
|
||||
virtSystem: VirtualSystemVMWare,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
{
|
||||
expectedSubstr: "virtio-pci",
|
||||
expectedCount: 0,
|
||||
virtSystem: VirtualSystemKVM,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
{
|
||||
expectedSubstr: "xen-platform-pci",
|
||||
expectedCount: 0,
|
||||
virtSystem: VirtualSystemXen,
|
||||
virtRole: VirtualSystemRoleGuest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, exp := range devicesInfoExpectations {
|
||||
if exp.expectedCount > 0 && strings.Count(rawDevicesList, exp.expectedSubstr) == exp.expectedCount {
|
||||
return exp.virtSystem, exp.virtRole
|
||||
}
|
||||
if exp.expectedCount == 0 && strings.Count(rawDevicesList, exp.expectedSubstr) > 0 {
|
||||
return exp.virtSystem, exp.virtRole
|
||||
}
|
||||
}
|
||||
|
||||
return UnknownValue, UnknownValue
|
||||
}
|
||||
219
client/virt_info_provider_test.go
Normal file
219
client/virt_info_provider_test.go
Normal file
@ -0,0 +1,219 @@
|
||||
package chclient
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type virtInfoTestCase struct {
|
||||
rawData string
|
||||
virtSystemToExpect string
|
||||
virtRoleToExpect string
|
||||
}
|
||||
|
||||
func TestGetVirtInfoFromPowershellServicesList(t *testing.T) {
|
||||
testCases := []virtInfoTestCase{
|
||||
{
|
||||
rawData: `
|
||||
Stopped PerfHost Performance Counter DLL Host
|
||||
Stopped PhoneSvc Phone Service
|
||||
Stopped PimIndexMainten... Contact Data_1400f45
|
||||
Stopped pla Performance Logs & Alerts
|
||||
Running PlugPlay Plug and Play
|
||||
Running PolicyAgent IPsec Policy Agent
|
||||
Running Power Power
|
||||
Stopped PrintNotify Printer Extensions and Notifications
|
||||
Running ProfSvc User Profile Service
|
||||
Stopped PhoneSvc Phone Service
|
||||
Stopped PimIndexMainten... Contact Data_1400f45
|
||||
Stopped pla Performance Logs & Alerts
|
||||
Running PlugPlay Plug and Play
|
||||
Running PolicyAgent IPsec Policy Agent
|
||||
Running Power Power
|
||||
Stopped PrintNotify Printer Extensions and Notifications
|
||||
Running ProfSvc User Profile Service
|
||||
Running Pulse Pulse by freeping.io
|
||||
Stopped QEMU Guest Agen... QEMU Guest Agent VSS Provider
|
||||
Running QEMU-GA QEMU Guest Agent
|
||||
Stopped WiaRpc Still Image Acquisition Events
|
||||
`,
|
||||
virtSystemToExpect: VirtualSystemKVM,
|
||||
virtRoleToExpect: VirtualSystemRoleGuest,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
Stopped PerfHost Performance Counter DLL Host
|
||||
Stopped PhoneSvc Phone Service
|
||||
Stopped PimIndexMainten... Contact Data_1400f45
|
||||
Stopped pla Performance Logs & Alerts
|
||||
Running PlugPlay Plug and Play
|
||||
Running PolicyAgent IPsec Policy Agent
|
||||
Running Power Power
|
||||
Stopped PrintNotify Printer Extensions and Notifications
|
||||
Running ProfSvc User Profile Service
|
||||
Running VMTools VMWare Service
|
||||
Stopped PimIndexMainten... Contact Data_1400f45
|
||||
Stopped pla Performance Logs & Alerts
|
||||
Running PlugPlay Plug and Play
|
||||
Running PolicyAgent IPsec Policy Agent
|
||||
Running Power Power
|
||||
Stopped PrintNotify Printer Extensions and Notifications
|
||||
Running ProfSvc User Profile Service
|
||||
Running Pulse Pulse by freeping.io
|
||||
Stopped WiaRpc Still Image Acquisition Events
|
||||
`,
|
||||
virtRoleToExpect: VirtualSystemRoleGuest,
|
||||
virtSystemToExpect: VirtualSystemVMWare,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
Stopped AppIDSvc Application Identity
|
||||
Stopped Appinfo Application Information
|
||||
Stopped AppMgmt Application Management
|
||||
Stopped AppReadiness App Readiness
|
||||
Stopped AppVClient Microsoft App-V Client
|
||||
Stopped AppXSvc AppX Deployment Service (AppXSVC)
|
||||
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
|
||||
Stopped Audiosrv Windows Audio
|
||||
Stopped AxInstSV ActiveX Installer (AxInstSV)
|
||||
Running BFE Base Filtering Engine
|
||||
Stopped BITS Background Intelligent Transfer Ser...
|
||||
Running BrokerInfrastru... Background Tasks Infrastructure Ser...
|
||||
Stopped Browser Computer Browser
|
||||
Stopped bthserv Bluetooth Support Service
|
||||
Running CDPSvc Connected Devices Platform Service
|
||||
Running CDPUserSvc_1400f45 CDPUserSvc_1400f45
|
||||
Running CertPropSvc Certificate Propagation
|
||||
Stopped ClipSVC Client License Service (ClipSVC)
|
||||
Running vmicheartbeat Hyper-V Service
|
||||
`,
|
||||
virtSystemToExpect: VirtualSystemHyperV,
|
||||
virtRoleToExpect: VirtualSystemRoleGuest,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
Stopped AppIDSvc Application Identity
|
||||
Stopped Appinfo Application Information
|
||||
Stopped AppMgmt Application Management
|
||||
Stopped AppReadiness App Readiness
|
||||
Stopped AppVClient Microsoft App-V Client
|
||||
Stopped AppXSvc AppX Deployment Service (AppXSVC)
|
||||
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
|
||||
Stopped Audiosrv Windows Audio
|
||||
Stopped AxInstSV ActiveX Installer (AxInstSV)
|
||||
Running BFE Base Filtering Engine
|
||||
Stopped BITS Background Intelligent Transfer Ser...
|
||||
Running BrokerInfrastru... Background Tasks Infrastructure Ser...
|
||||
Stopped Browser Computer Browser
|
||||
Stopped bthserv Bluetooth Support Service
|
||||
Running CDPSvc Connected Devices Platform Service
|
||||
Running CDPUserSvc_1400f45 CDPUserSvc_1400f45
|
||||
Running vmcompute Hyper-V Service
|
||||
`,
|
||||
virtRoleToExpect: VirtualSystemRoleHost,
|
||||
virtSystemToExpect: VirtualSystemHyperV,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
Stopped AppIDSvc Application Identity
|
||||
Stopped Appinfo Application Information
|
||||
Stopped AppMgmt Application Management
|
||||
Stopped AppReadiness App Readiness
|
||||
Stopped AppVClient Microsoft App-V Client
|
||||
Stopped AppXSvc AppX Deployment Service (AppXSVC)
|
||||
Stopped AudioEndpointBu... Windows Audio Endpoint Builder
|
||||
`,
|
||||
virtRoleToExpect: UnknownValue,
|
||||
virtSystemToExpect: UnknownValue,
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
virtSystemGiven, virtRoleGiven := getVirtInfoFromPowershellServicesList(testCase.rawData)
|
||||
|
||||
assert.Equal(t, testCase.virtRoleToExpect, virtRoleGiven)
|
||||
assert.Equal(t, testCase.virtSystemToExpect, virtSystemGiven)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVirtInfoFromNixDevicesList(t *testing.T) {
|
||||
cases := []struct {
|
||||
rawData string
|
||||
expectedVirtSystem string
|
||||
expectedVirtRole string
|
||||
}{
|
||||
{
|
||||
rawData: `
|
||||
0000 80861237 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0008 80867000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0009 80867010 0 1f0 3f6 170 376 e0a1 0 0 8 0 8 0 10 0 0 ata_piix
|
||||
000a 80867020 b 0 0 0 0 e041 0 0 0 0 0 0 20 0 0 uhci_hcd
|
||||
000b 80867113 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 piix4_smbus
|
||||
0010 12341111 0 fd000008 0 fea50000 0 0 0 c0002 1000000 0 1000 0 0 0 20000
|
||||
0018 1af41002 a e061 0 0 0 fe40000c 0 0 20 0 0 0 4000 0 0 virtio-pci
|
||||
0028 1af41004 a e001 fea51000 0 0 fe40400c 0 0 40 1000 0 0 4000 0 0 virtio-pci
|
||||
0090 1af41000 b e081 fea52000 0 0 fe40800c 0 fea00000 20 1000 0 0 4000 0 40000 virtio-pci
|
||||
00f0 1b360001 a fea53004 0 0 0 0 0 0 100 0 0 0 0 0 0
|
||||
00f8 1b360001 b fea54004 0 0 0 0 0 0 100 0 0 0 0 0 0
|
||||
`,
|
||||
expectedVirtRole: VirtualSystemRoleGuest,
|
||||
expectedVirtSystem: VirtualSystemKVM,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
b0a8 80869018 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0
|
||||
b0b0 80962018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
b0b4 80862020 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
b100 9005028f 20 f6100004 0 0 0 c002 0 0 8000 0 0 0 100 0 0 smartpqi
|
||||
b202 80861572 22 f400000c 0 0 f610000c 0 0 f6080100 1000000 0 0 8000 0 0 80000 i40e
|
||||
b204 80861572 22 f500000c 0 0 f600801c 0 0 0 1000000 0 0 8000 0 0 0 i40e
|
||||
`,
|
||||
expectedVirtRole: UnknownValue,
|
||||
expectedVirtSystem: UnknownValue,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
b0a8 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
b0b0 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
b0b4 70862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 hyperv_fb
|
||||
2100 60861572 22 f400000c 0 0 f600002c 0 0 f6082000 1000000 0 0 8000 0 0 80000 i40e
|
||||
a101 20861572 22 f500001c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
|
||||
`,
|
||||
expectedVirtRole: VirtualSystemRoleGuest,
|
||||
expectedVirtSystem: VirtualSystemHyperV,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
a0a9 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
r0b0 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
t0b4 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
w100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 vmwgfx
|
||||
q200 80861532 22 f400100c 0 0 f600000c 0 0 f87080000 1000000 0 0 8000 0 0 80000 i40e
|
||||
5201 80861572 22 f500000c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
|
||||
`,
|
||||
expectedVirtRole: VirtualSystemRoleGuest,
|
||||
expectedVirtSystem: VirtualSystemVMWare,
|
||||
},
|
||||
{
|
||||
rawData: `
|
||||
b1a8 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
t0b0 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
q0b4 80862018 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
100 9005028f 20 f6100004 0 0 0 c001 0 0 8000 0 0 0 100 0 0 xen-platform-pci
|
||||
21200 80861572 22 f400000c 0 0 f600000c 0 0 f6080000 1000000 0 0 8000 0 0 80000 i40e
|
||||
a201 80861572 22 f500000c 0 0 f600800c 0 0 0 1000000 0 0 8000 0 0 0 i40e
|
||||
`,
|
||||
expectedVirtRole: VirtualSystemRoleGuest,
|
||||
expectedVirtSystem: VirtualSystemXen,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
actualVirtSystem, actualVirtRole := getVirtInfoFromNixDevicesList(tc.rawData)
|
||||
|
||||
assert.Equal(t, tc.expectedVirtSystem, actualVirtSystem)
|
||||
assert.Equal(t, tc.expectedVirtRole, actualVirtRole)
|
||||
}
|
||||
}
|
||||
@ -75,6 +75,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
|
||||
"os_arch": "amd64",
|
||||
"os_family": "debian",
|
||||
"os_kernel": "linux",
|
||||
"os_full_name": "Debian",
|
||||
"os_version": "5.4.0-37",
|
||||
"os_virtualization_system":"KVM",
|
||||
"os_virtualization_role":"guest",
|
||||
"cpu_family":"59",
|
||||
"cpu_model":"6",
|
||||
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
|
||||
"num_cpus":16,
|
||||
"mem_total":67020316672,
|
||||
"timezone":"UTC (UTC+00:00)",
|
||||
"hostname": "my-devvm-v3",
|
||||
"ipv4": [
|
||||
"192.168.3.148"
|
||||
@ -97,6 +107,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
|
||||
"os_arch": "amd64",
|
||||
"os_family": "alpine",
|
||||
"os_kernel": "linux",
|
||||
"os_full_name": "Alpine Linux",
|
||||
"os_version": "4.19.80-0",
|
||||
"os_virtualization_system":"",
|
||||
"os_virtualization_role":"",
|
||||
"cpu_family":"6",
|
||||
"cpu_model":"79",
|
||||
"cpu_model_name":"Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
|
||||
"num_cpus":4,
|
||||
"mem_total":8363900928,
|
||||
"timezone":"CEST (UTC+02:00)",
|
||||
"hostname": "alpine-3-10-tk-01",
|
||||
"ipv4": [
|
||||
"192.168.122.117"
|
||||
@ -134,4 +154,4 @@ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/bin/rportd
|
||||
That's all.
|
||||
Now you can use "0.0.0.0:443" as API address.
|
||||
|
||||
You need to run the above command everytime you change the rpotd binary, for example after every update.
|
||||
You need to run the above command everytime you change the rpotd binary, for example after every update.
|
||||
|
||||
@ -22,6 +22,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
|
||||
"os_arch": "amd64",
|
||||
"os_family": "debian",
|
||||
"os_kernel": "linux",
|
||||
"os_full_name": "Debian",
|
||||
"os_version": "5.4.0-37",
|
||||
"os_virtualization_system":"KVM",
|
||||
"os_virtualization_role":"guest",
|
||||
"cpu_family":"59",
|
||||
"cpu_model":"6",
|
||||
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
|
||||
"num_cpus":16,
|
||||
"mem_total":67020316672,
|
||||
"timezone":"UTC (UTC+00:00)",
|
||||
"hostname": "my-devvm-v3",
|
||||
"ipv4": [
|
||||
"192.168.3.148"
|
||||
@ -44,6 +54,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
|
||||
"os_arch": "amd64",
|
||||
"os_family": "alpine",
|
||||
"os_kernel": "linux",
|
||||
"os_full_name": "Debian",
|
||||
"os_version": "5.4.0-37",
|
||||
"os_virtualization_system":"KVM",
|
||||
"os_virtualization_role":"guest",
|
||||
"cpu_family":"59",
|
||||
"cpu_model":"6",
|
||||
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
|
||||
"num_cpus":16,
|
||||
"mem_total":67020316672,
|
||||
"timezone":"UTC (UTC+00:00)",
|
||||
"hostname": "alpine-3-10-tk-01",
|
||||
"ipv4": [
|
||||
"192.168.122.117"
|
||||
|
||||
@ -65,6 +65,16 @@ curl -s -u admin:foobaz http://localhost:3000/api/v1/clients|jq
|
||||
"os_arch": "amd64",
|
||||
"os_family": "debian",
|
||||
"os_kernel": "linux",
|
||||
"os_full_name": "Debian",
|
||||
"os_version": "5.4.0-37",
|
||||
"os_virtualization_system":"KVM",
|
||||
"os_virtualization_role":"guest",
|
||||
"cpu_family":"59",
|
||||
"cpu_model":"6",
|
||||
"cpu_model_name":"Intel(R) Xeon(R) Silver 4110 CPU @ 2.10GHz",
|
||||
"num_cpus":16,
|
||||
"mem_total":67020316672,
|
||||
"timezone":"UTC (UTC+00:00)",
|
||||
"hostname": "my-devvm-v3",
|
||||
"ipv4": [
|
||||
"192.168.3.148"
|
||||
|
||||
@ -139,6 +139,15 @@ You can filter entries by `id`, `client_id`, `created_by`, `created_at`, `key` f
|
||||
|
||||
`http://localhost:3000/api/v1/vault?filter[key]=one` will list you entries with the key=one.
|
||||
|
||||
Note:
|
||||
If you use curl to test filters, you should switch off URL globbing parser by providing `-g` flag (see curl documentation for the details), e.g.:
|
||||
|
||||
```
|
||||
curl -g -X GET 'http://localhost:3000/api/v1/vault?filter[created_by]=admin' \
|
||||
-u admin:foobaz \
|
||||
-H 'Content-Type: application/json'
|
||||
```
|
||||
|
||||
You can combine filters for multiple fields:
|
||||
`http://localhost:3000/api/v1/vault?filter[client_id]=client123&filter[created_by]=admin` - gives you list of entries for client `client123` and created by `admin`
|
||||
|
||||
@ -179,7 +188,7 @@ The response will be
|
||||
}
|
||||
```
|
||||
|
||||
In the "value" field you will find the decrypted secure value. If `required_group` value of the stored vault entry is not empty, only users of this group can read this value, e.g. if `required_group` = 'admin' and the current user doesn't belong to this group, an error will be returned.
|
||||
In the "value" field you will find the decrypted secure value. If `required_group` value of the stored vault entry is not empty, only users of this group can read this value, e.g. if `required_group` = 'Administrators' and the current user doesn't belong to this group, an error will be returned.
|
||||
|
||||
### Add a new secured value
|
||||
|
||||
@ -257,7 +266,7 @@ You can delete a vault entry by calling the following API:
|
||||
|
||||
```
|
||||
curl -X DELETE 'http://localhost:3000/api/v1/vault/1' \
|
||||
-u admin:foobaz'
|
||||
-u admin:foobaz
|
||||
```
|
||||
|
||||
If `required_group` value of the entry you want to delete is not empty, only users of this group can change this value, otherwise an error will be returned.
|
||||
|
||||
109
server/api.go
109
server/api.go
@ -14,6 +14,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cloudradar-monitoring/rport/share/query"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/tomasen/realip"
|
||||
@ -565,21 +567,34 @@ func (al *APIListener) handleGetStatus(w http.ResponseWriter, req *http.Request)
|
||||
}
|
||||
|
||||
func (al *APIListener) handleGetClients(w http.ResponseWriter, req *http.Request) {
|
||||
var err error
|
||||
sortFunc, desc, err := getCorrespondingSortFunc(req.URL.Query().Get(queryParamSort))
|
||||
if err != nil {
|
||||
al.jsonErrorResponse(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
clients, err := al.clientService.GetAll()
|
||||
if err != nil {
|
||||
al.jsonErrorResponse(w, http.StatusInternalServerError, err)
|
||||
filterOptions := query.ExtractFilterOptions(req)
|
||||
filterErr := query.ValidateFilterOptions(filterOptions, clientsSupportedFields)
|
||||
if filterErr != nil {
|
||||
al.jsonError(w, filterErr)
|
||||
return
|
||||
}
|
||||
|
||||
sortFunc(clients, desc)
|
||||
var cls []*clients.Client
|
||||
if len(filterOptions) > 0 {
|
||||
cls, err = al.clientService.GetFiltered(filterOptions)
|
||||
} else {
|
||||
cls, err = al.clientService.GetAll()
|
||||
}
|
||||
if err != nil {
|
||||
al.jsonError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
clientsPayload := convertToClientsPayload(clients)
|
||||
sortFunc(cls, desc)
|
||||
|
||||
clientsPayload := convertToClientsPayload(cls)
|
||||
al.writeJSONResponse(w, http.StatusOK, api.NewSuccessPayload(clientsPayload))
|
||||
}
|
||||
|
||||
@ -661,44 +676,64 @@ func (al *APIListener) handleDeleteUser(w http.ResponseWriter, req *http.Request
|
||||
}
|
||||
|
||||
type ClientPayload struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
Hostname string `json:"hostname"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
Address string `json:"address"`
|
||||
Tunnels []*clients.Tunnel `json:"tunnels"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at"`
|
||||
ConnectionState clients.ConnectionState `json:"connection_state"`
|
||||
ClientAuthID string `json:"client_auth_id"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisconnectedAt *time.Time `json:"disconnected_at"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
Hostname string `json:"hostname"`
|
||||
OSFullName string `json:"os_full_name"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSVirtualizationSystem string `json:"os_virtualization_system"`
|
||||
OSVirtualizationRole string `json:"os_virtualization_role"`
|
||||
NumCPUs int `json:"num_cpus"`
|
||||
CPUFamily string `json:"cpu_family"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUModelName string `json:"cpu_model_name"`
|
||||
MemoryTotal uint64 `json:"mem_total"`
|
||||
Timezone string `json:"timezone"`
|
||||
Address string `json:"address"`
|
||||
ClientAuthID string `json:"client_auth_id"`
|
||||
Version string `json:"version"`
|
||||
ConnectionState clients.ConnectionState `json:"connection_state"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Tunnels []*clients.Tunnel `json:"tunnels"`
|
||||
}
|
||||
|
||||
func convertToClientsPayload(clients []*clients.Client) []ClientPayload {
|
||||
r := make([]ClientPayload, 0, len(clients))
|
||||
for _, cur := range clients {
|
||||
r = append(r, ClientPayload{
|
||||
ID: cur.ID,
|
||||
Name: cur.Name,
|
||||
OS: cur.OS,
|
||||
OSArch: cur.OSArch,
|
||||
OSFamily: cur.OSFamily,
|
||||
OSKernel: cur.OSKernel,
|
||||
Hostname: cur.Hostname,
|
||||
IPv4: cur.IPv4,
|
||||
IPv6: cur.IPv6,
|
||||
Tags: cur.Tags,
|
||||
Version: cur.Version,
|
||||
Address: cur.Address,
|
||||
Tunnels: cur.Tunnels,
|
||||
DisconnectedAt: cur.DisconnectedAt,
|
||||
ConnectionState: cur.ConnectionState(),
|
||||
ClientAuthID: cur.ClientAuthID,
|
||||
ID: cur.ID,
|
||||
Name: cur.Name,
|
||||
OS: cur.OS,
|
||||
OSArch: cur.OSArch,
|
||||
OSFamily: cur.OSFamily,
|
||||
OSKernel: cur.OSKernel,
|
||||
Hostname: cur.Hostname,
|
||||
IPv4: cur.IPv4,
|
||||
IPv6: cur.IPv6,
|
||||
Tags: cur.Tags,
|
||||
Version: cur.Version,
|
||||
Address: cur.Address,
|
||||
Tunnels: cur.Tunnels,
|
||||
DisconnectedAt: cur.DisconnectedAt,
|
||||
ConnectionState: cur.ConnectionState(),
|
||||
ClientAuthID: cur.ClientAuthID,
|
||||
OSFullName: cur.OSFullName,
|
||||
OSVersion: cur.OSVersion,
|
||||
OSVirtualizationSystem: cur.OSVirtualizationSystem,
|
||||
OSVirtualizationRole: cur.OSVirtualizationRole,
|
||||
CPUFamily: cur.CPUFamily,
|
||||
CPUModel: cur.CPUModel,
|
||||
CPUModelName: cur.CPUModelName,
|
||||
Timezone: cur.Timezone,
|
||||
NumCPUs: cur.NumCPUs,
|
||||
MemoryTotal: cur.MemoryTotal,
|
||||
})
|
||||
}
|
||||
return r
|
||||
|
||||
@ -127,7 +127,7 @@ func NewAPIListener(
|
||||
}
|
||||
|
||||
scriptLogger := chshare.NewLogger("scripts", config.Logging.LogOutput, config.Logging.LogLevel)
|
||||
scriptDb, err := script.NewSqliteProvider(path.Join(config.Server.DataDir, "scripts.db"), scriptLogger)
|
||||
scriptDb, err := script.NewSqliteProvider(path.Join(config.Server.DataDir, "library.db"), scriptLogger)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -583,7 +583,7 @@ func TestHandleDeleteClient(t *testing.T) {
|
||||
al := APIListener{
|
||||
insecureForTests: true,
|
||||
Server: &Server{
|
||||
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour)),
|
||||
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour, testLog)),
|
||||
config: &Config{
|
||||
Server: ServerConfig{
|
||||
AuthWrite: tc.clientAuthWrite,
|
||||
@ -819,7 +819,7 @@ func TestHandlePostCommand(t *testing.T) {
|
||||
al := APIListener{
|
||||
insecureForTests: true,
|
||||
Server: &Server{
|
||||
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour)),
|
||||
clientService: NewClientService(nil, clients.NewClientRepository(tc.clients, &hour, testLog)),
|
||||
config: &Config{
|
||||
Server: ServerConfig{
|
||||
RunRemoteCmdTimeoutSec: defaultTimeout,
|
||||
@ -1054,7 +1054,7 @@ func TestHandleGetClients(t *testing.T) {
|
||||
al := APIListener{
|
||||
insecureForTests: true,
|
||||
Server: &Server{
|
||||
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2}, &hour)),
|
||||
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2}, &hour, testLog)),
|
||||
config: &Config{
|
||||
Server: ServerConfig{MaxRequestBytes: 1024 * 1024},
|
||||
},
|
||||
@ -1070,11 +1070,17 @@ func TestHandleGetClients(t *testing.T) {
|
||||
"data":[
|
||||
{
|
||||
"id":"client-1",
|
||||
"mem_total":100000,
|
||||
"name":"Random Rport Client",
|
||||
"num_cpus":2,
|
||||
"os":"Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
"os_arch":"amd64",
|
||||
"os_family":"alpine",
|
||||
"os_full_name":"Debian 18.0",
|
||||
"os_kernel":"linux",
|
||||
"os_version":"18.0",
|
||||
"os_virtualization_role":"guest",
|
||||
"os_virtualization_system":"LVM",
|
||||
"hostname":"alpine-3-10-tk-01",
|
||||
"ipv4":[
|
||||
"192.168.122.111"
|
||||
@ -1088,6 +1094,7 @@ func TestHandleGetClients(t *testing.T) {
|
||||
],
|
||||
"version":"0.1.12",
|
||||
"address":"88.198.189.161:50078",
|
||||
"timezone":"UTC-0",
|
||||
"tunnels":[
|
||||
{
|
||||
"lhost":"0.0.0.0",
|
||||
@ -1113,16 +1120,25 @@ func TestHandleGetClients(t *testing.T) {
|
||||
}
|
||||
],
|
||||
"connection_state":"connected",
|
||||
"cpu_family":"Virtual CPU",
|
||||
"cpu_model":"Virtual CPU",
|
||||
"cpu_model_name":"",
|
||||
"disconnected_at":null,
|
||||
"client_auth_id":"user1"
|
||||
},
|
||||
{
|
||||
"id":"client-2",
|
||||
"mem_total":100000,
|
||||
"name":"Random Rport Client",
|
||||
"num_cpus":2,
|
||||
"os":"Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
"os_arch":"amd64",
|
||||
"os_family":"alpine",
|
||||
"os_full_name":"Debian 18.0",
|
||||
"os_kernel":"linux",
|
||||
"os_version": "18.0",
|
||||
"os_virtualization_role":"guest",
|
||||
"os_virtualization_system":"LVM",
|
||||
"hostname":"alpine-3-10-tk-01",
|
||||
"ipv4":[
|
||||
"192.168.122.111"
|
||||
@ -1136,6 +1152,7 @@ func TestHandleGetClients(t *testing.T) {
|
||||
],
|
||||
"version":"0.1.12",
|
||||
"address":"88.198.189.161:50078",
|
||||
"timezone":"UTC-0",
|
||||
"tunnels":[
|
||||
{
|
||||
"lhost":"0.0.0.0",
|
||||
@ -1161,12 +1178,14 @@ func TestHandleGetClients(t *testing.T) {
|
||||
}
|
||||
],
|
||||
"connection_state":"disconnected",
|
||||
"cpu_family":"Virtual CPU",
|
||||
"cpu_model":"Virtual CPU",
|
||||
"cpu_model_name":"",
|
||||
"disconnected_at":"2020-08-19T13:04:23+03:00",
|
||||
"client_auth_id":"user1"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.JSONEq(t, expectedJSON, w.Body.String())
|
||||
}
|
||||
@ -1286,7 +1305,7 @@ func TestHandlePostMultiClientCommand(t *testing.T) {
|
||||
al := APIListener{
|
||||
insecureForTests: true,
|
||||
Server: &Server{
|
||||
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2, c3}, &hour)),
|
||||
clientService: NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1, c2, c3}, &hour, testLog)),
|
||||
config: &Config{
|
||||
Server: ServerConfig{
|
||||
RunRemoteCmdTimeoutSec: defaultTimeout,
|
||||
|
||||
@ -10,6 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cloudradar-monitoring/rport/share/query"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
||||
"github.com/cloudradar-monitoring/rport/server/api/errors"
|
||||
@ -26,6 +28,18 @@ type ClientService struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var clientsSupportedFields = map[string]bool{
|
||||
"os_full_name": true,
|
||||
"os_virtualization_system": true,
|
||||
"os_virtualization_role": true,
|
||||
"cpu_model_name": true,
|
||||
"timezone": true,
|
||||
"os_version": true,
|
||||
"cpu_family": true,
|
||||
"cpu_model": true,
|
||||
"num_cpus": true,
|
||||
}
|
||||
|
||||
// NewClientService returns a new instance of client service.
|
||||
func NewClientService(
|
||||
portDistributor *ports.PortDistributor,
|
||||
@ -42,8 +56,9 @@ func InitClientService(
|
||||
portDistributor *ports.PortDistributor,
|
||||
provider clients.ClientProvider,
|
||||
keepLostClients *time.Duration,
|
||||
logger *chshare.Logger,
|
||||
) (*ClientService, error) {
|
||||
repo, err := clients.InitClientRepository(ctx, provider, keepLostClients)
|
||||
repo, err := clients.InitClientRepository(ctx, provider, keepLostClients, logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to init Client Repository: %v", err)
|
||||
}
|
||||
@ -111,6 +126,10 @@ func (s *ClientService) GetAll() ([]*clients.Client, error) {
|
||||
return s.repo.GetAll()
|
||||
}
|
||||
|
||||
func (s *ClientService) GetFiltered(filterOptions []query.FilterOption) ([]*clients.Client, error) {
|
||||
return s.repo.GetFiltered(filterOptions)
|
||||
}
|
||||
|
||||
func (s *ClientService) StartClient(
|
||||
ctx context.Context, clientAuthID, clientID string, sshConn ssh.Conn, authMultiuseCreds bool,
|
||||
req *chshare.ConnectionRequest, clog *chshare.Logger,
|
||||
@ -148,23 +167,34 @@ func (s *ClientService) StartClient(
|
||||
}
|
||||
|
||||
client := &clients.Client{
|
||||
ID: clientID,
|
||||
ClientAuthID: clientAuthID,
|
||||
Name: req.Name,
|
||||
Tags: req.Tags,
|
||||
OS: req.OS,
|
||||
OSArch: req.OSArch,
|
||||
OSFamily: req.OSFamily,
|
||||
OSKernel: req.OSKernel,
|
||||
Hostname: req.Hostname,
|
||||
Version: req.Version,
|
||||
IPv4: req.IPv4,
|
||||
IPv6: req.IPv6,
|
||||
Address: clientHost,
|
||||
Tunnels: make([]*clients.Tunnel, 0),
|
||||
Connection: sshConn,
|
||||
Context: ctx,
|
||||
Logger: clog,
|
||||
ID: clientID,
|
||||
Name: req.Name,
|
||||
OS: req.OS,
|
||||
OSArch: req.OSArch,
|
||||
OSFamily: req.OSFamily,
|
||||
OSKernel: req.OSKernel,
|
||||
OSFullName: req.OSFullName,
|
||||
OSVersion: req.OSVersion,
|
||||
OSVirtualizationSystem: req.OSVirtualizationSystem,
|
||||
OSVirtualizationRole: req.OSVirtualizationRole,
|
||||
Hostname: req.Hostname,
|
||||
CPUFamily: req.OSFamily,
|
||||
CPUModel: req.CPUModel,
|
||||
CPUModelName: req.CPUModelName,
|
||||
NumCPUs: req.NumCPUs,
|
||||
MemoryTotal: req.MemoryTotal,
|
||||
Timezone: req.Timezone,
|
||||
IPv4: req.IPv4,
|
||||
IPv6: req.IPv6,
|
||||
Tags: req.Tags,
|
||||
Version: req.Version,
|
||||
Address: clientHost,
|
||||
Tunnels: make([]*clients.Tunnel, 0),
|
||||
DisconnectedAt: nil,
|
||||
ClientAuthID: clientAuthID,
|
||||
Connection: sshConn,
|
||||
Context: ctx,
|
||||
Logger: clog,
|
||||
}
|
||||
|
||||
_, err = s.startClientTunnels(client, req.Remotes)
|
||||
|
||||
@ -84,7 +84,7 @@ func TestStartClient(t *testing.T) {
|
||||
repo: clients.NewClientRepository([]*clients.Client{{
|
||||
ID: "test-client",
|
||||
ClientAuthID: "test-client-auth",
|
||||
}}, nil),
|
||||
}}, nil, testLog),
|
||||
portDistributor: ports.NewPortDistributor(mapset.NewThreadUnsafeSet()),
|
||||
}
|
||||
_, err := cs.StartClient(
|
||||
@ -140,7 +140,7 @@ func TestDeleteOfflineClient(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// given
|
||||
clientService := NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1Active, c2Active, c3Offline, c4Offline}, &hour))
|
||||
clientService := NewClientService(nil, clients.NewClientRepository([]*clients.Client{c1Active, c2Active, c3Offline, c4Offline}, &hour, testLog))
|
||||
before, err := clientService.Count()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 4, before)
|
||||
|
||||
@ -18,7 +18,7 @@ func TestCleanup(t *testing.T) {
|
||||
clients := []*Client{c1, c2, c3}
|
||||
p := newFakeClientProvider(t, hour, c1, c2, c3)
|
||||
defer p.Close()
|
||||
repo := newClientRepositoryWithDB(clients, &hour, p)
|
||||
repo := newClientRepositoryWithDB(clients, &hour, p, testLog)
|
||||
require.Len(t, repo.clients, 3)
|
||||
gotObsolete, err := p.get(ctx, c3.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
@ -26,19 +26,29 @@ const (
|
||||
|
||||
// Client represents client connection
|
||||
type Client struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
Hostname string `json:"hostname"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
Address string `json:"address"`
|
||||
Tunnels []*Tunnel `json:"tunnels"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
OSFullName string `json:"os_full_name"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSVirtualizationSystem string `json:"os_virtualization_system"`
|
||||
OSVirtualizationRole string `json:"os_virtualization_role"`
|
||||
CPUFamily string `json:"cpu_family"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUModelName string `json:"cpu_model_name"`
|
||||
NumCPUs int `json:"num_cpus"`
|
||||
MemoryTotal uint64 `json:"mem_total"`
|
||||
Timezone string `json:"timezone"`
|
||||
Hostname string `json:"hostname"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Version string `json:"version"`
|
||||
Address string `json:"address"`
|
||||
Tunnels []*Tunnel `json:"tunnels"`
|
||||
// DisconnectedAt is a time when a client was disconnected. If nil - it's connected.
|
||||
DisconnectedAt *time.Time `json:"disconnected_at"`
|
||||
ClientAuthID string `json:"client_auth_id"`
|
||||
|
||||
@ -69,18 +69,28 @@ func (b ClientBuilder) Connection(conn ssh.Conn) ClientBuilder {
|
||||
|
||||
func (b ClientBuilder) Build() *Client {
|
||||
return &Client{
|
||||
ID: b.id,
|
||||
Name: "Random Rport Client",
|
||||
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
Hostname: "alpine-3-10-tk-01",
|
||||
IPv4: []string{"192.168.122.111"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
|
||||
Tags: []string{"Linux", "Datacenter 1"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.161:50078",
|
||||
NumCPUs: 2,
|
||||
MemoryTotal: 100000,
|
||||
ID: b.id,
|
||||
Name: "Random Rport Client",
|
||||
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
OSFullName: "Debian 18.0",
|
||||
OSVersion: "18.0",
|
||||
OSVirtualizationSystem: "LVM",
|
||||
OSVirtualizationRole: "guest",
|
||||
CPUFamily: "Virtual CPU",
|
||||
CPUModel: "Virtual CPU",
|
||||
CPUModelName: "",
|
||||
Timezone: "UTC-0",
|
||||
Hostname: "alpine-3-10-tk-01",
|
||||
IPv4: []string{"192.168.122.111"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
|
||||
Tags: []string{"Linux", "Datacenter 1"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.161:50078",
|
||||
Tunnels: []*Tunnel{
|
||||
{
|
||||
ID: "1",
|
||||
@ -103,10 +113,8 @@ func (b ClientBuilder) Build() *Client {
|
||||
},
|
||||
DisconnectedAt: b.disconnectedAt,
|
||||
ClientAuthID: b.clientAuthID,
|
||||
|
||||
Connection: b.conn,
|
||||
Connection: b.conn,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func generateRandomClientAuthID() string {
|
||||
|
||||
@ -2,9 +2,15 @@ package clients
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
chshare "github.com/cloudradar-monitoring/rport/share"
|
||||
"github.com/cloudradar-monitoring/rport/share/query"
|
||||
)
|
||||
|
||||
type ClientRepository struct {
|
||||
@ -14,16 +20,17 @@ type ClientRepository struct {
|
||||
KeepLostClients *time.Duration
|
||||
// storage
|
||||
provider ClientProvider
|
||||
logger *chshare.Logger
|
||||
}
|
||||
|
||||
// NewClientRepository returns a new thread-safe in-memory cache to store client connections populated with given clients if any.
|
||||
// keepLostClients is a duration to keep disconnected clients. If a client was disconnected longer than a given
|
||||
// duration it will be treated as obsolete.
|
||||
func NewClientRepository(initClients []*Client, keepLostClients *time.Duration) *ClientRepository {
|
||||
return newClientRepositoryWithDB(initClients, keepLostClients, nil)
|
||||
func NewClientRepository(initClients []*Client, keepLostClients *time.Duration, logger *chshare.Logger) *ClientRepository {
|
||||
return newClientRepositoryWithDB(initClients, keepLostClients, nil, logger)
|
||||
}
|
||||
|
||||
func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Duration, provider ClientProvider) *ClientRepository {
|
||||
func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Duration, provider ClientProvider, logger *chshare.Logger) *ClientRepository {
|
||||
clients := make(map[string]*Client)
|
||||
for i := range initClients {
|
||||
clients[initClients[i].ID] = initClients[i]
|
||||
@ -32,16 +39,22 @@ func newClientRepositoryWithDB(initClients []*Client, keepLostClients *time.Dura
|
||||
clients: clients,
|
||||
KeepLostClients: keepLostClients,
|
||||
provider: provider,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func InitClientRepository(ctx context.Context, provider ClientProvider, keepLostClients *time.Duration) (*ClientRepository, error) {
|
||||
func InitClientRepository(
|
||||
ctx context.Context,
|
||||
provider ClientProvider,
|
||||
keepLostClients *time.Duration,
|
||||
logger *chshare.Logger,
|
||||
) (*ClientRepository, error) {
|
||||
initClients, err := GetInitState(ctx, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newClientRepositoryWithDB(initClients, keepLostClients, provider), nil
|
||||
return newClientRepositoryWithDB(initClients, keepLostClients, provider, logger), nil
|
||||
}
|
||||
|
||||
func (s *ClientRepository) Save(client *Client) error {
|
||||
@ -127,7 +140,7 @@ func (s *ClientRepository) CountDisconnected() (int, error) {
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// GetActiveByID returns non-obsolete active or disconnected client by a given id.
|
||||
// GetByID returns non-obsolete active or disconnected client by a given id.
|
||||
func (s *ClientRepository) GetByID(id string) (*Client, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@ -168,6 +181,13 @@ func (s *ClientRepository) GetAll() ([]*Client, error) {
|
||||
return s.getNonObsolete()
|
||||
}
|
||||
|
||||
// GetFiltered returns all non-obsolete active and disconnected clients filtered by os parameters
|
||||
func (s *ClientRepository) GetFiltered(filterOptions []query.FilterOption) ([]*Client, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.getNonObsoleteFiltered(filterOptions)
|
||||
}
|
||||
|
||||
func (s *ClientRepository) GetAllActive() []*Client {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
@ -189,3 +209,87 @@ func (s *ClientRepository) getNonObsolete() ([]*Client, error) {
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ClientRepository) getNonObsoleteFiltered(filterOptions []query.FilterOption) ([]*Client, error) {
|
||||
result := make([]*Client, 0, len(s.clients))
|
||||
for _, client := range s.clients {
|
||||
if !client.Obsolete(s.KeepLostClients) {
|
||||
matches, err := s.clientMatchesFilters(client, filterOptions)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if !matches {
|
||||
continue
|
||||
}
|
||||
result = append(result, client)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *ClientRepository) clientMatchesFilters(cl *Client, filterOptions []query.FilterOption) (bool, error) {
|
||||
for _, f := range filterOptions {
|
||||
matches, err := s.clientMatchesFilter(cl, f)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !matches {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *ClientRepository) clientMatchesFilter(cl *Client, filter query.FilterOption) (bool, error) {
|
||||
clientMap, err := s.clientToMap(cl)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
clientFieldValueToMatch, ok := clientMap[filter.Column]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported filter column: %s", filter.Column)
|
||||
}
|
||||
clientFieldValueToMatchStr := fmt.Sprint(clientFieldValueToMatch)
|
||||
|
||||
regx := regexp.MustCompile(`[^\\]\*+`)
|
||||
for _, filterValue := range filter.Values {
|
||||
hasUnescapedWildCard := regx.MatchString(filterValue)
|
||||
if !hasUnescapedWildCard {
|
||||
if filterValue == clientFieldValueToMatchStr {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
filterValueRegex, err := regexp.Compile(strings.ReplaceAll(filterValue, "*", ".*"))
|
||||
if err != nil {
|
||||
s.logger.Errorf("failed to generate regex for '%s': %v", filterValue, err)
|
||||
if filterValue == clientFieldValueToMatchStr {
|
||||
return true, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if filterValueRegex.MatchString(clientFieldValueToMatchStr) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (s *ClientRepository) clientToMap(cl *Client) (map[string]interface{}, error) {
|
||||
clientBytes, err := json.Marshal(cl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := make(map[string]interface{})
|
||||
|
||||
err = json.Unmarshal(clientBytes, &res)
|
||||
|
||||
return res, err
|
||||
}
|
||||
|
||||
@ -4,6 +4,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cloudradar-monitoring/rport/share/query"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@ -12,7 +14,7 @@ func TestCRWithExpiration(t *testing.T) {
|
||||
now = nowMockF
|
||||
|
||||
exp := 2 * time.Hour
|
||||
repo := NewClientRepository([]*Client{c1, c2}, &exp)
|
||||
repo := NewClientRepository([]*Client{c1, c2}, &exp, testLog)
|
||||
|
||||
assert := assert.New(t)
|
||||
assert.NoError(repo.Save(c3))
|
||||
@ -61,7 +63,7 @@ func TestCRWithExpiration(t *testing.T) {
|
||||
func TestCRWithNoExpiration(t *testing.T) {
|
||||
now = nowMockF
|
||||
|
||||
repo := NewClientRepository([]*Client{c1, c2, c3}, nil)
|
||||
repo := NewClientRepository([]*Client{c1, c2, c3}, nil, testLog)
|
||||
c4Active := shallowCopy(c4)
|
||||
c4Active.DisconnectedAt = nil
|
||||
|
||||
@ -103,3 +105,162 @@ func TestCRWithNoExpiration(t *testing.T) {
|
||||
assert.NoError(err)
|
||||
assert.ElementsMatch([]*Client{c1, c2, c3}, gotClients)
|
||||
}
|
||||
|
||||
func TestCRWithFilter(t *testing.T) {
|
||||
testCases := []struct {
|
||||
filters []query.FilterOption
|
||||
expectedClientIDs []string
|
||||
}{
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_full_name",
|
||||
Values: []string{
|
||||
"Alpine Linux",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{
|
||||
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
|
||||
},
|
||||
},
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_full_name",
|
||||
Values: []string{
|
||||
"Alpine*",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{
|
||||
"aa1210c7-1899-491e-8e71-564cacaf1df8",
|
||||
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
|
||||
},
|
||||
},
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_full_name",
|
||||
Values: []string{
|
||||
"Alpine*",
|
||||
"Microsoft Windows Server 2016 Standard",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{
|
||||
"2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
|
||||
"aa1210c7-1899-491e-8e71-564cacaf1df8",
|
||||
"daflkdfjqlkerlkejrqlwedalfdfadfa",
|
||||
},
|
||||
},
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_virtualization_system",
|
||||
Values: []string{
|
||||
"KVM",
|
||||
"Microsoft Windows Server 2016 Standard",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "os_virtualization_role",
|
||||
Values: []string{
|
||||
"guest",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{
|
||||
"aa1210c7-1899-491e-8e71-564cacaf1df8",
|
||||
},
|
||||
},
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_full_name",
|
||||
Values: []string{
|
||||
"Oracle",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{},
|
||||
},
|
||||
{
|
||||
filters: []query.FilterOption{
|
||||
{
|
||||
Column: "os_full_name",
|
||||
Values: []string{
|
||||
"Microsoft Windows Server 2016 Standard",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "os_version",
|
||||
Values: []string{
|
||||
"10.0.14393 Build 14393",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "cpu_family",
|
||||
Values: []string{
|
||||
"1",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "cpu_model",
|
||||
Values: []string{
|
||||
"4",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "cpu_model_name",
|
||||
Values: []string{
|
||||
"Intel*",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "num_cpus",
|
||||
Values: []string{
|
||||
"2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Column: "timezone",
|
||||
Values: []string{
|
||||
"UTC*",
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedClientIDs: []string{
|
||||
"daflkdfjqlkerlkejrqlwedalfdfadfa",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testcase := range testCases {
|
||||
repo := NewClientRepository([]*Client{c1, c2, c5}, nil, testLog)
|
||||
|
||||
actualClients, err := repo.GetFiltered(testcase.filters)
|
||||
require.NoError(t, err)
|
||||
|
||||
actualClientIDs := make([]string, 0, len(actualClients))
|
||||
|
||||
for _, actualClient := range actualClients {
|
||||
actualClientIDs = append(actualClientIDs, actualClient.ID)
|
||||
}
|
||||
|
||||
assert.ElementsMatch(t, testcase.expectedClientIDs, actualClientIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCRWithUnsupportedFilter(t *testing.T) {
|
||||
repo := NewClientRepository([]*Client{c1}, nil, testLog)
|
||||
_, err := repo.GetFiltered([]query.FilterOption{
|
||||
{
|
||||
Column: "unknown_field",
|
||||
Values: []string{
|
||||
"1",
|
||||
},
|
||||
},
|
||||
})
|
||||
require.EqualError(t, err, "unsupported filter column: unknown_field")
|
||||
}
|
||||
|
||||
@ -22,18 +22,28 @@ var (
|
||||
)
|
||||
|
||||
var c1 = &Client{
|
||||
ID: "aa1210c7-1899-491e-8e71-564cacaf1df8",
|
||||
Name: "Random Rport Client 1",
|
||||
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
Hostname: "alpine-3-10-tk-01",
|
||||
IPv4: []string{"192.168.122.111"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
|
||||
Tags: []string{"Linux", "Datacenter 1"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.161:50078",
|
||||
ID: "aa1210c7-1899-491e-8e71-564cacaf1df8",
|
||||
Name: "Random Rport Client 1",
|
||||
OS: "Linux alpine-3-10-tk-01 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
OSFullName: "Alpine",
|
||||
OSVersion: "3.14.0",
|
||||
OSVirtualizationRole: "guest",
|
||||
OSVirtualizationSystem: "KVM",
|
||||
CPUFamily: "6",
|
||||
CPUModel: "79",
|
||||
CPUModelName: "Common KVM processor",
|
||||
NumCPUs: 2,
|
||||
MemoryTotal: 1000000,
|
||||
Timezone: "CEST (UTC+02:00)",
|
||||
Hostname: "alpine-3-10-tk-01",
|
||||
IPv4: []string{"192.168.122.111"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b1"},
|
||||
Tags: []string{"Linux", "Datacenter 1"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.161:50078",
|
||||
Tunnels: []*Tunnel{
|
||||
{
|
||||
ID: "1",
|
||||
@ -59,18 +69,28 @@ var c1 = &Client{
|
||||
}
|
||||
|
||||
var c2 = &Client{
|
||||
ID: "2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
|
||||
Name: "Random Rport Client 2",
|
||||
OS: "Linux alpine-3-10-tk-02 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
Hostname: "alpine-3-10-tk-02",
|
||||
IPv4: []string{"192.168.122.112"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b2"},
|
||||
Tags: []string{"Linux", "Datacenter 2"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.162:50078",
|
||||
ID: "2fb5eca74d7bdf5f5b879ebadb446af7c113b076354d74e1882d8101e9f4b918",
|
||||
Name: "Random Rport Client 2",
|
||||
OS: "Linux alpine-3-10-tk-02 4.19.80-0-virt #1-Alpine SMP Fri Oct 18 11:51:24 UTC 2019 x86_64 Linux",
|
||||
OSArch: "amd64",
|
||||
OSFamily: "alpine",
|
||||
OSKernel: "linux",
|
||||
OSFullName: "Alpine Linux",
|
||||
OSVersion: "2.0.0",
|
||||
OSVirtualizationRole: "",
|
||||
OSVirtualizationSystem: "",
|
||||
CPUFamily: "5",
|
||||
CPUModel: "33",
|
||||
CPUModelName: "Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
|
||||
NumCPUs: 4,
|
||||
MemoryTotal: 1500000,
|
||||
Timezone: "CEST (UTC+00:00)",
|
||||
Hostname: "alpine-3-10-tk-02",
|
||||
IPv4: []string{"192.168.122.112"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe59:a0b2"},
|
||||
Tags: []string{"Linux", "Datacenter 2"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.162:50078",
|
||||
Tunnels: []*Tunnel{
|
||||
{
|
||||
ID: "1",
|
||||
@ -122,6 +142,34 @@ var c4 = &Client{
|
||||
DisconnectedAt: &c4DisconnectedTime,
|
||||
}
|
||||
|
||||
var c5 = &Client{
|
||||
ID: "daflkdfjqlkerlkejrqlwedalfdfadfa",
|
||||
Name: "Windows Client",
|
||||
OS: "Windows",
|
||||
OSArch: "x86_64",
|
||||
OSFamily: "Server",
|
||||
OSKernel: "10.0.1 4393 Build 14393",
|
||||
OSFullName: "Microsoft Windows Server 2016 Standard",
|
||||
OSVersion: "10.0.14393 Build 14393",
|
||||
OSVirtualizationRole: "",
|
||||
OSVirtualizationSystem: "",
|
||||
CPUFamily: "1",
|
||||
CPUModel: "4",
|
||||
CPUModelName: "Intel(R) Xeon(R) CPU E5-2630 v4 @ 2.20GHz",
|
||||
NumCPUs: 2,
|
||||
MemoryTotal: 4294422528,
|
||||
Timezone: "PDT (UTC-07:00)",
|
||||
Hostname: "RPORT-WIN-SRV2016",
|
||||
IPv4: []string{"192.168.122.124"},
|
||||
IPv6: []string{"fe80::b84f:aff:fe56:a0b4"},
|
||||
Tags: []string{"Linux", "Datacenter 4"},
|
||||
Version: "0.1.12",
|
||||
Address: "88.198.189.124:50078",
|
||||
Tunnels: make([]*Tunnel, 0),
|
||||
ClientAuthID: "client-5",
|
||||
DisconnectedAt: &c4DisconnectedTime,
|
||||
}
|
||||
|
||||
// shallowCopy is used only in tests.
|
||||
func shallowCopy(c *Client) *Client {
|
||||
if c == nil {
|
||||
@ -129,21 +177,31 @@ func shallowCopy(c *Client) *Client {
|
||||
}
|
||||
|
||||
return &Client{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
OS: c.OS,
|
||||
OSArch: c.OSArch,
|
||||
OSFamily: c.OSFamily,
|
||||
OSKernel: c.OSKernel,
|
||||
Hostname: c.Hostname,
|
||||
IPv4: append([]string{}, c.IPv4...),
|
||||
IPv6: append([]string{}, c.IPv6...),
|
||||
Tags: append([]string{}, c.Tags...),
|
||||
Version: c.Version,
|
||||
Address: c.Address,
|
||||
Tunnels: append([]*Tunnel{}, c.Tunnels...),
|
||||
DisconnectedAt: c.DisconnectedAt,
|
||||
ClientAuthID: c.ClientAuthID,
|
||||
NumCPUs: c.NumCPUs,
|
||||
MemoryTotal: c.MemoryTotal,
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
OS: c.OS,
|
||||
OSArch: c.OSArch,
|
||||
OSFamily: c.OSFamily,
|
||||
OSKernel: c.OSKernel,
|
||||
OSFullName: c.OSFullName,
|
||||
OSVersion: c.OSVersion,
|
||||
OSVirtualizationSystem: c.OSVirtualizationSystem,
|
||||
OSVirtualizationRole: c.OSVirtualizationRole,
|
||||
CPUFamily: c.CPUFamily,
|
||||
CPUModel: c.CPUModel,
|
||||
CPUModelName: c.CPUModelName,
|
||||
Timezone: c.Timezone,
|
||||
Hostname: c.Hostname,
|
||||
IPv4: append([]string{}, c.IPv4...),
|
||||
IPv6: append([]string{}, c.IPv6...),
|
||||
Tags: append([]string{}, c.Tags...),
|
||||
Version: c.Version,
|
||||
Address: c.Address,
|
||||
Tunnels: append([]*Tunnel{}, c.Tunnels...),
|
||||
DisconnectedAt: c.DisconnectedAt,
|
||||
ClientAuthID: c.ClientAuthID,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -97,18 +97,28 @@ func convertToSqlite(v *Client) *clientSqlite {
|
||||
ID: v.ID,
|
||||
ClientAuthID: v.ClientAuthID,
|
||||
Details: &clientDetails{
|
||||
Name: v.Name,
|
||||
OS: v.OS,
|
||||
OSArch: v.OSArch,
|
||||
OSFamily: v.OSFamily,
|
||||
OSKernel: v.OSKernel,
|
||||
Hostname: v.Hostname,
|
||||
Version: v.Version,
|
||||
Address: v.Address,
|
||||
IPv4: v.IPv4,
|
||||
IPv6: v.IPv6,
|
||||
Tags: v.Tags,
|
||||
Tunnels: v.Tunnels,
|
||||
Name: v.Name,
|
||||
OS: v.OS,
|
||||
OSArch: v.OSArch,
|
||||
OSFamily: v.OSFamily,
|
||||
OSKernel: v.OSKernel,
|
||||
Hostname: v.Hostname,
|
||||
Version: v.Version,
|
||||
Address: v.Address,
|
||||
OSFullName: v.OSFullName,
|
||||
OSVersion: v.OSVersion,
|
||||
OSVirtualizationSystem: v.OSVirtualizationSystem,
|
||||
OSVirtualizationRole: v.OSVirtualizationRole,
|
||||
CPUFamily: v.CPUFamily,
|
||||
CPUModel: v.CPUModel,
|
||||
CPUModelName: v.CPUModelName,
|
||||
NumCPUs: v.NumCPUs,
|
||||
MemoryTotal: v.MemoryTotal,
|
||||
Timezone: v.Timezone,
|
||||
IPv4: v.IPv4,
|
||||
IPv6: v.IPv6,
|
||||
Tags: v.Tags,
|
||||
Tunnels: v.Tunnels,
|
||||
},
|
||||
}
|
||||
if v.DisconnectedAt != nil {
|
||||
@ -125,18 +135,28 @@ type clientSqlite struct {
|
||||
}
|
||||
|
||||
type clientDetails struct {
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
Hostname string `json:"hostname"`
|
||||
Version string `json:"version"`
|
||||
Address string `json:"address"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Tunnels []*Tunnel `json:"tunnels"`
|
||||
NumCPUs int `json:"num_cpus"`
|
||||
MemoryTotal uint64 `json:"mem_total"`
|
||||
Name string `json:"name"`
|
||||
OS string `json:"os"`
|
||||
OSArch string `json:"os_arch"`
|
||||
OSFamily string `json:"os_family"`
|
||||
OSKernel string `json:"os_kernel"`
|
||||
OSFullName string `json:"os_full_name"`
|
||||
OSVersion string `json:"os_version"`
|
||||
OSVirtualizationSystem string `json:"os_virtualization_system"`
|
||||
OSVirtualizationRole string `json:"os_virtualization_role"`
|
||||
CPUFamily string `json:"cpu_family"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
CPUModelName string `json:"cpu_model_name"`
|
||||
Timezone string `json:"timezone"`
|
||||
Hostname string `json:"hostname"`
|
||||
Version string `json:"version"`
|
||||
Address string `json:"address"`
|
||||
IPv4 []string `json:"ipv4"`
|
||||
IPv6 []string `json:"ipv6"`
|
||||
Tags []string `json:"tags"`
|
||||
Tunnels []*Tunnel `json:"tunnels"`
|
||||
}
|
||||
|
||||
func (d *clientDetails) Scan(value interface{}) error {
|
||||
@ -168,20 +188,30 @@ func (d *clientDetails) Value() (driver.Value, error) {
|
||||
func (s *clientSqlite) convert() *Client {
|
||||
d := s.Details
|
||||
res := &Client{
|
||||
ID: s.ID,
|
||||
ClientAuthID: s.ClientAuthID,
|
||||
Name: d.Name,
|
||||
OS: d.OS,
|
||||
OSArch: d.OSArch,
|
||||
OSFamily: d.OSFamily,
|
||||
OSKernel: d.OSKernel,
|
||||
Hostname: d.Hostname,
|
||||
IPv4: d.IPv4,
|
||||
IPv6: d.IPv6,
|
||||
Tags: d.Tags,
|
||||
Version: d.Version,
|
||||
Address: d.Address,
|
||||
Tunnels: d.Tunnels,
|
||||
ID: s.ID,
|
||||
ClientAuthID: s.ClientAuthID,
|
||||
Name: d.Name,
|
||||
OS: d.OS,
|
||||
OSArch: d.OSArch,
|
||||
OSFamily: d.OSFamily,
|
||||
OSKernel: d.OSKernel,
|
||||
Hostname: d.Hostname,
|
||||
IPv4: d.IPv4,
|
||||
IPv6: d.IPv6,
|
||||
Tags: d.Tags,
|
||||
Version: d.Version,
|
||||
Address: d.Address,
|
||||
Tunnels: d.Tunnels,
|
||||
OSFullName: d.OSFullName,
|
||||
OSVersion: d.OSVersion,
|
||||
OSVirtualizationSystem: d.OSVirtualizationSystem,
|
||||
OSVirtualizationRole: d.OSVirtualizationRole,
|
||||
CPUFamily: d.CPUFamily,
|
||||
CPUModel: d.CPUModel,
|
||||
CPUModelName: d.CPUModelName,
|
||||
NumCPUs: d.NumCPUs,
|
||||
MemoryTotal: d.MemoryTotal,
|
||||
Timezone: d.Timezone,
|
||||
}
|
||||
if s.DisconnectedAt.Valid {
|
||||
res.DisconnectedAt = &s.DisconnectedAt.Time
|
||||
|
||||
@ -44,7 +44,7 @@ func NewManager(db DbProvider, ex *Executor, logger *chshare.Logger) *Manager {
|
||||
}
|
||||
|
||||
func (m *Manager) List(ctx context.Context, re *http.Request) ([]Script, error) {
|
||||
listOptions := query.ConvertGetParamsToFilterOptions(re)
|
||||
listOptions := query.GetSortAndFilterOptions(re)
|
||||
|
||||
err := query.ValidateListOptions(listOptions, supportedFields)
|
||||
if err != nil {
|
||||
|
||||
@ -108,6 +108,7 @@ func NewServer(config *Config, filesAPI files.FileAPI) (*Server, error) {
|
||||
ports.NewPortDistributor(config.AllowedPorts()),
|
||||
s.clientProvider,
|
||||
keepLostClients,
|
||||
s.Logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -253,7 +253,7 @@ func (m *Manager) List(ctx context.Context, re *http.Request) ([]ValueKey, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listOptions := query.ConvertGetParamsToFilterOptions(re)
|
||||
listOptions := query.GetSortAndFilterOptions(re)
|
||||
|
||||
err = query.ValidateListOptions(listOptions, supportedFields)
|
||||
if err != nil {
|
||||
|
||||
@ -7,18 +7,28 @@ import (
|
||||
|
||||
// ConnectionRequest represents configuration options when initiating client-server connection
|
||||
type ConnectionRequest struct {
|
||||
Version string
|
||||
ID string
|
||||
Name string
|
||||
OS string
|
||||
OSArch string
|
||||
OSFamily string
|
||||
OSKernel string
|
||||
Hostname string
|
||||
IPv4 []string
|
||||
IPv6 []string
|
||||
Tags []string
|
||||
Remotes []*Remote
|
||||
ID string
|
||||
Name string
|
||||
OS string
|
||||
OSFullName string
|
||||
OSVersion string
|
||||
OSVirtualizationSystem string
|
||||
OSVirtualizationRole string
|
||||
OSArch string
|
||||
OSFamily string
|
||||
OSKernel string
|
||||
Version string
|
||||
Hostname string
|
||||
CPUFamily string
|
||||
CPUModel string
|
||||
CPUModelName string
|
||||
NumCPUs int
|
||||
MemoryTotal uint64
|
||||
Timezone string
|
||||
IPv4 []string
|
||||
IPv6 []string
|
||||
Tags []string
|
||||
Remotes []*Remote
|
||||
}
|
||||
|
||||
func DecodeConnectionRequest(b []byte) (*ConnectionRequest, error) {
|
||||
|
||||
@ -24,14 +24,14 @@ type ListOptions struct {
|
||||
Filters []FilterOption
|
||||
}
|
||||
|
||||
func ConvertGetParamsToFilterOptions(req *http.Request) *ListOptions {
|
||||
func GetSortAndFilterOptions(req *http.Request) *ListOptions {
|
||||
return &ListOptions{
|
||||
Sorts: extractSortOptions(req),
|
||||
Filters: extractFilterOptions(req),
|
||||
Sorts: ExtractSortOptions(req),
|
||||
Filters: ExtractFilterOptions(req),
|
||||
}
|
||||
}
|
||||
|
||||
func extractSortOptions(req *http.Request) []SortOption {
|
||||
func ExtractSortOptions(req *http.Request) []SortOption {
|
||||
res := make([]SortOption, 0)
|
||||
query := req.URL.Query()
|
||||
|
||||
@ -61,23 +61,13 @@ func extractSortOptions(req *http.Request) []SortOption {
|
||||
return res
|
||||
}
|
||||
|
||||
func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error {
|
||||
func ValidateFilterOptions(fo []FilterOption, supportedFields map[string]bool) errors2.APIErrors {
|
||||
errs := errors2.APIErrors{}
|
||||
for i := range lo.Sorts {
|
||||
ok := supportedFields[lo.Sorts[i].Column]
|
||||
for i := range fo {
|
||||
ok := supportedFields[fo[i].Column]
|
||||
if !ok {
|
||||
errs = append(errs, errors2.APIError{
|
||||
Message: fmt.Sprintf("unsupported sort field '%s'", lo.Sorts[i].Column),
|
||||
Code: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for i := range lo.Filters {
|
||||
ok := supportedFields[lo.Filters[i].Column]
|
||||
if !ok {
|
||||
errs = append(errs, errors2.APIError{
|
||||
Message: fmt.Sprintf("unsupported filter field '%s'", lo.Filters[i].Column),
|
||||
Message: fmt.Sprintf("unsupported filter field '%s'", fo[i].Column),
|
||||
Code: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
@ -90,7 +80,45 @@ func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractFilterOptions(req *http.Request) []FilterOption {
|
||||
func ValidateSortOptions(so []SortOption, supportedFields map[string]bool) errors2.APIErrors {
|
||||
errs := errors2.APIErrors{}
|
||||
for i := range so {
|
||||
ok := supportedFields[so[i].Column]
|
||||
if !ok {
|
||||
errs = append(errs, errors2.APIError{
|
||||
Message: fmt.Sprintf("unsupported sort field '%s'", so[i].Column),
|
||||
Code: http.StatusBadRequest,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateListOptions(lo *ListOptions, supportedFields map[string]bool) error {
|
||||
errs := errors2.APIErrors{}
|
||||
sortErrs := ValidateSortOptions(lo.Sorts, supportedFields)
|
||||
if sortErrs != nil {
|
||||
errs = append(errs, sortErrs...)
|
||||
}
|
||||
|
||||
filterErrs := ValidateFilterOptions(lo.Filters, supportedFields)
|
||||
if filterErrs != nil {
|
||||
errs = append(errs, filterErrs...)
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExtractFilterOptions(req *http.Request) []FilterOption {
|
||||
res := make([]FilterOption, 0)
|
||||
for filterKey, filterValues := range req.URL.Query() {
|
||||
if !strings.HasPrefix(filterKey, "filter") || len(filterValues) == 0 {
|
||||
|
||||
@ -67,7 +67,7 @@ func TestConvertGetParamsToFilterOptions(t *testing.T) {
|
||||
URL: inputURL,
|
||||
}
|
||||
|
||||
actualListOptions := ConvertGetParamsToFilterOptions(req)
|
||||
actualListOptions := GetSortAndFilterOptions(req)
|
||||
|
||||
assert.ElementsMatch(t, testCases[i].expectedListOptions.Sorts, actualListOptions.Sorts)
|
||||
assert.ElementsMatch(t, testCases[i].expectedListOptions.Filters, actualListOptions.Filters)
|
||||
|
||||
BIN
vault.sqlite
BIN
vault.sqlite
Binary file not shown.
Loading…
Reference in New Issue
Block a user