11
11
from pyinfra .facts .gpg import GpgKey
12
12
13
13
from . import files
14
+ from pathlib import Path
14
15
15
16
16
17
@operation ()
@@ -21,17 +22,23 @@ def key(
21
22
keyid : str | list [str ] | None = None ,
22
23
dearmor : bool = True ,
23
24
mode : str = "0644" ,
25
+ present : bool = True ,
24
26
):
25
27
"""
26
- Install GPG keys from various sources.
28
+ Install or remove GPG keys from various sources.
27
29
28
30
Args:
29
31
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 )
31
33
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 )
33
35
dearmor: whether to convert ASCII armored keys to binary format
34
36
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)
35
42
36
43
Examples:
37
44
gpg.key(
@@ -40,6 +47,26 @@ def key(
40
47
dest="/etc/apt/keyrings/docker.gpg",
41
48
)
42
49
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
+
43
70
gpg.key(
44
71
name="Fetch keys from keyserver",
45
72
keyserver="hkps://keyserver.ubuntu.com",
@@ -48,17 +75,73 @@ def key(
48
75
)
49
76
"""
50
77
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
51
134
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 " )
53
136
54
137
if keyserver and not keyid :
55
138
raise OperationError ("`keyid` must be provided with `keyserver`" )
56
139
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" )
59
142
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 )
62
145
yield from files .directory ._inner (
63
146
path = dest_dir ,
64
147
mode = "0755" ,
@@ -126,7 +209,6 @@ def key(
126
209
present = True ,
127
210
)
128
211
129
-
130
212
@operation ()
131
213
def dearmor (src : str , dest : str , mode : str = "0644" ):
132
214
"""
@@ -168,8 +250,11 @@ def _install_key_file(src_file: str, dest_path: str, dearmor: bool, mode: str):
168
250
Helper function to install a GPG key file, dearmoring if necessary.
169
251
"""
170
252
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
171
255
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'
172
256
else :
257
+ # Simple copy for binary keys or when dearmoring is disabled
173
258
yield f'cp "{ src_file } " "{ dest_path } "'
174
259
175
260
# Set proper permissions using pyinfra
0 commit comments