Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VNC screenshot support on the API and on virtctl #8465

Merged
merged 7 commits into from Sep 26, 2022

Conversation

rmohr
Copy link
Member

@rmohr rmohr commented Sep 13, 2022

What this PR does / why we need it:

Add a GET subresource endpoint virtualmachineinstances/<name>/vnc/screenshot which allows taking VNC screenshots in PNG format in a server-side fashion.

virtctl support is also included:

Support saving VNC screenshots in PNG format to files:

virctl vnc screenshot testvmi -f screenshot.png

Support piping VNC screenshots in PNG format:

virtctl vnc screenshot testvmi -f - | display

Support waking up the VNC screen when e.g. a screensaver hides the actual screen:

virtcl vnc screenshot testvmi --move-cursor -f screenshot.png

This allows addressing the following use-cases:

  • Allow users discovering bluescreens, crashing and stuck VMs while not giving them interactive access to vnc and console.
  • Allow UIs to give a quick debug-view on VMs without having to open heavy-weight VNC connections
  • Allow scripts collecting screenshot for automatic tasks
  • Allow debugging VMs when websocket is disabled due to integration constraints
  • Allow easier debuging of VMs when no interactive display is available and port-forwarding is difficult
  • Allow users to inspect the display, if they have no local VNC client available which we support

Which issue(s) this PR fixes (optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged):
Fixes #

Special notes for your reviewer:

Release note:

Add a vnc screenshot REST endpoint and a "virtctl vnc screenshot" command for UI and script integration

@kubevirt-bot kubevirt-bot added release-note Denotes a PR that will be considered when it comes time to generate release notes. dco-signoff: yes Indicates the PR's author has DCO signed all their commits. size/XL labels Sep 13, 2022
@rmohr rmohr changed the title Screenshot VNC screenshot support on the API and on virtctl Sep 13, 2022
@rmohr
Copy link
Member Author

rmohr commented Sep 13, 2022

/cc @0xFelix

regarding to the virtctl changes. Would be great if you could to a review.

@rmohr
Copy link
Member Author

rmohr commented Sep 15, 2022

/test pull-kubevirt-goveralls

Copy link
Contributor

@vasiliy-ul vasiliy-ul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice feature. Looks good for me in general, I just left a couple of minor comments inline.

Comment on lines 100 to 111
rects := msg.(*vnc.FramebufferUpdateMessage).Rectangles

w := int(rects[0].Width)
h := int(rects[0].Height)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth checking len(rects) > 0 before doing rect[0]?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 81 to 82
_ = c.PointerEvent(0, 0, 0)
_ = c.PointerEvent(0, 1, 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a side effect (potentially unwanted). Maybe better to make it optional somehow (e.g. via an additional argument)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a --move-cursor arguement and a moveCursor query parameter. Defaults to not moving.

if _, err := os.Stdout.Write(screenshot); err != nil {
return fmt.Errorf("failed to write image to stdout: %v", err)
}
} else if err := os.WriteFile(fileName, screenshot, os.ModePerm); err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://pkg.go.dev/io/fs#ModePerm:

ModePerm FileMode = 0777 // Unix permission bits

0777 seems too permissive. Maybe 0600?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A common default would be 0644 IMO. But actually os.ModePerm should be alright, if os.WriteFile respects umask (which it should, see here).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, still let me go with 0644, it will still make prper use of umask and is much more appropriate. Thanks.

// setup connection with VM
screenshot, err := virtCli.VirtualMachineInstance(namespace).Screenshot(vmi)
if err != nil {
return fmt.Errorf("Can't access VMI %s: %v: %s", vmi, err, string(screenshot))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think if you just convert []byte as string(screenshot) it will print garbage and potentially unprintable or special characters. I would personally omit this part, but alternatively it can be encoded to e.g. base64

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main issue is that the error is very unspecific and the body contains the actual error (a general problem in our webhooks I think). Maybe I can find a solution to add it to the actual error.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So maybe I misunderstood it, but I got the impression that screenshot will contain only the PNG bits, no?
Or do you mean that in the error case, it will include the rest response?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, if there is a server error the err.Error() string just contains .e.g 500 internal error, and the actual reason for the error is in the body. That does not look right, due to potential issues, like you describe, but it is in virtctl at this moment the only way to print the actual reason for the error.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And depending on when it fails, it could indeed add some png parts already, resulting to ugly output. I will try to see if I can fix this the right way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Fixed by properly retrieving the error from the body in client-go which has the code to properly detect if a StatusError is sent in the body instead of the actual requested payload.

cmd := clientcmd.NewVirtctlCommand("vnc", "screenshot", "--namespace", vmi.Namespace, "--file", filePath, vmi.Name)
Expect(cmd.Execute()).To(Succeed())

f, err := os.Open(filePath)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably defer f.close() is needed here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep. 👍

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines +257 to +259
Expect(img.Bounds().Size().X).To(BeNumerically("==", 720))
Expect(img.Bounds().Size().Y).To(BeNumerically("==", 400))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm: the framebuffer size 720x400 is specific to that particular VM image that is run in the test, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is the default resolution of the qemu boot screen. Since the VM we start has no actual VM image, it should be stable to use, because there boots no OS which can change it.

Copy link
Member

@0xFelix 0xFelix left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks quite good! Added some comments.

activeConnectionMetric := apimetrics.NewActiveVNCConnection(request.PathParameter("namespace"), request.PathParameter("name"))
defer activeConnectionMetric.Dec()

ch := make(chan vnc.ServerMessage)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this above line 68?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


// Then send a buffer update request
err = c.FramebufferUpdateRequest(false, 0, 0, c.FrameBufferWidth, c.FrameBufferHeight)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove new line?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return c.Run(cmd, args)
},
}
cmd.Flags().StringVarP(&fileName, "file", "f", "", "where to store the VNC screenshot in PNG format. User '-' for stdout")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think output or o would make it more clear that this is the output of the command. The flag file is often used as an input.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep f and file because o is prominently used in kubectl for the output format (-o wide, ...), so I would it expect to be used for the image type if we would for instance start supporting different image formats (but no plans to do so right now).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, then let's keep it.

"kubevirt.io/kubevirt/pkg/virtctl/templates"
)

var fileName string
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't fileName be part of the Screenshot struct instead of a global variable?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Done.

return err
}

vmi := args[0]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this above line 63?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

if _, err := os.Stdout.Write(screenshot); err != nil {
return fmt.Errorf("failed to write image to stdout: %v", err)
}
} else if err := os.WriteFile(fileName, screenshot, os.ModePerm); err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A common default would be 0644 IMO. But actually os.ModePerm should be alright, if os.WriteFile respects umask (which it should, see here).

@@ -131,3 +131,8 @@ func convert(err error) error {
}
return err
}

type readWriteCloser struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Removed. 👍

@@ -53,3 +53,10 @@ func (c *wsConn) SetDeadline(t time.Time) error {
}
return c.Conn.SetReadDeadline(t)
}

func NewWebsocketStreamer(conn *websocket.Conn, done chan struct{}) *wsStreamer {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually used?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in rest/vnc.go

Allow virt-api websocket dialers to return the virt-handler websocket
connection itself or the underlying connection based on the need.

Signed-off-by: Roman Mohr <rmohr@google.com>
Make use of the more flexible virt-handler dialers to directly get
access to the websocket connection and use the websocket stream
functionality to connect to vnc-go.

Signed-off-by: Roman Mohr <rmohr@google.com>
Signed-off-by: Roman Mohr <rmohr@google.com>
Signed-off-by: Roman Mohr <rmohr@google.com>
Signed-off-by: Roman Mohr <rmohr@google.com>
Support saving VNC screenshots in PNG format to files:

   virctl vnc screenshot testvmi -f screenshot.png

Support piping VNC screenshots in PNG format:

   virtctl vnc screenshot testvmi -f - | display

Signed-off-by: Roman Mohr <rmohr@google.com>
Start a VM without a boot disk and take a screenshot. Due to not having
a boot disk, the default resolution is known and we can check in the e2e
test for it.

Signed-off-by: Roman Mohr <rmohr@google.com>
Copy link
Member Author

@rmohr rmohr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reviews! PTAL.

return c.Run(cmd, args)
},
}
cmd.Flags().StringVarP(&fileName, "file", "f", "", "where to store the VNC screenshot in PNG format. User '-' for stdout")
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to keep f and file because o is prominently used in kubectl for the output format (-o wide, ...), so I would it expect to be used for the image type if we would for instance start supporting different image formats (but no plans to do so right now).

"kubevirt.io/kubevirt/pkg/virtctl/templates"
)

var fileName string
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Definitely. Done.

return err
}

vmi := args[0]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// setup connection with VM
screenshot, err := virtCli.VirtualMachineInstance(namespace).Screenshot(vmi)
if err != nil {
return fmt.Errorf("Can't access VMI %s: %v: %s", vmi, err, string(screenshot))
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Fixed by properly retrieving the error from the body in client-go which has the code to properly detect if a StatusError is sent in the body instead of the actual requested payload.

if _, err := os.Stdout.Write(screenshot); err != nil {
return fmt.Errorf("failed to write image to stdout: %v", err)
}
} else if err := os.WriteFile(fileName, screenshot, os.ModePerm); err != nil {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, still let me go with 0644, it will still make prper use of umask and is much more appropriate. Thanks.

Comment on lines 100 to 111
rects := msg.(*vnc.FramebufferUpdateMessage).Rectangles

w := int(rects[0].Width)
h := int(rects[0].Height)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@@ -131,3 +131,8 @@ func convert(err error) error {
}
return err
}

type readWriteCloser struct {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. Removed. 👍

@@ -53,3 +53,10 @@ func (c *wsConn) SetDeadline(t time.Time) error {
}
return c.Conn.SetReadDeadline(t)
}

func NewWebsocketStreamer(conn *websocket.Conn, done chan struct{}) *wsStreamer {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in rest/vnc.go

Comment on lines 81 to 82
_ = c.PointerEvent(0, 0, 0)
_ = c.PointerEvent(0, 1, 1)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a --move-cursor arguement and a moveCursor query parameter. Defaults to not moving.

activeConnectionMetric := apimetrics.NewActiveVNCConnection(request.PathParameter("namespace"), request.PathParameter("name"))
defer activeConnectionMetric.Dec()

ch := make(chan vnc.ServerMessage)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member Author

@rmohr rmohr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the reviews! PTAL.

Copy link
Contributor

@vasiliy-ul vasiliy-ul left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍

@kubevirt-bot kubevirt-bot added the lgtm Indicates that a PR is ready to be merged. label Sep 20, 2022
@0xFelix
Copy link
Member

0xFelix commented Sep 21, 2022

Looks good to me!

/lgtm

@rmohr
Copy link
Member Author

rmohr commented Sep 21, 2022

/retest

@rmohr
Copy link
Member Author

rmohr commented Sep 26, 2022

thanks everyone for the reviews!

/approve

@kubevirt-bot
Copy link
Contributor

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: rmohr

The full list of commands accepted by this bot can be found here.

The pull request process is described here

Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@kubevirt-bot kubevirt-bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Sep 26, 2022
@kubevirt-commenter-bot
Copy link

/retest-required
This bot automatically retries required jobs that failed/flaked on approved PRs.
Silence the bot with an /lgtm cancel or /hold comment for consistent failures.

@kubevirt-bot kubevirt-bot merged commit 16a9313 into kubevirt:main Sep 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
approved Indicates a PR has been approved by an approver from all required OWNERS files. dco-signoff: yes Indicates the PR's author has DCO signed all their commits. lgtm Indicates that a PR is ready to be merged. release-note Denotes a PR that will be considered when it comes time to generate release notes. size/XL
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants