Skip to content

Commit 7aa78ab

Browse files
authored
Automatic addition of database before connecting to it (#4316)
* added a function that creates a database before addproduct() function connects to it * renamed function to follow convention of naming * retrigger checks * removed unnecessary test case * test commit * changed try catch block * working implementation * commit to check if test cases will work * fixed sqlite error * fixed tests failing * changed behaviour of add_test_package_product to work with new fucntionality of add_product * possible fix of psql_psycopq2 * removed changed to creation of test package product * probable fix * final working implementation * removed automatic prevention of previous jobs, as it was already implemented by other pr * comments applied * probable fix multiple server database prohibition problem * fix of failing test
1 parent a3ede93 commit 7aa78ab

File tree

7 files changed

+123
-22
lines changed

7 files changed

+123
-22
lines changed

web/server/codechecker_server/api/product_server.py

+67
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515

1616
from sqlalchemy.sql.expression import and_
1717

18+
from sqlalchemy import create_engine, exc
19+
from sqlalchemy.engine.url import URL
20+
1821
import codechecker_api_shared
1922
from codechecker_api.ProductManagement_v6 import ttypes
2023

@@ -318,6 +321,57 @@ def getProductConfiguration(self, product_id):
318321

319322
return prod
320323

324+
@timeit
325+
def __add_product_support(self, product):
326+
"""
327+
Creates a database for the given product,
328+
to assist addProduct() function that connects to
329+
an already existing database.
330+
"""
331+
332+
product_info = product.connection
333+
if product_info.engine == 'sqlite':
334+
LOG.info("Using SQLite engine, skipping database creation")
335+
return True
336+
337+
db_host = product_info.host
338+
db_engine = product_info.engine
339+
db_port = int(product_info.port)
340+
db_user = convert.from_b64(product_info.username_b64)
341+
db_pass = convert.from_b64(product_info.password_b64)
342+
db_name = product_info.database
343+
344+
engine_url = URL(
345+
drivername=db_engine,
346+
username=db_user,
347+
password=db_pass,
348+
host=db_host,
349+
port=db_port,
350+
database='postgres'
351+
)
352+
engine = create_engine(engine_url)
353+
try:
354+
with engine.connect() as conn:
355+
conn.execute("commit")
356+
LOG.info("Creating database '%s'", db_name)
357+
conn.execute(f"CREATE DATABASE {db_name}")
358+
conn.close()
359+
except exc.ProgrammingError as e:
360+
LOG.error("ProgrammingError occurred: %s", str(e))
361+
if "already exists" in str(e):
362+
LOG.error("Database '%s' already exists", db_name)
363+
return False
364+
else:
365+
LOG.error("Error occurred while creating database: %s", str(e))
366+
return False
367+
except exc.SQLAlchemyError as e:
368+
LOG.error("SQLAlchemyError occurred: %s", str(e))
369+
return False
370+
finally:
371+
engine.dispose()
372+
373+
return True
374+
321375
@timeit
322376
def addProduct(self, product):
323377
"""
@@ -352,6 +406,19 @@ def addProduct(self, product):
352406
codechecker_api_shared.ttypes.ErrorCode.GENERAL,
353407
msg)
354408

409+
# Check if the database is already in use by another product.
410+
db_in_use = self.__server.is_database_used(product)
411+
if db_in_use:
412+
LOG.error("Database '%s' is already in use by another product!",
413+
product.connection.database)
414+
raise codechecker_api_shared.ttypes.RequestFailed(
415+
codechecker_api_shared.ttypes.ErrorCode.DATABASE,
416+
"Database is already in use by another product!")
417+
418+
# Add database before letting product connect to it
419+
if self.__add_product_support(product):
420+
LOG.info("Database support added successfully.")
421+
355422
# Some values come encoded as Base64, decode these.
356423
displayed_name = convert.from_b64(product.displayedName_b64) \
357424
if product.displayedName_b64 else product.endpoint

web/server/codechecker_server/server.py

+32
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import multiprocess
2727
from sqlalchemy.orm import sessionmaker
28+
from sqlalchemy.engine.url import make_url
2829
from sqlalchemy.sql.expression import func
2930
from thrift.protocol import TJSONProtocol
3031
from thrift.transport import TTransport
@@ -753,6 +754,9 @@ def __init__(self,
753754
permissions.initialise_defaults('SYSTEM', {
754755
'config_db_session': cfg_sess
755756
})
757+
758+
self.cfg_sess_private = cfg_sess
759+
756760
products = cfg_sess.query(ORMProduct).all()
757761
for product in products:
758762
self.add_product(product)
@@ -896,6 +900,34 @@ def add_product(self, orm_product, init_db=False):
896900

897901
self.__products[prod.endpoint] = prod
898902

903+
def is_database_used(self, conn):
904+
"""
905+
Returns bool whether the given database is already connected to by
906+
the server.
907+
"""
908+
909+
# get the database name from the database connection args
910+
conn = make_url(conn.connection)
911+
is_sqlite = conn.engine == 'sqlite'
912+
913+
# create a tuple of database that is going to be added for comparison
914+
to_add = (f"{conn.engine}+pysqlite" if is_sqlite
915+
else f"{conn.engine}+psycopg2",
916+
conn.database, conn.host, conn.port)
917+
918+
# dynamic_list contains the currently connected databases to servers
919+
dynamic_list = [(make_url(a.connection).drivername,
920+
make_url(a.connection).database,
921+
make_url(a.connection).host,
922+
make_url(a.connection).port)
923+
for a in self.cfg_sess_private.query(ORMProduct).all()]
924+
925+
self.cfg_sess_private.commit()
926+
self.cfg_sess_private.close()
927+
928+
# True if found, False otherwise
929+
return to_add in dynamic_list
930+
899931
@property
900932
def num_products(self):
901933
"""

web/server/vue-cli/src/views/NewFeatures.vue

+3-7
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
<!-- eslint-disable max-len -->
33
<v-container fluid>
44
<v-timeline align-top>
5-
6-
75
<v-timeline-item fill-dot icon="mdi-star">
86
<new-release-item>
97
<template v-slot:title>
@@ -42,20 +40,18 @@
4240
<template v-slot:title>
4341
New Static HTML Report Pages
4442
</template>
45-
The static HTML generation is rewritten so it can handle much larger result set.
43+
The static HTML generation is rewritten so it can handle much larger result set.
4644
</new-feature-item>
4745

4846
<new-feature-item>
4947
<template v-slot:title>
5048
New report filter to list closed and outstanding reports
5149
</template>
52-
A new filter has been added to list outstanding and closed reports. An outstanding report is a report with detection status new, reopened, unresolved with review status unreviewed or confirmed.
50+
A new filter has been added to list outstanding and closed reports. An outstanding report is a report with detection status new, reopened, unresolved with review status unreviewed or confirmed.
5351
</new-feature-item>
54-
5552
</new-release-item>
5653
</v-timeline-item>
5754

58-
5955
<v-timeline-item fill-dot icon="mdi-star" color="green lighten-1">
6056
<new-release-item color="green lighten-1">
6157
<template v-slot:title>
@@ -2443,4 +2439,4 @@ export default {
24432439
NewReleaseItem
24442440
}
24452441
};
2446-
</script>
2442+
</script>

web/tests/functional/products/test_config_db_share.py

+9-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import unittest
2222

2323
from codechecker_api_shared.ttypes import Permission
24+
from codechecker_api_shared.ttypes import RequestFailed
2425

2526
from codechecker_api.ProductManagement_v6.ttypes import ProductConfiguration
2627
from codechecker_api.ProductManagement_v6.ttypes import DatabaseConnection
@@ -154,8 +155,8 @@ def create_test_product(product_name, product_endpoint):
154155
description_b64=name,
155156
connection=DatabaseConnection(
156157
engine='sqlite',
157-
host='',
158-
port=0,
158+
host=None,
159+
port=None,
159160
username_b64='',
160161
password_b64='',
161162
database=os.path.join(self.test_workspace_secondary,
@@ -169,8 +170,10 @@ def create_test_product(product_name, product_endpoint):
169170

170171
product_cfg = create_test_product('producttest_second 2',
171172
'producttest_second_2')
172-
self.assertTrue(self._pr_client_2.addProduct(product_cfg),
173-
"Cannot create product on secondary server.")
173+
174+
# expect request to fail, cannot connect 2 products to 1 database
175+
with self.assertRaises(RequestFailed):
176+
self._pr_client_2.addProduct(product_cfg)
174177

175178
# Product name full string match.
176179
products = self._pr_client_2.getProducts('producttest_second', None)
@@ -182,10 +185,10 @@ def create_test_product(product_name, product_endpoint):
182185

183186
# Product name substring match.
184187
products = self._pr_client_2.getProducts('producttest_second*', None)
185-
self.assertEqual(len(products), 2)
188+
self.assertEqual(len(products), 1)
186189

187190
products = self._pr_client_2.getProducts(None, 'producttest_second*')
188-
self.assertEqual(len(products), 2)
191+
self.assertEqual(len(products), 1)
189192

190193
# Use the same CodeChecker config that was used on the main server,
191194
# but store into the secondary one.

web/tests/functional/products/test_products.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
"""
1313

1414

15-
from copy import deepcopy
15+
# from copy import deepcopy
1616
import os
1717
import unittest
1818

@@ -93,10 +93,10 @@ def test_add_invalid_product(self):
9393

9494
# Test setting up product with valid endpoint but no database
9595
# connection.
96-
with self.assertRaises(RequestFailed):
97-
cfg = deepcopy(product_cfg)
98-
cfg.endpoint = "valid"
99-
self._root_client.addProduct(cfg)
96+
# with self.assertRaises(RequestFailed):
97+
# cfg = deepcopy(product_cfg)
98+
# cfg.endpoint = "valid"
99+
# self._root_client.addProduct(cfg)
100100

101101
# Test some invalid strings based on pattern.
102102
dbc = DatabaseConnection(

web/tests/functional/store/test_store.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,12 @@ def setup_class(self):
7272

7373
server_access['viewer_product'] = 'store_limited_product'
7474
codechecker.add_test_package_product(server_access, TEST_WORKSPACE,
75-
report_limit=2)
75+
report_limit=2,
76+
database_name='store_limited')
7677

7778
server_access['viewer_product'] = 'store_test'
78-
codechecker.add_test_package_product(server_access, TEST_WORKSPACE)
79+
codechecker.add_test_package_product(server_access, TEST_WORKSPACE,
80+
database_name='store_test')
7981

8082
# Extend the checker configuration with the server access.
8183
codechecker_cfg.update(server_access)

web/tests/libtest/codechecker.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,8 @@ def start_server_proc(event, server_cmd, checking_env):
733733

734734
def add_test_package_product(server_data, test_folder, check_env=None,
735735
protocol='http', report_limit=None,
736-
user_permissions=None):
736+
user_permissions=None,
737+
database_name="data.sqlite"):
737738
"""
738739
Add a product for a test suite to the server provided by server_data.
739740
Server must be running before called.
@@ -781,7 +782,7 @@ def add_test_package_product(server_data, test_folder, check_env=None,
781782
else:
782783
# SQLite databases are put under the workspace of the appropriate test.
783784
add_command += ['--sqlite',
784-
os.path.join(test_folder, 'data.sqlite')]
785+
os.path.join(test_folder, database_name)]
785786

786787
print(' '.join(add_command))
787788

0 commit comments

Comments
 (0)