def delete_database(**kwargs): # pylint: disable=unused-argument """ dcae.nodes.pgaas.database: Delete a database from a cluster """ try: debug("delete_database() invoked") dbname = ctx.node.properties['name'] warn("delete_database({0})".format(safestr(dbname))) if not chkdbname(dbname): return debug('delete_database(): dbname checked out') if ctx.node.properties['use_existing']: return debug('delete_database(): !use_existing') dbinfo = dbgetinfo(ctx) debug('Got db server info') with rootconn(dbinfo) as conn: crx = conn.cursor() admu = ctx.instance.runtime_properties['admin']['user'] usru = ctx.instance.runtime_properties['user']['user'] vwru = ctx.instance.runtime_properties['viewer']['user'] cusr = '******'.format(dbname) cvwr = '{0}_common_viewer_role'.format(dbname) dbexecute(crx, 'DROP DATABASE IF EXISTS {0}'.format(dbname)) for r in [usru, vwru, admu, cusr, cvwr]: dbexecute(crx, 'DROP ROLE IF EXISTS {0}'.format(r)) warn('All gone') except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e
def get_existing_clusterinfo(wfqdn, rfqdn, related): """ Retrieve all of the information specific to an existing cluster. """ if rfqdn != '': raiseNonRecoverableError( 'Read-only FQDN must not be specified when using an existing cluster, fqdn={0}' .format(safestr(rfqdn))) if len(related) != 0: raiseNonRecoverableError( 'Cluster SSH keypair must not be specified when using an existing cluster' ) try: fn = '{0}/{1}'.format(OPT_MANAGER_RESOURCES_PGAAS, wfqdn.lower()) with open(fn, 'r') as f: data = json.load(f) data['rw'] = wfqdn return data except Exception as e: # pylint: disable=broad-except warn("Error: {0}".format(e)) msg = 'Cluster must be deployed when using an existing cluster.\nCheck your domain name: fqdn={0}\nerr={1}'.format( safestr(wfqdn), e) if not os.path.isdir(OPT_MANAGER_RESOURCES_PGAAS): msg += '\nThe directory {} does not exist. No PostgreSQL clusters have been deployed on this manager.'.format( OPT_MANAGER_RESOURCES_PGAAS) else: msg += get_valid_domains() # warn("Stack: {0}".format(traceback.format_exc())) raiseNonRecoverableError(msg)
def raiseRecoverableError(msg): """ Print a warning message and raise a RecoverableError exception. This is a handy endpoint to add other extended debugging calls. """ warn(msg) raise RecoverableError(msg)
def chkdbname(dbname): """ verify that a database name is valid """ ret = re.match('[a-zA-Z][a-zA-Z0-9]{0,43}', dbname) is not None and dbname != 'postgres' if not ret: warn("Invalid dbname: {0}".format(safestr(dbname))) return ret
def getclusterinfo(wfqdn, reuse, rfqdn, initialpassword, related): """ Retrieve all of the information specific to a cluster. if reuse, retrieve it else create and store it """ # debug("getclusterinfo({}, {}, {}, {}, ..related..)".format(safestr(wfqdn), safestr(reuse), safestr(rfqdn), safestr(initialpassword))) debug("getclusterinfo({}, {}, {}, ..related..)".format( safestr(wfqdn), safestr(reuse), safestr(rfqdn))) if not chkfqdn(wfqdn): raiseNonRecoverableError( 'Invalid FQDN specified for admin/read-write access, fqdn={0}'. format(safestr(wfqdn))) if reuse: return get_existing_clusterinfo(wfqdn, rfqdn, related) if rfqdn == '': rfqdn = wfqdn elif not chkfqdn(rfqdn): raiseNonRecoverableError( 'Invalid FQDN specified for read-only access, fqdn={0}'.format( safestr(rfqdn))) if len(related) != 1: raiseNonRecoverableError( 'Cluster SSH keypair must be specified using a dcae.relationships.pgaas_cluster_uses_sshkeypair ' + 'relationship to a dcae.nodes.sshkeypair node') data = { 'ro': rfqdn, 'pubkey': related[0].instance.runtime_properties['public'], 'data': related[0].instance.runtime_properties['base64private'], 'hash': 'sha256' } os.umask(0o77) try: os.makedirs('{0}'.format(OPT_MANAGER_RESOURCES_PGAAS)) except: # pylint: disable=bare-except pass try: with open('{0}/{1}'.format(OPT_MANAGER_RESOURCES_PGAAS, wfqdn.lower()), 'w') as f: f.write(json.dumps(data)) except Exception as e: # pylint: disable=broad-except warn("Error: {0}".format(e)) warn("Stack: {0}".format(traceback.format_exc())) raiseNonRecoverableError( 'Cannot write cluster information to {0}: fqdn={1}, err={2}'. format(OPT_MANAGER_RESOURCES_PGAAS, safestr(wfqdn), e)) data['rw'] = wfqdn if initialpassword: with rootconn(data, initialpassword=initialpassword) as conn: crr = conn.cursor() dbexecute_trunc_print( crr, "ALTER USER postgres WITH PASSWORD %s", (getpass(data, 'postgres', wfqdn, 'postgres'), )) crr.close() return data
def rm_pgaas_cluster(**kwargs): # pylint: disable=unused-argument """ dcae.nodes.pgaas.cluster: Remove key generation data for cluster """ try: warn("rm_pgaas_cluster()") wfqdn = ctx.node.properties['writerfqdn'] if chkfqdn(wfqdn) and not ctx.node.properties['use_existing']: os.remove('{0}/{1}'.format(OPT_MANAGER_RESOURCES_PGAAS, wfqdn)) warn('All done') except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e
def add_pgaas_cluster(**kwargs): # pylint: disable=unused-argument """ dcae.nodes.pgaas.cluster: Record key generation data for cluster """ try: warn("add_pgaas_cluster() invoked") data = getclusterinfo(ctx.node.properties['writerfqdn'], ctx.node.properties['use_existing'], ctx.node.properties['readerfqdn'], ctx.node.properties['initialpassword'], find_related_nodes('dcae.relationships.pgaas_cluster_uses_sshkeypair')) ctx.instance.runtime_properties['public'] = data['pubkey'] ctx.instance.runtime_properties['base64private'] = data['data'] ctx.instance.runtime_properties['postgrespswd'] = getpass(data, 'postgres', ctx.node.properties['writerfqdn'], 'postgres') warn('All done') except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e
def update_database(refctx, **kwargs): """ dcae.nodes.pgaas.database: Update the password for a database from a cluster refctx is auto injected into the function when called as a workflow """ try: debug("update_database() invoked") ################################################ # Verify refctx contains the <nodes> attribute. # # The workflow context might not be consistent # # across different cloudify versions # ################################################ if not hasattr(refctx, 'nodes'): raiseNonRecoverableError( 'workflow context does not contain attribute=<nodes>. dir(refctx)={}' .format(dir(refctx))) ############################################ # Verify that refctx.nodes is iterable # ############################################ if not isinstance(refctx.nodes, collections.Iterable): raiseNonRecoverableError( "refctx.nodes is not an iterable. Type={}".format( type(refctx.nodes))) ctx_node = None ############################################## # Iterate through the nodes until we find # # one with the properties we are looking for # ############################################## for i in refctx.nodes: ############################################ # Safeguard: If a given node doesn't have # # properties then skip it. # # Don't cause an exception since the nodes # # entry we are searching might still exist # ############################################ if not hasattr(i, 'properties'): warn( 'Encountered a ctx node that does not have attr=<properties>. dir={}' .format(dir(i))) continue debug("ctx node has the following Properties: {}".format( list(i.properties.keys()))) if ('name' in i.properties) and ('writerfqdn' in i.properties): ctx_node = i break ############################################### # If none of the nodes have properties: # # <name> and <writerfqdn> then fatal error # ############################################### if not ctx_node: raiseNonRecoverableError( 'Either <name> or <writerfqdn> is not found in refctx.nodes.properties.' ) debug("name is {}".format(ctx_node.properties['name'])) debug("host is {}".format(ctx_node.properties['writerfqdn'])) dbname = ctx_node.properties['name'] debug("update_database({0})".format(safestr(dbname))) ########################### # dbname must be valid # ########################### if not chkdbname(dbname): raiseNonRecoverableError('dbname is null') hostport = ctx_node.properties['writerfqdn'] debug('update_database(): wfqdn={}'.format(hostport)) dbinfo = dbgetinfo_for_update(hostport) #debug('Got db server info={}'.format(dbinfo)) hostPortDbname = '{0}/{1}:{2}'.format(OPT_MANAGER_RESOURCES_PGAAS, hostport.lower(), dbname.lower()) debug('update_database(): hostPortDbname={}'.format(hostPortDbname)) try: appended = False with open(hostPortDbname, "a") as fp: with open("/dev/urandom", "rb") as rp: b = rp.read(16) print(binascii.hexlify(b).decode('utf-8'), file=fp) appended = True if not appended: ctx.logger.warn( "Error: the password for {} {} was not successfully changed" .format(hostport, dbname)) except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e descs = dbdescs(dbinfo, dbname) ########################################## # Verify we have expected keys # # <admin>, <user>, and <viewer> as well # # as "sub-key" <user> # ########################################## if not isinstance(descs, dict): raiseNonRecoverableError( 'db descs has unexpected type=<{}> was expected type dict'. format(type(descs))) for key in ("admin", "user", "viewer"): if key not in descs: raiseNonRecoverableError( 'db descs does not contain key=<{}>. Keys found for descs are: {}' .format(key, list(descs.keys()))) if 'user' not in descs[key]: raiseNonRecoverableError( 'db descs[{}] does not contain key=<user>. Keys found for descs[{}] are: {}' .format(key, key, list(descs[key].keys()))) with rootconn(dbinfo) as conn: crx = conn.cursor() admu = descs['admin']['user'] usru = descs['user']['user'] vwru = descs['viewer']['user'] for r in [usru, vwru, admu]: dbexecute_trunc_print( crx, "ALTER USER {} WITH PASSWORD '{}'".format( r, getpass(dbinfo, r, hostport, dbname))) #debug("user={} password={}".format(r, getpass(dbinfo, r, hostport, dbname))) warn('All users updated for database {}'.format(dbname)) except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e
def create_database(**kwargs): """ dcae.nodes.pgaas.database: Create a database on a cluster """ try: debug("create_database() invoked") dbname = ctx.node.properties['name'] warn("create_database({0})".format(safestr(dbname))) if not chkdbname(dbname): raiseNonRecoverableError( 'Unacceptable or missing database name: {0}'.format( safestr(dbname))) debug('create_database(): dbname checked out') dbinfo = dbgetinfo(ctx) debug('Got db server info') descs = dbdescs(dbinfo, dbname) ctx.instance.runtime_properties['admin'] = descs['admin'] ctx.instance.runtime_properties['user'] = descs['user'] ctx.instance.runtime_properties['viewer'] = descs['viewer'] with rootconn(dbinfo) as conn: crx = conn.cursor() dbexecute( crx, 'SELECT datname FROM pg_database WHERE datistemplate = false') existingdbs = [x[0] for x in crx] if ctx.node.properties['use_existing']: if dbname not in existingdbs: raiseNonRecoverableError( 'use_existing specified but database does not exist, dbname={0}' .format(safestr(dbname))) return dbexecute(crx, 'SELECT rolname FROM pg_roles') existingroles = [x[0] for x in crx] admu = descs['admin']['user'] usru = descs['user']['user'] vwru = descs['viewer']['user'] cusr = '******'.format(dbname) cvwr = '{0}_common_viewer_role'.format(dbname) schm = '{0}_db_common'.format(dbname) if admu not in existingroles: dbexecute_trunc_print( crx, 'CREATE USER {0} WITH PASSWORD %s'.format(admu), (descs['admin']['password'], )) if usru not in existingroles: dbexecute_trunc_print( crx, 'CREATE USER {0} WITH PASSWORD %s'.format(usru), (descs['user']['password'], )) if vwru not in existingroles: dbexecute_trunc_print( crx, 'CREATE USER {0} WITH PASSWORD %s'.format(vwru), (descs['viewer']['password'], )) if cusr not in existingroles: dbexecute(crx, 'CREATE ROLE {0}'.format(cusr)) if cvwr not in existingroles: dbexecute(crx, 'CREATE ROLE {0}'.format(cvwr)) if dbname not in existingdbs: dbexecute( crx, 'CREATE DATABASE {0} WITH OWNER {1}'.format(dbname, admu)) crx.close() with rootconn(dbinfo, dbname) as dbconn: crz = dbconn.cursor() for r in [cusr, cvwr, usru, vwru]: dbexecute( crz, 'REVOKE ALL ON DATABASE {0} FROM {1}'.format(dbname, r)) dbexecute(crz, 'GRANT {0} TO {1}'.format(cvwr, cusr)) dbexecute(crz, 'GRANT {0} TO {1}'.format(cusr, admu)) dbexecute( crz, 'GRANT CONNECT ON DATABASE {0} TO {1}'.format(dbname, cvwr)) dbexecute( crz, 'CREATE SCHEMA IF NOT EXISTS {0} AUTHORIZATION {1}'.format( schm, admu)) for r in [admu, cusr, cvwr, usru, vwru]: dbexecute( crz, 'ALTER ROLE {0} IN DATABASE {1} SET search_path = public, {2}' .format(r, dbname, schm)) dbexecute(crz, 'GRANT USAGE ON SCHEMA {0} to {1}'.format(schm, cvwr)) dbexecute(crz, 'GRANT CREATE ON SCHEMA {0} to {1}'.format(schm, admu)) dbexecute( crz, 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} GRANT SELECT ON TABLES TO {1}' .format(admu, cvwr)) dbexecute( crz, 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} GRANT INSERT, UPDATE, DELETE, TRUNCATE ON TABLES TO {1}' .format(admu, cusr)) dbexecute( crz, 'ALTER DEFAULT PRIVILEGES FOR ROLE {0} GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO {1}' .format(admu, cusr)) dbexecute(crz, 'GRANT TEMP ON DATABASE {0} TO {1}'.format(dbname, cusr)) dbexecute(crz, 'GRANT {0} to {1}'.format(cusr, usru)) dbexecute(crz, 'GRANT {0} to {1}'.format(cvwr, vwru)) crz.close() warn('All done') except Exception as e: # pylint: disable=broad-except ctx.logger.warn("Error: {0}".format(e)) ctx.logger.warn("Stack: {0}".format(traceback.format_exc())) raise e