Skip to content

Commit 4e04fe1

Browse files
committed
feat: add PiKVM module
The PiKVM 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. Power management commands: on, off, force-off, reset, force-reset, and status. Keyboard control commands: type, key, combo, and paste. Virtual media commands: mount, mount-url, unmount, and media-status. The module connects to a PiKVM device using HTTP/HTTPS with configurable host, user, password, and timeout. It supports both short and long ATX button presses for power and reset control, allows sending keyboard input and key combinations, and enables mounting ISO images from local files or URLs. Signed-off-by: llogen <[email protected]>
1 parent 13a4ace commit 4e04fe1

File tree

8 files changed

+1137
-0
lines changed

8 files changed

+1137
-0
lines changed

cmds/dutagent/modules.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
_ "github.com/BlindspotSoftware/dutctl/pkg/module/gpio"
1919
_ "github.com/BlindspotSoftware/dutctl/pkg/module/ipmi"
2020
_ "github.com/BlindspotSoftware/dutctl/pkg/module/pdu"
21+
_ "github.com/BlindspotSoftware/dutctl/pkg/module/pikvm"
2122
_ "github.com/BlindspotSoftware/dutctl/pkg/module/serial"
2223
_ "github.com/BlindspotSoftware/dutctl/pkg/module/shell"
2324
_ "github.com/BlindspotSoftware/dutctl/pkg/module/ssh"

pkg/module/pikvm/README.md

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# PiKVM
2+
3+
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.
4+
5+
## Features
6+
7+
### Power Management
8+
Control the DUT's power state via ATX power and reset buttons:
9+
10+
```
11+
COMMANDS:
12+
on Power on via short ATX power button press
13+
off Graceful shutdown via short ATX power button press
14+
force-off Force power off via long ATX power button press (4-5 seconds)
15+
reset Reset via short ATX reset button press
16+
force-reset Force reset via long ATX reset button press
17+
status Query current power state
18+
```
19+
20+
### Keyboard Control
21+
Send keyboard input to the DUT:
22+
23+
```
24+
COMMANDS:
25+
type <text> Type a text string
26+
key <keyname> Send a single key (e.g., Enter, Escape, F12)
27+
combo <keys> Send key combination (e.g., Ctrl+Alt+Delete)
28+
paste Paste text from stdin
29+
```
30+
31+
### Virtual Media
32+
Mount ISO images or disk images as virtual USB devices:
33+
34+
```
35+
COMMANDS:
36+
mount <path> Mount an image file from the agent's filesystem
37+
mount-url <url> Mount an image from a URL
38+
unmount Unmount current virtual media
39+
media-status Show mounted media information
40+
```
41+
42+
## Configuration Options
43+
44+
| Option | Type | Default | Description |
45+
| -------- | ------ | ------- | -------------------------------------------------------- |
46+
| host | string | - | Address of the PiKVM device (e.g., "192.168.1.100") |
47+
| user | string | admin | Username for authentication |
48+
| password | string | - | Password for authentication |
49+
| timeout | string | 10s | Timeout for HTTP requests (e.g., "10s", "30s") |
50+
51+
⚠️ **Security Warning**: Passwords are stored in plaintext in the configuration file. This should only be used in trusted environments.
52+
53+
## API Endpoints Used
54+
55+
This module interacts with the following PiKVM API endpoints:
56+
57+
- `/api/atx/power` - Power management status
58+
- `/api/atx/click` - ATX button control (power/reset)
59+
- `/api/hid/events/send_key` - Keyboard input simulation
60+
- `/api/msd` - Mass Storage Device (virtual media) control
61+
- `/api/msd/write` - Upload images to PiKVM
62+
- `/api/msd/set_connected` - Mount/unmount media
63+
64+
## Usage Examples
65+
66+
See [pikvm-example-cfg.yml](./pikvm-example-cfg.yml) for comprehensive configuration examples.
67+
68+
### Basic Power Control
69+
70+
```yaml
71+
version: 0
72+
devices:
73+
my-server:
74+
desc: "Server controlled via PiKVM"
75+
cmds:
76+
power-on:
77+
desc: "Power on the server"
78+
modules:
79+
- module: pikvm
80+
main: true
81+
args:
82+
- on
83+
options:
84+
host: https://pikvm.local
85+
user: admin
86+
password: admin
87+
```
88+
89+
### Boot Menu Access
90+
91+
```yaml
92+
enter-bios:
93+
desc: "Press F12 to enter boot menu"
94+
modules:
95+
- module: pikvm
96+
main: true
97+
args:
98+
- key
99+
- F12
100+
options:
101+
host: https://pikvm.local
102+
user: admin
103+
password: admin
104+
```
105+
106+
### ISO Mounting
107+
108+
```yaml
109+
mount-installer:
110+
desc: "Mount Ubuntu installer ISO"
111+
modules:
112+
- module: pikvm
113+
main: true
114+
args:
115+
- mount-url
116+
- https://releases.ubuntu.com/22.04/ubuntu-22.04-live-server-amd64.iso
117+
options:
118+
host: https://pikvm.local
119+
user: admin
120+
password: admin
121+
```
122+
123+
## Requirements
124+
125+
- PiKVM device with API access enabled
126+
- Network connectivity between dutagent and PiKVM
127+
- Valid authentication credentials
128+
129+
## Notes
130+
131+
- The module defaults to HTTPS if no scheme is specified in the host
132+
- HTTP can be used by explicitly specifying `http://` in the host
133+
- Long button presses (force-off, force-reset) hold the button for 4-5 seconds
134+
- Virtual media images are uploaded to PiKVM's storage when using the `mount` command
135+
- The `mount-url` command instructs PiKVM to download the image directly from the URL

pkg/module/pikvm/keyboard.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright 2025 Blindspot Software
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package pikvm
6+
7+
import (
8+
"bytes"
9+
"context"
10+
"encoding/json"
11+
"fmt"
12+
"io"
13+
"net/http"
14+
"strings"
15+
16+
"github.com/BlindspotSoftware/dutctl/pkg/module"
17+
)
18+
19+
// handleKeyboardCommand dispatches keyboard-related commands.
20+
func (p *PiKVM) handleKeyboardCommand(ctx context.Context, s module.Session, command string, args []string) error {
21+
switch command {
22+
case typeCmd:
23+
if len(args) < minArgsRequired {
24+
s.Println("Error: 'type' command requires text argument")
25+
26+
return nil
27+
}
28+
29+
return p.handleType(ctx, s, strings.Join(args[1:], " "))
30+
case key:
31+
if len(args) < minArgsRequired {
32+
s.Println("Error: 'key' command requires key name argument")
33+
34+
return nil
35+
}
36+
37+
return p.handleKey(ctx, s, args[1])
38+
case keyCombo:
39+
if len(args) < minArgsRequired {
40+
s.Println("Error: 'key-combo' command requires key combination argument")
41+
42+
return nil
43+
}
44+
45+
return p.handleCombo(ctx, s, args[1])
46+
case paste:
47+
return p.handlePaste(ctx, s)
48+
default:
49+
return fmt.Errorf("unknown keyboard command: %s", command)
50+
}
51+
}
52+
53+
func (p *PiKVM) handleType(ctx context.Context, s module.Session, text string) error {
54+
payload := map[string]interface{}{
55+
"keys": text,
56+
}
57+
58+
jsonData, err := json.Marshal(payload)
59+
if err != nil {
60+
return err
61+
}
62+
63+
resp, err := p.doRequest(ctx, http.MethodPost, "/api/hid/events/send_key", bytes.NewReader(jsonData), "application/json")
64+
if err != nil {
65+
return fmt.Errorf("failed to type text: %v", err)
66+
}
67+
defer resp.Body.Close()
68+
69+
s.Printf("Typed: %s\n", text)
70+
71+
return nil
72+
}
73+
74+
func (p *PiKVM) handleKey(ctx context.Context, s module.Session, keyName string) error {
75+
payload := map[string]interface{}{
76+
"key": keyName,
77+
}
78+
79+
jsonData, err := json.Marshal(payload)
80+
if err != nil {
81+
return err
82+
}
83+
84+
resp, err := p.doRequest(ctx, http.MethodPost, "/api/hid/events/send_key", bytes.NewReader(jsonData), "application/json")
85+
if err != nil {
86+
return fmt.Errorf("failed to send key: %v", err)
87+
}
88+
defer resp.Body.Close()
89+
90+
s.Printf("Key sent: %s\n", keyName)
91+
92+
return nil
93+
}
94+
95+
func (p *PiKVM) handleCombo(ctx context.Context, s module.Session, comboStr string) error {
96+
// Parse combo like "Ctrl+Alt+Delete" into array of keys
97+
keys := strings.Split(comboStr, "+")
98+
for i, k := range keys {
99+
keys[i] = strings.TrimSpace(k)
100+
}
101+
102+
payload := map[string]interface{}{
103+
"keys": keys,
104+
}
105+
106+
jsonData, err := json.Marshal(payload)
107+
if err != nil {
108+
return err
109+
}
110+
111+
resp, err := p.doRequest(ctx, http.MethodPost, "/api/hid/events/send_key", bytes.NewReader(jsonData), "application/json")
112+
if err != nil {
113+
return fmt.Errorf("failed to send key combo: %v", err)
114+
}
115+
defer resp.Body.Close()
116+
117+
s.Printf("Key combination sent: %s\n", comboStr)
118+
119+
return nil
120+
}
121+
122+
func (p *PiKVM) handlePaste(ctx context.Context, s module.Session) error {
123+
stdin, _, _ := s.Console()
124+
125+
data, err := io.ReadAll(stdin)
126+
if err != nil {
127+
return fmt.Errorf("failed to read stdin: %v", err)
128+
}
129+
130+
if len(data) == 0 {
131+
s.Println("No data to paste")
132+
133+
return nil
134+
}
135+
136+
return p.handleType(ctx, s, string(data))
137+
}

0 commit comments

Comments
 (0)