gocage/cmd/list.go

485 lines
13 KiB
Go
Raw Normal View History

2021-12-18 13:13:25 +01:00
package cmd
import (
"os"
"fmt"
"log"
2021-12-19 16:49:07 +01:00
"errors"
2021-12-18 15:22:55 +01:00
"reflect"
"strings"
2021-12-18 13:13:25 +01:00
"io/ioutil"
"gocage/jail"
"encoding/json"
"github.com/spf13/viper"
)
2021-12-19 16:49:07 +01:00
// Recurse into structure, returning reflect.Value of wanted field.
// Nested fields are named with a dot (ex "MyStruct.MyField")
func getStructFieldValue(parentStruct interface{}, fieldName string) (reflect.Value, string, error) {
v := reflect.ValueOf(parentStruct)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if false {
for i := 0 ; i < v.NumField(); i++ {
f := v.Field(i)
if f.Kind() == reflect.String {
fmt.Printf("%v=%v\n", v.Type().Field(i).Name, f.Interface())
}
}
}
if strings.Contains(fieldName, ".") {
fs := strings.Split(fieldName, ".")
f := v.FieldByName(fs[0])
if f.Kind() == reflect.Struct {
return getStructFieldValue(f.Interface(), strings.Join(fs[1:], "."))
} else {
log.Fatalln(fmt.Sprintf("%s is not a struct: %s\n", fs[0], f.Kind().String()))
}
} else {
f := v.FieldByName(fieldName)
if f.IsValid() {
return f, fieldName, nil
} else {
return v, fieldName, errors.New(fmt.Sprintf("Field not found: %s", fieldName))
}
}
return v, fieldName, nil
}
// TODO : Replace by getStructFieldValue
// Recurse into structure, returning reflect.Value of wanted field.
// Nested fields are named with a dot (ex "MyStruct.MyField")
func getStructField(parentStruct interface{}, fieldName string) (reflect.Value, string) {
v := reflect.ValueOf(parentStruct)
if v.Kind() == reflect.Ptr {
v = v.Elem()
}
if false {
for i := 0 ; i < v.NumField(); i++ {
f := v.Field(i)
if f.Kind() == reflect.String {
fmt.Printf("%v=%v\n", v.Type().Field(i).Name, f.Interface())
}
}
}
if strings.Contains(fieldName, ".") {
fs := strings.Split(fieldName, ".")
f := v.FieldByName(fs[0])
if f.Kind() == reflect.Struct {
return getStructField(f.Interface(), strings.Join(fs[1:], "."))
} else {
log.Fatalln(fmt.Sprintf("%s is not a struct: %s\n", fs[0], f.Kind().String()))
}
}
return v, fieldName
}
2021-12-18 15:22:55 +01:00
/* Pretty display of jails field
Fields to show are given in a string array parameter
Ex. : displayJails(["Name", "JID", "RootPath"])
*/
func displayStructFields(jails []Jail, valsToDisplay []string) {
/* A line is defined by :
Nr of fields
For each field :
Its name
Its max length
Its value
*/
type Field struct {
Name string
MaxLen int
Value string
}
type Line []Field
type Output []Line
var out Output
//fmt.Printf("%d fields in a %d items structure\n", len(valsToDisplay), len(jails))
// Browse structure to get max length and values
for _, j := range jails {
// Have to use a pointer, else reflect.Value.Elem() will panic : https://pkg.go.dev/reflect#Value.Elem
tj := &j
line := make([]Field, len(valsToDisplay))
for i, f := range valsToDisplay {
a, f := getStructField(tj, f)
2021-12-18 15:22:55 +01:00
field := Field {
Name: f,
}
if a.FieldByName(f).IsValid() {
2021-12-18 15:22:55 +01:00
// For now this just contains this item length, will adjust later
// We need to divide length by number of items in string fields (because we can have more than 1 ip4_addr)
if reflect.TypeOf(a.FieldByName(f).Interface()).Kind() == reflect.String {
itnr := len(strings.Split(string(a.FieldByName(f).Interface().(string)), ","))
field.MaxLen = len(fmt.Sprintf("%v", a.FieldByName(f).Interface())) / itnr
} else {
field.MaxLen = len(fmt.Sprintf("%v", a.FieldByName(f).Interface()))
}
field.Value = fmt.Sprintf("%v", a.FieldByName(f).Interface())
} else {
fmt.Printf("Invalid field name: %s\n", f)
2021-12-18 15:22:55 +01:00
}
line[i] = field
}
out = append(out, line)
}
2021-12-19 16:49:07 +01:00
if len(out) == 0 {
fmt.Printf("Nothing to see here!\n")
return
}
2021-12-18 15:22:55 +01:00
// Get real maximum length
maxlen := make([]int, len(valsToDisplay))
for i := 0; i< len(valsToDisplay); i++ {
maxlen[i] = len(valsToDisplay[i])
2021-12-18 15:22:55 +01:00
}
for _, l := range out {
for i, f := range l {
if f.MaxLen > maxlen[i] {
maxlen[i] = f.MaxLen
}
}
}
// Align maxlen to the real maximum length
for io, l := range out {
for ii, _ := range l {
// We need to access slice by index if we want to modify content
out[io][ii].MaxLen = maxlen[ii]
}
}
totalLen := 0
// For each field, add separator and 2 blank spaces
2021-12-18 15:22:55 +01:00
for _, f := range out[0] {
totalLen += f.MaxLen + 3
}
// Then add starting delimiter
totalLen += 1
Debug := false
if Debug == true {
for _, f := range out[0] {
fmt.Printf("%s max length : %d\n", f.Name, f.MaxLen)
}
}
// Lets draw things on the screen!
// First, headers: 1st separator line
for i, f := range out[0] {
if i == 0 { fmt.Printf("+") }
for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("=") }
fmt.Printf("+")
2021-12-18 15:22:55 +01:00
}
fmt.Printf("\n")
// Column names
2021-12-18 15:22:55 +01:00
for i, f := range out[0] {
if i == 0 {
fmt.Printf("|")
}
/* Use vlsToDisplay to get real name (with "Config.")
2021-12-18 15:22:55 +01:00
fmt.Printf(" %s", f.Name)
for i := len(f.Name)+1 ; i < f.MaxLen+1 ; i++ { */
fmt.Printf(" %s", valsToDisplay[i])
for i := len(valsToDisplay[i])+1 ; i < f.MaxLen+1 ; i++ {
2021-12-18 15:22:55 +01:00
fmt.Printf(" ")
}
fmt.Printf(" |")
}
// Finally separator line
2021-12-18 15:22:55 +01:00
fmt.Printf("\n")
for i, f := range out[0] {
if i == 0 { fmt.Printf("+") }
for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("=") }
fmt.Printf("+")
2021-12-18 15:22:55 +01:00
}
fmt.Printf("\n")
// Then display data
// Loop through lines
2021-12-18 15:22:55 +01:00
for _, l := range out {
2021-12-19 16:49:07 +01:00
// Loop through fields
// In case we need to add a line for a 2nd IP, or whatever object
var supplines = make(map[string]string)
2021-12-18 15:22:55 +01:00
for i, f := range l {
if i == 0 {
fmt.Printf("|")
}
// Special cases of value displaying
if f.Name == "JID" && f.Value == "0" {
fmt.Printf(" ")
} else if f.Name == "Ip4_addr" {
ia := strings.Split(f.Value, ",")
// If we have more than 1 value we need to finish this line, and store value for writing at the end of line loop
for i, inter := range ia {
if i > 0 {
supplines[f.Name] = inter
} else {
fmt.Printf(" %s", inter)
}
}
//fmt.Printf(" %s", strings.Split(strings.Split(f.Value, "|")[1], "/")[0])
} else {
fmt.Printf(" %s", f.Value)
}
// Complete with spaces to the max length
2021-12-18 15:22:55 +01:00
for i := len(f.Value)+1 ; i < f.MaxLen+1 ; i++ {
fmt.Printf(" ")
}
fmt.Printf(" |")
}
// Draw supplementary lines
if len(supplines) > 0 {
for i, f := range l {
if i == 0 {
fmt.Printf("\n|")
}
// Overwrite value in this scope
v, exists := supplines[f.Name]
if exists {
fmt.Printf(" %s", v)
} else {
// 1st option : Do not redisplay precedent line values
fmt.Printf(" ")
// 2nd option : Redisplay precedenet line values
//fmt.Printf(" %s", f.Value)
}
// Complete with spaces to the max length
for i := len(v)+1 ; i < f.MaxLen+1 ; i++ {
fmt.Printf(" ")
}
fmt.Printf(" |")
}
}
// Draw line separator between jails
if !gNoLineSep {
fmt.Printf("\n")
for i, f := range out[0] {
if i == 0 { fmt.Printf("+") }
for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("-") }
fmt.Printf("+")
}
}
2021-12-18 15:22:55 +01:00
fmt.Printf("\n")
}
if gNoLineSep {
for i, f := range out[0] {
if i == 0 { fmt.Printf("+") }
for i := 0 ; i < f.MaxLen+2 ; i++ { fmt.Printf("-") }
fmt.Printf("+")
}
2021-12-18 15:22:55 +01:00
}
2021-12-18 15:22:55 +01:00
fmt.Printf("\n")
}
2021-12-18 13:13:25 +01:00
/* Get Jails from datastores. Store config and running metadata into gJails global var */
func ListJails(args []string, display bool) {
2021-12-19 13:05:30 +01:00
fields := strings.Split(gDisplayColumns, ",")
2021-12-18 13:13:25 +01:00
for _, d := range viper.GetStringSlice("datastore") {
listJailsFromDatastore(d)
}
2021-12-19 16:49:07 +01:00
// This is the structure we will filter, then display
var jails []Jail
/************************************************************************************
/ Filter jails with "filter" options
/***********************************************************************************/
if len(gFilterJails) > 0 && gFilterJails != "none" {
flts := make(map[string]string)
for _, flt := range strings.Split(gFilterJails, ",") {
sa := strings.Split(flt, "=")
if len(sa) != 2 {
fmt.Printf("Invalid format for filter %s\n", gFilterJails)
return
}
flts[sa[0]] = sa[1]
}
if len(flts) > 0 {
// Browse global gJails array to get filtered results
for _, j := range gJails {
for key, val := range flts {
v, _, err := getStructFieldValue(&j, key)
if err != nil {
fmt.Printf("ERROR: Field %s not found\n", key)
return
}
if strings.EqualFold(fmt.Sprintf("%v", v.Interface()), val) {
jails = append(jails, j)
}
}
}
}
2021-12-19 16:49:07 +01:00
} else {
jails = gJails
}
/************************************************************************************
/ Filter jails by names given on command line
/***********************************************************************************/
if len(args) > 0 {
var js []Jail
for _, a := range args {
for _, j := range jails {
if j.Name == a {
js = append(js, j)
break
}
}
}
jails = js
}
2021-12-20 22:10:38 +01:00
/************************************************************************************
/ Sort jails
/ We support 3 sort criteria max
/***********************************************************************************/
2021-12-21 20:47:46 +01:00
if len(gSortFields) > 0 && gSortFields != "none" {
2021-12-20 22:10:38 +01:00
js := initSortStruct()
// The way we manage criteria quantity is not very elegant...
var fct1, fct2, fct3 reflect.Value
for i, c := range strings.Split(gSortFields, ",") {
var fctName string
if strings.HasPrefix(c, "-") {
fctName = fmt.Sprintf("%sDec", strings.Replace(c, "-", "", 1))
} else { // Par defaut (pas de prefix +/-) on considere un tri incremental
fctName = fmt.Sprintf("%sInc", strings.Replace(c, "+", "", 1))
}
// Get function by its name
fct, _, err := getStructFieldValue(js, fctName)
if err != nil {
fieldName := strings.Replace(strings.Replace(c, "-", "", 1), "+", "", 1)
fmt.Printf("ERROR getting JailSort struct field %s. Please check the field name: %s\n", fctName, fieldName)
return
}
switch i+1 {
case 1: fct1 = fct
case 2: fct2 = fct
case 3: fct3 = fct
}
}
switch len(strings.Split(gSortFields, ",")) {
case 1:
OrderedBy(fct1.Interface().(lessFunc)).Sort(jails)
case 2:
OrderedBy(fct1.Interface().(lessFunc), fct2.Interface().(lessFunc)).Sort(jails)
case 3:
OrderedBy(fct1.Interface().(lessFunc), fct2.Interface().(lessFunc), fct3.Interface().(lessFunc)).Sort(jails)
}
}
/************************************************************************************
/ Finally, display jails
/***********************************************************************************/
2021-12-19 16:49:07 +01:00
if display {
displayStructFields(jails, fields)
}
2021-12-18 13:13:25 +01:00
}
func listJailsFromDatastore(datastore string) {
fileInfo, err := os.Stat(datastore)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", datastore)) }
if fileInfo.IsDir() == false { log.Fatalln(fmt.Sprintf("%s is not a directory", datastore)) }
// A datastore have to contain a "jails" directory
jailsDir := fmt.Sprintf("%s/jails", datastore)
fileInfo, err = os.Stat(jailsDir)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", jailsDir)) }
if fileInfo.IsDir() == false { log.Fatalln(fmt.Sprintf("%s is not a directory", jailsDir)) }
listJailsFromDirectory(jailsDir)
}
func listJailsFromDirectory(dir string) ([]Jail) {
files, err := ioutil.ReadDir(dir)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to browse %s, check path and/or rights", dir)) }
for _, fi := range files {
if fi.IsDir() == true {
// 1. Get conf from config.json
jailConfPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "config.json")
jailConf, err := getJailConfig(jailConfPath)
if err != nil { log.Println("ERROR reading jail config for %s", jailConfPath) }
// 2. Build jail object from config
jailRootPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "root")
j := Jail{
Name: jailConf.Host_hostname,
Config: jailConf,
ConfigPath: jailConfPath,
RootPath: jailRootPath,
Running: false,
}
// 3. Add current running informations
rjails, err := jail.GetJails()
if err != nil { log.Fatalln("Unable to list running jails") }
for _, rj := range rjails {
if rj.Path == j.RootPath {
j.JID = rj.Jid
j.Running = true
j.InternalName = rj.Name
break
2021-12-18 13:13:25 +01:00
}
}
/* This op take some 600ms for ~40 jails :^( */
out, err := executeCommand(fmt.Sprintf("zfs list -H -o name %s", j.RootPath))
if err != nil {
fmt.Printf("ERROR getting dataset from %s: %s\n", j.RootPath, err.Error())
} else {
j.Zpool = strings.Split(strings.TrimSuffix(out, "\n"), "/")[0]
}
2021-12-18 13:13:25 +01:00
gJails = append(gJails, j)
}
}
return gJails
}
func getJailConfig(jailConfigPath string) (JailConfig, error) {
content, err := ioutil.ReadFile(jailConfigPath)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to read %s, check path and/or rights", jailConfigPath)) }
var jc JailConfig
err = json.Unmarshal([]byte(content), &jc)
if err != nil { log.Fatalln(fmt.Sprintf("Error occured during unmarshaling %s: %s", jailConfigPath, err.Error())) }
return jc, err
}