Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ DUT agent on it. Below you can find the currently supported modules with example
| [GPIO Button](./pkg/module/gpio/README.md) | :white_check_mark: |
| [GPIO Switch](./pkg/module/gpio/README.md) | :white_check_mark: |
| [IPMI Power Control](./pkg/module/ipmi/README.md) | :white_check_mark: |
| [PiKVM](./pkg/module/pikvm/README.md) | :white_check_mark: |
| [Power Distribution Unit (Intellinet)](./pkg/module/pdu/README.md) | :white_check_mark: |
| Power Distribution Unit (Delock) | :hourglass_flowing_sand: |
| [SPI Flash Emulator](./pkg/module/flash-emulate/README.md) | :white_check_mark: |
Expand Down
1 change: 1 addition & 0 deletions cmds/dutagent/modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
_ "github.com/BlindspotSoftware/dutctl/pkg/module/gpio"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/ipmi"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/pdu"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/pikvm"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/serial"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/shell"
_ "github.com/BlindspotSoftware/dutctl/pkg/module/ssh"
Expand Down
78 changes: 70 additions & 8 deletions cmds/dutctl/dutctl.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package main

import (
"crypto/sha256"
"crypto/tls"
"errors"
"flag"
Expand All @@ -16,6 +17,8 @@ import (
"net"
"net/http"
"os"
"strconv"
"strings"

"connectrpc.com/connect"
"github.com/BlindspotSoftware/dutctl/internal/buildinfo"
Expand Down Expand Up @@ -176,16 +179,18 @@ func (app *application) start() {
app.exit(errInvalidCmdline)
}

err := app.listRPC()
app.exit(err)
app.exit(app.listRPC())
}

if len(app.args) == 1 {
device := app.args[0]
err := app.commandsRPC(device)
app.exit(err)
app.exit(app.commandsRPC(app.args[0]))
}

app.dispatchDeviceCommand()
}

// dispatchDeviceCommand handles the "device command [args...]" invocation form.
func (app *application) dispatchDeviceCommand() {
device := app.args[0]
command := app.args[1]
cmdArgs := app.args[2:]
Expand All @@ -198,12 +203,16 @@ func (app *application) start() {
}

if len(cmdArgs) > 0 && cmdArgs[0] == "help" {
err := app.detailsRPC(device, command, "help")
app.exit(app.detailsRPC(device, command, "help"))
}

// Preprocess arguments for optimization (e.g., calculate hashes)
cmdArgs, err := app.preprocessArgs(command, cmdArgs)
if err != nil {
app.exit(err)
}

err := app.runRPC(device, command, cmdArgs)
app.exit(err)
app.exit(app.runRPC(device, command, cmdArgs))
}

// exit terminates the application. If the provided error is not nil, it is printed to
Expand Down Expand Up @@ -235,6 +244,59 @@ func (app *application) exit(err error) {
app.exitFunc(1)
}

// preprocessArgs preprocesses command arguments for optimization.
// For example, it calculates file hashes for PiKVM media mount commands.
func (app *application) preprocessArgs(command string, args []string) ([]string, error) {
// Argument counts for "media mount <path> [hash] [size]".
const (
mediaMountMinArgs = 2 // mount + path
mediaMountWithHash = 3 // mount + path + hash
)

// Optimize PiKVM media mount: calculate hash locally to avoid unnecessary transfers
if strings.ToLower(command) == "media" && len(args) >= mediaMountMinArgs && strings.ToLower(args[0]) == "mount" {
imagePath := args[1]

// If hash is already provided (args[2]), skip preprocessing
if len(args) >= mediaMountWithHash {
return args, nil
}

// Check if file exists
fileInfo, err := os.Stat(imagePath)
if err != nil {
// If file doesn't exist locally, let the agent handle the error
return args, nil
}

// Calculate SHA256 hash
log.Printf("Calculating SHA256 hash of %s...", imagePath)

file, err := os.Open(imagePath)
if err != nil {
return args, fmt.Errorf("failed to open file for hashing: %w", err)
}
defer file.Close()

hash := sha256.New()

_, err = io.Copy(hash, file)
if err != nil {
return args, fmt.Errorf("failed to calculate hash: %w", err)
}

hashSum := fmt.Sprintf("%x", hash.Sum(nil))
fileSize := fileInfo.Size()

log.Printf("Hash: %s, Size: %d bytes", hashSum, fileSize)

// Append hash and size to arguments
return append(args, hashSum, strconv.FormatInt(fileSize, 10)), nil
}

return args, nil
}

func (app *application) printVersion() {
app.formatter.WriteContent(output.Content{
Type: output.TypeVersion,
Expand Down
97 changes: 97 additions & 0 deletions pkg/module/pikvm/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# PiKVM

This module provides comprehensive control of a DUT via a PiKVM device. It offers power management through ATX control, keyboard input simulation, and virtual media mounting capabilities.
Comment thread
llogen marked this conversation as resolved.

PiKVM documentation: https://docs.pikvm.org/

Compatibility: This module uses PiKVM's HTTP API endpoints (e.g. `/api/atx/*`, `/api/hid/*`, `/api/msd/*`, `/api/streamer/snapshot`). It is intended for PiKVM (kvmd/PiKVM OS) systems, and should work with other devices only if they expose a compatible API.

## Features

### Power Management
Control the DUT's power state via ATX power and reset buttons:

- Power commands use PiKVM's ATX control API:
- `on` is idempotent (does nothing if already powered on)
- `off` performs a graceful shutdown via power button press
- `force-off` performs a hard power off via long press (5+ seconds)
- `reset` triggers an ATX reset button press
- `force-reset` triggers a hardware hot reset

```
COMMANDS:
on Power on (does nothing if already on)
off Graceful shutdown (soft power-off)
force-off Force power off (hard shutdown, 5+ second press)
reset Reset via ATX reset button
force-reset Force reset (hardware hot reset)
status Query current power state
```

### Keyboard Control
Send keyboard input to the DUT:

See [pikvm-key-strings.md](./pikvm-key-strings.md) for the full list of supported key strings.

```
COMMANDS:
type <text> Type a text string
key <keyname> Send a single key (e.g., Enter, Escape, F12)
key-combo <keys> Send key combination (e.g., Ctrl+Alt+Delete)
Comment thread
llogen marked this conversation as resolved.

FLAGS (must come before the action):
--delay <duration> Delay between key events for key-combo (default: 500ms)
```

### Virtual Media
Mount ISO images or disk images as virtual USB devices:

- `mount` uploads images to PiKVM's storage (with automatic space management)
- `mount-url` instructs PiKVM to download the image directly from the URL
- Old images are automatically deleted when storage space is needed
- SHA256 hashing prevents duplicate uploads

```
COMMANDS:
mount <path> Mount an image file from the agent's filesystem
mount-url <url> Mount an image from a URL
unmount Unmount current virtual media
media-status Show mounted media information
```

### Screenshot
Capture the current display output from PiKVM's video stream.

## Configuration Options

| Option | Type | Default | Description |
| -------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| host | string | - | Address of the PiKVM device (e.g., "192.168.1.100") Defaults to HTTPS if no scheme is specified, HTTP can be used by explicitly specifying `http://` |
| user | string | admin | Username for authentication |
| password | string | - | Password for authentication |
| timeout | string | 10s | Timeout for HTTP requests (e.g., "10s", "30s") |
| mode | string | - | **Required**: Mode ("power", "keyboard", "media", "screenshot") |

⚠️ **Security Warning**: Passwords are stored in plaintext in the configuration file. This should only be used in trusted environments.

## Usage Examples

See [pikvm-example-cfg.yml](./pikvm-example-cfg.yml) for comprehensive configuration examples.

### Basic Power Control
See [pikvm-example-power.yml](./pikvm-example-power.yml).

### Boot Menu Access
See [pikvm-example-keyboard.yml](./pikvm-example-keyboard.yml).

### ISO Mounting
See [pikvm-example-media.yml](./pikvm-example-media.yml).

### Screenshot Capture
See [pikvm-example-screenshot.yml](./pikvm-example-screenshot.yml).

## Requirements

- PiKVM device with API access enabled
- Network connectivity between dutagent and PiKVM
- Valid authentication credentials
Loading