Viewing: mdc_changelog.c

// SPDX-License-Identifier: GPL-2.0

/*
 * Copyright (c) 2017, Commissariat a l'Energie Atomique et aux Energies
 *                     Alternatives.
 * Copyright (c) 2017, Intel Corporation.
 */

/*
 * This file is part of Lustre, http://www.lustre.org/
 *
 * Author: Henri Doreau <henri.doreau@cea.fr>
 */

#define DEBUG_SUBSYSTEM S_MDC

#include <linux/init.h>
#include <linux/kthread.h>
#include <linux/poll.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <linux/idr.h>

#include <lustre_log.h>
#include <uapi/linux/lustre/lustre_ioctl.h>

#include "mdc_internal.h"


/* -- Changelog delivery through character device -- */

/* Mutex to protect chlg_registered_devices below */
static DEFINE_MUTEX(chlg_registered_dev_lock);

/* Global linked list of all registered devices (one per MDT). */
static LIST_HEAD(chlg_registered_devices);

struct chlg_registered_dev {
	/* Device name of the form "changelog-{MDTNAME}" */
	char			 ced_name[32];
	/* changelog char device */
	struct cdev		 ced_cdev;
	struct device		 ced_device;
	/* OBDs referencing this device (multiple mount point) */
	struct list_head	 ced_obds;
	/* Reference counter for proper deregistration */
	struct kref		 ced_refs;
	/* Link within the global chlg_registered_devices */
	struct list_head	 ced_link;
};

struct chlg_reader_state {
	/* Shortcut to the corresponding OBD device */
	struct obd_device	   *crs_obd;
	/* the corresponding chlg_registered_dev */
	struct chlg_registered_dev *crs_ced;
	/* Producer thread (if any) */
	struct task_struct	   *crs_prod_task;
	/* An error occurred that prevents from reading further */
	int			    crs_err;
	/* EOF, no more records available */
	bool			    crs_eof;
	/* Desired start position */
	__u64			    crs_start_offset;
	/* Wait queue for the catalog processing thread */
	wait_queue_head_t	    crs_waitq_prod;
	/* Wait queue for the record copy threads */
	wait_queue_head_t	    crs_waitq_cons;
	/* Mutex protecting crs_rec_count and crs_rec_queue */
	struct mutex		    crs_lock;
	/* Number of item in the list */
	__u64			    crs_rec_count;
	/* List of prefetched enqueued_record::enq_linkage_items */
	struct list_head	    crs_rec_queue;
	unsigned int		    crs_last_catidx;
	unsigned int		    crs_last_idx;
	unsigned int		    crs_flags;
	/* Changelog filter mask (0 = off by default ) */
	__u64			    crs_user_mask;
};

struct chlg_rec_entry {
	/* Link within the chlg_reader_state::crs_rec_queue list */
	struct list_head	enq_linkage;
	/* Data (enq_record) field length */
	__u64			enq_length;
	/* Copy of a changelog record (see struct llog_changelog_rec) */
	struct changelog_rec	enq_record[];
};

enum {
	/* Number of records to prefetch locally. */
	CDEV_CHLG_MAX_PREFETCH = 1024,
};

DEFINE_IDR(mdc_changelog_minor_idr);
static DEFINE_SPINLOCK(chlg_minor_lock);

static int chlg_minor_alloc(int *pminor)
{
	void *minor_allocated = (void *)-1;
	int minor;

	idr_preload(GFP_KERNEL);
	spin_lock(&chlg_minor_lock);
	minor = idr_alloc(&mdc_changelog_minor_idr, minor_allocated, 0,
			  MDC_CHANGELOG_DEV_COUNT, GFP_NOWAIT);
	spin_unlock(&chlg_minor_lock);
	idr_preload_end();

	if (minor < 0)
		return minor;

	*pminor = minor;
	return 0;
}

static void chlg_minor_free(int minor)
{
	spin_lock(&chlg_minor_lock);
	idr_remove(&mdc_changelog_minor_idr, minor);
	spin_unlock(&chlg_minor_lock);
}

static void chlg_device_release(struct device *dev)
{
	struct chlg_registered_dev *entry = dev_get_drvdata(dev);

	chlg_minor_free(MINOR(entry->ced_cdev.dev));
	OBD_FREE_PTR(entry);
}

/* Deregister a changelog character device whose refcount has reached zero. */
static void chlg_dev_clear(struct kref *kref)
{
	struct chlg_registered_dev *entry;

	ENTRY;
	entry = container_of(kref, struct chlg_registered_dev,
			     ced_refs);

	list_del(&entry->ced_link);
	cdev_device_del(&entry->ced_cdev, &entry->ced_device);
	put_device(&entry->ced_device);
	EXIT;
}

static inline struct obd_device *chlg_obd_get(struct chlg_registered_dev *dev)
{
	struct obd_device *obd;

	mutex_lock(&chlg_registered_dev_lock);
	if (list_empty(&dev->ced_obds)) {
		mutex_unlock(&chlg_registered_dev_lock);
		return NULL;
	}

	obd = list_first_entry(&dev->ced_obds, struct obd_device,
			       u.cli.cl_chg_dev_linkage);
	class_incref(obd, "changelog", dev);
	mutex_unlock(&chlg_registered_dev_lock);
	return obd;
}

static inline void chlg_obd_put(struct chlg_registered_dev *dev,
			 struct obd_device *obd)
{
	class_decref(obd, "changelog", dev);
}

/**
 * chlg_read_cat_process_cb() - Changelog catalog processing callback
 * @env: (unused)
 * @llh: Client-side handle used to identify the llog
 * @hdr: Header of the current llog record
 * @data: chlg_reader_state passed from caller [in,out]
 *
 * ChangeLog catalog processing callback invoked on each record.
 * If the current record is eligible to userland delivery, push
 * it into the crs_rec_queue where the consumer code will fetch it.
 *
 * Return %0 or LLOG_PROC_* control code on success, %negated error on failure.
 */
static int chlg_read_cat_process_cb(const struct lu_env *env,
				    struct llog_handle *llh,
				    struct llog_rec_hdr *hdr, void *data)
{
	struct llog_changelog_rec *rec;
	struct chlg_reader_state *crs = data;
	struct chlg_rec_entry *enq;
	size_t len;
	int rc;

	ENTRY;

	LASSERT(crs != NULL);
	LASSERT(hdr != NULL);

	rec = container_of(hdr, struct llog_changelog_rec, cr_hdr);

	crs->crs_last_catidx = llh->lgh_hdr->llh_cat_idx;
	crs->crs_last_idx = hdr->lrh_index;

	if (rec->cr_hdr.lrh_type != CHANGELOG_REC) {
		rc = -EINVAL;
		CERROR("%s: not a changelog rec %x/%d in llog : rc = %d\n",
		       crs->crs_obd->obd_name, rec->cr_hdr.lrh_type,
		       rec->cr.cr_type, rc);
		RETURN(rc);
	}

	/* Check if we can skip the entire llog plain */
	if (llog_is_plain_skipable(llh->lgh_hdr, hdr, rec->cr.cr_index,
				   crs->crs_start_offset))
		RETURN(LLOG_SKIP_PLAIN);

	/* Skip undesired records */
	if (rec->cr.cr_index < crs->crs_start_offset)
		RETURN(0);

	/* Check if this record type matches the user's mask */
	if (crs->crs_user_mask &&
	    !(crs->crs_user_mask & BIT(rec->cr.cr_type)))
		RETURN(0);

	CDEBUG(D_HSM, "%llu %02d%-5s %llu 0x%x t="DFID" p="DFID" %.*s\n",
	       rec->cr.cr_index, rec->cr.cr_type,
	       changelog_type2str(rec->cr.cr_type), rec->cr.cr_time,
	       rec->cr.cr_flags & CLF_FLAGMASK,
	       PFID(&rec->cr.cr_tfid), PFID(&rec->cr.cr_pfid),
	       rec->cr.cr_namelen, changelog_rec_name(&rec->cr));

	wait_event_interruptible(crs->crs_waitq_prod,
				 crs->crs_rec_count < CDEV_CHLG_MAX_PREFETCH ||
				 kthread_should_stop());

	if (kthread_should_stop())
		RETURN(LLOG_PROC_BREAK);

	len = changelog_rec_size(&rec->cr) + rec->cr.cr_namelen;
	OBD_ALLOC(enq, sizeof(*enq) + len);
	if (enq == NULL)
		RETURN(-ENOMEM);

	INIT_LIST_HEAD(&enq->enq_linkage);
	enq->enq_length = len;
	memcpy(enq->enq_record, &rec->cr, len);

	mutex_lock(&crs->crs_lock);
	list_add_tail(&enq->enq_linkage, &crs->crs_rec_queue);
	crs->crs_rec_count++;
	mutex_unlock(&crs->crs_lock);

	wake_up(&crs->crs_waitq_cons);

	RETURN(0);
}

/* Remove record from the list it is attached to and free it. */
static void enq_record_delete(struct chlg_rec_entry *rec)
{
	list_del(&rec->enq_linkage);
	OBD_FREE(rec, sizeof(*rec) + rec->enq_length);
}

/**
 * chlg_load() - Record prefetch thread entry point. Opens the changelog catalog
 *               and starts reading records.
 * @args: chlg_reader_state passed from caller. [in,out]
 *
 * Return %0 on success, %negated error code on failure.
 */
static int chlg_load(void *args)
{
	struct chlg_reader_state *crs = args;
	struct chlg_registered_dev *ced = crs->crs_ced;
	struct obd_device *obd = NULL;
	struct llog_ctxt *ctx = NULL;
	struct llog_handle *llh = NULL;
	enum llog_flag nid_be_flag = 0;
	int rc;

	ENTRY;
	crs->crs_last_catidx = 0;
	crs->crs_last_idx = 0;
again:
	obd = chlg_obd_get(ced);
	if (obd == NULL)
		RETURN(-ENODEV);

	crs->crs_obd = obd;

	ctx = llog_get_context(obd, LLOG_CHANGELOG_REPL_CTXT);
	if (ctx == NULL)
		GOTO(err_out, rc = -ENOENT);

	rc = llog_open(NULL, ctx, &llh, NULL, CHANGELOG_CATALOG,
		       LLOG_OPEN_EXISTS);
	if (rc) {
		CERROR("%s: fail to open changelog catalog: rc = %d\n",
		       obd->obd_name, rc);
		GOTO(err_out, rc);
	}

	if (crs->crs_flags & CLFE_NID_BE)
		nid_be_flag = LLOG_F_EXT_X_NID_BE;

	rc = llog_init_handle(NULL, llh,
			      LLOG_F_IS_CAT |
			      LLOG_F_EXT_JOBID |
			      LLOG_F_EXT_EXTRA_FLAGS |
			      LLOG_F_EXT_X_UIDGID |
			      LLOG_F_EXT_X_NID | nid_be_flag |
			      LLOG_F_EXT_X_OMODE |
			      LLOG_F_EXT_X_XATTR,
			      NULL);
	if (rc) {
		CERROR("%s: fail to init llog handle: rc = %d\n",
		       obd->obd_name, rc);
		GOTO(err_out, rc);
	}

	rc = llog_cat_process(NULL, llh, chlg_read_cat_process_cb, crs,
				crs->crs_last_catidx, crs->crs_last_idx);
	if (rc < 0) {
		CERROR("%s: fail to process llog: rc = %d\n", obd->obd_name, rc);
		GOTO(err_out, rc);
	}
	if (!kthread_should_stop() &&
	    (crs->crs_flags & CHANGELOG_FLAG_FOLLOW)) {
		llog_cat_close(NULL, llh);
		llog_ctxt_put(ctx);
		class_decref(obd, "changelog", crs);
		schedule_timeout_interruptible(cfs_time_seconds(1));
		goto again;
	}

	crs->crs_eof = true;

err_out:
	if (rc < 0)
		crs->crs_err = rc;

	wake_up(&crs->crs_waitq_cons);

	if (llh != NULL)
		llog_cat_close(NULL, llh);

	if (ctx != NULL)
		llog_ctxt_put(ctx);

	crs->crs_obd = NULL;
	chlg_obd_put(ced, obd);
	wait_event_interruptible(crs->crs_waitq_prod, kthread_should_stop());

	RETURN(rc);
}

static int chlg_start_thread(struct file *file)
{
	struct chlg_reader_state *crs = file->private_data;
	struct task_struct *task;
	int rc = 0;

	if (likely(crs->crs_prod_task))
		return 0;
	if (unlikely(file->f_mode & FMODE_READ) == 0)
		return 0;

	mutex_lock(&crs->crs_lock);
	if (crs->crs_prod_task == NULL) {
		task = kthread_run(chlg_load, crs, "chlg_load_thread");
		if (IS_ERR(task)) {
			rc = PTR_ERR(task);
			CERROR("%s: cannot start changelog thread: rc = %d\n",
			       crs->crs_ced->ced_name, rc);
			GOTO(out, rc);
		}
		crs->crs_prod_task = task;
	}
out:
	mutex_unlock(&crs->crs_lock);
	return rc;
}

/**
 * chlg_read() - Read Handler
 * @file: File pointer to the character device.
 * @buff: Userland buffer where to copy the records. [out]
 * @count: Userland buffer size.
 * @ppos: File position, updated with index number of next record to read. [out]
 *
 * Read handler, dequeues records from the chlg_reader_state if any.
 * No partial records are copied to userland so this function can return less
 * data than required (short read).
 *
 * Return number of copied bytes on success, %negated error code on failure.
 */
static ssize_t chlg_read(struct file *file, char __user *buff, size_t count,
			 loff_t *ppos)
{
	struct chlg_reader_state *crs = file->private_data;
	struct chlg_rec_entry *rec;
	struct chlg_rec_entry *tmp;
	size_t written_total = 0;
	ssize_t rc;
	LIST_HEAD(consumed);

	ENTRY;

	if (file->f_flags & O_NONBLOCK && crs->crs_rec_count == 0) {
		if (crs->crs_err < 0)
			RETURN(crs->crs_err);
		else if (crs->crs_eof)
			RETURN(0);
		else
			RETURN(-EAGAIN);
	}

	rc = chlg_start_thread(file);
	if (rc)
		RETURN(rc);

	rc = wait_event_interruptible(crs->crs_waitq_cons,
			crs->crs_rec_count > 0 || crs->crs_eof || crs->crs_err);

	mutex_lock(&crs->crs_lock);
	list_for_each_entry_safe(rec, tmp, &crs->crs_rec_queue, enq_linkage) {
		if (written_total + rec->enq_length > count)
			break;

		if (copy_to_user(buff, rec->enq_record, rec->enq_length)) {
			rc = -EFAULT;
			break;
		}

		buff += rec->enq_length;
		written_total += rec->enq_length;

		crs->crs_rec_count--;
		list_move_tail(&rec->enq_linkage, &consumed);

		crs->crs_start_offset = rec->enq_record->cr_index + 1;
	}
	mutex_unlock(&crs->crs_lock);

	if (written_total > 0) {
		rc = written_total;
		wake_up(&crs->crs_waitq_prod);
	} else if (rc == 0) {
		rc = crs->crs_err;
	}

	list_for_each_entry_safe(rec, tmp, &consumed, enq_linkage)
		enq_record_delete(rec);

	*ppos = crs->crs_start_offset;

	RETURN(rc);
}

/**
 * chlg_set_start_offset() - Jump to a given record index.
 * @crs: Internal reader state. [in,out]
 * @offset: Desired offset (index record).
 *
 * Jump to a given record index. Helper for chlg_llseek().
 *
 * Return 0 on success, negated error code on failure.
 */
static int chlg_set_start_offset(struct chlg_reader_state *crs, __u64 offset)
{
	struct chlg_rec_entry *rec;
	struct chlg_rec_entry *tmp;

	mutex_lock(&crs->crs_lock);
	if (offset < crs->crs_start_offset) {
		mutex_unlock(&crs->crs_lock);
		return -ERANGE;
	}

	crs->crs_start_offset = offset;
	list_for_each_entry_safe(rec, tmp, &crs->crs_rec_queue, enq_linkage) {
		struct changelog_rec *cr = rec->enq_record;

		if (cr->cr_index >= crs->crs_start_offset)
			break;

		crs->crs_rec_count--;
		enq_record_delete(rec);
	}

	mutex_unlock(&crs->crs_lock);
	wake_up(&crs->crs_waitq_prod);
	return 0;
}

/**
 * chlg_llseek() - Move read pointer to a certain record index, encoded as an
 *                 offset.
 * @file: File pointer to the changelog character device [in, out]
 * @off: Offset to skip, actually a record index, not byte count
 * @whence: Relative/Absolute interpretation of the offset
 *
 * Return the resulting position on success or %negated error code on failure.
 */
static loff_t chlg_llseek(struct file *file, loff_t off, int whence)
{
	struct chlg_reader_state *crs = file->private_data;
	loff_t pos;
	int rc;

	switch (whence) {
	case SEEK_SET:
		pos = off;
		break;
	case SEEK_CUR:
		pos = file->f_pos + off;
		break;
	case SEEK_END:
	default:
		return -EINVAL;
	}

	/* We cannot go backward */
	if (pos < file->f_pos)
		return -EINVAL;

	rc = chlg_set_start_offset(crs, pos);
	if (rc != 0)
		return rc;

	file->f_pos = pos;
	return pos;
}

/**
 * chlg_clear() - Clear record range for a given changelog reader.
 * @crs: Current internal state.
 * @reader: Changelog reader ID (cl1, cl2...)
 * @record: Record index up which to clear
 *
 * Return %0 on success, %negated error code on failure.
 */
static int chlg_clear(struct chlg_reader_state *crs, __u32 reader, __u64 record)
{
	struct obd_device *obd = NULL;
	struct changelog_setinfo cs  = {
		.cs_recno = record,
		.cs_id    = reader
	};
	int rc;

	obd = chlg_obd_get(crs->crs_ced);
	if (obd == NULL)
		return -ENODEV;

	rc = obd_set_info_async(NULL, obd->obd_self_export,
				strlen(KEY_CHANGELOG_CLEAR),
				KEY_CHANGELOG_CLEAR, sizeof(cs), &cs, NULL);

	chlg_obd_put(crs->crs_ced, obd);
	return rc;
}

/** Maximum changelog control command size */
#define CHLG_CONTROL_CMD_MAX	64

/**
 * chlg_write() - Handle writes into the changelog character device.
 * @file:  File pointer to the changelog character device
 * @buff:  User supplied data (written data)
 * @count: Number of written bytes
 * @off:   (unused)
 *
 * Handle writes() into the changelog character device. Write() can be used
 * to request special control operations.
 *
 * Return number of written bytes on success, negated error code on failure.
 */
static ssize_t chlg_write(struct file *file, const char __user *buff,
			  size_t count, loff_t *off)
{
	struct chlg_reader_state *crs = file->private_data;
	char *kbuf;
	__u64 record;
	__u32 reader;
	int rc = 0;

	ENTRY;

	if (count > CHLG_CONTROL_CMD_MAX)
		RETURN(-EINVAL);

	OBD_ALLOC(kbuf, CHLG_CONTROL_CMD_MAX);
	if (kbuf == NULL)
		RETURN(-ENOMEM);

	if (copy_from_user(kbuf, buff, count))
		GOTO(out_kbuf, rc = -EFAULT);

	kbuf[CHLG_CONTROL_CMD_MAX - 1] = '\0';

	if (sscanf(kbuf, "clear:cl%u:%llu", &reader, &record) == 2)
		rc = chlg_clear(crs, reader, record);
	else
		rc = -EINVAL;

	EXIT;
out_kbuf:
	OBD_FREE(kbuf, CHLG_CONTROL_CMD_MAX);
	return rc < 0 ? rc : count;
}

/**
 * chlg_open() - Open handler
 * @inode: Inode struct for the open character device.
 * @file: Corresponding file pointer.
 *
 * Open handler, initialize internal CRS state and spawn prefetch thread if
 * needed.
 *
 * Return %0 on success, %negated error code on failure.
 */
static int chlg_open(struct inode *inode, struct file *file)
{
	struct chlg_reader_state *crs;
	struct chlg_registered_dev *dev;

	ENTRY;

	dev = container_of(inode->i_cdev, struct chlg_registered_dev, ced_cdev);

	OBD_ALLOC_PTR(crs);
	if (!crs)
		RETURN(-ENOMEM);

	kref_get(&dev->ced_refs);
	crs->crs_ced = dev;
	crs->crs_err = false;
	crs->crs_eof = false;

	mutex_init(&crs->crs_lock);
	INIT_LIST_HEAD(&crs->crs_rec_queue);
	init_waitqueue_head(&crs->crs_waitq_prod);
	init_waitqueue_head(&crs->crs_waitq_cons);
	crs->crs_prod_task = NULL;
	crs->crs_user_mask = 0;

	file->private_data = crs;
	RETURN(0);
}

/**
 * chlg_release() - Close handler, release resources.
 * @inode: Inode struct for the open character device.
 * @file: Corresponding file pointer.
 *
 * Return %0 on success, %negated error code on failure.
 */
static int chlg_release(struct inode *inode, struct file *file)
{
	struct chlg_reader_state *crs = file->private_data;
	struct chlg_rec_entry *rec;
	struct chlg_rec_entry *tmp;
	int rc = 0;

	if (crs->crs_prod_task)
		rc = kthread_stop(crs->crs_prod_task);

	list_for_each_entry_safe(rec, tmp, &crs->crs_rec_queue, enq_linkage)
		enq_record_delete(rec);

	kref_put(&crs->crs_ced->ced_refs, chlg_dev_clear);
	OBD_FREE_PTR(crs);

	return rc;
}

/**
 * chlg_poll() - Poll handler
 * @file: Device file pointer.
 * @wait: (opaque)
 *
 * Poll handler, indicates whether the device is readable (new records) and
 * writable (always).
 *
 * Return combination of the poll status flags.
 */
static unsigned int chlg_poll(struct file *file, poll_table *wait)
{
	struct chlg_reader_state *crs = file->private_data;
	unsigned int mask = 0;
	int rc;

	rc = chlg_start_thread(file);
	if (rc)
		RETURN(rc);

	mutex_lock(&crs->crs_lock);
	poll_wait(file, &crs->crs_waitq_cons, wait);
	if (crs->crs_rec_count > 0)
		mask |= POLLIN | POLLRDNORM;
	if (crs->crs_err)
		mask |= POLLERR;
	if (crs->crs_eof)
		mask |= POLLHUP;
	mutex_unlock(&crs->crs_lock);
	return mask;
}

/**
 * mdc_changelog_get_user_info() - Send MDS_GET_INFO RPC to fetch changelog
 *                                 user information.
 * @imp: MDC import
 * @in: User-specific changelog filter
 * @out: Returned changelog user information [out]
 *
 * Return %0 on success with @out properly filled, %negated error code on
 * failure.
 */
static int mdc_changelog_get_user_info(struct obd_import *imp,
				       const struct changelog_filter *in,
				       struct changelog_filter *out)
{
	struct ptlrpc_request *req;
	struct changelog_filter *val_in;
	struct changelog_filter *val_out;
	char *key;
	int rc;

	ENTRY;

	req = ptlrpc_request_alloc(imp, &RQF_MDS_GET_INFO);
	if (req == NULL)
		RETURN(-ENOMEM);

	/* Set request fields size and pack request buffers */
	req_capsule_set_size(&req->rq_pill, &RMF_GETINFO_KEY, RCL_CLIENT,
			     strlen(KEY_CHANGELOG_USER) + 1);
	req_capsule_set_size(&req->rq_pill, &RMF_GETINFO_VAL, RCL_CLIENT,
			     sizeof(struct changelog_filter));
	rc = ptlrpc_request_pack(req, LUSTRE_MDS_VERSION, MDS_GET_INFO);
	if (rc) {
		ptlrpc_request_free(req);
		RETURN(rc);
	}

	/* Fill in KEY */
	key = req_capsule_client_get(&req->rq_pill, &RMF_GETINFO_KEY);
	memcpy(key, KEY_CHANGELOG_USER, strlen(KEY_CHANGELOG_USER) + 1);
	/* Fill in VAL*/
	val_in = req_capsule_client_get(&req->rq_pill, &RMF_GETINFO_VAL);
	memcpy(val_in, in, sizeof(struct changelog_filter));

	/* Set reply size */
	req_capsule_set_size(&req->rq_pill, &RMF_GETINFO_VAL, RCL_SERVER,
			     sizeof(struct changelog_filter));

	ptlrpc_request_set_replen(req);
	rc = ptlrpc_queue_wait(req);
	if (rc)
		GOTO(out, rc);

	/* Get reply */
	val_out = req_capsule_server_get(&req->rq_pill, &RMF_GETINFO_VAL);
	if (val_out == NULL)
		GOTO(out, rc = -EPROTO);

	*out = *val_out;
out:
	ptlrpc_req_put(req);
	RETURN(rc);
}

static long chlg_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
	int rc;
	struct chlg_reader_state *crs = file->private_data;

	switch (cmd) {
	case OBD_IOC_CHLG_POLL:
		crs->crs_flags = arg;
		rc = 0;
		break;
	case OBD_IOC_CHANGELOG_FILTER: {
		struct changelog_filter in;	/* filter request */
		struct changelog_filter out;	/* user info reply */
		struct obd_device *obd;

		/* Unpack ioctl data */
		if (copy_from_user(&in, (void __user *)arg, sizeof(in)))
			return -EFAULT;

		/* Get changelog user info */
		obd = chlg_obd_get(crs->crs_ced);
		if (obd == NULL)
			return -ENODEV;
		rc = mdc_changelog_get_user_info(obd->u.cli.cl_import,
						 &in, &out);
		if (rc) {
			CERROR("%s: Failed to get changelog user info for cl%u(%s): rc = %d\n",
			       obd->obd_name, in.cf_user_id, in.cf_username,
			       rc);
			chlg_obd_put(crs->crs_ced, obd);
			break;
		}
		chlg_obd_put(crs->crs_ced, obd);

		mutex_lock(&crs->crs_lock);
		if (in.cf_mask == 0)
			crs->crs_user_mask = out.cf_mask;
		else
			crs->crs_user_mask = in.cf_mask & out.cf_mask;
		mutex_unlock(&crs->crs_lock);

		CDEBUG(D_INFO,
		       "Set changelog filter: username=cl%u(%s), mask=0x%llx\n",
		       out.cf_user_id, out.cf_username, crs->crs_user_mask);
		rc = 0;
		break;
	}
	default:
		rc = -EINVAL;
		break;
	}

	RETURN(rc);
}

static const struct file_operations chlg_fops = {
	.owner		= THIS_MODULE,
	.llseek		= chlg_llseek,
	.read		= chlg_read,
	.write		= chlg_write,
	.open		= chlg_open,
	.release	= chlg_release,
	.poll		= chlg_poll,
	.unlocked_ioctl	= chlg_ioctl,
};

/**
 * get_target_name() - Get changelog defined name from OBD Name
 * @name: changelog define name which was retrived from OBD Name [out]
 * @name_len: size of OBD Name
 * @obd: OBD to get name
 *
 * This uses obd_name of the form: "testfs-MDT0000-mdc-ffff88006501600"
 * and returns a name of the form: "changelog-testfs-MDT0000".
 */
static void get_target_name(char *name, size_t name_len, struct obd_device *obd)
{
	int i;

	snprintf(name, name_len, "%s", obd->obd_name);

	/* Find the 2nd '-' from the end and truncate on it */
	for (i = 0; i < 2; i++) {
		char *p = strrchr(name, '-');

		if (p == NULL)
			return;
		*p = '\0';
	}
}

/**
 * chlg_registered_dev_find_by_name() - Find changelog character device by name.
 * @name: Name of changelog char device to be searched
 *
 * All devices registered during MDC setup are listed in a global list with
 * their names attached.
 *
 * Return struct chlg_registered_dev on Success or %NULL if not found
 */
static struct chlg_registered_dev *
chlg_registered_dev_find_by_name(const char *name)
{
	struct chlg_registered_dev *dit;

	LASSERT(mutex_is_locked(&chlg_registered_dev_lock));
	list_for_each_entry(dit, &chlg_registered_devices, ced_link)
		if (strcmp(name, dit->ced_name) == 0)
			return dit;
	return NULL;
}

/**
 * chlg_registered_dev_find_by_obd() - Find changelog character device by obd
 * @obd: Find changelog device for this OBD device.
 *
 * Find chlg_registered_dev structure for a given OBD device.
 * This is bad O(n^2) but for each filesystem:
 *   - N is # of MDTs times # of mount points
 *   - this only runs at shutdown
 *
 * Return struct chlg_registered_dev on Success or %NULL if not found
 */
static struct chlg_registered_dev *
chlg_registered_dev_find_by_obd(const struct obd_device *obd)
{
	struct chlg_registered_dev *dit;
	struct obd_device *oit;

	LASSERT(mutex_is_locked(&chlg_registered_dev_lock));
	list_for_each_entry(dit, &chlg_registered_devices, ced_link)
		list_for_each_entry(oit, &dit->ced_obds,
				    u.cli.cl_chg_dev_linkage)
			if (oit == obd)
				return dit;
	return NULL;
}

/**
 * mdc_changelog_cdev_init() - Changelog character device initialization.
 * @obd: This MDC obd_device.
 *
 * Register a misc character device with a dynamic minor number, under a name
 * of the form: 'changelog-fsname-MDTxxxx'. Reference this OBD device with it.
 *
 * Return %0 on success, negated error code on failure.
 */
int mdc_changelog_cdev_init(struct obd_device *obd)
{
	struct chlg_registered_dev *exist;
	struct chlg_registered_dev *entry;
	int minor, rc;

	ENTRY;

	OBD_ALLOC_PTR(entry);
	if (entry == NULL)
		RETURN(-ENOMEM);

	get_target_name(entry->ced_name, sizeof(entry->ced_name), obd);

	kref_init(&entry->ced_refs);
	INIT_LIST_HEAD(&entry->ced_obds);
	INIT_LIST_HEAD(&entry->ced_link);

	mutex_lock(&chlg_registered_dev_lock);
	exist = chlg_registered_dev_find_by_name(entry->ced_name);
	if (exist != NULL) {
		kref_get(&exist->ced_refs);
		list_add_tail(&obd->u.cli.cl_chg_dev_linkage, &exist->ced_obds);
		GOTO(out_unlock, rc = 0);
	}

	list_add_tail(&obd->u.cli.cl_chg_dev_linkage, &entry->ced_obds);
	list_add_tail(&entry->ced_link, &chlg_registered_devices);

	rc = chlg_minor_alloc(&minor);
	if (rc)
		GOTO(out_listrm, rc);

	device_initialize(&entry->ced_device);
	entry->ced_device.devt = MKDEV(MAJOR(mdc_changelog_dev), minor);
	entry->ced_device.class = mdc_changelog_class;
	entry->ced_device.release = chlg_device_release;
	dev_set_drvdata(&entry->ced_device, entry);
	rc = dev_set_name(&entry->ced_device, "%s-%s", MDC_CHANGELOG_DEV_NAME,
			  entry->ced_name);
	if (rc)
		GOTO(out_minor, rc);

	/* Register new character device */
	cdev_init(&entry->ced_cdev, &chlg_fops);
	entry->ced_cdev.owner = THIS_MODULE;
	rc = cdev_device_add(&entry->ced_cdev, &entry->ced_device);
	if (rc)
		GOTO(out_device_name, rc);

	entry = NULL;	/* prevent it from being freed below */
	GOTO(out_unlock, rc = 0);

out_device_name:
	kfree_const(entry->ced_device.kobj.name);

out_minor:
	chlg_minor_free(minor);

out_listrm:
	list_del_init(&obd->u.cli.cl_chg_dev_linkage);
	list_del(&entry->ced_link);

out_unlock:
	mutex_unlock(&chlg_registered_dev_lock);
	OBD_FREE_PTR(entry);
	RETURN(rc);
}

/**
 * mdc_changelog_cdev_finish() - Release OBD, decrease reference count of the
 *                               corresponding changelog device.
 * @obd: OBD device of changelog
 */
void mdc_changelog_cdev_finish(struct obd_device *obd)
{
	struct chlg_registered_dev *dev;

	ENTRY;
	mutex_lock(&chlg_registered_dev_lock);
	dev = chlg_registered_dev_find_by_obd(obd);
	list_del_init(&obd->u.cli.cl_chg_dev_linkage);
	kref_put(&dev->ced_refs, chlg_dev_clear);
	mutex_unlock(&chlg_registered_dev_lock);
	EXIT;
}