331 lines
13 KiB
Python
Executable File
331 lines
13 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding:utf-8 -*-
|
|
#############################################################################
|
|
# Copyright (c) 2022 Huawei Technologies Co.,Ltd.
|
|
#
|
|
# openGauss is licensed under Mulan PSL v2.
|
|
# You can use this software according to the terms
|
|
# and conditions of the Mulan PSL v2.
|
|
# You may obtain a copy of Mulan PSL v2 at:
|
|
#
|
|
# http://license.coscl.org.cn/MulanPSL2
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OF ANY KIND,
|
|
# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
|
# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
|
# See the Mulan PSL v2 for more details.
|
|
# ----------------------------------------------------------------------------
|
|
# Description : cm_install is a utility to deploy CM tool to openGauss database cluster.
|
|
#############################################################################
|
|
|
|
import cmd
|
|
import getopt
|
|
import os
|
|
import sys
|
|
import re
|
|
import subprocess
|
|
import xml.etree.cElementTree as ETree
|
|
from CMLog import CMLog
|
|
from Common import *
|
|
from ErrorCode import ErrorCode
|
|
from InstallImpl import InstallImpl
|
|
|
|
sys.path.append(sys.path[0] + "/../../")
|
|
|
|
class Install:
|
|
"""
|
|
The class is used to do perform installation
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.envFile = ""
|
|
self.xmlFile = ""
|
|
self.gaussHome = ""
|
|
self.gaussLog = ""
|
|
self.toolPath = ""
|
|
self.tmpPath = ""
|
|
self.cmDirs = []
|
|
self.hostnames = []
|
|
self.localhostName = ""
|
|
self.cmpkg = ""
|
|
self.nodesInfo = dict()
|
|
self.clusterStopped = False
|
|
self.maxTerm = 0
|
|
|
|
def getLocalhostName(self):
|
|
import socket
|
|
self.localhostName = socket.gethostname()
|
|
|
|
def getEnvParams(self):
|
|
self.gaussHome = getEnvParam(self.envFile, "GAUSSHOME")
|
|
self.gaussLog = getEnvParam(self.envFile, "GAUSSLOG")
|
|
self.toolPath = getEnvParam(self.envFile, "GPHOME")
|
|
self.tmpPath = getEnvParam(self.envFile, "PGHOST")
|
|
|
|
def checkExeUser(self):
|
|
if os.getuid() == 0:
|
|
CMLog.exitWithError(ErrorCode.GAUSS_501["GAUSS_50105"])
|
|
|
|
def usage(self):
|
|
"""
|
|
cm_install is a utility to deploy CM tool to openGauss database cluster.
|
|
|
|
Usage:
|
|
cm_install -? | --help
|
|
cm_install -X XMLFILE [-e envFile] --cmpkg=cmpkgPath
|
|
General options:
|
|
-X Path of the XML configuration file.
|
|
-e Path of env file.
|
|
Default value "~/.bashrc".
|
|
--cmpkg Path of CM pacakage.
|
|
-?, --help Show help information for this
|
|
utility, and exit the command line mode.
|
|
"""
|
|
print(self.usage.__doc__)
|
|
|
|
def parseCommandLine(self):
|
|
if len(sys.argv) == 1:
|
|
self.usage()
|
|
sys.exit(1)
|
|
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], "?X:e:", ["help", "cmpkg="])
|
|
except getopt.GetoptError as e:
|
|
CMLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50000"] % str(e))
|
|
|
|
for opt, value in opts:
|
|
if opt in ("-?", "--help"):
|
|
self.usage()
|
|
sys.exit(0)
|
|
elif opt in ("-X"):
|
|
self.xmlFile = value
|
|
elif opt in ("-e"):
|
|
self.envFile = value
|
|
elif opt in ("--cmpkg"):
|
|
self.cmpkg = value
|
|
|
|
def checkParam(self):
|
|
if self.xmlFile == "":
|
|
CMLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50001"] % 'X' + ".")
|
|
checkXMLFile(self.xmlFile)
|
|
|
|
if self.cmpkg == "":
|
|
CMLog.exitWithError(ErrorCode.GAUSS_500["GAUSS_50001"] % '-cmpkg' + ".")
|
|
if not os.path.exists(self.cmpkg):
|
|
CMLog.exitWithError(ErrorCode.GAUSS_502["GAUSS_50201"] % self.cmpkg)
|
|
if not os.path.isfile(self.cmpkg):
|
|
CMLog.exitWithError(ErrorCode.GAUSS_502["GAUSS_50210"] % ("cmpkg " + self.cmpkg))
|
|
|
|
if self.envFile == "":
|
|
self.envFile = os.path.join(os.environ['HOME'], ".bashrc")
|
|
if not os.path.exists(self.envFile):
|
|
CMLog.exitWithError(ErrorCode.GAUSS_502["GAUSS_50201"] % ("envFile " + self.envFile))
|
|
if not os.path.isfile(self.envFile):
|
|
CMLog.exitWithError(ErrorCode.GAUSS_502["GAUSS_50210"] % ("envFile " + self.envFile))
|
|
mppdbEnv = getEnvParam(self.envFile, "MPPDB_ENV_SEPARATE_PATH")
|
|
if mppdbEnv != "":
|
|
self.envFile = mppdbEnv
|
|
if self.envFile == "" or not os.path.exists(self.envFile) or not os.path.isfile(self.envFile):
|
|
CMLog.exitWithError(ErrorCode.GAUSS_518["GAUSS_51802"] % 'MPPDB_ENV_SEPARATE_PATH' + ".")
|
|
|
|
def checkOm(self):
|
|
"""
|
|
check whether there is om tool
|
|
"""
|
|
cmd = "source %s; gs_om --version" % self.envFile
|
|
status, output = subprocess.getstatusoutput(cmd)
|
|
if status != 0:
|
|
errorDetail = "\nCommand: %s\nStatus: %s\nOutput: %s\n" % (
|
|
cmd, status, output)
|
|
self.logger.logExit("OM tool is required." + errorDetail)
|
|
|
|
def checkXMLFileSecurity(self):
|
|
"""
|
|
function : check XML contain DTDs
|
|
input : String
|
|
output : NA
|
|
"""
|
|
# Check xml for security requirements
|
|
# if it have "<!DOCTYPE" or it have "<!ENTITY",
|
|
# exit and print "File have security risks."
|
|
try:
|
|
with open(self.xmlFile, "r", encoding='utf-8') as fb:
|
|
lines = fb.readlines()
|
|
for line in lines:
|
|
if re.findall("<!DOCTYPE", line) or re.findall("<!ENTITY", line):
|
|
raise Exception("File have security risks.")
|
|
except Exception as e:
|
|
raise Exception(str(e))
|
|
|
|
def initParserXMLFile(self):
|
|
"""
|
|
function : Init parser xml file
|
|
input : String
|
|
output : Object
|
|
"""
|
|
try:
|
|
# check xml for security requirements
|
|
self.checkXMLFileSecurity()
|
|
dom_tree = ETree.parse(self.xmlFile)
|
|
rootNode = dom_tree.getroot()
|
|
except Exception as e:
|
|
raise Exception(ErrorCode.GAUSS_512["GAUSS_51236"] + " Error: \n%s." % str(e))
|
|
return rootNode
|
|
|
|
def getInfoListOfAllNodes(self):
|
|
"""
|
|
get hostname and cmDir list of all nodes
|
|
check other CM infos in xml
|
|
TODO: check the consistence of xml and installed cluster.
|
|
"""
|
|
self.localhostName = getLocalhostName()
|
|
|
|
# get hostnames and port from static file
|
|
cmd = "source %s; gs_om -t view" % self.envFile
|
|
status, output = subprocess.getstatusoutput(cmd)
|
|
if status != 0:
|
|
self.logger.logExit((ErrorCode.GAUSS_514["GAUSS_51400"] % cmd) + \
|
|
"\nStatus:%d\nOutput:" + output)
|
|
nodesStaticInfoStr = re.split("azName.*:.*", output)
|
|
if len(nodesStaticInfoStr) == 0:
|
|
self.logger.logExit("Failed to get cluster info from static file.")
|
|
if len(nodesStaticInfoStr) < 2:
|
|
self.logger.logExit("CM is not supported in single instance.")
|
|
nodesStaticInfo = nodesStaticInfoStr[1:]
|
|
for nodeInfo in nodesStaticInfo:
|
|
if nodeInfo == "":
|
|
continue
|
|
nodename = re.findall("nodeName:(.*)", nodeInfo)[0]
|
|
self.hostnames.append(nodename)
|
|
dataPath = re.findall("datanodeLocalDataPath.*:(.*)", nodeInfo)[0]
|
|
port = re.findall("datanodePort.*:(.*)", nodeInfo)[0]
|
|
self.nodesInfo[nodename] = {"dataPath": dataPath, "port": port}
|
|
|
|
# get node info from XML
|
|
hostnamesInXML = []
|
|
rootNode = self.initParserXMLFile()
|
|
elementName = 'DEVICELIST'
|
|
if not rootNode.findall('DEVICELIST'):
|
|
raise Exception(ErrorCode.GAUSS_512["GAUSS_51200"] % elementName)
|
|
deviceArray = rootNode.findall('DEVICELIST')[0]
|
|
deviceNodes = deviceArray.findall('DEVICE')
|
|
cmDict = {"cmsNum": "", "cmServerPortBase": "", "cmServerPortStandby": "",
|
|
"cmServerlevel": "", "cmServerListenIp1": "", "cmServerRelation": ""}
|
|
for dev in deviceNodes:
|
|
paramList = dev.findall('PARAM')
|
|
for param in paramList:
|
|
paraName = param.attrib['name']
|
|
paraValue = param.attrib['value']
|
|
if paraName == 'name':
|
|
hostnamesInXML.append(paraValue)
|
|
elif paraName == 'cmDir':
|
|
self.cmDirs.append(paraValue)
|
|
elif paraName in cmDict.keys():
|
|
cmDict[paraName] = paraValue
|
|
# check whether XML contains all nodes info
|
|
if self.hostnames != hostnamesInXML:
|
|
self.logger.logExit("XML info is not consistent with static file.")
|
|
# check params in xml
|
|
for item in cmDict:
|
|
if item == 'cmServerPortStandby':
|
|
continue
|
|
if cmDict[item] == "":
|
|
self.logger.logExit(ErrorCode.GAUSS_512["GAUSS_51200"] % item)
|
|
if cmDict['cmsNum'] != '1':
|
|
self.logger.logExit(ErrorCode.GAUSS_500["GAUSS_50024"] % 'cmsNum')
|
|
if cmDict['cmServerlevel'] != '1':
|
|
self.logger.logExit(ErrorCode.GAUSS_500["GAUSS_50024"] % 'cmServerlevel')
|
|
if not cmDict['cmServerPortBase'].isdigit():
|
|
self.logger.logExit(ErrorCode.GAUSS_500["GAUSS_50024"] % 'cmServerPortBase')
|
|
if cmDict['cmServerPortStandby'] != "" and not cmDict['cmServerPortStandby'].isdigit():
|
|
self.logger.logExit(ErrorCode.GAUSS_500["GAUSS_50024"] % 'cmServerPortStandby')
|
|
if len(self.hostnames) != len(self.cmDirs):
|
|
self.logger.logExit("\"cmDir\" of all nodes must be provided.")
|
|
|
|
def checkHostTrust(self):
|
|
checkHostsTrust(self.hostnames)
|
|
|
|
def initLogger(self):
|
|
logPath = os.path.join(self.gaussLog, "cm", "cm_tool")
|
|
if not os.path.exists(logPath):
|
|
os.makedirs(logPath)
|
|
self.logger = CMLog(logPath, "cm_install", "cm_install")
|
|
|
|
def checkCM(self):
|
|
"""
|
|
Check whether there is CM in current cluster.
|
|
"""
|
|
checkCMExistCmd = "source %s; gs_om -t status --detail | " \
|
|
"grep 'CMServer State' > /dev/null" % self.envFile
|
|
status, output = subprocess.getstatusoutput(checkCMExistCmd)
|
|
if status == 0:
|
|
self.logger.logExit("CM exists in current cluster.")
|
|
|
|
def checkCluster(self):
|
|
"""
|
|
check the status of the current cluster
|
|
"""
|
|
cmd = "source %s; gs_om -t status --detail" % self.envFile
|
|
status, output = subprocess.getstatusoutput(cmd)
|
|
if status != 0:
|
|
erroeDetail = "Detail:\nCommand:\n" + cmd + "\noutput:" + output
|
|
self.logger.logExit(ErrorCode.GAUSS_516["GAUSS_51600"] + erroeDetail)
|
|
if "cluster_state : Unavailable" in output:
|
|
# It’s permitted to deploy CM tool when cluster is stopped,
|
|
# but not permitted when cluster is unavailable.
|
|
if output.count("Manually stopped") == len(self.hostnames):
|
|
self.clusterStopped = True
|
|
return
|
|
self.logger.logExit("The cluster is unavailable currently.")
|
|
if "cluster_state : Normal" not in output:
|
|
self.logger.logExit("Cluster is running but its status is abnormal.")
|
|
# check whether term of primary is invalid and biggest.
|
|
primaryCount = 0
|
|
primaryTerm = 0
|
|
sqlCmd = "select term from pg_last_xlog_replay_location();"
|
|
for host in self.hostnames:
|
|
isLocal = False
|
|
if host == self.localhostName:
|
|
isLocal = True
|
|
findPrimaryCmd = "source %s; gs_ctl query -D %s | grep -i 'local_role.*Primary' > /dev/null" % \
|
|
(self.envFile, self.nodesInfo[host]["dataPath"])
|
|
notPrimary, output = executeCmdOnHost(host, findPrimaryCmd, isLocal)
|
|
if notPrimary == 0:
|
|
primaryCount += 1
|
|
getTermLsnCmd = "source %s; gsql -d postgres -p %s -tA -c '%s'" % \
|
|
(self.envFile, self.nodesInfo[host]["port"], sqlCmd)
|
|
status, term = executeCmdOnHost(host, getTermLsnCmd, isLocal)
|
|
if status != 0:
|
|
self.logger.logExit("Failed to get term of host %s." % host)
|
|
if notPrimary == 0:
|
|
primaryTerm = int(term)
|
|
if self.maxTerm < int(term):
|
|
self.maxTerm = int(term)
|
|
|
|
if primaryCount != 1:
|
|
self.logger.logExit("The number of primary is invalid.")
|
|
if primaryTerm == 0 or primaryTerm < self.maxTerm:
|
|
self.logger.logExit("Term of primary is invalid or not maximal.\n"
|
|
"Hint: it seems that the cluster is newly installed, so it's "
|
|
"recommended to deploy CM tool while installing the cluster.")
|
|
|
|
def run(self):
|
|
self.checkExeUser()
|
|
self.parseCommandLine()
|
|
self.checkParam()
|
|
self.getEnvParams()
|
|
self.initLogger()
|
|
self.checkOm()
|
|
self.checkCM()
|
|
self.getInfoListOfAllNodes()
|
|
self.getLocalhostName()
|
|
self.checkHostTrust()
|
|
self.checkCluster()
|
|
installImpl = InstallImpl(self)
|
|
installImpl.run()
|
|
|
|
if __name__ == "__main__":
|
|
install = Install()
|
|
install.run()
|