Merge branch '4.22'

This commit is contained in:
Suresh Kumar Anaparti 2026-02-19 13:20:04 +05:30
commit 30e6c224bd
No known key found for this signature in database
GPG Key ID: D7CEAE3A9E71D0AA
19 changed files with 128 additions and 41 deletions

View File

@ -59,6 +59,9 @@ public interface Host extends StateObject<Status>, Identity, Partition, HAResour
String HOST_INSTANCE_CONVERSION = "host.instance.conversion";
String HOST_OVFTOOL_VERSION = "host.ovftool.version";
String HOST_VIRTV2V_VERSION = "host.virtv2v.version";
String HOST_SSH_PORT = "host.ssh.port";
int DEFAULT_SSH_PORT = 22;
/**
* @return name of the machine.

View File

@ -60,7 +60,8 @@ public class AddHostCmd extends BaseCmd {
@Parameter(name = ApiConstants.POD_ID, type = CommandType.UUID, entityType = PodResponse.class, required = true, description = "The Pod ID for the host")
private Long podId;
@Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL")
@Parameter(name = ApiConstants.URL, type = CommandType.STRING, required = true, description = "The host URL, optionally add ssh port (format: 'host:port') for KVM hosts," +
" otherwise falls back to the port defined at the config 'kvm.host.discovery.ssh.port'")
private String url;
@Parameter(name = ApiConstants.ZONE_ID, type = CommandType.UUID, entityType = ZoneResponse.class, required = true, description = "The Zone ID for the host")

View File

@ -54,6 +54,10 @@ public interface AgentManager {
"This timeout overrides the wait global config. This holds a comma separated key value pairs containing timeout (in seconds) for specific commands. " +
"For example: DhcpEntryCommand=600, SavePasswordCommand=300, VmDataCommand=300", false);
ConfigKey<Integer> KVMHostDiscoverySshPort = new ConfigKey<>(ConfigKey.CATEGORY_ADVANCED, Integer.class,
"kvm.host.discovery.ssh.port", String.valueOf(Host.DEFAULT_SSH_PORT), "SSH port used for KVM host discovery and any other operations on host (using SSH)." +
" Please note that this is applicable when port is not defined through host url while adding the KVM host.", true, ConfigKey.Scope.Cluster);
enum TapAgentsAction {
Add, Del, Contains,
}
@ -172,4 +176,6 @@ public interface AgentManager {
void propagateChangeToAgents(Map<String, String> params);
boolean transferDirectAgentsFromMS(String fromMsUuid, long fromMsId, long timeoutDurationInMs, boolean excludeHostsInMaintenance);
int getHostSshPort(HostVO host);
}

View File

@ -42,6 +42,7 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
import javax.naming.ConfigurationException;
import com.cloud.utils.StringUtils;
import org.apache.cloudstack.agent.lb.IndirectAgentLB;
import org.apache.cloudstack.ca.CAManager;
import org.apache.cloudstack.command.ReconcileCommandService;
@ -64,7 +65,6 @@ import org.apache.cloudstack.utils.reflectiontostringbuilderutils.ReflectionToSt
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.ThreadContext;
import com.cloud.agent.AgentManager;
@ -2111,7 +2111,7 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
return new ConfigKey<?>[] { CheckTxnBeforeSending, Workers, Port, Wait, AlertWait, DirectAgentLoadSize,
DirectAgentPoolSize, DirectAgentThreadCap, EnableKVMAutoEnableDisable, ReadyCommandWait,
GranularWaitTimeForCommands, RemoteAgentSslHandshakeTimeout, RemoteAgentMaxConcurrentNewConnections,
RemoteAgentNewConnectionsMonitorInterval };
RemoteAgentNewConnectionsMonitorInterval, KVMHostDiscoverySshPort };
}
protected class SetHostParamsListener implements Listener {
@ -2234,6 +2234,25 @@ public class AgentManagerImpl extends ManagerBase implements AgentManager, Handl
return true;
}
@Override
public int getHostSshPort(HostVO host) {
if (host == null) {
return KVMHostDiscoverySshPort.value();
}
if (host.getHypervisorType() != HypervisorType.KVM) {
return Host.DEFAULT_SSH_PORT;
}
_hostDao.loadDetails(host);
String hostPort = host.getDetail(Host.HOST_SSH_PORT);
if (StringUtils.isBlank(hostPort)) {
return KVMHostDiscoverySshPort.valueIn(host.getClusterId());
}
return Integer.parseInt(hostPort);
}
private GlobalLock getHostJoinLock(Long hostId) {
return GlobalLock.getInternLock(String.format("%s-%s", "Host-Join", hostId));
}

View File

@ -22,6 +22,7 @@ import com.cloud.agent.api.ReadyCommand;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.StartupRoutingCommand;
import com.cloud.exception.ConnectionException;
import com.cloud.host.Host;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
import com.cloud.host.dao.HostDao;
@ -104,4 +105,36 @@ public class AgentManagerImplTest {
Assert.assertEquals(50, result);
}
@Test
public void testGetHostSshPortWithHostNull() {
int hostSshPort = mgr.getHostSshPort(null);
Assert.assertEquals(22, hostSshPort);
}
@Test
public void testGetHostSshPortWithNonKVMHost() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.XenServer);
int hostSshPort = mgr.getHostSshPort(host);
Assert.assertEquals(22, hostSshPort);
}
@Test
public void testGetHostSshPortWithKVMHostDefaultPort() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(host.getClusterId()).thenReturn(1L);
int hostSshPort = mgr.getHostSshPort(host);
Assert.assertEquals(22, hostSshPort);
}
@Test
public void testGetHostSshPortWithKVMHostCustomPort() {
HostVO host = Mockito.mock(HostVO.class);
Mockito.when(host.getHypervisorType()).thenReturn(Hypervisor.HypervisorType.KVM);
Mockito.when(host.getDetail(Host.HOST_SSH_PORT)).thenReturn(String.valueOf(3922));
int hostSshPort = mgr.getHostSshPort(host);
Assert.assertEquals(3922, hostSshPort);
}
}

View File

@ -383,7 +383,7 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
public VolumeDaoImpl() {
AllFieldsSearch = createSearchBuilder();
AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.EQ);
AllFieldsSearch.and("state", AllFieldsSearch.entity().getState(), Op.IN);
AllFieldsSearch.and("accountId", AllFieldsSearch.entity().getAccountId(), Op.EQ);
AllFieldsSearch.and("dcId", AllFieldsSearch.entity().getDataCenterId(), Op.EQ);
AllFieldsSearch.and("pod", AllFieldsSearch.entity().getPodId(), Op.EQ);
@ -581,17 +581,16 @@ public class VolumeDaoImpl extends GenericDaoBase<VolumeVO, Long> implements Vol
@Override
public List<VolumeVO> listVolumesToBeDestroyed() {
SearchCriteria<VolumeVO> sc = AllFieldsSearch.create();
sc.setParameters("state", Volume.State.Destroy);
return listBy(sc);
return listVolumesToBeDestroyed(null);
}
@Override
public List<VolumeVO> listVolumesToBeDestroyed(Date date) {
SearchCriteria<VolumeVO> sc = AllFieldsSearch.create();
sc.setParameters("state", Volume.State.Destroy);
sc.setParameters("updateTime", date);
sc.setParameters("state", Volume.State.Destroy, Volume.State.Expunging);
if (date != null) {
sc.setParameters("updateTime", date);
}
return listBy(sc);
}

View File

@ -436,6 +436,9 @@ public class VolumeServiceImpl implements VolumeService {
// no need to change state in volumes table
volume.processEventOnly(Event.DestroyRequested);
} else if (volume.getDataStore().getRole() == DataStoreRole.Primary) {
if (vol.getState() == Volume.State.Expunging) {
logger.info("Volume {} is already in Expunging, retrying", volume);
}
volume.processEvent(Event.ExpungeRequested);
}

View File

@ -16,6 +16,7 @@
// under the License.
package org.apache.cloudstack.backup;
import com.cloud.agent.AgentManager;
import com.cloud.dc.dao.ClusterDao;
import com.cloud.host.HostVO;
import com.cloud.host.Status;
@ -134,6 +135,9 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
@Inject
private DiskOfferingDao diskOfferingDao;
@Inject
private AgentManager agentMgr;
private static String getUrlDomain(String url) throws URISyntaxException {
URI uri;
try {
@ -251,8 +255,13 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
String nstRegex = "\\bcompleted savetime=([0-9]{10})";
Pattern saveTimePattern = Pattern.compile(nstRegex);
if (host == null) {
LOG.warn("Unable to take backup, host is null");
return null;
}
try {
Pair<Boolean, String> response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22,
Pair<Boolean, String> response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host),
username, null, password, command, 120000, 120000, 3600000);
if (!response.first()) {
LOG.error("Backup Script failed on HYPERVISOR {} due to: {}", host, response.second());
@ -271,9 +280,13 @@ public class NetworkerBackupProvider extends AdapterBase implements BackupProvid
return null;
}
private boolean executeRestoreCommand(HostVO host, String username, String password, String command) {
if (host == null) {
LOG.warn("Unable to restore backup, host is null");
return false;
}
try {
Pair<Boolean, String> response = SshHelper.sshExecute(host.getPrivateIpAddress(), 22,
Pair<Boolean, String> response = SshHelper.sshExecute(host.getPrivateIpAddress(), agentMgr.getHostSshPort(host),
username, null, password, command, 120000, 120000, 3600000);
if (!response.first()) {

View File

@ -493,7 +493,7 @@ public class RabbitMQEventBus extends ManagerBase implements EventBus {
@Override
public synchronized boolean stop() {
if (s_connection.isOpen()) {
if (s_connection != null && s_connection.isOpen()) {
for (String subscriberId : s_subscribers.keySet()) {
Ternary<String, Channel, EventSubscriber> subscriberDetails = s_subscribers.get(subscriberId);
Channel channel = subscriberDetails.second();

View File

@ -184,12 +184,8 @@ public class OauthLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
protected Long getDomainIdFromParams(Map<String, Object[]> params, StringBuilder auditTrailSb, String responseType) {
String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID);
if (domainIdArr == null) {
domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID);
}
Long domainId = null;
if ((domainIdArr != null) && (domainIdArr.length > 0)) {
if (domainIdArr != null && domainIdArr.length > 0) {
try {
//check if UUID is passed in for domain
domainId = _apiServer.fetchDomainId(domainIdArr[0]);

View File

@ -158,11 +158,17 @@ public class SAML2LoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthent
String domainPath = null;
if (params.containsKey(ApiConstants.IDP_ID)) {
idpId = ((String[])params.get(ApiConstants.IDP_ID))[0];
String[] idpIds = (String[])params.get(ApiConstants.IDP_ID);
if (idpIds != null && idpIds.length > 0) {
idpId = idpIds[0];
}
}
if (params.containsKey(ApiConstants.DOMAIN)) {
domainPath = ((String[])params.get(ApiConstants.DOMAIN))[0];
String[] domainPaths = (String[])params.get(ApiConstants.DOMAIN);
if (domainPaths != null && domainPaths.length > 0) {
domainPath = domainPaths[0];
}
}
if (domainPath != null && !domainPath.isEmpty()) {

View File

@ -38,6 +38,7 @@ import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.cloudstack.api.APICommand;
import com.cloud.api.auth.DefaultForgotPasswordAPIAuthenticatorCmd;
import org.apache.cloudstack.api.ApiConstants;
import org.apache.cloudstack.api.ApiErrorCode;
import org.apache.cloudstack.api.ApiServerService;
@ -200,7 +201,6 @@ public class ApiServlet extends HttpServlet {
LOGGER.warn(message);
}
});
}
void processRequestInContext(final HttpServletRequest req, final HttpServletResponse resp) {
@ -273,7 +273,6 @@ public class ApiServlet extends HttpServlet {
}
if (command != null && !command.equals(ValidateUserTwoFactorAuthenticationCodeCmd.APINAME)) {
APIAuthenticator apiAuthenticator = authManager.getAPIAuthenticator(command);
if (apiAuthenticator != null) {
auditTrailSb.append("command=");
@ -309,7 +308,9 @@ public class ApiServlet extends HttpServlet {
} catch (ServerApiException e) {
httpResponseCode = e.getErrorCode().getHttpCode();
responseString = e.getMessage();
LOGGER.debug("Authentication failure: " + e.getMessage());
if (!DefaultForgotPasswordAPIAuthenticatorCmd.APINAME.equalsIgnoreCase(command) || StringUtils.isNotBlank(username)) {
LOGGER.debug("Authentication failure: {}", e.getMessage());
}
}
if (apiAuthenticator.getAPIType() == APIAuthenticationType.LOGOUT_API) {
@ -390,7 +391,7 @@ public class ApiServlet extends HttpServlet {
}
}
if (! requestChecksoutAsSane(resp, auditTrailSb, responseType, params, session, command, userId, account, accountObj))
if (!requestChecksoutAsSane(resp, auditTrailSb, responseType, params, session, command, userId, account, accountObj))
return;
} else {
CallContext.register(accountMgr.getSystemUser(), accountMgr.getSystemAccount());
@ -422,7 +423,6 @@ public class ApiServlet extends HttpServlet {
apiServer.getSerializedApiError(HttpServletResponse.SC_UNAUTHORIZED, "unable to verify user credentials and/or request signature", params,
responseType);
HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_UNAUTHORIZED, responseType, ApiServer.JSONcontentType.value());
}
} catch (final ServerApiException se) {
final String serializedResponseText = apiServer.getSerializedApiError(se, params, responseType);
@ -653,6 +653,9 @@ public class ApiServlet extends HttpServlet {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace(msg);
}
if (session == null) {
return;
}
session.invalidate();
} catch (final IllegalStateException ise) {
if (LOGGER.isTraceEnabled()) {

View File

@ -44,13 +44,13 @@ import java.net.InetAddress;
import java.util.List;
import java.util.Map;
@APICommand(name = "forgotPassword",
@APICommand(name = DefaultForgotPasswordAPIAuthenticatorCmd.APINAME,
description = "Sends an email to the user with a token to reset the password using resetPassword command.",
since = "4.20.0.0",
requestHasSensitiveInfo = true,
responseObject = SuccessResponse.class)
public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
public static final String APINAME = "forgotPassword";
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
@ -108,10 +108,12 @@ public class DefaultForgotPasswordAPIAuthenticatorCmd extends BaseCmd implements
if (userDomain != null) {
domainId = userDomain.getId();
} else {
logger.debug("Unable to find the domain from the path {}", domain);
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, String.format("Unable to find the domain from the path %s", domain));
}
final UserAccount userAccount = _accountService.getActiveUserAccount(username[0], domainId);
if (userAccount != null && List.of(User.Source.SAML2, User.Source.OAUTH2, User.Source.LDAP).contains(userAccount.getSource())) {
logger.debug("Forgot Password is not allowed for the user {} from source {}", username[0], userAccount.getSource());
throw new ServerApiException(ApiErrorCode.PARAM_ERROR, "Forgot Password is not allowed for this user");
}
boolean success = _apiServer.forgotPassword(userAccount, userDomain);

View File

@ -49,7 +49,6 @@ import java.net.InetAddress;
@APICommand(name = "login", description = "Logs a user into the CloudStack. A successful login attempt will generate a JSESSIONID cookie value that can be passed in subsequent Query command calls until the \"logout\" command has been issued or the session has expired.", requestHasSensitiveInfo = true, responseObject = LoginCmdResponse.class, entityType = {})
public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////
@ -112,17 +111,13 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
if (HTTPMethod.valueOf(req.getMethod()) != HTTPMethod.POST) {
throw new ServerApiException(ApiErrorCode.METHOD_NOT_ALLOWED, "Please use HTTP POST to authenticate using this API");
}
// FIXME: ported from ApiServlet, refactor and cleanup
final String[] username = (String[])params.get(ApiConstants.USERNAME);
final String[] password = (String[])params.get(ApiConstants.PASSWORD);
String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID);
if (domainIdArr == null) {
domainIdArr = (String[])params.get(ApiConstants.DOMAIN__ID);
}
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
final String[] domainIdArr = (String[])params.get(ApiConstants.DOMAIN_ID);
Long domainId = null;
if ((domainIdArr != null) && (domainIdArr.length > 0)) {
if (domainIdArr != null && domainIdArr.length > 0) {
try {
//check if UUID is passed in for domain
domainId = _apiServer.fetchDomainId(domainIdArr[0]);
@ -140,6 +135,7 @@ public class DefaultLoginAPIAuthenticatorCmd extends BaseCmd implements APIAuthe
}
String domain = null;
final String[] domainName = (String[])params.get(ApiConstants.DOMAIN);
domain = getDomainName(auditTrailSb, domainName, domain);
String serializedResponse = null;

View File

@ -53,7 +53,6 @@ import java.util.Map;
responseObject = SuccessResponse.class)
public class DefaultResetPasswordAPIAuthenticatorCmd extends BaseCmd implements APIAuthenticator {
/////////////////////////////////////////////////////
//////////////// API parameters /////////////////////
/////////////////////////////////////////////////////

View File

@ -272,7 +272,12 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements
}
}
sshConnection = new Connection(agentIp, 22);
int port = uri.getPort();
if (port <= 0) {
port = AgentManager.KVMHostDiscoverySshPort.valueIn(clusterId);
}
sshConnection = new Connection(agentIp, port);
sshConnection.connect(null, 60000, 60000);
@ -380,6 +385,9 @@ public abstract class LibvirtServerDiscoverer extends DiscovererBase implements
Map<String, String> hostDetails = connectedHost.getDetails();
hostDetails.put("password", password);
hostDetails.put("username", username);
if (uri.getPort() > 0) {
hostDetails.put(Host.HOST_SSH_PORT, String.valueOf(uri.getPort()));
}
_hostDao.saveDetails(connectedHost);
return resources;
} catch (DiscoveredWithErrorException e) {

View File

@ -851,7 +851,6 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager,
_clusterDetailsDao.persist(cluster_cpu_detail);
_clusterDetailsDao.persist(cluster_memory_detail);
}
}
uri = validatedHostUrl(url, hypervisorType);
@ -945,7 +944,6 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager,
hosts.add(host);
}
discoverer.postDiscovery(hosts, _nodeId);
}
logger.info("server resources successfully discovered by " + discoverer.getName());
return hosts;
@ -3873,7 +3871,7 @@ public class ResourceManagerImpl extends ManagerBase implements ResourceManager,
*/
protected void connectAndRestartAgentOnHost(HostVO host, String username, String password, String privateKey) {
final com.trilead.ssh2.Connection connection = SSHCmdHelper.acquireAuthorizedConnection(
host.getPrivateIpAddress(), 22, username, password, privateKey);
host.getPrivateIpAddress(), _agentMgr.getHostSshPort(host), username, password, privateKey);
if (connection == null) {
throw new CloudRuntimeException(String.format("SSH to agent is enabled, but failed to connect to %s via IP address [%s].", host, host.getPrivateIpAddress()));
}

View File

@ -387,12 +387,14 @@ public class ResourceManagerImplTest {
@Test
public void testConnectAndRestartAgentOnHost() {
when(agentManager.getHostSshPort(any())).thenReturn(22);
resourceManager.connectAndRestartAgentOnHost(host, hostUsername, hostPassword, hostPrivateKey);
}
@Test
public void testHandleAgentSSHEnabledNotConnectedAgent() {
when(host.getStatus()).thenReturn(Status.Disconnected);
when(agentManager.getHostSshPort(any())).thenReturn(22);
resourceManager.handleAgentIfNotConnected(host, false);
verify(resourceManager).getHostCredentials(eq(host));
verify(resourceManager).connectAndRestartAgentOnHost(eq(host), eq(hostUsername), eq(hostPassword), eq(hostPrivateKey));

View File

@ -77,7 +77,7 @@ public class SSHCmdHelper {
}
public static com.trilead.ssh2.Connection acquireAuthorizedConnection(String ip, int port, String username, String password) {
return acquireAuthorizedConnection(ip, 22, username, password, null);
return acquireAuthorizedConnection(ip, port, username, password, null);
}
public static boolean acquireAuthorizedConnectionWithPublicKey(final com.trilead.ssh2.Connection sshConnection, final String username, final String privateKey) {