#!/usr/bin/python -E


from __future__ import print_function
import os
import errno
import shutil
import sys
from optparse import OptionParser

import ctypes

sepol = ctypes.cdll.LoadLibrary('libsepol.so.1')

try:
	import selinux
	import semanage
except:
	print("You must install libselinux-python and libsemanage-python before running this tool", file=sys.stderr)
	exit(1)


def copy_file(src, dst):
	if DEBUG:
		print("copying %s to %s" % (src, dst))
	try:
		shutil.copy(src, dst)
	except OSError as the_err:
		(err, strerr) = the_err.args
		print("Could not copy %s to %s, %s" %(src, dst, strerr), file=sys.stderr)
		exit(1)


def create_dir(dst, mode):
	if DEBUG: print("Making directory %s" % dst)
	try:
		os.makedirs(dst, mode)
	except OSError as the_err:
		(err, stderr) = the_err.args
		if err == errno.EEXIST:
			pass
		else:
			print("Error creating %s" % dst, file=sys.stderr)
			exit(1)


def create_file(dst):
	if DEBUG: print("Making file %s" % dst)
	try:
		open(dst, 'a').close()
	except OSError as the_err:
		(err, stderr) = the_err.args
		print("Error creating %s" % dst, file=sys.stderr)
		exit(1)


def copy_module(store, name, base):
	if DEBUG: print("Install module %s" % name)
	(file, ext) = os.path.splitext(name)
	if ext != ".pp":
		# Stray non-pp file in modules directory, skip
		print("warning: %s has invalid extension, skipping" % name, file=sys.stderr)
		return
	try:
		if base:
			root = oldstore_path(store)
		else:
			root = oldmodules_path(store)

		bottomdir = bottomdir_path(store)

		os.mkdir("%s/%s" % (bottomdir, file))

		copy_file(os.path.join(root, name), "%s/%s/hll" % (bottomdir, file))

		# This is the ext file that will eventually be used to choose a compiler
		efile = open("%s/%s/lang_ext" % (bottomdir, file), "w+", 0o600)
		efile.write("pp")
		efile.close()

	except:
		print("Error installing module %s" % name, file=sys.stderr)
		exit(1)


def disable_module(file, name, disabledmodules):
	if DEBUG: print("Disabling %s" % name)
	(disabledname, disabledext) = os.path.splitext(file)
	create_file("%s/%s" % (disabledmodules, disabledname))

def migrate_store(store):

	oldstore = oldstore_path(store);
	oldmodules = oldmodules_path(store);
	disabledmodules = disabledmodules_path(store);
	newstore = newstore_path(store);
	newmodules = newmodules_path(store);
	bottomdir = bottomdir_path(store);

	print("Migrating from %s to %s" % (oldstore, newstore))

	# Build up new directory structure
	create_dir("%s/%s" % (newroot_path(), store), 0o755)
	create_dir(newstore, 0o700)
	create_dir(newmodules, 0o700)
	create_dir(bottomdir, 0o700)
	create_dir(disabledmodules, 0o700)

	# Special case for base since it was in a different location
	copy_module(store, "base.pp", 1)

	# Dir structure built, start copying files
	for root, dirs, files in os.walk(oldstore):
		if root == oldstore:
			# This is the top level directory, need to move
			for name in files:
				# Check to see if it is in TOPPATHS and copy if so
				if name in TOPPATHS:
					if name == "seusers":
						newname = "seusers.local"
					else:
						newname = name
					copy_file(os.path.join(root, name), os.path.join(newstore, newname))

		elif root == oldmodules:
			# This should be the modules directory
			for name in files:
				(file, ext) = os.path.splitext(name)
				if name == "base.pp":
					print("Error installing module %s, name conflicts with base" % name, file=sys.stderr)
					exit(1)
				elif ext == ".disabled":
					disable_module(file, name, disabledmodules)
				else:
					copy_module(store, name, 0)

def rebuild_policy():
	# Ok, the modules are loaded, lets try to rebuild the policy
	print("Attempting to rebuild policy from %s" % newroot_path())

	curstore = selinux.selinux_getpolicytype()[1]

	handle = semanage.semanage_handle_create()
	if not handle:
		print("Could not create semanage handle", file=sys.stderr)
		exit(1)

	semanage.semanage_select_store(handle, curstore, semanage.SEMANAGE_CON_DIRECT)

	if not semanage.semanage_is_managed(handle):
		semanage.semanage_handle_destroy(handle)
		print("SELinux policy is not managed or store cannot be accessed.", file=sys.stderr)
		exit(1)

	rc = semanage.semanage_access_check(handle)
	if rc < semanage.SEMANAGE_CAN_WRITE:
		semanage.semanage_handle_destroy(handle)
		print("Cannot write to policy store.", file=sys.stderr)
		exit(1)

	rc = semanage.semanage_connect(handle)
	if rc < 0:
		semanage.semanage_handle_destroy(handle)
		print("Could not establish semanage connection", file=sys.stderr)
		exit(1)

	semanage.semanage_set_rebuild(handle, 1)

	rc = semanage.semanage_begin_transaction(handle)
	if rc < 0:
		semanage.semanage_handle_destroy(handle)
		print("Could not begin transaction", file=sys.stderr)
		exit(1)

	rc = semanage.semanage_commit(handle)
	if rc < 0:
		print("Could not commit transaction", file=sys.stderr)

	semanage.semanage_handle_destroy(handle)


def oldroot_path():
	return "%s/etc/selinux" % ROOT

def oldstore_path(store):
	return "%s/%s/modules/active" % (oldroot_path(), store)

def oldmodules_path(store):
	return "%s/modules" % oldstore_path(store)

def disabledmodules_path(store):
	return "%s/disabled" % newmodules_path(store)

def newroot_path():
	return "%s%s" % (ROOT, PATH)

def newstore_path(store):
	return "%s/%s/active" % (newroot_path(), store)

def newmodules_path(store):
	return "%s/modules" % newstore_path(store)

def bottomdir_path(store):
	return "%s/%s" % (newmodules_path(store), PRIORITY)


if __name__ == "__main__":

	parser = OptionParser()
	parser.add_option("-p", "--priority", dest="priority", default="100",
			  help="Set priority of modules in new store (default: 100)")
	parser.add_option("-s", "--store", dest="store", default=None,
			  help="Store to read from and write to")
	parser.add_option("-d", "--debug", dest="debug", action="store_true", default=False,
			  help="Output debug information")
	parser.add_option("-c", "--clean", dest="clean", action="store_true", default=False,
			  help="Clean old modules directory after migrate (default: no)")
	parser.add_option("-n", "--norebuild", dest="norebuild", action="store_true", default=False,
			  help="Disable rebuilding policy after migration (default: no)")
	parser.add_option("-P", "--path", dest="path",
			  help="Set path for the policy store (default: /var/lib/selinux)")
	parser.add_option("-r", "--root", dest="root",
			  help="Set an alternative root for the migration (default: /)")

	(options, args) = parser.parse_args()

	DEBUG = options.debug
	PRIORITY = options.priority
	TYPE = options.store
	CLEAN = options.clean
	NOREBUILD = options.norebuild
	PATH = options.path
	if PATH is None:
		PATH = "/var/lib/selinux"

	ROOT = options.root
	if ROOT is None:
		ROOT = ""

	# List of paths that go in the active 'root'
	TOPPATHS = [
		"commit_num",
		"ports.local",
		"interfaces.local",
		"nodes.local",
		"booleans.local",
		"file_contexts.local",
		"seusers",
		"users.local",
		"users_extra",
		"users_extra.local",
		"disable_dontaudit",
		"preserve_tunables",
		"policy.kern",
		"file_contexts",
		"homedir_template"]


	create_dir(newroot_path(), 0o755)

	stores = None
	if TYPE is not None:
		stores = [TYPE]
	else:
		stores = os.listdir(oldroot_path())

	# find stores in oldroot and migrate them to newroot if necessary
	for store in stores:
		if not os.path.isdir(oldmodules_path(store)):
			# already migrated or not an selinux store
			continue

		if os.path.isdir(newstore_path(store)):
			# store has already been migrated, but old modules dir still exits
			print("warning: Policy type %s has already been migrated, but modules still exist in the old store. Skipping store." % store, file=sys.stderr)
			continue

		migrate_store(store)

		if CLEAN is True:
			def remove_error(function, path, execinfo):
				print("warning: Unable to remove old store modules directory %s. Cleaning failed." % oldmodules_path(store), file=sys.stderr)
			shutil.rmtree(oldmodules_path(store), onerror=remove_error)

	if NOREBUILD is False:
		rebuild_policy()