package cmd import ( "encoding/json" "fmt" "io/ioutil" "os" "strconv" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" // TODO : Use log //log "github.com/sirupsen/logrus" ) const ( gVersion = "0.27c" ) var ( gJails []Jail gUseSudo bool gConfigFile string gDisplayColumns string gFilterJails string gSortFields string gNoLineSep bool gHostVersion float64 gTimeZone string gSnapshotName string gMigrateDestPool string rootCmd = &cobra.Command{ Use: "gocage", Short: "GoCage is a FreeBSD Jail management tool", Long: `GoCage is a jail management tool. It support VNET, host-only, NAT networks. Provides snapshots and cloning. It support iocage jails and can coexist with iocage.`, Run: func(cmd *cobra.Command, args []string) { fmt.Printf("GoCage v.%s on FreeBSD %.1f\n", gVersion, gHostVersion) fmt.Printf("Use -h flag to display help\n") }, } versionCmd = &cobra.Command{ Use: "version", Short: "Print the version number of GoCage", Long: `Let this show you how much fail I had to get this *cough* perfect`, Run: func(cmd *cobra.Command, args []string) { fmt.Printf("GoCage v.%s on FreeBSD %.1f\n", gVersion, gHostVersion) }, } listCmd = &cobra.Command{ Use: "list", Short: "Print jails", Long: `Display jails, their IP and OS. Jail list can be restricted by adding name on command line ex: gocage list srv-db srv-web`, Run: func(cmd *cobra.Command, args []string) { ListJails(args, true) }, } listPropsCmd = &cobra.Command{ Use: "properties", Short: "Print jails properties", Long: "Display jails properties. You can use properties to filter, get or set them.", Run: func(cmd *cobra.Command, args []string) { ListJailsProps(args) }, } stopCmd = &cobra.Command{ Use: "stop", Short: "stop jail", Long: "shutdown jail", Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) StopJail(args) }, } startCmd = &cobra.Command{ Use: "start", Short: "start jail", Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) StartJail(args) WriteConfigToDisk(false) }, } /* shellCmd = &cobra.Command { Use: "console", Short: "Execute shell on jail", Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) ShellJail(args) }, } */ setCmd = &cobra.Command{ Use: "set", Short: "Set a jail property", Long: `Set jail property value. Specify property=value, end command with jail name. Multiples properties can be specified, separated with space (Ex: gocage set allow_mlock=1 boot=1 myjail)`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) SetJailProperties(args) WriteConfigToDisk(true) }, } getCmd = &cobra.Command{ Use: "get", Short: "Get a jail property", Long: `Get jail property value. Specify property, end command with jail name. Multiples properties can be specified, separated with space (Ex: gocage get allow_mlock boot myjail) For all properties specify "all" (Ex: gocage get all myjail)`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) GetJailProperties(args) }, } snapshotCmd = &cobra.Command{ Use: "snapshot", Short: "snapshot jail", Long: "Commands to manage jail snapshots. If no arguments given, ", Run: func(cmd *cobra.Command, args []string) { }, } snapshotListCmd = &cobra.Command{ Use: "list", Short: "list snapshots", Long: `List snapshots of a jail by specifying its name. List all snapshots if no jail name specified. You can specify multiple jails.`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) ListJailsSnapshots(args) }, } snapshotCreateCmd = &cobra.Command{ Use: "create", Short: "create snapshots", Long: `Create snapshot of a jail by specifying snapshot name and jail name.`, // You can specify multiple jails.`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) CreateJailSnapshot(args) }, } snapshotRollbackCmd = &cobra.Command{ Use: "rollback", Short: "Rollback snapshots", Long: `Rollback jail to specifyed snapshot.`, // You can specify multiple jails.`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) RollbackJailSnapshot(args) }, } snapshotDeleteCmd = &cobra.Command{ Use: "destroy", Short: "destroy snapshots", Long: `Destroy snapshot of a jail by specifying snapshot name and jail name.`, // You can specify multiple jails.`, Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) DeleteJailSnapshot(args) }, } migrateCmd = &cobra.Command{ Use: "migrate", Short: "Migrate jail to another zpool", Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) MigrateJail(args) WriteConfigToDisk(false) }, } migrateCleanCmd = &cobra.Command{ Use: "clean", Short: "Clean previous aborted/in error jail migration", Run: func(cmd *cobra.Command, args []string) { // Load inventory ListJails(args, false) CleanMigrateMess(args) }, } ) // TODO : Init log level and log output func init() { cobra.OnInitialize(initConfig) // Global switches rootCmd.PersistentFlags().StringVarP(&gConfigFile, "config", "c", "/usr/local/etc/gocage.conf.yml", "GoCage configuration file") rootCmd.PersistentFlags().BoolVarP(&gUseSudo, "sudo", "u", false, "Use sudo to run commands") rootCmd.PersistentFlags().StringVarP(&gTimeZone, "timezone", "t", "", "Specify timezone. Will get from /var/db/zoneinfo if not set.") // Command dependant switches // These are persistent so we can reuse them in "gocage list snapshot myjail" command (TODO) listCmd.PersistentFlags().StringVarP(&gDisplayColumns, "outcol", "o", "JID,Name,Config.Release,Config.Ip4_addr,Running", "Show these columns in output") listCmd.PersistentFlags().BoolVarP(&gNoLineSep, "nolinesep", "l", false, "Do not display line separator between jails") listCmd.PersistentFlags().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.PersistentFlags().StringVarP(&gSortFields, "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.") // This is local flag : Only available to gocage snapshot create command snapshotCreateCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to create") snapshotCreateCmd.MarkFlagRequired("snapname") snapshotDeleteCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to destroy") snapshotDeleteCmd.MarkFlagRequired("snapname") snapshotRollbackCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to rollback to") snapshotRollbackCmd.MarkFlagRequired("snapname") migrateCmd.Flags().StringVarP(&gMigrateDestPool, "destpool", "d", "", "Name of zfs destination pool for jail") migrateCmd.MarkFlagRequired("destpool") // Now declare commands rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(listCmd) listCmd.AddCommand(listPropsCmd) rootCmd.AddCommand(stopCmd) rootCmd.AddCommand(startCmd) rootCmd.AddCommand(getCmd) rootCmd.AddCommand(setCmd) rootCmd.AddCommand(snapshotCmd) rootCmd.AddCommand(migrateCmd) snapshotCmd.AddCommand(snapshotListCmd) snapshotCmd.AddCommand(snapshotCreateCmd) snapshotCmd.AddCommand(snapshotDeleteCmd) snapshotCmd.AddCommand(snapshotRollbackCmd) migrateCmd.AddCommand(migrateCleanCmd) // Get FreeBSD version out, err := executeCommand("freebsd-version") if err != nil { fmt.Printf("Error running \"freebsd-version\": %s", err.Error()) os.Exit(1) } gHostVersion, _ = strconv.ParseFloat(strings.Split(out, "-")[0], 32) } func initConfig() { if gConfigFile == "" { fmt.Println("No config file!") os.Exit(1) } // fmt.Printf("We are in initConfig(), with config file %s\n", gConfigFile) viper.SetConfigFile(gConfigFile) if err := viper.ReadInConfig(); err != nil { fmt.Printf("ERROR reading config file %s : %s\n", gConfigFile, err.Error()) os.Exit(1) } // fmt.Println("Using config file:", viper.ConfigFileUsed()) // fmt.Printf("datastore in config : %s\n", viper.GetStringSlice("datastore")) // fmt.Printf("datastore.0 in config : %s\n", viper.GetStringSlice("datastore.0")) // Command line flags have priority on config file if rootCmd.Flags().Lookup("sudo") != nil && false == rootCmd.Flags().Lookup("sudo").Changed { gUseSudo = viper.GetBool("sudo") } if rootCmd.Flags().Lookup("timezone") != nil && false == rootCmd.Flags().Lookup("timezone").Changed { gTimeZone = viper.GetString("timezone") } // If neither on cmdline nor config file, get from /var/db/zoneinfo if len(gTimeZone) == 0 { tz, err := ioutil.ReadFile("/var/db/zoneinfo") if err != nil { fmt.Println("Error reading /var/db/zoneinfo: %s\n", err.Error()) os.Exit(1) } gTimeZone = strings.Trim(string(tz), "\n") } if listCmd.Flags().Lookup("outcol") != nil && false == listCmd.Flags().Lookup("outcol").Changed { gDisplayColumns = viper.GetString("outcol") } if listCmd.Flags().Lookup("nolinesep") != nil && false == listCmd.Flags().Lookup("nolinesep").Changed { gNoLineSep = viper.GetBool("nolinesep") } if listCmd.Flags().Lookup("filter") != nil && false == listCmd.Flags().Lookup("filter").Changed { gFilterJails = viper.GetString("filter") } if listCmd.Flags().Lookup("sort") != nil && false == listCmd.Flags().Lookup("sort").Changed { gSortFields = viper.GetString("sort") } if len(strings.Split(gSortFields, ",")) > 3 { fmt.Printf("More than 3 sort criteria is not supported!\n") os.Exit(1) } } /******************************************************************************** * Write jails config which been updated to disk. * If changeauto not set, values which are in "auto" mode on disk * won't be overwritten (p.ex defaultrouter wont be overwritten with current * default route, so if route change on jailhost this will reflect on jail next * start) *******************************************************************************/ func WriteConfigToDisk(changeauto bool) { for _, j := range gJails { if j.ConfigUpdated { // we will manipulate properties so get a copy jc := j.Config if changeauto == false { // Overwrite "auto" properties ondiskjc, err := getJailConfig(j.ConfigPath) if err != nil { panic(err) } // TODO : List all fields, then call getStructFieldValue to compare value with "auto" // If "auto" then keep it that way before writing ondiskjc to disk var properties []string properties = getStructFieldNames(ondiskjc, properties, "") for _, p := range properties { v, _, err := getStructFieldValue(ondiskjc, p) if err != nil { panic(err) } if v.String() == "auto" { err = setStructFieldValue(&jc, p, "auto") if err != nil { fmt.Printf("ERROR sanitizing config: %s\n", err.Error()) os.Exit(1) } } } } marshaled, err := json.MarshalIndent(jc, "", " ") if err != nil { fmt.Printf("ERROR marshaling config: %s\n", err.Error()) } //fmt.Printf(string(marshaled)) if os.WriteFile(j.ConfigPath, []byte(marshaled), 0644); err != nil { fmt.Printf("Error writing config file %s: %v\n", j.ConfigPath, err) os.Exit(1) } } } } func Execute() { if err := rootCmd.Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } }