Skip to content

Commit

Permalink
Add max_resource_units to enterprise license (#50910)
Browse files Browse the repository at this point in the history
The enterprise license type must have "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.

Includes a BWC layer to return "max_nodes: ${max_resource_units}" in
the GET license API.

Backport of: #50735
  • Loading branch information
tvernum committed Jan 14, 2020
1 parent f0924e6 commit c2acb88
Show file tree
Hide file tree
Showing 13 changed files with 313 additions and 90 deletions.
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 @@ -159,13 +165,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 @@ -182,13 +189,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 @@ -217,6 +227,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 @@ -233,8 +244,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 @@ -252,6 +263,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 @@ -300,12 +312,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 @@ -392,20 +413,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 @@ -420,6 +460,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 @@ -442,6 +485,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 @@ -506,7 +552,16 @@ 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 if (hideEnterprise && maxNodes == -1) {
builder.field(Fields.MAX_NODES, 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 @@ -551,6 +606,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 @@ -593,7 +650,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 @@ -625,8 +682,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 @@ -675,7 +731,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 @@ -700,7 +756,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 @@ -719,6 +775,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 @@ -762,6 +819,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 @@ -817,6 +875,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 @@ -831,17 +894,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 @@ -850,15 +914,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 @@ -874,11 +938,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 @@ -122,6 +122,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 @@ -292,11 +293,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 @@ -11,8 +11,6 @@
import org.elasticsearch.license.License.LicenseType;
import org.elasticsearch.rest.RestStatus;

import java.util.stream.StreamSupport;

public class LicenseUtils {

public static final String EXPIRED_FEATURE_METADATA = "es.license.expired.feature";
Expand Down Expand Up @@ -49,25 +47,30 @@ 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 getMaxLicenseVersion(currentNodes.getMinNodeVersion());
}

if (StreamSupport.stream(currentNodes.spliterator(), false)
.allMatch(node -> node.getVersion().onOrAfter(Version.V_6_4_0))) {
// License.VERSION_CRYPTO_ALGORITHMS was introduced in 6.4.0
return License.VERSION_CRYPTO_ALGORITHMS;
} else {
return License.VERSION_START_DATE;
public static int getMaxLicenseVersion(Version version){
if (version != null) {
if (version.before(Version.V_6_4_0)) {
return License.VERSION_START_DATE;
}
if (version.before(Version.V_7_6_0)) {
return License.VERSION_CRYPTO_ALGORITHMS;
}
}
assert License.VERSION_ENTERPRISE == License.VERSION_CURRENT : "update this method when adding a new version";
return License.VERSION_ENTERPRISE;
}
}
}
Loading

0 comments on commit c2acb88

Please sign in to comment.