ZK goes EC2 (Part 3)

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

Running Application

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.

    Properties File

    properties

  • Encryption

    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

    DynDNS Hostname

    AWS instance tag

    Update Method

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
Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s