// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package com.cloud.storage.upload;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.cloudstack.api.command.user.iso.ExtractIsoCmd;
import org.apache.cloudstack.api.command.user.volume.ExtractVolumeCmd;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import com.cloud.agent.Listener;
import com.cloud.agent.api.AgentControlAnswer;
import com.cloud.agent.api.AgentControlCommand;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.StartupStorageCommand;
import com.cloud.agent.api.storage.UploadAnswer;
import com.cloud.agent.api.storage.UploadCommand;
import com.cloud.agent.api.storage.UploadProgressCommand;
import com.cloud.agent.api.storage.UploadProgressCommand.RequestType;
import org.apache.cloudstack.api.command.user.template.ExtractTemplateCmd;
import org.apache.cloudstack.api.response.ExtractResponse;
import com.cloud.api.ApiDBUtils;
import com.cloud.async.AsyncJobManager;
import com.cloud.async.AsyncJobResult;
import com.cloud.exception.AgentUnavailableException;
import com.cloud.host.HostVO;
import com.cloud.storage.Storage;
import com.cloud.storage.Upload.Status;
import com.cloud.storage.Upload.Type;
import com.cloud.storage.UploadVO;
import com.cloud.storage.dao.UploadDao;
import com.cloud.storage.upload.UploadState.UploadEvent;
import com.cloud.utils.exception.CloudRuntimeException;
public class UploadListener implements Listener {
private static final class StatusTask extends TimerTask {
private final UploadListener ul;
private final RequestType reqType;
public StatusTask( UploadListener ul, RequestType req) {
this.reqType = req;
this.ul = ul;
}
@Override
public void run() {
ul.sendCommand(reqType);
}
}
private static final class TimeoutTask extends TimerTask {
private final UploadListener ul;
public TimeoutTask( UploadListener ul) {
this.ul = ul;
}
@Override
public void run() {
ul.checkProgress();
}
}
public static final Logger s_logger = Logger.getLogger(UploadListener.class.getName());
public static final int SMALL_DELAY = 100;
public static final long STATUS_POLL_INTERVAL = 10000L;
public static final String UPLOADED=Status.UPLOADED.toString();
public static final String NOT_UPLOADED=Status.NOT_UPLOADED.toString();
public static final String UPLOAD_ERROR=Status.UPLOAD_ERROR.toString();
public static final String UPLOAD_IN_PROGRESS=Status.UPLOAD_IN_PROGRESS.toString();
public static final String UPLOAD_ABANDONED=Status.ABANDONED.toString();
public static final Map<String,String> responseNameMap;
static{
Map<String, String>tempMap = new HashMap<String, String>();
tempMap.put(Type.ISO.toString(), ExtractIsoCmd.getStaticName());
tempMap.put(Type.TEMPLATE.toString(), ExtractTemplateCmd.getStaticName());
tempMap.put(Type.VOLUME.toString(), ExtractVolumeCmd.getStaticName());
tempMap.put("DEFAULT","extractresponse");
responseNameMap = Collections.unmodifiableMap(tempMap);
}
private HostVO sserver;
private boolean uploadActive = true;
private UploadDao uploadDao;
private final UploadMonitorImpl uploadMonitor;
private UploadState currState;
private UploadCommand cmd;
private Timer timer;
private StatusTask statusTask;
private TimeoutTask timeoutTask;
private Date lastUpdated = new Date();
private String jobId;
private Long accountId;
private String typeName;
private Type type;
private long asyncJobId;
private long eventId;
private AsyncJobManager asyncMgr;
private ExtractResponse resultObj;
public AsyncJobManager getAsyncMgr() {
return asyncMgr;
}
public void setAsyncMgr(AsyncJobManager asyncMgr) {
this.asyncMgr = asyncMgr;
}
public long getAsyncJobId() {
return asyncJobId;
}
public void setAsyncJobId(long asyncJobId) {
this.asyncJobId = asyncJobId;
}
public long getEventId() {
return eventId;
}
public void setEventId(long eventId) {
this.eventId = eventId;
}
private final Map<String, UploadState> stateMap = new HashMap<String, UploadState>();
private Long uploadId;
public UploadListener(HostVO host, Timer _timer, UploadDao uploadDao,
UploadVO uploadObj, UploadMonitorImpl uploadMonitor, UploadCommand cmd,
Long accountId, String typeName, Type type, long eventId, long asyncJobId, AsyncJobManager asyncMgr) {
this.sserver = host;
this.uploadDao = uploadDao;
this.uploadMonitor = uploadMonitor;
this.cmd = cmd;
this.uploadId = uploadObj.getId();
this.accountId = accountId;
this.typeName = typeName;
this.type = type;
initStateMachine();
this.currState = getState(Status.NOT_UPLOADED.toString());
this.timer = _timer;
this.timeoutTask = new TimeoutTask(this);
this.timer.schedule(timeoutTask, 3*STATUS_POLL_INTERVAL);
this.eventId = eventId;
this.asyncJobId = asyncJobId;
this.asyncMgr = asyncMgr;
String extractId = null;
if ( type == Type.VOLUME ){
extractId = ApiDBUtils.findVolumeById(uploadObj.getTypeId()).getUuid();
}
else{
extractId = ApiDBUtils.findTemplateById(uploadObj.getTypeId()).getUuid();
}
this.resultObj = new ExtractResponse(extractId, typeName, ApiDBUtils.findAccountById(accountId).getUuid(), Status.NOT_UPLOADED.toString(),
ApiDBUtils.findUploadById(uploadId).getUuid());
resultObj.setResponseName(responseNameMap.get(type.toString()));
updateDatabase(Status.NOT_UPLOADED, cmd.getUrl(),"");
}
public UploadListener(UploadMonitorImpl monitor) {
uploadMonitor = monitor;
}
public void checkProgress() {
transition(UploadEvent.TIMEOUT_CHECK, null);
}
@Override
public int getTimeout() {
return -1;
}
@Override
public boolean isRecurring() {
return false;
}
public void setCommand(UploadCommand _cmd) {
this.cmd = _cmd;
}
public void setJobId(String _jobId) {
this.jobId = _jobId;
}
public String getJobId() {
return jobId;
}
@Override
public boolean processAnswers(long agentId, long seq, Answer[] answers) {
boolean processed = false;
if(answers != null & answers.length > 0) {
if(answers[0] instanceof UploadAnswer) {
final UploadAnswer answer = (UploadAnswer)answers[0];
if (getJobId() == null) {
setJobId(answer.getJobId());
} else if (!getJobId().equalsIgnoreCase(answer.getJobId())){
return false;//TODO
}
transition(UploadEvent.UPLOAD_ANSWER, answer);
processed = true;
}
}
return processed;
}
@Override
public boolean processCommands(long agentId, long seq, Command[] commands) {
return false;
}
@Override
public void processConnect(HostVO agent, StartupCommand cmd, boolean forRebalance) {
if (!(cmd instanceof StartupStorageCommand)) {
return;
}
long agentId = agent.getId();
StartupStorageCommand storage = (StartupStorageCommand)cmd;
if (storage.getResourceType() == Storage.StorageResourceType.STORAGE_HOST ||
storage.getResourceType() == Storage.StorageResourceType.SECONDARY_STORAGE )
{
uploadMonitor.handleUploadSync(agentId);
}
}
@Override
public AgentControlAnswer processControlCommand(long agentId,
AgentControlCommand cmd) {
return null;
}
public void setUploadInactive(Status reason) {
uploadActive=false;
uploadMonitor.handleUploadEvent(sserver, accountId, typeName, type, uploadId, reason, eventId);
}
public void logUploadStart() {
//uploadMonitor.logEvent(accountId, event, "Storage server " + sserver.getName() + " started upload of " +type.toString() + " " + typeName, EventVO.LEVEL_INFO, eventId);
}
public void cancelTimeoutTask() {
if (timeoutTask != null) timeoutTask.cancel();
}
public void cancelStatusTask() {
if (statusTask != null) statusTask.cancel();
}
@Override
public boolean processDisconnect(long agentId, com.cloud.host.Status state) {
setDisconnected();
return true;
}
@Override
public boolean processTimeout(long agentId, long seq) {
return true;
}
private void initStateMachine() {
stateMap.put(Status.NOT_UPLOADED.toString(), new NotUploadedState(this));
stateMap.put(Status.UPLOADED.toString(), new UploadCompleteState(this));
stateMap.put(Status.UPLOAD_ERROR.toString(), new UploadErrorState(this));
stateMap.put(Status.UPLOAD_IN_PROGRESS.toString(), new UploadInProgressState(this));
stateMap.put(Status.ABANDONED.toString(), new UploadAbandonedState(this));
}
private UploadState getState(String stateName) {
return stateMap.get(stateName);
}
private synchronized void transition(UploadEvent event, Object evtObj) {
if (currState == null) {
return;
}
String prevName = currState.getName();
String nextState = currState.handleEvent(event, evtObj);
if (nextState != null) {
currState = getState(nextState);
if (currState != null) {
currState.onEntry(prevName, event, evtObj);
} else {
throw new CloudRuntimeException("Invalid next state: currState="+prevName+", evt="+event + ", next=" + nextState);
}
} else {
throw new CloudRuntimeException("Unhandled event transition: currState="+prevName+", evt="+event);
}
}
public Date getLastUpdated() {
return lastUpdated;
}
public void setLastUpdated() {
lastUpdated = new Date();
}
public void log(String message, Level level) {
s_logger.log(level, message + ", " + type.toString() + " = " + typeName + " at host " + sserver.getName());
}
public void setDisconnected() {
transition(UploadEvent.DISCONNECT, null);
}
public void scheduleStatusCheck(com.cloud.agent.api.storage.UploadProgressCommand.RequestType getStatus) {
if (statusTask != null) statusTask.cancel();
statusTask = new StatusTask(this, getStatus);
timer.schedule(statusTask, STATUS_POLL_INTERVAL);
}
public void scheduleTimeoutTask(long delay) {
if (timeoutTask != null) timeoutTask.cancel();
timeoutTask = new TimeoutTask(this);
timer.schedule(timeoutTask, delay);
if (s_logger.isDebugEnabled()) {
log("Scheduling timeout at " + delay + " ms", Level.DEBUG);
}
}
public void updateDatabase(Status state, String uploadErrorString) {
resultObj.setResultString(uploadErrorString);
resultObj.setState(state.toString());
asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
asyncMgr.updateAsyncJobStatus(asyncJobId, AsyncJobResult.STATUS_IN_PROGRESS, resultObj);
UploadVO vo = uploadDao.createForUpdate();
vo.setUploadState(state);
vo.setLastUpdated(new Date());
vo.setErrorString(uploadErrorString);
uploadDao.update(getUploadId(), vo);
}
public void updateDatabase(Status state, String uploadUrl,String uploadErrorString) {
resultObj.setResultString(uploadErrorString);
resultObj.setState(state.toString());
asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
asyncMgr.updateAsyncJobStatus(asyncJobId, AsyncJobResult.STATUS_IN_PROGRESS, resultObj);
UploadVO vo = uploadDao.createForUpdate();
vo.setUploadState(state);
vo.setLastUpdated(new Date());
vo.setUploadUrl(uploadUrl);
vo.setJobId(null);
vo.setUploadPercent(0);
vo.setErrorString(uploadErrorString);
uploadDao.update(getUploadId(), vo);
}
private Long getUploadId() {
return uploadId;
}
public synchronized void updateDatabase(UploadAnswer answer) {
if(answer.getErrorString().startsWith("553")){
answer.setErrorString(answer.getErrorString().concat("Please check if the file name already exists."));
}
resultObj.setResultString(answer.getErrorString());
resultObj.setState(answer.getUploadStatus().toString());
resultObj.setUploadPercent(answer.getUploadPct());
if (answer.getUploadStatus() == Status.UPLOAD_IN_PROGRESS){
asyncMgr.updateAsyncJobAttachment(asyncJobId, type.toString(), 1L);
asyncMgr.updateAsyncJobStatus(asyncJobId, AsyncJobResult.STATUS_IN_PROGRESS, resultObj);
}else if(answer.getUploadStatus() == Status.UPLOADED){
resultObj.setResultString("Success");
asyncMgr.completeAsyncJob(asyncJobId, AsyncJobResult.STATUS_SUCCEEDED, 1, resultObj);
}else{
asyncMgr.completeAsyncJob(asyncJobId, AsyncJobResult.STATUS_FAILED, 2, resultObj);
}
UploadVO updateBuilder = uploadDao.createForUpdate();
updateBuilder.setUploadPercent(answer.getUploadPct());
updateBuilder.setUploadState(answer.getUploadStatus());
updateBuilder.setLastUpdated(new Date());
updateBuilder.setErrorString(answer.getErrorString());
updateBuilder.setJobId(answer.getJobId());
uploadDao.update(getUploadId(), updateBuilder);
}
public void sendCommand(RequestType reqType) {
if (getJobId() != null) {
if (s_logger.isTraceEnabled()) {
log("Sending progress command ", Level.TRACE);
}
try {
uploadMonitor.send(sserver.getId(), new UploadProgressCommand(getCommand(), getJobId(), reqType), this);
} catch (AgentUnavailableException e) {
s_logger.debug("Send command failed", e);
setDisconnected();
}
}
}
private UploadCommand getCommand() {
return cmd;
}
public void logDisconnect() {
s_logger.warn("Unable to monitor upload progress of " + typeName + " at host " + sserver.getName());
}
public void scheduleImmediateStatusCheck(RequestType request) {
if (statusTask != null) statusTask.cancel();
statusTask = new StatusTask(this, request);
timer.schedule(statusTask, SMALL_DELAY);
}
public void setCurrState(Status uploadState) {
this.currState = getState(currState.toString());
}
}