# -*- coding: utf-8 -*- """ libs.tool ~~~~~~~~~~~~~~ 工具类 :copyright: (c) 2018 by taochengwei. :license: Apache2.0, see LICENSE for more details. """ from __future__ import absolute_import from utils.tool import logger, get_current_timestamp, create_mysql_engine from utils.aes_cbc import CBC as KeyGenerationClass import re import time import json import MySQLdb import MySQLdb.cursors class IncetDB(object): """封装的Inception操作类""" def __init__(self, host, port, max_idle_time=8*3600, connect_timeout=3, **kwargs): #打开数据库连接 self.db = None self.host = host self.port = port self.max_idle_time = float(max_idle_time) self.last_use_time = time.time() self.kwargs = dict(host=host, port=port, charset="utf8", connect_timeout=connect_timeout, cursorclass=MySQLdb.cursors.DictCursor, **kwargs) try: self.connect() except Exception: raise def __repr__(self): try: version = self.db.get_server_info() except Exception: return "<Server: %s, Status: disconnected>" % self.host else: return "<Server: %s, Status: connected, Version: %s>" % (self.host, version) def __del__(self): self.close() def __ensure_connected(self): if (self.db is None or (time.time() - self.last_use_time > self.max_idle_time)): self.connect() self.last_use_time = time.time() def connect(self): self.close() self.db = MySQLdb.connect(**self.kwargs) self.db.autocommit = True def close(self): """Closes this database connection.""" if getattr(self, "db", None) is not None: self.db.close() self.db = None def execute(self, sql, *args, **kwargs): #获取操作游标 self.__ensure_connected() cur = self.db.cursor() try: cur.execute(sql, kwargs or args) ret = cur.fetchall() except MySQLdb.MySQLError: raise else: return ret finally: cur.close() def get(self, sql, *args, **kwargs): rows = self.execute(sql, *args, **kwargs) if not rows: return None elif len(rows) > 1: raise Exception("Multiple rows returned for Database.get() query") else: return rows[0] def parse_inception_result(result, scene): """解析inception的结果集""" if scene == 1: # 获取sqlsha1列表, errlevel列表 return dict(sqlsha1=[ i['sqlsha1'] for i in result if i and isinstance(i, dict) and i.get('sqlsha1') ], errlevel=[ i['errlevel'] for i in result if i and isinstance(i, dict) ]) elif scene == 2: # 获取errlevel列表和errormessage return dict(errormessage=[ i['errormessage'] for i in result if i and isinstance(i, dict) and i.get('errormessage') and i.get('errormessage') != 'None' ], errlevel=[ i['errlevel'] for i in result if i and isinstance(i, dict) ], stagestatus=[ i['stagestatus'] for i in result if i and isinstance(i, dict) and i.get('stagestatus') and i.get('stagestatus') != 'None' ]) elif scene == 3: # 获取执行结果中的backup_dbname, sequence, errlevel return dict(backup_dbname=[ i['backup_dbname'] for i in result if i and isinstance(i, dict) and i.get('backup_dbname') and i.get('backup_dbname') != 'None' ], errlevel=[ i['errlevel'] for i in result if i and isinstance(i, dict) ], sequence=[ i['sequence'].replace("'","").replace('"','') for i in result if i and isinstance(i, dict) and i.get('sequence') and "".join(i.get('sequence').replace("'","").replace('"','').split('_')[:2]) != '00' ]) def check_sql(sqlContent): """针对多行情况检测sql语句""" if sqlContent: return sqlContent.rstrip().endswith(';') return False def check_ipport(connect): pat = re.compile(r'^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\:([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$') if connect and pat.match(connect): return True def sql_criticalddl_check(sql_content): """ 识别DROP DATABASE, DROP TABLE, TRUNCATE PARTITION, TRUNCATE TABLE等高危DDL操作,因为对于这些操作,inception在备份时只能备份METADATA,而不会备份数据! 如果识别到包含高危操作,则返回“审核不通过” :return msg str: OK表示可以通过审核; 其他字符串表示含有高危ddl,审核不通过,为具体拒绝信息。 """ for row in sql_content.rstrip(';').split(';'): if re.match(r"([\s\S]*)drop(\s+)database(\s+.*)|([\s\S]*)drop(\s+)table(\s+.*)|([\s\S]*)truncate(\s+)partition(\s+.*)|([\s\S]*)truncate(\s+)table(\s+.*)", row.lower()): return "Reject High Danger SQL." return "OK" def sql_inceptionpre_check(sql_content): """ 在提交给inception之前,预先识别一些Inception不能正确审核的SQL 比如"alter table t1;"或"alter table test.t1;" 以免导致inception core dump :param sql_content: :return: msg str: OK是预检测通过; 其他信息不通过。 2. msg str: false是提示信息 """ for row in sql_content.rstrip(';').split(';'): if re.match(r"(\s*)alter(\s+)table(\s+)(\S+)(\s*);|(\s*)alter(\s+)table(\s+)(\S+)\.(\S+)(\s*);", row.lower() + ";"): return "Pre-detect sql syntax error." return "OK" def InceptionProxy(Action, sqlContent, dbData, inception, **kwargs): """Inception代理器-通过参数使用Inception服务处理SQL @kwargs: sd(str), 任务描述 applicant(str),申请人 autoviewResult(list),自动审核结果 enableRemoteBackup,enableIgnoreWarnings(int),启用备份、禁用警告 """ res = dict(code=1, msg=None) aes = KeyGenerationClass("YRRGBRYQqrV1gv5A") #此处key值保持与service.py中DBService一致,否则无法解密 if True: #检查参数 if Action in ("Check", "Execute") and sqlContent and inception and check_sql(sqlContent) == True and check_ipport(inception): #高危sql检测 scc = sql_criticalddl_check(sqlContent) if scc == "OK": # 未发现高危sql; 执行预检测 sic = sql_inceptionpre_check(sqlContent) if sic == "OK": # 预检测通过; 先通过dbId查出mysql if dbData and isinstance(dbData, dict) and "host" in dbData and "user" in dbData and "port" in dbData and "passwd" in dbData: dbData["passwd"] = unicode(aes.decrypt(dbData["passwd"])) # 根据不同Action,使用inception执行 idb = IncetDB(host=inception.split(":")[0], port=int(inception.split(":")[-1])) mysql = create_mysql_engine() if Action == "Check": # 通过Inception检查任务中sql sql = u'''/*--user=%s;--password=%s;--host=%s;--port=%s;--enable-check;*/ inception_magic_start; %s inception_magic_commit;''' %(dbData["user"], dbData["passwd"], dbData["host"], dbData["port"], sqlContent) result = idb.execute(sql) logger.debug(result) if result is None or len(result) == 0: res.update(code=2, msg="The return of Inception is null. May be something wrong with the SQL") else: res.update(data=result, code=0) elif Action == "Execute": # 通过Inception立即执行任务中sql,请先执行Check后再使用Execute ebiw, taskId = "", kwargs["taskId"] if int(kwargs.get("enableRemoteBackup", 1)) == 1: ebiw += "--enable-remote-backup;" else: ebiw += "--disable-remote-backup;" if int(kwargs.get("enableIgnoreWarnings", 0)) == 1: ebiw += "--enable-ignore-warnings;" sql = u'''/*--user=%s;--password=%s;--host=%s;--port=%s;--enable-execute;%s*/ inception_magic_start; %s inception_magic_commit;''' %(dbData["user"], dbData["passwd"], dbData["host"], dbData["port"], ebiw, sqlContent) logger.debug(sql) if taskId: # 设置状态为执行中,并设置执行时间 mysql.update("update incetops_task set status=3,ftime=%s where taskId=%s", get_current_timestamp(), taskId) result = idb.execute(sql) logger.debug(result) if result is None or len(result) == 0: status, statusMsg = 4, "Execution result is empty" res.update(code=2, msg="The return of Inception is null. May be something wrong with the SQL") else: # 分析执行结果 pir = parse_inception_result(result, 2) statusMsg = "\n".join(pir["errormessage"]) + "\n" if 2 in pir["errlevel"]: status = 4 elif 1 in pir["errlevel"]: statusMsg += "\n".join(pir["stagestatus"]) status = 4 if int(kwargs.get("enableIgnoreWarnings", 0)) == 0 else 0 else: status, statusMsg = 0, "\n".join(pir["stagestatus"]) res.update(data=result, code=0) mysql.update("update incetops_task set status=%s,statusMsg=%s,executeResult=%s where taskId=%s", status, statusMsg, json.dumps(result), taskId) else: res.update(msg="Invalid taskId") else: res.update(msg="Invaild dbId") else: # 预检测出sql通过inception执行可能会core dump,拒绝审核通过 res.update(msg=sic) else: # 发现高危sql,拒绝审核通过 res.update(msg=scc) else: cs = check_sql(sqlContent) res.update(msg="There are invalid parameters" if cs == False else cs) return res def InceptionOSC(Action, sqlsha1, inception): """查询pt-osc执行进度""" res = dict(code=1, msg=None) if Action in ("Query", "Stop") and sqlsha1 and inception and check_ipport(inception) and len(sqlsha1) == 41 and sqlsha1.startswith('*'): if Action == "Query": # 通过Inception查询osc sql = 'inception get osc_percent %s;' idb = IncetDB(host=inception.split(":")[0], port=int(inception.split(":")[-1])) result = idb.get(sql, sqlsha1) if not result: res.update(code=2, msg="No query to OSC progress") else: res.update(data=result, code=0) elif Action == "Stop": # 通过Inception中止OSC执行 sql = 'inception stop alter %s;' idb = IncetDB(host=inception.split(":")[0], port=int(inception.split(":")[-1])) try: result = idb.get(sql, sqlsha1) logger.debug("stop osc result " + result) except Exception,e: res.update(msg=str(e)) else: if not result: res.update(code=2, msg="No query to OSC progress") else: res.update(data=result, code=0) else: res.update(msg="There are invalid parameters") return res def QueryRollbackSQL(taskId, backup_mysql_url): """查询任务执行成功后的SQL回滚语句""" res = dict(code=1, msg=None) if taskId and isinstance(taskId, int) and backup_mysql_url: dbbackup = create_mysql_engine(backup_mysql_url) dblocal = create_mysql_engine() sql = "SELECT executeResult FROM incetops_task WHERE taskId=%s AND status=0" data = dblocal.get(sql, taskId) if data and isinstance(data, dict) and data.get("executeResult"): result = json.loads(data["executeResult"]) rollback_sqls = [] for row in result: if row.get('backup_dbname') and row.get('backup_dbname') != 'None': backup_dbname = row.get("backup_dbname") opid_time = row.get("sequence").replace("'", "") rowdata = dict(opid_time=opid_time, backup_dbname=backup_dbname, sql=None, rollback=None, errmsg=None) # 以下是实际查询操作 sql_table = "select sql_statement,tablename from {}.$_$Inception_backup_information$_$ where opid_time=%s".format(backup_dbname) try: table_data = dbbackup.get(sql_table, opid_time) except Exception,e: logger.warning(e) res.update(msg=str(e)) else: if not table_data: rowdata["errmsg"] = "Get table data error" else: rowdata["sql"] = table_data["sql_statement"] sql_back = "select rollback_statement from {}.{} where opid_time=%s".format(backup_dbname, table_data['tablename']) back_data = dbbackup.query(sql_back, opid_time) if not back_data: rowdata["errmsg"] = "Get rollback data error" else: rowdata["rollback"] = "\n".join([ i["rollback_statement"] for i in back_data if isinstance(i, dict) and "rollback_statement" in i ]) rollback_sqls.append(rowdata) else: continue if rollback_sqls: res.update(code=0, data=rollback_sqls) else: res.update(code=2, msg=res["msg"] or "No rollback sql") else: res.update(msg="Invaild task") else: res.update(msg="Invaild params") return res