diff options
Diffstat (limited to 'pgaas/src/stage/opt/app/pgaas/bin/dcae_admin_db.py')
-rwxr-xr-x | pgaas/src/stage/opt/app/pgaas/bin/dcae_admin_db.py | 892 |
1 files changed, 892 insertions, 0 deletions
diff --git a/pgaas/src/stage/opt/app/pgaas/bin/dcae_admin_db.py b/pgaas/src/stage/opt/app/pgaas/bin/dcae_admin_db.py new file mode 100755 index 0000000..e1ac744 --- /dev/null +++ b/pgaas/src/stage/opt/app/pgaas/bin/dcae_admin_db.py @@ -0,0 +1,892 @@ +#!/usr/bin/python3 +# -*- indent-tabs-mode: nil -*- +# Copyright (C) 2017 AT&T Intellectual Property. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this code 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. + + +""" + +NAME + dcae_admin_db.py - given a database description json file, update the current VM accordingly + +USAGE + dcae_admin_db.py [options] configurationChanged json-file + dcae_admin_db.py [options] suspend + dcae_admin_db.py [options] resume + dcae_admin_db.py [options] test + + options: + + -H / --dbhost= - host name, defaults to CFG['dcae_admin_db_hostname'] + -d / --dbdir= - database directory path, defaults to CFG['db_directory'] + -c / --dbconf= - database configuration path, defaults to CFG['db_configuration'] + -D / --dbname= - database name, defaults to CFG['dcae_admin_db_databasename'] + -U / --user= - user to login as, defaults to CFG['dcae_admin_db_username'] + -P / --password= - password for user, defaults to CFG['dcae_admin_db_password'] + -B / --bindir= - postgresql bin directory, defaults to CFG['pg_bin_directory'] + -i / --ignorefile= - skip configuration if this file is present, defaults to CFG['skip_configuration_file'] + -n / --nocreate - do not create the databases / users + -I / --ignoredb - ignore current state of database + -R / --remove - remove old databases / users + -J / --jsontop= - top of json tree, as in \"['pgaas']\" + -e / --errors= - where to redirect error output, defaults to CFG['dcae_admin_db_errors_file'] then stderr + -t / --trace= - where to redirect trace output, defaults to CFG['dcae_admin_db_trace_file'] then stderr + -v / --verbose - verbose, defaults to CFG['dcae_admin_db_verbosity'] + +DESCRIPTION + This program is intended to be executed by the DCAE controller manager. + +When creating a database and set of users, execute the equivalent of this: + + CREATE USER tstdb_admin WITH PASSWORD 'tst'; + CREATE USER tstdb_user WITH PASSWORD 'tst'; + CREATE USER tstdb_viewer WITH PASSWORD 'tst'; + + CREATE ROLE testdb_common_user_role; + CREATE ROLE testdb_common_viewer_role; + + CREATE DATABASE testdb with owner tstdb_admin; + + \connect testdb + + REVOKE ALL on DATABASE testdb FROM testdb_common_viewer_role; + REVOKE ALL on DATABASE testdb FROM testdb_common_user_role; + REVOKE ALL on DATABASE testdb FROM tstdb_user; + REVOKE ALL on DATABASE testdb FROM tstdb_viewer; + + GRANT testdb_common_viewer_role TO testdb_common_user_role; /* user can do everything viewer can */ + GRANT testdb_common_user_role TO tstdb_admin; /* admin can do everything user and viewer can */ + + GRANT CONNECT ON DATABASE testdb TO testdb_common_viewer_role; /* viewer, user, admin can connect */ + + CREATE SCHEMA testdb_db_common AUTHORIZATION tstdb_admin; /* create a schema we can optionally use */ + + ALTER ROLE tstdb_admin IN DATABASE testdb SET search_path = public, testdb_db_common; /* search_path is not inherited, so set it here */ + ALTER ROLE testdb_common_user_role IN DATABASE testdb SET search_path = public, testdb_db_common; /* search_path is not inherited, so set it here */ + ALTER ROLE testdb_common_viewer_role IN DATABASE testdb SET search_path = public, testdb_db_common; /* search_path is not inherited, so set it here */ + + GRANT USAGE ON SCHEMA testdb_db_common TO testdb_common_viewer_role; /* viewer,user can select from schema */ + GRANT CREATE ON SCHEMA testdb_db_common TO tstdb_admin; /* admin can create on schema */ + + ALTER DEFAULT PRIVILEGES FOR ROLE tstdb_admin GRANT SELECT ON TABLES TO testdb_common_viewer_role; /* viewer, user, admin can select on tables */ + ALTER DEFAULT PRIVILEGES FOR ROLE tstdb_admin GRANT INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO testdb_common_user_role; /* user, admin can ins/upd/del/tru on tables */ + ALTER DEFAULT PRIVILEGES FOR ROLE tstdb_admin GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO testdb_common_user_role; /* user, admin can update on sequences */ + + GRANT TEMP ON DATABASE testdb TO testdb_common_user_role; /* user, admin can create temp tables */ + + GRANT testdb_common_user_role TO tstdb_user; + GRANT testdb_common_viewer_role TO tstdb_viewer; + ALTER ROLE tstdb_user IN DATABASE testdb SET search_path = public, testdb_db_common; /* search_path is not inherited, so set it here */ + ALTER ROLE tstdb_viewer IN DATABASE testdb SET search_path = public, testdb_db_common; /* search_path is not inherited, so set it here */ + +""" + +import getopt +import psycopg2 +import sys +import re +import subprocess +import json +import os +import time + +sys.path.append("/opt/app/pgaas/lib") +sys.path.append("/opt/app/dcae-commonlogging/python") +import CommonLogger + +verbose = 0 +quiet = False +errorOutput = sys.stderr +traceOutput = sys.stderr +errorLogger = debugLogger = auditLogger = metricsLogger = None + +def usage(msg = None): + """ + Print a usage message and exit + """ + sys.stdout = sys.stderr + if msg != None: + print(msg) + print("Usage:") + print("dcae_admin_db.py [options] configurationChanged json-file") + print("dcae_admin_db.py [options] suspend") + print("dcae_admin_db.py [options] resume") + print("dcae_admin_db.py [options] test") + print("dcae_admin_db.py [options] newdb dbname admin-pswd user-pswd viewer-pswd") + print("") + print("options:") + print("-H / --dbhost= - host name, defaults to CFG['dcae_admin_db_hostname']") + print("-d / --dbdir= - database directory path, defaults to CFG['db_directory']") + print("-c / --dbconf= - database directory path, defaults to CFG['db_configuration']") + print("-D / --dbname= - database name, defaults to CFG['dcae_admin_db_databasename']") + print("-n / --nocreate - do not create the databases / users") + print("-I / --ignoredb - ignore current state of database") + print("-U / --user= - user to login as, defaults to CFG['dcae_admin_db_username']") + print("-P / --password= - password for user, defaults to CFG['dcae_admin_db_password']") + print("-B / --bindir= - postgresql bin directory, defaults to CFG['pg_bin_directory']") + print("-i / --ignorefile= - skip configuration if this file is present, defaults to CFG['skip_configuration_file']") + print("-R / --remove - remove old databases / users") + print("-J / --jsontop= - top of json tree, as in \"['pgaas']\"") + print("-l / --logcfg= - ECOMP DCAE Common Logging configuration file") + print("-e / --errors= - where to redirect error output, defaults to CFG['dcae_admin_db_errors_file'] then stderr") + print("-t / --trace= - where to redirect trace output, defaults to CFG['dcae_admin_db_trace_file'] then stderr") + print("-v - verbose") + sys.exit(2) + +def checkOption(options, name, propname, optletter, encrypted=False, cdfPropname = None): + """ + Check if the specified option exists. If not, grab it from the configuration file. + Complain if it still does not exist. + """ + if name not in options: + ret = getPgaasPropValue(propname, encrypted=encrypted, dflt=None, skipComplaining=True) + if ret is None and cdfPropname is not None: + ret = getCdfPropValue(cdfPropname, encrypted=encrypted) + options[name] = ret + requireOption("either %s or config[%s]" % (optletter, propname), options[name]) + +def reviewOpts(): + """ + Parse the options passed to the command, and return them in the dictionary + """ + try: + opts, args = getopt.getopt(sys.argv[1:], "B:c:D:d:e:H:IJ:l:nP:Rt:U:hv?", + [ "dbhost=", "dbdir=", "dbconf=", + "dbname=", "dbuser=", "dbpassword=", + "bindir=", "errors=", "trace=", "logcfg=", + "nocreate", "ignoredb", "remove", "ignorefile=", + "jsontop=", + "help", "verbose"]) + except getopt.GetoptError as err: + usage(str(err)) + + propVerbosity = getPgaasPropValue("dcae_admin_db_verbosity", dflt='0') + if propVerbosity is not None: + global verbose + verbose = int(propVerbosity) + retOptions = { } + ignoreFile = getPgaasPropValue("skip_configuration_file", dflt=None) + for o, a in opts: + if o in ("-v", "--verbose"): + # global verbose + verbose += 1 + elif o in ("-c", "--dbconf"): + retOptions["dbconf"] = a + elif o in ("-H", "--dbhost"): + retOptions["dbhost"] = a + elif o in ("-d", "--dbdir"): + retOptions["dbdir"] = a + elif o in ("-D", "--dbname"): + retOptions["dbname"] = a + elif o in ("-U", "--dbuser"): + retOptions["dbuser"] = a + elif o in ("-P", "--dbpassword"): + retOptions["dbpassword"] = a + elif o in ("-B", "--bindir"): + retOptions["bindir"] = a + elif o in ("-n", "--nocreate"): + retOptions["nocreate"] = True + elif o in ("-I", "--ignoredb"): + retOptions["ignoredb"] = True + elif o in ("-R", "--remove"): + retOptions["noremove"] = True + elif o in ("-J", "--jsontop"): + retOptions["jsontop"] = a + elif o in ("-l", "--logcfg"): + retOptions["logcfg"] = a + elif o in ("-e", "--errors"): + retOptions["errors"] = a + elif o in ("-i", "--ignorefile"): + ignoreFile = a + elif o in ("-t", "--trace"): + retOptions["trace"] = a + elif o in ("-h", "--help"): + usage() + else: + usage("unhandled option: %s" % o) + if "errors" not in retOptions: + retOptions["errors"] = getPgaasPropValue("dcae_admin_db_errors_file") + if "errors" in retOptions and retOptions["errors"] is not None: + try: + errorOutput = open(retOptions["errors"], "a") + except Exception as e: + die("Cannot open errors file '%s': %s" % (retOptions["errors"], e)) + if ignoreFile is not None: + trace("checking to see if skip_configuration_file(%s) exists" % ignoreFile) + retOptions["ignorefile"] = "yes" if os.path.isfile(ignoreFile) else "no" + trace("ignorefile=%s" % retOptions["ignorefile"]) + else: + retOptions["ignorefile"] = None + if "trace" not in retOptions: + retOptions["trace"] = getPgaasPropValue("dcae_admin_db_trace_file") + if "trace" in retOptions and retOptions["trace"] is not None: + try: + traceOutput = open(retOptions["trace"], "a") + except Exception as e: + die("Cannot open trace file '%s': %s" % (retOptions["trace"], e)) + if "logcfg" not in retOptions: + retOptions["logcfg"] = getPgaasPropValue("dcae_admin_db_common_logger_config") + if "logcfg" in retOptions and retOptions["logcfg"] is not None: + logcfg = retOptions["logcfg"] + import uuid + instanceUUID = uuid.uuid1() + serviceName = "DCAE/pgaas" + # print(">>>>>>>>>>>>>>>> using common logger. UUID=%s, serviceName=%s, cfg=%s" % (instanceUUID, serviceName, logcfg)) + global errorLogger, debugLogger, auditLogger, metricsLogger + errorLogger = CommonLogger.CommonLogger(logcfg, "error", instanceUUID=instanceUUID, serviceName=serviceName) + debugLogger = CommonLogger.CommonLogger(logcfg, "debug", instanceUUID=instanceUUID, serviceName=serviceName) + auditLogger = CommonLogger.CommonLogger(logcfg, "audit", instanceUUID=instanceUUID, serviceName=serviceName) + metricsLogger = CommonLogger.CommonLogger(logcfg, "metrics", instanceUUID=instanceUUID, serviceName=serviceName) + auditLogger.info("using common logger. UUID=%s, serviceName=%s, cfg=%s" % (instanceUUID, serviceName, logcfg)) + + checkOption(retOptions, "dbname", "dcae_admin_db_databasename", "-D") + checkOption(retOptions, "dbuser", "dcae_admin_db_username", "-U") + checkOption(retOptions, "dbpassword", "dcae_admin_db_password", "-P", encrypted=True, cdfPropname="postgres") + checkOption(retOptions, "dbhost", "dcae_admin_db_hostname", "-H") + checkOption(retOptions, "dbdir", "db_directory", "-d") + checkOption(retOptions, "bindir", "pg_bin_directory", "-B") + if "jsontop" not in retOptions: + retOptions["jsontop"] = getPgaasPropValue("dcae_admin_db_jsontop") + trace("env=%s" % str(os.environ)) + trace("ignorefile=%s" % ignoreFile) + return retOptions, args + +def main(): + keyedOptions, args = reviewOpts() + trace("Invoked as: %s" % str(sys.argv)) + audit("Invoked as: %s" % str(sys.argv)) + + if len(args) == 0: + usage("no operation specified") + elif args[0] == "configurationChanged": + if len(args) != 2: + usage("too many arguments") + configurationChanged(keyedOptions, args[1]) + elif args[0] == "suspend": + if len(args) != 1: + usage("too many arguments") + suspendOperations(keyedOptions) + elif args[0] == "resume": + if len(args) != 1: + usage("too many arguments") + resumeOperations(keyedOptions) + elif args[0] == "test": + if len(args) != 1: + usage("too many arguments") + testOperations(keyedOptions) + elif args[0] == "newdb": + if len(args) != 5: + usage("wrong number of arguments") + newDb(keyedOptions, args[1], args[2], args[3], args[4]) + else: + usage("unrecognized operation '%s'" % args[0]) + +def suspendOperations(options): + """ + Execute the "suspend" sub-command. + """ + runProgram(["pkill", "repmgrd"]) + program = options["bindir"] + "/pg_ctl" + cmd = [program, "stop", "-D", options["dbdir"]] + runProgram(cmd) + audit("suspendOperations") + +def resumeOperations(options): + """ + Execute the "resume" sub-command. + """ + cmd = [options["bindir"] + "/pg_ctl", "start", "-D", options["dbdir"], "-o", "configfile=" + options["dbconf"]] + runProgram(cmd) + runProgram(["/opt/app/pgaas/bin/repmgrcd", "-d"]) + audit("resumeOperations") + +def testOperations(options): + """ + Respond to the "test" sub-command. + """ + program = options["bindir"] + "/pg_ctl" + cmd = [program, "status", "-D", options["dbdir"]] + ret = runProgram(cmd) + # pg_ctl: no server running + # pg_ctl: server is running (PID: 13988) + # does /var/run/postgresql/inmaintenance exist? -> YELLOW + cmdRepmgr = ["pgrep", "repmgrd"] + retRepmgr = runProgram(cmdRepmgr) + + msg = "????" + if os.path.isfile("/var/run/postgresql/inmaintenance"): + msg = "YELLOW: in maintenance mode" + elif re.search("no server running", ret): + msg = "RED: no PG server running" + elif re.search("server is running", ret) and re.search("[0-9]+", retRepmgr): + msg = "GREEN" + elif re.search("server is running", ret): + msg = "YELLOW: no repmgrd running" + elif re.search("[0-9]+", retRepmgr): + msg = "YELLOW: no PG server running" + else: + msg = "YELLOW: neither PG server nor repmgrd are running" + audit("test: " + msg) + print(msg, end="") + +def runProgram(cmd): + """ + Run the given command, returning the standard output as a string. + If there is an error, return None. + """ + try: + p=subprocess.Popen(cmd,shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE) + (stdout, stderr) = p.communicate() + except Exception as e: + print("Error running program because: {0}".format(e), file=errorOutput) + return None + else: + if stderr: + print("Error running program because: {0} ".format(stderr), file=errorOutput) + return None + else: + trace("runProgram() => " + str(stdout), minLevel=2) + return stdout.decode('utf-8').rstrip('\n') + +def configurationChanged(options, jsonFile): + """ + We received a new JSON configuration file + """ + audit("configurationChanged " + jsonFile) + if options["ignorefile"] == "yes": + trace("skipping database reconfiguration because skip_configuration_file exists") + return + + if not os.path.isfile(jsonFile): + die("json file %s does not exist" % jsonFile) + + try: + inp = json.load(open(jsonFile,"r")) + except Exception as e: + die("Cannot open jsonFile '%s': %s" % (jsonFile, e)) + + if verbose: + dumpJSON(inp, "incoming JSON") + + jsonTop = options["jsontop"] + if not jsonTop is None: + e = "inp" + jsonTop + trace("eval(%s)" % e) + inp = eval(e,{"__builtins__":None},{"inp":inp}) + if verbose: + dumpJSON(inp, "modified JSON") + + setupDictionaryDatabases(options, inp) + +def setupDictionaryDatabases(options, inp): + """ + Set up the databases listed in the dictionary + """ + + # trace("version=%s" % requireJSON("version", inp, "version")) + requireJSON("databases", inp, "databases") + con = None + try: + con = dbConnect(database = options["dbname"], user = options["dbuser"], password = options["dbpassword"], host = options["dbhost"]) + setupDatabases(con, options, requireJSON("databases", inp, "databases")) + + except psycopg2.DatabaseError as e: + die('Error %s' % e) + + finally: + if con: + con.commit() + con.close() + +def newDb(options, dbName, adminPswd, userPswd, viewerPswd): + """ + Given the database name and passwords, set up a database and corresponding users. + For example, with dbname="foo", adminPswd="fooa", userPswd="foou" and viewerPswd="foov", + act the same as if we had received the json configuration: + { + 'databases': { + 'foo': { + 'ownerRole': 'foo_admin', + 'roles': { + 'foo_admin': { + 'password': 'fooa', + 'role': 'admin' + }, + 'foo_user': { + 'password': 'foou', + 'role': 'writer' + }, + 'foo_viewer': { + 'password': 'foov', + 'role': 'reader' + } + } + } + } + } + """ + if not re.match("^[A-Za-z][A-Za-z0-9_]*$", dbName): + errorPrint("'%s' is not a valid database name" % dbName) + return + + adminName = dbName + "_admin" + userName = dbName + "_user" + viewerName = dbName + "_viewer" + + setupDictionaryDatabases(options, { + 'databases': { + dbName: { + 'ownerRole': adminName, + 'roles': { + adminName: { + 'password': adminPswd, + 'role': 'admin' + }, + userName: { + 'password': userPswd, + 'role': 'writer' + }, + viewerName: { + 'password': viewerPswd, + 'role': 'reader' + } + } + } + } + }) + +def dumpJSON(js, msg): + tracePrint("vvvvvvvvvvvvvvvv %s" % msg) + tracePrint(json.dumps(js, indent=4)) + tracePrint("^^^^^^^^^^^^^^^^ %s" % msg) + +def setupDatabases(con, options, dbList): + """ + Do what is needed to set up all of the databases + """ + currentDatabases = dbGetFirstColumnAsMap(con, "select datname from pg_database where datistemplate = false") + currentRolenames = dbGetFirstColumnAsMap(con, "select rolname from pg_roles") + trace("currentDatabases = " + str(currentDatabases)) + for dbName in dbList: + trace("dbName='%s'" % str(dbName)) + setupDatabase(con, options, currentDatabases, currentRolenames, dbName, dbList[dbName]) + +def setupDatabase(con, options, currentDatabases, currentRolenames, dbName, dbInfo): + """ + Do what is needed to set up a given databases and its users + """ + + dbOwnerRole = requireJSON("databases[].ownerRole", dbInfo, "ownerRole") + trace("dbName='%s', dbOwnerRole='%s'" % (dbName, dbOwnerRole)) + doesDbExist = dbName in currentDatabases + trace("does %s exist? %s" % (dbName, doesDbExist)) + foundOwnerRole = False + dbRoles = dbInfo["roles"] + for name in dbRoles: + u = dbRoles[name] + if name == dbOwnerRole and u["role"] == "admin": + foundOwnerRole = True + if u["role"] not in ("admin","writer","reader"): + die("For database %s, the role '%s' is not one of admin/writer/reader" % (dbName, u.role)) + if not foundOwnerRole: + die("For database %s, information on the ownerRole '%s' was not found" % (dbName, dbOwnerRole)) + for name in dbRoles: + userInfo = dbRoles[name] + if name in currentRolenames and ("ignoredb" not in options or not options["ignoredb"]): + trace("The role %s already exists, skipping" % name) + updatePassword(con, options, dbName, name, userInfo) + else: + setupUser(con, options, dbName, name, userInfo) + if doesDbExist and ("ignoredb" not in options or not options["ignoredb"]): + trace("The database %s already exists, skipping" % dbName) + else: + makeDatabase(con, options, dbName, dbOwnerRole, dbInfo, dbRoles) + for name in dbRoles: + userInfo = dbRoles[name] + if name in currentRolenames and ("ignoredb" not in options or not options["ignoredb"]): + trace("The role %s already exists, skipping grants" % name) + else: + modifyGrants(con, options, dbName, name, userInfo) + +def makeDatabase(con, options, dbName, dbOwnerRole, dbInfo, dbRoles): + """ + Execute the SQL to create a database + + TODO: verify grants against what is actually there + """ + ownerRole = dbInfo["ownerRole"] + userRole = "{0}_common_user_role".format(dbName) + viewerRole = "{0}_common_viewer_role".format(dbName) + + optionalDbExecute(con, options, "CREATE ROLE {0}".format(userRole)) + optionalDbExecute(con, options, "CREATE ROLE {0}".format(viewerRole)) + + trace("Creating database %s with owner '%s'" % (dbName, ownerRole)) + optionalDbExecute(con, options, "CREATE DATABASE %s WITH OWNER %s" % (dbName, ownerRole)) + con2 = None + try: + con2 = dbConnect(database = dbName, user = options["dbuser"], password = options["dbpassword"], host = options["dbhost"]) + + optionalDbExecute(con2, options, "REVOKE ALL on DATABASE {0} FROM {1}".format(dbName, viewerRole)) + optionalDbExecute(con2, options, "REVOKE ALL on DATABASE {0} FROM {1}".format(dbName, userRole)) + for name in dbRoles: + userInfo = dbRoles[name] + if userInfo["role"] == "writer" or userInfo["role"] == "reader": + optionalDbExecute(con2, options, "REVOKE ALL on DATABASE {0} FROM {1}".format(dbName, name)) + + # user can do everything viewer can + optionalDbExecute(con2, options, "GRANT {0} TO {1}".format(viewerRole, userRole)) + # admin can do everything user and viewer can + optionalDbExecute(con2, options, "GRANT {0} TO {1}".format(userRole, ownerRole)) + + # viewer, user, admin can connect + optionalDbExecute(con2, options, "GRANT CONNECT ON DATABASE {0} TO {1}".format(dbName, viewerRole)) + + # create a schema we can optionally use * + schemaName = "{0}_db_common".format(dbName) + optionalDbExecute(con2, options, "CREATE SCHEMA if not exists {0} AUTHORIZATION {1}".format(schemaName, ownerRole)) + + # search_path is not inherited, so set it here + for role in [ ownerRole, userRole, viewerRole ]: + optionalDbExecute(con2, options, "ALTER ROLE {1} IN DATABASE {0} SET search_path = public, {2}".format(dbName, role, schemaName)) + + # viewer,user can select from schema + optionalDbExecute(con2, options, "GRANT USAGE ON SCHEMA {0} TO {1}".format(schemaName, viewerRole)) + # admin can create on schema + optionalDbExecute(con2, options, "GRANT CREATE ON SCHEMA {0} TO {1}".format(schemaName, ownerRole)) + + # viewer, user, admin can select on tables + optionalDbExecute(con2, options, "ALTER DEFAULT PRIVILEGES FOR ROLE {1} GRANT SELECT ON TABLES TO {0}".format(viewerRole, ownerRole)) + # user, admin can ins/upd/del/tru on tables + optionalDbExecute(con2, options, "ALTER DEFAULT PRIVILEGES FOR ROLE {1} GRANT INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO {0}".format(userRole, ownerRole)) + # user, admin can update on sequences + optionalDbExecute(con2, options, "ALTER DEFAULT PRIVILEGES FOR ROLE {1} GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO {0}".format(userRole, ownerRole)) + + # user, admin can create temp tables + optionalDbExecute(con2, options, "GRANT TEMP ON DATABASE {0} TO {1}".format(dbName, userRole)) + + for name in dbRoles: + userInfo = dbRoles[name] + if userInfo["role"] == "writer": + optionalDbExecute(con2, options, "GRANT {0} TO {1}".format(userRole, name)) + elif userInfo["role"] == "reader": + optionalDbExecute(con2, options, "GRANT {0} TO {1}".format(viewerRole, name)) + + # search_path is not inherited, so set it here + optionalDbExecute(con2, options, "ALTER ROLE {1} IN DATABASE {0} SET search_path = public, {2}".format(dbName, name, schemaName)) + + except psycopg2.DatabaseError as e: + die('Error %s' % e) + + finally: + if con2: + con2.commit() + con2.close() + +def checkUsername(userName): + """ + A value of type name is a string of 63 or fewer characters1. A name must start + with a letter or an underscore; the rest of the string can contain letters, + digits, and underscores. + """ + trace("checkUsername(%s)" % userName) + if re.match("[A-Za-z_][A-Za-z0-9_]*$", userName): + return True + else: + errorPrint("%s is not a valid userName" % userName) + return False + +def setupUser(con, options, dbName, userName, userInfo): + """ + Do what is needed to to set up a user for a database + """ + if checkUsername(userName): + trace("For dbName='%s', create user '%s'" % (dbName, userName)) + userPassword = userInfo["password"] + optionalDbExecute(con, options, "create user %s with password '%s'" % (userName, userPassword)) + +def updatePassword(con, options, dbName, userName, userInfo): + """ + Do what is needed to update a user's password + """ + if checkUsername(userName): + trace("For dbName='%s', alter user '%s' password" % (dbName, userName)) + userPassword = userInfo["password"] + optionalDbExecute(con, options, "alter user %s with password '%s'" % (userName, userPassword)) + +def modifyGrants(con, options, dbName, userName, userInfo): + """ + Do what is needed to to set up a user for a database with the proper grants + + TODO: if user exist, verify current grants + """ + if checkUsername(userName): + userRole = userInfo["role"] + trace("For dbName='%s', set up user '%s' as a '%s'" % (dbName, userName, userRole)) + if userRole == "writer": + optionalDbExecute(con, options, "grant %s_common_user_role to %s" % (dbName, userName)) + elif userRole == "reader": + optionalDbExecute(con, options, "grant %s_common_viewer_role to %s" % (dbName, userName)) + # elif userRole == "admin": + # optionalDbExecute(con, options, "grant %s_common_admin_role to %s" % (dbName, userName)) + else: + trace("nothing to grant %s" % userName) + +def optionalDbExecute(con, options, cmd): + if "nocreate" in options and options["nocreate"]: + print(cmd) + else: + audit("Running: " + cmd) + dbExecute(con, cmd) + +""" +database utility functions +""" + +# def dbGetMap(con, cmd, args=[], skipTrace=False): +# def dbGetOneRowMap(con, cmd, args=[], skipTrace=False): + +def dbGetFirstRowOneValue(con, cmd, args=[], skipTrace=False): + """ + Do a select and return a single value from the first row + """ + row = dbGetFirstRow(con, cmd, args, skipTrace) + trace("row=" + str(row)) + if row is not None and len(row) > 0: + return row[0] + return None + +def dbGetFirstRow(con, cmd, args=[], skipTrace=False): + """ + Do a select and return the values from the first row + """ + cursor = dbExecute(con, cmd, args, skipTrace) + return cursor.fetchone() + +def dbGetFirstColumn(con, cmd, args=[], skipTrace=False): + """ + Do a select and return the first column's value from each row + """ + ret = [] + cursor = dbExecute(con, cmd, args, skipTrace) + for row in cursor: + for col in row: + ret.append(col) + break + return ret + +def dbGetFirstColumnAsMap(con, cmd, args=[], skipTrace=False, val=1): + """ + Do a select and return the first column's value from each row + """ + ret = {} + cursor = dbExecute(con, cmd, args, skipTrace) + for row in cursor: + for col in row: + ret[col] = val + break + return ret + +def dumpTable(con, tableName, max=-1): + """ + If being extra verbose, print out the entire table + """ + if verbose < 2: + return + print("================ " + tableName + " ================", file=traceOutput) + + cols = dbGetFirstColumn(con, "select column_name from information_schema.columns where table_name='" + tableName + "'", skipTrace=True) + print("num", end="|", file=traceOutput) + for col in cols: + print(col, end="|", file=traceOutput) + print("", file=traceOutput) + + if max > -1: + cursor = dbExecute(con, "select * from " + tableName + " limit " + str(max), skipTrace=True) + else: + cursor = dbExecute(con, "select * from " + tableName, skipTrace=True) + i = 0 + for row in cursor: + print("%d" % i, end="|", file=traceOutput) + i += 1 + for col in row: + print("%s" % (col), end="|", file=traceOutput) + print("", file=traceOutput) + print("================================================", file=traceOutput) + +def dbExecute(con, statement, args=[], skipTrace=False): + """ + Create a cursor, instantiate the arguments into a statement, trace print the statement, and execute the statement. + Return the cursor + """ + cursor = con.cursor() + stmt = cursor.mogrify(statement, args); + if not skipTrace: + trace("executing:" + str(stmt)) + cursor.execute(stmt) + global quiet + if not skipTrace: + trace("statusmessage=" + cursor.statusmessage + ", rowcount=" + str(cursor.rowcount)) + return cursor + +def dbConnect(database, user, password, host, autocommit = True): + """ + Create a connection, logging it in the process + Return the connection + """ + trace("connecting to database %s as %s on host %s" % (database, user, host)) + con =psycopg2.connect(database = database, user = user, password = password, host = host) + con.autocommit = autocommit + return con + +""" +Utility functions +""" + +def die(msg): + """ + Print a message to the error file and exit. + """ + errorPrint(msg) + sys.exit(1) + +def errorPrint(msg, file=errorOutput): + """ + Print a message to the error file. + """ + global errorLogger + # print("----------------> errorLogger=%s" % str(errorLogger)) + if errorLogger is not None: + errorLogger.error(msg) + else: + taggedPrint("ERROR", msg, file=file) + + +def tracePrint(msg, file=traceOutput): + """ + Print a message to the trace file. + """ + global debugLogger + # print("----------------> debugLogger=%s" % str(debugLogger)) + if debugLogger is not None: + debugLogger.debug(msg) + else: + taggedPrint("DEBUG", msg, file=file) + +def taggedPrint(tag, msg, file): + """ + Print a message to the trace file. + """ + dt = time.strftime('%Y-%m-%d %T', time.localtime()) + print("%s %s: %s" % (dt, tag, msg), file=file) + +def requireOption(nm, val): + """ + Die if a program parameter is not set + """ + return require("option", nm, val) + +def requireJSON(prnm, dict, nm): + """ + Die if a JSON value is not set + """ + if nm not in dict: + die("The JSON value '%s' is missing" % prnm) + return dict[nm] + +def require(type, nm, val): + """ + Die if a value is not set + """ + if val is None: + die("The %s '%s' is missing" % (type, nm)) + return val + +def trace(msg, minLevel=1): + """ + Print a message to trace output if verbose is turned on. + """ + global verbose + if verbose >= minLevel: + tracePrint(msg) + +def audit(msg): + """ + Print a message to audit log if one is being used + """ + global auditLogger + if auditLogger is not None: + auditLogger.info(msg) + +def getCdfPropValue(nm, encrypted=False, cfg="/opt/app/cdf/lib/cdf.cfg", dflt=None, skipComplaining=False): + """ + Return a value from the configuration file /opt/app/cdf/lib/cdf.cfg + """ + return getPropValue(nm=nm, encrypted=encrypted, cfg=cfg, dflt=dflt, skipComplaining=skipComplaining) + +def getPgaasPropValue(nm, encrypted=False, cfg="/opt/app/pgaas/lib/pgaas.cfg", dflt=None, skipComplaining=False): + """ + Return a value from the configuration file /opt/app/pgaas/lib/pgaas.cfg + """ + return getPropValue(nm=nm, encrypted=encrypted, cfg=cfg, dflt=dflt, skipComplaining=skipComplaining) + +getPropDict = { } + +def getPropValue(nm, encrypted=False, cfg=None, dflt=None, skipComplaining=False): + """ + Return a value from the specified configuration file + """ + if cfg is None: + return None + global getPropDict + if getPropDict.get(cfg): + savedDate = getPropDict[cfg] + # trace("getPropValue: savedDate[" + cfg + "]=" + str(savedDate)) + cfgDate = os.path.getmtime(cfg) + # trace("getPropValue: cfgDate=" + str(cfgDate)) + if float(savedDate) >= float(cfgDate): # cfg has not changed + val = getPropDict.get(cfg + ":" + nm) + # trace("getPropValue: val=" + val) + if val is not None: + # trace("getPropValue: getPropValue(saved) => '%s'" % str(val)) + return val + else: # clear out any previously saved keys + cfgcolon = cfg + ":" + for k in list(getPropDict.keys()): + if re.match(cfgcolon, k): + del getPropDict[k] + getPropValueProgram = '/opt/app/cdf/bin/getpropvalue' + if encrypted: + cmd = [getPropValueProgram, "-f", cfg, "-x", "-n", nm] + else: + cmd = [getPropValueProgram, "-f", cfg, "-n", nm] + # trace("getPgaasPropValue: cmd=" + str(cmd)) + + try: + with subprocess.Popen(cmd,shell=False,stdout=subprocess.PIPE,stderr=subprocess.PIPE) as p: + (origString, stderrString) = p.communicate() + except Exception as e: + traceback.print_exc() + print("Error decoding string because {0}".format(e), file=errorOutput) + return None + else: + if stderrString: + if not re.search("Configuration property .* must be defined", stderrString.decode('utf-8')) and not skipComplaining: + print("Error decoding string because: {0} ".format(stderr), file=errorOutput) + return dflt + else: + trace("getPgaasPropValue() => " + str(origString), minLevel=2) + return origString.decode('utf-8').rstrip('\n') + +if __name__ == "__main__": + main() |