postgresql-charms-single-kernel 16.0.2__py3-none-any.whl → 16.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.0.2
3
+ Version: 16.1.1
4
4
  Summary: Shared and reusable code for PostgreSQL-related charms
5
5
  Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
6
6
  License-Expression: Apache-2.0
@@ -0,0 +1,11 @@
1
+ single_kernel_postgresql/__init__.py,sha256=F57JUcML43xgR0kpj9csryZkAoDSBPLLRjzSTHPaTd4,116
2
+ single_kernel_postgresql/abstract_charm.py,sha256=jWYV7nTZLiwxCZEedMZwyXzU3iNDRUcz5ZdI4LGUkUw,841
3
+ single_kernel_postgresql/config/__init__.py,sha256=k9Ud5ZZNd3l0nn8xi8AIlT45oZk8hJ542BjAFGDJzuc,140
4
+ single_kernel_postgresql/config/literals.py,sha256=88r33TBX7SXqgpV6EEXtZmSJrfOtsFiVJ_omop0RuBo,643
5
+ single_kernel_postgresql/utils/__init__.py,sha256=VwAEW3wYjs99q38bid47aS6Os4s3catK4H5HfdPkp94,121
6
+ single_kernel_postgresql/utils/filesystem.py,sha256=CJ2iXqFPKM0NfqqZSTDlENOrM0RW7rmTmcuzwV0bR9A,552
7
+ single_kernel_postgresql/utils/postgresql.py,sha256=IiEOFzrpiDXRWgwnvywiul9f8gU8hxCBaSqVFUyFgjU,79617
8
+ postgresql_charms_single_kernel-16.1.1.dist-info/METADATA,sha256=UD9GAm4GA4tQKAaWsDiR5uyN-k921jq2nt8Uwq6sqsE,484
9
+ postgresql_charms_single_kernel-16.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ postgresql_charms_single_kernel-16.1.1.dist-info/top_level.txt,sha256=fH85HKyfDV3--1JuYe-rWJmN5XTlXVXGOBO5iNA36Rk,25
11
+ postgresql_charms_single_kernel-16.1.1.dist-info/RECORD,,
@@ -4,7 +4,7 @@
4
4
 
5
5
  from ops.charm import CharmBase
6
6
 
7
- from .config.literals import SYSTEM_USERS, USER
7
+ from .config.literals import SYSTEM_USERS, USER, Substrates
8
8
  from .utils.postgresql import PostgreSQL
9
9
 
10
10
 
@@ -15,6 +15,7 @@ class AbstractPostgreSQLCharm(CharmBase):
15
15
  super().__init__(*args)
16
16
 
17
17
  self.postgresql = PostgreSQL(
18
+ substrate=Substrates.VM,
18
19
  primary_host="localhost",
19
20
  current_host="localhost",
20
21
  user=USER,
@@ -5,6 +5,8 @@
5
5
  This module should contain the literals used in the charms (paths, enums, etc).
6
6
  """
7
7
 
8
+ from enum import Enum
9
+
8
10
  # Permissions.
9
11
  POSTGRESQL_STORAGE_PERMISSIONS = 0o700
10
12
 
@@ -19,3 +21,10 @@ REWIND_USER = "rewind"
19
21
  SNAP_USER = "_daemon_"
20
22
  USER = "operator"
21
23
  SYSTEM_USERS = [MONITORING_USER, REPLICATION_USER, REWIND_USER, USER]
24
+
25
+
26
+ class Substrates(str, Enum):
27
+ """Possible substrates."""
28
+
29
+ K8S = "k8s"
30
+ VM = "vm"
@@ -30,7 +30,13 @@ import psycopg2
30
30
  from ops import ConfigData
31
31
  from psycopg2.sql import SQL, Identifier, Literal
32
32
 
33
- from ..config.literals import BACKUP_USER, POSTGRESQL_STORAGE_PERMISSIONS, SNAP_USER, SYSTEM_USERS
33
+ from ..config.literals import (
34
+ BACKUP_USER,
35
+ POSTGRESQL_STORAGE_PERMISSIONS,
36
+ SNAP_USER,
37
+ SYSTEM_USERS,
38
+ Substrates,
39
+ )
34
40
  from .filesystem import change_owner
35
41
 
36
42
  # Groups to distinguish HBA access
@@ -60,6 +66,7 @@ ALLOWED_ROLES = {
60
66
  }
61
67
 
62
68
  INVALID_DATABASE_NAME_BLOCKING_MESSAGE = "invalid database name"
69
+ INVALID_DATABASE_NAMES = ["databases", "postgres", "template0", "template1"]
63
70
  INVALID_EXTRA_USER_ROLE_BLOCKING_MESSAGE = "invalid role(s) for extra user roles"
64
71
 
65
72
  REQUIRED_PLUGINS = {
@@ -107,6 +114,10 @@ class PostgreSQLCreateUserError(PostgreSQLBaseError):
107
114
  self.message = message
108
115
 
109
116
 
117
+ class PostgreSQLUpdateUserError(PostgreSQLBaseError):
118
+ """Exception raised when creating a user fails."""
119
+
120
+
110
121
  class PostgreSQLUndefinedHostError(PostgreSQLBaseError):
111
122
  """Exception when host is not set."""
112
123
 
@@ -139,6 +150,10 @@ class PostgreSQLGetPostgreSQLVersionError(PostgreSQLBaseError):
139
150
  """Exception raised when retrieving PostgreSQL version fails."""
140
151
 
141
152
 
153
+ class PostgreSQLListDatabasesError(PostgreSQLBaseError):
154
+ """Exception raised when retrieving the databases."""
155
+
156
+
142
157
  class PostgreSQLListAccessibleDatabasesForUserError(PostgreSQLBaseError):
143
158
  """Exception raised when retrieving the accessible databases for a user fails."""
144
159
 
@@ -216,6 +231,7 @@ class PostgreSQL:
216
231
 
217
232
  def __init__(
218
233
  self,
234
+ substrate: Substrates,
219
235
  primary_host: Optional[str],
220
236
  current_host: Optional[str],
221
237
  user: str,
@@ -223,6 +239,18 @@ class PostgreSQL:
223
239
  database: str,
224
240
  system_users: Optional[List[str]] = None,
225
241
  ):
242
+ """Create a PostgreSQL helper.
243
+
244
+ Args:
245
+ substrate: substrate where the charm is running (Substrates.K8S or Substrates.VM).
246
+ primary_host: hostname or address for primary database host.
247
+ current_host: hostname or address for the current database host.
248
+ user: username to connect as.
249
+ password: password for the user.
250
+ database: default database name.
251
+ system_users: list of system users.
252
+ """
253
+ self.substrate = substrate
226
254
  self.primary_host = primary_host
227
255
  self.current_host = current_host
228
256
  self.user = user
@@ -322,7 +350,7 @@ class PostgreSQL:
322
350
  if len(database) > 49:
323
351
  logger.error(f"Invalid database name (it must not exceed 49 characters): {database}.")
324
352
  raise PostgreSQLCreateDatabaseError(INVALID_DATABASE_NAME_BLOCKING_MESSAGE)
325
- if database in ["postgres", "template0", "template1"]:
353
+ if database in INVALID_DATABASE_NAMES:
326
354
  logger.error(f"Invalid database name: {database}.")
327
355
  raise PostgreSQLCreateDatabaseError(INVALID_DATABASE_NAME_BLOCKING_MESSAGE)
328
356
  plugins = plugins if plugins else []
@@ -419,14 +447,28 @@ class PostgreSQL:
419
447
  Returns:
420
448
  A tuple containing the adjusted user definition and a list of additional statements.
421
449
  """
450
+ db_roles, connect_statements = self._adjust_user_roles(user, roles, database)
451
+ if db_roles:
452
+ str_roles = [f'"{role}"' for role in db_roles]
453
+ user_definition += f" IN ROLE {', '.join(str_roles)}"
454
+ return user_definition, connect_statements
455
+
456
+ def _adjust_user_roles(
457
+ self, user: str, roles: Optional[List[str]], database: Optional[str]
458
+ ) -> Tuple[List[str], List[str]]:
459
+ """Adjusts the user definition to include additional statements.
460
+
461
+ Returns:
462
+ A tuple containing the adjusted user definition and a list of additional statements.
463
+ """
464
+ db_roles = []
422
465
  connect_statements = []
423
466
  if database:
424
467
  if roles is not None and not any(
425
- True
426
- for role in roles
427
- if role in [ROLE_STATS, ROLE_READ, ROLE_DML, ROLE_BACKUP, ROLE_DBA]
468
+ role in [ROLE_STATS, ROLE_READ, ROLE_DML, ROLE_BACKUP, ROLE_DBA] for role in roles
428
469
  ):
429
- user_definition += f' IN ROLE "charmed_{database}_admin", "charmed_{database}_dml"'
470
+ db_roles.append(f"charmed_{database}_admin")
471
+ db_roles.append(f"charmed_{database}_dml")
430
472
  else:
431
473
  connect_statements.append(
432
474
  SQL("GRANT CONNECT ON DATABASE {} TO {};").format(
@@ -434,9 +476,7 @@ class PostgreSQL:
434
476
  )
435
477
  )
436
478
  if roles is not None and any(
437
- True
438
- for role in roles
439
- if role
479
+ role
440
480
  in [
441
481
  ROLE_STATS,
442
482
  ROLE_READ,
@@ -446,6 +486,7 @@ class PostgreSQL:
446
486
  ROLE_ADMIN,
447
487
  ROLE_DATABASES_OWNER,
448
488
  ]
489
+ for role in roles
449
490
  ):
450
491
  for system_database in ["postgres", "template1"]:
451
492
  connect_statements.append(
@@ -453,7 +494,7 @@ class PostgreSQL:
453
494
  Identifier(system_database), Identifier(user)
454
495
  )
455
496
  )
456
- return user_definition, connect_statements
497
+ return db_roles, connect_statements
457
498
 
458
499
  def _process_extra_user_roles(
459
500
  self, user: str, extra_user_roles: Optional[List[str]] = None
@@ -1077,7 +1118,7 @@ class PostgreSQL:
1077
1118
  if temp_location is not None:
1078
1119
  # Fix permissions on the temporary tablespace location when a reboot happens and tmpfs is being used.
1079
1120
  temp_location_stats = os.stat(temp_location)
1080
- if (
1121
+ if self.substrate == Substrates.VM and (
1081
1122
  pwd.getpwuid(temp_location_stats.st_uid).pw_name != SNAP_USER
1082
1123
  or int(temp_location_stats.st_mode & 0o777) != POSTGRESQL_STORAGE_PERMISSIONS
1083
1124
  ):
@@ -1821,3 +1862,50 @@ $$ LANGUAGE plpgsql security definer;""" # noqa: S608
1821
1862
  finally:
1822
1863
  if connection:
1823
1864
  connection.close()
1865
+
1866
+ def list_databases(self, prefix: Optional[str] = None) -> List[str]:
1867
+ """List non-system databases starting with prefix."""
1868
+ prefix_stmt = (
1869
+ SQL(" AND datname LIKE {}").format(Literal(prefix + "%")) if prefix else SQL("")
1870
+ )
1871
+ try:
1872
+ with self._connect_to_database() as connection, connection.cursor() as cursor:
1873
+ cursor.execute(
1874
+ SQL(
1875
+ "SELECT datname FROM pg_database WHERE datistemplate = false AND datname <>'postgres'{};"
1876
+ ).format(prefix_stmt)
1877
+ )
1878
+ return [row[0] for row in cursor.fetchall()]
1879
+ except psycopg2.Error as e:
1880
+ raise PostgreSQLListDatabasesError() from e
1881
+ finally:
1882
+ if connection:
1883
+ connection.close()
1884
+
1885
+ def add_user_to_databases(
1886
+ self, user: str, databases: List[str], extra_user_roles: Optional[List[str]] = None
1887
+ ) -> None:
1888
+ """Grant user access to database."""
1889
+ try:
1890
+ roles, _ = self._process_extra_user_roles(user, extra_user_roles)
1891
+ connect_stmt = []
1892
+ for database in databases:
1893
+ db_roles, db_connect_stmt = self._adjust_user_roles(user, roles, database)
1894
+ roles += db_roles
1895
+ connect_stmt += db_connect_stmt
1896
+ with self._connect_to_database() as connection, connection.cursor() as cursor:
1897
+ cursor.execute(SQL("RESET ROLE;"))
1898
+ cursor.execute(SQL("BEGIN;"))
1899
+ cursor.execute(SQL("SET LOCAL log_statement = 'none';"))
1900
+ cursor.execute(SQL("COMMIT;"))
1901
+
1902
+ # Add extra user roles to the new user.
1903
+ for role in roles:
1904
+ cursor.execute(
1905
+ SQL("GRANT {} TO {};").format(Identifier(role), Identifier(user))
1906
+ )
1907
+ for statement in connect_stmt:
1908
+ cursor.execute(statement)
1909
+ except psycopg2.Error as e:
1910
+ logger.error(f"Failed to create user: {e}")
1911
+ raise PostgreSQLUpdateUserError() from e
@@ -1,11 +0,0 @@
1
- single_kernel_postgresql/__init__.py,sha256=F57JUcML43xgR0kpj9csryZkAoDSBPLLRjzSTHPaTd4,116
2
- single_kernel_postgresql/abstract_charm.py,sha256=grlJCmpz8rq7_NKVkyuoK7_8I2r6DRkh5DhbtyPWna8,792
3
- single_kernel_postgresql/config/__init__.py,sha256=k9Ud5ZZNd3l0nn8xi8AIlT45oZk8hJ542BjAFGDJzuc,140
4
- single_kernel_postgresql/config/literals.py,sha256=4EpAnoxCEMfOCPpFD5dyAU_0h2OwAHgEX9OjTWNcaQg,527
5
- single_kernel_postgresql/utils/__init__.py,sha256=VwAEW3wYjs99q38bid47aS6Os4s3catK4H5HfdPkp94,121
6
- single_kernel_postgresql/utils/filesystem.py,sha256=CJ2iXqFPKM0NfqqZSTDlENOrM0RW7rmTmcuzwV0bR9A,552
7
- single_kernel_postgresql/utils/postgresql.py,sha256=ZhTZZcshgHjZgP4aRCWyfmW9ArUISLdqo9NnJBg1fdM,75963
8
- postgresql_charms_single_kernel-16.0.2.dist-info/METADATA,sha256=TCKebjoRLWERCEzeLgiRcX9dfZx-TLXf0CU55Ki7xik,484
9
- postgresql_charms_single_kernel-16.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
- postgresql_charms_single_kernel-16.0.2.dist-info/top_level.txt,sha256=fH85HKyfDV3--1JuYe-rWJmN5XTlXVXGOBO5iNA36Rk,25
11
- postgresql_charms_single_kernel-16.0.2.dist-info/RECORD,,