forked from kubevirt/kubevirt
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Kevin Wiesmueller <[email protected]>
- Loading branch information
Kevin Wiesmueller
committed
Jul 12, 2021
1 parent
3276e54
commit 5559ea1
Showing
19 changed files
with
2,407 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# RDP into a VirtualMachineInstance | ||
|
||
Every VM and VMI provides a `/portforward` subresource that can be used to create a websocket backed | ||
network tunnel to a port inside the instance similar to Kubernetes pods. | ||
|
||
One use-case for this subresource is to forward RDP traffic into the VMI either from the CLI | ||
or a web-UI. | ||
|
||
## Usage | ||
|
||
To connect to a Windows Guest via RDP, first open a `port-forward` tunnel: | ||
|
||
```sh | ||
virtctl port-forward vm/win10 udp/3389 tcp/3389 | ||
``` | ||
|
||
Then you can use the tunnel with an RDP client of your preference: | ||
|
||
```sh | ||
freerdp /u:Administrator /p:YourPassword /v:127.0.0.1:3389 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,141 @@ | ||
# SSH into a VirtualMachineInstance | ||
|
||
Every VM and VMI provides a `/portforward` subresource that can be used to create a websocket backed | ||
network tunnel to a port inside the instance similar to Kubernetes pods. | ||
|
||
One use-case for this subresource is to forward SSH traffic into the VMI either from the CLI | ||
or a web-UI. | ||
|
||
## Usage | ||
|
||
### virtcli | ||
|
||
To connect to a VMI from your local machine, `virtcli` provides the `ssh` command as a lightweight | ||
SSH client. Refer to the commands help for more details. | ||
|
||
```sh | ||
virtctl ssh testvmi | ||
``` | ||
|
||
#### Port Forward | ||
|
||
If you prefer to use your local OpenSSH client, the `virtctl port-forward` command provides an option | ||
to tunnel a single port to your local Stdout/Stdin. | ||
This allows for the command to be used in the `ProxyCommand` option. | ||
|
||
```sh | ||
ssh -o 'ProxyCommand=virtctl port-forward --stdio=true testvmi 22' [email protected] | ||
``` | ||
|
||
This can also be used with `scp`. | ||
|
||
To provide easier access to different VMs you can add the following to your `ssh-config`: | ||
|
||
``` | ||
Host vmi/* | ||
ProxyCommand virtctl port-forward --stdio %h %p | ||
Host vm/* | ||
ProxyCommand virtctl port-forward --stdio %h %p | ||
``` | ||
|
||
This allows you to simply call `ssh fedora@vmi/testvmi.default` and your config and virtctl will do the rest. | ||
Using this setup it also becomes trivial to setup different identities for different namespaces inside your `ssh-config` | ||
|
||
Note that all traffic sent over those tunnels will be proxied over the Kubernetes controlplane. | ||
A high amount of traffic and connections can increase pressure on the apiserver. | ||
If you need regular high amount of connections and traffic, consider using a dedicated Kubernetes Service instead. | ||
|
||
### Example | ||
|
||
1. Create VM | ||
```yaml | ||
# ssh-test.vm.yaml | ||
apiVersion: kubevirt.io/v1alpha3 | ||
kind: VirtualMachine | ||
metadata: | ||
annotations: | ||
kubevirt.io/latest-observed-api-version: v1alpha3 | ||
kubevirt.io/storage-observed-api-version: v1alpha3 | ||
name.os.template.kubevirt.io/fedora32: Fedora 31 or higher | ||
name: ssh-test | ||
labels: | ||
app: ssh-test | ||
flavor.template.kubevirt.io/tiny: 'true' | ||
os.template.kubevirt.io/fedora32: 'true' | ||
vm.kubevirt.io/template: fedora-server-tiny-v0.11.3 | ||
vm.kubevirt.io/template.namespace: openshift | ||
vm.kubevirt.io/template.revision: '1' | ||
vm.kubevirt.io/template.version: v0.12.4 | ||
workload.template.kubevirt.io/server: 'true' | ||
spec: | ||
running: false | ||
template: | ||
metadata: | ||
labels: | ||
flavor.template.kubevirt.io/tiny: 'true' | ||
kubevirt.io/domain: ssh-test | ||
kubevirt.io/size: tiny | ||
os.template.kubevirt.io/fedora32: 'true' | ||
vm.kubevirt.io/name: ssh-test | ||
workload.template.kubevirt.io/server: 'true' | ||
spec: | ||
domain: | ||
cpu: | ||
cores: 1 | ||
sockets: 1 | ||
threads: 1 | ||
devices: | ||
disks: | ||
- disk: | ||
bus: virtio | ||
name: cloudinitdisk | ||
- bootOrder: 1 | ||
disk: | ||
bus: virtio | ||
name: rootdisk | ||
interfaces: | ||
- masquerade: {} | ||
model: virtio | ||
name: nic-0 | ||
networkInterfaceMultiqueue: true | ||
rng: {} | ||
machine: | ||
type: pc-q35-rhel8.2.0 | ||
resources: | ||
requests: | ||
memory: 1Gi | ||
hostname: ssh-test | ||
networks: | ||
- name: nic-0 | ||
pod: {} | ||
terminationGracePeriodSeconds: 180 | ||
volumes: | ||
- cloudInitNoCloud: | ||
userData: | | ||
#cloud-config | ||
user: fedora | ||
password: ssh-demo | ||
chpasswd: | ||
expire: false | ||
name: cloudinitdisk | ||
- containerDisk: | ||
image: kubevirt/fedora-cloud-container-disk-demo | ||
name: rootdisk | ||
``` | ||
```sh | ||
kubectl apply -f ssh-test.vm.yaml | ||
``` | ||
|
||
2. Start VM | ||
```sh | ||
kubectl virt start ssh-test | ||
``` | ||
|
||
3. SSH into VM | ||
```sh | ||
kubectl virt ssh --username=fedora ssh-test | ||
``` | ||
or | ||
```sh | ||
ssh -o 'ProxyCommand=kubectl virt port-forward --stdio=true ssh-test 22' [email protected] | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
load("@io_bazel_rules_go//go:def.bzl", "go_library") | ||
|
||
go_library( | ||
name = "go_default_library", | ||
srcs = [ | ||
"native.go", | ||
"resize_unix.go", | ||
"resize_windows.go", | ||
"ssh.go", | ||
"wrapped.go", | ||
], | ||
importpath = "kubevirt.io/kubevirt/pkg/virtctl/ssh", | ||
visibility = ["//visibility:public"], | ||
deps = [ | ||
"//pkg/virtctl/templates:go_default_library", | ||
"//staging/src/github.com/golang/glog:go_default_library", | ||
"//staging/src/kubevirt.io/client-go/kubecli:go_default_library", | ||
"//vendor/github.com/spf13/cobra:go_default_library", | ||
"//vendor/golang.org/x/crypto/ssh:go_default_library", | ||
"//vendor/golang.org/x/crypto/ssh/agent:go_default_library", | ||
"//vendor/golang.org/x/term:go_default_library", | ||
"//vendor/k8s.io/client-go/tools/clientcmd:go_default_library", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package ssh | ||
|
||
import ( | ||
"fmt" | ||
"io/ioutil" | ||
"net" | ||
"os" | ||
|
||
"github.com/golang/glog" | ||
"golang.org/x/crypto/ssh" | ||
"golang.org/x/crypto/ssh/agent" | ||
"golang.org/x/term" | ||
) | ||
|
||
func (o *SSH) prepareSSHClient(kind, namespace, name string) (*ssh.Client, error) { | ||
streamer, err := o.prepareSSHTunnel(kind, namespace, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
conn := streamer.AsConn() | ||
addr := fmt.Sprintf("%s/%s.%s", kind, name, namespace) | ||
authMethods, err := o.getAuthMethods(kind, namespace, name) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
sshConn, chans, reqs, err := ssh.NewClientConn(conn, | ||
addr, | ||
&ssh.ClientConfig{ | ||
HostKeyCallback: ssh.InsecureIgnoreHostKey(), | ||
Auth: authMethods, | ||
User: sshUsername, | ||
}, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return ssh.NewClient(sshConn, chans, reqs), nil | ||
} | ||
|
||
func (o *SSH) getAuthMethods(kind, namespace, name string) ([]ssh.AuthMethod, error) { | ||
var ( | ||
methods []ssh.AuthMethod | ||
err error | ||
) | ||
|
||
if len(identityFilePath) < 1 { | ||
methods = trySSHAgent(methods) | ||
} else { | ||
methods, err = usePrivateKey(methods) | ||
if err != nil { | ||
return nil, err | ||
} | ||
} | ||
|
||
methods = append(methods, ssh.PasswordCallback(func() (secret string, err error) { | ||
password, err := readPassword(fmt.Sprintf("%s@%s/%s.%s's password: ", sshUsername, kind, name, namespace)) | ||
fmt.Println() | ||
return string(password), err | ||
})) | ||
|
||
return methods, nil | ||
} | ||
|
||
func trySSHAgent(methods []ssh.AuthMethod) []ssh.AuthMethod { | ||
socket := os.Getenv("SSH_AUTH_SOCK") | ||
if len(socket) < 1 { | ||
return methods | ||
} | ||
conn, err := net.Dial("unix", socket) | ||
if err != nil { | ||
glog.Error(err) | ||
return methods | ||
} | ||
agentClient := agent.NewClient(conn) | ||
|
||
return append(methods, ssh.PublicKeysCallback(agentClient.Signers)) | ||
} | ||
|
||
func usePrivateKey(methods []ssh.AuthMethod) ([]ssh.AuthMethod, error) { | ||
key, err := ioutil.ReadFile(identityFilePath) | ||
if err != nil { | ||
return methods, err | ||
} | ||
|
||
signer, err := ssh.ParsePrivateKey(key) | ||
if _, isPassErr := err.(*ssh.PassphraseMissingError); isPassErr { | ||
signer, err = parsePrivateKeyWithPassphrase(key) | ||
if err != nil { | ||
return methods, err | ||
} | ||
} | ||
return append(methods, ssh.PublicKeys(signer)), nil | ||
} | ||
|
||
func parsePrivateKeyWithPassphrase(key []byte) (ssh.Signer, error) { | ||
password, err := readPassword(fmt.Sprintf("Key %s requires a password: ", identityFilePath)) | ||
fmt.Println() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return ssh.ParsePrivateKeyWithPassphrase(key, password) | ||
} | ||
|
||
func readPassword(reason string) ([]byte, error) { | ||
fmt.Print(reason) | ||
return term.ReadPassword(int(os.Stdin.Fd())) | ||
} | ||
|
||
func (o *SSH) startSession(client *ssh.Client) error { | ||
session, err := client.NewSession() | ||
if err != nil { | ||
return err | ||
} | ||
defer session.Close() | ||
|
||
session.Stdin = os.Stdin | ||
session.Stderr = os.Stderr | ||
session.Stdout = os.Stdout | ||
|
||
restore, err := setupTerminal(int(os.Stdin.Fd())) | ||
if err != nil { | ||
return err | ||
} | ||
defer restore() | ||
|
||
if err := requestPty(session); err != nil { | ||
return err | ||
} | ||
|
||
if err := session.Shell(); err != nil { | ||
return err | ||
} | ||
|
||
err = session.Wait() | ||
if _, exited := err.(*ssh.ExitError); !exited { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func setupTerminal(fd int) (func(), error) { | ||
state, err := term.MakeRaw(fd) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return func() { term.Restore(fd, state) }, nil | ||
} | ||
|
||
func requestPty(session *ssh.Session) error { | ||
w, h, err := term.GetSize(int(os.Stdin.Fd())) | ||
if err != nil { | ||
return err | ||
} | ||
if err := session.RequestPty( | ||
os.Getenv("TERM"), | ||
h, w, | ||
ssh.TerminalModes{}, | ||
); err != nil { | ||
return err | ||
} | ||
|
||
go resizeSessionOnWindowChange(session, os.Stdin.Fd()) | ||
|
||
return nil | ||
} |
Oops, something went wrong.