1111from pyinfra .facts .gpg import GpgKey
1212
1313from . import files
14+ from pathlib import Path
1415
1516
1617@operation ()
@@ -21,17 +22,23 @@ def key(
2122 keyid : str | list [str ] | None = None ,
2223 dearmor : bool = True ,
2324 mode : str = "0644" ,
25+ present : bool = True ,
2426):
2527 """
26- Install GPG keys from various sources.
28+ Install or remove GPG keys from various sources.
2729
2830 Args:
2931 src: filename or URL to a key (ASCII .asc or binary .gpg)
30- dest: destination path for the key file (required)
32+ dest: destination path for the key file (required for installation, optional for removal )
3133 keyserver: keyserver URL for fetching keys by ID
32- keyid: key ID or list of key IDs (required with keyserver)
34+ keyid: key ID or list of key IDs (required with keyserver, optional for removal )
3335 dearmor: whether to convert ASCII armored keys to binary format
3436 mode: file permissions for the installed key
37+ present: whether the key should be present (True) or absent (False)
38+ When False: if dest is provided, removes from specific keyring;
39+ if dest is None, removes from all APT keyrings;
40+ if keyid is provided, removes specific key(s);
41+ if keyid is None, removes entire keyring file(s)
3542
3643 Examples:
3744 gpg.key(
@@ -40,6 +47,26 @@ def key(
4047 dest="/etc/apt/keyrings/docker.gpg",
4148 )
4249
50+ gpg.key(
51+ name="Remove old GPG key file",
52+ dest="/etc/apt/keyrings/old-key.gpg",
53+ present=False,
54+ )
55+
56+ gpg.key(
57+ name="Remove specific key by ID",
58+ dest="/etc/apt/keyrings/vendor.gpg",
59+ keyid="0xABCDEF12",
60+ present=False,
61+ )
62+
63+ gpg.key(
64+ name="Remove key from all APT keyrings",
65+ keyid="0xCOMPROMISED123",
66+ present=False,
67+ # dest=None means search in all keyrings
68+ )
69+
4370 gpg.key(
4471 name="Fetch keys from keyserver",
4572 keyserver="hkps://keyserver.ubuntu.com",
@@ -48,17 +75,73 @@ def key(
4875 )
4976 """
5077
78+ # Validate parameters based on operation type
79+ if present is True :
80+ # For installation, dest is required
81+ if not dest :
82+ raise OperationError ("`dest` must be provided for installation" )
83+ elif present is False :
84+ # For removal, either dest or keyid must be provided
85+ if not dest and not keyid :
86+ raise OperationError ("For removal, either `dest` or `keyid` must be provided" )
87+
88+ # For removal, handle different scenarios
89+ if present is False :
90+ if not dest and keyid :
91+ # Remove key(s) from all APT keyrings
92+ if isinstance (keyid , str ):
93+ keyid = [keyid ]
94+
95+ # Define all APT keyring locations
96+ keyring_patterns = [
97+ "/etc/apt/trusted.gpg.d/*.gpg" ,
98+ "/etc/apt/keyrings/*.gpg" ,
99+ "/usr/share/keyrings/*.gpg"
100+ ]
101+
102+ for pattern in keyring_patterns :
103+ for kid in keyid :
104+ # Remove key from all matching keyrings
105+ yield f'for keyring in { pattern } ; do [ -e "$keyring" ] && gpg --batch --no-default-keyring --keyring "$keyring" --delete-keys { kid } 2>/dev/null || true; done'
106+
107+ # Clean up empty keyrings
108+ yield f'for keyring in { pattern } ; do [ -e "$keyring" ] && ! gpg --batch --no-default-keyring --keyring "$keyring" --list-keys 2>/dev/null | grep -q "pub" && rm -f "$keyring" || true; done'
109+
110+ return
111+
112+ elif dest and keyid :
113+ # Remove specific key(s) by ID from specific keyring
114+ if isinstance (keyid , str ):
115+ keyid = [keyid ]
116+
117+ for kid in keyid :
118+ # Remove the specific key from the keyring
119+ yield f'gpg --batch --no-default-keyring --keyring "{ dest } " --delete-keys { kid } 2>/dev/null || true'
120+
121+ # If keyring becomes empty, remove the file
122+ yield f'if ! gpg --batch --no-default-keyring --keyring "{ dest } " --list-keys 2>/dev/null | grep -q "pub"; then rm -f "{ dest } "; fi'
123+ return
124+
125+ elif dest and not keyid :
126+ # Remove entire keyring file
127+ yield from files .file ._inner (
128+ path = dest ,
129+ present = False ,
130+ )
131+ return
132+
133+ # For installation, validate required parameters
51134 if not src and not keyserver :
52- raise OperationError ("Either `src` or `keyserver` must be provided" )
135+ raise OperationError ("Either `src` or `keyserver` must be provided for installation " )
53136
54137 if keyserver and not keyid :
55138 raise OperationError ("`keyid` must be provided with `keyserver`" )
56139
57- if not dest :
58- raise OperationError ("`dest ` must be provided" )
140+ if keyid and not keyserver and not src :
141+ raise OperationError ("When using `keyid` for installation, either `keyserver` or `src ` must be provided" )
59142
60- # Ensure destination directory exists
61- dest_dir = dest . rsplit ( "/" , 1 )[ 0 ]
143+ # For installation (present=True), ensure destination directory exists
144+ dest_dir = str ( Path ( dest ). parent )
62145 yield from files .directory ._inner (
63146 path = dest_dir ,
64147 mode = "0755" ,
@@ -126,7 +209,6 @@ def key(
126209 present = True ,
127210 )
128211
129-
130212@operation ()
131213def dearmor (src : str , dest : str , mode : str = "0644" ):
132214 """
@@ -168,8 +250,11 @@ def _install_key_file(src_file: str, dest_path: str, dearmor: bool, mode: str):
168250 Helper function to install a GPG key file, dearmoring if necessary.
169251 """
170252 if dearmor :
253+ # Check if it's an ASCII armored key and handle accordingly
254+ # Note: Could be enhanced using GpgKey fact for better detection
171255 yield f'if grep -q "BEGIN PGP PUBLIC KEY BLOCK" "{ src_file } "; then gpg --batch --dearmor -o "{ dest_path } " "{ src_file } "; else cp "{ src_file } " "{ dest_path } "; fi'
172256 else :
257+ # Simple copy for binary keys or when dearmoring is disabled
173258 yield f'cp "{ src_file } " "{ dest_path } "'
174259
175260 # Set proper permissions using pyinfra
0 commit comments