Increase integration test coverage. 84/32184/1
authorShigeru Yasuda <s-yasuda@da.jp.nec.com>
Wed, 6 Jan 2016 12:14:18 +0000 (21:14 +0900)
committerShigeru Yasuda <s-yasuda@da.jp.nec.com>
Wed, 6 Jan 2016 12:14:18 +0000 (21:14 +0900)
  * Path map

Change-Id: Iae5ab95ff9d33ec147c9ef4dc6f79b88a08bbeb6
Signed-off-by: Shigeru Yasuda <s-yasuda@da.jp.nec.com>
manager/it/core/src/test/java/org/opendaylight/vtn/manager/it/core/PathMapServiceTest.java [new file with mode: 0644]
manager/it/core/src/test/java/org/opendaylight/vtn/manager/it/core/VTNManagerIT.java
manager/it/util/src/main/java/org/opendaylight/vtn/manager/it/util/pathmap/PathMap.java
manager/it/util/src/main/java/org/opendaylight/vtn/manager/it/util/pathmap/PathMapSet.java

diff --git a/manager/it/core/src/test/java/org/opendaylight/vtn/manager/it/core/PathMapServiceTest.java b/manager/it/core/src/test/java/org/opendaylight/vtn/manager/it/core/PathMapServiceTest.java
new file mode 100644 (file)
index 0000000..14c2e0b
--- /dev/null
@@ -0,0 +1,467 @@
+/*
+ * Copyright (c) 2015 NEC Corporation. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+
+package org.opendaylight.vtn.manager.it.core;
+
+import static org.opendaylight.vtn.manager.it.core.VTNManagerIT.LOG;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+
+import org.opendaylight.vtn.manager.it.util.RpcErrorTag;
+import org.opendaylight.vtn.manager.it.util.VirtualNetwork;
+import org.opendaylight.vtn.manager.it.util.pathmap.PathMap;
+import org.opendaylight.vtn.manager.it.util.pathmap.PathMapSet;
+import org.opendaylight.vtn.manager.it.util.vnode.VTenantConfig;
+
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.ClearPathMapInput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.ClearPathMapInputBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.RemovePathMapInput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.RemovePathMapInputBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.SetPathMapInput;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.SetPathMapInputBuilder;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.VtnPathMapService;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.pathmap.rev150328.set.path.map.input.PathMapList;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.types.rev150209.VtnErrorTag;
+import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.types.rev150209.VtnUpdateType;
+
+/**
+ * Test case for {@link VtnPathMapService}.
+ */
+public final class PathMapServiceTest extends TestMethodBase {
+    /**
+     * The number used to generate random path policy ID.
+     */
+    private static final int  PATH_POLICY_BOUNDARY = PATH_POLICY_ID_MAX + 2;
+
+    /**
+     * Construct a new instance.
+     *
+     * @param vit  A {@link VTNManagerIT} instance.
+     */
+    public PathMapServiceTest(VTNManagerIT vit) {
+        super(vit);
+    }
+
+    /**
+     * Generate a random path policy ID.
+     *
+     * @param rand  A pseudo random generator.
+     * @return  An integer that specifies the path policy or {@code null}.
+     */
+    private Integer randomPolicy(Random rand) {
+        int num = rand.nextInt(PATH_POLICY_BOUNDARY);
+        return (num <= PATH_POLICY_ID_MAX) ? Integer.valueOf(num) : null;
+    }
+
+    // TestMethodBase
+
+    /**
+     * Run the test.
+     *
+     * @throws Exception  An error occurred.
+     */
+    @Override
+    protected void runTest() throws Exception {
+        Random rand = new Random(111222333444555L);
+        VTNManagerIT vit = getTest();
+        VtnPathMapService pmSrv = vit.getPathMapService();
+
+        // Test for global path map.
+        testPathMapService(pmSrv, rand, null);
+
+        // Test for VTN path map.
+        String[] tenants = {
+            "vtn_1", "vtn_2", "vtn_3",
+        };
+        for (String tname: tenants) {
+            testPathMapService(pmSrv, rand, tname);
+        }
+
+        // Error tests.
+
+        // Null input.
+        checkRpcError(pmSrv.setPathMap(null),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+        checkRpcError(pmSrv.removePathMap(null),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+        checkRpcError(pmSrv.clearPathMap(null),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // Invalid VTN name.
+        for (String tname: INVALID_VNODE_NAMES) {
+            notFoundTest(pmSrv, rand, tname);
+        }
+
+        // Errors should never affect path policies.
+        VirtualNetwork vnet = getVirtualNetwork();
+        vnet.verify();
+
+        // Remove all the VTN path maps using remove-path-map.
+        Map<String, PathMapSet> vtnMaps = new HashMap<>();
+        for (String tname: tenants) {
+            PathMapSet pmSet = vnet.getTenant(tname).getPathMaps();
+            assertEquals(null, vtnMaps.put(tname, pmSet.clone()));
+            pmSet.removeAll(pmSrv, tname);
+            vnet.verify();
+        }
+
+        // Remove all the global path maps using remove-path-map.
+        PathMapSet gpmSet = vnet.getPathMaps();
+        PathMapSet gpmSaved = gpmSet.clone();
+        gpmSet.removeAll(pmSrv, null);
+        vnet.verify();
+
+        // Restore all the global and VTN path maps.
+        gpmSet.add(gpmSaved).restore(pmSrv, null);
+        vnet.verify();
+
+        for (Entry<String, PathMapSet> entry: vtnMaps.entrySet()) {
+            String tname = entry.getKey();
+            PathMapSet saved = entry.getValue();
+            PathMapSet pmSet = vnet.getTenant(tname).getPathMaps();
+            pmSet.add(saved).restore(pmSrv, tname);
+            vnet.verify();
+        }
+
+        // Remove all the global path maps using clear-path-map.
+        assertEquals(VtnUpdateType.REMOVED, clearPathMap(null));
+        gpmSet.clear();
+        vnet.verify();
+        assertEquals(null, clearPathMap(null));
+
+        // Remove all the VTN path maps using clear-path-map.
+        for (String tname: tenants) {
+            PathMapSet pmSet = vnet.getTenant(tname).getPathMaps();
+            assertEquals(VtnUpdateType.REMOVED, clearPathMap(tname));
+            pmSet.clear();
+            vnet.verify();
+            assertEquals(null, clearPathMap(tname));
+        }
+
+        // Restore VTN path maps again.
+        for (Entry<String, PathMapSet> entry: vtnMaps.entrySet()) {
+            String tname = entry.getKey();
+            PathMapSet saved = entry.getValue();
+            PathMapSet pmSet = vnet.getTenant(tname).getPathMaps();
+            pmSet.add(saved).restore(pmSrv, tname);
+            vnet.verify();
+        }
+
+        // Remove VTNs.
+        for (String tname: tenants) {
+            removeVtn(tname);
+            vnet.removeTenant(tname).verify();
+        }
+    }
+
+    /**
+     * Test case for {@link VtnPathMapService}.
+     *
+     * @param pmSrv  vtn-path-map service.
+     * @param rand   A pseudo random generator.
+     * @param tname  The name of the target VTN.
+     *               {@code null} indicates the global path map.
+     * @throws Exception  An error occurred.
+     */
+    private void testPathMapService(VtnPathMapService pmSrv, Random rand,
+                                    String tname) throws Exception {
+        LOG.debug("testPathMapService: VTN={}",
+                  (tname == null) ? "<global>" : tname);
+
+        VirtualNetwork vnet = getVirtualNetwork();
+        PathMapSet pmSet;
+        if (tname == null) {
+            // The target is the global path map.
+            pmSet = vnet.getPathMaps();
+        } else {
+            // Ensure that path map RPCs return NOTFOUND error if the specified
+            // VTN is not present.
+            notFoundTest(pmSrv, rand, tname);
+
+            // Errors should never affect path policies.
+            vnet.verify();
+
+            // Create the target VTN.
+            VTenantConfig tconf = new VTenantConfig();
+            vnet.addTenant(tname, tconf).apply();
+            pmSet = tconf.getPathMaps();
+        }
+
+        // Configure path maps.
+        Set<Integer> idxSet = new HashSet<>();
+        PathMap pmap1 = new PathMap(VTN_INDEX_MIN, "cond_1");
+        assertEquals(true, idxSet.add(pmap1.getIndex()));
+        assertEquals(VtnUpdateType.CREATED, pmSet.addMap(pmSrv, tname, pmap1));
+        vnet.verify();
+
+        PathMap pmap2 = new PathMap(VTN_INDEX_MAX, "cond_2",
+                                    PATH_POLICY_ID_MAX);
+        assertEquals(true, idxSet.add(pmap2.getIndex()));
+        Map<Integer, VtnUpdateType> pmResult = new HashMap<>();
+        assertEquals(null, pmResult.put(pmap1.getIndex(), null));
+        assertEquals(null,
+                     pmResult.put(pmap2.getIndex(), VtnUpdateType.CREATED));
+        pmSet.add(pmap2);
+        assertEquals(pmResult, pmSet.update(pmSrv, tname));
+        vnet.verify();
+
+        PathMap pmap3 = new PathMap(
+            createVtnIndex(rand, idxSet), "cond_3", 0, 0, 0);
+        PathMap pmap4 = new PathMap(
+            createVtnIndex(rand, idxSet), "cond_4", randomPolicy(rand),
+            65534, 65535);
+        PathMap pmap5 = new PathMap(
+            createVtnIndex(rand, idxSet), "cond_5", randomPolicy(rand),
+            100, 101);
+        PathMap pmap6 = new PathMap(
+            createVtnIndex(rand, idxSet), "cond_6", randomPolicy(rand),
+            3333, 4444);
+        pmResult.clear();
+        assertEquals(null,
+                     pmResult.put(pmap3.getIndex(), VtnUpdateType.CREATED));
+        assertEquals(null,
+                     pmResult.put(pmap4.getIndex(), VtnUpdateType.CREATED));
+        assertEquals(null,
+                     pmResult.put(pmap5.getIndex(), VtnUpdateType.CREATED));
+        assertEquals(null,
+                     pmResult.put(pmap6.getIndex(), VtnUpdateType.CREATED));
+        assertEquals(pmResult,
+                     pmSet.addMaps(pmSrv, tname, pmap3, pmap4, pmap5, pmap6));
+        vnet.verify();
+
+        // Update pmap1.
+        pmap1.setPolicy(PATH_POLICY_ID_MIN);
+        assertEquals(VtnUpdateType.CHANGED, pmSet.addMap(pmSrv, tname, pmap1));
+        vnet.verify();
+
+        // Update pmap2 and pmap6.
+        pmResult.clear();
+        pmap2.setPolicy(null).
+            setCondition("cond_22").
+            setIdleTimeout(12345).
+            setHardTimeout(23456);
+        assertEquals(null,
+                     pmResult.put(pmap2.getIndex(), VtnUpdateType.CHANGED));
+
+        pmap6.setIdleTimeout(1000).
+            setHardTimeout(0);
+        assertEquals(null,
+                     pmResult.put(pmap6.getIndex(), VtnUpdateType.CHANGED));
+
+        assertEquals(null, pmResult.put(pmap1.getIndex(), null));
+        assertEquals(null, pmResult.put(pmap3.getIndex(), null));
+        assertEquals(pmResult,
+                     pmSet.addMaps(pmSrv, tname, pmap1, pmap2, pmap3, pmap6));
+        vnet.verify();
+
+        // Update pmap3, and create pmap7.
+        pmResult.clear();
+        pmap3.setPolicy(PATH_POLICY_ID_MIN + 1).
+            setIdleTimeout(null).
+            setHardTimeout(null);
+        assertEquals(null,
+                     pmResult.put(pmap3.getIndex(), VtnUpdateType.CHANGED));
+
+        PathMap pmap7 = new PathMap(
+            createVtnIndex(rand, idxSet), "cond_7", randomPolicy(rand),
+            9999, 10000);
+        assertEquals(null,
+                     pmResult.put(pmap7.getIndex(), VtnUpdateType.CREATED));
+
+        assertEquals(null, pmResult.put(pmap5.getIndex(), null));
+        assertEquals(pmResult,
+                     pmSet.addMaps(pmSrv, tname, pmap3, pmap5, pmap7));
+        vnet.verify();
+
+        // Remove pmap4.
+        assertEquals(VtnUpdateType.REMOVED,
+                     pmSet.removeMap(pmSrv, tname, pmap4));
+        vnet.verify();
+
+        // Remove pmap2 and pmap5.
+        List<Integer> indices = new ArrayList<>();
+        pmResult.clear();
+        assertEquals(null,
+                     pmResult.put(pmap2.getIndex(), VtnUpdateType.REMOVED));
+        assertEquals(null,
+                     pmResult.put(pmap5.getIndex(), VtnUpdateType.REMOVED));
+        assertEquals(null, pmResult.put(pmap4.getIndex(), null));
+
+        // Duplicate indices should be ignored.
+        Collections.addAll(
+            indices, pmap2.getIndex(), pmap4.getIndex(), pmap5.getIndex(),
+            pmap2.getIndex(), pmap4.getIndex(), pmap5.getIndex());
+
+        // Invalid indices in remove-path-map input should be ignored.
+        for (int idx = -10; idx <= 10; idx++) {
+            if (idx < VTN_INDEX_MIN || idx > VTN_INDEX_MAX) {
+                pmResult.put(idx, null);
+                indices.add(idx);
+            }
+        }
+
+        assertEquals(pmResult, pmSet.removeMaps(pmSrv, tname, indices));
+        vnet.verify();
+
+        // Error tests.
+
+        // No path map in set-path-map input.
+        SetPathMapInput input = new SetPathMapInputBuilder().
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        input = new SetPathMapInputBuilder().
+            setTenantName(tname).
+            setPathMapList(Collections.<PathMapList>emptyList()).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // Null path map.
+        input = new SetPathMapInputBuilder().
+            setTenantName(tname).
+            setPathMapList(Collections.singletonList((PathMapList)null)).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // No path map index.
+        String cond = "cond";
+        PathMap pmap = new PathMap(null, cond);
+        input = PathMapSet.newInputBuilder(pmap).
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // No flow condition name.
+        Integer index = 1;
+        pmap = new PathMap(index, null);
+        input = PathMapSet.newInputBuilder(pmap).
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        Integer policy = PATH_POLICY_ID_MIN;
+        int[] timeouts = {1, 3000, 65535};
+        for (int t: timeouts) {
+            // Specifying idle timeout without hard timeout.
+            Integer timeout = Integer.valueOf(t);
+            pmap = new PathMap(index, cond, policy, timeout, null);
+            input = PathMapSet.newInputBuilder(pmap).
+                setTenantName(tname).
+                build();
+            checkRpcError(pmSrv.setPathMap(input),
+                          RpcErrorTag.BAD_ELEMENT, VtnErrorTag.BADREQUEST);
+
+            // Specifying hard timeout without specifying idle timeout.
+            pmap = new PathMap(index, cond, policy, null, timeout);
+            input = PathMapSet.newInputBuilder(pmap).
+                setTenantName(tname).
+                build();
+            checkRpcError(pmSrv.setPathMap(input),
+                          RpcErrorTag.BAD_ELEMENT, VtnErrorTag.BADREQUEST);
+
+            // Inconsistent timeout.
+            pmap = new PathMap(index, cond, policy, timeout, timeout);
+            input = PathMapSet.newInputBuilder(pmap).
+                setTenantName(tname).
+                build();
+            checkRpcError(pmSrv.setPathMap(input),
+                          RpcErrorTag.BAD_ELEMENT, VtnErrorTag.BADREQUEST);
+
+            if (timeout != 65535) {
+                pmap = new PathMap(index, cond, policy, t + 1, timeout);
+                input = PathMapSet.newInputBuilder(pmap).
+                    setTenantName(tname).
+                    build();
+                checkRpcError(pmSrv.setPathMap(input),
+                              RpcErrorTag.BAD_ELEMENT, VtnErrorTag.BADREQUEST);
+            }
+        }
+
+        // Duplicate path map index.
+        input = PathMapSet.newInputBuilder(
+            new PathMap(index, cond),
+            new PathMap(VTN_INDEX_MAX, cond, policy),
+            new PathMap(index, cond, policy)).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.BAD_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // No path map index in remove-path-map input.
+        RemovePathMapInput rinput = new RemovePathMapInputBuilder().
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.removePathMap(rinput),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        rinput = new RemovePathMapInputBuilder().
+            setTenantName(tname).
+            setMapIndex(Collections.<Integer>emptyList()).
+            build();
+        checkRpcError(pmSrv.removePathMap(rinput),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // Null map index.
+        rinput = new RemovePathMapInputBuilder().
+            setTenantName(tname).
+            setMapIndex(Collections.singletonList((Integer)null)).
+            build();
+        checkRpcError(pmSrv.removePathMap(rinput),
+                      RpcErrorTag.MISSING_ELEMENT, VtnErrorTag.BADREQUEST);
+
+        // Errors should never affect path policies.
+        vnet.verify();
+    }
+
+    /**
+     * Ensure that path map RPCs return NOTFOUND error if the specified VTN
+     * is not present.
+     *
+     * @param pmSrv  vtn-path-map service.
+     * @param rand   A pseudo random generator.
+     * @param tname  The name of the target VTN.
+     * @throws Exception  An error occurred.
+     */
+    private void notFoundTest(VtnPathMapService pmSrv, Random rand,
+                              String tname) throws Exception {
+        PathMapSet pset = new PathMapSet().add(rand);
+        List<Integer> idxList = Collections.singletonList(Integer.valueOf(1));
+
+        SetPathMapInput input = pset.newInputBuilder().
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.setPathMap(input),
+                      RpcErrorTag.DATA_MISSING, VtnErrorTag.NOTFOUND);
+
+        RemovePathMapInput rinput = new RemovePathMapInputBuilder().
+            setTenantName(tname).
+            setMapIndex(idxList).
+            build();
+        checkRpcError(pmSrv.removePathMap(rinput),
+                      RpcErrorTag.DATA_MISSING, VtnErrorTag.NOTFOUND);
+
+        ClearPathMapInput cinput = new ClearPathMapInputBuilder().
+            setTenantName(tname).
+            build();
+        checkRpcError(pmSrv.clearPathMap(cinput),
+                      RpcErrorTag.DATA_MISSING, VtnErrorTag.NOTFOUND);
+    }
+}
index e5ad2e49ae9b93746b08d9d772054f0443181bb3..13d98eabc01218f9a9a287d3015af427aed355a5 100644 (file)
@@ -437,6 +437,21 @@ public final class VTNManagerIT extends ModelDrivenTestBase
         new PathPolicyServiceTest(this).runTest();
     }
 
+    /**
+     * Test case for {@link VtnPathMapService}.
+     *
+     * <p>
+     *   This test is independent of inventory information.
+     * </p>
+     *
+     * @throws Exception  An error occurred.
+     */
+    @Test
+    public void testPathMapSevice() throws Exception {
+        LOG.info("Running testPathMapSevice().");
+        new PathMapServiceTest(this).runTest();
+    }
+
     /**
      * Ensure that the state of virtual bridge and virtual interface are
      * changed according to inventory events.
index a2dc14f0698e5d788834a6c946ed4525edbb0baf..3ee279a45f388afb34419df4fcb1c87b4024a256 100644 (file)
@@ -66,6 +66,16 @@ public final class PathMap {
      */
     private Integer  hardTimeout;
 
+    /**
+     * Construct a new instance without specifying policy ID and flow timeout.
+     *
+     * @param idx   The index of the path map.
+     * @param cond  The name of the flow condition.
+     */
+    public PathMap(Integer idx, String cond) {
+        this(idx, cond, null, null, null);
+    }
+
     /**
      * Construct a new instance without specifying flow timeout.
      *
index 42cf2573ced44f1400215231dd77502bd632fea4..a52a30b137f8162e136b6b437893f22fe8098cf0 100644 (file)
@@ -16,10 +16,12 @@ import static org.opendaylight.vtn.manager.it.util.ModelDrivenTestBase.getRpcRes
 import static org.opendaylight.vtn.manager.it.util.TestBase.RANDOM_ADD_MAX;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Random;
@@ -45,11 +47,11 @@ import org.opendaylight.yang.gen.v1.urn.opendaylight.vtn.types.rev150209.VtnUpda
 /**
  * {@code PathMapSet} describes a set of path map configurations.
  */
-public final class PathMapSet {
+public final class PathMapSet implements Cloneable {
     /**
      * A map that keeps path map configurations.
      */
-    private final Map<Integer, PathMap> pathMaps = new HashMap<>();
+    private Map<Integer, PathMap> pathMaps = new HashMap<>();
 
     /**
      * Remove all the path maps in the specified path map list.
@@ -106,6 +108,37 @@ public final class PathMapSet {
         return getResultMap(output.getRemovePathMapResult());
     }
 
+    /**
+     * Create a new input builder for set-path-map RPC.
+     *
+     * @param pmaps  An array of {@link PathMap} instances.
+     * @return  A {@link SetPathMapInputBuilder} instance.
+     */
+    public static SetPathMapInputBuilder newInputBuilder(PathMap ... pmaps) {
+        List<PathMap> list = (pmaps == null) ? null : Arrays.asList(pmaps);
+        return newInputBuilder(list);
+    }
+
+    /**
+     * Create a new input builder for set-path-map RPC.
+     *
+     * @param pmaps  A collection of {@link PathMap} instances.
+     * @return  A {@link SetPathMapInputBuilder} instance.
+     */
+    public static SetPathMapInputBuilder newInputBuilder(
+        Collection<PathMap> pmaps) {
+        SetPathMapInputBuilder builder = new SetPathMapInputBuilder();
+        if (pmaps != null) {
+            List<PathMapList> list = new ArrayList<>(pmaps.size());
+            for (PathMap pmap: pmaps) {
+                list.add(pmap.toPathMapList());
+            }
+            builder.setPathMapList(list);
+        }
+
+        return builder;
+    }
+
     /**
      * Create a map that indicates the given result of path map RPC.
      *
@@ -151,22 +184,52 @@ public final class PathMapSet {
     /**
      * Add the given path map configuration.
      *
-     * @param pmap  A {@link PathMap} instance.
+     * @param pmaps  An array of {@link PathMap} instances.
      * @return  This instance.
      */
-    public PathMapSet add(PathMap pmap) {
-        pathMaps.put(pmap.getIndex(), pmap);
+    public PathMapSet add(PathMap ... pmaps) {
+        for (PathMap pmap: pmaps) {
+            pathMaps.put(pmap.getIndex(), pmap);
+        }
+        return this;
+    }
+
+    /**
+     * Add all the path maps in the given path map set.
+     *
+     * @param pmSet  A {@link PathMapSet} instance.
+     * @return  This instance.
+     */
+    public PathMapSet add(PathMapSet pmSet) {
+        for (PathMap pmap: pmSet.pathMaps.values()) {
+            pathMaps.put(pmap.getIndex(), pmap);
+        }
         return this;
     }
 
     /**
      * Remove the path map configuration specified by the given index.
      *
-     * @param idx  The index of the path map.
+     * @param pmaps  An array of {@link PathMap} instances.
+     * @return  This instance.
+     */
+    public PathMapSet remove(PathMap ... pmaps) {
+        for (PathMap pmap: pmaps) {
+            pathMaps.remove(pmap.getIndex());
+        }
+        return this;
+    }
+
+    /**
+     * Remove the path map configuration specified by the given index.
+     *
+     * @param indices  An array of the path map indices to be removed.
      * @return  This instance.
      */
-    public PathMapSet remove(Integer idx) {
-        pathMaps.remove(idx);
+    public PathMapSet remove(Integer ... indices) {
+        for (Integer idx: indices) {
+            pathMaps.remove(idx);
+        }
         return this;
     }
 
@@ -206,17 +269,12 @@ public final class PathMapSet {
      * @return  A {@link SetPathMapInputBuilder} instance.
      */
     public SetPathMapInputBuilder newInputBuilder() {
-        List<PathMapList> pmaps;
-        if (pathMaps.isEmpty()) {
+        Collection<PathMap> pmaps = pathMaps.values();
+        if (pmaps.isEmpty()) {
             pmaps = null;
-        } else {
-            pmaps = new ArrayList<>(pathMaps.size());
-            for (PathMap pmap: pathMaps.values()) {
-                pmaps.add(pmap.toPathMapList());
-            }
         }
 
-        return new SetPathMapInputBuilder().setPathMapList(pmaps);
+        return newInputBuilder(pmaps);
     }
 
     /**
@@ -247,6 +305,186 @@ public final class PathMapSet {
         return getResultMap(output.getSetPathMapResult());
     }
 
+    /**
+     * Restore all the path maps in this instance.
+     *
+     * <p>
+     *   This method expects that this instance contains at least one path map,
+     *   and all the path maps in this instance are not configured yet.
+     * </p>
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     */
+    public void restore(VtnPathMapService service, String tname) {
+        assertEquals(false, pathMaps.isEmpty());
+
+        Map<Integer, VtnUpdateType> expected = new HashMap<>();
+        for (Integer idx: pathMaps.keySet()) {
+            assertEquals(null, expected.put(idx, VtnUpdateType.CREATED));
+        }
+
+        assertEquals(expected, update(service, tname));
+    }
+
+    /**
+     * Add the given path map using set-path-map RPC, and then add it to
+     * this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param pmap     A {@link PathMap} instance.
+     * @return  A {@link VtnUpdateType} instance returned by the RPC.
+     */
+    public VtnUpdateType addMap(VtnPathMapService service, String tname,
+                                PathMap pmap) {
+        Map<Integer, VtnUpdateType> result = addMaps(service, tname, pmap);
+        Integer idx = pmap.getIndex();
+        assertEquals(Collections.singleton(idx), result.keySet());
+        return result.get(idx);
+    }
+
+    /**
+     * Add the given path maps using set-path-map RPC, and then add them to
+     * this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param pmaps     An array of {@link PathMap} instances to be added.
+     * @return  A map that contains the RPC result.
+     */
+    public Map<Integer, VtnUpdateType> addMaps(
+        VtnPathMapService service, String tname, PathMap ... pmaps) {
+        List<PathMap> list = (pmaps == null) ? null : Arrays.asList(pmaps);
+        return addMaps(service, tname, list);
+    }
+
+    /**
+     * Add the given path maps using set-path-map RPC, and then add them to
+     * this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param pmaps     A collection of {@link PathMap} instances to be added.
+     * @return  A map that contains the RPC result.
+     */
+    public Map<Integer, VtnUpdateType> addMaps(
+        VtnPathMapService service, String tname, Collection<PathMap> pmaps) {
+        SetPathMapInput input = newInputBuilder(pmaps).
+            setTenantName(tname).
+            build();
+        SetPathMapOutput output = getRpcOutput(service.setPathMap(input));
+        Map<Integer, VtnUpdateType> result =
+            getResultMap(output.getSetPathMapResult());
+
+        if (pmaps != null) {
+            for (PathMap pmap: pmaps) {
+                pathMaps.put(pmap.getIndex(), pmap);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Remove the given path map using remove-path-map RPC, and then remove it
+     * from this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param pmap     A {@link PathMap} instance.
+     * @return  A {@link VtnUpdateType} instance returned by the RPC.
+     */
+    public VtnUpdateType removeMap(VtnPathMapService service, String tname,
+                                   PathMap pmap) {
+        return removeMap(service, tname, pmap.getIndex());
+    }
+
+    /**
+     * Remove the given path map using remove-path-map RPC, and then remove it
+     * from this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param idx      The index for the path map to be removed.
+     * @return  A {@link VtnUpdateType} instance returned by the RPC.
+     */
+    public VtnUpdateType removeMap(VtnPathMapService service, String tname,
+                                   Integer idx) {
+        Map<Integer, VtnUpdateType> result = removeMaps(service, tname, idx);
+        assertEquals(Collections.singleton(idx), result.keySet());
+        return result.get(idx);
+    }
+
+    /**
+     * Remove the given path maps using remove-path-map RPC, and then remove
+     * them from this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param indices  An array of path map indices.
+     * @return  A map that contains the RPC result.
+     */
+    public Map<Integer, VtnUpdateType> removeMaps(
+        VtnPathMapService service, String tname, Integer ... indices) {
+        List<Integer> list = (indices == null) ? null : Arrays.asList(indices);
+        return removeMaps(service, tname, list);
+    }
+
+    /**
+     * Remove the given path maps using remove-path-map RPC, and then remove
+     * them from this instance.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     * @param indices  A list of path map indices.
+     * @return  A map that contains the RPC result.
+     */
+    public Map<Integer, VtnUpdateType> removeMaps(
+        VtnPathMapService service, String tname, List<Integer> indices) {
+        Map<Integer, VtnUpdateType> result =
+            removePathMap(service, tname, indices);
+
+        if (indices != null) {
+            for (Integer idx: indices) {
+                pathMaps.remove(idx);
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Remove all the path maps in this instance using remove-path-map RPC.
+     *
+     * @param service  The vtn-path-map service.
+     * @param tname    The name of the VTN.
+     *                 {@code null} implies the global path map.
+     */
+    public void removeAll(VtnPathMapService service, String tname) {
+        if (!pathMaps.isEmpty()) {
+            List<Integer> indices = new ArrayList<>();
+            Map<Integer, VtnUpdateType> resMap = new HashMap<>();
+            for (Iterator<Integer> it = pathMaps.keySet().iterator();
+                 it.hasNext();) {
+                Integer idx = it.next();
+                indices.add(idx);
+                assertEquals(null, resMap.put(idx, VtnUpdateType.REMOVED));
+                it.remove();
+            }
+
+            assertEquals(resMap, removePathMap(service, tname, indices));
+        }
+    }
+
     /**
      * Verify the given path map container.
      *
@@ -316,21 +554,40 @@ public final class PathMapSet {
                                  clearPathMap(service, tname));
                 } else {
                     List<Integer> unwanted = new ArrayList<>();
+                    Map<Integer, VtnUpdateType> resMap = new HashMap<>();
                     for (VtnPathMap vpm: vpms) {
                         Integer index = vpm.getIndex();
                         if (!pathMaps.containsKey(index)) {
                             unwanted.add(index);
+                            resMap.put(index, VtnUpdateType.REMOVED);
                         }
                     }
 
-                    Map<Integer, VtnUpdateType> result =
-                        removePathMap(service, tname, unwanted);
-                    assertEquals(unwanted.size(), result.size());
-                    for (Integer index: unwanted) {
-                        assertEquals(VtnUpdateType.REMOVED, result.get(index));
+                    if (!unwanted.isEmpty()) {
+                        assertEquals(resMap,
+                                     removePathMap(service, tname, unwanted));
                     }
                 }
             }
         }
     }
+
+    // Object
+
+    /**
+     * Create a shallow copy of this instance.
+     *
+     * @return  A shallow copy of this instance.
+     */
+    @Override
+    public PathMapSet clone() {
+        try {
+            PathMapSet pmSet = (PathMapSet)super.clone();
+            pmSet.pathMaps = new HashMap<>(pathMaps);
+            return pmSet;
+        } catch (CloneNotSupportedException e) {
+            // This should never happen.
+            throw new IllegalStateException("clone() failed", e);
+        }
+    }
 }