2021-04-26 20:07:00 +02:00
#coding:utf-8
2022-01-26 21:10:46 +01:00
"""
ID : issue - 6298
ISSUE : 6298
TITLE : Provide ability to see current state of DB encryption
DESCRIPTION :
2023-03-11 08:00:53 +01:00
Test adds lot of data to database , changes FW to ON and runs ' alter database encrypt ... ' .
Then it starts loop with query :
' select mon$crypt_page, mon$crypt_state from mon$database ' , with repeating it every 0.5 second .
Loop continues until we find < ENCRYPTING_PAGES_MIN_CNT > different number of encrypted pages ,
2023-03-11 08:04:23 +01:00
or if timeout < MAX_WAITING_ENCR_FINISH > ms expired .
2023-03-11 08:00:53 +01:00
If ( after loop ) number of pages detected in encrypted state less then < ENCRYPTING_PAGES_MIN_CNT >
then test is considered as failed .
NB : we do NOT wait for the encryption process to complete for the whole database because this
time strongly depends on hardware of testing host and concurrent workload . We just want to see
* several * different values of mon $ crypt_page where mon $ crypt_state = < RUNNING_ENCRYPTING_STATE > .
2022-01-26 21:10:46 +01:00
JIRA : CORE - 6048
2022-02-02 15:46:19 +01:00
FBTEST : bugs . core_6048
2022-06-13 21:55:59 +02:00
NOTES :
[ 13.06 .2022 ] pzotov
Checked on 5.0 .0 .509 - both on Linux and Windows .
2023-03-11 08:00:53 +01:00
[ 11.03 .2023 ] pzotov
Checked on 5.0 .0 .972 , 4.0 .3 .2907 ( Windows ) .
2022-06-13 21:55:59 +02:00
: : : NB - 1 : : :
2023-03-11 08:00:53 +01:00
Before ~ 06 - sep - 2021 encryption * blocked * obtaining data from monitoring tables .
2022-06-13 21:55:59 +02:00
See also : https : / / github . com / FirebirdSQL / firebird / issues / 6947
: : : NB - 2 : : :
2022-06-13 21:59:51 +02:00
Careful tuning required on each tesing box for this test .
2025-01-18 14:26:13 +01:00
[ 18.01 .2025 ] pzotov
Resultset of cursor that executes using instance of selectable PreparedStatement must be stored
in some variable in order to have ability close it EXPLICITLY ( before PS will be freed ) .
Otherwise access violation raises during Python GC and pytest hangs at final point ( does not return control to OS ) .
This occurs at least for : Python 3.11 .2 / pytest : 7.4 .4 / firebird . driver : 1.10 .6 / Firebird . Qa : 0.19 .3
The reason of that was explained by Vlad , 26.10 .24 17 : 42 ( " oddities when use instances of selective statements " ) .
2022-01-26 21:10:46 +01:00
"""
2021-04-26 20:07:00 +02:00
2022-06-13 21:55:59 +02:00
import os
import time
import datetime as py_dt
from datetime import timedelta
2021-04-26 20:07:00 +02:00
import pytest
2022-01-26 21:10:46 +01:00
from firebird . qa import *
2022-06-13 21:55:59 +02:00
from firebird . driver import DatabaseError , DbWriteMode
2021-04-26 20:07:00 +02:00
2023-03-11 08:00:53 +01:00
###########################
### S E T T I N G S ###
###########################
# QA_GLOBALS -- dict, is defined in qa/plugin.py, obtain settings
# from act.files_dir/'test_config.ini':
enc_settings = QA_GLOBALS [ ' encryption ' ]
# ACHTUNG: this must be carefully tuned on every new host:
#
MAX_WAITING_ENCR_FINISH = int ( enc_settings [ ' MAX_WAIT_FOR_ENCR_FINISH_WIN ' if os . name == ' nt ' else ' MAX_WAIT_FOR_ENCR_FINISH_NIX ' ] )
assert MAX_WAITING_ENCR_FINISH > 0
ENCRYPTION_PLUGIN = enc_settings [ ' encryption_plugin ' ] # fbSampleDbCrypt
ENCRYPTION_KEY = enc_settings [ ' encryption_key ' ] # Red
# How many *different* page numbers we want to see as being encrypted before break and finish test:
#
ENCRYPTING_PAGES_MIN_CNT = 3
# Value in mon$database.mon$crypt_state for completed encryption:
#
COMPLETED_ENCRYPTION_STATE = 1
# Value in mon$database.mon$crypt_state for CURRENTLY running encryption:
#
RUNNING_ENCRYPTING_STATE = 3
# How many rows will be inserted in order to make encryption thread do its work for some valuable time:
N_ROWS = 15000 if os . name == ' nt ' else 10000
2021-04-26 20:07:00 +02:00
2022-06-13 21:55:59 +02:00
F_LEN = 16383
init_script = f """
set bail on ;
create table tlog ( id bigint generated by default as identity constraint pk_tlog primary key , crypt_page int , crypt_state smallint ) ;
create table test ( s varchar ( { F_LEN } ) ) ;
commit ;
set term ^ ;
execute block as
declare n bigint = { N_ROWS } ;
begin
while ( n > 0 ) do
begin
insert into test ( s ) values ( lpad ( ' ' , { F_LEN } , uuid_to_char ( gen_uuid ( ) ) ) ) ;
n = n - 1 ;
end
end
^
commit
^
"""
2023-03-11 08:00:53 +01:00
db = db_factory ( init = init_script , charset = ' none ' , page_size = 4096 )
2022-01-26 21:10:46 +01:00
act = python_act ( ' db ' , substitutions = [ ( ' [ \t ]+ ' , ' ' ) ] )
2021-04-26 20:07:00 +02:00
2022-06-13 21:55:59 +02:00
@pytest.mark.encryption
2023-03-11 08:00:53 +01:00
@pytest.mark.version ( ' >=4.0.2 ' )
2022-06-13 21:55:59 +02:00
def test_1 ( act : Action , capsys ) :
2022-07-31 12:49:47 +02:00
2022-06-13 21:55:59 +02:00
with act . connect_server ( ) as srv :
srv . database . set_write_mode ( database = act . db . db_path , mode = DbWriteMode . SYNC )
encryption_finished = False
encryption_started = False
with act . db . connect ( ) as con , act . db . connect ( ) as con2 :
t1 = py_dt . datetime . now ( )
d1 = t1 - t1
sttm = f ' alter database encrypt with " { ENCRYPTION_PLUGIN } " key " { ENCRYPTION_KEY } " '
try :
con . execute_immediate ( sttm )
con . commit ( )
encryption_started = True
except DatabaseError as e :
# -ALTER DATABASE failed
# -Crypt plugin fbSampleDbCrypt failed to load
# ==> no sense to do anything else, encryption_started remains False.
print ( e . __str__ ( ) )
cur2 = con2 . cursor ( )
2025-01-18 14:26:13 +01:00
ps , rs = None , None
try :
ps = cur2 . prepare ( ' select mon$crypt_page, mon$crypt_state from mon$database ' )
# This will store different number of pages which are currently encrypted.
# When length of this set will exceed ENCRYPTING_PAGES_MIN_CNT then we break from loop:
#
encrypting_pages_set = set ( )
waiting_in_loop = - 1
while encryption_started :
t2 = py_dt . datetime . now ( )
d1 = t2 - t1
waiting_in_loop = d1 . seconds * 1000 + d1 . microseconds / / 1000
if waiting_in_loop > MAX_WAITING_ENCR_FINISH :
print ( f ' TIMEOUT EXPIRATION: encryption took { d1 . seconds * 1000 + d1 . microseconds / / 1000 } ms which exceeds limit = { MAX_WAITING_ENCR_FINISH } ms. ' )
break
# ::: NB ::: 'ps' returns data, i.e. this is SELECTABLE expression.
# We have to store result of cur.execute(<psInstance>) in order to
# close it explicitly.
# Otherwise AV can occur during Python garbage collection and this
# causes pytest to hang on its final point.
# Explained by hvlad, email 26.10.24 17:42
rs = cur2 . execute ( ps )
for r in rs :
crypt_page , crypt_state = r [ : 2 ]
con2 . commit ( )
# 0 = non crypted;
# 1 = has been encrypted;
# 2 = is DEcrypting;
# 3 = is Encrypting;
if crypt_state == RUNNING_ENCRYPTING_STATE :
encrypting_pages_set . add ( crypt_page , )
if crypt_state == COMPLETED_ENCRYPTION_STATE :
encryption_finished = True
break
elif len ( encrypting_pages_set ) > ENCRYPTING_PAGES_MIN_CNT :
break
else :
time . sleep ( 0.1 )
except DatabaseError as e :
print ( e . __str__ ( ) )
print ( e . gds_codes )
finally :
if rs :
rs . close ( ) # <<< EXPLICITLY CLOSING CURSOR RESULTS
if ps :
ps . free ( )
2023-03-11 08:00:53 +01:00
# ---------------------------------------------------------
expected_msg = f ' EXPECTED: at least { ENCRYPTING_PAGES_MIN_CNT } different mon$crypt_page values found during encryption process. '
if encryption_started :
if len ( encrypting_pages_set ) > ENCRYPTING_PAGES_MIN_CNT :
print ( expected_msg )
2022-06-13 21:55:59 +02:00
else :
2023-03-11 08:00:53 +01:00
print ( f ' UNEXPECTED: only { len ( encrypting_pages_set ) } different mon$crypt_page values found for { waiting_in_loop } ms: %s ' % ' , ' . join ( ( str ( x ) for x in sorted ( encrypting_pages_set ) ) ) )
print ( f ' At least { ENCRYPTING_PAGES_MIN_CNT } different values expected to be found; encryption_finished = { encryption_finished } ' )
else :
print ( ' UNEXPECTED: encryption did not start. ' )
# ---------------------------------------------------------
expected_stdout = f """
{ expected_msg }
"""
2022-06-13 21:55:59 +02:00
act . expected_stdout = expected_stdout
act . stdout = capsys . readouterr ( ) . out
assert act . clean_stdout == act . clean_expected_stdout
act . reset ( )