From a2d2e71251cb05fb0ebc03b67793858419ff531d Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Fri, 24 Feb 2023 19:10:58 +0300 Subject: [PATCH 01/20] ragent & ras in container Docker multistage build Now `docker compose` starting ragent, ras and postgres services Dockerfile improved: ragent, ras, rac are separated, minimized 1cv8 components bloat Working remote debug configuration Use entrypoints instead of cmd It allows to properly set PATH for 1cv7 platform binaries Use PYTHONUNBUFFERED env var Change rac launch in docker compose according to entrypoint presence Add ragent persistence Drop unused port forwarding Move 1cv8 installer JRE not required for 1cv8 to run RAGENT_HOME is env variable now Modify `ragent` container volume mount method to make it operate correctly Add echo output to ragent entrypoint Use postgres from postgres.pro Add `*.deb` to `.gitignore` 1c server in container actualized Drop empty docker compose --- .dockerignore | 1 + Dockerfile | 47 ++++ Dockerfile-postgres | 36 +++ docker-compose.debug.yml | 55 ++++ docker/posgres-entrypoint.sh | 34 +++ docker/posgres-runtime/env-defaults | 21 ++ docker/posgres-runtime/functions | 391 ++++++++++++++++++++++++++++ docker/rac-entrypoint.sh | 10 + docker/ragent-entrypoint.sh | 42 +++ docker/ras-entrypoint.sh | 12 + rac.py | 1 + 11 files changed, 650 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 Dockerfile-postgres create mode 100644 docker-compose.debug.yml create mode 100644 docker/posgres-entrypoint.sh create mode 100644 docker/posgres-runtime/env-defaults create mode 100644 docker/posgres-runtime/functions create mode 100644 docker/rac-entrypoint.sh create mode 100644 docker/ragent-entrypoint.sh create mode 100644 docker/ras-entrypoint.sh create mode 100644 rac.py diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8d98f9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +.* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d4c51c9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,47 @@ +FROM debian:12 AS v8-base +ENV RAGENT_HOST ragent +ENV RAGENT_PORT 1540 +ENV RAGENT_REGPORT 1541 +ENV RAGENT_PORTRANGE 1560:1591 +ENV RAS_PORT 1545 +WORKDIR /distr +COPY /docker/1c-enterprise-*-common_*_amd64.deb common_amd64.deb +COPY /docker/1c-enterprise-*-server_*_amd64.deb server_amd64.deb +RUN dpkg -i common_amd64.deb && dpkg -i server_amd64.deb && rm *.deb +WORKDIR /opt/1cv8 + +FROM v8-base AS ragent +ENV RAGENT_HOME /home/1c/ragent +RUN mkdir -p ${RAGENT_HOME}/reg_${RAGENT_REGPORT} +COPY docker/ragent-entrypoint.sh /opt/docker/entrypoint.sh +RUN chmod +x /opt/docker/entrypoint.sh +ENTRYPOINT ["/opt/docker/entrypoint.sh"] + +FROM v8-base AS ras +COPY docker/ras-entrypoint.sh /opt/docker/entrypoint.sh +RUN chmod +x /opt/docker/entrypoint.sh +ENTRYPOINT ["/opt/docker/entrypoint.sh"] + +FROM python:3.12 AS python-base +ENV PYTHONUNBUFFERED 1 +ENV POETRY_VERSION=1.8.4 +ENV POETRY_HOME=/opt/poetry +ENV POETRY_VENV=/opt/poetry-venv +ENV POETRY_CACHE_DIR=/opt/.cache + +FROM python-base AS poetry-base +RUN python3 -m venv $POETRY_VENV \ + && $POETRY_VENV/bin/pip install -U pip setuptools \ + && $POETRY_VENV/bin/pip install poetry==${POETRY_VERSION} + +FROM python-base AS rac +COPY --from=ras /opt/1cv8 /opt/1cv8 +COPY --from=poetry-base ${POETRY_VENV} ${POETRY_VENV} +ENV PATH="${PATH}:${POETRY_VENV}/bin" +WORKDIR /app +COPY pyproject.toml poetry.lock ./ +RUN poetry check \ + && poetry install --no-interaction --no-cache --no-root --without dev --with debug +COPY docker/rac-entrypoint.sh /opt/docker/entrypoint.sh +RUN chmod +x /opt/docker/entrypoint.sh +ENTRYPOINT ["/opt/docker/entrypoint.sh"] diff --git a/Dockerfile-postgres b/Dockerfile-postgres new file mode 100644 index 0000000..29f112f --- /dev/null +++ b/Dockerfile-postgres @@ -0,0 +1,36 @@ +FROM debian:12 + +ENV DEBIAN_FRONTEND=noninteractive + +ENV PG_APP_HOME="/etc/docker-postgresql"\ + PG_VERSION=15 \ + PG_USER=postgres \ + PG_HOME=/var/lib/postgresql \ + PG_RUNDIR=/run/postgresql \ + PG_LOGDIR=/var/log/postgresql \ + PG_CERTDIR=/etc/postgresql/certs + +ENV PG_BINDIR=/opt/pgpro/1c-${PG_VERSION}/bin \ + PG_DATADIR=${PG_HOME}/${PG_VERSION}/main + +RUN apt-get update \ + && apt-get install -y sudo locales wget gnupg2 \ + && localedef -i ru_RU -c -f UTF-8 -A /usr/share/locale/locale.alias ru_RU.UTF-8 \ + && update-locale LANG=ru_RU.UTF-8 + +ENV LANG ru_RU.UTF-8 + +RUN wget --quiet -O - http://repo.postgrespro.ru/keys/GPG-KEY-POSTGRESPRO | apt-key add - \ + && echo 'deb http://repo.postgrespro.ru/1c/1c-'${PG_VERSION}'/debian bookworm main' > /etc/apt/sources.list.d/postgrespro-1c.list \ + && apt-get update \ + && apt-get install -y postgrespro-1c-${PG_VERSION} \ + && rm -rf /var/lib/apt/lists/* + +COPY docker/posgres-runtime/ ${PG_APP_HOME}/ +COPY docker/posgres-entrypoint.sh /opt/docker/entrypoint.sh + +EXPOSE 5432 +VOLUME ${PG_DATADIR} +WORKDIR ${PG_HOME} +RUN chmod +x /opt/docker/entrypoint.sh +ENTRYPOINT ["/opt/docker/entrypoint.sh"] diff --git a/docker-compose.debug.yml b/docker-compose.debug.yml new file mode 100644 index 0000000..9ec629a --- /dev/null +++ b/docker-compose.debug.yml @@ -0,0 +1,55 @@ +version: '3.4' +services: + ragent: + build: + context: . + target: ragent + container_name: ragent + depends_on: + - db + environment: + - RAGENT_PORT=1540 + - RAGENT_REGPORT=1541 + - RAGENT_VOLUME=/home/1c/volume + hostname: ragent + volumes: + - .ragent-data:/home/1c/volume + ras: + build: + context: . + target: ras + container_name: ras + depends_on: + - ragent + environment: + - RAGENT_HOST=ragent + - RAGENT_PORT=1540 + - RAS_PORT=1545 + db: + build: + context: . + dockerfile: Dockerfile-postgres + container_name: db + environment: + - PG_DATADIR=/var/lib/postgresql/data + - PG_USER=postgres + - PG_PASSWORD=supersecretpassword + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + volumes: + - .postgres-data:/var/lib/postgresql/data + rac: + build: + context: . + target: rac + command: ["rac.py"] + container_name: rac + environment: + - POETRY_VENV=/opt/poetry-venv + ports: + - 5678:5678 + volumes: + - .:/app diff --git a/docker/posgres-entrypoint.sh b/docker/posgres-entrypoint.sh new file mode 100644 index 0000000..bd51f61 --- /dev/null +++ b/docker/posgres-entrypoint.sh @@ -0,0 +1,34 @@ +#!/bin/bash +set -e +source ${PG_APP_HOME}/functions + +[[ ${DEBUG} == true ]] && set -x + +# allow arguments to be passed to postgres +if [[ ${1:0:1} = '-' ]]; then + EXTRA_ARGS="$@" + set -- +elif [[ ${1} == postgres || ${1} == $(which postgres) ]]; then + EXTRA_ARGS="${@:2}" + set -- +fi + +# default behaviour is to launch postgres +if [[ -z ${1} ]]; then + map_uidgid + + create_datadir + create_certdir + create_logdir + create_rundir + + set_resolvconf_perms + + configure_postgresql + + echo "Starting PostgreSQL ${PG_VERSION}..." + exec start-stop-daemon --start --chuid ${PG_USER}:${PG_USER} \ + --exec ${PG_BINDIR}/postgres -- -D ${PG_DATADIR} ${EXTRA_ARGS} +else + exec "$@" +fi diff --git a/docker/posgres-runtime/env-defaults b/docker/posgres-runtime/env-defaults new file mode 100644 index 0000000..4cff412 --- /dev/null +++ b/docker/posgres-runtime/env-defaults @@ -0,0 +1,21 @@ +#!/bin/bash + +PG_SSL=${PG_SSL:-} + +PG_TRUST_LOCALNET=${PG_TRUST_LOCALNET:-$PSQL_TRUST_LOCALNET} # backward compatibility +PG_TRUST_LOCALNET=${PG_TRUST_LOCALNET:-false} + +REPLICATION_MODE=${REPLICATION_MODE:-$PSQL_MODE} # backward compatibility +REPLICATION_MODE=${REPLICATION_MODE:-} +REPLICATION_USER=${REPLICATION_USER:-} +REPLICATION_PASS=${REPLICATION_PASS:-} +REPLICATION_HOST=${REPLICATION_HOST:-} +REPLICATION_PORT=${REPLICATION_PORT:-5432} +REPLICATION_SSLMODE=${REPLICATION_SSLMODE:-prefer} + +DB_NAME=${DB_NAME:-} +DB_USER=${DB_USER:-} +DB_PASS=${DB_PASS:-} +DB_TEMPLATE=${DB_TEMPLATE:-template1} + +DB_EXTENSION=${DB_EXTENSION:-} diff --git a/docker/posgres-runtime/functions b/docker/posgres-runtime/functions new file mode 100644 index 0000000..440c206 --- /dev/null +++ b/docker/posgres-runtime/functions @@ -0,0 +1,391 @@ +#!/bin/bash +set -e +source ${PG_APP_HOME}/env-defaults + +PG_CONF=${PG_DATADIR}/postgresql.conf +PG_HBA_CONF=${PG_DATADIR}/pg_hba.conf +PG_IDENT_CONF=${PG_DATADIR}/pg_ident.conf +PG_RECOVERY_CONF=${PG_DATADIR}/recovery.conf + +## Execute command as PG_USER +exec_as_postgres() { + sudo -HEu ${PG_USER} "$@" +} + +map_uidgid() { + USERMAP_ORIG_UID=$(id -u ${PG_USER}) + USERMAP_ORIG_GID=$(id -g ${PG_USER}) + USERMAP_GID=${USERMAP_GID:-${USERMAP_UID:-$USERMAP_ORIG_GID}} + USERMAP_UID=${USERMAP_UID:-$USERMAP_ORIG_UID} + if [[ ${USERMAP_UID} != ${USERMAP_ORIG_UID} ]] || [[ ${USERMAP_GID} != ${USERMAP_ORIG_GID} ]]; then + echo "Adapting uid and gid for ${PG_USER}:${PG_USER} to $USERMAP_UID:$USERMAP_GID" + groupmod -o -g ${USERMAP_GID} ${PG_USER} + sed -i -e "s|:${USERMAP_ORIG_UID}:${USERMAP_GID}:|:${USERMAP_UID}:${USERMAP_GID}:|" /etc/passwd + fi +} + +create_datadir() { + echo "Initializing datadir..." + mkdir -p ${PG_HOME} + if [[ -d ${PG_DATADIR} ]]; then + find ${PG_DATADIR} -type f -exec chmod 0600 {} \; + find ${PG_DATADIR} -type d -exec chmod 0700 {} \; + fi + chown -R ${PG_USER}:${PG_USER} ${PG_HOME} +} + +create_certdir() { + echo "Initializing certdir..." + mkdir -p ${PG_CERTDIR} + [[ -f ${PG_CERTDIR}/server.crt ]] && chmod 0644 ${PG_CERTDIR}/server.crt + [[ -f ${PG_CERTDIR}/server.key ]] && chmod 0640 ${PG_CERTDIR}/server.key + chmod 0755 ${PG_CERTDIR} + chown -R root:${PG_USER} ${PG_CERTDIR} +} + +create_logdir() { + echo "Initializing logdir..." + mkdir -p ${PG_LOGDIR} + chmod -R 1775 ${PG_LOGDIR} + chown -R root:${PG_USER} ${PG_LOGDIR} +} + +create_rundir() { + echo "Initializing rundir..." + mkdir -p ${PG_RUNDIR} ${PG_RUNDIR}/${PG_VERSION}-main.pg_stat_tmp + chmod -R 0755 ${PG_RUNDIR} + chmod g+s ${PG_RUNDIR} + chown -R ${PG_USER}:${PG_USER} ${PG_RUNDIR} +} + +set_postgresql_param() { + local key=${1} + local value=${2} + local verbosity=${3:-verbose} + + if [[ -n ${value} ]]; then + local current=$(exec_as_postgres sed -n -e "s/^\(${key} = '\)\([^ ']*\)\(.*\)$/\2/p" ${PG_CONF}) + if [[ "${current}" != "${value}" ]]; then + if [[ ${verbosity} == verbose ]]; then + echo "‣ Setting postgresql.conf parameter: ${key} = '${value}'" + fi + value="$(echo "${value}" | sed 's|[&]|\\&|g')" + exec_as_postgres sed -i "s|^[#]*[ ]*${key} = .*|${key} = '${value}'|" ${PG_CONF} + fi + fi +} + +set_recovery_param() { + local key=${1} + local value=${2} + local hide=${3} + if [[ -n ${value} ]]; then + local current=$(exec_as_postgres sed -n -e "s/^\(.*\)\(${key}=\)\([^ ']*\)\(.*\)$/\3/p" ${PG_RECOVERY_CONF}) + if [[ "${current}" != "${value}" ]]; then + case ${hide} in + true) echo "‣ Setting primary_conninfo parameter: ${key}" ;; + *) echo "‣ Setting primary_conninfo parameter: ${key} = '${value}'" ;; + esac + exec_as_postgres sed -i "s|${key}=[^ ']*|${key}=${value}|" ${PG_RECOVERY_CONF} + fi + fi +} + +set_hba_param() { + local value=${1} + if ! grep -q "$(sed "s| | \\\+|g" <<< ${value})" ${PG_HBA_CONF}; then + echo "${value}" >> ${PG_HBA_CONF} + fi +} + +configure_ssl() { + ## NOT SURE IF THIS IS A GOOD ALTERNATIVE TO ENABLE SSL SUPPORT BY DEFAULT ## + ## BECAUSE USERS WHO PULL A PREBUILT IMAGE WILL HAVE THE SAME CERTIFICATES ## + # if [[ ! -f ${PG_CERTDIR}/server.crt && ! -f ${PG_CERTDIR}/server.key ]]; then + # if [[ -f /etc/ssl/certs/ssl-cert-snakeoil.pem && -f /etc/ssl/private/ssl-cert-snakeoil.key ]]; then + # ln -sf /etc/ssl/certs/ssl-cert-snakeoil.pem ${PG_CERTDIR}/server.crt + # ln -sf /etc/ssl/private/ssl-cert-snakeoil.key ${PG_CERTDIR}/server.key + # fi + # fi + + if [[ -f ${PG_CERTDIR}/server.crt && -f ${PG_CERTDIR}/server.key ]]; then + PG_SSL=${PG_SSL:-on} + set_postgresql_param "ssl_cert_file" "${PG_CERTDIR}/server.crt" + set_postgresql_param "ssl_key_file" "${PG_CERTDIR}/server.key" + fi + PG_SSL=${PG_SSL:-off} + set_postgresql_param "ssl" "${PG_SSL}" +} + +configure_hot_standby() { + case ${REPLICATION_MODE} in + slave|snapshot|backup) ;; + *) + echo "Configuring hot standby..." + set_postgresql_param "wal_level" "hot_standby" + set_postgresql_param "max_wal_senders" "16" + set_postgresql_param "checkpoint_segments" "8" + set_postgresql_param "wal_keep_segments" "32" + set_postgresql_param "hot_standby" "on" + ;; + esac +} + +initialize_database() { + if [[ ! -f ${PG_DATADIR}/PG_VERSION ]]; then + case ${REPLICATION_MODE} in + slave|snapshot|backup) + if [[ -z $REPLICATION_HOST ]]; then + echo "ERROR! Cannot continue without the REPLICATION_HOST. Exiting..." + exit 1 + fi + + if [[ -z $REPLICATION_USER ]]; then + echo "ERROR! Cannot continue without the REPLICATION_USER. Exiting..." + exit 1 + fi + + if [[ -z $REPLICATION_PASS ]]; then + echo "ERROR! Cannot continue without the REPLICATION_PASS. Exiting..." + exit 1 + fi + + echo -n "Waiting for $REPLICATION_HOST to accept connections (60s timeout)" + timeout=60 + while ! ${PG_BINDIR}/pg_isready -h $REPLICATION_HOST -p $REPLICATION_PORT -t 1 >/dev/null 2>&1 + do + timeout=$(expr $timeout - 1) + if [[ $timeout -eq 0 ]]; then + echo "Timeout! Exiting..." + exit 1 + fi + echo -n "." + sleep 1 + done + echo + + case ${REPLICATION_MODE} in + slave) + echo "Replicating initial data from $REPLICATION_HOST..." + exec_as_postgres PGPASSWORD=$REPLICATION_PASS ${PG_BINDIR}/pg_basebackup -D ${PG_DATADIR} \ + -h ${REPLICATION_HOST} -p ${REPLICATION_PORT} -U ${REPLICATION_USER} -X stream -w >/dev/null + ;; + snapshot) + echo "Generating a snapshot data on $REPLICATION_HOST..." + exec_as_postgres PGPASSWORD=$REPLICATION_PASS ${PG_BINDIR}/pg_basebackup -D ${PG_DATADIR} \ + -h ${REPLICATION_HOST} -p ${REPLICATION_PORT} -U ${REPLICATION_USER} -X fetch -w >/dev/null + ;; + backup) + echo "Backing up data on $REPLICATION_HOST..." + exec_as_postgres PGPASSWORD=$REPLICATION_PASS ${PG_BINDIR}/pg_basebackup -D ${PG_DATADIR} \ + -h ${REPLICATION_HOST} -p ${REPLICATION_PORT} -U ${REPLICATION_USER} -X fetch -w >/dev/null + exit 0 + ;; + esac + ;; + *) + echo "Initializing database..." + PG_OLD_VERSION=$(find ${PG_HOME}/[0-9].[0-9]/main -maxdepth 1 -name PG_VERSION 2>/dev/null | grep -v $PG_VERSION | sort -r | head -n1 | cut -d'/' -f5) + if [[ -n ${PG_OLD_VERSION} ]]; then + echo "‣ Migrating PostgreSQL ${PG_OLD_VERSION} data to ${PG_VERSION}..." + + # protect the existing data from being altered by apt-get + mv ${PG_HOME}/${PG_OLD_VERSION} ${PG_HOME}/${PG_OLD_VERSION}.migrating + + echo "‣ Installing PostgreSQL ${PG_OLD_VERSION}..." + if ! ( apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y postgresql-${PG_OLD_VERSION} postgresql-client-${PG_OLD_VERSION} ) >/dev/null; then + echo "ERROR! Failed to install PostgreSQL ${PG_OLD_VERSION}. Exiting..." + # first move the old data back + rm -rf ${PG_HOME}/${PG_OLD_VERSION} + mv ${PG_HOME}/${PG_OLD_VERSION}.migrating ${PG_HOME}/${PG_OLD_VERSION} + exit 1 + fi + rm -rf /var/lib/apt/lists/* + + # we're ready to migrate, move back the old data and remove the trap + rm -rf ${PG_HOME}/${PG_OLD_VERSION} + mv ${PG_HOME}/${PG_OLD_VERSION}.migrating ${PG_HOME}/${PG_OLD_VERSION} + fi + + if [[ -n $PG_PASSWORD ]]; then + echo "${PG_PASSWORD}" > /tmp/pwfile + fi + + exec_as_postgres ${PG_BINDIR}/initdb --pgdata=${PG_DATADIR} \ + --username=${PG_USER} --encoding=unicode --auth=trust ${PG_PASSWORD:+--pwfile=/tmp/pwfile} >/dev/null + + if [[ -n ${PG_OLD_VERSION} ]]; then + PG_OLD_BINDIR=/usr/lib/postgresql/${PG_OLD_VERSION}/bin + PG_OLD_DATADIR=${PG_HOME}/${PG_OLD_VERSION}/main + PG_OLD_CONF=${PG_OLD_DATADIR}/postgresql.conf + PG_OLD_HBA_CONF=${PG_OLD_DATADIR}/pg_hba.conf + PG_OLD_IDENT_CONF=${PG_OLD_DATADIR}/pg_ident.conf + + echo -n "‣ Migration in progress. Please be patient..." + exec_as_postgres ${PG_BINDIR}/pg_upgrade \ + -b ${PG_OLD_BINDIR} -B ${PG_BINDIR} \ + -d ${PG_OLD_DATADIR} -D ${PG_DATADIR} \ + -o "-c config_file=${PG_OLD_CONF} --hba_file=${PG_OLD_HBA_CONF} --ident_file=${PG_OLD_IDENT_CONF}" \ + -O "-c config_file=${PG_CONF} --hba_file=${PG_HBA_CONF} --ident_file=${PG_IDENT_CONF}" >/dev/null + echo + fi + ;; + esac + + configure_hot_standby + + # Change DSM from `posix' to `sysv' if we are inside an lx-brand container + if [[ $(uname -v) == "BrandZ virtual linux" ]]; then + set_postgresql_param "dynamic_shared_memory_type" "sysv" + fi + fi + + # configure path to data_directory + set_postgresql_param "data_directory" "${PG_DATADIR}" + + # configure logging + set_postgresql_param "log_directory" "${PG_LOGDIR}" + set_postgresql_param "log_filename" "postgresql-${PG_VERSION}-main.log" + + # trust connections from local network + if [[ ${PG_TRUST_LOCALNET} == true ]]; then + echo "Trusting connections from the local network..." + set_hba_param "host all all samenet trust" + fi + + # allow remote connections to postgresql database + set_hba_param "host all all 0.0.0.0/0 md5" +} + +set_resolvconf_perms() { + echo "Setting resolv.conf ACLs..." + setfacl -m user:${PG_USER}:r /etc/resolv.conf || true +} + +configure_recovery() { + if [[ ${REPLICATION_MODE} == slave ]]; then + echo "Configuring recovery..." + if [[ ! -f ${PG_RECOVERY_CONF} ]]; then + # initialize recovery.conf on the firstrun (slave only) + exec_as_postgres touch ${PG_RECOVERY_CONF} + ( echo "standby_mode = 'on'"; + echo "primary_conninfo = 'host=${REPLICATION_HOST} port=${REPLICATION_PORT} user=${REPLICATION_USER} password=${REPLICATION_PASS} sslmode=${REPLICATION_SSLMODE}'"; + ) > ${PG_RECOVERY_CONF} + else + set_recovery_param "host" "${REPLICATION_HOST}" + set_recovery_param "port" "${REPLICATION_PORT}" + set_recovery_param "user" "${REPLICATION_USER}" + set_recovery_param "password" "${REPLICATION_PASS}" "true" + set_recovery_param "sslmode" "${REPLICATION_SSLMODE}" + fi + else + # recovery.conf can only exist on a slave node, its existence otherwise causes problems + rm -rf ${PG_RECOVERY_CONF} + fi +} + +create_user() { + if [[ -n ${DB_USER} ]]; then + case $REPLICATION_MODE in + slave|snapshot|backup) + echo "INFO! Database user cannot be created on a $REPLICATION_MODE node. Skipping..." + ;; + *) + if [[ -z ${DB_PASS} ]]; then + echo "ERROR! Please specify a password for DB_USER in DB_PASS. Exiting..." + exit 1 + fi + echo "Creating database user: ${DB_USER}" + if [[ -z $(psql -U ${PG_USER} -Atc "SELECT 1 FROM pg_catalog.pg_user WHERE usename = '${DB_USER}'";) ]]; then + psql -U ${PG_USER} -c "CREATE ROLE \"${DB_USER}\" with LOGIN CREATEDB PASSWORD '${DB_PASS}';" >/dev/null + fi + ;; + esac + fi +} + +load_extensions() { + local database=${1?missing argument} + + if [[ ${DB_UNACCENT} == true ]]; then + echo + echo "WARNING: " + echo " The DB_UNACCENT option will be deprecated in favour of DB_EXTENSION soon." + echo " Please migrate to using DB_EXTENSION" + echo + echo "‣ Loading unaccent extension..." + psql -U ${PG_USER} -d ${database} -c "CREATE EXTENSION IF NOT EXISTS unaccent;" >/dev/null 2>&1 + fi + + for extension in $(awk -F',' '{for (i = 1 ; i <= NF ; i++) print $i}' <<< "${DB_EXTENSION}"); do + echo "‣ Loading ${extension} extension..." + psql -U ${PG_USER} -d ${database} -c "CREATE EXTENSION IF NOT EXISTS ${extension};" >/dev/null 2>&1 + done +} + +create_database() { + if [[ -n ${DB_NAME} ]]; then + case $REPLICATION_MODE in + slave|snapshot|backup) + echo "INFO! Database cannot be created on a $REPLICATION_MODE node. Skipping..." + ;; + *) + for database in $(awk -F',' '{for (i = 1 ; i <= NF ; i++) print $i}' <<< "${DB_NAME}"); do + echo "Creating database: ${database}..." + if [[ -z $(psql -U ${PG_USER} -Atc "SELECT 1 FROM pg_catalog.pg_database WHERE datname = '${database}'";) ]]; then + psql -U ${PG_USER} -c "CREATE DATABASE \"${database}\" WITH TEMPLATE = \"${DB_TEMPLATE}\";" >/dev/null + fi + + load_extensions ${database} + + if [[ -n ${DB_USER} ]]; then + echo "‣ Granting access to ${DB_USER} user..." + psql -U ${PG_USER} -c "GRANT ALL PRIVILEGES ON DATABASE \"${database}\" to \"${DB_USER}\";" >/dev/null + fi + done + ;; + esac + fi +} + +create_replication_user() { + if [[ -n ${REPLICATION_USER} ]]; then + case $REPLICATION_MODE in + slave|snapshot|backup) ;; # replication user can only be created on the master + *) + if [[ -z ${REPLICATION_PASS} ]]; then + echo "ERROR! Please specify a password for REPLICATION_USER in REPLICATION_PASS. Exiting..." + exit 1 + fi + + echo "Creating replication user: ${REPLICATION_USER}" + if [[ -z $(psql -U ${PG_USER} -Atc "SELECT 1 FROM pg_catalog.pg_user WHERE usename = '${REPLICATION_USER}'";) ]]; then + psql -U ${PG_USER} -c "CREATE ROLE \"${REPLICATION_USER}\" WITH REPLICATION LOGIN ENCRYPTED PASSWORD '${REPLICATION_PASS}';" >/dev/null + fi + + set_hba_param "host replication ${REPLICATION_USER} 0.0.0.0/0 md5" + ;; + esac + fi +} + +configure_postgresql() { + initialize_database + configure_recovery + configure_ssl + + # start postgres server internally for the creation of users and databases + rm -rf ${PG_DATADIR}/postmaster.pid + set_postgresql_param "listen_addresses" "127.0.0.1" quiet + exec_as_postgres ${PG_BINDIR}/pg_ctl -D ${PG_DATADIR} -w start >/dev/null + + create_user + create_database + create_replication_user + + # stop the postgres server + exec_as_postgres ${PG_BINDIR}/pg_ctl -D ${PG_DATADIR} -w stop >/dev/null + + # listen on all interfaces + set_postgresql_param "listen_addresses" "*" quiet +} diff --git a/docker/rac-entrypoint.sh b/docker/rac-entrypoint.sh new file mode 100644 index 0000000..ba1bc3b --- /dev/null +++ b/docker/rac-entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +set -e + +export PATH="$PATH:/opt/1cv8/x86_64/$(ls /opt/1cv8/x86_64)" + +echo "IP: `awk 'END{print $1}' /etc/hosts`" +echo "PATH: $PATH" +echo "CMD: $@" + +poetry run python -m debugpy --wait-for-client --listen 0.0.0.0:5678 $@ diff --git a/docker/ragent-entrypoint.sh b/docker/ragent-entrypoint.sh new file mode 100644 index 0000000..7176cba --- /dev/null +++ b/docker/ragent-entrypoint.sh @@ -0,0 +1,42 @@ +#!/bin/bash +export PATH="$PATH:/opt/1cv8/x86_64/$(ls /opt/1cv8/x86_64)" +export RAGENT_WORKDIR="$RAGENT_HOME/reg_$RAGENT_REGPORT" + +echo "IP: `awk 'END{print $1}' /etc/hosts`" +echo "PATH: $PATH" +echo "RAGENT_HOME: $RAGENT_HOME" +echo "RAGENT_PORT: $RAGENT_PORT" +echo "RAGENT_REGPORT: $RAGENT_REGPORT" +echo "RAGENT_PORTRANGE: $RAGENT_PORTRANGE" + +sync() { + if [[ -n "${RAGENT_VOLUME}" ]]; then + echo "Syncing configuration from $RAGENT_VOLUME to $RAGENT_HOME" + cp -f "$RAGENT_VOLUME/1cv8wsrv.lst" "$RAGENT_HOME/1cv8wsrv.lst" + cp -f "$RAGENT_VOLUME/rescntsrv.lst" "$RAGENT_WORKDIR/rescntsrv.lst" + cp -f "$RAGENT_VOLUME/1CV8Clst.lst" "$RAGENT_WORKDIR/1CV8Clst.lst" + echo "Sync completed" + else + echo "RAGENT_VOLUME variable is not set, configuration will not be persisted" + fi +} + +sync_reverse() { + if [[ -n "${RAGENT_VOLUME}" ]]; then + echo "Syncing configuration from $RAGENT_HOME to $RAGENT_VOLUME" + cp -f "$RAGENT_HOME/1cv8wsrv.lst" "$RAGENT_VOLUME/1cv8wsrv.lst" + cp -f "$RAGENT_WORKDIR/rescntsrv.lst" "$RAGENT_VOLUME/rescntsrv.lst" + cp -f "$RAGENT_WORKDIR/1CV8Clst.lst" "$RAGENT_VOLUME/1CV8Clst.lst" + echo "Sync completed" + fi +} + +sync + +trap 'sync_reverse' SIGTERM + +echo "Starting ragent" + +ragent -port $RAGENT_PORT -regport $RAGENT_REGPORT -range $RAGENT_PORTRANGE -d $RAGENT_HOME & + +wait $! diff --git a/docker/ras-entrypoint.sh b/docker/ras-entrypoint.sh new file mode 100644 index 0000000..58e1514 --- /dev/null +++ b/docker/ras-entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +export PATH="$PATH:/opt/1cv8/x86_64/$(ls /opt/1cv8/x86_64)" + +echo "IP: `awk 'END{print $1}' /etc/hosts`" +echo "PATH: $PATH" +echo "RAS_PORT: $RAS_PORT" +echo "RAGENT_HOST: $RAGENT_HOST" +echo "RAGENT_PORT: $RAGENT_PORT" + +ras cluster --port $RAS_PORT $RAGENT_HOST:$RAGENT_PORT diff --git a/rac.py b/rac.py new file mode 100644 index 0000000..132b439 --- /dev/null +++ b/rac.py @@ -0,0 +1 @@ +help('modules') From b6d81b49cb67dd76d2985df9aa3c800247457c70 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sat, 25 Feb 2023 23:34:22 +0300 Subject: [PATCH 02/20] rac: WIP Add settings for rac mode Add descriprion in README.md for new settings Update pyproject.toml format, add toml package to dev dependencies Some refactoring for `ClusterControlInterface` implementation can be injected Add test for `ClusterControler` injector Reformatted with black Sync poetry.lock after rebase Poetry update after rebase Lock file fixup after rebase Fixup missing import Add `get_info_base` abstract method to ClusterControler class Code style fixups Mocks for cluster contol mode Added stub method for `ClusterRACControler` Drop debug temporary module --- README.md | 2 + conf/default_settings.py | 5 + core/cluster/abc.py | 8 + core/cluster/rac.py | 599 +++++++++++++++++++++++ core/cluster/tests/conftest.py | 14 + core/cluster/tests/test_cluster_utils.py | 14 +- core/cluster/tests/test_rac.py | 0 core/cluster/utils.py | 12 +- poetry.lock | 33 +- pyproject.toml | 3 + rac.py | 1 - settings.tpl.py | 5 + 12 files changed, 690 insertions(+), 6 deletions(-) create mode 100644 core/cluster/rac.py create mode 100644 core/cluster/tests/test_rac.py delete mode 100644 rac.py diff --git a/README.md b/README.md index dab18ed..07ef989 100644 --- a/README.md +++ b/README.md @@ -120,10 +120,12 @@ TIP: для работы с путями в шаблонном файле нас |Параметр|Описание| |-------:|:-------| |`V8_CLUSTER_ADMIN_CREDENTIALS`|Учетные данные администратора кластера 1С Предприятие| +|`V8_CLUSTER_CONTROL_MODE `|Режим взаимодействия с кластером 1С Предприятие: через COM-компоненту (`'com'`) или черз клиент администрирования кластера (`'rac'`)| |`V8_INFOBASES_CREDENTIALS` |Сопоставление с именами информационных баз, именами пользователей и паролями, которые будут использованы для подключения к информационным базам. Если информационная база не указана в списке в явном виде, для подклчения к ней будут использованы данные от записи `default`| |`V8_INFOBASES_EXCLUDE` |Список с именами информационных баз, которые будут пропущены. Никакие операции с ними выполняться не будут| |`V8_INFOBASES_ONLY` |Если список не пустой, все действия будут проводиться только с информационными базами, указанными в нём| |`V8_LOCK_INFO_BASE_PAUSE` |Пауза в секундах между блокировкой фоновых заданий ИБ и продолжением дальнейших действий. Бывает полезно т.к. некоторые фоновые задания могут долго инициализироваться и создать сеанс уже после установления блокировки| +|`V8_RAS` |Параметры подключения к серверу администрирования кластера 1С Предприятие: address и port| |`V8_SERVER_AGENT` |Параметры подключения к агенту сервера 1С Предприятие: address и port| |`V8_PERMISSION_CODE` |Код блокировки начала новых сеансов, который будет устанавливаться при совершении операций с информационной базой| |`V8_PLATFORM_PATH` |Путь к платформе 1С Предприятие. Последняя версия платформы будет определена автоматически| diff --git a/conf/default_settings.py b/conf/default_settings.py index df08f29..fa930fb 100644 --- a/conf/default_settings.py +++ b/conf/default_settings.py @@ -5,12 +5,17 @@ ## ------------- ## V8_CLUSTER_ADMIN_CREDENTIALS = ("Администратор", "") +V8_CLUSTER_CONTROL_MODE = "com" V8_INFOBASES_CREDENTIALS = { "default": ("Администратор", ""), } V8_INFOBASES_EXCLUDE = [] V8_INFOBASES_ONLY = [] V8_LOCK_INFO_BASE_PAUSE = 5 +V8_RAS = { + "address": "localhost", + "port": "1545", +} V8_SERVER_AGENT = { "address": "localhost", "port": "1540", diff --git a/core/cluster/abc.py b/core/cluster/abc.py index 0f8059e..1e998a9 100644 --- a/core/cluster/abc.py +++ b/core/cluster/abc.py @@ -38,6 +38,14 @@ def terminate_info_base_sessions(self, infobase: str): """ ... + @abstractmethod + def get_info_base(self, infobase: str): + """ + Получает сведения об ИБ из кластера + :param infobase: имя информационной базы + """ + ... + def get_info_bases(self) -> List[str]: """ Получает имена всех ИБ, кроме указанных в списке V8_INFOBASES_EXCLUDE diff --git a/core/cluster/rac.py b/core/cluster/rac.py new file mode 100644 index 0000000..8b9d348 --- /dev/null +++ b/core/cluster/rac.py @@ -0,0 +1,599 @@ +import logging + +from core.cluster.abc import ClusterControler + +log = logging.getLogger(__name__) + + +class ClusterRACControler(ClusterControler): + + def get_cluster_info_bases(self): + """ + Получает список всех ИБ из кластера + """ + ... + + + def lock_info_base(self, infobase: str, permission_code: str, message: str): + """ + Блокирует фоновые задания и новые сеансы информационной базы + :param infobase: имя информационной базы + :param permission_code: Код доступа к информационной базе во время блокировки сеансов + :param message: Сообщение будет выводиться при попытке установить сеанс с ИБ + """ + ... + + + def unlock_info_base(self, infobase: str): + """ + Снимает блокировку фоновых заданий и сеансов информационной базы + :param infobase: имя информационной базы + """ + ... + + + def terminate_info_base_sessions(self, infobase: str): + """ + Принудительно завершает текущие сеансы информационной базы + :param infobase: имя информационной базы + """ + ... + + + def get_info_base(self, infobase: str): + """ + Получает сведения об ИБ из кластера + :param infobase: имя информационной базы + """ + ... + + +""" +Use: + + rac cluster [command] [options] [arguments] + +Shared options: + + --version | -v + get the utility version + + --help | -h | -? + display brief utility description + +Shared arguments: + + [:] + administration server address (default: localhost:1545) + +Mode: + + cluster + Server cluster administration mode + +Commands: + + admin + management of cluster administrators + + Additional commands: + list + receipt of the cluster administrator list + + register + adding a new cluster administrator + + --name= + (required) administrator name + + --pwd= + administrator password in case of password authentication + + --descr= + description of the administrator + + --auth=pwd[,os] + available authentication methods: + pwd - using the user name and password + os - authentication using OS + + --os-user= + OS user name + + --agent-user= + name of the cluster agent administrator + + --agent-pwd= + password of the cluster agent administrator + + remove + deleting the cluster administrator + + --name= + (required) name of the cluster administrator + + --cluster= + (required) server cluster identifier + + --cluster-user= + name of the cluster administrator + + --cluster-pwd= + password of the cluster administrator + + info + receipt of cluster information + + --cluster= + (required) server cluster identifier + + list + receipt of the cluster information list + + insert + new cluster registration + + --host= + (required) name (or IP-address) of the computer where + the cluster registry and the main cluster manager process are located + + --port= + (required) main port of the main manager + + --name= + cluster name (presentation) + + --expiration-timeout= + forced termination time (seconds) + + --lifetime-limit= + restart time of cluster working processes (seconds) + + --max-memory-size= + maximum virtual address space (KB), + used by the working process + + --max-memory-time-limit= + maximum period of exceeding critical memory limit (seconds) + + --security-level= + connection security level + + --session-fault-tolerance-level= + fault-tolerance level + + --load-balancing-mode=performance|memory + load balancing mode + performance - priority by available performance + memory - priority by available memory + + --errors-count-threshold= + server errors threshold (percentage) + + --kill-problem-processes= + terminate corrupted processes + + --kill-by-memory-with-dump= + create process dump when maximum memory amount is exceeded + + --agent-user= + name of the cluster agent administrator + + --agent-pwd= + password of the cluster agent administrator + + update + cluster parameter update + + --cluster= + (required) server cluster identifier + + --name= + cluster name (presentation) + + --expiration-timeout= + forced termination time (seconds) + + --lifetime-limit= + restart time of cluster working processes (seconds) + + --max-memory-size= + maximum virtual address space (KB), + used by the working process + + --max-memory-time-limit= + maximum period of exceeding critical memory limit (seconds) + + --security-level= + connection security level + + --session-fault-tolerance-level= + fault-tolerance level + + --load-balancing-mode=performance|memory + load balancing mode + performance - priority by available performance + memory - priority by available memory + + --errors-count-threshold= + server errors threshold (percentage) + + --kill-problem-processes= + terminate corrupted processes + + --kill-by-memory-with-dump= + create process dump when maximum memory amount is exceeded + + --agent-user= + name of the cluster agent administrator + + --agent-pwd= + password of the cluster agent administrator + + remove + deleting the cluster + + --cluster= + (required) server cluster identifier + + --cluster-user= + name of the cluster administrator + + --cluster-pwd= + password of the cluster administrator + + +./rac cluster admin list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +name : Администратор +auth : pwd +os-user : +descr : + +./rac cluster info --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +cluster : b930e651-0160-47c6-aeae-68b8ed937120 +host : ragent +port : 1541 +name : "Local cluster" +expiration-timeout : 60 +lifetime-limit : 0 +max-memory-size : 0 +max-memory-time-limit : 0 +security-level : 0 +session-fault-tolerance-level : 0 +load-balancing-mode : performance +errors-count-threshold : 0 +kill-problem-processes : 1 +kill-by-memory-with-dump : 0 + +./rac cluster list ras:1545 +cluster : b930e651-0160-47c6-aeae-68b8ed937120 +host : ragent +port : 1541 +name : "Local cluster" +expiration-timeout : 60 +lifetime-limit : 0 +max-memory-size : 0 +max-memory-time-limit : 0 +security-level : 0 +session-fault-tolerance-level : 0 +load-balancing-mode : performance +errors-count-threshold : 0 +kill-problem-processes : 1 +kill-by-memory-with-dump : 0 +""" +""" +./rac infobase summary list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 + + +Use: + + rac infobase [command] [options] [arguments] + +Shared options: + + --version | -v + get the utility version + + --help | -h | -? + display brief utility description + +Shared arguments: + + [:] + administration server address (default: localhost:1545) + +Mode: + + infobase + Infobase administration mode + +Parameters: + + --cluster= + (required) server cluster identifier + + --cluster-user= + name of the cluster administrator + + --cluster-pwd= + password of the cluster administrator + +Commands: + + info + receiving the information about the infobase + + --infobase= + (required) infobase identifier + + --infobase-user= + name of the infobase administrator + + --infobase-pwd= + password of the infobase administrator + + summary + management of brief information on infobases + + Additional commands: + info + receiving brief information on the infobase + + --infobase= + (required) infobase identifier + + list + receiving the list of brief information on infobases + + update + updating brief information on the infobase + + --infobase= + (required) infobase identifier + + --descr= + infobase description + + create + infobase creation + + --create-database + Create database when creating infobase + + --name= + (required) name of infobase + + --dbms=MSSQLServer|PostgreSQL|IBMDB2|OracleDatabase + (required) type of the Database Management System where the infobase is located: + MSSQLServer - MS SQL Server + PostgreSQL - PostgreSQL + IBMDB2 - IBM DB2 + OracleDatabase - Oracle Database + + --db-server= + (required) the name of the database server + + --db-name= + (required) database name + + --locale= + (required) identifier of national settings of the infobase + + --db-user= + database administrator name + + --db-pwd= + database administrator password + + --descr= + infobase description + + --date-offset= + date offset in the infobase + + --security-level= + infobase connection security level + + --scheduled-jobs-deny=on|off + scheduled job lock management + on - scheduled job execution prohibited + off - scheduled job execution permitted + + --license-distribution=deny|allow + management of licenses granting by 1C:Enterprise server + deny - licensing is forbidden + allow - licensing is allowed + + update + updating information on infobase + + --infobase= + (required) infobase identifier + + --infobase-user= + name of the infobase administrator + + --infobase-pwd= + password of the infobase administrator + + --dbms=MSSQLServer|PostgreSQL|IBMDB2|OracleDatabase + type of the Database Management System where the infobase is located: + MSSQLServer - MS SQL Server + PostgreSQL - PostgreSQL + IBMDB2 - IBM DB2 + OracleDatabase - Oracle Database + + --db-server= + the name of the database server + + --db-name= + database name + + --db-user= + database administrator name + + --db-pwd= + database administrator password + + --descr= + infobase description + + --denied-from= + start of the time interval within which the session lock mode is enabled + + --denied-message= + message displayed upon session lock violation + + --denied-parameter= + session lock parameter + + --denied-to= + end of the time interval within which the session lock mode is enabled + + --permission-code= + access code that allows the session to start in spite of enabled session lock + + --sessions-deny=on|off + session lock mode management + on - mode of session start lock enabled + off - mode of session start lock disabled + + --scheduled-jobs-deny=on|off + scheduled job lock management + on - scheduled job execution prohibited + off - scheduled job execution permitted + + --license-distribution=deny|allow + management of licenses granting by 1C:Enterprise server + deny - licensing is forbidden + allow - licensing is allowed + + --external-session-manager-connection-string= + external session management parameter + + --external-session-manager-required=yes|no + external session management required + yes - external session management is a must + no - external session management is optional + + --reserve-working-processes=yes|no + Workflow backup + yes - Workflow backup is enabled + no - Workflow backup is disabled + + --security-profile-name= + infobase security profile + + --safe-mode-security-profile-name= + external code security profile + + drop + remote infobase mode + + --infobase= + (required) infobase identifier + + --infobase-user= + name of the infobase administrator + + --infobase-pwd= + password of the infobase administrator + + --drop-database + delete database upon deleting infobase + + --clear-database + clear database upon deleting infobase + + +./rac infobase create --create-database --name=infobase01 --dbms=MSSQLServer --db-server=db --db-name=infobase01 --locale=ru --db-user=sa --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +./rac infobase create --create-database --name=infobase01 --dbms=PostgreSQL --db-server=db --db-name=infobase01 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +./rac infobase create --create-database --name=infobase02 --dbms=PostgreSQL --db-server=db --db-name=infobase02 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +--- +infobase : 9046c0db-1939-42a7-9b2d-f0370ca950df +--- +./rac infobase summary list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +--- +server_addr=tcp://ragent:1540 descr=32(0x00000020): Broken pipe line=1470 file=src/rtrsrvc/src/DataExchangeTcpClientImpl.cpp +--- +infobase : 01c55c12-0f4c-4101-8113-7707d450e83c +name : infobase01 +descr : + +infobase : 9046c0db-1939-42a7-9b2d-f0370ca950df +name : infobase02 +descr : +""" + +""" +Use: + + rac session [command] [options] [arguments] + +Shared options: + + --version | -v + get the utility version + + --help | -h | -? + display brief utility description + +Shared arguments: + + [:] + administration server address (default: localhost:1545) + +Mode: + + session + Infobase session administration mode + +Parameters: + + --cluster= + (required) server cluster identifier + + --cluster-user= + name of the cluster administrator + + --cluster-pwd= + password of the cluster administrator + +Commands: + + info + receiving information on the session + + --session= + (required) infobase session identifier + + --licenses + displaying information on licenses granted to the session + + list + receiving the session information list + + --infobase= + infobase identifier + + --licenses + displaying information on licenses granted to the session + + terminate + Forced termination of the session + + --session= + (required) infobase session identifier + + --error-message= + Session termination reason message + + interrupt-current-server-call + current server call termination + + --session= + (required) infobase session identifier + + --error-message= + termination cause message +""" diff --git a/core/cluster/tests/conftest.py b/core/cluster/tests/conftest.py index 640c5d7..899b108 100644 --- a/core/cluster/tests/conftest.py +++ b/core/cluster/tests/conftest.py @@ -29,6 +29,20 @@ def mock_only_infobases(mocker: MockerFixture, infobases): return only_infobases +@pytest.fixture +def mock_cluster_control_mode_com(mocker: MockerFixture): + cluster_control_mode = "com" + mocker.patch("conf.settings.V8_CLUSTER_CONTROL_MODE", new_callable=PropertyMock(return_value=cluster_control_mode)) + return cluster_control_mode + + +@pytest.fixture +def mock_cluster_control_mode_rac(mocker: MockerFixture): + cluster_control_mode = "rac" + mocker.patch("conf.settings.V8_CLUSTER_CONTROL_MODE", new_callable=PropertyMock(return_value=cluster_control_mode)) + return cluster_control_mode + + @surrogate("win32com.client") @pytest.fixture def mock_win32com_client_dispatch(mocker: MockerFixture): diff --git a/core/cluster/tests/test_cluster_utils.py b/core/cluster/tests/test_cluster_utils.py index 3dd1c65..6fe82cb 100644 --- a/core/cluster/tests/test_cluster_utils.py +++ b/core/cluster/tests/test_cluster_utils.py @@ -15,6 +15,7 @@ from core import models as core_models from core.cluster.comcntr import ClusterCOMControler +from core.cluster.rac import ClusterRACControler from core.cluster.utils import ( com_func_wrapper, get_cluster_controller, @@ -23,9 +24,9 @@ from core.exceptions import V8Exception -def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_com(): +def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_com(mock_cluster_control_mode_com): """ - `get_cluster_controller_class` returns `ClusterCOMControler` class + `get_cluster_controller_class` returns `ClusterCOMControler` class when `V8_CLUSTER_CONTROL_MODE = 'com'` """ controller_class = get_cluster_controller_class() assert controller_class == ClusterCOMControler @@ -41,6 +42,15 @@ def test_get_cluster_controller_retuns_comcntr_when_mode_is_com(mocker: MockerFi @pytest.mark.asyncio +def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_rac(mock_cluster_control_mode_rac): + """ + `get_cluster_controller_class` returns `ClusterRACControler` class when `V8_CLUSTER_CONTROL_MODE = 'rac'` + """ + controller_class = get_cluster_controller_class() + assert controller_class == ClusterRACControler + + +@pytest.mark.asyncio() async def test_com_func_wrapper_awaits_inner_func(infobase): """ `com_func_wrapper` awaits inner coroutine diff --git a/core/cluster/tests/test_rac.py b/core/cluster/tests/test_rac.py new file mode 100644 index 0000000..e69de29 diff --git a/core/cluster/utils.py b/core/cluster/utils.py index 08c6c64..59ee618 100644 --- a/core/cluster/utils.py +++ b/core/cluster/utils.py @@ -16,11 +16,19 @@ log = logging.getLogger(__name__) +RAC_CLUSTER_CONTROL_MODE = "rac" +COM_CLUSTER_CONTROL_MODE = "com" + def get_cluster_controller_class(): - from core.cluster.comcntr import ClusterCOMControler + if settings.V8_CLUSTER_CONTROL_MODE == RAC_CLUSTER_CONTROL_MODE: + from core.cluster.rac import ClusterRACControler + + controller_class = ClusterRACControler + else: + from core.cluster.comcntr import ClusterCOMControler - controller_class = ClusterCOMControler + controller_class = ClusterCOMControler return controller_class diff --git a/poetry.lock b/poetry.lock index cd35ce9..4e08ce9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -632,6 +632,37 @@ requests = ">=1.0.0" [package.extras] yaml = ["PyYAML (>=3.10)"] +[[package]] +name = "debugpy" +version = "1.8.1" +description = "An implementation of the Debug Adapter Protocol for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "debugpy-1.8.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741"}, + {file = "debugpy-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e"}, + {file = "debugpy-1.8.1-cp310-cp310-win32.whl", hash = "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0"}, + {file = "debugpy-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd"}, + {file = "debugpy-1.8.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb"}, + {file = "debugpy-1.8.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099"}, + {file = "debugpy-1.8.1-cp311-cp311-win32.whl", hash = "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146"}, + {file = "debugpy-1.8.1-cp311-cp311-win_amd64.whl", hash = "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8"}, + {file = "debugpy-1.8.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539"}, + {file = "debugpy-1.8.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace"}, + {file = "debugpy-1.8.1-cp312-cp312-win32.whl", hash = "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0"}, + {file = "debugpy-1.8.1-cp312-cp312-win_amd64.whl", hash = "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98"}, + {file = "debugpy-1.8.1-cp38-cp38-macosx_11_0_x86_64.whl", hash = "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39"}, + {file = "debugpy-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7"}, + {file = "debugpy-1.8.1-cp38-cp38-win32.whl", hash = "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9"}, + {file = "debugpy-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234"}, + {file = "debugpy-1.8.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42"}, + {file = "debugpy-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703"}, + {file = "debugpy-1.8.1-cp39-cp39-win32.whl", hash = "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23"}, + {file = "debugpy-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3"}, + {file = "debugpy-1.8.1-py2.py3-none-any.whl", hash = "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242"}, + {file = "debugpy-1.8.1.zip", hash = "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -1778,4 +1809,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "8a942919173f1856cbdde6f66fd4cb2dbb06a6d7823b224a488ac7a97a9d6714" +content-hash = "e99fca471ad0054b24c2b721d42df73fe7a98d17f823b1a89c59c5b93a270117" diff --git a/pyproject.toml b/pyproject.toml index b3e1af5..1956c73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,9 @@ flake8-pytest-style = "*" flake8-quotes = "*" flake8-simplify = "*" +[tool.poetry.group.debug.dependencies] +debugpy = "^1.6.6" + [tool.pytest.ini_options] spec_header_format = "Case: {test_case}\nPath: {module_path}" spec_test_format = '{result} {docstring_summary}' diff --git a/rac.py b/rac.py deleted file mode 100644 index 132b439..0000000 --- a/rac.py +++ /dev/null @@ -1 +0,0 @@ -help('modules') diff --git a/settings.tpl.py b/settings.tpl.py index ea6b174..8072483 100644 --- a/settings.tpl.py +++ b/settings.tpl.py @@ -5,6 +5,7 @@ ## ------------- ## V8_CLUSTER_ADMIN_CREDENTIALS = ("Администратор", "cluster_admin_password") +V8_CLUSTER_CONTROL_MODE = "com" V8_INFOBASES_CREDENTIALS = { "default": ("Администратор", "infobase_user_password"), "accounting": ("БухАдминистратор", "infobase_user_password"), @@ -12,6 +13,10 @@ } V8_INFOBASES_EXCLUDE = ["accounting_for_tests", "trade_copy"] V8_INFOBASES_ONLY = ["accounting_production", "trade_production"] +V8_RAS = { + "address": "localhost", + "port": "1545", +} V8_SERVER_AGENT = { "address": "localhost", "port": "1540", From 0a010f7e144fb18b32714ff18dc5d856fbe78d5d Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sat, 7 Dec 2024 23:23:47 +0300 Subject: [PATCH 03/20] Move from isort, black and flake8 to ruff --- .flake8 | 24 -- .github/workflows/pytest.yaml | 4 +- .github/workflows/{flake8.yaml => ruff.yaml} | 18 +- .pre-commit-config.yaml | 23 +- Makefile | 14 +- backup.py | 23 +- core/aws.py | 13 +- core/cluster/abc.py | 3 +- core/cluster/comcntr.py | 5 +- core/cluster/rac.py | 5 - core/cluster/tests/conftest.py | 20 +- core/cluster/tests/test_cluster_utils.py | 8 +- core/cluster/tests/test_comcntr.py | 3 +- core/exceptions.py | 4 + core/process.py | 16 +- core/tests/conftest.py | 13 +- core/tests/test_analyze.py | 14 +- core/tests/test_aws.py | 30 +- core/tests/test_process.py | 64 +++- core/tests/test_utils.py | 9 +- maintenance.py | 6 +- poetry.lock | 353 ++----------------- pyproject.toml | 35 +- surrogate/__init__.py | 10 +- surrogate/tests/test_surrogate.py | 2 +- tests/test_backup.py | 60 +++- tests/test_maintenance.py | 8 +- tests/test_update.py | 83 ++++- update.py | 8 +- utils/common.py | 2 +- utils/notification.py | 4 +- utils/postgres.py | 4 +- utils/tests/conftest.py | 5 +- utils/tests/test_notification.py | 4 +- 34 files changed, 409 insertions(+), 488 deletions(-) delete mode 100644 .flake8 rename .github/workflows/{flake8.yaml => ruff.yaml} (57%) diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b664458..0000000 --- a/.flake8 +++ /dev/null @@ -1,24 +0,0 @@ -[flake8] -exclude = - .git, - .github, - .pytest_cache - .vscode - __pycache__ -max-line-length = 120 -ignore = - E501 - W503 -per-file-ignores = - # too many leading '#' for block comment - conf/default_settings.py: E266 - # Multi-line container not broken after opening character - core/analyze.py: JS101 - # too many leading '#' for block comment - settings.tpl.py: E266 - # module imported but unused - surrogate/tests/test_surrogate.py: F401 - # local variable 'succeeded' is assigned to but never used - conftest.py: F841 -inline-quotes = " -pytest-fixture-no-parentheses = True diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index e0512df..2072a3f 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -15,11 +15,11 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Install Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Poetry - uses: abatilo/actions-poetry@v2.3.0 + uses: abatilo/actions-poetry@v3 - name: Install dependencies run: make install-dev - name: Test with coverage diff --git a/.github/workflows/flake8.yaml b/.github/workflows/ruff.yaml similarity index 57% rename from .github/workflows/flake8.yaml rename to .github/workflows/ruff.yaml index 6021a5e..63005e7 100644 --- a/.github/workflows/flake8.yaml +++ b/.github/workflows/ruff.yaml @@ -1,29 +1,29 @@ -# This workflow will check modified files with flake8 +# This workflow will check modified files with ruff -name: flake8 +name: ruff on: pull_request: types: [opened, reopened, synchronize] jobs: - flake8: + ruff: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: "3.12" - name: Install Poetry - uses: abatilo/actions-poetry@v2.3.0 + uses: abatilo/actions-poetry@v3 - name: Install dependencies run: make install-dev - name: Get changed python source files id: changed-files - uses: tj-actions/changed-files@v41 + uses: tj-actions/changed-files@v45 with: files: | *.py - - name: Check code with flake8 + - name: Check code with ruff if: steps.changed-files.outputs.any_changed == 'true' - run: poetry run flake8 ${{ steps.changed-files.outputs.all_changed_files }} + run: poetry run ruff check ${{ steps.changed-files.outputs.all_changed_files }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a84ca34..ed826d4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,15 +1,10 @@ repos: - - repo: https://github.com/pycqa/isort - rev: 5.10.1 - hooks: - - id: isort - args: ["--profile", "black", "--filter-files"] - - repo: https://github.com/psf/black-pre-commit-mirror # Using this mirror lets us use mypyc-compiled black, which is about 2x faster - rev: 23.10.1 - hooks: - - id: black - # It is recommended to specify the latest version of Python - # supported by your project here, or alternatively use - # pre-commit's default_language_version, see - # https://pre-commit.com/#top_level-default_language_version - language_version: python3.11 +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.8.2 + hooks: + # Run the linter. + - id: ruff + args: [ --fix ] + # Run the formatter. + - id: ruff-format diff --git a/Makefile b/Makefile index 37325ca..a9265ce 100644 --- a/Makefile +++ b/Makefile @@ -15,11 +15,11 @@ test: test-coverage: poetry run coverage run -m pytest -.PHONY: code-style -code-style: - poetry run isort . - poetry run black . +.PHONY: ruff +ruff: + poetry run ruff check -.PHONY: flake8 -flake8: - poetry run flake8 . +.PHONY: format +format: + poetry run ruff check --fix + poetry run ruff format diff --git a/backup.py b/backup.py index e00262e..85076dd 100644 --- a/backup.py +++ b/backup.py @@ -71,7 +71,10 @@ async def _backup_v8(ib_name: str, *args, **kwargs) -> core_models.InfoBaseBacku # Формирует команду для выгрузки info_base_user, info_base_pwd = utils.get_info_base_credentials(ib_name) ib_and_time_str = utils.get_ib_and_time_string(ib_name) - dt_filename = os.path.join(settings.BACKUP_PATH, utils.append_file_extension_to_string(ib_and_time_str, "dt")) + dt_filename = os.path.join( + settings.BACKUP_PATH, + utils.append_file_extension_to_string(ib_and_time_str, "dt"), + ) log_filename = os.path.join(settings.LOG_PATH, utils.append_file_extension_to_string(ib_and_time_str, "log")) # https://its.1c.ru/db/v838doc#bookmark:adm:TI000000526 v8_command = ( @@ -135,7 +138,8 @@ async def _backup_pgdump( ib_and_time_str = utils.get_ib_and_time_string(ib_name) backup_filename = os.path.join( - settings.BACKUP_PATH, utils.append_file_extension_to_string(ib_and_time_str, "pgdump") + settings.BACKUP_PATH, + utils.append_file_extension_to_string(ib_and_time_str, "pgdump"), ) log_filename = os.path.join(settings.LOG_PATH, utils.append_file_extension_to_string(ib_and_time_str, "log")) pg_dump_path = os.path.join(settings.PG_BIN_PATH, "pg_dump.exe") @@ -209,7 +213,11 @@ def create_aws_upload_task( ): if settings.AWS_ENABLED and backup_result.succeeded: return asyncio.create_task( - aws.upload_infobase_to_s3(backup_result.infobase_name, backup_result.backup_filename, aws_semaphore), + aws.upload_infobase_to_s3( + backup_result.infobase_name, + backup_result.backup_filename, + aws_semaphore, + ), name=f"Task :: Upload {backup_result.infobase_name} to S3", ) @@ -240,7 +248,8 @@ def analyze_results( def send_email_notification( - backup_result: List[core_models.InfoBaseBackupTaskResult], aws_result: List[core_models.InfoBaseAWSUploadTaskResult] + backup_result: List[core_models.InfoBaseBackupTaskResult], + aws_result: List[core_models.InfoBaseAWSUploadTaskResult], ): if settings.NOTIFY_EMAIL_ENABLED: log.info(f"<{log_prefix}> Sending email notification") @@ -259,7 +268,11 @@ async def main(): initialize_semaphore(settings.AWS_CONCURRENCY, log_prefix, "AWS") if settings.AWS_ENABLED else None ) backup_replication_semaphore = ( - initialize_semaphore(settings.BACKUP_REPLICATION_CONCURRENCY, log_prefix, "backup replication") + initialize_semaphore( + settings.BACKUP_REPLICATION_CONCURRENCY, + log_prefix, + "backup replication", + ) if settings.BACKUP_REPLICATION else None ) diff --git a/core/aws.py b/core/aws.py index dcb1a09..81eb340 100644 --- a/core/aws.py +++ b/core/aws.py @@ -106,10 +106,19 @@ async def upload_to_s3(backup_results: core_models.InfoBaseBackupTaskResult): datetime_start = datetime.now() result = await asyncio.gather( *[ - upload_infobase_to_s3(backup_result.infobase_name, backup_result.backup_filename, semaphore) + upload_infobase_to_s3( + backup_result.infobase_name, + backup_result.backup_filename, + semaphore, + ) for backup_result in backup_results if backup_result.succeeded ] ) datetime_finish = datetime.now() - analyze_s3_result(result, [e.infobase_name for e in backup_results], datetime_start, datetime_finish) + analyze_s3_result( + result, + [e.infobase_name for e in backup_results], + datetime_start, + datetime_finish, + ) diff --git a/core/cluster/abc.py b/core/cluster/abc.py index 1e998a9..dc8c753 100644 --- a/core/cluster/abc.py +++ b/core/cluster/abc.py @@ -57,7 +57,8 @@ def get_info_bases(self) -> List[str]: if settings.V8_INFOBASES_ONLY: info_bases = list( filter( - lambda ib: ib.lower() in [ib_only.lower() for ib_only in settings.V8_INFOBASES_ONLY], info_bases_raw + lambda ib: ib.lower() in [ib_only.lower() for ib_only in settings.V8_INFOBASES_ONLY], + info_bases_raw, ) ) else: diff --git a/core/cluster/comcntr.py b/core/cluster/comcntr.py index bddaa95..8a18942 100644 --- a/core/cluster/comcntr.py +++ b/core/cluster/comcntr.py @@ -146,7 +146,10 @@ def get_info_base_metadata(self, infobase: str, infobase_user: str, infobase_pwd return name, version def lock_info_base( - self, infobase: str, permission_code: str = "0000", message: str = "Выполняется обслуживание ИБ" + self, + infobase: str, + permission_code: str = "0000", + message: str = "Выполняется обслуживание ИБ", ): """ Блокирует фоновые задания и новые сеансы информационной базы diff --git a/core/cluster/rac.py b/core/cluster/rac.py index 8b9d348..d04af74 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -6,14 +6,12 @@ class ClusterRACControler(ClusterControler): - def get_cluster_info_bases(self): """ Получает список всех ИБ из кластера """ ... - def lock_info_base(self, infobase: str, permission_code: str, message: str): """ Блокирует фоновые задания и новые сеансы информационной базы @@ -23,7 +21,6 @@ def lock_info_base(self, infobase: str, permission_code: str, message: str): """ ... - def unlock_info_base(self, infobase: str): """ Снимает блокировку фоновых заданий и сеансов информационной базы @@ -31,7 +28,6 @@ def unlock_info_base(self, infobase: str): """ ... - def terminate_info_base_sessions(self, infobase: str): """ Принудительно завершает текущие сеансы информационной базы @@ -39,7 +35,6 @@ def terminate_info_base_sessions(self, infobase: str): """ ... - def get_info_base(self, infobase: str): """ Получает сведения об ИБ из кластера diff --git a/core/cluster/tests/conftest.py b/core/cluster/tests/conftest.py index 899b108..6698f26 100644 --- a/core/cluster/tests/conftest.py +++ b/core/cluster/tests/conftest.py @@ -18,28 +18,40 @@ def mock_infobase_version(): @pytest.fixture def mock_excluded_infobases(mocker: MockerFixture, infobases): excluded_infobases = [infobases[-1]] - mocker.patch("conf.settings.V8_INFOBASES_EXCLUDE", new_callable=PropertyMock(return_value=excluded_infobases)) + mocker.patch( + "conf.settings.V8_INFOBASES_EXCLUDE", + new_callable=PropertyMock(return_value=excluded_infobases), + ) return excluded_infobases @pytest.fixture def mock_only_infobases(mocker: MockerFixture, infobases): only_infobases = infobases[:-1] - mocker.patch("conf.settings.V8_INFOBASES_ONLY", new_callable=PropertyMock(return_value=only_infobases)) + mocker.patch( + "conf.settings.V8_INFOBASES_ONLY", + new_callable=PropertyMock(return_value=only_infobases), + ) return only_infobases @pytest.fixture def mock_cluster_control_mode_com(mocker: MockerFixture): cluster_control_mode = "com" - mocker.patch("conf.settings.V8_CLUSTER_CONTROL_MODE", new_callable=PropertyMock(return_value=cluster_control_mode)) + mocker.patch( + "conf.settings.V8_CLUSTER_CONTROL_MODE", + new_callable=PropertyMock(return_value=cluster_control_mode), + ) return cluster_control_mode @pytest.fixture def mock_cluster_control_mode_rac(mocker: MockerFixture): cluster_control_mode = "rac" - mocker.patch("conf.settings.V8_CLUSTER_CONTROL_MODE", new_callable=PropertyMock(return_value=cluster_control_mode)) + mocker.patch( + "conf.settings.V8_CLUSTER_CONTROL_MODE", + new_callable=PropertyMock(return_value=cluster_control_mode), + ) return cluster_control_mode diff --git a/core/cluster/tests/test_cluster_utils.py b/core/cluster/tests/test_cluster_utils.py index 6fe82cb..edd7d03 100644 --- a/core/cluster/tests/test_cluster_utils.py +++ b/core/cluster/tests/test_cluster_utils.py @@ -24,7 +24,9 @@ from core.exceptions import V8Exception -def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_com(mock_cluster_control_mode_com): +def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_com( + mock_cluster_control_mode_com, +): """ `get_cluster_controller_class` returns `ClusterCOMControler` class when `V8_CLUSTER_CONTROL_MODE = 'com'` """ @@ -42,7 +44,9 @@ def test_get_cluster_controller_retuns_comcntr_when_mode_is_com(mocker: MockerFi @pytest.mark.asyncio -def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_rac(mock_cluster_control_mode_rac): +def test_get_cluster_controller_class_retuns_comcntr_when_mode_is_rac( + mock_cluster_control_mode_rac, +): """ `get_cluster_controller_class` returns `ClusterRACControler` class when `V8_CLUSTER_CONTROL_MODE = 'rac'` """ diff --git a/core/cluster/tests/test_comcntr.py b/core/cluster/tests/test_comcntr.py index b9cddf9..8394ba7 100644 --- a/core/cluster/tests/test_comcntr.py +++ b/core/cluster/tests/test_comcntr.py @@ -100,7 +100,8 @@ def test_cluster_control_interface_get_working_process_connection_info_base_auth "test_infobase02": ("test_user02", "test_password02"), } mocker.patch( - "conf.settings.V8_INFOBASES_CREDENTIALS", new_callable=PropertyMock(return_value=infobases_credentials) + "conf.settings.V8_INFOBASES_CREDENTIALS", + new_callable=PropertyMock(return_value=infobases_credentials), ) cci = ClusterCOMControler() cci.get_working_process_connection_with_info_base_auth() diff --git a/core/exceptions.py b/core/exceptions.py index c43308f..1a5e064 100644 --- a/core/exceptions.py +++ b/core/exceptions.py @@ -4,3 +4,7 @@ class SubprocessException(Exception): class V8Exception(SubprocessException): pass + + +class RACException(Exception): + pass diff --git a/core/process.py b/core/process.py index 5b195c9..79f4f81 100644 --- a/core/process.py +++ b/core/process.py @@ -104,7 +104,14 @@ async def execute_v8_command( if permission_code: # Снимает блокировку фоновых заданий и сеансов cci.unlock_info_base(ib_name) - _check_subprocess_return_code(ib_name, v8_process, log_filename, "utf-8-sig", V8Exception, log_output_on_success) + _check_subprocess_return_code( + ib_name, + v8_process, + log_filename, + "utf-8-sig", + V8Exception, + log_output_on_success, + ) async def execute_subprocess_command( @@ -124,5 +131,10 @@ async def execute_subprocess_command( log.debug(f"<{ib_name}> Subprocess PID is {subprocess.pid}") await _wait_for_subprocess(subprocess, timeout) _check_subprocess_return_code( - ib_name, subprocess, log_filename, "utf-8", SubprocessException, log_output_on_success + ib_name, + subprocess, + log_filename, + "utf-8", + SubprocessException, + log_output_on_success, ) diff --git a/core/tests/conftest.py b/core/tests/conftest.py index 8982ae3..d7c584d 100644 --- a/core/tests/conftest.py +++ b/core/tests/conftest.py @@ -56,7 +56,7 @@ async def __anext__(self): try: return next(self.iter) except StopIteration: - raise StopAsyncIteration + raise StopAsyncIteration from None bucket_obj = AsyncMock() bucket_obj.last_modified = AsyncMock(return_value=last_modified)() @@ -80,7 +80,8 @@ async def mock_aioboto3_bucket_objects_old(mock_aioboto3_session): from conf import settings return create_bucket_object( - mock_aioboto3_session, datetime.now(tz=timezone.utc) - timedelta(days=settings.AWS_RETENTION_DAYS + 2) + mock_aioboto3_session, + datetime.now(tz=timezone.utc) - timedelta(days=settings.AWS_RETENTION_DAYS + 2), ) @@ -100,7 +101,8 @@ def mock_os_stat(mocker: MockerFixture): def mock_os_platform_path(mocker: MockerFixture, mock_platform_versions): mocker.patch("os.path.isdir", return_value=True) return mocker.patch( - "os.listdir", return_value=mock_platform_versions + ["test_common", "test_conf", "test_srvinfo"] + "os.listdir", + return_value=mock_platform_versions + ["test_common", "test_conf", "test_srvinfo"], ) @@ -128,7 +130,10 @@ def mock_datetime(): def mock_infobases_credentials(mocker: MockerFixture, infobases): creds = {infobase: (f"test_{infobase}_login", f"test_{infobase}_password") for infobase in infobases} creds.update(settings.V8_INFOBASES_CREDENTIALS) - mocker.patch("conf.settings.V8_INFOBASES_CREDENTIALS", new_callable=PropertyMock(return_value=creds)) + mocker.patch( + "conf.settings.V8_INFOBASES_CREDENTIALS", + new_callable=PropertyMock(return_value=creds), + ) return creds diff --git a/core/tests/test_analyze.py b/core/tests/test_analyze.py index e762fe7..e98b4a9 100644 --- a/core/tests/test_analyze.py +++ b/core/tests/test_analyze.py @@ -30,7 +30,13 @@ def test_analyze_result_log_subprefix(caplog, infobases, success_base_result): datetime_start = datetime.now() datetime_finish = datetime_start + timedelta(minutes=5) with caplog.at_level(logging.INFO): - analyze_result(success_base_result, infobases, datetime_start, datetime_finish, log_subprefix) + analyze_result( + success_base_result, + infobases, + datetime_start, + datetime_finish, + log_subprefix, + ) assert log_subprefix in caplog.text @@ -86,7 +92,11 @@ def test_analyze_result_maintenance(mock_analyze_result, infobases, success_main datetime_finish = datetime_start + timedelta(minutes=5) analyze_maintenance_result(success_maintenance_result, infobases, datetime_start, datetime_finish) mock_analyze_result.assert_called_with( - success_maintenance_result, infobases, datetime_start, datetime_finish, "Maintenance" + success_maintenance_result, + infobases, + datetime_start, + datetime_finish, + "Maintenance", ) diff --git a/core/tests/test_aws.py b/core/tests/test_aws.py index b36956b..3b00a14 100644 --- a/core/tests/test_aws.py +++ b/core/tests/test_aws.py @@ -28,7 +28,10 @@ def test_get_aws_endpoint_url_parameter_returns_dict_if_set(mocker: MockerFixtur """ Custom AWS endpoint url parameter is dict """ - mocker.patch("conf.settings.AWS_ENDPOINT_URL", new_callable=PropertyMock(return_value="test.aws.endpoint")) + mocker.patch( + "conf.settings.AWS_ENDPOINT_URL", + new_callable=PropertyMock(return_value="test.aws.endpoint"), + ) result = _get_aws_endpoint_url_parameter() assert isinstance(result, dict) @@ -41,12 +44,17 @@ def test_get_aws_endpoint_url_parameter_returns_empty_dict_if_not_set(): assert not result -def test_get_aws_endpoint_url_parameter_returns_dict_with_value_if_set(mocker: MockerFixture): +def test_get_aws_endpoint_url_parameter_returns_dict_with_value_if_set( + mocker: MockerFixture, +): """ Custom AWS endpoint url parameter contatins parameter name and value """ endpoint_url = "test.aws.endpoint" - mocker.patch("conf.settings.AWS_ENDPOINT_URL", new_callable=PropertyMock(return_value=endpoint_url)) + mocker.patch( + "conf.settings.AWS_ENDPOINT_URL", + new_callable=PropertyMock(return_value=endpoint_url), + ) result = _get_aws_endpoint_url_parameter() assert result["endpoint_url"] == endpoint_url @@ -63,7 +71,10 @@ def test_get_aws_region_parameter_returns_dict_if_set(mocker: MockerFixture): """ Custom AWS region name parameter is dict """ - mocker.patch("conf.settings.AWS_REGION_NAME", new_callable=PropertyMock(return_value="test-us-east-1")) + mocker.patch( + "conf.settings.AWS_REGION_NAME", + new_callable=PropertyMock(return_value="test-us-east-1"), + ) result = _get_aws_region_parameter() assert isinstance(result, dict) @@ -81,7 +92,10 @@ def test_get_aws_region_parameter_returns_dict_with_value_if_set(mocker: MockerF Custom AWS region name parameter contatins parameter name and value """ region_name = "test-us-east-1" - mocker.patch("conf.settings.AWS_REGION_NAME", new_callable=PropertyMock(return_value=region_name)) + mocker.patch( + "conf.settings.AWS_REGION_NAME", + new_callable=PropertyMock(return_value=region_name), + ) result = _get_aws_region_parameter() assert result["region_name"] == region_name @@ -147,7 +161,11 @@ async def test_upload_infobase_to_s3_make_retries( @pytest.mark.asyncio async def test_internal_upload_infobase_to_s3_call( - mocker: MockerFixture, infobase, success_backup_result, mock_aioboto3_session, mock_os_stat + mocker: MockerFixture, + infobase, + success_backup_result, + mock_aioboto3_session, + mock_os_stat, ): """ boto3.Session.client.upload_file inside should be called when uploading files to AWS diff --git a/core/tests/test_process.py b/core/tests/test_process.py index 9303beb..ea3936f 100644 --- a/core/tests/test_process.py +++ b/core/tests/test_process.py @@ -69,7 +69,9 @@ def test_check_subprocess_return_code_logs_message_when_subprocess_failed(mocker @pytest.mark.asyncio -async def test_kill_process_emergency_creates_subprocess(mock_asyncio_subprocess_succeeded): +async def test_kill_process_emergency_creates_subprocess( + mock_asyncio_subprocess_succeeded, +): """ `_kill_process_emergency` creates subprocess to try to kill process by pid """ @@ -115,7 +117,10 @@ async def test_kill_process_emergency_logs_result_when_communication_error( @pytest.mark.asyncio async def test_execute_v8_command_pass_command_to_subprocess( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` pass command to create subprocess correctly @@ -129,7 +134,10 @@ async def test_execute_v8_command_pass_command_to_subprocess( @pytest.mark.asyncio async def test_execute_v8_command_raises_if_nonzero_return_code( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_failed, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_failed, + mock_cluster_com_controller, ): """ `execute_v8_command` raises exception if subprocess returns non-zero return code @@ -144,7 +152,10 @@ async def test_execute_v8_command_raises_if_nonzero_return_code( @pytest.mark.asyncio async def test_execute_v8_command_passes_timeout_to_asyncio_wait_for( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` passes timeout value to `asyncio.wait_for` @@ -160,7 +171,10 @@ async def test_execute_v8_command_passes_timeout_to_asyncio_wait_for( @pytest.mark.asyncio async def test_execute_v8_command_terminates_subprocess_when_timed_out( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_timeouted, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_timeouted, + mock_cluster_com_controller, ): """ `execute_v8_command` terminates subprocess when timed out @@ -176,7 +190,10 @@ async def test_execute_v8_command_terminates_subprocess_when_timed_out( @pytest.mark.asyncio async def test_execute_v8_command_calls_emergency_on_termination_error( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_termination_error, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_termination_error, + mock_cluster_com_controller, ): """ `execute_v8_command` calls `_kill_process_emergency` when got expection while terminating subprocess @@ -192,7 +209,10 @@ async def test_execute_v8_command_calls_emergency_on_termination_error( @pytest.mark.asyncio async def test_execute_v8_command_calls_emergency_on_communication_error( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_timeouted, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_timeouted, + mock_cluster_com_controller, ): """ `execute_v8_command` calls `_kill_process_emergency` when got expection while communicating with subprocess @@ -208,7 +228,10 @@ async def test_execute_v8_command_calls_emergency_on_communication_error( @pytest.mark.asyncio async def test_execute_v8_command_locks_infobase_if_code_passed( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` locks infobase if permission code passed @@ -223,7 +246,10 @@ async def test_execute_v8_command_locks_infobase_if_code_passed( @pytest.mark.asyncio async def test_execute_v8_command_unlocks_infobase_if_code_passed( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` unlocks infobase if permission code passed @@ -238,7 +264,10 @@ async def test_execute_v8_command_unlocks_infobase_if_code_passed( @pytest.mark.asyncio async def test_execute_v8_command_does_not_lock_infobase_if_code_is_none( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` does not lock infobase if permission code is none @@ -252,7 +281,10 @@ async def test_execute_v8_command_does_not_lock_infobase_if_code_is_none( @pytest.mark.asyncio async def test_execute_v8_command_does_not_unlock_infobase_if_code_is_none( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` does not unlock infobase if permission code is none @@ -266,7 +298,10 @@ async def test_execute_v8_command_does_not_unlock_infobase_if_code_is_none( @pytest.mark.asyncio async def test_execute_v8_command_terminates_infobase_sessions( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` terminates infobase sessions @@ -280,7 +315,10 @@ async def test_execute_v8_command_terminates_infobase_sessions( @pytest.mark.asyncio async def test_execute_v8_sleeps_before_create_subprocess_if_parameter_passed( - mocker: MockerFixture, infobase, mock_asyncio_subprocess_succeeded, mock_cluster_com_controller + mocker: MockerFixture, + infobase, + mock_asyncio_subprocess_succeeded, + mock_cluster_com_controller, ): """ `execute_v8_command` locks infobase if permission code passed diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py index bac485f..95c584a 100644 --- a/core/tests/test_utils.py +++ b/core/tests/test_utils.py @@ -90,7 +90,9 @@ def test_get_ib_and_time_string_has_infobase_name_in_result_string(infobase): @pytest.mark.freeze_time("2022-01-01 12:01:01") -def test_get_ib_and_time_string_has_properly_formatted_datetime_in_result_string(infobase): +def test_get_ib_and_time_string_has_properly_formatted_datetime_in_result_string( + infobase, +): """ Datetime is properly formatted in `ib_and_time` string """ @@ -259,7 +261,10 @@ async def test_remove_old_files_by_pattern_removes_old_files(mocker: MockerFixtu retention_days = 1 files = ["test_file1", "test_file2"] mocker.patch("glob.glob", return_value=files) - mocker.patch("os.path.getmtime", return_value=(datetime.now() - timedelta(days=retention_days + 1)).timestamp()) + mocker.patch( + "os.path.getmtime", + return_value=(datetime.now() - timedelta(days=retention_days + 1)).timestamp(), + ) aioremove_mock = mocker.patch("aiofiles.os.remove", return_value=AsyncMock()) await remove_old_files_by_pattern("", retention_days) assert aioremove_mock.await_count == len(files) diff --git a/maintenance.py b/maintenance.py index 9832d97..7fecd9f 100644 --- a/maintenance.py +++ b/maintenance.py @@ -50,7 +50,11 @@ async def _maintenance_v8(ib_name: str, *args, **kwargs) -> core_models.InfoBase ) try: await execute_v8_command( - ib_name, v8_command, log_filename, timeout=settings.MAINTENANCE_TIMEOUT_V8, log_output_on_success=True + ib_name, + v8_command, + log_filename, + timeout=settings.MAINTENANCE_TIMEOUT_V8, + log_output_on_success=True, ) except V8Exception: return core_models.InfoBaseMaintenanceTaskResult(ib_name, False) diff --git a/poetry.lock b/poetry.lock index 4e08ce9..0b8f7f1 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "aioboto3" @@ -220,17 +220,6 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" -[[package]] -name = "astor" -version = "0.8.1" -description = "Read/rewrite/write Python ASTs" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -files = [ - {file = "astor-0.8.1-py2.py3-none-any.whl", hash = "sha256:070a54e890cefb5b3739d19f30f5a5ec840ffc9c50ffa7d23cc9fc1a38ebbfc5"}, - {file = "astor-0.8.1.tar.gz", hash = "sha256:6a6effda93f4e1ce9f618779b2dd1d9d84f1e32812c23a29b3fff6fd7f63fa5e"}, -] - [[package]] name = "async-timeout" version = "4.0.3" @@ -318,52 +307,6 @@ docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphi tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] -[[package]] -name = "black" -version = "23.12.1" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.8" -files = [ - {file = "black-23.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0aaf6041986767a5e0ce663c7a2f0e9eaf21e6ff87a5f95cbf3675bfd4c41d2"}, - {file = "black-23.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c88b3711d12905b74206227109272673edce0cb29f27e1385f33b0163c414bba"}, - {file = "black-23.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a920b569dc6b3472513ba6ddea21f440d4b4c699494d2e972a1753cdc25df7b0"}, - {file = "black-23.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:3fa4be75ef2a6b96ea8d92b1587dd8cb3a35c7e3d51f0738ced0781c3aa3a5a3"}, - {file = "black-23.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8d4df77958a622f9b5a4c96edb4b8c0034f8434032ab11077ec6c56ae9f384ba"}, - {file = "black-23.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:602cfb1196dc692424c70b6507593a2b29aac0547c1be9a1d1365f0d964c353b"}, - {file = "black-23.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c4352800f14be5b4864016882cdba10755bd50805c95f728011bcb47a4afd59"}, - {file = "black-23.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:0808494f2b2df923ffc5723ed3c7b096bd76341f6213989759287611e9837d50"}, - {file = "black-23.12.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:25e57fd232a6d6ff3f4478a6fd0580838e47c93c83eaf1ccc92d4faf27112c4e"}, - {file = "black-23.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d9e13db441c509a3763a7a3d9a49ccc1b4e974a47be4e08ade2a228876500ec"}, - {file = "black-23.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d1bd9c210f8b109b1762ec9fd36592fdd528485aadb3f5849b2740ef17e674e"}, - {file = "black-23.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:ae76c22bde5cbb6bfd211ec343ded2163bba7883c7bc77f6b756a1049436fbb9"}, - {file = "black-23.12.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1fa88a0f74e50e4487477bc0bb900c6781dbddfdfa32691e780bf854c3b4a47f"}, - {file = "black-23.12.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a4d6a9668e45ad99d2f8ec70d5c8c04ef4f32f648ef39048d010b0689832ec6d"}, - {file = "black-23.12.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b18fb2ae6c4bb63eebe5be6bd869ba2f14fd0259bda7d18a46b764d8fb86298a"}, - {file = "black-23.12.1-cp38-cp38-win_amd64.whl", hash = "sha256:c04b6d9d20e9c13f43eee8ea87d44156b8505ca8a3c878773f68b4e4812a421e"}, - {file = "black-23.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3e1b38b3135fd4c025c28c55ddfc236b05af657828a8a6abe5deec419a0b7055"}, - {file = "black-23.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4f0031eaa7b921db76decd73636ef3a12c942ed367d8c3841a0739412b260a54"}, - {file = "black-23.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97e56155c6b737854e60a9ab1c598ff2533d57e7506d97af5481141671abf3ea"}, - {file = "black-23.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:dd15245c8b68fe2b6bd0f32c1556509d11bb33aec9b5d0866dd8e2ed3dba09c2"}, - {file = "black-23.12.1-py3-none-any.whl", hash = "sha256:78baad24af0f033958cad29731e27363183e140962595def56423e626f4bee3e"}, - {file = "black-23.12.1.tar.gz", hash = "sha256:4ce3ef14ebe8d9509188014d96af1c456a910d5b5cbf434a09fef7e024b3d0d5"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - [[package]] name = "boto3" version = "1.34.69" @@ -526,20 +469,6 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] -[[package]] -name = "click" -version = "8.1.7" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.7" -files = [ - {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, - {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - [[package]] name = "colorama" version = "0.4.6" @@ -684,17 +613,6 @@ files = [ {file = "docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491"}, ] -[[package]] -name = "eradicate" -version = "2.3.0" -description = "Removes commented-out code." -optional = false -python-versions = "*" -files = [ - {file = "eradicate-2.3.0-py3-none-any.whl", hash = "sha256:2b29b3dd27171f209e4ddd8204b70c02f0682ae95eecb353f10e8d72b149c63e"}, - {file = "eradicate-2.3.0.tar.gz", hash = "sha256:06df115be3b87d0fc1c483db22a2ebb12bcf40585722810d809cc770f5031c37"}, -] - [[package]] name = "exceptiongroup" version = "1.2.2" @@ -735,157 +653,6 @@ docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2. testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"] typing = ["typing-extensions (>=4.12.2)"] -[[package]] -name = "flake8" -version = "7.1.1" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213"}, - {file = "flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.12.0,<2.13.0" -pyflakes = ">=3.2.0,<3.3.0" - -[[package]] -name = "flake8-bugbear" -version = "24.8.19" -description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -optional = false -python-versions = ">=3.8.1" -files = [ - {file = "flake8_bugbear-24.8.19-py3-none-any.whl", hash = "sha256:25bc3867f7338ee3b3e0916bf8b8a0b743f53a9a5175782ddc4325ed4f386b89"}, - {file = "flake8_bugbear-24.8.19.tar.gz", hash = "sha256:9b77627eceda28c51c27af94560a72b5b2c97c016651bdce45d8f56c180d2d32"}, -] - -[package.dependencies] -attrs = ">=19.2.0" -flake8 = ">=6.0.0" - -[package.extras] -dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"] - -[[package]] -name = "flake8-eradicate" -version = "1.5.0" -description = "Flake8 plugin to find commented out code" -optional = false -python-versions = ">=3.8,<4.0" -files = [ - {file = "flake8_eradicate-1.5.0-py3-none-any.whl", hash = "sha256:18acc922ad7de623f5247c7d5595da068525ec5437dd53b22ec2259b96ce9d22"}, - {file = "flake8_eradicate-1.5.0.tar.gz", hash = "sha256:aee636cb9ecb5594a7cd92d67ad73eb69909e5cc7bd81710cf9d00970f3983a6"}, -] - -[package.dependencies] -attrs = "*" -eradicate = ">=2.0,<3.0" -flake8 = ">5" - -[[package]] -name = "flake8-multiline-containers" -version = "0.0.19" -description = "Ensure a consistent format for multiline containers." -optional = false -python-versions = "*" -files = [ - {file = "flake8-multiline-containers-0.0.19.tar.gz", hash = "sha256:7c47527f1a2b0a991b876e58a2758e0ecc6b2d10a5fd4ee7740d042722f2f281"}, - {file = "flake8_multiline_containers-0.0.19-py3-none-any.whl", hash = "sha256:1b684da84b401f42f1ad36f0f90e24b7f49b2691458cb582911fb99d871bb0b2"}, -] - -[package.dependencies] -attrs = ">=19.3.0" -flake8 = ">=3.7.9" - -[[package]] -name = "flake8-pep3101" -version = "2.1.0" -description = "Checks for old string formatting" -optional = false -python-versions = ">=3.8" -files = [ - {file = "flake8_pep3101-2.1.0-py3-none-any.whl", hash = "sha256:2d2b8b997ccf0bf0df91532e861465dcfa32a6a306d1dc98f93889cae49f4231"}, - {file = "flake8_pep3101-2.1.0.tar.gz", hash = "sha256:1b84b61685f1e631f2f710e5d5ed3ca68b5fc45fb9402fa8ae1b8a9a058a3387"}, -] - -[package.dependencies] -flake8 = "*" - -[package.extras] -test = ["pytest"] - -[[package]] -name = "flake8-plugin-utils" -version = "1.3.3" -description = "The package provides base classes and utils for flake8 plugin writing" -optional = false -python-versions = ">=3.6,<4.0" -files = [ - {file = "flake8-plugin-utils-1.3.3.tar.gz", hash = "sha256:39f6f338d038b301c6fd344b06f2e81e382b68fa03c0560dff0d9b1791a11a2c"}, - {file = "flake8_plugin_utils-1.3.3-py3-none-any.whl", hash = "sha256:e4848c57d9d50f19100c2d75fa794b72df068666a9041b4b0409be923356a3ed"}, -] - -[[package]] -name = "flake8-print" -version = "5.0.0" -description = "print statement checker plugin for flake8" -optional = false -python-versions = ">=3.7" -files = [ - {file = "flake8-print-5.0.0.tar.gz", hash = "sha256:76915a2a389cc1c0879636c219eb909c38501d3a43cc8dae542081c9ba48bdf9"}, - {file = "flake8_print-5.0.0-py3-none-any.whl", hash = "sha256:84a1a6ea10d7056b804221ac5e62b1cee1aefc897ce16f2e5c42d3046068f5d8"}, -] - -[package.dependencies] -flake8 = ">=3.0" -pycodestyle = "*" - -[[package]] -name = "flake8-pytest-style" -version = "2.0.0" -description = "A flake8 plugin checking common style issues or inconsistencies with pytest-based tests." -optional = false -python-versions = "<4.0.0,>=3.8.1" -files = [ - {file = "flake8_pytest_style-2.0.0-py3-none-any.whl", hash = "sha256:abcb9f56f277954014b749e5a0937fae215be01a21852e9d05e7600c3de6aae5"}, - {file = "flake8_pytest_style-2.0.0.tar.gz", hash = "sha256:919c328cacd4bc4f873ea61ab4db0d8f2c32e0db09a3c73ab46b1de497556464"}, -] - -[package.dependencies] -flake8-plugin-utils = ">=1.3.2,<2.0.0" - -[[package]] -name = "flake8-quotes" -version = "3.4.0" -description = "Flake8 lint for quotes." -optional = false -python-versions = "*" -files = [ - {file = "flake8-quotes-3.4.0.tar.gz", hash = "sha256:aad8492fb710a2d3eabe68c5f86a1428de650c8484127e14c43d0504ba30276c"}, -] - -[package.dependencies] -flake8 = "*" -setuptools = "*" - -[[package]] -name = "flake8-simplify" -version = "0.21.0" -description = "flake8 plugin which checks for code that can be simplified" -optional = false -python-versions = ">=3.6.1" -files = [ - {file = "flake8_simplify-0.21.0-py3-none-any.whl", hash = "sha256:439391e762a9370b371208add0b5c5c40c3d25a98e1f5421d263215d08194183"}, - {file = "flake8_simplify-0.21.0.tar.gz", hash = "sha256:c95ff1dcc1de5949af47e0087cbf1164445881131b15bcd7a71252670f492f4d"}, -] - -[package.dependencies] -astor = ">=0.1" -flake8 = ">=3.7" - [[package]] name = "freezegun" version = "1.5.1" @@ -1025,20 +792,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jmespath" version = "1.0.1" @@ -1050,17 +803,6 @@ files = [ {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "multidict" version = "6.1.0" @@ -1165,17 +907,6 @@ files = [ [package.dependencies] typing-extensions = {version = ">=4.1.0", markers = "python_version < \"3.11\""} -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "nodeenv" version = "1.9.1" @@ -1198,17 +929,6 @@ files = [ {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, ] -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - [[package]] name = "platformdirs" version = "4.3.6" @@ -1258,28 +978,6 @@ nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" -[[package]] -name = "pycodestyle" -version = "2.12.1" -description = "Python style guide checker" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3"}, - {file = "pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521"}, -] - -[[package]] -name = "pyflakes" -version = "3.2.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"}, - {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, -] - [[package]] name = "pytest" version = "7.4.4" @@ -1499,6 +1197,33 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "ruff" +version = "0.8.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"}, + {file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"}, + {file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"}, + {file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"}, + {file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"}, + {file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"}, + {file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"}, + {file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"}, + {file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"}, +] + [[package]] name = "s3transfer" version = "0.10.2" @@ -1516,26 +1241,6 @@ botocore = ">=1.33.2,<2.0a.0" [package.extras] crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] -[[package]] -name = "setuptools" -version = "75.1.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "setuptools-75.1.0-py3-none-any.whl", hash = "sha256:35ab7fd3bcd95e6b7fd704e4a1539513edad446c097797f2985e0e4b960772f2"}, - {file = "setuptools-75.1.0.tar.gz", hash = "sha256:d59a21b17a275fb872a9c3dae73963160ae079f1049ed956880cd7c09b120538"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=2.6.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.11.*)", "pytest-mypy"] - [[package]] name = "six" version = "1.16.0" @@ -1809,4 +1514,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.8.1" -content-hash = "e99fca471ad0054b24c2b721d42df73fe7a98d17f823b1a89c59c5b93a270117" +content-hash = "4ed933cbe1ea015cf7f36e38f7aee70795fdea9f82afbabf93ba97ec1fc8b6de" diff --git a/pyproject.toml b/pyproject.toml index 1956c73..499e37f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,25 +17,15 @@ packaging = "23.2" asyncpg = "^0.29.0" [tool.poetry.group.dev.dependencies] -black = "^23.10.1" pytest = "^7.1.2" pytest-spec = "^3.2.0" pytest-mock = "^3.7.0" pytest-asyncio = "^0.18.3" pytest-env = "^0.6.2" pytest-freezer = "^0.4.8" -isort = "^5.10.1" pre-commit = "^2.19.0" coveralls = "^3.3.1" -flake8 = "^7" -flake8-bugbear = "*" -flake8-eradicate = "*" -flake8-multiline-containers = "*" -flake8-pep3101 = "*" -flake8-print = "*" -flake8-pytest-style = "*" -flake8-quotes = "*" -flake8-simplify = "*" +ruff = "^0.8.2" [tool.poetry.group.debug.dependencies] debugpy = "^1.6.6" @@ -52,12 +42,25 @@ env = """ 1CV8MGMT_SETTINGS_MODULE=tests.settings """ -[tool.isort] -profile = "black" - -[tool.black] +[tool.ruff] line-length = 120 -target-version = ['py311'] +indent-width = 4 +target-version = "py312" + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F", "B", "W", "Q"] +ignore = [] +fixable = ["ALL"] +unfixable = [] + +[tool.ruff.lint.per-file-ignores] +"surrogate/tests/test_surrogate.py" = ["F401"] + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" [build-system] requires = ["poetry-core>=1.0.0"] diff --git a/surrogate/__init__.py b/surrogate/__init__.py index 8a38b0f..afab712 100644 --- a/surrogate/__init__.py +++ b/surrogate/__init__.py @@ -80,7 +80,9 @@ def _create_module_stubs(self): # last module in our sequence # it should be loaded last_module = type( - self.elements[-1], (object,), {"__all__": [], "_importing_path": self._get_importing_path(self.elements)} + self.elements[-1], + (object,), + {"__all__": [], "_importing_path": self._get_importing_path(self.elements)}, ) modules = [last_module] @@ -92,7 +94,11 @@ def _create_module_stubs(self): # sequence for element in reversed(self.elements[:-1]): next_module = modules[-1] - module = type(element, (object,), {next_module.__name__: next_module, "__all__": [next_module.__name__]}) + module = type( + element, + (object,), + {next_module.__name__: next_module, "__all__": [next_module.__name__]}, + ) modules.append(module) self.modules = list(reversed(modules)) self.modules[0].__path__ = [] diff --git a/surrogate/tests/test_surrogate.py b/surrogate/tests/test_surrogate.py index 67ecddf..fc9b02f 100644 --- a/surrogate/tests/test_surrogate.py +++ b/surrogate/tests/test_surrogate.py @@ -31,7 +31,7 @@ def stubbed(): try: stubbed() except Exception as e: - raise Exception(f"Modules are not stubbed correctly: {e}") + raise Exception(f"Modules are not stubbed correctly: {e}") from None def test_context_manager(self): with surrogate("my"), surrogate("my.module.one"), surrogate("my.module.two"): diff --git a/tests/test_backup.py b/tests/test_backup.py index 93616e9..9dd2040 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -81,7 +81,10 @@ async def test_rotate_backups_calls_old_file_remover_for_replication_paths(mocke """ replication_paths = ["test/replication/path/01", "test/replication/path/02"] mocker.patch("conf.settings.BACKUP_REPLICATION", new_callable=PropertyMock(return_value=True)) - mocker.patch("conf.settings.BACKUP_REPLICATION_PATHS", new_callable=PropertyMock(return_value=replication_paths)) + mocker.patch( + "conf.settings.BACKUP_REPLICATION_PATHS", + new_callable=PropertyMock(return_value=replication_paths), + ) remove_old_mock = mocker.patch("core.utils.remove_old_files_by_pattern", return_value=AsyncMock()) await rotate_backups(infobase) assert remove_old_mock.await_count == len(replication_paths) + 1 # plus one for initial backup place @@ -103,7 +106,10 @@ async def test_backup_v8_makes_retries(mocker: MockerFixture, infobase, mock_get Backup with 1cv8 tools makes retries according to retry policy """ backup_retries = 1 - mocker.patch("conf.settings.BACKUP_RETRIES_V8", new_callable=PropertyMock(return_value=backup_retries)) + mocker.patch( + "conf.settings.BACKUP_RETRIES_V8", + new_callable=PropertyMock(return_value=backup_retries), + ) execute_v8_mock = mocker.patch("backup.execute_v8_command", side_effect=V8Exception) await _backup_v8(infobase) assert execute_v8_mock.await_count == backup_retries + 1 # plus one for initial call @@ -183,7 +189,10 @@ async def test_backup_v8_return_backup_result_succeeded_false_when_failed( @pytest.mark.asyncio async def test_backup_pgdump_calls_execute_subprocess_command( - mocker: MockerFixture, infobase, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16 + mocker: MockerFixture, + infobase, + mock_prepare_postgres_connection_vars, + mock_get_postgres_version_16, ): """ Backup with pgdump calls `execute_subprocess_command` @@ -205,7 +214,10 @@ async def test_backup_pgdump_makes_retries( Backup with pgdump makes retries according to retry policy """ backup_retries = 1 - mocker.patch("conf.settings.BACKUP_RETRIES_PG", new_callable=PropertyMock(return_value=backup_retries)) + mocker.patch( + "conf.settings.BACKUP_RETRIES_PG", + new_callable=PropertyMock(return_value=backup_retries), + ) execute_subprocess_mock = mocker.patch("backup.execute_subprocess_command", side_effect=SubprocessException) await _backup_pgdump(infobase, "", "", "") assert execute_subprocess_mock.await_count == backup_retries + 1 # plus one for initial call @@ -521,7 +533,15 @@ def test_analyze_results_calls_backup_analyze(mocker: MockerFixture, infobases, datetime_start = datetime.now() datetime_finish = datetime_start + timedelta(minutes=5) analyze_backup_result_mock = mocker.patch("backup.analyze_backup_result") - analyze_results(infobases, mixed_backup_result, datetime_start, datetime_finish, None, None, None) + analyze_results( + infobases, + mixed_backup_result, + datetime_start, + datetime_finish, + None, + None, + None, + ) analyze_backup_result_mock.assert_called_with(mixed_backup_result, infobases, datetime_start, datetime_finish) @@ -592,7 +612,10 @@ def test_send_email_notification_calls_inner_send_func(mocker: MockerFixture, mi """ `send_email_notification` calls inner util func for sending notification """ - mocker.patch("conf.settings.NOTIFY_EMAIL_ENABLED", new_callable=PropertyMock(return_value=True)) + mocker.patch( + "conf.settings.NOTIFY_EMAIL_ENABLED", + new_callable=PropertyMock(return_value=True), + ) mocker.patch("backup.make_html_table") send_notification_mock = mocker.patch("backup.send_notification") send_email_notification(mixed_backup_result, mixed_aws_result) @@ -603,7 +626,10 @@ def test_send_email_notification_makes_backup_table(mocker: MockerFixture, mixed """ `send_email_notification` calls `make_html_table` for backup results """ - mocker.patch("conf.settings.NOTIFY_EMAIL_ENABLED", new_callable=PropertyMock(return_value=True)) + mocker.patch( + "conf.settings.NOTIFY_EMAIL_ENABLED", + new_callable=PropertyMock(return_value=True), + ) mocker.patch("backup.send_notification") make_html_table_mock = mocker.patch("backup.make_html_table") send_email_notification(mixed_backup_result, mixed_aws_result) @@ -616,7 +642,10 @@ def test_send_email_notification_not_makes_aws_table_when_aws_disabled( """ `send_email_notification` calls `make_html_table` only for backup results when AWS_ENABLED is False """ - mocker.patch("conf.settings.NOTIFY_EMAIL_ENABLED", new_callable=PropertyMock(return_value=True)) + mocker.patch( + "conf.settings.NOTIFY_EMAIL_ENABLED", + new_callable=PropertyMock(return_value=True), + ) mocker.patch("backup.send_notification") make_html_table_mock = mocker.patch("backup.make_html_table") send_email_notification(mixed_backup_result, mixed_aws_result) @@ -629,7 +658,10 @@ def test_send_email_notification_makes_aws_table_when_aws_enabled( """ `send_email_notification` calls `make_html_table` both for aws and backup results """ - mocker.patch("conf.settings.NOTIFY_EMAIL_ENABLED", new_callable=PropertyMock(return_value=True)) + mocker.patch( + "conf.settings.NOTIFY_EMAIL_ENABLED", + new_callable=PropertyMock(return_value=True), + ) mocker.patch("conf.settings.AWS_ENABLED", new_callable=PropertyMock(return_value=True)) mocker.patch("backup.send_notification") make_html_table_mock = mocker.patch("backup.make_html_table") @@ -643,7 +675,10 @@ def test_send_email_notification_makes_aws_and_backup_tables_when_aws_enabled( """ `send_email_notification` calls `make_html_table` both for aws and backup results """ - mocker.patch("conf.settings.NOTIFY_EMAIL_ENABLED", new_callable=PropertyMock(return_value=True)) + mocker.patch( + "conf.settings.NOTIFY_EMAIL_ENABLED", + new_callable=PropertyMock(return_value=True), + ) mocker.patch("conf.settings.AWS_ENABLED", new_callable=PropertyMock(return_value=True)) mocker.patch("backup.send_notification") make_html_table_mock = mocker.patch("backup.make_html_table") @@ -696,7 +731,10 @@ def test_create_backup_replication_task_does_not_create_task_if_replication_not_ """ semaphore = asyncio.Semaphore(1) value = core_models.InfoBaseBackupTaskResult(infobase, True, "test/backup.path") - mocker.patch("conf.settings.BACKUP_REPLICATION", new_callable=PropertyMock(return_value=False)) + mocker.patch( + "conf.settings.BACKUP_REPLICATION", + new_callable=PropertyMock(return_value=False), + ) mock_create_task = mocker.patch("asyncio.create_task") create_backup_replication_task(value, semaphore) mock_create_task.assert_not_called() diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 56f02bc..04884ad 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -131,7 +131,9 @@ async def test_maintenance_vacuumdb_returns_maintenance_result_type_when_failed( @pytest.mark.asyncio -async def test_maintenance_vacuumdb_returns_maintenance_result_type_when_no_credentials(infobase): +async def test_maintenance_vacuumdb_returns_maintenance_result_type_when_no_credentials( + infobase, +): """ Maintenance with vacuumdb returns result of `InfoBaseMaintenanceTaskResult` type if no credentials found for db """ @@ -164,7 +166,9 @@ async def test_maintenance_vacuumdb_returns_result_for_exact_infobase_when_faile @pytest.mark.asyncio -async def test_maintenance_vacuumdb_returns_result_for_exact_infobase_when_no_credentials(infobase): +async def test_maintenance_vacuumdb_returns_result_for_exact_infobase_when_no_credentials( + infobase, +): """ Maintenance with vacuumdb returns result for exact infobase which was provided if no credentials found for db """ diff --git a/tests/test_update.py b/tests/test_update.py index 8e44d61..f7e1285 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -11,7 +11,9 @@ ) -def test_get_name_and_version_from_manifest_returns_name_from_manifest(mock_configuration_manifest): +def test_get_name_and_version_from_manifest_returns_name_from_manifest( + mock_configuration_manifest, +): """ Configuration name is extracted from manifest """ @@ -19,7 +21,9 @@ def test_get_name_and_version_from_manifest_returns_name_from_manifest(mock_conf assert result[0] == mock_configuration_manifest[0] -def test_get_name_and_version_from_manifest_returns_version_from_manifest(mock_configuration_manifest): +def test_get_name_and_version_from_manifest_returns_version_from_manifest( + mock_configuration_manifest, +): """ Configuration version is extracted from manifest """ @@ -27,7 +31,9 @@ def test_get_name_and_version_from_manifest_returns_version_from_manifest(mock_c assert result[1] == mock_configuration_manifest[1] -def test_get_updatable_versions_returns_versions_from_updinfo(mock_configuration_manifest_updinfo): +def test_get_updatable_versions_returns_versions_from_updinfo( + mock_configuration_manifest_updinfo, +): """ Updatable versions are extracted from updinfo """ @@ -36,45 +42,78 @@ def test_get_updatable_versions_returns_versions_from_updinfo(mock_configuration def test_find_suitable_manifests_returns_updated_when_exists( - mocker: MockerFixture, mock_configuration_metadata, mock_configuration_manifest, mock_configuration_manifest_updinfo + mocker: MockerFixture, + mock_configuration_metadata, + mock_configuration_manifest, + mock_configuration_manifest_updinfo, ): """ Applicable update manifest is found """ - mocker.patch("update.get_name_and_version_from_manifest", return_value=mock_configuration_manifest) - mocker.patch("update.get_updatable_versions", return_value=mock_configuration_manifest_updinfo) + mocker.patch( + "update.get_name_and_version_from_manifest", + return_value=mock_configuration_manifest, + ) + mocker.patch( + "update.get_updatable_versions", + return_value=mock_configuration_manifest_updinfo, + ) result = _find_suitable_manifests( - ["manifest_test_path/1/1cv8.mft"], mock_configuration_metadata[0], mock_configuration_metadata[1] + ["manifest_test_path/1/1cv8.mft"], + mock_configuration_metadata[0], + mock_configuration_metadata[1], ) assert len(result) == 1 def test_find_suitable_manifests_returns_correct_manifest_filename( - mocker: MockerFixture, mock_configuration_metadata, mock_configuration_manifest, mock_configuration_manifest_updinfo + mocker: MockerFixture, + mock_configuration_metadata, + mock_configuration_manifest, + mock_configuration_manifest_updinfo, ): """ Applicable update manifest filename is correct """ manifest_filename = "manifest_test_path/1/1cv8.mft" - mocker.patch("update.get_name_and_version_from_manifest", return_value=mock_configuration_manifest) - mocker.patch("update.get_updatable_versions", return_value=mock_configuration_manifest_updinfo) + mocker.patch( + "update.get_name_and_version_from_manifest", + return_value=mock_configuration_manifest, + ) + mocker.patch( + "update.get_updatable_versions", + return_value=mock_configuration_manifest_updinfo, + ) result = _find_suitable_manifests( - [manifest_filename], mock_configuration_metadata[0], mock_configuration_metadata[1] + [manifest_filename], + mock_configuration_metadata[0], + mock_configuration_metadata[1], ) assert result[0][0] == manifest_filename def test_find_suitable_manifests_returns_correct_manifest_version( - mocker: MockerFixture, mock_configuration_metadata, mock_configuration_manifest, mock_configuration_manifest_updinfo + mocker: MockerFixture, + mock_configuration_metadata, + mock_configuration_manifest, + mock_configuration_manifest_updinfo, ): """ Applicable update manifest version is correct """ manifest_filename = "manifest_test_path/1/1cv8.mft" - mocker.patch("update.get_name_and_version_from_manifest", return_value=mock_configuration_manifest) - mocker.patch("update.get_updatable_versions", return_value=mock_configuration_manifest_updinfo) + mocker.patch( + "update.get_name_and_version_from_manifest", + return_value=mock_configuration_manifest, + ) + mocker.patch( + "update.get_updatable_versions", + return_value=mock_configuration_manifest_updinfo, + ) result = _find_suitable_manifests( - [manifest_filename], mock_configuration_metadata[0], mock_configuration_metadata[1] + [manifest_filename], + mock_configuration_metadata[0], + mock_configuration_metadata[1], ) assert result[0][1] == mock_configuration_manifest[1] @@ -89,10 +128,18 @@ def test_find_suitable_manifests_returns_empty_list_when_no_manifests_are_applic `_find_suitable_manifests` returns empty list when no manifests are applicable """ manifest_filename = "manifest_test_path/1/1cv8.mft" - mocker.patch("update.get_name_and_version_from_manifest", return_value=mock_configuration_manifest_new) - mocker.patch("update.get_updatable_versions", return_value=mock_configuration_manifest_updinfo_new) + mocker.patch( + "update.get_name_and_version_from_manifest", + return_value=mock_configuration_manifest_new, + ) + mocker.patch( + "update.get_updatable_versions", + return_value=mock_configuration_manifest_updinfo_new, + ) result = _find_suitable_manifests( - [manifest_filename], mock_configuration_metadata[0], mock_configuration_metadata[1] + [manifest_filename], + mock_configuration_metadata[0], + mock_configuration_metadata[1], ) assert len(result) == 0 diff --git a/update.py b/update.py index cf40c5b..fc88cd1 100644 --- a/update.py +++ b/update.py @@ -179,7 +179,13 @@ async def _update_info_base(ib_name, dry=False): # Процесс не может получить доступ к файлу, так как этот файл занят другим процессом. pause = (random.randint(0, 100_000)) / 10_000 # Обновляет информационную базу и конфигурацию БД - await execute_v8_command(ib_name, v8_command, log_filename, permission_code, create_subprocess_pause=pause) + await execute_v8_command( + ib_name, + v8_command, + log_filename, + permission_code, + create_subprocess_pause=pause, + ) if is_multiupdate: # Если в цепочке несколько обновлений, то после каждого проверяет версию ИБ, # и продолжает только в случае, если ИБ обновилась. diff --git a/utils/common.py b/utils/common.py index 7a4ccc4..879d4e5 100644 --- a/utils/common.py +++ b/utils/common.py @@ -3,4 +3,4 @@ def sizeof_fmt(num, suffix="B", radix=1024.0, radix_suffix="i"): if abs(num) < radix: return f"{num:3.1f}{unit}{suffix}" num /= radix - return f'{num:.1f}{"Y" + radix_suffix}{suffix}' + return f"{num:.1f}{'Y' + radix_suffix}{suffix}" diff --git a/utils/notification.py b/utils/notification.py index 43e4834..484a225 100644 --- a/utils/notification.py +++ b/utils/notification.py @@ -44,7 +44,9 @@ def make_html_table(caption: str, resultset: List[core_models.InfoBaseTaskResult def send_notification(caption, html_body): with smtplib.SMTP( - settings.NOTIFY_EMAIL_SMTP_HOST, settings.NOTIFY_EMAIL_SMTP_PORT, timeout=settings.NOTIFY_EMAIL_CONNECT_TIMEOUT + settings.NOTIFY_EMAIL_SMTP_HOST, + settings.NOTIFY_EMAIL_SMTP_PORT, + timeout=settings.NOTIFY_EMAIL_CONNECT_TIMEOUT, ) as session: if settings.NOTIFY_EMAIL_SMTP_SSL_REQUIRED: session.starttls() diff --git a/utils/postgres.py b/utils/postgres.py index f13eba0..9ce79db 100644 --- a/utils/postgres.py +++ b/utils/postgres.py @@ -26,8 +26,8 @@ def prepare_postgres_connection_vars(db_server: str, db_user: str) -> Tuple[str, db_host, db_port = get_postgres_host_and_port(db_server) try: db_pwd = settings.PG_CREDENTIALS[db_user_string] - except KeyError: - raise KeyError(f"{POSTGRES_NAME} password not found for user {db_user_string}") + except KeyError as e: + raise KeyError(f"{POSTGRES_NAME} password not found for user {db_user_string}") from e return db_host, db_port, db_pwd diff --git a/utils/tests/conftest.py b/utils/tests/conftest.py index 287fbd0..e072b4c 100644 --- a/utils/tests/conftest.py +++ b/utils/tests/conftest.py @@ -25,4 +25,7 @@ def mock_smtp_sendmail(mock_smtp): @pytest.fixture def mock_email_message(mocker: MockerFixture): - return mocker.patch("email.mime.multipart.MIMEMultipart.as_string", return_value="test_email_message_string") + return mocker.patch( + "email.mime.multipart.MIMEMultipart.as_string", + return_value="test_email_message_string", + ) diff --git a/utils/tests/test_notification.py b/utils/tests/test_notification.py index d634512..37f5a66 100644 --- a/utils/tests/test_notification.py +++ b/utils/tests/test_notification.py @@ -78,7 +78,9 @@ def test_send_notification_calls_smtp(mock_smtp, mock_smtp_login, mock_smtp_send """ send_notification("", "") mock_smtp.assert_called_with( - settings.NOTIFY_EMAIL_SMTP_HOST, settings.NOTIFY_EMAIL_SMTP_PORT, timeout=settings.NOTIFY_EMAIL_CONNECT_TIMEOUT + settings.NOTIFY_EMAIL_SMTP_HOST, + settings.NOTIFY_EMAIL_SMTP_PORT, + timeout=settings.NOTIFY_EMAIL_CONNECT_TIMEOUT, ) From b9d50e3b54b3193ca23b5b977e69ac35b488982a Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sat, 7 Dec 2024 23:38:58 +0300 Subject: [PATCH 04/20] Add debug configurations for vscode --- .vscode/launch.json | 29 +++++++++++++++++++++++++++++ .vscode/tasks.json | 18 ++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..e3d472c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,29 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug: Remote Attach", + "type": "debugpy", + "request": "attach", + "connect": { + "host": "localhost", + "port": 5678 + }, + "pathMappings": [ + { + "localRoot": "${workspaceFolder}", + "remoteRoot": "." + } + ] + }, + { + "name": "Debug: pytest", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": [ + "--spec" + ] + } + ] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..5d66b1e --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Run `docker compose up` with debug config", + "type": "docker-compose", + "dockerCompose": { + "up": { + "detached": false, + "build": false + }, + "files": [ + "${workspaceFolder}/docker-compose.debug.yml" + ] + } + } + ] +} From 98df6ae47b3b457d386ecc71e42ed1bee3691815 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 20:33:58 +0300 Subject: [PATCH 05/20] Improve postgres dockerfile --- Dockerfile-postgres | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile-postgres b/Dockerfile-postgres index 29f112f..a491083 100644 --- a/Dockerfile-postgres +++ b/Dockerfile-postgres @@ -21,7 +21,7 @@ RUN apt-get update \ ENV LANG ru_RU.UTF-8 RUN wget --quiet -O - http://repo.postgrespro.ru/keys/GPG-KEY-POSTGRESPRO | apt-key add - \ - && echo 'deb http://repo.postgrespro.ru/1c/1c-'${PG_VERSION}'/debian bookworm main' > /etc/apt/sources.list.d/postgrespro-1c.list \ + && echo 'deb http://repo.postgrespro.ru/1c/1c-'${PG_VERSION}'/debian '$(env -i bash -c '. /etc/os-release; echo $VERSION_CODENAME')' main' > /etc/apt/sources.list.d/postgrespro-1c.list \ && apt-get update \ && apt-get install -y postgrespro-1c-${PG_VERSION} \ && rm -rf /var/lib/apt/lists/* From e488aa3261dc305b6950fc602a700780768748a6 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 20:35:57 +0300 Subject: [PATCH 06/20] ras connections settings for tests --- tests/settings.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/settings.py b/tests/settings.py index d79215a..2ec8042 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,3 +1,8 @@ AWS_RETRY_PAUSE = 0 V8_LOCK_INFO_BASE_PAUSE = 0 + +V8_RAS = { + "address": "ras", + "port": "1545", +} From 249862dd4ebf0d6334779b423442b26f7c15df32 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 20:36:59 +0300 Subject: [PATCH 07/20] Declare some data models to use with cluster controller classes --- core/cluster/models.py | 90 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 core/cluster/models.py diff --git a/core/cluster/models.py b/core/cluster/models.py new file mode 100644 index 0000000..e745d65 --- /dev/null +++ b/core/cluster/models.py @@ -0,0 +1,90 @@ +from abc import ABC, abstractmethod +from typing import List + + +class V8CModel(ABC): + @property + @abstractmethod + def keys(self) -> List[str]: ... + + @property + @abstractmethod + def id(self) -> str: ... + + def __init__(self, **kwargs): + self.__dict__.update((k, v) for k, v in kwargs.items() if k in self.keys) + + +class V8CCluster(V8CModel): + @property + def id(self): + return self.cluster + + @property + def keys(self): + return [ + "cluster", + "host", + "port", + "name", + "expiration_timeout", + "lifetime_limit", + "max_memory_size", + "max_memory_time_limit", + "security_level", + "session_fault_tolerance_level", + "load_balancing_mode", + "errors_count_threshold", + "kill_problem_processes", + "kill_by_memory_with_dump", + ] + + +class V8CInfobaseShort(V8CModel): + @property + def id(self): + return self.infobase + + @property + def keys(self): + return [ + "infobase", + "name", + "descr", + ] + + +class V8CInfobase(V8CModel): + @property + def id(self): + return self.infobase + + @property + def keys(self): + return [ + "infobase", + "name", + "dbms", + "db_server", + "db_name", + "db_user", + "security_level", + "license_distribution", + "scheduled_jobs_deny", + "sessions_deny", + "denied_from", + "denied_message", + "denied_parameter", + "denied_to", + "permission_code", + "external_session_manager_connection_string", + "external_session_manager_required", + "security_profile_name", + "safe_mode_security_profile_name", + "reserve_working_processes", + "descr", + "disable_local_speech_to_text", + "configuration_unload_delay_by_working_process_without_active_users", + "minimum_scheduled_jobs_start_period_without_active_users", + "maximum_scheduled_jobs_start_shift_without_active_users", + ] From 4d58f0f8c50d2bed96e81f1b5c038eb7fac4152f Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 20:40:03 +0300 Subject: [PATCH 08/20] RAC controller now is able to retrieve cluster and infobase data --- core/cluster/rac.py | 505 +++++++++----------------------------------- 1 file changed, 98 insertions(+), 407 deletions(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index d04af74..e77adde 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -1,16 +1,94 @@ +import subprocess import logging +from typing import List + +from conf import settings +from core.exceptions import RACException from core.cluster.abc import ClusterControler +from core.cluster.models import V8CModel, V8CCluster, V8CInfobaseShort, V8CInfobase log = logging.getLogger(__name__) class ClusterRACControler(ClusterControler): + def __init__(self): + self.ras_host = settings.V8_RAS["address"] + self.ras_port = settings.V8_RAS["port"] + self.cluster_admin_name = settings.V8_CLUSTER_ADMIN_CREDENTIALS[0] + self.cluster_admin_pwd = settings.V8_CLUSTER_ADMIN_CREDENTIALS[1] + self.infobases_credentials = settings.V8_INFOBASES_CREDENTIALS + + def _get_rac_exec_path(self): + # TODO: поддержка windows/linux + # TODO: поддержка сценариев, когда утилита не включена в PATH + return "rac" + + def _rac_output_to_objects(self, output: str, obj_class) -> List[V8CModel]: + objects = [] + lines = [line for line in output.splitlines()] + kw = dict() + for line in lines: + if not line.strip(): + objects.append(obj_class(**kw)) + kw.clear() + continue + key, value = line.split(":") + kw.setdefault(key.replace("-", "_").strip(), value.strip('"').strip()) + return objects + + def _rac_output_to_object(self, output: str, obj_class) -> V8CModel: + return self._rac_output_to_objects(output, obj_class)[0] + + def _rac_call(self, command: str) -> str: + call_str = f"{self._get_rac_exec_path()} {self.ras_host}:{self.ras_port} {command} ; exit 0" + log.debug(f"Created rac command [{call_str}]") + try: + out = subprocess.check_output(call_str, stderr=subprocess.STDOUT, shell=True) + out = out.decode("utf-8") + except subprocess.CalledProcessError as e: + raise RACException() from e + return out + + def _get_clusters(self) -> List[V8CCluster]: + cmd = "cluster list" + output = self._rac_call(cmd) + return self._rac_output_to_objects(output, V8CCluster) + + def _get_cluster(self, name=None) -> V8CCluster: + return self._get_clusters()[0] + + def _with_cluster_auth(self) -> str: + cluster = self._get_cluster() + auth = f"--cluster={cluster.id}" + if self.cluster_admin_name: + auth += f" --cluster-user={self.cluster_admin_name}" + if self.cluster_admin_pwd: + auth += f" --cluster-pwd={self.cluster_admin_pwd}" + return auth + + def _with_infobase_auth(self, infobase: V8CInfobaseShort): + auth = f"--infobase={infobase.id}" + default = self.infobases_credentials.get("default") + creds = self.infobases_credentials.get(infobase.name, default) + if creds[0]: + auth += f" --infobase-user={creds[0]}" + if creds[1]: + auth += f" --infobase-pwd={creds[1]}" + return auth + + def _filter_infobase(self, infobases: List[V8CInfobaseShort], name: str): + for ib in infobases: + if ib.name.lower() == name.lower(): + return ib + def get_cluster_info_bases(self): """ Получает список всех ИБ из кластера """ - ... + cmd = f"infobase summary list {self._with_cluster_auth()} " + output = self._rac_call(cmd) + return self._rac_output_to_objects(output, V8CInfobaseShort) def lock_info_base(self, infobase: str, permission_code: str, message: str): """ @@ -35,250 +113,33 @@ def terminate_info_base_sessions(self, infobase: str): """ ... - def get_info_base(self, infobase: str): + def get_info_base(self, infobase: str) -> V8CInfobase: """ Получает сведения об ИБ из кластера :param infobase: имя информационной базы """ - ... - - -""" -Use: - - rac cluster [command] [options] [arguments] - -Shared options: - - --version | -v - get the utility version - - --help | -h | -? - display brief utility description - -Shared arguments: - - [:] - administration server address (default: localhost:1545) - -Mode: - - cluster - Server cluster administration mode - -Commands: - - admin - management of cluster administrators - - Additional commands: - list - receipt of the cluster administrator list - - register - adding a new cluster administrator - - --name= - (required) administrator name - - --pwd= - administrator password in case of password authentication - - --descr= - description of the administrator - - --auth=pwd[,os] - available authentication methods: - pwd - using the user name and password - os - authentication using OS - - --os-user= - OS user name - - --agent-user= - name of the cluster agent administrator - - --agent-pwd= - password of the cluster agent administrator - - remove - deleting the cluster administrator - - --name= - (required) name of the cluster administrator - - --cluster= - (required) server cluster identifier - - --cluster-user= - name of the cluster administrator - - --cluster-pwd= - password of the cluster administrator - - info - receipt of cluster information - - --cluster= - (required) server cluster identifier - - list - receipt of the cluster information list - - insert - new cluster registration - - --host= - (required) name (or IP-address) of the computer where - the cluster registry and the main cluster manager process are located - - --port= - (required) main port of the main manager - - --name= - cluster name (presentation) - - --expiration-timeout= - forced termination time (seconds) - - --lifetime-limit= - restart time of cluster working processes (seconds) - - --max-memory-size= - maximum virtual address space (KB), - used by the working process - - --max-memory-time-limit= - maximum period of exceeding critical memory limit (seconds) - - --security-level= - connection security level - - --session-fault-tolerance-level= - fault-tolerance level - - --load-balancing-mode=performance|memory - load balancing mode - performance - priority by available performance - memory - priority by available memory - - --errors-count-threshold= - server errors threshold (percentage) - - --kill-problem-processes= - terminate corrupted processes - - --kill-by-memory-with-dump= - create process dump when maximum memory amount is exceeded - - --agent-user= - name of the cluster agent administrator - - --agent-pwd= - password of the cluster agent administrator - - update - cluster parameter update - - --cluster= - (required) server cluster identifier - - --name= - cluster name (presentation) - - --expiration-timeout= - forced termination time (seconds) - - --lifetime-limit= - restart time of cluster working processes (seconds) - - --max-memory-size= - maximum virtual address space (KB), - used by the working process - - --max-memory-time-limit= - maximum period of exceeding critical memory limit (seconds) - - --security-level= - connection security level - - --session-fault-tolerance-level= - fault-tolerance level - - --load-balancing-mode=performance|memory - load balancing mode - performance - priority by available performance - memory - priority by available memory - - --errors-count-threshold= - server errors threshold (percentage) - - --kill-problem-processes= - terminate corrupted processes - - --kill-by-memory-with-dump= - create process dump when maximum memory amount is exceeded + ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + cmd = f"infobase info {self._with_cluster_auth()} {self._with_infobase_auth(ib)}" + output = self._rac_call(cmd) + return self._rac_output_to_object(output, V8CInfobase) - --agent-user= - name of the cluster agent administrator - --agent-pwd= - password of the cluster agent administrator - - remove - deleting the cluster - - --cluster= - (required) server cluster identifier - - --cluster-user= - name of the cluster administrator - - --cluster-pwd= - password of the cluster administrator - - -./rac cluster admin list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 -name : Администратор -auth : pwd -os-user : -descr : - -./rac cluster info --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 -cluster : b930e651-0160-47c6-aeae-68b8ed937120 -host : ragent -port : 1541 -name : "Local cluster" -expiration-timeout : 60 -lifetime-limit : 0 -max-memory-size : 0 -max-memory-time-limit : 0 -security-level : 0 -session-fault-tolerance-level : 0 -load-balancing-mode : performance -errors-count-threshold : 0 -kill-problem-processes : 1 -kill-by-memory-with-dump : 0 - -./rac cluster list ras:1545 -cluster : b930e651-0160-47c6-aeae-68b8ed937120 -host : ragent -port : 1541 -name : "Local cluster" -expiration-timeout : 60 -lifetime-limit : 0 -max-memory-size : 0 -max-memory-time-limit : 0 -security-level : 0 -session-fault-tolerance-level : 0 -load-balancing-mode : performance -errors-count-threshold : 0 -kill-problem-processes : 1 -kill-by-memory-with-dump : 0 -""" """ -./rac infobase summary list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 +rac ras:1545 cluster admin list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 cluster info --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 infobase summary list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 infobase create --create-database --name=infobase01 --dbms=MSSQLServer --db-server=db --db-name=infobase01 --locale=ru --db-user=sa --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 infobase create --create-database --name=infobase01 --dbms=PostgreSQL --db-server=db --db-name=infobase01 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 infobase create --create-database --name=infobase02 --dbms=PostgreSQL --db-server=db --db-name=infobase02 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +rac ras:1545 infobase summary list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 +--- +infobase : ecc1909f-4807-4423-becf-f81cf3b96cde +name : infobase01 +descr : +infobase : 4e242939-180b-47a9-afa3-bdab8ece7f10 +name : infobase02 +descr : Use: @@ -348,176 +209,6 @@ def get_info_base(self, infobase: str): --descr= infobase description - - create - infobase creation - - --create-database - Create database when creating infobase - - --name= - (required) name of infobase - - --dbms=MSSQLServer|PostgreSQL|IBMDB2|OracleDatabase - (required) type of the Database Management System where the infobase is located: - MSSQLServer - MS SQL Server - PostgreSQL - PostgreSQL - IBMDB2 - IBM DB2 - OracleDatabase - Oracle Database - - --db-server= - (required) the name of the database server - - --db-name= - (required) database name - - --locale= - (required) identifier of national settings of the infobase - - --db-user= - database administrator name - - --db-pwd= - database administrator password - - --descr= - infobase description - - --date-offset= - date offset in the infobase - - --security-level= - infobase connection security level - - --scheduled-jobs-deny=on|off - scheduled job lock management - on - scheduled job execution prohibited - off - scheduled job execution permitted - - --license-distribution=deny|allow - management of licenses granting by 1C:Enterprise server - deny - licensing is forbidden - allow - licensing is allowed - - update - updating information on infobase - - --infobase= - (required) infobase identifier - - --infobase-user= - name of the infobase administrator - - --infobase-pwd= - password of the infobase administrator - - --dbms=MSSQLServer|PostgreSQL|IBMDB2|OracleDatabase - type of the Database Management System where the infobase is located: - MSSQLServer - MS SQL Server - PostgreSQL - PostgreSQL - IBMDB2 - IBM DB2 - OracleDatabase - Oracle Database - - --db-server= - the name of the database server - - --db-name= - database name - - --db-user= - database administrator name - - --db-pwd= - database administrator password - - --descr= - infobase description - - --denied-from= - start of the time interval within which the session lock mode is enabled - - --denied-message= - message displayed upon session lock violation - - --denied-parameter= - session lock parameter - - --denied-to= - end of the time interval within which the session lock mode is enabled - - --permission-code= - access code that allows the session to start in spite of enabled session lock - - --sessions-deny=on|off - session lock mode management - on - mode of session start lock enabled - off - mode of session start lock disabled - - --scheduled-jobs-deny=on|off - scheduled job lock management - on - scheduled job execution prohibited - off - scheduled job execution permitted - - --license-distribution=deny|allow - management of licenses granting by 1C:Enterprise server - deny - licensing is forbidden - allow - licensing is allowed - - --external-session-manager-connection-string= - external session management parameter - - --external-session-manager-required=yes|no - external session management required - yes - external session management is a must - no - external session management is optional - - --reserve-working-processes=yes|no - Workflow backup - yes - Workflow backup is enabled - no - Workflow backup is disabled - - --security-profile-name= - infobase security profile - - --safe-mode-security-profile-name= - external code security profile - - drop - remote infobase mode - - --infobase= - (required) infobase identifier - - --infobase-user= - name of the infobase administrator - - --infobase-pwd= - password of the infobase administrator - - --drop-database - delete database upon deleting infobase - - --clear-database - clear database upon deleting infobase - - -./rac infobase create --create-database --name=infobase01 --dbms=MSSQLServer --db-server=db --db-name=infobase01 --locale=ru --db-user=sa --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 -./rac infobase create --create-database --name=infobase01 --dbms=PostgreSQL --db-server=db --db-name=infobase01 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 -./rac infobase create --create-database --name=infobase02 --dbms=PostgreSQL --db-server=db --db-name=infobase02 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 ---- -infobase : 9046c0db-1939-42a7-9b2d-f0370ca950df ---- -./rac infobase summary list --cluster-user=Администратор --cluster=b930e651-0160-47c6-aeae-68b8ed937120 ras:1545 ---- -server_addr=tcp://ragent:1540 descr=32(0x00000020): Broken pipe line=1470 file=src/rtrsrvc/src/DataExchangeTcpClientImpl.cpp ---- -infobase : 01c55c12-0f4c-4101-8113-7707d450e83c -name : infobase01 -descr : - -infobase : 9046c0db-1939-42a7-9b2d-f0370ca950df -name : infobase02 -descr : """ """ From 085c93faf0f3ccfcdc3a67c325b5c5df26a85574 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 20:58:05 +0300 Subject: [PATCH 09/20] fix bug with parsing rac output values --- core/cluster/rac.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index e77adde..6646347 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -34,7 +34,7 @@ def _rac_output_to_objects(self, output: str, obj_class) -> List[V8CModel]: kw.clear() continue key, value = line.split(":") - kw.setdefault(key.replace("-", "_").strip(), value.strip('"').strip()) + kw.setdefault(key.replace("-", "_").strip(), value.strip().strip('"')) return objects def _rac_output_to_object(self, output: str, obj_class) -> V8CModel: From 31cb0aded3e4159201ab22772321f533e468d2a0 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 21:01:41 +0300 Subject: [PATCH 10/20] RAC controller now can lock & unlock infobase --- core/cluster/rac.py | 79 ++++----------------------------------------- 1 file changed, 6 insertions(+), 73 deletions(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index 6646347..0cb47d9 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -97,14 +97,18 @@ def lock_info_base(self, infobase: str, permission_code: str, message: str): :param permission_code: Код доступа к информационной базе во время блокировки сеансов :param message: Сообщение будет выводиться при попытке установить сеанс с ИБ """ - ... + ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + cmd = f"infobase update {self._with_cluster_auth()} {self._with_infobase_auth(ib)} --sessions-deny=on --scheduled-jobs-deny=on --permission-code={permission_code} --denied-message={message}" + self._rac_call(cmd) def unlock_info_base(self, infobase: str): """ Снимает блокировку фоновых заданий и сеансов информационной базы :param infobase: имя информационной базы """ - ... + ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + cmd = f"infobase update {self._with_cluster_auth()} {self._with_infobase_auth(ib)} --sessions-deny=off --scheduled-jobs-deny=off" + self._rac_call(cmd) def terminate_info_base_sessions(self, infobase: str): """ @@ -141,77 +145,6 @@ def get_info_base(self, infobase: str) -> V8CInfobase: name : infobase02 descr : -Use: - - rac infobase [command] [options] [arguments] - -Shared options: - - --version | -v - get the utility version - - --help | -h | -? - display brief utility description - -Shared arguments: - - [:] - administration server address (default: localhost:1545) - -Mode: - - infobase - Infobase administration mode - -Parameters: - - --cluster= - (required) server cluster identifier - - --cluster-user= - name of the cluster administrator - - --cluster-pwd= - password of the cluster administrator - -Commands: - - info - receiving the information about the infobase - - --infobase= - (required) infobase identifier - - --infobase-user= - name of the infobase administrator - - --infobase-pwd= - password of the infobase administrator - - summary - management of brief information on infobases - - Additional commands: - info - receiving brief information on the infobase - - --infobase= - (required) infobase identifier - - list - receiving the list of brief information on infobases - - update - updating brief information on the infobase - - --infobase= - (required) infobase identifier - - --descr= - infobase description -""" - -""" Use: rac session [command] [options] [arguments] From b036de55a49f1a1468cdb1a57b6ea8ca6ba66879 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 21:32:07 +0300 Subject: [PATCH 11/20] V8CInfobase can act as V8CInfobaseShort --- core/cluster/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/cluster/models.py b/core/cluster/models.py index e745d65..9a89900 100644 --- a/core/cluster/models.py +++ b/core/cluster/models.py @@ -54,7 +54,7 @@ def keys(self): ] -class V8CInfobase(V8CModel): +class V8CInfobase(V8CInfobaseShort): @property def id(self): return self.infobase From d97f2a2f003e2f0c4c9f2bbd4aae78c92e51f957 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 21:32:22 +0300 Subject: [PATCH 12/20] Add V8CSession model --- core/cluster/models.py | 60 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/core/cluster/models.py b/core/cluster/models.py index 9a89900..991625a 100644 --- a/core/cluster/models.py +++ b/core/cluster/models.py @@ -88,3 +88,63 @@ def keys(self): "minimum_scheduled_jobs_start_period_without_active_users", "maximum_scheduled_jobs_start_shift_without_active_users", ] + + +class V8CSession(V8CModel): + @property + def id(self): + return self.session + + @property + def keys(self): + return [ + "session", + "session_id", + "infobase", + "connection", + "process", + "user_name", + "host", + "app_id", + "locale", + "started_at", + "last_active_at", + "hibernate", + "passive_session_hibernate_time", + "hibernate_session_terminate_time", + "blocked_by_dbms", + "blocked_by_ls", + "bytes_all", + "bytes_last_5min", + "calls_all", + "calls_last_5min", + "dbms_bytes_all", + "dbms_bytes_last_5min", + "db_proc_info", + "db_proc_took", + "db_proc_took_at", + "duration_all", + "duration_all_dbms", + "duration_current", + "duration_current_dbms", + "duration_last_5min", + "duration_last_5min_dbms", + "memory_current", + "memory_last_5min", + "memory_total", + "read_current", + "read_last_5min", + "read_total", + "write_current", + "write_last_5min", + "write_total", + "duration_current_service", + "duration_last_5min_service", + "duration_all_service", + "current_service_name", + "cpu_time_current", + "cpu_time_last_5min", + "cpu_time_total", + "data_separation", + "client_ip", + ] From 4a6b8977466483d53f92d875edfa223b121ba7fa Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 21:32:50 +0300 Subject: [PATCH 13/20] RAC controller finished --- core/cluster/rac.py | 121 +++++++++----------------------------------- 1 file changed, 23 insertions(+), 98 deletions(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index 0cb47d9..fa97917 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -1,12 +1,12 @@ import subprocess import logging -from typing import List +from typing import List, Type from conf import settings from core.exceptions import RACException from core.cluster.abc import ClusterControler -from core.cluster.models import V8CModel, V8CCluster, V8CInfobaseShort, V8CInfobase +from core.cluster.models import V8CModel, V8CCluster, V8CInfobaseShort, V8CInfobase, V8CSession log = logging.getLogger(__name__) @@ -24,7 +24,7 @@ def _get_rac_exec_path(self): # TODO: поддержка сценариев, когда утилита не включена в PATH return "rac" - def _rac_output_to_objects(self, output: str, obj_class) -> List[V8CModel]: + def _rac_output_to_objects(self, output: str, obj_class: Type[V8CModel]) -> List[V8CModel]: objects = [] lines = [line for line in output.splitlines()] kw = dict() @@ -37,7 +37,7 @@ def _rac_output_to_objects(self, output: str, obj_class) -> List[V8CModel]: kw.setdefault(key.replace("-", "_").strip(), value.strip().strip('"')) return objects - def _rac_output_to_object(self, output: str, obj_class) -> V8CModel: + def _rac_output_to_object(self, output: str, obj_class: Type[V8CModel]) -> V8CModel: return self._rac_output_to_objects(output, obj_class)[0] def _rac_call(self, command: str) -> str: @@ -82,6 +82,18 @@ def _filter_infobase(self, infobases: List[V8CInfobaseShort], name: str): if ib.name.lower() == name.lower(): return ib + def _get_infobase_short(self, infobase_name: str) -> V8CInfobaseShort: + return self._filter_infobase(self.get_cluster_info_bases(), infobase_name) + + def _get_infobase_sessions(self, infobase: V8CInfobaseShort) -> List[V8CSession]: + cmd = f"session list {self._with_cluster_auth()} --infobase={infobase.id}" + output = self._rac_call(cmd) + return self._rac_output_to_objects(output, V8CSession) + + def _terminate_session(self, session: V8CSession): + cmd = f"session terminate {self._with_cluster_auth()} --session={session.id}" + self._rac_call(cmd) + def get_cluster_info_bases(self): """ Получает список всех ИБ из кластера @@ -97,7 +109,7 @@ def lock_info_base(self, infobase: str, permission_code: str, message: str): :param permission_code: Код доступа к информационной базе во время блокировки сеансов :param message: Сообщение будет выводиться при попытке установить сеанс с ИБ """ - ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + ib = self._get_infobase_short(infobase) cmd = f"infobase update {self._with_cluster_auth()} {self._with_infobase_auth(ib)} --sessions-deny=on --scheduled-jobs-deny=on --permission-code={permission_code} --denied-message={message}" self._rac_call(cmd) @@ -106,7 +118,7 @@ def unlock_info_base(self, infobase: str): Снимает блокировку фоновых заданий и сеансов информационной базы :param infobase: имя информационной базы """ - ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + ib = self._get_infobase_short(infobase) cmd = f"infobase update {self._with_cluster_auth()} {self._with_infobase_auth(ib)} --sessions-deny=off --scheduled-jobs-deny=off" self._rac_call(cmd) @@ -115,104 +127,17 @@ def terminate_info_base_sessions(self, infobase: str): Принудительно завершает текущие сеансы информационной базы :param infobase: имя информационной базы """ - ... + ib = self._get_infobase_short(infobase) + sessions = self._get_infobase_sessions(ib) + for s in sessions: + self._terminate_session(s) def get_info_base(self, infobase: str) -> V8CInfobase: """ Получает сведения об ИБ из кластера :param infobase: имя информационной базы """ - ib = self._filter_infobase(self.get_cluster_info_bases(), infobase) + ib = self._get_infobase_short(infobase) cmd = f"infobase info {self._with_cluster_auth()} {self._with_infobase_auth(ib)}" output = self._rac_call(cmd) return self._rac_output_to_object(output, V8CInfobase) - - -""" -rac ras:1545 cluster admin list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 cluster info --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 infobase summary list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 infobase create --create-database --name=infobase01 --dbms=MSSQLServer --db-server=db --db-name=infobase01 --locale=ru --db-user=sa --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 infobase create --create-database --name=infobase01 --dbms=PostgreSQL --db-server=db --db-name=infobase01 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 infobase create --create-database --name=infobase02 --dbms=PostgreSQL --db-server=db --db-name=infobase02 --locale=ru --db-user=postgres --db-pwd=supersecretpassword --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 -rac ras:1545 infobase summary list --cluster-user=Администратор --cluster=167b70e8-31d3-40ce-a06f-0bf091b04fb3 ---- -infobase : ecc1909f-4807-4423-becf-f81cf3b96cde -name : infobase01 -descr : - -infobase : 4e242939-180b-47a9-afa3-bdab8ece7f10 -name : infobase02 -descr : - -Use: - - rac session [command] [options] [arguments] - -Shared options: - - --version | -v - get the utility version - - --help | -h | -? - display brief utility description - -Shared arguments: - - [:] - administration server address (default: localhost:1545) - -Mode: - - session - Infobase session administration mode - -Parameters: - - --cluster= - (required) server cluster identifier - - --cluster-user= - name of the cluster administrator - - --cluster-pwd= - password of the cluster administrator - -Commands: - - info - receiving information on the session - - --session= - (required) infobase session identifier - - --licenses - displaying information on licenses granted to the session - - list - receiving the session information list - - --infobase= - infobase identifier - - --licenses - displaying information on licenses granted to the session - - terminate - Forced termination of the session - - --session= - (required) infobase session identifier - - --error-message= - Session termination reason message - - interrupt-current-server-call - current server call termination - - --session= - (required) infobase session identifier - - --error-message= - termination cause message -""" From d4383f4885e6471cb67819073d5daeb92594b6c2 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 22:03:38 +0300 Subject: [PATCH 14/20] Added simple test for rac --- core/cluster/models.py | 10 ++++++ core/cluster/tests/test_rac.py | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/core/cluster/models.py b/core/cluster/models.py index 991625a..a2e0cec 100644 --- a/core/cluster/models.py +++ b/core/cluster/models.py @@ -14,6 +14,16 @@ def id(self) -> str: ... def __init__(self, **kwargs): self.__dict__.update((k, v) for k, v in kwargs.items() if k in self.keys) + def __eq__(self, other): + result = True + for k in self.keys: + a = getattr(self, k, None) + b = getattr(other, k, None) + if a is None and b is None: + continue + result &= a == b + return result + class V8CCluster(V8CModel): @property diff --git a/core/cluster/tests/test_rac.py b/core/cluster/tests/test_rac.py index e69de29..b9572a1 100644 --- a/core/cluster/tests/test_rac.py +++ b/core/cluster/tests/test_rac.py @@ -0,0 +1,63 @@ +import textwrap +from core.cluster.models import V8CCluster +from core.cluster.rac import ClusterRACControler + + +def test_cluster_rac_control_interface_parse_cluster(): + """ + `_rac_output_to_object` parse cluster object from rac output correctly + """ + reference_object = V8CCluster( + cluster="ceef02b4-da53-41bb-8332-fdc8fa7db83a", + host="1c01", + port="1541", + name="Главный кластер", + expiration_timeout="0", + lifetime_limit="86400", + max_memory_size="0", + max_memory_time_limit="0", + security_level="0", + session_fault_tolerance_level="0", + load_balancing_mode="performance", + errors_count_threshold="0", + kill_problem_processes="0", + kill_by_memory_with_dump="0", + ) + cluster_output = textwrap.dedent( + """cluster : ceef02b4-da53-41bb-8332-fdc8fa7db83a + host : 1c01 + port : 1541 + name : "Главный кластер" + expiration-timeout : 0 + lifetime-limit : 86400 + max-memory-size : 0 + max-memory-time-limit : 0 + security-level : 0 + session-fault-tolerance-level : 0 + load-balancing-mode : performance + errors-count-threshold : 0 + kill-problem-processes : 0 + kill-by-memory-with-dump : 0 + + cluster : 167b70e8-31d3-40ce-a06f-0bf091b04fb3 + host : ragent + port : 1541 + name : "Local cluster" + expiration-timeout : 60 + lifetime-limit : 0 + max-memory-size : 0 + max-memory-time-limit : 0 + security-level : 0 + session-fault-tolerance-level : 0 + load-balancing-mode : performance + errors-count-threshold : 0 + kill-problem-processes : 1 + kill-by-memory-with-dump : 0 + allow-access-right-audit-events-recording : 0 + ping-period : 0 + ping-timeout : 0 + + """ + ) + constructed_object = ClusterRACControler()._rac_output_to_object(cluster_output, V8CCluster) + assert constructed_object == reference_object From 4efb2ff27a7f835c35a0efe611a840d79bd9796a Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 22:03:51 +0300 Subject: [PATCH 15/20] Renamed com controller tests --- core/cluster/tests/test_comcntr.py | 46 +++++++++++++++--------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/core/cluster/tests/test_comcntr.py b/core/cluster/tests/test_comcntr.py index 8394ba7..6d58bfb 100644 --- a/core/cluster/tests/test_comcntr.py +++ b/core/cluster/tests/test_comcntr.py @@ -31,7 +31,7 @@ def test_get_server_agent_port_is_str(): assert type(result) is str -def test_cluster_control_interface_initialization(mock_win32com_client_dispatch): +def test_cluster_com_control_interface_initialization(mock_win32com_client_dispatch): """ ClusterCOMControler instance is initialized sucessfully """ @@ -39,7 +39,7 @@ def test_cluster_control_interface_initialization(mock_win32com_client_dispatch) mock_win32com_client_dispatch.assert_called_once() -def test_cluster_control_interface_connect_agent(mock_connect_agent): +def test_cluster_com_control_interface_connect_agent(mock_connect_agent): """ `get_agent_connection` makes `COMConnector.ConnectAgent` call """ @@ -47,7 +47,7 @@ def test_cluster_control_interface_connect_agent(mock_connect_agent): mock_connect_agent.assert_called_once() -def test_cluster_control_interface_get_cluster(mock_connect_agent): +def test_cluster_com_control_interface_get_cluster(mock_connect_agent): """ `get_cluster` makes `IServerAgentConnection.GetClusters` call """ @@ -56,7 +56,7 @@ def test_cluster_control_interface_get_cluster(mock_connect_agent): mock_connect_agent.return_value.GetClusters.assert_called_once() -def test_cluster_control_interface_cluster_auth(mock_connect_agent): +def test_cluster_com_control_interface_cluster_auth(mock_connect_agent): """ `cluster_auth` makes `IClusterInfo.Authenticate` call """ @@ -65,7 +65,7 @@ def test_cluster_control_interface_cluster_auth(mock_connect_agent): mock_connect_agent.return_value.Authenticate.assert_called_once() -def test_cluster_control_interface_get_working_process_connection( +def test_cluster_com_control_interface_get_working_process_connection( mock_win32com_client_dispatch, mock_connect_agent, mock_connect_working_process ): """ @@ -76,7 +76,7 @@ def test_cluster_control_interface_get_working_process_connection( mock_win32com_client_dispatch.return_value.ConnectWorkingProcess.assert_called_once() -def test_cluster_control_interface_get_working_process_connection_admin_auth( +def test_cluster_com_control_interface_get_working_process_connection_admin_auth( mock_connect_agent, mock_connect_working_process ): """ @@ -88,7 +88,7 @@ def test_cluster_control_interface_get_working_process_connection_admin_auth( mock_connect_working_process.return_value.AuthenticateAdmin.assert_called_once() -def test_cluster_control_interface_get_working_process_connection_info_base_auth( +def test_cluster_com_control_interface_get_working_process_connection_info_base_auth( mocker: MockerFixture, mock_connect_agent, mock_connect_working_process ): """ @@ -108,7 +108,7 @@ def test_cluster_control_interface_get_working_process_connection_info_base_auth assert mock_connect_working_process.return_value.AddAuthentication.call_count == len(infobases_credentials) -def test_cluster_control_interface_get_cluster_info_bases(mock_connect_agent, mock_connect_working_process): +def test_cluster_com_control_interface_get_cluster_info_bases(mock_connect_agent, mock_connect_working_process): """ `get_cluster_info_bases` calls `IWorkingProcessConnection.GetInfoBases` """ @@ -117,7 +117,7 @@ def test_cluster_control_interface_get_cluster_info_bases(mock_connect_agent, mo mock_connect_working_process.return_value.GetInfoBases.assert_called() -def test_cluster_control_interface_get_info_base(infobase, mock_connect_agent, mock_connect_working_process): +def test_cluster_com_control_interface_get_info_base(infobase, mock_connect_agent, mock_connect_working_process): """ `get_info_base` finds exact infobase in list """ @@ -126,7 +126,7 @@ def test_cluster_control_interface_get_info_base(infobase, mock_connect_agent, m assert infobase_com_obj.Name == infobase -def test_cluster_control_interface_get_cluster_info_bases_short(mock_connect_agent): +def test_cluster_com_control_interface_get_cluster_info_bases_short(mock_connect_agent): """ `get_cluster_info_bases_short` calls `IServerAgentConnection.GetInfoBases` """ @@ -137,7 +137,7 @@ def test_cluster_control_interface_get_cluster_info_bases_short(mock_connect_age mock_connect_agent.return_value.GetInfoBases.assert_called() -def test_cluster_control_interface_get_info_base_short(infobase, mock_connect_agent): +def test_cluster_com_control_interface_get_info_base_short(infobase, mock_connect_agent): """ `_get_info_base_short` finds exact infobase in list """ @@ -148,7 +148,7 @@ def test_cluster_control_interface_get_info_base_short(infobase, mock_connect_ag assert infobase_com_obj.Name == infobase -def test_cluster_control_interface_get_info_base_metadata(infobase, mock_external_connection): +def test_cluster_com_control_interface_get_info_base_metadata(infobase, mock_external_connection): """ `get_info_base_metadata` calls `COMConnector.Connect` """ @@ -157,7 +157,7 @@ def test_cluster_control_interface_get_info_base_metadata(infobase, mock_externa mock_external_connection.assert_called() -def test_cluster_control_interface_get_info_base_metadata_connects_to_correct_infobase( +def test_cluster_com_control_interface_get_info_base_metadata_connects_to_correct_infobase( infobase, mock_external_connection ): """ @@ -168,7 +168,7 @@ def test_cluster_control_interface_get_info_base_metadata_connects_to_correct_in assert metadata[0] == infobase -def test_cluster_control_interface_lock_info_base(infobase, mock_connect_agent, mock_connect_working_process): +def test_cluster_com_control_interface_lock_info_base(infobase, mock_connect_agent, mock_connect_working_process): """ `lock_info_base` calls `IWorkingProcessConnection.UpdateInfoBase` """ @@ -177,7 +177,7 @@ def test_cluster_control_interface_lock_info_base(infobase, mock_connect_agent, mock_connect_working_process.return_value.UpdateInfoBase.assert_called_with(infobase_com_obj) -def test_cluster_control_interface_lock_info_base_set_sessions_denied( +def test_cluster_com_control_interface_lock_info_base_set_sessions_denied( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -188,7 +188,7 @@ def test_cluster_control_interface_lock_info_base_set_sessions_denied( assert infobase_com_obj.SessionsDenied is True -def test_cluster_control_interface_lock_info_base_set_scheduled_jobs_denied( +def test_cluster_com_control_interface_lock_info_base_set_scheduled_jobs_denied( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -199,7 +199,7 @@ def test_cluster_control_interface_lock_info_base_set_scheduled_jobs_denied( assert infobase_com_obj.ScheduledJobsDenied is True -def test_cluster_control_interface_lock_info_base_set_permission_code( +def test_cluster_com_control_interface_lock_info_base_set_permission_code( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -211,7 +211,7 @@ def test_cluster_control_interface_lock_info_base_set_permission_code( assert infobase_com_obj.PermissionCode == permission_code -def test_cluster_control_interface_lock_info_base_set_denied_message( +def test_cluster_com_control_interface_lock_info_base_set_denied_message( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -223,7 +223,7 @@ def test_cluster_control_interface_lock_info_base_set_denied_message( assert infobase_com_obj.DeniedMessage == denied_message -def test_cluster_control_interface_unlock_info_base(infobase, mock_connect_agent, mock_connect_working_process): +def test_cluster_com_control_interface_unlock_info_base(infobase, mock_connect_agent, mock_connect_working_process): """ `unlock_info_base` calls `IWorkingProcessConnection.UpdateInfoBase` """ @@ -232,7 +232,7 @@ def test_cluster_control_interface_unlock_info_base(infobase, mock_connect_agent mock_connect_working_process.return_value.UpdateInfoBase.assert_called_with(infobase_com_obj) -def test_cluster_control_interface_unlock_info_base_set_sessions_denied( +def test_cluster_com_control_interface_unlock_info_base_set_sessions_denied( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -243,7 +243,7 @@ def test_cluster_control_interface_unlock_info_base_set_sessions_denied( assert infobase_com_obj.SessionsDenied is False -def test_cluster_control_interface_unlock_info_base_set_scheduled_jobs_denied( +def test_cluster_com_control_interface_unlock_info_base_set_scheduled_jobs_denied( infobase, mock_connect_agent, mock_connect_working_process ): """ @@ -254,7 +254,7 @@ def test_cluster_control_interface_unlock_info_base_set_scheduled_jobs_denied( assert infobase_com_obj.ScheduledJobsDenied is False -def test_cluster_control_interface_terminate_info_base_sessions_get_infobase_session(infobase, mock_connect_agent): +def test_cluster_com_control_interface_terminate_info_base_sessions_get_infobase_session(infobase, mock_connect_agent): """ `terminate_info_base_sessions` calls `IServerAgentConnection.GetInfoBaseSessions` """ @@ -263,7 +263,7 @@ def test_cluster_control_interface_terminate_info_base_sessions_get_infobase_ses mock_connect_agent.return_value.GetInfoBaseSessions.assert_called() -def test_cluster_control_interface_terminate_info_base_sessions_terminate_session(infobase, mock_connect_agent): +def test_cluster_com_control_interface_terminate_info_base_sessions_terminate_session(infobase, mock_connect_agent): """ `terminate_info_base_sessions` calls `IServerAgentConnection.TerminateSession` """ From df5704c2e4bf222fd38f1741e57a8376112dd50f Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Sun, 8 Dec 2024 22:30:58 +0300 Subject: [PATCH 16/20] Refactor COM controller to use cluster models --- backup.py | 8 ++------ conftest.py | 29 ++++++++++++++++------------- core/cluster/abc.py | 7 ++++--- core/cluster/comcntr.py | 25 ++++++++++++++++++++----- core/cluster/rac.py | 2 +- core/cluster/tests/test_comcntr.py | 4 ++-- maintenance.py | 8 ++------ 7 files changed, 47 insertions(+), 36 deletions(-) diff --git a/backup.py b/backup.py index 85076dd..562a1a1 100644 --- a/backup.py +++ b/backup.py @@ -172,12 +172,8 @@ async def _backup_pgdump( async def _backup_info_base(ib_name: str) -> core_models.InfoBaseBackupTaskResult: cci = cluster_utils.get_cluster_controller_class()() ib_info = cci.get_info_base(ib_name) - db_server = ib_info.dbServerName - dbms = ib_info.DBMS - db_name = ib_info.dbName - db_user = ib_info.dbUser - if settings.BACKUP_PG and postgres.dbms_is_postgres(dbms): - result = await _backup_pgdump(ib_name, db_server, db_name, db_user) + if settings.BACKUP_PG and postgres.dbms_is_postgres(ib_info.dbms): + result = await _backup_pgdump(ib_name, ib_info.db_server, ib_info.db_name, ib_info.db_user) else: result = await cluster_utils.com_func_wrapper(_backup_v8, ib_name) return result diff --git a/conftest.py b/conftest.py index 4a568dd..c1c514e 100644 --- a/conftest.py +++ b/conftest.py @@ -1,7 +1,7 @@ import asyncio import random from textwrap import dedent -from unittest.mock import AsyncMock, Mock, mock_open +from unittest.mock import AsyncMock, mock_open import asyncpg import pytest @@ -9,6 +9,7 @@ from pytest_mock import MockerFixture from core import models as core_models +from core.cluster import models as cluster_models from utils.postgres import POSTGRES_NAME random.seed(0) @@ -235,12 +236,13 @@ def mock_cluster_postgres_infobase(mocker: MockerFixture, mock_cluster_com_contr db_server = "test_postgres_db_server" db_name = "test_postgres_db_name" db_user = "test_postgres_db_user" - info_base_mock = Mock() - info_base_mock.return_value.DBMS = POSTGRES_NAME - info_base_mock.return_value.dbServerName = db_server - info_base_mock.return_value.dbName = db_name - info_base_mock.return_value.dbUser = db_user - mock_cluster_com_controller.return_value.get_info_base = info_base_mock + ib = cluster_models.V8CInfobase( + dbms=POSTGRES_NAME, + db_server=db_server, + db_name=db_name, + db_user=db_user, + ) + mock_cluster_com_controller.return_value.get_info_base.return_value = ib return db_server, db_name, db_user @@ -249,12 +251,13 @@ def mock_cluster_mssql_infobase(mocker: MockerFixture, mock_cluster_com_controll db_server = "test_mssql_db_server" db_name = "test_mssql_db_name" db_user = "test_mssql_db_user" - info_base_mock = Mock() - info_base_mock.return_value.DBMS = "MSSQL" - info_base_mock.return_value.dbServerName = db_server - info_base_mock.return_value.dbName = db_name - info_base_mock.return_value.dbUser = db_user - mock_cluster_com_controller.return_value.get_info_base = info_base_mock + ib = cluster_models.V8CInfobase( + dbms="MSSQL", + db_server=db_server, + db_name=db_name, + db_user=db_user, + ) + mock_cluster_com_controller.return_value.get_info_base.return_value = ib return db_server, db_name, db_user diff --git a/core/cluster/abc.py b/core/cluster/abc.py index dc8c753..58153c2 100644 --- a/core/cluster/abc.py +++ b/core/cluster/abc.py @@ -2,11 +2,12 @@ from typing import List from conf import settings +from core.cluster.models import V8CInfobase, V8CInfobaseShort class ClusterControler(ABC): @abstractmethod - def get_cluster_info_bases(self): + def get_cluster_info_bases(self) -> List[V8CInfobaseShort]: """ Получает список всех ИБ из кластера """ @@ -39,7 +40,7 @@ def terminate_info_base_sessions(self, infobase: str): ... @abstractmethod - def get_info_base(self, infobase: str): + def get_info_base(self, infobase: str) -> V8CInfobase: """ Получает сведения об ИБ из кластера :param infobase: имя информационной базы @@ -53,7 +54,7 @@ def get_info_bases(self) -> List[str]: :return: массив с именами ИБ """ info_bases_obj = self.get_cluster_info_bases() - info_bases_raw = [ib.Name for ib in info_bases_obj] + info_bases_raw = [ib.name for ib in info_bases_obj] if settings.V8_INFOBASES_ONLY: info_bases = list( filter( diff --git a/core/cluster/comcntr.py b/core/cluster/comcntr.py index 8a18942..28e7696 100644 --- a/core/cluster/comcntr.py +++ b/core/cluster/comcntr.py @@ -2,6 +2,7 @@ from typing import List from core.cluster.abc import ClusterControler +from core.cluster.models import V8CInfobase, V8CInfobaseShort from core.cluster.utils import get_server_agent_address, get_server_agent_port try: @@ -112,15 +113,29 @@ def _filter_infobase(self, info_bases: List, name: str): if ib.Name.lower() == name.lower(): return ib - def get_cluster_info_bases(self): + def _get_cluster_info_bases(self) -> List: working_process_connection = self.get_working_process_connection_with_info_base_auth() info_bases = working_process_connection.GetInfoBases() return info_bases - def get_info_base(self, name): - info_bases = self.get_cluster_info_bases() + def _get_info_base(self, name): + info_bases = self._get_cluster_info_bases() return self._filter_infobase(info_bases, name) + def get_cluster_info_bases(self) -> List[V8CInfobaseShort]: + info_bases = self._get_cluster_info_bases() + return [V8CInfobaseShort(name=ib.Name) for ib in info_bases] + + def get_info_base(self, name) -> V8CInfobase: + com_infobase = self._get_info_base(name) + return V8CInfobase( + name=com_infobase.Name, + db_server=com_infobase.dbServerName, + dbms=com_infobase.DBMS, + db_name=com_infobase.dbName, + db_user=com_infobase.dbUser, + ) + def get_cluster_info_bases_short(self, agent_connection, cluster): info_bases_short = agent_connection.GetInfoBases(cluster) return info_bases_short @@ -157,7 +172,7 @@ def lock_info_base( :param permission_code: Код доступа к информационной базе во время блокировки сеансов :param message: Сообщение будет выводиться при попытке установить сеанс с ИБ """ - infobase_com_obj = self.get_info_base(infobase) + infobase_com_obj = self._get_info_base(infobase) infobase_com_obj.ScheduledJobsDenied = True infobase_com_obj.SessionsDenied = True infobase_com_obj.PermissionCode = permission_code @@ -176,7 +191,7 @@ def unlock_info_base(self, infobase: str): Снимает блокировку фоновых заданий и сеансов информационной базы :param infobase: имя информационной базы """ - infobase_com_obj = self.get_info_base(infobase) + infobase_com_obj = self._get_info_base(infobase) infobase_com_obj.ScheduledJobsDenied = False infobase_com_obj.SessionsDenied = False infobase_com_obj.DeniedMessage = "" diff --git a/core/cluster/rac.py b/core/cluster/rac.py index fa97917..e808bc2 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -94,7 +94,7 @@ def _terminate_session(self, session: V8CSession): cmd = f"session terminate {self._with_cluster_auth()} --session={session.id}" self._rac_call(cmd) - def get_cluster_info_bases(self): + def get_cluster_info_bases(self) -> List[V8CInfobaseShort]: """ Получает список всех ИБ из кластера """ diff --git a/core/cluster/tests/test_comcntr.py b/core/cluster/tests/test_comcntr.py index 6d58bfb..67c2586 100644 --- a/core/cluster/tests/test_comcntr.py +++ b/core/cluster/tests/test_comcntr.py @@ -122,8 +122,8 @@ def test_cluster_com_control_interface_get_info_base(infobase, mock_connect_agen `get_info_base` finds exact infobase in list """ cci = ClusterCOMControler() - infobase_com_obj = cci.get_info_base(infobase) - assert infobase_com_obj.Name == infobase + infobase_obj = cci.get_info_base(infobase) + assert infobase_obj.name == infobase def test_cluster_com_control_interface_get_cluster_info_bases_short(mock_connect_agent): diff --git a/maintenance.py b/maintenance.py index 7fecd9f..7e61e9f 100644 --- a/maintenance.py +++ b/maintenance.py @@ -90,18 +90,14 @@ async def maintenance_info_base( ) -> core_models.InfoBaseMaintenanceTaskResult: cci = cluster_utils.get_cluster_controller_class()() ib_info = cci.get_info_base(ib_name) - db_server = ib_info.dbServerName - dbms = ib_info.DBMS - db_name = ib_info.dbName - db_user = ib_info.dbUser async with semaphore: try: succeeded = True if settings.MAINTENANCE_V8: result_v8 = await cluster_utils.com_func_wrapper(_maintenance_v8, ib_name) succeeded &= result_v8.succeeded - if settings.MAINTENANCE_PG and postgres.dbms_is_postgres(dbms): - result_pg = await _maintenance_vacuumdb(ib_name, db_server, db_name, db_user) + if settings.MAINTENANCE_PG and postgres.dbms_is_postgres(ib_info.dbms): + result_pg = await _maintenance_vacuumdb(ib_name, ib_info.db_server, ib_info.db_name, ib_info.db_user) succeeded &= result_pg.succeeded result_logs = await rotate_logs(ib_name) succeeded &= result_logs.succeeded From 429df80703d29d17ba17db90a19dfcf8f0bdc933 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Mon, 9 Dec 2024 19:07:55 +0300 Subject: [PATCH 17/20] Add test for parsing infobase rac output --- core/cluster/rac.py | 2 +- core/cluster/tests/test_rac.py | 66 +++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index e808bc2..e7a0e3c 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -33,7 +33,7 @@ def _rac_output_to_objects(self, output: str, obj_class: Type[V8CModel]) -> List objects.append(obj_class(**kw)) kw.clear() continue - key, value = line.split(":") + key, value = line.split(":", maxsplit=1) kw.setdefault(key.replace("-", "_").strip(), value.strip().strip('"')) return objects diff --git a/core/cluster/tests/test_rac.py b/core/cluster/tests/test_rac.py index b9572a1..ae6b372 100644 --- a/core/cluster/tests/test_rac.py +++ b/core/cluster/tests/test_rac.py @@ -1,5 +1,5 @@ import textwrap -from core.cluster.models import V8CCluster +from core.cluster.models import V8CCluster, V8CInfobase from core.cluster.rac import ClusterRACControler @@ -61,3 +61,67 @@ def test_cluster_rac_control_interface_parse_cluster(): ) constructed_object = ClusterRACControler()._rac_output_to_object(cluster_output, V8CCluster) assert constructed_object == reference_object + + +def test_cluster_rac_control_interface_parse_infobase(): + """ + `_rac_output_to_object` parse infobase object from rac output correctly + """ + reference_object = V8CInfobase( + infobase="f3466741-0208-4680-a9d3-21f16672048f", + name="buh", + dbms="MSSQLServer", + db_server="10.0.0.0", + db_name="buh", + db_user="sa", + security_level="0", + license_distribution="allow", + scheduled_jobs_deny="off", + sessions_deny="off", + denied_from="2020-09-12T18:49:18", + denied_message="", + denied_parameter="", + denied_to="2020-09-12T18:50:18", + permission_code="0000", + external_session_manager_connection_string="", + external_session_manager_required="no", + security_profile_name="", + safe_mode_security_profile_name="", + reserve_working_processes="no", + descr="Бухгалтерия", + disable_local_speech_to_text="no", + configuration_unload_delay_by_working_process_without_active_users="0", + minimum_scheduled_jobs_start_period_without_active_users="0", + maximum_scheduled_jobs_start_shift_without_active_users="0", + ) + cluster_output = textwrap.dedent( + """infobase : f3466741-0208-4680-a9d3-21f16672048f + name : buh + dbms : MSSQLServer + db-server : 10.0.0.0 + db-name : buh + db-user : sa + security-level : 0 + license-distribution : allow + scheduled-jobs-deny : off + sessions-deny : off + denied-from : 2020-09-12T18:49:18 + denied-message : + denied-parameter : + denied-to : 2020-09-12T18:50:18 + permission-code : "0000" + external-session-manager-connection-string : + external-session-manager-required : no + security-profile-name : + safe-mode-security-profile-name : + reserve-working-processes : no + descr : "Бухгалтерия" + disable-local-speech-to-text : no + configuration-unload-delay-by-working-process-without-active-users : 0 + minimum-scheduled-jobs-start-period-without-active-users : 0 + maximum-scheduled-jobs-start-shift-without-active-users : 0 + + """ + ) + constructed_object = ClusterRACControler()._rac_output_to_object(cluster_output, V8CInfobase) + assert constructed_object == reference_object From 287bd657a166cee383192db4f374049af7458576 Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Mon, 9 Dec 2024 19:08:46 +0300 Subject: [PATCH 18/20] Add rac full path feature --- backup.py | 2 +- conftest.py | 4 ++-- core/cluster/rac.py | 7 +++---- core/tests/test_utils.py | 10 +++++----- core/utils.py | 12 ++++++++---- maintenance.py | 2 +- tests/test_backup.py | 32 ++++++++++++++++---------------- tests/test_maintenance.py | 16 +++++++++------- update.py | 2 +- 9 files changed, 46 insertions(+), 41 deletions(-) diff --git a/backup.py b/backup.py index 562a1a1..63c1f86 100644 --- a/backup.py +++ b/backup.py @@ -78,7 +78,7 @@ async def _backup_v8(ib_name: str, *args, **kwargs) -> core_models.InfoBaseBacku log_filename = os.path.join(settings.LOG_PATH, utils.append_file_extension_to_string(ib_and_time_str, "log")) # https://its.1c.ru/db/v838doc#bookmark:adm:TI000000526 v8_command = ( - rf'"{utils.get_platform_full_path()}" ' + rf'"{utils.get_1cv8_service_full_path()}" ' rf"DESIGNER /S {cluster_utils.get_server_agent_address()}\{ib_name} " rf'/N"{info_base_user}" /P"{info_base_pwd}" ' rf"/Out {log_filename} -NoTruncate " diff --git a/conftest.py b/conftest.py index c1c514e..55099a0 100644 --- a/conftest.py +++ b/conftest.py @@ -222,8 +222,8 @@ def mock_asyncio_subprocess_termination_error(mocker: MockerFixture): @pytest.fixture -def mock_get_platform_full_path(mocker: MockerFixture): - return mocker.patch("core.utils.get_platform_full_path", return_value="") +def mock_get_1cv8_service_full_path(mocker: MockerFixture): + return mocker.patch("core.utils.get_1cv8_service_full_path", return_value="") @pytest.fixture diff --git a/core/cluster/rac.py b/core/cluster/rac.py index e7a0e3c..9f285c4 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -7,6 +7,7 @@ from core.exceptions import RACException from core.cluster.abc import ClusterControler from core.cluster.models import V8CModel, V8CCluster, V8CInfobaseShort, V8CInfobase, V8CSession +from core import utils log = logging.getLogger(__name__) @@ -20,9 +21,7 @@ def __init__(self): self.infobases_credentials = settings.V8_INFOBASES_CREDENTIALS def _get_rac_exec_path(self): - # TODO: поддержка windows/linux - # TODO: поддержка сценариев, когда утилита не включена в PATH - return "rac" + return utils.get_1cv8_service_full_path("rac") def _rac_output_to_objects(self, output: str, obj_class: Type[V8CModel]) -> List[V8CModel]: objects = [] @@ -41,7 +40,7 @@ def _rac_output_to_object(self, output: str, obj_class: Type[V8CModel]) -> V8CMo return self._rac_output_to_objects(output, obj_class)[0] def _rac_call(self, command: str) -> str: - call_str = f"{self._get_rac_exec_path()} {self.ras_host}:{self.ras_port} {command} ; exit 0" + call_str = f"{self._get_rac_exec_path()} {self.ras_host}:{self.ras_port} {command}" log.debug(f"Created rac command [{call_str}]") try: out = subprocess.check_output(call_str, stderr=subprocess.STDOUT, shell=True) diff --git a/core/tests/test_utils.py b/core/tests/test_utils.py index 95c584a..a1e8860 100644 --- a/core/tests/test_utils.py +++ b/core/tests/test_utils.py @@ -25,26 +25,26 @@ get_info_base_credentials, get_info_bases, get_infobase_glob_pattern, - get_platform_full_path, + get_1cv8_service_full_path, path_leaf, read_file_content, remove_old_files_by_pattern, ) -def test_get_platform_full_path_contains_platform_version(mock_os_platform_path, mock_platform_last_version): +def test_get_1cv8_service_full_path_contains_platform_version(mock_os_platform_path, mock_platform_last_version): """ Full path to platform binary contains last platform version directory """ - result = get_platform_full_path() + result = get_1cv8_service_full_path() assert mock_platform_last_version in result -def test_get_platform_full_path_contains_executable(mock_os_platform_path): +def test_get_1cv8_service_full_path_contains_executable(mock_os_platform_path): """ Full path to platform binary contains executable file """ - result = get_platform_full_path() + result = get_1cv8_service_full_path() assert "1cv8" in result diff --git a/core/utils.py b/core/utils.py index 9f498c0..c225241 100644 --- a/core/utils.py +++ b/core/utils.py @@ -15,14 +15,18 @@ log = logging.getLogger(__name__) -def get_platform_full_path() -> str: +def get_platform_directory() -> str: platformPath = settings.V8_PLATFORM_PATH platformVersion = version.find_platform_last_version(platformPath) - platformDirectory = os.path.join(platformPath, str(platformVersion)) + return os.path.join(platformPath, str(platformVersion)) + + +def get_1cv8_service_full_path(service: str = "1cv8") -> str: + platformDirectory = get_platform_directory() if platform.system() == "Windows": - full_path = os.path.join(platformDirectory, "bin", "1cv8.exe") + full_path = os.path.join(platformDirectory, "bin", f"{service}.exe") if platform.system() == "Linux": - full_path = os.path.join(platformDirectory, "1cv8") + full_path = os.path.join(platformDirectory, service) return full_path diff --git a/maintenance.py b/maintenance.py index 7e61e9f..ff3ef77 100644 --- a/maintenance.py +++ b/maintenance.py @@ -42,7 +42,7 @@ async def _maintenance_v8(ib_name: str, *args, **kwargs) -> core_models.InfoBase reduce_date = datetime.now() - timedelta(days=settings.MAINTENANCE_REGISTRATION_LOG_RETENTION_DAYS) reduce_date_str = utils.get_formatted_date_for_1cv8(reduce_date) v8_command = ( - rf'"{utils.get_platform_full_path()}" ' + rf'"{utils.get_1cv8_service_full_path()}" ' rf"DESIGNER /S {cluster_utils.get_server_agent_address()}\{ib_name} " rf'/N"{info_base_user}" /P"{info_base_pwd}" ' rf"/Out {log_filename} -NoTruncate " diff --git a/tests/test_backup.py b/tests/test_backup.py index 9dd2040..c4ac8f5 100644 --- a/tests/test_backup.py +++ b/tests/test_backup.py @@ -91,7 +91,7 @@ async def test_rotate_backups_calls_old_file_remover_for_replication_paths(mocke @pytest.mark.asyncio -async def test_backup_v8_calls_execute_v8_command(mocker: MockerFixture, infobase, mock_get_platform_full_path): +async def test_backup_v8_calls_execute_v8_command(mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path): """ Backup with 1cv8 tools calls `execute_v8_command` """ @@ -101,7 +101,7 @@ async def test_backup_v8_calls_execute_v8_command(mocker: MockerFixture, infobas @pytest.mark.asyncio -async def test_backup_v8_makes_retries(mocker: MockerFixture, infobase, mock_get_platform_full_path): +async def test_backup_v8_makes_retries(mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path): """ Backup with 1cv8 tools makes retries according to retry policy """ @@ -117,7 +117,7 @@ async def test_backup_v8_makes_retries(mocker: MockerFixture, infobase, mock_get @pytest.mark.asyncio async def test_backup_v8_return_backup_result_type_object_when_succeeded( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object of type `InfoBaseBackupTaskResult` when succeeded @@ -129,7 +129,7 @@ async def test_backup_v8_return_backup_result_type_object_when_succeeded( @pytest.mark.asyncio async def test_backup_v8_return_backup_result_type_object_when_failed( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object of type `InfoBaseBackupTaskResult` when failed @@ -141,7 +141,7 @@ async def test_backup_v8_return_backup_result_type_object_when_failed( @pytest.mark.asyncio async def test_backup_v8_return_result_for_exact_infobase_when_succeeded( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object for exact infobase which was provided when succeeded @@ -153,7 +153,7 @@ async def test_backup_v8_return_result_for_exact_infobase_when_succeeded( @pytest.mark.asyncio async def test_backup_v8_return_backup_result_succeeded_true_when_succeeded( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object with succeeded is True when succeeded @@ -165,7 +165,7 @@ async def test_backup_v8_return_backup_result_succeeded_true_when_succeeded( @pytest.mark.asyncio async def test_backup_v8_return_result_for_exact_infobase_when_failed( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object for exact infobase which was provided when faild @@ -177,7 +177,7 @@ async def test_backup_v8_return_result_for_exact_infobase_when_failed( @pytest.mark.asyncio async def test_backup_v8_return_backup_result_succeeded_false_when_failed( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Backup with 1cv8 tools returns object with succeeded is False when failed @@ -206,7 +206,7 @@ async def test_backup_pgdump_calls_execute_subprocess_command( async def test_backup_pgdump_makes_retries( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -227,7 +227,7 @@ async def test_backup_pgdump_makes_retries( async def test_backup_pgdump_return_backup_result_type_object_when_succeeded( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -243,7 +243,7 @@ async def test_backup_pgdump_return_backup_result_type_object_when_succeeded( async def test_backup_pgdump_return_backup_result_type_object_when_failed( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -259,7 +259,7 @@ async def test_backup_pgdump_return_backup_result_type_object_when_failed( async def test_backup_pgdump_return_backup_result_succeeded_true_when_succeeded( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -275,7 +275,7 @@ async def test_backup_pgdump_return_backup_result_succeeded_true_when_succeeded( async def test_backup_pgdump_return_backup_result_succeeded_false_when_failed( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -291,7 +291,7 @@ async def test_backup_pgdump_return_backup_result_succeeded_false_when_failed( async def test_backup_pgdump_return_result_for_exact_infobase_when_succeeded( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -307,7 +307,7 @@ async def test_backup_pgdump_return_result_for_exact_infobase_when_succeeded( async def test_backup_pgdump_return_result_for_exact_infobase_when_failed( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): @@ -323,7 +323,7 @@ async def test_backup_pgdump_return_result_for_exact_infobase_when_failed( async def test_backup_pgdump_return_negative_result_when_failed( mocker: MockerFixture, infobase, - mock_get_platform_full_path, + mock_get_1cv8_service_full_path, mock_prepare_postgres_connection_vars, mock_get_postgres_version_16, ): diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index 04884ad..c9797f1 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -27,7 +27,9 @@ async def test_rotate_logs_calls_inner_func(mocker: MockerFixture, infobase): @pytest.mark.asyncio -async def test_maintenance_v8_calls_execute_v8_command(mocker: MockerFixture, infobase, mock_get_platform_full_path): +async def test_maintenance_v8_calls_execute_v8_command( + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path +): """ Maintenance with 1cv8 tools calls execute_v8_command to run created command """ @@ -38,7 +40,7 @@ async def test_maintenance_v8_calls_execute_v8_command(mocker: MockerFixture, in @pytest.mark.asyncio async def test_maintenance_v8_returns_maintenance_result_type_when_succeeded( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Maintenance with 1cv8 tools returns result of `InfoBaseMaintenanceTaskResult` type if no errors @@ -50,7 +52,7 @@ async def test_maintenance_v8_returns_maintenance_result_type_when_succeeded( @pytest.mark.asyncio async def test_maintenance_v8_returns_maintenance_result_type_when_failed( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Maintenance with 1cv8 tools returns result of `InfoBaseMaintenanceTaskResult` type if an error occured @@ -62,7 +64,7 @@ async def test_maintenance_v8_returns_maintenance_result_type_when_failed( @pytest.mark.asyncio async def test_maintenance_v8_returns_success_result_for_exact_infobase( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Maintenance with 1cv8 tools returns success result for exact infobase which was provided if no errors @@ -74,7 +76,7 @@ async def test_maintenance_v8_returns_success_result_for_exact_infobase( @pytest.mark.asyncio async def test_maintenance_v8_returns_failed_result_for_exact_infobase( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Maintenance with 1cv8 tools returns failed result for exact infobase which was provided if an error occured @@ -85,7 +87,7 @@ async def test_maintenance_v8_returns_failed_result_for_exact_infobase( @pytest.mark.asyncio -async def test_maintenance_v8_returns_success_result(mocker: MockerFixture, infobase, mock_get_platform_full_path): +async def test_maintenance_v8_returns_success_result(mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path): """ Maintenance with 1cv8 tools returns success result if no errors """ @@ -96,7 +98,7 @@ async def test_maintenance_v8_returns_success_result(mocker: MockerFixture, info @pytest.mark.asyncio async def test_maintenance_v8_returns_failed_result_if_error( - mocker: MockerFixture, infobase, mock_get_platform_full_path + mocker: MockerFixture, infobase, mock_get_1cv8_service_full_path ): """ Maintenance with 1cv8 tools returns failed result if an error occured diff --git a/update.py b/update.py index fc88cd1..3517494 100644 --- a/update.py +++ b/update.py @@ -163,7 +163,7 @@ async def _update_info_base(ib_name, dry=False): log_filename = os.path.join(settings.LOG_PATH, utils.get_ib_and_time_filename(ib_name, "log")) # https://its.1c.ru/db/v838doc#bookmark:adm:TI000000530 v8_command = ( - rf'"{utils.get_platform_full_path()}" ' + rf'"{utils.get_1cv8_service_full_path()}" ' rf"DESIGNER /S {cluster_utils.get_server_agent_address()}\{ib_name} " rf'/N"{info_base_user}" /P"{info_base_pwd}" ' rf"/Out {log_filename} -NoTruncate " From 754e9fa4cb108f88c751a9ee08c73ce293ac11ab Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Mon, 9 Dec 2024 19:30:26 +0300 Subject: [PATCH 19/20] Set shell encoding for rac calls --- core/cluster/rac.py | 10 +++++++--- tests/settings.py | 5 +++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/cluster/rac.py b/core/cluster/rac.py index 9f285c4..ffea510 100644 --- a/core/cluster/rac.py +++ b/core/cluster/rac.py @@ -1,3 +1,4 @@ +import platform import subprocess import logging @@ -19,9 +20,13 @@ def __init__(self): self.cluster_admin_name = settings.V8_CLUSTER_ADMIN_CREDENTIALS[0] self.cluster_admin_pwd = settings.V8_CLUSTER_ADMIN_CREDENTIALS[1] self.infobases_credentials = settings.V8_INFOBASES_CREDENTIALS + if platform.system() == "Windows": + self.shell_encoding = "cp866" + if platform.system() == "Linux": + self.shell_encoding = "utf-8" def _get_rac_exec_path(self): - return utils.get_1cv8_service_full_path("rac") + return f'"{utils.get_1cv8_service_full_path("rac")}"' def _rac_output_to_objects(self, output: str, obj_class: Type[V8CModel]) -> List[V8CModel]: objects = [] @@ -43,8 +48,7 @@ def _rac_call(self, command: str) -> str: call_str = f"{self._get_rac_exec_path()} {self.ras_host}:{self.ras_port} {command}" log.debug(f"Created rac command [{call_str}]") try: - out = subprocess.check_output(call_str, stderr=subprocess.STDOUT, shell=True) - out = out.decode("utf-8") + out = subprocess.check_output(call_str, stderr=subprocess.STDOUT, shell=True, encoding=self.shell_encoding) except subprocess.CalledProcessError as e: raise RACException() from e return out diff --git a/tests/settings.py b/tests/settings.py index 2ec8042..b2ea89b 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,3 +1,6 @@ +from os.path import join + + AWS_RETRY_PAUSE = 0 V8_LOCK_INFO_BASE_PAUSE = 0 @@ -6,3 +9,5 @@ "address": "ras", "port": "1545", } + +V8_PLATFORM_PATH = join("/opt", "1cv8", "x86_64") From f067dadd281ad5e17d0c80358b0e8f4ad6b2a9df Mon Sep 17 00:00:00 2001 From: Ilya Bogdanov Date: Mon, 9 Dec 2024 21:43:25 +0300 Subject: [PATCH 20/20] Add rac info in readme --- README.md | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 07ef989..ea21f9e 100644 --- a/README.md +++ b/README.md @@ -14,10 +14,10 @@ 7. Уведомления на email В планах: -1. Резервное копирование конфигурации кластера -2. Восстановление конфигурации кластера -3. Уведомления в telegram -4. Возможность работы через утилиту `rac` и сервер администрирования `ras` вместо использования COM-компоненты +1. [ ] Резервное копирование конфигурации кластера +2. [ ] Восстановление конфигурации кластера +3. [ ] Уведомления в telegram +4. [x] Возможность работы через утилиту `rac` и сервер администрирования `ras` вместо использования COM-компоненты # Установка @@ -58,6 +58,9 @@ scoop install python ### Регистрация COM-компоненты 1С Предприятие +> [!TIP] +> Регистрация COM-компоненты не требуется, если предполагается работа с кластером через утилиту `rac` + Регистрировать необходимо COM-компоненту той же версии, что и агент сервера, к которому будет выполняться подключение Выполнить команду с **правами администратора** @@ -117,10 +120,14 @@ TIP: для работы с путями в шаблонном файле нас Настройки взаимодействия с платформой 1С Предприятие. Эти настройки необходимы для работы любых частей приложения +> [!CAUTION] +> В режиме взаимодействия с кластером 1С Предприятие через клиент администрирования кластера невозможно выполнять обновление информационных баз. +> На текущий момент обновление ИБ поддерживается только при работе через COM-компоненту + |Параметр|Описание| |-------:|:-------| |`V8_CLUSTER_ADMIN_CREDENTIALS`|Учетные данные администратора кластера 1С Предприятие| -|`V8_CLUSTER_CONTROL_MODE `|Режим взаимодействия с кластером 1С Предприятие: через COM-компоненту (`'com'`) или черз клиент администрирования кластера (`'rac'`)| +|`V8_CLUSTER_CONTROL_MODE `|Режим взаимодействия с кластером 1С Предприятие: через COM-компоненту (`'com'`) или через клиент администрирования кластера (`'rac'`)| |`V8_INFOBASES_CREDENTIALS` |Сопоставление с именами информационных баз, именами пользователей и паролями, которые будут использованы для подключения к информационным базам. Если информационная база не указана в списке в явном виде, для подклчения к ней будут использованы данные от записи `default`| |`V8_INFOBASES_EXCLUDE` |Список с именами информационных баз, которые будут пропущены. Никакие операции с ними выполняться не будут| |`V8_INFOBASES_ONLY` |Если список не пустой, все действия будут проводиться только с информационными базами, указанными в нём|