Add API v2.13 client and new server metrics
This commit is contained in:
@@ -9,6 +9,7 @@ import Foundation
|
||||
|
||||
enum APIVersion: String, CaseIterable {
|
||||
case v2_12 = "2.12"
|
||||
case v2_13 = "2.13"
|
||||
|
||||
static func from(versionString: String) -> APIVersion? {
|
||||
if let version = APIVersion(rawValue: versionString) {
|
||||
@@ -22,7 +23,8 @@ enum APIVersion: String, CaseIterable {
|
||||
let minor = components[1]
|
||||
|
||||
switch (major, minor) {
|
||||
case (2, 12...): return .v2_12
|
||||
case (2, 12): return .v2_12
|
||||
case (2, 13...): return .v2_13
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
@@ -69,6 +71,8 @@ class APIFactory {
|
||||
switch version {
|
||||
case .v2_12:
|
||||
return AnyServerAPIWrapper(APIv2_12(baseURL: baseURL))
|
||||
case .v2_13:
|
||||
return AnyServerAPIWrapper(APIv2_13(baseURL: baseURL))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@ class APIFactory {
|
||||
}
|
||||
}
|
||||
|
||||
return AnyServerAPIWrapper(APIv2_12(baseURL: baseURL))
|
||||
return AnyServerAPIWrapper(APIv2_13(baseURL: baseURL))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -222,10 +222,12 @@ private extension ServerAPI.ServerResponseEnvelope {
|
||||
serverTime: data.serverTime,
|
||||
uptime: data.uptime,
|
||||
processCount: data.processCount,
|
||||
emailsInQueue: nil,
|
||||
apacheVersion: data.apacheVersion,
|
||||
phpVersion: data.phpVersion,
|
||||
mysqlVersion: data.mysqlVersion,
|
||||
mariadbVersion: data.mariadbVersion,
|
||||
operatingSystem: nil,
|
||||
ports: data.ports?.map { ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto) },
|
||||
load: ServerInfo.Load(
|
||||
minute1: data.load.minute1,
|
||||
|
||||
@@ -146,6 +146,7 @@ struct ServerInfo: Codable, Hashable, Equatable {
|
||||
var mysqlVersion: String?
|
||||
var mariadbVersion: String?
|
||||
var operatingSystem: OperatingSystem?
|
||||
var emailsInQueue: Int?
|
||||
var ports: [ServicePort]?
|
||||
var load: Load
|
||||
var memory: Memory
|
||||
@@ -204,6 +205,7 @@ extension ServerInfo {
|
||||
phpVersion: "8.2.12",
|
||||
mysqlVersion: "8.0.33",
|
||||
mariadbVersion: nil,
|
||||
emailsInQueue: 0,
|
||||
operatingSystem: OperatingSystem(
|
||||
label: "Debian 12.12 (64-bit)",
|
||||
distribution: "Debian",
|
||||
|
||||
@@ -248,6 +248,7 @@ private extension APIv2_12 {
|
||||
}
|
||||
|
||||
let processCount: Int
|
||||
let emailsInQueue: Int?
|
||||
let load: Load
|
||||
let diskSpace: Disk
|
||||
let memory: Memory
|
||||
@@ -341,6 +342,7 @@ private extension APIv2_12 {
|
||||
serverTime: meta.serverTime,
|
||||
uptime: meta.uptime.formatted,
|
||||
processCount: utilization.processCount,
|
||||
emailsInQueue: utilization.emailsInQueue,
|
||||
apacheVersion: components.apache,
|
||||
phpVersion: components.php,
|
||||
mysqlVersion: components.mysql,
|
||||
|
||||
411
Sources/Model/API/Versions/APIv2_13.swift
Normal file
411
Sources/Model/API/Versions/APIv2_13.swift
Normal file
@@ -0,0 +1,411 @@
|
||||
//
|
||||
// APIv2_12.swift
|
||||
// iKeyMon
|
||||
//
|
||||
// Created by tracer on 13.11.25.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
extension APIv2_12 {
|
||||
struct Load: Codable {
|
||||
let current: LoadMetrics
|
||||
let historical: [LoadMetrics]
|
||||
|
||||
struct LoadMetrics: Codable {
|
||||
let oneMinute: Double
|
||||
let fiveMinute: Double
|
||||
let fifteenMinute: Double
|
||||
let timestamp: Date
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case oneMinute = "load_1"
|
||||
case fiveMinute = "load_5"
|
||||
case fifteenMinute = "load_15"
|
||||
case timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Memory: Codable {
|
||||
let system: SystemMemory
|
||||
let swap: SwapMemory?
|
||||
|
||||
struct SystemMemory: Codable {
|
||||
let total: Int64
|
||||
let used: Int64
|
||||
let free: Int64
|
||||
let available: Int64
|
||||
let buffers: Int64?
|
||||
let cached: Int64?
|
||||
}
|
||||
|
||||
struct SwapMemory: Codable {
|
||||
let total: Int64
|
||||
let used: Int64
|
||||
let free: Int64
|
||||
}
|
||||
}
|
||||
|
||||
struct Utilization: Codable {
|
||||
let cpu: CPUUtilization
|
||||
let memory: MemoryUtilization
|
||||
let disk: [DiskUtilization]
|
||||
let network: [NetworkUtilization]?
|
||||
|
||||
struct CPUUtilization: Codable {
|
||||
let overall: Double
|
||||
let cores: [Double]
|
||||
let processes: [ProcessInfo]?
|
||||
|
||||
struct ProcessInfo: Codable {
|
||||
let pid: Int
|
||||
let name: String
|
||||
let cpuPercent: Double
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pid
|
||||
case name
|
||||
case cpuPercent = "cpu_percent"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MemoryUtilization: Codable {
|
||||
let percent: Double
|
||||
let topProcesses: [ProcessMemoryInfo]?
|
||||
|
||||
struct ProcessMemoryInfo: Codable {
|
||||
let pid: Int
|
||||
let name: String
|
||||
let memoryMB: Double
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case pid
|
||||
case name
|
||||
case memoryMB = "memory_mb"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DiskUtilization: Codable {
|
||||
let device: String
|
||||
let mountpoint: String
|
||||
let usedPercent: Double
|
||||
let totalBytes: Int64
|
||||
let usedBytes: Int64
|
||||
let freeBytes: Int64
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case device
|
||||
case mountpoint
|
||||
case usedPercent = "used_percent"
|
||||
case totalBytes = "total_bytes"
|
||||
case usedBytes = "used_bytes"
|
||||
case freeBytes = "free_bytes"
|
||||
}
|
||||
}
|
||||
|
||||
struct NetworkUtilization: Codable {
|
||||
let interface: String
|
||||
let bytesIn: Int64
|
||||
let bytesOut: Int64
|
||||
let packetsIn: Int64
|
||||
let packetsOut: Int64
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case interface
|
||||
case bytesIn = "bytes_in"
|
||||
case bytesOut = "bytes_out"
|
||||
case packetsIn = "packets_in"
|
||||
case packetsOut = "packets_out"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class APIv2_12: BaseAPIClient, ServerAPIProtocol {
|
||||
typealias LoadType = APIv2_12.Load
|
||||
typealias MemoryType = APIv2_12.Memory
|
||||
typealias UtilizationType = APIv2_12.Utilization
|
||||
|
||||
private enum Endpoint: String {
|
||||
case systemInfo = "/api/v2.12/system/info"
|
||||
case load = "/api/v2.12/metrics/load"
|
||||
case memory = "/api/v2.12/metrics/memory"
|
||||
case utilization = "/api/v2.12/metrics/utilization"
|
||||
|
||||
func url(baseURL: URL) -> URL {
|
||||
return baseURL.appendingPathComponent(self.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchSystemInfo() async throws -> SystemInfo {
|
||||
let url = Endpoint.systemInfo.url(baseURL: baseURL)
|
||||
let request = URLRequest(url: url)
|
||||
return try await performRequest(request, responseType: SystemInfo.self)
|
||||
}
|
||||
|
||||
func fetchLoad() async throws -> LoadType {
|
||||
let url = Endpoint.load.url(baseURL: baseURL)
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return try await performRequest(request, responseType: LoadType.self)
|
||||
}
|
||||
|
||||
func fetchMemory() async throws -> MemoryType {
|
||||
let url = Endpoint.memory.url(baseURL: baseURL)
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return try await performRequest(request, responseType: MemoryType.self)
|
||||
}
|
||||
|
||||
func fetchUtilization() async throws -> UtilizationType {
|
||||
let url = Endpoint.utilization.url(baseURL: baseURL)
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
return try await performRequest(request, responseType: UtilizationType.self)
|
||||
}
|
||||
|
||||
func fetchServerSummary(apiKey: String) async throws -> ServerInfo {
|
||||
let summaryURL = baseURL.appendingPathComponent("api/v2/server")
|
||||
var request = URLRequest(url: summaryURL)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue(apiKey, forHTTPHeaderField: "X-API-KEY")
|
||||
request.timeoutInterval = 30
|
||||
|
||||
let (data, response) = try await session.data(for: request)
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
throw APIError.invalidResponse
|
||||
}
|
||||
|
||||
guard httpResponse.statusCode == 200 else {
|
||||
throw APIError.httpError(httpResponse.statusCode)
|
||||
}
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
||||
let envelope = try decoder.decode(ServerSummaryEnvelope.self, from: data)
|
||||
return envelope.toDomain()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Server Summary Mapping
|
||||
|
||||
private extension APIv2_12 {
|
||||
struct ServerSummaryEnvelope: Decodable {
|
||||
let meta: Meta
|
||||
let operatingSystem: OperatingSystem?
|
||||
let utilization: Utilization
|
||||
let components: Components
|
||||
let ports: [Port]?
|
||||
let additionalPhpInterpreters: [AdditionalInterpreter]?
|
||||
|
||||
struct Meta: Decodable {
|
||||
struct Uptime: Decodable {
|
||||
let days: Int
|
||||
let hours: Int
|
||||
let minutes: Int
|
||||
let seconds: Int
|
||||
|
||||
var formatted: String {
|
||||
"\(days) Days / \(hours) Hours / \(minutes) Minutes / \(seconds) Seconds"
|
||||
}
|
||||
}
|
||||
|
||||
let hostname: String
|
||||
let ipAddresses: [String]
|
||||
let serverTime: String
|
||||
let uptime: Uptime
|
||||
let panelVersion: String
|
||||
let panelBuild: Int
|
||||
let apiVersion: String
|
||||
}
|
||||
|
||||
struct Utilization: Decodable {
|
||||
struct Load: Decodable {
|
||||
let minute1: Double
|
||||
let minute5: Double
|
||||
let minute15: Double
|
||||
let cpuCount: Int
|
||||
let percent: Double
|
||||
let level: String
|
||||
}
|
||||
|
||||
struct Memory: Decodable {
|
||||
let free: Int
|
||||
let used: Int
|
||||
let total: Int
|
||||
let percent: Double
|
||||
}
|
||||
|
||||
struct Disk: Decodable {
|
||||
let free: Int
|
||||
let used: Int
|
||||
let total: Int
|
||||
let percent: Double
|
||||
}
|
||||
|
||||
let processCount: Int
|
||||
let emailsInQueue: Int?
|
||||
let load: Load
|
||||
let diskSpace: Disk
|
||||
let memory: Memory
|
||||
let swap: Memory
|
||||
}
|
||||
|
||||
struct Components: Decodable {
|
||||
let apache: String
|
||||
let php: String
|
||||
let mysql: String?
|
||||
let mariadb: String?
|
||||
}
|
||||
|
||||
struct Port: Decodable {
|
||||
let service: String
|
||||
let status: String
|
||||
let port: Int
|
||||
let proto: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case service
|
||||
case status
|
||||
case port
|
||||
case proto = "protocol"
|
||||
}
|
||||
}
|
||||
|
||||
struct AdditionalInterpreter: Decodable {
|
||||
let version: String
|
||||
let path: String?
|
||||
let configFile: String?
|
||||
}
|
||||
|
||||
struct OperatingSystem: Decodable {
|
||||
struct Updates: Decodable {
|
||||
let updateCount: Int
|
||||
let securityUpdateCount: Int
|
||||
let rebootRequired: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case updateCount = "update_count"
|
||||
case securityUpdateCount = "security_update_count"
|
||||
case rebootRequired = "reboot_required"
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
updateCount = try container.decodeIfPresent(Int.self, forKey: .updateCount) ?? 0
|
||||
securityUpdateCount = try container.decodeIfPresent(Int.self, forKey: .securityUpdateCount) ?? 0
|
||||
rebootRequired = try container.decodeIfPresent(Bool.self, forKey: .rebootRequired) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
let label: String
|
||||
let distribution: String
|
||||
let version: String
|
||||
let architecture: String
|
||||
let endOfLife: Bool
|
||||
let updates: Updates?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case label
|
||||
case distribution
|
||||
case version
|
||||
case architecture
|
||||
case endOfLife = "end_of_life"
|
||||
case updates
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
label = try container.decodeIfPresent(String.self, forKey: .label) ?? ""
|
||||
distribution = try container.decodeIfPresent(String.self, forKey: .distribution) ?? ""
|
||||
version = try container.decodeIfPresent(String.self, forKey: .version) ?? ""
|
||||
architecture = try container.decodeIfPresent(String.self, forKey: .architecture) ?? ""
|
||||
endOfLife = try container.decodeIfPresent(Bool.self, forKey: .endOfLife) ?? false
|
||||
updates = try container.decodeIfPresent(Updates.self, forKey: .updates)
|
||||
}
|
||||
}
|
||||
|
||||
func toDomain() -> ServerInfo {
|
||||
let load = utilization.load
|
||||
let disk = utilization.diskSpace
|
||||
let memory = utilization.memory
|
||||
let swapMemory = utilization.swap
|
||||
|
||||
return ServerInfo(
|
||||
hostname: meta.hostname,
|
||||
ipAddresses: meta.ipAddresses,
|
||||
cpuCores: load.cpuCount,
|
||||
serverTime: meta.serverTime,
|
||||
uptime: meta.uptime.formatted,
|
||||
processCount: utilization.processCount,
|
||||
emailsInQueue: utilization.emailsInQueue,
|
||||
apacheVersion: components.apache,
|
||||
phpVersion: components.php,
|
||||
mysqlVersion: components.mysql,
|
||||
mariadbVersion: components.mariadb,
|
||||
operatingSystem: operatingSystem.map {
|
||||
ServerInfo.OperatingSystem(
|
||||
label: $0.label,
|
||||
distribution: $0.distribution,
|
||||
version: $0.version,
|
||||
architecture: $0.architecture,
|
||||
endOfLife: $0.endOfLife,
|
||||
updates: $0.updates.map {
|
||||
ServerInfo.OperatingSystem.UpdateStatus(
|
||||
updateCount: $0.updateCount,
|
||||
securityUpdateCount: $0.securityUpdateCount,
|
||||
rebootRequired: $0.rebootRequired
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
ports: ports?.map {
|
||||
ServerInfo.ServicePort(service: $0.service, status: $0.status, port: $0.port, proto: $0.proto)
|
||||
},
|
||||
load: ServerInfo.Load(
|
||||
minute1: load.minute1,
|
||||
minute5: load.minute5,
|
||||
minute15: load.minute15,
|
||||
percent: load.percent,
|
||||
cpuCount: load.cpuCount,
|
||||
level: load.level
|
||||
),
|
||||
memory: ServerInfo.Memory(
|
||||
free: memory.free,
|
||||
used: memory.used,
|
||||
total: memory.total,
|
||||
percent: memory.percent
|
||||
),
|
||||
swap: ServerInfo.Memory(
|
||||
free: swapMemory.free,
|
||||
used: swapMemory.used,
|
||||
total: swapMemory.total,
|
||||
percent: swapMemory.percent
|
||||
),
|
||||
diskSpace: ServerInfo.DiskSpace(
|
||||
free: disk.free,
|
||||
used: disk.used,
|
||||
total: disk.total,
|
||||
percent: disk.percent
|
||||
),
|
||||
panelVersion: meta.panelVersion,
|
||||
panelBuild: String(meta.panelBuild),
|
||||
apiVersion: meta.apiVersion,
|
||||
additionalPHPInterpreters: additionalPhpInterpreters?.map {
|
||||
ServerInfo.PHPInterpreter(
|
||||
version: $0.version,
|
||||
path: $0.path,
|
||||
configFile: $0.configFile,
|
||||
extensions: [],
|
||||
memoryLimit: nil,
|
||||
maxExecutionTime: nil
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user