2021-06-14 04:49:47 +00:00
package steward
import (
2021-06-14 15:07:31 +00:00
"bufio"
2021-06-16 04:03:09 +00:00
"encoding/json"
2021-06-14 04:49:47 +00:00
"flag"
"fmt"
2021-06-15 20:21:18 +00:00
"log"
2021-06-14 04:49:47 +00:00
"os"
2021-06-15 20:21:18 +00:00
"reflect"
"strconv"
2021-06-16 04:03:09 +00:00
"strings"
2021-06-16 19:11:50 +00:00
"time"
2021-06-14 04:49:47 +00:00
"github.com/rivo/tview"
)
type Stew struct {
stewardSocket string
}
func NewStew ( ) ( * Stew , error ) {
stewardSocket := flag . String ( "stewardSocket" , "/usr/local/steward/tmp/steward.sock" , "specify the full path of the steward socket file" )
flag . Parse ( )
_ , err := os . Stat ( * stewardSocket )
if err != nil {
return nil , fmt . Errorf ( "error: specify the full path to the steward.sock file: %v" , err )
}
s := Stew {
stewardSocket : * stewardSocket ,
}
return & s , nil
}
func ( s * Stew ) Start ( ) error {
err := console ( )
if err != nil {
return fmt . Errorf ( "error: console failed: %v" , err )
}
return nil
}
2021-06-14 15:07:31 +00:00
// ---------------------------------------------------
2021-06-14 04:49:47 +00:00
func console ( ) error {
2021-06-17 06:28:10 +00:00
// Check that the message struct used within stew are up to date, and
// consistent with the fields used in the main Steward message file.
// If it throws an error here we need to update the msg struct type,
// or add a case for the field to except.
err := checkFieldsMsg ( )
if err != nil {
log . Printf ( "%v\n" , err )
os . Exit ( 1 )
}
2021-06-14 04:49:47 +00:00
app := tview . NewApplication ( )
2021-06-14 15:07:31 +00:00
reqFillForm := tview . NewForm ( )
reqFillForm . SetBorder ( true ) . SetTitle ( "Request values" ) . SetTitleAlign ( tview . AlignLeft )
2021-06-14 04:49:47 +00:00
2021-06-16 19:11:50 +00:00
logView := tview . NewTextView ( )
logView . SetBorder ( true ) . SetTitle ( "Log/Status" ) . SetTitleAlign ( tview . AlignLeft )
logView . SetChangedFunc ( func ( ) {
app . Draw ( )
} )
2021-06-14 15:07:31 +00:00
// Create a flex layout.
flexContainer := tview . NewFlex ( ) .
2021-06-16 19:11:50 +00:00
SetDirection ( tview . FlexRow ) .
AddItem ( reqFillForm , 0 , 10 , false ) .
AddItem ( logView , 0 , 2 , false )
2021-06-14 15:07:31 +00:00
2021-06-16 19:11:50 +00:00
drawFormREQ ( reqFillForm , logView , app )
2021-06-15 20:21:18 +00:00
2021-06-16 19:11:50 +00:00
if err := app . SetRoot ( flexContainer , true ) . SetFocus ( reqFillForm ) . EnableMouse ( true ) . Run ( ) ; err != nil {
2021-06-15 20:21:18 +00:00
panic ( err )
2021-06-14 15:07:31 +00:00
}
2021-06-14 04:49:47 +00:00
2021-06-15 20:21:18 +00:00
return nil
}
2021-06-14 15:07:31 +00:00
2021-06-17 06:28:10 +00:00
// Creating a copy of the real Message struct here to use within the
// field specification, but without the control kind of fields from
// the original to avoid changing them to pointer values in the main
// struct which would be needed when json marshaling to omit those
// empty fields.
2021-06-16 14:14:43 +00:00
type msg struct {
// The node to send the message to
ToNode node ` json:"toNode" yaml:"toNode" `
// The actual data in the message
Data [ ] string ` json:"data" yaml:"data" `
// Method, what is this message doing, etc. CLI, syslog, etc.
Method Method ` json:"method" yaml:"method" `
// ReplyMethod, is the method to use for the reply message.
// By default the reply method will be set to log to file, but
// you can override it setting your own here.
ReplyMethod Method ` json:"replyMethod" yaml:"replyMethod" `
// From what node the message originated
ACKTimeout int ` json:"ACKTimeout" yaml:"ACKTimeout" `
// Resend retries
Retries int ` json:"retries" yaml:"retries" `
// The ACK timeout of the new message created via a request event.
ReplyACKTimeout int ` json:"replyACKTimeout" yaml:"replyACKTimeout" `
// The retries of the new message created via a request event.
ReplyRetries int ` json:"replyRetries" yaml:"replyRetries" `
// Timeout for long a process should be allowed to operate
MethodTimeout int ` json:"methodTimeout" yaml:"methodTimeout" `
// Directory is a string that can be used to create the
//directory structure when saving the result of some method.
// For example "syslog","metrics", or "metrics/mysensor"
// The type is typically used in the handler of a method.
Directory string ` json:"directory" yaml:"directory" `
// FileExtension is used to be able to set a wanted extension
// on a file being saved as the result of data being handled
// by a method handler.
FileExtension string ` json:"fileExtension" yaml:"fileExtension" `
2021-06-17 07:36:42 +00:00
// operation are used to give an opCmd and opArg's.
Operation * Operation ` json:"operation,omitempty" `
2021-06-16 14:14:43 +00:00
}
2021-06-17 06:28:10 +00:00
// Will check and compare all the fields of the main message struct
// used in Steward, and the message struct used in Stew that they are
// equal.
// If they are not equal an error will be returned to the user with
// the name of the field that was missing in the Stew message struct.
//
// Some of the fields in the Steward Message struct are used by the
// system for control, and not needed when creating an initial message
// template, and we can add case statements for those fields below
// that we do not wan't to check.
func checkFieldsMsg ( ) error {
stewardM := Message { }
stewM := msg { }
stewardRefVal := reflect . ValueOf ( stewardM )
stewRefVal := reflect . ValueOf ( stewM )
// Loop trough all the fields of the Message struct, and create
// a an input field or dropdown selector for each field.
// If a field of the struct is not defined below, it will be
// created a "no defenition" element, so it we can easily spot
// Message fields who miss an item in the form.
for i := 0 ; i < stewardRefVal . NumField ( ) ; i ++ {
found := false
for ii := 0 ; ii < stewRefVal . NumField ( ) ; ii ++ {
if stewardRefVal . Type ( ) . Field ( i ) . Name == stewRefVal . Type ( ) . Field ( ii ) . Name {
found = true
break
}
}
// Case statements for the fields we don't care about for
// the message template.
if ! found {
switch stewardRefVal . Type ( ) . Field ( i ) . Name {
case "ID" :
// Not used in message template.
2021-06-17 07:36:42 +00:00
case "FromNode" :
2021-06-17 06:28:10 +00:00
// Not used in message template.
case "PreviousMessage" :
// Not used in message template.
case "done" :
// Not used in message template.
default :
return fmt . Errorf ( "error: %v within the steward Message struct were not found in the stew msg struct" , stewardRefVal . Type ( ) . Field ( i ) . Name )
}
}
}
return nil
}
2021-06-16 19:11:50 +00:00
func drawFormREQ ( reqFillForm * tview . Form , logForm * tview . TextView , app * tview . Application ) error {
2021-06-17 06:28:10 +00:00
m := msg { }
2021-06-14 15:07:31 +00:00
2021-06-21 01:11:50 +00:00
// opCmdItems := map[string]tview.FormItem{}
// OpCmd
// Method
// AllowedNodes
type procItem struct {
label string
formItem tview . FormItem
}
startProcItems := func ( ) [ ] procItem {
procItems := [ ] procItem { }
var m Method
ma := m . GetMethodsAvailable ( )
values := [ ] string { }
for k := range ma . methodhandlers {
values = append ( values , string ( k ) )
}
// reqFillForm.AddDropDown(mRefVal.Type().Field(i).Name, values, 0, nil).SetItemPadding(1)
dn := tview . NewDropDown ( )
dn . SetLabel ( "Method" ) . SetOptions ( values , nil )
procItems = append ( procItems , procItem { label : "Method" , formItem : dn } )
inp := tview . NewInputField ( )
inp . SetLabel ( "AllowedNodes" )
procItems = append ( procItems , procItem { label : "AllowedNodes" , formItem : inp } )
return procItems
} ( )
2021-06-15 20:21:18 +00:00
2021-06-17 06:28:10 +00:00
// Loop trough all the fields of the Message struct, and create
// a an input field or dropdown selector for each field.
// If a field of the struct is not defined below, it will be
// created a "no defenition" element, so it we can easily spot
// Message fields who miss an item in the form.
2021-06-21 01:11:50 +00:00
mRefVal := reflect . ValueOf ( m )
2021-06-15 20:21:18 +00:00
for i := 0 ; i < mRefVal . NumField ( ) ; i ++ {
var err error
values := [ ] string { "1" , "2" }
2021-06-14 15:07:31 +00:00
2021-06-15 20:21:18 +00:00
switch mRefVal . Type ( ) . Field ( i ) . Name {
case "ToNode" :
// Get nodes from file.
values , err = getNodeNames ( "nodeslist.cfg" )
if err != nil {
return err
}
reqFillForm . AddDropDown ( mRefVal . Type ( ) . Field ( i ) . Name , values , 0 , nil ) . SetItemPadding ( 1 )
case "ID" :
case "Data" :
value := ` "bash","-c","..." `
reqFillForm . AddInputField ( "Data" , value , 30 , nil , nil )
case "Method" :
var m Method
ma := m . GetMethodsAvailable ( )
values := [ ] string { }
for k := range ma . methodhandlers {
values = append ( values , string ( k ) )
}
reqFillForm . AddDropDown ( mRefVal . Type ( ) . Field ( i ) . Name , values , 0 , nil ) . SetItemPadding ( 1 )
case "ReplyMethod" :
var m Method
2021-06-16 19:38:33 +00:00
rm := m . getReplyMethods ( )
2021-06-15 20:21:18 +00:00
values := [ ] string { }
2021-06-16 19:38:33 +00:00
for _ , k := range rm {
2021-06-15 20:21:18 +00:00
values = append ( values , string ( k ) )
}
reqFillForm . AddDropDown ( mRefVal . Type ( ) . Field ( i ) . Name , values , 0 , nil ) . SetItemPadding ( 1 )
case "ACKTimeout" :
value := 30
reqFillForm . AddInputField ( "ACKTimeout" , fmt . Sprintf ( "%d" , value ) , 30 , validateInteger , nil )
case "Retries" :
value := 1
reqFillForm . AddInputField ( "Retries" , fmt . Sprintf ( "%d" , value ) , 30 , validateInteger , nil )
case "ReplyACKTimeout" :
value := 30
reqFillForm . AddInputField ( "ACKTimeout" , fmt . Sprintf ( "%d" , value ) , 30 , validateInteger , nil )
case "ReplyRetries" :
value := 1
reqFillForm . AddInputField ( "ReplyRetries" , fmt . Sprintf ( "%d" , value ) , 30 , validateInteger , nil )
case "MethodTimeout" :
value := 120
reqFillForm . AddInputField ( "MethodTimeout" , fmt . Sprintf ( "%d" , value ) , 30 , validateInteger , nil )
case "Directory" :
value := "/some-dir/"
reqFillForm . AddInputField ( "Directory" , value , 30 , nil , nil )
case "FileExtension" :
value := ".log"
reqFillForm . AddInputField ( "FileExtension" , value , 30 , nil , nil )
2021-06-17 07:36:42 +00:00
case "Operation" :
2021-06-21 01:11:50 +00:00
values := [ ] string { "ps" , "startProc" , "stopProc" }
selectedFunc := func ( option string , optionIndex int ) {
switch option {
case "ps" :
// No elements to be drawn for ps
case "startProc" :
fi := reqFillForm . GetFormItemByLabel ( "startProc" )
if fi != nil {
in := reqFillForm . GetFormItemIndex ( "startProc" )
reqFillForm . RemoveFormItem ( in )
}
for _ , v := range startProcItems {
reqFillForm . AddFormItem ( v . formItem )
}
case "stopProc" :
default :
fmt . Fprintf ( logForm , "%v: error: missing menu item for %v\n" , time . Now ( ) . Format ( "Mon Jan _2 15:04:05 2006" ) , option )
}
}
reqFillForm . AddDropDown ( mRefVal . Type ( ) . Field ( i ) . Name , values , 0 , selectedFunc ) . SetItemPadding ( 1 )
2021-06-15 20:21:18 +00:00
default :
2021-06-17 06:28:10 +00:00
// Add a no definition fields to the form if a a field within the
// struct were missing an action above, so we can easily detect
// if there is missing a case action for one of the struct fields.
reqFillForm . AddDropDown ( "error: no case for: " + mRefVal . Type ( ) . Field ( i ) . Name , values , 0 , nil ) . SetItemPadding ( 1 )
2021-06-14 15:07:31 +00:00
}
2021-06-14 04:49:47 +00:00
}
2021-06-15 20:21:18 +00:00
reqFillForm .
2021-06-17 06:28:10 +00:00
// Add a generate button, which when pressed will loop trouug
2021-06-15 20:21:18 +00:00
AddButton ( "generate file" , func ( ) {
2021-06-17 06:28:10 +00:00
fh , err := os . Create ( "message.json" )
2021-06-15 20:21:18 +00:00
if err != nil {
log . Fatalf ( "error: failed to create test.log file: %v\n" , err )
}
defer fh . Close ( )
2021-06-16 04:03:09 +00:00
m := msg { }
// Loop trough all the form fields
2021-06-15 20:21:18 +00:00
for i := 0 ; i < reqFillForm . GetFormItemCount ( ) ; i ++ {
fi := reqFillForm . GetFormItem ( i )
2021-06-16 04:03:09 +00:00
label , value := getLabelAndValue ( fi )
switch label {
case "ToNode" :
m . ToNode = node ( value )
case "Data" :
2021-06-16 14:14:43 +00:00
// Split the comma separated string into a
// and remove the start and end ampersand.
2021-06-16 04:03:09 +00:00
sp := strings . Split ( value , "," )
2021-06-16 19:11:50 +00:00
2021-06-16 14:14:43 +00:00
var data [ ] string
for _ , v := range sp {
2021-06-16 19:11:50 +00:00
// Check if format is correct, return if not.
2021-06-16 14:14:43 +00:00
pre := strings . HasPrefix ( v , "\"" )
2021-06-16 19:11:50 +00:00
suf := strings . HasSuffix ( v , "\"" )
if ! pre || ! suf {
fmt . Fprintf ( logForm , "%v : error: malformed format for command, should be \"cmd\",\"arg1\",\"arg2\" ...\n" , time . Now ( ) . Format ( "Mon Jan _2 15:04:05 2006" ) )
return
2021-06-16 14:14:43 +00:00
}
2021-06-16 19:11:50 +00:00
// Remove leading and ending ampersand.
v = v [ 1 : ]
v = strings . TrimSuffix ( v , "\"" )
2021-06-16 14:14:43 +00:00
data = append ( data , v )
}
m . Data = data
2021-06-16 04:03:09 +00:00
case "Method" :
m . Method = Method ( value )
case "ReplyMethod" :
m . ReplyMethod = Method ( value )
case "ACKTimeout" :
v , _ := strconv . Atoi ( value )
m . ACKTimeout = v
case "Retries" :
v , _ := strconv . Atoi ( value )
m . Retries = v
case "ReplyACKTimeout" :
v , _ := strconv . Atoi ( value )
m . ReplyACKTimeout = v
case "ReplyRetries" :
v , _ := strconv . Atoi ( value )
m . ReplyRetries = v
case "MethodTimeout" :
v , _ := strconv . Atoi ( value )
m . MethodTimeout = v
case "Directory" :
m . Directory = value
case "FileExtension" :
m . FileExtension = value
2021-06-17 07:36:42 +00:00
case "Operation" :
m . Operation = nil
2021-06-17 06:28:10 +00:00
default :
fmt . Fprintf ( logForm , "%v : error: did not find case defenition for how to handle the \"%v\" within the switch statement\n" , time . Now ( ) . Format ( "Mon Jan _2 15:04:05 2006" ) , label )
2021-06-15 20:21:18 +00:00
}
}
2021-06-16 04:03:09 +00:00
msgs := [ ] msg { }
msgs = append ( msgs , m )
jEnc := json . NewEncoder ( fh )
jEnc . Encode ( msgs )
2021-06-15 20:21:18 +00:00
} ) .
AddButton ( "exit" , func ( ) {
app . Stop ( )
} )
app . SetFocus ( reqFillForm )
2021-06-14 04:49:47 +00:00
return nil
}
2021-06-16 14:14:43 +00:00
// Will return the Label And the text Value of an input or dropdown form field.
2021-06-16 04:03:09 +00:00
func getLabelAndValue ( fi tview . FormItem ) ( string , string ) {
var label string
var value string
switch v := fi . ( type ) {
case * tview . InputField :
value = v . GetText ( )
label = v . GetLabel ( )
case * tview . DropDown :
label = v . GetLabel ( )
_ , value = v . GetCurrentOption ( )
}
return label , value
}
2021-06-16 14:14:43 +00:00
// Check if number is int.
2021-06-15 20:21:18 +00:00
func validateInteger ( text string , ch rune ) bool {
if text == "-" {
return true
}
_ , err := strconv . Atoi ( text )
return err == nil
}
2021-06-14 15:07:31 +00:00
// getNodes will load all the node names from a file, and return a slice of
// string values, each representing a unique node.
func getNodeNames ( filePath string ) ( [ ] string , error ) {
fh , err := os . Open ( filePath )
if err != nil {
return nil , fmt . Errorf ( "error: unable to open node file: %v" , err )
2021-06-14 04:49:47 +00:00
}
2021-06-14 15:07:31 +00:00
defer fh . Close ( )
nodes := [ ] string { }
2021-06-14 04:49:47 +00:00
2021-06-14 15:07:31 +00:00
scanner := bufio . NewScanner ( fh )
for scanner . Scan ( ) {
node := scanner . Text ( )
nodes = append ( nodes , node )
2021-06-14 04:49:47 +00:00
}
2021-06-14 15:07:31 +00:00
return nodes , nil
2021-06-14 04:49:47 +00:00
}