11 Commits

Author SHA1 Message Date
yo
d636d963ff WIP: fetch command 2022-10-16 15:20:00 +02:00
yo
56b4d8ea84 WIP: fetch command 2022-10-16 15:19:51 +02:00
yo
abaa4a11f9 Check if dataset exist, create dataset 2022-10-16 15:19:03 +02:00
yo
74602dc0df Add arch property to jailhost 2022-10-16 15:18:35 +02:00
yo
be756edea7 rm blank line 2022-10-16 15:17:56 +02:00
yo
546382ded7 Add TODO in readme 2022-10-15 16:38:17 +02:00
yo
07eccffbd1 Add Devfs_ruleset property to reflect generated RS 2022-10-15 16:33:29 +02:00
yo
7809107ea4 Update readme 2022-10-15 15:24:24 +02:00
yo
5ab0a59db4 Update readme 2022-10-15 15:23:58 +02:00
yo
1b27753718 Add service file 2022-10-15 15:16:26 +02:00
yo
c97f5317dd Add start and stop all jails for boot/shutdown, add debug mode 2022-10-15 14:53:43 +02:00
12 changed files with 440 additions and 34 deletions

1
.gitignore vendored
View File

@ -1,3 +1,2 @@
gocage
go.sum

View File

@ -143,7 +143,7 @@ gocage datastore list
+------------+-------------+------------+-----------+----------+------------+
</pre></code>
#### Filter datastores
### Filter datastores
As with jails and snapshots, you can filter by name:
<pre><code>
gocage datastore list iocage
@ -154,7 +154,7 @@ gocage datastore list iocage
+------------+-------------+------------+-----------+----------+------------+
</pre></code>
#### Sort datastores
### Sort datastores
You can sort datastores:
<pre><code>
gocage datastore list -s -Available
@ -175,6 +175,11 @@ 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.
### Warning
Be aware the moment you migrate a jail to another datastore than /iocage default, you lose compatibility with iocage.
Then you need to disable iocage service, and enable gocage so the jails will start automatically at boot.
Also make sure, if you don't destroy source jail, that it won't have the "boot" property set or you will have the 2 jails up at boot.
<pre><code>
gocage migrate -d fastiocage srv-random
Snapshot data/iocage/jails/srv-random: Done
@ -182,3 +187,12 @@ 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>
TODO
----------
gocage update
gocage upgrade
gocage create
gocage destroy

206
cmd/fetch.go Normal file
View File

@ -0,0 +1,206 @@
package cmd
import (
"io"
"os"
"fmt"
"bufio"
"bytes"
//"errors"
"strings"
"net/http"
"encoding/hex"
"crypto/sha256"
log "github.com/sirupsen/logrus"
)
const (
ReleaseServer = "download.freebsd.org"
ReleaseRootDir = "ftp/releases"
)
var (
FetchFiles = []string{"base.txz", "lib32.txz", "src.txz"}
)
// TODO: Check if files already exist
// Fetch release files, verify, put in datastore under ${datastore}/releases
// Only support http
func fetchRelease(release string, proto string, arch string, datastore string) {
var ds Datastore
log.SetReportCaller(true)
if false == strings.EqualFold(proto, "http") {
fmt.Printf("Unsupported protocol: %s\n", proto)
return
}
for _, ds = range gDatastores {
if strings.EqualFold(datastore, ds.Name) {
break
}
}
if false == strings.EqualFold(datastore, ds.Name) {
fmt.Printf("Datastore not found: %s\n", datastore)
return
}
// Check datastore have a releases dataset, and it is mounted
releaseDsName := fmt.Sprintf("%s/releases", ds.ZFSDataset)
releaseDsMountPoint := fmt.Sprintf("%s/releases", ds.Mountpoint)
exist, err := doZfsDatasetExist(releaseDsName)
if err != nil {
fmt.Printf("Error accessing dataset %s: %v\n", releaseDsName, err)
return
}
if false == exist {
// Then create dataset
if err := createZfsDataset(releaseDsName, releaseDsMountPoint, "lz4"); err != nil {
fmt.Printf("Error creating dataset %s: %v\n", releaseDsName, err)
return
}
}
// Create XX.X-RELEASE directory if necessary
fileDir := fmt.Sprintf("%s/%s-RELEASE", releaseDsMountPoint, release)
_, err = os.Stat(fileDir)
if os.IsNotExist(err) {
if err := os.Mkdir(fileDir, 0755); err != nil {
fmt.Printf("Error creating directory %s: %v\n", fileDir, err)
return
}
}
fetchUrl := fmt.Sprintf("%s://%s/%s/%s/%s-RELEASE", proto, ReleaseServer, ReleaseRootDir, arch, release)
log.Debugf("FetchURL = %s", fetchUrl)
// check if proto/server/arch/release is available
if strings.EqualFold(proto, "http") {
resp, err := http.Get(fetchUrl)
if err != nil {
fmt.Printf("Can not get %s: %v\n", fetchUrl, err)
return
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
fmt.Printf("Get %s returned %d, check release name\n", fetchUrl, resp.StatusCode)
return
}
}
// Fetch files
// Get MANIFEST so we get sha256 sums
if err := fetchHTTPFile(fetchUrl, "MANIFEST", fileDir, []byte{}); err != nil {
fmt.Printf("%v\n", err)
return
}
// Build an array of "file;checksum"
checksumMap, err := buildFileChecksumFromManifest(fmt.Sprintf("%s/MANIFEST", fileDir), FetchFiles)
if err != nil {
fmt.Printf("%v\n", err)
return
}
// Fetch remaining files, verify integrity and write to disk
for f, c := range checksumMap {
if strings.EqualFold(proto, "http") {
if err := fetchHTTPFile(fetchUrl, f, fileDir, c); err != nil {
fmt.Printf("%v\n", err)
return
}
}
}
}
func fetchHTTPFile(baseUrl, fileName, storeDir string, checksum []byte) error {
// Check storeDir exist
_, err := os.Stat(storeDir)
if os.IsNotExist(err) {
return fmt.Errorf("Directory does not exist: %s\n", storeDir)
}
url := fmt.Sprintf("%s/%s", baseUrl, fileName)
fmt.Printf("Fetching %s...", url)
resp, err := http.Get(url)
if err != nil {
fmt.Printf(" Error\n")
return fmt.Errorf("Can not get %s: %v\n", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("Can not read %s response body: %v\n", url, err)
}
// Check integrity
if len(checksum) > 0 {
err = checkIntegrity(body, checksum)
if err != nil {
return fmt.Errorf("Error checking integrity")
}
}
dest := fmt.Sprintf("%s/%s", storeDir, fileName)
f, err := os.Create(dest) // creates a file at current directory
if err != nil {
return fmt.Errorf("Can not create file %s: %v\n", dest, err)
}
defer f.Close()
f.Write(body)
fmt.Printf(" Done.\n")
return nil
}
func checkIntegrity(data []byte, checksum []byte) error {
sum := sha256.Sum256(data)
if false == bytes.Equal(checksum[:],sum[:]) {
return fmt.Errorf("Invalid checksum: %x != %x", sum, checksum)
}
return nil
}
// Get checksum from manifest, for each file in fileList
/* MANIFEST format:
* base-dbg.txz a5b51f3d54686509e91ca9c30e9f1cd93dc757f25c643609b3c35e7119c0531d 1654 base_dbg "Base system (Debugging)" off
* base.txz e85b256930a2fbc04b80334106afecba0f11e52e32ffa197a88d7319cf059840 26492 base "Base system (MANDATORY)" on
* kernel-dbg.txz 6b47a6cb83637af1f489aa8cdb802d9db936ea864887188cfc69d8075762214e 912 kernel_dbg "Kernel (Debugging)" on
*/
func buildFileChecksumFromManifest(manifest string, fileList []string) (map[string][]byte, error) {
var ckarr = make(map[string][]byte)
rm, err := os.Open(manifest)
if err != nil {
return ckarr, fmt.Errorf("Unable to open MANIFEST: %v", err)
}
fscan := bufio.NewScanner(rm)
fscan.Split(bufio.ScanLines)
// For each MANIFEST line...
for fscan.Scan() {
fields := strings.Fields(fscan.Text())
fn := fields[0]
fck := fields[1]
hexSum, err := hex.DecodeString(fck)
if err != nil {
return ckarr, fmt.Errorf("Invalid value for checksum %s", fck)
}
// ... Find the corresponding file in fileList, then add to checksum array ckarr
for _, f := range fileList {
if strings.EqualFold(f, fn) {
ckarr[fn] = hexSum
break
}
}
}
if len(ckarr) < len(fileList) {
return ckarr, fmt.Errorf("Missing file in MANIFEST")
}
return ckarr, nil
}

View File

@ -179,6 +179,15 @@ func getHostId() (string, error) {
return strings.Split(string(content), "\n")[0], nil
}
func getArch() (string, error) {
out, err := executeCommand("/usr/bin/uname -p")
if err != nil {
return "", fmt.Errorf("Error executing \"/usr/bin/uname -p\": %v", err)
}
return strings.Split(out, "\n")[0], nil
}
func getFreeBSDVersion() (FreeBSDVersion, error) {
var version FreeBSDVersion
regex := `([0-9]{1,2})(\.)?([0-9]{1,2})?\-([^\-]*)(\-)?(p[0-9]{1,2})?`
@ -219,6 +228,9 @@ func NewJailHost() (JailHost, error) {
if jh.hostname, err = getHostname(); err != nil {
return jh, err
}
if jh.arch, err = getArch(); err != nil {
return jh, err
}
if jh.hostid, err = getHostId(); err != nil {
return jh, err
}

View File

@ -273,6 +273,7 @@ func listJailsFromDirectory(dir string, dsname string) ([]Jail, error) {
j.JID = rj.Jid
j.Running = true
j.InternalName = rj.Name
j.Devfs_ruleset = rj.Devfs_ruleset
break
}
}

View File

@ -10,11 +10,11 @@ import (
"github.com/spf13/cobra"
"github.com/spf13/viper"
// TODO : Use log
//log "github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
)
const (
gVersion = "0.29g"
gVersion = "0.31"
// TODO : Get from $jail_zpool/defaults.json
MIN_DYN_DEVFS_RULESET = 1000
@ -27,6 +27,7 @@ var (
gUseSudo bool
gForce bool
gDebug bool
gConfigFile string
gDisplayJColumns string
@ -50,6 +51,10 @@ var (
gMigrateDestDatastore string
gYesToAll bool
gFetchRelease string
gFetchIntoDS string
rootCmd = &cobra.Command{
Use: "gocage",
Short: "GoCage is a FreeBSD Jail management tool",
@ -109,7 +114,11 @@ ex: gocage list srv-db srv-web`,
Run: func(cmd *cobra.Command, args []string) {
// Load inventory
ListJails(args, false)
StopJail(args)
if len(args) == 0 {
StopAllRunningJails()
} else {
StopJail(args)
}
},
}
@ -119,11 +128,17 @@ ex: gocage list srv-db srv-web`,
Run: func(cmd *cobra.Command, args []string) {
// Load inventory
ListJails(args, false)
StartJail(args)
if len(args) == 0 {
StartJailsAtBoot()
} else {
StartJail(args)
}
WriteConfigToDisk(false)
},
}
restartCmd = &cobra.Command{
Use: "restart",
Short: "restart jail",
@ -274,6 +289,14 @@ You can specify multiple datastores.`,
},
}
fetchCmd = &cobra.Command{
Use: "fetch",
Short: "Fetch FreeBSD release to local datastore",
Run: func(cmd *cobra.Command, args []string) {
fetchRelease(gFetchRelease, "http", gJailHost.arch, gFetchIntoDS)
},
}
testCmd = &cobra.Command{
Use: "test",
Short: "temporary command to test some code snippet",
@ -293,6 +316,7 @@ func init() {
rootCmd.PersistentFlags().StringVarP(&gConfigFile, "config", "c", "/usr/local/etc/gocage.conf.yml", "GoCage configuration file")
rootCmd.PersistentFlags().BoolVarP(&gUseSudo, "sudo", "u", false, "Use sudo to run commands")
rootCmd.PersistentFlags().StringVarP(&gTimeZone, "timezone", "t", "", "Specify timezone. Will get from /var/db/zoneinfo if not set.")
rootCmd.PersistentFlags().BoolVar(&gDebug, "debug", false, "Debug mode")
// Command dependant switches
@ -325,6 +349,12 @@ func init() {
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")
fetchCmd.Flags().StringVarP(&gFetchRelease, "release", "r", "", "Release to fetch (e.g.: \"13.1\"")
fetchCmd.Flags().StringVarP(&gFetchIntoDS, "datastore", "o", "", "Datastore release will be saved to")
fetchCmd.MarkFlagRequired("release")
fetchCmd.MarkFlagRequired("datastore")
// Now declare commands
rootCmd.AddCommand(versionCmd)
@ -340,6 +370,7 @@ func init() {
rootCmd.AddCommand(snapshotCmd)
rootCmd.AddCommand(migrateCmd)
rootCmd.AddCommand(datastoreCmd)
rootCmd.AddCommand(fetchCmd)
rootCmd.AddCommand(testCmd)
@ -414,6 +445,12 @@ func initConfig() {
fmt.Printf("More than 3 sort criteria is not supported!\n")
os.Exit(1)
}
if gDebug {
log.SetLevel(log.DebugLevel)
log.Debugf("Debug mode enabled\n")
}
}
/********************************************************************************

View File

@ -1049,6 +1049,34 @@ func cleanAfterStartCrash() {
}
// Start all jails with boot=true, in priority order
func StartJailsAtBoot() {
var startList []Jail
// Get boot enabled jails
for _, j := range gJails {
if j.Config.Boot > 0 {
startList = append(startList, j)
}
}
// Order by priority
js := initJailSortStruct()
fct, _, err := getStructFieldValue(js, "Config.PriorityInc")
if err != nil {
log.Errorf("ERROR getting JailSort struct field \"Config.PriorityInc\"\n")
return
}
JailsOrderedBy(fct.Interface().(jailLessFunc)).Sort(startList)
for _, j := range startList {
jFullName := fmt.Sprintf("%s/%s", j.Datastore, j.Name)
log.Debugf("Starting %s with priority %s\n", jFullName, j.Config.Priority)
StartJail([]string{jFullName})
}
}
/*
Start jail:
Check jail fstab?
@ -1088,7 +1116,7 @@ func StartJail(args []string) {
}
if cj.Running == true {
fmt.Printf("Jail %s is already running!\n", cj.Name)
fmt.Printf("Jail %s/%s is already running!\n", cj.Datastore, cj.Name)
continue
}
@ -1303,8 +1331,9 @@ func StartJail(args []string) {
fmt.Printf(" > Start jail: OK\n")
fmt.Printf(" > With devfs ruleset %d\n", dynrs)
// Update running state and JID
// Update running state, JID and Devfs_ruleset
cj.Running = true
cj.Devfs_ruleset = dynrs
rjails, err := jail.GetJails()
if err != nil {
fmt.Printf("Error: Unable to list running jails\n")

View File

@ -10,6 +10,8 @@ import (
//"reflect"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
// TODO : Use SYS_RCTL_GET_RACCT syscall
@ -166,6 +168,33 @@ func stopJail(jail *Jail) error {
return nil
}
// Stop all running jails by reverse priority
func StopAllRunningJails() {
var stopList []Jail
// Get boot enabled jails
for _, j := range gJails {
if j.Running == true {
stopList = append(stopList, j)
}
}
// Order by priority
js := initJailSortStruct()
fct, _, err := getStructFieldValue(js, "Config.PriorityDec")
if err != nil {
log.Errorf("ERROR getting JailSort struct field \"Config.PriorityDec\"\n")
return
}
JailsOrderedBy(fct.Interface().(jailLessFunc)).Sort(stopList)
for _, j := range stopList {
jFullName := fmt.Sprintf("%s/%s", j.Datastore, j.Name)
log.Debugf("Stopping %s with priority %s\n", jFullName, j.Config.Priority)
StopJail([]string{jFullName})
}
}
/*
Stop jail:
Remove rctl rules
@ -257,20 +286,13 @@ func StopJail(args []string) {
fmt.Printf(" > Destroy VNet interfaces: OK\n")
}
}
// Get currently used ruleset from /var/run/jail.$internal_name.conf
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)
fmt.Printf(" > Remove devfs ruleset %d: \n", cj.Devfs_ruleset)
err = deleteDevfsRuleset(cj.Devfs_ruleset)
if err != nil {
fmt.Printf("ERROR: %s\n", err.Error())
} else {
fmt.Printf(" > Remove devfsruleset %d: OK\n", rsi)
fmt.Printf(" > Remove devfsruleset %d: OK\n", cj.Devfs_ruleset)
}
fmt.Printf(" > Stop jail %s:\n", cj.Name)
@ -372,6 +394,9 @@ func StopJail(args []string) {
if err = setStructFieldValue(&gJails[i], "InternalName", ""); err != nil {
fmt.Printf("ERROR: clearing InternalName property: %s\n", err.Error())
}
if err = setStructFieldValue(&gJails[i], "Devfs_ruleset", "0"); err != nil {
fmt.Printf("ERROR: setting Devfs_ruleset property to 0: %s\n", err.Error())
}
}
}
}

View File

@ -27,6 +27,7 @@ type Jail struct {
Running bool
// No need, Config.Release always represent what is running (plus it know release for non-running jails)
//Release string
Devfs_ruleset int // The effective devfs ruleset generated at runtime
Zpool string
Datastore string
}
@ -212,6 +213,7 @@ type FreeBSDVersion struct {
type JailHost struct {
hostname string
hostid string
arch string
default_gateway4 string
default_gateway6 string
default_interface string
@ -247,6 +249,8 @@ type JailSort struct {
DatastoreDec jailLessFunc
ZpoolInc jailLessFunc
ZpoolDec jailLessFunc
Devfs_rulesetInc jailLessFunc
Devfs_rulesetDec jailLessFunc
Config JailConfigSort
}

View File

@ -4,7 +4,7 @@ import (
"io"
"os"
"fmt"
"log"
//"log"
"sort"
"bufio"
"errors"
@ -15,6 +15,7 @@ import (
"io/ioutil"
"github.com/google/shlex"
"github.com/c2h5oh/datasize"
log "github.com/sirupsen/logrus"
)
const (
@ -411,6 +412,31 @@ func zfsCopyIncremental(firstsnap string, secondsnap string, dest string) error
return nil
}
// Return true if dataset exist, false if not, false & error if anything else
func doZfsDatasetExist(dataset string) (bool, error) {
cmd := fmt.Sprintf("zfs list %s", dataset)
out, err := executeCommand(cmd)
if err != nil {
if strings.HasSuffix(strings.TrimSuffix(out, "\n"), "dataset does not exist") {
return false, nil
} else {
return false, errors.New(fmt.Sprintf("%v; command returned \"%s\"", err, out))
}
}
return true, nil
}
// Create ZFS dataset. mountpoint can be "none", then the dataset won't be mounted
func createZfsDataset(dataset, mountpoint, compression string) error {
cmd := fmt.Sprintf("zfs create -o mountpoint=%s -o compression=%s %s", mountpoint, compression, dataset)
out, err := executeCommand(cmd)
if err != nil {
return errors.New(fmt.Sprintf("%v; command returned \"%s\"", err, out))
}
return nil
}
/*****************************************************************************
*
* rc.conf management

View File

@ -14,16 +14,17 @@ package jail
*/
import "C"
import (
"strconv"
"strconv"
// "syscall"
"unsafe"
)
type Jail struct {
Name string
Jid int
Path string
Name string
Jid int
Path string
Devfs_ruleset int
}
@ -33,8 +34,8 @@ func GetJails() ([]Jail, error) {
var jl Jail
var err error
// Make "params" a list of 4 jails parameters
params := make([]C.struct_jailparam, 4)
// Make "params" a list of 5 jails parameters
params := make([]C.struct_jailparam, 5)
// initialize parameter names
csname := C.CString("name")
@ -43,27 +44,31 @@ func GetJails() ([]Jail, error) {
defer C.free(unsafe.Pointer(csjid))
cspath := C.CString("path")
defer C.free(unsafe.Pointer(cspath))
csdevfsrs := C.CString("devfs_ruleset")
defer C.free(unsafe.Pointer(csdevfsrs))
cslastjid := C.CString("lastjid")
defer C.free(unsafe.Pointer(cslastjid))
// initialize params struct with parameter names
C.jailparam_init(&params[0], csname)
C.jailparam_init(&params[1], csjid)
C.jailparam_init(&params[2], cspath)
C.jailparam_init(&params[3], csdevfsrs)
// The key to retrieve jail. lastjid = 0 returns first jail and its jid as jailparam_get return value
C.jailparam_init(&params[3], cslastjid)
C.jailparam_init(&params[4], cslastjid)
lastjailid := 0
cslastjidval := C.CString(strconv.Itoa(lastjailid))
defer C.free(unsafe.Pointer(cslastjidval))
C.jailparam_import(&params[3], cslastjidval)
C.jailparam_import(&params[4], cslastjidval)
// loop on existing jails
for lastjailid >= 0 {
// get parameter values
lastjailid = int(C.jailparam_get(&params[0], 4, 0))
lastjailid = int(C.jailparam_get(&params[0], 5, 0))
if lastjailid > 0 {
nametmp := C.jailparam_export(&params[0])
jl.Name = C.GoString(nametmp)
@ -75,23 +80,28 @@ func GetJails() ([]Jail, error) {
// Memory mgmt : Non gere par Go
C.free(unsafe.Pointer(jidtmp))
pathtmp := C.jailparam_export(&params[2])
pathtmp := C.jailparam_export(&params[2])
jl.Path = C.GoString(pathtmp)
// Memory mgmt : Non gere par Go
C.free(unsafe.Pointer(pathtmp))
drstmp := C.jailparam_export(&params[3])
jl.Devfs_ruleset, _ = strconv.Atoi(C.GoString(drstmp))
// Memory mgmt : Non gere par Go
C.free(unsafe.Pointer(drstmp))
jls = append(jls, jl)
//log.Debug("Got jid " + strconv.Itoa(jl.jid) + " with name " + jl.name)
// Prepare next loop iteration
cslastjidval := C.CString(strconv.Itoa(lastjailid))
defer C.free(unsafe.Pointer(cslastjidval))
C.jailparam_import(&params[3], cslastjidval)
C.jailparam_import(&params[4], cslastjidval)
}
}
// Free 4 items of params list
C.jailparam_free(&params[0], 4)
// Free 5 items of params list
C.jailparam_free(&params[0], 5)
return jls, err
}

43
service/gocage Executable file
View File

@ -0,0 +1,43 @@
#!/bin/sh
#
# $FreeBSD$
#
# PROVIDE: gocage
# REQUIRE: LOGIN cleanvar
# KEYWORD: shutdown
# Add the following lines to /etc/rc.conf to enable :
#
# gocage_enable="YES"
#
# gocage_conf="/usr/local/etc/gocage.conf.yml"
#
. /etc/rc.subr
name="gocage"
rcvar=gocage_enable
# read configuration and set defaults
load_rc_config "$name"
: ${gocage_enable:="NO"}
: ${gocage_conf="/usr/local/etc/gocage.conf.yml"}
start_cmd=${name}_start
stop_cmd=${name}_stop
gocage_start()
{
echo "Gocage starting jails... "
/usr/local/bin/gocage -c ${gocage_conf} start
}
gocage_stop()
{
echo "Gocage stopping jails... "
/usr/local/bin/gocage -c ${gocage_conf} stop
}
run_rc_command "$1"