Access Virtual Machines' graphic console using noVNC

Introduction

NoVNC is a JavaScript VNC client using WebSockets and HTML5 Canvas. We provide websocket api for VNC access under

APISERVER:/apis/subresources.kubevirt.io/v1alpha3/namespaces/NAMESPACE/virtualmachineinstances/VM/vnc

but we can not access the VNC api directly since authorization is needed. In order to solve the problem, we provide a component using kubectl proxy to provide a authorized vnc acess, we name this Component virtVNC.

In this post we are going to show how to do this in detail.

The detailed method

Prepare Docker Image

First prepare docker build dicrectory.

mkdir -p virtvnc/static

Then clone noVNC files from github.

git clone https://github.com/novnc/noVNC

And then copy noVNC files to docker build directory.

cp noVNC/app virtvnc/static/
cp noVNC/core virtvnc/static/
cp noVNC/vender virtvnc/static/
cp noVNC/*.html virtvnc/static/

Create a file index.html to virtvnc/static/ with the following content. The page will display VMs and corresponding VNC links.

<html>
  <meta charset="utf-8">
    <style>
     td {
        padding: 5px;
     }
     .button {
       background-color: white;
       border: 2px solid black;
       color: black;
       padding: 5px;
       text-align: center;
       text-decoration: none;
       display: inline-block;
       font-size: 16px;
       -webkit-transition-duration: 0.4s;
       transition-duration: 0.4s;
     }
     .button:hover{
       background-color: black;
       color: white;
       cursor: pointer;
     }
     button[disabled] {
       opacity: .65;
     }
     button[disabled]:hover {
       color: black;
       background: white;
     }
   </style>
    <!-- Promise polyfill for IE11 -->
    <script src="vendor/promise.js"></script>

    <!-- ES2015/ES6 modules polyfill -->
    <script nomodule src="vendor/browser-es-module-loader/dist/browser-es-module-loader.js"></script>


    <script type="module" crossorigin="anonymous">
      import * as WebUtil from "./app/webutil.js";
      const apiPrefix='k8s/apis'
      function loadVMI(namespace) {
        WebUtil.fetchJSON('/' + apiPrefix + '/kubevirt.io/v1alpha3/namespaces/' + namespace + '/virtualmachineinstances/')
          .then((resp) => {
            let vmis = []; 
            resp.items.forEach(i => {
              let tr = document.createElement('tr');
              tr.innerHTML="<td>" + i.metadata.name + "</td><td>" + String(i.status.phase) + "</td><td>" + String(i.status.interfaces !== undefined ? i.status.interfaces[0].ipAddress : '')  + "</td><td>" + String(i.status.nodeName !== undefined ? i.status.nodeName : '') + "</td><td><button class='button' " + String(i.status.phase =="Running" ? "" : "disabled")  + " onclick=\"window.open('vnc_lite.html?path=" + apiPrefix + "/subresources.kubevirt.io/v1alpha3/namespaces/" + namespace + "/virtualmachineinstances/" + i.metadata.name + "/vnc', 'novnc_window', 'resizable=yes,toolbar=no,location=no,status=no,scrollbars=no,menubar=no,width=1030,height=800')\">VNC</button></td>";
              document.getElementById("vmis").appendChild(tr);
            });
            if (resp.items.length === 0) { 
              document.body.append("No virtual machines in the namespace.");
            }
          })
          .catch(err => console.log("Failed to get vmis: " + err));
       }
       let namespace = WebUtil.getQueryVar('namespace', 'default');
       loadVMI(namespace);
    </script>
  </meta>

  <body>
   <table><tbody id="vmis">
   </tbody></table>
  </body>
</html>

Create dockerfile with following content to add static html files and set up kubectl proxy command line args.

FROM quay.io/bitnami/kubectl:1.15
ADD static /static
CMD ["proxy", "--www=/static", "--accept-hosts=^.*$", "--address=[::]", "--api-prefix=/k8s/", "--www-prefix="]

Finally use docker build to build docker image.

cd virtvnc
docker build -t quay.io/samblade/virtvnc:v0.1 .

Setting Up RBAC

Create a service account for virtvnc.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: virtvnc
  namespace: kubevirt

Then define cluster role for kubevirt, setting up permissions needed.

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: virtvnc
rules:
- apiGroups:
  - subresources.kubevirt.io
  resources:
  - virtualmachineinstances/console
  - virtualmachineinstances/vnc
  verbs:
  - get
- apiGroups:
  - kubevirt.io
  resources:
  - virtualmachines
  - virtualmachineinstances
  - virtualmachineinstancepresets
  - virtualmachineinstancereplicasets
  - virtualmachineinstancemigrations
  verbs:
  - get
  - list
  - watch

And then binding cluster role to service accout.

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: virtvnc
subjects:
- kind: ServiceAccount
  name: virtvnc
  namespace: kubevirt
roleRef:
  kind: ClusterRole
  name: virtvnc
  apiGroup: rbac.authorization.k8s.io

Deploy to kubernetes

Create following yaml, and then apply to kubernetes to setup virtvnc deployment.

apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  name: virtvnc
  namespace: kubevirt
spec:
  replicas: 1
  selector:
    matchLabels:
      app: virtvnc
  template:
    metadata:
      labels:
        app: virtvnc
    spec:
      serviceAccountName: virtvnc
      nodeSelector:
        node-role.kubernetes.io/master: ''
      tolerations: 
      - key: "node-role.kubernetes.io/master"
        operator: "Equal"
        value: ""
        effect: "NoSchedule"
      containers:
      - name: virtvnc
        image: quay.io/samblade/virtvnc:v0.1
        livenessProbe:
          httpGet:
            port: 8001
            path: /
            scheme: HTTP
          failureThreshold: 30
          initialDelaySeconds: 30
          periodSeconds: 10
          successThreshold: 1
          timeoutSeconds: 5

Expose a NodePort service, then we can access the web page from node network.

apiVersion: v1
kind: Service
metadata:
  labels:
    app: virtvnc
  name: virtvnc
  namespace: kubevirt
spec:
  ports:
  - port: 8001
    protocol: TCP
    targetPort: 8001
  selector:
    app: virtvnc
  type: NodePort

Note that this will make all your virtual machines vnc & console accessible to node network.

The Simple Way

In this github repo and registry you’ll find a ready to use version of the above which you can deploy in a single command like this:

kubectl apply -f https://github.com/wavezhang/virtVNC/raw/master/k8s/virtvnc.yaml

Access VNC

First get node port of virtvnc service.

kubectl get svc -n kubevirt virtvnc

Then visit the following url in browser:

http://NODEIP:NODEPORT/

If you want manage virtual machines in other namespace, you can specify namespace using query param namespace like following:

http://NODEIP:NODEPORT/?namespace=test

VirtVNC

References