Add api to upgrade from basic to trial license (elastic/x-pack-elasticsearch#2419)

This is related to elastic/x-pack-elasticsearch#1941.

Currently we support self-generating either a basic or trial license at
cluster startup. With the addition of the basic option, it is possible
that a user would choose to self-generate and eventually register a
basic license.

This commit allows a user to upgrade to a 30-day trial license if they
have not already utilized this 30-day trial license before. Additionally
it adds a get route to check if the user is eligible to upgrade. This
route will allow kibana to implement a cleaner UI.

Original commit: elastic/x-pack-elasticsearch@7f19b33a08
This commit is contained in:
Tim Brooks 2017-09-22 14:18:07 -06:00 committed by GitHub
parent b55ab98914
commit 6b2e7fbed8
26 changed files with 757 additions and 41 deletions

View File

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
public class GetTrialStatusAction extends Action<GetTrialStatusRequest, GetTrialStatusResponse, GetTrialStatusRequestBuilder> {
public static final GetTrialStatusAction INSTANCE = new GetTrialStatusAction();
public static final String NAME = "cluster:admin/xpack/license/trial_status";
private GetTrialStatusAction() {
super(NAME);
}
@Override
public GetTrialStatusRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new GetTrialStatusRequestBuilder(client, this);
}
@Override
public GetTrialStatusResponse newResponse() {
return new GetTrialStatusResponse();
}
}

View File

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.master.MasterNodeReadRequest;
import org.elasticsearch.common.io.stream.StreamInput;
import java.io.IOException;
public class GetTrialStatusRequest extends MasterNodeReadRequest<GetTrialStatusRequest> {
public GetTrialStatusRequest() {
}
public GetTrialStatusRequest(StreamInput in) throws IOException {
super(in);
}
@Override
public ActionRequestValidationException validate() {
return null;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
class GetTrialStatusRequestBuilder extends ActionRequestBuilder<GetTrialStatusRequest,
GetTrialStatusResponse, GetTrialStatusRequestBuilder> {
GetTrialStatusRequestBuilder(ElasticsearchClient client, GetTrialStatusAction action) {
super(client, action, new GetTrialStatusRequest());
}
}

View File

@ -0,0 +1,38 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
class GetTrialStatusResponse extends ActionResponse {
private boolean eligibleToStartTrial;
GetTrialStatusResponse() {
}
GetTrialStatusResponse(boolean eligibleToStartTrial) {
this.eligibleToStartTrial = eligibleToStartTrial;
}
boolean isEligibleToStartTrial() {
return eligibleToStartTrial;
}
@Override
public void readFrom(StreamInput in) throws IOException {
eligibleToStartTrial = in.readBoolean();
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeBoolean(eligibleToStartTrial);
}
}

View File

@ -8,6 +8,7 @@ package org.elasticsearch.license;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.Version;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.AckedClusterStateUpdateTask;
import org.elasticsearch.cluster.ClusterChangedEvent;
@ -227,8 +228,14 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
@Override
public ClusterState execute(ClusterState currentState) throws Exception {
MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(newLicense));
MetaData currentMetadata = currentState.metaData();
LicensesMetaData licensesMetaData = currentMetadata.custom(LicensesMetaData.TYPE);
Version trialVersion = null;
if (licensesMetaData != null) {
trialVersion = licensesMetaData.getMostRecentTrialVersion();
}
MetaData.Builder mdBuilder = MetaData.builder(currentMetadata);
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(newLicense, trialVersion));
return ClusterState.builder(currentState).metaData(mdBuilder).build();
}
});
@ -237,7 +244,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
}
static TimeValue days(int days) {
private static TimeValue days(int days) {
return TimeValue.timeValueHours(days * 24);
}
@ -273,7 +280,9 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
final LicensesMetaData currentLicenses = metaData.custom(LicensesMetaData.TYPE);
if (currentLicenses.getLicense() != LicensesMetaData.LICENSE_TOMBSTONE) {
MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(LicensesMetaData.LICENSE_TOMBSTONE));
LicensesMetaData newMetadata = new LicensesMetaData(LicensesMetaData.LICENSE_TOMBSTONE,
currentLicenses.getMostRecentTrialVersion());
mdBuilder.putCustom(LicensesMetaData.TYPE, newMetadata);
return ClusterState.builder(currentState).metaData(mdBuilder).build();
} else {
return currentState;
@ -287,6 +296,40 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
return license == LicensesMetaData.LICENSE_TOMBSTONE ? null : license;
}
void upgradeSelfGeneratedLicense(final ActionListener<PostStartTrialResponse> listener) {
clusterService.submitStateUpdateTask("upgrade self generated license",
new ClusterStateUpdateTask() {
@Override
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
LicensesMetaData licensesMetaData = oldState.metaData().custom(LicensesMetaData.TYPE);
logger.debug("upgraded self generated license: {}", licensesMetaData);
if (licensesMetaData == null || licensesMetaData.isEligibleForTrial()) {
listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.STATUS.UPGRADED_TO_TRIAL));
} else {
listener.onResponse(new PostStartTrialResponse(PostStartTrialResponse.STATUS.TRIAL_ALREADY_ACTIVATED));
}
}
@Override
public ClusterState execute(ClusterState currentState) throws Exception {
LicensesMetaData licensesMetaData = currentState.metaData().custom(LicensesMetaData.TYPE);
if (licensesMetaData == null || licensesMetaData.isEligibleForTrial()) {
return updateWithLicense(currentState, "trial");
} else {
return currentState;
}
}
@Override
public void onFailure(String source, @Nullable Exception e) {
logger.error(new ParameterizedMessage("unexpected failure during [{}]", source), e);
listener.onFailure(e);
}
});
}
/**
* Master-only operation to generate a one-time global trial license.
* The trial license is only generated and stored if the current cluster state metaData
@ -299,7 +342,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
public void clusterStateProcessed(String source, ClusterState oldState, ClusterState newState) {
LicensesMetaData licensesMetaData = newState.metaData().custom(LicensesMetaData.TYPE);
if (logger.isDebugEnabled()) {
logger.debug("registered trial license: {}", licensesMetaData);
logger.debug("registered self generated license: {}", licensesMetaData);
}
}
@ -307,27 +350,18 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
public ClusterState execute(ClusterState currentState) throws Exception {
final MetaData metaData = currentState.metaData();
final LicensesMetaData currentLicensesMetaData = metaData.custom(LicensesMetaData.TYPE);
MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
// do not generate a trial license if any license is present
if (currentLicensesMetaData == null) {
long issueDate = clock.millis();
String type = SELF_GENERATED_LICENSE_TYPE.get(settings);
if (validSelfGeneratedType(type) == false) {
throw new IllegalArgumentException("Illegal self generated license type [" + type +
"]. Must be trial or basic.");
}
License.Builder specBuilder = License.builder()
.uid(UUID.randomUUID().toString())
.issuedTo(clusterService.getClusterName().value())
.maxNodes(selfGeneratedLicenseMaxNodes)
.issueDate(issueDate)
.type(type)
.expiryDate(issueDate + SELF_GENERATED_LICENSE_DURATION.getMillis());
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder);
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(selfGeneratedLicense));
return ClusterState.builder(currentState).metaData(mdBuilder).build();
return updateWithLicense(currentState, type);
} else {
return currentState;
}
return currentState;
}
@Override
@ -338,6 +372,27 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
});
}
private ClusterState updateWithLicense(ClusterState currentState, String type) {
long issueDate = clock.millis();
MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
License.Builder specBuilder = License.builder()
.uid(UUID.randomUUID().toString())
.issuedTo(clusterService.getClusterName().value())
.maxNodes(selfGeneratedLicenseMaxNodes)
.issueDate(issueDate)
.type(type)
.expiryDate(issueDate + SELF_GENERATED_LICENSE_DURATION.getMillis());
License selfGeneratedLicense = SelfGeneratedLicense.create(specBuilder);
LicensesMetaData licensesMetaData;
if ("trial".equals(type)) {
licensesMetaData = new LicensesMetaData(selfGeneratedLicense, Version.CURRENT);
} else {
licensesMetaData = new LicensesMetaData(selfGeneratedLicense, null);
}
mdBuilder.putCustom(LicensesMetaData.TYPE, licensesMetaData);
return ClusterState.builder(currentState).metaData(mdBuilder).build();
}
@Override
protected void doStart() throws ElasticsearchException {
clusterService.addListener(this);
@ -436,7 +491,7 @@ public class LicenseService extends AbstractLifecycleComponent implements Cluste
* Additionally schedules license expiry notifications and event callbacks
* relative to the current license's expiry
*/
void onUpdate(final LicensesMetaData currentLicensesMetaData) {
private void onUpdate(final LicensesMetaData currentLicensesMetaData) {
final License license = getLicense(currentLicensesMetaData);
// license can be null if the trial license is yet to be auto-generated
// in this case, it is a no-op

View File

@ -5,15 +5,17 @@
*/
package org.elasticsearch.license;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.AbstractNamedDiffable;
import org.elasticsearch.cluster.MergableCustomMetaData;
import org.elasticsearch.cluster.NamedDiff;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.license.License.OperationMode;
import org.elasticsearch.cluster.MergableCustomMetaData;
import java.io.IOException;
import java.util.EnumSet;
@ -48,33 +50,56 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
private License license;
LicensesMetaData(License license) {
// This field describes the version of x-pack for which this cluster has exercised a trial. If the field
// is null, then no trial has been exercised. We keep the version to leave open the possibility that we
// may eventually allow a cluster to exercise a trial every time they upgrade to a new major version.
@Nullable
private Version trialVersion;
LicensesMetaData(License license, Version trialVersion) {
this.license = license;
this.trialVersion = trialVersion;
}
public License getLicense() {
return license;
}
boolean isEligibleForTrial() {
if (trialVersion == null) {
return true;
}
return Version.CURRENT.major > trialVersion.major;
}
Version getMostRecentTrialVersion() {
return trialVersion;
}
@Override
public String toString() {
if (license != null) {
return license.toString();
}
return "";
return "LicensesMetaData{" +
"license=" + license +
", trialVersion=" + trialVersion +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
LicensesMetaData that = (LicensesMetaData) o;
return !(license != null ? !license.equals(that.license) : that.license != null);
if (license != null ? !license.equals(that.license) : that.license != null) return false;
return trialVersion != null ? trialVersion.equals(that.trialVersion) : that.trialVersion == null;
}
@Override
public int hashCode() {
return license != null ? license.hashCode() : 0;
int result = license != null ? license.hashCode() : 0;
result = 31 * result + (trialVersion != null ? trialVersion.hashCode() : 0);
return result;
}
@Override
@ -89,6 +114,7 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
public static LicensesMetaData fromXContent(XContentParser parser) throws IOException {
License license = LICENSE_TOMBSTONE;
Version trialLicense = null;
XContentParser.Token token;
while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) {
if (token == XContentParser.Token.FIELD_NAME) {
@ -101,11 +127,14 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
} else if (token == XContentParser.Token.VALUE_NULL) {
license = LICENSE_TOMBSTONE;
}
} else if (fieldName.equals(Fields.TRIAL_LICENSE)) {
parser.nextToken();
trialLicense = Version.fromString(parser.text());
}
}
}
}
return new LicensesMetaData(license);
return new LicensesMetaData(license, trialLicense);
}
@Override
@ -117,6 +146,9 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
license.toInnerXContent(builder, params);
builder.endObject();
}
if (trialVersion != null) {
builder.field(Fields.TRIAL_LICENSE, trialVersion.toString());
}
return builder;
}
@ -128,6 +160,15 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
streamOutput.writeBoolean(true); // has a license
license.writeTo(streamOutput);
}
// TODO Eventually this should be 6.0. But it is 7.0 temporarily for bwc
if (streamOutput.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
if (trialVersion == null) {
streamOutput.writeBoolean(false);
} else {
streamOutput.writeBoolean(true);
Version.writeVersion(trialVersion, streamOutput);
}
}
}
LicensesMetaData(StreamInput streamInput) throws IOException {
@ -136,6 +177,13 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
} else {
license = LICENSE_TOMBSTONE;
}
// TODO Eventually this should be 6.0. But it is 7.0 temporarily for bwc
if (streamInput.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) {
boolean hasExercisedTrial = streamInput.readBoolean();
if (hasExercisedTrial) {
this.trialVersion = Version.readVersion(streamInput);
}
}
}
public static NamedDiff<MetaData.Custom> readDiffFrom(StreamInput streamInput) throws IOException {
@ -155,5 +203,6 @@ class LicensesMetaData extends AbstractNamedDiffable<MetaData.Custom> implements
private static final class Fields {
private static final String LICENSE = "license";
private static final String TRIAL_LICENSE = "trial_license";
}
}

View File

@ -36,7 +36,7 @@ public class Licensing implements ActionPlugin {
public static final String NAME = "license";
protected final Settings settings;
protected final boolean isTransportClient;
private final boolean isTransportClient;
private final boolean isTribeNode;
public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
@ -53,6 +53,7 @@ public class Licensing implements ActionPlugin {
LicensesMetaData::fromXContent));
return entries;
}
public Licensing(Settings settings) {
this.settings = settings;
isTransportClient = transportClientMode(settings);
@ -66,7 +67,9 @@ public class Licensing implements ActionPlugin {
}
return Arrays.asList(new ActionHandler<>(PutLicenseAction.INSTANCE, TransportPutLicenseAction.class),
new ActionHandler<>(GetLicenseAction.INSTANCE, TransportGetLicenseAction.class),
new ActionHandler<>(DeleteLicenseAction.INSTANCE, TransportDeleteLicenseAction.class));
new ActionHandler<>(DeleteLicenseAction.INSTANCE, TransportDeleteLicenseAction.class),
new ActionHandler<>(PostStartTrialAction.INSTANCE, TransportPostStartTrialAction.class),
new ActionHandler<>(GetTrialStatusAction.INSTANCE, TransportGetTrialStatusAction.class));
}
@Override
@ -78,6 +81,8 @@ public class Licensing implements ActionPlugin {
if (false == isTribeNode) {
handlers.add(new RestPutLicenseAction(settings, restController));
handlers.add(new RestDeleteLicenseAction(settings, restController));
handlers.add(new RestGetTrialStatus(settings, restController));
handlers.add(new RestPostStartTrialLicense(settings, restController));
}
return handlers;
}

View File

@ -39,4 +39,16 @@ public class LicensingClient {
public void deleteLicense(DeleteLicenseRequest request, ActionListener<DeleteLicenseResponse> listener) {
client.execute(DeleteLicenseAction.INSTANCE, request, listener);
}
public PostStartTrialRequestBuilder preparePutUpgradeToTrial() {
return new PostStartTrialRequestBuilder(client, PostStartTrialAction.INSTANCE);
}
public GetTrialStatusRequestBuilder prepareGetUpgradeToTrial() {
return new GetTrialStatusRequestBuilder(client, GetTrialStatusAction.INSTANCE);
}
public void putUpgradeToTrial(PostStartTrialRequest request, ActionListener<PostStartTrialResponse> listener) {
client.execute(PostStartTrialAction.INSTANCE, request, listener);
}
}

View File

@ -0,0 +1,29 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.Action;
import org.elasticsearch.client.ElasticsearchClient;
public class PostStartTrialAction extends Action<PostStartTrialRequest, PostStartTrialResponse, PostStartTrialRequestBuilder> {
public static final PostStartTrialAction INSTANCE = new PostStartTrialAction();
public static final String NAME = "cluster:admin/xpack/license/start_trial";
private PostStartTrialAction() {
super(NAME);
}
@Override
public PostStartTrialRequestBuilder newRequestBuilder(ElasticsearchClient client) {
return new PostStartTrialRequestBuilder(client, this);
}
@Override
public PostStartTrialResponse newResponse() {
return new PostStartTrialResponse();
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.action.support.master.MasterNodeRequest;
public class PostStartTrialRequest extends MasterNodeRequest<PostStartTrialRequest> {
@Override
public ActionRequestValidationException validate() {
return null;
}
}

View File

@ -0,0 +1,17 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionRequestBuilder;
import org.elasticsearch.client.ElasticsearchClient;
class PostStartTrialRequestBuilder extends ActionRequestBuilder<PostStartTrialRequest,
PostStartTrialResponse, PostStartTrialRequestBuilder> {
PostStartTrialRequestBuilder(ElasticsearchClient client, PostStartTrialAction action) {
super(client, action, new PostStartTrialRequest());
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import java.io.IOException;
class PostStartTrialResponse extends ActionResponse {
enum STATUS {
UPGRADED_TO_TRIAL,
TRIAL_ALREADY_ACTIVATED
}
private STATUS status;
PostStartTrialResponse() {
}
PostStartTrialResponse(STATUS status) {
this.status = status;
}
public STATUS getStatus() {
return status;
}
@Override
public void readFrom(StreamInput in) throws IOException {
status = in.readEnum(STATUS.class);
}
@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeEnum(status);
}
}

View File

@ -0,0 +1,48 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.XPackClient;
import org.elasticsearch.xpack.rest.XPackRestHandler;
import java.io.IOException;
import static org.elasticsearch.rest.RestRequest.Method.GET;
public class RestGetTrialStatus extends XPackRestHandler {
RestGetTrialStatus(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(GET, URI_BASE + "/license/trial_status", this);
}
@Override
protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException {
return channel -> client.licensing().prepareGetUpgradeToTrial().execute(
new RestBuilderListener<GetTrialStatusResponse>(channel) {
@Override
public RestResponse buildResponse(GetTrialStatusResponse response, XContentBuilder builder) throws Exception {
builder.startObject();
builder.field("eligible_to_start_trial", response.isEligibleToStartTrial());
builder.endObject();
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
@Override
public String getName() {
return "xpack_trial_status_action";
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;
import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.XPackClient;
import org.elasticsearch.xpack.rest.XPackRestHandler;
import java.io.IOException;
import static org.elasticsearch.rest.RestRequest.Method.POST;
import static org.elasticsearch.rest.RestRequest.Method.PUT;
public class RestPostStartTrialLicense extends XPackRestHandler {
RestPostStartTrialLicense(Settings settings, RestController controller) {
super(settings);
controller.registerHandler(POST, URI_BASE + "/license/start_trial", this);
}
@Override
protected RestChannelConsumer doPrepareRequest(RestRequest request, XPackClient client) throws IOException {
return channel -> client.licensing().preparePutUpgradeToTrial().execute(
new RestBuilderListener<PostStartTrialResponse>(channel) {
@Override
public RestResponse buildResponse(PostStartTrialResponse response, XContentBuilder builder) throws Exception {
PostStartTrialResponse.STATUS status = response.getStatus();
if (status == PostStartTrialResponse.STATUS.TRIAL_ALREADY_ACTIVATED) {
builder.startObject()
.field("trial_was_started", false)
.field("error_message", "Operation failed: Trial was already activated.")
.endObject();
return new BytesRestResponse(RestStatus.FORBIDDEN, builder);
} else if (status == PostStartTrialResponse.STATUS.UPGRADED_TO_TRIAL) {
builder.startObject().field("trial_was_started", true).endObject();
return new BytesRestResponse(RestStatus.OK, builder);
} else {
throw new IllegalArgumentException("Unexpected status for PostStartTrialResponse: [" + status + "]");
}
}
});
}
@Override
public String getName() {
return "xpack_upgrade_to_trial_action";
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.master.TransportMasterNodeReadAction;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
public class TransportGetTrialStatusAction extends TransportMasterNodeReadAction<GetTrialStatusRequest, GetTrialStatusResponse> {
@Inject
public TransportGetTrialStatusAction(Settings settings, TransportService transportService, ClusterService clusterService,
ThreadPool threadPool, ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver) {
super(settings, GetTrialStatusAction.NAME, transportService, clusterService, threadPool, actionFilters,
GetTrialStatusRequest::new, indexNameExpressionResolver);
}
@Override
protected String executor() {
return ThreadPool.Names.SAME;
}
@Override
protected GetTrialStatusResponse newResponse() {
return new GetTrialStatusResponse();
}
@Override
protected void masterOperation(GetTrialStatusRequest request, ClusterState state,
ActionListener<GetTrialStatusResponse> listener) throws Exception {
LicensesMetaData licensesMetaData = state.metaData().custom(LicensesMetaData.TYPE);
listener.onResponse(new GetTrialStatusResponse(licensesMetaData == null || licensesMetaData.isEligibleForTrial()));
}
@Override
protected ClusterBlockException checkBlock(GetTrialStatusRequest request, ClusterState state) {
return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_READ);
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.master.TransportMasterNodeAction;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.block.ClusterBlockException;
import org.elasticsearch.cluster.block.ClusterBlockLevel;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
public class TransportPostStartTrialAction extends TransportMasterNodeAction<PostStartTrialRequest, PostStartTrialResponse> {
private final LicenseService licenseService;
@Inject
public TransportPostStartTrialAction(Settings settings, TransportService transportService, ClusterService clusterService,
LicenseService licenseService, ThreadPool threadPool, ActionFilters actionFilters,
IndexNameExpressionResolver indexNameExpressionResolver) {
super(settings, PostStartTrialAction.NAME, transportService, clusterService, threadPool, actionFilters,
indexNameExpressionResolver, PostStartTrialRequest::new);
this.licenseService = licenseService;
}
@Override
protected String executor() {
return ThreadPool.Names.SAME;
}
@Override
protected PostStartTrialResponse newResponse() {
return new PostStartTrialResponse();
}
@Override
protected void masterOperation(PostStartTrialRequest request, ClusterState state,
ActionListener<PostStartTrialResponse> listener) throws Exception {
licenseService.upgradeSelfGeneratedLicense(listener);
}
@Override
protected ClusterBlockException checkBlock(PostStartTrialRequest request, ClusterState state) {
return state.blocks().globalBlockedException(ClusterBlockLevel.METADATA_WRITE);
}
}

View File

@ -49,16 +49,20 @@ public abstract class AbstractLicenseServiceTestCase extends ESTestCase {
}
protected void setInitialState(License license, XPackLicenseState licenseState, Settings settings) {
setInitialState(license, licenseState, settings, randomBoolean() ? "trial" : "basic");
}
protected void setInitialState(License license, XPackLicenseState licenseState, Settings settings, String selfGeneratedType) {
Path tempDir = createTempDir();
when(environment.configFile()).thenReturn(tempDir);
licenseType = randomBoolean() ? "trial" : "basic";
licenseType = selfGeneratedType;
settings = Settings.builder().put(settings).put(LicenseService.SELF_GENERATED_LICENSE_TYPE.getKey(), licenseType).build();
licenseService = new LicenseService(settings, clusterService, clock, environment, resourceWatcherService, licenseState);
ClusterState state = mock(ClusterState.class);
final ClusterBlocks noBlock = ClusterBlocks.builder().build();
when(state.blocks()).thenReturn(noBlock);
MetaData metaData = mock(MetaData.class);
when(metaData.custom(LicensesMetaData.TYPE)).thenReturn(new LicensesMetaData(license));
when(metaData.custom(LicensesMetaData.TYPE)).thenReturn(new LicensesMetaData(license, null));
when(state.metaData()).thenReturn(metaData);
final DiscoveryNode mockNode = new DiscoveryNode("b", buildNewFakeTransportAddress(), emptyMap(), emptySet(), Version.CURRENT);
when(discoveryNodes.getMasterNode()).thenReturn(mockNode);

View File

@ -65,7 +65,7 @@ public abstract class AbstractLicensesIntegrationTestCase extends ESIntegTestCas
@Override
public ClusterState execute(ClusterState currentState) throws Exception {
MetaData.Builder mdBuilder = MetaData.builder(currentState.metaData());
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license));
mdBuilder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license, null));
return ClusterState.builder(currentState).metaData(mdBuilder).build();
}

View File

@ -47,7 +47,7 @@ public class LicenseClusterChangeTests extends AbstractLicenseServiceTestCase {
public void testNotificationOnNewLicense() throws Exception {
ClusterState oldState = ClusterState.builder(new ClusterName("a")).build();
final License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24));
MetaData metaData = MetaData.builder().putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license)).build();
MetaData metaData = MetaData.builder().putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license, null)).build();
ClusterState newState = ClusterState.builder(new ClusterName("a")).metaData(metaData).build();
licenseService.clusterChanged(new ClusterChangedEvent("simulated", newState, oldState));
assertThat(licenseState.activeUpdates.size(), equalTo(1));
@ -56,7 +56,7 @@ public class LicenseClusterChangeTests extends AbstractLicenseServiceTestCase {
public void testNoNotificationOnExistingLicense() throws Exception {
final License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(24));
MetaData metaData = MetaData.builder().putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license)).build();
MetaData metaData = MetaData.builder().putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license, null)).build();
ClusterState newState = ClusterState.builder(new ClusterName("a")).metaData(metaData).build();
ClusterState oldState = ClusterState.builder(newState).build();
licenseService.clusterChanged(new ClusterChangedEvent("simulated", newState, oldState));

View File

@ -18,9 +18,9 @@ import static org.mockito.Mockito.when;
public class LicenseRegistrationTests extends AbstractLicenseServiceTestCase {
public void testTrialLicenseRequestOnEmptyLicenseState() throws Exception {
public void testSelfGeneratedTrialLicense() throws Exception {
XPackLicenseState licenseState = new XPackLicenseState();
setInitialState(null, licenseState, Settings.EMPTY);
setInitialState(null, licenseState, Settings.EMPTY, "trial");
when(discoveryNodes.isLocalNodeElectedMaster()).thenReturn(true);
licenseService.start();
@ -31,6 +31,26 @@ public class LicenseRegistrationTests extends AbstractLicenseServiceTestCase {
LicensesMetaData licenseMetaData = stateWithLicense.metaData().custom(LicensesMetaData.TYPE);
assertNotNull(licenseMetaData);
assertNotNull(licenseMetaData.getLicense());
assertFalse(licenseMetaData.isEligibleForTrial());
assertEquals("trial", licenseMetaData.getLicense().type());
assertEquals(clock.millis() + LicenseService.SELF_GENERATED_LICENSE_DURATION.millis(), licenseMetaData.getLicense().expiryDate());
}
public void testSelfGeneratedBasicLicense() throws Exception {
XPackLicenseState licenseState = new XPackLicenseState();
setInitialState(null, licenseState, Settings.EMPTY, "basic");
when(discoveryNodes.isLocalNodeElectedMaster()).thenReturn(true);
licenseService.start();
ClusterState state = ClusterState.builder(new ClusterName("a")).build();
ArgumentCaptor<ClusterStateUpdateTask> stateUpdater = ArgumentCaptor.forClass(ClusterStateUpdateTask.class);
verify(clusterService, Mockito.times(1)).submitStateUpdateTask(any(), stateUpdater.capture());
ClusterState stateWithLicense = stateUpdater.getValue().execute(state);
LicensesMetaData licenseMetaData = stateWithLicense.metaData().custom(LicensesMetaData.TYPE);
assertNotNull(licenseMetaData);
assertNotNull(licenseMetaData.getLicense());
assertTrue(licenseMetaData.isEligibleForTrial());
assertEquals("basic", licenseMetaData.getLicense().type());
assertEquals(clock.millis() + LicenseService.SELF_GENERATED_LICENSE_DURATION.millis(), licenseMetaData.getLicense().expiryDate());
}
}

View File

@ -5,6 +5,7 @@
*/
package org.elasticsearch.license;
import org.elasticsearch.Version;
import org.elasticsearch.cluster.ClusterModule;
import org.elasticsearch.cluster.metadata.MetaData;
import org.elasticsearch.cluster.metadata.RepositoriesMetaData;
@ -29,9 +30,10 @@ import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
public class LicensesMetaDataSerializationTests extends ESTestCase {
public void testXContentSerializationOneSignedLicense() throws Exception {
License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(2));
LicensesMetaData licensesMetaData = new LicensesMetaData(license);
LicensesMetaData licensesMetaData = new LicensesMetaData(license, null);
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
builder.startObject("licenses");
@ -40,12 +42,27 @@ public class LicensesMetaDataSerializationTests extends ESTestCase {
builder.endObject();
LicensesMetaData licensesMetaDataFromXContent = getLicensesMetaDataFromXContent(createParser(builder));
assertThat(licensesMetaDataFromXContent.getLicense(), equalTo(license));
assertNull(licensesMetaDataFromXContent.getMostRecentTrialVersion());
}
public void testXContentSerializationOneSignedLicenseWithUsedTrial() throws Exception {
License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(2));
LicensesMetaData licensesMetaData = new LicensesMetaData(license, Version.CURRENT);
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
builder.startObject("licenses");
licensesMetaData.toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endObject();
builder.endObject();
LicensesMetaData licensesMetaDataFromXContent = getLicensesMetaDataFromXContent(createParser(builder));
assertThat(licensesMetaDataFromXContent.getLicense(), equalTo(license));
assertEquals(licensesMetaDataFromXContent.getMostRecentTrialVersion(), Version.CURRENT);
}
public void testLicenseMetadataParsingDoesNotSwallowOtherMetaData() throws Exception {
new Licensing(Settings.EMPTY); // makes sure LicensePlugin is registered in Custom MetaData
License license = TestUtils.generateSignedLicense(TimeValue.timeValueHours(2));
LicensesMetaData licensesMetaData = new LicensesMetaData(license);
LicensesMetaData licensesMetaData = new LicensesMetaData(license, Version.CURRENT);
RepositoryMetaData repositoryMetaData = new RepositoryMetaData("repo", "fs", Settings.EMPTY);
RepositoriesMetaData repositoriesMetaData = new RepositoriesMetaData(repositoryMetaData);
final MetaData.Builder metaDataBuilder = MetaData.builder();
@ -79,7 +96,7 @@ public class LicensesMetaDataSerializationTests extends ESTestCase {
.type(randomBoolean() ? "trial" : "basic")
.expiryDate(issueDate + TimeValue.timeValueHours(2).getMillis());
final License trialLicense = SelfGeneratedLicense.create(specBuilder);
LicensesMetaData licensesMetaData = new LicensesMetaData(trialLicense);
LicensesMetaData licensesMetaData = new LicensesMetaData(trialLicense, Version.CURRENT);
XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
builder.startObject("licenses");
@ -88,6 +105,7 @@ public class LicensesMetaDataSerializationTests extends ESTestCase {
builder.endObject();
LicensesMetaData licensesMetaDataFromXContent = getLicensesMetaDataFromXContent(createParser(builder));
assertThat(licensesMetaDataFromXContent.getLicense(), equalTo(trialLicense));
assertEquals(licensesMetaDataFromXContent.getMostRecentTrialVersion(), Version.CURRENT);
}
public void testLicenseTombstoneFromXContext() throws Exception {
@ -101,6 +119,19 @@ public class LicensesMetaDataSerializationTests extends ESTestCase {
assertThat(metaDataFromXContent.getLicense(), equalTo(LicensesMetaData.LICENSE_TOMBSTONE));
}
public void testLicenseTombstoneWithUsedTrialFromXContext() throws Exception {
final XContentBuilder builder = XContentFactory.jsonBuilder();
builder.startObject();
builder.startObject("licenses");
builder.nullField("license");
builder.field("trial_license", Version.CURRENT.toString());
builder.endObject();
builder.endObject();
LicensesMetaData metaDataFromXContent = getLicensesMetaDataFromXContent(createParser(builder));
assertThat(metaDataFromXContent.getLicense(), equalTo(LicensesMetaData.LICENSE_TOMBSTONE));
assertEquals(metaDataFromXContent.getMostRecentTrialVersion(), Version.CURRENT);
}
private static LicensesMetaData getLicensesMetaDataFromXContent(XContentParser parser) throws Exception {
parser.nextToken(); // consume null
parser.nextToken(); // consume "licenses"

View File

@ -348,6 +348,6 @@ public class TestUtils {
}
public static void putLicense(MetaData.Builder builder, License license) {
builder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license));
builder.putCustom(LicensesMetaData.TYPE, new LicensesMetaData(license, null));
}
}

View File

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.license;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.network.NetworkModule;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.transport.Netty4Plugin;
import org.elasticsearch.xpack.XPackPlugin;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import static org.elasticsearch.test.ESIntegTestCase.Scope.SUITE;
@ESIntegTestCase.ClusterScope(scope = SUITE)
public class UpgradeToTrialTests extends AbstractLicensesIntegrationTestCase {
@Override
protected Settings nodeSettings(int nodeOrdinal) {
return Settings.builder()
.put(super.nodeSettings(nodeOrdinal))
.put("node.data", true)
.put(LicenseService.SELF_GENERATED_LICENSE_TYPE.getKey(), "basic")
.put(NetworkModule.HTTP_ENABLED.getKey(), true).build();
}
@Override
protected Collection<Class<? extends Plugin>> nodePlugins() {
return Arrays.asList(XPackPlugin.class, Netty4Plugin.class);
}
@Override
protected Collection<Class<? extends Plugin>> transportClientPlugins() {
return nodePlugins();
}
public void testUpgradeToTrial() throws Exception {
LicensingClient licensingClient = new LicensingClient(client());
GetLicenseResponse getLicenseResponse = licensingClient.prepareGetLicense().get();
assertEquals("basic", getLicenseResponse.license().type());
RestClient restClient = getRestClient();
Response response = restClient.performRequest("GET", "/_xpack/license/trial_status");
String body = Streams.copyToString(new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response.getStatusLine().getStatusCode());
assertEquals("{\"eligible_to_start_trial\":true}", body);
Response response2 = restClient.performRequest("POST", "/_xpack/license/start_trial");
String body2 = Streams.copyToString(new InputStreamReader(response2.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response2.getStatusLine().getStatusCode());
assertEquals("{\"trial_was_started\":true}", body2);
Response response3 = restClient.performRequest("GET", "/_xpack/license/trial_status");
String body3 = Streams.copyToString(new InputStreamReader(response3.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(200, response3.getStatusLine().getStatusCode());
assertEquals("{\"eligible_to_start_trial\":false}", body3);
ResponseException ex = expectThrows(ResponseException.class,
() -> restClient.performRequest("POST", "/_xpack/license/start_trial"));
Response response4 = ex.getResponse();
String body4 = Streams.copyToString(new InputStreamReader(response4.getEntity().getContent(), StandardCharsets.UTF_8));
assertEquals(403, response4.getStatusLine().getStatusCode());
assertTrue(body4.contains("\"trial_was_started\":false"));
assertTrue(body4.contains("\"error_message\":\"Operation failed: Trial was already activated.\""));
}
}

View File

@ -158,3 +158,5 @@ indices:data/write/delete/byquery
indices:data/write/reindex
cluster:admin/xpack/deprecation/info
cluster:admin/xpack/ml/job/forecast
cluster:admin/xpack/license/start_trial
cluster:admin/xpack/license/trial_status

View File

@ -0,0 +1,15 @@
{
"xpack.license.get_trial_status": {
"documentation": "https://www.elastic.co/guide/en/x-pack/current/license-management.html",
"methods": ["GET"],
"url": {
"path": "/_xpack/license/trial_status",
"paths": ["/_xpack/license/trial_status"],
"parts" : {
},
"params": {
}
},
"body": null
}
}

View File

@ -0,0 +1,15 @@
{
"xpack.license.post_start_trial": {
"documentation": "https://www.elastic.co/guide/en/x-pack/current/license-management.html",
"methods": ["POST"],
"url": {
"path": "/_xpack/license/start_trial",
"paths": ["/_xpack/license/start_trial"],
"parts" : {
},
"params": {
}
},
"body": null
}
}