| #!/usr/bin/python3 |
| # -*- coding: utf-8 -*- |
| |
| # 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 |
| import sys |
| import subprocess |
| import glob |
| from random import choice |
| import string |
| from optparse import OptionParser |
| import subprocess |
| import shutil |
| |
| # squelch mysqldb spurious warnings |
| import warnings |
| |
| warnings.simplefilter("ignore") |
| # ---- 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 ( |
| "@PYTHONDIR@", |
| 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 ---- |
| |
| |
| def runCmd(cmds): |
| process = subprocess.Popen( |
| " ".join(cmds), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE |
| ) |
| stdout, stderr = process.communicate() |
| if process.returncode != 0: |
| raise Exception(stderr) |
| return stdout.decode("utf-8") |
| |
| |
| class DBDeployer(object): |
| parser = None |
| options = None |
| args = None |
| isDebug = False |
| mgmtsecretkey = None |
| dbsecretkey = None |
| encryptiontype = None |
| dbConfPath = r"@MSCONF@" |
| dbDotProperties = {} |
| dbDotPropertiesIndex = 0 |
| encryptionKeyFile = "@MSCONF@/key" |
| encryptionJarPath = "@COMMONLIBDIR@/lib/cloudstack-utils.jar" |
| success = False |
| magicString = "This_is_a_magic_string_i_think_no_one_will_duplicate" |
| |
| def preRun(self): |
| def backUpDbDotProperties(): |
| dbpPath = os.path.join(self.dbConfPath, "db.properties") |
| copyPath = os.path.join(self.dbConfPath, "db.properties.origin") |
| |
| if os.path.isfile(dbpPath): |
| shutil.copy2(dbpPath, copyPath) |
| |
| backUpDbDotProperties() |
| |
| def postRun(self): |
| def cleanOrRecoverDbDotProperties(): |
| dbpPath = os.path.join(self.dbConfPath, "db.properties") |
| copyPath = os.path.join(self.dbConfPath, "db.properties.origin") |
| if os.path.isfile(copyPath): |
| if not self.success: |
| shutil.copy2(copyPath, dbpPath) |
| os.remove(copyPath) |
| |
| cleanOrRecoverDbDotProperties() |
| |
| def info(self, msg, result=None): |
| output = "" |
| if msg is not None: |
| output = "%-80s" % msg |
| |
| if result is True: |
| output += "[ \033[92m%-2s\033[0m ]\n" % "OK" |
| elif result is False: |
| output += "[ \033[91m%-6s\033[0m ]\n" % "FAILED" |
| sys.stdout.write(output) |
| sys.stdout.flush() |
| |
| def debug(self, msg): |
| msg = "DEBUG:%s" % msg |
| sys.stdout.write(msg) |
| sys.stdout.flush() |
| |
| def putDbProperty(self, key, value): |
| if key in self.dbDotProperties: |
| (oldValue, index) = self.dbDotProperties[key] |
| self.dbDotProperties[key] = (value, index) |
| else: |
| self.dbDotProperties[key] = (value, self.dbDotPropertiesIndex) |
| self.dbDotPropertiesIndex += 1 |
| |
| def getDbProperty(self, key): |
| if key not in self.dbDotProperties: |
| return None |
| (value, index) = self.dbDotProperties[key] |
| return value |
| |
| def errorAndExit(self, msg): |
| self.postRun() |
| err = ( |
| """\n\nWe apologize for below error: |
| *************************************************************** |
| %s |
| *************************************************************** |
| Please run: |
| |
| cloud-setup-encryption -h |
| |
| for full help |
| """ |
| % msg |
| ) |
| sys.stderr.write(err) |
| sys.stderr.flush() |
| sys.exit(1) |
| |
| def prepareDBFiles(self): |
| def prepareDBDotProperties(): |
| dbpPath = os.path.join(self.dbConfPath, "db.properties") |
| dbproperties = file(dbpPath).read().splitlines() |
| newdbp = [] |
| emptyLine = 0 |
| for line in dbproperties: |
| passed = False |
| line = line.strip() |
| if line.startswith("#"): |
| key = line |
| value = "" |
| passed = True |
| if line == "" or line == "\n": |
| key = self.magicString + str(emptyLine) |
| value = "" |
| emptyLine += 1 |
| passed = True |
| |
| try: |
| if not passed: |
| (key, value) = line.split("=", 1) |
| except Exception as e: |
| err = """Wrong format in %s (%s): |
| Besides comments beginning "#" and empty line, all key-value pairs must be in formula of |
| key=value |
| for example: |
| db.cloud.username = cloud |
| """ % ( |
| dbpPath, |
| line, |
| ) |
| self.errorAndExit(err) |
| self.putDbProperty(key, value) |
| self.info("Preparing %s" % dbpPath, True) |
| |
| prepareDBDotProperties() |
| |
| def finalize(self): |
| def finalizeDbProperties(): |
| entries = [] |
| for key in list(self.dbDotProperties.keys()): |
| (value, index) = self.dbDotProperties[key] |
| if key.startswith("#"): |
| entries.insert(index, key) |
| elif key.startswith(self.magicString): |
| entries.insert(index, "") |
| else: |
| entries.insert(index, "%s=%s" % (key, value)) |
| file(os.path.join(self.dbConfPath, "db.properties"), "w").write( |
| "\n".join(entries) |
| ) |
| |
| self.info("Finalizing setup ...", None) |
| finalizeDbProperties() |
| self.info(None, True) |
| self.success = True # At here, we have done successfully and nothing more after this flag is set |
| |
| def processEncryptionStuff(self): |
| def encrypt(value): |
| cmd = [ |
| "java", |
| "-classpath", |
| '"' + self.encryptionJarPath + '"', |
| "com.cloud.utils.crypt.EncryptionCLI", |
| "-i", |
| '"' + value + '"', |
| "-p", |
| '"' + self.mgmtsecretkey + '"', |
| ] |
| return runCmd(cmd).strip("\n") |
| |
| def saveMgmtServerSecretKey(): |
| if self.encryptiontype == "file": |
| file(self.encryptionKeyFile, "w").write(self.mgmtsecretkey) |
| |
| def formatEncryptResult(value): |
| return "ENC(%s)" % value |
| |
| def encryptDBSecretKey(): |
| self.putDbProperty( |
| "db.cloud.encrypt.secret", |
| formatEncryptResult(encrypt(self.dbsecretkey)), |
| ) |
| |
| def encryptDBPassword(): |
| dbPassword = self.getDbProperty("db.cloud.password") |
| if dbPassword == "": |
| return # Don't encrypt empty password |
| if dbPassword == None: |
| self.errorAndExit( |
| "Cannot find db.cloud.password in %s" |
| % os.path.join(self.dbConfPath, "db.properties") |
| ) |
| self.putDbProperty( |
| "db.cloud.password", formatEncryptResult(encrypt(dbPassword)) |
| ) |
| |
| usagePassword = self.getDbProperty("db.usage.password") |
| if usagePassword == "": |
| return # Don't encrypt empty password |
| if usagePassword == None: |
| self.errorAndExit( |
| "Cannot find db.usage.password in %s" |
| % os.path.join(self.dbConfPath, "db.properties") |
| ) |
| self.putDbProperty( |
| "db.usage.password", formatEncryptResult(encrypt(usagePassword)) |
| ) |
| |
| self.info("Processing encryption ...", None) |
| self.putDbProperty("db.cloud.encryption.type", self.encryptiontype) |
| saveMgmtServerSecretKey() |
| encryptDBSecretKey() |
| encryptDBPassword() |
| self.info(None, True) |
| |
| def parseOptions(self): |
| def parseOtherOptions(): |
| self.encryptiontype = self.options.encryptiontype |
| self.mgmtsecretkey = self.options.mgmtsecretkey |
| self.dbsecretkey = self.options.dbsecretkey |
| self.isDebug = self.options.debug |
| |
| def validateParameters(): |
| if self.encryptiontype != "file" and self.encryptiontype != "web": |
| self.errorAndExit( |
| 'Wrong encryption type %s, --encrypt-type can only be "file" or "web' |
| % self.encryptiontype |
| ) |
| |
| # ---------------------- option parsing and command line checks ------------------------ |
| usage = """%prog [-e ENCRYPTIONTYPE] [-m MGMTSECRETKEY] [-k DBSECRETKEY] [--debug] |
| |
| This command sets up the CloudStack Encryption. |
| |
| """ |
| self.parser = OptionParser(usage=usage) |
| self.parser.add_option( |
| "-v", |
| "--debug", |
| action="store_true", |
| dest="debug", |
| default=False, |
| help="If enabled, print the commands it will run as they run", |
| ) |
| self.parser.add_option( |
| "-e", |
| "--encrypt-type", |
| action="store", |
| type="string", |
| dest="encryptiontype", |
| default="file", |
| help="Encryption method used for db password encryption. Valid values are file, web. Default is file.", |
| ) |
| self.parser.add_option( |
| "-m", |
| "--managementserver-secretkey", |
| action="store", |
| type="string", |
| dest="mgmtsecretkey", |
| default="password", |
| help="Secret key used to encrypt confidential parameters in db.properties. A string, default is password", |
| ) |
| self.parser.add_option( |
| "-k", |
| "--database-secretkey", |
| action="store", |
| type="string", |
| dest="dbsecretkey", |
| default="password", |
| help="Secret key used to encrypt sensitive database values. A string, default is password", |
| ) |
| |
| (self.options, self.args) = self.parser.parse_args() |
| parseOtherOptions() |
| validateParameters() |
| |
| def run(self): |
| try: |
| self.preRun() |
| self.parseOptions() |
| self.prepareDBFiles() |
| self.processEncryptionStuff() |
| self.finalize() |
| finally: |
| self.postRun() |
| |
| print("") |
| print("CloudStack has successfully setup Encryption") |
| print("") |
| |
| |
| if __name__ == "__main__": |
| o = DBDeployer() |
| o.run() |