Merge pull request #443 from gierschv/driver-rados
Storage Driver: Ceph Object Storage (RADOS)master
						commit
						318af0b1ce
					
				|  | @ -83,6 +83,11 @@ | |||
| 			"ImportPath": "github.com/stevvooe/resumable", | ||||
| 			"Rev": "51ad44105773cafcbe91927f70ac68e1bf78f8b4" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"ImportPath": "github.com/noahdesu/go-ceph/rados", | ||||
| 			"Comment": "v.0.3.0-29-gb15639c", | ||||
| 			"Rev": "b15639c44c05368348355229070361395d9152ee" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"ImportPath": "github.com/yvasiyarov/go-metrics", | ||||
| 			"Rev": "57bccd1ccd43f94bb17fdd8bf3007059b802f85e" | ||||
|  |  | |||
|  | @ -0,0 +1,300 @@ | |||
| package rados | ||||
| 
 | ||||
| // #cgo LDFLAGS: -lrados
 | ||||
| // #include <stdlib.h>
 | ||||
| // #include <rados/librados.h>
 | ||||
| import "C" | ||||
| 
 | ||||
| import "unsafe" | ||||
| import "bytes" | ||||
| 
 | ||||
| // ClusterStat represents Ceph cluster statistics.
 | ||||
| type ClusterStat struct { | ||||
| 	Kb          uint64 | ||||
| 	Kb_used     uint64 | ||||
| 	Kb_avail    uint64 | ||||
| 	Num_objects uint64 | ||||
| } | ||||
| 
 | ||||
| // Conn is a connection handle to a Ceph cluster.
 | ||||
| type Conn struct { | ||||
| 	cluster C.rados_t | ||||
| } | ||||
| 
 | ||||
| // PingMonitor sends a ping to a monitor and returns the reply.
 | ||||
| func (c *Conn) PingMonitor(id string) (string, error) { | ||||
| 	c_id := C.CString(id) | ||||
| 	defer C.free(unsafe.Pointer(c_id)) | ||||
| 
 | ||||
| 	var strlen C.size_t | ||||
| 	var strout *C.char | ||||
| 
 | ||||
| 	ret := C.rados_ping_monitor(c.cluster, c_id, &strout, &strlen) | ||||
| 	defer C.rados_buffer_free(strout) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		reply := C.GoStringN(strout, (C.int)(strlen)) | ||||
| 		return reply, nil | ||||
| 	} else { | ||||
| 		return "", RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Connect establishes a connection to a RADOS cluster. It returns an error,
 | ||||
| // if any.
 | ||||
| func (c *Conn) Connect() error { | ||||
| 	ret := C.rados_connect(c.cluster) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Shutdown disconnects from the cluster.
 | ||||
| func (c *Conn) Shutdown() { | ||||
| 	C.rados_shutdown(c.cluster) | ||||
| } | ||||
| 
 | ||||
| // ReadConfigFile configures the connection using a Ceph configuration file.
 | ||||
| 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) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(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) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| func (c *Conn) OpenIOContext(pool string) (*IOContext, error) { | ||||
| 	c_pool := C.CString(pool) | ||||
| 	defer C.free(unsafe.Pointer(c_pool)) | ||||
| 	ioctx := &IOContext{} | ||||
| 	ret := C.rados_ioctx_create(c.cluster, c_pool, &ioctx.ioctx) | ||||
| 	if ret == 0 { | ||||
| 		return ioctx, nil | ||||
| 	} else { | ||||
| 		return nil, RadosError(int(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)))) | ||||
| 		if ret < 0 { | ||||
| 			return nil, RadosError(int(ret)) | ||||
| 		} | ||||
| 
 | ||||
| 		if ret > len(buf) { | ||||
| 			buf = make([]byte, ret) | ||||
| 			continue | ||||
| 		} | ||||
| 
 | ||||
| 		tmp := bytes.SplitAfter(buf[:ret-1], []byte{0}) | ||||
| 		for _, s := range tmp { | ||||
| 			if len(s) > 0 { | ||||
| 				name := C.GoString((*C.char)(unsafe.Pointer(&s[0]))) | ||||
| 				names = append(names, name) | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		return names, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // SetConfigOption sets the value of the configuration option identified by
 | ||||
| // the given name.
 | ||||
| func (c *Conn) SetConfigOption(option, value string) error { | ||||
| 	c_opt, c_val := C.CString(option), C.CString(value) | ||||
| 	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) | ||||
| 	if ret < 0 { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetConfigOption returns the value of the Ceph configuration option
 | ||||
| // identified by the given name.
 | ||||
| func (c *Conn) GetConfigOption(name string) (value string, err error) { | ||||
| 	buf := make([]byte, 4096) | ||||
| 	c_name := C.CString(name) | ||||
| 	defer C.free(unsafe.Pointer(c_name)) | ||||
| 	ret := int(C.rados_conf_get(c.cluster, c_name, | ||||
| 		(*C.char)(unsafe.Pointer(&buf[0])), C.size_t(len(buf)))) | ||||
| 	// FIXME: ret may be -ENAMETOOLONG if the buffer is not large enough. We
 | ||||
| 	// can handle this case, but we need a reliable way to test for
 | ||||
| 	// -ENAMETOOLONG constant. Will the syscall/Errno stuff in Go help?
 | ||||
| 	if ret == 0 { | ||||
| 		value = C.GoString((*C.char)(unsafe.Pointer(&buf[0]))) | ||||
| 		return value, nil | ||||
| 	} else { | ||||
| 		return "", RadosError(ret) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // WaitForLatestOSDMap blocks the caller until the latest OSD map has been
 | ||||
| // retrieved.
 | ||||
| func (c *Conn) WaitForLatestOSDMap() error { | ||||
| 	ret := C.rados_wait_for_latest_osdmap(c.cluster) | ||||
| 	if ret < 0 { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetClusterStat returns statistics about the cluster associated with the
 | ||||
| // connection.
 | ||||
| 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)) | ||||
| 	} else { | ||||
| 		return ClusterStat{ | ||||
| 			Kb:          uint64(c_stat.kb), | ||||
| 			Kb_used:     uint64(c_stat.kb_used), | ||||
| 			Kb_avail:    uint64(c_stat.kb_avail), | ||||
| 			Num_objects: uint64(c_stat.num_objects), | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ParseCmdLineArgs configures the connection from command line arguments.
 | ||||
| func (c *Conn) ParseCmdLineArgs(args []string) error { | ||||
| 	// add an empty element 0 -- Ceph treats the array as the actual contents
 | ||||
| 	// of argv and skips the first element (the executable name)
 | ||||
| 	argc := C.int(len(args) + 1) | ||||
| 	argv := make([]*C.char, argc) | ||||
| 
 | ||||
| 	// make the first element a string just in case it is ever examined
 | ||||
| 	argv[0] = C.CString("placeholder") | ||||
| 	defer C.free(unsafe.Pointer(argv[0])) | ||||
| 
 | ||||
| 	for i, arg := range args { | ||||
| 		argv[i+1] = C.CString(arg) | ||||
| 		defer C.free(unsafe.Pointer(argv[i+1])) | ||||
| 	} | ||||
| 
 | ||||
| 	ret := C.rados_conf_parse_argv(c.cluster, argc, &argv[0]) | ||||
| 	if ret < 0 { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} else { | ||||
| 		return nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // 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) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(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)))) | ||||
| 	// 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 | ||||
| 	} else { | ||||
| 		return "", RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetInstanceID returns a globally unique identifier for the cluster
 | ||||
| // connection instance.
 | ||||
| func (c *Conn) GetInstanceID() uint64 { | ||||
| 	// FIXME: are there any error cases for this?
 | ||||
| 	return uint64(C.rados_get_instance_id(c.cluster)) | ||||
| } | ||||
| 
 | ||||
| // MakePool creates a new pool with default settings.
 | ||||
| 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)) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(ret) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // DeletePool deletes a pool and all the data inside the pool.
 | ||||
| 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)) | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(ret) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // MonCommand sends a command to one of the monitors
 | ||||
| func (c *Conn) MonCommand(args []byte) (buffer []byte, info string, err error) { | ||||
| 	argv := make([]*C.char, len(args)) | ||||
| 	for i, _ := range args { | ||||
| 		argv[i] = (*C.char)(unsafe.Pointer(&args[i])) | ||||
| 	} | ||||
| 
 | ||||
| 	var ( | ||||
| 		outs, outbuf       *C.char | ||||
| 		outslen, outbuflen C.size_t | ||||
| 	) | ||||
| 	inbuf := C.CString("") | ||||
| 	defer C.free(unsafe.Pointer(inbuf)) | ||||
| 
 | ||||
| 	ret := C.rados_mon_command(c.cluster, | ||||
| 		&argv[0], C.size_t(len(args)), | ||||
| 		inbuf,       // bulk input (e.g. crush map)
 | ||||
| 		C.size_t(0), // length inbuf
 | ||||
| 		&outbuf,     // buffer
 | ||||
| 		&outbuflen,  // buffer length
 | ||||
| 		&outs,       // status string
 | ||||
| 		&outslen) | ||||
| 
 | ||||
| 	if outslen > 0 { | ||||
| 		info = C.GoStringN(outs, C.int(outslen)) | ||||
| 		C.free(unsafe.Pointer(outs)) | ||||
| 	} | ||||
| 	if outbuflen > 0 { | ||||
| 		buffer = C.GoBytes(unsafe.Pointer(outbuf), C.int(outbuflen)) | ||||
| 		C.free(unsafe.Pointer(outbuf)) | ||||
| 	} | ||||
| 	if ret != 0 { | ||||
| 		err = RadosError(int(ret)) | ||||
| 		return nil, info, err | ||||
| 	} | ||||
| 
 | ||||
| 	return | ||||
| } | ||||
|  | @ -0,0 +1,4 @@ | |||
| /* | ||||
| Set of wrappers around librados API. | ||||
| */ | ||||
| package rados | ||||
|  | @ -0,0 +1,547 @@ | |||
| package rados | ||||
| 
 | ||||
| // #cgo LDFLAGS: -lrados
 | ||||
| // #include <stdlib.h>
 | ||||
| // #include <rados/librados.h>
 | ||||
| import "C" | ||||
| 
 | ||||
| import "unsafe" | ||||
| import "time" | ||||
| 
 | ||||
| // PoolStat represents Ceph pool statistics.
 | ||||
| type PoolStat struct { | ||||
| 	// space used in bytes
 | ||||
| 	Num_bytes uint64 | ||||
| 	// space used in KB
 | ||||
| 	Num_kb uint64 | ||||
| 	// number of objects in the pool
 | ||||
| 	Num_objects uint64 | ||||
| 	// number of clones of objects
 | ||||
| 	Num_object_clones uint64 | ||||
| 	// num_objects * num_replicas
 | ||||
| 	Num_object_copies              uint64 | ||||
| 	Num_objects_missing_on_primary uint64 | ||||
| 	// number of objects found on no OSDs
 | ||||
| 	Num_objects_unfound uint64 | ||||
| 	// number of objects replicated fewer times than they should be
 | ||||
| 	// (but found on at least one OSD)
 | ||||
| 	Num_objects_degraded uint64 | ||||
| 	Num_rd               uint64 | ||||
| 	Num_rd_kb            uint64 | ||||
| 	Num_wr               uint64 | ||||
| 	Num_wr_kb            uint64 | ||||
| } | ||||
| 
 | ||||
| // ObjectStat represents an object stat information
 | ||||
| type ObjectStat struct { | ||||
| 	// current length in bytes
 | ||||
| 	Size        uint64 | ||||
| 	// last modification time
 | ||||
| 	ModTime     time.Time | ||||
| } | ||||
| 
 | ||||
| // IOContext represents a context for performing I/O within a pool.
 | ||||
| type IOContext struct { | ||||
| 	ioctx C.rados_ioctx_t | ||||
| } | ||||
| 
 | ||||
| // Pointer returns a uintptr representation of the IOContext.
 | ||||
| func (ioctx *IOContext) Pointer() uintptr { | ||||
| 	return uintptr(ioctx.ioctx) | ||||
| } | ||||
| 
 | ||||
| // Write writes len(data) bytes to the object with key oid starting at byte
 | ||||
| // offset offset. It returns an error, if any.
 | ||||
| func (ioctx *IOContext) Write(oid string, data []byte, offset uint64) error { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
| 	ret := C.rados_write(ioctx.ioctx, c_oid, | ||||
| 		(*C.char)(unsafe.Pointer(&data[0])), | ||||
| 		(C.size_t)(len(data)), | ||||
| 		(C.uint64_t)(offset)) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Read reads up to len(data) bytes from the object with key oid starting at byte
 | ||||
| // offset offset. It returns the number of bytes read and an error, if any.
 | ||||
| func (ioctx *IOContext) Read(oid string, data []byte, offset uint64) (int, error) { | ||||
| 	if len(data) == 0 { | ||||
| 		return 0, nil | ||||
| 	} | ||||
| 
 | ||||
| 	c_oid := C.CString(oid) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
| 	ret := C.rados_read( | ||||
| 		ioctx.ioctx, | ||||
| 		c_oid, | ||||
| 		(*C.char)(unsafe.Pointer(&data[0])), | ||||
| 		(C.size_t)(len(data)), | ||||
| 		(C.uint64_t)(offset)) | ||||
| 
 | ||||
| 	if ret >= 0 { | ||||
| 		return int(ret), nil | ||||
| 	} else { | ||||
| 		return 0, RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Delete deletes the object with key oid. It returns an error, if any.
 | ||||
| func (ioctx *IOContext) Delete(oid string) error { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
| 	ret := C.rados_remove(ioctx.ioctx, c_oid) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Truncate resizes the object with key oid to size size. If the operation
 | ||||
| // enlarges the object, the new area is logically filled with zeroes. If the
 | ||||
| // operation shrinks the object, the excess data is removed. It returns an
 | ||||
| // error, if any.
 | ||||
| func (ioctx *IOContext) Truncate(oid string, size uint64) error { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
| 	ret := C.rados_trunc(ioctx.ioctx, c_oid, (C.uint64_t)(size)) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Destroy informs librados that the I/O context is no longer in use.
 | ||||
| // Resources associated with the context may not be freed immediately, and the
 | ||||
| // context should not be used again after calling this method.
 | ||||
| func (ioctx *IOContext) Destroy() { | ||||
| 	C.rados_ioctx_destroy(ioctx.ioctx) | ||||
| } | ||||
| 
 | ||||
| // Stat returns a set of statistics about the pool associated with this I/O
 | ||||
| // context.
 | ||||
| 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{}, RadosError(int(ret)) | ||||
| 	} else { | ||||
| 		return PoolStat{ | ||||
| 			Num_bytes:                      uint64(c_stat.num_bytes), | ||||
| 			Num_kb:                         uint64(c_stat.num_kb), | ||||
| 			Num_objects:                    uint64(c_stat.num_objects), | ||||
| 			Num_object_clones:              uint64(c_stat.num_object_clones), | ||||
| 			Num_object_copies:              uint64(c_stat.num_object_copies), | ||||
| 			Num_objects_missing_on_primary: uint64(c_stat.num_objects_missing_on_primary), | ||||
| 			Num_objects_unfound:            uint64(c_stat.num_objects_unfound), | ||||
| 			Num_objects_degraded:           uint64(c_stat.num_objects_degraded), | ||||
| 			Num_rd:                         uint64(c_stat.num_rd), | ||||
| 			Num_rd_kb:                      uint64(c_stat.num_rd_kb), | ||||
| 			Num_wr:                         uint64(c_stat.num_wr), | ||||
| 			Num_wr_kb:                      uint64(c_stat.num_wr_kb), | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetPoolName returns the name of the pool associated with the I/O context.
 | ||||
| func (ioctx *IOContext) GetPoolName() (name string, err error) { | ||||
| 	buf := make([]byte, 128) | ||||
| 	for { | ||||
| 		ret := C.rados_ioctx_get_pool_name(ioctx.ioctx, | ||||
| 			(*C.char)(unsafe.Pointer(&buf[0])), C.unsigned(len(buf))) | ||||
| 		if ret == -34 { // FIXME
 | ||||
| 			buf = make([]byte, len(buf)*2) | ||||
| 			continue | ||||
| 		} else if ret < 0 { | ||||
| 			return "", RadosError(ret) | ||||
| 		} | ||||
| 		name = C.GoStringN((*C.char)(unsafe.Pointer(&buf[0])), ret) | ||||
| 		return name, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // ObjectListFunc is the type of the function called for each object visited
 | ||||
| // by ListObjects.
 | ||||
| type ObjectListFunc func(oid string) | ||||
| 
 | ||||
| // ListObjects lists all of the objects in the pool associated with the I/O
 | ||||
| // context, and called the provided listFn function for each object, passing
 | ||||
| // to the function the name of the object.
 | ||||
| func (ioctx *IOContext) ListObjects(listFn ObjectListFunc) error { | ||||
| 	var ctx C.rados_list_ctx_t | ||||
| 	ret := C.rados_objects_list_open(ioctx.ioctx, &ctx) | ||||
| 	if ret < 0 { | ||||
| 		return RadosError(ret) | ||||
| 	} | ||||
| 	defer func() { C.rados_objects_list_close(ctx) }() | ||||
| 
 | ||||
| 	for { | ||||
| 		var c_entry *C.char | ||||
| 		ret := C.rados_objects_list_next(ctx, &c_entry, nil) | ||||
| 		if ret == -2 { // FIXME
 | ||||
| 			return nil | ||||
| 		} else if ret < 0 { | ||||
| 			return RadosError(ret) | ||||
| 		} | ||||
| 		listFn(C.GoString(c_entry)) | ||||
| 	} | ||||
| 
 | ||||
| 	panic("invalid state") | ||||
| } | ||||
| 
 | ||||
| // Stat returns the size of the object and its last modification time
 | ||||
| func (ioctx *IOContext) Stat(object string) (stat ObjectStat, err error) { | ||||
| 	var c_psize C.uint64_t | ||||
| 	var c_pmtime C.time_t | ||||
| 	c_object := C.CString(object) | ||||
| 	defer C.free(unsafe.Pointer(c_object)) | ||||
| 
 | ||||
| 	ret := C.rados_stat( | ||||
| 		ioctx.ioctx, | ||||
| 		c_object, | ||||
| 		&c_psize, | ||||
| 		&c_pmtime) | ||||
| 
 | ||||
| 	if ret < 0 { | ||||
| 		return ObjectStat{}, RadosError(int(ret)) | ||||
| 	} else { | ||||
| 		return ObjectStat{ | ||||
| 			Size: uint64(c_psize), | ||||
| 			ModTime: time.Unix(int64(c_pmtime), 0), | ||||
| 		}, nil | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // GetXattr gets an xattr with key `name`, it returns the length of
 | ||||
| // the key read or an error if not successful
 | ||||
| func (ioctx *IOContext) GetXattr(object string, name string, data []byte) (int, error) { | ||||
| 	c_object := C.CString(object) | ||||
| 	c_name := C.CString(name) | ||||
| 	defer C.free(unsafe.Pointer(c_object)) | ||||
| 	defer C.free(unsafe.Pointer(c_name)) | ||||
| 
 | ||||
| 	ret := C.rados_getxattr( | ||||
| 		ioctx.ioctx, | ||||
| 		c_object, | ||||
| 		c_name, | ||||
| 		(*C.char)(unsafe.Pointer(&data[0])), | ||||
| 		(C.size_t)(len(data))) | ||||
| 
 | ||||
| 	if ret >= 0 { | ||||
| 		return int(ret), nil | ||||
| 	} else { | ||||
| 		return 0, RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Sets an xattr for an object with key `name` with value as `data`
 | ||||
| func (ioctx *IOContext) SetXattr(object string, name string, data []byte) error { | ||||
| 	c_object := C.CString(object) | ||||
| 	c_name := C.CString(name) | ||||
| 	defer C.free(unsafe.Pointer(c_object)) | ||||
| 	defer C.free(unsafe.Pointer(c_name)) | ||||
| 
 | ||||
| 	ret := C.rados_setxattr( | ||||
| 		ioctx.ioctx, | ||||
| 		c_object, | ||||
| 		c_name, | ||||
| 		(*C.char)(unsafe.Pointer(&data[0])), | ||||
| 		(C.size_t)(len(data))) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // function that lists all the xattrs for an object, since xattrs are
 | ||||
| // a k-v pair, this function returns a map of k-v pairs on
 | ||||
| // success, error code on failure
 | ||||
| func (ioctx *IOContext) ListXattrs(oid string) (map[string][]byte, error) { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
| 	var it C.rados_xattrs_iter_t | ||||
| 
 | ||||
| 	ret := C.rados_getxattrs(ioctx.ioctx, c_oid, &it) | ||||
| 	if ret < 0 { | ||||
| 		return nil, RadosError(ret) | ||||
| 	} | ||||
| 	defer func() { C.rados_getxattrs_end(it) }() | ||||
| 	m := make(map[string][]byte) | ||||
| 	for { | ||||
| 		var c_name, c_val *C.char | ||||
| 		var c_len C.size_t | ||||
| 		defer C.free(unsafe.Pointer(c_name)) | ||||
| 		defer C.free(unsafe.Pointer(c_val)) | ||||
| 
 | ||||
| 		ret := C.rados_getxattrs_next(it, &c_name, &c_val, &c_len) | ||||
| 		if ret < 0 { | ||||
| 			return nil, RadosError(int(ret)) | ||||
| 		} | ||||
| 		// rados api returns a null name,val & 0-length upon
 | ||||
| 		// end of iteration
 | ||||
| 		if c_name == nil { | ||||
| 			return m, nil // stop iteration
 | ||||
| 		} | ||||
| 		m[C.GoString(c_name)] = C.GoBytes(unsafe.Pointer(c_val), (C.int)(c_len)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Remove an xattr with key `name` from object `oid`
 | ||||
| func (ioctx *IOContext) RmXattr(oid string, name string) error { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	c_name := C.CString(name) | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 	defer C.free(unsafe.Pointer(c_name)) | ||||
| 
 | ||||
| 	ret := C.rados_rmxattr( | ||||
| 		ioctx.ioctx, | ||||
| 		c_oid, | ||||
| 		c_name) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return nil | ||||
| 	} else { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Append the map `pairs` to the omap `oid`
 | ||||
| func (ioctx *IOContext) SetOmap(oid string, pairs map[string][]byte) error { | ||||
|         c_oid := C.CString(oid) | ||||
|         defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
|         var s C.size_t | ||||
|         var c *C.char | ||||
|         ptrSize := unsafe.Sizeof(c) | ||||
| 
 | ||||
|         c_keys := C.malloc(C.size_t(len(pairs)) * C.size_t(ptrSize)) | ||||
|         c_values := C.malloc(C.size_t(len(pairs)) * C.size_t(ptrSize)) | ||||
|         c_lengths := C.malloc(C.size_t(len(pairs)) * C.size_t(unsafe.Sizeof(s))) | ||||
| 
 | ||||
|         defer C.free(unsafe.Pointer(c_keys)) | ||||
|         defer C.free(unsafe.Pointer(c_values)) | ||||
|         defer C.free(unsafe.Pointer(c_lengths)) | ||||
| 
 | ||||
|         i := 0 | ||||
|         for key, value := range pairs { | ||||
|             // key
 | ||||
|             c_key_ptr := (**C.char)(unsafe.Pointer(uintptr(c_keys) + uintptr(i) * ptrSize)) | ||||
|             *c_key_ptr = C.CString(key) | ||||
|             defer C.free(unsafe.Pointer(*c_key_ptr)) | ||||
| 
 | ||||
|             // value and its length
 | ||||
|             c_value_ptr := (**C.char)(unsafe.Pointer(uintptr(c_values) + uintptr(i) * ptrSize)) | ||||
| 
 | ||||
|             var c_length C.size_t | ||||
|             if len(value) > 0 { | ||||
|                 *c_value_ptr = (*C.char)(unsafe.Pointer(&value[0])) | ||||
|                 c_length = C.size_t(len(value)) | ||||
|             } else { | ||||
|                 *c_value_ptr = nil | ||||
|                 c_length = C.size_t(0) | ||||
|             } | ||||
| 
 | ||||
|             c_length_ptr := (*C.size_t)(unsafe.Pointer(uintptr(c_lengths) + uintptr(i) * ptrSize)) | ||||
|             *c_length_ptr = c_length | ||||
| 
 | ||||
|             i++ | ||||
|         } | ||||
| 
 | ||||
|         op := C.rados_create_write_op() | ||||
|         C.rados_write_op_omap_set( | ||||
|                 op, | ||||
|                 (**C.char)(c_keys), | ||||
|                 (**C.char)(c_values), | ||||
|                 (*C.size_t)(c_lengths), | ||||
|                 C.size_t(len(pairs))) | ||||
| 
 | ||||
|         ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0) | ||||
|         C.rados_release_write_op(op) | ||||
| 
 | ||||
|         if ret == 0 { | ||||
|                 return nil | ||||
|         } else { | ||||
|                 return RadosError(int(ret)) | ||||
|         } | ||||
| } | ||||
| 
 | ||||
| // OmapListFunc is the type of the function called for each omap key
 | ||||
| // visited by ListOmapValues
 | ||||
| type OmapListFunc func(key string, value []byte) | ||||
| 
 | ||||
| // Iterate on a set of keys and their values from an omap
 | ||||
| // `startAfter`: iterate only on the keys after this specified one
 | ||||
| // `filterPrefix`: iterate only on the keys beginning with this prefix
 | ||||
| // `maxReturn`: iterate no more than `maxReturn` key/value pairs
 | ||||
| // `listFn`: the function called at each iteration
 | ||||
| func (ioctx *IOContext) ListOmapValues(oid string, startAfter string, filterPrefix string, maxReturn int64, listFn OmapListFunc) error { | ||||
| 	c_oid := C.CString(oid) | ||||
| 	c_start_after := C.CString(startAfter) | ||||
| 	c_filter_prefix := C.CString(filterPrefix) | ||||
| 	c_max_return := C.uint64_t(maxReturn) | ||||
| 
 | ||||
| 	defer C.free(unsafe.Pointer(c_oid)) | ||||
| 	defer C.free(unsafe.Pointer(c_start_after)) | ||||
| 	defer C.free(unsafe.Pointer(c_filter_prefix)) | ||||
| 
 | ||||
| 	op := C.rados_create_read_op() | ||||
| 
 | ||||
| 	var c_iter C.rados_omap_iter_t | ||||
| 	var c_prval C.int | ||||
| 	C.rados_read_op_omap_get_vals( | ||||
| 		op, | ||||
| 		c_start_after, | ||||
| 		c_filter_prefix, | ||||
| 		c_max_return, | ||||
| 		&c_iter, | ||||
| 		&c_prval, | ||||
| 	) | ||||
| 
 | ||||
| 	ret := C.rados_read_op_operate(op, ioctx.ioctx, c_oid, 0) | ||||
| 
 | ||||
| 	if int(c_prval) != 0 { | ||||
| 		return RadosError(int(c_prval)) | ||||
| 	} else if int(ret) != 0 { | ||||
| 		return RadosError(int(ret)) | ||||
| 	} | ||||
| 
 | ||||
| 	for { | ||||
| 		var c_key *C.char | ||||
| 		var c_val *C.char | ||||
| 		var c_len C.size_t | ||||
| 
 | ||||
| 		ret = C.rados_omap_get_next(c_iter, &c_key, &c_val, &c_len) | ||||
| 
 | ||||
| 		if int(ret) != 0 { | ||||
| 			return RadosError(int(ret)) | ||||
| 		} | ||||
| 
 | ||||
| 		if c_key == nil { | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		listFn(C.GoString(c_key), C.GoBytes(unsafe.Pointer(c_val), C.int(c_len))) | ||||
| 	} | ||||
| 
 | ||||
| 	C.rados_omap_get_end(c_iter) | ||||
| 	C.rados_release_read_op(op) | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Fetch a set of keys and their values from an omap and returns then as a map
 | ||||
| // `startAfter`: retrieve only the keys after this specified one
 | ||||
| // `filterPrefix`: retrieve only the keys beginning with this prefix
 | ||||
| // `maxReturn`: retrieve no more than `maxReturn` key/value pairs
 | ||||
| func (ioctx *IOContext) GetOmapValues(oid string, startAfter string, filterPrefix string, maxReturn int64) (map[string][]byte, error) { | ||||
| 	omap := map[string][]byte{} | ||||
| 
 | ||||
| 	err := ioctx.ListOmapValues( | ||||
| 		oid, startAfter, filterPrefix, maxReturn, | ||||
| 		func(key string, value []byte) { | ||||
| 			omap[key] = value | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	return omap, err | ||||
| } | ||||
| 
 | ||||
| // Fetch all the keys and their values from an omap and returns then as a map
 | ||||
| // `startAfter`: retrieve only the keys after this specified one
 | ||||
| // `filterPrefix`: retrieve only the keys beginning with this prefix
 | ||||
| // `iteratorSize`: internal number of keys to fetch during a read operation
 | ||||
| func (ioctx *IOContext) GetAllOmapValues(oid string, startAfter string, filterPrefix string, iteratorSize int64) (map[string][]byte, error) { | ||||
| 	omap := map[string][]byte{} | ||||
| 	omapSize := 0 | ||||
| 
 | ||||
| 	for { | ||||
| 		err := ioctx.ListOmapValues( | ||||
| 			oid, startAfter, filterPrefix, iteratorSize, | ||||
| 			func (key string, value []byte) { | ||||
| 				omap[key] = value | ||||
| 				startAfter = key | ||||
| 			}, | ||||
| 		) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return omap, err | ||||
| 		} | ||||
| 
 | ||||
| 		// End of omap
 | ||||
| 		if len(omap) == omapSize { | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		omapSize = len(omap) | ||||
| 	} | ||||
| 
 | ||||
| 	return omap, nil | ||||
| } | ||||
| 
 | ||||
| // Remove the specified `keys` from the omap `oid`
 | ||||
| func (ioctx *IOContext) RmOmapKeys(oid string, keys []string) error { | ||||
|         c_oid := C.CString(oid) | ||||
|         defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
|         var c *C.char | ||||
|         ptrSize := unsafe.Sizeof(c) | ||||
| 
 | ||||
|         c_keys := C.malloc(C.size_t(len(keys)) * C.size_t(ptrSize)) | ||||
|         defer C.free(unsafe.Pointer(c_keys)) | ||||
| 
 | ||||
|         i := 0 | ||||
|         for _, key := range keys { | ||||
|                 c_key_ptr := (**C.char)(unsafe.Pointer(uintptr(c_keys) + uintptr(i) * ptrSize)) | ||||
|                 *c_key_ptr = C.CString(key) | ||||
|                 defer C.free(unsafe.Pointer(*c_key_ptr)) | ||||
|                 i++ | ||||
|         } | ||||
| 
 | ||||
|         op := C.rados_create_write_op() | ||||
|         C.rados_write_op_omap_rm_keys( | ||||
|                 op, | ||||
|                 (**C.char)(c_keys), | ||||
|                 C.size_t(len(keys))) | ||||
| 
 | ||||
|         ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0) | ||||
|         C.rados_release_write_op(op) | ||||
| 
 | ||||
|         if ret == 0 { | ||||
|                 return nil | ||||
|         } else { | ||||
|                 return RadosError(int(ret)) | ||||
|         } | ||||
| } | ||||
| 
 | ||||
| // Clear the omap `oid`
 | ||||
| func (ioctx *IOContext) CleanOmap(oid string) error { | ||||
|         c_oid := C.CString(oid) | ||||
|         defer C.free(unsafe.Pointer(c_oid)) | ||||
| 
 | ||||
|         op := C.rados_create_write_op() | ||||
|         C.rados_write_op_omap_clear(op) | ||||
| 
 | ||||
|         ret := C.rados_write_op_operate(op, ioctx.ioctx, c_oid, nil, 0) | ||||
|         C.rados_release_write_op(op) | ||||
| 
 | ||||
|         if ret == 0 { | ||||
|                 return nil | ||||
|         } else { | ||||
|                 return RadosError(int(ret)) | ||||
|         } | ||||
| } | ||||
|  | @ -0,0 +1,54 @@ | |||
| package rados | ||||
| 
 | ||||
| // #cgo LDFLAGS: -lrados
 | ||||
| // #include <stdlib.h>
 | ||||
| // #include <rados/librados.h>
 | ||||
| import "C" | ||||
| 
 | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"unsafe" | ||||
| ) | ||||
| 
 | ||||
| type RadosError int | ||||
| 
 | ||||
| func (e RadosError) Error() string { | ||||
| 	return fmt.Sprintf("rados: ret=%d", e) | ||||
| } | ||||
| 
 | ||||
| // Version returns the major, minor, and patch components of the version of
 | ||||
| // the RADOS library linked against.
 | ||||
| func Version() (int, int, int) { | ||||
| 	var c_major, c_minor, c_patch C.int | ||||
| 	C.rados_version(&c_major, &c_minor, &c_patch) | ||||
| 	return int(c_major), int(c_minor), int(c_patch) | ||||
| } | ||||
| 
 | ||||
| // NewConn creates a new connection object. It returns the connection and an
 | ||||
| // error, if any.
 | ||||
| func NewConn() (*Conn, error) { | ||||
| 	conn := &Conn{} | ||||
| 	ret := C.rados_create(&conn.cluster, nil) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return conn, nil | ||||
| 	} else { | ||||
| 		return nil, RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // NewConnWithUser creates a new connection object with a custom username.
 | ||||
| // It returns the connection and an error, if any.
 | ||||
| func NewConnWithUser(user string) (*Conn, error) { | ||||
| 	c_user := C.CString(user) | ||||
| 	defer C.free(unsafe.Pointer(c_user)) | ||||
| 
 | ||||
| 	conn := &Conn{} | ||||
| 	ret := C.rados_create(&conn.cluster, c_user) | ||||
| 
 | ||||
| 	if ret == 0 { | ||||
| 		return conn, nil | ||||
| 	} else { | ||||
| 		return nil, RadosError(int(ret)) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										703
									
								
								Godeps/_workspace/src/github.com/noahdesu/go-ceph/rados/rados_test.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										703
									
								
								Godeps/_workspace/src/github.com/noahdesu/go-ceph/rados/rados_test.go
								
								
									generated
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,703 @@ | |||
| package rados_test | ||||
| 
 | ||||
| import "testing" | ||||
| 
 | ||||
| //import "bytes"
 | ||||
| import "github.com/noahdesu/go-ceph/rados" | ||||
| import "github.com/stretchr/testify/assert" | ||||
| import "os" | ||||
| import "os/exec" | ||||
| import "io" | ||||
| import "io/ioutil" | ||||
| import "time" | ||||
| import "net" | ||||
| import "fmt" | ||||
| import "sort" | ||||
| import "encoding/json" | ||||
| 
 | ||||
| func GetUUID() string { | ||||
| 	out, _ := exec.Command("uuidgen").Output() | ||||
| 	return string(out[:36]) | ||||
| } | ||||
| 
 | ||||
| func TestVersion(t *testing.T) { | ||||
| 	var major, minor, patch = rados.Version() | ||||
| 	assert.False(t, major < 0 || major > 1000, "invalid major") | ||||
| 	assert.False(t, minor < 0 || minor > 1000, "invalid minor") | ||||
| 	assert.False(t, patch < 0 || patch > 1000, "invalid patch") | ||||
| } | ||||
| 
 | ||||
| func TestGetSetConfigOption(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 
 | ||||
| 	// rejects invalid options
 | ||||
| 	err := conn.SetConfigOption("wefoijweojfiw", "welfkwjelkfj") | ||||
| 	assert.Error(t, err, "Invalid option") | ||||
| 
 | ||||
| 	// verify SetConfigOption changes a values
 | ||||
| 	log_file_val, err := conn.GetConfigOption("log_file") | ||||
| 	assert.NotEqual(t, log_file_val, "/dev/null") | ||||
| 
 | ||||
| 	err = conn.SetConfigOption("log_file", "/dev/null") | ||||
| 	assert.NoError(t, err, "Invalid option") | ||||
| 
 | ||||
| 	log_file_val, err = conn.GetConfigOption("log_file") | ||||
| 	assert.Equal(t, log_file_val, "/dev/null") | ||||
| } | ||||
| 
 | ||||
| func TestParseDefaultConfigEnv(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 
 | ||||
| 	log_file_val, _ := conn.GetConfigOption("log_file") | ||||
| 	assert.NotEqual(t, log_file_val, "/dev/null") | ||||
| 
 | ||||
| 	err := os.Setenv("CEPH_ARGS", "--log-file /dev/null") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	err = conn.ParseDefaultConfigEnv() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	log_file_val, _ = conn.GetConfigOption("log_file") | ||||
| 	assert.Equal(t, log_file_val, "/dev/null") | ||||
| } | ||||
| 
 | ||||
| func TestParseCmdLineArgs(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 
 | ||||
| 	mon_host_val, _ := conn.GetConfigOption("mon_host") | ||||
| 	assert.NotEqual(t, mon_host_val, "1.1.1.1") | ||||
| 
 | ||||
| 	args := []string{"--mon-host", "1.1.1.1"} | ||||
| 	err := conn.ParseCmdLineArgs(args) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	mon_host_val, _ = conn.GetConfigOption("mon_host") | ||||
| 	assert.Equal(t, mon_host_val, "1.1.1.1") | ||||
| } | ||||
| 
 | ||||
| func TestGetClusterStats(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	poolname := GetUUID() | ||||
| 	err := conn.MakePool(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// grab current stats
 | ||||
| 	prev_stat, err := conn.GetClusterStats() | ||||
| 	fmt.Printf("prev_stat: %+v\n", prev_stat) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// make some changes to the cluster
 | ||||
| 	buf := make([]byte, 1<<20) | ||||
| 	for i := 0; i < 10; i++ { | ||||
| 		objname := GetUUID() | ||||
| 		pool.Write(objname, buf, 0) | ||||
| 	} | ||||
| 
 | ||||
| 	// wait a while for the stats to change
 | ||||
| 	for i := 0; i < 30; i++ { | ||||
| 		stat, err := conn.GetClusterStats() | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// wait for something to change
 | ||||
| 		if stat == prev_stat { | ||||
| 			fmt.Printf("curr_stat: %+v (trying again...)\n", stat) | ||||
| 			time.Sleep(time.Second) | ||||
| 		} else { | ||||
| 			// success
 | ||||
| 			fmt.Printf("curr_stat: %+v (change detected)\n", stat) | ||||
| 			conn.Shutdown() | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| 	conn.Shutdown() | ||||
| 	t.Error("Cluster stats aren't changing") | ||||
| } | ||||
| 
 | ||||
| func TestGetFSID(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	fsid, err := conn.GetFSID() | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NotEqual(t, fsid, "") | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestGetInstanceID(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	id := conn.GetInstanceID() | ||||
| 	assert.NotEqual(t, id, 0) | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestMakeDeletePool(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	// get current list of pool
 | ||||
| 	pools, err := conn.ListPools() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// check that new pool name is unique
 | ||||
| 	new_name := GetUUID() | ||||
| 	for _, poolname := range pools { | ||||
| 		if new_name == poolname { | ||||
| 			t.Error("Random pool name exists!") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// create pool
 | ||||
| 	err = conn.MakePool(new_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// get updated list of pools
 | ||||
| 	pools, err = conn.ListPools() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// verify that the new pool name exists
 | ||||
| 	found := false | ||||
| 	for _, poolname := range pools { | ||||
| 		if new_name == poolname { | ||||
| 			found = true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if !found { | ||||
| 		t.Error("Cannot find newly created pool") | ||||
| 	} | ||||
| 
 | ||||
| 	// delete the pool
 | ||||
| 	err = conn.DeletePool(new_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// verify that it is gone
 | ||||
| 
 | ||||
| 	// get updated list of pools
 | ||||
| 	pools, err = conn.ListPools() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// verify that the new pool name exists
 | ||||
| 	found = false | ||||
| 	for _, poolname := range pools { | ||||
| 		if new_name == poolname { | ||||
| 			found = true | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	if found { | ||||
| 		t.Error("Deleted pool still exists") | ||||
| 	} | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestPingMonitor(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	// mon id that should work with vstart.sh
 | ||||
| 	reply, err := conn.PingMonitor("a") | ||||
| 	if err == nil { | ||||
| 		assert.NotEqual(t, reply, "") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// mon id that should work with micro-osd.sh
 | ||||
| 	reply, err = conn.PingMonitor("0") | ||||
| 	if err == nil { | ||||
| 		assert.NotEqual(t, reply, "") | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	// try to use a hostname as the monitor id
 | ||||
| 	mon_addr, _ := conn.GetConfigOption("mon_host") | ||||
| 	hosts, _ := net.LookupAddr(mon_addr) | ||||
| 	for _, host := range hosts { | ||||
| 		reply, err := conn.PingMonitor(host) | ||||
| 		if err == nil { | ||||
| 			assert.NotEqual(t, reply, "") | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	t.Error("Could not find a valid monitor id") | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestReadConfigFile(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 
 | ||||
| 	// check current log_file value
 | ||||
| 	log_file_val, err := conn.GetConfigOption("log_file") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.NotEqual(t, log_file_val, "/dev/null") | ||||
| 
 | ||||
| 	// create a temporary ceph.conf file that changes the log_file conf
 | ||||
| 	// option.
 | ||||
| 	file, err := ioutil.TempFile("/tmp", "go-rados") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	_, err = io.WriteString(file, "[global]\nlog_file = /dev/null\n") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// parse the config file
 | ||||
| 	err = conn.ReadConfigFile(file.Name()) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// check current log_file value
 | ||||
| 	log_file_val, err = conn.GetConfigOption("log_file") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, log_file_val, "/dev/null") | ||||
| 
 | ||||
| 	// cleanup
 | ||||
| 	file.Close() | ||||
| 	os.Remove(file.Name()) | ||||
| } | ||||
| 
 | ||||
| func TestWaitForLatestOSDMap(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	err := conn.WaitForLatestOSDMap() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestReadWrite(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	// make pool
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_in := []byte("input data") | ||||
| 	err = pool.Write("obj", bytes_in, 0) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_out := make([]byte, len(bytes_in)) | ||||
| 	n_out, err := pool.Read("obj", bytes_out, 0) | ||||
| 
 | ||||
| 	assert.Equal(t, n_out, len(bytes_in)) | ||||
| 	assert.Equal(t, bytes_in, bytes_out) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
| func TestObjectStat(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_in := []byte("input data") | ||||
| 	err = pool.Write("obj", bytes_in, 0) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	stat, err := pool.Stat("obj") | ||||
| 	assert.Equal(t, uint64(len(bytes_in)), stat.Size) | ||||
| 	assert.NotNil(t, stat.ModTime) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestGetPoolStats(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	poolname := GetUUID() | ||||
| 	err := conn.MakePool(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// grab current stats
 | ||||
| 	prev_stat, err := pool.GetPoolStats() | ||||
| 	fmt.Printf("prev_stat: %+v\n", prev_stat) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// make some changes to the cluster
 | ||||
| 	buf := make([]byte, 1<<20) | ||||
| 	for i := 0; i < 10; i++ { | ||||
| 		objname := GetUUID() | ||||
| 		pool.Write(objname, buf, 0) | ||||
| 	} | ||||
| 
 | ||||
| 	// wait a while for the stats to change
 | ||||
| 	for i := 0; i < 30; i++ { | ||||
| 		stat, err := pool.GetPoolStats() | ||||
| 		assert.NoError(t, err) | ||||
| 
 | ||||
| 		// wait for something to change
 | ||||
| 		if stat == prev_stat { | ||||
| 			fmt.Printf("curr_stat: %+v (trying again...)\n", stat) | ||||
| 			time.Sleep(time.Second) | ||||
| 		} else { | ||||
| 			// success
 | ||||
| 			fmt.Printf("curr_stat: %+v (change detected)\n", stat) | ||||
| 			conn.Shutdown() | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| 	conn.Shutdown() | ||||
| 	t.Error("Pool stats aren't changing") | ||||
| } | ||||
| 
 | ||||
| func TestGetPoolName(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	poolname := GetUUID() | ||||
| 	err := conn.MakePool(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	ioctx, err := conn.OpenIOContext(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	poolname_ret, err := ioctx.GetPoolName() | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	assert.Equal(t, poolname, poolname_ret) | ||||
| 
 | ||||
| 	ioctx.Destroy() | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestMonCommand(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	command, err := json.Marshal(map[string]string{"prefix": "df", "format": "json"}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	buf, info, err := conn.MonCommand(command) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, info, "") | ||||
| 
 | ||||
| 	var message map[string]interface{} | ||||
| 	err = json.Unmarshal(buf, &message) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	conn.Shutdown() | ||||
| } | ||||
| 
 | ||||
| func TestObjectIterator(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	poolname := GetUUID() | ||||
| 	err := conn.MakePool(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	ioctx, err := conn.OpenIOContext(poolname) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	objectList := []string{} | ||||
| 	err = ioctx.ListObjects(func(oid string) { | ||||
| 		objectList = append(objectList, oid) | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.True(t, len(objectList) == 0) | ||||
| 
 | ||||
| 	createdList := []string{} | ||||
| 	for i := 0; i < 200; i++ { | ||||
| 		oid := GetUUID() | ||||
| 		bytes_in := []byte("input data") | ||||
| 		err = ioctx.Write(oid, bytes_in, 0) | ||||
| 		assert.NoError(t, err) | ||||
| 		createdList = append(createdList, oid) | ||||
| 	} | ||||
| 	assert.True(t, len(createdList) == 200) | ||||
| 
 | ||||
| 	err = ioctx.ListObjects(func(oid string) { | ||||
| 		objectList = append(objectList, oid) | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, len(objectList), len(createdList)) | ||||
| 
 | ||||
| 	sort.Strings(objectList) | ||||
| 	sort.Strings(createdList) | ||||
| 
 | ||||
| 	assert.Equal(t, objectList, createdList) | ||||
| } | ||||
| 
 | ||||
| func TestNewConnWithUser(t *testing.T) { | ||||
| 	_, err := rados.NewConnWithUser("admin") | ||||
| 	assert.Equal(t, err, nil) | ||||
| } | ||||
| 
 | ||||
| func TestReadWriteXattr(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	// make pool
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_in := []byte("input data") | ||||
| 	err = pool.Write("obj", bytes_in, 0) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	my_xattr_in := []byte("my_value") | ||||
| 	err = pool.SetXattr("obj", "my_key", my_xattr_in) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	my_xattr_out := make([]byte, len(my_xattr_in)) | ||||
| 	n_out, err := pool.GetXattr("obj", "my_key", my_xattr_out) | ||||
| 
 | ||||
| 	assert.Equal(t, n_out, len(my_xattr_in)) | ||||
| 	assert.Equal(t, my_xattr_in, my_xattr_out) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
| func TestListXattrs(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	// make pool
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_in := []byte("input data") | ||||
| 	err = pool.Write("obj", bytes_in, 0) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	input_xattrs := make(map[string][]byte) | ||||
| 	for i := 0; i < 200; i++ { | ||||
| 		name := fmt.Sprintf("key_%d", i) | ||||
| 		data := []byte(GetUUID()) | ||||
| 		err = pool.SetXattr("obj", name, data) | ||||
| 		assert.NoError(t, err) | ||||
| 		input_xattrs[name] = data | ||||
| 	} | ||||
| 
 | ||||
| 	output_xattrs := make(map[string][]byte) | ||||
| 	output_xattrs, err = pool.ListXattrs("obj") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, len(input_xattrs), len(output_xattrs)) | ||||
| 	assert.Equal(t, input_xattrs, output_xattrs) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
| func TestRmXattr(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	bytes_in := []byte("input data") | ||||
| 	err = pool.Write("obj", bytes_in, 0) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	key := "key1" | ||||
| 	val := []byte("val1") | ||||
| 	err = pool.SetXattr("obj", key, val) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	key = "key2" | ||||
| 	val = []byte("val2") | ||||
| 	err = pool.SetXattr("obj", key, val) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	xattr_list := make(map[string][]byte) | ||||
| 	xattr_list, err = pool.ListXattrs("obj") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, len(xattr_list), 2) | ||||
| 
 | ||||
| 	pool.RmXattr("obj", "key2") | ||||
| 	xattr_list, err = pool.ListXattrs("obj") | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, len(xattr_list), 1) | ||||
| 
 | ||||
| 	found := false | ||||
| 	for key, _ = range xattr_list { | ||||
| 		if key == "key2" { | ||||
| 			found = true | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	if found { | ||||
| 		t.Error("Deleted pool still exists") | ||||
| 	} | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
| func TestReadWriteOmap(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// Set
 | ||||
| 	orig := map[string][]byte{ | ||||
| 	    "key1": []byte("value1"), | ||||
| 	    "key2": []byte("value2"), | ||||
| 	    "prefixed-key3": []byte("value3"), | ||||
| 	    "empty": []byte(""), | ||||
| 	} | ||||
| 
 | ||||
| 	err = pool.SetOmap("obj", orig) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// List
 | ||||
| 	remaining := map[string][]byte{} | ||||
| 	for k, v := range orig { | ||||
| 		remaining[k] = v | ||||
| 	} | ||||
| 
 | ||||
| 	err = pool.ListOmapValues("obj", "", "", 4, func(key string, value []byte) { | ||||
| 		assert.Equal(t, remaining[key], value) | ||||
| 		delete(remaining, key) | ||||
| 	}) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, 0, len(remaining)) | ||||
| 
 | ||||
| 	// Get (with a fixed number of keys)
 | ||||
| 	fetched, err := pool.GetOmapValues("obj", "",  "", 4) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, orig, fetched) | ||||
| 
 | ||||
| 	// Get All (with an iterator size bigger than the map size)
 | ||||
| 	fetched, err = pool.GetAllOmapValues("obj", "",  "", 100) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, orig, fetched) | ||||
| 
 | ||||
| 	// Get All (with an iterator size smaller than the map size)
 | ||||
| 	fetched, err = pool.GetAllOmapValues("obj", "",  "", 1) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, orig, fetched) | ||||
| 
 | ||||
| 	// Remove
 | ||||
| 	err = pool.RmOmapKeys("obj", []string{"key1", "prefixed-key3"}) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	fetched, err = pool.GetOmapValues("obj", "",  "", 4) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string][]byte{ | ||||
| 	    "key2": []byte("value2"), | ||||
| 	    "empty": []byte(""), | ||||
| 	}, fetched) | ||||
| 
 | ||||
| 	// Clear
 | ||||
| 	err = pool.CleanOmap("obj") | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	fetched, err = pool.GetOmapValues("obj", "",  "", 4) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string][]byte{}, fetched) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
| func TestReadFilterOmap(t *testing.T) { | ||||
| 	conn, _ := rados.NewConn() | ||||
| 	conn.ReadDefaultConfigFile() | ||||
| 	conn.Connect() | ||||
| 
 | ||||
| 	pool_name := GetUUID() | ||||
| 	err := conn.MakePool(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	pool, err := conn.OpenIOContext(pool_name) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	orig := map[string][]byte{ | ||||
| 	    "key1": []byte("value1"), | ||||
| 	    "prefixed-key3": []byte("value3"), | ||||
| 	    "key2": []byte("value2"), | ||||
| 	} | ||||
| 
 | ||||
| 	err = pool.SetOmap("obj", orig) | ||||
| 	assert.NoError(t, err) | ||||
| 
 | ||||
| 	// filter by prefix
 | ||||
| 	fetched, err := pool.GetOmapValues("obj", "", "prefixed", 4) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string][]byte{ | ||||
| 	    "prefixed-key3": []byte("value3"), | ||||
| 	}, fetched) | ||||
| 
 | ||||
| 	// "start_after" a key
 | ||||
| 	fetched, err = pool.GetOmapValues("obj", "key1", "", 4) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string][]byte{ | ||||
| 	    "prefixed-key3": []byte("value3"), | ||||
| 	    "key2": []byte("value2"), | ||||
| 	}, fetched) | ||||
| 
 | ||||
| 	// maxReturn
 | ||||
| 	fetched, err = pool.GetOmapValues("obj", "", "key", 1) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, map[string][]byte{ | ||||
| 	    "key1": []byte("value1"), | ||||
| 	}, fetched) | ||||
| 
 | ||||
| 	pool.Destroy() | ||||
| } | ||||
| 
 | ||||
							
								
								
									
										6
									
								
								Makefile
								
								
								
								
							
							
						
						
									
										6
									
								
								Makefile
								
								
								
								
							|  | @ -18,7 +18,7 @@ version/version.go: | |||
| 
 | ||||
| ${PREFIX}/bin/registry: version/version.go $(shell find . -type f -name '*.go') | ||||
| 	@echo "+ $@" | ||||
| 	@go build -o $@ ${GO_LDFLAGS} ./cmd/registry | ||||
| 	@go build -tags "${DOCKER_BUILDTAGS}" -o $@ ${GO_LDFLAGS} ./cmd/registry | ||||
| 
 | ||||
| ${PREFIX}/bin/registry-api-descriptor-template: version/version.go $(shell find . -type f -name '*.go') | ||||
| 	@echo "+ $@" | ||||
|  | @ -46,11 +46,11 @@ lint: | |||
| 
 | ||||
| build: | ||||
| 	@echo "+ $@" | ||||
| 	@go build -v ${GO_LDFLAGS} ./... | ||||
| 	@go build -tags "${DOCKER_BUILDTAGS}" -v ${GO_LDFLAGS} ./... | ||||
| 
 | ||||
| test: | ||||
| 	@echo "+ $@" | ||||
| 	@go test -test.short ./... | ||||
| 	@go test -test.short -tags "${DOCKER_BUILDTAGS}" ./... | ||||
| 
 | ||||
| test-full: | ||||
| 	@echo "+ $@" | ||||
|  |  | |||
|  | @ -3,6 +3,9 @@ machine: | |||
|   pre: | ||||
|   # Install gvm | ||||
|     - bash < <(curl -s -S -L https://raw.githubusercontent.com/moovweb/gvm/1.0.22/binscripts/gvm-installer) | ||||
|   # Install ceph to test rados driver & create pool | ||||
|     - sudo -i ~/distribution/contrib/ceph/ci-setup.sh | ||||
|     - ceph osd pool create docker-distribution 1 | ||||
| 
 | ||||
|   post: | ||||
|   # Install many go versions | ||||
|  | @ -18,8 +21,11 @@ machine: | |||
|     BASE_OLD: ../../../$HOME/.gvm/pkgsets/old/global/$BASE_DIR | ||||
|     BASE_STABLE: ../../../$HOME/.gvm/pkgsets/stable/global/$BASE_DIR | ||||
|   # BASE_BLEED: ../../../$HOME/.gvm/pkgsets/bleed/global/$BASE_DIR | ||||
|     DOCKER_BUILDTAGS: "include_rados" | ||||
|   # Workaround Circle parsing dumb bugs and/or YAML wonkyness | ||||
|     CIRCLE_PAIN: "mode: set" | ||||
|   # Ceph config | ||||
|     RADOS_POOL: "docker-distribution" | ||||
| 
 | ||||
|   hosts: | ||||
|   # Not used yet | ||||
|  | @ -95,7 +101,7 @@ test: | |||
|     - gvm use stable; go list ./... | xargs -L 1 -I{} rm -f $GOPATH/src/{}/coverage.out: | ||||
|         pwd: $BASE_STABLE | ||||
| 
 | ||||
|     - gvm use stable; go list ./... | xargs -L 1 -I{} godep go test -test.short -coverprofile=$GOPATH/src/{}/coverage.out {}: | ||||
|     - gvm use stable; go list ./... | xargs -L 1 -I{} godep go test -tags "$DOCKER_BUILDTAGS" -test.short -coverprofile=$GOPATH/src/{}/coverage.out {}: | ||||
|         timeout: 600 | ||||
|         pwd: $BASE_STABLE | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,5 @@ | |||
| // +build include_rados
 | ||||
| 
 | ||||
| package main | ||||
| 
 | ||||
| import _ "github.com/docker/distribution/registry/storage/driver/rados" | ||||
|  | @ -0,0 +1,119 @@ | |||
| #! /bin/bash | ||||
| # | ||||
| # Ceph cluster setup in Circle CI | ||||
| # | ||||
| 
 | ||||
| set -x | ||||
| set -e | ||||
| set -u | ||||
| 
 | ||||
| NODE=$(hostname) | ||||
| CEPHDIR=/tmp/ceph | ||||
| 
 | ||||
| mkdir cluster | ||||
| pushd cluster | ||||
| 
 | ||||
| # Install | ||||
| retries=0 | ||||
| until [ $retries -ge 5 ]; do | ||||
|   pip install ceph-deploy && break | ||||
|   retries=$[$retries+1] | ||||
|   sleep 30 | ||||
| done | ||||
| 
 | ||||
| retries=0 | ||||
| until [ $retries -ge 5 ]; do | ||||
|   ceph-deploy install --release hammer $NODE && break | ||||
|   retries=$[$retries+1] | ||||
|   sleep 30 | ||||
| done | ||||
| 
 | ||||
| retries=0 | ||||
| until [ $retries -ge 5 ]; do | ||||
|   ceph-deploy pkg --install librados-dev $NODE && break | ||||
|   retries=$[$retries+1] | ||||
|   sleep 30 | ||||
| done | ||||
| 
 | ||||
| echo $(ip route get 1 | awk '{print $NF;exit}') $(hostname) >> /etc/hosts | ||||
| ssh-keygen -t rsa -f ~/.ssh/id_rsa -q -N "" | ||||
| cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys | ||||
| ssh-keyscan $NODE >> ~/.ssh/known_hosts | ||||
| ceph-deploy new $NODE | ||||
| 
 | ||||
| cat >> ceph.conf <<EOF | ||||
| osd objectstore = memstore | ||||
| memstore device bytes = 2147483648 | ||||
| osd data = $CEPHDIR | ||||
| osd journal = $CEPHDIR/journal | ||||
| osd crush chooseleaf type = 0 | ||||
| osd pool default size = 1 | ||||
| osd pool default min size = 1 | ||||
| osd scrub load threshold = 1000 | ||||
| 
 | ||||
| debug_lockdep = 0/0 | ||||
| debug_context = 0/0 | ||||
| debug_crush = 0/0 | ||||
| debug_buffer = 0/0 | ||||
| debug_timer = 0/0 | ||||
| debug_filer = 0/0 | ||||
| debug_objecter = 0/0 | ||||
| debug_rados = 0/0 | ||||
| debug_rbd = 0/0 | ||||
| debug_journaler = 0/0 | ||||
| debug_objectcatcher = 0/0 | ||||
| debug_client = 0/0 | ||||
| debug_osd = 0/0 | ||||
| debug_optracker = 0/0 | ||||
| debug_objclass = 0/0 | ||||
| debug_filestore = 0/0 | ||||
| debug_journal = 0/0 | ||||
| debug_ms = 0/0 | ||||
| debug_monc = 0/0 | ||||
| debug_tp = 0/0 | ||||
| debug_auth = 0/0 | ||||
| debug_finisher = 0/0 | ||||
| debug_heartbeatmap = 0/0 | ||||
| debug_perfcounter = 0/0 | ||||
| debug_asok = 0/0 | ||||
| debug_throttle = 0/0 | ||||
| debug_mon = 0/0 | ||||
| debug_paxos = 0/0 | ||||
| debug_rgw = 0/0 | ||||
| osd_op_num_threads_per_shard = 1 //You may want to try with 1 as well | ||||
| osd_op_num_shards = 5    //Depends on your cpu util | ||||
| ms_nocrc = true | ||||
| cephx_sign_messages = false | ||||
| cephx_require_signatures = false | ||||
| ms_dispatch_throttle_bytes = 0 | ||||
| throttler_perf_counter = false | ||||
| 
 | ||||
| [osd] | ||||
| osd_client_message_size_cap = 0 | ||||
| osd_client_message_cap = 0 | ||||
| osd_enable_op_tracker = false | ||||
| EOF | ||||
| 
 | ||||
| sed -i -r 's/mon_host =.*/mon_host = 127.0.0.1/' ceph.conf | ||||
| sed -i -r 's/auth_cluster_required =.*/auth_cluster_required = none/' ceph.conf | ||||
| sed -i -r 's/auth_service_required =.*/auth_service_required = none/' ceph.conf | ||||
| sed -i -r 's/auth_client_required =.*/auth_client_required = none/' ceph.conf | ||||
| 
 | ||||
| # Setup monitor and keyrings | ||||
| ceph-deploy mon create-initial $NODE | ||||
| ceph-deploy admin $NODE | ||||
| sudo chmod a+r /etc/ceph/ceph.client.admin.keyring | ||||
| 
 | ||||
| # Setup OSD | ||||
| mkdir -p $CEPHDIR | ||||
| OSD=$(ceph osd create) | ||||
| ceph osd crush add osd.${OSD} 1 root=default host=$NODE | ||||
| ceph-osd --id ${OSD} --mkjournal --mkfs | ||||
| ceph-osd --id ${OSD} | ||||
| 
 | ||||
| # Status | ||||
| ceph status | ||||
| ceph health detail | ||||
| ceph osd tree | ||||
| 
 | ||||
| popd | ||||
|  | @ -121,3 +121,15 @@ $ make | |||
| 
 | ||||
| If that is successful, standard `go` commands, such as `go test` should work, | ||||
| per package, without issue. | ||||
| 
 | ||||
| ### Optional build tags | ||||
| 
 | ||||
| Optional [build tags](http://golang.org/pkg/go/build/) can be provided using | ||||
| the environment variable `DOCKER_BUILDTAGS`. | ||||
| 
 | ||||
| To enable the [Ceph RADOS storage driver](storage-drivers/rados.md) | ||||
| (librados-dev and librbd-dev will be required to build the bindings): | ||||
| 
 | ||||
| ```sh | ||||
| export DOCKER_BUILDTAGS='include_rados' | ||||
| ``` | ||||
|  |  | |||
|  | @ -43,6 +43,10 @@ storage: | |||
| 		v4auth: true | ||||
| 		chunksize: 5242880 | ||||
| 		rootdirectory: /s3/object/name/prefix | ||||
| 	rados: | ||||
| 		poolname: radospool | ||||
| 		username: radosuser | ||||
| 		chunksize: 4194304 | ||||
| 	cache: | ||||
| 		blobdescriptor: redis | ||||
| 	maintenance: | ||||
|  | @ -261,6 +265,10 @@ storage: | |||
| 		v4auth: true | ||||
| 		chunksize: 5242880 | ||||
| 		rootdirectory: /s3/object/name/prefix | ||||
| 	rados: | ||||
| 		poolname: radospool | ||||
| 		username: radosuser | ||||
| 		chunksize: 4194304 | ||||
| 	cache: | ||||
| 		blobdescriptor: inmemory | ||||
| 	maintenance: | ||||
|  | @ -348,6 +356,51 @@ This storage backend uses Microsoft's Azure Storage platform. | |||
| </table> | ||||
| 
 | ||||
| 
 | ||||
| ### rados | ||||
| 
 | ||||
| This storage backend uses [Ceph Object Storage](http://ceph.com/docs/master/rados/). | ||||
| 
 | ||||
| <table> | ||||
|   <tr> | ||||
|     <th>Parameter</th> | ||||
|     <th>Required</th> | ||||
|     <th>Description</th> | ||||
|   </tr> | ||||
|   <tr> | ||||
|     <td> | ||||
|       <code>poolname</code> | ||||
|     </td> | ||||
|     <td> | ||||
|       yes | ||||
|     </td> | ||||
|     <td> | ||||
|       Ceph pool name. | ||||
|     </td> | ||||
|   </tr> | ||||
|    <tr> | ||||
|     <td> | ||||
|       <code>username</code> | ||||
|     </td> | ||||
|     <td> | ||||
|       no | ||||
|     </td> | ||||
|     <td> | ||||
|       Ceph cluster user to connect as (i.e. admin, not client.admin). | ||||
|     </td> | ||||
|   </tr> | ||||
|    <tr> | ||||
|     <td> | ||||
|       <code>chunksize</code> | ||||
|     </td> | ||||
|     <td> | ||||
|       no | ||||
|     </td> | ||||
|     <td> | ||||
|       Size of the written RADOS objects. Default value is 4MB (4194304). | ||||
|     </td> | ||||
|   </tr> | ||||
| </table> | ||||
| 
 | ||||
| 
 | ||||
| ### S3 | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,4 +10,5 @@ | |||
| - ['registry/storage-drivers/azure.md', '**HIDDEN**' ] | ||||
| - ['registry/storage-drivers/filesystem.md', '**HIDDEN**' ] | ||||
| - ['registry/storage-drivers/inmemory.md', '**HIDDEN**' ] | ||||
| - ['registry/storage-drivers/rados.md', '**HIDDEN**' ] | ||||
| - ['registry/storage-drivers/s3.md','**HIDDEN**' ] | ||||
|  |  | |||
|  | @ -0,0 +1,37 @@ | |||
| <!--GITHUB | ||||
| page_title: Ceph RADOS storage driver | ||||
| page_description: Explains how to use the Ceph RADOS storage driver | ||||
| page_keywords: registry, service, driver, images, storage, ceph, rados | ||||
| IGNORES--> | ||||
| 
 | ||||
| # Ceph RADOS storage driver | ||||
| 
 | ||||
| An implementation of the `storagedriver.StorageDriver` interface which uses | ||||
| [Ceph RADOS Object Storage][rados] for storage backend. | ||||
| 
 | ||||
| ## Parameters | ||||
| 
 | ||||
| The following parameters must be used to configure the storage driver | ||||
| (case-sensitive): | ||||
| 
 | ||||
| * `poolname`: Name of the Ceph pool | ||||
| * `username` *optional*: The user to connect as (i.e. admin, not client.admin) | ||||
| * `chunksize` *optional*: Size of the written RADOS objects. Default value is | ||||
| 4MB (4194304). | ||||
| 
 | ||||
| This drivers loads the [Ceph client configuration][rados-config] from the | ||||
| following regular paths (the first found is used): | ||||
| 
 | ||||
| * `$CEPH_CONF` (environment variable) | ||||
| * `/etc/ceph/ceph.conf` | ||||
| * `~/.ceph/config` | ||||
| * `ceph.conf` (in the current working directory) | ||||
| 
 | ||||
| ## Developing | ||||
| 
 | ||||
| To include this driver when building Docker Distribution, use the build tag | ||||
| `include_rados`. Please see the [building documentation][building] for details. | ||||
| 
 | ||||
| [rados]: http://ceph.com/docs/master/rados/ | ||||
| [rados-config]: http://ceph.com/docs/master/rados/configuration/ceph-conf/ | ||||
| [building]: https://github.com/docker/distribution/blob/master/docs/building.md#optional-build-tags | ||||
|  | @ -16,6 +16,7 @@ This storage driver package comes bundled with several drivers: | |||
| - [filesystem](storage-drivers/filesystem.md): A local storage driver configured to use a directory tree in the local filesystem. | ||||
| - [s3](storage-drivers/s3.md): A driver storing objects in an Amazon Simple Storage Solution (S3) bucket. | ||||
| - [azure](storage-drivers/azure.md): A driver storing objects in [Microsoft Azure Blob Storage](http://azure.microsoft.com/en-us/services/storage/). | ||||
| - [rados](storage-drivers/rados.md): A driver storing objects in a [Ceph Object Storage](http://ceph.com/docs/master/rados/) pool. | ||||
| 
 | ||||
| ## Storage Driver API | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,628 @@ | |||
| package rados | ||||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/binary" | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"io/ioutil" | ||||
| 	"path" | ||||
| 	"strconv" | ||||
| 
 | ||||
| 	"code.google.com/p/go-uuid/uuid" | ||||
| 	log "github.com/Sirupsen/logrus" | ||||
| 	"github.com/docker/distribution/context" | ||||
| 	storagedriver "github.com/docker/distribution/registry/storage/driver" | ||||
| 	"github.com/docker/distribution/registry/storage/driver/base" | ||||
| 	"github.com/docker/distribution/registry/storage/driver/factory" | ||||
| 	"github.com/noahdesu/go-ceph/rados" | ||||
| ) | ||||
| 
 | ||||
| const driverName = "rados" | ||||
| 
 | ||||
| // Prefix all the stored blob
 | ||||
| const objectBlobPrefix = "blob:" | ||||
| 
 | ||||
| // Stripes objects size to 4M
 | ||||
| const defaultChunkSize = 4 << 20 | ||||
| const defaultXattrTotalSizeName = "total-size" | ||||
| 
 | ||||
| // Max number of keys fetched from omap at each read operation
 | ||||
| const defaultKeysFetched = 1 | ||||
| 
 | ||||
| //DriverParameters A struct that encapsulates all of the driver parameters after all values have been set
 | ||||
| type DriverParameters struct { | ||||
| 	poolname  string | ||||
| 	username  string | ||||
| 	chunksize uint64 | ||||
| } | ||||
| 
 | ||||
| func init() { | ||||
| 	factory.Register(driverName, &radosDriverFactory{}) | ||||
| } | ||||
| 
 | ||||
| // radosDriverFactory implements the factory.StorageDriverFactory interface
 | ||||
| type radosDriverFactory struct{} | ||||
| 
 | ||||
| func (factory *radosDriverFactory) Create(parameters map[string]interface{}) (storagedriver.StorageDriver, error) { | ||||
| 	return FromParameters(parameters) | ||||
| } | ||||
| 
 | ||||
| type driver struct { | ||||
| 	Conn      *rados.Conn | ||||
| 	Ioctx     *rados.IOContext | ||||
| 	chunksize uint64 | ||||
| } | ||||
| 
 | ||||
| type baseEmbed struct { | ||||
| 	base.Base | ||||
| } | ||||
| 
 | ||||
| // Driver is a storagedriver.StorageDriver implementation backed by Ceph RADOS
 | ||||
| // Objects are stored at absolute keys in the provided bucket.
 | ||||
| type Driver struct { | ||||
| 	baseEmbed | ||||
| } | ||||
| 
 | ||||
| // FromParameters constructs a new Driver with a given parameters map
 | ||||
| // Required parameters:
 | ||||
| // - poolname: the ceph pool name
 | ||||
| func FromParameters(parameters map[string]interface{}) (*Driver, error) { | ||||
| 
 | ||||
| 	pool, ok := parameters["poolname"] | ||||
| 	if !ok { | ||||
| 		return nil, fmt.Errorf("No poolname parameter provided") | ||||
| 	} | ||||
| 
 | ||||
| 	username, ok := parameters["username"] | ||||
| 	if !ok { | ||||
| 		username = "" | ||||
| 	} | ||||
| 
 | ||||
| 	chunksize := uint64(defaultChunkSize) | ||||
| 	chunksizeParam, ok := parameters["chunksize"] | ||||
| 	if ok { | ||||
| 		chunksize, ok = chunksizeParam.(uint64) | ||||
| 		if !ok { | ||||
| 			return nil, fmt.Errorf("The chunksize parameter should be a number") | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	params := DriverParameters{ | ||||
| 		fmt.Sprint(pool), | ||||
| 		fmt.Sprint(username), | ||||
| 		chunksize, | ||||
| 	} | ||||
| 
 | ||||
| 	return New(params) | ||||
| } | ||||
| 
 | ||||
| // New constructs a new Driver
 | ||||
| func New(params DriverParameters) (*Driver, error) { | ||||
| 	var conn *rados.Conn | ||||
| 	var err error | ||||
| 
 | ||||
| 	if params.username != "" { | ||||
| 		log.Infof("Opening connection to pool %s using user %s", params.poolname, params.username) | ||||
| 		conn, err = rados.NewConnWithUser(params.username) | ||||
| 	} else { | ||||
| 		log.Infof("Opening connection to pool %s", params.poolname) | ||||
| 		conn, err = rados.NewConn() | ||||
| 	} | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = conn.ReadDefaultConfigFile() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	err = conn.Connect() | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	log.Infof("Connected") | ||||
| 
 | ||||
| 	ioctx, err := conn.OpenIOContext(params.poolname) | ||||
| 
 | ||||
| 	log.Infof("Connected to pool %s", params.poolname) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	d := &driver{ | ||||
| 		Ioctx:     ioctx, | ||||
| 		Conn:      conn, | ||||
| 		chunksize: params.chunksize, | ||||
| 	} | ||||
| 
 | ||||
| 	return &Driver{ | ||||
| 		baseEmbed: baseEmbed{ | ||||
| 			Base: base.Base{ | ||||
| 				StorageDriver: d, | ||||
| 			}, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // Implement the storagedriver.StorageDriver interface
 | ||||
| 
 | ||||
| func (d *driver) Name() string { | ||||
| 	return driverName | ||||
| } | ||||
| 
 | ||||
| // GetContent retrieves the content stored at "path" as a []byte.
 | ||||
| func (d *driver) GetContent(ctx context.Context, path string) ([]byte, error) { | ||||
| 	rc, err := d.ReadStream(ctx, path, 0) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	defer rc.Close() | ||||
| 
 | ||||
| 	p, err := ioutil.ReadAll(rc) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return p, nil | ||||
| } | ||||
| 
 | ||||
| // PutContent stores the []byte content at a location designated by "path".
 | ||||
| func (d *driver) PutContent(ctx context.Context, path string, contents []byte) error { | ||||
| 	if _, err := d.WriteStream(ctx, path, 0, bytes.NewReader(contents)); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // ReadStream retrieves an io.ReadCloser for the content stored at "path" with a
 | ||||
| // given byte offset.
 | ||||
| type readStreamReader struct { | ||||
| 	driver *driver | ||||
| 	oid    string | ||||
| 	size   uint64 | ||||
| 	offset uint64 | ||||
| } | ||||
| 
 | ||||
| func (r *readStreamReader) Read(b []byte) (n int, err error) { | ||||
| 	// Determine the part available to read
 | ||||
| 	bufferOffset := uint64(0) | ||||
| 	bufferSize := uint64(len(b)) | ||||
| 
 | ||||
| 	// End of the object, read less than the buffer size
 | ||||
| 	if bufferSize > r.size-r.offset { | ||||
| 		bufferSize = r.size - r.offset | ||||
| 	} | ||||
| 
 | ||||
| 	// Fill `b`
 | ||||
| 	for bufferOffset < bufferSize { | ||||
| 		// Get the offset in the object chunk
 | ||||
| 		chunkedOid, chunkedOffset := r.driver.getChunkNameFromOffset(r.oid, r.offset) | ||||
| 
 | ||||
| 		// Determine the best size to read
 | ||||
| 		bufferEndOffset := bufferSize | ||||
| 		if bufferEndOffset-bufferOffset > r.driver.chunksize-chunkedOffset { | ||||
| 			bufferEndOffset = bufferOffset + (r.driver.chunksize - chunkedOffset) | ||||
| 		} | ||||
| 
 | ||||
| 		// Read the chunk
 | ||||
| 		n, err = r.driver.Ioctx.Read(chunkedOid, b[bufferOffset:bufferEndOffset], chunkedOffset) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return int(bufferOffset), err | ||||
| 		} | ||||
| 
 | ||||
| 		bufferOffset += uint64(n) | ||||
| 		r.offset += uint64(n) | ||||
| 	} | ||||
| 
 | ||||
| 	// EOF if the offset is at the end of the object
 | ||||
| 	if r.offset == r.size { | ||||
| 		return int(bufferOffset), io.EOF | ||||
| 	} | ||||
| 
 | ||||
| 	return int(bufferOffset), nil | ||||
| } | ||||
| 
 | ||||
| func (r *readStreamReader) Close() error { | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (d *driver) ReadStream(ctx context.Context, path string, offset int64) (io.ReadCloser, error) { | ||||
| 	// get oid from filename
 | ||||
| 	oid, err := d.getOid(path) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// get object stat
 | ||||
| 	stat, err := d.Stat(ctx, path) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	if offset > stat.Size() { | ||||
| 		return nil, storagedriver.InvalidOffsetError{Path: path, Offset: offset} | ||||
| 	} | ||||
| 
 | ||||
| 	return &readStreamReader{ | ||||
| 		driver: d, | ||||
| 		oid:    oid, | ||||
| 		size:   uint64(stat.Size()), | ||||
| 		offset: uint64(offset), | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| func (d *driver) WriteStream(ctx context.Context, path string, offset int64, reader io.Reader) (totalRead int64, err error) { | ||||
| 	buf := make([]byte, d.chunksize) | ||||
| 	totalRead = 0 | ||||
| 
 | ||||
| 	oid, err := d.getOid(path) | ||||
| 	if err != nil { | ||||
| 		switch err.(type) { | ||||
| 		// Trying to write new object, generate new blob identifier for it
 | ||||
| 		case storagedriver.PathNotFoundError: | ||||
| 			oid = d.generateOid() | ||||
| 			err = d.putOid(path, oid) | ||||
| 			if err != nil { | ||||
| 				return 0, err | ||||
| 			} | ||||
| 		default: | ||||
| 			return 0, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Check total object size only for existing ones
 | ||||
| 		totalSize, err := d.getXattrTotalSize(ctx, oid) | ||||
| 		if err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 
 | ||||
| 		// If offset if after the current object size, fill the gap with zeros
 | ||||
| 		for totalSize < uint64(offset) { | ||||
| 			sizeToWrite := d.chunksize | ||||
| 			if totalSize-uint64(offset) < sizeToWrite { | ||||
| 				sizeToWrite = totalSize - uint64(offset) | ||||
| 			} | ||||
| 
 | ||||
| 			chunkName, chunkOffset := d.getChunkNameFromOffset(oid, uint64(totalSize)) | ||||
| 			err = d.Ioctx.Write(chunkName, buf[:sizeToWrite], uint64(chunkOffset)) | ||||
| 			if err != nil { | ||||
| 				return totalRead, err | ||||
| 			} | ||||
| 
 | ||||
| 			totalSize += sizeToWrite | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Writer
 | ||||
| 	for { | ||||
| 		// Align to chunk size
 | ||||
| 		sizeRead := uint64(0) | ||||
| 		sizeToRead := uint64(offset+totalRead) % d.chunksize | ||||
| 		if sizeToRead == 0 { | ||||
| 			sizeToRead = d.chunksize | ||||
| 		} | ||||
| 
 | ||||
| 		// Read from `reader`
 | ||||
| 		for sizeRead < sizeToRead { | ||||
| 			nn, err := reader.Read(buf[sizeRead:sizeToRead]) | ||||
| 			sizeRead += uint64(nn) | ||||
| 
 | ||||
| 			if err != nil { | ||||
| 				if err != io.EOF { | ||||
| 					return totalRead, err | ||||
| 				} | ||||
| 
 | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// End of file and nothing was read
 | ||||
| 		if sizeRead == 0 { | ||||
| 			break | ||||
| 		} | ||||
| 
 | ||||
| 		// Write chunk object
 | ||||
| 		chunkName, chunkOffset := d.getChunkNameFromOffset(oid, uint64(offset+totalRead)) | ||||
| 		err = d.Ioctx.Write(chunkName, buf[:sizeRead], uint64(chunkOffset)) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return totalRead, err | ||||
| 		} | ||||
| 
 | ||||
| 		// Update total object size as xattr in the first chunk of the object
 | ||||
| 		err = d.setXattrTotalSize(oid, uint64(offset+totalRead)+sizeRead) | ||||
| 		if err != nil { | ||||
| 			return totalRead, err | ||||
| 		} | ||||
| 
 | ||||
| 		totalRead += int64(sizeRead) | ||||
| 
 | ||||
| 		// End of file
 | ||||
| 		if sizeRead < sizeToRead { | ||||
| 			break | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return totalRead, nil | ||||
| } | ||||
| 
 | ||||
| // Stat retrieves the FileInfo for the given path, including the current size
 | ||||
| func (d *driver) Stat(ctx context.Context, path string) (storagedriver.FileInfo, error) { | ||||
| 	// get oid from filename
 | ||||
| 	oid, err := d.getOid(path) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// the path is a virtual directory?
 | ||||
| 	if oid == "" { | ||||
| 		return storagedriver.FileInfoInternal{ | ||||
| 			FileInfoFields: storagedriver.FileInfoFields{ | ||||
| 				Path:  path, | ||||
| 				Size:  0, | ||||
| 				IsDir: true, | ||||
| 			}, | ||||
| 		}, nil | ||||
| 	} | ||||
| 
 | ||||
| 	// stat first chunk
 | ||||
| 	stat, err := d.Ioctx.Stat(oid + "-0") | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	// get total size of chunked object
 | ||||
| 	totalSize, err := d.getXattrTotalSize(ctx, oid) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	return storagedriver.FileInfoInternal{ | ||||
| 		FileInfoFields: storagedriver.FileInfoFields{ | ||||
| 			Path:    path, | ||||
| 			Size:    int64(totalSize), | ||||
| 			ModTime: stat.ModTime, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
| 
 | ||||
| // List returns a list of the objects that are direct descendants of the given path.
 | ||||
| func (d *driver) List(ctx context.Context, dirPath string) ([]string, error) { | ||||
| 	files, err := d.listDirectoryOid(dirPath) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 
 | ||||
| 	keys := make([]string, 0, len(files)) | ||||
| 	for k := range files { | ||||
| 		keys = append(keys, path.Join(dirPath, k)) | ||||
| 	} | ||||
| 
 | ||||
| 	return keys, nil | ||||
| } | ||||
| 
 | ||||
| // Move moves an object stored at sourcePath to destPath, removing the original
 | ||||
| // object.
 | ||||
| func (d *driver) Move(ctx context.Context, sourcePath string, destPath string) error { | ||||
| 	// Get oid
 | ||||
| 	oid, err := d.getOid(sourcePath) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Move reference
 | ||||
| 	err = d.putOid(destPath, oid) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Delete old reference
 | ||||
| 	err = d.deleteOid(sourcePath) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Delete recursively deletes all objects stored at "path" and its subpaths.
 | ||||
| func (d *driver) Delete(ctx context.Context, objectPath string) error { | ||||
| 	// Get oid
 | ||||
| 	oid, err := d.getOid(objectPath) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Deleting virtual directory
 | ||||
| 	if oid == "" { | ||||
| 		objects, err := d.listDirectoryOid(objectPath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for object := range objects { | ||||
| 			err = d.Delete(ctx, path.Join(objectPath, object)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// Delete object chunks
 | ||||
| 		totalSize, err := d.getXattrTotalSize(ctx, oid) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		for offset := uint64(0); offset < totalSize; offset += d.chunksize { | ||||
| 			chunkName, _ := d.getChunkNameFromOffset(oid, offset) | ||||
| 
 | ||||
| 			err = d.Ioctx.Delete(chunkName) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// Delete reference
 | ||||
| 		err = d.deleteOid(objectPath) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // URLFor returns a URL which may be used to retrieve the content stored at the given path.
 | ||||
| // May return an UnsupportedMethodErr in certain StorageDriver implementations.
 | ||||
| func (d *driver) URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error) { | ||||
| 	return "", storagedriver.ErrUnsupportedMethod | ||||
| } | ||||
| 
 | ||||
| // Generate a blob identifier
 | ||||
| func (d *driver) generateOid() string { | ||||
| 	return objectBlobPrefix + uuid.New() | ||||
| } | ||||
| 
 | ||||
| // Reference a object and its hierarchy
 | ||||
| func (d *driver) putOid(objectPath string, oid string) error { | ||||
| 	directory := path.Dir(objectPath) | ||||
| 	base := path.Base(objectPath) | ||||
| 	createParentReference := true | ||||
| 
 | ||||
| 	// After creating this reference, skip the parents referencing since the
 | ||||
| 	// hierarchy already exists
 | ||||
| 	if oid == "" { | ||||
| 		firstReference, err := d.Ioctx.GetOmapValues(directory, "", "", 1) | ||||
| 		if (err == nil) && (len(firstReference) > 0) { | ||||
| 			createParentReference = false | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	oids := map[string][]byte{ | ||||
| 		base: []byte(oid), | ||||
| 	} | ||||
| 
 | ||||
| 	// Reference object
 | ||||
| 	err := d.Ioctx.SetOmap(directory, oids) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Esure parent virtual directories
 | ||||
| 	if createParentReference && directory != "/" { | ||||
| 		return d.putOid(directory, "") | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Get the object identifier from an object name
 | ||||
| func (d *driver) getOid(objectPath string) (string, error) { | ||||
| 	directory := path.Dir(objectPath) | ||||
| 	base := path.Base(objectPath) | ||||
| 
 | ||||
| 	files, err := d.Ioctx.GetOmapValues(directory, "", base, 1) | ||||
| 
 | ||||
| 	if (err != nil) || (files[base] == nil) { | ||||
| 		return "", storagedriver.PathNotFoundError{Path: objectPath} | ||||
| 	} | ||||
| 
 | ||||
| 	return string(files[base]), nil | ||||
| } | ||||
| 
 | ||||
| // List the objects of a virtual directory
 | ||||
| func (d *driver) listDirectoryOid(path string) (list map[string][]byte, err error) { | ||||
| 	return d.Ioctx.GetAllOmapValues(path, "", "", defaultKeysFetched) | ||||
| } | ||||
| 
 | ||||
| // Remove a file from the files hierarchy
 | ||||
| func (d *driver) deleteOid(objectPath string) error { | ||||
| 	// Remove object reference
 | ||||
| 	directory := path.Dir(objectPath) | ||||
| 	base := path.Base(objectPath) | ||||
| 	err := d.Ioctx.RmOmapKeys(directory, []string{base}) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	// Remove virtual directory if empty (no more references)
 | ||||
| 	firstReference, err := d.Ioctx.GetOmapValues(directory, "", "", 1) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 
 | ||||
| 	if len(firstReference) == 0 { | ||||
| 		// Delete omap
 | ||||
| 		err := d.Ioctx.Delete(directory) | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		// Remove reference on parent omaps
 | ||||
| 		if directory != "/" { | ||||
| 			return d.deleteOid(directory) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| // Takes an offset in an chunked object and return the chunk name and a new
 | ||||
| // offset in this chunk object
 | ||||
| func (d *driver) getChunkNameFromOffset(oid string, offset uint64) (string, uint64) { | ||||
| 	chunkID := offset / d.chunksize | ||||
| 	chunkedOid := oid + "-" + strconv.FormatInt(int64(chunkID), 10) | ||||
| 	chunkedOffset := offset % d.chunksize | ||||
| 	return chunkedOid, chunkedOffset | ||||
| } | ||||
| 
 | ||||
| // Set the total size of a chunked object `oid`
 | ||||
| func (d *driver) setXattrTotalSize(oid string, size uint64) error { | ||||
| 	// Convert uint64 `size` to []byte
 | ||||
| 	xattr := make([]byte, binary.MaxVarintLen64) | ||||
| 	binary.LittleEndian.PutUint64(xattr, size) | ||||
| 
 | ||||
| 	// Save the total size as a xattr in the first chunk
 | ||||
| 	return d.Ioctx.SetXattr(oid+"-0", defaultXattrTotalSizeName, xattr) | ||||
| } | ||||
| 
 | ||||
| // Get the total size of the chunked object `oid` stored as xattr
 | ||||
| func (d *driver) getXattrTotalSize(ctx context.Context, oid string) (uint64, error) { | ||||
| 	// Fetch xattr as []byte
 | ||||
| 	xattr := make([]byte, binary.MaxVarintLen64) | ||||
| 	xattrLength, err := d.Ioctx.GetXattr(oid+"-0", defaultXattrTotalSizeName, xattr) | ||||
| 
 | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 
 | ||||
| 	if xattrLength != len(xattr) { | ||||
| 		context.GetLogger(ctx).Errorf("object %s xattr length mismatch: %d != %d", oid, xattrLength, len(xattr)) | ||||
| 		return 0, storagedriver.PathNotFoundError{Path: oid} | ||||
| 	} | ||||
| 
 | ||||
| 	// Convert []byte as uint64
 | ||||
| 	totalSize := binary.LittleEndian.Uint64(xattr) | ||||
| 
 | ||||
| 	return totalSize, nil | ||||
| } | ||||
|  | @ -0,0 +1,38 @@ | |||
| package rados | ||||
| 
 | ||||
| import ( | ||||
| 	"os" | ||||
| 	"testing" | ||||
| 
 | ||||
| 	storagedriver "github.com/docker/distribution/registry/storage/driver" | ||||
| 	"github.com/docker/distribution/registry/storage/driver/testsuites" | ||||
| 
 | ||||
| 	"gopkg.in/check.v1" | ||||
| ) | ||||
| 
 | ||||
| // Hook up gocheck into the "go test" runner.
 | ||||
| func Test(t *testing.T) { check.TestingT(t) } | ||||
| 
 | ||||
| func init() { | ||||
| 	poolname := os.Getenv("RADOS_POOL") | ||||
| 	username := os.Getenv("RADOS_USER") | ||||
| 
 | ||||
| 	driverConstructor := func() (storagedriver.StorageDriver, error) { | ||||
| 		parameters := DriverParameters{ | ||||
| 			poolname, | ||||
| 			username, | ||||
| 			defaultChunkSize, | ||||
| 		} | ||||
| 
 | ||||
| 		return New(parameters) | ||||
| 	} | ||||
| 
 | ||||
| 	skipCheck := func() string { | ||||
| 		if poolname == "" { | ||||
| 			return "RADOS_POOL must be set to run Rado tests" | ||||
| 		} | ||||
| 		return "" | ||||
| 	} | ||||
| 
 | ||||
| 	testsuites.RegisterInProcessSuite(driverConstructor, skipCheck) | ||||
| } | ||||
		Loading…
	
		Reference in New Issue