Skip to content

Commit f756431

Browse files
Merge pull request #17 from rustprooflabs/improve-new-config
Rework comparisons to use `pg_settings` data
2 parents d386c41 + 851948d commit f756431

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+843
-4097
lines changed

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
webapp/__pycache__/
21
pytest.xml
3-
tests/__pycache__/
2+
**/__pycache__/
43
docs/_build/
54
.coverage
65
*.log
6+
**/.ipynb_checkpoints
7+
*.ipynb

.pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ confidence=
5959
# --enable=similarities". If you want to run only the classes checker, but have
6060
# no Warning level messages displayed, use"--disable=all --enable=classes
6161
# --disable=W"
62-
disable=import-star-module-level,old-octal-literal,oct-method,print-statement,unpacking-in-except,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
62+
#disable=old-octal-literal,parameter-unpacking,backtick,old-raise-syntax,old-ne-operator,long-suffix,dict-view-method,dict-iter-method,metaclass-assignment,next-method-called,raising-string,indexing-exception,raw_input-builtin,long-builtin,file-builtin,execfile-builtin,coerce-builtin,cmp-builtin,buffer-builtin,basestring-builtin,apply-builtin,filter-builtin-not-iterating,using-cmp-argument,useless-suppression,range-builtin-not-iterating,suppressed-message,no-absolute-import,old-division,cmp-method,reload-builtin,zip-builtin-not-iterating,intern-builtin,unichr-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,input-builtin,round-builtin,hex-method,nonzero-method,map-builtin-not-iterating
6363

6464

6565
[REPORTS]

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2018 - 2023 Ryan Lambert
3+
Copyright (c) 2018 - 2024 Ryan Lambert
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,43 @@ python run_server.py
3333

3434
## Add new config files
3535

36-
To add a new configuration version, build target Postgres version from source. [Example in post](https://blog.rustprooflabs.com/2019/07/postgresql-postgis-install-from-source-raspberry-pi). Take the contents of the `postgresql.conf` file,
37-
clean out all comments, uncomment all default GUCs. Place in `webapp/config`
38-
and update `VERSIONS` list ini `pgconfig.py`.
39-
40-
Example of getting configuration after building from source.
36+
To add a new configuration version you need a Postgres database instance running
37+
that you can connect to. Activate the Python venv and start `ipython`.
4138

4239
```bash
43-
cat /usr/local/pgsql/data/postgresql.conf
40+
source ~/venv/pgconfig/bin/activate
41+
cd ~/git/pgconfig-ce/config_from_pg
42+
ipython
43+
```
44+
45+
Import
46+
```python
47+
import generate
48+
```
49+
50+
You'll be prompted for the database connection parameters. Ideally you are using
51+
a `~/.pgpass` file, but the option is there to enter your password.
52+
4453
```
54+
Database host [127.0.0.1]:
55+
Database port [5432]:
56+
Database name: postgres
57+
Enter PgSQL username: your_username
58+
Enter password (empty for pgpass):
59+
```
60+
61+
Run the generation. Will create a file in the appropriate spot for the webapp.
62+
When adding a new version you need to add it to `webapp/pgconfig.py` as well
63+
as generating this file.
64+
65+
```python
66+
generate.run()
67+
```
68+
69+
Preparing database objects...
70+
Database objects ready.
71+
Pickled config data saved to: ../webapp/config/pg15.pkl
72+
4573

4674

4775
## Unit tests
@@ -74,11 +102,12 @@ webapp/routes.py 83 58 30% 20, 24, 30, 35, 40-43, 51, 56-75, 87,
74102
TOTAL 279 95 66%
75103
```
76104

105+
## Pylint
77106

78107
Run pylint.
79108

80109
```
81-
pylint --rcfile=./.pylintrc -f parseable ./webapp/*.py
110+
pylint --rcfile=./.pylintrc -f parseable ./webapp/*.py ./config_from_pg/*.py
82111
```
83112

84113
## History

config_from_pg/__init__.py

Whitespace-only changes.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
2+
DROP VIEW IF EXISTS pgconfig.settings;
3+
CREATE VIEW pgconfig.settings AS
4+
SELECT name, setting, unit, context, source, category,
5+
reset_val, boot_val,
6+
CASE WHEN vartype IN ('string', 'enum')
7+
THEN
8+
name || ' = ' || CHR(39) || current_setting(name) || CHR(39)
9+
ELSE
10+
name || ' = ' || current_setting(name)
11+
END AS postgresconf_line,
12+
name || ' = ' || CHR(39) ||
13+
-- Recalculates 8kB units to more comprehensible kB units.
14+
-- above line gets to use the current_setting() func, didn't find any
15+
-- such option for this one
16+
CASE WHEN boot_val = '-1' THEN boot_val
17+
WHEN unit = '8kB' THEN ((boot_val::numeric * 8)::BIGINT)::TEXT
18+
ELSE boot_val
19+
END
20+
|| COALESCE(CASE WHEN boot_val = '-1' THEN NULL
21+
WHEN unit = '8kB' THEN 'kB'
22+
ELSE unit
23+
END, '') || CHR(39)
24+
AS default_config_line,
25+
short_desc,
26+
CASE WHEN name LIKE 'lc%'
27+
THEN True
28+
WHEN name LIKE 'unix%'
29+
THEN True
30+
WHEN name IN ('application_name', 'TimeZone', 'timezone_abbreviations',
31+
'default_text_search_config')
32+
THEN True
33+
WHEN category IN ('File Locations')
34+
THEN True
35+
ELSE False
36+
END AS frequent_override,
37+
CASE WHEN boot_val = setting THEN True
38+
ELSE False
39+
END AS system_default,
40+
CASE WHEN reset_val = setting THEN False
41+
ELSE True
42+
END AS session_override,
43+
pending_restart,
44+
vartype, min_val, max_val, enumvals
45+
FROM pg_catalog.pg_settings
46+
;
47+
48+
COMMENT ON COLUMN pgconfig.settings.postgresconf_line IS 'Current configuration in format suitable for postgresql.conf. All setting values quoted in single quotes since that always works, and omitting the quotes does not. Uses pg_catalog.current_setting() which converts settings into sensible units for display.';
49+
COMMENT ON COLUMN pgconfig.settings.default_config_line IS 'Postgres default configuration for setting. Some are hard coded, some are determined at build time.';
50+
COMMENT ON COLUMN pgconfig.settings.setting IS 'Raw setting value in the units defined in the "units" column.';

config_from_pg/db.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Database helper module.
2+
Modified from https://gist.github.com/rustprooflabs/3b8564a8e7b7fe611436b30a95b7cd17,
3+
adapted to psycopg 3 from psycopg2.
4+
"""
5+
import getpass
6+
import psycopg
7+
8+
9+
def prepare():
10+
"""Ensures latest `pgconfig.settings` view exists in DB to generate config.
11+
"""
12+
print('Preparing database objects...')
13+
ensure_schema_exists()
14+
ensure_view_exists()
15+
print('Database objects ready.')
16+
17+
18+
def ensure_schema_exists():
19+
"""Ensures the `pgconfig` schema exists."""
20+
sql_raw = 'CREATE SCHEMA IF NOT EXISTS pgconfig;'
21+
_execute_query(sql_raw, params=None, qry_type='ddl')
22+
23+
24+
def ensure_view_exists():
25+
"""Ensures the view `pgconfig.settings` exists."""
26+
sql_file = 'create_pgconfig_settings_view.sql'
27+
with open(sql_file) as f:
28+
sql_raw = f.read()
29+
30+
_execute_query(sql_raw, params=None, qry_type='ddl')
31+
32+
33+
def select_one(sql_raw: str, params: dict) -> dict:
34+
""" Runs SELECT query that will return zero or 1 rows.
35+
36+
Parameters
37+
-----------------
38+
sql_raw : str
39+
params : dict
40+
Params is required, can be `None` if query returns a single row
41+
such as `SELECT version();`
42+
43+
Returns
44+
-----------------
45+
data : dict
46+
"""
47+
return _execute_query(sql_raw, params, 'sel_single')
48+
49+
50+
def select_multi(sql_raw, params=None) -> list:
51+
""" Runs SELECT query that will return multiple. `params` is optional.
52+
53+
Parameters
54+
-----------------
55+
sql_raw : str
56+
params : dict
57+
Params is optional, defaults to `None`.
58+
59+
Returns
60+
------------------
61+
data : list
62+
List of dictionaries.
63+
"""
64+
return _execute_query(sql_raw, params, 'sel_multi')
65+
66+
67+
def get_db_string() -> str:
68+
"""Prompts user for details to create connection string
69+
70+
Returns
71+
------------------------
72+
database_string : str
73+
"""
74+
db_host = input('Database host [127.0.0.1]: ') or '127.0.0.1'
75+
db_port = input('Database port [5432]: ') or '5432'
76+
db_name = input('Database name: ')
77+
db_user = input('Enter PgSQL username: ')
78+
db_pw = getpass.getpass('Enter password (empty for pgpass): ') or None
79+
80+
if db_pw is None:
81+
database_string = 'postgresql://{user}@{host}:{port}/{dbname}'
82+
else:
83+
database_string = 'postgresql://{user}:{pw}@{host}:{port}/{dbname}'
84+
85+
return database_string.format(user=db_user, pw=db_pw, host=db_host,
86+
port=db_port, dbname=db_name)
87+
88+
DB_STRING = get_db_string()
89+
90+
def get_db_conn():
91+
"""Uses DB_STRING to establish psycopg connection."""
92+
db_string = DB_STRING
93+
94+
try:
95+
conn = psycopg.connect(db_string)
96+
except psycopg.OperationalError as err:
97+
err_msg = f'DB Connection Error - Error: {err}'
98+
print(err_msg)
99+
return False
100+
101+
return conn
102+
103+
104+
def _execute_query(sql_raw, params, qry_type):
105+
""" Handles executing all types of queries based on the `qry_type` passed in.
106+
Returns False if there are errors during connection or execution.
107+
if results == False:
108+
print('Database error')
109+
else:
110+
print(results)
111+
You cannot use `if not results:` b/c 0 results is a false negative.
112+
"""
113+
try:
114+
conn = get_db_conn()
115+
except psycopg.ProgrammingError as err:
116+
print(f'Connection not configured properly. Err: {err}')
117+
return False
118+
119+
if not conn:
120+
return False
121+
122+
cur = conn.cursor(row_factory=psycopg.rows.dict_row)
123+
124+
try:
125+
cur.execute(sql_raw, params)
126+
if qry_type == 'sel_single':
127+
results = cur.fetchone()
128+
elif qry_type == 'sel_multi':
129+
results = cur.fetchall()
130+
elif qry_type == 'ddl':
131+
conn.commit()
132+
results = True
133+
else:
134+
raise Exception('Invalid query type defined.')
135+
136+
except psycopg.BINARYProgrammingError as err:
137+
print('Database error via psycopg. %s', err)
138+
results = False
139+
except psycopg.IntegrityError as err:
140+
print('PostgreSQL integrity error via psycopg. %s', err)
141+
results = False
142+
finally:
143+
conn.close()
144+
145+
return results

config_from_pg/generate.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""Generates config based on pgconfig.settings and pickles for reuse in webapp.
2+
3+
This code is expected to be used on Postgres 10 and newer.
4+
"""
5+
import pickle
6+
import db
7+
8+
9+
def run():
10+
"""Saves pickled config data from defined database connection.
11+
"""
12+
db.prepare()
13+
pg_version_num = get_pg_version_num()
14+
pg_config_data = get_config_data()
15+
save_config_data(data=pg_config_data, pg_version_num=pg_version_num)
16+
17+
18+
def get_pg_version_num() -> int:
19+
"""Returns the Postgres version number as an integer.
20+
21+
Expected to be used on Postgres 10 and newer only.
22+
23+
Returns
24+
------------------------
25+
pg_version_num : int
26+
"""
27+
sql_raw = """SELECT current_setting('server_version_num')::BIGINT / 10000
28+
AS pg_version_num;
29+
"""
30+
results = db.select_one(sql_raw, params=None)
31+
pg_version_num = results['pg_version_num']
32+
return pg_version_num
33+
34+
35+
def get_config_data() -> list:
36+
"""Query Postgres for data about default settings.
37+
38+
Returns
39+
--------------------
40+
results : list
41+
"""
42+
sql_raw = """
43+
SELECT default_config_line, name, unit, context, category,
44+
boot_val, short_desc, frequent_override,
45+
vartype, min_val, max_val, enumvals,
46+
boot_val || COALESCE(' ' || unit, '') AS boot_val_display
47+
FROM pgconfig.settings
48+
/* Excluding read-only present options. Not included in delivered
49+
postgresql.conf files per docs:
50+
https://www.postgresql.org/docs/current/runtime-config-preset.html
51+
*/
52+
WHERE category != 'Preset Options'
53+
/* Configuration options that typically are customized such as
54+
application_name do not make sense to compare against "defaults"
55+
*/
56+
AND NOT frequent_override
57+
ORDER BY name
58+
;
59+
"""
60+
results = db.select_multi(sql_raw)
61+
return results
62+
63+
def save_config_data(data: list, pg_version_num: int):
64+
"""Pickles config data for reuse later.
65+
66+
Parameters
67+
----------------------
68+
data : list
69+
List of dictionaries to pickle.
70+
71+
pg_version_num : int
72+
Integer of Postgres version.
73+
"""
74+
filename = f'../webapp/config/pg{pg_version_num}.pkl'
75+
with open(filename, 'wb') as data_file:
76+
pickle.dump(data, data_file)
77+
print(f'Pickled config data saved to: {filename}')
78+
79+
80+
if __name__ == "__main__":
81+
run()

docs/__init__.py

Whitespace-only changes.

docs/conf.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
# -- Project information -----------------------------------------------------
1919

2020
project = 'pgConfig'
21-
copyright = '2018 - 2020, Ryan Lambert, RustProof Labs'
21+
copyright = '2018 - 2024, Ryan Lambert, RustProof Labs'
2222
author = 'Ryan Lambert, RustProof Labs'
2323

24-
version = '0.0.5'
24+
version = '0.1.0'
2525

2626

2727
# -- General configuration ---------------------------------------------------

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Jinja2==3.1.2
77
MarkupSafe==2.1.3
88
numpy==1.26.1
99
pandas==2.1.2
10+
psycopg==3.1.16
11+
psycopg-binary==3.1.16
1012
pylint==2.17.5
1113
pytest==7.4.3
1214
python-dateutil==2.8.2

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)