CORD-176 Added web resources and codecs for service network and port
Change-Id: I15db1036fa9ee4041520abbcc36bae022ff03a1c
diff --git a/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java b/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java
new file mode 100644
index 0000000..0352f98
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/CodecRegistrator.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed 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 org.opencord.cordvtn.codec;
+
+import org.apache.felix.scr.annotations.Activate;
+import org.apache.felix.scr.annotations.Component;
+import org.apache.felix.scr.annotations.Deactivate;
+import org.apache.felix.scr.annotations.Reference;
+import org.apache.felix.scr.annotations.ReferenceCardinality;
+import org.onosproject.codec.CodecService;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.opencord.cordvtn.api.ServicePort;
+import org.slf4j.Logger;
+
+import static org.slf4j.LoggerFactory.getLogger;
+
+/**
+ * Implementation of the JSON codec brokering service for VTN.
+ */
+@Component(immediate = true)
+public class CodecRegistrator {
+ private final Logger log = getLogger(getClass());
+
+ @Reference(cardinality = ReferenceCardinality.MANDATORY_UNARY)
+ protected CodecService codecService;
+
+ @Activate
+ public void activate() {
+ codecService.registerCodec(ServiceNetwork.class, new ServiceNetworkCodec());
+ codecService.registerCodec(ServicePort.class, new ServicePortCodec());
+ log.info("Started");
+ }
+
+ @Deactivate
+ public void deactivate() {
+ codecService.unregisterCodec(ServiceNetwork.class);
+ codecService.unregisterCodec(ServicePort.class);
+ log.info("Stopped");
+ }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java b/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java
new file mode 100644
index 0000000..52f9a7e
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/ServiceNetworkCodec.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed 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 org.opencord.cordvtn.codec;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.opencord.cordvtn.api.NetworkId;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType.BIDIRECTIONAL;
+import static org.opencord.cordvtn.api.ServiceNetwork.DirectAccessType.UNIDIRECTIONAL;
+import static org.opencord.cordvtn.api.ServiceNetwork.ServiceNetworkType.valueOf;
+
+/**
+ * Service network JSON codec.
+ */
+public final class ServiceNetworkCodec extends JsonCodec<ServiceNetwork> {
+
+ private static final String ID = "id";
+ private static final String TYPE = "type";
+ private static final String PROVIDER_NETWORKS = "providerNetworks";
+ private static final String BIDIRECT = "bidirectional";
+
+ @Override
+ public ObjectNode encode(ServiceNetwork snet, CodecContext context) {
+ ObjectNode result = context.mapper().createObjectNode()
+ .put(ID, snet.id().id())
+ .put(TYPE, snet.type().name().toLowerCase());
+
+ ArrayNode providers = context.mapper().createArrayNode();
+ snet.providers().entrySet().forEach(provider -> {
+ ObjectNode providerJson = context.mapper().createObjectNode()
+ .put(ID, provider.getKey().id())
+ .put(BIDIRECT, provider.getValue() == BIDIRECTIONAL ? TRUE : FALSE);
+ providers.add(providerJson);
+ });
+
+ result.set(PROVIDER_NETWORKS, providers);
+ return result;
+ }
+
+ @Override
+ public ServiceNetwork decode(ObjectNode json, CodecContext context) {
+ if (json == null || !json.isObject()) {
+ return null;
+ }
+
+ ServiceNetwork.Builder snetBuilder = ServiceNetwork.builder()
+ .id(NetworkId.of(json.get(ID).asText()))
+ .type(valueOf(json.get(TYPE).asText().toUpperCase()));
+
+ if (json.get(PROVIDER_NETWORKS) != null) {
+ json.get(PROVIDER_NETWORKS).forEach(provider -> {
+ NetworkId providerId = NetworkId.of(provider.get(ID).asText());
+ DirectAccessType type = provider.get(BIDIRECT).asBoolean() ?
+ BIDIRECTIONAL : UNIDIRECTIONAL;
+ snetBuilder.addProvider(providerId, type);
+ });
+ }
+ return snetBuilder.build();
+ }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java b/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java
new file mode 100644
index 0000000..9c4343f
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/ServicePortCodec.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed 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 org.opencord.cordvtn.codec;
+
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.packet.IpAddress;
+import org.onlab.packet.MacAddress;
+import org.onlab.packet.VlanId;
+import org.onosproject.codec.CodecContext;
+import org.onosproject.codec.JsonCodec;
+import org.opencord.cordvtn.api.AddressPair;
+import org.opencord.cordvtn.api.PortId;
+import org.opencord.cordvtn.api.ServicePort;
+
+/**
+ * Service port JSON codec.
+ */
+public final class ServicePortCodec extends JsonCodec<ServicePort> {
+
+ private static final String ID = "id";
+ private static final String VLAN_ID = "vlan_id";
+ private static final String FLOATING_ADDRESS_PAIRS = "floating_address_pairs";
+ private static final String IP_ADDRESS = "ip_address";
+ private static final String MAC_ADDRESS = "mac_address";
+
+ @Override
+ public ObjectNode encode(ServicePort sport, CodecContext context) {
+ ObjectNode result = context.mapper().createObjectNode()
+ .put(ID, sport.id().id());
+ if (sport.vlanId().isPresent()) {
+ result.put(VLAN_ID, sport.vlanId().get().id());
+ }
+
+ ArrayNode addressPairs = context.mapper().createArrayNode();
+ sport.addressPairs().forEach(pair -> {
+ ObjectNode pairJson = context.mapper().createObjectNode()
+ .put(IP_ADDRESS, pair.ip().toString())
+ .put(MAC_ADDRESS, pair.mac().toString());
+ addressPairs.add(pairJson);
+ });
+ result.set(FLOATING_ADDRESS_PAIRS, addressPairs);
+ return result;
+ }
+
+ @Override
+ public ServicePort decode(ObjectNode json, CodecContext context) {
+ if (json == null || !json.isObject()) {
+ return null;
+ }
+
+ ServicePort.Builder sportBuilder = ServicePort.builder()
+ .id(PortId.of(json.get(ID).asText()));
+
+ if (json.get(VLAN_ID) != null) {
+ sportBuilder.vlanId(VlanId.vlanId(json.get(VLAN_ID).asText()));
+ }
+
+ if (json.get(FLOATING_ADDRESS_PAIRS) != null) {
+ json.get(FLOATING_ADDRESS_PAIRS).forEach(pair -> {
+ AddressPair addrPair = AddressPair.of(
+ IpAddress.valueOf(pair.get(IP_ADDRESS).asText()),
+ MacAddress.valueOf(pair.get(MAC_ADDRESS).asText()));
+ sportBuilder.addAddressPair(addrPair);
+ });
+ }
+ return sportBuilder.build();
+ }
+}
diff --git a/src/main/java/org/opencord/cordvtn/codec/package-info.java b/src/main/java/org/opencord/cordvtn/codec/package-info.java
new file mode 100644
index 0000000..1ac7b31
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/codec/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed 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.
+ */
+
+/**
+ * JSON codecs for the web resources.
+ */
+package org.opencord.cordvtn.codec;
\ No newline at end of file
diff --git a/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java b/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
index a9af9d2..fd26c63 100644
--- a/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
+++ b/src/main/java/org/opencord/cordvtn/rest/CordVtnWebApplication.java
@@ -26,7 +26,9 @@
public class CordVtnWebApplication extends AbstractWebApplication {
@Override
public Set<Class<?>> getClasses() {
- return getClasses(ServiceDependencyWebResource.class,
+ return getClasses(ServiceNetworkWebResource.class,
+ ServicePortWebResource.class,
+ ServiceDependencyWebResource.class,
NeutronMl2NetworksWebResource.class,
NeutronMl2SubnetsWebResource.class,
NeutronMl2PortsWebResource.class);
diff --git a/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java b/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java
new file mode 100644
index 0000000..fbd6ddd
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/rest/ServiceNetworkWebResource.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 2016-present Open Networking Laboratory
+ *
+ * Licensed 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 org.opencord.cordvtn.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.rest.AbstractWebResource;
+import org.opencord.cordvtn.api.CordVtnStore;
+import org.opencord.cordvtn.api.NetworkId;
+import org.opencord.cordvtn.api.ServiceNetwork;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.status;
+
+/**
+ * Query and manage service networks.
+ */
+@Path("serviceNetworks")
+public class ServiceNetworkWebResource extends AbstractWebResource {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ private static final String MESSAGE = "Received service network ";
+ private static final String SERVICE_NETWORK = "ServiceNetwork";
+ private static final String SERVICE_NETWORKS = "ServiceNetworks";
+
+ private final CordVtnStore service = DefaultServiceDirectory.getService(CordVtnStore.class);
+
+ @Context
+ private UriInfo uriInfo;
+
+ /**
+ * Creates a service network from the JSON input stream.
+ *
+ * @param input service network JSON stream
+ * @return 201 CREATED if the JSON is correct, 400 BAD_REQUEST if the JSON
+ * is invalid or duplicated network with different properties exists
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createServiceNetwork(InputStream input) {
+ try {
+ JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+ log.trace(MESSAGE + "CREATE " + mapper().writeValueAsString(jsonTree));
+
+ ObjectNode snetJson = (ObjectNode) jsonTree.get(SERVICE_NETWORK);
+ if (snetJson == null) {
+ throw new IllegalArgumentException();
+ }
+
+ final ServiceNetwork snet = codec(ServiceNetwork.class).decode(snetJson, this);
+ service.createServiceNetwork(snet);
+
+ UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+ .path(SERVICE_NETWORKS)
+ .path(snet.id().id());
+
+ return created(locationBuilder.build()).build();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Updates the service network with the specified identifier.
+ *
+ * @param id network identifier
+ * @return 200 OK with a service network, 400 BAD_REQUEST if the requested
+ * network does not exist
+ */
+ @PUT
+ @Path("{id}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response updateServiceNetwork(@PathParam("id") String id, InputStream input) {
+ try {
+ JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+ log.trace(MESSAGE + "UPDATE " + mapper().writeValueAsString(jsonTree));
+
+ ObjectNode snetJson = (ObjectNode) jsonTree.get(SERVICE_NETWORK);
+ if (snetJson == null) {
+ throw new IllegalArgumentException();
+ }
+
+ final ServiceNetwork snet = codec(ServiceNetwork.class).decode(snetJson, this);
+ service.updateServiceNetwork(snet);
+
+ ObjectNode result = this.mapper().createObjectNode();
+ result.set(SERVICE_NETWORK, codec(ServiceNetwork.class).encode(snet, this));
+ return ok(result).build();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Returns all service networks.
+ *
+ * @return 200 OK with set of service networks
+ */
+ @GET
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getServiceNetworks() {
+ log.trace(MESSAGE + "GET");
+
+ Set<ServiceNetwork> snets = service.getServiceNetworks();
+ return ok(encodeArray(ServiceNetwork.class, SERVICE_NETWORKS, snets)).build();
+ }
+
+ /**
+ * Returns the service network with the specified identifier.
+ *
+ * @param id network identifier
+ * @return 200 OK with a service network, 404 NOT_FOUND if the requested
+ * network does not exist
+ */
+ @GET
+ @Path("{id}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getServiceNetwork(@PathParam("id") String id) {
+ log.trace(MESSAGE + "GET " + id);
+
+ ServiceNetwork snet = service.getServiceNetwork(NetworkId.of(id));
+ if (snet == null) {
+ return status(NOT_FOUND).build();
+ }
+
+ ObjectNode result = this.mapper().createObjectNode();
+ result.set(SERVICE_NETWORK, codec(ServiceNetwork.class).encode(snet, this));
+ return ok(result).build();
+ }
+
+ /**
+ * Removes the service network.
+ *
+ * @param id network identifier
+ * @return 204 NO CONTENT, 400 BAD_REQUEST if the network does not exist
+ */
+ @DELETE
+ @Path("{id}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response deleteServiceNetwork(@PathParam("id") String id) {
+ log.trace(MESSAGE + "DELETE " + id);
+
+ service.removeServiceNetwork(NetworkId.of(id));
+ return noContent().build();
+ }
+}
diff --git a/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java b/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java
new file mode 100644
index 0000000..7b18e10
--- /dev/null
+++ b/src/main/java/org/opencord/cordvtn/rest/ServicePortWebResource.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2016-present Open Porting Laboratory
+ *
+ * Licensed 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 org.opencord.cordvtn.rest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import org.onlab.osgi.DefaultServiceDirectory;
+import org.onosproject.rest.AbstractWebResource;
+import org.opencord.cordvtn.api.CordVtnStore;
+import org.opencord.cordvtn.api.PortId;
+import org.opencord.cordvtn.api.ServicePort;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import javax.ws.rs.core.UriInfo;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static javax.ws.rs.core.Response.Status.NOT_FOUND;
+import static javax.ws.rs.core.Response.created;
+import static javax.ws.rs.core.Response.noContent;
+import static javax.ws.rs.core.Response.status;
+
+/**
+ * Query and manage service ports.
+ */
+@Path("servicePorts")
+public class ServicePortWebResource extends AbstractWebResource {
+ protected final Logger log = LoggerFactory.getLogger(getClass());
+
+ private static final String MESSAGE = "Received service port ";
+ private static final String SERVICE_PORT = "ServicePort";
+ private static final String SERVICE_PORTS = "ServicePorts";
+
+ private final CordVtnStore service = DefaultServiceDirectory.getService(CordVtnStore.class);
+
+ @Context
+ private UriInfo uriInfo;
+
+ /**
+ * Creates a service port from the JSON input stream.
+ *
+ * @param input service port JSON stream
+ * @return 201 CREATED if the JSON is correct, 400 BAD_REQUEST if the JSON
+ * is invalid or duplicated port with different properties exists
+ */
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response createServicePort(InputStream input) {
+ try {
+ JsonNode jsonTree = mapper().enable(INDENT_OUTPUT).readTree(input);
+ log.trace(MESSAGE + "CREATE " + mapper().writeValueAsString(jsonTree));
+
+ ObjectNode portJson = (ObjectNode) jsonTree.get(SERVICE_PORT);
+ if (portJson == null) {
+ throw new IllegalArgumentException();
+ }
+
+ final ServicePort sport = codec(ServicePort.class).decode(portJson, this);
+ service.createServicePort(sport);
+
+ UriBuilder locationBuilder = uriInfo.getBaseUriBuilder()
+ .path(SERVICE_PORTS)
+ .path(sport.id().id());
+
+ return created(locationBuilder.build()).build();
+ } catch (IOException e) {
+ throw new IllegalArgumentException(e);
+ }
+ }
+
+ /**
+ * Returns all service ports.
+ *
+ * @return 200 OK with set of service ports
+ */
+ @GET
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getServicePorts() {
+ log.debug(MESSAGE + "GET");
+
+ Set<ServicePort> sports = service.getServicePorts();
+ return ok(encodeArray(ServicePort.class, SERVICE_PORTS, sports)).build();
+ }
+
+ /**
+ * Returns the service port with the specified identifier.
+ *
+ * @param id port identifier
+ * @return 200 OK with a service port, 404 NOT_FOUND if the requested
+ * port does not exist
+ */
+ @GET
+ @Path("{id}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response getServicePort(@PathParam("id") String id) {
+ log.debug(MESSAGE + "GET " + id);
+
+ ServicePort sport = service.getServicePort(PortId.of(id));
+ if (sport == null) {
+ return status(NOT_FOUND).build();
+ }
+
+ ObjectNode result = this.mapper().createObjectNode();
+ result.set(SERVICE_PORT, codec(ServicePort.class).encode(sport, this));
+ return ok(result).build();
+ }
+
+ /**
+ * Removes the service port.
+ *
+ * @param id port identifier
+ * @return 204 NO CONTENT, 400 BAD_REQUEST if the network does not exist
+ */
+ @DELETE
+ @Path("{id}")
+ @Consumes(MediaType.APPLICATION_JSON)
+ @Produces(MediaType.APPLICATION_JSON)
+ public Response deleteServicePort(@PathParam("id") String id) {
+ log.debug(MESSAGE + "DELETE " + id);
+
+ service.removeServicePort(PortId.of(id));
+ return noContent().build();
+ }
+}