/*
 * Copyright (C) 2001-2004 Sistina Software, Inc. All rights reserved.
 * Copyright (C) 2004-2011 Red Hat, Inc. All rights reserved.
 *
 * This file is part of LVM2.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License v.2.1.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 */

#include "lib/misc/lib.h"
#include "lib/locking/locking.h"
#include "lib/locking/lvmlockd.h"
#include "locking_types.h"
#include "lib/misc/lvm-string.h"
#include "lib/activate/activate.h"
#include "lib/commands/toolcontext.h"
#include "lib/mm/memlock.h"
#include "lib/config/defaults.h"
#include "lib/cache/lvmcache.h"
#include "lib/misc/lvm-signal.h"

#include <assert.h>
#include <sys/stat.h>
#include <limits.h>
#include <unistd.h>

static struct locking_type _locking;

static int _vg_lock_count = 0;		/* Number of locks held */
static int _vg_write_lock_held = 0;	/* VG write lock held? */
static int _blocking_supported = 0;
static int _file_locking_readonly = 0;
static int _file_locking_sysinit = 0;
static int _file_locking_ignorefail = 0;
static int _file_locking_failed = 0;

static void _unblock_signals(void)
{
	/* Don't unblock signals while any locks are held */
	if (!_vg_lock_count)
		unblock_signals();
}

void reset_locking(void)
{
	int was_locked = _vg_lock_count;

	/* file locking disabled */
	if (!_locking.flags)
		return;

	_vg_lock_count = 0;
	_vg_write_lock_held = 0;

	if (_locking.reset_locking)
		_locking.reset_locking();

	if (was_locked)
		_unblock_signals();

	memlock_reset();
}

static void _update_vg_lock_count(const char *resource, uint32_t flags)
{
	/* Ignore locks not associated with updating VG metadata */
	if (!strcmp(resource, VG_GLOBAL))
		return;

	if ((flags & LCK_TYPE_MASK) == LCK_UNLOCK)
		_vg_lock_count--;
	else
		_vg_lock_count++;

	/* We don't bother to reset this until all VG locks are dropped */
	if ((flags & LCK_TYPE_MASK) == LCK_WRITE)
		_vg_write_lock_held = 1;
	else if (!_vg_lock_count)
		_vg_write_lock_held = 0;
}

/*
 * A mess of options have been introduced over time to override
 * or tweak the behavior of file locking, and indirectly other
 * behaviors.  These options are allowed in different but
 * overlapping sets of commands (see command-lines.in)
 *
 * --nolocking
 *
 * Command won't try to set up or use file locks at all.
 *
 * --readonly
 *
 * Command will grant any read lock request, without trying
 * to acquire an actual file lock.  Command will refuse any
 * write lock request.  (Activation, which uses a write lock,
 * is not allowed.)
 *
 * --ignorelockingfailure
 *
 * Command tries to set up file locks and will use them
 * (both read and write) if successful.  If command fails
 * to set up file locks it falls back to readonly behavior
 * above, while allowing activation.
 *
 * --sysinit
 *
 * The same as ignorelockingfailure.
 *
 * --sysinit --readonly
 *
 * The combination of these two flags acts like --readonly,
 * refusing write lock requests, but makes an exception to
 * allow activation.
 *
 * global/metadata_read_only
 *
 * The command acquires actual read locks and refuses
 * write lock requests.
 */

int init_locking(struct cmd_context *cmd,
		 int file_locking_sysinit, int file_locking_readonly, int file_locking_ignorefail)
{
	int suppress_messages = 0;

	if (file_locking_sysinit || getenv("LVM_SUPPRESS_LOCKING_FAILURE_MESSAGES"))
		suppress_messages = 1;

	_blocking_supported = find_config_tree_bool(cmd, global_wait_for_locks_CFG, NULL);
	_file_locking_readonly = file_locking_readonly;
	_file_locking_sysinit = file_locking_sysinit;
	_file_locking_ignorefail = file_locking_ignorefail;

	log_debug("File locking settings: readonly:%d sysinit:%d ignorelockingfailure:%d global/metadata_read_only:%d global/wait_for_locks:%d.",
		  _file_locking_readonly, _file_locking_sysinit, _file_locking_ignorefail,
		  cmd->metadata_read_only, _blocking_supported);

	if (!init_file_locking(&_locking, cmd, suppress_messages)) {
		log_error_suppress(suppress_messages, "File locking initialisation failed.");

		_file_locking_failed = 1;

		if (file_locking_sysinit || file_locking_ignorefail)
			return 1;

		return 0;
	}

	return 1;
}

void fin_locking(struct cmd_context *cmd)
{
	/* file locking disabled */
	if (!_locking.flags)
		return;

	/*
	 * These may be automatically released when the
	 * command ends, without an explicit unlock call,
	 * in which case these flags would not be cleared.
	 */
	cmd->lockf_global_ex = 0;
	cmd->lockd_global_ex = 0;

	_locking.fin_locking();
}

/*
 * VG locking is by VG name.
 * FIXME This should become VG uuid.
 */
static int _lock_vol(struct cmd_context *cmd, const char *resource, uint32_t flags)
{
	int ret = 0;

	block_signals(flags);

	if ((ret = _locking.lock_resource(cmd, resource, flags, NULL)))
		/* ensure signals are blocked while VG_GLOBAL lock is held */
		_update_vg_lock_count(resource, flags);
	else
		stack;

	_unblock_signals();

	return ret;
}

int lock_vol(struct cmd_context *cmd, const char *vol, uint32_t flags, const struct logical_volume *lv)
{
	char resource[258] __attribute__((aligned(8)));
	uint32_t lck_type = flags & LCK_TYPE_MASK;
	int is_global = !strcmp(vol, VG_GLOBAL);

	if (is_orphan_vg(vol))
		return 1;

	if (!_blocking_supported)
		flags |= LCK_NONBLOCK;

	if (!_dm_strncpy(resource, vol, sizeof(resource))) {
		log_error(INTERNAL_ERROR "Resource name %s is too long.", vol);
		return 0;
	}

	/*
	 * File locking is disabled by --nolocking.
	 */
	if (!_locking.flags)
		goto out_hold;

	/*
	 * When file locking could not be initialized, --ignorelockingfailure
	 * and --sysinit behave like --readonly, but allow activation.
	 */
	if (_file_locking_failed && (_file_locking_sysinit || _file_locking_ignorefail)) {
		if (lck_type != LCK_WRITE)
			goto out_hold;

		if (cmd->is_activating && !is_global)
			goto out_hold;

		goto out_fail;
	}

	/*
	 * When --readonly is set, grant read lock requests without trying to
	 * acquire an actual lock, and refuse write lock requests.
	 */
	if (_file_locking_readonly && !_file_locking_sysinit) {
		if (lck_type != LCK_WRITE)
			goto out_hold;

		log_error("Operation prohibited while --readonly is set.");
		goto out_fail;
	}

	/*
	 * When --readonly and --sysinit are set, grant read lock requests without
	 * trying to acquire an actual lock, and refuse write lock requests except
	 * in the case of activation which is permitted.
	 */
	if (_file_locking_readonly && _file_locking_sysinit) {
		if (lck_type != LCK_WRITE)
			goto out_hold;

		if (cmd->is_activating) {
			log_warn("Allowing activation with --readonly --sysinit.");
			goto out_hold;
		}

		log_error("Operation prohibited while --readonly is set.");
		goto out_fail;
	}

	/*
	 * When global/metadata_read_only is set, acquire actual read locks and
	 * refuse write lock requests.
	 */
	if (cmd->metadata_read_only) {
		if (lck_type == LCK_WRITE) {
			log_error("Operation prohibited while global/metadata_read_only is set.");
			goto out_fail;
		}

		/* continue and acquire a read file lock */
	}

	if (!_lock_vol(cmd, resource, flags))
		goto out_fail;

out_hold:
	if (is_global)
		return 1;

	/*
	 * FIXME: other parts of the code want to check if a VG is
	 * locked by looking in lvmcache.  They shouldn't need to
	 * do that, and we should be able to remove this.
	 */
	if (lck_type != LCK_UNLOCK)
		lvmcache_lock_vgname(resource, lck_type == LCK_READ);
	else if (lck_type == LCK_UNLOCK)
		lvmcache_unlock_vgname(resource);

	return 1;

out_fail:
	if (is_global)
		return 0;
	if (lck_type == LCK_UNLOCK)
		_update_vg_lock_count(resource, flags);
	return 0;
}

/* Lock a list of LVs */
int activate_lvs(struct cmd_context *cmd, struct dm_list *lvs, unsigned exclusive)
{
	struct dm_list *lvh;
	struct lv_list *lvl;

	dm_list_iterate_items(lvl, lvs) {
		if (!activate_lv(cmd, lvl->lv)) {
			log_error("Failed to activate %s", display_lvname(lvl->lv));

			dm_list_uniterate(lvh, lvs, &lvl->list) {
				lvl = dm_list_item(lvh, struct lv_list);
				if (!deactivate_lv(cmd, lvl->lv))
					stack;
			}
			return 0;
		}
	}

	return 1;
}

int vg_write_lock_held(void)
{
	return _vg_write_lock_held;
}

int sync_local_dev_names(struct cmd_context* cmd)
{
	dm_devs_cache_destroy();
	memlock_unlock(cmd);
	fs_unlock();
	return 1;
}

/*
 * The lockf_global_ex flag is used to prevent changing
 * an explicitly acquired ex global lock to sh in process_each.
 */

static int _lockf_global(struct cmd_context *cmd, const char *mode, int convert, int nonblock)
{
	uint32_t flags = 0;
	int ret;

	if (convert)
		flags |= LCK_CONVERT;

	if (nonblock)
		flags |= LCK_NONBLOCK;

	if (!strcmp(mode, "ex")) {
		flags |= LCK_WRITE;

		if (cmd->lockf_global_ex) {
			log_warn("global flock already held ex");
			return 1;
		}

		ret = lock_vol(cmd, VG_GLOBAL, flags, NULL);
		if (ret)
			cmd->lockf_global_ex = 1;

	} else if (!strcmp(mode, "sh")) {
		if (cmd->lockf_global_ex)
			return 1;

		flags |= LCK_READ;

		ret = lock_vol(cmd, VG_GLOBAL, flags, NULL);

	} else if (!strcmp(mode, "un")) {
		ret = lock_vol(cmd, VG_GLOBAL, LCK_UNLOCK, NULL);
		cmd->lockf_global_ex = 0;
	} else {
		log_error(INTERNAL_ERROR "Unknown locking mode %s.", mode);
		return 0;
	}

	return ret;
}

int lockf_global(struct cmd_context *cmd, const char *mode)
{
	return _lockf_global(cmd, mode, 0, 0);
}

int lockf_global_convert(struct cmd_context *cmd, const char *mode)
{
	/* some uncommon cases like pvchange -a can call this multiple times */
	if (cmd->lockf_global_ex && !strcmp(mode, "ex"))
		return 1;

	return _lockf_global(cmd, mode, 1, 0);
}

int lockf_global_nonblock(struct cmd_context *cmd, const char *mode)
{
	return _lockf_global(cmd, mode, 0, 1);
}

int lock_global(struct cmd_context *cmd, const char *mode)
{
	if (!lockf_global(cmd, mode))
		return 0;

	if (!lockd_global(cmd, mode)) {
		lockf_global(cmd, "un");
		return 0;
	}

	return 1;
}

/*
 * The global lock is already held, convert it to another mode.
 *
 * Currently only used for sh->ex.
 *
 * (The lockf_global_ex flag would need overriding
 * to handle ex->sh.)
 */

int lock_global_convert(struct cmd_context *cmd, const char *mode)
{
	if (!lockf_global_convert(cmd, mode))
		return 0;

	if (!lockd_global(cmd, mode))
		return 0;

	return 1;
}
