/*******************************************************************************
* Copyright 2012 Urbancode, Inc
*
* Licensed 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.urbancode.terraform.main;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.UUID;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.apache.commons.codec.binary.Base64;
import org.apache.log4j.Logger;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import com.urbancode.terraform.commands.vmware.ResumeCommand;
import com.urbancode.terraform.commands.vmware.SuspendCommand;
import com.urbancode.terraform.commands.vmware.TakeSnapshotCommand;
import com.urbancode.terraform.credentials.aws.CredentialsAWS;
import com.urbancode.terraform.credentials.aws.CredentialsParserAWS;
import com.urbancode.terraform.credentials.common.Credentials;
import com.urbancode.terraform.credentials.common.CredentialsException;
import com.urbancode.terraform.credentials.common.CredentialsParser;
import com.urbancode.terraform.credentials.common.CredentialsParserRegistry;
import com.urbancode.terraform.credentials.microsoft.CredentialsMicrosoft;
import com.urbancode.terraform.credentials.microsoft.CredentialsParserMicrosoft;
import com.urbancode.terraform.credentials.rackspace.CredentialsParserRackspace;
import com.urbancode.terraform.credentials.rackspace.CredentialsRackspace;
import com.urbancode.terraform.credentials.vcloud.CredentialsParserVCloud;
import com.urbancode.terraform.credentials.vcloud.CredentialsVCloud;
import com.urbancode.terraform.credentials.vmware.CredentialsParserVmware;
import com.urbancode.terraform.credentials.vmware.CredentialsVmware;
import com.urbancode.terraform.tasks.aws.ContextAWS;
import com.urbancode.terraform.tasks.common.TerraformContext;
import com.urbancode.terraform.tasks.vmware.ContextVmware;
import com.urbancode.x2o.tasks.CreationException;
import com.urbancode.x2o.tasks.DestructionException;
import com.urbancode.x2o.tasks.RestorationException;
import com.urbancode.x2o.util.Property;
import com.urbancode.x2o.util.PropertyResolver;
import com.urbancode.x2o.xml.XmlModelParser;
import com.urbancode.x2o.xml.XmlParsingException;
import com.urbancode.x2o.xml.XmlWrite;
public class Main {
//**********************************************************************************************
// CLASS
//**********************************************************************************************
static private final Logger log = Logger.getLogger(Main.class);
//----------------------------------------------------------------------------------------------
/**
* This method initializes Terraform from the command line.
* The static main method verifies the command line arguments and terminates if they are incorrect.
* @param args
* @throws IOException
* @throws XmlParsingException
* @throws CredentialsException
* @throws RestorationException
* @throws DestructionException
* @throws CreationException
*/
static public void main(String[] args)
throws IOException, XmlParsingException, CredentialsException, CreationException, DestructionException, RestorationException {
File inputXmlFile = null;
File creds = null;
List<String> unparsedArgs = new ArrayList<String>();
String command = null;
if (args != null && args.length >= 3) {
if (!AllowedCommands.contains(args[0])) {
String msg = "Invalid first argument: "+args[0];
log.fatal(msg);
throw new IOException(msg);
}
else {
command = args[0].toLowerCase();
}
inputXmlFile = createFile(args[1]);
creds = createFile(args[2]);
Collections.addAll(unparsedArgs, args);
// make args just the unparsed properties
unparsedArgs.remove(0); // remove inputxmlpath
unparsedArgs.remove(0); // remove create/destroy
unparsedArgs.remove(0); // remove creds
}
else {
log.fatal("Invalid number of arguments!\n" +
"Found " + args.length + "\n" +
"Expected at least 3");
throw new IOException("improper args");
}
// check to make sure we have legit args
if (inputXmlFile == null) {
String msg = "No input xml file specified!";
throw new IOException(msg);
}
if (creds == null) {
String msg = "No credentials file specified!";
throw new IOException(msg);
}
Main myMain = new Main(command, inputXmlFile, creds, unparsedArgs);
myMain.execute();
}
//----------------------------------------------------------------------------------------------
/**
* Creates a file and runs checks on it
*
* @param filePath
* @return
* @throws FileNotFoundException
*/
static private File createFile(String filePath)
throws FileNotFoundException {
File result = null;
if (!"".equals(filePath)) {
result = new File(filePath);
if (result.exists()) {
if (result.isFile()) {
if (!result.canRead()) {
String msg = "Input file does not exist: "+filePath;
log.fatal(msg);
throw new FileNotFoundException(msg);
}
}
else {
String msg = "Input file is not a file: "+filePath;
log.fatal(msg);
throw new FileNotFoundException(msg);
}
}
else {
String msg = "Input file does not exist: "+filePath;
log.fatal(msg);
throw new FileNotFoundException(msg);
}
}
return result;
}
//**********************************************************************************************
// INSTANCE
//**********************************************************************************************
private String command;
private File inputXmlFile;
private File outputXmlFile;
private File credsFile;
private List<Property> props;
//----------------------------------------------------------------------------------------------
private Main(String command, File inputXmlFile, File credsFile, List<String> unparsed) {
this.command = command;
startCredsParser();
this.credsFile = credsFile;
this.inputXmlFile = inputXmlFile;
props = new ArrayList<Property>();
if (unparsed != null) {
for (String prop : unparsed) {
try {
props.add(parseProperty(prop));
}
catch (IOException e) {
log.error("Unable to parse property: " + prop);
}
}
}
}
//----------------------------------------------------------------------------------------------
/**
* Initializes Terraform so it can execute the given commands.
* Here is the order of operations:
* Parses the credentials file and verifies the given credentials.
* Generates a random string for this environment, which is appended to the output xml file.
* Parses the xml file.
* Runs the specified command (create, destroy, etc).
* @throws XmlParsingException
* @throws IOException
* @throws CredentialsException
* @throws CreationException
* @throws DestructionException
* @throws RestorationException
*/
public void execute()
throws XmlParsingException, IOException, CredentialsException, CreationException, DestructionException, RestorationException {
TerraformContext context = null;
try {
// parse xml and set context
context = parseContext(inputXmlFile);
Credentials credentials = parseCredentials(credsFile);
context.setCredentials(credentials);
if (AllowedCommands.CREATE.getCommandName().equalsIgnoreCase(command)) {
// create new file if creating a new environment
UUID uuid = UUID.randomUUID();
//convert uuid to base 62 (allowed chars: 0-9 a-z A-Z)
ByteBuffer bb = ByteBuffer.wrap(new byte[16]);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
String suffix = Base64.encodeBase64URLSafeString(bb.array());
suffix = suffix.replaceAll("-", "Y");
suffix = suffix.replaceAll("_", "Z");
suffix = suffix.substring(0, 4);
if (context.getEnvironment() != null) {
context.getEnvironment().addSuffixToEnvName(suffix);
log.debug("UUID for env " + context.getEnvironment().getName() + " is " + suffix);
}
else {
throw new NullPointerException("No environment on context!");
}
String name = context.getEnvironment().getName();
log.debug("Output filename = " + name);
outputXmlFile = new File("env-" + name + ".xml");
log.debug("Calling create() on context");
context.create();
}
else if (AllowedCommands.DESTROY.getCommandName().equalsIgnoreCase(command)) {
String suffix = parseSuffix(context.getEnvironment().getName());
context.getEnvironment().setSuffix(suffix);
log.debug("found suffix " + suffix);
// write out instance failure regardless of success or failure
outputXmlFile = inputXmlFile;
log.debug("Calling destroy() on context");
context.destroy();
}
else if (AllowedCommands.SUSPEND.getCommandName().equalsIgnoreCase(command)) {
outputXmlFile = inputXmlFile;
log.debug("Calling restore() on context");
log.info("Attempting to suspend power on all instances/VMs in the environment.");
context.restore();
if (context instanceof ContextVmware) {
SuspendCommand newCommand = new SuspendCommand((ContextVmware) context);
newCommand.execute();
}
else if (context instanceof ContextAWS) {
com.urbancode.terraform.commands.aws.SuspendCommand newCommand =
new com.urbancode.terraform.commands.aws.SuspendCommand((ContextAWS) context);
newCommand.execute();
}
else {
log.warn("Could not resolve context to call command \"" + command + "\"");
}
}
else if (AllowedCommands.RESUME.getCommandName().equalsIgnoreCase(command)) {
outputXmlFile = inputXmlFile;
log.debug("Calling restore() on context");
context.restore();
log.info("Attempting to power on all instances/VMs in the environment.");
if (context instanceof ContextVmware) {
ResumeCommand newCommand = new ResumeCommand((ContextVmware) context);
newCommand.execute();
}
else if (context instanceof ContextAWS) {
com.urbancode.terraform.commands.aws.ResumeCommand newCommand =
new com.urbancode.terraform.commands.aws.ResumeCommand((ContextAWS) context);
newCommand.execute();
}
else {
log.warn("Could not resolve context to call command \"" + command + "\"");
}
}
else if (AllowedCommands.TAKE_SNAPSHOT.getCommandName().equalsIgnoreCase(command)) {
outputXmlFile = inputXmlFile;
log.debug("Calling restore() on context");
context.restore();
log.info("Attempting to take snapshots of all instances/VMs in the environment.");
if (context instanceof ContextVmware) {
TakeSnapshotCommand newCommand = new TakeSnapshotCommand((ContextVmware) context);
newCommand.execute();
}
else if (context instanceof ContextAWS) {
log.warn("Taking snapshots is not currently supported with Terraform and AWS.");
}
else {
log.warn("Could not resolve context to call command \"" + command + "\"");
}
}
}
catch (ParserConfigurationException e1) {
throw new XmlParsingException("ParserConfigurationException: " + e1.getMessage(), e1);
}
catch (SAXException e2) {
throw new XmlParsingException("SAXException: " + e2.getMessage(), e2);
}
finally {
if (context != null && context.doWriteContext() && outputXmlFile != null) {
log.debug("Writing context out to " + outputXmlFile);
writeEnvToXml(outputXmlFile, context);
}
}
}
//----------------------------------------------------------------------------------------------
private PropertyResolver createResolver() {
return new PropertyResolver(props);
}
//----------------------------------------------------------------------------------------------
private Credentials parseCredentials(File credsFile) {
Credentials result = null;
Properties credProps = loadPropertiesFromFile(credsFile);
for (Property cmdlineProp : props) {
if (credProps.get(cmdlineProp.getName()) == null) {
credProps.put(cmdlineProp.getName(), cmdlineProp.getValue());
}
}
result = parseCredsFromProps(credProps);
if (result != null) {
log.info("Restored Credentials: " + credsFile + " : " + result.getName());
}
else {
log.info("Did not restore Credentials for " + credsFile);
}
return result;
}
//----------------------------------------------------------------------------------------------
private Properties loadPropertiesFromFile(File propFile) {
Properties result = new Properties();
String path = propFile.getAbsolutePath();
InputStream in = null;
try {
in = new FileInputStream(propFile);
result.load(in);
}
catch (FileNotFoundException e) {
log.error("Unable to load properties from " + path, e);
}
catch (IOException e) {
log.error("IOException when loading properties from " + path, e);
// swallow
}
finally {
try {
if (in != null) {
in.close();
}
}
catch (IOException e) {
// swallow
}
}
return result;
}
//----------------------------------------------------------------------------------------------
private Credentials parseCredsFromProps(Properties props) {
Credentials result = null;
String type = props.getProperty("type");
if (type == null || "".equals(type)) {
throw new NullPointerException("No credentials type specified in props: " + props);
}
CredentialsParser parser = CredentialsParserRegistry.getInstance().getParser(type);
result = parser.parse(props);
return result;
}
//----------------------------------------------------------------------------------------------
private TerraformContext parseContext(File xmlFileToRead)
throws ParserConfigurationException, XmlParsingException, SAXException, IOException {
TerraformContext result = null;
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
dbFactory.setNamespaceAware(true);
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(xmlFileToRead);
Element rootElement = doc.getDocumentElement();
rootElement.normalize();
XmlModelParser parser = new XmlModelParser();
parser.setPropertyResolver(createResolver());
result = (TerraformContext) parser.parse(rootElement);
return result;
}
//----------------------------------------------------------------------------------------------
private Property parseProperty(String arg)
throws IOException {
String[] args;
Property result = null;
if (arg != null && arg.contains("=") && (args = arg.split("=")).length == 2) {
result = new Property(args[0], args[1]);
}
else {
log.error("bad property! Check your format. \nFound: " + arg);
throw new IOException("bad parameter format");
}
return result;
}
//----------------------------------------------------------------------------------------------
public void writeEnvToXml(File file, TerraformContext context)
throws XmlParsingException {
try {
XmlWrite write = new XmlWrite();
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.newDocument();
write.makeXml(context, doc, null);
write.writeDocToFile(file, doc);
}
catch(Exception e) {
throw new XmlParsingException("Exception while writing out XML: " + e.getMessage(), e);
}
}
//----------------------------------------------------------------------------------------------
private void startCredsParser() {
CredentialsParserRegistry.getInstance().register(CredentialsAWS.class.getName(), CredentialsParserAWS.class);
CredentialsParserRegistry.getInstance().register(CredentialsVmware.class.getName(), CredentialsParserVmware.class);
CredentialsParserRegistry.getInstance().register(CredentialsMicrosoft.class.getName(), CredentialsParserMicrosoft.class);
CredentialsParserRegistry.getInstance().register(CredentialsRackspace.class.getName(), CredentialsParserRackspace.class);
CredentialsParserRegistry.getInstance().register(CredentialsVCloud.class.getName(), CredentialsParserVCloud.class);
}
//----------------------------------------------------------------------------------------------
private String parseSuffix(String envName) {
String result = null;
try {
String suffix = envName.substring(envName.length()-4);
if (matchesBase62(suffix)) {
result = suffix;
}
}
catch (IndexOutOfBoundsException e) {
//swallow
}
return result;
}
//----------------------------------------------------------------------------------------------
private boolean matchesBase62(String s) {
return s.matches("\\A\\b[0-9a-zA-Z]+\\b\\Z");
}
}