Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
Signed-off-by: Lucas Bremgartner <[email protected]>
  • Loading branch information
breml authored and stgraber committed Nov 27, 2024
1 parent 664c04a commit 100dc8f
Show file tree
Hide file tree
Showing 7 changed files with 106 additions and 7 deletions.
36 changes: 31 additions & 5 deletions cmd/incusd/instance_console.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,12 @@ func instanceConsolePost(d *Daemon, r *http.Request) response.Response {
// description: Project name
// type: string
// example: default
// - in: query
// name: type
// description: Console type
// type: string
// enum: [log, vga]
// example: vga
// responses:
// "200":
// description: Raw console log
Expand All @@ -554,6 +560,10 @@ func instanceConsolePost(d *Daemon, r *http.Request) response.Response {
// schema:
// type: string
// example: some-text
// image/png:
// schema:
// type: string
// format: binary
// "400":
// $ref: "#/responses/BadRequest"
// "403":
Expand All @@ -576,6 +586,11 @@ func instanceConsoleLogGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(err)
}

consoleLogType := request.QueryParam(r, "type")
if consoleLogType != "" && consoleLogType != "log" && consoleLogType != "vga" {
return response.SmartError(fmt.Errorf("Invalid value for type parameter: %s", consoleLogType))
}

if internalInstance.IsSnapshot(name) {
return response.BadRequest(fmt.Errorf("Invalid instance name"))
}
Expand Down Expand Up @@ -649,14 +664,25 @@ func instanceConsoleLogGet(d *Daemon, r *http.Request) response.Response {
return response.SmartError(fmt.Errorf("Failed to cast inst to VM"))
}

logContents, err := v.ConsoleLog()
if err != nil {
return response.SmartError(err)
if consoleLogType == "vga" {
screenshot, err := v.ConsoleScreenshot()
if err != nil {
return response.SmartError(err)
}

ent.File = bytes.NewReader(screenshot)
ent.FileSize = int64(len(screenshot))
} else {
logContents, err := v.ConsoleLog()
if err != nil {
return response.SmartError(err)
}

ent.File = bytes.NewReader([]byte(logContents))
ent.FileSize = int64(len(logContents))
}

ent.File = bytes.NewReader([]byte(logContents))
ent.FileModified = time.Now()
ent.FileSize = int64(len(logContents))

return response.FileResponse(r, []response.FileResponseEntry{ent}, nil)
}
Expand Down
4 changes: 4 additions & 0 deletions doc/api-extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2660,3 +2660,7 @@ The following configuration options have been added:
## `storage_live_migration`

This adds support for virtual-machines live-migration between storage pools.

## `instance_console_screenshot`

This adds support to take screenshots of the current VGA console of a VM.
42 changes: 41 additions & 1 deletion internal/server/instance/drivers/driver_qemu.go
Original file line number Diff line number Diff line change
Expand Up @@ -9371,7 +9371,7 @@ func (d *qemu) ConsoleLog() (string, error) {

// If we got data back, append it to the log file for this instance.
if logString != "" {
logFile, err := os.OpenFile(d.common.ConsoleBufferLogPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
logFile, err := os.OpenFile(d.common.ConsoleBufferLogPath(), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o600)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -9445,3 +9445,43 @@ func (d *qemu) consoleSwapSocketWithRB() error {

return monitor.ChardevChange("console", qmp.ChardevChangeInfo{Type: "ringbuf"})
}

// ConsoleScreenshot returns a screenshot of the current VGA console in PNG format.
func (d *qemu) ConsoleScreenshot() ([]byte, error) {
if !d.IsRunning() {
return nil, fmt.Errorf("Instance is not running")
}

// Check if the agent is running.
monitor, err := qmp.Connect(d.monitorPath(), qemuSerialChardevName, d.getMonitorEventHandler(), d.QMPLogFilePath())
if err != nil {
return nil, err
}

// Create a target screenshot file.
screenshotPath := filepath.Join(d.Path(), "screenshot.png")
f, err := os.Create(screenshotPath)
if err != nil {
return nil, fmt.Errorf("Couldn't create screenshot path")
}

defer f.Close()

err = f.Chown(int(d.state.OS.UnprivUID), -1)
if err != nil {
return nil, fmt.Errorf("Failed to chown screenshot path: %w", err)
}

// Take the screenshot.
err = monitor.Screendump(filepath.Join(d.Path(), "screenshot.png"))
if err != nil {
return nil, fmt.Errorf("Failed taking screenshot: %w", err)
}

screenshot, err := os.ReadFile(filepath.Join(d.Path(), "screenshot.png"))
if err != nil {
return nil, fmt.Errorf("Failed to read screenshot: %w", err)
}

return screenshot, nil
}
23 changes: 23 additions & 0 deletions internal/server/instance/drivers/qmp/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -1274,3 +1274,26 @@ func (m *Monitor) ChardevChange(device string, info ChardevChangeInfo) error {

return fmt.Errorf("Unsupported chardev type %q", info.Type)
}

// Screendump takes a screenshot of the current VGA console.
// The screendump is stored to the filename provided as argument.
//
// In order to have access to the screendump from the outside of the VM
// one needs to first send a file and then pass /proc/self/fd/NUMBER
// as the filename to this function.
func (m *Monitor) Screendump(filename string) error {
var args struct {
Filename string `json:"filename"`
Device string `json:"device,omitempty"`
Head int `json:"head,omitempty"`
Format string `json:"format,omitempty"`
}
args.Filename = filename
args.Format = "png"

var queryResp struct {
Return struct{} `json:"return"`
}

return m.Run("screendump", args, &queryResp)
}
1 change: 1 addition & 0 deletions internal/server/instance/instance_interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ type VM interface {

AgentCertificate() *x509.Certificate
ConsoleLog() (string, error)
ConsoleScreenshot() ([]byte, error)
}

// CriuMigrationArgs arguments for CRIU migration.
Expand Down
6 changes: 5 additions & 1 deletion internal/server/response/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,11 @@ func (r *fileResponse) Render(w http.ResponseWriter) error {
rs = f
}

w.Header().Set("Content-Type", "application/octet-stream")
// Only set Content-Type header if not yet set.
if w.Header().Get("Content-Type") == "" {
w.Header().Set("Content-Type", "application/octet-stream")
}

w.Header().Set("Content-Length", fmt.Sprintf("%d", sz))
w.Header().Set("Content-Disposition", fmt.Sprintf("inline;filename=%s", r.files[0].Filename))

Expand Down
1 change: 1 addition & 0 deletions internal/version/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,7 @@ var APIExtensions = []string{
"custom_volume_refresh_exclude_older_snapshots",
"storage_initial_owner",
"storage_live_migration",
"instance_console_screenshot",
}

// APIExtensionsCount returns the number of available API extensions.
Expand Down

0 comments on commit 100dc8f

Please sign in to comment.