tart prune: allow pruning local VMs with --entries=vms (#557)

This commit is contained in:
Nikolay Edigaryev 2023-07-13 22:05:28 +04:00 committed by GitHub
parent 93a1b70ecb
commit 6c377029d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 51 additions and 18 deletions

View File

@ -5,23 +5,40 @@ import SwiftUI
import SwiftDate import SwiftDate
struct Prune: AsyncParsableCommand { struct Prune: AsyncParsableCommand {
static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches") static var configuration = CommandConfiguration(abstract: "Prune OCI and IPSW caches or local VMs")
@Option(help: ArgumentHelp("Remove cache entries last accessed more than n days ago", @Option(help: ArgumentHelp("Entries to remove: \"caches\" targets OCI and IPSW caches and \"vms\" targets local VMs."))
var entries: String = "caches"
@Option(help: ArgumentHelp("Remove entries that were last accessed more than n days ago",
discussion: "For example, --older-than=7 will remove entries that weren't accessed by Tart in the last 7 days.", discussion: "For example, --older-than=7 will remove entries that weren't accessed by Tart in the last 7 days.",
valueName: "n")) valueName: "n"))
var olderThan: UInt? var olderThan: UInt?
@Option(help: ArgumentHelp("Remove least recently used cache entries that do not fit the specified cache size budget n, expressed in gigabytes", @Option(help: .hidden)
discussion: "For example, --cache-budget=50 will effectively shrink all caches to a total size of 50 gigabytes.",
valueName: "n"))
var cacheBudget: UInt? var cacheBudget: UInt?
@Option(help: ArgumentHelp("Remove the least recently used entries that do not fit the specified space size budget n, expressed in gigabytes",
discussion: "For example, --space-budget=50 will effectively shrink all entries to a total size of 50 gigabytes.",
valueName: "n"))
var spaceBudget: UInt?
@Flag(help: .hidden) @Flag(help: .hidden)
var gc: Bool = false var gc: Bool = false
func validate() throws { mutating func validate() throws {
if olderThan == nil && cacheBudget == nil && !gc { // --cache-budget deprecation logic
if let cacheBudget = cacheBudget {
fputs("--cache-budget is deprecated, please use --space-budget\n", stderr)
if spaceBudget != nil {
throw ValidationError("--cache-budget is deprecated, please use --space-budget")
}
spaceBudget = cacheBudget
}
if olderThan == nil && spaceBudget == nil && !gc {
throw ValidationError("at least one pruning criteria must be specified") throw ValidationError("at least one pruning criteria must be specified")
} }
} }
@ -31,43 +48,53 @@ struct Prune: AsyncParsableCommand {
try VMStorageOCI().gc() try VMStorageOCI().gc()
} }
// Build a list of prunable storages that we're going to prune based on user's request
let prunableStorages: [PrunableStorage]
switch entries {
case "caches":
prunableStorages = [VMStorageOCI(), try IPSWCache()]
case "vms":
prunableStorages = [VMStorageLocal()]
default:
throw ValidationError("unsupported --entries value, please specify either \"caches\" or \"vms\"")
}
// Clean up cache entries based on last accessed date // Clean up cache entries based on last accessed date
if let olderThan = olderThan { if let olderThan = olderThan {
let olderThanInterval = Int(exactly: olderThan)!.days.timeInterval let olderThanInterval = Int(exactly: olderThan)!.days.timeInterval
let olderThanDate = Date() - olderThanInterval let olderThanDate = Date() - olderThanInterval
try Prune.pruneOlderThan(olderThanDate: olderThanDate) try Prune.pruneOlderThan(prunableStorages: prunableStorages, olderThanDate: olderThanDate)
} }
// Clean up cache entries based on imposed cache size limit and entry's last accessed date // Clean up cache entries based on imposed cache size limit and entry's last accessed date
if let cacheBudget = cacheBudget { if let spaceBudget = spaceBudget {
try Prune.pruneCacheBudget(cacheBudgetBytes: UInt64(cacheBudget) * 1024 * 1024 * 1024) try Prune.pruneSpaceBudget(prunableStorages: prunableStorages, spaceBudgetBytes: UInt64(spaceBudget) * 1024 * 1024 * 1024)
} }
} }
static func pruneOlderThan(olderThanDate: Date) throws { static func pruneOlderThan(prunableStorages: [PrunableStorage], olderThanDate: Date) throws {
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()]
let prunables: [Prunable] = try prunableStorages.flatMap { try $0.prunables() } let prunables: [Prunable] = try prunableStorages.flatMap { try $0.prunables() }
try prunables.filter { try $0.accessDate() <= olderThanDate }.forEach { try $0.delete() } try prunables.filter { try $0.accessDate() <= olderThanDate }.forEach { try $0.delete() }
} }
static func pruneCacheBudget(cacheBudgetBytes: UInt64) throws { static func pruneSpaceBudget(prunableStorages: [PrunableStorage], spaceBudgetBytes: UInt64) throws {
let prunableStorages: [PrunableStorage] = [VMStorageOCI(), try IPSWCache()]
let prunables: [Prunable] = try prunableStorages let prunables: [Prunable] = try prunableStorages
.flatMap { try $0.prunables() } .flatMap { try $0.prunables() }
.sorted { try $0.accessDate() > $1.accessDate() } .sorted { try $0.accessDate() > $1.accessDate() }
var cacheBudgetBytes = cacheBudgetBytes var spaceBudgetBytes = spaceBudgetBytes
var prunablesToDelete: [Prunable] = [] var prunablesToDelete: [Prunable] = []
for prunable in prunables { for prunable in prunables {
let prunableSizeBytes = UInt64(try prunable.sizeBytes()) let prunableSizeBytes = UInt64(try prunable.sizeBytes())
if prunableSizeBytes <= cacheBudgetBytes { if prunableSizeBytes <= spaceBudgetBytes {
// Don't mark for deletion as // Don't mark for deletion as
// there's a budget available // there's a budget available
cacheBudgetBytes -= prunableSizeBytes spaceBudgetBytes -= prunableSizeBytes
} else { } else {
// Mark for deletion // Mark for deletion
prunablesToDelete.append(prunable) prunablesToDelete.append(prunable)

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
class VMStorageLocal { class VMStorageLocal: PrunableStorage {
let baseURL: URL = try! Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true) let baseURL: URL = try! Config().tartHomeDir.appendingPathComponent("vms", isDirectory: true)
private func vmURL(_ name: String) -> URL { private func vmURL(_ name: String) -> URL {
@ -16,6 +16,8 @@ class VMStorageLocal {
try vmDir.validate(userFriendlyName: name) try vmDir.validate(userFriendlyName: name)
try vmDir.baseURL.updateAccessDate()
return vmDir return vmDir
} }
@ -63,6 +65,10 @@ class VMStorageLocal {
} }
} }
func prunables() throws -> [Prunable] {
try list().map { (_, vmDir) in vmDir }
}
func hasVMsWithMACAddress(macAddress: String) throws -> Bool { func hasVMsWithMACAddress(macAddress: String) throws -> Bool {
try list().contains { try $1.macAddress() == macAddress } try list().contains { try $1.macAddress() == macAddress }
} }