The third part of the tutorial where I improve a few things. I will not walk through the complete code but highlight a few important points and give you the complete sourcecode at the end.
To recap, my requirements:
- I want to allow users in my company to start and stop instances on their own without them login to AWS console.
- Only specific instances are available to them.
- Avoid using elastic IP’s (you pay for them if they are not assigned)
- Make it configurable
The improvements in this version:
- Remove the hardcoded access keys and place them encrypted in a properties file.
- Only instances that are not protected can be started or stopped.
- Update DynDNS entries from the application
- Some cosmetic cleanup of the control panel
Prerequisites:
- The project from part 1 and 2 (link)
Important points:
- Read from and write to properties files
The file will be located in our domain folder. We encrypt the password and secret access key. - Encryption
- Update DynDNS
This we can avoid paying for unused elastic IP’s since our instances only run occasionally.
I use a tag in the instance properties for the dyndns name
Complete sourcecode:
instances.zul
<?xml version="1.0" encoding="UTF-8"?>
<zk xmlns="http://www.zkoss.org/2005/zul">
<style dynamic="true">
.style1 {
font-family: monospace, courier;font-size: 13px; }
</style>
<window id="list" apply="controller.instancesControllerX" title="ZK EC2 CloudControl 0.2" width="100%">
<listbox id="lstInstance" width="100%" >
<listhead sizable="true">
<listheader label="Instance ID"/>
<listheader label="Name"/>
<listheader label="Public IP" />
<listheader label="DynDNS" />
<listheader label="State" />
<listheader label="Protected" />
<listheader label="Launch Time" />
</listhead>
</listbox>
<separator bar="true"/>
<grid width="100%" >
<columns>
<column label="" width="10%"/>
<column label=""/>
</columns>
<rows>
<row>
<label value="Selected Endpoint" />
<label sclass="style1" id="lblEndpoint"/>
</row>
<row>
<label value="Status:" />
<label sclass="style1" id="lblStatus"/>
</row>
<row>
<button width="120px" id="btnReconnect" label="Reconnect" />
<button width="120px" id="btnRefresh" label="Refresh" />
</row>
<row>
<button width="120px" id="btnStart" label="Start instance" />
<button width="120px" id="btnStop" label="Stop instance" />
</row>
<row>
<label/>
<button width="120px" id="btnDynDNS" label="Update DynDNS" />
</row>
<row>
<button width="120px" id="btnEndPoints" label="Show endpoints" onClick='regions.open(list,"overlap")'/>
<button width="120px" id="btnkeys" label="Settings" onClick='ec2keys.open(list,"overlap")'/>
</row>
</rows>
</grid>
<popup id="regions" width="350px" >
<listbox id="lstRegion" width="100%">
<listhead sizable="true" >
<listheader id="a" label="Region Name"/>
<listheader id="b" label="Region Endpoint" />
</listhead>
</listbox>
<button id="btnSelectEndpoint" label="Select Endpoint" onClick=""/>
<button id="btnCloseEndPoints" label="Close" onClick='regions.close()'/>
</popup>
<popup id="ec2keys" width="450px" >
<grid>
<columns>
<column label="" width="30%"/>
<column label=""/>
</columns>
<rows>
<row>
<label value="AWS Access Key"/>
<textbox id="txtKey" cols="40"/>
</row>
<row>
<label value="AWS Secret Access Key"/>
<textbox id="txtSkey" cols="40" type="password"/>
</row>
<row>
<label value="DynDNS Username"/>
<textbox id="txtDynUser" cols="40"/>
</row>
<row>
<label value="DynDNS Password"/>
<textbox id="txtDynPw" cols="40" type="password"/>
</row>
<row>
<button id="btnSaveKeys" label="Save Settings" />
<button id="btnCloseKeys" label="Close" onClick='ec2keys.close()'/>
</row>
</rows>
</grid>
</popup>
</window>
</zk>
instancesController.java
package controller;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.ec2.AmazonEC2;
import com.amazonaws.services.ec2.AmazonEC2Client;
import com.amazonaws.services.ec2.model.DescribeInstancesResult;
import com.amazonaws.services.ec2.model.DescribeRegionsResult;
import com.amazonaws.services.ec2.model.Instance;
import com.amazonaws.services.ec2.model.InstanceState;
import com.amazonaws.services.ec2.model.Region;
import com.amazonaws.services.ec2.model.Reservation;
import com.amazonaws.services.ec2.model.StartInstancesRequest;
import com.amazonaws.services.ec2.model.StartInstancesResult;
import com.amazonaws.services.ec2.model.StopInstancesRequest;
import com.amazonaws.services.ec2.model.StopInstancesResult;
import com.amazonaws.services.ec2.model.Tag;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESKeySpec;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.EventListener;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.ComposerExt;
import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zul.Button;
import org.zkoss.zul.Label;
import org.zkoss.zul.ListModelList;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listcell;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.ListitemRenderer;
import org.zkoss.zul.Messagebox;
import org.zkoss.zul.Popup;
import org.zkoss.zul.Textbox;
import org.zkoss.zul.Window;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;
public class instancesControllerX extends GenericForwardComposer implements ComposerExt {
// ZK Variables
private Listbox lstInstance;
private Listbox lstRegion;
private Label lblStatus;
private Label lblEndpoint;
private Button btnStart;
private Button btnStop;
private Button btnRefresh;
private Button btnEndPoints;
private Button btnDynDNS;
private Textbox txtKey;
private Textbox txtSkey;
private Textbox txtDynPw;
private Textbox txtDynUser;
private Popup regions;
private Popup ec2keys;
private Window list;
//Amazon Variables
AmazonEC2 ec2;
List<Reservation> listEC2Reservations = null;
List<Instance> listEC2Instances = null;
List<Region> listEC2Regions = null;
//Others
String msgboxTitle = "ZKEC2CloudControl";
String defEndpoint = "ec2.ap-southeast-1.amazonaws.com";
static String crypKey = "somekey";
static String propFile = "ec2cloudcontrol.properties";
String accessKey = "";
String secretAccessKey = "";
String endPoint = "";
String dynDNSuser = "";
String dynDNSpw = "";
boolean ec2Conn = false;
private void initEC2() {
try {
BasicAWSCredentials ecProp = new BasicAWSCredentials(accessKey, secretAccessKey);
ec2 = new AmazonEC2Client(ecProp);
ec2.setEndpoint(endPoint);
// Retrieve the available EC2 regions
DescribeRegionsResult regionsResult = ec2.describeRegions();
listEC2Regions = regionsResult.getRegions();
lstRegion.setModel(new ListModelList(listEC2Regions));
ec2Conn = true;
lblStatus.setValue("Connected.");
btnRefresh.setDisabled(false);
btnEndPoints.setDisabled(false);
} catch (Exception ex) {
ec2Conn = false;
lblStatus.setValue(ex.getMessage());
btnRefresh.setDisabled(true);
btnEndPoints.setDisabled(true);
}
}
@Override
public void doAfterCompose(Component comp) throws Exception {
super.doAfterCompose(comp);
btnStart.setDisabled(true);
btnStop.setDisabled(true);
btnRefresh.setDisabled(true);
if (readProperties()) {
if (endPoint.isEmpty()) {
endPoint = defEndpoint;
}
txtKey.setValue(accessKey);
txtSkey.setValue(secretAccessKey);
txtDynPw.setValue(dynDNSpw);
txtDynUser.setValue(dynDNSuser);
initEC2();
} else {
lblStatus.setValue("Missing AWS keys.");
}
setupRenderer();
lblEndpoint.setValue(endPoint);
if (ec2Conn) {
listReservationsInstances();
}
}
private void listReservationsInstances() {
btnStart.setDisabled(true);
btnStop.setDisabled(true);
btnDynDNS.setDisabled(true);
Clients.showBusy("Retrieving instance information..");
Events.echoEvent("onListReservationsInstances", list, null);
}
public void onListReservationsInstances(Event event) {
DescribeInstancesResult describeInstancesRequest = ec2.describeInstances();
listEC2Reservations = describeInstancesRequest.getReservations();
Set<Instance> instances = new HashSet<Instance>();
for (Reservation reservation : listEC2Reservations) {
instances.addAll(reservation.getInstances());
}
listEC2Instances = new ArrayList<Instance>(instances);
lstInstance.setModel(new ListModelList(listEC2Instances));
lblStatus.setValue("Loaded " + listEC2Instances.size() + " instances.");
Clients.clearBusy();
}
public void onClick$btnRefresh(Event evt) throws InterruptedException {
listReservationsInstances();
lblStatus.setValue("");
}
public void onClick$btnReconnect(Event evt) throws InterruptedException {
lblStatus.setValue("");
listEC2Instances.clear();
lstInstance.setModel(new ListModelList(listEC2Instances));
initEC2();
}
public void onClick$btnSelectEndpoint(Event evt) throws InterruptedException {
if (lstRegion.getSelectedIndex() > -1) {
String newEndpoint = ((Region) lstRegion.getSelectedItem().getValue()).getEndpoint();
ec2.setEndpoint(newEndpoint);
lblEndpoint.setValue(newEndpoint);
listReservationsInstances();
regions.close();
}
}
public void onClick$btnStart(Event evt) throws InterruptedException {
if (lstInstance.getSelectedIndex() > -1) {
Messagebox.show("Start instance <" + ((Instance) lstInstance.getSelectedItem().getValue()).getInstanceId() + "> ?", msgboxTitle, Messagebox.OK | Messagebox.CANCEL,
Messagebox.QUESTION,
new EventListener() {
public void onEvent(Event evt) {
switch (((Integer) evt.getData()).intValue()) {
case Messagebox.OK:
doStart();
break; //the Yes button is pressed
}
}
});
}
}
private void doStart() {
List<String> startIDs = new ArrayList<String>();
startIDs.add(((Instance) lstInstance.getSelectedItem().getValue()).getInstanceId());
StartInstancesRequest start = new StartInstancesRequest(startIDs);
StartInstancesResult result = ec2.startInstances(start);
lblStatus.setValue(result.toString() + " Refresh after 10 or more seconds.");
}
public void onClick$btnStop(Event evt) throws InterruptedException {
if (lstInstance.getSelectedIndex() > -1) {
Messagebox.show("Stop instance <" + ((Instance) lstInstance.getSelectedItem().getValue()).getInstanceId() + "> ?", msgboxTitle, Messagebox.OK | Messagebox.CANCEL,
Messagebox.QUESTION,
new EventListener() {
public void onEvent(Event evt) {
switch (((Integer) evt.getData()).intValue()) {
case Messagebox.OK:
doStop();
break; //the Yes button is pressed
}
}
});
}
}
private void doStop() {
List<String> stopIDs = new ArrayList<String>();
stopIDs.add(((Instance) lstInstance.getSelectedItem().getValue()).getInstanceId());
StopInstancesRequest stop = new StopInstancesRequest(stopIDs);
StopInstancesResult result = ec2.stopInstances(stop);
lblStatus.setValue(result.toString() + " Refresh after 10 or more seconds.");
}
public void onClick$btnSaveKeys(Event evt) throws InterruptedException {
accessKey = txtKey.getValue();
secretAccessKey = txtSkey.getValue();
dynDNSpw = txtDynPw.getValue();
dynDNSuser = txtDynUser.getValue();
if (endPoint.isEmpty()) {
endPoint = defEndpoint;
}
if (writeProperties()) {
lblStatus.setValue("Configuration saved. Please reconnect.");
}
ec2keys.close();
}
public void onClick$btnDynDNS(Event evt) throws InterruptedException {
if (lstInstance.getSelectedIndex() > -1) {
String ip = ((Instance) lstInstance.getSelectedItem().getValue()).getPublicIpAddress();
String dynDNShost = findTagValuebyKey(((Instance) lstInstance.getSelectedItem().getValue()).getTags(), "dyndns");
// only instance with dns hostnames
if (!dynDNShost.toUpperCase().equals("NA")) {
String feedback = updateDynDNS(dynDNShost, ip);
lblStatus.setValue(feedback);
} else {
lblStatus.setValue("No dynDNS hostname configured. Cannot be attached.");
}
}
}
public void onSelect$lstInstance(Event evt) throws InterruptedException {
InstanceState state = ((Instance) lstInstance.getSelectedItem().getValue()).getState();
String prot = findTagValuebyKey(((Instance) lstInstance.getSelectedItem().getValue()).getTags(), "protected");
String dyndns = findTagValuebyKey(((Instance) lstInstance.getSelectedItem().getValue()).getTags(), "dyndns");
btnStart.setDisabled(true);
btnStop.setDisabled(true);
btnDynDNS.setDisabled(true);
if ((state.getCode() == 16) && (prot.equalsIgnoreCase("na"))) {
btnStop.setDisabled(false);
if (!dyndns.equalsIgnoreCase("na"))
btnDynDNS.setDisabled(false);
}
if ((state.getCode() == 80) && (prot.equalsIgnoreCase("na"))) {
btnStart.setDisabled(false);
}
}
private void setupRenderer() {
// EC2 Instances list renderer
ListitemRenderer listRenderInstance = new ListitemRenderer() {
@Override
public void render(Listitem item, Object data) throws Exception {
item.setValue(data);
item.appendChild(new Listcell(((Instance) data).getInstanceId()));
item.appendChild(new Listcell(findTagValuebyKey(((Instance) data).getTags(), "name")));
item.appendChild(new Listcell(((Instance) data).getPublicIpAddress()));
String dyndns = findTagValuebyKey(((Instance) data).getTags(), "dyndns");
if (dyndns.equalsIgnoreCase("na")) {
item.appendChild(new Listcell(" "));
} else {
item.appendChild(new Listcell(dyndns));
}
InstanceState state = ((Instance) data).getState();
item.appendChild(new Listcell(state.getName()));
String prot = findTagValuebyKey(((Instance) data).getTags(), "protected");
if (prot.equalsIgnoreCase("yes")) {
item.appendChild(new Listcell("Yes"));
} else {
item.appendChild(new Listcell(" "));
}
if (state.getCode() == 16) {
item.appendChild(new Listcell(((Instance) data).getLaunchTime().toString()));
} else {
item.appendChild(new Listcell(" "));
}
Listcell listcell = new Listcell();
item.appendChild(listcell);
}
};
lstInstance.setItemRenderer(listRenderInstance);
// EC2 Regions list renderer
ListitemRenderer listitem = new ListitemRenderer() {
@Override
public void render(Listitem item, Object data) throws Exception {
item.setValue(data);
item.appendChild(new Listcell(((Region) data).getRegionName()));
item.appendChild(new Listcell(((Region) data).getEndpoint()));
Listcell listcell = new Listcell();
item.appendChild(listcell);
}
};
lstRegion.setItemRenderer(listitem);
}
// HELPER METHODS
private String findTagValuebyKey(List<Tag> tags, String key) {
for (Tag tag : tags) {
if (tag.getKey().toUpperCase().equals(key.toUpperCase())) {
return tag.getValue();
}
}
return "na";
}
private String encryptString(String inStr) {
try {
DESKeySpec keySpec = new DESKeySpec(crypKey.getBytes("UTF8"));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(keySpec);
sun.misc.BASE64Encoder base64encoder = new BASE64Encoder();
byte[] cleartext = inStr.getBytes("UTF8");
Cipher cipher = Cipher.getInstance("DES"); // cipher is not thread safe
cipher.init(Cipher.ENCRYPT_MODE, key);
String encrypted = base64encoder.encode(cipher.doFinal(cleartext));
return encrypted;
} catch (Exception ex) {
System.out.println(ex.getMessage());
return null;
}
}
private String decryptString(String inStr) {
try {
DESKeySpec keySpec = new DESKeySpec(crypKey.getBytes("UTF8"));
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
SecretKey key = keyFactory.generateSecret(keySpec);
sun.misc.BASE64Decoder base64decoder = new BASE64Decoder();
byte[] encrypedBytes = base64decoder.decodeBuffer(inStr);
Cipher cipher = Cipher.getInstance("DES");
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] plainText = (cipher.doFinal(encrypedBytes));
return new String(plainText);
} catch (Exception ex) {
System.out.println(ex.getMessage());
return null;
}
}
private boolean readProperties() {
Properties properties = new Properties();
try {
properties.load(new FileInputStream(propFile));
accessKey = decryptString(properties.getProperty("accesskey"));
secretAccessKey = decryptString(properties.getProperty("secretaccesskey"));
endPoint = properties.getProperty("endpoint", "");
dynDNSuser = properties.getProperty("dyndnsuser", "");
dynDNSpw = decryptString(properties.getProperty("dyndnspw", ""));
if ((accessKey.isEmpty()) || secretAccessKey.isEmpty()) {
return false;
} else {
return true;
}
} catch (Exception ex) {
System.out.println(ex.getMessage());
return false;
}
}
private boolean writeProperties() {
Properties properties = new Properties();
properties.setProperty("accesskey", encryptString(accessKey));
properties.setProperty("secretaccesskey", encryptString(secretAccessKey));
properties.setProperty("endpoint", endPoint);
properties.setProperty("dyndnspw", encryptString(dynDNSpw));
properties.setProperty("dyndnsuser", dynDNSuser);
try {
properties.store(new FileOutputStream(propFile), null);
return true;
} catch (Exception ex) {
System.out.println(ex.getMessage());
return false;
}
}
private String updateDynDNS(String hostName, String hostIP) {
String userName = dynDNSuser;
String userPassword = dynDNSpw;
int responseCode;
String feedback = "";
try {
// Encode username and password
BASE64Encoder enc = new sun.misc.BASE64Encoder();
String userpassword = userName + ":" + userPassword;
String encodedAuthorization = enc.encode(userpassword.getBytes());
// Connect to DynDNS
URL url = new URL("http://members.dyndns.org/nic/update?hostname=" + hostName + "&myip=" + hostIP);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
connection.setRequestMethod("GET");
connection.setRequestProperty("User-Agent", "DynDNS Updater");
connection.setRequestProperty("Authorization", "Basic " + encodedAuthorization);
// Execute GET
responseCode = connection.getResponseCode();
System.out.println(responseCode + ":" + connection.getResponseMessage());
// Print feedback
String line;
InputStreamReader in = new InputStreamReader((InputStream) connection.getContent());
BufferedReader buff = new BufferedReader(in);
do {
line = buff.readLine();
if (line != null) {
feedback = line;
}
System.out.println(line);
} while (line != null);
connection.disconnect();
} catch (Exception ex) {
System.out.println(ex.getMessage());
feedback = ex.getMessage();
}
return feedback;
}
}
Remarks:
- I still like to add a EJB timer to start and stop the instances automatically
- The sorting is still random
Advertisement






