Skip to content

Commit

Permalink
Add a simple web interface for goconserver
Browse files Browse the repository at this point in the history
Web interface is implemented with frontend techniques like webpack,
gulp, jquery, xtermjs and scss. At golang part, goconserver also
act as a web server to accept the request from browser.
  • Loading branch information
chenglch committed Apr 14, 2018
1 parent 75077f1 commit 7945d12
Show file tree
Hide file tree
Showing 22 changed files with 689 additions and 35 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@ vendor/
goconserver
congo
build
frontend/package-lock.json
frontend/node_modules
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export PATH
GITHUB_DIR=${GOPATH}/src/github.com/xcat2/
REPO_DIR=${GOPATH}/src/github.com/xcat2/goconserver
CURRENT_DIR=$(shell pwd)
FRONTEND_DIR=${CURRENT_DIR}/frontend
REPO_DIR_LINK=$(shell readlink -f ${REPO_DIR})
SERVER_CONF_FILE=/etc/goconserver/server.conf
CLIENT_CONF_FILE=~/congo.sh
Expand All @@ -20,7 +21,7 @@ endif
ifeq ($(PLATFORM), Linux)
PLATFORM=linux
endif
VERSION=0.2.2
VERSION=0.3.0
BUILD_TIME=`date +%FT%T%z`
LDFLAGS=-ldflags "-X main.Version=${VERSION} -X main.BuildTime=${BUILD_TIME} -X main.Commit=${COMMIT}"

Expand Down Expand Up @@ -50,6 +51,12 @@ build: link
go build ${LDFLAGS} -o ${CLIENT_BINARY} cmd/congo.go; \
cd -

frontend:
cd ${FRONTEND_DIR}; \
npm install --unsafe-perm --save-dev; \
gulp build; \
cd -

install: build
cp ${SERVER_BINARY} /usr/local/bin/${SERVER_BINARY}
cp ${CLIENT_BINARY} /usr/local/bin/${CLIENT_BINARY}
Expand Down Expand Up @@ -86,4 +93,4 @@ clean:
rm -rf build
rm -rf bin pkg

.PHONY: binary deps fmt build clean link tar deb rpm
.PHONY: binary deps fmt frontend build clean link tar deb rpm
75 changes: 48 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,46 +37,48 @@ api interface.
- file: Store the host information in a json file.
- etcd: Support goconserver cluster [experimental].

### Multiple client types

- terminal: Get console session via TCP(or with TLS) connection.
- web: Get console session from web terminal.

![preview](/goconserver2.gif)

### Design Structure
`goconserver` can be divided into two parts:
- daemon part: `goconserver`, expose rest api interface to define and control
the session node.
`goconserver` can be divided into three parts:
- daemon part: `goconserver`, expose REST api interface to define and control
the session host.

- client part: `congo`, a command line tool to define session or connect to the
session. Multiple client sessions could be shared.
- client part: `congo`, a command line tool to manage the configuration of
session hosts. A tty console client is also provided and multiple clients
could share the same session.

- frontend part: A web page is provided to list the session status and expose
a web terminal for the selected node. The tty client from `congo` can share
the same session from web browser.

## Setup goconserver from release

### Setup goconserver from binary
Download the binary tarball for release from
### Setup
Download binary or RPM tarball from
[goconserver](https://github.com/xcat2/goconserver/releases)
```
tar xvfz goconserver_linux_amd64.tar.gz
cd goconserver_linux_amd64
./setup.sh
yum install <goconserver.rpm>
systemctl start goconserver.service
```

Modify the congiguration file `/etc/goconserver/server.conf` based on your
environment, then run `goconserver` to start the daemon service. To support a
large amount of sessions, please use `ulimit -n <number>` command to set the
number of open files.
```
goconserver [--congi-file <file>]
```
### Configuration
For the server side, modify the congiguration file
`/etc/goconserver/server.conf` based on your environment, then restart
goconserver service.

For client, modify the the environment variables in `/etc/profile.d/congo.sh`
based on your environment, then try the `congo` command. For example:

Modify the the environment variables in `/etc/profile.d/congo.sh` based on your
environment, then try the `congo` command.
```
source /etc/profile.d/congo.sh
congo list
```
### Setup goconserver from rpm or deb

```
tar xvfz <tarball for rpm or deb>
yum install <rpm>
dpkg -i <deb>
```

## Development

Expand All @@ -94,10 +96,29 @@ make deps
make install
```

### Setup SSL (optional)
### Setup SSL/TLS (optional)

Please refer to [ssl](/scripts/ssl/)

### Web Interface (ongoing)

Setup nodejs(9.0+) and npm(5.6.0+) toolkit at first. An example steps could be
found at [node env](/frontend/). Then follow the steps below:

```
npm install -g gulp webpack webpack-cli
make frontend
```

The frontend content is generated at `build/dist` directory. To enable it,
modify the configuration in `/etc/gconserver/server.conf` like below, then
restart `goconserver` service. The web url is available on
`http(s)://<ip or domain name>:<api-port>/`.
```
api:
dist_dir : "<dist directory>"
```

## Command Example

### Start service
Expand Down
58 changes: 58 additions & 0 deletions api/web.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package api

import (
"compress/gzip"
"fmt"
"github.com/gorilla/mux"
"github.com/xcat2/goconserver/console"
"golang.org/x/net/websocket"
"io"
"net/http"
"strings"
)

type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}

func (w gzipResponseWriter) Write(b []byte) (int, error) {
return w.Writer.Write(b)
}

func MakeGzipHandler(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Check if the client can accept the gzip encoding.
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
handler.ServeHTTP(w, r)
return
}

// Set the HTTP header indicating encoding.
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
handler.ServeHTTP(gzw, r)
})
}

func WebHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
plog.Info(fmt.Sprintf("Receive %s request %s from %s.", r.Method, r.URL.Path, r.RemoteAddr))
if r.URL.EscapedPath() == "/" || r.URL.EscapedPath() == "/index.html" {
w.Header().Add("Cache-Control", "no-store")
}
http.FileServer(http.Dir(serverConfig.API.DistDir)).ServeHTTP(w, r)
})
}

func RegisterBackendHandler(router *mux.Router) {
router.PathPrefix("/session").Handler(websocket.Handler(websocketHandler))
router.PathPrefix("/").Handler(MakeGzipHandler(WebHandler()))
}

func websocketHandler(ws *websocket.Conn) {
plog.Info(fmt.Sprintf("Recieve websocket request from %s\n", ws.RemoteAddr().String()))
console.AcceptWesocketClient(ws)
}
2 changes: 2 additions & 0 deletions common/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ type ServerConfig struct {
API struct {
Port string `yaml:"port"`
HttpTimeout int `yaml:"http_timeout"`
DistDir string `yaml:"dist_dir"`
}
Console struct {
Port string `yaml:"port"`
Expand Down Expand Up @@ -128,6 +129,7 @@ func InitServerConfig(confFile string) (*ServerConfig, error) {
serverConfig.Global.StorageType = "file"
serverConfig.API.Port = "12429"
serverConfig.API.HttpTimeout = 10
serverConfig.API.DistDir = ""
serverConfig.Console.Port = "12430"
serverConfig.Console.DataDir = "/var/lib/goconserver/"
serverConfig.Console.LogTimestamp = true
Expand Down
3 changes: 3 additions & 0 deletions common/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ const (
STORAGE_NOT_EXIST
TASK_NOT_EXIST

NULL_OBJECT

INVALID_PARAMETER
LOCKED
CONNECTION_ERROR
Expand All @@ -34,6 +36,7 @@ var (
ErrCommandNotExist = NewErr(COMMAND_NOT_EXIST, "Command not exist")
ErrStorageNotExist = NewErr(STORAGE_NOT_EXIST, "Storage not exist")
ErrTaskNotExist = NewErr(TASK_NOT_EXIST, "Task not exist")
ErrNullObject = NewErr(NULL_OBJECT, "Null object")

ErrInvalidParameter = NewErr(INVALID_PARAMETER, "Invalid parameter")
ErrLocked = NewErr(LOCKED, "Locked")
Expand Down
8 changes: 8 additions & 0 deletions common/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"golang.org/x/net/websocket"
"io"
"io/ioutil"
"net"
Expand Down Expand Up @@ -123,6 +125,12 @@ func (self *network) ResetWriteTimeout(conn net.Conn) error {

func (self *network) SendBytes(conn net.Conn, b []byte) error {
n := 0
// TODO(chenglch): A workaround to solve 1006 error from websocket at
// frontend side due to UTF8 encoding problem.
if _, ok := conn.(*websocket.Conn); ok {
s := base64.StdEncoding.EncodeToString(b)
b = []byte(s)
}
for n < len(b) {
tmp, err := conn.Write(b[n:])
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion console/console.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func (self *Console) Accept(conn net.Conn) {
self.bufConn[conn] = make(chan []byte)
self.mutex.Unlock()
go self.writeTarget(conn)
go self.writeClient(conn)
self.writeClient(conn)
}

// Disconnect from client
Expand Down
25 changes: 20 additions & 5 deletions console/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
pl "github.com/xcat2/goconserver/console/pipeline"
"github.com/xcat2/goconserver/plugins"
"github.com/xcat2/goconserver/storage"
"golang.org/x/net/websocket"
"net"
"net/http"
"os"
Expand All @@ -32,10 +33,11 @@ const (
)

var (
plog = common.GetLogger("github.com/xcat2/goconserver/console")
nodeManager *NodeManager
serverConfig = common.GetServerConfig()
STATUS_MAP = map[int]string{
plog = common.GetLogger("github.com/xcat2/goconserver/console")
nodeManager *NodeManager
consoleServer *ConsoleServer
serverConfig = common.GetServerConfig()
STATUS_MAP = map[int]string{
STATUS_AVAIABLE: "avaiable",
STATUS_ENROLL: "enroll",
STATUS_CONNECTED: "connected",
Expand Down Expand Up @@ -124,6 +126,8 @@ func (self *Node) restartMonitor() {
plog.DebugNode(self.StorageNode.Name, "Exit reconnect goroutine")
return
}
// before start console, both request from client and reconnecting monitor try to get the lock at first
// so that only one startConsole goroutine is running for the node.
if err = self.RequireLock(false); err != nil {
plog.ErrorNode(self.StorageNode.Name, err.Error())
break
Expand Down Expand Up @@ -274,6 +278,14 @@ func NewConsoleServer(host string, port string) *ConsoleServer {
return &ConsoleServer{host: host, port: port}
}

func AcceptWesocketClient(ws *websocket.Conn) error {
if consoleServer == nil {
return common.ErrNullObject
}
consoleServer.handle(ws)
return nil
}

func (self *ConsoleServer) getConnectionInfo(conn interface{}) (string, string) {
var node, command string
var ok bool
Expand Down Expand Up @@ -364,6 +376,7 @@ func (self *ConsoleServer) handle(conn interface{}) {
nodeManager.RWlock.RUnlock()
if command == COMMAND_START_CONSOLE {
if node.status != STATUS_CONNECTED {
// NOTE(chenglch): Get the lock at first, then allow to connect to the console target.
if err = node.RequireLock(false); err != nil {
plog.ErrorNode(node.StorageNode.Name, fmt.Sprintf("Could not start console, error: %s.", err))
err = common.Network.SendIntWithTimeout(conn.(net.Conn), STATUS_ERROR, clientTimeout)
Expand All @@ -376,6 +389,8 @@ func (self *ConsoleServer) handle(conn interface{}) {
if node.status == STATUS_CONNECTED {
node.Release(false)
} else {
// NOTE(chenglch): Already got the lock, but the console connection is not established, start
// console at the backend.
go node.startConsole()
if err = common.TimeoutChan(node.ready, serverConfig.Console.TargetTimeout); err != nil {
plog.ErrorNode(node.StorageNode.Name, fmt.Sprintf("Could not start console, error: %s.", err))
Expand Down Expand Up @@ -482,7 +497,7 @@ func GetNodeManager() *NodeManager {
nodeManager.hostname = hostname
nodeManager.Nodes = make(map[string]*Node)
nodeManager.RWlock = new(sync.RWMutex)
consoleServer := NewConsoleServer(serverConfig.Global.Host, serverConfig.Console.Port)
consoleServer = NewConsoleServer(serverConfig.Global.Host, serverConfig.Console.Port)
stor, err := storage.NewStorage(serverConfig.Global.StorageType)
if err != nil {
panic(err)
Expand Down
2 changes: 2 additions & 0 deletions etc/goconserver/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ api:
port: "12429"
# in second
http_timeout: 5
# dist frontend directory for the web console
dist_dir : ""

console:
# the console session port for client(congo) to connect.
Expand Down
9 changes: 9 additions & 0 deletions frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Setup node and npm environment
Example for amd64 system

```
wget https://nodejs.org/dist/v9.11.1/node-v9.11.1-linux-x64.tar.xz
xz -d node-v9.11.1-linux-x64.tar.xz
tar xvf node-v9.11.1-linux-x64.tar
export PATH=$PATH:<work dir>/node-v9.11.1-linux-x64/bin
```
22 changes: 22 additions & 0 deletions frontend/gulpfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
var gp = require("gulp");
var webpack = require('webpack-stream');

gp.task("webpack", function() {
return gp.src([
'src/js/index.js',
'src/sass/index.scss'
])
.pipe(webpack(require('./webpack.config.js')))
.pipe(gp.dest('../build/dist/'))
})

gp.task("build", ["webpack"], function() {
gp.src(['./src/html/*.html'])
.pipe(gp.dest('../build/dist'))
})

gp.task("run", ["build"], function() {
gp.watch('src/*.js', function() {
gulp.run('run');
});
})
Loading

0 comments on commit 7945d12

Please sign in to comment.