//
// 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.network.resource;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import javax.naming.ConfigurationException;

import org.apache.log4j.Logger;

import com.cloud.agent.IAgentControl;
import com.cloud.agent.api.Answer;
import com.cloud.agent.api.Command;
import com.cloud.agent.api.ConfigurePortForwardingRulesOnLogicalRouterAnswer;
import com.cloud.agent.api.ConfigurePortForwardingRulesOnLogicalRouterCommand;
import com.cloud.agent.api.ConfigurePublicIpsOnLogicalRouterAnswer;
import com.cloud.agent.api.ConfigurePublicIpsOnLogicalRouterCommand;
import com.cloud.agent.api.ConfigureStaticNatRulesOnLogicalRouterAnswer;
import com.cloud.agent.api.ConfigureStaticNatRulesOnLogicalRouterCommand;
import com.cloud.agent.api.CreateLogicalRouterAnswer;
import com.cloud.agent.api.CreateLogicalRouterCommand;
import com.cloud.agent.api.CreateLogicalSwitchAnswer;
import com.cloud.agent.api.CreateLogicalSwitchCommand;
import com.cloud.agent.api.CreateLogicalSwitchPortAnswer;
import com.cloud.agent.api.CreateLogicalSwitchPortCommand;
import com.cloud.agent.api.DeleteLogicalRouterAnswer;
import com.cloud.agent.api.DeleteLogicalRouterCommand;
import com.cloud.agent.api.DeleteLogicalSwitchAnswer;
import com.cloud.agent.api.DeleteLogicalSwitchCommand;
import com.cloud.agent.api.DeleteLogicalSwitchPortAnswer;
import com.cloud.agent.api.DeleteLogicalSwitchPortCommand;
import com.cloud.agent.api.FindLogicalSwitchPortAnswer;
import com.cloud.agent.api.FindLogicalSwitchPortCommand;
import com.cloud.agent.api.MaintainAnswer;
import com.cloud.agent.api.MaintainCommand;
import com.cloud.agent.api.PingCommand;
import com.cloud.agent.api.ReadyAnswer;
import com.cloud.agent.api.ReadyCommand;
import com.cloud.agent.api.StartupCommand;
import com.cloud.agent.api.StartupNiciraNvpCommand;
import com.cloud.agent.api.UpdateLogicalSwitchPortAnswer;
import com.cloud.agent.api.UpdateLogicalSwitchPortCommand;
import com.cloud.agent.api.to.PortForwardingRuleTO;
import com.cloud.agent.api.to.StaticNatRuleTO;
import com.cloud.host.Host;
import com.cloud.host.Host.Type;
import com.cloud.network.nicira.ControlClusterStatus;
import com.cloud.network.nicira.DestinationNatRule;
import com.cloud.network.nicira.L3GatewayAttachment;
import com.cloud.network.nicira.LogicalRouter;
import com.cloud.network.nicira.LogicalRouterPort;
import com.cloud.network.nicira.LogicalSwitch;
import com.cloud.network.nicira.LogicalSwitchPort;
import com.cloud.network.nicira.Match;
import com.cloud.network.nicira.NatRule;
import com.cloud.network.nicira.NiciraNvpApi;
import com.cloud.network.nicira.NiciraNvpApiException;
import com.cloud.network.nicira.NiciraNvpList;
import com.cloud.network.nicira.NiciraNvpTag;
import com.cloud.network.nicira.PatchAttachment;
import com.cloud.network.nicira.RouterNextHop;
import com.cloud.network.nicira.SingleDefaultRouteImplicitRoutingConfig;
import com.cloud.network.nicira.SourceNatRule;
import com.cloud.network.nicira.TransportZoneBinding;
import com.cloud.network.nicira.VifAttachment;
import com.cloud.resource.ServerResource;

public class NiciraNvpResource implements ServerResource {
    private static final int NAME_MAX_LEN = 40;

    private static final Logger s_logger = Logger.getLogger(NiciraNvpResource.class);

    private String name;
    private String guid;
    private String zoneId;
    private int numRetries;

    private NiciraNvpApi niciraNvpApi;

    protected NiciraNvpApi createNiciraNvpApi() {
        return new NiciraNvpApi();
    }

    @Override
    public boolean configure(String ignoredName, final Map<String, Object> params) throws ConfigurationException {

        name = (String)params.get("name");
        if (name == null) {
            throw new ConfigurationException("Unable to find name");
        }

        guid = (String)params.get("guid");
        if (guid == null) {
            throw new ConfigurationException("Unable to find the guid");
        }

        zoneId = (String)params.get("zoneId");
        if (zoneId == null) {
            throw new ConfigurationException("Unable to find zone");
        }

        numRetries = 2;

        String ip = (String)params.get("ip");
        if (ip == null) {
            throw new ConfigurationException("Unable to find IP");
        }

        String adminuser = (String)params.get("adminuser");
        if (adminuser == null) {
            throw new ConfigurationException("Unable to find admin username");
        }

        String adminpass = (String)params.get("adminpass");
        if (adminpass == null) {
            throw new ConfigurationException("Unable to find admin password");
        }

        niciraNvpApi = createNiciraNvpApi();
        niciraNvpApi.setControllerAddress(ip);
        niciraNvpApi.setAdminCredentials(adminuser, adminpass);

        return true;
    }

    @Override
    public boolean start() {
        return true;
    }

    @Override
    public boolean stop() {
        return true;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Type getType() {
        // Think up a better name for this Type?
        return Host.Type.L2Networking;
    }

    @Override
    public StartupCommand[] initialize() {
        StartupNiciraNvpCommand sc = new StartupNiciraNvpCommand();
        sc.setGuid(guid);
        sc.setName(name);
        sc.setDataCenter(zoneId);
        sc.setPod("");
        sc.setPrivateIpAddress("");
        sc.setStorageIpAddress("");
        sc.setVersion(NiciraNvpResource.class.getPackage().getImplementationVersion());
        return new StartupCommand[] {sc};
    }

    @Override
    public PingCommand getCurrentStatus(final long id) {
        try {
            ControlClusterStatus ccs = niciraNvpApi.getControlClusterStatus();
            if (!"stable".equals(ccs.getClusterStatus())) {
                s_logger.error("ControlCluster state is not stable: " + ccs.getClusterStatus());
                return null;
            }
        } catch (NiciraNvpApiException e) {
            s_logger.error("getControlClusterStatus failed", e);
            return null;
        }
        return new PingCommand(Host.Type.L2Networking, id);
    }

    @Override
    public Answer executeRequest(final Command cmd) {
        return executeRequest(cmd, numRetries);
    }

    public Answer executeRequest(final Command cmd, final int numRetries) {
        if (cmd instanceof ReadyCommand) {
            return executeRequest((ReadyCommand)cmd);
        } else if (cmd instanceof MaintainCommand) {
            return executeRequest((MaintainCommand)cmd);
        } else if (cmd instanceof CreateLogicalSwitchCommand) {
            return executeRequest((CreateLogicalSwitchCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteLogicalSwitchCommand) {
            return executeRequest((DeleteLogicalSwitchCommand)cmd, numRetries);
        } else if (cmd instanceof CreateLogicalSwitchPortCommand) {
            return executeRequest((CreateLogicalSwitchPortCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteLogicalSwitchPortCommand) {
            return executeRequest((DeleteLogicalSwitchPortCommand)cmd, numRetries);
        } else if (cmd instanceof UpdateLogicalSwitchPortCommand) {
            return executeRequest((UpdateLogicalSwitchPortCommand)cmd, numRetries);
        } else if (cmd instanceof FindLogicalSwitchPortCommand) {
            return executeRequest((FindLogicalSwitchPortCommand)cmd, numRetries);
        } else if (cmd instanceof CreateLogicalRouterCommand) {
            return executeRequest((CreateLogicalRouterCommand)cmd, numRetries);
        } else if (cmd instanceof DeleteLogicalRouterCommand) {
            return executeRequest((DeleteLogicalRouterCommand)cmd, numRetries);
        } else if (cmd instanceof ConfigureStaticNatRulesOnLogicalRouterCommand) {
            return executeRequest((ConfigureStaticNatRulesOnLogicalRouterCommand)cmd, numRetries);
        } else if (cmd instanceof ConfigurePortForwardingRulesOnLogicalRouterCommand) {
            return executeRequest((ConfigurePortForwardingRulesOnLogicalRouterCommand)cmd, numRetries);
        } else if (cmd instanceof ConfigurePublicIpsOnLogicalRouterCommand) {
            return executeRequest((ConfigurePublicIpsOnLogicalRouterCommand)cmd, numRetries);
        }
        s_logger.debug("Received unsupported command " + cmd.toString());
        return Answer.createUnsupportedCommandAnswer(cmd);
    }

    @Override
    public void disconnected() {
    }

    @Override
    public IAgentControl getAgentControl() {
        return null;
    }

    @Override
    public void setAgentControl(final IAgentControl agentControl) {
    }

    private Answer executeRequest(final CreateLogicalSwitchCommand cmd, int numRetries) {
        LogicalSwitch logicalSwitch = new LogicalSwitch();
        logicalSwitch.setDisplayName(truncate("lswitch-" + cmd.getName(), NAME_MAX_LEN));
        logicalSwitch.setPortIsolationEnabled(false);

        // Set transport binding
        List<TransportZoneBinding> ltzb = new ArrayList<TransportZoneBinding>();
        ltzb.add(new TransportZoneBinding(cmd.getTransportUuid(), cmd.getTransportType()));
        logicalSwitch.setTransportZones(ltzb);

        // Tags set to scope cs_account and account name
        List<NiciraNvpTag> tags = new ArrayList<NiciraNvpTag>();
        tags.add(new NiciraNvpTag("cs_account", cmd.getOwnerName()));
        logicalSwitch.setTags(tags);

        try {
            logicalSwitch = niciraNvpApi.createLogicalSwitch(logicalSwitch);
            return new CreateLogicalSwitchAnswer(cmd, true, "Logicalswitch " + logicalSwitch.getUuid() + " created", logicalSwitch.getUuid());
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new CreateLogicalSwitchAnswer(cmd, e);
            }
        }

    }

    private Answer executeRequest(final DeleteLogicalSwitchCommand cmd, int numRetries) {
        try {
            niciraNvpApi.deleteLogicalSwitch(cmd.getLogicalSwitchUuid());
            return new DeleteLogicalSwitchAnswer(cmd, true, "Logicalswitch " + cmd.getLogicalSwitchUuid() + " deleted");
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new DeleteLogicalSwitchAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final CreateLogicalSwitchPortCommand cmd, int numRetries) {
        String logicalSwitchUuid = cmd.getLogicalSwitchUuid();
        String attachmentUuid = cmd.getAttachmentUuid();

        try {
            // Tags set to scope cs_account and account name
            List<NiciraNvpTag> tags = new ArrayList<NiciraNvpTag>();
            tags.add(new NiciraNvpTag("cs_account", cmd.getOwnerName()));

            LogicalSwitchPort logicalSwitchPort = new LogicalSwitchPort(attachmentUuid, tags, true);
            LogicalSwitchPort newPort = niciraNvpApi.createLogicalSwitchPort(logicalSwitchUuid, logicalSwitchPort);
            try {
                niciraNvpApi.updateLogicalSwitchPortAttachment(cmd.getLogicalSwitchUuid(), newPort.getUuid(), new VifAttachment(attachmentUuid));
            } catch (NiciraNvpApiException ex) {
                s_logger.warn("modifyLogicalSwitchPort failed after switchport was created, removing switchport");
                niciraNvpApi.deleteLogicalSwitchPort(cmd.getLogicalSwitchUuid(), newPort.getUuid());
                throw (ex); // Rethrow the original exception
            }
            return new CreateLogicalSwitchPortAnswer(cmd, true, "Logical switch port " + newPort.getUuid() + " created", newPort.getUuid());
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new CreateLogicalSwitchPortAnswer(cmd, e);
            }
        }

    }

    private Answer executeRequest(final DeleteLogicalSwitchPortCommand cmd, int numRetries) {
        try {
            niciraNvpApi.deleteLogicalSwitchPort(cmd.getLogicalSwitchUuid(), cmd.getLogicalSwitchPortUuid());
            return new DeleteLogicalSwitchPortAnswer(cmd, true, "Logical switch port " + cmd.getLogicalSwitchPortUuid() + " deleted");
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new DeleteLogicalSwitchPortAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final UpdateLogicalSwitchPortCommand cmd, int numRetries) {
        String logicalSwitchUuid = cmd.getLogicalSwitchUuid();
        String logicalSwitchPortUuid = cmd.getLogicalSwitchPortUuid();
        String attachmentUuid = cmd.getAttachmentUuid();

        try {
            // Tags set to scope cs_account and account name
            List<NiciraNvpTag> tags = new ArrayList<NiciraNvpTag>();
            tags.add(new NiciraNvpTag("cs_account", cmd.getOwnerName()));

            niciraNvpApi.updateLogicalSwitchPortAttachment(logicalSwitchUuid, logicalSwitchPortUuid, new VifAttachment(attachmentUuid));
            return new UpdateLogicalSwitchPortAnswer(cmd, true, "Attachment for  " + logicalSwitchPortUuid + " updated", logicalSwitchPortUuid);
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new UpdateLogicalSwitchPortAnswer(cmd, e);
            }
        }

    }

    private Answer executeRequest(final FindLogicalSwitchPortCommand cmd, int numRetries) {
        String logicalSwitchUuid = cmd.getLogicalSwitchUuid();
        String logicalSwitchPortUuid = cmd.getLogicalSwitchPortUuid();

        try {
            NiciraNvpList<LogicalSwitchPort> ports = niciraNvpApi.findLogicalSwitchPortsByUuid(logicalSwitchUuid, logicalSwitchPortUuid);
            if (ports.getResultCount() == 0) {
                return new FindLogicalSwitchPortAnswer(cmd, false, "Logical switchport " + logicalSwitchPortUuid + " not found", null);
            } else {
                return new FindLogicalSwitchPortAnswer(cmd, true, "Logical switchport " + logicalSwitchPortUuid + " found", logicalSwitchPortUuid);
            }
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new FindLogicalSwitchPortAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final CreateLogicalRouterCommand cmd, int numRetries) {
        String routerName = cmd.getName();
        String gatewayServiceUuid = cmd.getGatewayServiceUuid();
        String logicalSwitchUuid = cmd.getLogicalSwitchUuid();

        List<NiciraNvpTag> tags = new ArrayList<NiciraNvpTag>();
        tags.add(new NiciraNvpTag("cs_account", cmd.getOwnerName()));

        String publicNetworkNextHopIp = cmd.getPublicNextHop();
        String publicNetworkIpAddress = cmd.getPublicIpCidr();
        String internalNetworkAddress = cmd.getInternalIpCidr();

        s_logger.debug("Creating a logical router with external ip " + publicNetworkIpAddress + " and internal ip " + internalNetworkAddress + "on gateway service " +
            gatewayServiceUuid);

        try {
            // Create the Router
            LogicalRouter lrc = new LogicalRouter();
            lrc.setDisplayName(truncate(routerName, NAME_MAX_LEN));
            lrc.setTags(tags);
            lrc.setRoutingConfig(new SingleDefaultRouteImplicitRoutingConfig(new RouterNextHop(publicNetworkNextHopIp)));
            lrc = niciraNvpApi.createLogicalRouter(lrc);

            // store the switchport for rollback
            LogicalSwitchPort lsp = null;

            try {
                // Create the outside port for the router
                LogicalRouterPort lrpo = new LogicalRouterPort();
                lrpo.setAdminStatusEnabled(true);
                lrpo.setDisplayName(truncate(routerName + "-outside-port", NAME_MAX_LEN));
                lrpo.setTags(tags);
                List<String> outsideIpAddresses = new ArrayList<String>();
                outsideIpAddresses.add(publicNetworkIpAddress);
                lrpo.setIpAddresses(outsideIpAddresses);
                lrpo = niciraNvpApi.createLogicalRouterPort(lrc.getUuid(), lrpo);

                // Attach the outside port to the gateway service on the correct VLAN
                L3GatewayAttachment attachment = new L3GatewayAttachment(gatewayServiceUuid);
                if (cmd.getVlanId() != 0) {
                    attachment.setVlanId(cmd.getVlanId());
                }
                niciraNvpApi.updateLogicalRouterPortAttachment(lrc.getUuid(), lrpo.getUuid(), attachment);

                // Create the inside port for the router
                LogicalRouterPort lrpi = new LogicalRouterPort();
                lrpi.setAdminStatusEnabled(true);
                lrpi.setDisplayName(truncate(routerName + "-inside-port", NAME_MAX_LEN));
                lrpi.setTags(tags);
                List<String> insideIpAddresses = new ArrayList<String>();
                insideIpAddresses.add(internalNetworkAddress);
                lrpi.setIpAddresses(insideIpAddresses);
                lrpi = niciraNvpApi.createLogicalRouterPort(lrc.getUuid(), lrpi);

                // Create the inside port on the lswitch
                lsp = new LogicalSwitchPort(truncate(routerName + "-inside-port", NAME_MAX_LEN), tags, true);
                lsp = niciraNvpApi.createLogicalSwitchPort(logicalSwitchUuid, lsp);

                // Attach the inside router port to the lswitch port with a PatchAttachment
                niciraNvpApi.updateLogicalRouterPortAttachment(lrc.getUuid(), lrpi.getUuid(), new PatchAttachment(lsp.getUuid()));

                // Attach the inside lswitch port to the router with a PatchAttachment
                niciraNvpApi.updateLogicalSwitchPortAttachment(logicalSwitchUuid, lsp.getUuid(), new PatchAttachment(lrpi.getUuid()));

                // Setup the source nat rule
                SourceNatRule snr = new SourceNatRule();
                snr.setToSourceIpAddressMin(publicNetworkIpAddress.split("/")[0]);
                snr.setToSourceIpAddressMax(publicNetworkIpAddress.split("/")[0]);
                Match match = new Match();
                match.setSourceIpAddresses(internalNetworkAddress);
                snr.setMatch(match);
                snr.setOrder(200);
                niciraNvpApi.createLogicalRouterNatRule(lrc.getUuid(), snr);
            } catch (NiciraNvpApiException e) {
                // We need to destroy the router if we already created it
                // this will also take care of any router ports and rules
                try {
                    niciraNvpApi.deleteLogicalRouter(lrc.getUuid());
                    if (lsp != null) {
                        niciraNvpApi.deleteLogicalSwitchPort(logicalSwitchUuid, lsp.getUuid());
                    }
                } catch (NiciraNvpApiException ex) {
                }

                throw e;
            }

            return new CreateLogicalRouterAnswer(cmd, true, "Logical Router created (uuid " + lrc.getUuid() + ")", lrc.getUuid());
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new CreateLogicalRouterAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final DeleteLogicalRouterCommand cmd, int numRetries) {
        try {
            niciraNvpApi.deleteLogicalRouter(cmd.getLogicalRouterUuid());
            return new DeleteLogicalRouterAnswer(cmd, true, "Logical Router deleted (uuid " + cmd.getLogicalRouterUuid() + ")");
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new DeleteLogicalRouterAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final ConfigurePublicIpsOnLogicalRouterCommand cmd, int numRetries) {
        try {
            NiciraNvpList<LogicalRouterPort> ports = niciraNvpApi.findLogicalRouterPortByGatewayServiceUuid(cmd.getLogicalRouterUuid(), cmd.getL3GatewayServiceUuid());
            if (ports.getResultCount() != 1) {
                return new ConfigurePublicIpsOnLogicalRouterAnswer(cmd, false, "No logical router ports found, unable to set ip addresses");
            }
            LogicalRouterPort lrp = ports.getResults().get(0);
            lrp.setIpAddresses(cmd.getPublicCidrs());
            niciraNvpApi.updateLogicalRouterPort(cmd.getLogicalRouterUuid(), lrp);

            return new ConfigurePublicIpsOnLogicalRouterAnswer(cmd, true, "Configured " + cmd.getPublicCidrs().size() + " ip addresses on logical router uuid " +
                cmd.getLogicalRouterUuid());
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new ConfigurePublicIpsOnLogicalRouterAnswer(cmd, e);
            }
        }

    }

    private Answer executeRequest(final ConfigureStaticNatRulesOnLogicalRouterCommand cmd, int numRetries) {
        try {
            NiciraNvpList<NatRule> existingRules = niciraNvpApi.findNatRulesByLogicalRouterUuid(cmd.getLogicalRouterUuid());
            // Rules of the game (also known as assumptions-that-will-make-stuff-break-later-on)
            // A SourceNat rule with a match other than a /32 cidr is assumed to be the "main" SourceNat rule
            // Any other SourceNat rule should have a corresponding DestinationNat rule

            for (StaticNatRuleTO rule : cmd.getRules()) {

                NatRule[] rulepair = generateStaticNatRulePair(rule.getDstIp(), rule.getSrcIp());

                NatRule incoming = null;
                NatRule outgoing = null;

                for (NatRule storedRule : existingRules.getResults()) {
                    if (storedRule.equalsIgnoreUuid(rulepair[1])) {
                        // The outgoing rule exists
                        outgoing = storedRule;
                        s_logger.debug("Found matching outgoing rule " + outgoing.getUuid());
                        if (incoming != null) {
                            break;
                        }
                    } else if (storedRule.equalsIgnoreUuid(rulepair[0])) {
                        // The incoming rule exists
                        incoming = storedRule;
                        s_logger.debug("Found matching incoming rule " + incoming.getUuid());
                        if (outgoing != null) {
                            break;
                        }
                    }
                }
                if (incoming != null && outgoing != null) {
                    if (rule.revoked()) {
                        s_logger.debug("Deleting incoming rule " + incoming.getUuid());
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), incoming.getUuid());

                        s_logger.debug("Deleting outgoing rule " + outgoing.getUuid());
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), outgoing.getUuid());
                    }
                } else {
                    if (rule.revoked()) {
                        s_logger.warn("Tried deleting a rule that does not exist, " + rule.getSrcIp() + " -> " + rule.getDstIp());
                        break;
                    }

                    rulepair[0] = niciraNvpApi.createLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[0]);
                    s_logger.debug("Created " + natRuleToString(rulepair[0]));

                    try {
                        rulepair[1] = niciraNvpApi.createLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[1]);
                        s_logger.debug("Created " + natRuleToString(rulepair[1]));
                    } catch (NiciraNvpApiException ex) {
                        s_logger.debug("Failed to create SourceNatRule, rolling back DestinationNatRule");
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[0].getUuid());
                        throw ex; // Rethrow original exception
                    }

                }
            }
            return new ConfigureStaticNatRulesOnLogicalRouterAnswer(cmd, true, cmd.getRules().size() + " StaticNat rules applied");
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new ConfigureStaticNatRulesOnLogicalRouterAnswer(cmd, e);
            }
        }
    }

    private Answer executeRequest(final ConfigurePortForwardingRulesOnLogicalRouterCommand cmd, int numRetries) {
        try {
            NiciraNvpList<NatRule> existingRules = niciraNvpApi.findNatRulesByLogicalRouterUuid(cmd.getLogicalRouterUuid());
            // Rules of the game (also known as assumptions-that-will-make-stuff-break-later-on)
            // A SourceNat rule with a match other than a /32 cidr is assumed to be the "main" SourceNat rule
            // Any other SourceNat rule should have a corresponding DestinationNat rule

            for (PortForwardingRuleTO rule : cmd.getRules()) {
                if (rule.isAlreadyAdded() && !rule.revoked()) {
                    // Don't need to do anything
                    continue;
                }

                if (rule.getDstPortRange()[0] != rule.getDstPortRange()[1] || rule.getSrcPortRange()[0] != rule.getSrcPortRange()[1]) {
                    return new ConfigurePortForwardingRulesOnLogicalRouterAnswer(cmd, false, "Nicira NVP doesn't support port ranges for port forwarding");
                }

                NatRule[] rulepair = generatePortForwardingRulePair(rule.getDstIp(), rule.getDstPortRange(), rule.getSrcIp(), rule.getSrcPortRange(), rule.getProtocol());

                NatRule incoming = null;
                NatRule outgoing = null;

                for (NatRule storedRule : existingRules.getResults()) {
                    if (storedRule.equalsIgnoreUuid(rulepair[1])) {
                        // The outgoing rule exists
                        outgoing = storedRule;
                        s_logger.debug("Found matching outgoing rule " + outgoing.getUuid());
                        if (incoming != null) {
                            break;
                        }
                    } else if (storedRule.equalsIgnoreUuid(rulepair[0])) {
                        // The incoming rule exists
                        incoming = storedRule;
                        s_logger.debug("Found matching incoming rule " + incoming.getUuid());
                        if (outgoing != null) {
                            break;
                        }
                    }
                }
                if (incoming != null && outgoing != null) {
                    if (rule.revoked()) {
                        s_logger.debug("Deleting incoming rule " + incoming.getUuid());
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), incoming.getUuid());

                        s_logger.debug("Deleting outgoing rule " + outgoing.getUuid());
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), outgoing.getUuid());
                    }
                } else {
                    if (rule.revoked()) {
                        s_logger.warn("Tried deleting a rule that does not exist, " + rule.getSrcIp() + " -> " + rule.getDstIp());
                        break;
                    }

                    rulepair[0] = niciraNvpApi.createLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[0]);
                    s_logger.debug("Created " + natRuleToString(rulepair[0]));

                    try {
                        rulepair[1] = niciraNvpApi.createLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[1]);
                        s_logger.debug("Created " + natRuleToString(rulepair[1]));
                    } catch (NiciraNvpApiException ex) {
                        s_logger.warn("NiciraNvpApiException during create call, rolling back previous create");
                        niciraNvpApi.deleteLogicalRouterNatRule(cmd.getLogicalRouterUuid(), rulepair[0].getUuid());
                        throw ex; // Rethrow the original exception
                    }

                }
            }
            return new ConfigurePortForwardingRulesOnLogicalRouterAnswer(cmd, true, cmd.getRules().size() + " PortForwarding rules applied");
        } catch (NiciraNvpApiException e) {
            if (numRetries > 0) {
                return retry(cmd, --numRetries);
            } else {
                return new ConfigurePortForwardingRulesOnLogicalRouterAnswer(cmd, e);
            }
        }

    }

    private Answer executeRequest(final ReadyCommand cmd) {
        return new ReadyAnswer(cmd);
    }

    private Answer executeRequest(final MaintainCommand cmd) {
        return new MaintainAnswer(cmd);
    }

    private Answer retry(final Command cmd, final int numRetries) {
        s_logger.warn("Retrying " + cmd.getClass().getSimpleName() + ". Number of retries remaining: " + numRetries);
        return executeRequest(cmd, numRetries);
    }

    private String natRuleToString(final NatRule rule) {

        StringBuilder natRuleStr = new StringBuilder();
        natRuleStr.append("Rule ");
        natRuleStr.append(rule.getUuid());
        natRuleStr.append(" (");
        natRuleStr.append(rule.getType());
        natRuleStr.append(") :");
        Match m = rule.getMatch();
        natRuleStr.append("match (");
        natRuleStr.append(m.getProtocol());
        natRuleStr.append(" ");
        natRuleStr.append(m.getSourceIpAddresses());
        natRuleStr.append(" [");
        natRuleStr.append(m.getSourcePort());
        natRuleStr.append(" ] -> ");
        natRuleStr.append(m.getDestinationIpAddresses());
        natRuleStr.append(" [");
        natRuleStr.append(m.getDestinationPort());
        natRuleStr.append(" ]) -->");
        if ("SourceNatRule".equals(rule.getType())) {
            natRuleStr.append(((SourceNatRule)rule).getToSourceIpAddressMin());
            natRuleStr.append("-");
            natRuleStr.append(((SourceNatRule)rule).getToSourceIpAddressMax());
            natRuleStr.append(" [");
            natRuleStr.append(((SourceNatRule)rule).getToSourcePort());
            natRuleStr.append(" ])");
        } else {
            natRuleStr.append(((DestinationNatRule)rule).getToDestinationIpAddress());
            natRuleStr.append(" [");
            natRuleStr.append(((DestinationNatRule)rule).getToDestinationPort());
            natRuleStr.append(" ])");
        }
        return natRuleStr.toString();
    }

    private String truncate(final String string, final int length) {
        if (string.length() <= length) {
            return string;
        } else {
            return string.substring(0, length);
        }
    }

    protected NatRule[] generateStaticNatRulePair(final String insideIp, final String outsideIp) {
        NatRule[] rulepair = new NatRule[2];
        rulepair[0] = new DestinationNatRule();
        rulepair[0].setType("DestinationNatRule");
        rulepair[0].setOrder(100);
        rulepair[1] = new SourceNatRule();
        rulepair[1].setType("SourceNatRule");
        rulepair[1].setOrder(100);

        Match m = new Match();
        m.setDestinationIpAddresses(outsideIp);
        rulepair[0].setMatch(m);
        ((DestinationNatRule)rulepair[0]).setToDestinationIpAddress(insideIp);

        // create matching snat rule
        m = new Match();
        m.setSourceIpAddresses(insideIp);
        rulepair[1].setMatch(m);
        ((SourceNatRule)rulepair[1]).setToSourceIpAddressMin(outsideIp);
        ((SourceNatRule)rulepair[1]).setToSourceIpAddressMax(outsideIp);

        return rulepair;

    }

    protected NatRule[] generatePortForwardingRulePair(final String insideIp, final int[] insidePorts, final String outsideIp, final int[] outsidePorts,
        final String protocol) {
        // Start with a basic static nat rule, then add port and protocol details
        NatRule[] rulepair = generateStaticNatRulePair(insideIp, outsideIp);

        ((DestinationNatRule)rulepair[0]).setToDestinationPort(insidePorts[0]);
        rulepair[0].getMatch().setDestinationPort(outsidePorts[0]);
        rulepair[0].setOrder(50);
        rulepair[0].getMatch().setEthertype("IPv4");
        if ("tcp".equals(protocol)) {
            rulepair[0].getMatch().setProtocol(6);
        } else if ("udp".equals(protocol)) {
            rulepair[0].getMatch().setProtocol(17);
        }

        ((SourceNatRule)rulepair[1]).setToSourcePort(outsidePorts[0]);
        rulepair[1].getMatch().setSourcePort(insidePorts[0]);
        rulepair[1].setOrder(50);
        rulepair[1].getMatch().setEthertype("IPv4");
        if ("tcp".equals(protocol)) {
            rulepair[1].getMatch().setProtocol(6);
        } else if ("udp".equals(protocol)) {
            rulepair[1].getMatch().setProtocol(17);
        }

        return rulepair;

    }

    @Override
    public void setName(final String name) {
        // TODO Auto-generated method stub
    }

    @Override
    public void setConfigParams(final Map<String, Object> params) {
        // TODO Auto-generated method stub
    }

    @Override
    public Map<String, Object> getConfigParams() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public int getRunLevel() {
        // TODO Auto-generated method stub
        return 0;
    }

    @Override
    public void setRunLevel(final int level) {
        // TODO Auto-generated method stub
    }

}
