Skip to content

Commit

Permalink
Add max_resource_units to enterprise license (#50735)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tvernum committed Jan 13, 2020
1 parent 8a655f3 commit 8727f27
Show file tree
Hide file tree
Showing 13 changed files with 307 additions and 82 deletions.
1 change: 1 addition & 0 deletions docs/reference/licensing/get-license.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,19 @@ static boolean isBasic(String typeName) {
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
Expand Down Expand Up @@ -153,13 +159,14 @@ static boolean isTrial(String typeName) {
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 {
Expand All @@ -176,13 +183,16 @@ public enum OperationMode {
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)
*/
Expand Down Expand Up @@ -211,6 +221,7 @@ public static OperationMode resolve(LicenseType type) {
* 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) {
Expand All @@ -227,8 +238,8 @@ public String description() {
}
}

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;
Expand All @@ -246,6 +257,7 @@ private License(int version, String uid, String issuer, String issuedTo, long is
this.expiryDate = expiryDate;
}
this.maxNodes = maxNodes;
this.maxResourceUnits = maxResourceUnits;
this.startDate = startDate;
this.operationMode = OperationMode.resolve(LicenseType.resolve(this));
validate();
Expand Down Expand Up @@ -294,12 +306,21 @@ public long expiryDate() {
}

/**
* @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
*/
Expand Down Expand Up @@ -386,20 +407,39 @@ private void validate() {
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);
Expand All @@ -414,6 +454,9 @@ public static License readLicense(StreamInput in) throws IOException {
}
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());
Expand All @@ -436,6 +479,9 @@ public void writeTo(StreamOutput out) throws IOException {
}
out.writeLong(expiryDate);
out.writeInt(maxNodes);
if (version >= VERSION_ENTERPRISE) {
out.writeInt(maxResourceUnits);
}
out.writeString(issuedTo);
out.writeString(issuer);
out.writeOptionalString(signature);
Expand Down Expand Up @@ -496,7 +542,14 @@ public XContentBuilder toInnerXContent(XContentBuilder builder, Params params) t
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) {
Expand Down Expand Up @@ -541,6 +594,8 @@ public static License fromXContent(XContentParser parser) throws IOException {
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)) {
Expand Down Expand Up @@ -583,7 +638,7 @@ public static License fromXContent(XContentParser parser) throws IOException {
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);
Expand Down Expand Up @@ -615,8 +670,7 @@ public static License fromSource(BytesReference bytes, XContentType xContentType
// 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) {
Expand Down Expand Up @@ -665,7 +719,7 @@ public boolean equals(Object o) {

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;
Expand All @@ -690,7 +744,7 @@ public int hashCode() {
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;
Expand All @@ -709,6 +763,7 @@ public static final class Fields {
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";
Expand Down Expand Up @@ -752,6 +807,7 @@ public static class Builder {
private long expiryDate = -1;
private long startDate = -1;
private int maxNodes = -1;
private int maxResourceUnits = -1;

public Builder uid(String uid) {
this.uid = uid;
Expand Down Expand Up @@ -807,6 +863,11 @@ public Builder maxNodes(int maxNodes) {
return this;
}

public Builder maxResourceUnits(int maxUnits) {
this.maxResourceUnits = maxUnits;
return this;
}

public Builder signature(String signature) {
if (signature != null) {
this.signature = signature;
Expand All @@ -821,17 +882,18 @@ public Builder startDate(long startDate) {

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);
}

/**
Expand All @@ -840,15 +902,15 @@ public Builder fromLicenseSpec(License license, String signature) {
*/
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() {
Expand All @@ -864,11 +926,10 @@ public Builder validate() {
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -291,11 +292,8 @@ public ClusterState execute(ClusterState currentState) throws Exception {
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -46,18 +47,26 @@ public static boolean licenseNeedsExtended(License license) {
* 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;
}
}
}
Loading

0 comments on commit 8727f27

Please sign in to comment.