82 Commits

Author SHA1 Message Date
yo
38266af66e Version 0.29f + WIP: gocage destroy 2022-09-25 13:45:40 +02:00
yo
fabe9cc330 Use hostuuid as jail Name 2022-09-25 13:45:12 +02:00
yo
0637686f91 Format 2022-09-25 12:01:16 +02:00
yo
99f7bed6f3 Add ability to delete multiple snapshots separated by comma 2022-07-14 14:00:41 +02:00
yo
9b86d786fe Fix setting Running to false 2022-07-14 12:32:14 +02:00
yo
efd28c03ad Reset Running and JID so we can call start() after stop() 2022-07-14 11:12:13 +02:00
yo
e1786a4d08 Fix poststart 2022-07-14 11:11:23 +02:00
yo
bf1037c1d3 Add gocage restart $jailname 2022-07-14 10:57:57 +02:00
yo
cbdcb039cb Version bump 2022-07-10 21:27:17 +02:00
yo
09ac7464f5 BUGFIX: Was adding 2 Datastore columns 2022-07-10 21:26:54 +02:00
yo
de1a92dec8 Remove Datastore from default columns (it will be added if jails on 2 DS) 2022-07-10 21:26:18 +02:00
yo
d16a2d6789 Execute "login -f root" when gocage console 2022-07-10 21:13:36 +02:00
yo
35b75ee7fb Version bump 2022-07-10 20:56:57 +02:00
yo
4d92e880c9 TODO comments 2022-07-10 20:56:49 +02:00
yo
52916f9ae9 TODO: Browse jailed ZFS dataset to migrate them 2022-07-10 20:56:17 +02:00
yo
31961b8e3a Start rtsold 2022-07-10 20:52:54 +02:00
yo
128e2aa0c4 Cosmetic 2022-07-10 20:48:40 +02:00
yo
0c7293ae66 Generate resolv.conf, copy localtime 2022-07-10 20:48:24 +02:00
yo
e1f6b4f6f9 Add default gateway, execute exec_start 2022-07-10 19:55:13 +02:00
yo
f29aeb2e23 Execute poststop 2022-07-10 19:54:23 +02:00
yo
1cb7c5fec7 Bugfixes, dont stop if fs already mounted in jail 2022-07-10 16:29:26 +02:00
yo
3dae685fc4 Check if jail is unique, else check if long name is used 2022-07-10 14:18:29 +02:00
yo
9b90f9c812 Use getJailFromArray so we can handle same name jails 2022-07-10 14:17:12 +02:00
yo
e13437b79e Fix version display 2022-07-10 14:16:39 +02:00
yo
484e05e8d1 Use getJailFromArray so we can handle same name jails + check pertinence before jailing non existing DS 2022-07-10 14:16:24 +02:00
yo
e7a6bdd376 Use getJailFromArray so we can handle same name jails 2022-07-10 14:15:26 +02:00
yo
170dce31b3 getJailFromArray now support long names 2022-07-10 14:15:02 +02:00
yo
1295fb86f6 Remove trailing \n 2022-07-10 14:14:32 +02:00
yo
d3410c281a use getJailFromArray so we can handle same name jails on different DS 2022-07-10 14:14:12 +02:00
yo
51dc7d1588 BUGFIX: path to root dataset was wrong 2022-07-04 20:47:19 +02:00
yo
92d8beb58f Delete dynamic ruleset, obtained from /var/run/jail.$InternalName.conf 2022-06-26 20:03:08 +02:00
yo
745811c39b Version bump 2022-06-26 20:02:29 +02:00
yo
276d01ed4c VNet configuration, jail and mount ZFS datasets into jail 2022-06-26 20:02:28 +02:00
yo
7266496cac Memo for network configuration 2022-06-26 20:02:28 +02:00
yo
5b0de24508 executeCommand supports quoted arguments, getValueFromRunningConfig 2022-06-26 20:02:28 +02:00
yo
2a8836721c GetInterfaces, GetBridgeMTU 2022-06-26 20:02:22 +02:00
yo
0f6b7b8b80 Hide ifconfig destroy output 2022-06-19 19:45:36 +02:00
yo
1462c383d3 Cosmetic rearrangement 2022-06-19 19:45:07 +02:00
yo
29ce0d9b58 Cosmetic rearrangement 2022-06-19 19:44:40 +02:00
yo
42e1085ad4 Fix disableRcKey bug when key do not exist 2022-06-19 19:14:58 +02:00
yo
0f4f76a9a2 fix Error unmounting display bug, unconditionally unmount /dev & /dev/fd, remove parameter file 2022-06-19 19:14:11 +02:00
yo
e87699e2dc executeScript function 2022-06-19 17:44:00 +02:00
yo
4d8bf6e0d5 executeScript function 2022-06-19 17:43:48 +02:00
yo
d9e1e20afc WIP on start: Build parameter file 2022-06-19 14:48:55 +02:00
yo
fcf7d68d06 Add FreeBSD version to JailHost struct 2022-06-19 13:55:41 +02:00
yo
f919ff2ec3 Initialise gJailHost 2022-06-19 13:55:07 +02:00
yo
1bc248fdcc Add hostname, hostid and version initialisation with NewJailHost() 2022-06-19 13:51:57 +02:00
yo
a7aaa11de6 Fix getJailFromArray: the returned reference was to a copy of the jail 2022-06-19 13:50:49 +02:00
yo
71f345dff4 temporary command to test code snippet 2022-06-18 20:09:32 +02:00
yo
e1410bf209 create dynamic devfs ruleset from configured or default 2022-06-18 20:08:12 +02:00
yo
c585678be9 create dynamic devfs ruleset from configured or default 2022-06-18 20:07:57 +02:00
yo
9218ffafe1 Add datastore to snapshots, force Datastore display when jail exist on multi datastores 2022-06-18 18:24:09 +02:00
yo
1c04f62ed8 Comments 2022-06-18 16:34:06 +02:00
yo
2151034a02 Fix migrate: use datastore.ZFSDataset for replication to dest 2022-06-18 16:24:53 +02:00
yo
e84c43c759 little progress on start/build devfs ruleset 2022-06-18 16:10:10 +02:00
yo
d4f6b9ddc7 Add datastore list, filter and sort 2022-06-18 16:09:38 +02:00
yo
86e08ec0f7 Add datastore list, filter and sort, add snapshot sorting 2022-06-18 16:09:22 +02:00
yo
b4fd7caca7 Update readme 2022-06-18 16:07:29 +02:00
yo
29e8736fbc WIP on gocage start : dynamic devfs rulesets 2022-06-18 11:10:06 +02:00
yo
203c4bff3b Cosmetic 2022-06-18 11:09:40 +02:00
yo
7356c0d3d0 gocage migrate now synchronize destination dataset after stoppoing jail 2022-06-18 11:09:06 +02:00
yo
a446a19a08 gocage console jailname now working 2022-06-18 11:08:03 +02:00
yo
0bf825ee5a WIP migration on running jail with minimized downtime 2022-06-05 18:43:02 +02:00
yo
542d2f96f6 zfsCopyIncremental to send/receive incremental snapshots 2022-06-05 18:42:20 +02:00
yo
6c6cb7edc8 Bugfix when "migrate clean" a jail which was already clean, code cleaning 2022-06-05 17:36:27 +02:00
yo
31fa6904db "gocage migrate jail -d destination_dataset" working for cold migrations 2022-06-05 14:10:07 +02:00
yo
fb3ee07585 "gocage migrate jail -d destination_dataset" working for cold migrations 2022-06-05 14:09:55 +02:00
yo
57c8bba09b Do not support 2 jails with same name, only keep the first seen 2022-06-05 14:09:04 +02:00
yo
249ab19173 getJailFromArray use name instead of internalName 2022-06-05 11:20:16 +02:00
yo
bc92f29900 executeCommand now returns stdout and stderr combined in output 2022-06-04 22:28:38 +02:00
yo
e4fc9c3a6c Some code reorg 2022-04-24 16:55:33 +02:00
yo
43f26d099f WIP on start, go fmt on * 2022-04-24 16:49:54 +02:00
yo
dbd9153513 Get default router and gateways, IPv4 & IPv6 2022-04-18 13:53:18 +02:00
yo
7b5ae7ce6e start : Check if nat doesnt conflict with running jails, get default router 2022-04-18 13:52:44 +02:00
yo
77a2e9dabf cleanAfterRun renamed to WriteConfigToDisk, dont overwrite "auto" values by default 2022-04-18 13:50:20 +02:00
yo
6821b14407 BUGFIX : getJailConfig returns 2 values 2022-04-18 13:37:40 +02:00
yo
f9ce3601df Convert setJailProperty to setStructFieldValue 2022-04-18 13:36:33 +02:00
yo
4f85f2e6ac WIP: checks before starting jail 2022-04-05 22:21:39 +02:00
yo
e0f371693a Raise error if /etc/hostid not readable 2022-04-05 22:21:04 +02:00
yo
0f97270a6a BUGFIX on hostid reading : remove trailing \n 2022-04-05 21:43:11 +02:00
yo
7dbbf8a757 started working on "gocage console" 2022-04-05 20:58:33 +02:00
yo
0053fd6c8b WIP on rollback, started "gocage console", default values for jail properties to stay compatible with iocage 2022-04-05 20:58:11 +02:00
16 changed files with 4403 additions and 1155 deletions

View File

@ -4,8 +4,9 @@ GoCage
Jail management tool for FreeBSD, written in Go. Jail management tool for FreeBSD, written in Go.
Support iocage jails, so they can coexist. Support iocage jails, so they can coexist.
Gocage is meant to be a complete jail management tool with network, snapshots, jail cloning support and a web interface. This is the hypothetic future. Gocage is meant to be a complete jail management tool with network, snapshots, jail cloning support and a web interface. This is the hypothetic future.
Gocage can handle multiple datastores, so you can have jails on HDD storage and jails on SSD storage.
At present time, it list and stops jails.
List jails List jails
---------- ----------
@ -118,3 +119,66 @@ Stop jails
---------- ----------
`gocage stop test` `gocage stop test`
Multi datastore
----------
A datastore is a ZFS dataset mounted. It should be declared in gocage.conf.yml, specifying its ZFS mountpoint :
<pre><code>
datastore:
- /iocage
- /fastiocage
</pre></code>
In gocage commands, datastore name is the mountpoint without its "/" prefix.
### List datastores
<pre><code>
gocage datastore list
+============+=============+============+===========+==========+============+
| Name | Mountpoint | ZFSDataset | Available | Used | Referenced |
+============+=============+============+===========+==========+============+
| iocage | /iocage | hdd/iocage | 1.6 TB | 414.9 GB | 27.5 KB |
+------------+-------------+------------+-----------+----------+------------+
| fastiocage | /fastiocage | ssd/iocage | 1.5 TB | 65.3 KB | 34.6 KB |
+------------+-------------+------------+-----------+----------+------------+
</pre></code>
#### Filter datastores
As with jails and snapshots, you can filter by name:
<pre><code>
gocage datastore list iocage
+============+=============+============+===========+==========+============+
| Name | Mountpoint | ZFSDataset | Available | Used | Referenced |
+============+=============+============+===========+==========+============+
| iocage | /iocage | hdd/iocage | 1.6 TB | 414.9 GB | 27.5 KB |
+------------+-------------+------------+-----------+----------+------------+
</pre></code>
#### Sort datastores
You can sort datastores:
<pre><code>
gocage datastore list -s -Available
+============+=============+============+===========+==========+============+
| Name | Mountpoint | ZFSDataset | Available | Used | Referenced |
+============+=============+============+===========+==========+============+
| iocage | /iocage | hdd/iocage | 1.6 TB | 415.0 GB | 27.5 KB |
+------------+-------------+------------+-----------+----------+------------+
| fastiocage | /fastiocage | ssd/iocage | 1.5 TB | 65.3 KB | 34.6 KB |
+------------+-------------+------------+-----------+----------+------------+
</pre></code>
See [cmd/struct.go](https://git.nosd.in/yo/gocage/src/branch/master/cmd/struct.go) for field names.
Migrating jails
----------
With multi datastore comes the need to migrate a jail between datastores.
Migration can be done with a minimal downtime, using zfs differential send/receive.
Source jail datasets are sent to the destination datastore, jail is stopped and a last differential sync is done before starting jail on new datastore.
<pre><code>
gocage migrate -d fastiocage srv-random
Snapshot data/iocage/jails/srv-random: Done
Snapshot data/iocage/jails/srv-random/root: Done
Migrate jail config dataset to fastdata/iocage/jails/srv-random: Done
Migrate jail filesystem dataset to fastdata/iocage/jails/srv-random/root: Done
</pre></code>

46
cmd/console.go Normal file
View File

@ -0,0 +1,46 @@
package cmd
import (
"os"
"fmt"
"log"
"errors"
"strconv"
"syscall"
)
func ShellJail(args []string) error {
// We cant shell more than one jail bc we replace gocage execution with jexec, so there wont be no return to gocage
if len(args) > 0 {
cj, err := getJailFromArray(args[0], gJails)
if err != nil {
fmt.Printf("Error getting jail %s: %v\n", args[0], err)
return err
}
if err := shellJail(cj); err != nil {
fmt.Printf("%v\n", err)
return err
}
}
return nil
}
func shellJail(jail *Jail) error {
if false == jail.Running {
return errors.New("Jail is not running")
}
jid := strconv.Itoa(jail.JID)
//err := syscall.Exec("/usr/sbin/jexec", []string{"jexec", jid, "/bin/csh"}, os.Environ())
err := syscall.Exec("/usr/sbin/jexec", []string{"jexec", jid, "login", "-f", "root"}, os.Environ())
// We should never get here, as syscall.Exec replace the gocage binary execution with jexec
// This means the moment syscall.Exec fires, gocage execution halt.
if err != nil {
log.Printf("Exec returned %v\n", err)
}
return nil
}

108
cmd/datastore.go Normal file
View File

@ -0,0 +1,108 @@
package cmd
import (
"fmt"
"errors"
"reflect"
"strconv"
"strings"
)
func ListDatastores(args []string, display bool) error {
/***************************************************************
/ Filter datastores by names given on command line
/**************************************************************/
for _, d := range args {
cmd := fmt.Sprintf("zfs list -p -H -o name,used,available,referenced %s", d)
out, err := executeCommand(cmd)
if err != nil {
if strings.HasSuffix(err.Error(), "No such file or directory") {
return errors.New(fmt.Sprintf("Datastore does not exist: %s", d))
} else {
return errors.New(fmt.Sprintf("Error executing command %s: %v; command returned: %s\n", cmd, err, out))
}
}
fields := strings.Fields(out)
if len(fields) < 4 {
return errors.New(fmt.Sprintf("Error parsing output of \"%s\": Not enough fields", cmd))
}
u, _ := strconv.ParseUint(fields[1], 10, 64)
a, _ := strconv.ParseUint(fields[2], 10, 64)
r, _ := strconv.ParseUint(fields[3], 10, 64)
ds := Datastore{Name: d[1:], Mountpoint: d, ZFSDataset: fields[0], Used: u, Referenced: r, Available: a}
err = loadDefaultsForDatastore(&ds)
if err != nil {
return err
}
gDatastores = append(gDatastores, ds)
}
fields := strings.Split(gDisplayDColumns, ",")
if true == display {
/***************************************************************
/ Sort datastores
/ We support 3 sort criteria max
/**************************************************************/
if len(gSortDSFields) > 0 && gSortDSFields != "none" {
ds := initDatastoreSortStruct()
// The way we manage criteria quantity is not very elegant...
var fct1, fct2, fct3 *reflect.Value
for i, c := range strings.Split(gSortDSFields, ",") {
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(ds, fctName)
if err != nil {
fieldName := strings.Replace(strings.Replace(c, "-", "", 1), "+", "", 1)
return errors.New(fmt.Sprintf("ERROR getting DatastoreSort struct field %s. Please check the field name: %s\n", fctName, fieldName))
}
switch i + 1 {
case 1:
fct1 = fct
case 2:
fct2 = fct
case 3:
fct3 = fct
}
}
switch len(strings.Split(gSortDSFields, ",")) {
case 1:
DatastoresOrderedBy(fct1.Interface().(datastoreLessFunc)).Sort(gDatastores)
case 2:
DatastoresOrderedBy(fct1.Interface().(datastoreLessFunc), fct2.Interface().(datastoreLessFunc)).Sort(gDatastores)
case 3:
DatastoresOrderedBy(fct1.Interface().(datastoreLessFunc), fct2.Interface().(datastoreLessFunc), fct3.Interface().(datastoreLessFunc)).Sort(gDatastores)
}
}
displayDatastoresFields(gDatastores, fields)
}
return nil
}
/********************************************************************************
* Load jails default config from $datastore/defaults.json
*******************************************************************************/
func loadDefaultsForDatastore(ds *Datastore) error {
jc, err := getJailConfig(fmt.Sprintf("%s/defaults.json", ds.Mountpoint))
if err != nil {
return err
}
ds.DefaultJailConfig = jc
//gDefaultConfig = append(gDefaultConfig, jc)
return nil
}

229
cmd/jailhost.go Normal file
View File

@ -0,0 +1,229 @@
package cmd
import (
"fmt"
"net"
"regexp"
"strconv"
"strings"
"io/ioutil"
"golang.org/x/net/route"
)
var defaultRoute4 = [4]byte{0, 0, 0, 0}
var defaultRoute6 = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
var local6 = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
func Inet4AddrToString(ip4 route.Inet4Addr) string {
return fmt.Sprintf("%v.%v.%v.%v", ip4.IP[0], ip4.IP[1], ip4.IP[2], ip4.IP[3])
}
func Inet6AddrToString(ip6 route.Inet6Addr) string {
return fmt.Sprintf("%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x",
ip6.IP[0], ip6.IP[1], ip6.IP[2], ip6.IP[3], ip6.IP[4], ip6.IP[5], ip6.IP[6], ip6.IP[7],
ip6.IP[8], ip6.IP[9], ip6.IP[10], ip6.IP[11], ip6.IP[12], ip6.IP[13], ip6.IP[14], ip6.IP[15])
}
/*****************************************************************************
* Initialize default_interface, default_gatway4, default_gateway6
*****************************************************************************/
func (jh *JailHost) InitNetworkProperties() {
rib, _ := route.FetchRIB(0, route.RIBTypeRoute, 0)
messages, err := route.ParseRIB(route.RIBTypeRoute, rib)
if err != nil {
panic(err)
}
for _, message := range messages {
route_message := message.(*route.RouteMessage)
addresses := route_message.Addrs
card_index := route_message.Index
if addresses[0].Family() == 2 {
var destination4, gateway4 *route.Inet4Addr
ok := false
if destination4, ok = addresses[0].(*route.Inet4Addr); !ok {
continue
}
if gateway4, ok = addresses[1].(*route.Inet4Addr); !ok {
continue
}
if destination4 == nil || gateway4 == nil {
continue
}
if destination4.IP == defaultRoute4 {
card, _ := net.InterfaceByIndex(card_index)
//fmt.Printf("Default IPv4 gateway is %v on card %s\n", Inet4AddrToString(*gateway4), card.Name)
jh.default_interface = card.Name
jh.default_gateway4 = Inet4AddrToString(*gateway4)
}
} else if addresses[0].Family() == 28 {
var destination6, gateway6 *route.Inet6Addr
ok := false
if destination6, ok = addresses[0].(*route.Inet6Addr); !ok {
continue
}
if gateway6, ok = addresses[1].(*route.Inet6Addr); !ok {
continue
}
if destination6 == nil || gateway6 == nil {
continue
}
if destination6.IP == defaultRoute6 && gateway6.IP != local6 {
card, _ := net.InterfaceByIndex(card_index)
//fmt.Printf("Default IPv6 gateway is %v on card %s\n", Inet6AddrToString(*gateway6), card.Name)
jh.default_interface = card.Name
jh.default_gateway6 = Inet6AddrToString(*gateway6)
}
}
}
}
func (jh *JailHost) GetDefaultInterface() string {
if len(jh.default_interface) == 0 {
jh.InitNetworkProperties()
}
return jh.default_interface
}
func (jh *JailHost) GetDefaultGateway4() string {
if len(jh.default_gateway4) == 0 {
jh.InitNetworkProperties()
}
return jh.default_gateway4
}
func (jh *JailHost) GetDefaultGateway6() string {
if len(jh.default_gateway6) == 0 {
jh.InitNetworkProperties()
}
return jh.default_gateway6
}
/*****************************************************************************
* Get all network interfaces
****************************************************************************/
func (jh *JailHost) GetInterfaces() ([]string, error) {
var names []string
interfaces, err := net.Interfaces()
if err != nil {
return names, fmt.Errorf("Error listing network interfaces: %v", err)
}
for _, n := range interfaces {
names = append(names, n.Name)
}
return names, nil
}
func (jh *JailHost) GetBridgeMTU(bridgeName string) (int, error) {
bridge, err := net.InterfaceByName(bridgeName)
if err != nil {
return 0, err
}
return bridge.MTU, nil
}
/*****************************************************************************
* Get all IPv4 currently in use on host
****************************************************************************/
func getHostInUseIPv4() ([]string, error) {
var ips []string
re := regexp.MustCompile(ifconfigipv4re)
out, err := executeCommand("/sbin/ifconfig")
if err != nil {
return ips, fmt.Errorf("Error executing \"/sbin/ifconfig\": %s", err)
}
for _, line := range strings.Split(out, "\n") {
if re.MatchString(line) {
ips = append(ips, re.FindStringSubmatch(line)[1])
}
}
return ips, nil
}
func getHostname() (string, error) {
out, err := executeCommand("/bin/hostname")
if err != nil {
return "", fmt.Errorf("Error executing \"/bin/hostname\": %v", err)
}
return strings.Split(out, "\n")[0], nil
}
func getHostId() (string, error) {
var content []byte
var err error
// return empty string if file does not exist
if content, err = ioutil.ReadFile("/etc/hostid"); err != nil {
if strings.HasSuffix(err.Error(), "no such file or directory") {
return "", nil
}
return "", err
}
return strings.Split(string(content), "\n")[0], nil
}
func getFreeBSDVersion() (FreeBSDVersion, error) {
var version FreeBSDVersion
regex := `([0-9]{1,2})(\.)?([0-9]{1,2})?\-([^\-]*)(\-)?(p[0-9]{1,2})?`
re := regexp.MustCompile(regex)
out, err := executeCommand("/bin/freebsd-version")
if err != nil {
return version, fmt.Errorf("Error executing \"/bin/freebsd-version\": %v", err)
}
if re.MatchString(out) {
version.major, err = strconv.Atoi(re.FindStringSubmatch(out)[1])
if err != nil {
return version, err
}
version.minor, err = strconv.Atoi(re.FindStringSubmatch(out)[3])
if err != nil {
return version, err
}
version.flavor = strings.Trim(re.FindStringSubmatch(out)[4], "\n")
// Skip the 'p' starting patch level
if len(re.FindStringSubmatch(out)[6]) > 0 {
version.patchLevel, err = strconv.Atoi(re.FindStringSubmatch(out)[6][1:])
if err != nil {
return version, err
}
}
}
return version, nil
}
func NewJailHost() (JailHost, error) {
var jh JailHost
var err error
if jh.hostname, err = getHostname(); err != nil {
return jh, err
}
if jh.hostid, err = getHostId(); err != nil {
return jh, err
}
if jh.version, err = getFreeBSDVersion(); err != nil {
return jh, err
}
return jh, nil
}

View File

@ -1,15 +1,14 @@
package cmd package cmd
import ( import (
"os" "encoding/json"
"fmt" "fmt"
"gocage/jail"
"io/ioutil"
"log" "log"
"os"
"reflect" "reflect"
"strings" "strings"
"io/ioutil"
"gocage/jail"
"encoding/json"
"github.com/spf13/viper"
) )
/******************************************************************************** /********************************************************************************
@ -18,8 +17,13 @@ import (
*******************************************************************************/ *******************************************************************************/
func ListJailsProps(args []string) { func ListJailsProps(args []string) {
var conf Jail var conf Jail
var jailconf JailConfig
var result []string var result []string
// Mandatory constructor to init default values
jailconf, err := NewJailConfig()
if err != nil {
fmt.Printf("Error allocating JailConfig: %s\n", err.Error())
return
}
conf.Config = jailconf conf.Config = jailconf
@ -35,11 +39,76 @@ func ListJailsProps(args []string) {
* into gJails global var * into gJails global var
*******************************************************************************/ *******************************************************************************/
func ListJails(args []string, display bool) { func ListJails(args []string, display bool) {
fields := strings.Split(gDisplayColumns, ",") type uniqueJailName struct {
jail string
for _, d := range viper.GetStringSlice("datastore") { unique bool
listJailsFromDatastore(d) uniqueName bool
} }
var nameChecked []*uniqueJailName
var curCheck *uniqueJailName
var found bool
var skip bool
for _, ds := range gDatastores {
listJailsFromDatastore(ds, args, display)
}
// Only when displaying jails, we accept to process multiple same name jails
if false == display {
for _, j := range gJails {
// If already checked and was using distinctive name, skip this occurence
for i, n := range nameChecked {
if strings.EqualFold(n.jail, j.Name) {
found = true
if false == n.unique && true == n.uniqueName {
skip = true
} else {
curCheck = nameChecked[i]
}
break
}
}
if true == skip {
continue
}
// Initialize if not found in nameChecked
if false == found {
curCheck = &uniqueJailName{jail: j.Name,
unique: true,
uniqueName: false}
} else {
found = false
}
if countOfJailsWithThisName(j.Name) > 1 {
//fmt.Printf("DEBUG: Jail %s exist multiple times, now checking if specified with full name\n", j.Name)
curCheck.unique = false
for _, a := range args {
//fmt.Printf("DEBUG: comparing %s/%s with %s\n", j.Datastore, j.Name, a)
if strings.EqualFold(a, fmt.Sprintf("%s/%s", j.Datastore, j.Name)) {
//fmt.Printf("DEBUG: Was specified with full name, GG!\n")
curCheck.uniqueName = true
}
}
}
nameChecked = append(nameChecked, curCheck)
}
// Now check
for _, a := range args {
for _, n := range nameChecked {
if strings.EqualFold(n.jail, a) && false == n.unique && false == n.uniqueName {
fmt.Printf("There is more than one jail named \"%s\", please use datastore/jail format\n", n.jail)
os.Exit(1)
}
}
}
}
fields := strings.Split(gDisplayJColumns, ",")
// This is the structure we will filter, then display // This is the structure we will filter, then display
var jails []Jail var jails []Jail
@ -77,7 +146,6 @@ func ListJails(args []string, display bool) {
jails = gJails jails = gJails
} }
/*************************************************************** /***************************************************************
/ Filter jails by names given on command line / Filter jails by names given on command line
/**************************************************************/ /**************************************************************/
@ -87,7 +155,6 @@ func ListJails(args []string, display bool) {
for _, j := range jails { for _, j := range jails {
if j.Name == a { if j.Name == a {
js = append(js, j) js = append(js, j)
break
} }
} }
} }
@ -98,12 +165,12 @@ func ListJails(args []string, display bool) {
/ Sort jails / Sort jails
/ We support 3 sort criteria max / We support 3 sort criteria max
/**************************************************************/ /**************************************************************/
if len(gSortFields) > 0 && gSortFields != "none" { if len(gSortJailFields) > 0 && gSortJailFields != "none" {
js := initJailSortStruct() js := initJailSortStruct()
// The way we manage criteria quantity is not very elegant... // The way we manage criteria quantity is not very elegant...
var fct1, fct2, fct3 *reflect.Value var fct1, fct2, fct3 *reflect.Value
for i, c := range strings.Split(gSortFields, ",") { for i, c := range strings.Split(gSortJailFields, ",") {
var fctName string var fctName string
if strings.HasPrefix(c, "-") { if strings.HasPrefix(c, "-") {
fctName = fmt.Sprintf("%sDec", strings.Replace(c, "-", "", 1)) fctName = fmt.Sprintf("%sDec", strings.Replace(c, "-", "", 1))
@ -119,13 +186,16 @@ func ListJails(args []string, display bool) {
return return
} }
switch i + 1 { switch i + 1 {
case 1: fct1 = fct case 1:
case 2: fct2 = fct fct1 = fct
case 3: fct3 = fct case 2:
fct2 = fct
case 3:
fct3 = fct
} }
} }
switch len(strings.Split(gSortFields, ",")) { switch len(strings.Split(gSortJailFields, ",")) {
case 1: case 1:
JailsOrderedBy(fct1.Interface().(jailLessFunc)).Sort(jails) JailsOrderedBy(fct1.Interface().(jailLessFunc)).Sort(jails)
case 2: case 2:
@ -143,25 +213,34 @@ func ListJails(args []string, display bool) {
} }
} }
func listJailsFromDatastore(ds Datastore, args []string, accept_multiple bool) {
func listJailsFromDatastore(datastore string) { fileInfo, err := os.Stat(ds.Mountpoint)
fileInfo, err := os.Stat(datastore) if err != nil {
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)) } }
if fileInfo.IsDir() == false {
// A datastore have to contain a "jails" directory log.Fatalln(fmt.Sprintf("%s is not a directory", ds.Mountpoint))
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)
} }
// A datastore have to contain a "jails" directory
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))
}
if fileInfo.IsDir() == false {
log.Fatalln(fmt.Sprintf("%s is not a directory", jailsDir))
}
func listJailsFromDirectory(dir string) ([]Jail) { listJailsFromDirectory(jailsDir, ds.Name)
}
func listJailsFromDirectory(dir string, dsname string) ([]Jail, error) {
files, err := ioutil.ReadDir(dir) files, err := ioutil.ReadDir(dir)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to browse %s, check path and/or rights", dir)) } if err != nil {
log.Fatalln(fmt.Sprintf("Unable to browse %s, check path and/or rights", dir))
}
for _, fi := range files { for _, fi := range files {
if fi.IsDir() == true { if fi.IsDir() == true {
@ -169,21 +248,26 @@ func listJailsFromDirectory(dir string) ([]Jail) {
// 1. Get conf from config.json // 1. Get conf from config.json
jailConfPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "config.json") jailConfPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "config.json")
jailConf, err := getJailConfig(jailConfPath) jailConf, err := getJailConfig(jailConfPath)
if err != nil { log.Println("ERROR reading jail config for %s", jailConfPath) } if err != nil {
log.Println("ERROR reading jail config for %s", jailConfPath)
}
// 2. Build jail object from config // 2. Build jail object from config
jailRootPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "root") jailRootPath := fmt.Sprintf("%s/%s/%s", dir, fi.Name(), "root")
j := Jail{ j := Jail{
Name: jailConf.Host_hostname, Name: jailConf.Host_hostuuid,
Config: jailConf, Config: jailConf,
ConfigPath: jailConfPath, ConfigPath: jailConfPath,
Datastore: dsname,
RootPath: jailRootPath, RootPath: jailRootPath,
Running: false, Running: false,
} }
// 3. Add current running informations // 3. Add current running informations
rjails, err := jail.GetJails() rjails, err := jail.GetJails()
if err != nil { log.Fatalln("Unable to list running jails") } if err != nil {
log.Fatalln("Unable to list running jails")
}
for _, rj := range rjails { for _, rj := range rjails {
if rj.Path == j.RootPath { if rj.Path == j.RootPath {
j.JID = rj.Jid j.JID = rj.Jid
@ -193,6 +277,7 @@ func listJailsFromDirectory(dir string) ([]Jail) {
} }
} }
// 4. Get Zpool
/* This op take some 600ms for ~40 jails :^( */ /* This op take some 600ms for ~40 jails :^( */
out, err := executeCommand(fmt.Sprintf("zfs list -H -o name %s", j.RootPath)) out, err := executeCommand(fmt.Sprintf("zfs list -H -o name %s", j.RootPath))
if err != nil { if err != nil {
@ -201,21 +286,40 @@ func listJailsFromDirectory(dir string) ([]Jail) {
j.Zpool = strings.Split(strings.TrimSuffix(out, "\n"), "/")[0] j.Zpool = strings.Split(strings.TrimSuffix(out, "\n"), "/")[0]
} }
// 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("Warning: A jail with name %s exist on datastore %s, use full name to interact!\n", j.Name, jj.Datastore)
fmt.Printf("(Ex: gocage start %s/%s)\n", jj.Datastore, j.Name)*/
// Add Datastore to avoid confusion
if false == strings.Contains(gDisplayJColumns, "Datastore") {
gDisplayJColumns += ",Datastore"
}
}
}
gJails = append(gJails, j) gJails = append(gJails, j)
} }
} }
return gJails return gJails, nil
} }
func getJailConfig(jailConfigPath string) (JailConfig, error) { func getJailConfig(jailConfigPath string) (JailConfig, error) {
content, err := ioutil.ReadFile(jailConfigPath) content, err := ioutil.ReadFile(jailConfigPath)
if err != nil { log.Fatalln(fmt.Sprintf("Unable to read %s, check path and/or rights", jailConfigPath)) } if err != nil {
log.Fatalln(fmt.Sprintf("Unable to read %s, check path and/or rights", jailConfigPath))
}
var jc JailConfig // Mandatory constructor to init default values
jc, err := NewJailConfig()
if err != nil {
return jc, err
}
err = json.Unmarshal([]byte(content), &jc) err = json.Unmarshal([]byte(content), &jc)
if err != nil { log.Fatalln(fmt.Sprintf("Error occured during unmarshaling %s: %s", jailConfigPath, err.Error())) } if err != nil {
log.Fatalln(fmt.Sprintf("Error occured during unmarshaling %s: %s", jailConfigPath, err.Error()))
}
return jc, err return jc, err
} }

224
cmd/migrate.go Normal file
View File

@ -0,0 +1,224 @@
package cmd
import (
"os"
"fmt"
"bufio"
"errors"
"strings"
)
const (
// 1MB
BUFFER_SIZE = 512
)
/********************************************************************************
* Migrate jail to another zfs pool
*******************************************************************************/
func MigrateJail(args []string) {
var jailNames []string
if len(args) > 0 {
for _, a := range args {
jailNames = append(jailNames, a)
}
}
for _, jn := range jailNames {
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")
fmt.Printf("Continue? (y/n) ")
scanr := bufio.NewScanner(os.Stdin)
scanr.Scan()
if false == strings.EqualFold(scanr.Text(), "y") {
fmt.Printf("Migration aborted\n")
return
}
}
/* TODO : Check dest pool (gMigrateDestDatastore) existence
zfs snapshot /iocage/jails/$jail@gocage_mig_first_snap
zfs snapshot /iocage/jails/$jail/root@gocage_mig_first_snap
zfs send jail@gocage_mig_first_snap | zfs receive destpool/jails/jail_name
zfs send jail/root@gocage_mig_first_snap | zfs receive destpool/jails/jail_name/root
shutdown jail if needed
if jail was shutdown
zfs snapshot /iocage/jails/$jail@gocage_mig_last_snap to get last data
zfs send -i jail@gocage_mig_first_snap jail/root@gocage_mig_last_snap | zfs receive -F destpool/jails/jail_name
start jail on new dest
zfs destroy destpool/jails/jail_name@gocage_mig_first_snap
zfs destroy destpool/jails/jail_name/root@gocage_mig_first_snap
*/
// Snapshot config
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)
return
}
fmt.Printf("Done\n")
// Snapshot filesystem
dsdata := strings.Join([]string{curDS.ZFSDataset, "jails", jn, "root"}, "/")
fmt.Printf("Snapshot %s: ", dsdata)
if err := zfsSnapshot(dsdata, "gocage_mig_init"); err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Done\n")
// TODO: Browse jailed ZFS dataset to migrate them (see poudriere for example)
// zfs send -R src/poudriere@init | zfs receive -Fu dest/poudriere
dsconfdest := strings.Join([]string{destDS.ZFSDataset, "jails", jn}, "/")
fmt.Printf("Migrate jail config dataset to %s: ", dsconfdest)
if err := zfsCopy(fmt.Sprintf("%s@gocage_mig_init", dsconf), dsconfdest); err != nil {
fmt.Printf("Error: %v\n", err)
}
fmt.Printf("Done\n")
dsdatadest := strings.Join([]string{destDS.ZFSDataset, "jails", jn, "root"}, "/")
fmt.Printf("Migrate jail filesystem dataset to %s: ", dsdatadest)
if err := zfsCopy(fmt.Sprintf("%s@gocage_mig_init", dsdata), dsdatadest); err != nil {
fmt.Printf("Error: %v\n", err)
}
fmt.Printf("Done\n")
// Running jail needs a last snapshot for an incremental send/recv after shutting down.
if cj.Running == true {
fmt.Printf("Shutdown jail %s for last data sync, this could take some time.\n", cj.Name)
if gYesToAll == false {
fmt.Printf("Continue? (y/n) ")
scanr := bufio.NewScanner(os.Stdin)
scanr.Scan()
if false == strings.EqualFold(scanr.Text(), "y") {
fmt.Printf("Migration aborted. Now cleaning destination pool.\n")
if err := CleanMigrateMess([]string{cj.Name}); err != nil {
fmt.Printf("Error: %v\n", err)
}
// TODO : Remove destination datasets, or handle the cas at the beginning of current function
// (when snapshot already exist on source and dest)
return
}
}
StopJail([]string{cj.Name})
fmt.Printf("Snapshot %s: ", dsconf)
if err = zfsSnapshot(dsconf, "gocage_mig_last_sync"); err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Done\n")
fmt.Printf("Snapshot %s: ", dsdata)
if err := zfsSnapshot(dsdata, "gocage_mig_last_sync"); err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Done\n")
fmt.Printf("Synchronize jail config to %s: ", dsconfdest)
if err := zfsCopyIncremental(fmt.Sprintf("%s@gocage_mig_init", dsconf),
fmt.Sprintf("%s@gocage_mig_last_sync", dsconf),
dsconfdest); err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Printf("Done\n")
fmt.Printf("Synchronize jail filesystem dataset to %s: ", dsdatadest)
if err := zfsCopyIncremental(fmt.Sprintf("%s@gocage_mig_init", dsdata),
fmt.Sprintf("%s@gocage_mig_last_sync", dsdata),
dsdatadest); err != nil {
return
fmt.Printf("Error: %v\n", err)
}
fmt.Printf("Done\n")
// TODO : Start jail on new datastore (! Currently ListJails won't support 2 jails with same name !)
// TODO : zfs destroy destpool/jails/jail_name@gocage_mig_first_snap
// TODO : zfs destroy destpool/jails/jail_name/root@gocage_mig_first_snap
// TODO : zfs destroy -r srcpool/iocage/jails/$jail
}
}
}
// Clean snapshots from an aborted migration
func CleanMigrateMess(args []string) error {
var jailNames []string
if len(args) > 0 {
for _, a := range args {
jailNames = append(jailNames, a)
}
}
for _, jn := range jailNames {
cj, err := getJailFromArray(jn, gJails)
if cj == nil {
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{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") {
fmt.Printf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)
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{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") {
fmt.Printf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)
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{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") {
fmt.Printf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)
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{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") {
fmt.Printf("Error executing command %s: %v; command returned: %s\n", cmd, err, out)
return errors.New(fmt.Sprintf("Error executing command %s: %v; command returned: %s\n", cmd, err, out))
}
}
}
return nil
}

View File

@ -1,22 +1,27 @@
package cmd package cmd
import ( import (
"fmt"
"errors" "errors"
"fmt"
"reflect" "reflect"
"strings"
"strconv" "strconv"
"strings"
) )
func GetJailProperties(args []string) { func GetJailProperties(args []string) {
var props []string var props []string
var jail Jail var jail *Jail
var err error
if len(args) > 0 { if len(args) > 0 {
for i, a := range args { for i, a := range args {
// Last arg is the jail name // Last arg is the jail name
if i == len(args)-1 { if i == len(args)-1 {
jail.Name = a jail, err = getJailFromArray(a, gJails)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
return
}
} else { } else {
props = append(props, a) props = append(props, a)
} }
@ -31,12 +36,12 @@ func GetJailProperties(args []string) {
if isStringInArray(props, "all") { if isStringInArray(props, "all") {
var result []string var result []string
result = getStructFieldNames(jail, result, "") result = getStructFieldNames(*jail, result, "")
props = result props = result
} }
for _, p := range props { for _, p := range props {
v, err := getJailProperty(&jail, p) v, err := getJailProperty(jail, p)
if err != nil { if err != nil {
fmt.Printf("Error: %s\n", err.Error()) fmt.Printf("Error: %s\n", err.Error())
return return
@ -47,9 +52,7 @@ func GetJailProperties(args []string) {
} }
func getJailProperty(jail *Jail, propName string) (string, error) { func getJailProperty(jail *Jail, propName string) (string, error) {
for i, j := range gJails { val, _, err := getStructFieldValue(jail, propName)
if j.Name == jail.Name {
val, _, err := getStructFieldValue(&gJails[i], propName)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -64,10 +67,8 @@ func getJailProperty(jail *Jail, propName string) (string, error) {
default: default:
return "", errors.New(fmt.Sprintf("Error: Unknown type for property %s: %s\n", propName, val.Kind())) return "", errors.New(fmt.Sprintf("Error: Unknown type for property %s: %s\n", propName, val.Kind()))
} }
}
}
return "", errors.New(fmt.Sprintf("Jail not found: %s", jail.Name)) return "", errors.New("Unknown error in getJailProperty")
} }
func SetJailProperties(args []string) { func SetJailProperties(args []string) {
@ -104,53 +105,19 @@ func SetJailProperties(args []string) {
return return
} }
// Get jail by index to modify it
for i, _ := range gJails {
if gJails[i].Name == jail.Name {
for _, p := range props { for _, p := range props {
err := setJailProperty(&jail, p.name, p.value) err := setStructFieldValue(&gJails[i], p.name, p.value)
if err != nil { if err != nil {
fmt.Printf("Error: %s\n", err.Error()) fmt.Printf("Error: %s\n", err.Error())
return return
}
}
}
// setJailProperty takes a string as propValue, whatever the real property type is.
// It will be converted.
func setJailProperty(jail *Jail, propName string, propValue string) error {
for i, j := range gJails {
if j.Name == jail.Name {
val, _, err := getStructFieldValue(&gJails[i], propName)
//val, _, err := getStructFieldValue(&gJails[i].Config, strings.Split(propName, ".")[1])
if err != nil {
return err
}
if val.CanSet() {
switch val.Kind() {
case reflect.String:
val.SetString(propValue)
case reflect.Int:
ival, err := strconv.ParseInt(propValue, 10, 64)
if err != nil {
return err
}
val.SetInt(ival)
case reflect.Bool:
bval, err := strconv.ParseBool(propValue)
if err != nil {
return err
}
val.SetBool(bval)
default:
return errors.New(fmt.Sprintf("Field is an unkown type: %s: %s", propName, val.Kind().String()))
}
fmt.Printf("%s: %s set to %s\n", jail.Name, propName, propValue)
gJails[i].ConfigUpdated = true
} else { } else {
return errors.New(fmt.Sprintf("Field is not writable : %s", propName)) fmt.Printf("%s: %s set to %s\n", gJails[i].Name, p.name, p.value)
gJails[i].ConfigUpdated = true
}
} }
} }
} }
return nil
} }

View File

@ -1,45 +1,64 @@
package cmd package cmd
import ( import (
"os"
"fmt"
"strconv"
"strings"
"io/ioutil"
"encoding/json" "encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
// TODO : Use log
//log "github.com/sirupsen/logrus"
) )
const ( const (
gVersion = "0.25" gVersion = "0.29g"
// TODO : Get from $jail_zpool/defaults.json
MIN_DYN_DEVFS_RULESET = 1000
) )
var ( var (
gJailHost JailHost
gJails []Jail gJails []Jail
gDatastores []Datastore
gUseSudo bool gUseSudo bool
gForce bool
gConfigFile string gConfigFile string
gDisplayColumns string gDisplayJColumns string
gDisplaySColumns string
gDisplayDColumns string
gFilterJails string gFilterJails string
gSortFields string gFilterSnaps string
gNoLineSep bool gFilterDS string
gSortJailFields string
gSortSnapFields string
gSortDSFields string
gNoJailLineSep bool
gNoSnapLineSep bool
gNoDSLineSep bool
gHostVersion float64 gHostVersion float64
gTimeZone string gTimeZone string
gSnapshotName string gSnapshotName string
gMigrateDestDatastore string
gYesToAll bool
rootCmd = &cobra.Command{ rootCmd = &cobra.Command{
Use: "gocage", Use: "gocage",
Short: "GoCage is a FreeBSD Jail management tool", 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. 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.`, It support iocage jails and can coexist with iocage.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Here we are in the Run") fv, _ := getFreeBSDVersion()
cleanAfterRun() fmt.Printf("GoCage v.%s on FreeBSD %d.%d-%s\n", gVersion, fv.major, fv.minor, fv.flavor)
fmt.Printf("Use -h flag to display help\n")
}, },
} }
@ -48,8 +67,8 @@ It support iocage jails and can coexist with iocage.`,
Short: "Print the version number of GoCage", Short: "Print the version number of GoCage",
Long: `Let this show you how much fail I had to get this *cough* perfect`, Long: `Let this show you how much fail I had to get this *cough* perfect`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("GoCage v.%s on FreeBSD %.1f\n", gVersion, gHostVersion) fv, _ := getFreeBSDVersion()
cleanAfterRun() fmt.Printf("GoCage v.%s on FreeBSD %d.%d-%s\n", gVersion, fv.major, fv.minor, fv.flavor)
}, },
} }
@ -61,7 +80,6 @@ Jail list can be restricted by adding name on command line
ex: gocage list srv-db srv-web`, ex: gocage list srv-db srv-web`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
ListJails(args, true) ListJails(args, true)
cleanAfterRun()
}, },
} }
@ -71,10 +89,19 @@ ex: gocage list srv-db srv-web`,
Long: "Display jails properties. You can use properties to filter, get or set them.", Long: "Display jails properties. You can use properties to filter, get or set them.",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
ListJailsProps(args) ListJailsProps(args)
cleanAfterRun()
}, },
} }
/* destroyCmd = &cobra.Command{
Use: "destroy",
Short: "destroy jails",
Long: `Destroy jail filesystem, snapshots and configuration file.`,
Run: func(cmd *cobra.Command, args []string) {
ListJails(args, false)
DestroyJails(args)
},
}
*/
stopCmd = &cobra.Command{ stopCmd = &cobra.Command{
Use: "stop", Use: "stop",
Short: "stop jail", Short: "stop jail",
@ -83,7 +110,6 @@ ex: gocage list srv-db srv-web`,
// Load inventory // Load inventory
ListJails(args, false) ListJails(args, false)
StopJail(args) StopJail(args)
cleanAfterRun()
}, },
} }
@ -94,7 +120,29 @@ ex: gocage list srv-db srv-web`,
// Load inventory // Load inventory
ListJails(args, false) ListJails(args, false)
StartJail(args) StartJail(args)
cleanAfterRun() WriteConfigToDisk(false)
},
}
restartCmd = &cobra.Command{
Use: "restart",
Short: "restart jail",
Run: func(cmd *cobra.Command, args []string) {
// Load inventory
ListJails(args, false)
StopJail(args)
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)
}, },
} }
@ -107,7 +155,7 @@ Multiples properties can be specified, separated with space (Ex: gocage set allo
// Load inventory // Load inventory
ListJails(args, false) ListJails(args, false)
SetJailProperties(args) SetJailProperties(args)
cleanAfterRun() WriteConfigToDisk(true)
}, },
} }
@ -121,7 +169,6 @@ For all properties specify "all" (Ex: gocage get all myjail)`,
// Load inventory // Load inventory
ListJails(args, false) ListJails(args, false)
GetJailProperties(args) GetJailProperties(args)
cleanAfterRun()
}, },
} }
@ -159,22 +206,87 @@ You can specify multiple jails.`,
}, },
} }
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{ snapshotDeleteCmd = &cobra.Command{
Use: "destroy", Use: "destroy",
Short: "destroy snapshots", Short: "destroy snapshots",
Long: `Destroy snapshot of a jail by specifying snapshot name and jail name.`, Long: `Destroy snapshot of a jail by specifying snapshot name and jail name.
// You can specify multiple jails.`, You can specify multiple snapshots separated by comma.`,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
// Load inventory // Load inventory
ListJails(args, false) ListJails(args, false)
DeleteJailSnapshot(args) DeleteJailSnapshot(args)
}, },
} }
migrateCmd = &cobra.Command{
Use: "migrate",
Short: "Migrate jail to another datastore",
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)
err := CleanMigrateMess(args)
if err != nil {
fmt.Printf("%v", err)
}
},
}
datastoreCmd = &cobra.Command{
Use: "datastore",
Short: "list datastores",
Long: "Commands to manage datastores. If no arguments given, list them.",
Run: func(cmd *cobra.Command, args []string) {
ListDatastores(args, true)
},
}
datastoreListCmd = &cobra.Command{
Use: "list",
Short: "list datastores",
Long: `List datastore by specifying its name.
List all datastores if no name specified.
You can specify multiple datastores.`,
Run: func(cmd *cobra.Command, args []string) {
ListDatastores(args, true)
},
}
testCmd = &cobra.Command{
Use: "test",
Short: "temporary command to test some code snippet",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf("Nope\n")
},
}
) )
// TODO : Init log level and log output
func init() { func init() {
var err error
cobra.OnInitialize(initConfig) cobra.OnInitialize(initConfig)
// Global switches // Global switches
@ -184,17 +296,35 @@ func init() {
// Command dependant switches // Command dependant switches
// These are persistent so we can reuse them in "gocage list snapshot myjail" command (TODO) // We reuse these flags in "gocage snapshot list myjail" and 'gocage datastore list" commands
listCmd.PersistentFlags().StringVarP(&gDisplayColumns, "outcol", "o", "JID,Name,Config.Release,Config.Ip4_addr,Running", "Show these columns in output") listCmd.Flags().StringVarP(&gDisplayJColumns, "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.Flags().BoolVarP(&gNoJailLineSep, "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.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.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.") 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.")
// destroyCmd.Flags().BoolVarP(&gForce, "force", "f", false, "Force stop jail if running")
snapshotListCmd.Flags().StringVarP(&gDisplaySColumns, "outcol", "o", "Jailname,Name,Creation,Referenced,Used", "Show these columns in output")
snapshotListCmd.Flags().BoolVarP(&gNoSnapLineSep, "nolinesep", "l", false, "Do not display line separator between snapshots")
snapshotListCmd.Flags().StringVarP(&gFilterSnaps, "filter", "f", "none", "Only display snapshots with these values. Ex: \"gocage snapshot list -f Config.Boot=1\" will only list started on boot jails")
snapshotListCmd.Flags().StringVarP(&gSortSnapFields, "sort", "s", "none", "Display snapshots sorted by field values. Ex: \"gocage snapshot list -s +Jailname,-Used\" will sort snapshots by jail decreasing name, then increasing used space. 3 critera max supported.")
datastoreListCmd.Flags().StringVarP(&gDisplayDColumns, "outcol", "o", "Name,Mountpoint,ZFSDataset,Available,Used,Referenced", "Show these columns in output")
datastoreListCmd.Flags().BoolVarP(&gNoDSLineSep, "nolinesep", "l", false, "Do not display line separator between datastores")
datastoreListCmd.Flags().StringVarP(&gFilterDS, "filter", "f", "none", "Only display datastores with these values. Ex: \"gocage datastore list -f Config.Boot=1\" will only list started on boot jails")
datastoreListCmd.Flags().StringVarP(&gSortDSFields, "sort", "s", "none", "Display datastores sorted by field values. Ex: \"gocage datastore list -s +Jailname,-Used\" will sort snapshots by jail decreasing name, then increasing used space. 3 critera max supported.")
// This is local flag : Only available to gocage snapshot create command // This is local flag : Only available to gocage snapshot create command
snapshotCreateCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to create") snapshotCreateCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to create")
snapshotCreateCmd.MarkFlagRequired("snapname") snapshotCreateCmd.MarkFlagRequired("snapname")
snapshotDeleteCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to destroy") snapshotDeleteCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to destroy")
snapshotDeleteCmd.MarkFlagRequired("snapname") snapshotDeleteCmd.MarkFlagRequired("snapname")
snapshotRollbackCmd.Flags().StringVarP(&gSnapshotName, "snapname", "n", "", "Name of the snapshot to rollback to")
snapshotRollbackCmd.MarkFlagRequired("snapname")
migrateCmd.Flags().StringVarP(&gMigrateDestDatastore, "datastore", "d", "", "Path of destination datastore for jail (Ex: \"/iocage\")")
migrateCmd.Flags().BoolVarP(&gYesToAll, "yes", "y", false, "Answer yes to all questions")
migrateCmd.MarkFlagRequired("datastore")
// Now declare commands // Now declare commands
rootCmd.AddCommand(versionCmd) rootCmd.AddCommand(versionCmd)
@ -202,21 +332,30 @@ func init() {
listCmd.AddCommand(listPropsCmd) listCmd.AddCommand(listPropsCmd)
rootCmd.AddCommand(stopCmd) rootCmd.AddCommand(stopCmd)
rootCmd.AddCommand(startCmd) rootCmd.AddCommand(startCmd)
rootCmd.AddCommand(restartCmd)
// rootCmd.AddCommand(destroyCmd)
rootCmd.AddCommand(shellCmd)
rootCmd.AddCommand(getCmd) rootCmd.AddCommand(getCmd)
rootCmd.AddCommand(setCmd) rootCmd.AddCommand(setCmd)
rootCmd.AddCommand(snapshotCmd) rootCmd.AddCommand(snapshotCmd)
rootCmd.AddCommand(migrateCmd)
rootCmd.AddCommand(datastoreCmd)
rootCmd.AddCommand(testCmd)
snapshotCmd.AddCommand(snapshotListCmd) snapshotCmd.AddCommand(snapshotListCmd)
snapshotCmd.AddCommand(snapshotCreateCmd) snapshotCmd.AddCommand(snapshotCreateCmd)
snapshotCmd.AddCommand(snapshotDeleteCmd) snapshotCmd.AddCommand(snapshotDeleteCmd)
snapshotCmd.AddCommand(snapshotRollbackCmd)
migrateCmd.AddCommand(migrateCleanCmd)
datastoreCmd.AddCommand(datastoreListCmd)
// Get FreeBSD version // Get FreeBSD version, hostname, hostid
out, err := executeCommand("freebsd-version") gJailHost, err = NewJailHost()
if err != nil { if err != nil {
fmt.Printf("Error running \"freebsd-version\": %s", err.Error()) fmt.Printf("Error initializing JailHost properties: %v\n", err)
os.Exit(1) os.Exit(1)
} }
gHostVersion, _ = strconv.ParseFloat(strings.Split(out, "-")[0], 32)
} }
func initConfig() { func initConfig() {
@ -225,8 +364,6 @@ func initConfig() {
os.Exit(1) os.Exit(1)
} }
// fmt.Printf("We are in initConfig(), with config file %s\n", gConfigFile)
viper.SetConfigFile(gConfigFile) viper.SetConfigFile(gConfigFile)
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
@ -234,6 +371,13 @@ func initConfig() {
os.Exit(1) os.Exit(1)
} }
// Load default configs from datastores
err := ListDatastores(viper.GetStringSlice("datastore"), false)
if err != nil {
fmt.Printf("ERROR: error checking datastores: %v\n", err)
os.Exit(1)
}
// fmt.Println("Using config file:", viper.ConfigFileUsed()) // fmt.Println("Using config file:", viper.ConfigFileUsed())
// fmt.Printf("datastore in config : %s\n", viper.GetStringSlice("datastore")) // fmt.Printf("datastore in config : %s\n", viper.GetStringSlice("datastore"))
// fmt.Printf("datastore.0 in config : %s\n", viper.GetStringSlice("datastore.0")) // fmt.Printf("datastore.0 in config : %s\n", viper.GetStringSlice("datastore.0"))
@ -255,28 +399,65 @@ func initConfig() {
gTimeZone = strings.Trim(string(tz), "\n") gTimeZone = strings.Trim(string(tz), "\n")
} }
if listCmd.Flags().Lookup("outcol") != nil && false == listCmd.Flags().Lookup("outcol").Changed { if listCmd.Flags().Lookup("outcol") != nil && false == listCmd.Flags().Lookup("outcol").Changed {
gDisplayColumns = viper.GetString("outcol") gDisplayJColumns = viper.GetString("outcol")
} }
if listCmd.Flags().Lookup("nolinesep") != nil && false == listCmd.Flags().Lookup("nolinesep").Changed { if listCmd.Flags().Lookup("nolinesep") != nil && false == listCmd.Flags().Lookup("nolinesep").Changed {
gNoLineSep = viper.GetBool("nolinesep") gNoJailLineSep = viper.GetBool("nolinesep")
} }
if listCmd.Flags().Lookup("filter") != nil && false == listCmd.Flags().Lookup("filter").Changed { if listCmd.Flags().Lookup("filter") != nil && false == listCmd.Flags().Lookup("filter").Changed {
gFilterJails = viper.GetString("filter") gFilterJails = viper.GetString("filter")
} }
if listCmd.Flags().Lookup("sort") != nil && false == listCmd.Flags().Lookup("sort").Changed { if listCmd.Flags().Lookup("sort") != nil && false == listCmd.Flags().Lookup("sort").Changed {
gSortFields = viper.GetString("sort") gSortJailFields = viper.GetString("sort")
} }
if len(strings.Split(gSortFields, ",")) > 3 { if len(strings.Split(gSortJailFields, ",")) > 3 {
fmt.Printf("More than 3 sort criteria, this is not supported!\n") fmt.Printf("More than 3 sort criteria is not supported!\n")
os.Exit(1) os.Exit(1)
} }
} }
// Called after execution /********************************************************************************
func cleanAfterRun() { * 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 { for _, j := range gJails {
if j.ConfigUpdated { if j.ConfigUpdated {
marshaled, err := json.MarshalIndent(j.Config, "", " ") //log.Debug("%s config has changed, write changes to disk\n", j.Name)
// 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 { if err != nil {
fmt.Printf("ERROR marshaling config: %s\n", err.Error()) fmt.Printf("ERROR marshaling config: %s\n", err.Error())
} }
@ -295,5 +476,3 @@ func Execute() {
os.Exit(1) os.Exit(1)
} }
} }

View File

@ -1,10 +1,15 @@
package cmd package cmd
import ( import (
"bufio"
"errors"
"fmt" "fmt"
"time" "os"
"regexp" "regexp"
"strconv"
"strings" "strings"
"reflect"
"time"
) )
/******************************************************************************** /********************************************************************************
@ -14,8 +19,15 @@ func ListJailsSnapshots(args []string) {
var jailNames []string var jailNames []string
var snapshots []Snapshot var snapshots []Snapshot
/***************************************************************
/ Filter snapshots by jailname
/**************************************************************/
if len(args) > 0 { if len(args) > 0 {
for _, a := range args { for _, a := range args {
/*if countOfJailsWithThisName(a) > 1 {
fmt.Printf("Nope")
return
}*/
jailNames = append(jailNames, a) jailNames = append(jailNames, a)
} }
} }
@ -33,9 +45,56 @@ func ListJailsSnapshots(args []string) {
} }
} }
} }
displaySnapshotsFields(snapshots, []string{"Jailname","Name","Creation","Referenced","Used"})
fields := strings.Split(gDisplaySColumns, ",")
/***************************************************************
/ Sort snapshots
/ We support 3 sort criteria max
/**************************************************************/
if len(gSortSnapFields) > 0 && gSortSnapFields != "none" {
ss := initSnapshotSortStruct()
// The way we manage criteria quantity is not very elegant...
var fct1, fct2, fct3 *reflect.Value
for i, c := range strings.Split(gSortSnapFields, ",") {
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(ss, fctName)
if err != nil {
fieldName := strings.Replace(strings.Replace(c, "-", "", 1), "+", "", 1)
fmt.Printf("ERROR getting SnapshotSort 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(gSortSnapFields, ",")) {
case 1:
SnapshotsOrderedBy(fct1.Interface().(snapshotLessFunc)).Sort(snapshots)
case 2:
SnapshotsOrderedBy(fct1.Interface().(snapshotLessFunc), fct2.Interface().(snapshotLessFunc)).Sort(snapshots)
case 3:
SnapshotsOrderedBy(fct1.Interface().(snapshotLessFunc), fct2.Interface().(snapshotLessFunc), fct3.Interface().(snapshotLessFunc)).Sort(snapshots)
}
}
displaySnapshotsFields(snapshots, fields)
}
/******************************************************************************** /********************************************************************************
* List all snapshots a jail have * List all snapshots a jail have
@ -45,23 +104,27 @@ func listJailSnapshots(jail Jail) []Snapshot {
// 1. List all datasets // 1. List all datasets
// TODO : Include mounted filesystems? // TODO : Include mounted filesystems?
rs := strings.Split(jail.RootPath, "/")
rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) curDS, err := getDatastoreFromArray(jail.Datastore, gDatastores)
cmd := fmt.Sprintf("zfs list -r -H -o name,mountpoint,used,referenced,creation -t snapshot %s", rootDataset) 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) out, err := executeCommand(cmd)
if err != nil { if err != nil {
fmt.Printf("Error: %s\n", err.Error()) fmt.Printf("Error: %s\n", err.Error())
return snapshots return snapshots
} }
dateLayout := "Mon Jan _2 15:04 2006"
loc, _ := time.LoadLocation(gTimeZone)
for _, line := range strings.Split(out, "\n") { for _, line := range strings.Split(out, "\n") {
if len(line) > 0 { if len(line) > 0 {
ls := strings.Split(line, "\t") ls := strings.Split(line, "\t")
// Parse creation date so we can use it to sort snapshots // 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 { if err != nil {
fmt.Println("Error while parsing date %s:", ls[4], err) fmt.Println("Error while parsing date %s:", ls[4], err)
return snapshots return snapshots
@ -69,13 +132,15 @@ func listJailSnapshots(jail Jail) []Snapshot {
// Get subdir to append to snapshot name // Get subdir to append to snapshot name
subdir := strings.Replace(strings.Split(ls[0], "@")[0], rootDataset, "", 1) 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), Name: fmt.Sprintf("%s%s", strings.Split(ls[0], "@")[1], subdir),
Jailname: jail.Name, Jailname: jail.Name,
Mountpoint: ls[1], Mountpoint: ls[1],
Used: ls[2], Used: u,
Referenced: ls[3], Referenced: r,
Creation: creationts}) Creation: time.Unix(creationts, 0)})
} }
} }
@ -86,7 +151,6 @@ func listJailSnapshots(jail Jail) []Snapshot {
return snapshots return snapshots
} }
/******************************************************************************** /********************************************************************************
* Create snapshot for jail(s) * Create snapshot for jail(s)
*******************************************************************************/ *******************************************************************************/
@ -112,8 +176,8 @@ func CreateJailSnapshot(args []string) {
* Create snapshot for a jail * Create snapshot for a jail
*******************************************************************************/ *******************************************************************************/
func createJailSnapshot(jail Jail) error { func createJailSnapshot(jail Jail) error {
rs := strings.Split(jail.RootPath, "/") curDS, _ := getDatastoreFromArray(jail.Datastore, gDatastores)
rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name)
cmd := fmt.Sprintf("zfs snapshot -r %s@%s", rootDataset, gSnapshotName) cmd := fmt.Sprintf("zfs snapshot -r %s@%s", rootDataset, gSnapshotName)
_, err := executeCommand(cmd) _, err := executeCommand(cmd)
@ -154,8 +218,8 @@ func deleteJailSnapshot(jail Jail) error {
var snaptodel []string var snaptodel []string
// Get all recursive snapshots // Get all recursive snapshots
rs := strings.Split(jail.RootPath, "/") curDS, _ := getDatastoreFromArray(jail.Datastore, gDatastores)
rootDataset := fmt.Sprintf("%s%s", jail.Zpool, strings.Join(rs[:len(rs)-1], "/")) rootDataset := fmt.Sprintf("%s/%s/%s", curDS.ZFSDataset, "jails", jail.Name)
cmd := fmt.Sprintf("zfs list -r -H -o name -t snapshot %s", rootDataset) cmd := fmt.Sprintf("zfs list -r -H -o name -t snapshot %s", rootDataset)
out, err := executeCommand(cmd) out, err := executeCommand(cmd)
if err != nil { if err != nil {
@ -166,13 +230,14 @@ func deleteJailSnapshot(jail Jail) error {
for _, line := range strings.Split(out, "\n") { for _, line := range strings.Split(out, "\n") {
if len(line) > 0 { if len(line) > 0 {
ls := strings.Split(line, "@") ls := strings.Split(line, "@")
for _, sn := range strings.Split(gSnapshotName, ",") {
matched, _ := regexp.Match(fmt.Sprintf("^%s(\\/.*)?$", gSnapshotName), []byte(ls[1])) matched, _ := regexp.Match(fmt.Sprintf("^%s(\\/.*)?$", sn), []byte(ls[1]))
if matched { if matched {
snaptodel = append(snaptodel, strings.Join(ls, "@")) snaptodel = append(snaptodel, strings.Join(ls, "@"))
} }
} }
} }
}
for _, s := range snaptodel { for _, s := range snaptodel {
cmd := fmt.Sprintf("zfs destroy %s", s) cmd := fmt.Sprintf("zfs destroy %s", s)
@ -186,3 +251,80 @@ func deleteJailSnapshot(jail Jail) error {
return nil return nil
} }
func RollbackJailSnapshot(args []string) error {
var jailNames []string
if len(args) > 0 {
for _, a := range args {
jailNames = append(jailNames, a)
}
}
for _, cj := range gJails {
for _, jn := range jailNames {
if strings.EqualFold(cj.Name, jn) {
rollbackJailSnapshot(cj)
}
}
}
return nil
}
/********************************************************************************
* rollback jail to snapshot gSnapshotName, destroy this snapshots and more
* recents snapshots and bookmarks
*******************************************************************************/
func rollbackJailSnapshot(jail Jail) error {
var snaptorb []string
if jail.Running {
fmt.Printf("Jail should be stoped to rollback, should we stop and rollback? (y/n)\n")
scanr := bufio.NewScanner(os.Stdin)
if scanr.Scan() {
if !strings.EqualFold(scanr.Text(), "y") {
return errors.New("Jail is running")
} else {
err := stopJail(&jail)
if err != nil {
return err
}
}
}
}
// We need to rollback parent and childs
// Get all recursive snapshots
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 {
fmt.Printf("Error: listing snapshots: %s\n", err.Error())
return err
}
for _, line := range strings.Split(out, "\n") {
if len(line) > 0 {
ls := strings.Split(line, "@")
matched, _ := regexp.Match(fmt.Sprintf("^%s(\\/.*)?$", gSnapshotName), []byte(ls[1]))
if matched {
snaptorb = append(snaptorb, strings.Join(ls, "@"))
}
}
}
for _, s := range snaptorb {
cmd := fmt.Sprintf("zfs rollback -r %s", s)
_, err := executeCommand(cmd)
if err != nil {
fmt.Printf("Error rolling back snapshot %s: %s\n", s, err.Error())
return err
}
}
fmt.Printf("Jail is back to %s\n", gSnapshotName)
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@ import (
"regexp" "regexp"
"os/exec" "os/exec"
//"reflect" //"reflect"
"strconv"
"strings" "strings"
) )
@ -38,7 +39,6 @@ func removeRctlRules(jail string, rules []string) error {
return nil return nil
} }
// TODO: Validate with >1 dataset // TODO: Validate with >1 dataset
func umountAndUnjailZFS(jail *Jail) error { func umountAndUnjailZFS(jail *Jail) error {
var ds []string var ds []string
@ -79,12 +79,12 @@ func umountAndUnjailZFS(jail *Jail) error {
return nil return nil
} }
func destroyVNetInterfaces(jail *Jail) error { func destroyVNetInterfaces(jail *Jail) error {
for _, i := range strings.Split(jail.Config.Ip4_addr, ",") { for _, i := range strings.Split(jail.Config.Ip4_addr, ",") {
iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID) iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID)
fmt.Printf("%s: ", iname) fmt.Printf("%s: ", iname)
_, err := executeCommand(fmt.Sprintf("ifconfig %s destroy", iname)) _, err := executeCommand(fmt.Sprintf("ifconfig %s destroy", iname))
//_, err := executeScript(fmt.Sprintf("ifconfig %s destroy >/dev/null 2>&1", iname))
if err != nil { if err != nil {
return err return err
} else { } else {
@ -101,16 +101,17 @@ func destroyVNetInterfaces(jail *Jail) error {
// or else it will require a restart of "devfs" service. // or else it will require a restart of "devfs" service.
// But, stoppign the jail already removes this >1000 ID. // But, stoppign the jail already removes this >1000 ID.
// So no need to call this function. // So no need to call this function.
func deleteDevfsRuleset(jail *Jail) error { func deleteDevfsRuleset(ruleset int) error {
cmd := "devfs rule showsets" cmd := "devfs rule showsets"
out, err := executeCommand(cmd) out, err := executeCommand(cmd)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("ERROR listing rulesets: %s", err.Error())) return errors.New(fmt.Sprintf("ERROR listing rulesets: %s", err.Error()))
} }
rs := strconv.Itoa(ruleset)
for _, r := range strings.Split(out, "\n") { for _, r := range strings.Split(out, "\n") {
if r == jail.Config.Devfs_ruleset { if r == rs {
cmd := fmt.Sprintf("devfs rule -s %s delset", jail.Config.Devfs_ruleset) cmd := fmt.Sprintf("devfs rule -s %d delset", ruleset)
_, err := executeCommand(cmd) _, err := executeCommand(cmd)
return err return err
} }
@ -119,7 +120,6 @@ func deleteDevfsRuleset(jail *Jail) error {
return nil return nil
} }
func umountJailFsFromHost(jail *Jail, mountpoint string) error { func umountJailFsFromHost(jail *Jail, mountpoint string) error {
cmd := "mount -p" cmd := "mount -p"
out, err := executeCommand(cmd) out, err := executeCommand(cmd)
@ -135,7 +135,7 @@ func umountJailFsFromHost(jail *Jail, mountpoint string) error {
cmd = fmt.Sprintf("umount %s%s", jail.RootPath, mountpoint) cmd = fmt.Sprintf("umount %s%s", jail.RootPath, mountpoint)
_, err := executeCommand(cmd) _, err := executeCommand(cmd)
if err != nil { if err != nil {
return errors.New(fmt.Sprintf("Error umounting %s/%s: %s", jail.RootPath, mountpoint, err.Error())) return errors.New(fmt.Sprintf("Error umounting %s%s: %s", jail.RootPath, mountpoint, err.Error()))
} }
return nil return nil
} }
@ -145,7 +145,6 @@ func umountJailFsFromHost(jail *Jail, mountpoint string) error {
return nil return nil
} }
// Internal usage only // Internal usage only
func stopJail(jail *Jail) error { func stopJail(jail *Jail) error {
cmd := "jail -q" cmd := "jail -q"
@ -191,25 +190,23 @@ func stopJail(jail *Jail) error {
func StopJail(args []string) { func StopJail(args []string) {
// Current jail were stopping // Current jail were stopping
var cj *Jail var cj *Jail
var err error
for _, j := range args { for _, a := range args {
fmt.Printf("> Stopping jail %s\n", j) // Check if jail exist and is distinctly named
cj, err = getJailFromArray(a, gJails)
for _, rj := range gJails { if err != nil {
if rj.Name == j { fmt.Printf("Error getting jail: %s\n", err)
cj = &rj
break
}
}
if cj == nil {
fmt.Printf("Jail not found: %s\n", j)
continue continue
} }
if cj.Running == false { if cj.Running == false {
fmt.Printf("Jail %s is not running!\n", cj.Name) fmt.Printf("Jail %s is not running!\n", cj.Name)
continue continue
} }
fmt.Printf("> Stopping jail %s\n", a)
out, err := executeCommand(fmt.Sprintf("rctl jail:%s", cj.InternalName)) out, err := executeCommand(fmt.Sprintf("rctl jail:%s", cj.InternalName))
if err == nil && len(out) > 0 { if err == nil && len(out) > 0 {
fmt.Printf(" > Remove RCTL rules:\n") fmt.Printf(" > Remove RCTL rules:\n")
@ -222,12 +219,12 @@ func StopJail(args []string) {
} }
if len(cj.Config.Exec_prestop) > 0 { if len(cj.Config.Exec_prestop) > 0 {
fmt.Printf(" > Execute prestop:\n") fmt.Printf(" > Execute pre-stop:\n")
_, err := executeCommand(cj.Config.Exec_prestop) _, err := executeCommand(cj.Config.Exec_prestop)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) fmt.Printf("ERROR: %s\n", err.Error())
} else { } else {
fmt.Printf(" > Execute prestop: OK\n") fmt.Printf(" > Execute pre-stop: OK\n")
} }
} }
@ -261,13 +258,20 @@ func StopJail(args []string) {
} }
} }
/*fmt.Printf(" > Remove devfsruleset %s:\n", cj.Config.Devfs_ruleset) // Get currently used ruleset from /var/run/jail.$internal_name.conf
err = deleteDevfsRuleset(cj) ruleset, err := getValueFromRunningConfig(cj.InternalName, "devfs_ruleset")
if err != nil {
fmt.Printf("ERROR getting current devfs ruleset: %s\n", err.Error())
return
}
rsi, _ := strconv.Atoi(ruleset)
fmt.Printf(" > Remove devfs ruleset %d: \n", rsi)
err = deleteDevfsRuleset(rsi)
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) fmt.Printf("ERROR: %s\n", err.Error())
} else { } else {
fmt.Printf(" > Remove devfsruleset %s: OK\n", cj.Config.Devfs_ruleset) fmt.Printf(" > Remove devfsruleset %d: OK\n", rsi)
}*/ }
fmt.Printf(" > Stop jail %s:\n", cj.Name) fmt.Printf(" > Stop jail %s:\n", cj.Name)
err = stopJail(cj) err = stopJail(cj)
@ -297,15 +301,16 @@ func StopJail(args []string) {
} }
} }
if cj.Config.Mount_fdescfs > 0 { // FIXME: /dev/fd is mounted even with Mount_fdescfs = 0 ?!
//if cj.Config.Mount_fdescfs > 0 {
fmt.Printf(" > Umount fdescfs:\n") fmt.Printf(" > Umount fdescfs:\n")
err := umountJailFsFromHost(cj, "/dev/fd") err = umountJailFsFromHost(cj, "/dev/fd")
if err != nil { if err != nil {
fmt.Printf("ERROR: %s\n", err.Error()) fmt.Printf("ERROR: %s\n", err.Error())
} else { } else {
fmt.Printf(" > Umount fdescfs: OK\n") fmt.Printf(" > Umount fdescfs: OK\n")
} }
} //}
if cj.Config.Mount_devfs > 0 { if cj.Config.Mount_devfs > 0 {
fmt.Printf(" > Umount devfs:\n") fmt.Printf(" > Umount devfs:\n")
@ -318,7 +323,6 @@ func StopJail(args []string) {
} }
// Remove local mounts from $JAIL/fstab // Remove local mounts from $JAIL/fstab
// The way we get fstab is presumptuous
fstab := strings.Replace(cj.ConfigPath, "config.json", "fstab", 1) fstab := strings.Replace(cj.ConfigPath, "config.json", "fstab", 1)
mounts, err := getFstab(fstab) mounts, err := getFstab(fstab)
if err != nil { if err != nil {
@ -338,6 +342,37 @@ func StopJail(args []string) {
fmt.Printf(" > Umount mountpoints from %s: OK\n", fstab) fmt.Printf(" > Umount mountpoints from %s: OK\n", fstab)
} }
} }
// TODO: Execute poststop
if len(cj.Config.Exec_poststop) > 0 {
fmt.Printf(" > Execute post-stop:\n")
_, err := executeCommand(cj.Config.Exec_poststop)
if err != nil {
fmt.Printf("ERROR: %s\n", err.Error())
} else {
fmt.Printf(" > Execute post-stop: OK\n")
} }
} }
// Remove parameter file
pfile := fmt.Sprintf("/var/run/jail.%s.conf", cj.InternalName)
if err = os.Remove(pfile); err != nil {
fmt.Printf("Error deleting parameter file %s\n", pfile)
}
// We need this to get a reference to cj.Running (bc cj.Running is just a copy of value in the scope of StopJail())
for i, j := range gJails {
if strings.EqualFold(j.Name, cj.Name) && strings.EqualFold(j.Datastore, cj.Datastore) {
if err = setStructFieldValue(&gJails[i], "Running", "false"); err != nil {
fmt.Printf("ERROR: setting Running property to false: %s\n", err.Error())
}
if err = setStructFieldValue(&gJails[i], "JID", "0"); err != nil {
fmt.Printf("ERROR: setting JID property to 0: %s\n", err.Error())
}
if err = setStructFieldValue(&gJails[i], "InternalName", ""); err != nil {
fmt.Printf("ERROR: clearing InternalName property: %s\n", err.Error())
}
}
}
}
}

View File

@ -4,6 +4,17 @@ import (
"time" "time"
) )
const (
IPv4 = 0
IPv6 = 1
)
type NatDesc struct {
Proto string
JailPort string
HostPort string
}
// To allow sorting, just duplicate fields in JailSort below // To allow sorting, just duplicate fields in JailSort below
type Jail struct { type Jail struct {
Name string Name string
@ -17,6 +28,7 @@ type Jail struct {
// No need, Config.Release always represent what is running (plus it know release for non-running jails) // No need, Config.Release always represent what is running (plus it know release for non-running jails)
//Release string //Release string
Zpool string Zpool string
Datastore string
} }
// iocage struct as stored in config.json // iocage struct as stored in config.json
@ -86,8 +98,10 @@ type JailConfig struct {
Host_time int `json:"host_time"` Host_time int `json:"host_time"`
Hostid string `json:"hostid"` Hostid string `json:"hostid"`
Hostid_strict_check int `json:"hostid_strict_check"` Hostid_strict_check int `json:"hostid_strict_check"`
// Specify multiple net cards with "vnet0:bridge0,vnet1:bridge1"
Interfaces string `json:"interfaces"` Interfaces string `json:"interfaces"`
Ip4 string `json:"ip4"` Ip4 string `json:"ip4"`
// Specify multiples IP with "vnet0|192.168.1.42,vnet1|10.0.0.3"
Ip4_addr string `json:"ip4_addr"` Ip4_addr string `json:"ip4_addr"`
Ip4_saddrsel string `json:"ip4_saddrsel"` Ip4_saddrsel string `json:"ip4_saddrsel"`
Ip6 string `json:"ip6"` Ip6 string `json:"ip6"`
@ -180,17 +194,42 @@ type Mount struct {
type Snapshot struct { type Snapshot struct {
// snapshot name is stored after '@' in dataset name // snapshot name is stored after '@' in dataset name
Name string Name string
Dsname string Datastore string
Jailname string Jailname string
Mountpoint string Mountpoint string
Used string Used uint64
Referenced string Referenced uint64
Creation time.Time Creation time.Time
} }
type FreeBSDVersion struct {
major int
minor int
flavor string
patchLevel int
}
type JailHost struct {
hostname string
hostid string
default_gateway4 string
default_gateway6 string
default_interface string
version FreeBSDVersion
}
type Datastore struct {
Name string
Mountpoint string
ZFSDataset string
DefaultJailConfig JailConfig
Used uint64
Referenced uint64
Available uint64
}
// Fields in this struct are acquired by their name using reflection // Fields in this struct are acquired by their name using reflection
// So these char are forbidden for field name: -+. // So these char are forbidden for field name: -+.
//
type JailSort struct { type JailSort struct {
NameInc jailLessFunc NameInc jailLessFunc
NameDec jailLessFunc NameDec jailLessFunc
@ -204,6 +243,8 @@ type JailSort struct {
ConfigPathDec jailLessFunc ConfigPathDec jailLessFunc
RunningInc jailLessFunc RunningInc jailLessFunc
RunningDec jailLessFunc RunningDec jailLessFunc
DatastoreInc jailLessFunc
DatastoreDec jailLessFunc
ZpoolInc jailLessFunc ZpoolInc jailLessFunc
ZpoolDec jailLessFunc ZpoolDec jailLessFunc
Config JailConfigSort Config JailConfigSort
@ -487,8 +528,8 @@ type JailConfigSort struct {
type SnapshotSort struct { type SnapshotSort struct {
NameInc snapshotLessFunc NameInc snapshotLessFunc
NameDec snapshotLessFunc NameDec snapshotLessFunc
DsnameInc snapshotLessFunc DatastoreInc snapshotLessFunc
DsnameDec snapshotLessFunc DatastoreDec snapshotLessFunc
JailnameInc snapshotLessFunc JailnameInc snapshotLessFunc
JailnameDec snapshotLessFunc JailnameDec snapshotLessFunc
MountpointInc snapshotLessFunc MountpointInc snapshotLessFunc
@ -500,3 +541,19 @@ type SnapshotSort struct {
CreationInc snapshotLessFunc CreationInc snapshotLessFunc
CreationDec snapshotLessFunc CreationDec snapshotLessFunc
} }
type DatastoreSort struct {
NameInc datastoreLessFunc
NameDec datastoreLessFunc
MountpointInc datastoreLessFunc
MountpointDec datastoreLessFunc
ZFSDatasetInc datastoreLessFunc
ZFSDatasetDec datastoreLessFunc
UsedInc datastoreLessFunc
UsedDec datastoreLessFunc
ReferencedInc datastoreLessFunc
ReferencedDec datastoreLessFunc
AvailableInc datastoreLessFunc
AvailableDec datastoreLessFunc
}

File diff suppressed because it is too large Load Diff

5
go.mod
View File

@ -3,8 +3,13 @@ module gocage
go 1.17 go 1.17
require ( require (
github.com/c-robinson/iplib v1.0.3
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1 github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.9.0 github.com/spf13/viper v1.9.0
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420
) )
require ( require (

View File

@ -1,11 +1,12 @@
# List datastore root directory (the one containing jails/ and defaults.json) # List datastore(s) root directory (the one containing jails/ and defaults.json)
datastore: datastore:
- /iocage - /iocage
# Prefix all commands with sudo
sudo: false sudo: false
# Columns to display when "gocage list". Column names are struct fields, see cmd/struct.go # Columns to display when "gocage list". Column names are struct fields, see cmd/struct.go
outcol: 'JID,Name,Config.Release,Config.Ip4_addr,Running' outcol: 'JID,Name,Config.Release,Config.Ip4_addr,Running'
# Do not add line separator between jails # Do not add line separator between jails
nolinesep: false nolinesep: false

View File

@ -7,4 +7,3 @@ import (
func main() { func main() {
cmd.Execute() cmd.Execute()
} }