package org.ovirt.engine.core.bll.hostdeploy;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.naming.AuthenticationException;
import org.apache.commons.lang.StringUtils;
import org.ovirt.engine.core.bll.NonTransactiveCommandAttribute;
import org.ovirt.engine.core.bll.VdsCommand;
import org.ovirt.engine.core.bll.VdsHandler;
import org.ovirt.engine.core.bll.context.CommandContext;
import org.ovirt.engine.core.bll.host.provider.HostProviderProxy;
import org.ovirt.engine.core.bll.hostedengine.HostedEngineHelper;
import org.ovirt.engine.core.bll.job.ExecutionContext;
import org.ovirt.engine.core.bll.job.ExecutionHandler;
import org.ovirt.engine.core.bll.provider.ProviderProxyFactory;
import org.ovirt.engine.core.bll.utils.ClusterUtils;
import org.ovirt.engine.core.bll.utils.EngineSSHClient;
import org.ovirt.engine.core.bll.utils.PermissionSubject;
import org.ovirt.engine.core.bll.validator.HostValidator;
import org.ovirt.engine.core.common.AuditLogType;
import org.ovirt.engine.core.common.VdcObjectType;
import org.ovirt.engine.core.common.action.RemoveVdsParameters;
import org.ovirt.engine.core.common.action.VdcActionType;
import org.ovirt.engine.core.common.action.VdcReturnValueBase;
import org.ovirt.engine.core.common.action.VdsActionParameters;
import org.ovirt.engine.core.common.action.hostdeploy.AddVdsActionParameters;
import org.ovirt.engine.core.common.action.hostdeploy.InstallVdsParameters;
import org.ovirt.engine.core.common.businessentities.Provider;
import org.ovirt.engine.core.common.businessentities.VDS;
import org.ovirt.engine.core.common.businessentities.VDSStatus;
import org.ovirt.engine.core.common.businessentities.VdsDynamic;
import org.ovirt.engine.core.common.businessentities.VdsStatistics;
import org.ovirt.engine.core.common.businessentities.pm.FenceAgent;
import org.ovirt.engine.core.common.config.Config;
import org.ovirt.engine.core.common.config.ConfigValues;
import org.ovirt.engine.core.common.errors.EngineError;
import org.ovirt.engine.core.common.errors.EngineException;
import org.ovirt.engine.core.common.errors.EngineMessage;
import org.ovirt.engine.core.common.job.Step;
import org.ovirt.engine.core.common.job.StepEnum;
import org.ovirt.engine.core.common.validation.group.CreateEntity;
import org.ovirt.engine.core.common.validation.group.PowerManagementCheck;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.dal.job.ExecutionMessageDirector;
import org.ovirt.engine.core.dao.FenceAgentDao;
import org.ovirt.engine.core.dao.VdsDao;
import org.ovirt.engine.core.dao.VdsDynamicDao;
import org.ovirt.engine.core.dao.VdsStaticDao;
import org.ovirt.engine.core.dao.VdsStatisticsDao;
import org.ovirt.engine.core.dao.provider.ProviderDao;
import org.ovirt.engine.core.utils.threadpool.ThreadPoolUtil;
import org.ovirt.engine.core.utils.transaction.TransactionSupport;
import org.ovirt.engine.core.uutils.ssh.ConstraintByteArrayOutputStream;
import org.ovirt.engine.core.uutils.ssh.SSHClient;
@NonTransactiveCommandAttribute(forceCompensation = true)
public class AddVdsCommand<T extends AddVdsActionParameters> extends VdsCommand<T> {
private final AuditLogType errorType = AuditLogType.USER_FAILED_ADD_VDS;
@Inject
private HostedEngineHelper hostedEngineHelper;
@Inject
private ProviderDao providerDao;
@Inject
private VdsDao vdsDao;
@Inject
private VdsStaticDao vdsStaticDao;
@Inject
private VdsDynamicDao vdsDynamicDao;
@Inject
private VdsStatisticsDao vdsStatisticsDao;
@Inject
private FenceAgentDao fenceAgentDao;
/**
* Constructor for command creation when compensation is applied on startup
*/
public AddVdsCommand(Guid commandId) {
super(commandId);
}
public AddVdsCommand(T parameters, CommandContext cmdContext) {
super(parameters, cmdContext);
setClusterId(parameters.getvds().getClusterId());
}
@Override
protected void setActionMessageParameters() {
addValidationMessage(EngineMessage.VAR__ACTION__ADD);
addValidationMessage(EngineMessage.VAR__TYPE__HOST);
addValidationMessageVariable("server", getParameters().getvds().getHostName());
}
private Provider<?> getHostProvider() {
return providerDao.get(getParameters().getVdsStaticData().getHostProviderId());
}
@Override
protected void executeCommand() {
Guid oVirtId = getParameters().getVdsForUniqueId();
if (oVirtId != null) {
// if fails to remove deprecated entry, we might attempt to add new oVirt host with an existing unique-id.
if (!removeDeprecatedOvirtEntry(oVirtId)) {
log.error("Failed to remove duplicated oVirt entry with id '{}'. Abort adding oVirt Host type",
oVirtId);
throw new EngineException(EngineError.HOST_ALREADY_EXISTS);
}
}
TransactionSupport.executeInNewTransaction(() -> {
addVdsStaticToDb();
addVdsDynamicToDb();
addVdsStatisticsToDb();
getCompensationContext().stateChanged();
return null;
});
if (getParameters().isProvisioned()) {
HostProviderProxy proxy = ProviderProxyFactory.getInstance().create(getHostProvider());
proxy.provisionHost(
getParameters().getvds(),
getParameters().getHostGroup(),
getParameters().getComputeResource(),
getParameters().getHostMac(),
getParameters().getDiscoverName(),
getParameters().getPassword(),
getParameters().getDiscoverIp()
);
addCustomValue("HostGroupName", getParameters().getHostGroup().getName());
auditLogDirector.log(this, AuditLogType.VDS_PROVISION);
}
// set vds spm id
if (getCluster().getStoragePoolId() != null) {
VdsActionParameters tempVar = new VdsActionParameters(getVdsIdRef());
tempVar.setSessionId(getParameters().getSessionId());
tempVar.setCompensationEnabled(true);
VdcReturnValueBase addVdsSpmIdReturn =
runInternalAction(VdcActionType.AddVdsSpmId,
tempVar,
cloneContext().withoutLock().withoutExecutionContext());
if (!addVdsSpmIdReturn.getSucceeded()) {
setSucceeded(false);
getReturnValue().setFault(addVdsSpmIdReturn.getFault());
return;
}
}
TransactionSupport.executeInNewTransaction(() -> {
initializeVds(true);
alertIfPowerManagementNotConfigured(getParameters().getVdsStaticData());
testVdsPowerManagementStatus(getParameters().getVdsStaticData());
setSucceeded(true);
setActionReturnValue(getVdsIdRef());
// If the installation failed, we don't want to compensate for the failure since it will remove the
// host, but instead the host should be left in an "install failed" status.
getCompensationContext().cleanupCompensationDataAfterSuccessfulCommand();
return null;
});
// do not install vds's which added in pending mode or for provisioning (currently power
// clients). they are installed as part of the approve process or automatically after provision
if (Config.<Boolean> getValue(ConfigValues.InstallVds) &&
!getParameters().isPending() &&
!getParameters().isProvisioned()) {
final InstallVdsParameters installVdsParameters = new InstallVdsParameters(getVdsId(), getParameters().getPassword());
installVdsParameters.setAuthMethod(getParameters().getAuthMethod());
installVdsParameters.setOverrideFirewall(getParameters().getOverrideFirewall());
installVdsParameters.setActivateHost(getParameters().getActivateHost());
installVdsParameters.setNetworkProviderId(getParameters().getVdsStaticData().getOpenstackNetworkProviderId());
installVdsParameters.setNetworkMappings(getParameters().getNetworkMappings());
installVdsParameters.setEnableSerialConsole(getParameters().getEnableSerialConsole());
if (getParameters().getHostedEngineDeployConfiguration() != null) {
Map<String, String> vdsDeployParams = hostedEngineHelper.createVdsDeployParams(
getVdsId(),
getParameters().getHostedEngineDeployConfiguration().getDeployAction());
installVdsParameters.setHostedEngineConfiguration(vdsDeployParams);
}
Map<String, String> values = new HashMap<>();
values.put(VdcObjectType.VDS.name().toLowerCase(), getParameters().getvds().getName());
Step installStep = executionHandler.addSubStep(getExecutionContext(),
getExecutionContext().getJob().getStep(StepEnum.EXECUTING),
StepEnum.INSTALLING_HOST,
ExecutionMessageDirector.resolveStepMessage(StepEnum.INSTALLING_HOST, values));
final ExecutionContext installCtx = new ExecutionContext();
installCtx.setJob(getExecutionContext().getJob());
installCtx.setStep(installStep);
installCtx.setMonitored(true);
installCtx.setShouldEndJob(true);
ThreadPoolUtil.execute(() -> runInternalAction(
VdcActionType.InstallVdsInternal,
installVdsParameters,
cloneContextAndDetachFromParent()
.withExecutionContext(installCtx)));
ExecutionHandler.setAsyncJob(getExecutionContext(), true);
}
}
protected boolean isGlusterSupportEnabled() {
return getCluster() != null && getCluster().supportsGlusterService() && getParameters().isGlusterPeerProbeNeeded();
}
/**
* The scenario in which a host is already exists when adding new host after the validate is when the existed
* host type is oVirt and its status is 'Pending Approval'. In this case the old entry is removed from the DB, since
* the oVirt node was added again, where the new host properties might be updated (e.g. cluster adjustment, data
* center, host name, host address) and a new entry with updated properties is added.
*
* @param oVirtId
* the deprecated host entry to remove
*/
private boolean removeDeprecatedOvirtEntry(final Guid oVirtId) {
final VDS vds = vdsDao.get(oVirtId);
if (vds == null || !VdsHandler.isPendingOvirt(vds)) {
return false;
}
String vdsName = getParameters().getVdsStaticData().getName();
log.info("Host '{}', id '{}' of type '{}' is being re-registered as Host '{}'",
vds.getName(),
vds.getId(),
vds.getVdsType().name(),
vdsName);
VdcReturnValueBase result =
TransactionSupport.executeInNewTransaction(() -> runInternalAction(VdcActionType.RemoveVds,
new RemoveVdsParameters(oVirtId)));
if (!result.getSucceeded()) {
String errors =
result.isValid() ? result.getFault().getError().name()
: StringUtils.join(result.getValidationMessages(), ",");
log.warn("Failed to remove Host '{}', id '{}', re-registering it as Host '{}' fails with errors {}",
vds.getName(),
vds.getId(),
vdsName,
errors);
} else {
log.info("Host '{}' is now known as Host '{}'",
vds.getName(),
vdsName);
}
return result.getSucceeded();
}
@Override
public AuditLogType getAuditLogTypeValue() {
return getSucceeded() ? AuditLogType.USER_ADD_VDS : errorType;
}
private void addVdsStaticToDb() {
getParameters().getVdsStaticData().setServerSslEnabled(
Config.<Boolean> getValue(ConfigValues.EncryptHostCommunication));
vdsStaticDao.save(getParameters().getVdsStaticData());
getCompensationContext().snapshotNewEntity(getParameters().getVdsStaticData());
setVdsIdRef(getParameters().getVdsStaticData().getId());
addFenceAgents();
setVds(null);
}
private void addVdsDynamicToDb() {
VdsDynamic vdsDynamic = new VdsDynamic();
vdsDynamic.setId(getParameters().getVdsStaticData().getId());
// TODO: oVirt type - here oVirt behaves like power client?
if (getParameters().isPending()) {
vdsDynamic.setStatus(VDSStatus.PendingApproval);
}
else if (getParameters().isProvisioned()) {
vdsDynamic.setStatus(VDSStatus.InstallingOS);
}
else if (Config.<Boolean> getValue(ConfigValues.InstallVds)) {
vdsDynamic.setStatus(VDSStatus.Installing);
}
vdsDynamicDao.save(vdsDynamic);
getCompensationContext().snapshotNewEntity(vdsDynamic);
}
private void addVdsStatisticsToDb() {
VdsStatistics vdsStatistics = new VdsStatistics();
vdsStatistics.setId(getParameters().getVdsStaticData().getId());
vdsStatisticsDao.save(vdsStatistics);
getCompensationContext().snapshotNewEntity(vdsStatistics);
}
protected boolean validateCluster() {
if (getCluster() == null) {
return failValidation(EngineMessage.VDS_CLUSTER_IS_NOT_VALID);
}
return true;
}
@Override
protected boolean validate() {
T params = getParameters();
setClusterId(params.getVdsStaticData().getClusterId());
params.setVdsForUniqueId(null);
// Check if this is a valid cluster
boolean returnValue = validateCluster();
if (returnValue) {
HostValidator validator = getHostValidator();
returnValue = validate(validator.nameNotEmpty())
&& validate(validator.nameLengthIsLegal())
&& validate(validator.hostNameIsValid())
&& validate(validator.nameNotUsed())
&& validate(validator.hostNameNotUsed())
&& validate(validator.portIsValid())
&& validate(validator.sshUserNameNotEmpty())
&& validate(validator.validateSingleHostAttachedToLocalStorage())
&& validate(validator.securityKeysExists())
&& validate(validator.provisioningComputeResourceValid(params.isProvisioned(),
params.getComputeResource()))
&& validate(validator.provisioningHostGroupValid(params.isProvisioned(),
params.getHostGroup()))
&& validate(validator.passwordNotEmpty(params.isPending(),
params.getAuthMethod(),
params.getPassword()))
&& validate(validator.supportsDeployingHostedEngine(params.getHostedEngineDeployConfiguration()));
}
if (!(returnValue
&& isPowerManagementLegal(params.getVdsStaticData().isPmEnabled(),
params.getFenceAgents(),
getCluster().getCompatibilityVersion().toString())
&& canConnect(params.getvds()))) {
return false;
}
if (params.getVdsStaticData().getOpenstackNetworkProviderId() != null
&& !validateNetworkProviderProperties(params.getVdsStaticData().getOpenstackNetworkProviderId(),
params.getNetworkMappings())) {
return false;
}
if (isGlusterSupportEnabled() && clusterHasNonInitializingServers()) {
// allow simultaneous installation of hosts, but if a host has completed install, only
// allow addition of another host if it can be peer probed to cluster.
VDS upServer = glusterUtil.getUpServer(getClusterId());
if (upServer == null) {
return failValidation(EngineMessage.ACTION_TYPE_FAILED_NO_GLUSTER_HOST_TO_PEER_PROBE);
}
}
return true;
}
protected HostValidator getHostValidator() {
return HostValidator.createInstance(getParameters().getvds());
}
private boolean clusterHasServers() {
return getClusterUtils().hasServers(getClusterId());
}
private boolean clusterHasNonInitializingServers() {
for (VDS vds : vdsDao.getAllForCluster(getClusterId())) {
if (vds.getStatus() != VDSStatus.Installing &&
vds.getStatus() != VDSStatus.InstallingOS &&
vds.getStatus() != VDSStatus.PendingApproval &&
vds.getStatus() != VDSStatus.Initializing &&
vds.getStatus() != VDSStatus.InstallFailed) {
return true;
}
}
return false;
}
protected ClusterUtils getClusterUtils() {
return ClusterUtils.getInstance();
}
public EngineSSHClient getSSHClient() throws Exception {
Long timeout =
TimeUnit.SECONDS.toMillis(Config.<Integer> getValue(ConfigValues.ConnectToServerTimeoutInSeconds));
EngineSSHClient sshclient = new EngineSSHClient();
sshclient.setVds(getParameters().getvds());
sshclient.setHardTimeout(timeout);
sshclient.setSoftTimeout(timeout);
sshclient.setPassword(getParameters().getPassword());
switch (getParameters().getAuthMethod()) {
case PublicKey:
sshclient.useDefaultKeyPair();
break;
case Password:
sshclient.setPassword(getParameters().getPassword());
break;
default:
throw new Exception("Invalid authentication method value was sent to AddVdsCommand");
}
return sshclient;
}
/**
* getInstalledVdsIdIfExists
*
* Communicate with host by SSH session and gather vdsm-id if exist
*
* @param client - already connected ssh client
*/
private String getInstalledVdsIdIfExists(SSHClient client) {
try {
ByteArrayOutputStream out = new ConstraintByteArrayOutputStream(256);
client.executeCommand(Config.getValue(ConfigValues.GetVdsmIdByVdsmToolCommand), null, out, null);
return new String(out.toByteArray(), StandardCharsets.UTF_8);
}
catch (Exception e) {
log.warn(
"Failed to initiate vdsm-id request on host: {}",
e.getMessage()
);
log.debug("Exception", e);
return null;
}
}
protected boolean canConnect(VDS vds) {
// execute the connectivity and id uniqueness validation for VDS type hosts
if (
!getParameters().isPending() && !getParameters().isProvisioned() &&
Config.<Boolean> getValue(ConfigValues.InstallVds)
) {
try (final EngineSSHClient sshclient = getSSHClient()) {
sshclient.connect();
sshclient.authenticate();
String hostUUID = getInstalledVdsIdIfExists(sshclient);
if (hostUUID != null && vdsDao.getAllWithUniqueId(hostUUID).size() != 0) {
return failValidation(EngineMessage.ACTION_TYPE_FAILED_VDS_WITH_SAME_UUID_EXIST);
}
return isValidGlusterPeer(sshclient, vds.getClusterId());
} catch (AuthenticationException e) {
log.error(
"Failed to authenticate session with host '{}': {}",
vds.getName(),
e.getMessage());
log.debug("Exception", e);
return failValidation(EngineMessage.VDS_CANNOT_AUTHENTICATE_TO_SERVER);
} catch (SecurityException e) {
log.error(
"Failed to connect to host '{}', fingerprint '{}': {}",
vds.getName(),
vds.getSshKeyFingerprint(),
e.getMessage());
log.debug("Exception", e);
addValidationMessage(EngineMessage.VDS_SECURITY_CONNECTION_ERROR);
addValidationMessageVariable("ErrorMessage", e.getMessage());
return failValidation(EngineMessage.VDS_CANNOT_AUTHENTICATE_TO_SERVER);
} catch (Exception e) {
log.error(
"Failed to establish session with host '{}': {}",
vds.getName(),
e.getMessage());
log.debug("Exception", e);
return failValidation(EngineMessage.VDS_CANNOT_CONNECT_TO_SERVER);
}
}
return true;
}
/**
* Checks if the server can be a valid gluster peer. Fails if it is already part of another cluster (when current
* cluster is not empty). This is done by executing the 'gluster peer status' command on the server.<p>
* In case glusterd is down or not installed on the server (which is a possibility on a new server), the command can
* fail. In such cases, we just log it as a debug message and return true.<p>
* Another interesting case is where one of the peers of the server is already present as part of this cluster in
* the engine DB. This means that one ore more hosts were probably added to the gluster cluster using gluster CLI,
* and hence this server should be allowed to be added.
*
* @param sshclient
* SSH client that can be used to execute 'gluster peer status' command on the server
* @param clusterId
* ID of the cluster to which the server is being added.
* @return true if the server is good to be added to a gluster cluster, else false.
*/
private boolean isValidGlusterPeer(SSHClient sshclient, Guid clusterId) {
if (isGlusterSupportEnabled() && clusterHasServers()) {
try {
// Must not allow adding a server that already is part of another gluster cluster
Set<String> peers = glusterUtil.getPeers(sshclient);
if (peers.size() > 0) {
for(String peer : peers) {
if(glusterDBUtils.serverExists(clusterId, peer)) {
// peer present in cluster. so server being added is valid.
return true;
}
}
// none of the peers present in the cluster. fail with appropriate error.
return failValidation(EngineMessage.SERVER_ALREADY_PART_OF_ANOTHER_CLUSTER);
}
} catch (Exception e) {
// This can happen if glusterd is not running on the server. Ignore it and let the server get added.
// Peer probe will anyway fail later and the server will then go to non-operational status.
log.debug("Could not check if server '{}' is already part of another gluster cluster. Will"
+ " allow adding it.",
sshclient.getHost());
log.debug("Exception", e);
}
}
return true;
}
@Override
public List<PermissionSubject> getPermissionCheckSubjects() {
return Collections.singletonList(new PermissionSubject(getClusterId(), VdcObjectType.Cluster,
getActionType().getActionGroup()));
}
@Override
protected List<Class<?>> getValidationGroups() {
addValidationGroup(CreateEntity.class);
if (getParameters().getVdsStaticData().isPmEnabled()) {
addValidationGroup(PowerManagementCheck.class);
}
return super.getValidationGroups();
}
@Override
public Map<String, String> getJobMessageProperties() {
if (jobProperties == null) {
jobProperties = super.getJobMessageProperties();
VDS vds = getParameters().getvds();
String vdsName = (vds != null && vds.getName() != null) ? vds.getName() : "";
jobProperties.put(VdcObjectType.VDS.name().toLowerCase(), vdsName);
}
return jobProperties;
}
private void addFenceAgents() {
if (getParameters().getFenceAgents() != null) { // if == null, means no update. Empty list means
for (FenceAgent agent : getParameters().getFenceAgents()) {
agent.setHostId(getVdsId());
fenceAgentDao.save(agent);
}
}
}
}