diff --git a/build/pom.xml b/build/pom.xml
index 5a097bb382a..57d54f39046 100755
--- a/build/pom.xml
+++ b/build/pom.xml
@@ -631,6 +631,11 @@
hostNetworkInterface
${project.version}
+
+ org.zstack
+ physicalServer
+ ${project.version}
+
org.zstack
ovn
@@ -977,6 +982,11 @@
hostNetworkInterface
${project.version}
+
+ org.zstack
+ physicalServer
+ ${project.version}
+
org.zstack
observabilityServer
diff --git a/conf/db/upgrade/V5.5.38.1__schema.sql b/conf/db/upgrade/V5.5.38.1__schema.sql
new file mode 100644
index 00000000000..fb1fb2c73ab
--- /dev/null
+++ b/conf/db/upgrade/V5.5.38.1__schema.sql
@@ -0,0 +1,88 @@
+CREATE TABLE IF NOT EXISTS `zstack`.`PhysicalServerVO` (
+ `uuid` varchar(32) NOT NULL,
+ `zoneUuid` varchar(32) DEFAULT NULL,
+ `poolUuid` varchar(32) DEFAULT NULL,
+ `serialNumber` varchar(255) NOT NULL,
+ `createDate` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
+ `lastOpDate` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`uuid`),
+ UNIQUE KEY `ukPhysicalServerSerialNumber` (`serialNumber`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CREATE TABLE IF NOT EXISTS `zstack`.`PhysicalServerResourceAssignmentVO` (
+ `uuid` varchar(32) NOT NULL,
+ `serverUuid` varchar(32) NOT NULL,
+ `resourceType` varchar(32) NOT NULL,
+ `assignmentType` varchar(64) NOT NULL,
+ `spec` text NOT NULL,
+ `specGeneration` bigint NOT NULL DEFAULT 1,
+ `status` text DEFAULT NULL,
+ `observedGeneration` bigint DEFAULT NULL,
+ `observedAt` timestamp NULL DEFAULT NULL,
+ `createDate` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00',
+ `lastOpDate` timestamp NOT NULL DEFAULT '2000-01-01 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
+ PRIMARY KEY (`uuid`),
+ UNIQUE KEY `ukPhysicalServerResourceAssignment` (`serverUuid`, `resourceType`, `assignmentType`),
+ CONSTRAINT `fkPhysicalServerResourceAssignmentServerUuid`
+ FOREIGN KEY (`serverUuid`) REFERENCES `zstack`.`PhysicalServerVO` (`uuid`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+
+CALL ADD_COLUMN('HostEO', 'serverUuid', 'VARCHAR(32)', 1, NULL);
+CALL ADD_COLUMN('ManagementNodeVO', 'serverUuid', 'VARCHAR(32)', 1, NULL);
+
+UPDATE `zstack`.`HostEO`
+SET `serverUuid` = NULL
+WHERE `deleted` IS NOT NULL AND `serverUuid` IS NOT NULL;
+
+DROP PROCEDURE IF EXISTS addPhysicalServerIdentityUniqueKeys;
+DELIMITER $$
+CREATE PROCEDURE addPhysicalServerIdentityUniqueKeys()
+BEGIN
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.statistics
+ WHERE table_schema = 'zstack'
+ AND table_name = 'HostEO'
+ AND index_name = 'ukHostEOServerUuid'
+ ) THEN
+ ALTER TABLE `zstack`.`HostEO`
+ ADD UNIQUE KEY `ukHostEOServerUuid` (`serverUuid`);
+ END IF;
+
+ IF NOT EXISTS (
+ SELECT 1 FROM information_schema.statistics
+ WHERE table_schema = 'zstack'
+ AND table_name = 'ManagementNodeVO'
+ AND index_name = 'ukManagementNodeVOServerUuid'
+ ) THEN
+ ALTER TABLE `zstack`.`ManagementNodeVO`
+ ADD UNIQUE KEY `ukManagementNodeVOServerUuid` (`serverUuid`);
+ END IF;
+END $$
+DELIMITER ;
+CALL addPhysicalServerIdentityUniqueKeys();
+DROP PROCEDURE IF EXISTS addPhysicalServerIdentityUniqueKeys;
+
+CALL ADD_CONSTRAINT(
+ 'HostEO',
+ 'fkHostEOServerUuid',
+ 'serverUuid',
+ 'PhysicalServerVO',
+ 'uuid',
+ 'SET NULL'
+);
+
+DROP VIEW IF EXISTS `zstack`.`HostVO`;
+CREATE VIEW `zstack`.`HostVO` AS
+ SELECT uuid, zoneUuid, clusterUuid, name, description, managementIp, hypervisorType,
+ state, status, createDate, lastOpDate, architecture, serverUuid
+ FROM `zstack`.`HostEO`
+ WHERE deleted IS NULL;
+
+CALL ADD_CONSTRAINT(
+ 'ManagementNodeVO',
+ 'fkManagementNodeVOServerUuid',
+ 'serverUuid',
+ 'PhysicalServerVO',
+ 'uuid',
+ 'SET NULL'
+);
diff --git a/conf/persistence.xml b/conf/persistence.xml
index e76141af3ad..371ebd523cc 100755
--- a/conf/persistence.xml
+++ b/conf/persistence.xml
@@ -229,5 +229,7 @@
org.zstack.network.hostNetworkInterface.PhysicalSwitchVO
org.zstack.network.hostNetworkInterface.PhysicalSwitchPortVO
org.zstack.header.core.external.service.ExternalServiceConfigurationVO
+ org.zstack.physicalserver.PhysicalServerVO
+ org.zstack.physicalserver.PhysicalServerResourceAssignmentVO
diff --git a/conf/serviceConfig/physicalServer.xml b/conf/serviceConfig/physicalServer.xml
new file mode 100644
index 00000000000..5778fc3384c
--- /dev/null
+++ b/conf/serviceConfig/physicalServer.xml
@@ -0,0 +1,23 @@
+
+
+ physicalServer
+ PhysicalServerApiInterceptor
+
+
+ org.zstack.physicalserver.APIQueryPhysicalServerMsg
+ query
+
+
+
+ org.zstack.physicalserver.APIQueryPhysicalServerResourceAssignmentMsg
+ query
+
+
+
+ org.zstack.physicalserver.APIUpdatePhysicalServerCpuAssignmentMsg
+
+
+
+ org.zstack.physicalserver.APIRefreshPhysicalServerCpuIsolationMsg
+
+
diff --git a/conf/springConfigXml/HostManager.xml b/conf/springConfigXml/HostManager.xml
index 8147620c4ae..67605682a22 100755
--- a/conf/springConfigXml/HostManager.xml
+++ b/conf/springConfigXml/HostManager.xml
@@ -74,4 +74,4 @@
-
+
diff --git a/conf/springConfigXml/Kvm.xml b/conf/springConfigXml/Kvm.xml
index a96965fc01b..4b8a25115ee 100755
--- a/conf/springConfigXml/Kvm.xml
+++ b/conf/springConfigXml/Kvm.xml
@@ -115,6 +115,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/springConfigXml/ManagementNodeManager.xml b/conf/springConfigXml/ManagementNodeManager.xml
index ab2d7eefaa1..d04ddd9ff2c 100755
--- a/conf/springConfigXml/ManagementNodeManager.xml
+++ b/conf/springConfigXml/ManagementNodeManager.xml
@@ -17,4 +17,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/springConfigXml/physicalServer.xml b/conf/springConfigXml/physicalServer.xml
new file mode 100644
index 00000000000..760c009035c
--- /dev/null
+++ b/conf/springConfigXml/physicalServer.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/springConfigXml/zbs.xml b/conf/springConfigXml/zbs.xml
index 2e0234ddc1d..89212209eed 100644
--- a/conf/springConfigXml/zbs.xml
+++ b/conf/springConfigXml/zbs.xml
@@ -29,4 +29,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf/zstack.xml b/conf/zstack.xml
index 17bbdfae090..a194805937e 100755
--- a/conf/zstack.xml
+++ b/conf/zstack.xml
@@ -46,6 +46,7 @@
+
diff --git a/core/src/main/java/org/zstack/core/Platform.java b/core/src/main/java/org/zstack/core/Platform.java
index 34df20e3e12..9bb9913dbf7 100755
--- a/core/src/main/java/org/zstack/core/Platform.java
+++ b/core/src/main/java/org/zstack/core/Platform.java
@@ -120,8 +120,10 @@ public class Platform {
};
private static final List UNUSABLE_MACHINE_IDENTITIES = Arrays.asList(
"none",
+ "n/a",
"unknown",
"not specified",
+ "to be filled",
"to be filled by o.e.m.",
"to be filled by oem",
"default string",
@@ -896,6 +898,18 @@ private static String getManagementServerFingerprint() {
return MANAGEMENT_SERVER_FINGERPRINT_ALGORITHM + DigestUtils.sha256Hex(StringUtils.join(identities, "\n"));
}
+ public static String getManagementServerSerialNumber() {
+ String serialNumber = normalizeMachineSerialNumber(
+ readMachineIdentity("/sys/class/dmi/id/product_serial"));
+ return serialNumber == null
+ ? normalizeMachineSerialNumber(readDmiMachineIdentity("system-serial-number"))
+ : serialNumber;
+ }
+
+ public static String normalizeMachineSerialNumber(String serialNumber) {
+ return normalizeMachineIdentity(serialNumber);
+ }
+
private static String readMachineIdentity(String path) {
File file = new File(path);
if (!file.isFile() || !file.canRead()) {
diff --git a/core/src/main/java/org/zstack/core/cpu/LocalCpuTopologyCollector.java b/core/src/main/java/org/zstack/core/cpu/LocalCpuTopologyCollector.java
new file mode 100644
index 00000000000..ae3647d5f2f
--- /dev/null
+++ b/core/src/main/java/org/zstack/core/cpu/LocalCpuTopologyCollector.java
@@ -0,0 +1,134 @@
+package org.zstack.core.cpu;
+
+import org.zstack.core.CoreGlobalProperty;
+import org.zstack.header.physicalserver.PhysicalServerCpuSet;
+import org.zstack.header.physicalserver.PhysicalServerCpuTopology;
+import org.zstack.header.physicalserver.PhysicalServerNumaNode;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class LocalCpuTopologyCollector {
+ private static final Path CPU_ROOT = Paths.get("/sys/devices/system/cpu");
+ private static final Path NODE_ROOT = Paths.get("/sys/devices/system/node");
+ private volatile PhysicalServerCpuTopology testTopology;
+
+ public PhysicalServerCpuTopology collect() {
+ if (CoreGlobalProperty.UNIT_TEST_ON && testTopology != null) {
+ return testTopology;
+ }
+ try {
+ SortedSet online = readCpuSet(CPU_ROOT.resolve("online"));
+ List nodePaths = nodePaths();
+ if (nodePaths.isEmpty()) {
+ return PhysicalServerCpuTopology.from(
+ Collections.singletonMap("0", node("0", online, online)));
+ }
+
+ Map result = new LinkedHashMap<>();
+ for (Path nodePath : nodePaths) {
+ String nodeId = nodePath.getFileName().toString().substring("node".length());
+ SortedSet cpus = readCpuSet(nodePath.resolve("cpulist"));
+ SortedSet nodeOnline = new TreeSet<>(cpus);
+ nodeOnline.retainAll(online);
+ if (!nodeOnline.isEmpty()) {
+ result.put(nodeId, node(nodeId, cpus, nodeOnline));
+ }
+ }
+ if (result.isEmpty()) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_UNAVAILABLE: no online NUMA CPU found");
+ }
+ return PhysicalServerCpuTopology.from(result);
+ } catch (IOException exception) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_UNAVAILABLE: " + exception.getMessage(), exception);
+ }
+ }
+
+ public void setTestTopology(PhysicalServerCpuTopology testTopology) {
+ if (!CoreGlobalProperty.UNIT_TEST_ON) {
+ throw new IllegalStateException("test topology is only available in unit-test mode");
+ }
+ this.testTopology = testTopology;
+ }
+
+ public void clearTestTopology() {
+ testTopology = null;
+ }
+
+ private List nodePaths() throws IOException {
+ if (!Files.isDirectory(NODE_ROOT)) {
+ return Collections.emptyList();
+ }
+ List paths = new ArrayList<>();
+ try (DirectoryStream stream = Files.newDirectoryStream(NODE_ROOT, "node[0-9]*")) {
+ for (Path path : stream) {
+ if (path.getFileName().toString().matches("node[0-9]+")) {
+ paths.add(path);
+ }
+ }
+ }
+ paths.sort(Comparator.comparingInt(path -> Integer.parseInt(
+ path.getFileName().toString().substring("node".length()))));
+ return paths;
+ }
+
+ private PhysicalServerNumaNode node(
+ String nodeId, SortedSet cpus, SortedSet online) throws IOException {
+ PhysicalServerNumaNode node = new PhysicalServerNumaNode();
+ node.setNodeId(nodeId);
+ node.setOnlineCpus(strings(online));
+ node.setCoreGroups(coreGroups(online));
+ return node;
+ }
+
+ private List> coreGroups(SortedSet online) throws IOException {
+ Set> groups = new LinkedHashSet<>();
+ for (Integer cpu : online) {
+ Path siblings = CPU_ROOT.resolve(String.format("cpu%s/topology/thread_siblings_list", cpu));
+ SortedSet group = readCpuSet(siblings);
+ group.retainAll(online);
+ if (group.isEmpty()) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_TOPOLOGY_INVALID: CPU[%s] has no online core sibling", cpu));
+ }
+ groups.add(group);
+ }
+ List> sorted = new ArrayList<>(groups);
+ sorted.sort(Comparator.comparingInt(SortedSet::first));
+ List> result = new ArrayList<>();
+ for (SortedSet group : sorted) {
+ result.add(strings(group));
+ }
+ return result;
+ }
+
+ private SortedSet readCpuSet(Path path) throws IOException {
+ String value = new String(Files.readAllBytes(path), StandardCharsets.US_ASCII).trim();
+ if (value.isEmpty()) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_INVALID: empty CPU list at " + path);
+ }
+ return PhysicalServerCpuSet.parse(value);
+ }
+
+ private List strings(Set cpus) {
+ List result = new ArrayList<>();
+ for (Integer cpu : cpus) {
+ result.add(String.valueOf(cpu));
+ }
+ return result;
+ }
+}
diff --git a/core/src/main/java/org/zstack/core/cpu/LocalSharedCpuSetExecutor.java b/core/src/main/java/org/zstack/core/cpu/LocalSharedCpuSetExecutor.java
new file mode 100644
index 00000000000..9d191eb64db
--- /dev/null
+++ b/core/src/main/java/org/zstack/core/cpu/LocalSharedCpuSetExecutor.java
@@ -0,0 +1,550 @@
+package org.zstack.core.cpu;
+
+import org.zstack.core.CoreGlobalProperty;
+import org.zstack.header.physicalserver.PhysicalServerCpuSet;
+import org.zstack.header.physicalserver.SharedCpuSetCommand;
+import org.zstack.header.physicalserver.SharedCpuSetResponse;
+import org.zstack.header.physicalserver.SharedCpuSetServiceHandle;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.PosixFileAttributes;
+import java.nio.file.attribute.PosixFilePermission;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class LocalSharedCpuSetExecutor {
+ private static final Path V2_ROOT = Paths.get("/sys/fs/cgroup");
+ private static final Path V1_ROOT = Paths.get("/sys/fs/cgroup/cpuset");
+ private static final long COMMAND_TIMEOUT_SECONDS = 30;
+
+ private final AtomicInteger testCalls = new AtomicInteger();
+ private volatile boolean testMode;
+ private volatile SharedCpuSetCommand lastTestCommand;
+
+ public SharedCpuSetResponse apply(SharedCpuSetCommand command) {
+ if (CoreGlobalProperty.UNIT_TEST_ON && testMode) {
+ return fakeApply(command);
+ }
+
+ Backend backend;
+ try {
+ backend = backend();
+ } catch (SharedCpuSetException exception) {
+ String reasonCode = reasonCode(exception);
+ if ("SHARED_CPUSET_UNAVAILABLE".equals(reasonCode)) {
+ return unavailable(command, reasonCode);
+ }
+ throw exception;
+ }
+ String desired = command.isEnabled()
+ ? PhysicalServerCpuSet.normalize(command.getCpuSet()) : "";
+ int expected = 0;
+ int covered = 0;
+ Set actualSets = new LinkedHashSet<>();
+ Set consumerKeys = new LinkedHashSet<>();
+ List unresolved = new ArrayList<>();
+
+ for (SharedCpuSetServiceHandle handle : command.getHandles()) {
+ try {
+ Path target = resolve(backend, command.getRoleKey(), handle);
+ if (target == null) {
+ continue;
+ }
+ expected++;
+ if (handle.getConsumerKey() != null) {
+ consumerKeys.add(handle.getConsumerKey());
+ }
+ actualSets.add(applyToGroup(backend, target, desired, command.isEnabled()));
+ covered++;
+ } catch (SharedCpuSetException exception) {
+ expected++;
+ unresolved.add(unresolvedHandle(handle, reasonCode(exception)));
+ }
+ }
+
+ String actual = command.isEnabled() && actualSets.size() == 1
+ ? actualSets.iterator().next() : "";
+ boolean ready = command.isEnabled()
+ ? expected > 0 && covered == expected && desired.equals(actual)
+ : covered == expected;
+ SharedCpuSetResponse response = new SharedCpuSetResponse();
+ response.setBackend(backend.name);
+ response.setActualCpuSet(actual);
+ response.setCoveredServiceCount(covered);
+ response.setExpectedServiceCount(expected);
+ response.setConsumerKeys(new ArrayList<>(consumerKeys));
+ response.setUnresolvedHandles(unresolved);
+ if (!ready) {
+ response.setReasonCode(unresolved.isEmpty()
+ ? "CPUSET_EFFECTIVE_MISMATCH" : "HANDLE_NOT_RESOLVED");
+ }
+ return response;
+ }
+
+ private SharedCpuSetResponse unavailable(
+ SharedCpuSetCommand command, String reasonCode) {
+ Set consumerKeys = new LinkedHashSet<>();
+ List unresolved = new ArrayList<>();
+ for (SharedCpuSetServiceHandle handle : command.getHandles()) {
+ if (handle.getConsumerKey() != null) {
+ consumerKeys.add(handle.getConsumerKey());
+ }
+ unresolved.add(unresolvedHandle(handle, reasonCode));
+ }
+ SharedCpuSetResponse response = new SharedCpuSetResponse();
+ response.setActualCpuSet("");
+ response.setExpectedServiceCount(command.getHandles().size());
+ response.setCoveredServiceCount(0);
+ response.setConsumerKeys(new ArrayList<>(consumerKeys));
+ response.setUnresolvedHandles(unresolved);
+ response.setReasonCode(reasonCode);
+ return response;
+ }
+
+ public void setTestMode(boolean testMode) {
+ if (!CoreGlobalProperty.UNIT_TEST_ON) {
+ throw new IllegalStateException("test executor is only available in unit-test mode");
+ }
+ this.testMode = testMode;
+ this.lastTestCommand = null;
+ this.testCalls.set(0);
+ }
+
+ public SharedCpuSetCommand getLastTestCommand() {
+ return lastTestCommand;
+ }
+
+ public int getTestCalls() {
+ return testCalls.get();
+ }
+
+ private SharedCpuSetResponse fakeApply(SharedCpuSetCommand command) {
+ lastTestCommand = command;
+ testCalls.incrementAndGet();
+ Set consumerKeys = new LinkedHashSet<>();
+ for (SharedCpuSetServiceHandle handle : command.getHandles()) {
+ if (handle.getConsumerKey() != null) {
+ consumerKeys.add(handle.getConsumerKey());
+ }
+ }
+ SharedCpuSetResponse response = new SharedCpuSetResponse();
+ response.setBackend("FAKE_CPUSET");
+ response.setActualCpuSet(command.isEnabled()
+ ? PhysicalServerCpuSet.normalize(command.getCpuSet()) : "");
+ response.setExpectedServiceCount(command.getHandles().size());
+ response.setCoveredServiceCount(command.getHandles().size());
+ response.setConsumerKeys(new ArrayList<>(consumerKeys));
+ response.setUnresolvedHandles(Collections.emptyList());
+ return response;
+ }
+
+ private Backend backend() {
+ Path controllers = V2_ROOT.resolve("cgroup.controllers");
+ if (Files.isRegularFile(controllers)) {
+ String values = read(controllers);
+ if (values.matches("(?s).*\\bcpuset\\b.*")
+ || Files.isRegularFile(V2_ROOT.resolve("cpuset.cpus.effective"))) {
+ return new Backend("CGROUP_V2_CPUSET", V2_ROOT, true);
+ }
+ }
+ if (Files.isRegularFile(V1_ROOT.resolve("cpuset.cpus"))) {
+ return new Backend("CGROUP_V1_CPUSET", V1_ROOT, false);
+ }
+ throw new SharedCpuSetException("SHARED_CPUSET_UNAVAILABLE");
+ }
+
+ private Path resolve(
+ Backend backend, String roleKey, SharedCpuSetServiceHandle handle) {
+ if (SharedCpuSetServiceHandle.SYSTEMD_UNIT.equals(handle.getHandleType())) {
+ return resolveSystemd(backend, handle);
+ }
+ if (SharedCpuSetServiceHandle.OWNER_PID_FILE.equals(handle.getHandleType())) {
+ return resolvePidFile(backend, roleKey, handle);
+ }
+ throw new SharedCpuSetException("HANDLE_TYPE_UNSUPPORTED");
+ }
+
+ private Path resolveSystemd(Backend backend, SharedCpuSetServiceHandle handle) {
+ Map properties = systemdProperties(handle.getValue());
+ if ("not-found".equals(properties.get("LoadState"))) {
+ if (handle.isOptional()) {
+ return null;
+ }
+ throw new SharedCpuSetException("SYSTEMD_UNIT_NOT_FOUND");
+ }
+ if (!"active".equals(properties.get("ActiveState"))) {
+ throw new SharedCpuSetException("SYSTEMD_UNIT_NOT_ACTIVE");
+ }
+ String controlGroup = properties.get("ControlGroup");
+ if (controlGroup == null || controlGroup.isEmpty()) {
+ throw new SharedCpuSetException("SYSTEMD_CONTROL_GROUP_MISSING");
+ }
+ Path target = underRoot(backend.root, backend.root.resolve(stripRoot(controlGroup)).normalize());
+ if (target.equals(backend.root)) {
+ throw new SharedCpuSetException("SYSTEMD_CONTROL_GROUP_INVALID");
+ }
+ if (!Files.isDirectory(target)) {
+ throw new SharedCpuSetException("SYSTEMD_CONTROL_GROUP_NOT_FOUND");
+ }
+ return target;
+ }
+
+ private Path resolvePidFile(
+ Backend backend, String roleKey, SharedCpuSetServiceHandle handle) {
+ Path pidFile = Paths.get(handle.getValue());
+ if (!Files.exists(pidFile, LinkOption.NOFOLLOW_LINKS)) {
+ if (handle.isOptional()) {
+ return null;
+ }
+ throw new SharedCpuSetException("PID_FILE_NOT_FOUND");
+ }
+ try {
+ PosixFileAttributes attributes = Files.readAttributes(
+ pidFile, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ if (!attributes.isRegularFile() || Files.isSymbolicLink(pidFile)) {
+ throw new SharedCpuSetException("PID_FILE_NOT_REGULAR");
+ }
+ Set permissions = attributes.permissions();
+ if (permissions.contains(PosixFilePermission.GROUP_WRITE)
+ || permissions.contains(PosixFilePermission.OTHERS_WRITE)) {
+ throw new SharedCpuSetException("PID_FILE_PERMISSION_INVALID");
+ }
+
+ String content = read(pidFile).trim();
+ if (!content.matches("[1-9][0-9]*")) {
+ throw new SharedCpuSetException("PID_FILE_FORMAT_INVALID");
+ }
+ long parsed = Long.parseLong(content);
+ if (parsed > Integer.MAX_VALUE) {
+ throw new SharedCpuSetException("PID_FILE_FORMAT_INVALID");
+ }
+ int pid = (int) parsed;
+ Path proc = Paths.get("/proc", String.valueOf(pid));
+ if (!Files.isDirectory(proc)) {
+ throw new SharedCpuSetException("PID_NOT_RUNNING");
+ }
+ if (attributes.lastModifiedTime().toMillis() + 1000 < processStartTimeMillis(pid)) {
+ throw new SharedCpuSetException("PID_FILE_STALE");
+ }
+ PosixFileAttributes procAttributes = Files.readAttributes(
+ proc, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
+ if (!"root".equals(attributes.owner().getName())
+ && !attributes.owner().equals(procAttributes.owner())) {
+ throw new SharedCpuSetException("PID_FILE_OWNER_MISMATCH");
+ }
+ String expected = handle.getExpectedCommandToken();
+ String command = new String(Files.readAllBytes(proc.resolve("cmdline")), StandardCharsets.UTF_8)
+ .replace('\0', ' ');
+ if (expected != null && !command.contains(expected)) {
+ throw new SharedCpuSetException("PID_COMMAND_MISMATCH");
+ }
+
+ String relative = pidCgroup(pid, backend);
+ if (!"/".equals(relative)) {
+ Path target = underRoot(backend.root,
+ backend.root.resolve(stripRoot(relative)).normalize());
+ if (!Files.isDirectory(target)) {
+ throw new SharedCpuSetException("PID_CONTROL_GROUP_NOT_FOUND");
+ }
+ return target;
+ }
+
+ Path target = backend.root.resolve("zstack-role-" + safeRole(roleKey));
+ mkdir(target);
+ enableV2Path(backend, target);
+ initializeMems(backend, target);
+ initializeCpus(backend, target);
+ write(target.resolve("cgroup.procs"), String.valueOf(pid));
+ return target;
+ } catch (NumberFormatException exception) {
+ throw new SharedCpuSetException("PID_FILE_FORMAT_INVALID");
+ } catch (IOException exception) {
+ throw new SharedCpuSetException("PID_FILE_READ_FAILED:" + exception.getMessage());
+ }
+ }
+
+ private long processStartTimeMillis(int pid) {
+ long bootTimeSeconds = -1;
+ for (String line : read(Paths.get("/proc/stat")).split("\\R")) {
+ if (line.startsWith("btime ")) {
+ try {
+ bootTimeSeconds = Long.parseLong(line.substring("btime ".length()).trim());
+ } catch (NumberFormatException exception) {
+ throw new SharedCpuSetException("PID_START_TIME_UNAVAILABLE");
+ }
+ break;
+ }
+ }
+ String stat = read(Paths.get("/proc", String.valueOf(pid), "stat"));
+ int commandEnd = stat.lastIndexOf(')');
+ if (bootTimeSeconds < 0 || commandEnd < 0) {
+ throw new SharedCpuSetException("PID_START_TIME_UNAVAILABLE");
+ }
+ String[] fields = stat.substring(commandEnd + 1).trim().split("\\s+");
+ if (fields.length <= 19) {
+ throw new SharedCpuSetException("PID_START_TIME_UNAVAILABLE");
+ }
+ try {
+ long startTicks = Long.parseLong(fields[19]);
+ long clockTicks = Long.parseLong(run(null, "getconf", "CLK_TCK").trim());
+ if (clockTicks <= 0) {
+ throw new SharedCpuSetException("PID_START_TIME_UNAVAILABLE");
+ }
+ return bootTimeSeconds * 1000 + startTicks * 1000 / clockTicks;
+ } catch (NumberFormatException exception) {
+ throw new SharedCpuSetException("PID_START_TIME_UNAVAILABLE");
+ }
+ }
+
+ private String applyToGroup(
+ Backend backend, Path target, String desired, boolean enabled) {
+ enableV2Path(backend, target);
+ initializeMems(backend, target);
+ Path cpuFile = target.resolve("cpuset.cpus");
+ if (!Files.isRegularFile(cpuFile)) {
+ throw new SharedCpuSetException("CPUSET_CONTROLLER_NOT_DELEGATED");
+ }
+ String value = enabled ? desired : backend.v2 ? "" : parentCpuSet(target);
+ String configured = normalizeOptional(read(cpuFile));
+ if (!configured.equals(value)) {
+ write(cpuFile, value.isEmpty() && backend.v2 ? "\n" : value);
+ }
+ if (!enabled) {
+ if (!normalizeOptional(read(cpuFile)).equals(value)) {
+ throw new SharedCpuSetException("CPUSET_RELEASE_MISMATCH");
+ }
+ return "";
+ }
+ Path effective = target.resolve("cpuset.cpus.effective");
+ return normalizeOptional(read(Files.isRegularFile(effective) ? effective : cpuFile));
+ }
+
+ private void enableV2Path(Backend backend, Path target) {
+ if (!backend.v2) {
+ return;
+ }
+ Path current = backend.root;
+ for (Path part : backend.root.relativize(target)) {
+ Path child = current.resolve(part);
+ if (!Files.isRegularFile(child.resolve("cpuset.cpus"))) {
+ Path control = current.resolve("cgroup.subtree_control");
+ if (!Files.isRegularFile(control)) {
+ throw new SharedCpuSetException("CGROUP_V2_SUBTREE_CONTROL_MISSING");
+ }
+ write(control, "+cpuset");
+ }
+ current = child;
+ }
+ }
+
+ private void initializeMems(Backend backend, Path target) {
+ Path mems = target.resolve("cpuset.mems");
+ if (!Files.isRegularFile(mems) || !read(mems).trim().isEmpty()) {
+ return;
+ }
+ Path parent = target.getParent();
+ Path source = parent.resolve("cpuset.mems.effective");
+ if (!Files.isRegularFile(source)) {
+ source = parent.resolve("cpuset.mems");
+ }
+ String value = read(source).trim();
+ if (value.isEmpty()) {
+ throw new SharedCpuSetException("CPUSET_MEMS_EMPTY");
+ }
+ write(mems, value);
+ }
+
+ private void initializeCpus(Backend backend, Path target) {
+ if (backend.v2) {
+ return;
+ }
+ Path cpus = target.resolve("cpuset.cpus");
+ if (read(cpus).trim().isEmpty()) {
+ write(cpus, parentCpuSet(target));
+ }
+ }
+
+ private String parentCpuSet(Path target) {
+ Path parent = target.getParent();
+ Path source = parent.resolve("cpuset.cpus.effective");
+ if (!Files.isRegularFile(source)) {
+ source = parent.resolve("cpuset.cpus");
+ }
+ String value = normalizeOptional(read(source));
+ if (value.isEmpty()) {
+ throw new SharedCpuSetException("PARENT_CPUSET_EMPTY");
+ }
+ return value;
+ }
+
+ private String pidCgroup(int pid, Backend backend) {
+ for (String line : read(Paths.get("/proc", String.valueOf(pid), "cgroup")).split("\\R")) {
+ String[] parts = line.split(":", 3);
+ if (parts.length != 3) {
+ continue;
+ }
+ if (backend.v2 && "0".equals(parts[0])) {
+ return parts[2];
+ }
+ if (!backend.v2) {
+ for (String controller : parts[1].split(",")) {
+ if ("cpuset".equals(controller)) {
+ return parts[2];
+ }
+ }
+ }
+ }
+ throw new SharedCpuSetException("PID_CPUSET_MEMBERSHIP_MISSING");
+ }
+
+ private Map systemdProperties(String unit) {
+ String output = run(null,
+ "systemctl", "show", unit,
+ "--property=LoadState", "--property=ActiveState", "--property=ControlGroup");
+ Map properties = new HashMap<>();
+ for (String line : output.split("\\R")) {
+ int separator = line.indexOf('=');
+ if (separator > 0) {
+ properties.put(line.substring(0, separator), line.substring(separator + 1));
+ }
+ }
+ return properties;
+ }
+
+ private void mkdir(Path path) {
+ if (Files.isDirectory(path)) {
+ return;
+ }
+ run(null, "sudo", "-n", "mkdir", "-p", path.toString());
+ }
+
+ private String read(Path path) {
+ try {
+ return new String(Files.readAllBytes(path), StandardCharsets.US_ASCII);
+ } catch (IOException exception) {
+ throw new SharedCpuSetException("READ_FAILED:" + path + ":" + exception.getMessage());
+ }
+ }
+
+ private void write(Path path, String value) {
+ run(value.getBytes(StandardCharsets.US_ASCII),
+ "sudo", "-n", "tee", path.toString());
+ }
+
+ private String run(byte[] input, String... command) {
+ Process process = null;
+ try {
+ process = new ProcessBuilder(command).redirectErrorStream(true).start();
+ if (input != null) {
+ try (OutputStream stream = process.getOutputStream()) {
+ stream.write(input);
+ }
+ } else {
+ process.getOutputStream().close();
+ }
+ if (!process.waitFor(COMMAND_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ throw new SharedCpuSetException("COMMAND_TIMEOUT:" + command[0]);
+ }
+ String output = readStream(process.getInputStream());
+ if (process.exitValue() != 0) {
+ throw new SharedCpuSetException(
+ "COMMAND_FAILED:" + command[0] + ":" + output.trim());
+ }
+ return output;
+ } catch (IOException exception) {
+ throw new SharedCpuSetException("COMMAND_FAILED:" + command[0] + ":" + exception.getMessage());
+ } catch (InterruptedException exception) {
+ Thread.currentThread().interrupt();
+ throw new SharedCpuSetException("COMMAND_INTERRUPTED:" + command[0]);
+ } finally {
+ if (process != null) {
+ process.destroy();
+ }
+ }
+ }
+
+ private String readStream(InputStream stream) throws IOException {
+ ByteArrayOutputStream output = new ByteArrayOutputStream();
+ byte[] buffer = new byte[4096];
+ int count;
+ while ((count = stream.read(buffer)) != -1) {
+ output.write(buffer, 0, count);
+ }
+ return new String(output.toByteArray(), StandardCharsets.UTF_8);
+ }
+
+ private String normalizeOptional(String value) {
+ String trimmed = value == null ? "" : value.trim();
+ return trimmed.isEmpty() ? "" : PhysicalServerCpuSet.normalize(trimmed);
+ }
+
+ private Path underRoot(Path root, Path path) {
+ if (!path.equals(root) && !path.startsWith(root)) {
+ throw new SharedCpuSetException("CONTROL_GROUP_PATH_INVALID");
+ }
+ return path;
+ }
+
+ private String stripRoot(String path) {
+ return path.startsWith("/") ? path.substring(1) : path;
+ }
+
+ private String safeRole(String roleKey) {
+ String value = roleKey == null ? "" : roleKey.replaceAll("[^a-zA-Z0-9_.-]", "-");
+ if (value.isEmpty()) {
+ throw new SharedCpuSetException("ROLE_KEY_INVALID");
+ }
+ return value;
+ }
+
+ private String reasonCode(SharedCpuSetException exception) {
+ String message = exception.getMessage();
+ if (message == null || message.isEmpty()) {
+ return "SHARED_CPUSET_ERROR";
+ }
+ int separator = message.indexOf(':');
+ return separator > 0 ? message.substring(0, separator) : message;
+ }
+
+ private String unresolvedHandle(SharedCpuSetServiceHandle handle, String reasonCode) {
+ String identity = SharedCpuSetServiceHandle.SYSTEMD_UNIT.equals(handle.getHandleType())
+ ? handle.getValue() : handle.getHandleType();
+ return String.format("%s:%s:%s", handle.getConsumerKey(), identity, reasonCode);
+ }
+
+ private static class Backend {
+ private final String name;
+ private final Path root;
+ private final boolean v2;
+
+ private Backend(String name, Path root, boolean v2) {
+ this.name = name;
+ this.root = root;
+ this.v2 = v2;
+ }
+ }
+
+ public static class SharedCpuSetException extends RuntimeException {
+ public SharedCpuSetException(String message) {
+ super(message);
+ }
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/host/HostAO.java b/header/src/main/java/org/zstack/header/host/HostAO.java
index 8f7bd58ef95..52c2f11a6f6 100755
--- a/header/src/main/java/org/zstack/header/host/HostAO.java
+++ b/header/src/main/java/org/zstack/header/host/HostAO.java
@@ -37,6 +37,9 @@ public class HostAO extends ResourceVO {
@Column
private String architecture;
+ @Column
+ private String serverUuid;
+
@Column
@Enumerated(EnumType.STRING)
private HostState state;
@@ -146,4 +149,12 @@ public String getArchitecture() {
public void setArchitecture(String architecture) {
this.architecture = architecture;
}
+
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
}
diff --git a/header/src/main/java/org/zstack/header/host/HostAO_.java b/header/src/main/java/org/zstack/header/host/HostAO_.java
index 921e2b5ac64..c6568126895 100755
--- a/header/src/main/java/org/zstack/header/host/HostAO_.java
+++ b/header/src/main/java/org/zstack/header/host/HostAO_.java
@@ -21,4 +21,5 @@ public class HostAO_ extends ResourceVO_ {
public static volatile SingularAttribute createDate;
public static volatile SingularAttribute lastOpDate;
public static volatile SingularAttribute architecture;
+ public static volatile SingularAttribute serverUuid;
}
diff --git a/header/src/main/java/org/zstack/header/host/HostInventory.java b/header/src/main/java/org/zstack/header/host/HostInventory.java
index 35958b05003..38d6baf8f93 100755
--- a/header/src/main/java/org/zstack/header/host/HostInventory.java
+++ b/header/src/main/java/org/zstack/header/host/HostInventory.java
@@ -185,6 +185,8 @@ public class HostInventory implements Serializable {
private String architecture;
+ private String serverUuid;
+
/**
* @desc the time this resource gets created
*/
@@ -207,6 +209,7 @@ protected HostInventory(HostVO vo) {
this.setZoneUuid(vo.getZoneUuid());
this.setClusterUuid(vo.getClusterUuid());
this.setArchitecture(vo.getArchitecture());
+ this.setServerUuid(vo.getServerUuid());
if (vo.getCapacity() != null) {
this.setTotalCpuCapacity(vo.getCapacity().getTotalCpu());
this.setAvailableCpuCapacity(vo.getCapacity().getAvailableCpu());
@@ -395,6 +398,14 @@ public void setArchitecture(String architecture) {
this.architecture = architecture;
}
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
+
public String getIpmiAddress() {
return ipmiAddress;
}
diff --git a/header/src/main/java/org/zstack/header/host/HostNUMANode.java b/header/src/main/java/org/zstack/header/host/HostNUMANode.java
index 733a90cc828..39993485e36 100644
--- a/header/src/main/java/org/zstack/header/host/HostNUMANode.java
+++ b/header/src/main/java/org/zstack/header/host/HostNUMANode.java
@@ -7,6 +7,8 @@
public class HostNUMANode {
public List distance;
public List cpus;
+ public List onlineCpus;
+ public List> coreGroups;
public Long free;
public Long size;
public String nodeID;
@@ -36,6 +38,22 @@ public void setCpus(List cpus) {
this.cpus = cpus;
}
+ public List getOnlineCpus() {
+ return onlineCpus;
+ }
+
+ public void setOnlineCpus(List onlineCpus) {
+ this.onlineCpus = onlineCpus;
+ }
+
+ public List> getCoreGroups() {
+ return coreGroups;
+ }
+
+ public void setCoreGroups(List> coreGroups) {
+ this.coreGroups = coreGroups;
+ }
+
public Long getFree() {
return free;
}
diff --git a/header/src/main/java/org/zstack/header/host/HostVO.java b/header/src/main/java/org/zstack/header/host/HostVO.java
index 319c26750c4..48f0067e435 100755
--- a/header/src/main/java/org/zstack/header/host/HostVO.java
+++ b/header/src/main/java/org/zstack/header/host/HostVO.java
@@ -80,6 +80,6 @@ protected HostVO(HostVO vo) {
this.setCapacity(vo.getCapacity());
this.setIpmi(vo.getIpmi());
this.setHwMonitorStatus(vo.getHwMonitorStatus());
+ this.setServerUuid(vo.getServerUuid());
}
}
-
diff --git a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeInventory.java b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeInventory.java
index 97ce31aec22..f19d8a74c8b 100755
--- a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeInventory.java
+++ b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeInventory.java
@@ -17,6 +17,7 @@ public class ManagementNodeInventory {
private String hostName;
private Timestamp joinDate;
private Timestamp heartBeat;
+ private String serverUuid;
public static ManagementNodeInventory valueOf(ManagementNodeVO vo) {
ManagementNodeInventory inv = new ManagementNodeInventory();
@@ -24,6 +25,7 @@ public static ManagementNodeInventory valueOf(ManagementNodeVO vo) {
inv.setHostName(vo.getHostName());
inv.setJoinDate(vo.getJoinDate());
inv.setUuid(vo.getUuid());
+ inv.setServerUuid(vo.getServerUuid());
return inv;
}
@@ -66,4 +68,12 @@ public Timestamp getJoinDate() {
public Timestamp getHeartBeat() {
return heartBeat;
}
+
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
}
diff --git a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO.java b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO.java
index bd89bcbbf3d..d82cb6ec0e9 100755
--- a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO.java
+++ b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO.java
@@ -26,6 +26,9 @@ public class ManagementNodeVO {
@Column
private int port;
+ @Column
+ private String serverUuid;
+
public String getHostName() {
return hostName;
}
@@ -73,4 +76,12 @@ public void setJoinDate(Timestamp joinDate) {
public void setHeartBeat(Timestamp heartBeat) {
this.heartBeat = heartBeat;
}
+
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
}
diff --git a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO_.java b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO_.java
index 70035f974cc..81d98c0450f 100755
--- a/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO_.java
+++ b/header/src/main/java/org/zstack/header/managementnode/ManagementNodeVO_.java
@@ -13,4 +13,5 @@ public class ManagementNodeVO_ {
public static volatile SingularAttribute joinDate;
public static volatile SingularAttribute heartBeat;
public static volatile SingularAttribute state;
+ public static volatile SingularAttribute serverUuid;
}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerAdapter.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerAdapter.java
new file mode 100644
index 00000000000..3cc7ad7706e
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerAdapter.java
@@ -0,0 +1,29 @@
+package org.zstack.header.physicalserver;
+
+import org.zstack.header.core.ReturnValueCompletion;
+
+import java.util.Set;
+
+public interface PhysicalServerCpuConsumerAdapter {
+ String getAssignmentType();
+
+ default void refreshAssociations() {
+ }
+
+ default void refreshCapacity(String serverUuid) {
+ }
+
+ Set getAssociatedServerUuids();
+
+ PhysicalServerCpuConsumerState getState(String serverUuid);
+
+ void collectTopology(
+ String serverUuid,
+ ReturnValueCompletion completion);
+
+ void apply(
+ String serverUuid,
+ String consumerUuid,
+ SharedCpuSetCommand command,
+ ReturnValueCompletion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerState.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerState.java
new file mode 100644
index 00000000000..4e9f8225fbb
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuConsumerState.java
@@ -0,0 +1,7 @@
+package org.zstack.header.physicalserver;
+
+public enum PhysicalServerCpuConsumerState {
+ AVAILABLE,
+ UNAVAILABLE,
+ MISSING
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuSet.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuSet.java
new file mode 100644
index 00000000000..d532c7ebea0
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuSet.java
@@ -0,0 +1,191 @@
+package org.zstack.header.physicalserver;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.math.BigInteger;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public final class PhysicalServerCpuSet {
+ private PhysicalServerCpuSet() {
+ }
+
+ public static String normalize(String value) {
+ return formatRanges(parseRanges(value));
+ }
+
+ public static SortedSet parse(String value, Set allowed) {
+ List ranges = parseRanges(value);
+ SortedSet cpus = new TreeSet<>();
+ for (Range range : ranges) {
+ long width = (long) range.end - range.start + 1;
+ if (width > allowed.size()) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SET range[%s-%s] contains CPUs outside the online topology", range.start, range.end));
+ }
+ for (long cursor = range.start; cursor <= range.end; cursor++) {
+ int cpu = (int) cursor;
+ if (!allowed.contains(cpu)) {
+ throw new IllegalArgumentException(String.format(
+ "CPU[%s] is outside the online topology", cpu));
+ }
+ cpus.add(cpu);
+ }
+ }
+ return cpus;
+ }
+
+ public static SortedSet parse(String value) {
+ List ranges = parseRanges(value);
+ SortedSet cpus = new TreeSet<>();
+ for (Range range : ranges) {
+ long width = (long) range.end - range.start + 1;
+ if (width > 1048576) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SET range[%s-%s] is too large", range.start, range.end));
+ }
+ for (long cursor = range.start; cursor <= range.end; cursor++) {
+ cpus.add((int) cursor);
+ }
+ }
+ return cpus;
+ }
+
+ public static String format(Collection cpus) {
+ if (cpus == null || cpus.isEmpty()) {
+ return "";
+ }
+ List sorted = new ArrayList<>(new LinkedHashSet<>(cpus));
+ Collections.sort(sorted);
+ List ranges = new ArrayList<>();
+ int start = sorted.get(0);
+ int end = start;
+ for (int index = 1; index < sorted.size(); index++) {
+ int current = sorted.get(index);
+ if ((long) current == (long) end + 1) {
+ end = current;
+ } else {
+ ranges.add(new Range(start, end));
+ start = current;
+ end = current;
+ }
+ }
+ ranges.add(new Range(start, end));
+ return formatRanges(ranges);
+ }
+
+ public static int count(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ return 0;
+ }
+ long count = 0;
+ for (Range range : parseRanges(value)) {
+ count += (long) range.end - range.start + 1;
+ if (count > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("CPU_SET contains too many CPUs");
+ }
+ }
+ return (int) count;
+ }
+
+ public static String union(String left, String right) {
+ boolean leftEmpty = left == null || left.trim().isEmpty();
+ boolean rightEmpty = right == null || right.trim().isEmpty();
+ if (leftEmpty && rightEmpty) {
+ return "";
+ }
+ if (leftEmpty) {
+ return normalize(right);
+ }
+ if (rightEmpty) {
+ return normalize(left);
+ }
+ return normalize(left + "," + right);
+ }
+
+ private static List parseRanges(String value) {
+ if (value == null || value.trim().isEmpty()) {
+ throw new IllegalArgumentException("CPU_SET cannot be empty");
+ }
+
+ List ranges = new ArrayList<>();
+ for (String item : value.split(",")) {
+ String part = item.trim();
+ if (part.isEmpty()) {
+ throw new IllegalArgumentException("CPU_SET contains an empty item");
+ }
+
+ String[] range = part.split("-", -1);
+ if (range.length > 2) {
+ throw new IllegalArgumentException(String.format("invalid CPU_SET item[%s]", part));
+ }
+
+ int start = parseCpu(range[0]);
+ int end = range.length == 1 ? start : parseCpu(range[1]);
+ if (start > end) {
+ throw new IllegalArgumentException(String.format("invalid CPU_SET range[%s]", part));
+ }
+ ranges.add(new Range(start, end));
+ }
+
+ ranges.sort(Comparator.comparingInt(range -> range.start));
+ List mergedRanges = new ArrayList<>();
+ Range merged = ranges.get(0);
+ for (int index = 1; index < ranges.size(); index++) {
+ Range current = ranges.get(index);
+ if ((long) current.start <= (long) merged.end + 1) {
+ merged.end = Math.max(merged.end, current.end);
+ } else {
+ mergedRanges.add(merged);
+ merged = current;
+ }
+ }
+ mergedRanges.add(merged);
+ return mergedRanges;
+ }
+
+ private static String formatRanges(List ranges) {
+ StringBuilder result = new StringBuilder();
+ for (Range range : ranges) {
+ appendRange(result, range.start, range.end);
+ }
+ return result.toString();
+ }
+
+ private static int parseCpu(String value) {
+ String normalized = value.trim();
+ if (!normalized.matches("[0-9]+")) {
+ throw new IllegalArgumentException(String.format("invalid CPU id[%s]", value));
+ }
+ BigInteger cpu = new BigInteger(normalized);
+ if (cpu.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
+ throw new IllegalArgumentException(String.format("CPU id[%s] is too large", value));
+ }
+ return cpu.intValue();
+ }
+
+ private static void appendRange(StringBuilder result, int start, int end) {
+ if (result.length() > 0) {
+ result.append(',');
+ }
+ result.append(start);
+ if (start != end) {
+ result.append('-').append(end);
+ }
+ }
+
+ private static class Range {
+ private final int start;
+ private int end;
+
+ private Range(int start, int end) {
+ this.start = start;
+ this.end = end;
+ }
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuTopology.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuTopology.java
new file mode 100644
index 00000000000..41dfbcbedf4
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerCpuTopology.java
@@ -0,0 +1,129 @@
+package org.zstack.header.physicalserver;
+
+import java.util.ArrayList;
+import java.math.BigInteger;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class PhysicalServerCpuTopology {
+ public static class CoreGroup {
+ private final String numaId;
+ private final SortedSet cpus;
+
+ CoreGroup(String numaId, Collection cpus) {
+ this.numaId = numaId;
+ this.cpus = new TreeSet<>(cpus);
+ }
+
+ public String getNumaId() {
+ return numaId;
+ }
+
+ public SortedSet getCpus() {
+ return new TreeSet<>(cpus);
+ }
+
+ int firstCpu() {
+ return cpus.first();
+ }
+ }
+
+ private final SortedSet onlineCpus;
+ private final List coreGroups;
+
+ private PhysicalServerCpuTopology(SortedSet onlineCpus, List coreGroups) {
+ this.onlineCpus = onlineCpus;
+ this.coreGroups = coreGroups;
+ }
+
+ public static PhysicalServerCpuTopology from(Map numaNodes) {
+ if (numaNodes == null || numaNodes.isEmpty()) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_UNAVAILABLE: NUMA topology is empty");
+ }
+
+ SortedSet online = new TreeSet<>();
+ List groups = new ArrayList<>();
+ Set grouped = new HashSet<>();
+ Map sortedNodes = new LinkedHashMap<>();
+ numaNodes.entrySet().stream()
+ .sorted(Map.Entry.comparingByKey())
+ .forEach(entry -> sortedNodes.put(entry.getKey(), entry.getValue()));
+
+ for (Map.Entry entry : sortedNodes.entrySet()) {
+ PhysicalServerNumaNode node = entry.getValue();
+ if (node.getOnlineCpus() == null || node.getCoreGroups() == null || node.getCoreGroups().isEmpty()) {
+ throw new IllegalArgumentException(
+ "CPU_TOPOLOGY_UNSUPPORTED: topology provider did not report online CPUs and core sibling groups");
+ }
+
+ Set nodeOnline = parseCpuIds(node.getOnlineCpus());
+ online.addAll(nodeOnline);
+ for (List rawGroup : node.getCoreGroups()) {
+ Set group = parseCpuIds(rawGroup);
+ if (group.isEmpty() || !nodeOnline.containsAll(group)) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_INVALID: core group is outside its NUMA online CPUs");
+ }
+ for (Integer cpu : group) {
+ if (!grouped.add(cpu)) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_TOPOLOGY_INVALID: CPU[%s] appears in multiple core groups", cpu));
+ }
+ }
+ groups.add(new CoreGroup(entry.getKey(), group));
+ }
+ }
+
+ if (!grouped.equals(online)) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_INVALID: core groups do not cover every online CPU exactly once");
+ }
+ groups.sort(Comparator.comparingInt(CoreGroup::firstCpu));
+ return new PhysicalServerCpuTopology(online, groups);
+ }
+
+ private static Set parseCpuIds(List values) {
+ Set result = new HashSet<>();
+ for (String value : values) {
+ if (value == null || !value.matches("[0-9]+")) {
+ throw new IllegalArgumentException(String.format("CPU_TOPOLOGY_INVALID: invalid CPU id[%s]", value));
+ }
+ BigInteger cpu = new BigInteger(value);
+ if (cpu.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) > 0) {
+ throw new IllegalArgumentException(String.format("CPU_TOPOLOGY_INVALID: CPU id[%s] is too large", value));
+ }
+ result.add(cpu.intValue());
+ }
+ return result;
+ }
+
+ public SortedSet getOnlineCpus() {
+ return new TreeSet<>(onlineCpus);
+ }
+
+ public List getCoreGroups() {
+ return new ArrayList<>(coreGroups);
+ }
+
+ public CoreGroup getCpuZeroGroup() {
+ for (CoreGroup group : coreGroups) {
+ if (group.cpus.contains(0)) {
+ return group;
+ }
+ }
+ throw new IllegalArgumentException("CPU_TOPOLOGY_INVALID: online topology does not contain CPU0");
+ }
+
+ public String fingerprint() {
+ List encoded = new ArrayList<>();
+ for (CoreGroup group : coreGroups) {
+ encoded.add(group.numaId + ":" + PhysicalServerCpuSet.format(group.cpus));
+ }
+ return String.join(";", encoded);
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerIdentitySpec.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerIdentitySpec.java
new file mode 100644
index 00000000000..0c9c6111ee2
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerIdentitySpec.java
@@ -0,0 +1,30 @@
+package org.zstack.header.physicalserver;
+
+public class PhysicalServerIdentitySpec {
+ private String serialNumber;
+ private String zoneUuid;
+
+ public PhysicalServerIdentitySpec() {
+ }
+
+ public PhysicalServerIdentitySpec(String serialNumber, String zoneUuid) {
+ this.serialNumber = serialNumber;
+ this.zoneUuid = zoneUuid;
+ }
+
+ public String getSerialNumber() {
+ return serialNumber;
+ }
+
+ public void setSerialNumber(String serialNumber) {
+ this.serialNumber = serialNumber;
+ }
+
+ public String getZoneUuid() {
+ return zoneUuid;
+ }
+
+ public void setZoneUuid(String zoneUuid) {
+ this.zoneUuid = zoneUuid;
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerManager.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerManager.java
new file mode 100644
index 00000000000..a27b2cab16f
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerManager.java
@@ -0,0 +1,27 @@
+package org.zstack.header.physicalserver;
+
+import org.zstack.header.core.Completion;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+
+public interface PhysicalServerManager {
+ Map resolveIdentities(Collection identities);
+
+ void ensureCpuAssignments(Collection serverUuids, String assignmentType);
+
+ default void ensureCpuAssignment(String serverUuid, String assignmentType) {
+ ensureCpuAssignments(Collections.singleton(serverUuid), assignmentType);
+ }
+
+ void reconcile(String serverUuid, boolean refreshFacts);
+
+ void reconcileAll();
+
+ void releaseCpuAssignment(
+ String serverUuid,
+ String assignmentType,
+ String consumerUuid,
+ Completion completion);
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerNumaNode.java b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerNumaNode.java
new file mode 100644
index 00000000000..01725e9b0e6
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/PhysicalServerNumaNode.java
@@ -0,0 +1,34 @@
+package org.zstack.header.physicalserver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PhysicalServerNumaNode {
+ private String nodeId;
+ private List onlineCpus = new ArrayList<>();
+ private List> coreGroups = new ArrayList<>();
+
+ public String getNodeId() {
+ return nodeId;
+ }
+
+ public void setNodeId(String nodeId) {
+ this.nodeId = nodeId;
+ }
+
+ public List getOnlineCpus() {
+ return onlineCpus;
+ }
+
+ public void setOnlineCpus(List onlineCpus) {
+ this.onlineCpus = onlineCpus;
+ }
+
+ public List> getCoreGroups() {
+ return coreGroups;
+ }
+
+ public void setCoreGroups(List> coreGroups) {
+ this.coreGroups = coreGroups;
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetCommand.java b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetCommand.java
new file mode 100644
index 00000000000..5941f2984ca
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetCommand.java
@@ -0,0 +1,52 @@
+package org.zstack.header.physicalserver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SharedCpuSetCommand {
+ private String roleKey;
+ private String cpuSet;
+ private boolean enabled;
+ private boolean includeAuxiliaryServices;
+ private List handles = new ArrayList<>();
+
+ public String getRoleKey() {
+ return roleKey;
+ }
+
+ public void setRoleKey(String roleKey) {
+ this.roleKey = roleKey;
+ }
+
+ public String getCpuSet() {
+ return cpuSet;
+ }
+
+ public void setCpuSet(String cpuSet) {
+ this.cpuSet = cpuSet;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public boolean isIncludeAuxiliaryServices() {
+ return includeAuxiliaryServices;
+ }
+
+ public void setIncludeAuxiliaryServices(boolean includeAuxiliaryServices) {
+ this.includeAuxiliaryServices = includeAuxiliaryServices;
+ }
+
+ public List getHandles() {
+ return handles;
+ }
+
+ public void setHandles(List handles) {
+ this.handles = handles;
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetResponse.java b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetResponse.java
new file mode 100644
index 00000000000..b66b742817a
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetResponse.java
@@ -0,0 +1,70 @@
+package org.zstack.header.physicalserver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class SharedCpuSetResponse {
+ private String backend;
+ private String actualCpuSet;
+ private Integer coveredServiceCount;
+ private Integer expectedServiceCount;
+ private List consumerKeys = new ArrayList<>();
+ private List unresolvedHandles = new ArrayList<>();
+ private String reasonCode;
+
+ public String getBackend() {
+ return backend;
+ }
+
+ public void setBackend(String backend) {
+ this.backend = backend;
+ }
+
+ public String getActualCpuSet() {
+ return actualCpuSet;
+ }
+
+ public void setActualCpuSet(String actualCpuSet) {
+ this.actualCpuSet = actualCpuSet;
+ }
+
+ public Integer getCoveredServiceCount() {
+ return coveredServiceCount;
+ }
+
+ public void setCoveredServiceCount(Integer coveredServiceCount) {
+ this.coveredServiceCount = coveredServiceCount;
+ }
+
+ public Integer getExpectedServiceCount() {
+ return expectedServiceCount;
+ }
+
+ public void setExpectedServiceCount(Integer expectedServiceCount) {
+ this.expectedServiceCount = expectedServiceCount;
+ }
+
+ public List getConsumerKeys() {
+ return consumerKeys;
+ }
+
+ public void setConsumerKeys(List consumerKeys) {
+ this.consumerKeys = consumerKeys;
+ }
+
+ public List getUnresolvedHandles() {
+ return unresolvedHandles;
+ }
+
+ public void setUnresolvedHandles(List unresolvedHandles) {
+ this.unresolvedHandles = unresolvedHandles;
+ }
+
+ public String getReasonCode() {
+ return reasonCode;
+ }
+
+ public void setReasonCode(String reasonCode) {
+ this.reasonCode = reasonCode;
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetServiceHandle.java b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetServiceHandle.java
new file mode 100644
index 00000000000..451d5868c9b
--- /dev/null
+++ b/header/src/main/java/org/zstack/header/physicalserver/SharedCpuSetServiceHandle.java
@@ -0,0 +1,68 @@
+package org.zstack.header.physicalserver;
+
+public class SharedCpuSetServiceHandle {
+ public static final String SYSTEMD_UNIT = "SYSTEMD_UNIT";
+ public static final String OWNER_PID_FILE = "OWNER_PID_FILE";
+
+ private String handleType;
+ private String value;
+ private String consumerKey;
+ private boolean optional;
+ private String expectedCommandToken;
+
+ public SharedCpuSetServiceHandle() {
+ }
+
+ public SharedCpuSetServiceHandle(
+ String handleType,
+ String value,
+ String consumerKey,
+ boolean optional,
+ String expectedCommandToken) {
+ this.handleType = handleType;
+ this.value = value;
+ this.consumerKey = consumerKey;
+ this.optional = optional;
+ this.expectedCommandToken = expectedCommandToken;
+ }
+
+ public String getHandleType() {
+ return handleType;
+ }
+
+ public void setHandleType(String handleType) {
+ this.handleType = handleType;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+
+ public String getConsumerKey() {
+ return consumerKey;
+ }
+
+ public void setConsumerKey(String consumerKey) {
+ this.consumerKey = consumerKey;
+ }
+
+ public boolean isOptional() {
+ return optional;
+ }
+
+ public void setOptional(boolean optional) {
+ this.optional = optional;
+ }
+
+ public String getExpectedCommandToken() {
+ return expectedCommandToken;
+ }
+
+ public void setExpectedCommandToken(String expectedCommandToken) {
+ this.expectedCommandToken = expectedCommandToken;
+ }
+}
diff --git a/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java b/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java
index feb4c5c11c7..daedf838732 100644
--- a/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java
+++ b/header/src/main/java/org/zstack/header/storage/addon/primary/PrimaryStorageControllerSvc.java
@@ -43,6 +43,10 @@ public interface PrimaryStorageControllerSvc {
*/
void syncConfig(String config);
+ default void beforePersistAddonInfo(AddonInfo addonInfo, Completion completion) {
+ completion.success();
+ }
+
void getCapacity(List requiredUrls, ReturnValueCompletion comp);
void reportCapacity(ReturnValueCompletion comp);
void reportHealthy(ReturnValueCompletion comp);
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostCapacityExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostCapacityExtension.java
index 16ff667f21d..de9e012e022 100755
--- a/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostCapacityExtension.java
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KVMHostCapacityExtension.java
@@ -30,10 +30,14 @@ public class KVMHostCapacityExtension implements KVMHostConnectExtensionPoint, H
private ResourceConfigFacade rcf;
public void reportCapacity(HostInventory host, Completion completion) {
+ reportCapacity(host.getUuid(), completion);
+ }
+
+ public void reportCapacity(String hostUuid, Completion completion) {
CheckHostCapacityMsg msg = new CheckHostCapacityMsg();
- msg.setHostUuid(host.getUuid());
+ msg.setHostUuid(hostUuid);
msg.setTimeout(TimeUnit.MINUTES.toMillis(30));
- bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, host.getUuid());
+ bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, hostUuid);
bus.send(msg, new CloudBusCallBack(completion) {
@Override
public void run(MessageReply rly) {
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerAdapter.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerAdapter.java
new file mode 100644
index 00000000000..6d75758c855
--- /dev/null
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerAdapter.java
@@ -0,0 +1,718 @@
+package org.zstack.kvm;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.zstack.compute.host.HostSystemTags;
+import org.zstack.compute.host.PostHostConnectExtensionPoint;
+import org.zstack.core.Platform;
+import org.zstack.core.cloudbus.CloudBus;
+import org.zstack.core.cloudbus.CloudBusCallBack;
+import org.zstack.core.db.DatabaseFacade;
+import org.zstack.core.db.Q;
+import org.zstack.header.Component;
+import org.zstack.header.core.Completion;
+import org.zstack.header.core.ReturnValueCompletion;
+import org.zstack.header.core.workflow.Flow;
+import org.zstack.header.core.workflow.FlowTrigger;
+import org.zstack.header.core.workflow.NoRollbackFlow;
+import org.zstack.header.errorcode.ErrorCode;
+import org.zstack.header.errorcode.ErrorableValue;
+import org.zstack.header.host.GetHostNumaTopologyMsg;
+import org.zstack.header.host.GetHostNumaTopologyReply;
+import org.zstack.header.host.HostAO_;
+import org.zstack.header.host.HostAfterConnectedExtensionPoint;
+import org.zstack.header.host.HostConstant;
+import org.zstack.header.host.HostDeleteExtensionPoint;
+import org.zstack.header.host.HostEO;
+import org.zstack.header.host.HostException;
+import org.zstack.header.host.HostInventory;
+import org.zstack.header.host.HostNUMANode;
+import org.zstack.header.host.HostStatus;
+import org.zstack.header.host.HostVO;
+import org.zstack.header.host.HostVO_;
+import org.zstack.header.managementnode.ManagementNodeReadyExtensionPoint;
+import org.zstack.header.message.MessageReply;
+import org.zstack.header.physicalserver.PhysicalServerCpuConsumerAdapter;
+import org.zstack.header.physicalserver.PhysicalServerCpuConsumerState;
+import org.zstack.header.physicalserver.PhysicalServerCpuTopology;
+import org.zstack.header.physicalserver.PhysicalServerIdentitySpec;
+import org.zstack.header.physicalserver.PhysicalServerManager;
+import org.zstack.header.physicalserver.PhysicalServerNumaNode;
+import org.zstack.header.physicalserver.SharedCpuSetCommand;
+import org.zstack.header.physicalserver.SharedCpuSetResponse;
+import org.zstack.header.physicalserver.SharedCpuSetServiceHandle;
+import org.zstack.header.tag.AbstractSystemTagLifeCycleListener;
+import org.zstack.header.tag.SystemTagInventory;
+import org.zstack.header.tag.SystemTagVO;
+import org.zstack.header.tag.SystemTagVO_;
+import org.zstack.utils.TagUtils;
+import org.zstack.utils.Utils;
+import org.zstack.utils.logging.CLogger;
+
+import javax.persistence.Query;
+import javax.persistence.Tuple;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.zstack.core.Platform.operr;
+
+public class KvmPhysicalServerAdapter implements
+ PhysicalServerCpuConsumerAdapter,
+ PostHostConnectExtensionPoint,
+ HostAfterConnectedExtensionPoint,
+ HostDeleteExtensionPoint,
+ ManagementNodeReadyExtensionPoint,
+ Component {
+ public static final String ASSIGNMENT_TYPE = "COMPUTE_SHARED";
+ public static final String APPLY_SHARED_CPUSET_PATH = "/host/sharedcpuset/apply";
+ private static final String ERROR_CODE = "ORG_ZSTACK_KVM_10000";
+ private static final CLogger logger = Utils.getLogger(KvmPhysicalServerAdapter.class);
+ private static final List COMPUTE_UNITS = Arrays.asList(
+ "virtlogd.service",
+ "zstack-network-agent.service",
+ "zstack-sharedblock-agent.service");
+ private static final List NODE_AUX_UNITS = Arrays.asList(
+ "node_exporter.service",
+ "collectd.service",
+ "collectd_exporter.service",
+ "process_exporter.service",
+ "zstack_service_exporter.service",
+ "pushgateway.service");
+
+ @Autowired(required = false)
+ private PhysicalServerManager physicalServerManager;
+ @Autowired
+ private DatabaseFacade dbf;
+ @Autowired
+ private CloudBus bus;
+ @Autowired
+ private KVMHostCapacityExtension capacityExtension;
+
+ @Override
+ public String getAssignmentType() {
+ return ASSIGNMENT_TYPE;
+ }
+
+ @Override
+ public Set getAssociatedServerUuids() {
+ return new LinkedHashSet<>(Q.New(HostVO.class)
+ .select(HostVO_.serverUuid)
+ .eq(HostVO_.hypervisorType, KVMConstant.KVM_HYPERVISOR_TYPE)
+ .notNull(HostVO_.serverUuid)
+ .listValues());
+ }
+
+ @Override
+ public void refreshAssociations() {
+ backfill(false);
+ }
+
+ @Override
+ public PhysicalServerCpuConsumerState getState(String serverUuid) {
+ Tuple host = Q.New(HostVO.class)
+ .select(HostVO_.uuid, HostVO_.status)
+ .eq(HostVO_.serverUuid, serverUuid)
+ .eq(HostVO_.hypervisorType, KVMConstant.KVM_HYPERVISOR_TYPE)
+ .findTuple();
+ if (host == null) {
+ return PhysicalServerCpuConsumerState.MISSING;
+ }
+ return host.get(1, HostStatus.class) == HostStatus.Connected
+ ? PhysicalServerCpuConsumerState.AVAILABLE
+ : PhysicalServerCpuConsumerState.UNAVAILABLE;
+ }
+
+ @Override
+ public void refreshCapacity(String serverUuid) {
+ String hostUuid = hostUuid(serverUuid, null);
+ if (hostUuid == null) {
+ return;
+ }
+ capacityExtension.reportCapacity(hostUuid, new Completion(null) {
+ @Override
+ public void success() {
+ }
+
+ @Override
+ public void fail(ErrorCode errorCode) {
+ logger.warn(String.format(
+ "failed to refresh capacity for host[uuid:%s] after " +
+ "physical server CPU assignment changed: %s",
+ hostUuid, errorCode));
+ }
+ });
+ }
+
+ @Override
+ public void collectTopology(
+ String serverUuid,
+ ReturnValueCompletion completion) {
+ String hostUuid = hostUuid(serverUuid, null);
+ if (hostUuid == null) {
+ completion.fail(operr(ERROR_CODE,
+ "HOST_RELATION_MISSING: physical server[uuid:%s] has no KVM host",
+ serverUuid));
+ return;
+ }
+
+ GetHostNumaTopologyMsg msg = new GetHostNumaTopologyMsg();
+ msg.setHostUuid(hostUuid);
+ msg.setTimeout(TimeUnit.MINUTES.toMillis(5));
+ bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, hostUuid);
+ bus.send(msg, new CloudBusCallBack(completion) {
+ @Override
+ public void run(MessageReply reply) {
+ if (!reply.isSuccess()) {
+ completion.fail(reply.getError());
+ return;
+ }
+ completeTopology(reply.castReply(), completion);
+ }
+ });
+ }
+
+ private void completeTopology(
+ GetHostNumaTopologyReply reply,
+ ReturnValueCompletion completion) {
+ completion.success(PhysicalServerCpuTopology.from(
+ neutralTopology(reply.getNuma())));
+ }
+
+ @Override
+ public void apply(
+ String serverUuid,
+ String consumerUuid,
+ SharedCpuSetCommand command,
+ ReturnValueCompletion completion) {
+ String hostUuid = hostUuid(serverUuid, consumerUuid);
+ if (hostUuid == null) {
+ completion.fail(operr(ERROR_CODE,
+ "HOST_RELATION_MISSING: physical server[uuid:%s] has no matching KVM host",
+ serverUuid));
+ return;
+ }
+
+ SharedCpuSetAgentCommand agentCommand = new SharedCpuSetAgentCommand();
+ agentCommand.setRoleKey(command.getRoleKey());
+ agentCommand.setCpuSet(command.getCpuSet());
+ agentCommand.setEnabled(command.isEnabled());
+ agentCommand.setHandles(handles(hostUuid, command.isIncludeAuxiliaryServices()));
+
+ KVMHostAsyncHttpCallMsg msg = new KVMHostAsyncHttpCallMsg();
+ msg.setPath(APPLY_SHARED_CPUSET_PATH);
+ msg.setHostUuid(hostUuid);
+ msg.setCommand(agentCommand);
+ msg.setTimeout(TimeUnit.MINUTES.toMillis(5));
+ bus.makeTargetServiceIdByResourceUuid(msg, HostConstant.SERVICE_ID, hostUuid);
+ bus.send(msg, new CloudBusCallBack(completion) {
+ @Override
+ public void run(MessageReply reply) {
+ ErrorableValue result =
+ KVMHostAsyncHttpCallReply.unwrap(
+ reply, SharedCpuSetAgentResponse.class);
+ if (!result.isSuccess()) {
+ completion.fail(result.error);
+ return;
+ }
+ completion.success(result.result.toInventory());
+ }
+ });
+ }
+
+ @Override
+ public Flow createPostHostConnectFlow(HostInventory host) {
+ return new NoRollbackFlow() {
+ String __name__ = "associate-kvm-host-with-physical-server";
+
+ @Override
+ public void run(FlowTrigger trigger, Map data) {
+ associate(host);
+ trigger.next();
+ }
+ };
+ }
+
+ @Override
+ public void afterHostConnected(HostInventory host) {
+ if (physicalServerManager == null) {
+ return;
+ }
+ String serverUuid = Q.New(HostVO.class)
+ .select(HostVO_.serverUuid)
+ .eq(HostVO_.uuid, host.getUuid())
+ .findValue();
+ if (serverUuid != null) {
+ physicalServerManager.reconcile(serverUuid, true);
+ }
+ }
+
+ @Override
+ public void preDeleteHost(HostInventory inventory) throws HostException {
+ }
+
+ @Override
+ public void beforeDeleteHost(HostInventory inventory) {
+ if (inventory.getServerUuid() == null) {
+ return;
+ }
+ String serverUuid = inventory.getServerUuid();
+ org.zstack.core.db.SQL.New(HostEO.class)
+ .eq(HostAO_.uuid, inventory.getUuid())
+ .eq(HostAO_.serverUuid, serverUuid)
+ .set(HostAO_.serverUuid, null)
+ .update();
+ if (physicalServerManager != null) {
+ physicalServerManager.reconcile(serverUuid, false);
+ }
+ }
+
+ @Override
+ public void afterDeleteHost(HostInventory inventory) {
+ }
+
+ @Override
+ public void managementNodeReady() {
+ backfill(true);
+ }
+
+ @Override
+ public boolean start() {
+ HostSystemTags.SYSTEM_SERIAL_NUMBER.installLifeCycleListener(
+ new AbstractSystemTagLifeCycleListener() {
+ @Override
+ public void tagCreated(SystemTagInventory tag) {
+ backfill(true);
+ }
+
+ @Override
+ public void tagUpdated(
+ SystemTagInventory old, SystemTagInventory newTag) {
+ backfill(true);
+ }
+ });
+ return true;
+ }
+
+ @Override
+ public boolean stop() {
+ return true;
+ }
+
+ @Transactional
+ public void associate(HostInventory host) {
+ if (physicalServerManager == null
+ || !KVMConstant.KVM_HYPERVISOR_TYPE.equals(host.getHypervisorType())) {
+ return;
+ }
+ String current = Q.New(HostVO.class)
+ .select(HostVO_.serverUuid)
+ .eq(HostVO_.uuid, host.getUuid())
+ .findValue();
+ if (current != null) {
+ physicalServerManager.ensureCpuAssignment(current, ASSIGNMENT_TYPE);
+ return;
+ }
+
+ String serialNumber = Platform.normalizeMachineSerialNumber(
+ HostSystemTags.SYSTEM_SERIAL_NUMBER.getTokenByResourceUuid(
+ host.getUuid(), HostSystemTags.SYSTEM_SERIAL_NUMBER_TOKEN));
+ if (serialNumber == null) {
+ logger.warn(String.format(
+ "cannot associate host[uuid:%s] with a physical server, " +
+ "reasonCode[IDENTITY_UNAVAILABLE]", host.getUuid()));
+ return;
+ }
+ String zoneUuid = host.getZoneUuid() == null
+ ? Q.New(HostVO.class).select(HostVO_.zoneUuid)
+ .eq(HostVO_.uuid, host.getUuid()).findValue()
+ : host.getZoneUuid();
+ String serverUuid = physicalServerManager.resolveIdentities(
+ Collections.singletonList(
+ new PhysicalServerIdentitySpec(serialNumber, zoneUuid)))
+ .get(serialNumber);
+ if (serverUuid == null) {
+ return;
+ }
+ clearDeletedHostLink(serverUuid);
+ if (linkedHost(serverUuid, host.getUuid()) != null) {
+ return;
+ }
+
+ Query update = dbf.getEntityManager().createNativeQuery(
+ "UPDATE IGNORE HostEO SET serverUuid = :serverUuid " +
+ "WHERE uuid = :hostUuid AND serverUuid IS NULL");
+ update.setParameter("serverUuid", serverUuid);
+ update.setParameter("hostUuid", host.getUuid());
+ update.executeUpdate();
+ current = Q.New(HostVO.class)
+ .select(HostVO_.serverUuid)
+ .eq(HostVO_.uuid, host.getUuid())
+ .findValue();
+ if (!serverUuid.equals(current)) {
+ logger.warn(String.format(
+ "cannot associate host[uuid:%s] with physical server[uuid:%s], " +
+ "reasonCode[IDENTITY_ASSOCIATION_CONFLICT]",
+ host.getUuid(), serverUuid));
+ return;
+ }
+ physicalServerManager.ensureCpuAssignment(serverUuid, ASSIGNMENT_TYPE);
+ }
+
+ @Transactional
+ private void backfill(boolean requestReconcile) {
+ if (physicalServerManager == null) {
+ return;
+ }
+ List hostTuples = Q.New(HostVO.class)
+ .select(HostVO_.uuid, HostVO_.zoneUuid)
+ .eq(HostVO_.hypervisorType, KVMConstant.KVM_HYPERVISOR_TYPE)
+ .isNull(HostVO_.serverUuid)
+ .listTuple();
+ if (hostTuples.isEmpty()) {
+ return;
+ }
+ Map zonesByHost = new LinkedHashMap<>();
+ for (Tuple tuple : hostTuples) {
+ zonesByHost.put(tuple.get(0, String.class), tuple.get(1, String.class));
+ }
+ Map> serialsByHost = serialsByHost(zonesByHost.keySet());
+ Map> candidatesBySerial = new LinkedHashMap<>();
+ for (Map.Entry host : zonesByHost.entrySet()) {
+ Set serials = serialsByHost.get(host.getKey());
+ if (serials == null || serials.size() != 1) {
+ continue;
+ }
+ candidatesBySerial
+ .computeIfAbsent(serials.iterator().next(), ignored -> new ArrayList<>())
+ .add(new HostCandidate(host.getKey(), host.getValue()));
+ }
+
+ Map candidates = new LinkedHashMap<>();
+ List identities = new ArrayList<>();
+ for (Map.Entry> candidate : candidatesBySerial.entrySet()) {
+ if (candidate.getValue().size() != 1) {
+ logger.warn(String.format(
+ "cannot backfill host physical server association for serialNumber[%s], " +
+ "reasonCode[IDENTITY_ALREADY_ASSOCIATED]",
+ candidate.getKey()));
+ continue;
+ }
+ HostCandidate host = candidate.getValue().get(0);
+ candidates.put(candidate.getKey(), host);
+ identities.add(new PhysicalServerIdentitySpec(candidate.getKey(), host.zoneUuid));
+ }
+ Map resolved = physicalServerManager.resolveIdentities(identities);
+ clearDeletedHostLinks(resolved.values());
+ Set used = new HashSet<>(Q.New(HostVO.class)
+ .select(HostVO_.serverUuid)
+ .notNull(HostVO_.serverUuid)
+ .listValues());
+ Map links = new LinkedHashMap<>();
+ for (Map.Entry candidate : candidates.entrySet()) {
+ String serverUuid = resolved.get(candidate.getKey());
+ if (serverUuid != null && used.add(serverUuid)) {
+ links.put(candidate.getValue().hostUuid, serverUuid);
+ }
+ }
+ updateHostLinks(links);
+
+ List linked = links.isEmpty() ? Collections.emptyList() : Q.New(HostVO.class)
+ .select(HostVO_.uuid, HostVO_.serverUuid)
+ .in(HostVO_.uuid, links.keySet())
+ .notNull(HostVO_.serverUuid)
+ .listTuple();
+ Set linkedServers = new LinkedHashSet<>();
+ for (Tuple tuple : linked) {
+ String serverUuid = tuple.get(1, String.class);
+ linkedServers.add(serverUuid);
+ }
+ if (requestReconcile && !linkedServers.isEmpty()) {
+ physicalServerManager.reconcileAll();
+ }
+ }
+
+ private Map> serialsByHost(Collection hostUuids) {
+ List tagTuples = Q.New(SystemTagVO.class)
+ .select(SystemTagVO_.resourceUuid, SystemTagVO_.tag)
+ .in(SystemTagVO_.resourceUuid, hostUuids)
+ .eq(SystemTagVO_.resourceType, HostVO.class.getSimpleName())
+ .like(SystemTagVO_.tag, TagUtils.tagPatternToSqlPattern(
+ HostSystemTags.SYSTEM_SERIAL_NUMBER.getTagFormat()))
+ .listTuple();
+ Map> result = new HashMap<>();
+ for (Tuple tuple : tagTuples) {
+ String hostUuid = tuple.get(0, String.class);
+ String serialNumber = Platform.normalizeMachineSerialNumber(
+ HostSystemTags.SYSTEM_SERIAL_NUMBER.getTokenByTag(
+ tuple.get(1, String.class),
+ HostSystemTags.SYSTEM_SERIAL_NUMBER_TOKEN));
+ if (serialNumber != null) {
+ result.computeIfAbsent(hostUuid, ignored -> new LinkedHashSet<>())
+ .add(serialNumber);
+ }
+ }
+ return result;
+ }
+
+ private void updateHostLinks(Map links) {
+ if (links.isEmpty()) {
+ return;
+ }
+ StringBuilder sql = new StringBuilder(
+ "UPDATE IGNORE HostEO SET serverUuid = CASE uuid");
+ int index = 0;
+ for (String ignored : links.keySet()) {
+ sql.append(" WHEN :host").append(index)
+ .append(" THEN :server").append(index);
+ index++;
+ }
+ sql.append(" ELSE serverUuid END WHERE serverUuid IS NULL AND uuid IN (");
+ for (int i = 0; i < links.size(); i++) {
+ if (i > 0) {
+ sql.append(',');
+ }
+ sql.append(":host").append(i);
+ }
+ sql.append(')');
+ Query query = dbf.getEntityManager().createNativeQuery(sql.toString());
+ index = 0;
+ for (Map.Entry link : links.entrySet()) {
+ query.setParameter("host" + index, link.getKey());
+ query.setParameter("server" + index, link.getValue());
+ index++;
+ }
+ query.executeUpdate();
+ }
+
+ private void clearDeletedHostLinks(Collection serverUuids) {
+ Set targets = new LinkedHashSet<>(serverUuids);
+ targets.remove(null);
+ if (targets.isEmpty()) {
+ return;
+ }
+ StringBuilder sql = new StringBuilder(
+ "UPDATE HostEO SET serverUuid = NULL " +
+ "WHERE deleted IS NOT NULL AND serverUuid IN (");
+ for (int i = 0; i < targets.size(); i++) {
+ if (i > 0) {
+ sql.append(',');
+ }
+ sql.append(":server").append(i);
+ }
+ sql.append(')');
+ Query query = dbf.getEntityManager().createNativeQuery(sql.toString());
+ int index = 0;
+ for (String serverUuid : targets) {
+ query.setParameter("server" + index, serverUuid);
+ index++;
+ }
+ query.executeUpdate();
+ }
+
+ private void clearDeletedHostLink(String serverUuid) {
+ Query query = dbf.getEntityManager().createNativeQuery(
+ "UPDATE HostEO SET serverUuid = NULL " +
+ "WHERE deleted IS NOT NULL AND serverUuid = :serverUuid");
+ query.setParameter("serverUuid", serverUuid);
+ query.executeUpdate();
+ }
+
+ private String linkedHost(String serverUuid, String excludedHostUuid) {
+ return Q.New(HostVO.class)
+ .select(HostVO_.uuid)
+ .eq(HostVO_.serverUuid, serverUuid)
+ .notEq(HostVO_.uuid, excludedHostUuid)
+ .findValue();
+ }
+
+ private String hostUuid(String serverUuid, String consumerUuid) {
+ if (consumerUuid != null) {
+ return Q.New(HostVO.class)
+ .select(HostVO_.uuid)
+ .eq(HostVO_.uuid, consumerUuid)
+ .eq(HostVO_.serverUuid, serverUuid)
+ .eq(HostVO_.hypervisorType, KVMConstant.KVM_HYPERVISOR_TYPE)
+ .findValue();
+ }
+ return Q.New(HostVO.class)
+ .select(HostVO_.uuid)
+ .eq(HostVO_.serverUuid, serverUuid)
+ .eq(HostVO_.hypervisorType, KVMConstant.KVM_HYPERVISOR_TYPE)
+ .findValue();
+ }
+
+ private Map neutralTopology(
+ Map topology) {
+ Map result = new LinkedHashMap<>();
+ if (topology == null) {
+ return result;
+ }
+ for (Map.Entry entry : topology.entrySet()) {
+ PhysicalServerNumaNode node = new PhysicalServerNumaNode();
+ node.setNodeId(entry.getKey());
+ node.setOnlineCpus(entry.getValue().getOnlineCpus());
+ node.setCoreGroups(entry.getValue().getCoreGroups());
+ result.put(entry.getKey(), node);
+ }
+ return result;
+ }
+
+ private List handles(
+ String hostUuid, boolean includeAuxiliaryServices) {
+ String consumerKey = String.format("host-agent:%s", hostUuid);
+ List result = new ArrayList<>();
+ result.add(new SharedCpuSetServiceHandle(
+ SharedCpuSetServiceHandle.OWNER_PID_FILE,
+ "/var/run/zstack/kvmagent.pid", consumerKey, false, "kvmagent"));
+ for (String unit : COMPUTE_UNITS) {
+ result.add(new SharedCpuSetServiceHandle(
+ SharedCpuSetServiceHandle.SYSTEMD_UNIT,
+ unit, consumerKey, true, null));
+ }
+ if (includeAuxiliaryServices) {
+ for (String unit : NODE_AUX_UNITS) {
+ result.add(new SharedCpuSetServiceHandle(
+ SharedCpuSetServiceHandle.SYSTEMD_UNIT,
+ unit, consumerKey, true, null));
+ }
+ }
+ return result;
+ }
+
+ public static class SharedCpuSetAgentCommand extends KVMAgentCommands.AgentCommand {
+ private String roleKey;
+ private String cpuSet;
+ private boolean enabled;
+ private List handles = new ArrayList<>();
+
+ public String getRoleKey() {
+ return roleKey;
+ }
+
+ public void setRoleKey(String roleKey) {
+ this.roleKey = roleKey;
+ }
+
+ public String getCpuSet() {
+ return cpuSet;
+ }
+
+ public void setCpuSet(String cpuSet) {
+ this.cpuSet = cpuSet;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public List getHandles() {
+ return handles;
+ }
+
+ public void setHandles(List handles) {
+ this.handles = handles;
+ }
+ }
+
+ public static class SharedCpuSetAgentResponse extends KVMAgentCommands.AgentResponse {
+ private String backend;
+ private String actualCpuSet;
+ private Integer coveredServiceCount;
+ private Integer expectedServiceCount;
+ private List consumerKeys = new ArrayList<>();
+ private List unresolvedHandles = new ArrayList<>();
+ private String reasonCode;
+
+ public String getBackend() {
+ return backend;
+ }
+
+ public void setBackend(String backend) {
+ this.backend = backend;
+ }
+
+ public String getActualCpuSet() {
+ return actualCpuSet;
+ }
+
+ public void setActualCpuSet(String actualCpuSet) {
+ this.actualCpuSet = actualCpuSet;
+ }
+
+ public Integer getCoveredServiceCount() {
+ return coveredServiceCount;
+ }
+
+ public void setCoveredServiceCount(Integer coveredServiceCount) {
+ this.coveredServiceCount = coveredServiceCount;
+ }
+
+ public Integer getExpectedServiceCount() {
+ return expectedServiceCount;
+ }
+
+ public void setExpectedServiceCount(Integer expectedServiceCount) {
+ this.expectedServiceCount = expectedServiceCount;
+ }
+
+ public List getConsumerKeys() {
+ return consumerKeys;
+ }
+
+ public void setConsumerKeys(List consumerKeys) {
+ this.consumerKeys = consumerKeys;
+ }
+
+ public List getUnresolvedHandles() {
+ return unresolvedHandles;
+ }
+
+ public void setUnresolvedHandles(List unresolvedHandles) {
+ this.unresolvedHandles = unresolvedHandles;
+ }
+
+ public String getReasonCode() {
+ return reasonCode;
+ }
+
+ public void setReasonCode(String reasonCode) {
+ this.reasonCode = reasonCode;
+ }
+
+ private SharedCpuSetResponse toInventory() {
+ SharedCpuSetResponse response = new SharedCpuSetResponse();
+ response.setBackend(backend);
+ response.setActualCpuSet(actualCpuSet);
+ response.setCoveredServiceCount(coveredServiceCount);
+ response.setExpectedServiceCount(expectedServiceCount);
+ response.setConsumerKeys(consumerKeys);
+ response.setUnresolvedHandles(unresolvedHandles);
+ response.setReasonCode(reasonCode);
+ return response;
+ }
+ }
+
+ private static class HostCandidate {
+ private final String hostUuid;
+ private final String zoneUuid;
+
+ private HostCandidate(String hostUuid, String zoneUuid) {
+ this.hostUuid = hostUuid;
+ this.zoneUuid = zoneUuid;
+ }
+ }
+}
diff --git a/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerCpuCascadeExtension.java b/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerCpuCascadeExtension.java
new file mode 100644
index 00000000000..7585e5d6c83
--- /dev/null
+++ b/plugin/kvm/src/main/java/org/zstack/kvm/KvmPhysicalServerCpuCascadeExtension.java
@@ -0,0 +1,112 @@
+package org.zstack.kvm;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.zstack.core.asyncbatch.While;
+import org.zstack.core.cascade.AbstractAsyncCascadeExtension;
+import org.zstack.core.cascade.CascadeAction;
+import org.zstack.core.cascade.CascadeConstant;
+import org.zstack.header.core.Completion;
+import org.zstack.header.core.WhileDoneCompletion;
+import org.zstack.header.errorcode.ErrorCode;
+import org.zstack.header.errorcode.ErrorCodeList;
+import org.zstack.header.host.HostInventory;
+import org.zstack.header.host.HostVO;
+import org.zstack.header.physicalserver.PhysicalServerManager;
+import org.zstack.utils.Utils;
+import org.zstack.utils.logging.CLogger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class KvmPhysicalServerCpuCascadeExtension extends AbstractAsyncCascadeExtension {
+ private static final CLogger logger = Utils.getLogger(
+ KvmPhysicalServerCpuCascadeExtension.class);
+ private static final String NAME = "PhysicalServerComputeSharedCpuAssignment";
+
+ @Autowired(required = false)
+ private PhysicalServerManager physicalServerManager;
+
+ @Override
+ public void asyncCascade(CascadeAction action, Completion completion) {
+ if (physicalServerManager == null
+ || !HostVO.class.getSimpleName().equals(action.getParentIssuer())
+ || !action.isActionCode(
+ CascadeConstant.DELETION_DELETE_CODE,
+ CascadeConstant.DELETION_FORCE_DELETE_CODE)) {
+ completion.success();
+ return;
+ }
+
+ List hosts = action.getParentIssuerContext();
+ if (hosts == null || hosts.isEmpty()) {
+ completion.success();
+ return;
+ }
+ boolean forceDelete = action.isActionCode(CascadeConstant.DELETION_FORCE_DELETE_CODE);
+ List forceDeleteFailures = Collections.synchronizedList(new ArrayList<>());
+
+ new While<>(hosts).each((host, each) -> {
+ if (host.getServerUuid() == null) {
+ each.done();
+ return;
+ }
+ physicalServerManager.releaseCpuAssignment(
+ host.getServerUuid(),
+ KvmPhysicalServerAdapter.ASSIGNMENT_TYPE,
+ host.getUuid(),
+ new Completion(each) {
+ @Override
+ public void success() {
+ each.done();
+ }
+
+ @Override
+ public void fail(ErrorCode errorCode) {
+ if (!forceDelete) {
+ each.addError(errorCode);
+ each.done();
+ return;
+ }
+ forceDeleteFailures.add(String.format(
+ "host[uuid:%s]: %s", host.getUuid(), errorCode));
+ logger.error(String.format(
+ "failed to release compute shared CPUSet before force deleting " +
+ "host[uuid:%s], keeping the assignment for recovery: %s",
+ host.getUuid(), errorCode));
+ each.done();
+ }
+ });
+ }).run(new WhileDoneCompletion(completion) {
+ @Override
+ public void done(ErrorCodeList errorCodeList) {
+ if (!forceDelete && !errorCodeList.getCauses().isEmpty()) {
+ completion.fail(errorCodeList.getCauses().get(0));
+ return;
+ }
+ if (!forceDeleteFailures.isEmpty()) {
+ logger.error(String.format(
+ "failed to release compute shared CPUSet for %s host(s) during " +
+ "force deletion; assignments are kept for recovery: %s",
+ forceDeleteFailures.size(), String.join("; ", forceDeleteFailures)));
+ }
+ completion.success();
+ }
+ });
+ }
+
+ @Override
+ public List getEdgeNames() {
+ return Collections.singletonList(HostVO.class.getSimpleName());
+ }
+
+ @Override
+ public String getCascadeResourceName() {
+ return NAME;
+ }
+
+ @Override
+ public CascadeAction createActionForChildResource(CascadeAction action) {
+ return null;
+ }
+}
diff --git a/plugin/physicalServer/pom.xml b/plugin/physicalServer/pom.xml
new file mode 100644
index 00000000000..3f7f96e718e
--- /dev/null
+++ b/plugin/physicalServer/pom.xml
@@ -0,0 +1,75 @@
+
+
+ 4.0.0
+
+ plugin
+ org.zstack
+ 5.5.0
+ ..
+
+ physicalServer
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ ${project.compiler.version}
+
+ groovy-eclipse-compiler
+ ${project.java.version}
+ ${project.java.version}
+ lines,vars,source
+ true
+
+
+
+ org.codehaus.groovy
+ groovy-eclipse-compiler
+ ${groovy.eclipse.compiler}
+
+
+ org.codehaus.groovy
+ groovy-eclipse-batch
+ ${groovy.eclipse.batch}
+
+
+
+
+ org.codehaus.mojo
+ aspectj-maven-plugin
+ ${aspectj.plugin.version}
+
+
+
+ compile
+ test-compile
+
+
+
+
+ ${project.java.version}
+ ${project.java.version}
+ ${project.java.version}
+ true
+
+
+ org.springframework
+ spring-aspects
+
+
+ org.zstack
+ core
+
+
+ org.zstack
+ header
+
+
+
+
+
+
+
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsg.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsg.java
new file mode 100644
index 00000000000..a48d70aa082
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsg.java
@@ -0,0 +1,25 @@
+package org.zstack.physicalserver;
+
+import org.springframework.http.HttpMethod;
+import org.zstack.header.identity.Action;
+import org.zstack.header.query.APIQueryMessage;
+import org.zstack.header.query.AutoQuery;
+import org.zstack.header.rest.RestRequest;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+@AutoQuery(replyClass = APIQueryPhysicalServerReply.class, inventoryClass = PhysicalServerInventory.class)
+@Action(category = PhysicalServerConstant.ACTION_CATEGORY, names = {"read"})
+@RestRequest(
+ path = "/physical-servers",
+ optionalPaths = {"/physical-servers/{uuid}"},
+ method = HttpMethod.GET,
+ responseClass = APIQueryPhysicalServerReply.class
+)
+public class APIQueryPhysicalServerMsg extends APIQueryMessage {
+ public static List __example__() {
+ return asList("uuid=" + uuid());
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsgDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsgDoc_zh_cn.groovy
new file mode 100644
index 00000000000..7547462f7a2
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerMsgDoc_zh_cn.groovy
@@ -0,0 +1,31 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.APIQueryPhysicalServerReply
+import org.zstack.header.query.APIQueryMessage
+
+doc {
+ title "查询物理服务器(QueryPhysicalServer)"
+
+ category "物理服务器"
+
+ desc """查询由计算节点或管理节点自动识别并关联的物理服务器。"""
+
+ rest {
+ request {
+ url "GET /v1/physical-servers"
+ url "GET /v1/physical-servers/{uuid}"
+
+ header (Authorization: 'OAuth the-session-uuid')
+
+ clz APIQueryPhysicalServerMsg.class
+
+ desc """"""
+
+ params APIQueryMessage.class
+ }
+
+ response {
+ clz APIQueryPhysicalServerReply.class
+ }
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReply.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReply.java
new file mode 100644
index 00000000000..a2e50d42f13
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReply.java
@@ -0,0 +1,19 @@
+package org.zstack.physicalserver;
+
+import org.zstack.header.query.APIQueryReply;
+import org.zstack.header.rest.RestResponse;
+
+import java.util.List;
+
+@RestResponse(allTo = "inventories")
+public class APIQueryPhysicalServerReply extends APIQueryReply {
+ private List inventories;
+
+ public List getInventories() {
+ return inventories;
+ }
+
+ public void setInventories(List inventories) {
+ this.inventories = inventories;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReplyDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReplyDoc_zh_cn.groovy
new file mode 100644
index 00000000000..bd38b3e1595
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerReplyDoc_zh_cn.groovy
@@ -0,0 +1,32 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.PhysicalServerInventory
+import org.zstack.header.errorcode.ErrorCode
+
+doc {
+
+ title "查询物理服务器的返回"
+
+ ref {
+ name "inventories"
+ path "org.zstack.physicalserver.APIQueryPhysicalServerReply.inventories"
+ desc "物理服务器清单"
+ type "List"
+ since "5.5.38"
+ clz PhysicalServerInventory.class
+ }
+ field {
+ name "success"
+ desc ""
+ type "boolean"
+ since "5.5.38"
+ }
+ ref {
+ name "error"
+ path "org.zstack.physicalserver.APIQueryPhysicalServerReply.error"
+ desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false
+ type "ErrorCode"
+ since "5.5.38"
+ clz ErrorCode.class
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsg.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsg.java
new file mode 100644
index 00000000000..01ddf6c4694
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsg.java
@@ -0,0 +1,28 @@
+package org.zstack.physicalserver;
+
+import org.springframework.http.HttpMethod;
+import org.zstack.header.identity.Action;
+import org.zstack.header.query.APIQueryMessage;
+import org.zstack.header.query.AutoQuery;
+import org.zstack.header.rest.RestRequest;
+
+import java.util.List;
+
+import static java.util.Arrays.asList;
+
+@AutoQuery(
+ replyClass = APIQueryPhysicalServerResourceAssignmentReply.class,
+ inventoryClass = PhysicalServerResourceAssignmentInventory.class
+)
+@Action(category = PhysicalServerConstant.ACTION_CATEGORY, names = {"read"})
+@RestRequest(
+ path = "/physical-servers/resource-assignments",
+ optionalPaths = {"/physical-servers/resource-assignments/{uuid}"},
+ method = HttpMethod.GET,
+ responseClass = APIQueryPhysicalServerResourceAssignmentReply.class
+)
+public class APIQueryPhysicalServerResourceAssignmentMsg extends APIQueryMessage {
+ public static List __example__() {
+ return asList("serverUuid=" + uuid(), "resourceType=CPU");
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsgDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsgDoc_zh_cn.groovy
new file mode 100644
index 00000000000..11aa68df519
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentMsgDoc_zh_cn.groovy
@@ -0,0 +1,31 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.APIQueryPhysicalServerResourceAssignmentReply
+import org.zstack.header.query.APIQueryMessage
+
+doc {
+ title "查询物理服务器资源分配(QueryPhysicalServerResourceAssignment)"
+
+ category "物理服务器"
+
+ desc """查询物理服务器资源分配的期望、实际状态及展示态。"""
+
+ rest {
+ request {
+ url "GET /v1/physical-servers/resource-assignments"
+ url "GET /v1/physical-servers/resource-assignments/{uuid}"
+
+ header (Authorization: 'OAuth the-session-uuid')
+
+ clz APIQueryPhysicalServerResourceAssignmentMsg.class
+
+ desc """"""
+
+ params APIQueryMessage.class
+ }
+
+ response {
+ clz APIQueryPhysicalServerResourceAssignmentReply.class
+ }
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReply.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReply.java
new file mode 100644
index 00000000000..ac43e18a06c
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReply.java
@@ -0,0 +1,19 @@
+package org.zstack.physicalserver;
+
+import org.zstack.header.query.APIQueryReply;
+import org.zstack.header.rest.RestResponse;
+
+import java.util.List;
+
+@RestResponse(allTo = "inventories")
+public class APIQueryPhysicalServerResourceAssignmentReply extends APIQueryReply {
+ private List inventories;
+
+ public List getInventories() {
+ return inventories;
+ }
+
+ public void setInventories(List inventories) {
+ this.inventories = inventories;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReplyDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReplyDoc_zh_cn.groovy
new file mode 100644
index 00000000000..83502502dc3
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIQueryPhysicalServerResourceAssignmentReplyDoc_zh_cn.groovy
@@ -0,0 +1,32 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.PhysicalServerResourceAssignmentInventory
+import org.zstack.header.errorcode.ErrorCode
+
+doc {
+
+ title "查询物理服务器资源分配的返回"
+
+ ref {
+ name "inventories"
+ path "org.zstack.physicalserver.APIQueryPhysicalServerResourceAssignmentReply.inventories"
+ desc "物理服务器资源分配清单"
+ type "List"
+ since "5.5.38"
+ clz PhysicalServerResourceAssignmentInventory.class
+ }
+ field {
+ name "success"
+ desc ""
+ type "boolean"
+ since "5.5.38"
+ }
+ ref {
+ name "error"
+ path "org.zstack.physicalserver.APIQueryPhysicalServerResourceAssignmentReply.error"
+ desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false
+ type "ErrorCode"
+ since "5.5.38"
+ clz ErrorCode.class
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEvent.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEvent.java
new file mode 100644
index 00000000000..59c40dbf333
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEvent.java
@@ -0,0 +1,14 @@
+package org.zstack.physicalserver;
+
+import org.zstack.header.message.APIEvent;
+import org.zstack.header.rest.RestResponse;
+
+@RestResponse
+public class APIRefreshPhysicalServerCpuIsolationEvent extends APIEvent {
+ public APIRefreshPhysicalServerCpuIsolationEvent() {
+ }
+
+ public APIRefreshPhysicalServerCpuIsolationEvent(String apiId) {
+ super(apiId);
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEventDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEventDoc_zh_cn.groovy
new file mode 100644
index 00000000000..1f43f46e194
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationEventDoc_zh_cn.groovy
@@ -0,0 +1,23 @@
+package org.zstack.physicalserver
+
+import org.zstack.header.errorcode.ErrorCode
+
+doc {
+
+ title "刷新物理服务器CPU隔离的返回"
+
+ field {
+ name "success"
+ desc ""
+ type "boolean"
+ since "5.5.38"
+ }
+ ref {
+ name "error"
+ path "org.zstack.physicalserver.APIRefreshPhysicalServerCpuIsolationEvent.error"
+ desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false
+ type "ErrorCode"
+ since "5.5.38"
+ clz ErrorCode.class
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsg.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsg.java
new file mode 100644
index 00000000000..e1739ce38c9
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsg.java
@@ -0,0 +1,35 @@
+package org.zstack.physicalserver;
+
+import org.springframework.http.HttpMethod;
+import org.zstack.header.identity.Action;
+import org.zstack.header.message.APIMessage;
+import org.zstack.header.message.APIParam;
+import org.zstack.header.rest.RestRequest;
+
+@Action(category = PhysicalServerConstant.ACTION_CATEGORY)
+@RestRequest(
+ path = "/physical-servers/{serverUuid}/cpu-isolation/actions",
+ method = HttpMethod.PUT,
+ responseClass = APIRefreshPhysicalServerCpuIsolationEvent.class,
+ isAction = true
+)
+public class APIRefreshPhysicalServerCpuIsolationMsg extends APIMessage implements PhysicalServerMessage {
+ @APIParam(resourceType = PhysicalServerVO.class, operationTarget = true)
+ private String serverUuid;
+
+ @Override
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
+
+ public static APIRefreshPhysicalServerCpuIsolationMsg __example__() {
+ APIRefreshPhysicalServerCpuIsolationMsg msg =
+ new APIRefreshPhysicalServerCpuIsolationMsg();
+ msg.setServerUuid(uuid());
+ return msg;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsgDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsgDoc_zh_cn.groovy
new file mode 100644
index 00000000000..e72793541b6
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIRefreshPhysicalServerCpuIsolationMsgDoc_zh_cn.groovy
@@ -0,0 +1,61 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.APIRefreshPhysicalServerCpuIsolationEvent
+
+doc {
+ title "刷新物理服务器CPU隔离(RefreshPhysicalServerCpuIsolation)"
+
+ category "物理服务器"
+
+ desc """异步触发指定物理服务器的CPU分配事实探测与收敛。接口返回成功仅表示任务已受理,实际结果通过资源分配查询接口查看。"""
+
+ rest {
+ request {
+ url "PUT /v1/physical-servers/{serverUuid}/cpu-isolation/actions"
+
+ header (Authorization: 'OAuth the-session-uuid')
+
+ clz APIRefreshPhysicalServerCpuIsolationMsg.class
+
+ desc """"""
+
+ params {
+
+ column {
+ name "serverUuid"
+ enclosedIn "refreshPhysicalServerCpuIsolation"
+ desc "物理服务器UUID"
+ location "url"
+ type "String"
+ optional false
+ since "5.5.38"
+
+ }
+ column {
+ name "systemTags"
+ enclosedIn ""
+ desc "系统标签"
+ location "body"
+ type "List"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "userTags"
+ enclosedIn ""
+ desc "用户标签"
+ location "body"
+ type "List"
+ optional true
+ since "5.5.38"
+
+ }
+ }
+ }
+
+ response {
+ clz APIRefreshPhysicalServerCpuIsolationEvent.class
+ }
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEvent.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEvent.java
new file mode 100644
index 00000000000..0e821267709
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEvent.java
@@ -0,0 +1,24 @@
+package org.zstack.physicalserver;
+
+import org.zstack.header.message.APIEvent;
+import org.zstack.header.rest.RestResponse;
+
+@RestResponse(allTo = "inventory")
+public class APIUpdatePhysicalServerCpuAssignmentEvent extends APIEvent {
+ private PhysicalServerResourceAssignmentInventory inventory;
+
+ public APIUpdatePhysicalServerCpuAssignmentEvent() {
+ }
+
+ public APIUpdatePhysicalServerCpuAssignmentEvent(String apiId) {
+ super(apiId);
+ }
+
+ public PhysicalServerResourceAssignmentInventory getInventory() {
+ return inventory;
+ }
+
+ public void setInventory(PhysicalServerResourceAssignmentInventory inventory) {
+ this.inventory = inventory;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEventDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEventDoc_zh_cn.groovy
new file mode 100644
index 00000000000..c26daff6910
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentEventDoc_zh_cn.groovy
@@ -0,0 +1,32 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.PhysicalServerResourceAssignmentInventory
+import org.zstack.header.errorcode.ErrorCode
+
+doc {
+
+ title "更新物理服务器CPU分配的返回"
+
+ ref {
+ name "inventory"
+ path "org.zstack.physicalserver.APIUpdatePhysicalServerCpuAssignmentEvent.inventory"
+ desc "已持久化的资源分配清单;实际状态可能仍在异步收敛"
+ type "PhysicalServerResourceAssignmentInventory"
+ since "5.5.38"
+ clz PhysicalServerResourceAssignmentInventory.class
+ }
+ field {
+ name "success"
+ desc ""
+ type "boolean"
+ since "5.5.38"
+ }
+ ref {
+ name "error"
+ path "org.zstack.physicalserver.APIUpdatePhysicalServerCpuAssignmentEvent.error"
+ desc "错误码,若不为null,则表示操作失败, 操作成功时该字段为null",false
+ type "ErrorCode"
+ since "5.5.38"
+ clz ErrorCode.class
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsg.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsg.java
new file mode 100644
index 00000000000..960b8f9ffe6
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsg.java
@@ -0,0 +1,94 @@
+package org.zstack.physicalserver;
+
+import org.springframework.http.HttpMethod;
+import org.zstack.header.identity.Action;
+import org.zstack.header.message.APIMessage;
+import org.zstack.header.message.APIParam;
+import org.zstack.header.rest.RestRequest;
+
+@Action(category = PhysicalServerConstant.ACTION_CATEGORY)
+@RestRequest(
+ path = "/physical-servers/{serverUuid}/cpu-assignments/{assignmentType}/actions",
+ method = HttpMethod.PUT,
+ responseClass = APIUpdatePhysicalServerCpuAssignmentEvent.class,
+ isAction = true
+)
+public class APIUpdatePhysicalServerCpuAssignmentMsg extends APIMessage implements PhysicalServerMessage {
+ @APIParam(resourceType = PhysicalServerVO.class, operationTarget = true)
+ private String serverUuid;
+
+ @APIParam
+ private String assignmentType;
+
+ @APIParam(required = false)
+ private String resourceType = PhysicalServerConstant.CPU_RESOURCE_TYPE;
+
+ @APIParam(required = false)
+ private Boolean enabled;
+
+ @APIParam(required = false)
+ private String selectionType;
+
+ @APIParam(required = false)
+ private String selectionValue;
+
+ @Override
+ public String getServerUuid() {
+ return serverUuid;
+ }
+
+ public void setServerUuid(String serverUuid) {
+ this.serverUuid = serverUuid;
+ }
+
+ public String getAssignmentType() {
+ return assignmentType;
+ }
+
+ public void setAssignmentType(String assignmentType) {
+ this.assignmentType = assignmentType;
+ }
+
+ public String getResourceType() {
+ return resourceType;
+ }
+
+ public void setResourceType(String resourceType) {
+ this.resourceType = resourceType;
+ }
+
+ public Boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getSelectionType() {
+ return selectionType;
+ }
+
+ public void setSelectionType(String selectionType) {
+ this.selectionType = selectionType;
+ }
+
+ public String getSelectionValue() {
+ return selectionValue;
+ }
+
+ public void setSelectionValue(String selectionValue) {
+ this.selectionValue = selectionValue;
+ }
+
+ public static APIUpdatePhysicalServerCpuAssignmentMsg __example__() {
+ APIUpdatePhysicalServerCpuAssignmentMsg msg =
+ new APIUpdatePhysicalServerCpuAssignmentMsg();
+ msg.setServerUuid(uuid());
+ msg.setAssignmentType(PhysicalServerCpuAssignmentType.COMPUTE_SHARED.name());
+ msg.setEnabled(true);
+ msg.setSelectionType(PhysicalServerCpuSelectionType.CPU_SET.name());
+ msg.setSelectionValue("0-3");
+ return msg;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsgDoc_zh_cn.groovy b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsgDoc_zh_cn.groovy
new file mode 100644
index 00000000000..566bd1711fa
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/APIUpdatePhysicalServerCpuAssignmentMsgDoc_zh_cn.groovy
@@ -0,0 +1,111 @@
+package org.zstack.physicalserver
+
+import org.zstack.physicalserver.APIUpdatePhysicalServerCpuAssignmentEvent
+
+doc {
+ title "更新物理服务器CPU分配(UpdatePhysicalServerCpuAssignment)"
+
+ category "物理服务器"
+
+ desc """更新指定物理服务器的CPU分配期望并异步触发收敛。ZBS_EXCLUSIVE 会影响计算容量;共享分配仅设置服务CPU亲和范围,不扣减计算容量。"""
+
+ rest {
+ request {
+ url "PUT /v1/physical-servers/{serverUuid}/cpu-assignments/{assignmentType}/actions"
+
+ header (Authorization: 'OAuth the-session-uuid')
+
+ clz APIUpdatePhysicalServerCpuAssignmentMsg.class
+
+ desc """"""
+
+ params {
+
+ column {
+ name "serverUuid"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "物理服务器UUID"
+ location "url"
+ type "String"
+ optional false
+ since "5.5.38"
+
+ }
+ column {
+ name "assignmentType"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "分配类型:ZBS_EXCLUSIVE、MANAGEMENT_SHARED或COMPUTE_SHARED"
+ location "url"
+ type "String"
+ optional false
+ since "5.5.38"
+
+ }
+ column {
+ name "resourceType"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "资源类型,当前仅支持CPU,默认CPU"
+ location "body"
+ type "String"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "enabled"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "是否启用该分配;可单独更新启用状态"
+ location "body"
+ type "Boolean"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "selectionType"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "选择方式:PERCENTAGE、COUNT或CPU_SET;必须与selectionValue同时填写"
+ location "body"
+ type "String"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "selectionValue"
+ enclosedIn "updatePhysicalServerCpuAssignment"
+ desc "选择值;百分比和数量使用整数,CPU集合使用如0-3,8-11的格式"
+ location "body"
+ type "String"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "systemTags"
+ enclosedIn ""
+ desc "系统标签"
+ location "body"
+ type "List"
+ optional true
+ since "5.5.38"
+
+ }
+ column {
+ name "userTags"
+ enclosedIn ""
+ desc "用户标签"
+ location "body"
+ type "List"
+ optional true
+ since "5.5.38"
+
+ }
+ }
+ }
+
+ response {
+ clz APIUpdatePhysicalServerCpuAssignmentEvent.class
+ }
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/FakeZbsCpuIsolationProvider.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/FakeZbsCpuIsolationProvider.java
new file mode 100644
index 00000000000..46706f56af1
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/FakeZbsCpuIsolationProvider.java
@@ -0,0 +1,291 @@
+package org.zstack.physicalserver;
+
+import org.zstack.core.CoreGlobalProperty;
+import org.zstack.header.core.Completion;
+import org.zstack.header.core.ReturnValueCompletion;
+
+import java.sql.Timestamp;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.zstack.core.Platform.operr;
+
+public class FakeZbsCpuIsolationProvider implements ZbsCpuIsolationProvider {
+ private final Map facts = new ConcurrentHashMap<>();
+ private final Map revisionFences = new ConcurrentHashMap<>();
+ private final AtomicInteger queryCalls = new AtomicInteger();
+ private final AtomicInteger updateCalls = new AtomicInteger();
+ private final AtomicBoolean holdNextQuery = new AtomicBoolean();
+ private final AtomicBoolean holdNextUpdate = new AtomicBoolean();
+ private final AtomicReference heldQuery = new AtomicReference<>();
+ private final AtomicReference heldUpdate = new AtomicReference<>();
+ private final AtomicLong lastUpdateRevision = new AtomicLong();
+ private volatile boolean enabled;
+ private volatile boolean autoApply;
+
+ @Override
+ public String getProviderType() {
+ return "FAKE";
+ }
+
+ @Override
+ public boolean isAvailable(ZbsNodeRef nodeRef) {
+ return CoreGlobalProperty.UNIT_TEST_ON && enabled;
+ }
+
+ @Override
+ public boolean supportsMonotonicRevisionFence() {
+ return true;
+ }
+
+ @Override
+ public void query(ZbsNodeRef nodeRef, ReturnValueCompletion completion) {
+ queryCalls.incrementAndGet();
+ if (!isAvailable(nodeRef)) {
+ completion.fail(operr(PhysicalServerConstant.ERROR_CODE,
+ "FAKE_ZBS_PROVIDER_DISABLED: fake provider never becomes production ready"));
+ return;
+ }
+ ZbsCpuIsolationFact fact = facts.get(nodeRef.getServerUuid());
+ if (fact == null) {
+ completion.fail(operr(PhysicalServerConstant.ERROR_CODE,
+ "FAKE_ZBS_FACT_MISSING: no fake fact for physical server[uuid:%s]", nodeRef.getServerUuid()));
+ return;
+ }
+ ZbsCpuIsolationFact response = copy(fact);
+ if (holdNextQuery.compareAndSet(true, false)) {
+ heldQuery.set(new HeldQuery(completion, response));
+ return;
+ }
+ completion.success(response);
+ }
+
+ @Override
+ public void update(ZbsNodeRef nodeRef, ZbsCpuIsolationUpdate update, Completion completion) {
+ updateCalls.incrementAndGet();
+ lastUpdateRevision.set(update.getRevision());
+ if (!isAvailable(nodeRef)) {
+ completion.fail(operr(PhysicalServerConstant.ERROR_CODE,
+ "FAKE_ZBS_PROVIDER_DISABLED: fake provider never becomes production ready"));
+ return;
+ }
+ AtomicReference rejected = new AtomicReference<>();
+ revisionFences.compute(nodeRef.getServerUuid(), (ignored, current) ->
+ acceptUpdate(nodeRef, update, current, rejected));
+ if (rejected.get() != null) {
+ completion.fail(operr(
+ PhysicalServerConstant.ERROR_CODE,
+ "%s", rejected.get()));
+ return;
+ }
+ if (holdNextUpdate.compareAndSet(true, false)) {
+ heldUpdate.set(new HeldUpdate(nodeRef, update, completion));
+ return;
+ }
+ if (autoApply) {
+ applyUpdate(nodeRef, update);
+ }
+ completion.success();
+ }
+
+ public void enable() {
+ enabled = true;
+ }
+
+ public void disable() {
+ enabled = false;
+ autoApply = false;
+ facts.clear();
+ revisionFences.clear();
+ queryCalls.set(0);
+ updateCalls.set(0);
+ holdNextQuery.set(false);
+ holdNextUpdate.set(false);
+ heldQuery.set(null);
+ heldUpdate.set(null);
+ lastUpdateRevision.set(0);
+ }
+
+ public void setAutoApply(boolean autoApply) {
+ this.autoApply = autoApply;
+ }
+
+ public void setFact(String serverUuid, ZbsCpuIsolationFact fact) {
+ facts.put(serverUuid, copy(fact));
+ }
+
+ public int getQueryCalls() {
+ return queryCalls.get();
+ }
+
+ public int getUpdateCalls() {
+ return updateCalls.get();
+ }
+
+ public void holdNextQuery() {
+ holdNextQuery.set(true);
+ }
+
+ public boolean hasHeldQuery() {
+ return heldQuery.get() != null;
+ }
+
+ public void completeHeldQuery() {
+ HeldQuery held = heldQuery.getAndSet(null);
+ if (held != null) {
+ held.completion.success(held.fact);
+ }
+ }
+
+ public void holdNextUpdate() {
+ holdNextUpdate.set(true);
+ }
+
+ public boolean hasHeldUpdate() {
+ return heldUpdate.get() != null;
+ }
+
+ public void completeHeldUpdate() {
+ HeldUpdate held = heldUpdate.getAndSet(null);
+ if (held != null) {
+ RevisionFence fence = revisionFences.get(held.nodeRef.getServerUuid());
+ if (autoApply && fence != null && fence.matches(held.update)) {
+ applyUpdate(held.nodeRef, held.update);
+ }
+ held.completion.success();
+ }
+ }
+
+ public long getLastUpdateRevision() {
+ return lastUpdateRevision.get();
+ }
+
+ private void applyUpdate(ZbsNodeRef nodeRef, ZbsCpuIsolationUpdate update) {
+ ZbsCpuIsolationFact fact = new ZbsCpuIsolationFact();
+ fact.setCapabilityVersion("1");
+ fact.setIsolationState(
+ "DISABLE".equals(update.getOperation()) ? "DISABLED" : "READY");
+ fact.setConfiguredCpuSet(update.getDesiredCpuSet());
+ fact.setEffectiveCpuSet(update.getDesiredCpuSet());
+ fact.setExpectedServiceCount(1);
+ fact.setCoveredServiceCount(1);
+ fact.setServiceReady(true);
+ fact.setOwnerAssignmentUuid(update.getAssignmentUuid());
+ fact.setOwnerGeneration(String.valueOf(update.getRevision()));
+ fact.setObservedAt(new Timestamp(System.currentTimeMillis()));
+ facts.put(nodeRef.getServerUuid(), fact);
+ }
+
+ private ZbsCpuIsolationFact copy(ZbsCpuIsolationFact source) {
+ ZbsCpuIsolationFact copy = new ZbsCpuIsolationFact();
+ copy.setCapabilityVersion(source.getCapabilityVersion());
+ copy.setIsolationState(source.getIsolationState());
+ copy.setConfiguredCpuSet(source.getConfiguredCpuSet());
+ copy.setEffectiveCpuSet(source.getEffectiveCpuSet());
+ copy.setCoveredServiceCount(source.getCoveredServiceCount());
+ copy.setExpectedServiceCount(source.getExpectedServiceCount());
+ copy.setServiceReady(source.isServiceReady());
+ copy.setOwnerAssignmentUuid(source.getOwnerAssignmentUuid());
+ copy.setOwnerGeneration(source.getOwnerGeneration());
+ copy.setObservedAt(source.getObservedAt());
+ copy.setReasonCode(source.getReasonCode());
+ return copy;
+ }
+
+ private RevisionFence acceptUpdate(
+ ZbsNodeRef nodeRef,
+ ZbsCpuIsolationUpdate update,
+ RevisionFence current,
+ AtomicReference rejected) {
+ if (update.getAssignmentUuid() == null || update.getAssignmentUuid().isEmpty()) {
+ rejected.set("OWNER_ASSIGNMENT_MISSING: assignmentUuid is required");
+ return current;
+ }
+ RevisionFence requested = new RevisionFence(update);
+ if (current == null) {
+ return requested;
+ }
+ if (current.assignmentUuid.equals(requested.assignmentUuid)) {
+ if (requested.revision < current.revision) {
+ rejected.set(String.format(
+ "OWNER_REVISION_STALE: revision[%s] is older than the highest accepted revision[%s]",
+ requested.revision, current.revision));
+ return current;
+ }
+ if (requested.revision == current.revision
+ && !current.payload.equals(requested.payload)) {
+ rejected.set(String.format(
+ "OWNER_REVISION_CONFLICT: revision[%s] was accepted with another payload",
+ requested.revision));
+ return current;
+ }
+ return requested.revision == current.revision ? current : requested;
+ }
+
+ ZbsCpuIsolationFact fact = facts.get(nodeRef.getServerUuid());
+ boolean previousReleased = fact != null
+ && current.assignmentUuid.equals(fact.getOwnerAssignmentUuid())
+ && "DISABLED".equals(fact.getIsolationState())
+ && (fact.getEffectiveCpuSet() == null || fact.getEffectiveCpuSet().isEmpty());
+ if (!previousReleased || !"SET".equals(requested.operation)) {
+ rejected.set(String.format(
+ "OWNER_ASSIGNMENT_STALE: assignmentUuid[%s] cannot replace active assignmentUuid[%s]",
+ requested.assignmentUuid, current.assignmentUuid));
+ return current;
+ }
+ return requested;
+ }
+
+ private static class HeldQuery {
+ private final ReturnValueCompletion completion;
+ private final ZbsCpuIsolationFact fact;
+
+ private HeldQuery(
+ ReturnValueCompletion completion,
+ ZbsCpuIsolationFact fact) {
+ this.completion = completion;
+ this.fact = fact;
+ }
+ }
+
+ private static class HeldUpdate {
+ private final ZbsNodeRef nodeRef;
+ private final ZbsCpuIsolationUpdate update;
+ private final Completion completion;
+
+ private HeldUpdate(
+ ZbsNodeRef nodeRef,
+ ZbsCpuIsolationUpdate update,
+ Completion completion) {
+ this.nodeRef = nodeRef;
+ this.update = update;
+ this.completion = completion;
+ }
+ }
+
+ private static class RevisionFence {
+ private final String assignmentUuid;
+ private final long revision;
+ private final String operation;
+ private final String payload;
+
+ private RevisionFence(ZbsCpuIsolationUpdate update) {
+ assignmentUuid = update.getAssignmentUuid();
+ revision = update.getRevision();
+ operation = update.getOperation();
+ payload = String.format("%s|%s|%s",
+ update.getOperation(), update.getDesiredCpuSet(), update.getPlanHash());
+ }
+
+ private boolean matches(ZbsCpuIsolationUpdate update) {
+ return assignmentUuid.equals(update.getAssignmentUuid())
+ && revision == update.getRevision()
+ && payload.equals(String.format("%s|%s|%s",
+ update.getOperation(), update.getDesiredCpuSet(), update.getPlanHash()));
+ }
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PackageInfo.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PackageInfo.java
new file mode 100644
index 00000000000..79a1fe224d5
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PackageInfo.java
@@ -0,0 +1,13 @@
+package org.zstack.physicalserver;
+
+import org.zstack.header.PackageAPIInfo;
+
+@PackageAPIInfo(
+ APICategoryName = "物理服务器",
+ permissions = {
+ PackageAPIInfo.PERMISSION_COMMUNITY_AVAILABLE,
+ PackageAPIInfo.PERMISSION_ZSV_BASIC_AVAILABLE
+ }
+)
+public class PackageInfo {
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerApiInterceptor.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerApiInterceptor.java
new file mode 100644
index 00000000000..f2da1aef103
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerApiInterceptor.java
@@ -0,0 +1,105 @@
+package org.zstack.physicalserver;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.zstack.core.cloudbus.CloudBus;
+import org.zstack.core.componentloader.PluginRegistry;
+import org.zstack.header.apimediator.ApiMessageInterceptionException;
+import org.zstack.header.apimediator.ApiMessageInterceptor;
+import org.zstack.header.message.APIMessage;
+
+import static org.zstack.core.Platform.argerr;
+
+public class PhysicalServerApiInterceptor implements ApiMessageInterceptor {
+ @Autowired
+ private CloudBus bus;
+ @Autowired
+ private PluginRegistry pluginRgty;
+
+ @Override
+ public APIMessage intercept(APIMessage msg) throws ApiMessageInterceptionException {
+ if (msg instanceof APIUpdatePhysicalServerCpuAssignmentMsg) {
+ validate((APIUpdatePhysicalServerCpuAssignmentMsg) msg);
+ }
+
+ if (msg instanceof PhysicalServerMessage) {
+ bus.makeTargetServiceIdByResourceUuid(
+ msg,
+ PhysicalServerConstant.SERVICE_ID,
+ PhysicalServerConstant.CONTROL_OWNER_KEY);
+ }
+ return msg;
+ }
+
+ private void validate(APIUpdatePhysicalServerCpuAssignmentMsg msg) {
+ PhysicalServerResourceAssignmentHandler handler = null;
+ for (PhysicalServerResourceAssignmentHandler candidate :
+ pluginRgty.getExtensionList(PhysicalServerResourceAssignmentHandler.class)) {
+ if (!candidate.getResourceType().equals(msg.getResourceType())) {
+ continue;
+ }
+ if (handler != null) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "RESOURCE_TYPE_AMBIGUOUS: multiple handlers registered for resourceType[%s]",
+ msg.getResourceType()));
+ }
+ handler = candidate;
+ }
+ if (handler == null || !PhysicalServerConstant.CPU_RESOURCE_TYPE.equals(
+ handler.getResourceType())) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "RESOURCE_TYPE_NOT_SUPPORTED: resourceType[%s] is not registered", msg.getResourceType()));
+ }
+
+ if (!handler.supportsAssignmentType(msg.getAssignmentType())) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "ASSIGNMENT_TYPE_NOT_SUPPORTED: assignmentType[%s] is not registered", msg.getAssignmentType()));
+ }
+ PhysicalServerCpuAssignmentType assignmentType = PhysicalServerCpuAssignmentType.valueOf(msg.getAssignmentType());
+
+ if ((msg.getSelectionType() == null) != (msg.getSelectionValue() == null)) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "selectionType and selectionValue must be specified together"));
+ }
+
+ if (msg.isEnabled() == null && msg.getSelectionType() == null) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "CPU_ASSIGNMENT_UPDATE_EMPTY: enabled or selection must be specified"));
+ }
+
+ if (msg.getSelectionType() == null) {
+ return;
+ }
+
+ if (!isSelectionType(msg.getSelectionType())) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE,
+ "CPU_SELECTION_TYPE_NOT_SUPPORTED: selectionType[%s] is not registered", msg.getSelectionType()));
+ }
+ PhysicalServerCpuSelectionType selectionType = PhysicalServerCpuSelectionType.valueOf(msg.getSelectionType());
+
+ if (assignmentType == PhysicalServerCpuAssignmentType.ZBS_EXCLUSIVE
+ && selectionType == PhysicalServerCpuSelectionType.COUNT) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE, "ZBS_EXCLUSIVE does not support COUNT selection"));
+ }
+ if (assignmentType != PhysicalServerCpuAssignmentType.ZBS_EXCLUSIVE
+ && selectionType == PhysicalServerCpuSelectionType.PERCENTAGE) {
+ throw new ApiMessageInterceptionException(argerr(
+ PhysicalServerConstant.ERROR_CODE, "shared CPU assignment does not support PERCENTAGE selection"));
+ }
+ }
+
+ private boolean isSelectionType(String value) {
+ for (PhysicalServerCpuSelectionType type : PhysicalServerCpuSelectionType.values()) {
+ if (type.name().equals(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerAssignmentRepository.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerAssignmentRepository.java
new file mode 100644
index 00000000000..a91f566d31d
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerAssignmentRepository.java
@@ -0,0 +1,407 @@
+package org.zstack.physicalserver;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.zstack.core.Platform;
+import org.zstack.core.db.DatabaseFacade;
+import org.zstack.core.db.Q;
+import org.zstack.core.db.SQL;
+import org.zstack.header.errorcode.OperationFailureException;
+import org.zstack.header.physicalserver.PhysicalServerCpuSet;
+import org.zstack.utils.Utils;
+import org.zstack.utils.gson.JSONObjectUtil;
+import org.zstack.utils.logging.CLogger;
+
+import javax.persistence.Query;
+import java.math.BigInteger;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.List;
+
+import static org.zstack.core.Platform.operr;
+import static org.zstack.utils.clouderrorcode.CloudOperationsErrorCode.ORG_ZSTACK_CORE_10000;
+
+public class PhysicalServerAssignmentRepository {
+ private static final CLogger logger = Utils.getLogger(PhysicalServerAssignmentRepository.class);
+
+ @Autowired
+ private DatabaseFacade dbf;
+
+ @Transactional
+ public PhysicalServerResourceAssignmentVO ensureDefault(
+ String serverUuid, PhysicalServerCpuAssignmentType assignmentType) {
+ PhysicalServerResourceAssignmentVO existing = find(serverUuid, assignmentType);
+ if (existing != null) {
+ return existing;
+ }
+
+ Query insert = dbf.getEntityManager().createNativeQuery(
+ "INSERT IGNORE INTO PhysicalServerResourceAssignmentVO " +
+ "(uuid, serverUuid, resourceType, assignmentType, spec, specGeneration, createDate, lastOpDate) " +
+ "VALUES (:uuid, :serverUuid, :resourceType, :assignmentType, :spec, 1, NOW(), NOW())");
+ insert.setParameter("uuid", Platform.getUuid());
+ insert.setParameter("serverUuid", serverUuid);
+ insert.setParameter("resourceType", PhysicalServerConstant.CPU_RESOURCE_TYPE);
+ insert.setParameter("assignmentType", assignmentType.name());
+ insert.setParameter("spec", JSONObjectUtil.toJsonString(defaultSpec(assignmentType)));
+ insert.executeUpdate();
+ return find(serverUuid, assignmentType);
+ }
+
+ @Transactional
+ public void ensureDefaults(
+ Collection serverUuids, PhysicalServerCpuAssignmentType assignmentType) {
+ if (serverUuids == null || serverUuids.isEmpty()) {
+ return;
+ }
+ Query insert = dbf.getEntityManager().createNativeQuery(
+ "INSERT IGNORE INTO PhysicalServerResourceAssignmentVO " +
+ "(uuid, serverUuid, resourceType, assignmentType, spec, specGeneration, createDate, lastOpDate) " +
+ "SELECT REPLACE(UUID(), '-', ''), p.uuid, :resourceType, :assignmentType, :spec, 1, NOW(), NOW() " +
+ "FROM PhysicalServerVO p WHERE p.uuid IN (:serverUuids)");
+ insert.setParameter("serverUuids", serverUuids);
+ insert.setParameter("resourceType", PhysicalServerConstant.CPU_RESOURCE_TYPE);
+ insert.setParameter("assignmentType", assignmentType.name());
+ insert.setParameter("spec", JSONObjectUtil.toJsonString(defaultSpec(assignmentType)));
+ insert.executeUpdate();
+ }
+
+ public PhysicalServerResourceAssignmentVO updateCpu(APIUpdatePhysicalServerCpuAssignmentMsg msg) {
+ PhysicalServerCpuAssignmentType assignmentType =
+ PhysicalServerCpuAssignmentType.valueOf(msg.getAssignmentType());
+ PhysicalServerResourceAssignmentVO current = requireAssignment(
+ msg.getServerUuid(), assignmentType);
+ PhysicalServerCpuAssignmentSpec spec = updatedSpec(current, msg);
+ String newSpec = JSONObjectUtil.toJsonString(spec);
+
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a " +
+ "set a.spec = :spec, a.specGeneration = a.specGeneration + 1 " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("spec", newSpec)
+ .param("uuid", current.getUuid())
+ .param("generation", current.getSpecGeneration())
+ .execute();
+ if (updated == 0) {
+ throw new OperationFailureException(operr(
+ ORG_ZSTACK_CORE_10000,
+ "CPU assignment on physical server[uuid:%s] changed concurrently, retry the request",
+ msg.getServerUuid()
+ ));
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logger.info(String.format(
+ "physical server resource assignment spec updated: " +
+ "serverUuid[%s], resourceType[%s], assignmentType[%s], " +
+ "accountUuid[%s], oldGeneration[%s], newGeneration[%s], " +
+ "oldSpec[%s], newSpec[%s]",
+ current.getServerUuid(), current.getResourceType(), current.getAssignmentType(),
+ msg.getSession() == null ? null : msg.getSession().getAccountUuid(),
+ current.getSpecGeneration(), result.getSpecGeneration(),
+ current.getSpec(), result.getSpec()));
+ return result;
+ }
+
+ public PhysicalServerCpuAssignmentSpec previewCpuUpdate(
+ APIUpdatePhysicalServerCpuAssignmentMsg msg) {
+ PhysicalServerCpuAssignmentType assignmentType =
+ PhysicalServerCpuAssignmentType.valueOf(msg.getAssignmentType());
+ return updatedSpec(requireAssignment(msg.getServerUuid(), assignmentType), msg);
+ }
+
+ public PhysicalServerResourceAssignmentVO prepareZbsRelease(String serverUuid) {
+ PhysicalServerResourceAssignmentVO current = find(
+ serverUuid, PhysicalServerCpuAssignmentType.ZBS_EXCLUSIVE);
+ if (current == null) {
+ return null;
+ }
+
+ PhysicalServerCpuAssignmentSpec spec = JSONObjectUtil.toObject(
+ current.getSpec(), PhysicalServerCpuAssignmentSpec.class);
+ if (spec.isCascadeReleaseRequested()) {
+ return current;
+ }
+
+ spec.setCascadeReleaseRequested(true);
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a " +
+ "set a.spec = :spec, a.specGeneration = a.specGeneration + 1 " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("spec", JSONObjectUtil.toJsonString(spec))
+ .param("uuid", current.getUuid())
+ .param("generation", current.getSpecGeneration())
+ .execute();
+ if (updated == 0) {
+ throw new OperationFailureException(operr(
+ ORG_ZSTACK_CORE_10000,
+ "CPU assignment on physical server[uuid:%s] changed concurrently, retry deleting the ZBS relationship",
+ serverUuid
+ ));
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logger.info(String.format(
+ "physical server ZBS CPU isolation Cascade release requested: " +
+ "serverUuid[%s], assignmentUuid[%s], oldGeneration[%s], newGeneration[%s]",
+ serverUuid, current.getUuid(), current.getSpecGeneration(), result.getSpecGeneration()));
+ return result;
+ }
+
+ public PhysicalServerResourceAssignmentVO cancelZbsRelease(String serverUuid) {
+ PhysicalServerResourceAssignmentVO current = find(
+ serverUuid, PhysicalServerCpuAssignmentType.ZBS_EXCLUSIVE);
+ if (current == null) {
+ return null;
+ }
+ PhysicalServerCpuAssignmentSpec spec = JSONObjectUtil.toObject(
+ current.getSpec(), PhysicalServerCpuAssignmentSpec.class);
+ if (!spec.isCascadeReleaseRequested()) {
+ return current;
+ }
+ spec.setCascadeReleaseRequested(false);
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a " +
+ "set a.spec = :spec, a.specGeneration = a.specGeneration + 1 " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("spec", JSONObjectUtil.toJsonString(spec))
+ .param("uuid", current.getUuid())
+ .param("generation", current.getSpecGeneration())
+ .execute();
+ if (updated == 0) {
+ throw new OperationFailureException(operr(
+ ORG_ZSTACK_CORE_10000,
+ "CPU assignment on physical server[uuid:%s] changed while canceling Cascade release",
+ serverUuid));
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logger.info(String.format(
+ "physical server ZBS CPU isolation Cascade release canceled: " +
+ "serverUuid[%s], assignmentUuid[%s], oldGeneration[%s], newGeneration[%s]",
+ serverUuid, current.getUuid(), current.getSpecGeneration(), result.getSpecGeneration()));
+ return result;
+ }
+
+ public boolean deleteIfGeneration(String uuid, long generation) {
+ boolean deleted = SQL.New(
+ "delete from PhysicalServerResourceAssignmentVO a " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("uuid", uuid)
+ .param("generation", generation)
+ .execute() == 1;
+ if (deleted) {
+ logger.info(String.format(
+ "physical server resource assignment deleted after owner release: " +
+ "assignmentUuid[%s], generation[%s]", uuid, generation));
+ }
+ return deleted;
+ }
+
+ public PhysicalServerResourceAssignmentVO find(
+ String serverUuid, PhysicalServerCpuAssignmentType assignmentType) {
+ return Q.New(PhysicalServerResourceAssignmentVO.class)
+ .eq(PhysicalServerResourceAssignmentVO_.serverUuid, serverUuid)
+ .eq(PhysicalServerResourceAssignmentVO_.resourceType, PhysicalServerConstant.CPU_RESOURCE_TYPE)
+ .eq(PhysicalServerResourceAssignmentVO_.assignmentType, assignmentType.name())
+ .find();
+ }
+
+ public List listCpuAssignments() {
+ return Q.New(PhysicalServerResourceAssignmentVO.class)
+ .eq(PhysicalServerResourceAssignmentVO_.resourceType,
+ PhysicalServerConstant.CPU_RESOURCE_TYPE)
+ .list();
+ }
+
+ public PhysicalServerResourceAssignmentVO updatePlan(
+ PhysicalServerResourceAssignmentVO current, PhysicalServerCpuPlan plan) {
+ PhysicalServerCpuAssignmentSpec spec = JSONObjectUtil.toObject(
+ current.getSpec(), PhysicalServerCpuAssignmentSpec.class);
+ if (equals(spec.getDesiredCpuSet(), plan.getDesiredCpuSet())
+ && equals(spec.getPlanHash(), plan.getPlanHash())) {
+ return current;
+ }
+ spec.setDesiredCpuSet(plan.getDesiredCpuSet());
+ spec.setPlanHash(plan.getPlanHash());
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a " +
+ "set a.spec = :spec, a.specGeneration = a.specGeneration + 1 " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("spec", JSONObjectUtil.toJsonString(spec))
+ .param("uuid", current.getUuid())
+ .param("generation", current.getSpecGeneration())
+ .execute();
+ if (updated == 0) {
+ return null;
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logger.info(String.format(
+ "physical server resource assignment plan updated: " +
+ "serverUuid[%s], resourceType[%s], assignmentType[%s], " +
+ "oldGeneration[%s], newGeneration[%s], oldSpec[%s], newSpec[%s]",
+ current.getServerUuid(), current.getResourceType(), current.getAssignmentType(),
+ current.getSpecGeneration(), result.getSpecGeneration(),
+ current.getSpec(), result.getSpec()));
+ return result;
+ }
+
+ public PhysicalServerResourceAssignmentVO updateStatus(
+ PhysicalServerResourceAssignmentVO current,
+ PhysicalServerCpuAssignmentStatus status,
+ Timestamp observedAt) {
+ String statusJson = JSONObjectUtil.toJsonString(status);
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a set a.status = :status, " +
+ "a.observedGeneration = :generation, a.observedAt = :observedAt " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("status", statusJson)
+ .param("generation", current.getSpecGeneration())
+ .param("observedAt", observedAt)
+ .param("uuid", current.getUuid())
+ .execute();
+ if (updated == 0) {
+ return null;
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logStatusChange(current, result, statusJson);
+ return result;
+ }
+
+ public PhysicalServerResourceAssignmentVO updateFailureStatus(
+ PhysicalServerResourceAssignmentVO current,
+ String ownerState,
+ String providerType,
+ String backend,
+ String reasonCode,
+ Integer sourceRefCount,
+ boolean ownerObserved) {
+ PhysicalServerCpuAssignmentStatus status = current.getStatus() == null
+ ? new PhysicalServerCpuAssignmentStatus()
+ : JSONObjectUtil.toObject(current.getStatus(), PhysicalServerCpuAssignmentStatus.class);
+ status.setOwnerState(ownerState);
+ status.setProviderType(providerType);
+ status.setBackend(backend);
+ status.setReasonCode(reasonCode);
+ status.setSourceRefCount(sourceRefCount);
+ if (ownerObserved) {
+ return updateStatus(current, status, new Timestamp(System.currentTimeMillis()));
+ }
+ String statusJson = JSONObjectUtil.toJsonString(status);
+ int updated = SQL.New(
+ "update PhysicalServerResourceAssignmentVO a set a.status = :status " +
+ "where a.uuid = :uuid and a.specGeneration = :generation")
+ .param("status", statusJson)
+ .param("uuid", current.getUuid())
+ .param("generation", current.getSpecGeneration())
+ .execute();
+ if (updated == 0) {
+ return null;
+ }
+ PhysicalServerResourceAssignmentVO result = dbf.findByUuid(
+ current.getUuid(), PhysicalServerResourceAssignmentVO.class);
+ logStatusChange(current, result, statusJson);
+ return result;
+ }
+
+ private void logStatusChange(
+ PhysicalServerResourceAssignmentVO current,
+ PhysicalServerResourceAssignmentVO result,
+ String statusJson) {
+ if (equals(current.getStatus(), statusJson)) {
+ return;
+ }
+ logger.info(String.format(
+ "physical server resource assignment observed status changed: " +
+ "serverUuid[%s], resourceType[%s], assignmentType[%s], " +
+ "specGeneration[%s], oldStatus[%s], newStatus[%s]",
+ current.getServerUuid(), current.getResourceType(), current.getAssignmentType(),
+ result.getSpecGeneration(), current.getStatus(), statusJson));
+ }
+
+ private boolean equals(Object left, Object right) {
+ return left == null ? right == null : left.equals(right);
+ }
+
+ private PhysicalServerResourceAssignmentVO requireAssignment(
+ String serverUuid, PhysicalServerCpuAssignmentType assignmentType) {
+ PhysicalServerResourceAssignmentVO current = find(serverUuid, assignmentType);
+ if (current == null) {
+ throw new OperationFailureException(operr(
+ ORG_ZSTACK_CORE_10000,
+ "CPU assignment[%s] does not exist on physical server[uuid:%s]",
+ assignmentType, serverUuid
+ ));
+ }
+ return current;
+ }
+
+ private PhysicalServerCpuAssignmentSpec updatedSpec(
+ PhysicalServerResourceAssignmentVO current,
+ APIUpdatePhysicalServerCpuAssignmentMsg msg) {
+ PhysicalServerCpuAssignmentSpec spec = JSONObjectUtil.toObject(
+ current.getSpec(), PhysicalServerCpuAssignmentSpec.class);
+ if (msg.isEnabled() != null) {
+ spec.setEnabled(msg.isEnabled());
+ }
+ if (msg.getSelectionType() != null) {
+ PhysicalServerCpuSelectionType selectionType =
+ PhysicalServerCpuSelectionType.valueOf(msg.getSelectionType());
+ spec.setSelection(new PhysicalServerCpuAssignmentSpec.Selection(
+ selectionType.name(), normalizeSelectionValue(
+ selectionType, msg.getSelectionValue())));
+ }
+ spec.setSource("USER");
+ spec.setCascadeReleaseRequested(false);
+ spec.setDesiredCpuSet(null);
+ spec.setPlanHash(null);
+ return spec;
+ }
+
+ private PhysicalServerCpuAssignmentSpec defaultSpec(PhysicalServerCpuAssignmentType assignmentType) {
+ PhysicalServerCpuAssignmentSpec spec = new PhysicalServerCpuAssignmentSpec();
+ spec.setSource("BUILT_IN_DEFAULT");
+ spec.setEnabled(true);
+ switch (assignmentType) {
+ case ZBS_EXCLUSIVE:
+ spec.setSelection(new PhysicalServerCpuAssignmentSpec.Selection("PERCENTAGE", "50"));
+ break;
+ case MANAGEMENT_SHARED:
+ case COMPUTE_SHARED:
+ spec.setSource("AWAITING_DEFAULT_FREEZE");
+ spec.setEnabled(false);
+ break;
+ default:
+ throw new IllegalArgumentException(String.format("unsupported assignmentType[%s]", assignmentType));
+ }
+ return spec;
+ }
+
+ private String normalizeSelectionValue(PhysicalServerCpuSelectionType selectionType, String value) {
+ switch (selectionType) {
+ case CPU_SET:
+ return PhysicalServerCpuSet.normalize(value);
+ case COUNT:
+ return String.valueOf(parseBoundedNumber(value, 1, Integer.MAX_VALUE, "COUNT"));
+ case PERCENTAGE:
+ return String.valueOf(parseBoundedNumber(value, 1, 99, "PERCENTAGE"));
+ default:
+ throw new IllegalArgumentException(String.format("unsupported selectionType[%s]", selectionType));
+ }
+ }
+
+ private int parseBoundedNumber(String value, int min, int max, String type) {
+ String normalized = value == null ? "" : value.trim();
+ if (!normalized.matches("[0-9]+")) {
+ throw new IllegalArgumentException(String.format("invalid %s value[%s]", type, value));
+ }
+ BigInteger number = new BigInteger(normalized);
+ if (number.compareTo(BigInteger.valueOf(min)) < 0 || number.compareTo(BigInteger.valueOf(max)) > 0) {
+ throw new IllegalArgumentException(String.format(
+ "%s value[%s] is outside range[%s, %s]", type, value, min, max));
+ }
+ return number.intValue();
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerConstant.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerConstant.java
new file mode 100644
index 00000000000..9da1e0a27cd
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerConstant.java
@@ -0,0 +1,10 @@
+package org.zstack.physicalserver;
+
+public interface PhysicalServerConstant {
+ String SERVICE_ID = "physicalServer";
+ String ACTION_CATEGORY = "physicalServer";
+ String CONTROL_OWNER_KEY = "physical-server-control-owner-v1";
+ String CPU_RESOURCE_TYPE = "CPU";
+ String ERROR_CODE = "ORG_ZSTACK_PHYSICALSERVER_10000";
+ long STATUS_TTL_MILLIS = 300_000L;
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentHandler.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentHandler.java
new file mode 100644
index 00000000000..532f658ad69
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentHandler.java
@@ -0,0 +1,28 @@
+package org.zstack.physicalserver;
+
+public class PhysicalServerCpuAssignmentHandler implements PhysicalServerResourceAssignmentHandler {
+ @Override
+ public String getResourceType() {
+ return PhysicalServerConstant.CPU_RESOURCE_TYPE;
+ }
+
+ @Override
+ public boolean supportsAssignmentType(String assignmentType) {
+ for (PhysicalServerCpuAssignmentType type : PhysicalServerCpuAssignmentType.values()) {
+ if (type.name().equals(assignmentType)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Class> getSpecClass() {
+ return PhysicalServerCpuAssignmentSpec.class;
+ }
+
+ @Override
+ public Class> getStatusClass() {
+ return PhysicalServerCpuAssignmentStatus.class;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentSpec.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentSpec.java
new file mode 100644
index 00000000000..351373e1158
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentSpec.java
@@ -0,0 +1,98 @@
+package org.zstack.physicalserver;
+
+public class PhysicalServerCpuAssignmentSpec {
+ public static final int SCHEMA_VERSION = 1;
+
+ private int schemaVersion = SCHEMA_VERSION;
+ private String source;
+ private boolean enabled;
+ private Selection selection;
+ private String desiredCpuSet;
+ private String planHash;
+ private boolean cascadeReleaseRequested;
+
+ public static class Selection {
+ private String type;
+ private String value;
+
+ public Selection() {
+ }
+
+ public Selection(String type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public void setType(String type) {
+ this.type = type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public void setValue(String value) {
+ this.value = value;
+ }
+ }
+
+ public int getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public void setSchemaVersion(int schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ }
+
+ public String getSource() {
+ return source;
+ }
+
+ public void setSource(String source) {
+ this.source = source;
+ }
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public Selection getSelection() {
+ return selection;
+ }
+
+ public void setSelection(Selection selection) {
+ this.selection = selection;
+ }
+
+ public String getDesiredCpuSet() {
+ return desiredCpuSet;
+ }
+
+ public void setDesiredCpuSet(String desiredCpuSet) {
+ this.desiredCpuSet = desiredCpuSet;
+ }
+
+ public String getPlanHash() {
+ return planHash;
+ }
+
+ public void setPlanHash(String planHash) {
+ this.planHash = planHash;
+ }
+
+ public boolean isCascadeReleaseRequested() {
+ return cascadeReleaseRequested;
+ }
+
+ public void setCascadeReleaseRequested(boolean cascadeReleaseRequested) {
+ this.cascadeReleaseRequested = cascadeReleaseRequested;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentStatus.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentStatus.java
new file mode 100644
index 00000000000..10554675daf
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentStatus.java
@@ -0,0 +1,171 @@
+package org.zstack.physicalserver;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PhysicalServerCpuAssignmentStatus {
+ public static final int SCHEMA_VERSION = 1;
+
+ private int schemaVersion = SCHEMA_VERSION;
+ private String ownerState;
+ private String configuredCpuSet;
+ private String actualCpuSet;
+ private String appliedPlanHash;
+ private String ownerAssignmentUuid;
+ private String ownerGeneration;
+ private String backend;
+ private String providerType;
+ private String reasonCode;
+ private Integer sourceRefCount;
+ private Integer coveredServiceCount;
+ private Integer expectedServiceCount;
+ private Integer requestedCount;
+ private Integer effectiveCount;
+ private Boolean clamped;
+ private List consumerKeys = new ArrayList<>();
+ private List unresolvedHandles = new ArrayList<>();
+
+ public int getSchemaVersion() {
+ return schemaVersion;
+ }
+
+ public void setSchemaVersion(int schemaVersion) {
+ this.schemaVersion = schemaVersion;
+ }
+
+ public String getOwnerState() {
+ return ownerState;
+ }
+
+ public void setOwnerState(String ownerState) {
+ this.ownerState = ownerState;
+ }
+
+ public String getConfiguredCpuSet() {
+ return configuredCpuSet;
+ }
+
+ public void setConfiguredCpuSet(String configuredCpuSet) {
+ this.configuredCpuSet = configuredCpuSet;
+ }
+
+ public String getActualCpuSet() {
+ return actualCpuSet;
+ }
+
+ public void setActualCpuSet(String actualCpuSet) {
+ this.actualCpuSet = actualCpuSet;
+ }
+
+ public String getAppliedPlanHash() {
+ return appliedPlanHash;
+ }
+
+ public void setAppliedPlanHash(String appliedPlanHash) {
+ this.appliedPlanHash = appliedPlanHash;
+ }
+
+ public String getOwnerAssignmentUuid() {
+ return ownerAssignmentUuid;
+ }
+
+ public void setOwnerAssignmentUuid(String ownerAssignmentUuid) {
+ this.ownerAssignmentUuid = ownerAssignmentUuid;
+ }
+
+ public String getOwnerGeneration() {
+ return ownerGeneration;
+ }
+
+ public void setOwnerGeneration(String ownerGeneration) {
+ this.ownerGeneration = ownerGeneration;
+ }
+
+ public String getBackend() {
+ return backend;
+ }
+
+ public void setBackend(String backend) {
+ this.backend = backend;
+ }
+
+ public String getProviderType() {
+ return providerType;
+ }
+
+ public void setProviderType(String providerType) {
+ this.providerType = providerType;
+ }
+
+ public String getReasonCode() {
+ return reasonCode;
+ }
+
+ public void setReasonCode(String reasonCode) {
+ this.reasonCode = reasonCode;
+ }
+
+ public Integer getSourceRefCount() {
+ return sourceRefCount;
+ }
+
+ public void setSourceRefCount(Integer sourceRefCount) {
+ this.sourceRefCount = sourceRefCount;
+ }
+
+ public Integer getCoveredServiceCount() {
+ return coveredServiceCount;
+ }
+
+ public void setCoveredServiceCount(Integer coveredServiceCount) {
+ this.coveredServiceCount = coveredServiceCount;
+ }
+
+ public Integer getExpectedServiceCount() {
+ return expectedServiceCount;
+ }
+
+ public void setExpectedServiceCount(Integer expectedServiceCount) {
+ this.expectedServiceCount = expectedServiceCount;
+ }
+
+ public Integer getRequestedCount() {
+ return requestedCount;
+ }
+
+ public void setRequestedCount(Integer requestedCount) {
+ this.requestedCount = requestedCount;
+ }
+
+ public Integer getEffectiveCount() {
+ return effectiveCount;
+ }
+
+ public void setEffectiveCount(Integer effectiveCount) {
+ this.effectiveCount = effectiveCount;
+ }
+
+ public Boolean getClamped() {
+ return clamped;
+ }
+
+ public void setClamped(Boolean clamped) {
+ this.clamped = clamped;
+ }
+
+ public List getConsumerKeys() {
+ return consumerKeys;
+ }
+
+ public void setConsumerKeys(List consumerKeys) {
+ this.consumerKeys = consumerKeys;
+ }
+
+ public List getUnresolvedHandles() {
+ return unresolvedHandles;
+ }
+
+ public void setUnresolvedHandles(List unresolvedHandles) {
+ this.unresolvedHandles = unresolvedHandles;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentType.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentType.java
new file mode 100644
index 00000000000..75d1acc1dda
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuAssignmentType.java
@@ -0,0 +1,7 @@
+package org.zstack.physicalserver;
+
+public enum PhysicalServerCpuAssignmentType {
+ ZBS_EXCLUSIVE,
+ MANAGEMENT_SHARED,
+ COMPUTE_SHARED
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlan.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlan.java
new file mode 100644
index 00000000000..22da5e6385f
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlan.java
@@ -0,0 +1,49 @@
+package org.zstack.physicalserver;
+
+public class PhysicalServerCpuPlan {
+ private String desiredCpuSet;
+ private String planHash;
+ private Integer requestedCount;
+ private Integer effectiveCount;
+ private boolean clamped;
+
+ public String getDesiredCpuSet() {
+ return desiredCpuSet;
+ }
+
+ public void setDesiredCpuSet(String desiredCpuSet) {
+ this.desiredCpuSet = desiredCpuSet;
+ }
+
+ public String getPlanHash() {
+ return planHash;
+ }
+
+ public void setPlanHash(String planHash) {
+ this.planHash = planHash;
+ }
+
+ public Integer getRequestedCount() {
+ return requestedCount;
+ }
+
+ public void setRequestedCount(Integer requestedCount) {
+ this.requestedCount = requestedCount;
+ }
+
+ public Integer getEffectiveCount() {
+ return effectiveCount;
+ }
+
+ public void setEffectiveCount(Integer effectiveCount) {
+ this.effectiveCount = effectiveCount;
+ }
+
+ public boolean isClamped() {
+ return clamped;
+ }
+
+ public void setClamped(boolean clamped) {
+ this.clamped = clamped;
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlanner.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlanner.java
new file mode 100644
index 00000000000..fffc7fac90d
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuPlanner.java
@@ -0,0 +1,199 @@
+package org.zstack.physicalserver;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.zstack.header.physicalserver.PhysicalServerCpuSet;
+import org.zstack.header.physicalserver.PhysicalServerCpuTopology;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class PhysicalServerCpuPlanner {
+ public PhysicalServerCpuPlan plan(
+ PhysicalServerCpuAssignmentType assignmentType,
+ PhysicalServerCpuAssignmentSpec spec,
+ PhysicalServerCpuTopology topology,
+ Collection exclusiveCpus) {
+ if (!spec.isEnabled()) {
+ return buildPlan(assignmentType, spec, topology, new TreeSet<>(), null, false);
+ }
+ if (spec.getSelection() == null) {
+ throw new IllegalArgumentException("CPU_SELECTION_REQUIRED: enabled assignment requires a selection");
+ }
+
+ PhysicalServerCpuSelectionType selectionType =
+ PhysicalServerCpuSelectionType.valueOf(spec.getSelection().getType());
+ SortedSet desired;
+ Integer requestedCount = null;
+ boolean clamped = false;
+ if (assignmentType == PhysicalServerCpuAssignmentType.ZBS_EXCLUSIVE) {
+ desired = planZbs(selectionType, spec.getSelection().getValue(), topology);
+ } else {
+ Set available = topology.getOnlineCpus();
+ available.removeAll(exclusiveCpus);
+ if (selectionType == PhysicalServerCpuSelectionType.CPU_SET) {
+ desired = PhysicalServerCpuSet.parse(spec.getSelection().getValue(), topology.getOnlineCpus());
+ if (!available.containsAll(desired)) {
+ throw new IllegalArgumentException("CPU_SET_CONFLICT: shared CPU_SET overlaps the exclusive CPU set");
+ }
+ } else if (selectionType == PhysicalServerCpuSelectionType.COUNT) {
+ requestedCount = parseNumber(spec.getSelection().getValue(), 1, Integer.MAX_VALUE, "COUNT");
+ desired = selectSharedCpus(requestedCount, topology, available);
+ clamped = desired.size() < requestedCount;
+ } else {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SELECTION_TYPE_NOT_SUPPORTED: shared assignment does not support %s", selectionType));
+ }
+ }
+ return buildPlan(assignmentType, spec, topology, desired, requestedCount, clamped);
+ }
+
+ private SortedSet planZbs(
+ PhysicalServerCpuSelectionType selectionType,
+ String selectionValue,
+ PhysicalServerCpuTopology topology) {
+ SortedSet desired = new TreeSet<>();
+ if (selectionType == PhysicalServerCpuSelectionType.PERCENTAGE) {
+ int percentage = parseNumber(selectionValue, 1, 99, "PERCENTAGE");
+ List groups = topology.getCoreGroups();
+ int selectedCount = groups.size() * percentage / 100;
+ if (selectedCount < 1) {
+ throw new IllegalArgumentException("CPU_TOPOLOGY_TOO_SMALL: percentage selects no complete core group");
+ }
+ for (int index = groups.size() - selectedCount; index < groups.size(); index++) {
+ desired.addAll(groups.get(index).getCpus());
+ }
+ } else if (selectionType == PhysicalServerCpuSelectionType.CPU_SET) {
+ desired = PhysicalServerCpuSet.parse(selectionValue, topology.getOnlineCpus());
+ for (PhysicalServerCpuTopology.CoreGroup group : topology.getCoreGroups()) {
+ boolean intersects = !disjoint(desired, group.getCpus());
+ if (intersects && !desired.containsAll(group.getCpus())) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SIBLING_SPLIT: CPU_SET splits core group[%s]",
+ PhysicalServerCpuSet.format(group.getCpus())));
+ }
+ }
+ } else {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SELECTION_TYPE_NOT_SUPPORTED: ZBS assignment does not support %s", selectionType));
+ }
+
+ if (!disjoint(desired, topology.getCpuZeroGroup().getCpus())) {
+ throw new IllegalArgumentException("CPU_ZERO_RESERVED: CPU0 core group must remain shared");
+ }
+ return desired;
+ }
+
+ public String normalizeZbsCpuSet(String cpuSet, PhysicalServerCpuTopology topology) {
+ if (cpuSet == null || cpuSet.trim().isEmpty()) {
+ return "";
+ }
+ SortedSet desired = PhysicalServerCpuSet.parse(cpuSet, topology.getOnlineCpus());
+ for (PhysicalServerCpuTopology.CoreGroup group : topology.getCoreGroups()) {
+ boolean intersects = !disjoint(desired, group.getCpus());
+ if (intersects && !desired.containsAll(group.getCpus())) {
+ throw new IllegalArgumentException(String.format(
+ "CPU_SIBLING_SPLIT: CPU_SET splits core group[%s]",
+ PhysicalServerCpuSet.format(group.getCpus())));
+ }
+ }
+ if (!disjoint(desired, topology.getCpuZeroGroup().getCpus())) {
+ throw new IllegalArgumentException("CPU_ZERO_RESERVED: CPU0 core group must remain shared");
+ }
+ return PhysicalServerCpuSet.format(desired);
+ }
+
+ private SortedSet selectSharedCpus(
+ int count, PhysicalServerCpuTopology topology, Set available) {
+ Map> byNuma = new LinkedHashMap<>();
+ for (PhysicalServerCpuTopology.CoreGroup group : topology.getCoreGroups()) {
+ byNuma.computeIfAbsent(group.getNumaId(), ignored -> new ArrayList<>()).add(group);
+ }
+
+ List spreadGroups = new ArrayList<>();
+ int groupIndex = 0;
+ boolean added;
+ do {
+ added = false;
+ for (List groups : byNuma.values()) {
+ if (groupIndex < groups.size()) {
+ spreadGroups.add(groups.get(groupIndex));
+ added = true;
+ }
+ }
+ groupIndex++;
+ } while (added);
+
+ SortedSet selected = new TreeSet<>();
+ int siblingIndex = 0;
+ while (selected.size() < count) {
+ boolean found = false;
+ for (PhysicalServerCpuTopology.CoreGroup group : spreadGroups) {
+ List siblings = new ArrayList<>(group.getCpus());
+ if (siblingIndex < siblings.size() && available.contains(siblings.get(siblingIndex))) {
+ selected.add(siblings.get(siblingIndex));
+ found = true;
+ if (selected.size() == count) {
+ return selected;
+ }
+ }
+ }
+ if (!found) {
+ return selected;
+ }
+ siblingIndex++;
+ }
+ return selected;
+ }
+
+ private PhysicalServerCpuPlan buildPlan(
+ PhysicalServerCpuAssignmentType assignmentType,
+ PhysicalServerCpuAssignmentSpec spec,
+ PhysicalServerCpuTopology topology,
+ SortedSet desired,
+ Integer requestedCount,
+ boolean clamped) {
+ String desiredCpuSet = PhysicalServerCpuSet.format(desired);
+ String hashInput = String.join("|",
+ String.valueOf(PhysicalServerCpuAssignmentSpec.SCHEMA_VERSION),
+ assignmentType.name(),
+ String.valueOf(spec.isEnabled()),
+ spec.getSelection() == null ? "" : spec.getSelection().getType(),
+ spec.getSelection() == null ? "" : spec.getSelection().getValue(),
+ topology.fingerprint(),
+ desiredCpuSet);
+ PhysicalServerCpuPlan plan = new PhysicalServerCpuPlan();
+ plan.setDesiredCpuSet(desiredCpuSet);
+ plan.setPlanHash(DigestUtils.sha256Hex(hashInput));
+ plan.setRequestedCount(requestedCount);
+ plan.setEffectiveCount(desired.size());
+ plan.setClamped(clamped);
+ return plan;
+ }
+
+ private int parseNumber(String value, int min, int max, String type) {
+ String normalized = value == null ? "" : value.trim();
+ if (!normalized.matches("[0-9]+")) {
+ throw new IllegalArgumentException(String.format("invalid %s value[%s]", type, value));
+ }
+ BigInteger number = new BigInteger(normalized);
+ if (number.compareTo(BigInteger.valueOf(min)) < 0 || number.compareTo(BigInteger.valueOf(max)) > 0) {
+ throw new IllegalArgumentException(String.format(
+ "%s value[%s] is outside range[%s, %s]", type, value, min, max));
+ }
+ return number.intValue();
+ }
+
+ private boolean disjoint(Collection left, Collection right) {
+ Set copy = new HashSet<>(left);
+ copy.retainAll(right);
+ return copy.isEmpty();
+ }
+}
diff --git a/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuReconciler.java b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuReconciler.java
new file mode 100644
index 00000000000..9bd706ad93a
--- /dev/null
+++ b/plugin/physicalServer/src/main/java/org/zstack/physicalserver/PhysicalServerCpuReconciler.java
@@ -0,0 +1,1740 @@
+package org.zstack.physicalserver;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.zstack.core.componentloader.PluginRegistry;
+import org.zstack.core.thread.ChainTask;
+import org.zstack.core.thread.SingleFlightTask;
+import org.zstack.core.thread.SyncTaskChain;
+import org.zstack.core.thread.ThreadFacade;
+import org.zstack.core.thread.ThreadFacadeImpl;
+import org.zstack.header.core.Completion;
+import org.zstack.header.core.ReturnValueCompletion;
+import org.zstack.header.errorcode.ErrorCode;
+import org.zstack.header.errorcode.OperationFailureException;
+import org.zstack.header.physicalserver.PhysicalServerCpuConsumerAdapter;
+import org.zstack.header.physicalserver.PhysicalServerCpuConsumerState;
+import org.zstack.header.physicalserver.PhysicalServerCpuSet;
+import org.zstack.header.physicalserver.PhysicalServerCpuTopology;
+import org.zstack.utils.Utils;
+import org.zstack.utils.gson.JSONObjectUtil;
+import org.zstack.utils.logging.CLogger;
+
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.zstack.core.Platform.operr;
+
+public class PhysicalServerCpuReconciler {
+ private static final String ZBS_PROVIDER_BACKEND = "ZBS_PROVIDER";
+ private static final long RECONCILE_REQUEUE_DELAY_MILLIS = 100;
+ private static final long RELEASE_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(4);
+ private static final long RELEASE_GATE_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(5);
+ private static final long RELEASE_RETRY_INTERVAL_MILLIS = TimeUnit.SECONDS.toMillis(1);
+ private static final CLogger logger = Utils.getLogger(PhysicalServerCpuReconciler.class);
+
+ private final AtomicReference