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 } { kid }  
106+                 
107+                 # Clean up empty keyrings 
108+                 yield  f'for keyring in { pattern }  
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 } { kid }  
120+             
121+             # If keyring becomes empty, remove the file 
122+             yield  f'if ! gpg --batch --no-default-keyring --keyring "{ dest } { dest }  
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 } { dest_path } { src_file } { src_file } { dest_path }  
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