##! OpenFlow plugin for the Ryu controller.

@load base/frameworks/openflow
@load base/utils/active-http
@load base/utils/exec

module OpenFlow;

export {
	redef enum Plugin += {
		RYU,
	};

	## Ryu controller constructor.
	##
	## host: Controller ip.
	##
	## host_port: Controller listen port.
	##
	## dpid: OpenFlow switch datapath id.
	##
	## Returns: OpenFlow::Controller record.
	global ryu_new: function(host: addr, host_port: count, dpid: count): OpenFlow::Controller;

	redef record ControllerState += {
		## Controller ip.
		ryu_host: addr &optional;
		## Controller listen port.
		ryu_port: count &optional;
		## OpenFlow switch datapath id.
		ryu_dpid: count &optional;
		## Enable debug mode - output JSON to stdout; do not perform actions.
		ryu_debug: bool &default=F;
	};
}

# Ryu ReST API flow_mod URL-path
const RYU_FLOWENTRY_PATH = "/stats/flowentry/";
# Ryu ReST API flow_stats URL-path
#const RYU_FLOWSTATS_PATH = "/stats/flow/";

# Ryu ReST API action_output type.
type ryu_flow_action: record {
	# Ryu uses strings as its ReST API output action.
	_type: string;
	# The output port for type OUTPUT
	_port: count &optional;
};

# The ReST API documentation can be found at
# https://media.readthedocs.org/pdf/ryu/latest/ryu.pdf
# Ryu ReST API flow_mod type.
type ryu_ofp_flow_mod: record {
	dpid: count;
	cookie: count &optional;
	cookie_mask: count &optional;
	table_id: count &optional;
	idle_timeout: count &optional;
	hard_timeout: count &optional;
	priority: count &optional;
	flags: count &optional;
	match: OpenFlow::ofp_match;
	actions: vector of ryu_flow_action;
	out_port: count &optional;
	out_group: count &optional;
};

# Mapping between ofp flow mod commands and ryu urls
const ryu_url: table[ofp_flow_mod_command] of string = {
	[OFPFC_ADD] = "add",
	[OFPFC_MODIFY] = "modify",
	[OFPFC_MODIFY_STRICT] = "modify_strict",
	[OFPFC_DELETE] = "delete",
	[OFPFC_DELETE_STRICT] = "delete_strict",
};

# Ryu flow_mod function
function ryu_flow_mod(state: OpenFlow::ControllerState, match: ofp_match, flow_mod: OpenFlow::ofp_flow_mod): bool
	{
	if ( state$_plugin != RYU )
		{
		Reporter::error("Ryu openflow plugin was called with state of non-ryu plugin");
		return F;
		}

	# Generate ryu_flow_actions because their type differs (using strings as type).
	local flow_actions: vector of ryu_flow_action = vector();

	for ( i in flow_mod$actions$out_ports )
		flow_actions += ryu_flow_action($_type="OUTPUT", $_port=flow_mod$actions$out_ports[i]);

	# Generate our ryu_flow_mod record for the ReST API call.
	local mod: ryu_ofp_flow_mod = ryu_ofp_flow_mod(
		$dpid=state$ryu_dpid,
		$cookie=flow_mod$cookie,
		$idle_timeout=flow_mod$idle_timeout,
		$hard_timeout=flow_mod$hard_timeout,
		$priority=flow_mod$priority,
		$flags=flow_mod$flags,
		$match=match,
		$actions=flow_actions
	);

	if ( flow_mod?$out_port )
		mod$out_port = flow_mod$out_port;
	if ( flow_mod?$out_group )
		mod$out_group = flow_mod$out_group;

	# Type of the command
	local command_type: string;

	if ( flow_mod$command in ryu_url )
		command_type = ryu_url[flow_mod$command];
	else
			{
			Reporter::warning(fmt("The given OpenFlow command type '%s' is not available", cat(flow_mod$command)));
			return F;
			}

	local url=cat("http://", cat(state$ryu_host), ":", cat(state$ryu_port), RYU_FLOWENTRY_PATH, command_type);

	if ( state$ryu_debug )
		{
		print url;
		print to_json(mod);
		event OpenFlow::flow_mod_success(state$_name, match, flow_mod);
		return T;
		}

	# Create the ActiveHTTP request and convert the record to a Ryu ReST API JSON string
	local request: ActiveHTTP::Request = ActiveHTTP::Request(
		$url=url,
		$method="POST",
		$client_data=to_json(mod)
	);

	# Execute call to Ryu's ReST API
	when [state, match, flow_mod, request] ( local result = ActiveHTTP::request(request) )
		{
		if (result$code == 200)
			event OpenFlow::flow_mod_success(state$_name, match, flow_mod, result$body);
		else
			{
			Reporter::warning(fmt("Flow modification failed with error: %s", result$body));
			event OpenFlow::flow_mod_failure(state$_name, match, flow_mod, result$body);
			return F;
			}
		}

	return T;
	}

function ryu_flow_clear(state: OpenFlow::ControllerState): bool
	{
	local url=cat("http://", cat(state$ryu_host), ":", cat(state$ryu_port), RYU_FLOWENTRY_PATH, "clear", "/", state$ryu_dpid);

	if ( state$ryu_debug )
		{
		print url;
		return T;
		}

	local request: ActiveHTTP::Request = ActiveHTTP::Request(
		$url=url,
		$method="DELETE"
	);

	when [request] ( local result = ActiveHTTP::request(request) )
		{
		}

	return T;
	}

function ryu_describe(state: ControllerState): string
	{
	return fmt("Ryu-%d-http://%s:%d", state$ryu_dpid, state$ryu_host, state$ryu_port);
	}

# Ryu controller constructor
function ryu_new(host: addr, host_port: count, dpid: count): OpenFlow::Controller
	{
	local c = OpenFlow::Controller($state=OpenFlow::ControllerState($ryu_host=host, $ryu_port=host_port, $ryu_dpid=dpid),
		$flow_mod=ryu_flow_mod, $flow_clear=ryu_flow_clear, $describe=ryu_describe, $supports_flow_removed=F);

	register_controller(OpenFlow::RYU, cat(host,host_port,dpid), c);

	return c;
	}
