Add rolling upgrade test.

Change-Id: Id1b09361aa69f1665f19c312933798b5206d46ac
diff --git a/apps/test/distributed-primitives/src/main/java/org/onosproject/distributedprimitives/cli/ConsistentMapTestCommand.java b/apps/test/distributed-primitives/src/main/java/org/onosproject/distributedprimitives/cli/ConsistentMapTestCommand.java
index ac09084..3896830 100644
--- a/apps/test/distributed-primitives/src/main/java/org/onosproject/distributedprimitives/cli/ConsistentMapTestCommand.java
+++ b/apps/test/distributed-primitives/src/main/java/org/onosproject/distributedprimitives/cli/ConsistentMapTestCommand.java
@@ -18,6 +18,7 @@
 import org.apache.karaf.shell.commands.Argument;
 import org.apache.karaf.shell.commands.Command;
 import org.onosproject.cli.AbstractShellCommand;
+import org.onosproject.core.Version;
 import org.onosproject.store.serializers.KryoNamespaces;
 import org.onosproject.store.service.ConsistentMap;
 import org.onosproject.store.service.Serializer;
@@ -62,9 +63,11 @@
     protected void execute() {
         StorageService storageService = get(StorageService.class);
         map = storageService.<String, String>consistentMapBuilder()
-                                    .withName(name)
-                                    .withSerializer(Serializer.using(KryoNamespaces.BASIC))
-                                    .build();
+            .withName(name)
+            .withSerializer(Serializer.using(KryoNamespaces.BASIC))
+            .withVersion(Version.version("1.0.0"))
+            .withCompatibilityFunction((value, version) -> version + ":" + value)
+            .build();
         if ("get".equals(operation)) {
             print(map.get(arg1));
         } else if ("put".equals(operation)) {
@@ -95,6 +98,22 @@
             } else {
                 print("%b", map.replace(arg1, arg2, arg3));
             }
+        } else if ("compatiblePut".equals(operation)) {
+            ConsistentMap<String, String> map = storageService.<String, String>consistentMapBuilder()
+                .withName(name)
+                .withSerializer(Serializer.using(KryoNamespaces.BASIC))
+                .withCompatibilityFunction((value, version) -> version + ":" + value)
+                .withVersion(Version.version("2.0.0"))
+                .build();
+            print(map.put(arg1, arg2));
+        } else if ("compatibleGet".equals(operation)) {
+            ConsistentMap<String, String> map = storageService.<String, String>consistentMapBuilder()
+                .withName(name)
+                .withSerializer(Serializer.using(KryoNamespaces.BASIC))
+                .withCompatibilityFunction((value, version) -> version + ":" + value)
+                .withVersion(Version.version("2.0.0"))
+                .build();
+            print(map.get(arg1));
         }
     }
 
diff --git a/core/api/src/main/java/org/onosproject/core/Version.java b/core/api/src/main/java/org/onosproject/core/Version.java
index ec0ea6d..379eda2 100644
--- a/core/api/src/main/java/org/onosproject/core/Version.java
+++ b/core/api/src/main/java/org/onosproject/core/Version.java
@@ -100,8 +100,9 @@
     public static Version fromInt(int version) {
         int major = (version >> 24) & 0xff;
         int minor = (version >> 16) & 0xff;
-        int patch = (version) & 0xffff;
-        return new Version(major, minor, String.valueOf(patch), null);
+        int patch = (version >> 8) & 0xff;
+        int build = version & 0xff;
+        return new Version(major, minor, String.valueOf(patch), String.valueOf(build));
     }
 
     /**
@@ -157,13 +158,30 @@
     public int toInt() {
         byte major = (byte) this.major;
         byte minor = (byte) this.minor;
-        short patch;
-        try {
-            patch = (short) Integer.parseInt(this.patch);
-        } catch (NumberFormatException e) {
+
+        byte patch;
+        if (this.patch != null) {
+            try {
+                patch = (byte) Integer.parseInt(this.patch.replaceAll("[^0-9]", ""));
+            } catch (NumberFormatException e) {
+                patch = 0;
+            }
+        } else {
             patch = 0;
         }
-        return major << 24 | (minor & 0xff) << 16 | (patch & 0xffff);
+
+        byte build;
+        if (this.build != null) {
+            try {
+                build = (byte) Integer.parseInt(this.build.replaceAll("[^0-9]", ""));
+            } catch (NumberFormatException e) {
+                build = 0;
+            }
+        } else {
+            build = 0;
+        }
+
+        return major << 24 | (minor & 0xff) << 16 | (patch & 0xff) << 8 | (build & 0xff);
     }
 
     @Override
diff --git a/core/api/src/test/java/org/onosproject/VersionTest.java b/core/api/src/test/java/org/onosproject/VersionTest.java
index 1c972ee..482d798 100644
--- a/core/api/src/test/java/org/onosproject/VersionTest.java
+++ b/core/api/src/test/java/org/onosproject/VersionTest.java
@@ -83,25 +83,30 @@
 
         version1 = version("1.2");
         version2 = Version.fromInt(version1.toInt());
-        assertEquals(version2, version(1, 2, "0", null));
+        assertEquals(version2, version(1, 2, "0", "0"));
 
         version1 = version("1.2.foo.bar");
         version2 = Version.fromInt(version1.toInt());
-        assertEquals(version2, version(1, 2, "0", null));
+        assertEquals(version2, version(1, 2, "0", "0"));
 
         version1 = version("1.2.3");
         version2 = Version.fromInt(version1.toInt());
-        assertEquals(version2, version(1, 2, "3", null));
+        assertEquals(version2, version(1, 2, "3", "0"));
 
-        version1 = version("255.254.65535.252");
+        version1 = version("1.2.3-SNAPSHOT");
         version2 = Version.fromInt(version1.toInt());
-        assertEquals(version2, version(255, 254, "65535", null));
+        assertEquals(version2, version(1, 2, "3", "0"));
+
+        version1 = version("255.254.253.252");
+        version2 = Version.fromInt(version1.toInt());
+        assertEquals(version2, version(255, 254, "253", "252"));
 
         assertTrue(version("0.0.2").toInt() > version("0.0.1").toInt());
         assertTrue(version("0.1.0").toInt() > version("0.0.1").toInt());
         assertTrue(version("1.0.0").toInt() > version("0.1.0").toInt());
         assertTrue(version("1.1.0").toInt() > version("1.0.1").toInt());
         assertTrue(version("2.1.1").toInt() > version("1.10.10").toInt());
+        assertTrue(version("0.1.0-rc2").toInt() > version("0.1.0-rc1").toInt());
     }
 
     @Test
diff --git a/core/net/src/main/java/org/onosproject/upgrade/impl/UpgradeManager.java b/core/net/src/main/java/org/onosproject/upgrade/impl/UpgradeManager.java
index 218a357..3910f4b 100644
--- a/core/net/src/main/java/org/onosproject/upgrade/impl/UpgradeManager.java
+++ b/core/net/src/main/java/org/onosproject/upgrade/impl/UpgradeManager.java
@@ -105,13 +105,6 @@
 
         Upgrade upgrade = getState();
 
-        // If the upgrade state is not initialized, ensure this node matches the version of the cluster.
-        if (!upgrade.status().active() && !Objects.equals(upgrade.source(), localVersion)) {
-            log.error("Node version {} inconsistent with cluster version {}", localVersion, upgrade.source());
-            throw new IllegalStateException("Node version " + localVersion +
-                    " inconsistent with cluster version " + upgrade.source());
-        }
-
         // If the upgrade state is initialized then check the node version.
         if (upgrade.status() == Upgrade.Status.INITIALIZED) {
             // If the source version equals the target version, attempt to update the target version.
diff --git a/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixConsistentMapTest.java b/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixConsistentMapTest.java
index 23f9a1f..3e7a872 100644
--- a/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixConsistentMapTest.java
+++ b/core/store/primitives/src/test/java/org/onosproject/store/primitives/resources/impl/AtomixConsistentMapTest.java
@@ -19,11 +19,17 @@
 import io.atomix.protocols.raft.proxy.RaftProxy;
 import io.atomix.protocols.raft.service.RaftService;
 import org.junit.Test;
+import org.onlab.util.HexString;
 import org.onlab.util.Tools;
 import org.onosproject.store.primitives.MapUpdate;
 import org.onosproject.store.primitives.TransactionId;
+import org.onosproject.store.primitives.impl.CompatibleValue;
+import org.onosproject.store.primitives.impl.DistributedPrimitives;
+import org.onosproject.store.serializers.KryoNamespaces;
+import org.onosproject.store.service.AsyncConsistentMap;
 import org.onosproject.store.service.MapEvent;
 import org.onosproject.store.service.MapEventListener;
+import org.onosproject.store.service.Serializer;
 import org.onosproject.store.service.TransactionLog;
 import org.onosproject.store.service.Version;
 import org.onosproject.store.service.Versioned;
@@ -584,6 +590,50 @@
         assertTrue(Arrays.equals(value2, event.newValue().value()));
     }
 
+    @Test
+    public void testCompatibilityFunction() throws Throwable {
+        AtomixConsistentMap atomixMap = newPrimitive("testCompatibilityFunction");
+
+        Serializer rawSerializer = Serializer.using(KryoNamespaces.API, CompatibleValue.class);
+        Serializer valueSerializer = Serializer.using(KryoNamespaces.BASIC);
+
+        // Convert the byte[] value to CompatibleValue<byte[]>
+        AsyncConsistentMap<String, CompatibleValue<byte[]>> rawMap = DistributedPrimitives.newTranscodingMap(
+            atomixMap,
+            key -> HexString.toHexString(rawSerializer.encode(key)),
+            string -> rawSerializer.decode(HexString.fromHexString(string)),
+            value -> value == null ? null : rawSerializer.encode(value),
+            bytes -> rawSerializer.decode(bytes));
+
+        // Convert the CompatibleValue<byte[]> value to CompatibleValue<V> using the user-provided serializer.
+        AsyncConsistentMap<String, CompatibleValue<String>> compatibleMap =
+            DistributedPrimitives.newTranscodingMap(
+                rawMap,
+                key -> key,
+                key -> key,
+                value -> value == null ? null :
+                    new CompatibleValue<byte[]>(valueSerializer.encode(value.value()), value.version()),
+                value -> value == null ? null :
+                    new CompatibleValue<String>(valueSerializer.decode(value.value()), value.version()));
+
+        AsyncConsistentMap<String, String> map1 = DistributedPrimitives.newCompatibleMap(
+            compatibleMap,
+            (value, version) -> version + ":" + value,
+            org.onosproject.core.Version.version("1.0.0"));
+        AsyncConsistentMap<String, String> map2 = DistributedPrimitives.newCompatibleMap(
+            compatibleMap,
+            (value, version) -> version + ":" + value,
+            org.onosproject.core.Version.version("1.0.1"));
+
+        map1.put("foo", "Hello world!").join();
+        assertEquals("Hello world!", map1.get("foo").join().value());
+        assertEquals("1.0.0:Hello world!", map2.get("foo").join().value());
+
+        map2.put("bar", "Hello world again!").join();
+        assertEquals("Hello world again!", map2.get("bar").join().value());
+        assertEquals("1.0.1:Hello world again!", map1.get("bar").join().value());
+    }
+
     private static class TestMapEventListener implements MapEventListener<String, byte[]> {
 
         private final BlockingQueue<MapEvent<String, byte[]>> queue = new ArrayBlockingQueue<>(1);
diff --git a/tools/test/scenarios/rolling-upgrade.xml b/tools/test/scenarios/rolling-upgrade.xml
new file mode 100644
index 0000000..3a4f88a
--- /dev/null
+++ b/tools/test/scenarios/rolling-upgrade.xml
@@ -0,0 +1,82 @@
+<!--
+  ~ Copyright 2018-present Open Networking Foundation
+  ~
+  ~ 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.
+  -->
+<scenario name="rolling-upgrade" description="ONOS cluster rolling upgrade">
+    <import file="${ONOS_SCENARIOS}/dist-setup.xml"/>
+    <dependency name="Distributed-Primitives-Setup"/>
+
+    <group name="Upgrade" requires="Distributed-Primitives-Setup">
+        <step name="Push-Bits" exec="onos-push-bits-through-proxy" if="${OCT}"/>
+
+        <group name="Perform-Upgrade">
+            <sequential var="${OC#}"
+                        starts="Stop-Service-${#}"
+                        ends="Wait-for-Start-${#-1}">
+                <step name="Stop-Service-${#}"
+                      exec="onos-service ${OC#} stop"/>
+
+                <step name="Wait-for-Stop-${#}"
+                      exec="onos-wait-for-stop ${OC#}"
+                      requires="~Stop-Service-${#}"/>
+
+                <step name="Uninstall-${#}"
+                      exec="onos-uninstall ${OC#}"
+                      requires="~Wait-for-Stop-${#}"/>
+
+                <step name="Push-Bits-${#}"
+                      exec="onos-push-bits ${OC#}"
+                      unless="${OCT}"
+                      requires="~Stop-Service-${#}"/>
+
+                <step name="Install-Upgrade-${#}"
+                      exec="onos-install -v ${OC#}"
+                      requires="Push-Bits-${#},Push-Bits,Uninstall-${#}"/>
+
+                <step name="Secure-SSH-${#}"
+                      exec="onos-secure-ssh -u ${ONOS_WEB_USER} -p ${ONOS_WEB_PASS} ${OC#}"
+                      requires="~Install-Upgrade-${#}"/>
+
+                <step name="Wait-for-Start-${#}"
+                      exec="onos-wait-for-start ${OC#}"
+                      requires="Secure-SSH-${#}"/>
+
+                <step name="Distributed-Primitives-Check-Apps-${#}"
+                      exec="onos-check-apps ${OC#} distributedprimitives includes"
+                      requires="Wait-for-Start-${#}"/>
+            </sequential>
+        </group>
+
+        <group name="Verify-Upgrade" requires="Perform-Upgrade">
+            <parallel var="${OC#}">
+                <step name="Check-Nodes-${#}"
+                      exec="onos-check-nodes ${OC#}"
+                      delay="3"/>
+
+                <step name="Check-Components-${#}"
+                      exec="onos-check-components ${OC#}"
+                      delay="5"
+                      requires="~Check-Nodes-${#}"/>
+
+                <step name="Check-Logs-${#}"
+                      exec="onos-check-logs ${OC#}"
+                      requires="~Check-Components-${#}"/>
+
+                <step name="Check-Apps-${#}"
+                      exec="onos-check-apps ${OC#} ${ONOS_APPS},distributedprimitives includes"
+                      requires="~Check-Components-${#}"/>
+            </parallel>
+        </group>
+    </group>
+</scenario>