#!/usr/bin/env python

# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
# 
#   http://www.apache.org/licenses/LICENSE-2.0
# 
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied.  See the License for the
# specific language governing permissions and limitations
# under the License.


import os,logging,sys
from optparse import OptionParser
import MySQLdb
import subprocess
import glob

# ---- This snippet of code adds the sources path and the waf configured PYTHONDIR to the Python path ----
# ---- We do this so cloud_utils can be looked up in the following order:
# ---- 1) Sources directory
# ---- 2) waf configured PYTHONDIR
# ---- 3) System Python path
for pythonpath in (
		"/usr/lib/python2.7/dist-packages",
		os.path.join(os.path.dirname(__file__),os.path.pardir,os.path.pardir,"python","lib"),
	):
		if os.path.isdir(pythonpath): sys.path.insert(0,pythonpath)
# ---- End snippet of code ----
from cloud_utils import check_selinux, CheckFailed, resolves_to_ipv6
import cloud_utils

# RUN ME LIKE THIS
# python setup/bindir/cloud-migrate-databases.in --config=client/tomcatconf/override/db.properties --resourcedir=setup/db  --dry-run
# --dry-run makes it so the changes to the database in the context of the migrator are rolled back

# This program / library breaks down as follows:
#   high-level breakdown:
#   the module calls main()
#   main processes command-line options
#   main() instantiates a migrator with a a list of possible migration steps
#   migrator discovers and topologically sorts migration steps from the given list
#   main() run()s the migrator
#      for each one of the migration steps:
#          the migrator instantiates the migration step with the context as first parameter
#          the instantiated migration step saves the context onto itself as self.context
#          the migrator run()s the instantiated migration step.  within run(), self.context is the context
#      the migrator commits the migration context to the database (or rollsback if --dry-run is specified)
#   that is it

# The specific library code is in cloud_utils.py
# What needs to be implemented is MigrationSteps
# Specifically in the FromInitialTo21 evolver.
# What Db20to21MigrationUtil.java does, needs to be done within run() of that class
# refer to the class docstring to find out how
# implement them below

class CloudContext(cloud_utils.MigrationContext):
	def __init__(self,host,port,username,password,database,configdir,resourcedir):
		self.host = host
		self.port = port
		self.username = username
		self.password = password
		self.database = database
		self.configdir = configdir
		self.resourcedir = resourcedir
		self.conn = MySQLdb.connect(host=self.host,
			user=self.username,
			passwd=self.password,
			db=self.database,
			port=self.port)
		self.conn.autocommit(False)
		self.db = self.conn.cursor()
		def wrapex(func):
			sqlogger = logging.getLogger("SQL")
			def f(stmt,parms=None):
				if parms: sqlogger.debug("%s | with parms %s",stmt,parms)
				else: sqlogger.debug("%s",stmt)
				return func(stmt,parms)
			return f
		self.db.execute = wrapex(self.db.execute)
	
	def __str__(self):
		return "CloudStack %s database at %s"%(self.database,self.host)
	
	def get_schema_level(self):
		return self.get_config_value('schema.level') or cloud_utils.INITIAL_LEVEL
	
	def set_schema_level(self,l):
		self.db.execute(
		"INSERT INTO configuration (category,instance,component,name,value,description) VALUES ('Hidden', 'DEFAULT', 'database', 'schema.level', %s, 'The schema level of this database') ON DUPLICATE KEY UPDATE value = %s", (l,l)
		)
		self.commit()

	def commit(self):
		self.conn.commit()
		#self.conn.close()
		
	def close(self):
		self.conn.close()
		
	def get_config_value(self,name):
		self.db.execute("select value from configuration where name = %s",(name,))
		try: return self.db.fetchall()[0][0]
		except IndexError: return
		
	def run_sql_resource(self,resource):
		sqlfiletext = file(os.path.join(self.resourcedir,resource)).read(-1)
		sqlstatements = sqlfiletext.split(";")
		for stmt in sqlstatements:
			if not stmt.strip(): continue # skip empty statements
			self.db.execute(stmt)


class FromInitialTo21NewSchema(cloud_utils.MigrationStep):
	def __str__(self): return "Altering the database schema"
	from_level = cloud_utils.INITIAL_LEVEL
	to_level = "2.1-01"
	def run(self): self.context.run_sql_resource("schema-20to21.sql")

class From21NewSchemaTo21NewSchemaPlusIndex(cloud_utils.MigrationStep):
	def __str__(self): return "Altering indexes"
	from_level = "2.1-01"
	to_level = "2.1-02"
	def run(self): self.context.run_sql_resource("index-20to21.sql")

class From21NewSchemaPlusIndexTo21DataMigratedPart1(cloud_utils.MigrationStep):
	def __str__(self): return "Performing data migration, stage 1"
	from_level = "2.1-02"
	to_level = "2.1-03"
	def run(self):	self.context.run_sql_resource("data-20to21.sql")

class From21step1toTo21datamigrated(cloud_utils.MigrationStep):
	def __str__(self): return "Performing data migration, stage 2"
	from_level = "2.1-03"
	to_level = "2.1-04"
	
	def run(self):
		systemjars = "commons-collections.jar commons-dbcp.jar commons-codec.jar commons-logging.jar commons-logging-api.jar commons-pool.jar commons-httpclient.jar ws-commons-util.jar mysql-connector-java.jar servlet-api-2.5.jar jsp-api-2.1.jar jasper.jar el-api-2.1.jar jasper-el.jar ecj.jar cglib.jar asm3.jar jsch.jar backport-util-concurrent.jar jetty.jar jetty-util.jar jetty-start-daemon.jar jetty-sslengine.jar jetty-start.jar jna.jar log4j-1.2.jar".split()
		pipe = subprocess.Popen(["build-classpath"]+systemjars,stdout=subprocess.PIPE)
		systemcp,throwaway = pipe.communicate()
		systemcp = systemcp.strip()
		if pipe.wait(): # this means that build-classpath failed miserably
			systemcp = "/usr/share/java/commons-collections.jar:/usr/share/java/commons-dbcp.jar:/usr/share/java/commons-codec.jar:/usr/share/java/commons-logging.jar:/usr/share/java/commons-logging-api.jar:/usr/share/java/commons-pool.jar:/usr/share/java/commons-httpclient.jar:/usr/share/java/ws-commons-util.jar:/usr/share/java/mysql-connector-java.jar:/usr/share/java/servlet-api-2.5.jar:/usr/share/java/jsp-api-2.1.jar:/usr/share/java/jasper.jar:/usr/share/java/el-api-2.1.jar:/usr/share/java/jasper-el.jar:/usr/share/java/ecj.jar:/usr/share/java/cglib.jar:/usr/share/java/asm3.jar:/usr/share/java/jsch.jar:/usr/share/java/backport-util-concurrent.jar:/usr/share/java/jetty.jar:/usr/share/java/jetty-util.jar:/usr/share/java/jetty-start-daemon.jar:/usr/share/java/jetty-sslengine.jar:/usr/share/java/jetty-start.jar:/usr/share/java/jna.jar:/usr/share/java/log4j-1.2.jar"
		pcp = os.path.pathsep.join( glob.glob( os.path.join ( "/usr/share/java/cloud-premium" , "*" ) ) )
		mscp = "/usr/share/java/cloud-utils.jar:/usr/share/java/cloud-api.jar:/usr/share/java/cloud-core.jar:/usr/share/java/cloud-server.jar:/usr/share/java/cloud-server-extras.jar:/usr/share/java/cloud-core-extras.jar:/usr/share/java/cloud-vmware-base.jar:/usr/share/java/cloud-ovm.jar:/usr/share/java/cloud-dp-user-concentrated-pod.jar:/usr/share/java/cloud-dp-user-dispersing.jar:/usr/share/java/cloud-host-allocator-random.jar:/usr/share/java/cloud-plugin-f5.jar:/usr/share/java/cloud-plugin-netscaler.jar:/usr/share/java/cloud-plugin-ovs.jar:/usr/share/java/cloud-plugin-srx.jar:/usr/share/java/cloud-storage-allocator-random.jar:/usr/share/java/cloud-user-authenticator-ldap.jar:/usr/share/java/cloud-user-authenticator-md5.jar:/usr/share/java/cloud-user-authenticator-plaintext.jar:/usr/share/java/cloud-vmware.jar:/usr/share/java/cloud-plugin-hypervisor-xen.jar:/usr/share/java/cloud-plugin-nicira-nvp.jar:/usr/share/java/cloud-plugin-elb.jar:/usr/share/java/cloud-plugin-netapp.jar"
		depscp = "/usr/share/java/not-yet-commons-ssl-0.3.9.jar:/usr/share/java/geronimo-stax-api_1.0_spec-1.0.1.jar:/usr/share/java/esapi-2.0GA.jar:/usr/share/java/activation-1.1.jar:/usr/share/java/axiom-api-1.2.8.jar:/usr/share/java/xmlrpc-client-3.1.3.jar:/usr/share/java/annogen-0.1.0.jar:/usr/share/java/commons-logging-1.1.1.jar:/usr/share/java/gson-1.7.1.jar:/usr/share/java/wss4j-1.5.8.jar:/usr/share/java/jsr107cache-1.0.jar:/usr/share/java/ant-launcher-1.7.0.jar:/usr/share/java/commons-codec-1.6.jar:/usr/share/java/oro-2.0.8.jar:/usr/share/java/httpcore-nio-4.0-beta1.jar:/usr/share/java/httpcore-4.0.jar:/usr/share/java/axis-1.4.jar:/usr/share/java/axis2-ant-plugin-1.4.1.jar:/usr/share/java/axiom-impl-1.2.7.jar:/usr/share/java/log4j-1.2.16.jar:/usr/share/java/commons-configuration-1.8.jar:/usr/share/java/commons-lang-2.1.jar:/usr/share/java/xml-apis-1.3.04.jar:/usr/share/java/commons-pool-1.6.jar:/usr/share/java/xml-resolver-1.2.jar:/usr/share/java/jsch-0.1.42.jar:/usr/share/java/axis2-adb-1.4.1.jar:/usr/share/java/mex-1.5.1-impl.jar:/usr/share/java/commons-collections-3.2.1.jar:/usr/share/java/jul-to-slf4j-1.6.1.jar:/usr/share/java/geronimo-activation_1.1_spec-1.0.1.jar:/usr/share/java/opensaml-2.5.1-1.jar:/usr/share/java/axis2-1.5.1.jar:/usr/share/java/libvirt-0.4.9.jar:/usr/share/java/jetty-6.1.26.jar:/usr/share/java/wsdl4j-1.6.2.jar:/usr/share/java/woden-impl-dom-1.0M8.jar:/usr/share/java/bcprov-jdk14-140.jar:/usr/share/java/cglib-nodep-2.2.2.jar:/usr/share/java/jmdns-2.1.jar:/usr/share/java/commons-discovery-0.5.jar:/usr/share/java/joda-time-1.5.2.jar:/usr/share/java/log4j-over-slf4j-1.6.1.jar:/usr/share/java/ws-commons-util-1.0.2.jar:/usr/share/java/hamcrest-core-1.1.jar:/usr/share/java/jasypt-1.9.0.jar:/usr/share/java/xmlParserAPIs-2.6.0.jar:/usr/share/java/servlet-api-2.5-20081211.jar:/usr/share/java/commons-httpclient-3.1.jar:/usr/share/java/axiom-dom-1.2.7.jar:/usr/share/java/slf4j-api-1.5.11.jar:/usr/share/java/bcprov-jdk16-1.45.jar:/usr/share/java/jcl-over-slf4j-1.6.1.jar:/usr/share/java/mail-1.4.jar:/usr/share/java/servlet-api-2.3.jar:/usr/share/java/javax.persistence-2.0.0.jar:/usr/share/java/axis2-kernel-1.4.1.jar:/usr/share/java/ehcache-1.5.0.jar:/usr/share/java/axis2-mtompolicy-1.5.1.jar:/usr/share/java/commons-fileupload-1.2.jar:/usr/share/java/junit-4.10.jar:/usr/share/java/ejb-api-3.0.jar:/usr/share/java/backport-util-concurrent-3.1.jar:/usr/share/java/xmlsec-1.4.2.jar:/usr/share/java/xercesImpl-2.8.1.jar:/usr/share/java/velocity-1.5.jar:/usr/share/java/xmltooling-1.3.1.jar:/usr/share/java/apache-log4j-extras-1.1.jar:/usr/share/java/XmlSchema-1.4.2.jar:/usr/share/java/axis2-java2wsdl-1.4.1.jar:/usr/share/java/jaxen-1.1.1.jar:/usr/share/java/axis2-adb-codegen-1.4.1.jar:/usr/share/java/woden-api-1.0M8.jar:/usr/share/java/xalan-2.7.0.jar:/usr/share/java/geronimo-jms_1.1_spec-1.1.jar:/usr/share/java/jstl-1.2.jar:/usr/share/java/ant-1.7.0.jar:/usr/share/java/CAStorSDK-1.3.1-CS40.jar:/usr/share/java/wstx-asl-3.2.4.jar:/usr/share/java/commons-io-1.4.jar:/usr/share/java/rampart-trust-1.5.jar:/usr/share/java/openws-1.4.1.jar:/usr/share/java/axis2-codegen-1.4.1.jar:/usr/share/java/commons-dbcp-1.4.jar:/usr/share/java/slf4j-jdk14-1.5.11.jar:/usr/share/java/json-simple-1.1.jar:/usr/share/java/bcprov-jdk15-1.45.jar:/usr/share/java/geronimo-javamail_1.4_spec-1.2.jar:/usr/share/java/xpp3_min-1.1.4c.jar:/usr/share/java/xmlrpc-common-3.1.3.jar:/usr/share/java/neethi-2.0.4.jar:/usr/share/java/jetty-util-6.1.26.jar:/usr/share/java/opensaml-1.1.jar:/usr/share/java/trilead-ssh2-build213-svnkit-1.3-patch.jar:/usr/share/java/axis-jaxrpc-1.4.jar:/usr/share/java/xstream-1.3.1.jar:/usr/share/java/xapi-5.6.100-1-SNAPSHOT.jar:/usr/share/java/jcip-annotations-1.0.jar:/usr/share/java/rampart-policy-1.5.jar:/usr/share/java/mysql-connector-java-5.1.21.jar:/usr/share/java/rampart-core-1.5.jar"
		migrationxml = "/etc/cloud/server"
		conf = self.context.configdir
		cp = os.path.pathsep.join([pcp,systemcp,depscp,mscp,migrationxml,conf])
		cmd = ["java"]
		cmd += ["-cp",cp]
		cmd += ["com.cloud.migration.Db20to21MigrationUtil"]
		logging.debug("Running command: %s"," ".join(cmd))
		subprocess.check_call(cmd)

class From21datamigratedTo21postprocessed(cloud_utils.MigrationStep):
	def __str__(self): return "Postprocessing migrated data"
	from_level = "2.1-04"
	to_level = "2.1"
	def run(self): self.context.run_sql_resource("postprocess-20to21.sql")

class From21To213(cloud_utils.MigrationStep):
	def __str__(self): return "Dropping obsolete indexes"
	from_level = "2.1"
	to_level = "2.1.3"
	def run(self): self.context.run_sql_resource("index-212to213.sql")

class From213To22data(cloud_utils.MigrationStep):
	def __str__(self): return "Migrating data"
	from_level = "2.1.3"
	to_level = "2.2-01"
	def run(self): self.context.run_sql_resource("data-21to22.sql")

class From22dataTo22(cloud_utils.MigrationStep):
	def __str__(self): return "Migrating indexes"
	from_level = "2.2-01"
	to_level = "2.2"
	def run(self): self.context.run_sql_resource("index-21to22.sql")

# command line harness functions

def setup_logging(level):
	l = logging.getLogger()
	l.setLevel(level)
	h = logging.StreamHandler(sys.stderr)
	l.addHandler(h)


def setup_optparse():
	usage = \
"""%prog [ options ... ]

This command migrates the CloudStack database."""
	parser = OptionParser(usage=usage)
	parser.add_option("-c", "--config", action="store", type="string",dest='configdir',
		default=os.path.join("/etc/cloud/management"),
		help="Configuration directory with a db.properties file, pointing to the CloudStack database")
	parser.add_option("-r", "--resourcedir", action="store", type="string",dest='resourcedir',
		default="/usr/share/cloud/setup",
		help="Resource directory with database SQL files used by the migration process")
	parser.add_option("-d", "--debug", action="store_true", dest='debug',
		default=False,
		help="Increase log level from INFO to DEBUG")
	parser.add_option("-e", "--dump-evolvers", action="store_true", dest='dumpevolvers',
		default=False,
		help="Dump evolvers in the order they would be executed, but do not run them")
	#parser.add_option("-n", "--dry-run", action="store_true", dest='dryrun',
		#default=False,
		#help="Run the process as it would normally run, but do not commit the final transaction, so database changes are never saved")
	parser.add_option("-f", "--start-at-level", action="store", type="string",dest='fromlevel',
		default=None,
		help="Rather than discovering the database schema level to start from, start migration from this level.  The special value '-' (a dash without quotes) represents the earliest schema level")
	parser.add_option("-t", "--end-at-level", action="store", type="string",dest='tolevel',
		default=None,
		help="Rather than evolving the database to the most up-to-date level, end migration at this level")
	return parser


def main(*args):
	"""The entry point of this program"""
	
	parser = setup_optparse()
	opts, args = parser.parse_args(*args)
	if args: parser.error("This command accepts no parameters")

	if opts.debug: loglevel = logging.DEBUG
	else: loglevel = logging.INFO
	setup_logging(loglevel)
	
	# FIXME implement
	opts.dryrun = False

	configdir = opts.configdir
	resourcedir = opts.resourcedir
	
	try:
		props = cloud_utils.read_properties(os.path.join(configdir,'db.properties'))
	except (IOError,OSError),e:
		logging.error("Cannot read from config file: %s",e)
		logging.error("You may want to point to a specific config directory with the --config= option")
		return 2
	
	if not os.path.isdir(resourcedir):
		logging.error("Cannot find directory with SQL files %s",resourcedir)
		logging.error("You may want to point to a specific resource directory with the --resourcedir= option")
		return 2
	
	host = props["db.cloud.host"]
	port = int(props["db.cloud.port"])
	username = props["db.cloud.username"]
	password = props["db.cloud.password"]
	database = props["db.cloud.name"]
	
	# tell the migrator to load its steps from the globals list
	migrator = cloud_utils.Migrator(globals().values())
	
	if opts.dumpevolvers:
		print "Evolution steps:"
		print "	%s	%s	%s"%("From","To","Evolver in charge")
		for f,t,e in migrator.get_evolver_chain():
			print "	%s	%s	%s"%(f,t,e)
		return
	
	#initialize a context with the read configuration
	context = CloudContext(host=host,port=port,username=username,password=password,database=database,configdir=configdir,resourcedir=resourcedir)
	try:
	    try:
		migrator.run(context,dryrun=opts.dryrun,starting_level=opts.fromlevel,ending_level=opts.tolevel)
	    finally:
		context.close()
	except (cloud_utils.NoMigrationPath,cloud_utils.NoMigrator),e:
		logging.error("%s",e)
		return 4

if __name__ == "__main__":
	retval = main()
	if retval: sys.exit(retval)
	else: sys.exit()
