@@ -976,6 +976,57 @@ def add_azure_core_repo(
976976 keys_location = keys ,
977977 )
978978
979+ def _is_package_lock_held (self ) -> bool :
980+ # `pidof dpkg dpkg-deb` alone misses holders such as
981+ # `unattended-upgrades` and `apt-get`, which hold the apt/dpkg
982+ # frontend lock without a running `dpkg` process. Check the lock
983+ # files directly so we wait for those holders to finish too.
984+ lock_files = " " .join (
985+ [
986+ "/var/lib/dpkg/lock-frontend" ,
987+ "/var/lib/dpkg/lock" ,
988+ "/var/lib/apt/lists/lock" ,
989+ "/var/cache/apt/archives/lock" ,
990+ ]
991+ )
992+ # `fuser` exits 0 if at least one listed file is opened by a process,
993+ # and non-zero otherwise (including 127 if `fuser` is unavailable, in
994+ # which case we fall back to the `pidof` check below).
995+ fuser_result = self ._node .execute (
996+ f"fuser { lock_files } " ,
997+ sudo = True ,
998+ shell = True ,
999+ no_info_log = True ,
1000+ )
1001+ return fuser_result .exit_code == 0
1002+
1003+ def _disable_unattended_upgrades (self ) -> None :
1004+ # Background apt jobs grab the dpkg frontend lock at boot, making the
1005+ # first package install fail with
1006+ # "Could not get lock /var/lib/dpkg/lock-frontend ... (unattended-upgr)".
1007+ # Mask these units so they cannot run now or after a reboot. `mask
1008+ # --now` is a systemd operation that also stops any in-flight run, and
1009+ # it does not need the dpkg lock, so it works even mid-upgrade. The
1010+ # `unattended-upgrade` binary stays installed, so tests that invoke it
1011+ # explicitly (e.g. the upgrade_packages transformer) are unaffected.
1012+ units = " " .join (
1013+ [
1014+ "apt-daily.timer" ,
1015+ "apt-daily.service" ,
1016+ "apt-daily-upgrade.timer" ,
1017+ "apt-daily-upgrade.service" ,
1018+ "unattended-upgrades.service" ,
1019+ ]
1020+ )
1021+ # Best effort: never fail provisioning if a unit is missing on the image.
1022+ self ._node .execute (
1023+ f"systemctl mask --now { units } " ,
1024+ sudo = True ,
1025+ shell = True ,
1026+ timeout = 120 ,
1027+ no_error_log = True ,
1028+ )
1029+
9791030 def wait_running_package_process (self ) -> None :
9801031 is_first_time : bool = True
9811032 # wait for 10 minutes
@@ -985,12 +1036,15 @@ def wait_running_package_process(self) -> None:
9851036 # fix the dpkg, in case it's broken.
9861037 self ._node .execute ("dpkg --force-all --configure -a" , sudo = True )
9871038 pidof_result = self ._node .execute ("pidof dpkg dpkg-deb" )
988- if pidof_result .exit_code == 1 :
989- # no dpkg process running, safe to exit and attempt repair.
1039+ dpkg_running = pidof_result .exit_code == 0
1040+ lock_held = self ._is_package_lock_held ()
1041+ if not dpkg_running and not lock_held :
1042+ # no dpkg process and no apt/dpkg lock holder; safe to exit
1043+ # and attempt repair.
9901044 break
9911045 if is_first_time :
9921046 is_first_time = False
993- self ._log .debug ("found system dpkg process, waiting it..." )
1047+ self ._log .debug ("found running package process, waiting it..." )
9941048 time .sleep (1 )
9951049
9961050 if timeout < timer .elapsed ():
@@ -1122,6 +1176,9 @@ def is_end_of_life_release(self) -> bool:
11221176 skipped_exceptions = [ReleaseEndOfLifeException , RepoNotExistException ],
11231177 )
11241178 def _initialize_package_installation (self ) -> None :
1179+ # disable background apt jobs that grab the dpkg lock at boot, so the
1180+ # first install does not race with unattended-upgrades.
1181+ self ._disable_unattended_upgrades ()
11251182 # wait running system package process.
11261183 self .wait_running_package_process ()
11271184 result = self ._node .execute ("apt-get update" , sudo = True , timeout = 1800 )
0 commit comments