瀏覽代碼

Add max_resource_units to enterprise license (#50735)

The enterprise license type must has "max_resource_units" and may not
have "max_nodes".

This change adds support for this new field, validation that the field
is present if-and-only-if the license is enterprise and bumps the
license version number to reflect the new field.
Tim Vernum 5 年之前
父節點
當前提交
8727f2717e

+ 1 - 0
docs/reference/licensing/get-license.asciidoc

@@ -60,6 +60,7 @@ GET /_license
     "expiry_date" : "2018-11-19T22:05:12.332Z",
     "expiry_date_in_millis" : 1542665112332,
     "max_nodes" : 1000,
+    "max_resource_units" : null,
     "issued_to" : "test",
     "issuer" : "elasticsearch",
     "start_date_in_millis" : -1

+ 95 - 34
x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java

@@ -113,13 +113,19 @@ public class License implements ToXContentObject {
         static boolean isTrial(String typeName) {
             return TRIAL.getTypeName().equals(typeName);
         }
+
+        static boolean isEnterprise(String typeName) {
+            return ENTERPRISE.getTypeName().equals(typeName);
+        }
+
     }
 
     public static final int VERSION_START = 1;
     public static final int VERSION_NO_FEATURE_TYPE = 2;
     public static final int VERSION_START_DATE = 3;
     public static final int VERSION_CRYPTO_ALGORITHMS = 4;
-    public static final int VERSION_CURRENT = VERSION_CRYPTO_ALGORITHMS;
+    public static final int VERSION_ENTERPRISE = 5;
+    public static final int VERSION_CURRENT = VERSION_ENTERPRISE;
 
     /**
      * XContent param name to deserialize license(s) with
@@ -153,13 +159,14 @@ public class License implements ToXContentObject {
     private final long expiryDate;
     private final long startDate;
     private final int maxNodes;
+    private final int maxResourceUnits;
     private final OperationMode operationMode;
 
     /**
      * Decouples operation mode of a license from the license type value.
      * <p>
      * Note: The mode indicates features that should be made available, but it does not indicate whether the license is active!
-     *
+     * <p>
      * The id byte is used for ordering operation modes
      */
     public enum OperationMode {
@@ -176,13 +183,16 @@ public class License implements ToXContentObject {
             this.id = id;
         }
 
-        /** Returns non-zero positive number when <code>opMode1</code> is greater than <code>opMode2</code> */
+        /**
+         * Returns non-zero positive number when <code>opMode1</code> is greater than <code>opMode2</code>
+         */
         public static int compare(OperationMode opMode1, OperationMode opMode2) {
             return Integer.compare(opMode1.id, opMode2.id);
         }
 
         /**
          * Determine the operating mode for a license type
+         *
          * @see LicenseType#resolve(License)
          * @see #parse(String)
          */
@@ -211,6 +221,7 @@ public class License implements ToXContentObject {
          * Parses an {@code OperatingMode} from a String.
          * The string must name an operating mode, and not a licensing level (that is, it cannot parse old style license levels
          * such as "dev" or "silver").
+         *
          * @see #description()
          */
         public static OperationMode parse(String mode) {
@@ -227,8 +238,8 @@ public class License implements ToXContentObject {
         }
     }
 
-    private License(int version, String uid, String issuer, String issuedTo, long issueDate, String type,
-                    String subscriptionType, String feature, String signature, long expiryDate, int maxNodes, long startDate) {
+    private License(int version, String uid, String issuer, String issuedTo, long issueDate, String type, String subscriptionType,
+                    String feature, String signature, long expiryDate, int maxNodes, int maxResourceUnits, long startDate) {
         this.version = version;
         this.uid = uid;
         this.issuer = issuer;
@@ -246,6 +257,7 @@ public class License implements ToXContentObject {
             this.expiryDate = expiryDate;
         }
         this.maxNodes = maxNodes;
+        this.maxResourceUnits = maxResourceUnits;
         this.startDate = startDate;
         this.operationMode = OperationMode.resolve(LicenseType.resolve(this));
         validate();
@@ -294,12 +306,21 @@ public class License implements ToXContentObject {
     }
 
     /**
-     * @return the maximum number of nodes this license has been issued for
+     * @return the maximum number of nodes this license has been issued for, or {@code -1} if this license is not node based.
      */
     public int maxNodes() {
         return maxNodes;
     }
 
+    /**
+     * @return the maximum number of "resource units" this license has been issued for, or {@code -1} if this license is not resource based.
+     * A "resource unit" is a measure of computing power (RAM/CPU), the definition of which is maintained outside of the license format,
+     * or this class.
+     */
+    public int maxResourceUnits() {
+        return maxResourceUnits;
+    }
+
     /**
      * @return a string representing the entity this licenses has been issued to
      */
@@ -386,20 +407,39 @@ public class License implements ToXContentObject {
             throw new IllegalStateException("uid can not be null");
         } else if (feature == null && version == VERSION_START) {
             throw new IllegalStateException("feature can not be null");
-        } else if (maxNodes == -1) {
-            throw new IllegalStateException("maxNodes has to be set");
         } else if (expiryDate == -1) {
             throw new IllegalStateException("expiryDate has to be set");
         } else if (expiryDate == LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS && LicenseType.isBasic(type) == false) {
             throw new IllegalStateException("only basic licenses are allowed to have no expiration");
         }
+
+        if (LicenseType.isEnterprise(type) && version < VERSION_ENTERPRISE) {
+            throw new IllegalStateException("license type [" + type + "] is not a valid for version [" + version + "] licenses");
+        }
+        validateLimits(type, maxNodes, maxResourceUnits);
+    }
+
+    private static void validateLimits(String type, int maxNodes, int maxResourceUnits) {
+        if (LicenseType.isEnterprise(type)) {
+            if (maxResourceUnits == -1) {
+                throw new IllegalStateException("maxResourceUnits must be set for enterprise licenses (type=[" + type + "])");
+            } else if (maxNodes != -1) {
+                throw new IllegalStateException("maxNodes may not be set for enterprise licenses (type=[" + type + "])");
+            }
+        } else {
+            if (maxNodes == -1) {
+                throw new IllegalStateException("maxNodes has to be set");
+            } else if (maxResourceUnits != -1) {
+                throw new IllegalStateException("maxResourceUnits may only be set for enterprise licenses (not permitted for type=[" +
+                    type + "])");
+            }
+        }
     }
 
     public static License readLicense(StreamInput in) throws IOException {
         int version = in.readVInt(); // Version for future extensibility
         if (version > VERSION_CURRENT) {
-            throw new ElasticsearchException("Unknown license version found, please upgrade all nodes to the latest elasticsearch-license" +
-                    " plugin");
+            throw new ElasticsearchException("Unknown license version found, please upgrade all nodes to the latest elasticsearch release");
         }
         Builder builder = builder();
         builder.version(version);
@@ -414,6 +454,9 @@ public class License implements ToXContentObject {
         }
         builder.expiryDate(in.readLong());
         builder.maxNodes(in.readInt());
+        if (version >= VERSION_ENTERPRISE) {
+            builder.maxResourceUnits(in.readInt());
+        }
         builder.issuedTo(in.readString());
         builder.issuer(in.readString());
         builder.signature(in.readOptionalString());
@@ -436,6 +479,9 @@ public class License implements ToXContentObject {
         }
         out.writeLong(expiryDate);
         out.writeInt(maxNodes);
+        if (version >= VERSION_ENTERPRISE) {
+            out.writeInt(maxResourceUnits);
+        }
         out.writeString(issuedTo);
         out.writeString(issuer);
         out.writeOptionalString(signature);
@@ -496,7 +542,14 @@ public class License implements ToXContentObject {
         if (expiryDate != LicenseService.BASIC_SELF_GENERATED_LICENSE_EXPIRATION_MILLIS) {
             builder.timeField(Fields.EXPIRY_DATE_IN_MILLIS, Fields.EXPIRY_DATE, expiryDate);
         }
-        builder.field(Fields.MAX_NODES, maxNodes);
+
+        if (version >= VERSION_ENTERPRISE) {
+            builder.field(Fields.MAX_NODES, maxNodes == -1 ? null : maxNodes);
+            builder.field(Fields.MAX_RESOURCE_UNITS, maxResourceUnits == -1 ? null : maxResourceUnits);
+        } else {
+            builder.field(Fields.MAX_NODES, maxNodes);
+        }
+
         builder.field(Fields.ISSUED_TO, issuedTo);
         builder.field(Fields.ISSUER, issuer);
         if (!licenseSpecMode && !restViewMode && signature != null) {
@@ -541,6 +594,8 @@ public class License implements ToXContentObject {
                         builder.startDate(parser.longValue());
                     } else if (Fields.MAX_NODES.equals(currentFieldName)) {
                         builder.maxNodes(parser.intValue());
+                    } else if (Fields.MAX_RESOURCE_UNITS.equals(currentFieldName)) {
+                        builder.maxResourceUnits(parser.intValue());
                     } else if (Fields.ISSUED_TO.equals(currentFieldName)) {
                         builder.issuedTo(parser.text());
                     } else if (Fields.ISSUER.equals(currentFieldName)) {
@@ -583,7 +638,7 @@ public class License implements ToXContentObject {
                 throw new ElasticsearchException("malformed signature for license [" + builder.uid + "]");
             } else if (version > VERSION_CURRENT) {
                 throw new ElasticsearchException("Unknown license version found, please upgrade all nodes to the latest " +
-                        "elasticsearch-license plugin");
+                    "elasticsearch-license plugin");
             }
             // signature version is the source of truth
             builder.version(version);
@@ -615,8 +670,7 @@ public class License implements ToXContentObject {
         // EMPTY is safe here because we don't call namedObject
         try (InputStream byteStream = bytes.streamInput();
              XContentParser parser = xContentType.xContent()
-                .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, byteStream))
-        {
+                 .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, byteStream)) {
             License license = null;
             if (parser.nextToken() == XContentParser.Token.START_OBJECT) {
                 if (parser.nextToken() == XContentParser.Token.FIELD_NAME) {
@@ -665,7 +719,7 @@ public class License implements ToXContentObject {
 
         if (issueDate != license.issueDate) return false;
         if (expiryDate != license.expiryDate) return false;
-        if (startDate!= license.startDate) return false;
+        if (startDate != license.startDate) return false;
         if (maxNodes != license.maxNodes) return false;
         if (version != license.version) return false;
         if (uid != null ? !uid.equals(license.uid) : license.uid != null) return false;
@@ -690,7 +744,7 @@ public class License implements ToXContentObject {
         result = 31 * result + (feature != null ? feature.hashCode() : 0);
         result = 31 * result + (signature != null ? signature.hashCode() : 0);
         result = 31 * result + (int) (expiryDate ^ (expiryDate >>> 32));
-        result = 31 * result + (int) (startDate ^ (startDate>>> 32));
+        result = 31 * result + (int) (startDate ^ (startDate >>> 32));
         result = 31 * result + maxNodes;
         result = 31 * result + version;
         return result;
@@ -709,6 +763,7 @@ public class License implements ToXContentObject {
         public static final String START_DATE_IN_MILLIS = "start_date_in_millis";
         public static final String START_DATE = "start_date";
         public static final String MAX_NODES = "max_nodes";
+        public static final String MAX_RESOURCE_UNITS = "max_resource_units";
         public static final String ISSUED_TO = "issued_to";
         public static final String ISSUER = "issuer";
         public static final String VERSION = "version";
@@ -752,6 +807,7 @@ public class License implements ToXContentObject {
         private long expiryDate = -1;
         private long startDate = -1;
         private int maxNodes = -1;
+        private int maxResourceUnits = -1;
 
         public Builder uid(String uid) {
             this.uid = uid;
@@ -807,6 +863,11 @@ public class License implements ToXContentObject {
             return this;
         }
 
+        public Builder maxResourceUnits(int maxUnits) {
+            this.maxResourceUnits = maxUnits;
+            return this;
+        }
+
         public Builder signature(String signature) {
             if (signature != null) {
                 this.signature = signature;
@@ -821,17 +882,18 @@ public class License implements ToXContentObject {
 
         public Builder fromLicenseSpec(License license, String signature) {
             return uid(license.uid())
-                    .version(license.version())
-                    .issuedTo(license.issuedTo())
-                    .issueDate(license.issueDate())
-                    .startDate(license.startDate())
-                    .type(license.type())
-                    .subscriptionType(license.subscriptionType)
-                    .feature(license.feature)
-                    .maxNodes(license.maxNodes())
-                    .expiryDate(license.expiryDate())
-                    .issuer(license.issuer())
-                    .signature(signature);
+                .version(license.version())
+                .issuedTo(license.issuedTo())
+                .issueDate(license.issueDate())
+                .startDate(license.startDate())
+                .type(license.type())
+                .subscriptionType(license.subscriptionType)
+                .feature(license.feature)
+                .maxNodes(license.maxNodes())
+                .maxResourceUnits(license.maxResourceUnits())
+                .expiryDate(license.expiryDate())
+                .issuer(license.issuer())
+                .signature(signature);
         }
 
         /**
@@ -840,15 +902,15 @@ public class License implements ToXContentObject {
          */
         public Builder fromPre20LicenseSpec(License pre20License) {
             return uid(pre20License.uid())
-                    .issuedTo(pre20License.issuedTo())
-                    .issueDate(pre20License.issueDate())
-                    .maxNodes(pre20License.maxNodes())
-                    .expiryDate(pre20License.expiryDate());
+                .issuedTo(pre20License.issuedTo())
+                .issueDate(pre20License.issueDate())
+                .maxNodes(pre20License.maxNodes())
+                .expiryDate(pre20License.expiryDate());
         }
 
         public License build() {
             return new License(version, uid, issuer, issuedTo, issueDate, type,
-                    subscriptionType, feature, signature, expiryDate, maxNodes, startDate);
+                subscriptionType, feature, signature, expiryDate, maxNodes, maxResourceUnits, startDate);
         }
 
         public Builder validate() {
@@ -864,11 +926,10 @@ public class License implements ToXContentObject {
                 throw new IllegalStateException("uid can not be null");
             } else if (signature == null) {
                 throw new IllegalStateException("signature can not be null");
-            } else if (maxNodes == -1) {
-                throw new IllegalStateException("maxNodes has to be set");
             } else if (expiryDate == -1) {
                 throw new IllegalStateException("expiryDate has to be set");
             }
+            validateLimits(type, maxNodes, maxResourceUnits);
             return this;
         }
 

+ 3 - 5
x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseService.java

@@ -121,6 +121,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
      * Max number of nodes licensed by generated trial license
      */
     static final int SELF_GENERATED_LICENSE_MAX_NODES = 1000;
+    static final int SELF_GENERATED_LICENSE_MAX_RESOURCE_UNITS = SELF_GENERATED_LICENSE_MAX_NODES;
 
     public static final String LICENSE_JOB = "licenseJob";
 
@@ -291,11 +292,8 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
     }
 
     private static boolean licenseIsCompatible(License license, Version version) {
-        if (License.LicenseType.ENTERPRISE.getTypeName().equalsIgnoreCase(license.type())) {
-            return version.onOrAfter(Version.V_7_6_0);
-        } else {
-            return true;
-        }
+        final int maxVersion = LicenseUtils.getMaxLicenseVersion(version);
+        return license.version() <= maxVersion;
     }
 
     private boolean isAllowedLicenseType(License.LicenseType type) {

+ 13 - 4
x-pack/plugin/core/src/main/java/org/elasticsearch/license/LicenseUtils.java

@@ -6,6 +6,7 @@
 package org.elasticsearch.license;
 
 import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.Version;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
 import org.elasticsearch.license.License.LicenseType;
 import org.elasticsearch.rest.RestStatus;
@@ -46,18 +47,26 @@ public class LicenseUtils {
      * recreated with the new key
      */
     public static boolean signatureNeedsUpdate(License license, DiscoveryNodes currentNodes) {
-        assert License.VERSION_CRYPTO_ALGORITHMS == License.VERSION_CURRENT : "update this method when adding a new version";
+        assert License.VERSION_ENTERPRISE == License.VERSION_CURRENT : "update this method when adding a new version";
 
         String typeName = license.type();
         return (LicenseType.isBasic(typeName) || LicenseType.isTrial(typeName)) &&
                 // only upgrade signature when all nodes are ready to deserialize the new signature
                 (license.version() < License.VERSION_CRYPTO_ALGORITHMS &&
-                    compatibleLicenseVersion(currentNodes) == License.VERSION_CRYPTO_ALGORITHMS
+                    compatibleLicenseVersion(currentNodes) >= License.VERSION_CRYPTO_ALGORITHMS
                 );
     }
 
     public static int compatibleLicenseVersion(DiscoveryNodes currentNodes) {
-        assert License.VERSION_CRYPTO_ALGORITHMS == License.VERSION_CURRENT : "update this method when adding a new version";
-        return License.VERSION_CRYPTO_ALGORITHMS;
+        return getMaxLicenseVersion(currentNodes.getMinNodeVersion());
+    }
+
+    public static int getMaxLicenseVersion(Version version) {
+        if (version != null && version.before(Version.V_7_6_0)) {
+            return License.VERSION_CRYPTO_ALGORITHMS;
+        } else {
+            assert License.VERSION_ENTERPRISE == License.VERSION_CURRENT : "update this method when adding a new version";
+            return License.VERSION_ENTERPRISE;
+        }
     }
 }

+ 5 - 1
x-pack/plugin/core/src/main/java/org/elasticsearch/license/StartTrialClusterTask.java

@@ -75,10 +75,14 @@ public class StartTrialClusterTask extends ClusterStateUpdateTask {
             License.Builder specBuilder = License.builder()
                     .uid(UUID.randomUUID().toString())
                     .issuedTo(clusterName)
-                    .maxNodes(LicenseService.SELF_GENERATED_LICENSE_MAX_NODES)
                     .issueDate(issueDate)
                     .type(request.getType())
                     .expiryDate(expiryDate);
+            if (License.LicenseType.isEnterprise(request.getType())) {
+                specBuilder.maxResourceUnits(LicenseService.SELF_GENERATED_LICENSE_MAX_RESOURCE_UNITS);
+            } else {
+                specBuilder.maxNodes(LicenseService.SELF_GENERATED_LICENSE_MAX_NODES);
+            }
             License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder, currentState.nodes());
             LicensesMetaData newLicensesMetaData = new LicensesMetaData(selfGeneratedLicense, Version.CURRENT);
             mdBuilder.putCustom(LicensesMetaData.TYPE, newLicensesMetaData);

+ 1 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/license/AbstractLicenseServiceTestCase.java

@@ -70,6 +70,7 @@ public abstract class AbstractLicenseServiceTestCase extends ESTestCase {
         when(discoveryNodes.getMasterNode()).thenReturn(mockNode);
         when(discoveryNodes.spliterator()).thenReturn(Arrays.asList(mockNode).spliterator());
         when(discoveryNodes.isLocalNodeElectedMaster()).thenReturn(false);
+        when(discoveryNodes.getMinNodeVersion()).thenReturn(mockNode.getVersion());
         when(state.nodes()).thenReturn(discoveryNodes);
         when(state.getNodes()).thenReturn(discoveryNodes); // it is really ridiculous we have nodes() and getNodes()...
         when(clusterService.state()).thenReturn(state);

+ 13 - 9
x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseOperationModeUpdateTests.java

@@ -35,15 +35,19 @@ public class LicenseOperationModeUpdateTests extends ESTestCase {
 
     public void testLicenseOperationModeUpdate() throws Exception {
         License.LicenseType type = randomFrom(License.LicenseType.values());
-        License license = License.builder()
-                .uid("id")
-                .expiryDate(0)
-                .issueDate(0)
-                .issuedTo("elasticsearch")
-                .issuer("issuer")
-                .type(type)
-                .maxNodes(1)
-                .build();
+        final License.Builder licenseBuilder = License.builder()
+            .uid("id")
+            .expiryDate(0)
+            .issueDate(0)
+            .issuedTo("elasticsearch")
+            .issuer("issuer")
+            .type(type);
+        if (type == License.LicenseType.ENTERPRISE) {
+            licenseBuilder.maxResourceUnits(1);
+        } else {
+            licenseBuilder.maxNodes(1);
+        }
+        License license = licenseBuilder.build();
 
         assertThat(license.operationMode(), equalTo(License.OperationMode.resolve(type)));
         OperationModeFileWatcherTests.writeMode("gold", licenseModeFile);

+ 2 - 1
x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseServiceTests.java

@@ -190,7 +190,8 @@ public class LicenseServiceTests extends ESTestCase {
             .issuer(randomAlphaOfLengthBetween(5, 60))
             .issuedTo(randomAlphaOfLengthBetween(5, 60))
             .issueDate(System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(randomLongBetween(1, 5000)))
-            .maxNodes(randomIntBetween(1, 500))
+            .maxNodes(type == License.LicenseType.ENTERPRISE ? -1 : randomIntBetween(1, 500))
+            .maxResourceUnits(type == License.LicenseType.ENTERPRISE ? randomIntBetween(10, 500) : -1)
             .signature(null)
             .build();
     }

+ 155 - 17
x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseTests.java

@@ -6,13 +6,18 @@
 package org.elasticsearch.license;
 
 import org.elasticsearch.ElasticsearchException;
+import org.elasticsearch.Version;
 import org.elasticsearch.common.Strings;
 import org.elasticsearch.common.UUIDs;
 import org.elasticsearch.common.bytes.BytesArray;
+import org.elasticsearch.common.io.stream.BytesStreamOutput;
+import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.xcontent.NamedXContentRegistry;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.common.xcontent.XContentType;
 import org.elasticsearch.test.ESTestCase;
+import org.elasticsearch.test.TestMatchers;
+import org.hamcrest.Matchers;
 
 import java.nio.BufferUnderflowException;
 import java.nio.charset.StandardCharsets;
@@ -27,7 +32,7 @@ import static org.hamcrest.Matchers.notNullValue;
 
 public class LicenseTests extends ESTestCase {
 
-    public void testFromXContent() throws Exception {
+    public void testFromXContentForGoldLicenseWithVersion2Signature() throws Exception {
         String licenseString = "{\"license\":" +
             "{\"uid\":\"4056779d-b823-4c12-a9cb-efa4a8d8c422\"," +
             "\"type\":\"gold\"," +
@@ -51,27 +56,107 @@ public class LicenseTests extends ESTestCase {
         assertThat(license.issuedTo(), equalTo("customer"));
         assertThat(license.expiryDate(), equalTo(1546596340459L));
         assertThat(license.issueDate(), equalTo(1546589020459L));
+        assertThat(license.maxNodes(), equalTo(5));
+        assertThat(license.maxResourceUnits(), equalTo(-1));
+        assertThat(license.version(), equalTo(2));
+    }
+
+    public void testFromXContentForGoldLicenseWithVersion4Signature() throws Exception {
+        String licenseString = "{\"license\":{" +
+            "\"uid\":\"4056779d-b823-4c12-a9cb-efa4a8d8c422\"," +
+            "\"type\":\"gold\"," +
+            "\"issue_date_in_millis\":1546589020459," +
+            "\"expiry_date_in_millis\":1546596340459," +
+            "\"max_nodes\":5," +
+            "\"issued_to\":\"customer\"," +
+            "\"issuer\":\"elasticsearch\"," +
+            "\"signature\":\"AAAABAAAAA22vXffI41oM4jLCwZ6AAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAH3oL4weubwYGjLGNZsz90" +
+            "EerX6yOX3Dh6wswG9EfqCiyv6lcjuC7aeKKuOkqhMRTHZ9vHnfMuakHWVlpuGC14WyGqaMwSmgTZ9jVAzt/W3sIotRxM/3rtlCXUc1rOUXNFcii1i3Kkrc" +
+            "kTzhENTKjdkOmUN3qZlTEmHkp93eYpx8++iIukHYU9K9Vm2VKgydFfxvYaN/Qr+iPfJSbHJB8+DmS2ywdrmdqW+ScE+1ZNouPNhnP3RKTleNvixXPG9l5B" +
+            "qZ2So1IlCrxVDByA1E6JH5AvjbOucpcGiWCm7IzvfpkzphKHMyxhUaIByoHl9UAf4AdPLhowWAQk0eHMRDDlo=\"," +
+            "\"start_date_in_millis\":-1}}\n";
+        License license = License.fromSource(new BytesArray(licenseString.getBytes(StandardCharsets.UTF_8)),
+            XContentType.JSON);
+        assertThat(license.type(), equalTo("gold"));
+        assertThat(license.uid(), equalTo("4056779d-b823-4c12-a9cb-efa4a8d8c422"));
+        assertThat(license.issuer(), equalTo("elasticsearch"));
+        assertThat(license.issuedTo(), equalTo("customer"));
+        assertThat(license.expiryDate(), equalTo(1546596340459L));
+        assertThat(license.issueDate(), equalTo(1546589020459L));
+        assertThat(license.maxNodes(), equalTo(5));
+        assertThat(license.maxResourceUnits(), equalTo(-1));
+        assertThat(license.version(), equalTo(4));
+    }
+
+    public void testFromXContentForEnterpriseLicenseWithV5Signature() throws Exception {
+        String licenseString = "{\"license\":{" +
+            "\"uid\":\"4056779d-b823-4c12-a9cb-efa4a8d8c422\"," +
+            "\"type\":\"enterprise\"," +
+            "\"issue_date_in_millis\":1546589020459," +
+            "\"expiry_date_in_millis\":1546596340459," +
+            "\"max_nodes\":null," +
+            "\"max_resource_units\":15," +
+            "\"issued_to\":\"customer\"," +
+            "\"issuer\":\"elasticsearch\"," +
+            "\"signature\":\"AAAABQAAAA2MUoEqXb9K9Ie5d6JJAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAAwVZKGAmDELUlS5PScBkhQsZa" +
+            "DaQTtJ4ZP5EnZ/nLpmCt9Dj7d/FRsgMtHmSJLrr2CdrIo4Vx5VuhmbwzZvXMttLz2lrJzG7770PX3TnC9e7F9GdnE9ec0FP2U0ZlLOBOtPuirX0q+j6GfB+DLyE" +
+            "5D+Lo1NQ3eLJGvbd3DBYPWJxkb+EBVHczCH2OrIEVWnN/TafmkdZCPX5PcultkNOs3j7d3s7b51EXHKoye8UTcB/RGmzZwMah+E6I/VJkqu7UHL8bB01wJeqo6W" +
+            "xI4LC/9+f5kpmHrUu3CHe5pHbmMGDk7O6/cwt1pw/hnJXKIFCi36IGaKcHLgORxQdN0uzE=\"," +
+            "\"start_date_in_millis\":-1}}";
+        License license = License.fromSource(new BytesArray(licenseString.getBytes(StandardCharsets.UTF_8)),
+            XContentType.JSON);
+        assertThat(license.type(), equalTo("enterprise"));
+        assertThat(license.uid(), equalTo("4056779d-b823-4c12-a9cb-efa4a8d8c422"));
+        assertThat(license.issuer(), equalTo("elasticsearch"));
+        assertThat(license.issuedTo(), equalTo("customer"));
+        assertThat(license.expiryDate(), equalTo(1546596340459L));
+        assertThat(license.issueDate(), equalTo(1546589020459L));
+        assertThat(license.maxNodes(), equalTo(-1));
+        assertThat(license.maxResourceUnits(), equalTo(15));
+        assertThat(license.version(), equalTo(5));
+    }
+
+    public void testThatEnterpriseLicenseMayNotHaveMaxNodes() throws Exception {
+        License.Builder builder = randomLicense(License.LicenseType.ENTERPRISE)
+            .maxNodes(randomIntBetween(1, 50))
+            .maxResourceUnits(randomIntBetween(10, 500));
+        final IllegalStateException ex = expectThrows(IllegalStateException.class, builder::build);
+        assertThat(ex, TestMatchers.throwableWithMessage("maxNodes may not be set for enterprise licenses (type=[enterprise])"));
+    }
+
+    public void testThatEnterpriseLicenseMustHaveMaxResourceUnits() throws Exception {
+        License.Builder builder = randomLicense(License.LicenseType.ENTERPRISE)
+            .maxResourceUnits(-1);
+        final IllegalStateException ex = expectThrows(IllegalStateException.class, builder::build);
+        assertThat(ex, TestMatchers.throwableWithMessage("maxResourceUnits must be set for enterprise licenses (type=[enterprise])"));
+    }
+
+    public void testThatRegularLicensesMustHaveMaxNodes() throws Exception {
+        License.LicenseType type = randomValueOtherThan(License.LicenseType.ENTERPRISE, () -> randomFrom(License.LicenseType.values()));
+        License.Builder builder = randomLicense(type)
+            .maxNodes(-1);
+        final IllegalStateException ex = expectThrows(IllegalStateException.class, builder::build);
+        assertThat(ex, TestMatchers.throwableWithMessage("maxNodes has to be set"));
+    }
+
+    public void testThatRegularLicensesMayNotHaveMaxResourceUnits() throws Exception {
+        License.LicenseType type = randomValueOtherThan(License.LicenseType.ENTERPRISE, () -> randomFrom(License.LicenseType.values()));
+        License.Builder builder = randomLicense(type)
+            .maxResourceUnits(randomIntBetween(10, 500))
+            .maxNodes(randomIntBetween(1, 50));
+        final IllegalStateException ex = expectThrows(IllegalStateException.class, builder::build);
+        assertThat(ex, TestMatchers.throwableWithMessage("maxResourceUnits may only be set for enterprise licenses (not permitted " +
+            "for type=[" + type.getTypeName() + "])"));
     }
 
     public void testLicenseToAndFromXContentForEveryLicenseType() throws Exception {
         for (License.LicenseType type : License.LicenseType.values()) {
-            final License license1 = License.builder()
-                .uid(UUIDs.randomBase64UUID(random()))
-                .type(type)
-                .issueDate(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(randomIntBetween(1, 10)))
-                .expiryDate(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(randomIntBetween(1, 1000)))
-                .maxNodes(randomIntBetween(1, 100))
-                .issuedTo(randomAlphaOfLengthBetween(5, 50))
-                .issuer(randomAlphaOfLengthBetween(5, 50))
+            final License license1 = randomLicense(type)
                 // We need a signature that parses correctly, but it doesn't need to verify
-                .signature("AAAAAgAAAA34V2kfTJVtvdL2LttwAAABmFJ6NGRnbEM3WVQrZVQwNkdKQmR1VytlMTMyM1J0dTZ1WGwyY2ZCVFhqMGtJU2gzZ3pnNTVpOW" +
-                    "F5Y1NaUkwyN2VsTEtCYnlZR2c5WWtjQ0phaDlhRjlDUXViUmUwMWhjSkE2TFcwSGdneTJHbUV4N2RHUWJxV20ybjRsZHRzV2xkN0ZmdDlYblJmNVc" +
-                    "xMlBWeU81V1hLUm1EK0V1dmF3cFdlSGZzTU5SZE1qUmFra3JkS1hCanBWVmVTaFFwV3BVZERzeG9Sci9rYnlJK2toODZXY09tNmFHUVNUL3IyUHEx" +
-                    "V3VSTlBneWNJcFQ0bXl0cmhNNnRwbE1CWE4zWjJ5eGFuWFo0NGhsb3B5WFd1eTdYbFFWQkxFVFFPSlBERlB0eVVJYXVSZ0lsR2JpRS9rN1h4MSsvN" +
-                    "UpOcGN6cU1NOHN1cHNtSTFIUGN1bWNGNEcxekhrblhNOXZ2VEQvYmRzQUFwbytUZEpRR3l6QU5oS2ZFSFdSbGxxNDZyZ0xvUHIwRjdBL2JqcnJnNG" +
-                    "FlK09Cek9pYlJ5Umc9PQAAAQAth77fQLF7CCEL7wA6Z0/UuRm/weECcsjW/50kBnPLO8yEs+9/bPa5LSU0bF6byEXOVeO0ebUQfztpjulbXh8TrBD" +
-                    "SG+6VdxGtohPo2IYPBaXzGs3LOOor6An/lhptxBWdwYmfbcp0m8mnXZh1vN9rmbTsZXnhBIoPTaRDwUBi3vJ3Ms3iLaEm4S8Slrfmtht2jUjgGZ2v" +
-                    "AeZ9OHU2YsGtrSpz6f")
+                .signature("AAAABQAAAA2MUoEqXb9K9Ie5d6JJAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAAwVZKGAmDELUlS5PScBkhQsZa" +
+                    "DaQTtJ4ZP5EnZ/nLpmCt9Dj7d/FRsgMtHmSJLrr2CdrIo4Vx5VuhmbwzZvXMttLz2lrJzG7770PX3TnC9e7F9GdnE9ec0FP2U0ZlLOBOtPuirX0q+j" +
+                    "6GfB+DLyE5D+Lo1NQ3eLJGvbd3DBYPWJxkb+EBVHczCH2OrIEVWnN/TafmkdZCPX5PcultkNOs3j7d3s7b51EXHKoye8UTcB/RGmzZwMah+E6I/VJk" +
+                    "qu7UHL8bB01wJeqo6WxI4LC/9+f5kpmHrUu3CHe5pHbmMGDk7O6/cwt1pw/hnJXKIFCi36IGaKcHLgORxQdN0uzE=")
                 .build();
             XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION,
                 Strings.toString(license1));
@@ -83,6 +168,46 @@ public class LicenseTests extends ESTestCase {
             assertThat(license2.issuedTo(), equalTo(license1.issuedTo()));
             assertThat(license2.expiryDate(), equalTo(license1.expiryDate()));
             assertThat(license2.issueDate(), equalTo(license1.issueDate()));
+            assertThat(license2.maxNodes(), equalTo(license1.maxNodes()));
+            assertThat(license2.maxResourceUnits(), equalTo(license1.maxResourceUnits()));
+        }
+    }
+
+    public void testSerializationOfLicenseForEveryLicenseType() throws Exception {
+        for (License.LicenseType type : License.LicenseType.values()) {
+            final String signature = randomBoolean() ? null : "AAAABQAAAA2MUoEqXb9K9Ie5d6JJAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEM" +
+                "hm4jAAABAAAwVZKGAmDELUlS5PScBkhQsZaDaQTtJ4ZP5EnZ/nLpmCt9Dj7d/FRsgMtHmSJLrr2CdrIo4Vx5VuhmbwzZvXMttLz2lrJzG7770PX3TnC9e7" +
+                "F9GdnE9ec0FP2U0ZlLOBOtPuirX0q+j6GfB+DLyE5D+Lo1NQ3eLJGvbd3DBYPWJxkb+EBVHczCH2OrIEVWnN/TafmkdZCPX5PcultkNOs3j7d3s7b51EXH" +
+                "Koye8UTcB/RGmzZwMah+E6I/VJkqu7UHL8bB01wJeqo6WxI4LC/9+f5kpmHrUu3CHe5pHbmMGDk7O6/cwt1pw/hnJXKIFCi36IGaKcHLgORxQdN0uzE=";
+            final int version;
+            if (type == License.LicenseType.ENTERPRISE) {
+                version = randomIntBetween(License.VERSION_ENTERPRISE, License.VERSION_CURRENT);
+            } else {
+                version = randomIntBetween(License.VERSION_NO_FEATURE_TYPE, License.VERSION_CURRENT);
+            }
+
+            final License license1 = randomLicense(type).signature(signature).version(version).build();
+
+            final BytesStreamOutput out = new BytesStreamOutput();
+            out.setVersion(Version.CURRENT);
+            license1.writeTo(out);
+
+            final StreamInput in = out.bytes().streamInput();
+            in.setVersion(Version.CURRENT);
+            final License license2 = License.readLicense(in);
+            assertThat(in.read(), Matchers.equalTo(-1));
+
+            assertThat(license2, notNullValue());
+            assertThat(license2.type(), equalTo(type.getTypeName()));
+            assertThat(license2.version(), equalTo(version));
+            assertThat(license2.signature(), equalTo(signature));
+            assertThat(license2.uid(), equalTo(license1.uid()));
+            assertThat(license2.issuer(), equalTo(license1.issuer()));
+            assertThat(license2.issuedTo(), equalTo(license1.issuedTo()));
+            assertThat(license2.expiryDate(), equalTo(license1.expiryDate()));
+            assertThat(license2.issueDate(), equalTo(license1.issueDate()));
+            assertThat(license2.maxNodes(), equalTo(license1.maxNodes()));
+            assertThat(license2.maxResourceUnits(), equalTo(license1.maxResourceUnits()));
         }
     }
 
@@ -158,4 +283,17 @@ public class LicenseTests extends ESTestCase {
         assertThat(exception.getMessage(), containsString("malformed signature for license [4056779d-b823-4c12-a9cb-efa4a8d8c422]"));
         assertThat(exception.getCause(), instanceOf(IllegalArgumentException.class));
     }
+
+    private License.Builder randomLicense(License.LicenseType type) {
+        return License.builder()
+            .uid(UUIDs.randomBase64UUID(random()))
+            .type(type)
+            .issueDate(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(randomIntBetween(1, 10)))
+            .expiryDate(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(randomIntBetween(1, 1000)))
+            .maxNodes(type == License.LicenseType.ENTERPRISE ? -1 : randomIntBetween(1, 100))
+            .maxResourceUnits(type == License.LicenseType.ENTERPRISE ? randomIntBetween(1, 100) : -1)
+            .issuedTo(randomAlphaOfLengthBetween(5, 50))
+            .issuer(randomAlphaOfLengthBetween(5, 50));
+    }
+
 }

+ 5 - 0
x-pack/plugin/core/src/test/java/org/elasticsearch/license/LicenseUtilsTests.java

@@ -6,6 +6,7 @@
 package org.elasticsearch.license;
 
 import org.elasticsearch.ElasticsearchSecurityException;
+import org.elasticsearch.cluster.node.DiscoveryNodes;
 import org.elasticsearch.test.ESTestCase;
 
 import java.util.Arrays;
@@ -33,4 +34,8 @@ public class LicenseUtilsTests extends ESTestCase {
         exception = new ElasticsearchSecurityException("msg");
         assertFalse(LicenseUtils.isLicenseExpiredException(exception));
     }
+
+    public void testVersionsUpToDate() {
+        assertThat(LicenseUtils.compatibleLicenseVersion(DiscoveryNodes.EMPTY_NODES), equalTo(License.VERSION_CURRENT));
+    }
 }

+ 1 - 0
x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/cluster/ClusterStatsMonitoringDocTests.java

@@ -367,6 +367,7 @@ public class ClusterStatsMonitoringDocTests extends BaseMonitoringDocTestCase<Cl
                     + "\"expiry_date\":\"2017-08-07T12:03:22.133Z\","
                     + "\"expiry_date_in_millis\":1502107402133,"
                     + "\"max_nodes\":2,"
+                    + "\"max_resource_units\":null,"
                     + "\"issued_to\":\"customer\","
                     + "\"issuer\":\"elasticsearch\","
                     + "\"start_date_in_millis\":-1"

+ 6 - 6
x-pack/plugin/src/test/resources/rest-api-spec/test/license/20_put_license.yml

@@ -20,8 +20,8 @@ teardown:
   - do:
       license.get: {}
 
-  ## a license object has 11 attributes
-  - length: { license: 11 }
+  ## a v5 license object has 12 attributes & the Rest API always outputs in current version
+  - length: { license: 12 }
 
   ## bwc for licenses format
   - do:
@@ -35,7 +35,7 @@ teardown:
   - do:
       license.get: {}
 
-  - length: { license: 11 }
+  - length: { license: 12 }
 
   ## license version: 1.x
   - do:
@@ -49,7 +49,7 @@ teardown:
   - do:
       license.get: {}
 
-  - length: { license: 11 }
+  - length: { license: 12 }
 
   ## multiple licenses version: 1.x
   - do:
@@ -63,7 +63,7 @@ teardown:
   - do:
       license.get: {}
 
-  - length: { license: 11 }
+  - length: { license: 12 }
   - match: { license.uid: "893361dc-9749-4997-93cb-802e3dofh7aa" }
 ---
 "Should throw 404 after license deletion":
@@ -91,7 +91,7 @@ teardown:
   - do:
       license.get: {}
 
-  - length: { license: 11 }
+  - length: { license: 12 }
 ---
 "Cannot start basic":
 

+ 7 - 5
x-pack/plugin/src/test/resources/rest-api-spec/test/license/30_enterprise_license.yml

@@ -15,18 +15,20 @@ teardown:
       license.post:
         acknowledge: true
         body: |
-          {"license":{"uid":"6e57906b-a8d1-4c1f-acb7-73a16edc3934","type":"enterprise","issue_date_in_millis":1523456691721,"expiry_date_in_millis":1838816691721,"max_nodes":50,"issued_to":"rest-test","issuer":"elasticsearch","signature":"AAAABAAAAA03e8BZRVXaCV4CpPGRAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAAZNhjABV6PRfa7P7sJgn70XCGoKtAVT75yU13JvKBd/UjD4TPhuZcztqZ/tcLEPxm/TSvGlogWmnw/Rw8xs8jMpBpKsJ+LOXjHhDdvXb2y7JJhCH8nlSEblMDRXysNvWpKe60Z/hb7hS4JynEUt0EBb6ji7BL42O07PNll1EGmkfsHazfs46iV91BG1VxXksI78XgWSaA0F/h7tvrNW9PTgsUaLo06InlQ8jA1dal90AoXp+MVDOHWQjVFZzUnO87/7lEb+VXt0IwchaW17ahihJqkCtGvKpWFwpuhx9xiFvkySN/g5LIVjYCvgBkiWExQ9p0Zzg3VoSlMBnVy0BWo=","start_date_in_millis":-1}}
+          {"license":{"uid":"6e57906b-a8d1-4c1f-acb7-73a16edc3934","type":"enterprise","issue_date_in_millis":1523456691721,"expiry_date_in_millis":1838816691721,"max_nodes":null,"max_resource_units":50,"issued_to":"rest-test","issuer":"elasticsearch","signature":"AAAABQAAAA0sKPJdf9T6DItbXVJKAAAAIAo5/x6hrsGh1GqqrJmy4qgmEC7gK0U4zQ6q5ZEMhm4jAAABAKFCHrix7w/xPG14+wdhld1RmphDmXmHfL1xeuI33Ahr1mOUYZ30eR6GZuh7CnK8BQhfq+z63lgctJepWlvwDSgkOvXWLHrJun7YSCrzz1bism0ZHWw7Swb9DO7vePomVBo/Hm9+eX0pV4/cFQNMmbFaX11tqJZYBEO6sNASVAFL7A1ZcVoB2evweGU9pUQYvFvmyzzySf99miDo3NH0XYdownEdtoNgFfmqa3+koCP7onmRZ1h9jhsDOi30RX/DTDXQKW+XoREnOHCoOAJFxwip/c1qaQAOqp1H6+P20ZGr2sIPiU97OVEU9kulm+E+jgiVW3LwGheOXsUOd1B8Mp0=","start_date_in_millis":-1}}
 
   - match: { license_status:  "valid" }
 
   - do:
       license.get: {}
 
-  ## a license object has 11 attributes
-  - length: { license: 11 }
+  ## a v5 (enterprise) license object has 12 attributes
+  - length: { license: 12 }
 
   ## In 8.0, the enterprise license is always reports truthfully
   - match: { license.type: "enterprise" }
+  - match: { license.max_resource_units: 50 }
+  - match: { license.max_nodes: null }
 
   - do:
       warnings:
@@ -34,8 +36,8 @@ teardown:
       license.get:
         accept_enterprise: "true"
 
-  ## a license object has 11 attributes
-  - length: { license: 11 }
+  ## a v5 license object has 12 attributes
+  - length: { license: 12 }
 
   ## Always returns real type
   - match: { license.type: "enterprise" }