diff --git a/dev-support/ranger-docker/Dockerfile.ranger b/dev-support/ranger-docker/Dockerfile.ranger index 875329bb80..a01a3f6804 100644 --- a/dev-support/ranger-docker/Dockerfile.ranger +++ b/dev-support/ranger-docker/Dockerfile.ranger @@ -25,12 +25,19 @@ ARG TARGETARCH COPY ./dist/ranger-${RANGER_VERSION}-admin.tar.gz /home/ranger/dist/ COPY ./scripts/admin/ranger.sh ${RANGER_SCRIPTS}/ -COPY ./scripts/admin/create-ranger-services.py ${RANGER_SCRIPTS}/ +COPY ./scripts/admin/user_password_bootstrap.py ${RANGER_SCRIPTS}/ +COPY ./scripts/python/log_config.py ${RANGER_SCRIPTS}/ +COPY ./scripts/admin/dba.py ${RANGER_SCRIPTS}/ +COPY ./scripts/admin/ranger_admin_xml_config.py ${RANGER_SCRIPTS}/ +COPY ./scripts/admin/create-ranger-services.py ${RANGER_SCRIPTS}/create_services.py RUN tar xvfz /home/ranger/dist/ranger-${RANGER_VERSION}-admin.tar.gz --directory=${RANGER_HOME} \ && ln -s ${RANGER_HOME}/ranger-${RANGER_VERSION}-admin ${RANGER_HOME}/admin \ && rm -f /home/ranger/dist/ranger-${RANGER_VERSION}-admin.tar.gz \ && rm -f /opt/ranger/admin/install.properties \ + && rm -f /opt/ranger/admin/setup.sh \ + && rm -f /opt/ranger/admin/dba_script.py \ + && rm -f /opt/ranger/admin/db_setup.py \ && mkdir -p /var/run/ranger /var/log/ranger /usr/share/java/ \ && chown -R ranger:ranger ${RANGER_HOME}/admin/ ${RANGER_SCRIPTS}/ /var/run/ranger/ /var/log/ranger/ \ && chmod 755 ${RANGER_SCRIPTS}/ranger.sh diff --git a/dev-support/ranger-docker/docker-compose.ranger.yml b/dev-support/ranger-docker/docker-compose.ranger.yml index 3d92f5e065..354ad8915e 100644 --- a/dev-support/ranger-docker/docker-compose.ranger.yml +++ b/dev-support/ranger-docker/docker-compose.ranger.yml @@ -17,7 +17,9 @@ services: - ./dist/version:/home/ranger/dist/version:ro - ./scripts/kdc/krb5.conf:/etc/krb5.conf:ro - ./scripts/hadoop/core-site.xml:/home/ranger/scripts/core-site.xml:ro - - ./scripts/admin/ranger-admin-install-${RANGER_DB_TYPE}.properties:/opt/ranger/admin/install.properties + - ./scripts/admin/core-site.xml:/opt/ranger/admin/configs/core-site.xml:ro + - ./scripts/admin/ranger-admin-site.xml:/opt/ranger/admin/configs/ranger-admin-site.xml:ro + - ./scripts/admin/ranger-admin-default-site.xml:/opt/ranger/admin/configs/ranger-admin-default-site.xml:ro stdin_open: true tty: true networks: @@ -38,6 +40,10 @@ services: - RANGER_DB_TYPE - KERBEROS_ENABLED - DEBUG_ADMIN=${DEBUG_ADMIN:-false} + - RANGER_ADMIN_DB_PASSWORD=rangerR0cks! + - RANGER_ADMIN_PASSWORD=rangerR0cks! + - RANGER_USERSYNC_PASSWORD=rangerR0cks! + - RANGER_TAGSYNC_PASSWORD=rangerR0cks! command: - /home/ranger/scripts/ranger.sh diff --git a/dev-support/ranger-docker/scripts/admin/core-site.xml b/dev-support/ranger-docker/scripts/admin/core-site.xml new file mode 100644 index 0000000000..89dc1c2a3c --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/core-site.xml @@ -0,0 +1,47 @@ + + + + + hadoop.security.authentication + simple + + + hadoop.security.authorization + true + + + fs.defaultFS + hdfs://localhost:9000 + + + hadoop.rpc.protection + authentication + + + hadoop.security.key.provider.path + kms://http@localhost:9292/kms + + + zookeeper.quorum + localhost:2181 + + + cluster.name + dev + + diff --git a/dev-support/ranger-docker/scripts/admin/create-ranger-services.py b/dev-support/ranger-docker/scripts/admin/create-ranger-services.py index 68d9b915e3..a94346786d 100644 --- a/dev-support/ranger-docker/scripts/admin/create-ranger-services.py +++ b/dev-support/ranger-docker/scripts/admin/create-ranger-services.py @@ -1,16 +1,17 @@ from apache_ranger.model.ranger_service import RangerService -from apache_ranger.client.ranger_client import RangerClient from json import JSONDecodeError -ranger_client = RangerClient('http://ranger:6080', ('admin', 'rangerR0cks!')) +from log_config import configure_logging, get_logger +from ranger_admin_xml_config import get_ranger_client +logger = get_logger(__name__) -def service_not_exists(service): +def service_not_exists(ranger_client, service): try: svc = ranger_client.get_service(service.name) except JSONDecodeError: - return 1 - return 0 if svc is not None else 1 + return True + return svc is None hdfs = RangerService({'name': 'dev_hdfs', 'type': 'hdfs', @@ -148,11 +149,24 @@ def service_not_exists(service): 'ranger.plugin.super.users': 'solr', 'ranger.plugin.solr.policy.refresh.synchronous':'true'}}) -services = [hdfs, yarn, hive, hbase, kafka, knox, kms, trino, ozone, solr] -for service in services: - try: - if service_not_exists(service): - ranger_client.create_service(service) - print(f" {service.name} service created!") - except Exception as e: - print(f"An exception occured: {e}") +def main() -> int: + configure_logging() + ranger_client = get_ranger_client() + services = [hdfs, yarn, hive, hbase, kafka, knox, kms, trino, ozone, solr] + + for service in services: + try: + if service_not_exists(ranger_client, service): + ranger_client.create_service(service) + logger.info("%s service created", service.name) + else: + logger.info("%s service already exists", service.name) + except Exception: + logger.exception("Failed to reconcile Ranger service %s", service.name) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/dev-support/ranger-docker/scripts/admin/dba.py b/dev-support/ranger-docker/scripts/admin/dba.py new file mode 100644 index 0000000000..32cdf84ea0 --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/dba.py @@ -0,0 +1,515 @@ +# +# Licensed 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. See accompanying LICENSE file. +# + +import os +import re +import shlex +import shutil +import subprocess +import sys +import hashlib +from dataclasses import dataclass + +from log_config import configure_logging, get_logger +from ranger_admin_xml_config import load_ranger_admin_site_properties, parse_jdbc_url + +logger = get_logger(__name__) + +JISQL_DEBUG = True + +# Regex used to extract the database name from a JDBC override URL. +_JDBC_DB_NAME_RE = re.compile(r"jdbc:postgresql://[^/]+:\d+/(\w+)") + +# Allowlist for sequence names interpolated into SQL commands. +_SAFE_IDENTIFIER_RE = re.compile(r"^[a-zA-Z_][a-zA-Z0-9_]*$") + +RANGER_HOME = os.getenv("RANGER_HOME") + +# --------------------------------------------------------------------------- +# Custom exceptions +# --------------------------------------------------------------------------- +class ConfigError(RuntimeError): + """Raised when required configuration is missing or invalid.""" + + +class ConnectionError(RuntimeError): # noqa: A001 — shadows builtin intentionally + """Raised when the database connection check fails.""" + + +class SchemaImportError(RuntimeError): + """Raised when a schema file import fails.""" + + +# --------------------------------------------------------------------------- +# SSL configuration dataclass +# --------------------------------------------------------------------------- +@dataclass +class SSLConfig: + """Holds all SSL-related settings with sensible defaults.""" + + enabled: bool = False + required: bool = False + verify_server_certificate: bool = False + auth_type: str = "2-way" + certificate_file: str = "" + key_store: str = "" + key_store_password: str = "" + key_store_type: str = "bcfks" + trust_store: str = "" + trust_store_password: str = "" + trust_store_type: str = "bcfks" + override_jdbc: bool = False + override_jdbc_connection_string: str = "" + + +def _is_true(config: dict, key: str) -> bool: + """Return True if *config[key]* is the string 'true' (case-insensitive).""" + return config.get(key, "false").lower() == "true" + + +def _extract_ssl_config(config: dict) -> SSLConfig: + """Build an SSLConfig from the raw config dictionary.""" + + ssl = SSLConfig() + ssl.override_jdbc = _is_true(config, "is_override_db_connection_string") + ssl.override_jdbc_connection_string = config.get("db_override_connection_string", "").strip() + ssl.enabled = _is_true(config, "db_ssl_enabled") + + if not ssl.enabled: + return ssl + + ssl.required = _is_true(config, "db_ssl_required") + ssl.verify_server_certificate = _is_true(config, "db_ssl_verifyServerCertificate") + ssl.auth_type = config.get("db_ssl_auth_type", "2-way").lower() + ssl.certificate_file = config.get("db_ssl_certificate_file", "") + ssl.trust_store = config.get("javax_net_ssl_trustStore", "") + ssl.trust_store_password = config.get("javax_net_ssl_trustStorePassword", "") + ssl.trust_store_type = config.get("javax_net_ssl_trustStore_type", "bcfks") + ssl.key_store = config.get("javax_net_ssl_keyStore", "") + ssl.key_store_password = config.get("javax_net_ssl_keyStorePassword", "") + ssl.key_store_type = config.get("javax_net_ssl_keyStore_type", "bcfks") + + return ssl + + +def _validate_ssl_files(ssl: SSLConfig) -> None: + """Validate that required SSL files and passwords exist. + + Raises ConfigError instead of calling sys.exit so callers + can handle the error or let it propagate to main. + """ + if not ssl.enabled or not ssl.verify_server_certificate: + return + + if ssl.certificate_file: + if not os.path.exists(ssl.certificate_file): + raise ConfigError(f"SSL certificate file not found: {ssl.certificate_file}") + elif ssl.auth_type == "1-way": + if not os.path.exists(ssl.trust_store): + raise ConfigError(f"SSL truststore file not found: {ssl.trust_store}") + if not ssl.trust_store_password: + raise ConfigError("SSL truststore password is not set") + + if ssl.auth_type == "2-way": + if not os.path.exists(ssl.key_store): + raise ConfigError(f"SSL keystore file not found: {ssl.key_store}") + if not ssl.key_store_password: + raise ConfigError("SSL keystore password is not set") + + +# --------------------------------------------------------------------------- +# Helper utilities +# --------------------------------------------------------------------------- +def _run_command(query: str) -> str: + """Run a shell command and return its stdout.""" + result = subprocess.run(shlex.split(query), stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=False) + return result.stdout + + +def _log_jisql(query: str, db_password: str) -> None: + """Log a Jisql command with the password masked.""" + if JISQL_DEBUG: + logger.info("JISQL %s", query.replace(f" -p '{db_password}'", " -p '********'")) + + +def _validate_identifier(name: str) -> str: + """Return *name* if it matches a strict SQL identifier pattern. + + Prevents accidental SQL injection when sequence/table names are + interpolated into command strings. + """ + if not _SAFE_IDENTIFIER_RE.match(name): + raise ValueError(f"Unsafe SQL identifier rejected: {name!r}") + return name + + +def load_runtime_config() -> dict: + """Load runtime config using XML for JDBC metadata and env for secrets. + + Raises ConfigError if required settings are missing. + """ + props = load_ranger_admin_site_properties() + if not props: + raise ConfigError(f"ranger-admin-site.xml not found or unreadable, RANGER_HOME={RANGER_HOME}") + + jdbc_url = props.get("ranger.jpa.jdbc.url", "") + jdbc_info = parse_jdbc_url(jdbc_url) + + db_flavor = (os.environ.get("RANGER_DB_TYPE") or jdbc_info.get("flavor") or "POSTGRES").upper() + if db_flavor == "POSTGRESQL": + db_flavor = "POSTGRES" + + db_host = os.environ.get("RANGER_ADMIN_DB_HOSTNAME") or jdbc_info.get("host", "") + db_port = os.environ.get("RANGER_ADMIN_DB_PORT") or jdbc_info.get("port", "") + db_name = os.environ.get("RANGER_ADMIN_DB_DATABASE") or jdbc_info.get("database", "") + db_user = os.environ.get("RANGER_ADMIN_DB_USERNAME") or props.get("ranger.jpa.jdbc.user", "") + db_password = os.environ.get("RANGER_ADMIN_DB_PASSWORD", "") + + if not all([db_host, db_name, db_user]): + raise ConfigError("Required JDBC settings (host/name/user) missing from ranger-admin-site.xml") + if not db_password: + raise ConfigError("Required env var RANGER_ADMIN_DB_PASSWORD is not set") + + config = dict(props) + config["DB_FLAVOR"] = db_flavor + config["SQL_CONNECTOR_JAR"] = props.get("ranger.jdbc.sqlconnectorjar", "/usr/share/java/postgresql.jar") + config["db_name"] = db_name + config["db_host"] = f"{db_host}:{db_port}" if db_port else db_host + config["db_user"] = db_user + config["db_password"] = db_password + config["postgres_core_file"] = "db/postgres/optimized/current/ranger_core_db_postgres.sql" + return config + + +# --------------------------------------------------------------------------- +# Database abstraction +# --------------------------------------------------------------------------- +class BaseDB: + """Interface that every DB flavour must implement.""" + + def check_connection(self, db_name, db_user, db_password): + logger.info("---------- Verifying DB connection ----------") + + def check_table(self, db_name, db_user, db_password, table_name): + logger.info("---------- Verifying table ----------") + + def import_db_file(self, db_name, db_user, db_password, file_name): + logger.info("---------- Importing db schema ----------") + + +class PostgresDB(BaseDB): + """PostgreSQL-specific implementation of the DB bootstrap interface.""" + + def __init__(self, *, host: str, sql_connector_jar: str, java_bin: str, ssl: SSLConfig): + self.host = host + self.sql_connector_jar = sql_connector_jar + self.java_bin = java_bin.strip("'") + self.ssl = ssl + + # -- private helpers --------------------------------------------------- + + def _resolve_db_name(self, db_name: str) -> str: + """Extract the actual DB name from a JDBC override URL if active.""" + if not (self.ssl.override_jdbc and self.ssl.override_jdbc_connection_string): + return db_name + match = _JDBC_DB_NAME_RE.search(self.ssl.override_jdbc_connection_string) + return match.group(1) if match else db_name + + def _build_jisql_classpath(self) -> str: + """Locate Jisql lib directories and build a Java classpath.""" + candidates = [os.path.join(RANGER_HOME, "admin", "jisql", "lib"), os.path.join(RANGER_HOME, "jisql", "lib")] + found = [d for d in candidates if os.path.isdir(d)] + if not found: + found = [candidates[0]] # Fall back to the most common layout. + return os.pathsep.join(os.path.join(d, "*") for d in found) + + def _build_ssl_params(self) -> tuple[str, str]: + """Return (url_param, jvm_cert_flags) for SSL.""" + if not self.ssl.enabled: + return "", "" + + ssl = self.ssl + + if ssl.certificate_file: + return f"?ssl=true&sslmode=verify-full&sslrootcert={ssl.certificate_file}", "" + + if ssl.verify_server_certificate or ssl.required: + url_param = "?ssl=true&sslmode=verify-full&sslfactory=org.postgresql.ssl.DefaultJavaSSLFactory" + if ssl.auth_type == "1-way": + cert_flags = ( + f" -Djavax.net.ssl.trustStore={ssl.trust_store}" + f" -Djavax.net.ssl.trustStorePassword={ssl.trust_store_password}" + f" -Djavax.net.ssl.trustStoreType={ssl.trust_store_type}" + ) + else: + cert_flags = ( + f" -Djavax.net.ssl.keyStore={ssl.key_store}" + f" -Djavax.net.ssl.keyStorePassword={ssl.key_store_password}" + f" -Djavax.net.ssl.trustStore={ssl.trust_store}" + f" -Djavax.net.ssl.trustStorePassword={ssl.trust_store_password}" + f" -Djavax.net.ssl.trustStoreType={ssl.trust_store_type}" + f" -Djavax.net.ssl.keyStoreType={ssl.key_store_type}" + ) + return url_param, cert_flags + + return "?ssl=true", "" + + def _get_jisql_cmd(self, user: str, password: str, db_name: str) -> str: + """Build the full Jisql invocation command string.""" + classpath = self._build_jisql_classpath() + ssl_url, ssl_cert = self._build_ssl_params() + cp = f"{self.sql_connector_jar}{os.pathsep}{classpath}" + + if self.ssl.override_jdbc and self.ssl.override_jdbc_connection_string: + cstring = self.ssl.override_jdbc_connection_string + else: + cstring = f"jdbc:postgresql://{self.host}/{db_name}{ssl_url}" + + return ( + f"{self.java_bin} {ssl_cert} -cp {cp} org.apache.util.sql.Jisql" + f" -driver postgresql -cstring '{cstring}'" + f" -u {user} -p '{password}' -noheader -trim -c \\;" + ) + + # -- public interface -------------------------------------------------- + + def check_connection(self, db_name: str, db_user: str, db_password: str) -> bool: + """Verify that we can reach the database. Raises ConnectionError on failure.""" + logger.info("Checking connection to database %s", db_name) + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + query = f'{cmd} -query "SELECT 1;"' + _log_jisql(query, db_password) + + output = _run_command(query) + if "1" in output: + logger.info("Connection successful") + return True + raise ConnectionError(f"Cannot establish connection to database {db_name}") + + def import_db_file(self, db_name: str, db_user: str, db_password: str, file_name: str) -> None: + """Import a SQL schema file into the database. + + Raises FileNotFoundError if the file is missing, or + SchemaImportError if the import subprocess fails. + """ + display_name = os.path.basename(file_name) + if not os.path.isfile(file_name): + raise FileNotFoundError(f"DB schema file not found: {display_name}") + + logger.info("Importing schema to %s from file: %s", db_name, display_name) + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + query = f"{cmd} -input {file_name}" + _log_jisql(query, db_password) + + ret = subprocess.call(shlex.split(query)) + if ret != 0: + raise SchemaImportError(f"Schema import failed for {display_name}") + logger.info("%s imported successfully", display_name) + + def check_table(self, db_name: str, db_user: str, db_password: str, table_name: str) -> bool: + """Return True if *table_name* exists in *db_name*.""" + db_name = self._resolve_db_name(db_name) + logger.info("Verifying table %s in database %s", table_name, db_name) + + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + query = ( + f'{cmd} -query "SELECT table_name FROM information_schema.tables' + f" WHERE table_catalog='{db_name}' AND table_name='{table_name}';\"" + ) + _log_jisql(query, db_password) + + try: + output = _run_command(query) + if output and table_name.lower() in output.lower(): + logger.info("Table %s exists in %s", table_name, db_name) + return True + logger.info("Table %s does not exist in %s", table_name, db_name) + return False + except (subprocess.SubprocessError, OSError) as exc: + logger.error("Error checking table: %s", exc) + return False + + def check_sequence(self, db_name: str, db_user: str, db_password: str, sequence_name: str) -> bool: + """Return True if *sequence_name* exists in *db_name*.""" + db_name = self._resolve_db_name(db_name) + seq = _validate_identifier(sequence_name).lower() + logger.info("Verifying sequence %s in database %s", seq, db_name) + + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + query = ( + f'{cmd} -query "SELECT sequence_name FROM information_schema.sequences' + f" WHERE sequence_schema='public' AND sequence_name='{seq}';\"" + ) + _log_jisql(query, db_password) + + try: + output = _run_command(query) + if output and seq in output.lower(): + logger.info("Sequence %s exists in %s", seq, db_name) + return True + logger.info("Sequence %s does not exist in %s", seq, db_name) + return False + except (subprocess.SubprocessError, OSError) as exc: + logger.error("Error checking sequence: %s", exc) + return False + + def ensure_sequence(self, db_name: str, db_user: str, db_password: str, sequence_name: str) -> bool: + """Ensure a sequence exists, creating it if necessary. + + Some Ranger distributions create sequences via patch SQL; when + bootstrapping with only the core schema, we may need to create + them explicitly. + """ + if self.check_sequence(db_name, db_user, db_password, sequence_name): + return True + + seq = _validate_identifier(sequence_name).lower() + logger.warning("Attempting to create missing sequence: %s", seq) + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + + for sql in (f"CREATE SEQUENCE {seq};", f"CREATE SEQUENCE IF NOT EXISTS {seq};"): + query = f'{cmd} -query "{sql}"' + _log_jisql(query, db_password) + try: + subprocess.call(shlex.split(query)) + except (subprocess.SubprocessError, OSError) as exc: + logger.warning("Sequence create raised: %s", exc) + + if self.check_sequence(db_name, db_user, db_password, sequence_name): + logger.info("Sequence %s is present", seq) + return True + + logger.error("Failed to ensure required sequence: %s", seq) + return False + + def update_portal_user_password(self, db_name: str, db_user: str, db_password: str, login_id: str, plain_password: str) -> None: + """Set a portal user's password using Ranger's legacy seed encoding. + + Fresh schema imports seed admin/usersync/tagsync with fixed hashes in SQL. + Rewrite those seeded hashes from env before Ranger Admin starts so first + login matches the container configuration. + """ + encoded_password = hashlib.md5(f"{plain_password}{{{login_id}}}".encode("utf-8")).hexdigest() + cmd = self._get_jisql_cmd(db_user, db_password, db_name) + query = ( + f'{cmd} -query "UPDATE x_portal_user ' + f"SET password='{encoded_password}' " + f"WHERE login_id='{login_id}';\"" + ) + logger.info("Setting initial password for Ranger user %s from environment", login_id) + _log_jisql(query, db_password) + + ret = subprocess.call(shlex.split(query)) + if ret != 0: + raise SchemaImportError(f"Failed to seed password for Ranger user {login_id}") + + +# --------------------------------------------------------------------------- +# Java binary resolution +# --------------------------------------------------------------------------- +def _resolve_java_bin() -> str: + """Determine the path to the java binary.""" + java_home = os.environ.get("JAVA_HOME", "").strip() + if java_home: + return os.path.join(java_home, "bin", "java") + java_bin = shutil.which("java") or "java" + logger.warning("JAVA_HOME not set; using JAVA_BIN=%s", java_bin) + return java_bin + + +# --------------------------------------------------------------------------- +# Schema file resolution +# --------------------------------------------------------------------------- +def _resolve_schema_file(core_file_rel: str) -> str: + """Return the absolute path to the core schema SQL file. + + Raises ConfigError if the file does not exist. + """ + if os.getenv("RANGER_HOME"): + path = os.path.join(RANGER_HOME, "admin", core_file_rel) + else: + path = os.path.join(RANGER_HOME, core_file_rel) + + logger.info("Schema file path: %s", path) + if not os.path.isfile(path): + raise ConfigError(f"Schema file not found: {path} (RANGER_HOME={RANGER_HOME})") + return path + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- +VERSION_TABLE = "x_db_version_h" +CRITICAL_SEQUENCE = "X_TRX_LOG_SEQ" + + +def seed_initial_user_passwords(db: PostgresDB, db_name: str, db_user: str, db_password: str) -> None: + for login_id, env_var in (("admin", "RANGER_ADMIN_PASSWORD"), ("rangerusersync", "RANGER_USERSYNC_PASSWORD"), ("rangertagsync", "RANGER_TAGSYNC_PASSWORD")): + plain_password = os.environ.get(env_var, "") + if plain_password: + db.update_portal_user_password(db_name, db_user, db_password, login_id, plain_password) + + +def main(argv: list[str]) -> None: + + configure_logging() + config = load_runtime_config() + + db_flavor = config["DB_FLAVOR"] + if db_flavor != "POSTGRES": + raise ConfigError("Ranger Admin docker currently supports only PostgreSQL") + + java_bin = _resolve_java_bin() + logger.info("DB FLAVOR: %s", db_flavor) + + ssl = _extract_ssl_config(config) + _validate_ssl_files(ssl) + + db_name, db_user, db_password = config["db_name"], config["db_user"], config["db_password"] + + db = PostgresDB(host=config["db_host"], sql_connector_jar=config["SQL_CONNECTOR_JAR"], java_bin=java_bin, ssl=ssl) + schema_file = _resolve_schema_file(config["postgres_core_file"]) + + logger.info("--------- Verifying Ranger DB connection ---------") + db.check_connection(db_name, db_user, db_password) + + if len(argv) > 1: + return # Extra CLI arguments present — skip schema initialisation. + + logger.info("--------- Verifying Ranger DB tables ---------") + if db.check_table(db_name, db_user, db_password, VERSION_TABLE): + logger.info("Database schema already initialised") + if not db.ensure_sequence(db_name, db_user, db_password, CRITICAL_SEQUENCE): + logger.warning("Critical sequence %s still missing, but schema appears initialised. Service creation may fail.", CRITICAL_SEQUENCE) + return + + logger.info("--------- Importing Ranger Core DB Schema ---------") + db.import_db_file(db_name, db_user, db_password, schema_file) + seed_initial_user_passwords(db, db_name, db_user, db_password) + + if not db.check_table(db_name, db_user, db_password, VERSION_TABLE): + raise SchemaImportError(f"Schema import completed but {VERSION_TABLE} table not found") + + if not db.ensure_sequence(db_name, db_user, db_password, CRITICAL_SEQUENCE): + raise SchemaImportError( f"Sequence {CRITICAL_SEQUENCE} required for service creation. Schema import/patch may be incomplete.") + + logger.info("Database schema imported successfully") + + +if __name__ == "__main__": + try: + main(sys.argv) + except (ConfigError, ConnectionError, SchemaImportError) as exc: + logger.error("%s", exc) + sys.exit(1) diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml b/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml new file mode 100644 index 0000000000..d020b5dff9 --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/ranger-admin-default-site.xml @@ -0,0 +1,615 @@ + + + + + + + ranger.jdbc.sqlconnectorjar + /usr/share/java/postgresql.jar + + + + ranger.service.user + ranger + + + + ranger.service.group + ranger + + + + ajp.enabled + false + + + + + + + ranger.db.maxrows.default + 200 + + + ranger.db.min_inlist + 20 + + + ranger.ui.defaultDateformat + MM/dd/yyyy + + + ranger.db.defaultDateformat + yyyy-MM-dd + + + + + ranger.ajax.auth.required.code + 401 + + + ranger.ajax.auth.success.page + /ajax_success.html + + + ranger.logout.success.page + /login.jsp?action=logged_out + + + ranger.ajax.auth.failure.page + /ajax_failure.jsp + + + + + ranger.users.roles.list + ROLE_SYS_ADMIN, ROLE_USER, ROLE_OTHER, ROLE_ANON, ROLE_KEY_ADMIN, ROLE_ADMIN_AUDITOR, ROLE_KEY_ADMIN_AUDITOR + + + + ranger.mail.enabled + true + + + ranger.mail.smtp.auth + false + + + ranger.mail.retry.sleep.ms + 2000 + + + ranger.mail.retry.max.count + 5 + + + ranger.mail.retry.sleep.incr_factor + 1 + + + ranger.mail.listener.enable + false + + + + ranger.second_level_cache + true + + + ranger.use_query_cache + true + + + + + ranger.user.firstname.maxlength + 16 + + + ranger.bookmark.name.maxlen + 150 + + + + + ranger.rbac.enable + false + + + + + ranger.rest.paths + org.apache.ranger.rest,xa.rest + + + + + ranger.password.hidden + ***** + + + ranger.resource.accessControl.enabled + true + + + ranger.xuser.createdByUserId + 1 + + + + + + ranger.allow.hack + 1 + + + + + + ranger.log.SC_NOT_MODIFIED + false + + + + + ranger.servlet.mapping.url.pattern + service + + + + + + + + ranger.file.separator + / + + + + ranger.db.access.filter.enable + true + + + ranger.moderation.enabled + false + + + ranger.userpref.enabled + false + + + + + + ranger.valve.errorreportvalve.showserverinfo + false + + + ranger.valve.errorreportvalve.showreport + false + + + + + + + ranger.unixauth.remote.login.enabled + true + + + ranger.unixauth.service.hostname + localhost + + + ranger.unixauth.service.port + 5151 + + + ranger.unixauth.ssl.enabled + true + + + ranger.unixauth.debug + false + + + ranger.unixauth.server.cert.validation + false + + + + ranger.unixauth.keystore + keystore.jks + + + ranger.unixauth.keystore.credential.alias + unixAuthKeyStoreAlias + + + ranger.unixauth.keystore.password + password + + + ranger.unixauth.truststore + cacerts + + + ranger.unixauth.truststore.credential.alias + unixAuthTrustStoreAlias + + + ranger.unixauth.truststore.password + changeit + + + + + + maven.project.version + 0.5.0 + + + + + + ranger.service.shutdown.port + 6085 + + + + ranger.service.shutdown.command + SHUTDOWN + + + + ranger.service.https.attrib.ssl.protocol + TLSv1.2 + + + + ranger.service.https.attrib.client.auth + false + + + + ranger.accesslog.dateformat + yyyy-MM-dd + + + + ranger.accesslog.pattern + %h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" + + + + ranger.contextName + / + + + + + ranger.jpa.showsql + false + + + + + ranger.env.local + true + + + + + ranger.jpa.jdbc.dialect + org.eclipse.persistence.platform.database.PostgreSQLPlatform + + + + + ranger.jpa.jdbc.maxpoolsize + 40 + + + + + ranger.jpa.jdbc.minpoolsize + 5 + + + + + ranger.jpa.jdbc.idletimeout + 300000 + + + + + ranger.jpa.jdbc.maxlifetime + 1800000 + + + + + ranger.jpa.jdbc.preferredtestquery + select 1 + + + + + ranger.jpa.jdbc.connectiontimeout + 30000 + + + + + ranger.jpa.jdbc.batch-clear.enable + true + property to enable bulk mode optimization + + + + ranger.jpa.jdbc.batch-clear.size + 10 + batch size (in number of policies) to flush and clear jdbc statements during batch policy import/delete + + + + ranger.jpa.jdbc.batch-persist.size + 500 + batch size (in number of objects) to flush and clear jdbc statements during jpa persistence + + + + ranger.jpa.jdbc.credential.alias + ranger.db.password + + + + + ranger.credential.provider.path + /etc/ranger/admin/rangeradmin.jceks + + + + + ranger.logs.base.dir + user.home + + + + + ranger.jpa.audit.jdbc.dialect + org.eclipse.persistence.platform.database.PostgreSQLPlatform + + + + + ranger.jpa.audit.jdbc.credential.alias + ranger.auditdb.password + + + + + ranger.ldap.binddn.credential.alias + ranger.ldap.binddn.password + + + + + ranger.ldap.ad.binddn.credential.alias + ranger.ad.binddn.password + + + + + ranger.resource.lookup.timeout.value.in.ms + 1000 + + + + + ranger.validate.config.timeout.value.in.ms + 10000 + + + + + ranger.timed.executor.max.threadpool.size + 10 + + + + ranger.timed.executor.queue.size + 100 + + + ranger.solr.audit.credential.alias + ranger.solr.password + + + + ranger.audit.solr.time.interval + 60000 + Time in milliseconds + + + ranger.audit.solr.bootstrap.enabled + true + + + ranger.audit.solr.max.retry + 30 + Maximum no. of retry to setup solr + + + ranger.sha256Password.update.disable + false + + + + ranger.password.history.count + 4 + + + + + + ranger.jpa.audit.jdbc.driver + net.sf.log4jdbc.DriverSpy + + + + ranger.jpa.audit.jdbc.url + jdbc:log4jdbc:mysql://localhost/rangeraudit + + + + ranger.jpa.audit.jdbc.user + rangerlogger + + + + ranger.jpa.audit.jdbc.password + rangerlogger + + + + ranger.supportedcomponents + + + + + ranger.sso.cookiename + hadoop-jwt + + + ranger.sso.query.param.originalurl + originalUrl + + + ranger.rest-csrf.enabled + true + + + ranger.rest-csrf.custom-header + X-XSRF-HEADER + + + ranger.rest-csrf.methods-to-ignore + GET,OPTIONS,HEAD,TRACE + + + ranger.rest-csrf.browser-useragents-regex + Mozilla,Opera,Chrome + + + ranger.krb.browser-useragents-regex + Mozilla,Opera,Chrome + + + ranger.db.ssl.enabled + false + + + ranger.db.ssl.required + false + + + ranger.db.ssl.verifyServerCertificate + false + + + ranger.db.ssl.auth.type + 2-way + + + ranger.db.ssl.certificateFile + + + + ranger.truststore.file.type + jks + + + ranger.keystore.file.type + jks + + + ranger.keystore.file + + + + ranger.keystore.alias + keyStoreAlias + + + ranger.keystore.password + + + + ranger.truststore.file + + + + ranger.truststore.alias + trustStoreAlias + + + ranger.truststore.password + + + + ranger.service.https.attrib.ssl.enabled.protocols + TLSv1.2 + + + + ranger.password.encryption.key + tzL1AKl5uc4NKYaoQ4P3WLGIBFPXWPWdu1fRm9004jtQiV + + + ranger.password.salt + f77aLYLo + + + ranger.password.iteration.count + 1000 + + + ranger.password.encryption.algorithm + PBEWithHmacSHA512AndAES_128 + + + ranger.default.browser-useragents + Mozilla,Opera,Chrome + + + ranger.admin.cookie.name + RANGERADMINSESSIONID + + + ranger.tomcat.work.dir + + + + ranger.allow.kerberos.auth.login.browser + false + + diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties b/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties deleted file mode 100644 index cf8a58feca..0000000000 --- a/dev-support/ranger-docker/scripts/admin/ranger-admin-install-postgres.properties +++ /dev/null @@ -1,109 +0,0 @@ -# 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. - -# -# This file provides a list of the deployment variables for the Policy Manager Web Application -# - -PYTHON_COMMAND_INVOKER=python3 -RANGER_ADMIN_LOG_DIR=/var/log/ranger -RANGER_PID_DIR_PATH=/var/run/ranger -DB_FLAVOR=POSTGRES -SQL_CONNECTOR_JAR=/usr/share/java/postgresql.jar -RANGER_ADMIN_LOGBACK_CONF_FILE=/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf/logback.xml - -db_root_user=postgres -db_root_password=rangerR0cks! -db_host=ranger-db - -db_name=ranger -db_user=rangeradmin -db_password=rangerR0cks! - -postgres_core_file=db/postgres/optimized/current/ranger_core_db_postgres.sql -postgres_audit_file=db/postgres/xa_audit_db_postgres.sql -mysql_core_file=db/mysql/optimized/current/ranger_core_db_mysql.sql -mysql_audit_file=db/mysql/xa_audit_db.sql - -rangerAdmin_password=rangerR0cks! -rangerTagsync_password=rangerR0cks! -rangerUsersync_password=rangerR0cks! -keyadmin_password=rangerR0cks! - - -audit_store=solr -audit_solr_urls=http://ranger-solr:8983/solr/ranger_audits -audit_solr_collection_name=ranger_audits - -# audit_store=elasticsearch -audit_elasticsearch_urls= -audit_elasticsearch_port=9200 -audit_elasticsearch_protocol=http -audit_elasticsearch_user=elastic -audit_elasticsearch_password=elasticsearch -audit_elasticsearch_index=ranger_audits -audit_elasticsearch_bootstrap_enabled=true - -policymgr_external_url=http://ranger-admin:6080 -policymgr_http_enabled=true - -unix_user=ranger -unix_user_pwd=ranger -unix_group=ranger - -# Following variables are referenced in db_setup.py. Do not remove these -oracle_core_file= -sqlserver_core_file= -sqlanywhere_core_file= -cred_keystore_filename= - -# ################# DO NOT MODIFY ANY VARIABLES BELOW ######################### -# -# --- These deployment variables are not to be modified unless you understand the full impact of the changes -# -################################################################################ -XAPOLICYMGR_DIR=$PWD -app_home=$PWD/ews/webapp -TMPFILE=$PWD/.fi_tmp -LOGFILE=$PWD/logfile -LOGFILES="$LOGFILE" - -JAVA_BIN='java' -JAVA_VERSION_REQUIRED='1.8' - -ranger_admin_max_heap_size=1g -#retry DB and Java patches after the given time in seconds. -PATCH_RETRY_INTERVAL=120 -STALE_PATCH_ENTRY_HOLD_TIME=10 - -hadoop_conf= -authentication_method=UNIX - -#------------ Kerberos Config ----------------- -spnego_principal=HTTP/ranger.rangernw@EXAMPLE.COM -spnego_keytab=/etc/keytabs/HTTP.keytab -token_valid=30 -admin_principal=rangeradmin/ranger.rangernw@EXAMPLE.COM -admin_keytab=/etc/keytabs/rangeradmin.keytab -lookup_principal=rangerlookup/ranger.rangernw@EXAMPLE.COM -lookup_keytab=/etc/keytabs/rangerlookup.keytab -audit_jaas_client_loginModuleName=com.sun.security.auth.module.Krb5LoginModule -audit_jaas_client_loginModuleControlFlag=required -audit_jaas_client_option_useKeyTab=true -audit_jaas_client_option_storeKey=true -audit_jaas_client_option_useTicketCache=true -audit_jaas_client_option_serviceName=ranger -audit_jaas_client_option_keyTab=/etc/keytabs/rangeradmin.keytab -audit_jaas_client_option_principal=rangeradmin/ranger.rangernw@EXAMPLE.COM diff --git a/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml b/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml new file mode 100644 index 0000000000..e8ee8a7902 --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/ranger-admin-site.xml @@ -0,0 +1,523 @@ + + + ranger.externalurl + http://ranger-admin:6080 + + + + ranger.scheduler.enabled + true + + + + ranger.jdbc.sqlconnectorjar + /usr/share/java/postgresql.jar + SQL connector JAR file path for the dockerized Ranger Admin runtime + + + ranger.jpa.jdbc.driver + org.postgresql.Driver + + + + ranger.jpa.jdbc.url + jdbc:postgresql://ranger-db:5432/ranger + + + + ranger.jpa.jdbc.user + rangeradmin + + + + ranger.jpa.jdbc.dialect + org.eclipse.persistence.platform.database.PostgreSQLPlatform + JPA database dialect for PostgreSQL + + + ranger.jpa.jdbc.maxpoolsize + 40 + Maximum connection pool size + + + ranger.jpa.jdbc.minpoolsize + 5 + Minimum connection pool size + + + ranger.jpa.jdbc.idletimeout + 300000 + Idle connection timeout in milliseconds (5 minutes) + + + ranger.jpa.jdbc.maxlifetime + 1800000 + Maximum connection lifetime in milliseconds (30 minutes) + + + ranger.jpa.jdbc.preferredtestquery + select 1 + Query used to test database connections + + + ranger.jpa.jdbc.connectiontimeout + 30000 + Connection timeout in milliseconds (30 seconds) + + + ranger.jpa.jdbc.batch-clear.enable + true + Enable bulk mode optimization for batch operations + + + ranger.jpa.jdbc.batch-clear.size + 10 + Batch size (in number of policies) to flush and clear JDBC statements during batch policy import/delete + + + ranger.jpa.jdbc.batch-persist.size + 500 + Batch size (in number of objects) to flush and clear JDBC statements during JPA persistence + + + ranger.jpa.jdbc.credential.alias + ranger.db.password + Credential alias for database password in credential store + + + ranger.credential.provider.path + /etc/ranger/admin/rangeradmin.jceks + Path to credential provider keystore file + + + ranger.jpa.audit.jdbc.dialect + org.eclipse.persistence.platform.database.PostgreSQLPlatform + JPA database dialect for audit database (PostgreSQL) + + + ranger.jpa.audit.jdbc.credential.alias + ranger.auditdb.password + Credential alias for audit database password in credential store + + + ranger.jpa.showsql + false + Enable SQL query logging (for debugging) + + + ranger.env.local + true + Local environment flag + + + ranger.db.maxrows.default + 200 + Default maximum rows returned from database queries + + + ranger.db.min_inlist + 20 + Minimum items for IN clause optimization + + + ranger.db.defaultDateformat + yyyy-MM-dd + Default date format for database operations + + + ranger.service.http.enabled + true + + + + ranger.authentication.method + UNIX + + + + + ranger.spnego.kerberos.principal + HTTP/ranger.rangernw@EXAMPLE.COM + + + + ranger.spnego.kerberos.keytab + /etc/keytabs/HTTP.keytab + + + + ranger.admin.kerberos.token.valid.seconds + 30 + + + + ranger.admin.kerberos.principal + rangeradmin/ranger.rangernw@EXAMPLE.COM + + + + ranger.admin.kerberos.keytab + /etc/keytabs/rangeradmin.keytab + + + + ranger.lookup.kerberos.principal + rangerlookup/ranger.rangernw@EXAMPLE.COM + + + + ranger.lookup.kerberos.keytab + /etc/keytabs/rangerlookup.keytab + + + + xasecure.audit.jaas.Client.loginModuleName + com.sun.security.auth.module.Krb5LoginModule + + + + xasecure.audit.jaas.Client.loginModuleControlFlag + required + + + + xasecure.audit.jaas.Client.option.useKeyTab + true + + + + xasecure.audit.jaas.Client.option.storeKey + true + + + + xasecure.audit.jaas.Client.option.useTicketCache + true + + + + xasecure.audit.jaas.Client.option.serviceName + ranger + + + + xasecure.audit.jaas.Client.option.keyTab + /etc/keytabs/rangeradmin.keytab + + + + xasecure.audit.jaas.Client.option.principal + rangeradmin/ranger.rangernw@EXAMPLE.COM + + + + + ranger.ldap.url + ldap:// + + + + ranger.ldap.user.dnpattern + uid={0},ou=users,dc=xasecure,dc=net + + + + ranger.ldap.group.searchbase + ou=groups,dc=xasecure,dc=net + + + + ranger.ldap.group.searchfilter + (member=uid={0},ou=users,dc=xasecure,dc=net) + + + + ranger.ldap.group.roleattribute + cn + + + + ranger.ldap.base.dn + + LDAP base dn or search base + + + ranger.ldap.bind.dn + + LDAP bind dn or manager dn + + + ranger.ldap.bind.password + + LDAP bind password + + + ranger.ldap.default.role + ROLE_USER + + + ranger.ldap.referral + + follow or ignore + + + ranger.ldap.ad.domain + example.com + + + + ranger.ldap.ad.url + + ldap:// + + + ranger.ldap.ad.base.dn + dc=example,dc=com + AD base dn or search base + + + ranger.ldap.ad.bind.dn + cn=administrator,ou=users,dc=example,dc=com + AD bind dn or manager dn + + + ranger.ldap.ad.bind.password + + AD bind password + + + ranger.ldap.ad.referral + + follow or ignore + + + ranger.service.https.attrib.ssl.enabled + false + + + ranger.service.https.attrib.keystore.keyalias + myKey + + + ranger.service.https.attrib.keystore.pass + _ + + + ranger.service.host + localhost + + + ranger.service.http.port + 6080 + + + ranger.service.https.port + 6080 + + + ranger.service.https.attrib.keystore.file + /etc/ranger/admin/keys/server.jks + + + ranger.ldap.user.searchfilter + (uid={0}) + + + + ranger.ldap.ad.user.searchfilter + (sAMAccountName={0}) + + + + ranger.authentication.allow.trustedproxy + false + + + ranger.sso.providerurl + + + + ranger.sso.enabled + true + + + ranger.sso.browser.useragent + Mozilla,chrome + + + ranger.supportedcomponents + + + + ranger.downloadpolicy.session.log.enabled + false + + + ranger.kms.service.user.hdfs + hdfs + + + ranger.kms.service.user.hive + hive + + + ranger.kms.service.user.om + om + + + ranger.kms.service.user.kudu + kudu + + + ranger.audit.hive.query.visibility + true + + + + ranger.service.https.attrib.keystore.credential.alias + keyStoreCredentialAlias + + + ranger.tomcat.ciphers + + + + ranger.admin.cookie.name + RANGERADMINSESSIONID + + + ranger.plugins.hdfs.serviceuser + hdfs + + + ranger.plugins.hive.serviceuser + hive + + + ranger.plugins.knox.serviceuser + knox + + + ranger.plugins.yarn.serviceuser + yarn + + + ranger.plugins.kafka.serviceuser + kafka + + + ranger.plugins.kraft.serviceuser + kraft + + + ranger.plugins.mirror_maker.serviceuser + kafka_mirror_maker + + + ranger.plugins.kudu.serviceuser + kudu + + + ranger.plugins.cruise_control.serviceuser + cruisecontrol + + + ranger.plugins.cruise_control_mr.serviceuser + cc_metric_reporter + + + ranger.plugins.schemaregistry.serviceuser + schemaregistry + + + ranger.plugins.streams_messaging_manager.serviceuser + streamsmsgmgr + + + ranger.plugins.streams_replication_manager.serviceuser + streamsrepmgr + + + ranger.plugins.sql_stream_builder.serviceuser + ssb + + + ranger.plugins.nifi.serviceuser + nifi + + + ranger.plugins.atlas.serviceuser + atlas + + + ranger.plugins.nifiregistry.serviceuser + nifiregistry + + + ranger.plugins.impala.serviceuser + impala + + + ranger.plugins.ozone.serviceuser + om + + + ranger.plugins.solr.serviceuser + solr + + + ranger.plugins.tagsync.serviceuser + rangertagsync + + + ranger.plugins.hue.serviceuser + hue + + + ranger.plugins.trino.serviceuser + trino + + + ranger.plugins.polaris.serviceuser + polaris + + + ranger.default.policy.groups + c_ranger_admin_groups + + + ranger.contextName + / + + + + ranger.audit.source.type + solr + solr (indexed audits) or db (legacy RDBMS) + + + ranger.audit.solr.urls + http://ranger-solr:8983/solr/ranger_audits + Standalone Solr audit (HTTP); core created by udf Solr image + + + ranger.audit.solr.zookeepers + + Empty for Solr HTTP mode (no SolrCloud ZK) + + + ranger.audit.solr.collection.name + ranger_audits + + + + ranger.solr.audit.user + + + + + ranger.solr.audit.user.password + + + + \ No newline at end of file diff --git a/dev-support/ranger-docker/scripts/admin/ranger.sh b/dev-support/ranger-docker/scripts/admin/ranger.sh index 6a8c26eb46..7aa3c31862 100755 --- a/dev-support/ranger-docker/scripts/admin/ranger.sh +++ b/dev-support/ranger-docker/scripts/admin/ranger.sh @@ -1,67 +1,288 @@ #!/bin/bash -# 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. - -if [ ! -e ${RANGER_HOME}/.setupDone ] -then - SETUP_RANGER=true -else - SETUP_RANGER=false -fi +set -euo pipefail + +RANGER_HOME="${RANGER_HOME:-/opt/ranger}" +RANGER_ADMIN_DIR="${RANGER_HOME}/admin" +CONF_DIR="${RANGER_ADMIN_CONF:-${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/classes/conf}" +CONFIGS_DIR="${RANGER_ADMIN_DIR}/configs" +CONFIG_XML_DEST="${CONF_DIR}/ranger-admin-site.xml" +ADMIN_XML_HELPER="/home/ranger/scripts/ranger_admin_xml_config.py" +USER_PASSWORD_BOOTSTRAP_HELPER="/home/ranger/scripts/user_password_bootstrap.py" +SERVICES_MARKER="/opt/ranger/.rangeradminservicescreated" + +sync_admin_configs() { + local conf_file + mkdir -p "${CONF_DIR}" + + for conf_file in \ + "ranger-admin-site.xml" \ + "core-site.xml" \ + "ranger-admin-default-site.xml"; do + if [ -f "${CONFIGS_DIR}/${conf_file}" ]; then + cp -f "${CONFIGS_DIR}/${conf_file}" "${CONF_DIR}/${conf_file}" + fi + done +} + +xml_prop() { + local key="$1" + local file="${2:-${CONFIG_XML_DEST}}" + + [ -f "${file}" ] || return 0 + python3 "${ADMIN_XML_HELPER}" get-property --file "${file}" --name "${key}" +} + +get_config_value() { + local env_key="$1" + local xml_key="$2" + local default_value="${3:-}" + local value="${!env_key:-}" + + if [ -n "${value}" ]; then + printf '%s\n' "${value}" + return 0 + fi + + value="$(xml_prop "${xml_key}")" + if [ -n "${value}" ]; then + printf '%s\n' "${value}" + return 0 + fi + + printf '%s\n' "${default_value}" +} + +db_config_field() { + local field="$1" + python3 "${ADMIN_XML_HELPER}" get-db-field --file "${CONFIG_XML_DEST}" --field "${field}" +} + +sync_db_password_property() { + local pass="${RANGER_ADMIN_DB_PASSWORD:-}" -if [ "${SETUP_RANGER}" == "true" ] -then - if [ "${KERBEROS_ENABLED}" == "true" ] - then - ${RANGER_SCRIPTS}/wait_for_keytab.sh rangeradmin.keytab - ${RANGER_SCRIPTS}/wait_for_keytab.sh rangerlookup.keytab - ${RANGER_SCRIPTS}/wait_for_keytab.sh HTTP.keytab - ${RANGER_SCRIPTS}/wait_for_testusers_keytab.sh + if [ -z "${pass}" ] || [ ! -f "${CONFIG_XML_DEST}" ]; then + return 0 fi - cd "${RANGER_HOME}"/admin || exit - if ./setup.sh; - then - if [ "${KERBEROS_ENABLED}" == "true" ] - then - cp ${RANGER_SCRIPTS}/core-site.xml ${RANGER_HOME}/admin/conf/core-site.xml + python3 "${ADMIN_XML_HELPER}" set-property \ + --file "${CONFIG_XML_DEST}" \ + --name "ranger.jpa.jdbc.password" \ + --value "${pass}" \ + --create +} + +ensure_jdbc_driver() { + # ensure the JDBC driver is visible to the webapp classloader. + local src + src="$(get_config_value "SQL_CONNECTOR_JAR" "ranger.jdbc.sqlconnectorjar" "/usr/share/java/postgresql.jar")" + local libdir="${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/lib" + + if [ -f "${src}" ]; then + mkdir -p "${libdir}" 2>/dev/null || true + if [ ! -f "${libdir}/$(basename "${src}")" ]; then + cp -f "${src}" "${libdir}/" 2>/dev/null || true fi + fi +} - touch "${RANGER_HOME}"/.setupDone - else - echo "Ranger Admin Setup Script didn't complete proper execution." +prepare_admin_runtime() { + local log_dir="${RANGER_ADMIN_LOG_DIR:-/var/log/ranger}" + local logback_conf="${RANGER_ADMIN_LOGBACK_CONF_FILE:-${CONF_DIR}/logback.xml}" + local pid_dir="${RANGER_PID_DIR_PATH:-/var/run/ranger}" + local env_logdir="${CONF_DIR}/ranger-admin-env-logdir.sh" + local env_logback="${CONF_DIR}/ranger-admin-env-logback-conf-file.sh" + local legacy_log_dir="${RANGER_ADMIN_DIR}/ews/logs" + local conf_dist_dir="${RANGER_ADMIN_DIR}/ews/webapp/WEB-INF/classes/conf.dist" + + export RANGER_ADMIN_LOG_DIR="${log_dir}" + export RANGER_ADMIN_LOGBACK_CONF_FILE="${logback_conf}" + export RANGER_PID_DIR_PATH="${pid_dir}" + + mkdir -p "${CONF_DIR}" "${log_dir}" "${pid_dir}" + + for conf_file in "security-applicationContext.xml" "logback.xml"; do + if [ -f "${conf_dist_dir}/${conf_file}" ]; then + cp -f "${conf_dist_dir}/${conf_file}" "${CONF_DIR}/${conf_file}" + fi + done + + if [ ! -e "${legacy_log_dir}" ]; then + ln -s "${log_dir}" "${legacy_log_dir}" 2>/dev/null || mkdir -p "${legacy_log_dir}" fi -fi -cd ${RANGER_HOME}/admin && ./ews/ranger-admin-services.sh start + printf 'export RANGER_ADMIN_LOG_DIR=%s\n' "${RANGER_ADMIN_LOG_DIR}" > "${env_logdir}" + chmod 755 "${env_logdir}" -if [ "${SETUP_RANGER}" == "true" ] -then - # Wait for Ranger Admin to become ready - sleep 30 - python3 ${RANGER_SCRIPTS}/create-ranger-services.py -fi + printf 'export RANGER_ADMIN_LOGBACK_CONF_FILE=%s\n' "${RANGER_ADMIN_LOGBACK_CONF_FILE}" > "${env_logback}" + chmod 755 "${env_logback}" +} + +admin_pid() { + local pid_dir="${RANGER_PID_DIR_PATH:-/var/run/ranger}" + local pid_name="${RANGER_ADMIN_PID_NAME:-rangeradmin.pid}" + local pidf="${pid_dir}/${pid_name}" + + if [ -f "${pidf}" ]; then + cat "${pidf}" 2>/dev/null || true + return 0 + fi + + ps -ef | grep java | grep -- '-Dproc_rangeradmin' | grep -v grep | awk '{ print $2 }' | head -n 1 +} -RANGER_ADMIN_PID=`ps -ef | grep -v grep | grep -i "org.apache.ranger.server.tomcat.EmbeddedServer" | awk '{print $2}'` +wait_for_admin() { + local timeout_s="${1:-180}" + local start + start="$(date +%s)" -# prevent the container from exiting -if [ -z "$RANGER_ADMIN_PID" ] -then - echo "Ranger Admin process probably exited, no process id found!" -else - tail --pid=$RANGER_ADMIN_PID -f /dev/null + while true; do + local pid + pid="$(admin_pid || true)" + if [ -n "${pid}" ] && ps -p "${pid}" >/dev/null 2>&1; then + if command -v curl >/dev/null 2>&1; then + # login.jsp returns 200 once the webapp is fully initialized + if curl -fsS "http://127.0.0.1:6080/login.jsp" >/dev/null 2>&1; then + return 0 + fi + else + return 0 + fi + fi + + if [ $(( $(date +%s) - start )) -ge "${timeout_s}" ]; then + return 1 + fi + sleep 3 + done +} + +port_open() { + local host="$1" + local port="$2" + + if command -v nc >/dev/null 2>&1; then + nc -z -w 2 "${host}" "${port}" >/dev/null 2>&1 + return $? + fi + + # Fallback: bash /dev/tcp (may be disabled in some environments) + (exec 3<>"/dev/tcp/${host}/${port}") >/dev/null 2>&1 +} + +check_db_ready() { + local flavor="$1" + local host="$2" + local port="$3" + local db="$4" + local user="$5" + local pass="$6" + + case "${flavor}" in + POSTGRES|postgres|Postgres|POSTGRESQL|postgresql) + if command -v pg_isready >/dev/null 2>&1; then + PGPASSWORD="${pass}" pg_isready -h "${host}" -p "${port}" -U "${user}" -d "${db}" >/dev/null 2>&1 + return $? + fi + if command -v psql >/dev/null 2>&1; then + PGPASSWORD="${pass}" psql "host=${host} port=${port} user=${user} dbname=${db} sslmode=disable" \ + -v ON_ERROR_STOP=1 -tAc "select 1" >/dev/null 2>&1 + return $? + fi + port_open "${host}" "${port}" + return $? + ;; + MYSQL|mysql|MySQL|MARIADB|mariadb) + if command -v mysqladmin >/dev/null 2>&1; then + MYSQL_PWD="${pass}" mysqladmin ping -h "${host}" -P "${port}" -u "${user}" --silent >/dev/null 2>&1 + return $? + fi + if command -v mysql >/dev/null 2>&1; then + MYSQL_PWD="${pass}" mysql -h "${host}" -P "${port}" -u "${user}" -D "${db}" -e "select 1" >/dev/null 2>&1 + return $? + fi + port_open "${host}" "${port}" + return $? + ;; + *) + port_open "${host}" "${port}" + return $? + ;; + esac +} + +wait_for_db_ready_or_timeout() { + local timeout_s="${1:-600}" + + local flavor host port db user pass + flavor="${DB_FLAVOR:-$(db_config_field flavor)}" + host="${RANGER_ADMIN_DB_HOSTNAME:-$(db_config_field host)}" + port="${RANGER_ADMIN_DB_PORT:-$(db_config_field port)}" + db="${RANGER_ADMIN_DB_DATABASE:-$(db_config_field database)}" + user="${RANGER_ADMIN_DB_USERNAME:-$(db_config_field user)}" + pass="${RANGER_ADMIN_DB_PASSWORD:-}" + + if [ -z "${host}" ] || [ -z "${port}" ]; then + echo "WARNING: DB host/port not configured in ranger-admin-site.xml; skipping DB wait" >&2 + return 0 + fi + if [ -z "${flavor}" ]; then + flavor="POSTGRES" + fi + + echo "Waiting for DB connectivity (flavor=${flavor} host=${host} port=${port} db=${db:-}) with timeout ${timeout_s}s" >&2 + + local start now elapsed + start="$(date +%s)" + + while true; do + if check_db_ready "${flavor}" "${host}" "${port}" "${db:-postgres}" "${user:-postgres}" "${pass:-}"; then + echo "DB is reachable" >&2 + return 0 + fi + + now="$(date +%s)" + elapsed=$(( now - start )) + if [ "${elapsed}" -ge "${timeout_s}" ]; then + echo "ERROR: Timed out after ${timeout_s}s waiting for DB connectivity (flavor=${flavor} host=${host} port=${port} db=${db:-})" >&2 + return 1 + fi + + echo "Waiting for DB connectivity... elapsed=${elapsed}s remaining=$(( timeout_s - elapsed ))s" >&2 + sleep 5 + done +} + +cd "${RANGER_ADMIN_DIR}" +sync_admin_configs +sync_db_password_property +ensure_jdbc_driver +prepare_admin_runtime + +wait_for_db_ready_or_timeout 600 +python3 "/home/ranger/scripts/dba.py" +./ews/ranger-admin-services.sh start + +if [ ! -f "${SERVICES_MARKER}" ]; then + if wait_for_admin 240; then + if python3 "${USER_PASSWORD_BOOTSTRAP_HELPER}"; then + if python3 "/home/ranger/scripts/create_services.py"; then + touch "${SERVICES_MARKER}" 2>/dev/null || true + else + echo "Warning: service creation failed" >&2 + fi + else + echo "Warning: admin bootstrap failed; skipping service creation" >&2 + fi + else + echo "ERROR: Ranger Admin did not become ready in time; skipping service creation" >&2 + fi fi + +pid="$(admin_pid || true)" +if [ -n "${pid}" ]; then + tail --pid="${pid}" -f /dev/null +fi + +echo "Ranger Admin process id not found; keeping container alive for debugging" >&2 +tail -f /dev/null diff --git a/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py b/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py new file mode 100644 index 0000000000..c68bf50d14 --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/ranger_admin_xml_config.py @@ -0,0 +1,209 @@ +import argparse +import os +import re +import sys +import xml.etree.ElementTree as ET + +DEFAULT_RANGER_ADMIN_SITE_CANDIDATES = ( + os.environ.get("RANGER_ADMIN_SITE_XML"), + os.path.join( + os.environ.get( + "RANGER_ADMIN_CONF", + "/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf", + ), + "ranger-admin-site.xml", + ), + "/opt/ranger/admin/configs/ranger-admin-site.xml", + "/opt/ranger/admin/ews/webapp/WEB-INF/classes/conf/ranger-admin-site.xml", +) + + +class RangerAdminXmlConfig: + def __init__(self, property_file=None): + self.property_file = property_file + + def find_ranger_admin_site_xml(self): + candidates = [] + if self.property_file: + candidates.append(self.property_file) + for candidate in DEFAULT_RANGER_ADMIN_SITE_CANDIDATES: + if candidate: + candidates.append(candidate) + + for candidate in candidates: + if os.path.isfile(candidate): + return candidate + return "" + + def load_properties(self): + property_file = self.find_ranger_admin_site_xml() + properties = {} + if not property_file: + return properties + + try: + tree = ET.parse(property_file) + root = tree.getroot() + for child in root.findall("property"): + name_elem = child.find("name") + value_elem = child.find("value") + if name_elem is None or value_elem is None or not name_elem.text: + continue + properties[name_elem.text.strip()] = (value_elem.text or "").strip() + except (ET.ParseError, AttributeError, OSError): + pass + return properties + + def get_property(self, property_name): + return self.load_properties().get(property_name, "") + + def get_config_value(self, env_var, xml_property, default=""): + return os.environ.get(env_var) or self.get_property(xml_property) or default + + @staticmethod + def parse_jdbc_url(jdbc_url): + info = {"flavor": "", "host": "", "port": "", "database": ""} + if not jdbc_url: + return info + + match = re.match(r"jdbc:(postgresql|mysql)://([^/:;]+)(?::(\d+))?/([^?;]+)", jdbc_url) + if match: + flavor = match.group(1).upper() + info["flavor"] = "POSTGRES" if flavor == "POSTGRESQL" else flavor + info["host"] = match.group(2) + info["port"] = match.group(3) or ("5432" if flavor == "POSTGRESQL" else "3306") + info["database"] = match.group(4) + return info + + match = re.match(r"jdbc:sqlserver://([^:;]+)(?::(\d+))?(?:;|$)", jdbc_url) + if match: + info["flavor"] = "MSSQL" + info["host"] = match.group(1) + info["port"] = match.group(2) or "1433" + db_match = re.search(r"(?:^|;)databaseName=([^;]+)", jdbc_url) + if db_match: + info["database"] = db_match.group(1) + return info + + match = re.match(r"jdbc:oracle:thin:@//([^/:]+)(?::(\d+))?/([^?;]+)", jdbc_url) + if match: + info["flavor"] = "ORACLE" + info["host"] = match.group(1) + info["port"] = match.group(2) or "1521" + info["database"] = match.group(3) + return info + + match = re.match(r"jdbc:oracle:thin:@([^:]+):(\d+):([^?;]+)", jdbc_url) + if match: + info["flavor"] = "ORACLE" + info["host"] = match.group(1) + info["port"] = match.group(2) + info["database"] = match.group(3) + return info + + def get_db_field(self, field): + props = self.load_properties() + jdbc_info = self.parse_jdbc_url(props.get("ranger.jpa.jdbc.url", "")) + values = { + "flavor": jdbc_info.get("flavor", ""), + "host": jdbc_info.get("host", ""), + "port": jdbc_info.get("port", ""), + "database": jdbc_info.get("database", ""), + "user": props.get("ranger.jpa.jdbc.user", ""), + "password": os.environ.get("RANGER_ADMIN_DB_PASSWORD", ""), + } + return values.get(field, "") + + def set_property(self, name, value, required, create=False): + property_file = self.find_ranger_admin_site_xml() + try: + tree = ET.parse(property_file) + root = tree.getroot() + except (ET.ParseError, OSError) as exc: + raise SystemExit(f"ERROR: failed to parse {property_file}: {exc}") + + updated = False + for prop in root.findall("property"): + name_elem = prop.find("name") + value_elem = prop.find("value") + if name_elem is None or value_elem is None: + continue + if (name_elem.text or "").strip() != name: + continue + value_elem.text = value + updated = True + break + + if create and not updated: + prop = ET.SubElement(root, "property") + name_elem = ET.SubElement(prop, "name") + name_elem.text = name + value_elem = ET.SubElement(prop, "value") + value_elem.text = value + updated = True + + if required and not updated: + raise SystemExit(f"ERROR: {name} missing in {property_file}") + + if updated: + tree.write(property_file, encoding="unicode") + + +def load_ranger_admin_site_properties(property_file=None): + return RangerAdminXmlConfig(property_file).load_properties() + + +def parse_jdbc_url(jdbc_url): + return RangerAdminXmlConfig.parse_jdbc_url(jdbc_url) + + +def get_ranger_client(): + from apache_ranger.client.ranger_client import RangerClient + + admin_pass = os.environ.get("RANGER_ADMIN_PASSWORD", "") + return RangerClient("http://localhost:6080", ("admin", admin_pass)) + + +def build_cli_parser(): + parser = argparse.ArgumentParser(description="Read and update Ranger admin XML properties") + subparsers = parser.add_subparsers(dest="command", required=True) + + get_prop_parser = subparsers.add_parser("get-property") + get_prop_parser.add_argument("--file") + get_prop_parser.add_argument("--name", required=True) + + get_db_field_parser = subparsers.add_parser("get-db-field") + get_db_field_parser.add_argument("--file") + get_db_field_parser.add_argument("--field", required=True) + + set_prop_parser = subparsers.add_parser("set-property") + set_prop_parser.add_argument("--file") + set_prop_parser.add_argument("--name", required=True) + set_prop_parser.add_argument("--value", required=True) + set_prop_parser.add_argument("--required", action="store_true") + set_prop_parser.add_argument("--create", action="store_true") + + return parser + + +def main(argv=None): + args = build_cli_parser().parse_args(argv) + config = RangerAdminXmlConfig(args.file) + + if args.command == "get-property": + print(config.get_property(args.name)) + return 0 + + if args.command == "get-db-field": + print(config.get_db_field(args.field)) + return 0 + + if args.command == "set-property": + config.set_property(args.name, args.value, args.required, args.create) + return 0 + + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py b/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py new file mode 100644 index 0000000000..c14fe07aa4 --- /dev/null +++ b/dev-support/ranger-docker/scripts/admin/user_password_bootstrap.py @@ -0,0 +1,123 @@ +#!/usr/bin/env python3 + +import os +import sys + +from apache_ranger.client.ranger_client import RangerClient +from apache_ranger.client.ranger_user_mgmt_client import RangerUserMgmtClient +from apache_ranger.exceptions import RangerServiceException + +from log_config import configure_logging, get_logger + +logger = get_logger(__name__) + +DEFAULT_BASE_URL = os.environ.get("RANGER_ADMIN_BASE_URL", "http://127.0.0.1:6080").rstrip("/") + + +class UserPasswordBootstrap: + def __init__(self, base_url: str): + self.base_url = base_url.rstrip("/") + + def _get_user_mgmt_client(self, username: str, password: str) -> RangerUserMgmtClient: + return RangerUserMgmtClient(RangerClient(self.base_url, (username, password))) + + def auth_probe_status(self, username: str, password: str) -> int: + try: + user_mgmt = self._get_user_mgmt_client(username, password) + result = user_mgmt.client_http.call_api(RangerUserMgmtClient.FIND_USERS) + return 200 if result is not None else 0 + except RangerServiceException as exc: + if exc.statusCode == 403: + return 403 + if exc.statusCode == 401: + return 401 + logger.debug("apache-ranger auth probe failed for %s: %s", username, exc, exc_info=True) + return exc.statusCode + except Exception as exc: + logger.debug("apache-ranger auth probe failed for %s: %s", username, exc, exc_info=True) + return 0 + + def can_authenticate(self, username: str, password: str) -> bool: + return self.auth_probe_status(username, password) in (200, 403) + + def _update_user_password_with_client(self, admin_password: str, username: str, desired: str) -> None: + user_mgmt = self._get_user_mgmt_client("admin", admin_password) + user = user_mgmt.get_user(username) + if user is None or user.id is None: + raise ValueError(f"Unable to find Ranger user {username}") + + user.password = desired + user_mgmt.update_user_by_id(user.id, user) + + def update_user_password(self, admin_password: str, username: str, desired: str) -> int: + try: + self._update_user_password_with_client(admin_password, username, desired) + return 0 + except RangerServiceException as exc: + logger.error("apache-ranger client update failed for %s: status=%s, message=%s", username, exc.statusCode, exc.msgDesc or exc) + return 1 + except Exception as exc: + logger.error("apache-ranger client update failed for %s: %s", username, exc) + return 1 + + def set_admin_password_if_needed(self, desired: str) -> int: + if not desired: + logger.warning("Ranger admin password not configured; skipping admin password update") + return 0 + + if self.can_authenticate("admin", desired): + return 0 + + logger.warning("Unable to authenticate to Ranger as admin with RANGER_ADMIN_PASSWORD.") + logger.warning(" admin: -> %s", self.auth_probe_status("admin", desired)) + logger.warning("For fresh installs, dba.py seeds the initial admin password from RANGER_ADMIN_PASSWORD during schema import. " + "If the database already exists, changing only RANGER_ADMIN_PASSWORD will not rotate the stored admin password." + ) + return 1 + + def update_user_password_if_needed(self, admin_password: str, username: str, desired: str) -> int: + if not desired: + logger.warning("No password configured for %s; skipping password update", username) + return 0 + + if self.can_authenticate(username, desired): + return 0 + + if not self.can_authenticate("admin", admin_password): + logger.warning("Unable to authenticate as admin; skipping password update for %s.", username) + logger.warning(" admin: -> %s", self.auth_probe_status("admin", admin_password)) + return 0 + + logger.info("Updating Ranger user password for %s to configured value", username) + if self.update_user_password(admin_password, username, desired) != 0: + return 1 + + if not self.can_authenticate(username, desired): + logger.error("Password update succeeded but auth check failed for %s", username) + return 1 + + return 0 + + def run(self, admin_password: str, usersync_password: str, tagsync_password: str) -> int: + if self.set_admin_password_if_needed(admin_password) != 0: + return 1 + if self.update_user_password_if_needed(admin_password, "rangerusersync", usersync_password) != 0: + return 1 + if self.update_user_password_if_needed(admin_password, "rangertagsync", tagsync_password) != 0: + return 1 + return 0 + + +def main() -> int: + configure_logging() + + bootstrap = UserPasswordBootstrap(DEFAULT_BASE_URL) + return bootstrap.run( + admin_password=os.environ.get("RANGER_ADMIN_PASSWORD", ""), + usersync_password=os.environ.get("RANGER_USERSYNC_PASSWORD", ""), + tagsync_password=os.environ.get("RANGER_TAGSYNC_PASSWORD", ""), + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/dev-support/ranger-docker/scripts/python/log_config.py b/dev-support/ranger-docker/scripts/python/log_config.py new file mode 100644 index 0000000000..9441b5f31d --- /dev/null +++ b/dev-support/ranger-docker/scripts/python/log_config.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python3 + +import logging +import os + + +DEFAULT_LOG_LEVEL = os.environ.get("RANGER_ADMIN_PY_LOG_LEVEL", "DEBUG").upper() +DEFAULT_LOG_FORMAT = os.environ.get("RANGER_ADMIN_PY_LOG_FORMAT", "%(asctime)-15s %(levelname)s %(message)s") +DEFAULT_LOGGER_LEVELS = { + "apache_ranger": os.environ.get("RANGER_ADMIN_PY_APACHE_RANGER_LOG_LEVEL", "INFO"), +} + +_LOGGING_CONFIGURED = False + + +def _parse_level(level_name): + if isinstance(level_name, int): + return level_name + return getattr(logging, str(level_name).upper(), logging.INFO) + + +def configure_logging(default_level=DEFAULT_LOG_LEVEL, logger_levels=None): + global _LOGGING_CONFIGURED + + if _LOGGING_CONFIGURED: + return + + logging.basicConfig(format=DEFAULT_LOG_FORMAT, level=_parse_level(default_level)) + + levels = dict(DEFAULT_LOGGER_LEVELS) + if logger_levels: + levels.update(logger_levels) + + for logger_name, logger_level in levels.items(): + logging.getLogger(logger_name).setLevel(_parse_level(logger_level)) + + _LOGGING_CONFIGURED = True + + +def get_logger(name): + return logging.getLogger(name)