Skip to content

Commit

Permalink
Add ssh client and device inspection
Browse files Browse the repository at this point in the history
  • Loading branch information
bkuen committed Dec 26, 2023
1 parent aca1541 commit 9292202
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 70 deletions.
51 changes: 51 additions & 0 deletions internal/pkg/inspect/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package inspect

import (
"bwizard/internal/pkg/ssh"
ssh2 "golang.org/x/crypto/ssh"
)

type DeviceInspection struct {
Hostname string
CPU string
Ram string
}

// Device inspects a device regarding hostname, cpu, memory and disk size
func Device(client *ssh.Client) (*DeviceInspection, error) {
session, err := client.NewSession()
if err != nil {
return nil, err
}

defer func(session *ssh2.Session) {
err := session.Close()
if err != nil {
panic(err)
}
}(session)

hostnameCmd := "hostname"
hostnameOutput, err := session.CombinedOutput(hostnameCmd)
if err != nil {
return nil, err
}

cpuCmd := "lscpu | grep 'Model name' | cut -f 2 -d \":\" | awk '{$1=$1}1'"
cpuOutput, err := session.CombinedOutput(cpuCmd)
if err != nil {
return nil, err
}

ramCmd := "cat /proc/meminfo | grep 'MemTotal' | cut -f 2 -d \":\" | awk '{$1=$1}1'"
ramOutput, err := session.CombinedOutput(ramCmd)
if err != nil {
return nil, err
}

return &DeviceInspection{
Hostname: string(hostnameOutput),
CPU: string(cpuOutput),
Ram: string(ramOutput),
}, nil
}
74 changes: 74 additions & 0 deletions internal/pkg/ssh/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package ssh

import (
"golang.org/x/crypto/ssh"
"net"
"os"
"strconv"
)

type Credentials struct {
Username string
PrivateKeyPath string
}

type Client struct {
host string
port uint16
config *ssh.ClientConfig
client *ssh.Client
}

// NewClient returns a new SSH client
func NewClient(host string, port uint16, credentials *Credentials) (*Client, error) {
privateKey, err := os.ReadFile(credentials.PrivateKeyPath)
if err != nil {
return nil, err
}

signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
return nil, err
}

config := &ssh.ClientConfig{
User: credentials.Username,
Auth: []ssh.AuthMethod{
ssh.PublicKeys(signer),
},
// TODO: Only use that for development. Implement a proper host key check
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}

return &Client{
host: host,
port: port,
config: config,
}, nil
}

// Connect starts a SSH session to the server
func (c *Client) Connect() error {
client, err := ssh.Dial("tcp", net.JoinHostPort(c.host, strconv.Itoa(int(c.port))), c.config)
if err != nil {
return err
}

c.client = client

return nil
}

// Close closes the connection to the server
func (c *Client) Close() error {
if c.client == nil {
return nil
}

return c.client.Close()
}

// NewSession returns a new session to run commands in
func (c *Client) NewSession() (*ssh.Session, error) {
return c.client.NewSession()
}
53 changes: 53 additions & 0 deletions internal/pkg/wizard/application/svc/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package device

import (
"bwizard/internal/pkg/inspect"
"bwizard/internal/pkg/ssh"
deviceSvc "bwizard/internal/pkg/wizard/domain/svc/device"
deviceValue "bwizard/internal/pkg/wizard/domain/valueobject/device"
)

type InspectionSvcImpl struct {
}

var _ deviceSvc.InspectionService = &InspectionSvcImpl{}

// Inspect gains information from a backup device such like the operating system or the agent
func (i *InspectionSvcImpl) Inspect(ips []deviceValue.IPAddress) (*deviceValue.Inspection, error) {
credentials := ssh.Credentials{
Username: "root",
}

var clients []*ssh.Client
for _, ip := range ips {

client, err := ssh.NewClient(string(ip), 22, &credentials)
if err != nil {
return nil, err
}

if err := client.Connect(); err != nil {
continue
}

clients = append(clients, client)

inspection, err := inspect.Device(client)
if err != nil {
return nil, err
}

return deviceValue.NewInspectionFromDeviceInspection(inspection), nil
}

defer func(clients []*ssh.Client) {
for _, client := range clients {
err := client.Close()
if err != nil {
panic(err)
}
}
}(clients)

return nil, nil
}
38 changes: 0 additions & 38 deletions internal/pkg/wizard/domain/entity/device.go

This file was deleted.

19 changes: 19 additions & 0 deletions internal/pkg/wizard/domain/entity/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package device

import (
"bwizard/internal/pkg/wizard/domain/valueobject/device"
"github.com/google/uuid"
"time"
)

type Device struct {
ID uuid.UUID `db:"id"`
Name string `db:"name"`
Kind device.Kind `db:"kind"`
Protection device.ProtectionStatus `db:"protection"`
LastBackup *time.Time `db:"last_backup" goqu:"omitempty"`
IPs []device.IPAddress `db:"ips"`
Agent string `db:"agent"`
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}
12 changes: 0 additions & 12 deletions internal/pkg/wizard/domain/repo/device.go

This file was deleted.

12 changes: 12 additions & 0 deletions internal/pkg/wizard/domain/repo/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package device

import (
"bwizard/internal/pkg/wizard/domain/entity/device"
"context"
"github.com/google/uuid"
)

type Repository interface {
GetDeviceByID(ctx context.Context, id uuid.UUID) (*device.Device, error)
SaveDevice(ctx context.Context, device *device.Device) error
}
9 changes: 9 additions & 0 deletions internal/pkg/wizard/domain/svc/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package device

import (
"bwizard/internal/pkg/wizard/domain/valueobject/device"
)

type InspectionService interface {
Inspect(ips []device.IPAddress) (*device.Inspection, error)
}
39 changes: 39 additions & 0 deletions internal/pkg/wizard/domain/valueobject/device/device.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package device

import "bwizard/internal/pkg/inspect"

type ProtectionStatus string
type Kind string
type IPAddress string

const (
ProtectionStatusOk ProtectionStatus = "ok"
ProtectionStatusWarning ProtectionStatus = "warning"
ProtectionStatusError ProtectionStatus = "error"

KindServer Kind = "server"
KindComputer Kind = "computer"
KindMobile Kind = "mobile"
)

type OperatingSystem struct {
Name string `db:"name"`
CPU string `db:"cpu"`
Ram string `db:"ram"`
TotalHardDrive string `db:"ram"`
}

type Inspection struct {
Hostname string `db:"hostname"`
CPU string `db:"cpu"`
Ram string `db:"ram"`
}

// NewInspectionFromDeviceInspection returns a new domain inspection from inspect.DeviceInspection
func NewInspectionFromDeviceInspection(inspection *inspect.DeviceInspection) *Inspection {
return &Inspection{
Hostname: inspection.Hostname,
CPU: inspection.CPU,
Ram: inspection.Ram,
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package repo
package device

import (
"bwizard/internal/pkg/wizard/domain/entity"
"bwizard/internal/pkg/wizard/domain/repo"
"bwizard/internal/pkg/wizard/domain/entity/device"
device2 "bwizard/internal/pkg/wizard/domain/repo/device"
"context"
"database/sql"
"errors"
Expand All @@ -11,54 +11,54 @@ import (
)

const (
DeviceDialect = "mysql"
DeviceTableName = "devices"
Dialect = "mysql"
TableName = "devices"
)

type DeviceRepoImpl struct {
type Impl struct {
db *sql.DB
}

var _ repo.DeviceRepository = &DeviceRepoImpl{}
var _ device2.Repository = &Impl{}

// NewDeviceRepository returns a new mysql device repository
func NewDeviceRepository(db *sql.DB) *DeviceRepoImpl {
return &DeviceRepoImpl{
// NewRepository returns a new mysql device repository
func NewRepository(db *sql.DB) *Impl {
return &Impl{
db: db,
}
}

// GetDeviceByID returns the device matching the given id from the database or nil
// if the device doesn't exist
func (i *DeviceRepoImpl) GetDeviceByID(ctx context.Context, id uuid.UUID) (*entity.Device, error) {
func (i *Impl) GetDeviceByID(ctx context.Context, id uuid.UUID) (*device.Device, error) {
query, _, err := goqu.
Dialect(DeviceDialect).
From(DeviceTableName).
Select(&entity.Device{}).
Dialect(Dialect).
From(TableName).
Select(&device.Device{}).
ToSQL()

if err != nil {
return nil, err
}

var device entity.Device
var deviceEntity device.Device
row := i.db.QueryRowContext(ctx, query)

switch rowErr := row.Scan(&device); {
switch rowErr := row.Scan(&deviceEntity); {
case errors.Is(rowErr, sql.ErrNoRows):
return nil, nil
case rowErr == nil:
return &device, nil
return &deviceEntity, nil
default:
return nil, rowErr
}
}

// SaveDevice inserts or updates a device in the database
func (i *DeviceRepoImpl) SaveDevice(ctx context.Context, device *entity.Device) error {
func (i *Impl) SaveDevice(ctx context.Context, device *device.Device) error {
insertSQL, _, _ := goqu.
Dialect(DeviceDialect).
Insert(DeviceTableName).
Dialect(Dialect).
Insert(TableName).
Rows(device).
OnConflict(goqu.DoUpdate("key", goqu.Record{"updated_at": goqu.L("NOW()")})).
ToSQL()
Expand Down

0 comments on commit 9292202

Please sign in to comment.