postgresql-charms-single-kernel 16.1.4__py3-none-any.whl → 16.1.6__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.3
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.1.4
3
+ Version: 16.1.6
4
4
  Summary: Shared and reusable code for PostgreSQL-related charms
5
5
  Author: Canonical Data Platform
6
6
  Author-email: Canonical Data Platform <data-platform@lists.launchpad.net>
@@ -4,8 +4,8 @@ single_kernel_postgresql/charmcraft.yaml,sha256=TbqNxQtjeAB-xPLpZXOnolxEnP75Rmgb
4
4
  single_kernel_postgresql/config/__init__.py,sha256=k9Ud5ZZNd3l0nn8xi8AIlT45oZk8hJ542BjAFGDJzuc,140
5
5
  single_kernel_postgresql/config/literals.py,sha256=88r33TBX7SXqgpV6EEXtZmSJrfOtsFiVJ_omop0RuBo,643
6
6
  single_kernel_postgresql/utils/__init__.py,sha256=VwAEW3wYjs99q38bid47aS6Os4s3catK4H5HfdPkp94,121
7
- single_kernel_postgresql/utils/filesystem.py,sha256=CJ2iXqFPKM0NfqqZSTDlENOrM0RW7rmTmcuzwV0bR9A,552
8
- single_kernel_postgresql/utils/postgresql.py,sha256=KMA9QYCuRXTsge5Wgj4H7p5Vd0XKLY0QsZGueF0MqCA,81209
9
- postgresql_charms_single_kernel-16.1.4.dist-info/WHEEL,sha256=ePp19eyRASTHUPCpRsqjeFjsybXoCLYnd2O6QHdRRAY,79
10
- postgresql_charms_single_kernel-16.1.4.dist-info/METADATA,sha256=00Edrj_dZxlZ-9G9O5zGM77psTx6dQkp4WN8qjOwxcM,13662
11
- postgresql_charms_single_kernel-16.1.4.dist-info/RECORD,,
7
+ single_kernel_postgresql/utils/filesystem.py,sha256=7sMR64DavtuIKlwf8p5UWG5l1x0B3NgCSV8Yg52mpb4,1832
8
+ single_kernel_postgresql/utils/postgresql.py,sha256=ZhyBU47F2idaLCzOY41ypgkUKeBoXv_-hd1tWLhPnTw,83440
9
+ postgresql_charms_single_kernel-16.1.6.dist-info/WHEEL,sha256=RRVLqVugUmFOqBedBFAmA4bsgFcROUBiSUKlERi0Hcg,79
10
+ postgresql_charms_single_kernel-16.1.6.dist-info/METADATA,sha256=c0HwkwL4DEnZDNqiCDIryxrhhL0z17tu7gB_d9aeWYw,13662
11
+ postgresql_charms_single_kernel-16.1.6.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.9.14
2
+ Generator: uv 0.9.21
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -18,3 +18,45 @@ def change_owner(path: str) -> None:
18
18
  user_database = pwd.getpwnam(SNAP_USER)
19
19
  # Set the correct ownership for the file or directory.
20
20
  os.chown(path, uid=user_database.pw_uid, gid=user_database.pw_gid)
21
+
22
+
23
+ def is_tmpfs(path: str) -> bool:
24
+ """Check if a path is on a tmpfs filesystem.
25
+
26
+ This function reads /proc/mounts to determine the filesystem type of the
27
+ mount point containing the given path.
28
+
29
+ Args:
30
+ path: Path to check.
31
+
32
+ Returns:
33
+ True if the path is on a tmpfs filesystem, False otherwise.
34
+ Returns False if /proc/mounts cannot be read (e.g., not on Linux).
35
+ """
36
+ try:
37
+ with open("/proc/mounts") as f:
38
+ mounts = f.readlines()
39
+ except (FileNotFoundError, PermissionError):
40
+ # Not on Linux or /proc not available, assume persistent storage
41
+ return False
42
+
43
+ # Get absolute path to handle relative paths and symlinks
44
+ abs_path = os.path.abspath(path)
45
+
46
+ # Find the longest matching mount point (most specific)
47
+ best_match_fs = None
48
+ best_match_len = 0
49
+
50
+ for line in mounts:
51
+ parts = line.split()
52
+ if len(parts) < 3:
53
+ continue
54
+ mount_point = parts[1]
55
+ fs_type = parts[2]
56
+
57
+ # Check if path is under this mount point and is more specific
58
+ if abs_path.startswith(mount_point) and len(mount_point) > best_match_len:
59
+ best_match_fs = fs_type
60
+ best_match_len = len(mount_point)
61
+
62
+ return best_match_fs == "tmpfs"
@@ -37,7 +37,7 @@ from ..config.literals import (
37
37
  SYSTEM_USERS,
38
38
  Substrates,
39
39
  )
40
- from .filesystem import change_owner
40
+ from .filesystem import change_owner, is_tmpfs
41
41
 
42
42
  # Groups to distinguish HBA access
43
43
  ACCESS_GROUP_IDENTITY = "identity_access"
@@ -644,6 +644,7 @@ class PostgreSQL:
644
644
  with self._connect_to_database(
645
645
  database
646
646
  ) as connection, connection.cursor() as cursor:
647
+ cursor.execute(SQL("RESET ROLE;"))
647
648
  cursor.execute(
648
649
  SQL("REASSIGN OWNED BY {} TO {};").format(
649
650
  Identifier(user), Identifier(self.user)
@@ -653,6 +654,7 @@ class PostgreSQL:
653
654
 
654
655
  # Delete the user.
655
656
  with self._connect_to_database() as connection, connection.cursor() as cursor:
657
+ cursor.execute(SQL("RESET ROLE;"))
656
658
  cursor.execute(SQL("DROP ROLE {};").format(Identifier(user)))
657
659
  except psycopg2.Error as e:
658
660
  logger.error(f"Failed to delete user: {e}")
@@ -1107,8 +1109,64 @@ class PostgreSQL:
1107
1109
  connection.close()
1108
1110
  return databases
1109
1111
 
1112
+ def _handle_temp_tablespace_on_reboot(
1113
+ self, cursor, temp_location: str, temp_tablespace_exists: bool
1114
+ ) -> None:
1115
+ """Handle temp tablespace when permissions need fixing after reboot.
1116
+
1117
+ Args:
1118
+ cursor: Database cursor.
1119
+ temp_location: Path to the temp tablespace location.
1120
+ temp_tablespace_exists: Whether the temp tablespace already exists.
1121
+ """
1122
+ if not temp_tablespace_exists:
1123
+ return
1124
+
1125
+ # Different handling based on storage type
1126
+ if is_tmpfs(temp_location):
1127
+ # tmpfs: Directory is empty after reboot, safe to rename and recreate
1128
+ # Rename existing temp tablespace instead of dropping it.
1129
+ # Timestamp collision is not possible: the charm ensures this code runs leader-only,
1130
+ # and it executes within a single database transaction holding exclusive locks.
1131
+ new_name = f"temp_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
1132
+ cursor.execute(f"ALTER TABLESPACE temp RENAME TO {new_name};")
1133
+
1134
+ # List temp tablespaces with suffix for operator follow-up cleanup
1135
+ cursor.execute("SELECT spcname FROM pg_tablespace WHERE spcname LIKE 'temp_%';")
1136
+ temp_tbls = sorted([row[0] for row in cursor.fetchall()])
1137
+ logger.info(
1138
+ "There are %d temp tablespaces that should be checked and removed: %s",
1139
+ len(temp_tbls),
1140
+ ", ".join(temp_tbls),
1141
+ )
1142
+ else:
1143
+ # Persistent storage: Tablespace is still valid, permissions already fixed
1144
+ # Log that we fixed permissions but didn't recreate
1145
+ logger.info(
1146
+ "Fixed permissions on temp tablespace directory at %s (persistent storage), "
1147
+ "existing tablespace remains valid",
1148
+ temp_location,
1149
+ )
1150
+
1110
1151
  def set_up_database(self, temp_location: Optional[str] = None) -> None:
1111
- """Set up postgres database with the right permissions."""
1152
+ """Set up postgres database with the right permissions.
1153
+
1154
+ This method configures the postgres database with appropriate permissions and
1155
+ optionally creates a temporary tablespace.
1156
+
1157
+ Args:
1158
+ temp_location: Optional path for the temp tablespace. If provided, the method
1159
+ will ensure proper permissions and create the tablespace if it doesn't exist.
1160
+
1161
+ Behavior on reboot:
1162
+ - For tmpfs storage: If permissions are incorrect after reboot, renames the old
1163
+ tablespace and creates a new one (tmpfs directory is empty after reboot).
1164
+ - For persistent storage: If permissions are incorrect after reboot, fixes
1165
+ permissions but keeps the existing tablespace (directory contents persist).
1166
+
1167
+ Raises:
1168
+ PostgreSQLDatabasesSetupError: If database setup fails.
1169
+ """
1112
1170
  connection = None
1113
1171
  cursor = None
1114
1172
  try:
@@ -1116,30 +1174,23 @@ class PostgreSQL:
1116
1174
  cursor = connection.cursor()
1117
1175
 
1118
1176
  if temp_location is not None:
1119
- # Fix permissions on the temporary tablespace location when a reboot happens and tmpfs is being used.
1177
+ # Fix permissions on the temporary tablespace location when a reboot happens.
1120
1178
  temp_location_stats = os.stat(temp_location)
1121
- if self.substrate == Substrates.VM and (
1179
+ permissions_need_fix = self.substrate == Substrates.VM and (
1122
1180
  pwd.getpwuid(temp_location_stats.st_uid).pw_name != SNAP_USER
1123
1181
  or int(temp_location_stats.st_mode & 0o777) != POSTGRESQL_STORAGE_PERMISSIONS
1124
- ):
1182
+ )
1183
+
1184
+ if permissions_need_fix:
1125
1185
  change_owner(temp_location)
1126
1186
  os.chmod(temp_location, POSTGRESQL_STORAGE_PERMISSIONS)
1127
- # Rename existing temp tablespace if it exists, instead of dropping it.
1128
- cursor.execute("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';")
1129
- if cursor.fetchone() is not None:
1130
- new_name = f"temp_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
1131
- cursor.execute(f"ALTER TABLESPACE temp RENAME TO {new_name};")
1132
1187
 
1133
- # List temp tablespaces with suffix for operator follow-up cleanup and log them.
1134
- cursor.execute(
1135
- "SELECT spcname FROM pg_tablespace WHERE spcname LIKE 'temp_%';"
1136
- )
1137
- temp_tbls = sorted([row[0] for row in cursor.fetchall()])
1138
- logger.info(
1139
- "There are %d temp tablespaces that should be checked and removed: %s",
1140
- len(temp_tbls),
1141
- ", ".join(temp_tbls),
1142
- )
1188
+ # Check if temp tablespace exists and handle it appropriately
1189
+ cursor.execute("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';")
1190
+ temp_tablespace_exists = cursor.fetchone() is not None
1191
+ self._handle_temp_tablespace_on_reboot(
1192
+ cursor, temp_location, temp_tablespace_exists
1193
+ )
1143
1194
 
1144
1195
  # Ensure a fresh temp tablespace exists at the expected location.
1145
1196
  cursor.execute("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';")