package net.floodlightcontroller.util; import java.io.IOException; import java.io.StringWriter; import java.io.Writer; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import org.projectfloodlight.openflow.protocol.OFBucket; import org.projectfloodlight.openflow.protocol.OFFactories; import org.projectfloodlight.openflow.protocol.OFGroupAdd; import org.projectfloodlight.openflow.protocol.OFGroupDelete; import org.projectfloodlight.openflow.protocol.OFGroupMod; import org.projectfloodlight.openflow.protocol.OFGroupModify; import org.projectfloodlight.openflow.protocol.OFGroupType; import org.projectfloodlight.openflow.types.OFGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.core.JsonGenerator.Feature; import com.fasterxml.jackson.databind.MappingJsonFactory; /** * Convert OFGroups to and from JSON strings. Used primarily by * the static flow pusher to store and retrieve flow entries. * * @author Ryan Izard <ryan.izard@bigswitch.com, rizard@g.clemson.edu> * */ public class GroupUtils { private static final Logger log = LoggerFactory.getLogger(GroupUtils.class); public static final String GROUP_ID = "group_id"; public static final String GROUP_ID_MAX = "max"; public static final String GROUP_ID_ANY = "any"; public static final String GROUP_ID_ALL = "all"; public static final String GROUP_TYPE = "group_type"; public static final String GROUP_TYPE_FF = "fast_failover"; public static final String GROUP_TYPE_ALL = "all"; public static final String GROUP_TYPE_SELECT = "select"; public static final String GROUP_TYPE_INDIRECT = "indirect"; public static final String GROUP_BUCKETS = "group_buckets"; public static final String BUCKET_ID = "bucket_id"; public static final String BUCKET_WEIGHT = "bucket_weight"; public static final String BUCKET_WATCH_PORT = "bucket_watch_port"; public static final String BUCKET_WATCH_GROUP = "bucket_watch_group"; public static final String BUCKET_ACTIONS = "bucket_actions"; private static final JsonFactory jsonFactory = new JsonFactory(); private static final String JSON_EMPTY_ARRAY = "[]"; private static final String JSON_EMPTY_VALUE = ""; private GroupUtils() { } public static OFGroupModify toGroupModify(OFGroupMod gm) { return OFFactories.getFactory(gm.getVersion()).buildGroupModify() .setBuckets(gm.getBuckets()) .setGroup(gm.getGroup()) .setGroupType(gm.getGroupType()) .setXid(gm.getXid()) .build(); } public static OFGroupDelete toGroupDelete(OFGroupMod gm) { return OFFactories.getFactory(gm.getVersion()).buildGroupDelete() /* don't care about buckets or type */ .setGroup(gm.getGroup()) .setGroupType(gm.getGroupType()) .setXid(gm.getXid()) .build(); } public static OFGroupAdd toGroupAdd(OFGroupMod gm) { return OFFactories.getFactory(gm.getVersion()).buildGroupAdd() .setBuckets(gm.getBuckets()) .setGroup(gm.getGroup()) .setGroupType(gm.getGroupType()) .setXid(gm.getXid()) .build(); } public static boolean setGroupIdFromString(OFGroupMod.Builder g, String s) { if (g == null) { throw new IllegalArgumentException("OFGroupMod cannot be null"); } if (s == null) { throw new IllegalArgumentException("String cannot be null"); } OFGroup group = groupIdFromString(s); if (group == null) { log.error("Could not set group ID {} due to parse error", s); return false; } else { g.setGroup(group); return true; } } public static OFGroup groupIdFromString(String s) { if (s == null) { throw new IllegalArgumentException("String cannot be null"); } s = s.trim().toLowerCase(); try { if (s.equals(GROUP_ID_ALL)) { return OFGroup.ALL; } else if (s.equals(GROUP_ID_ANY)) { return OFGroup.ANY; } else if (s.equals(GROUP_ID_MAX)) { return OFGroup.MAX; } else { return OFGroup.of(ParseUtils.parseHexOrDecInt(s)); } } catch (Exception e) { log.error("Could not parse group ID {}", s); return null; } } public static String groupIdToString(OFGroup g) { if (g == null) { throw new IllegalArgumentException("Group ID cannot be null"); } if (g.equals(OFGroup.ALL)) { return GROUP_ID_ALL; } if (g.equals(OFGroup.ANY)) { return GROUP_ID_ANY; } if (g.equals(OFGroup.MAX)) { return GROUP_ID_MAX; } return Integer.toString(g.getGroupNumber()); } public static OFGroupType groupTypeFromString(String s) { if (s == null) { throw new IllegalArgumentException("String cannot be null"); } s = s.trim().toLowerCase(); switch (s) { case GROUP_TYPE_ALL: return OFGroupType.ALL; case GROUP_TYPE_FF: return OFGroupType.FF; case GROUP_TYPE_INDIRECT: return OFGroupType.INDIRECT; case GROUP_TYPE_SELECT: return OFGroupType.SELECT; default: log.error("Unrecognized group type {}", s); return null; } } public static boolean setGroupTypeFromString(OFGroupMod.Builder g, String s) { if (g == null) { throw new IllegalArgumentException("OFGroupMod cannot be null"); } OFGroupType t = groupTypeFromString(s); if (t != null) { g.setGroupType(t); return true; } return false; } public static String groupTypeToString(OFGroupType t) { if (t == null) { throw new IllegalArgumentException("OFGroupType cannot be null"); } switch (t) { case ALL: return GROUP_TYPE_ALL; case FF: return GROUP_TYPE_FF; case INDIRECT: return GROUP_TYPE_INDIRECT; case SELECT: return GROUP_TYPE_SELECT; default: log.error("Unrecognized group type {}", t); return JSON_EMPTY_VALUE; } } /** * Append an array of buckets to an existing JsonGenerator. * This method assumes the field name of the array has been * written already, if required. The appended data will * be formatted as follows: * [ * { * bucket-1 * }, * { * bucket-2 * }, * ..., * { * bucket-n * } * ] * @param jsonGen * @param bucketList */ public static void groupBucketsToJsonArray(JsonGenerator jsonGen, List<OFBucket> bucketList) { jsonGen.configure(Feature.WRITE_NUMBERS_AS_STRINGS, true); try { int bucketId = 1; jsonGen.writeStartArray(); for (OFBucket b : bucketList) { jsonGen.writeStartObject(); jsonGen.writeNumberField(BUCKET_ID, bucketId++); /* not preserved from original, but indicates order */ jsonGen.writeStringField(BUCKET_WATCH_GROUP, groupIdToString(b.getWatchGroup())); jsonGen.writeStringField(BUCKET_WATCH_PORT, MatchUtils.portToString(b.getWatchPort())); jsonGen.writeNumberField(BUCKET_WEIGHT, b.getWeight()); jsonGen.writeStringField(BUCKET_ACTIONS, ActionUtils.actionsToString(b.getActions())); /* TODO update to object array */ jsonGen.writeEndObject(); } jsonGen.writeEndArray(); } catch (IOException e) { log.error("Error composing group bucket JSON array. {}", e.getMessage()); return; } } /** * Convert a list of group buckets into a JSON-formatted * string. The string data will be formatted as follows: * [ * { * bucket-1 * }, * { * bucket-2 * }, * ..., * { * bucket-n * } * ] * @param bucketList * @return the string, formatted as described above */ public static String groupBucketsToJsonArray(List<OFBucket> bucketList) { Writer w = new StringWriter(); JsonGenerator jsonGen; try { jsonGen = jsonFactory.createGenerator(w); } catch (IOException e) { log.error("Could not instantiate JSON Generator. {}", e.getMessage()); return JSON_EMPTY_ARRAY; } groupBucketsToJsonArray(jsonGen, bucketList); return w.toString(); /* overridden impl returns contents of Writer's StringBuffer */ } /** * Convert a JSON-formatted string of group buckets to a * Java list of buckets. The JSON format expected is: * [ * { * bucket-1 * }, * { * bucket-2 * }, * ..., * { * bucket-n * } * ] * @param g the group-mod message to add the buckets * @param json the string, formatted as described above */ public static boolean setGroupBucketsFromJsonArray(OFGroupMod.Builder g, String json) { final Map<Integer, OFBucket> bucketsById = new HashMap<Integer, OFBucket>(); final MappingJsonFactory f = new MappingJsonFactory(); if (g == null) { throw new IllegalArgumentException("OFGroupMod cannot be null"); } if (json == null) { throw new IllegalArgumentException("JSON string cannot be null"); } final JsonParser jp; try { jp = f.createParser(json); } catch (IOException e) { log.error("Could not create JSON parser for {}", json); return false; } try { if (jp.nextToken() != JsonToken.START_ARRAY) { throw new IOException("Expected START_ARRAY"); } while (jp.nextToken() != JsonToken.END_ARRAY) { if (jp.getCurrentToken() != JsonToken.START_OBJECT) { throw new IOException("Expected START_OBJECT"); } OFBucket.Builder b = OFFactories.getFactory(g.getVersion()).buildBucket(); int bucketId = -1; while (jp.nextToken() != JsonToken.END_OBJECT) { if (jp.getCurrentToken() != JsonToken.FIELD_NAME) { throw new IOException("Expected FIELD_NAME"); } String key = jp.getCurrentName().toLowerCase().trim(); jp.nextToken(); String value = jp.getText().toLowerCase().trim(); switch (key) { case BUCKET_ID: bucketId = ParseUtils.parseHexOrDecInt(value); break; case BUCKET_WATCH_GROUP: b.setWatchGroup(groupIdFromString(value)); break; case BUCKET_WATCH_PORT: b.setWatchPort(MatchUtils.portFromString(value)); break; case BUCKET_WEIGHT: b.setWeight(ParseUtils.parseHexOrDecInt(value)); break; case BUCKET_ACTIONS: b.setActions(ActionUtils.fromString(value, b.getVersion())); // TODO update to JSON break; default: log.warn("Unknown bucket key {}", key); break; } } if (bucketId != -1) { bucketsById.put(bucketId, b.build()); } else { log.error("Must provide a bucket ID for bucket {}", b); } } } catch (IOException e) { log.error("Could not parse: {}", json); log.error("JSON parse error message: {}", e.getMessage()); return false; } g.setBuckets( bucketsById.entrySet() .stream() .sorted( /* invert to get sorted smallest to largest */ (e1, e2) -> Integer.compare(e1.getKey(), e2.getKey()) ) .map(Map.Entry::getValue) .collect(Collectors.toList()) ); return true; } }