# Copyright (C) 2015-2016 Red Hat, Inc. All rights reserved.
#
# 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 General Public License v.2.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from subprocess import Popen, PIPE
import time
import threading
from itertools import chain
import collections
import traceback
import os

from lvmdbusd import cfg
from lvmdbusd.utils import pv_dest_ranges, log_debug, log_error, add_no_notify
from lvmdbusd.lvm_shell_proxy import LVMShellProxy

try:
	import simplejson as json
except ImportError:
	import json

SEP = '{|}'

total_time = 0.0
total_count = 0

# We need to prevent different threads from using the same lvm shell
# at the same time.
cmd_lock = threading.RLock()


class LvmExecutionMeta(object):

	def __init__(self, start, ended, cmd, ec, stdout_txt, stderr_txt):
		self.lock = threading.RLock()
		self.start = start
		self.ended = ended
		self.cmd = cmd
		self.ec = ec
		self.stdout_txt = stdout_txt
		self.stderr_txt = stderr_txt

	def __str__(self):
		with self.lock:
			return "EC= %d for %s\n" \
				"STARTED: %f, ENDED: %f\n" \
				"STDOUT=%s\n" \
				"STDERR=%s\n" % \
				(self.ec, str(self.cmd), self.start, self.ended, self.stdout_txt,
				self.stderr_txt)


class LvmFlightRecorder(object):

	def __init__(self, size=16):
		self.queue = collections.deque(maxlen=size)

	def add(self, lvm_exec_meta):
		self.queue.append(lvm_exec_meta)

	def dump(self):
		with cmd_lock:
			if len(self.queue):
				log_error("LVM dbus flight recorder START")
				for c in self.queue:
					log_error(str(c))
				log_error("LVM dbus flight recorder END")


cfg.blackbox = LvmFlightRecorder()


def _debug_c(cmd, exit_code, out):
	log_error('CMD= %s' % ' '.join(cmd))
	log_error(("EC= %d" % exit_code))
	log_error(("STDOUT=\n %s\n" % out[0]))
	log_error(("STDERR=\n %s\n" % out[1]))


def call_lvm(command, debug=False):
	"""
	Call an executable and return a tuple of exitcode, stdout, stderr
	:param command:     Command to execute
	:param debug:       Dump debug to stdout
	"""
	# print 'STACK:'
	# for line in traceback.format_stack():
	#    print line.strip()

	# Prepend the full lvm executable so that we can run different versions
	# in different locations on the same box
	command.insert(0, cfg.LVM_CMD)
	command = add_no_notify(command)

	process = Popen(command, stdout=PIPE, stderr=PIPE, close_fds=True,
					env=os.environ)
	out = process.communicate()

	stdout_text = bytes(out[0]).decode("utf-8")
	stderr_text = bytes(out[1]).decode("utf-8")

	if debug or process.returncode != 0:
		_debug_c(command, process.returncode, (stdout_text, stderr_text))

	return process.returncode, stdout_text, stderr_text

# The actual method which gets called to invoke the lvm command, can vary
# from forking a new process to using lvm shell
_t_call = call_lvm


def _shell_cfg():
	global _t_call
	# noinspection PyBroadException
	try:
		lvm_shell = LVMShellProxy()
		_t_call = lvm_shell.call_lvm
		cfg.SHELL_IN_USE = lvm_shell
		return True
	except Exception:
		_t_call = call_lvm
		cfg.SHELL_IN_USE = None
		log_error(traceback.format_exc())
		log_error("Unable to utilize lvm shell, dropping back to fork & exec")
		return False


def set_execution(shell):
	global _t_call
	with cmd_lock:
		# If the user requested lvm shell and we are currently setup that
		# way, just return
		if cfg.SHELL_IN_USE and shell:
			return True
		else:
			if not shell and cfg.SHELL_IN_USE:
				cfg.SHELL_IN_USE.exit_shell()
				cfg.SHELL_IN_USE = None

		_t_call = call_lvm
		if shell:
			if cfg.args.use_json:
				return _shell_cfg()
			else:
				return False
		return True


def time_wrapper(command, debug=False):
	global total_time
	global total_count

	with cmd_lock:
		start = time.time()
		results = _t_call(command, debug)
		ended = time.time()
		total_time += (ended - start)
		total_count += 1
		cfg.blackbox.add(LvmExecutionMeta(start, ended, command, *results))
	return results


call = time_wrapper


# Default cmd
# Place default arguments for every command here.
def _dc(cmd, args):
	c = [cmd, '--noheading', '--separator', '%s' % SEP, '--nosuffix',
		'--unbuffered', '--units', 'b']
	c.extend(args)
	return c


def parse(out):
	rc = []

	for line in out.split('\n'):
		# This line includes separators, so process them
		if SEP in line:
			elem = line.split(SEP)
			cleaned_elem = []
			for e in elem:
				e = e.strip()
				cleaned_elem.append(e)

			if len(cleaned_elem) > 1:
				rc.append(cleaned_elem)
		else:
			t = line.strip()
			if len(t) > 0:
				rc.append(t)
	return rc


def parse_column_names(out, column_names):
	lines = parse(out)
	rc = []

	for i in range(0, len(lines)):
		d = dict(list(zip(column_names, lines[i])))
		rc.append(d)

	return rc


def options_to_cli_args(options):
	rc = []
	for k, v in list(dict(options).items()):
		if k.startswith("-"):
			rc.append(k)
		else:
			rc.append("--%s" % k)
		if v != "":
			rc.append(str(v))
	return rc


def pv_remove(device, remove_options):
	cmd = ['pvremove']
	cmd.extend(options_to_cli_args(remove_options))
	cmd.append(device)
	return call(cmd)


def _qt(tag_name):
	return '@%s' % tag_name


def _tag(operation, what, add, rm, tag_options):
	cmd = [operation]
	cmd.extend(options_to_cli_args(tag_options))

	if isinstance(what, list):
		cmd.extend(what)
	else:
		cmd.append(what)

	if add:
		cmd.extend(list(chain.from_iterable(
			('--addtag', _qt(x)) for x in add)))
	if rm:
		cmd.extend(list(chain.from_iterable(
			('--deltag', _qt(x)) for x in rm)))

	return call(cmd, False)


def pv_tag(pv_devices, add, rm, tag_options):
	return _tag('pvchange', pv_devices, add, rm, tag_options)


def vg_tag(vg_name, add, rm, tag_options):
	return _tag('vgchange', vg_name, add, rm, tag_options)


def lv_tag(lv_name, add, rm, tag_options):
	return _tag('lvchange', lv_name, add, rm, tag_options)


def vg_rename(vg, new_name, rename_options):
	cmd = ['vgrename']
	cmd.extend(options_to_cli_args(rename_options))
	cmd.extend([vg, new_name])
	return call(cmd)


def vg_remove(vg_name, remove_options):
	cmd = ['vgremove']
	cmd.extend(options_to_cli_args(remove_options))
	cmd.extend(['-f', vg_name])
	return call(cmd)


def vg_lv_create(vg_name, create_options, name, size_bytes, pv_dests):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(create_options))
	cmd.extend(['--size', str(size_bytes) + 'B'])
	cmd.extend(['--name', name, vg_name, '--yes'])
	pv_dest_ranges(cmd, pv_dests)
	return call(cmd)


def vg_lv_snapshot(vg_name, snapshot_options, name, size_bytes):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(snapshot_options))
	cmd.extend(["-s"])

	if size_bytes != 0:
		cmd.extend(['--size', str(size_bytes) + 'B'])

	cmd.extend(['--name', name, vg_name])
	return call(cmd)


def _vg_lv_create_common_cmd(create_options, size_bytes, thin_pool):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(create_options))

	if not thin_pool:
		cmd.extend(['--size', str(size_bytes) + 'B'])
	else:
		cmd.extend(['--thin', '--size', str(size_bytes) + 'B'])

	cmd.extend(['--yes'])
	return cmd


def vg_lv_create_linear(vg_name, create_options, name, size_bytes, thin_pool):
	cmd = _vg_lv_create_common_cmd(create_options, size_bytes, thin_pool)
	cmd.extend(['--name', name, vg_name])
	return call(cmd)


def vg_lv_create_striped(vg_name, create_options, name, size_bytes,
							num_stripes, stripe_size_kb, thin_pool):
	cmd = _vg_lv_create_common_cmd(create_options, size_bytes, thin_pool)
	cmd.extend(['--stripes', str(num_stripes)])

	if stripe_size_kb != 0:
		cmd.extend(['--stripesize', str(stripe_size_kb)])

	cmd.extend(['--name', name, vg_name])
	return call(cmd)


def _vg_lv_create_raid(vg_name, create_options, name, raid_type, size_bytes,
						num_stripes, stripe_size_kb):
	cmd = ['lvcreate']

	cmd.extend(options_to_cli_args(create_options))

	cmd.extend(['--type', raid_type])
	cmd.extend(['--size', str(size_bytes) + 'B'])

	if num_stripes != 0:
		cmd.extend(['--stripes', str(num_stripes)])

	if stripe_size_kb != 0:
		cmd.extend(['--stripesize', str(stripe_size_kb)])

	cmd.extend(['--name', name, vg_name, '--yes'])
	return call(cmd)


def vg_lv_create_raid(vg_name, create_options, name, raid_type, size_bytes,
						num_stripes, stripe_size_kb):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(create_options))

	return _vg_lv_create_raid(vg_name, create_options, name, raid_type,
								size_bytes, num_stripes, stripe_size_kb)


def vg_lv_create_mirror(
		vg_name, create_options, name, size_bytes, num_copies):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(create_options))

	cmd.extend(['--type', 'mirror'])
	cmd.extend(['--mirrors', str(num_copies)])
	cmd.extend(['--size', str(size_bytes) + 'B'])
	cmd.extend(['--name', name, vg_name, '--yes'])
	return call(cmd)


def vg_create_cache_pool(md_full_name, data_full_name, create_options):
	cmd = ['lvconvert']
	cmd.extend(options_to_cli_args(create_options))
	cmd.extend(['--type', 'cache-pool', '--force', '-y',
				'--poolmetadata', md_full_name, data_full_name])
	return call(cmd)


def vg_create_thin_pool(md_full_name, data_full_name, create_options):
	cmd = ['lvconvert']
	cmd.extend(options_to_cli_args(create_options))
	cmd.extend(['--type', 'thin-pool', '--force', '-y',
				'--poolmetadata', md_full_name, data_full_name])
	return call(cmd)


def lv_remove(lv_path, remove_options):
	cmd = ['lvremove']
	cmd.extend(options_to_cli_args(remove_options))
	cmd.extend(['-f', lv_path])
	return call(cmd)


def lv_rename(lv_path, new_name, rename_options):
	cmd = ['lvrename']
	cmd.extend(options_to_cli_args(rename_options))
	cmd.extend([lv_path, new_name])
	return call(cmd)


def lv_resize(lv_full_name, size_change, pv_dests,
				resize_options):
	cmd = ['lvresize', '--force']

	cmd.extend(options_to_cli_args(resize_options))

	if size_change < 0:
		cmd.append("-L-%dB" % (-size_change))
	else:
		cmd.append("-L+%dB" % (size_change))

	cmd.append(lv_full_name)
	pv_dest_ranges(cmd, pv_dests)
	return call(cmd)


def lv_lv_create(lv_full_name, create_options, name, size_bytes):
	cmd = ['lvcreate']
	cmd.extend(options_to_cli_args(create_options))
	cmd.extend(['--virtualsize', str(size_bytes) + 'B', '-T'])
	cmd.extend(['--name', name, lv_full_name, '--yes'])
	return call(cmd)


def lv_cache_lv(cache_pool_full_name, lv_full_name, cache_options):
	# lvconvert --type cache --cachepool VG/CachePoolLV VG/OriginLV
	cmd = ['lvconvert']
	cmd.extend(options_to_cli_args(cache_options))
	cmd.extend(['-y', '--type', 'cache', '--cachepool',
				cache_pool_full_name, lv_full_name])
	return call(cmd)


def lv_detach_cache(lv_full_name, detach_options, destroy_cache):
	cmd = ['lvconvert']
	if destroy_cache:
		option = '--uncache'
	else:
		# Currently fairly dangerous
		# see: https://bugzilla.redhat.com/show_bug.cgi?id=1248972
		option = '--splitcache'
	cmd.extend(options_to_cli_args(detach_options))
	# needed to prevent interactive questions
	cmd.extend(["--yes", "--force"])
	cmd.extend([option, lv_full_name])
	return call(cmd)


def supports_json():
	cmd = ['help']
	rc, out, err = call(cmd)
	if rc == 0:
		if cfg.SHELL_IN_USE:
			return True
		else:
			if 'fullreport' in err:
				return True
	return False


def lvm_full_report_json():
	pv_columns = ['pv_name', 'pv_uuid', 'pv_fmt', 'pv_size', 'pv_free',
					'pv_used', 'dev_size', 'pv_mda_size', 'pv_mda_free',
					'pv_ba_start', 'pv_ba_size', 'pe_start', 'pv_pe_count',
					'pv_pe_alloc_count', 'pv_attr', 'pv_tags', 'vg_name',
					'vg_uuid', 'pv_missing']

	pv_seg_columns = ['pvseg_start', 'pvseg_size', 'segtype',
						'pv_uuid', 'lv_uuid', 'pv_name']

	vg_columns = ['vg_name', 'vg_uuid', 'vg_fmt', 'vg_size', 'vg_free',
					'vg_sysid', 'vg_extent_size', 'vg_extent_count',
					'vg_free_count', 'vg_profile', 'max_lv', 'max_pv',
					'pv_count', 'lv_count', 'snap_count', 'vg_seqno',
					'vg_mda_count', 'vg_mda_free', 'vg_mda_size',
					'vg_mda_used_count', 'vg_attr', 'vg_tags']

	lv_columns = ['lv_uuid', 'lv_name', 'lv_path', 'lv_size',
				'vg_name', 'pool_lv_uuid', 'pool_lv', 'origin_uuid',
				'origin', 'data_percent',
				'lv_attr', 'lv_tags', 'vg_uuid', 'lv_active', 'data_lv',
				'metadata_lv', 'lv_parent', 'lv_role', 'lv_layout',
				'snap_percent', 'metadata_percent', 'copy_percent',
				'sync_percent', 'lv_metadata_size', 'move_pv', 'move_pv_uuid']

	lv_seg_columns = ['seg_pe_ranges', 'segtype', 'lv_uuid']

	cmd = _dc('fullreport', [
		'-a',		# Need hidden too
		'--configreport', 'pv', '-o', ','.join(pv_columns),
		'--configreport', 'vg', '-o', ','.join(vg_columns),
		'--configreport', 'lv', '-o', ','.join(lv_columns),
		'--configreport', 'seg', '-o', ','.join(lv_seg_columns),
		'--configreport', 'pvseg', '-o', ','.join(pv_seg_columns),
		'--reportformat', 'json'
	])

	rc, out, err = call(cmd)
	if rc == 0:
		# With the current implementation, if we are using the shell then we
		# are using JSON and JSON is returned back to us as it was parsed to
		# figure out if we completed OK or not
		if cfg.SHELL_IN_USE:
			assert(type(out) == dict)
			return out
		else:
			return json.loads(out)
	return None


def pv_retrieve_with_segs(device=None):
	d = []
	err = ""
	out = ""
	rc = 0

	columns = ['pv_name', 'pv_uuid', 'pv_fmt', 'pv_size', 'pv_free',
				'pv_used', 'dev_size', 'pv_mda_size', 'pv_mda_free',
				'pv_ba_start', 'pv_ba_size', 'pe_start', 'pv_pe_count',
				'pv_pe_alloc_count', 'pv_attr', 'pv_tags', 'vg_name',
				'vg_uuid', 'pvseg_start', 'pvseg_size', 'segtype', 'pv_missing']

	# Lvm has some issues where it returns failure when querying pvs when other
	# operations are in process, see:
	# https://bugzilla.redhat.com/show_bug.cgi?id=1274085
	for i in range(0, 10):
		cmd = _dc('pvs', ['-o', ','.join(columns)])

		if device:
			cmd.extend(device)

		rc, out, err = call(cmd)

		if rc == 0:
			d = parse_column_names(out, columns)
			break
		else:
			time.sleep(0.2)
			log_debug("LVM Bug workaround, retrying pvs command...")

	if rc != 0:
		msg = "We were unable to get pvs to return without error after " \
			"trying 10 times, RC=%d, STDERR=(%s), STDOUT=(%s)" % \
			(rc, err, out)
		log_error(msg)
		raise RuntimeError(msg)

	return d


def pv_resize(device, size_bytes, create_options):
	cmd = ['pvresize']

	cmd.extend(options_to_cli_args(create_options))

	if size_bytes != 0:
		cmd.extend(['--yes', '--setphysicalvolumesize', str(size_bytes) + 'B'])

	cmd.extend([device])
	return call(cmd)


def pv_create(create_options, devices):
	cmd = ['pvcreate', '-ff']
	cmd.extend(options_to_cli_args(create_options))
	cmd.extend(devices)
	return call(cmd)


def pv_allocatable(device, yes, allocation_options):
	yn = 'n'

	if yes:
		yn = 'y'

	cmd = ['pvchange']
	cmd.extend(options_to_cli_args(allocation_options))
	cmd.extend(['-x', yn, device])
	return call(cmd)


def pv_scan(activate, cache, device_paths, major_minors, scan_options):
	cmd = ['pvscan']
	cmd.extend(options_to_cli_args(scan_options))

	if activate:
		cmd.extend(['--activate', "ay"])

	if cache:
		cmd.append('--cache')

		if len(device_paths) > 0:
			for d in device_paths:
				cmd.append(d)

		if len(major_minors) > 0:
			for mm in major_minors:
				cmd.append("%s:%s" % (mm))

	return call(cmd)


def vg_create(create_options, pv_devices, name):
	cmd = ['vgcreate']
	cmd.extend(options_to_cli_args(create_options))
	cmd.append(name)
	cmd.extend(pv_devices)
	return call(cmd)


def vg_change(change_options, name):
	cmd = ['vgchange']
	cmd.extend(options_to_cli_args(change_options))
	cmd.append(name)
	return call(cmd)


def vg_reduce(vg_name, missing, pv_devices, reduce_options):
	cmd = ['vgreduce']
	cmd.extend(options_to_cli_args(reduce_options))

	if missing:
		cmd.append('--removemissing')
	elif len(pv_devices) == 0:
		cmd.append('--all')

	cmd.append(vg_name)
	cmd.extend(pv_devices)
	return call(cmd)


def vg_extend(vg_name, extend_devices, extend_options):
	cmd = ['vgextend']
	cmd.extend(options_to_cli_args(extend_options))
	cmd.append(vg_name)
	cmd.extend(extend_devices)
	return call(cmd)


def _vg_value_set(name, arguments, options):
	cmd = ['vgchange']
	cmd.extend(options_to_cli_args(options))
	cmd.append(name)
	cmd.extend(arguments)
	return call(cmd)


def vg_allocation_policy(vg_name, policy, policy_options):
	return _vg_value_set(vg_name, ['--alloc', policy], policy_options)


def vg_max_pv(vg_name, number, max_options):
	return _vg_value_set(vg_name, ['--maxphysicalvolumes', str(number)],
							max_options)


def vg_max_lv(vg_name, number, max_options):
	return _vg_value_set(vg_name, ['-l', str(number)], max_options)


def vg_uuid_gen(vg_name, ignore, options):
	assert ignore is None
	return _vg_value_set(vg_name, ['--uuid'], options)


def activate_deactivate(op, name, activate, control_flags, options):
	cmd = [op]
	cmd.extend(options_to_cli_args(options))

	op = '-a'

	if control_flags:
		# Autoactivation
		if (1 << 0) & control_flags:
			op += 'a'
		# Exclusive locking (Cluster)
		if (1 << 1) & control_flags:
			op += 'e'

		# Local node activation
		if (1 << 2) & control_flags:
			op += 'l'

		# Activation modes
		if (1 << 3) & control_flags:
			cmd.extend(['--activationmode', 'complete'])
		elif (1 << 4) & control_flags:
			cmd.extend(['--activationmode', 'partial'])

		# Ignore activation skip
		if (1 << 5) & control_flags:
			cmd.append('--ignoreactivationskip')

	if activate:
		op += 'y'
	else:
		op += 'n'

	cmd.append(op)
	cmd.append(name)
	return call(cmd)


def vg_retrieve(vg_specific):
	if vg_specific:
		assert isinstance(vg_specific, list)

	columns = ['vg_name', 'vg_uuid', 'vg_fmt', 'vg_size', 'vg_free',
				'vg_sysid', 'vg_extent_size', 'vg_extent_count',
				'vg_free_count', 'vg_profile', 'max_lv', 'max_pv',
				'pv_count', 'lv_count', 'snap_count', 'vg_seqno',
				'vg_mda_count', 'vg_mda_free', 'vg_mda_size',
				'vg_mda_used_count', 'vg_attr', 'vg_tags']

	cmd = _dc('vgs', ['-o', ','.join(columns)])

	if vg_specific:
		cmd.extend(vg_specific)

	d = []
	rc, out, err = call(cmd)
	if rc == 0:
		d = parse_column_names(out, columns)

	return d


def lv_retrieve_with_segments():
	columns = ['lv_uuid', 'lv_name', 'lv_path', 'lv_size',
				'vg_name', 'pool_lv_uuid', 'pool_lv', 'origin_uuid',
				'origin', 'data_percent',
				'lv_attr', 'lv_tags', 'vg_uuid', 'lv_active', 'data_lv',
				'metadata_lv', 'seg_pe_ranges', 'segtype', 'lv_parent',
				'lv_role', 'lv_layout',
				'snap_percent', 'metadata_percent', 'copy_percent',
				'sync_percent', 'lv_metadata_size', 'move_pv', 'move_pv_uuid']

	cmd = _dc('lvs', ['-a', '-o', ','.join(columns)])
	rc, out, err = call(cmd)

	d = []

	if rc == 0:
		d = parse_column_names(out, columns)

	return d


if __name__ == '__main__':
	pv_data = pv_retrieve_with_segs()

	for p in pv_data:
		print(str(p))
