postgresql-charms-single-kernel 16.1.3__tar.gz → 16.1.5__tar.gz

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.
Files changed (13) hide show
  1. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/PKG-INFO +1 -1
  2. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/pyproject.toml +4 -4
  3. postgresql_charms_single_kernel-16.1.5/single_kernel_postgresql/utils/filesystem.py +62 -0
  4. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/utils/postgresql.py +77 -22
  5. postgresql_charms_single_kernel-16.1.3/single_kernel_postgresql/utils/filesystem.py +0 -20
  6. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/LICENSE +0 -0
  7. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/README.md +0 -0
  8. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/__init__.py +0 -0
  9. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/abstract_charm.py +0 -0
  10. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/charmcraft.yaml +0 -0
  11. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/config/__init__.py +0 -0
  12. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/config/literals.py +0 -0
  13. {postgresql_charms_single_kernel-16.1.3 → postgresql_charms_single_kernel-16.1.5}/single_kernel_postgresql/utils/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: postgresql-charms-single-kernel
3
- Version: 16.1.3
3
+ Version: 16.1.5
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,7 +4,7 @@
4
4
  [project]
5
5
  name = "postgresql-charms-single-kernel"
6
6
  description = "Shared and reusable code for PostgreSQL-related charms"
7
- version = "16.1.3"
7
+ version = "16.1.5"
8
8
  readme = "README.md"
9
9
  license = {file = "LICENSE"}
10
10
  authors = [
@@ -31,15 +31,15 @@ lib = [
31
31
  "psycopg2>=2.9.10",
32
32
  ]
33
33
  format = [
34
- "ruff==0.14.5"
34
+ "ruff==0.14.9"
35
35
  ]
36
36
  lint = [
37
37
  "codespell==2.4.1",
38
38
  "pyright==1.1.407"
39
39
  ]
40
40
  unit = [
41
- "coverage[toml]==7.11.3; python_version >= '3.10'",
42
- "pytest==9.0.1; python_version >= '3.10'"
41
+ "coverage[toml]==7.13.0; python_version >= '3.10'",
42
+ "pytest==9.0.2; python_version >= '3.10'"
43
43
  ]
44
44
 
45
45
  # Testing tools configuration
@@ -0,0 +1,62 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+ """Filesystem utilities."""
4
+
5
+ import os
6
+ import pwd
7
+
8
+ from ..config.literals import SNAP_USER
9
+
10
+
11
+ def change_owner(path: str) -> None:
12
+ """Change the ownership of a file or a directory to the snap user.
13
+
14
+ Args:
15
+ path: path to a file or directory.
16
+ """
17
+ # Get the uid/gid for the snap user.
18
+ user_database = pwd.getpwnam(SNAP_USER)
19
+ # Set the correct ownership for the file or directory.
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"
@@ -1107,8 +1107,64 @@ class PostgreSQL:
1107
1107
  connection.close()
1108
1108
  return databases
1109
1109
 
1110
+ def _handle_temp_tablespace_on_reboot(
1111
+ self, cursor, temp_location: str, temp_tablespace_exists: bool
1112
+ ) -> None:
1113
+ """Handle temp tablespace when permissions need fixing after reboot.
1114
+
1115
+ Args:
1116
+ cursor: Database cursor.
1117
+ temp_location: Path to the temp tablespace location.
1118
+ temp_tablespace_exists: Whether the temp tablespace already exists.
1119
+ """
1120
+ if not temp_tablespace_exists:
1121
+ return
1122
+
1123
+ # Different handling based on storage type
1124
+ if is_tmpfs(temp_location):
1125
+ # tmpfs: Directory is empty after reboot, safe to rename and recreate
1126
+ # Rename existing temp tablespace instead of dropping it.
1127
+ # Timestamp collision is not possible: the charm ensures this code runs leader-only,
1128
+ # and it executes within a single database transaction holding exclusive locks.
1129
+ new_name = f"temp_{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
1130
+ cursor.execute(f"ALTER TABLESPACE temp RENAME TO {new_name};")
1131
+
1132
+ # List temp tablespaces with suffix for operator follow-up cleanup
1133
+ cursor.execute("SELECT spcname FROM pg_tablespace WHERE spcname LIKE 'temp_%';")
1134
+ temp_tbls = sorted([row[0] for row in cursor.fetchall()])
1135
+ logger.info(
1136
+ "There are %d temp tablespaces that should be checked and removed: %s",
1137
+ len(temp_tbls),
1138
+ ", ".join(temp_tbls),
1139
+ )
1140
+ else:
1141
+ # Persistent storage: Tablespace is still valid, permissions already fixed
1142
+ # Log that we fixed permissions but didn't recreate
1143
+ logger.info(
1144
+ "Fixed permissions on temp tablespace directory at %s (persistent storage), "
1145
+ "existing tablespace remains valid",
1146
+ temp_location,
1147
+ )
1148
+
1110
1149
  def set_up_database(self, temp_location: Optional[str] = None) -> None:
1111
- """Set up postgres database with the right permissions."""
1150
+ """Set up postgres database with the right permissions.
1151
+
1152
+ This method configures the postgres database with appropriate permissions and
1153
+ optionally creates a temporary tablespace.
1154
+
1155
+ Args:
1156
+ temp_location: Optional path for the temp tablespace. If provided, the method
1157
+ will ensure proper permissions and create the tablespace if it doesn't exist.
1158
+
1159
+ Behavior on reboot:
1160
+ - For tmpfs storage: If permissions are incorrect after reboot, renames the old
1161
+ tablespace and creates a new one (tmpfs directory is empty after reboot).
1162
+ - For persistent storage: If permissions are incorrect after reboot, fixes
1163
+ permissions but keeps the existing tablespace (directory contents persist).
1164
+
1165
+ Raises:
1166
+ PostgreSQLDatabasesSetupError: If database setup fails.
1167
+ """
1112
1168
  connection = None
1113
1169
  cursor = None
1114
1170
  try:
@@ -1116,30 +1172,23 @@ class PostgreSQL:
1116
1172
  cursor = connection.cursor()
1117
1173
 
1118
1174
  if temp_location is not None:
1119
- # Fix permissions on the temporary tablespace location when a reboot happens and tmpfs is being used.
1175
+ # Fix permissions on the temporary tablespace location when a reboot happens.
1120
1176
  temp_location_stats = os.stat(temp_location)
1121
- if self.substrate == Substrates.VM and (
1177
+ permissions_need_fix = self.substrate == Substrates.VM and (
1122
1178
  pwd.getpwuid(temp_location_stats.st_uid).pw_name != SNAP_USER
1123
1179
  or int(temp_location_stats.st_mode & 0o777) != POSTGRESQL_STORAGE_PERMISSIONS
1124
- ):
1180
+ )
1181
+
1182
+ if permissions_need_fix:
1125
1183
  change_owner(temp_location)
1126
1184
  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
1185
 
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
- )
1186
+ # Check if temp tablespace exists and handle it appropriately
1187
+ cursor.execute("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';")
1188
+ temp_tablespace_exists = cursor.fetchone() is not None
1189
+ self._handle_temp_tablespace_on_reboot(
1190
+ cursor, temp_location, temp_tablespace_exists
1191
+ )
1143
1192
 
1144
1193
  # Ensure a fresh temp tablespace exists at the expected location.
1145
1194
  cursor.execute("SELECT TRUE FROM pg_tablespace WHERE spcname='temp';")
@@ -1731,9 +1780,15 @@ $$ LANGUAGE plpgsql security definer;""" # noqa: S608
1731
1780
  "vacuum",
1732
1781
  )):
1733
1782
  continue
1734
- parameter = "_".join(config.split("_")[1:])
1783
+ if "-" in config:
1784
+ parameter = "_".join(config.split("-")[1:])
1785
+ else:
1786
+ parameter = "_".join(config.split("_")[1:])
1735
1787
  if parameter in ["date_style", "time_zone"]:
1736
- parameter = "".join(x.capitalize() for x in parameter.split("_"))
1788
+ if "-" in config:
1789
+ parameter = "".join(x.capitalize() for x in parameter.split("-"))
1790
+ else:
1791
+ parameter = "".join(x.capitalize() for x in parameter.split("_"))
1737
1792
  parameters[parameter] = value
1738
1793
  shared_buffers_max_value_in_mb = int(available_memory * 0.4 / 10**6)
1739
1794
  shared_buffers_max_value = int(shared_buffers_max_value_in_mb * 10**3 / 8)
@@ -1,20 +0,0 @@
1
- # Copyright 2025 Canonical Ltd.
2
- # See LICENSE file for licensing details.
3
- """Filesystem utilities."""
4
-
5
- import os
6
- import pwd
7
-
8
- from ..config.literals import SNAP_USER
9
-
10
-
11
- def change_owner(path: str) -> None:
12
- """Change the ownership of a file or a directory to the snap user.
13
-
14
- Args:
15
- path: path to a file or directory.
16
- """
17
- # Get the uid/gid for the snap user.
18
- user_database = pwd.getpwnam(SNAP_USER)
19
- # Set the correct ownership for the file or directory.
20
- os.chown(path, uid=user_database.pw_uid, gid=user_database.pw_gid)