7 Commits

10 changed files with 76 additions and 291 deletions

View File

@ -18,8 +18,7 @@ Create jails
------------ ------------
For now, we can't pass config at creation time. We have to define config after creation: For now, we can't pass config at creation time. We have to define config after creation:
<pre><code> <pre><code>
gocage create jail1 -r 13.2-RELEASE gocage create jail1 -r 13.2-RELEASE -p "Config.Ip4_addr='vnet0|192.168.1.91/24',Config.Ip6=none,Config.Boot=1"
gocage set Config.Ip4_addr="vnet0|192.168.1.91/24" Config.Vnet=1 jail1
</code></pre> </code></pre>
Create basejail (jail based on a release, system will be nullfs read-only mounted from the release directory): Create basejail (jail based on a release, system will be nullfs read-only mounted from the release directory):
@ -30,7 +29,6 @@ gocage create -b -r 14.0-RELEASE basejail1
List jails List jails
---------- ----------
Nothing fancy, just use
`gocage list` `gocage list`
### Specify fields to display ### Specify fields to display
@ -233,7 +231,7 @@ Fetch
Files can be fetched from custom repository, or from local directory with "from" option. Files can be fetched from custom repository, or from local directory with "from" option.
For example if you destroyed releases/12.3-RELEASE and still have the downloaded files in /iocage/download/12.3-RELEASE: For example if you destroyed releases/12.3-RELEASE and still have the downloaded files in /iocage/download/12.3-RELEASE:
<pre><code> <pre><code>
gocage fetch -r 12.3 -o iocage --from file:/iocage/download gocage fetch -r 12.3 -d iocage -f file:/iocage/download
</pre></code> </pre></code>

View File

@ -1,96 +0,0 @@
package cmd
import (
//"io"
"fmt"
"strings"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
)
func printJails(jails []string, fields []string) ([]byte, error) {
var res []byte
res = append(res, []byte("[")...)
// Build a filtered jail list
var filteredJails []Jail
if (len(jails) >= 1 && len(jails[0]) > 0) {
for _, j := range gJails {
if isStringInArray(jails, j.Name) {
filteredJails = append(filteredJails, j)
}
}
} else {
filteredJails = gJails
}
for i, j := range filteredJails {
out, err := j.PrintJSON(fields)
if err != nil {
return res, err
}
res = append(res, out...)
if i < len(filteredJails) - 1 {
res = append(res, []byte(",")...)
}
}
res = append(res, []byte("]")...)
return res, nil
}
func startApi(address string, port int) {
// Initialize gJails.
// FIXME: How to update this at running time without doubling the same jails?
// core will do this for us in add and delete functions
ListJails([]string{""}, false)
r := gin.Default()
// Cross Origin Request Support
config := cors.DefaultConfig()
// DANGER !
config.AllowOrigins = []string{"*"}
r.Use(cors.New(config))
r.GET("/jail/list", func(c *gin.Context) {
jailstr := c.Query("jail")
fields := c.Query("fields")
if len(fields) == 0 {
// TODO : Put this default value in (user?) configuration
fields = "Name,Running,Config.Release,Config.Ip4_addr"
}
jsout, err := printJails(strings.Split(jailstr, ","), strings.Split(fields, ","))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "error": err.Error()})
} else {
c.Data(http.StatusOK, "application/json", jsout)
}
})
r.POST("/jail/start/:jail", func(c *gin.Context) {
jailstr := c.Params.ByName("jail")
// TODO : Capture or redirect output so we can know if jail was started or not
StartJail(strings.Split(jailstr, ","))
c.JSON(http.StatusOK, gin.H{"status": 0, "error": ""})
})
r.POST("/jail/stop/:jail", func(c *gin.Context) {
jailstr := c.Params.ByName("jail")
// TODO : Capture or redirect output so we can know if jail was started or not
StopJail(strings.Split(jailstr, ","))
c.JSON(http.StatusOK, gin.H{"status": 0, "error": ""})
})
r.GET("/jail/properties", func(c *gin.Context) {
props, err := ListJailsProps("json")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": 1, "error": err.Error()})
} else {
c.Data(http.StatusOK, "application/json", []byte(props))
}
})
r.Run(fmt.Sprintf("%s:%d", address, port))
}

View File

@ -44,7 +44,7 @@ func CreateJail(args []string) {
var ds *Datastore var ds *Datastore
if len(gCreateArgs.Datastore) > 0 { if len(gCreateArgs.Datastore) > 0 {
fmt.Printf("DEBUG: Use %s datastore\n", gCreateArgs.Datastore) log.Debugf("Use %s datastore\n", gCreateArgs.Datastore)
ds, err = getDatastoreFromArray(gCreateArgs.Datastore, gDatastores) ds, err = getDatastoreFromArray(gCreateArgs.Datastore, gDatastores)
if err != nil { if err != nil {
fmt.Printf("ERROR Getting datastore: %s\n", gCreateArgs.Datastore, err.Error()) fmt.Printf("ERROR Getting datastore: %s\n", gCreateArgs.Datastore, err.Error())
@ -272,7 +272,6 @@ func CreateJail(args []string) {
j.Config.Host_hostname = jname j.Config.Host_hostname = jname
j.Config.Host_hostuuid = jname j.Config.Host_hostuuid = jname
j.Config.Jailtype = "jail" j.Config.Jailtype = "jail"
j.WriteConfigToDisk(false) j.WriteConfigToDisk(false)
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
@ -285,5 +284,17 @@ func CreateJail(args []string) {
defer fstabHandle.Close() defer fstabHandle.Close()
fmt.Printf(" > Jail created!\n") fmt.Printf(" > Jail created!\n")
} }
var cmdline []string
for _, props := range strings.Split(gCreateArgs.Properties, ",") {
cmdline = append(cmdline, props)
}
// Reload jail list so SetJailProperties will see it
ListJails(nil, false)
cmdline = append(cmdline, jname)
log.Debugf("cmdline: \"%v\"", cmdline)
SetJailProperties(cmdline)
} }
} }

View File

@ -15,37 +15,23 @@ import (
* List all properties a jail have, with their internal name * List all properties a jail have, with their internal name
* Only print properties name. To get name & values, use GetJailProperties() * Only print properties name. To get name & values, use GetJailProperties()
*******************************************************************************/ *******************************************************************************/
func ListJailsProps(format string) (string, error) { func ListJailsProps(args []string) {
var conf Jail var conf Jail
var props []string var result []string
var out string
// Mandatory constructor to init default values // Mandatory constructor to init default values
jailconf, err := NewJailConfig() jailconf, err := NewJailConfig()
if err != nil { if err != nil {
fmt.Printf("Error allocating JailConfig: %s\n", err.Error()) fmt.Printf("Error allocating JailConfig: %s\n", err.Error())
return out, err return
} }
conf.Config = jailconf conf.Config = jailconf
props = getStructFieldNames(conf, props, "") result = getStructFieldNames(conf, result, "")
if format == "json" { for _, f := range result {
out = "{\"properties\":[" fmt.Printf("%s\n", f)
for i, p := range props {
out += fmt.Sprintf("\"%s\"", p)
if i < len(props) - 1 {
out += ","
}
}
out += "]}"
} else {
for _, p := range props {
out += fmt.Sprintf("%s\n", p)
}
} }
return out, nil
} }
/******************************************************************************** /********************************************************************************

View File

@ -25,12 +25,10 @@ type createArgs struct {
BaseJail bool BaseJail bool
Datastore string Datastore string
JailType string JailType string
Properties string
} }
var ( var (
gApiAddress string
gApiPort int
gJailHost JailHost gJailHost JailHost
gJails []Jail gJails []Jail
gDatastores []Datastore gDatastores []Datastore
@ -127,12 +125,7 @@ ex: gocage list srv-db srv-web`,
Short: "Print jails properties", Short: "Print jails properties",
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) {
out, err := ListJailsProps("text") ListJailsProps(args)
if err != nil {
fmt.Printf("Error listing properties : %v\n", err)
} else {
fmt.Printf("%s\n", out)
}
}, },
} }
@ -362,14 +355,6 @@ You can specify multiple datastores.`,
}, },
} }
apiCmd = &cobra.Command{
Use: "startapi",
Short: "Run docage as a daemon, exposing API",
Run: func(cmd *cobra.Command, args []string) {
startApi(gApiAddress, gApiPort)
},
}
testCmd = &cobra.Command{ testCmd = &cobra.Command{
Use: "test", Use: "test",
Short: "temporary command to test some code snippet", Short: "temporary command to test some code snippet",
@ -395,6 +380,7 @@ func init() {
initCmd.Flags().StringVarP(&gZPool, "pool", "p", "", "ZFS pool to create datastore on") initCmd.Flags().StringVarP(&gZPool, "pool", "p", "", "ZFS pool to create datastore on")
initCmd.Flags().StringVarP(&gBridge, "bridge", "b", "", "bridge to create for jails networking") initCmd.Flags().StringVarP(&gBridge, "bridge", "b", "", "bridge to create for jails networking")
initCmd.Flags().StringVarP(&gInterface, "interface", "i", "", "interface to add as bridge member. This should be your main interface") initCmd.Flags().StringVarP(&gInterface, "interface", "i", "", "interface to add as bridge member. This should be your main interface")
initCmd.MarkFlagRequired("bridge")
initCmd.MarkFlagsRequiredTogether("bridge", "interface") initCmd.MarkFlagsRequiredTogether("bridge", "interface")
// We reuse these flags in "gocage snapshot list myjail" and 'gocage datastore list" commands // We reuse these flags in "gocage snapshot list myjail" and 'gocage datastore list" commands
@ -428,8 +414,8 @@ func init() {
migrateCmd.MarkFlagRequired("datastore") migrateCmd.MarkFlagRequired("datastore")
fetchCmd.Flags().StringVarP(&gFetchRelease, "release", "r", "", "Release to fetch (e.g.: \"13.1-RELEASE\"") fetchCmd.Flags().StringVarP(&gFetchRelease, "release", "r", "", "Release to fetch (e.g.: \"13.1-RELEASE\"")
fetchCmd.Flags().StringVarP(&gFetchIntoDS, "datastore", "o", "", "Datastore release will be saved to") fetchCmd.Flags().StringVarP(&gFetchIntoDS, "datastore", "d", "", "Datastore release will be saved to")
fetchCmd.Flags().StringVarP(&gFetchFrom, "from", "d", "", "Repository to download from. Should contain XY.Z-RELEASE. File protocol supported") fetchCmd.Flags().StringVarP(&gFetchFrom, "from", "f", "", "Repository to download from. Should contain XY.Z-RELEASE. File protocol supported")
fetchCmd.MarkFlagRequired("release") fetchCmd.MarkFlagRequired("release")
fetchCmd.MarkFlagRequired("datastore") fetchCmd.MarkFlagRequired("datastore")
@ -439,9 +425,7 @@ func init() {
createCmd.Flags().StringVarP(&gCreateArgs.Release, "release", "r", "", "Release for the jail (e.g.: \"13.1-RELEASE\"") createCmd.Flags().StringVarP(&gCreateArgs.Release, "release", "r", "", "Release for the jail (e.g.: \"13.1-RELEASE\"")
createCmd.Flags().BoolVarP(&gCreateArgs.BaseJail, "basejail", "b", false, "Basejail. This will create a jail mounted read only from a release, so every up(date|grade) made to this release will immediately propagate to new jail.\n") createCmd.Flags().BoolVarP(&gCreateArgs.BaseJail, "basejail", "b", false, "Basejail. This will create a jail mounted read only from a release, so every up(date|grade) made to this release will immediately propagate to new jail.\n")
createCmd.Flags().StringVarP(&gCreateArgs.Datastore, "datastore", "d", "", "Datastore to create the jail on. Defaults to first declared in config.") createCmd.Flags().StringVarP(&gCreateArgs.Datastore, "datastore", "d", "", "Datastore to create the jail on. Defaults to first declared in config.")
createCmd.Flags().StringVarP(&gCreateArgs.Properties, "configuration", "p", "", "Configuration properties with format k1=v1,k2=v2 (Ex: \"Config.Ip4_addr='vnet0|192.168.1.2',Config.Ip6=none\")")
apiCmd.Flags().StringVarP(&gApiAddress, "listen-addr", "l", "127.0.0.1", "API listening address")
apiCmd.Flags().IntVarP(&gApiPort, "listen-port", "p", 1234, "API listening port")
// Now declare commands // Now declare commands
rootCmd.AddCommand(initCmd) rootCmd.AddCommand(initCmd)
@ -462,7 +446,6 @@ func init() {
rootCmd.AddCommand(updateCmd) rootCmd.AddCommand(updateCmd)
rootCmd.AddCommand(upgradeCmd) rootCmd.AddCommand(upgradeCmd)
rootCmd.AddCommand(createCmd) rootCmd.AddCommand(createCmd)
rootCmd.AddCommand(apiCmd)
rootCmd.AddCommand(testCmd) rootCmd.AddCommand(testCmd)
snapshotCmd.AddCommand(snapshotListCmd) snapshotCmd.AddCommand(snapshotListCmd)

View File

@ -1421,25 +1421,6 @@ func StartJail(args []string) {
} }
} }
// Get a reference to current jail, to update its properties in RAM so API will get fresh data
for i, j := range gJails {
if strings.EqualFold(j.Name, cj.Name) && strings.EqualFold(j.Datastore, cj.Datastore) {
if err = setStructFieldValue(&gJails[i], "Running", "true"); err != nil {
fmt.Printf("ERROR: setting Running property to true: %s\n", err.Error())
}
if err = setStructFieldValue(&gJails[i], "JID", fmt.Sprintf("%d", cj.JID)); err != nil {
fmt.Printf("ERROR: setting JID property to %d: %s\n", cj.JID, err.Error())
}
if err = setStructFieldValue(&gJails[i], "InternalName", cj.InternalName); err != nil {
fmt.Printf("ERROR: Setting InternalName property: %s\n", err.Error())
}
// FIXME: this value of devfs_ruleset should not go in Config, it should reside in Jail root properties as it is volatile (only valid while jail is running)
/*if err = setStructFieldValue(&gJails[i], "Devfs_ruleset", "0"); err != nil {
fmt.Printf("ERROR: setting Devfs_ruleset property to 0: %s\n", err.Error())
}*/
}
}
hostInt, err := gJailHost.GetInterfaces() hostInt, err := gJailHost.GetInterfaces()
if err != nil { if err != nil {
fmt.Printf("Error listing jail host interfaces: %v\n", err) fmt.Printf("Error listing jail host interfaces: %v\n", err)

View File

@ -7,8 +7,8 @@ import (
"sync" "sync"
"errors" "errors"
"regexp" "regexp"
"slices"
"os/exec" "os/exec"
//"reflect"
"strconv" "strconv"
"strings" "strings"
@ -83,39 +83,26 @@ func umountAndUnjailZFS(jail *Jail) error {
} }
func destroyVNetInterfaces(jail *Jail) error { func destroyVNetInterfaces(jail *Jail) error {
// Wherever ipv6/ipv4 is enabled, if interface exist we destroy it if !strings.EqualFold(jail.Config.Ip4_addr, "none") {
var vnetnames []string for _, i := range strings.Split(jail.Config.Ip4_addr, ",") {
for _, i := range strings.Split(jail.Config.Ip4_addr, ",") {
if len(strings.Split(i, "|")) == 2 {
iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID) iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID)
if !slices.Contains(vnetnames, iname) { fmt.Printf("%s: ", iname)
vnetnames = append(vnetnames, iname)
}
}
}
for _, i := range strings.Split(jail.Config.Ip6_addr, ",") {
if len(strings.Split(i, "|")) == 2 {
iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID)
if !slices.Contains(vnetnames, iname) {
vnetnames = append(vnetnames, iname)
}
}
}
for _, iname := range vnetnames {
fmt.Printf(" >%s: ", iname)
_, err := executeCommand(fmt.Sprintf("ifconfig %s", iname))
if err != nil {
if strings.Contains(err.Error(), "does not exist") {
fmt.Printf("OK\n")
} else {
fmt.Printf("ERR: %v\n", err)
return err
}
} else {
_, 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 {
return err
} else {
fmt.Printf("OK\n")
}
}
}
if !strings.EqualFold(jail.Config.Ip6_addr, "none") {
for _, i := range strings.Split(jail.Config.Ip6_addr, ",") {
iname := fmt.Sprintf("%s.%d", strings.Split(i, "|")[0], jail.JID)
fmt.Printf("%s: ", 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 {
fmt.Printf("ERR: %v\n", err)
return err return err
} else { } else {
fmt.Printf("OK\n") fmt.Printf("OK\n")

View File

@ -1,10 +1,7 @@
package cmd package cmd
import ( import (
"fmt"
"sort"
"time" "time"
"strings"
) )
const ( const (
@ -564,90 +561,3 @@ type DatastoreSort struct {
AvailableDec datastoreLessFunc AvailableDec datastoreLessFunc
} }
type Field struct {
Name string
MaxLen int
Value string
}
func (j *Jail) PrintJSON(fields []string) ([]byte, error) {
return j.PrintJSONWithUpper("", fields)
}
func (j *Jail) PrintJSONWithUpper(upper string, fields []string) ([]byte, error) {
var res []byte
if len(fields) < 1 {
return res, nil
}
if len(upper) > 0 {
res = append(res, []byte(fmt.Sprintf("\"%s\": {", upper))...)
} else {
res = append(res, []byte("{")...)
}
// Reorder fields so we got fields of a sub-structure adjacents
sort.Strings(fields)
var ffname string
offset := 0
for i, _ := range fields {
if i + offset >= len(fields) {
break
}
// Is this struct in struct?
if strings.Contains(fields[i+offset], ".") {
// Count items in this struct
var subfields []string
var newUpper string
for _, sf := range fields[i+offset:] {
if strings.Split(sf, ".")[0] == strings.Split(fields[i+offset], ".")[0] {
newUpper = strings.Split(sf, ".")[0]
sub := strings.Join(strings.Split(string(sf), ".")[1:], ".")
subfields = append(subfields, sub)
}
}
out, err := j.PrintJSONWithUpper(newUpper, subfields)
if err != nil {
return []byte{}, err
}
res = append(res, out...)
offset += len(subfields)-1
if i + offset < len(fields) - 1 {
res = append(res, []byte(",")...)
}
continue
}
if len(upper) > 0 {
ffname = fmt.Sprintf("%s.%s", upper, fields[i+offset])
} else {
ffname = fields[i+offset]
}
val, _, err := getStructFieldValue(j, ffname)
if err != nil {
return []byte{}, fmt.Errorf("Error accessing field \"%s\" : %v\n", fields[i+offset], err)
}
res = append(res, []byte(fmt.Sprintf("\"%s\": ", fields[i+offset]))...)
switch val.Interface().(type) {
case string:
res = append(res, []byte(fmt.Sprintf("\"%s\"", val.Interface().(string)))...)
case int:
res = append(res, []byte(fmt.Sprintf("%d", val.Interface().(int)))...)
case bool:
res = append(res, []byte(fmt.Sprintf("%t", val.Interface().(bool)))...)
default:
fmt.Printf("ERREUR dans Jail.PrintJSONWithUpper() : Type inconnu : %T\n", val.Interface())
}
if i + offset < len(fields) - 1 {
res = append(res, []byte(",")...)
}
}
res = append(res, []byte("}")...)
return res, nil
}

View File

@ -228,7 +228,7 @@ CreateBootEnv no
"type": "jail", "type": "jail",
"used": "readonly", "used": "readonly",
"vmemoryuse": "off", "vmemoryuse": "off",
"vnet": 0, "vnet": 1,
"vnet0_mac": "none", "vnet0_mac": "none",
"vnet1_mac": "none", "vnet1_mac": "none",
"vnet2_mac": "none", "vnet2_mac": "none",
@ -709,8 +709,10 @@ func executeCommandInJail(jail *Jail, cmdline string) (string, error) {
// else // else
word = word + string(c) word = word + string(c)
} }
log.Debugf("executeCommandInJail: will execute \"%s\"\n", strings.Join(cmd, " ")) if gDebug {
fmt.Printf("DEBUG: executeCommandInJail: prepare to execute \"%s\"\n", cmd)
}
out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput() out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
@ -749,6 +751,30 @@ func executeScript(script string) (string, error) {
return string(out), err return string(out), err
} }
/*****************************************************************************
*
* Network related operations
*
*****************************************************************************/
func getBridgeMembers(bridge string) ([]string, error) {
var members []string
cmd := fmt.Sprintf("/sbin/ifconfig %s", bridge)
out, err := executeCommand(cmd)
if err != nil {
return members, errors.New(fmt.Sprintf("%v; command returned \"%s\"", err, out))
}
for _, line := range strings.Split(out, "\n") {
if strings.HasPrefix(strings.TrimLeft(line, " \t"), "member:") {
m := strings.Split(strings.TrimLeft(strings.Split(line, ":")[1], " "), " ")[0]
log.Debugf("%s is member of %s\n", m, bridge)
members = append(members, m)
}
}
return members, nil
}
/***************************************************************************** /*****************************************************************************
* *
* ZFS datasets/pools operations * ZFS datasets/pools operations
@ -1031,6 +1057,7 @@ func addRcKeyValue(rcconfpath string, key string, value string) error {
} }
return nil return nil
} }
/***************************************************************************** /*****************************************************************************
* Parse an fstab file, returning an array of Mount * Parse an fstab file, returning an array of Mount
*****************************************************************************/ *****************************************************************************/

4
go.mod
View File

@ -1,12 +1,10 @@
module gocage module gocage
go 1.21 go 1.17
require ( require (
github.com/c-robinson/iplib v1.0.3 github.com/c-robinson/iplib v1.0.3
github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b github.com/c2h5oh/datasize v0.0.0-20220606134207-859f65c6625b
github.com/gin-contrib/cors v1.7.2
github.com/gin-gonic/gin v1.10.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/otiai10/copy v1.12.0 github.com/otiai10/copy v1.12.0
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1