Skip to content
Permalink
Browse files

Add vm serial console page

  • Loading branch information...
subuk committed Sep 2, 2019
1 parent 1e2a377 commit e08d470dedb9b9cb96ae117eda833db4a496b925
@@ -152,6 +152,10 @@ func (service *Service) VirtualMachineDetachInterface(id, mac string) error {
return service.virt.DetachInterface(id, mac)
}

func (service *Service) VirtualMachineGetConsoleStream(id string) (VirtualMachineConsoleStream, error) {
return service.virt.GetConsoleStream(id)
}

func (service *Service) VolumeList() ([]*Volume, error) {
return service.vol.List()
}
@@ -9,11 +9,18 @@ type VirtualMachineRepository interface {
DetachVolume(id, path string) error
AttachInterface(id, network, mac, model string, accessVlan uint, netType NetworkType) (*VirtualMachineAttachedInterface, error)
DetachInterface(id, mac string) error
GetConsoleStream(id string) (VirtualMachineConsoleStream, error)
Poweroff(id string) error
Reboot(id string) error
Start(id string) error
}

type VirtualMachineConsoleStream interface {
Read(buf []byte) (int, error)
Write(buf []byte) (int, error)
Close() error
}

type VirtualMachineState int

const (
1 go.mod
@@ -9,6 +9,7 @@ require (
github.com/gorilla/csrf v1.6.0
github.com/gorilla/mux v1.7.3
github.com/gorilla/sessions v1.2.0
github.com/gorilla/websocket v1.4.1
github.com/hashicorp/hcl v1.0.0
github.com/imdario/mergo v0.3.7
github.com/libvirt/libvirt-go v5.6.1+incompatible
2 go.sum
@@ -17,6 +17,8 @@ github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyC
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.0 h1:S7P+1Hm5V/AT9cjEcUD5uDaQSX0OE577aCXgoaKpYbQ=
github.com/gorilla/sessions v1.2.0/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/imdario/mergo v0.3.7 h1:Y+UAYTZ7gDEuOfhxKWy+dvb5dRQ6rJjFSdX2HZY1/gI=
@@ -363,6 +363,43 @@ func (repo *VirtualMachineRepository) attachInterface(virDomainConfig *libvirtxm
return nil
}

type virStreamReadWriteCloser struct {
*libvirt.Stream
}

func (r *virStreamReadWriteCloser) Read(b []byte) (int, error) {
return r.Recv(b)
}

func (r *virStreamReadWriteCloser) Write(b []byte) (int, error) {
return r.Send(b)
}

func (r *virStreamReadWriteCloser) Close() error {
return r.Stream.Finish()
}

func (repo *VirtualMachineRepository) GetConsoleStream(id string) (compute.VirtualMachineConsoleStream, error) {
conn, err := repo.pool.Acquire()
if err != nil {
return nil, util.NewError(err, "cannot acquire libvirt connection")
}
defer repo.pool.Release(conn)

virDomain, err := conn.LookupDomainByName(id)
if err != nil {
return nil, util.NewError(err, "cannot get vm")
}
stream, err := conn.NewStream(0)
if err != nil {
return nil, util.NewError(err, "cannot create stream")
}
if err := virDomain.OpenConsole("", stream, libvirt.DOMAIN_CONSOLE_FORCE); err != nil {
return nil, util.NewError(err, "cannot open domain console")
}
return &virStreamReadWriteCloser{stream}, nil
}

func (repo *VirtualMachineRepository) Create(id string, arch compute.Arch, vcpus int, memoryKb uint, volumes []*compute.VirtualMachineAttachedVolume, interfaces []*compute.VirtualMachineAttachedInterface, config *compute.VirtualMachineConfig) (*compute.VirtualMachine, error) {
conn, err := repo.pool.Acquire()
if err != nil {
@@ -0,0 +1,37 @@
(function(exports){
exports.Vmango = exports.Vmango || {};
exports.Vmango.WSConsole = function(el){
var loc = window.location,
$consoleEl = $(el),
$consoleWindowEl = $consoleEl.find('.JS-WSConsole-Window'),
$consoleInputFormEl = $consoleEl.find('.JS-WSConsole-InputForm'),
$consoleInputFieldEl = $consoleInputFormEl.find("input[name='Command']"),
wsUri;
if (loc.protocol === "https:") {
wsUri = "wss:";
} else {
wsUri = "ws:";
}
wsUri += "//" + loc.host;
wsUri += $consoleEl.attr('data-JSConsole-WSUrl');
var socket = new WebSocket(wsUri);
socket.onopen = function(){
$consoleWindowEl.text('');
$consoleWindowEl.text("Connected, send any text to start");
}
socket.onmessage = function(event){
$consoleWindowEl.append(event.data);
$consoleWindowEl.scrollTop($consoleWindowEl.prop('scrollHeight'));
}
socket.onclose = function(){
$consoleWindowEl.append("\nConnection closed, reconnecting in 3 seconds...\n");
setTimeout(function(){start(websocketServerLocation)}, 3000);
};
$consoleInputFormEl.on("submit", function(){
socket.send($consoleInputFieldEl.val() + "\n");
$consoleInputFieldEl.val('');
return false
});
}

})(window);
@@ -10,8 +10,12 @@

<script src="{{ Static "vmango/vmango.CheckboxCard.js" }}"></script>
<script src="{{ Static "vmango/vmango.ReactiveForm.js" }}"></script>
<script src="{{ Static "vmango/vmango.WSConsole.js" }}"></script>
<script>
$(function(){
$('.JS-WSConsole').each(function(idx, el){
Vmango.WSConsole(el);
});
$('.JS-CheckboxCards').each(function(idx, el){
Vmango.CheckboxCards(el);
});
@@ -20,4 +24,3 @@
});
});
</script>

@@ -0,0 +1,26 @@
{{ template "header" . }}

<!-- Breadcrumb -->
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/">Home</a></li>
<li class="breadcrumb-item"><a href="{{ Url "virtual-machine-list" }}">Virtual Machines</a></li>
<li class="breadcrumb-item"><a href="{{ Url "virtual-machine-detail" "id" .Vm.Id }}">{{ .Vm.Id }}</a></li>
<li class="breadcrumb-item active">Console</li>
</ol>

<div class="container">
<div class="card border-0">
<div class="card-block JS-WSConsole" data-JSConsole-WSUrl="{{ Url "virtual-machine-console-ws" "id" .Vm.Id }}">
<pre style="width: 100%; height: 600px; background-color:black; color: white; overflow: scroll;" class="JS-WSConsole-Window">Connecting...</pre>
<form class="form-inline JS-WSConsole-InputForm">
<div class="form-group mb-2">
<input autofocus type="text" class="form-control" name="Command">
</div>
<button class="btn btn-primary mb-2" type="submit">Send</button>
</form>
</div>
</div>
</div>


{{ template "footer" . }}
@@ -35,6 +35,7 @@ <h1>{{ .Vm.Id }}</h1>
<p>

{{ if .Vm.IsRunning }}
<a class="btn btn-primary" href="{{ Url "virtual-machine-console-show" "id" .Vm.Id }}">Console</a>
<a class="btn btn-primary" href="{{ Url "virtual-machine-state-form" "id" .Vm.Id "action" "poweroff" }}">Power Off</a>
<a class="btn btn-primary" href="{{ Url "virtual-machine-state-form" "id" .Vm.Id "action" "reboot" }}">Reboot</a>
{{ else }}
@@ -13,9 +13,9 @@ import (
"time"

"github.com/gorilla/csrf"

"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"github.com/unrolled/render"
"golang.org/x/crypto/bcrypt"
@@ -30,6 +30,7 @@ type Environ struct {
sessions sessions.Store
random *rand.Rand
compute *libcompute.Service
ws *websocket.Upgrader
cfg *config.WebConfig
}

@@ -138,6 +139,7 @@ func New(cfg *config.Config, logger zerolog.Logger, compute *libcompute.Service)

env.random = rand.New(rand.NewSource(time.Now().UnixNano()))
env.render = renderer
env.ws = &websocket.Upgrader{}
env.logger = logger
env.router = router
env.compute = compute
@@ -171,6 +173,8 @@ func New(cfg *config.Config, logger zerolog.Logger, compute *libcompute.Service)
router.HandleFunc("/machines/add/", env.authenticated(env.VirtualMachineAddFormShow)).Name("virtual-machine-add")
router.HandleFunc("/machines/{id}/", env.authenticated(env.VirtualMachineDetail)).Name("virtual-machine-detail")
router.HandleFunc("/machines/{id}/attach-disk/", env.authenticated(env.VirtualMachineAttachDiskFormProcess)).Methods("POST").Name("virtual-machine-attach-disk")
router.HandleFunc("/machines/{id}/console/", env.authenticated(env.VirtualMachineConsoleShow)).Name("virtual-machine-console-show")
router.HandleFunc("/machines/{id}/console-ws/", env.authenticated(env.VirtualMachineConsoleWS)).Name("virtual-machine-console-ws")
router.HandleFunc("/machines/{id}/detach-volume/", env.authenticated(env.VirtualMachineDetachVolumeFormProcess)).Methods("POST").Name("virtual-machine-detach-volume")
router.HandleFunc("/machines/{id}/attach-interface/", env.authenticated(env.VirtualMachineAttachInterfaceFormProcess)).Methods("POST").Name("virtual-machine-attach-interface")
router.HandleFunc("/machines/{id}/detach-interface/", env.authenticated(env.VirtualMachineDetachInterfaceFormProcess)).Methods("POST").Name("virtual-machine-detach-interface")
@@ -7,6 +7,7 @@ import (
"subuk/vmango/compute"

"github.com/gorilla/mux"
"github.com/gorilla/websocket"
)

func (env *Environ) VirtualMachineList(rw http.ResponseWriter, req *http.Request) {
@@ -316,3 +317,68 @@ func (env *Environ) VirtualMachineDetachInterfaceFormProcess(rw http.ResponseWri
redirectUrl := env.url("virtual-machine-detail", "id", urlvars["id"])
http.Redirect(rw, req, redirectUrl.Path, http.StatusFound)
}

func (env *Environ) VirtualMachineConsoleShow(rw http.ResponseWriter, req *http.Request) {
urlvars := mux.Vars(req)
vm, err := env.compute.VirtualMachineDetail(urlvars["id"])
if err != nil {
env.error(rw, req, err, "cannot get vm", http.StatusInternalServerError)
return
}
data := struct {
Title string
Vm *compute.VirtualMachine
User *User
Request *http.Request
}{"Virtual Machine Serial Console", vm, env.Session(req).AuthUser(), req}
if err := env.render.HTML(rw, http.StatusOK, "virtual-machine/console", data); err != nil {
env.error(rw, req, err, "failed to render template", http.StatusInternalServerError)
return
}
}

func (env *Environ) VirtualMachineConsoleWS(rw http.ResponseWriter, req *http.Request) {
urlvars := mux.Vars(req)

wsconn, err := env.ws.Upgrade(rw, req, nil)
if err != nil {
env.logger.Debug().Err(err).Msg("cannot upgrade websocket connection")
return
}

console, err := env.compute.VirtualMachineGetConsoleStream(urlvars["id"])
if err != nil {
env.error(rw, req, err, "cannot get vm console", http.StatusInternalServerError)
return
}
defer console.Close()

go func() {
for {
buf := make([]byte, 1024)
n, err := console.Read(buf)
if err != nil {
env.logger.Debug().Err(err).Msg("console read error")
return
}
if err := wsconn.WriteMessage(websocket.TextMessage, buf[0:n]); err != nil {
env.logger.Debug().Err(err).Msg("wsconn write error")
return
}
}
}()
for {
mt, message, err := wsconn.ReadMessage()
if err != nil {
env.logger.Debug().Err(err).Msg("ws read error")
return
}
if mt != websocket.TextMessage {
continue
}
if _, err := console.Write(message); err != nil {
env.logger.Debug().Err(err).Msg("console write error")
return
}
}
}

0 comments on commit e08d470

Please sign in to comment.
You can’t perform that action at this time.