Repository: ceph/go-ceph
Pull request #211 (2020-03-28 15:45:53)

ci: add a test github workflow/actions
Experiment with using github workflow/actions for ci.


phlogistonjohn: Unfortunately for me, github won't execute the actions if the config only exists in a pr/branch and not on master [1]. I did the remainder of my experimentation in a [custom clone](https://github.com/phlogistonjohn/go-ceph-try1) and you can see some of [the results here](https://github.com/phlogistonjohn/go-ceph-try1/actions/runs/65384869).

1 - I suppose there's probably a security consideration here but it didn't make it easy to test. :-)
phlogistonjohn: Documentation for this github feature: https://help.github.com/en/actions
nixpanic: Looks reasonable to me. I like it that the CI shows up under the Actions tab.

Also see comments in #209

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..84d7038
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,39 @@
+
+name: CI
+
+# Run tests on pull requests and when changes are directly
+# commited to master.
+on:
+  push:
+    branches: [ master ]
+  pull_request:
+    branches: [ master ]
+
+jobs:
+  # Run static/code-quality checks
+  check:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/setup-go@v2-beta
+      with:
+        go-version: 1.14
+    - uses: actions/checkout@v2
+    - name: Install revive
+      run: go get github.com/mgechev/revive
+    - name: Run checks
+      run: make check
+
+  # Run the test suite in a container per-ceph-codename
+  test-suite:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        ceph_version:
+        - "luminous"
+        - "mimic"
+        - "nautilus"
+        - "octopus"
+    steps:
+    - uses: actions/checkout@v2
+    - name: Run test container
+      run: make test-container "CEPH_VERSION=${{ matrix.ceph_version }}"
diff --git a/Makefile b/Makefile
index d10d31d..528cfae 100644
--- a/Makefile
+++ b/Makefile
@@ -20,7 +20,7 @@ test:
 .PHONY: test-docker test-container
 test-docker: test-container
 test-container: check-ceph-version .build-docker
-	$(CONTAINER_CMD) run --device /dev/fuse --cap-add SYS_ADMIN $(CONTAINER_OPTS) --rm -it -v $(CURDIR):/go/src/github.com/ceph/go-ceph$(VOLUME_FLAGS) $(DOCKER_CI_IMAGE)
+	$(CONTAINER_CMD) run --device /dev/fuse --cap-add SYS_ADMIN $(CONTAINER_OPTS) --rm -v $(CURDIR):/go/src/github.com/ceph/go-ceph$(VOLUME_FLAGS) $(DOCKER_CI_IMAGE)
 
 .PHONY: ci-image
 ci-image: .build-docker
@@ -38,7 +38,7 @@ check-ceph-version:
 check:
 	# Configure project's revive checks using .revive.toml
 	# See: https://github.com/mgechev/revive
-	@for d in $$(go list ./... | grep -v /vendor/); do revive -config .revive.toml $${d}; done
+	revive -config .revive.toml $$(go list ./... | grep -v /vendor/)
 
 # Do a quick compile only check of the tests and impliclity the
 # library code as well.
diff --git a/cephfs/cephfs.go b/cephfs/cephfs.go
index 03727d3..689d9d9 100644
--- a/cephfs/cephfs.go
+++ b/cephfs/cephfs.go
@@ -9,36 +9,11 @@ package cephfs
 import "C"
 
 import (
-	"fmt"
 	"unsafe"
 
-	"github.com/ceph/go-ceph/internal/errutil"
 	"github.com/ceph/go-ceph/rados"
 )
 
-// revive:disable:exported Temporarily live with stuttering
-
-// CephFSError represents an error condition returned from the CephFS APIs.
-type CephFSError int
-
-// revive:enable:exported
-
-// Error returns the error string for the CephFSError type.
-func (e CephFSError) Error() string {
-	errno, s := errutil.FormatErrno(int(e))
-	if s == "" {
-		return fmt.Sprintf("cephfs: ret=%d", errno)
-	}
-	return fmt.Sprintf("cephfs: ret=%d, %s", errno, s)
-}
-
-func getError(e C.int) error {
-	if e == 0 {
-		return nil
-	}
-	return CephFSError(e)
-}
-
 // MountInfo exports ceph's ceph_mount_info from libcephfs.cc
 type MountInfo struct {
 	mount *C.struct_ceph_mount_info
diff --git a/cephfs/errors.go b/cephfs/errors.go
new file mode 100644
index 0000000..a28573e
--- /dev/null
+++ b/cephfs/errors.go
@@ -0,0 +1,35 @@
+package cephfs
+
+/*
+#include <errno.h>
+*/
+import "C"
+
+import (
+	"fmt"
+
+	"github.com/ceph/go-ceph/internal/errutil"
+)
+
+// revive:disable:exported Temporarily live with stuttering
+
+// CephFSError represents an error condition returned from the CephFS APIs.
+type CephFSError int
+
+// revive:enable:exported
+
+// Error returns the error string for the CephFSError type.
+func (e CephFSError) Error() string {
+	errno, s := errutil.FormatErrno(int(e))
+	if s == "" {
+		return fmt.Sprintf("cephfs: ret=%d", errno)
+	}
+	return fmt.Sprintf("cephfs: ret=%d, %s", errno, s)
+}
+
+func getError(e C.int) error {
+	if e == 0 {
+		return nil
+	}
+	return CephFSError(e)
+}
diff --git a/rados/conn.go b/rados/conn.go
index 78c3b26..cd69a18 100644
--- a/rados/conn.go
+++ b/rados/conn.go
@@ -7,15 +7,9 @@ import "C"
 
 import (
 	"bytes"
-	"errors"
 	"unsafe"
 )
 
-var (
-	// ErrNotConnected is returned when functions are called without a RADOS connection
-	ErrNotConnected = errors.New("RADOS not connected")
-)
-
 // ClusterStat represents Ceph cluster statistics.
 type ClusterStat struct {
 	Kb          uint64
@@ -53,7 +47,7 @@ func (c *Conn) PingMonitor(id string) (string, error) {
 		reply := C.GoStringN(strout, (C.int)(strlen))
 		return reply, nil
 	}
-	return "", RadosError(int(ret))
+	return "", getError(ret)
 }
 
 // Connect establishes a connection to a RADOS cluster. It returns an error,
@@ -61,7 +55,7 @@ func (c *Conn) PingMonitor(id string) (string, error) {
 func (c *Conn) Connect() error {
 	ret := C.rados_connect(c.cluster)
 	if ret != 0 {
-		return RadosError(int(ret))
+		return getError(ret)
 	}
 	c.connected = true
 	return nil
@@ -80,14 +74,14 @@ func (c *Conn) ReadConfigFile(path string) error {
 	c_path := C.CString(path)
 	defer C.free(unsafe.Pointer(c_path))
 	ret := C.rados_conf_read_file(c.cluster, c_path)
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // ReadDefaultConfigFile configures the connection using a Ceph configuration
 // file located at default locations.
 func (c *Conn) ReadDefaultConfigFile() error {
 	ret := C.rados_conf_read_file(c.cluster, nil)
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // OpenIOContext creates and returns a new IOContext for the given pool.
@@ -103,20 +97,20 @@ func (c *Conn) OpenIOContext(pool string) (*IOContext, error) {
 	if ret == 0 {
 		return ioctx, nil
 	}
-	return nil, RadosError(int(ret))
+	return nil, getError(ret)
 }
 
 // ListPools returns the names of all existing pools.
 func (c *Conn) ListPools() (names []string, err error) {
 	buf := make([]byte, 4096)
 	for {
-		ret := int(C.rados_pool_list(c.cluster,
-			(*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))))
+		ret := C.rados_pool_list(c.cluster,
+			(*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf)))
 		if ret < 0 {
-			return nil, RadosError(int(ret))
+			return nil, getError(ret)
 		}
 
-		if ret > len(buf) {
+		if int(ret) > len(buf) {
 			buf = make([]byte, ret)
 			continue
 		}
@@ -140,7 +134,7 @@ func (c *Conn) SetConfigOption(option, value string) error {
 	defer C.free(unsafe.Pointer(c_opt))
 	defer C.free(unsafe.Pointer(c_val))
 	ret := C.rados_conf_set(c.cluster, c_opt, c_val)
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // GetConfigOption returns the value of the Ceph configuration option
@@ -165,7 +159,7 @@ func (c *Conn) GetConfigOption(name string) (value string, err error) {
 // retrieved.
 func (c *Conn) WaitForLatestOSDMap() error {
 	ret := C.rados_wait_for_latest_osdmap(c.cluster)
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 func (c *Conn) ensure_connected() error {
@@ -184,7 +178,7 @@ func (c *Conn) GetClusterStats() (stat ClusterStat, err error) {
 	c_stat := C.struct_rados_cluster_stat_t{}
 	ret := C.rados_cluster_stat(c.cluster, &c_stat)
 	if ret < 0 {
-		return ClusterStat{}, RadosError(int(ret))
+		return ClusterStat{}, getError(ret)
 	}
 	return ClusterStat{
 		Kb:          uint64(c_stat.kb),
@@ -211,28 +205,28 @@ func (c *Conn) ParseCmdLineArgs(args []string) error {
 	}
 
 	ret := C.rados_conf_parse_argv(c.cluster, argc, &argv[0])
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // ParseDefaultConfigEnv configures the connection from the default Ceph
 // environment variable(s).
 func (c *Conn) ParseDefaultConfigEnv() error {
 	ret := C.rados_conf_parse_env(c.cluster, nil)
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // GetFSID returns the fsid of the cluster as a hexadecimal string. The fsid
 // is a unique identifier of an entire Ceph cluster.
 func (c *Conn) GetFSID() (fsid string, err error) {
 	buf := make([]byte, 37)
-	ret := int(C.rados_cluster_fsid(c.cluster,
-		(*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf))))
+	ret := C.rados_cluster_fsid(c.cluster,
+		(*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf)))
 	// FIXME: the success case isn't documented correctly in librados.h
 	if ret == 36 {
 		fsid = C.GoString((*C.char)(unsafe.Pointer(&buf[0])))
 		return fsid, nil
 	}
-	return "", RadosError(int(ret))
+	return "", getError(ret)
 }
 
 // GetInstanceID returns a globally unique identifier for the cluster
@@ -246,8 +240,8 @@ func (c *Conn) GetInstanceID() uint64 {
 func (c *Conn) MakePool(name string) error {
 	c_name := C.CString(name)
 	defer C.free(unsafe.Pointer(c_name))
-	ret := int(C.rados_pool_create(c.cluster, c_name))
-	return getRadosError(int(ret))
+	ret := C.rados_pool_create(c.cluster, c_name)
+	return getError(ret)
 }
 
 // DeletePool deletes a pool and all the data inside the pool.
@@ -257,8 +251,8 @@ func (c *Conn) DeletePool(name string) error {
 	}
 	c_name := C.CString(name)
 	defer C.free(unsafe.Pointer(c_name))
-	ret := int(C.rados_pool_delete(c.cluster, c_name))
-	return getRadosError(int(ret))
+	ret := C.rados_pool_delete(c.cluster, c_name)
+	return getError(ret)
 }
 
 // GetPoolByName returns the ID of the pool with a given name.
@@ -329,7 +323,7 @@ func (c *Conn) monCommand(args, inputBuffer []byte) (buffer []byte, info string,
 		C.free(unsafe.Pointer(outbuf))
 	}
 	if ret != 0 {
-		err = RadosError(int(ret))
+		err = getError(ret)
 		return nil, info, err
 	}
 
@@ -400,7 +394,7 @@ func (c *Conn) pgCommand(pgid []byte, args [][]byte, inputBuffer []byte) (buffer
 		C.free(unsafe.Pointer(outbuf))
 	}
 	if ret != 0 {
-		err = RadosError(int(ret))
+		err = getError(ret)
 		return nil, info, err
 	}
 
diff --git a/rados/errors.go b/rados/errors.go
new file mode 100644
index 0000000..d01a425
--- /dev/null
+++ b/rados/errors.go
@@ -0,0 +1,63 @@
+package rados
+
+/*
+#include <errno.h>
+*/
+import "C"
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/ceph/go-ceph/internal/errutil"
+)
+
+// revive:disable:exported Temporarily live with stuttering
+
+// RadosError represents an error condition returned from the Ceph RADOS APIs.
+type RadosError int
+
+// revive:enable:exported
+
+// Error returns the error string for the RadosError type.
+func (e RadosError) Error() string {
+	errno, s := errutil.FormatErrno(int(e))
+	if s == "" {
+		return fmt.Sprintf("rados: ret=%d", errno)
+	}
+	return fmt.Sprintf("rados: ret=%d, %s", errno, s)
+}
+
+func getError(e C.int) error {
+	if e == 0 {
+		return nil
+	}
+	return RadosError(e)
+}
+
+// Public go errors:
+
+var (
+	// ErrNotConnected is returned when functions are called without a RADOS connection
+	ErrNotConnected = errors.New("RADOS not connected")
+)
+
+// Public RadosErrors:
+
+const (
+	// ErrNotFound indicates a missing resource.
+	ErrNotFound = RadosError(-C.ENOENT)
+	// ErrPermissionDenied indicates a permissions issue.
+	ErrPermissionDenied = RadosError(-C.EPERM)
+	// ErrObjectExists indicates that an exclusive object creation failed.
+	ErrObjectExists = RadosError(-C.EEXIST)
+
+	// RadosErrorNotFound indicates a missing resource.
+	//
+	// Deprecated: use ErrNotFound instead
+	RadosErrorNotFound = ErrNotFound
+	// RadosErrorPermissionDenied indicates a permissions issue.
+	//
+	// Deprecated: use ErrPermissionDenied instead
+	RadosErrorPermissionDenied = ErrPermissionDenied
+)
diff --git a/rados/ioctx.go b/rados/ioctx.go
index f91e437..45069a9 100644
--- a/rados/ioctx.go
+++ b/rados/ioctx.go
@@ -121,7 +121,7 @@ func (ioctx *IOContext) Create(oid string, exclusive CreateOption) error {
 	ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0)
 	C.rados_release_write_op(op)
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // Write writes len(data) bytes to the object with key oid starting at byte
@@ -140,7 +140,7 @@ func (ioctx *IOContext) Write(oid string, data []byte, offset uint64) error {
 		(C.size_t)(len(data)),
 		(C.uint64_t)(offset))
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // WriteFull writes len(data) bytes to the object with key oid.
@@ -153,7 +153,7 @@ func (ioctx *IOContext) WriteFull(oid string, data []byte) error {
 	ret := C.rados_write_full(ioctx.ioctx, c_oid,
 		(*C.char)(unsafe.Pointer(&data[0])),
 		(C.size_t)(len(data)))
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // Append appends len(data) bytes to the object with key oid.
@@ -166,7 +166,7 @@ func (ioctx *IOContext) Append(oid string, data []byte) error {
 	ret := C.rados_append(ioctx.ioctx, c_oid,
 		(*C.char)(unsafe.Pointer(&data[0])),
 		(C.size_t)(len(data)))
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // Read reads up to len(data) bytes from the object with key oid starting at byte
@@ -190,7 +190,7 @@ func (ioctx *IOContext) Read(oid string, data []byte, offset uint64) (int, error
 	if ret >= 0 {
 		return int(ret), nil
 	}
-	return 0, getRadosError(int(ret))
+	return 0, getError(ret)
 }
 
 // Delete deletes the object with key oid. It returns an error, if any.
@@ -198,7 +198,7 @@ func (ioctx *IOContext) Delete(oid string) error {
 	c_oid := C.CString(oid)
 	defer C.free(unsafe.Pointer(c_oid))
 
-	return getRadosError(int(C.rados_remove(ioctx.ioctx, c_oid)))
+	return getError(C.rados_remove(ioctx.ioctx, c_oid))
 }
 
 // Truncate resizes the object with key oid to size size. If the operation
@@ -209,7 +209,7 @@ func (ioctx *IOContext) Truncate(oid string, size uint64) error {
 	c_oid := C.CString(oid)
 	defer C.free(unsafe.Pointer(c_oid))
 
-	return getRadosError(int(C.rados_trunc(ioctx.ioctx, c_oid, (C.uint64_t)(size))))
+	return getError(C.rados_trunc(ioctx.ioctx, c_oid, (C.uint64_t)(size)))
 }
 
 // Destroy informs librados that the I/O context is no longer in use.
@@ -229,7 +229,7 @@ func (ioctx *IOContext) GetPoolStats() (stat PoolStat, err error) {
 	c_stat := C.struct_rados_pool_stat_t{}
 	ret := C.rados_ioctx_pool_stat(ioctx.ioctx, &c_stat)
 	if ret < 0 {
-		return PoolStat{}, getRadosError(int(ret))
+		return PoolStat{}, getError(ret)
 	}
 	return PoolStat{
 		Num_bytes:                      uint64(c_stat.num_bytes),
@@ -257,7 +257,7 @@ func (ioctx *IOContext) GetPoolName() (name string, err error) {
 			buf = make([]byte, len(buf)*2)
 			continue
 		} else if ret < 0 {
-			return "", getRadosError(int(ret))
+			return "", getError(ret)
 		}
 		name = C.GoStringN((*C.char)(unsafe.Pointer(&buf[0])), ret)
 		return name, nil
@@ -277,7 +277,7 @@ func (ioctx *IOContext) ListObjects(listFn ObjectListFunc) error {
 	var ctx C.rados_list_ctx_t
 	ret := C.rados_nobjects_list_open(ioctx.ioctx, &ctx)
 	if ret < 0 {
-		return getRadosError(int(ret))
+		return getError(ret)
 	}
 	defer func() { C.rados_nobjects_list_close(ctx) }()
 
@@ -287,7 +287,7 @@ func (ioctx *IOContext) ListObjects(listFn ObjectListFunc) error {
 		if ret == -C.ENOENT {
 			return nil
 		} else if ret < 0 {
-			return getRadosError(int(ret))
+			return getError(ret)
 		}
 		listFn(C.GoString(c_entry))
 	}
@@ -307,7 +307,7 @@ func (ioctx *IOContext) Stat(object string) (stat ObjectStat, err error) {
 		&c_pmtime)
 
 	if ret < 0 {
-		return ObjectStat{}, getRadosError(int(ret))
+		return ObjectStat{}, getError(ret)
 	}
 	return ObjectStat{
 		Size:    uint64(c_psize),
@@ -333,7 +333,7 @@ func (ioctx *IOContext) GetXattr(object string, name string, data []byte) (int,
 	if ret >= 0 {
 		return int(ret), nil
 	}
-	return 0, getRadosError(int(ret))
+	return 0, getError(ret)
 }
 
 // SetXattr sets an xattr for an object with key `name` with value as `data`
@@ -350,7 +350,7 @@ func (ioctx *IOContext) SetXattr(object string, name string, data []byte) error
 		(*C.char)(unsafe.Pointer(&data[0])),
 		(C.size_t)(len(data)))
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // ListXattrs lists all the xattrs for an object. The xattrs are returned as a
@@ -363,7 +363,7 @@ func (ioctx *IOContext) ListXattrs(oid string) (map[string][]byte, error) {
 
 	ret := C.rados_getxattrs(ioctx.ioctx, c_oid, &it)
 	if ret < 0 {
-		return nil, getRadosError(int(ret))
+		return nil, getError(ret)
 	}
 	defer func() { C.rados_getxattrs_end(it) }()
 	m := make(map[string][]byte)
@@ -375,7 +375,7 @@ func (ioctx *IOContext) ListXattrs(oid string) (map[string][]byte, error) {
 
 		ret := C.rados_getxattrs_next(it, &c_name, &c_val, &c_len)
 		if ret < 0 {
-			return nil, getRadosError(int(ret))
+			return nil, getError(ret)
 		}
 		// rados api returns a null name,val & 0-length upon
 		// end of iteration
@@ -398,7 +398,7 @@ func (ioctx *IOContext) RmXattr(oid string, name string) error {
 		c_oid,
 		c_name)
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // LockExclusive takes an exclusive lock on an object.
@@ -445,7 +445,7 @@ func (ioctx *IOContext) LockExclusive(oid, name, cookie, desc string, duration t
 	case -C.EEXIST:
 		return int(ret), nil
 	default:
-		return int(ret), RadosError(int(ret))
+		return int(ret), getError(ret)
 	}
 }
 
@@ -496,7 +496,7 @@ func (ioctx *IOContext) LockShared(oid, name, cookie, tag, desc string, duration
 	case -C.EEXIST:
 		return int(ret), nil
 	default:
-		return int(ret), RadosError(int(ret))
+		return int(ret), getError(ret)
 	}
 }
 
@@ -525,7 +525,7 @@ func (ioctx *IOContext) Unlock(oid, name, cookie string) (int, error) {
 	case -C.ENOENT:
 		return int(ret), nil
 	default:
-		return int(ret), RadosError(int(ret))
+		return int(ret), getError(ret)
 	}
 }
 
@@ -582,7 +582,7 @@ func (ioctx *IOContext) ListLockers(oid, name string) (*LockInfo, error) {
 	}
 
 	if ret < 0 {
-		return nil, RadosError(int(ret))
+		return nil, RadosError(ret)
 	}
 	return &LockInfo{int(ret), c_exclusive == 1, C.GoString(c_tag), splitCString(c_clients, c_clients_len), splitCString(c_cookies, c_cookies_len), splitCString(c_addrs, c_addrs_len)}, nil
 }
@@ -618,6 +618,6 @@ func (ioctx *IOContext) BreakLock(oid, name, client, cookie string) (int, error)
 	case -C.EINVAL: // -EINVAL
 		return int(ret), nil
 	default:
-		return int(ret), RadosError(int(ret))
+		return int(ret), getError(ret)
 	}
 }
diff --git a/rados/object_iter.go b/rados/object_iter.go
index c0969eb..6add2bd 100644
--- a/rados/object_iter.go
+++ b/rados/object_iter.go
@@ -20,7 +20,7 @@ type IterToken uint32
 func (ioctx *IOContext) Iter() (*Iter, error) {
 	iter := Iter{}
 	if cerr := C.rados_nobjects_list_open(ioctx.ioctx, &iter.ctx); cerr < 0 {
-		return nil, getRadosError(int(cerr))
+		return nil, getError(cerr)
 	}
 	return &iter, nil
 }
@@ -53,7 +53,7 @@ func (iter *Iter) Next() bool {
 	var c_entry *C.char
 	var c_namespace *C.char
 	if cerr := C.rados_nobjects_list_next(iter.ctx, &c_entry, nil, &c_namespace); cerr < 0 {
-		iter.err = getRadosError(int(cerr))
+		iter.err = getError(cerr)
 		return false
 	}
 	iter.entry = C.GoString(c_entry)
diff --git a/rados/omap.go b/rados/omap.go
index bea0dda..7d279c2 100644
--- a/rados/omap.go
+++ b/rados/omap.go
@@ -63,7 +63,7 @@ func (ioctx *IOContext) SetOmap(oid string, pairs map[string][]byte) error {
 	ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0)
 	C.rados_release_write_op(op)
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // OmapListFunc is the type of the function called for each omap key
@@ -104,9 +104,9 @@ func (ioctx *IOContext) ListOmapValues(oid string, startAfter string, filterPref
 	ret := C.rados_read_op_operate(op, ioctx.ioctx, c_oid, 0)
 
 	if int(ret) != 0 {
-		return getRadosError(int(ret))
+		return getError(ret)
 	} else if int(c_prval) != 0 {
-		return RadosError(int(c_prval))
+		return getError(c_prval)
 	}
 
 	for {
@@ -117,7 +117,7 @@ func (ioctx *IOContext) ListOmapValues(oid string, startAfter string, filterPref
 		ret = C.rados_omap_get_next(c_iter, &c_key, &c_val, &c_len)
 
 		if int(ret) != 0 {
-			return getRadosError(int(ret))
+			return getError(ret)
 		}
 
 		if c_key == nil {
@@ -210,7 +210,7 @@ func (ioctx *IOContext) RmOmapKeys(oid string, keys []string) error {
 	ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0)
 	C.rados_release_write_op(op)
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
 
 // CleanOmap clears the omap `oid`
@@ -224,5 +224,5 @@ func (ioctx *IOContext) CleanOmap(oid string) error {
 	ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0)
 	C.rados_release_write_op(op)
 
-	return getRadosError(int(ret))
+	return getError(ret)
 }
diff --git a/rados/rados.go b/rados/rados.go
index 008db3c..d1f000a 100644
--- a/rados/rados.go
+++ b/rados/rados.go
@@ -7,41 +7,15 @@ package rados
 import "C"
 
 import (
-	"fmt"
 	"runtime"
 	"unsafe"
-
-	"github.com/ceph/go-ceph/internal/errutil"
 )
 
-// revive:disable:exported Temporarily live with stuttering
-
-// RadosError represents an error condition returned from the Ceph RADOS APIs.
-type RadosError int
-
-// revive:enable:exported
-
-// Error returns the error string for the RadosError type.
-func (e RadosError) Error() string {
-	errno, s := errutil.FormatErrno(int(e))
-	if s == "" {
-		return fmt.Sprintf("rados: ret=%d", errno)
-	}
-	return fmt.Sprintf("rados: ret=%d, %s", errno, s)
-}
-
 const (
 	// AllNamespaces is used to reset a selected namespace to all
 	// namespaces. See the IOContext SetNamespace function.
 	AllNamespaces = C.LIBRADOS_ALL_NSPACES
 
-	// ErrNotFound indicates a missing resource.
-	ErrNotFound = RadosError(-C.ENOENT)
-	// ErrPermissionDenied indicates a permissions issue.
-	ErrPermissionDenied = RadosError(-C.EPERM)
-	// ErrObjectExists indicates that an exclusive object creation failed.
-	ErrObjectExists = RadosError(-C.EEXIST)
-
 	// FIXME: for backwards compatibility
 
 	// RadosAllNamespaces is used to reset a selected namespace to all
@@ -49,23 +23,8 @@ const (
 	//
 	// Deprecated: use AllNamespaces instead
 	RadosAllNamespaces = AllNamespaces
-	// RadosErrorNotFound indicates a missing resource.
-	//
-	// Deprecated: use ErrNotFound instead
-	RadosErrorNotFound = ErrNotFound
-	// RadosErrorPermissionDenied indicates a permissions issue.
-	//
-	// Deprecated: use ErrPermissionDenied instead
-	RadosErrorPermissionDenied = ErrPermissionDenied
 )
 
-func getRadosError(err int) error {
-	if err == 0 {
-		return nil
-	}
-	return RadosError(err)
-}
-
 // Version returns the major, minor, and patch components of the version of
 // the RADOS library linked against.
 func Version() (int, int, int) {
@@ -83,7 +42,7 @@ func newConn(user *C.char) (*Conn, error) {
 	ret := C.rados_create(&conn.cluster, user)
 
 	if ret != 0 {
-		return nil, RadosError(int(ret))
+		return nil, getError(ret)
 	}
 
 	runtime.SetFinalizer(conn, freeConn)
@@ -116,7 +75,7 @@ func NewConnWithClusterAndUser(clusterName string, userName string) (*Conn, erro
 	conn := makeConn()
 	ret := C.rados_create2(&conn.cluster, c_cluster_name, c_name, 0)
 	if ret != 0 {
-		return nil, RadosError(int(ret))
+		return nil, getError(ret)
 	}
 
 	runtime.SetFinalizer(conn, freeConn)
diff --git a/rbd/errors.go b/rbd/errors.go
new file mode 100644
index 0000000..1e6022e
--- /dev/null
+++ b/rbd/errors.go
@@ -0,0 +1,63 @@
+package rbd
+
+/*
+#include <errno.h>
+*/
+import "C"
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/ceph/go-ceph/internal/errutil"
+)
+
+// revive:disable:exported Temporarily live with stuttering
+
+// RBDError represents an error condition returned from the librbd APIs.
+type RBDError int
+
+// revive:enable:exported
+
+func (e RBDError) Error() string {
+	errno, s := errutil.FormatErrno(int(e))
+	if s == "" {
+		return fmt.Sprintf("rbd: ret=%d", errno)
+	}
+	return fmt.Sprintf("rbd: ret=%d, %s", errno, s)
+}
+
+func getError(err C.int) error {
+	if err != 0 {
+		if err == -C.ENOENT {
+			return ErrNotFound
+		}
+		return RBDError(err)
+	} else {
+		return nil
+	}
+}
+
+// Public go errors:
+
+var (
+	// ErrNoIOContext may be returned if an api call requires an IOContext and
+	// it is not provided.
+	ErrNoIOContext = errors.New("RBD image does not have an IOContext")
+	// ErrNoName may be returned if an api call requires a name and it is
+	// not provided.
+	ErrNoName = errors.New("RBD image does not have a name")
+	// ErrSnapshotNoName may be returned if an aip call requires a snapshot
+	// name and it is not provided.
+	ErrSnapshotNoName = errors.New("RBD snapshot does not have a name")
+	// ErrImageNotOpen may be returnened if an api call requires an open image handle and one is not provided.
+	ErrImageNotOpen = errors.New("RBD image not open")
+	// ErrNotFound may be returned from an api call when the requested item is
+	// missing.
+	ErrNotFound = errors.New("RBD image not found")
+
+	// revive:disable:exported for compatibility with old versions
+	RbdErrorImageNotOpen = ErrImageNotOpen
+	RbdErrorNotFound     = ErrNotFound
+	// revive:enable:exported
+)
diff --git a/rbd/rbd.go b/rbd/rbd.go
index 02d6c0e..df5da19 100644
--- a/rbd/rbd.go
+++ b/rbd/rbd.go
@@ -13,12 +13,10 @@ import "C"
 import (
 	"bytes"
 	"errors"
-	"fmt"
 	"io"
 	"time"
 	"unsafe"
 
-	"github.com/ceph/go-ceph/internal/errutil"
 	"github.com/ceph/go-ceph/rados"
 )
 
@@ -45,35 +43,6 @@ const (
 	NoSnapshot = ""
 )
 
-// revive:disable:exported Temporarily live with stuttering
-
-// RBDError represents an error condition returned from the librbd APIs.
-type RBDError int
-
-// revive:enable:exported
-
-var (
-	// ErrNoIOContext may be returned if an api call requires an IOContext and
-	// it is not provided.
-	ErrNoIOContext = errors.New("RBD image does not have an IOContext")
-	// ErrNoName may be returned if an api call requires a name and it is
-	// not provided.
-	ErrNoName = errors.New("RBD image does not have a name")
-	// ErrSnapshotNoName may be returned if an aip call requires a snapshot
-	// name and it is not provided.
-	ErrSnapshotNoName = errors.New("RBD snapshot does not have a name")
-	// ErrImageNotOpen may be returnened if an api call requires an open image handle and one is not provided.
-	ErrImageNotOpen = errors.New("RBD image not open")
-	// ErrNotFound may be returned from an api call when the requested item is
-	// missing.
-	ErrNotFound = errors.New("RBD image not found")
-
-	// revive:disable:exported for compatibility with old versions
-	RbdErrorImageNotOpen = ErrImageNotOpen
-	RbdErrorNotFound     = ErrNotFound
-	// revive:enable:exported
-)
-
 // ImageInfo represents the status information for an image.
 type ImageInfo struct {
 	Size              uint64
@@ -156,25 +125,6 @@ func (image *Image) validate(req uint32) error {
 	return nil
 }
 
-func (e RBDError) Error() string {
-	errno, s := errutil.FormatErrno(int(e))
-	if s == "" {
-		return fmt.Sprintf("rbd: ret=%d", errno)
-	}
-	return fmt.Sprintf("rbd: ret=%d, %s", errno, s)
-}
-
-func getError(err C.int) error {
-	if err != 0 {
-		if err == -C.ENOENT {
-			return ErrNotFound
-		}
-		return RBDError(err)
-	} else {
-		return nil
-	}
-}
-
 // Version returns the major, minor, and patch level of the librbd library.
 func Version() (int, int, int) {
 	var c_major, c_minor, c_patch C.int


Test patch:
diff --git a/cephfs/cephfs_test.go b/cephfs/cephfs_test.go
index 1947735..6e82dfc 100644
--- a/cephfs/cephfs_test.go
+++ b/cephfs/cephfs_test.go
@@ -256,19 +256,6 @@ func TestChown(t *testing.T) {
 
 }
 
-func TestCephFSError(t *testing.T) {
-	err := getError(0)
-	assert.NoError(t, err)
-
-	err = getError(-5) // IO error
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "cephfs: ret=5, Input/output error")
-
-	err = getError(345) // no such errno
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "cephfs: ret=345")
-}
-
 func radosConnect(t *testing.T) *rados.Conn {
 	conn, err := rados.NewConn()
 	require.NoError(t, err)
diff --git a/cephfs/errors_test.go b/cephfs/errors_test.go
new file mode 100644
index 0000000..147d38e
--- /dev/null
+++ b/cephfs/errors_test.go
@@ -0,0 +1,20 @@
+package cephfs
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCephFSError(t *testing.T) {
+	err := getError(0)
+	assert.NoError(t, err)
+
+	err = getError(-5) // IO error
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "cephfs: ret=5, Input/output error")
+
+	err = getError(345) // no such errno
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "cephfs: ret=345")
+}
diff --git a/rados/errors_test.go b/rados/errors_test.go
new file mode 100644
index 0000000..427b517
--- /dev/null
+++ b/rados/errors_test.go
@@ -0,0 +1,20 @@
+package rados
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRadosError(t *testing.T) {
+	err := getError(0)
+	assert.NoError(t, err)
+
+	err = getError(-5) // IO error
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "rados: ret=5, Input/output error")
+
+	err = getError(345) // no such errno
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "rados: ret=345")
+}
diff --git a/rados/rados_test.go b/rados/rados_test.go
index e7be822..85c8ef0 100644
--- a/rados/rados_test.go
+++ b/rados/rados_test.go
@@ -1200,16 +1200,3 @@ func (suite *RadosTestSuite) TestOpenIOContextInvalidPool() {
 func TestRadosTestSuite(t *testing.T) {
 	suite.Run(t, new(RadosTestSuite))
 }
-
-func TestRadosError(t *testing.T) {
-	err := getRadosError(0)
-	assert.NoError(t, err)
-
-	err = getRadosError(-5) // IO error
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "rados: ret=5, Input/output error")
-
-	err = getRadosError(345) // no such errno
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "rados: ret=345")
-}
diff --git a/rbd/errors_test.go b/rbd/errors_test.go
new file mode 100644
index 0000000..043c3a1
--- /dev/null
+++ b/rbd/errors_test.go
@@ -0,0 +1,20 @@
+package rbd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRBDError(t *testing.T) {
+	err := getError(0)
+	assert.NoError(t, err)
+
+	err = getError(-39) // NOTEMPTY (image still has a snapshot)
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "rbd: ret=39, Directory not empty")
+
+	err = getError(345) // no such errno
+	assert.Error(t, err)
+	assert.Equal(t, err.Error(), "rbd: ret=345")
+}
diff --git a/rbd/rbd_test.go b/rbd/rbd_test.go
index 5e96039..3e31919 100644
--- a/rbd/rbd_test.go
+++ b/rbd/rbd_test.go
@@ -24,19 +24,6 @@ func GetUUID() string {
 	return uuid.Must(uuid.NewV4()).String()
 }
 
-func TestRBDError(t *testing.T) {
-	err := getError(0)
-	assert.NoError(t, err)
-
-	err = getError(-39) // NOTEMPTY (image still has a snapshot)
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "rbd: ret=39, Directory not empty")
-
-	err = getError(345) // no such errno
-	assert.Error(t, err)
-	assert.Equal(t, err.Error(), "rbd: ret=345")
-}
-
 func TestVersion(t *testing.T) {
 	var major, minor, patch = Version()
 	assert.False(t, major < 0 || major > 1000, "invalid major")