package com.emc.ecs.sync.ctl;
import com.emc.ecs.sync.config.XmlGenerator;
import com.emc.ecs.sync.rest.*;
import com.sun.jersey.api.client.Client;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.UniformInterfaceException;
import org.apache.commons.cli.*;
import org.apache.log4j.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
/**
* Entry point class for the ECS Sync CLI
*/
public class EcsSyncCtl {
private static final Logger l4j = Logger.getLogger(EcsSyncCtl.class);
private static final String DEBUG_OPT = "debug";
private static final String VERBOSE_OPT = "verbose";
private static final String PAUSE_OPT = "pause";
private static final String RESUME_OPT = "resume";
private static final String STOP_OPT = "stop";
private static final String DELETE_OPT = "delete";
private static final String STATUS_OPT = "status";
private static final String SUBMIT_OPT = "submit";
private static final String SET_THREADS_OPT = "set-threads";
private static final String THREADS_OPT = "threads";
private static final String LOG_FILE_OPT = "log-file";
private static final String LOG_PATTERN_OPT = "log-pattern";
private static final String LIST_JOBS_OPT = "list-jobs";
private static final String ENDPOINT_OPT = "endpoint";
private static final String XML_GEN_OPT = "xml-gen";
private static final String XG_SOURCE_OPT = "xml-source";
private static final String XG_TARGET_OPT = "xml-target";
private static final String XG_FILTERS_OPT = "xml-filters";
private static final String XG_COMMENTS_OPT = "xml-comments";
private static final String XG_SIMPLE_OPT = "xml-simple";
private static final String HOST_INFO_OPT = "host-info";
private static final String SET_LOG_LEVEL_OPT = "set-log-level";
private static final String DEFAULT_ENDPOINT = "http://localhost:9200";
private static final String JAR_NAME = "ecs-sync-ctl-{version}";
private static final String LAYOUT_STRING_FILE = "%d{yyyy-MM-dd HH:mm:ss} %-5p [%t] %c{1}:%L - %m%n";
private static final String LAYOUT_STRING_CONSOLE = "%d{MM-dd HH:mm:ss}%-5p [%t] %c{1}:%L - %m%n";
private static final String LAYOUT_STRING_CONSOLE_NOANSI = "%d{MM-dd HH:mm:ss} %-5p [%t] %c{1}:%L - %m%n";
private static final int EXIT_SUCCESS = 0;
private static final int EXIT_ARG_ERROR = 255;
private static final int EXIT_FILE_NOT_FOUND = 2;
private static final int EXIT_LOGGER_ERROR = 3;
private static final int EXIT_JOB_CONFLICT = 4;
private static final int EXIT_NO_JOB = 5;
private static final int EXIT_UNKNOWN_ERROR=99;
public static void main(String[] args) {
Options opts = new Options();
OptionGroup commands = new OptionGroup();
commands.addOption(Option.builder().longOpt(PAUSE_OPT).hasArg().argName("job-id")
.desc("Pauses the specified sync job").build());
commands.addOption(Option.builder().longOpt(RESUME_OPT).hasArg().argName("job-id")
.desc("Resumes a specified sync job").build());
commands.addOption(Option.builder().longOpt(STOP_OPT).hasArg().argName("job-id")
.desc("Terminates a specified sync job").build());
commands.addOption(Option.builder().longOpt(DELETE_OPT).hasArg().argName("job-id")
.desc("Deletes a sync job from the server. The job must be stopped first. Note that the database is left in-tact").build());
commands.addOption(Option.builder().longOpt(STATUS_OPT).hasArg().argName("job-id")
.desc("Queries the server for job status").build());
commands.addOption(Option.builder().longOpt(SUBMIT_OPT).hasArg().argName("xml-file")
.desc("Submits a new job to the server").build());
commands.addOption(Option.builder().longOpt(SET_THREADS_OPT).hasArg().argName("job-id")
.desc("Sets the number of sync threads on the server. " +
"Requires --" + THREADS_OPT + " argument").build());
commands.addOption(Option.builder().longOpt(LIST_JOBS_OPT).desc("Lists jobs in the server").build());
commands.addOption(Option.builder().longOpt(XML_GEN_OPT).hasArg().argName("output-file")
.desc("Generates a verbose XML config file for the specified plugins").build());
commands.addOption(Option.builder().longOpt(SET_LOG_LEVEL_OPT).hasArg().argName("log-level").type(Level.class)
.desc("Sets the logging level of the ecs-sync service").build());
commands.addOption(Option.builder().longOpt(HOST_INFO_OPT).desc("Gets host information, including ecs-sync version").build());
commands.setRequired(true);
opts.addOptionGroup(commands);
opts.addOption(Option.builder().longOpt(DEBUG_OPT).desc("maximum logging for the ctl client").build());
opts.addOption(Option.builder().longOpt(VERBOSE_OPT).desc("additional logging for the ctl client").build());
opts.addOption(Option.builder().longOpt(THREADS_OPT).hasArg().argName("thread-count").desc(
"Used in conjunction with --" + SET_THREADS_OPT +
" to set the number of threads to use for a job.").build());
opts.addOption(Option.builder().longOpt(LOG_FILE_OPT).hasArg().argName("filename")
.desc("Filename to write log messages. Setting to STDOUT or STDERR will write log messages to the " +
"appropriate process stream. Default is STDERR.").build());
opts.addOption(Option.builder().longOpt(LOG_PATTERN_OPT).hasArg().argName("log4j-pattern").desc("Sets the " +
"Log4J pattern to use when writing log messages. Defaults to " +
LAYOUT_STRING_FILE).build());
opts.addOption(Option.builder().longOpt(XG_SOURCE_OPT).hasArg().argName("source-prefix")
.desc("The prefix for the storage plugin to use as the source in the generated config file").build());
opts.addOption(Option.builder().longOpt(XG_TARGET_OPT).hasArg().argName("target-prefix")
.desc("The prefix for the storage plugin to use as the target in the generated config file").build());
opts.addOption(Option.builder().longOpt(XG_FILTERS_OPT).hasArg().argName("filter-list")
.desc("A comma-delimited list of names of filters to use as the source in the generated config file (optional)").build());
opts.addOption(Option.builder().longOpt(XG_COMMENTS_OPT).desc("Adds descriptive comments to the generated config file").build());
opts.addOption(Option.builder().longOpt(XG_SIMPLE_OPT).desc("Does not include advanced options in the generated config file").build());
opts.addOption(Option.builder().longOpt(ENDPOINT_OPT).hasArg().argName("url")
.desc("Sets the server endpoint to connect to. Default is " + DEFAULT_ENDPOINT).build());
DefaultParser dp = new DefaultParser();
CommandLine cmd = null;
try {
cmd = dp.parse(opts, args);
} catch (ParseException e) {
System.err.println("Error: " + e.getMessage());
printHelp(opts);
System.exit(EXIT_ARG_ERROR);
}
//
// Configure Logging
//
String logFileName = "STDERR";
if(cmd.hasOption(LOG_FILE_OPT)) {
logFileName = cmd.getOptionValue(LOG_FILE_OPT);
}
// Pattern
String layoutString = LAYOUT_STRING_FILE;
if (!System.getProperty("os.name").startsWith("Windows")) {
if ("STDERR".equals(logFileName) || "STDOUT".equals(logFileName)) {
layoutString = LAYOUT_STRING_CONSOLE;
}
} else {
// No easy ANSI colors in Windows console :(
if ("STDERR".equals(logFileName) || "STDOUT".equals(logFileName)) {
layoutString = LAYOUT_STRING_CONSOLE_NOANSI;
}
}
if(cmd.hasOption(LOG_PATTERN_OPT)) {
layoutString = cmd.getOptionValue(LOG_PATTERN_OPT);
}
PatternLayout layout = new PatternLayout(layoutString);
// Appender
Appender appender = null;
if(logFileName.equals("STDERR")) {
appender = new ConsoleAppender(layout, "System.err");
} else if(logFileName.equals("STDOUT")) {
appender = new ConsoleAppender(layout, "System.out");
} else {
// Just a regular file.
try {
appender = new FileAppender(layout, logFileName);
} catch (IOException e) {
System.err.println("FATAL: Could not configure appender");
e.printStackTrace();
System.exit(EXIT_LOGGER_ERROR);
}
}
LogManager.getRootLogger().addAppender(appender);
// Log level
if (cmd.hasOption(DEBUG_OPT)) {
LogManager.getRootLogger().setLevel(Level.DEBUG);
} else if (cmd.hasOption(VERBOSE_OPT)) {
LogManager.getRootLogger().setLevel(Level.INFO);
} else {
LogManager.getRootLogger().setLevel(Level.WARN);
}
String endpoint = DEFAULT_ENDPOINT;
if(cmd.hasOption(ENDPOINT_OPT)) {
endpoint = cmd.getOptionValue(ENDPOINT_OPT);
}
EcsSyncCtl cli = new EcsSyncCtl(endpoint, null, null);
if(cmd.hasOption(PAUSE_OPT)) {
int jobId = Integer.parseInt(cmd.getOptionValue(PAUSE_OPT));
LogMF.info(l4j, "Command: Pause Job #{0}", jobId);
cli.pause(jobId);
} else if(cmd.hasOption(RESUME_OPT)) {
int jobId = Integer.parseInt(cmd.getOptionValue(RESUME_OPT));
LogMF.info(l4j, "Command: Resume Job #{0}", jobId);
cli.resume(jobId);
} else if (cmd.hasOption(STOP_OPT)) {
int jobId = Integer.parseInt(cmd.getOptionValue(STOP_OPT));
LogMF.info(l4j, "Command: Terminate Job #{0}", jobId);
cli.stop(jobId);
} else if(cmd.hasOption(DELETE_OPT)) {
int jobId = Integer.parseInt(cmd.getOptionValue(DELETE_OPT));
LogMF.info(l4j, "Command: Delete Job #{0}", jobId);
cli.delete(jobId);
} else if(cmd.hasOption(STATUS_OPT)) {
int jobId = Integer.parseInt(cmd.getOptionValue(STATUS_OPT));
LogMF.info(l4j, "Command: Status Job #{0}", jobId);
cli.status(jobId);
} else if(cmd.hasOption(SUBMIT_OPT)) {
String xmlFile = cmd.getOptionValue(SUBMIT_OPT);
LogMF.info(l4j, "Command: Submit file {0}", xmlFile);
cli.submit(xmlFile);
} else if(cmd.hasOption(SET_THREADS_OPT)) {
if (!cmd.hasOption(THREADS_OPT)) {
System.err.printf("Error: the argument --%s is required for --%s\n", THREADS_OPT, SET_THREADS_OPT);
printHelp(opts);
System.exit(EXIT_ARG_ERROR);
}
int jobId = Integer.parseInt(cmd.getOptionValue(SET_THREADS_OPT));
Integer threadCount = null;
if (cmd.hasOption(THREADS_OPT)) {
threadCount = new Integer(cmd.getOptionValue(THREADS_OPT));
}
LogMF.info(l4j, "Command: Set job {0} thread count = {1}",
jobId, threadCount);
cli.setThreadCount(jobId, threadCount);
} else if(cmd.hasOption(LIST_JOBS_OPT)) {
l4j.info("Command: List Jobs");
cli.listJobs();
} else if (cmd.hasOption(XML_GEN_OPT)) {
l4j.info("Command: Generate XML");
if (!cmd.hasOption(XG_SOURCE_OPT) || !cmd.hasOption(XG_TARGET_OPT)) {
System.err.printf("Error: the arguments --%s and --%s are required for --%s", XG_SOURCE_OPT, XG_TARGET_OPT, XML_GEN_OPT);
printHelp(opts);
System.exit(EXIT_ARG_ERROR);
}
String outputFile = cmd.getOptionValue(XML_GEN_OPT);
String source = cmd.getOptionValue(XG_SOURCE_OPT);
String target = cmd.getOptionValue(XG_TARGET_OPT);
String filters = cmd.getOptionValue(XG_FILTERS_OPT);
boolean addComments = cmd.hasOption(XG_COMMENTS_OPT);
boolean simple = cmd.hasOption(XG_SIMPLE_OPT);
try {
cli.genXml(outputFile, source, target, filters, addComments, !simple);
} catch (Exception e) {
System.err.printf("Error: " + e);
System.exit(EXIT_UNKNOWN_ERROR);
}
} else if (cmd.hasOption(HOST_INFO_OPT)) {
l4j.info("Command: Host Info");
cli.hostInfo();
} else if (cmd.hasOption(SET_LOG_LEVEL_OPT)) {
l4j.info("Command: Set Log Level");
cli.setLogLevel(LogLevel.valueOf(cmd.getOptionValue(SET_LOG_LEVEL_OPT)));
} else {
throw new RuntimeException("Unknown command");
}
System.exit(EXIT_SUCCESS);
}
private static void printHelp(Options opts) {
HelpFormatter hf = new HelpFormatter();
hf.printHelp("java -jar " + JAR_NAME + ".jar", opts, true);
}
private void setThreadCount(int jobId, Integer threadCount) {
JobControl control = new JobControl();
if (threadCount != null) {
control.setThreadCount(threadCount);
}
controlJob(jobId, control);
}
private void resume(int jobId) {
JobControl control = new JobControl();
control.setStatus(JobControlStatus.Running);
controlJob(jobId, control);
}
private void pause(int jobId) {
JobControl control = new JobControl();
control.setStatus(JobControlStatus.Paused);
controlJob(jobId, control);
}
private void stop(int jobId) {
JobControl control = new JobControl();
control.setStatus(JobControlStatus.Stopped);
controlJob(jobId, control);
}
private void controlJob(int jobId, JobControl control) {
Client client = new Client();
String uri = String.format("%s/job/%d/control", endpoint, jobId);
ClientResponse response = client.resource(uri).entity(control, "application/xml").post(ClientResponse.class);
if(response.getStatus() == 404) {
System.out.printf("No job %d\n", jobId);
System.exit(EXIT_NO_JOB);
} else if(response.getStatus() == 200) {
System.out.println("Command completed successfully");
System.exit(EXIT_SUCCESS);
} else {
System.err.printf("Error controlling job %d: HTTP %d: %s\n", jobId, response.getStatus(),
response.getStatusInfo().getReasonPhrase());
String s = response.getEntity(String.class);
if(s != null) {
System.err.println(s);
}
System.exit(EXIT_UNKNOWN_ERROR);
}
}
private void status(int jobId) {
Client client = new Client();
String uri = String.format("%s/job/%d/progress", endpoint, jobId);
try {
SyncProgress progress = client.resource(uri).get(SyncProgress.class);
// when byte *and* object estimates are available, ETA is based on a weighted average of the two percentages
// with the lesser value counted twice i.e.:
// ( 2 * min(bytePercent, objectPercent) + max(bytePercent, objectPercent) ) / 3
double bw = 0, xput = 0, byteRatio = 0, objectRatio = 0, completionRatio = 0;
long totalBytes = progress.getTotalBytesExpected() - progress.getBytesSkipped();
long totalObjects = progress.getTotalObjectsExpected() - progress.getObjectsSkipped();
long etaMs = 0;
if (progress.getRuntimeMs() > 0) {
bw = (double) progress.getBytesComplete() * 1000 / progress.getRuntimeMs();
xput = (double) progress.getObjectsComplete() * 1000 / progress.getRuntimeMs();
if (totalBytes > 0) {
byteRatio = (double) progress.getBytesComplete() / totalBytes;
completionRatio = byteRatio;
}
if (totalObjects > 0) {
objectRatio = (double) progress.getObjectsComplete() / totalObjects;
completionRatio = objectRatio;
}
if (byteRatio > 0 && objectRatio > 0)
completionRatio = (2 * Math.min(byteRatio, objectRatio) + Math.max(byteRatio, objectRatio)) / 3;
if (completionRatio > 0)
etaMs = (long) (progress.getRuntimeMs() / completionRatio - progress.getRuntimeMs());
}
String generalError = progress.getRunError() == null ? "" : progress.getRunError();
System.out.printf("Job Status: %s\n", progress.getStatus());
System.out.printf("Job Time: %s\n", duration(progress.getRuntimeMs()));
System.out.printf("Active Query Threads: %d\n", progress.getActiveQueryTasks());
System.out.printf("Active Sync Threads: %d\n", progress.getActiveSyncTasks());
System.out.printf("CPU Time: %dms\n", progress.getCpuTimeMs());
System.out.printf("CPU Usage: %.1f %%\n", progress.getProcessCpuLoad() * 100);
System.out.printf("Memory Usage: %sB\n", simpleSize(progress.getProcessMemoryUsed()));
System.out.printf("Objects Expected: %d %s\n", progress.getTotalObjectsExpected(),
progress.isEstimatingTotals() ? "(calculating...)" : "");
System.out.printf("Objects Completed: %d\n", progress.getObjectsComplete());
System.out.printf("Objects Skipped: %d\n", progress.getObjectsSkipped());
System.out.printf("Objects Awaiting Retry: %d\n", progress.getObjectsAwaitingRetry());
System.out.printf("Error Count: %d\n", progress.getObjectsFailed());
System.out.printf("Bytes Expected: %sB\n", simpleSize(progress.getTotalBytesExpected()));
System.out.printf("Bytes Completed: %sB\n", simpleSize(progress.getBytesComplete()));
System.out.printf("Bytes Skipped: %sB\n", simpleSize(progress.getBytesSkipped()));
System.out.printf("Current BW (source): read: %sB/s write: %sB/s\n",
simpleSize(progress.getSourceReadRate()), simpleSize(progress.getSourceWriteRate()));
System.out.printf("Current BW (target): read: %sB/s write: %sB/s\n",
simpleSize(progress.getTargetReadRate()), simpleSize(progress.getTargetWriteRate()));
System.out.printf("Current Throughput: completed %d/s skipped %d/s failed %d/s\n",
progress.getObjectCompleteRate(), progress.getObjectSkipRate(), progress.getObjectErrorRate());
System.out.printf("Average BW: %sB/s\n", simpleSize((long) bw));
System.out.printf("Average Throughput: %.1f/s\n", xput);
System.out.printf("ETA: %s\n", etaMs > 0 ? duration(etaMs) : "N/A");
System.out.printf("General Error: %s\n", generalError);
} catch(UniformInterfaceException e) {
if(e.getResponse().getStatus() == 404) {
System.out.printf("No job #%d found\n", jobId);
System.exit(EXIT_NO_JOB);
} else {
ClientResponse resp = e.getResponse();
System.err.printf("Error getting job status: HTTP %d: %s\n", resp.getStatus(), resp.getStatusInfo().getReasonPhrase());
System.exit(EXIT_UNKNOWN_ERROR);
}
}
}
private void delete(int jobId) {
Client client = new Client();
String uri = String.format("%s/job/%d?keepDatabase=true", endpoint, jobId);
ClientResponse resp = client.resource(uri).delete(ClientResponse.class);
if(resp.getStatus() == 404) {
System.out.println("No job running");
System.exit(EXIT_NO_JOB);
} else if(resp.getStatus() == 200) {
System.out.println("Job deleted");
System.exit(EXIT_SUCCESS);
} else {
System.err.printf("Error deleting job: HTTP %d: %s", resp.getStatus(), resp.getStatusInfo().getReasonPhrase());
System.exit(EXIT_UNKNOWN_ERROR);
}
}
private void submit(String xmlFile) {
File f = new File(xmlFile);
if(!f.exists()) {
System.err.printf("Job file %s does not exist!\n", xmlFile);
System.exit(EXIT_FILE_NOT_FOUND);
}
Client client = new Client();
ClientResponse resp = client.resource(endpoint + "/job").entity(f, "application/xml").put(ClientResponse.class);
LogMF.debug(l4j, "HTTP Response {0}:{1}", resp.getStatus(), resp.getStatusInfo().getReasonPhrase());
if(resp.getStatus() == 409) {
System.err.println("Cannot submit job, another job is already running");
System.exit(EXIT_JOB_CONFLICT);
} else if (resp.getStatus() > 399) {
System.err.println("Error submitting job:\n" + resp.getEntity(String.class));
}
String job = resp.getHeaders().getFirst("x-emc-job-id");
System.out.printf("Submitted Job %s\n", job);
}
private void listJobs() {
Client client = new Client();
JobList list = client.resource(endpoint + "/job").accept("application/xml").get(JobList.class);
System.out.printf("JobID Status\n");
System.out.printf("-----------------------\n");
for(JobInfo ji : list.getJobs()) {
System.out.printf("%5d %s\n", ji.getJobId(), ji.getStatus());
}
}
private void genXml(String outputFile, String source, String target, String filters, boolean addComments, boolean advancedOptions)
throws Exception {
if (!source.endsWith(":")) source += ":";
if (!target.endsWith(":")) target += ":";
String[] filterA = (filters == null) ? new String[0] : filters.split(",");
String xml = XmlGenerator.generateXml(addComments, advancedOptions, source, target, filterA);
try (OutputStream outputStream = new FileOutputStream(outputFile)) {
outputStream.write(xml.getBytes(StandardCharsets.UTF_8));
}
}
private void hostInfo() {
Client client = new Client();
HostInfo info = client.resource(endpoint + "/host").accept("application/xml").get(HostInfo.class);
System.out.printf("JobID Status\n");
System.out.printf("-----------------------\n");
System.out.printf("ECS-Sync version: " + info.getEcsSyncVersion() + "\n");
System.out.printf("CPU Count: " + info.getHostCpuCount() + "\n");
System.out.printf("CPU Load: " + info.getHostCpuLoad() + "\n");
System.out.printf("Total Memory: " + simpleSize(info.getHostTotalMemory()) + "\n");
System.out.printf("Used Memory: " + simpleSize(info.getHostMemoryUsed()) + "\n");
System.out.printf("Log Level: " + info.getLogLevel() + "\n");
}
private void setLogLevel(LogLevel logLevel) {
Client client = new Client();
ClientResponse response = client.resource(endpoint + "/host/logging?level=" + logLevel).accept("application/xml").post(ClientResponse.class);
if (response.getStatus() == 200) {
System.out.println("Command completed successfully");
System.exit(EXIT_SUCCESS);
} else {
System.err.printf("Error setting log level: HTTP %d: %s\n", response.getStatus(),
response.getStatusInfo().getReasonPhrase());
String s = response.getEntity(String.class);
if (s != null) {
System.err.println(s);
}
System.exit(EXIT_UNKNOWN_ERROR);
}
}
private String duration(long millis) {
long secs = (millis / 1000) % 60;
long mins = (millis / 60000) % 60;
long hours = millis / 3600000;
String duration = String.format("%dms", millis % 1000);
if (secs > 0) duration = String.format("%ds ", secs) + duration;
if (mins > 0) duration = String.format("%dm ", mins) + duration;
if (hours > 0) duration = String.format("%dh ", hours) + duration;
return duration;
}
private String simpleSize(long size) {
if (size <= 0) return "" + size;
long base = 1024L;
int decimals = 1;
List prefix = Arrays.asList("", 'K', 'M', 'G', 'T');
int i = (int) (Math.log(size) / Math.log(base));
i = (i >= prefix.size() ? prefix.size() - 1 : i);
double value = Math.round((size / Math.pow(base, i)) * Math.pow(10, decimals)) / Math.pow(10, decimals);
return i == 0 ? "" + size : String.format("%.1f%s", value, prefix.get(i));
}
private String endpoint;
private String user;
private String pass;
public EcsSyncCtl(String endpoint, String user, String pass) {
this.endpoint = endpoint;
this.user = user;
this.pass = pass;
}
}