2020)
2121from pyinfra .facts .deb import DebPackage , DebPackages
2222from pyinfra .facts .files import File
23- from pyinfra .facts .gpg import GpgKey
23+ from pyinfra .facts .gpg import GpgKey , GpgKeyrings
2424from pyinfra .facts .server import Date
2525from pyinfra .operations import files , gpg
2626
@@ -98,28 +98,57 @@ def _derive_dest_from_src_and_keyids(
9898 return f"/etc/apt/keyrings/{ base } .gpg"
9999
100100
101+ def _get_apt_keys_comprehensive () -> dict [str , str ]:
102+ """
103+ Get all GPG keys available in APT directories using the GpgKeyrings fact.
104+ This provides more comprehensive coverage than AptKeys fact.
105+ Falls back gracefully if GpgKeyrings data is not available.
106+
107+ Returns:
108+ dict: Key ID -> keyring file path mapping
109+ """
110+ try :
111+ apt_directories = ["/etc/apt/trusted.gpg.d" , "/etc/apt/keyrings" , "/usr/share/keyrings" ]
112+ keyrings_info = host .get_fact (GpgKeyrings , directories = apt_directories )
113+
114+ all_keys = {}
115+ for keyring_path , keyring_data in keyrings_info .items ():
116+ keys = keyring_data .get ("keys" , {})
117+ for key_id in keys .keys ():
118+ all_keys [key_id ] = keyring_path
119+
120+ return all_keys
121+ except (KeyError , AttributeError ):
122+ # Fallback to empty dict if GpgKeyrings fact is not available (e.g., in tests)
123+ return {}
124+
125+
101126@operation ()
102127def key (
103128 src : str | None = None ,
104129 keyserver : str | None = None ,
105130 keyid : str | list [str ] | None = None ,
106131 dest : str | None = None ,
132+ present : bool = True ,
107133):
108134 """
109- Add apt GPG keys *without* apt-key:
110- - Keys are stored under /etc/apt/keyrings/<name>.gpg (binary, dearmored if needed).
111- - You must reference the resulting file in your apt source via `signed-by=...`.
135+ Add or remove apt GPG keys using modern keyring management.
136+
137+ This operation manages GPG keys for APT repositories without using the deprecated apt-key command.
138+ Keys are stored in /etc/apt/keyrings/ and can be referenced in source lists via signed-by=.
112139
113140 Args:
114141 src: filename or URL to a key (ASCII .asc or binary .gpg)
115142 keyserver: keyserver URL for fetching keys by ID
116- keyid: key ID or list of key IDs (required with keyserver)
143+ keyid: key ID or list of key IDs (required with keyserver, optional for removal )
117144 dest: optional keyring filename/path ('.gpg' will be enforced, defaults under /etc/apt/keyrings)
145+ present: whether the key should be present (True) or absent (False)
118146
119147 Behavior:
120- - Idempotent via AptKeys: if the key IDs are already present in any apt keyring, nothing is changed.
121- - If src is ASCII (.asc), it will be dearmored; if binary (.gpg), it's copied as-is.
122- - Keyserver flow uses a temporary GNUPGHOME, then exports and dearmors to the destination keyring.
148+ - Installation: Idempotent via AptKeys - if key IDs are already present, nothing changes
149+ - Removal: Uses GpgKeyrings fact to find and remove keys from APT directories
150+ - If src is ASCII (.asc), it will be dearmored; if binary (.gpg), it's copied as-is
151+ - Keyserver flow uses temporary GNUPGHOME, then exports to destination keyring
123152
124153 Examples:
125154 apt.key(
@@ -129,9 +158,15 @@ def key(
129158 )
130159
131160 apt.key(
132- name="Install VirtualBox key",
133- src="https://www.virtualbox.org/download/oracle_vbox_2016.asc",
134- dest="oracle-virtualbox.gpg",
161+ name="Remove specific keyring file",
162+ dest="old-vendor.gpg",
163+ present=False,
164+ )
165+
166+ apt.key(
167+ name="Remove key by ID from all APT keyrings",
168+ keyid="0xCOMPROMISED123",
169+ present=False,
135170 )
136171
137172 apt.key(
@@ -142,8 +177,26 @@ def key(
142177 )
143178 """
144179
145- # Gather currently installed keys (across trusted.gpg.d/, keyrings/, etc.)
180+ # Handle removal operations using the GPG infrastructure
181+ if present is False :
182+ # Use the GPG operation for removal, but restrict to APT directories
183+ apt_working_dirs = ["/etc/apt/trusted.gpg.d" , "/etc/apt/keyrings" , "/usr/share/keyrings" ]
184+ yield from gpg .key ._inner (
185+ dest = dest ,
186+ keyid = keyid ,
187+ present = False ,
188+ working_dirs = apt_working_dirs ,
189+ )
190+ return
191+
192+ # Installation logic (existing code)
193+ # Get comprehensive view of all keys in APT directories
194+ existing_keys_comprehensive = _get_apt_keys_comprehensive ()
195+ # Also get the legacy AptKeys fact for compatibility
146196 existing_keys = host .get_fact (AptKeys )
197+
198+ # Combine both sources of key information for complete coverage
199+ all_available_keys = set (existing_keys_comprehensive .keys ()) | set (existing_keys .keys ())
147200
148201 # Check idempotency for src branch
149202 if src :
@@ -152,7 +205,7 @@ def key(
152205
153206 # If we don't know the IDs (eg. unreachable URL), we cannot determine idempotency -> try to install.
154207 # Otherwise, skip if all key IDs are already present.
155- if keyids_from_src and all (kid in existing_keys for kid in keyids_from_src ):
208+ if keyids_from_src and all (kid in all_available_keys for kid in keyids_from_src ):
156209 host .noop (f"All keys from { src } are already available in the apt keychain" )
157210 return
158211
@@ -166,7 +219,7 @@ def key(
166219 if isinstance (keyid , str ):
167220 keyid = [keyid ]
168221
169- needed_keys = sorted (set (keyid ) - set ( existing_keys . keys ()) )
222+ needed_keys = sorted (set (keyid ) - all_available_keys )
170223 if not needed_keys :
171224 host .noop (f"Keys { ', ' .join (keyid )} are already available in the apt keychain" )
172225 return
0 commit comments