diff --git a/a_test.go b/a_test.go index da96b49..5a663ec 100644 --- a/a_test.go +++ b/a_test.go @@ -25,6 +25,7 @@ func Test(t *testing.T) { zfsTestDatasetSnapshot(t) zfsTestDatasetOpenAll(t) zfsTestDatasetSetProperty(t) + zfsTestDatasetHoldRelease(t) zfsTestDatasetDestroy(t) diff --git a/zfs.go b/zfs.go index b76b0d8..4956c05 100644 --- a/zfs.go +++ b/zfs.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "strings" + "time" "unsafe" ) @@ -37,6 +38,12 @@ const ( DatasetTypeBookmark = (1 << 4) ) +// HoldTag - user holds tags +type HoldTag struct { + Name string + Timestamp time.Time +} + // Dataset - ZFS dataset object type Dataset struct { list C.dataset_list_ptr @@ -99,6 +106,16 @@ func DatasetCloseAll(datasets []Dataset) { // DatasetOpen open dataset and all of its recursive children datasets func DatasetOpen(path string) (d Dataset, err error) { + if d, err = DatasetOpenSingle(path); err != nil { + return + } + err = d.openChildren() + return +} + +// DatasetOpenSingle open dataset without opening all of its recursive +// children datasets +func DatasetOpenSingle(path string) (d Dataset, err error) { csPath := C.CString(path) d.list = C.dataset_open(csPath) C.free(unsafe.Pointer(csPath)) @@ -117,7 +134,6 @@ func DatasetOpen(path string) (d Dataset, err error) { if err != nil { return } - err = d.openChildren() return } @@ -201,6 +217,16 @@ func (d *Dataset) Destroy(Defer bool) (err error) { return } +// IsSnapshot - retrun true if datset is snapshot +func (d *Dataset) IsSnapshot() (ok bool, err error) { + var path string + if path, err = d.Path(); err != nil { + return + } + ok = d.Type == DatasetTypeSnapshot || strings.Contains(path, "@") + return +} + // DestroyRecursive recursively destroy children of dataset and dataset. func (d *Dataset) DestroyRecursive() (err error) { var path string @@ -306,6 +332,7 @@ func (d *Dataset) GetProperty(p Prop) (prop Property, err error) { return } +// GetUserProperty - lookup and return user propery func (d *Dataset) GetUserProperty(p string) (prop Property, err error) { if d.list == nil { err = errors.New(msgDatasetIsNil) @@ -345,6 +372,7 @@ func (d *Dataset) SetProperty(p Prop, value string) (err error) { return } +// SetUserProperty - func (d *Dataset) SetUserProperty(prop, value string) (err error) { if d.list == nil { err = errors.New(msgDatasetIsNil) @@ -517,6 +545,93 @@ func (d *Dataset) UnmountAll(flags int) (err error) { return d.Unmount(flags) } +// Hold - Adds a single reference, named with the tag argument, to the snapshot. +// Each snapshot has its own tag namespace, and tags must be unique within that space. +func (d *Dataset) Hold(flag string) (err error) { + var path string + var pd Dataset + if path, err = d.Path(); err != nil { + return + } + if !strings.Contains(path, "@") { + err = fmt.Errorf("'%s' is not a snapshot", path) + return + } + pd, err = DatasetOpenSingle(path[:strings.Index(path, "@")]) + if err != nil { + return + } + defer pd.Close() + csSnapName := C.CString(path[strings.Index(path, "@")+1:]) + defer C.free(unsafe.Pointer(csSnapName)) + csFlag := C.CString(flag) + defer C.free(unsafe.Pointer(csFlag)) + if 0 != C.zfs_hold(pd.list.zh, csSnapName, csFlag, booleanT(false), -1) { + err = LastError() + } + return +} + +// Release - Removes a single reference, named with the tag argument, from the specified snapshot. +// The tag must already exist for each snapshot. If a hold exists on a snapshot, attempts to destroy +// that snapshot by using the zfs destroy command return EBUSY. +func (d *Dataset) Release(flag string) (err error) { + var path string + var pd Dataset + if path, err = d.Path(); err != nil { + return + } + if !strings.Contains(path, "@") { + err = fmt.Errorf("'%s' is not a snapshot", path) + return + } + pd, err = DatasetOpenSingle(path[:strings.Index(path, "@")]) + if err != nil { + return + } + defer pd.Close() + csSnapName := C.CString(path[strings.Index(path, "@")+1:]) + defer C.free(unsafe.Pointer(csSnapName)) + csFlag := C.CString(flag) + defer C.free(unsafe.Pointer(csFlag)) + if 0 != C.zfs_release(pd.list.zh, csSnapName, csFlag, booleanT(false)) { + err = LastError() + } + return +} + +// Holds - Lists all existing user references for the given snapshot +func (d *Dataset) Holds() (tags []HoldTag, err error) { + var nvl *C.nvlist_t + var nvp *C.nvpair_t + var tu64 C.uint64_t + var path string + if path, err = d.Path(); err != nil { + return + } + if !strings.Contains(path, "@") { + err = fmt.Errorf("'%s' is not a snapshot", path) + return + } + if 0 != C.zfs_get_holds(d.list.zh, &nvl) { + err = LastError() + return + } + defer C.nvlist_free(nvl) + tags = make([]HoldTag, 0, 5) + for nvp = C.nvlist_next_nvpair(nvl, nvp); nvp != nil; { + tag := C.nvpair_name(nvp) + C.nvpair_value_uint64(nvp, &tu64) + tags = append(tags, HoldTag{ + Name: C.GoString(tag), + Timestamp: time.Unix(int64(tu64), 0), + }) + + nvp = C.nvlist_next_nvpair(nvl, nvp) + } + return +} + // DatasetPropertyToName convert property to name // ( returns built in string representation of property name). // This is optional, you can represent each property with string diff --git a/zfs_test.go b/zfs_test.go index 22628ac..dbd9ea0 100644 --- a/zfs_test.go +++ b/zfs_test.go @@ -147,6 +147,47 @@ func zfsTestDatasetSnapshot(t *testing.T) { print("PASS\n\n") } +func zfsTestDatasetHoldRelease(t *testing.T) { + println("TEST Hold/Release(", TSTDatasetPathSnap, ", true, ...) ... ") + d, err := zfs.DatasetOpen(TSTDatasetPathSnap) + if err != nil { + t.Error(err) + return + } + defer d.Close() + err = d.Hold("keep") + if err != nil { + t.Error(err) + return + } + + var tags []zfs.HoldTag + tags, err = d.Holds() + if err != nil { + t.Error(err) + return + } + for _, tag := range tags { + println("tag:", tag.Name, "timestamp:", tag.Timestamp.String()) + } + + err = d.Release("keep") + if err != nil { + t.Error(err) + return + } + + tags, err = d.Holds() + if err != nil { + t.Error(err) + return + } + for _, tag := range tags { + println("* tag:", tag.Name, "timestamp:", tag.Timestamp.String()) + } + print("PASS\n\n") +} + func zfsTestDatasetDestroy(t *testing.T) { println("TEST DATASET Destroy( ", TSTDatasetPath, " ) ... ") d, err := zfs.DatasetOpen(TSTDatasetPath) diff --git a/zpool_test.go b/zpool_test.go index 05dc12f..56fda54 100644 --- a/zpool_test.go +++ b/zpool_test.go @@ -1,7 +1,6 @@ package zfs_test import ( - "encoding/json" "fmt" "io/ioutil" "os" @@ -531,33 +530,33 @@ func ExamplePool_State() { println("POOL TESTPOOL state:", zfs.PoolStateToName(pstate)) } -func TestPool_VDevTree(t *testing.T) { - type fields struct { - poolName string - } - tests := []struct { - name string - fields fields - wantErr bool - }{ - // TODO: Add test cases. - { - name: "test1", - fields: fields{"TESTPOOL"}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - pool, _ := zfs.PoolOpen(tt.fields.poolName) - defer pool.Close() - gotVdevs, err := pool.VDevTree() - if (err != nil) != tt.wantErr { - t.Errorf("Pool.VDevTree() error = %v, wantErr %v", err, tt.wantErr) - return - } - jsonData, _ := json.MarshalIndent(gotVdevs, "", "\t") - t.Logf("gotVdevs: %s", string(jsonData)) - }) - } -} +// func TestPool_VDevTree(t *testing.T) { +// type fields struct { +// poolName string +// } +// tests := []struct { +// name string +// fields fields +// wantErr bool +// }{ +// // TODO: Add test cases. +// { +// name: "test1", +// fields: fields{"TESTPOOL"}, +// wantErr: false, +// }, +// } +// for _, tt := range tests { +// t.Run(tt.name, func(t *testing.T) { +// pool, _ := zfs.PoolOpen(tt.fields.poolName) +// defer pool.Close() +// gotVdevs, err := pool.VDevTree() +// if (err != nil) != tt.wantErr { +// t.Errorf("Pool.VDevTree() error = %v, wantErr %v", err, tt.wantErr) +// return +// } +// jsonData, _ := json.MarshalIndent(gotVdevs, "", "\t") +// t.Logf("gotVdevs: %s", string(jsonData)) +// }) +// } +// }