Skip to content

Commit

Permalink
Merge pull request xcat2#37 from chenglch/web
Browse files Browse the repository at this point in the history
Add a simple web interface for goconserver
  • Loading branch information
chenglch authored Apr 17, 2018
2 parents 75077f1 + 7945d12 commit f3dba5e
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 f3dba5e

Please sign in to comment.