From 9218ffafe1c49acdf17106cbb591c3f4770d7fdc Mon Sep 17 00:00:00 2001 From: yo Date: Sat, 18 Jun 2022 18:24:09 +0200 Subject: [PATCH] Add datastore to snapshots, force Datastore display when jail exist on multi datastores --- cmd/list.go | 36 ++++++++++---------- cmd/migrate.go | 44 ++++++++++++------------- cmd/root.go | 9 +++-- cmd/snapshots.go | 47 ++++++++++++++++---------- cmd/struct.go | 15 +++++---- cmd/utils.go | 86 +++++++++++++++++++++++++++++++++--------------- 6 files changed, 145 insertions(+), 92 deletions(-) diff --git a/cmd/list.go b/cmd/list.go index 6824378..9357bda 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -3,7 +3,6 @@ package cmd import ( "encoding/json" "fmt" - "github.com/spf13/viper" "gocage/jail" "io/ioutil" "log" @@ -40,12 +39,12 @@ func ListJailsProps(args []string) { * into gJails global var *******************************************************************************/ func ListJails(args []string, display bool) { - fields := strings.Split(gDisplayJColumns, ",") - - for _, d := range viper.GetStringSlice("datastore") { - listJailsFromDatastore(d) + for _, ds := range gDatastores { + listJailsFromDatastore(ds) } - + + fields := strings.Split(gDisplayJColumns, ",") + // This is the structure we will filter, then display var jails []Jail @@ -91,7 +90,6 @@ func ListJails(args []string, display bool) { for _, j := range jails { if j.Name == a { js = append(js, j) - break } } } @@ -150,17 +148,17 @@ func ListJails(args []string, display bool) { } } -func listJailsFromDatastore(datastore string) { - fileInfo, err := os.Stat(datastore) +func listJailsFromDatastore(ds Datastore) { + fileInfo, err := os.Stat(ds.Mountpoint) if err != nil { - log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", datastore)) + log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", ds.Mountpoint)) } if fileInfo.IsDir() == false { - log.Fatalln(fmt.Sprintf("%s is not a directory", datastore)) + log.Fatalln(fmt.Sprintf("%s is not a directory", ds.Mountpoint)) } // A datastore have to contain a "jails" directory - jailsDir := fmt.Sprintf("%s/jails", datastore) + jailsDir := fmt.Sprintf("%s/jails", ds.Mountpoint) fileInfo, err = os.Stat(jailsDir) if err != nil { log.Fatalln(fmt.Sprintf("Unable to access %s, check path and/or rights", jailsDir)) @@ -169,10 +167,10 @@ func listJailsFromDatastore(datastore string) { log.Fatalln(fmt.Sprintf("%s is not a directory", jailsDir)) } - listJailsFromDirectory(jailsDir) + listJailsFromDirectory(jailsDir, ds.Name) } -func listJailsFromDirectory(dir string) []Jail { +func listJailsFromDirectory(dir string, dsname string) []Jail { files, err := ioutil.ReadDir(dir) if err != nil { log.Fatalln(fmt.Sprintf("Unable to browse %s, check path and/or rights", dir)) @@ -194,6 +192,7 @@ func listJailsFromDirectory(dir string) []Jail { Name: jailConf.Host_hostname, Config: jailConf, ConfigPath: jailConfPath, + Datastore: dsname, RootPath: jailRootPath, Running: false, } @@ -223,9 +222,12 @@ func listJailsFromDirectory(dir string) []Jail { // Check if jail with the same name already exist on another DS for _, jj := range gJails { if strings.EqualFold(jj.Name, j.Name) { - fmt.Printf("ERROR: A jail with name %s already exist on datastore %s!\n", j.Name, jj.Zpool) - fmt.Printf("Jail %s on datastore %s wont be handled\n", j.Name, j.Zpool) - return gJails + fmt.Printf(" ---------------------------------------------- \n") + fmt.Printf("Warning: A jail with name %s already exist on datastore %s!\n", j.Name, jj.Datastore) + fmt.Printf(" ---------------------------------------------- \n") + // Add Datastore to avoid confusion + gDisplayJColumns += ",Datastore" + gDisplaySColumns += ",Datastore" } } diff --git a/cmd/migrate.go b/cmd/migrate.go index 826654d..39f5aad 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -18,7 +18,6 @@ const ( *******************************************************************************/ func MigrateJail(args []string) { var jailNames []string - var destDS Datastore if len(args) > 0 { for _, a := range args { @@ -27,26 +26,24 @@ func MigrateJail(args []string) { } for _, jn := range jailNames { - // Check if destination datastore exist - found := false - for _, ds := range gDatastores { - if strings.EqualFold(gMigrateDestDatastore, ds.Name) { - found = true - destDS = ds - break - } - } - if false == found { - fmt.Printf("Unkown datastore: %s\n", gMigrateDestDatastore) - return - } - cj, err := getJailFromArray(jn, gJails) if cj == nil { fmt.Printf("Error getting jail %s: Not found\n", jn) return } + // Check if destination datastore exist & get current DS + destDS, err := getDatastoreFromArray(gMigrateDestDatastore, gDatastores) + if err != nil { + fmt.Printf("Error getting datastore \"%s\": %v\n", gMigrateDestDatastore, err) + return + } + curDS, err := getDatastoreFromArray(cj.Datastore, gDatastores) + if err != nil { + fmt.Printf("Error getting datastore \"%s\": %v\n", gMigrateDestDatastore, err) + return + } + if cj.Running == true && gYesToAll == false { fmt.Printf("WARNING: Jail %s is running\n", cj.Name) fmt.Printf("Migration will stop it for data sync before starting on the new pool. You will be prompted for shutdown.\n") @@ -74,7 +71,7 @@ func MigrateJail(args []string) { */ // Snapshot config - dsconf := strings.Join([]string{cj.Zpool, "iocage", "jails", jn}, "/") + dsconf := strings.Join([]string{curDS.ZFSDataset, "jails", jn}, "/") fmt.Printf("Snapshot %s: ", dsconf) if err = zfsSnapshot(dsconf, "gocage_mig_init"); err != nil { fmt.Printf("Error: %v\n", err) @@ -83,7 +80,7 @@ func MigrateJail(args []string) { fmt.Printf("Done\n") // Snapshot filesystem - dsdata := strings.Join([]string{cj.Zpool, "iocage", "jails", jn, "root"}, "/") + dsdata := strings.Join([]string{curDS.ZFSDataset, "iocage", "jails", jn, "root"}, "/") fmt.Printf("Snapshot %s: ", dsdata) if err := zfsSnapshot(dsdata, "gocage_mig_init"); err != nil { fmt.Printf("Error: %v\n", err) @@ -178,11 +175,14 @@ func CleanMigrateMess(args []string) error { for _, jn := range jailNames { cj, err := getJailFromArray(jn, gJails) if cj == nil { - fmt.Printf("Error getting jail %s: Not found\n", jn) return errors.New(fmt.Sprintf("Error getting jail %s: Not found\n", jn)) } + curDS, err := getDatastoreFromArray(cj.Datastore, gDatastores) + if err != nil { + return errors.New(fmt.Sprintf("Error getting datastore \"%s\": %v\n", cj.Datastore, err)) + } - cmd := fmt.Sprintf("zfs destroy %s@gocage_mig_init", strings.Join([]string{cj.Zpool, "iocage", "jails", jn}, "/")) + cmd := fmt.Sprintf("zfs destroy %s@gocage_mig_init", strings.Join([]string{curDS.ZFSDataset, "jails", jn}, "/")) out, err := executeCommand(cmd) if err != nil { if false == strings.HasSuffix(out, "could not find any snapshots to destroy; check snapshot names.\n") { @@ -190,7 +190,7 @@ func CleanMigrateMess(args []string) error { return errors.New(fmt.Sprintf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)) } } - cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_init", strings.Join([]string{cj.Zpool, "iocage", "jails", jn, "root"}, "/")) + cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_init", strings.Join([]string{curDS.ZFSDataset, "jails", jn, "root"}, "/")) out, err = executeCommand(cmd) if err != nil { if false == strings.HasSuffix(out, "could not find any snapshots to destroy; check snapshot names.\n") { @@ -198,7 +198,7 @@ func CleanMigrateMess(args []string) error { return errors.New(fmt.Sprintf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)) } } - cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_last_sync", strings.Join([]string{cj.Zpool, "iocage", "jails", jn}, "/")) + cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_last_sync", strings.Join([]string{curDS.ZFSDataset, "jails", jn}, "/")) out, err = executeCommand(cmd) if err != nil { if false == strings.HasSuffix(out, "could not find any snapshots to destroy; check snapshot names.\n") { @@ -206,7 +206,7 @@ func CleanMigrateMess(args []string) error { return errors.New(fmt.Sprintf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)) } } - cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_last_sync", strings.Join([]string{cj.Zpool, "iocage", "jails", jn, "root"}, "/")) + cmd = fmt.Sprintf("zfs destroy %s@gocage_mig_last_sync", strings.Join([]string{curDS.ZFSDataset, "jails", jn, "root"}, "/")) out, err = executeCommand(cmd) if err != nil { if false == strings.HasSuffix(out, "could not find any snapshots to destroy; check snapshot names.\n") { diff --git a/cmd/root.go b/cmd/root.go index e5a370f..fa34dce 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -225,7 +225,10 @@ You can specify multiple jails.`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) - CleanMigrateMess(args) + err := CleanMigrateMess(args) + if err != nil { + fmt.Printf("%v", err) + } }, } @@ -262,8 +265,8 @@ func init() { // Command dependant switches - // We reuse these in "gocage snapshot list myjail" and 'gocage datastore list" commands (TODO) - listCmd.Flags().StringVarP(&gDisplayJColumns, "outcol", "o", "JID,Name,Config.Release,Config.Ip4_addr,Running", "Show these columns in output") + // We reuse these flags in "gocage snapshot list myjail" and 'gocage datastore list" commands + listCmd.Flags().StringVarP(&gDisplayJColumns, "outcol", "o", "JID,Name,Datastore,Config.Release,Config.Ip4_addr,Running", "Show these columns in output") listCmd.Flags().BoolVarP(&gNoJailLineSep, "nolinesep", "l", false, "Do not display line separator between jails") listCmd.Flags().StringVarP(&gFilterJails, "filter", "f", "none", "Only display jails with these values. Ex: \"gocage list -f Config.Boot=1\" will only list started on boot jails") listCmd.Flags().StringVarP(&gSortJailFields, "sort", "s", "none", "Display jails sorted by field values. Ex: \"gocage list -s +Name,-Config.Priority\" will sort jails by their decreasing name, then increasing start priority. 3 critera max supported.") diff --git a/cmd/snapshots.go b/cmd/snapshots.go index fe0074e..d531452 100644 --- a/cmd/snapshots.go +++ b/cmd/snapshots.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "regexp" + "strconv" "strings" "reflect" "time" @@ -23,6 +24,10 @@ func ListJailsSnapshots(args []string) { /**************************************************************/ if len(args) > 0 { for _, a := range args { + /*if countOfJailsWithThisName(a) > 1 { + fmt.Printf("Nope") + return + }*/ jailNames = append(jailNames, a) } } @@ -99,23 +104,27 @@ func listJailSnapshots(jail Jail) []Snapshot { // 1. List all datasets // TODO : Include mounted filesystems? - rs := strings.Split(jail.RootPath, "/") - rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) - cmd := fmt.Sprintf("zfs list -r -H -o name,mountpoint,used,referenced,creation -t snapshot %s", rootDataset) + + curDS, err := getDatastoreFromArray(jail.Datastore, gDatastores) + if err != nil { + fmt.Printf("Error getting datastore \"%s\": %v\n", jail.Datastore, err) + return snapshots + } + + rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name) + cmd := fmt.Sprintf("zfs list -p -r -H -o name,mountpoint,used,referenced,creation -t snapshot %s", rootDataset) out, err := executeCommand(cmd) if err != nil { fmt.Printf("Error: %s\n", err.Error()) return snapshots } - - dateLayout := "Mon Jan _2 15:04 2006" - loc, _ := time.LoadLocation(gTimeZone) - + for _, line := range strings.Split(out, "\n") { if len(line) > 0 { ls := strings.Split(line, "\t") // Parse creation date so we can use it to sort snapshots - creationts, err := time.ParseInLocation(dateLayout, ls[4], loc) + //creationts, err := time.ParseInLocation(dateLayout, ls[4], loc) + creationts, err := strconv.ParseInt(ls[4], 10, 64) if err != nil { fmt.Println("Error while parsing date %s:", ls[4], err) return snapshots @@ -123,13 +132,15 @@ func listJailSnapshots(jail Jail) []Snapshot { // Get subdir to append to snapshot name subdir := strings.Replace(strings.Split(ls[0], "@")[0], rootDataset, "", 1) - snapshots = append(snapshots, Snapshot{Dsname: ls[0], + u, _ := strconv.ParseUint(ls[2], 10, 64) + r, _ := strconv.ParseUint(ls[3], 10, 64) + snapshots = append(snapshots, Snapshot{Datastore: curDS.Name, Name: fmt.Sprintf("%s%s", strings.Split(ls[0], "@")[1], subdir), Jailname: jail.Name, Mountpoint: ls[1], - Used: ls[2], - Referenced: ls[3], - Creation: creationts}) + Used: u, + Referenced: r, + Creation: time.Unix(creationts, 0)}) } } @@ -165,8 +176,8 @@ func CreateJailSnapshot(args []string) { * Create snapshot for a jail *******************************************************************************/ func createJailSnapshot(jail Jail) error { - rs := strings.Split(jail.RootPath, "/") - rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) + curDS, _ := getDatastoreFromArray(jail.Datastore, gDatastores) + rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name) cmd := fmt.Sprintf("zfs snapshot -r %s@%s", rootDataset, gSnapshotName) _, err := executeCommand(cmd) @@ -207,8 +218,8 @@ func deleteJailSnapshot(jail Jail) error { var snaptodel []string // Get all recursive snapshots - rs := strings.Split(jail.RootPath, "/") - rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) + curDS, _ := getDatastoreFromArray(jail.Datastore, gDatastores) + rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name) cmd := fmt.Sprintf("zfs list -r -H -o name -t snapshot %s", rootDataset) out, err := executeCommand(cmd) if err != nil { @@ -284,8 +295,8 @@ func rollbackJailSnapshot(jail Jail) error { // We need to rollback parent and childs // Get all recursive snapshots - rs := strings.Split(jail.RootPath, "/") - rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) + curDS, _ := getDatastoreFromArray(jail.Datastore, gDatastores) + rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name) cmd := fmt.Sprintf("zfs list -r -H -o name -t snapshot %s", rootDataset) out, err := executeCommand(cmd) if err != nil { diff --git a/cmd/struct.go b/cmd/struct.go index 9704ebe..386493b 100644 --- a/cmd/struct.go +++ b/cmd/struct.go @@ -27,7 +27,8 @@ type Jail struct { Running bool // No need, Config.Release always represent what is running (plus it know release for non-running jails) //Release string - Zpool string + Zpool string + Datastore string } // iocage struct as stored in config.json @@ -191,11 +192,11 @@ type Mount struct { type Snapshot struct { // snapshot name is stored after '@' in dataset name Name string - Dsname string + Datastore string Jailname string Mountpoint string - Used string - Referenced string + Used uint64 + Referenced uint64 Creation time.Time } @@ -215,6 +216,8 @@ type JailSort struct { ConfigPathDec jailLessFunc RunningInc jailLessFunc RunningDec jailLessFunc + DatastoreInc jailLessFunc + DatastoreDec jailLessFunc ZpoolInc jailLessFunc ZpoolDec jailLessFunc Config JailConfigSort @@ -498,8 +501,8 @@ type JailConfigSort struct { type SnapshotSort struct { NameInc snapshotLessFunc NameDec snapshotLessFunc - DsnameInc snapshotLessFunc - DsnameDec snapshotLessFunc + DatastoreInc snapshotLessFunc + DatastoreDec snapshotLessFunc JailnameInc snapshotLessFunc JailnameDec snapshotLessFunc MountpointInc snapshotLessFunc diff --git a/cmd/utils.go b/cmd/utils.go index c7cd12a..f7a780d 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -391,7 +391,7 @@ func getJailFromArray(name string, jarray []Jail) (*Jail, error) { return &Jail{}, errors.New("Jail not found") } -/***************************************************************************** +/****************************************************************************** * * Generic utilities * @@ -405,6 +405,28 @@ func isStringInArray(strarr []string, searched string) bool { return false } +func getDatastoreFromArray(name string, dsa []Datastore) (*Datastore, error) { + for _, d := range dsa { + if name == d.Name { + return &d, nil + } + } + return &Datastore{}, errors.New("Datastore not found") +} + +/****************************************************************************** + * Return the quantity of jails with the name passed as parameter + *****************************************************************************/ +func countOfJailsWithThisName(name string) int { + count := 0 + for _, j := range gJails { + if strings.EqualFold(j.Name, name) { + count++ + } + } + return count +} + /******************************************************************************** * Recurse into structure, returning reflect.Kind of named field. * Nested fields are named with a dot (ex "MyStruct.MyField") @@ -836,7 +858,7 @@ func displayJailsFields(jails []Jail, valsToDisplay []string) { /******************************************************************************** * Pretty display of snapshots field * Fields to show are given in a string array parameter - * Ex. : displaySnapshotsFields(snapshots, ["Name", "Dsname", "Used"]) + * Ex. : displaySnapshotsFields(snapshots, ["Name", "Datastore", "Used"]) *******************************************************************************/ func displaySnapshotsFields(snaps []Snapshot, valsToDisplay []string) { /* A line is defined by : @@ -878,7 +900,14 @@ func displaySnapshotsFields(snaps []Snapshot, valsToDisplay []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())) + // Special case of disk size : We will print human readable values + if field.Name == "Used" || field.Name == "Referenced" || field.Name == "Available" { + var v datasize.ByteSize + v.UnmarshalText([]byte(fmt.Sprintf("%v", a.FieldByName(f).Interface()))) + field.MaxLen = len(fmt.Sprintf("%v", v.HumanReadable())) + } else { + field.MaxLen = len(fmt.Sprintf("%v", a.FieldByName(f).Interface())) + } } field.Value = fmt.Sprintf("%v", a.FieldByName(f).Interface()) } else { @@ -983,25 +1012,24 @@ func displaySnapshotsFields(snaps []Snapshot, valsToDisplay []string) { 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 - for i := len(f.Value) + 1; i < f.MaxLen+1; i++ { - fmt.Printf(" ") + // Pretty print of sizes + if f.Name == "Used" || f.Name == "Referenced" || f.Name == "Available" { + var v datasize.ByteSize + err := v.UnmarshalText([]byte(f.Value)) + if err != nil { + return + } + fmt.Printf(" %s", v.HumanReadable()) + // Complete with spaces to the max length + for i := len(v.HumanReadable()) + 1; i < f.MaxLen+1; i++ { + fmt.Printf(" ") + } + } else { + fmt.Printf(" %s", f.Value) + // Complete with spaces to the max length + for i := len(f.Value) + 1; i < f.MaxLen+1; i++ { + fmt.Printf(" ") + } } fmt.Printf(" |") } @@ -2125,6 +2153,12 @@ func initJailSortStruct() JailSort { ConfigPathDec: func(j1, j2 *Jail) bool { return j1.ConfigPath > j2.ConfigPath }, + DatastoreInc: func(j1, j2 *Jail) bool { + return j1.Datastore < j2.Datastore + }, + DatastoreDec: func(j1, j2 *Jail) bool { + return j1.Datastore > j2.Datastore + }, InternalNameInc: func(j1, j2 *Jail) bool { return j1.InternalName < j2.InternalName }, @@ -2241,11 +2275,11 @@ func initSnapshotSortStruct() SnapshotSort { NameDec: func(s1, s2 *Snapshot) bool { return s1.Name > s2.Name }, - DsnameInc: func(s1, s2 *Snapshot) bool { - return s1.Dsname < s2.Dsname + DatastoreInc: func(s1, s2 *Snapshot) bool { + return s1.Datastore < s2.Datastore }, - DsnameDec: func(s1, s2 *Snapshot) bool { - return s1.Dsname > s2.Dsname + DatastoreDec: func(s1, s2 *Snapshot) bool { + return s1.Datastore > s2.Datastore }, JailnameInc: func(s1, s2 *Snapshot) bool { return s1.Jailname < s2.Jailname