Просмотр исходного кода

Parse the per-edition appliance licensing status

The licensing server now reports the appliance usage counters separately
for each licence edition.

Split the `licence` type into per-edition stats buckets and parse both,
falling back to the old flat fields for older servers.
Add `LicenceUtils` for the edition labels and the `appliance ID` version selection.

Signed-off-by: Mihaela Balutoiu <mbalutoiu@cloudbasesolutions.com>
Mihaela Balutoiu 1 неделя назад
Родитель
Сommit
00e28d12e6
3 измененных файлов с 154 добавлено и 15 удалено
  1. 17 4
      src/@types/Licence.ts
  2. 36 11
      src/sources/LincenceSource.ts
  3. 101 0
      src/utils/LicenceUtils.ts

+ 17 - 4
src/@types/Licence.ts

@@ -12,10 +12,13 @@ You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
 
-export type Licence = {
-  applianceId: string;
-  earliestLicenceExpiryDate: Date;
-  latestLicenceExpiryDate: Date;
+export const LICENCE_VERSION_V1 = "v1";
+export const LICENCE_VERSION_V2 = "v2";
+export const LICENCE_VERSION_V2_SAP = "v2-sap";
+
+export type LicenceKind = "standard" | "sap";
+
+export type LicenceStats = {
   currentPerformedMigrations: number;
   currentPerformedReplicas: number;
   lifetimePerformedMigrations: number;
@@ -26,6 +29,16 @@ export type Licence = {
   lifetimeAvailableReplicas: number;
 };
 
+export type Licence = {
+  applianceId: string;
+  earliestLicenceExpiryDate: Date;
+  latestLicenceExpiryDate: Date;
+  /** Allowances issued by standard (`v2`) licences. */
+  standardStats: LicenceStats;
+  /** Allowances issued by SAP (`v2-sap`) licences. */
+  sapStats: LicenceStats;
+};
+
 export type LicenceServerStatus = {
   hostname: string;
   multi_appliance: boolean;

+ 36 - 11
src/sources/LincenceSource.ts

@@ -14,8 +14,33 @@ along with this program.  If not, see <http://www.gnu.org/licenses/>.
 import Api from "@src/utils/ApiCaller";
 
 import configLoader from "@src/utils/Config";
+import LicenceUtils from "@src/utils/LicenceUtils";
 
-import type { Licence, LicenceServerStatus } from "@src/@types/Licence";
+import type {
+  Licence,
+  LicenceServerStatus,
+  LicenceStats,
+} from "@src/@types/Licence";
+
+const parseStats = (body: any): LicenceStats => {
+  if (!body) {
+    return LicenceUtils.emptyStats();
+  }
+  const num = (value: any): number => {
+    const parsed = Number(value);
+    return Number.isFinite(parsed) ? parsed : 0;
+  };
+  return {
+    currentPerformedMigrations: num(body.current_performed_migrations),
+    currentPerformedReplicas: num(body.current_performed_replicas),
+    lifetimePerformedMigrations: num(body.lifetime_performed_migrations),
+    lifetimePerformedReplicas: num(body.lifetime_performed_replicas),
+    currentAvailableMigrations: num(body.current_available_migrations),
+    currentAvailableReplicas: num(body.current_available_replicas),
+    lifetimeAvailableMigrations: num(body.lifetime_available_migrations),
+    lifetimeAvailableReplicas: num(body.lifetime_available_replicas),
+  };
+};
 
 class LicenceSource {
   async loadAppliancesIds(config?: {
@@ -42,8 +67,9 @@ class LicenceSource {
       skipLog: config?.skipLog,
     });
     const status: LicenceServerStatus = response.data.status;
-    status.supported_licence_versions.sort((a, b) => b.localeCompare(a));
-    return response.data.status;
+    // newest licence format first:
+    status.supported_licence_versions?.sort((a, b) => b.localeCompare(a));
+    return status;
   }
 
   async loadLicenceInfo(
@@ -53,18 +79,17 @@ class LicenceSource {
     const url = `${configLoader.config.servicesUrls.coriolisLicensing}/appliances/${applianceId}/status`;
     const response = await Api.send({ url, quietError: true, skipLog });
     const root = response.data.appliance_licence_status;
+    if (!root) {
+      throw new Error(
+        "The licensing server returned no 'appliance_licence_status' body.",
+      );
+    }
     const licence: Licence = {
       applianceId: root.appliance_id,
       earliestLicenceExpiryDate: new Date(root.earliest_licence_expiry_time),
       latestLicenceExpiryDate: new Date(root.latest_licence_expiry_time),
-      currentPerformedMigrations: root.current_performed_migrations,
-      currentPerformedReplicas: root.current_performed_replicas,
-      lifetimePerformedMigrations: root.lifetime_performed_migrations,
-      lifetimePerformedReplicas: root.lifetime_performed_replicas,
-      currentAvailableMigrations: root.current_available_migrations,
-      currentAvailableReplicas: root.current_available_replicas,
-      lifetimeAvailableMigrations: root.lifetime_available_migrations,
-      lifetimeAvailableReplicas: root.lifetime_available_replicas,
+      standardStats: parseStats(root.standard_licence_stats || root),
+      sapStats: parseStats(root.sap_licence_stats),
     };
 
     return licence;

+ 101 - 0
src/utils/LicenceUtils.ts

@@ -0,0 +1,101 @@
+/*
+Copyright (C) 2026  Cloudbase Solutions SRL
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU Affero General Public License for more details.
+You should have received a copy of the GNU Affero General Public License
+along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+import {
+  LICENCE_VERSION_V2,
+  LICENCE_VERSION_V2_SAP,
+} from "@src/@types/Licence";
+
+import type {
+  Licence,
+  LicenceKind,
+  LicenceServerStatus,
+  LicenceStats,
+} from "@src/@types/Licence";
+
+/** User facing names for the licence flavours. Never show the raw `v2-sap`. */
+export const LICENCE_KIND_LABELS: Record<LicenceKind, string> = {
+  standard: "Standard",
+  sap: "SAP",
+};
+
+class LicenceUtils {
+  static emptyStats(): LicenceStats {
+    return {
+      currentPerformedMigrations: 0,
+      currentPerformedReplicas: 0,
+      lifetimePerformedMigrations: 0,
+      lifetimePerformedReplicas: 0,
+      currentAvailableMigrations: 0,
+      currentAvailableReplicas: 0,
+      lifetimeAvailableMigrations: 0,
+      lifetimeAvailableReplicas: 0,
+    };
+  }
+
+  static isSapVersion(version: string): boolean {
+    return version === LICENCE_VERSION_V2_SAP;
+  }
+
+  static getKindLabel(kind: LicenceKind): string {
+    return LICENCE_KIND_LABELS[kind];
+  }
+
+  /**
+   * The appliance ID is handed to the Coriolis representative suffixed with the
+   * licence format the appliance expects. SAP licences are a variant of the
+   * same `v2` format rather than a newer one, so they must not be picked here.
+   */
+  static getApplianceIdWithVersion(
+    applianceId: string,
+    serverStatus: LicenceServerStatus,
+  ): string {
+    const versions = serverStatus.supported_licence_versions || [];
+    const version =
+      versions.find(v => !LicenceUtils.isSapVersion(v)) || LICENCE_VERSION_V2;
+    return `${applianceId}-licence${version}`;
+  }
+
+  /** Whether any allowance at all was ever issued for this flavour. */
+  static hasStats(stats: LicenceStats): boolean {
+    return (
+      stats.currentAvailableMigrations > 0 ||
+      stats.currentAvailableReplicas > 0 ||
+      stats.lifetimeAvailableMigrations > 0 ||
+      stats.lifetimeAvailableReplicas > 0
+    );
+  }
+
+  /**
+   * The flavours the appliance actually holds licences for, in display order.
+   * Falls back to Standard so an appliance with no allowance at all still
+   * renders its (empty) standard counters rather than nothing.
+   */
+  static getActiveKinds(licence: Licence): LicenceKind[] {
+    const kinds: LicenceKind[] = [];
+    if (LicenceUtils.hasStats(licence.standardStats)) {
+      kinds.push("standard");
+    }
+    if (LicenceUtils.hasStats(licence.sapStats)) {
+      kinds.push("sap");
+    }
+    return kinds.length ? kinds : ["standard"];
+  }
+
+  static getStats(licence: Licence, kind: LicenceKind): LicenceStats {
+    return kind === "sap" ? licence.sapStats : licence.standardStats;
+  }
+}
+
+export default LicenceUtils;