[License] Feature agnostic licensing model

This commit changes the license plugin to work with license that are not tied to any specific feature in a bwc way. It refactors the license plugin api into a lighter weight API, enabling the license plugin to manage license expiration and acknowledgment triggers.

closes elastic/elasticsearch#683, elastic/elasticsearch#686, elastic/elasticsearch#687, elastic/elasticsearch#691

Original commit: elastic/x-pack-elasticsearch@537cd3933a
This commit is contained in:
Areek Zillur 2015-10-09 00:32:15 -04:00
parent fa85d04523
commit daf4a9765c
17 changed files with 258 additions and 400 deletions

View File

@ -60,7 +60,8 @@ public class ClusterInfoCollector extends AbstractCollector<ClusterInfoMarvelDoc
List<MarvelDoc> results = new ArrayList<>(1);
// Retrieves all licenses
List<License> licenses = licenseService.licenses();
// TODO: we should only work with one license
List<License> licenses = Collections.singletonList(licenseService.license());
// Retrieves additional cluster stats
ClusterStatsResponse clusterStats = client.admin().cluster().prepareClusterStats().get(marvelSettings.clusterStatsTimeout());

View File

@ -37,7 +37,6 @@ public class ClusterInfoRenderer extends AbstractRenderer<ClusterInfoMarvelDoc>
builder.field(Fields.UID, license.uid());
builder.field(Fields.TYPE, license.type());
builder.dateValueField(Fields.ISSUE_DATE_IN_MILLIS, Fields.ISSUE_DATE, license.issueDate());
builder.field(Fields.FEATURE, license.feature());
builder.dateValueField(Fields.EXPIRY_DATE_IN_MILLIS, Fields.EXPIRY_DATE, license.expiryDate());
builder.field(Fields.MAX_NODES, license.maxNodes());
builder.field(Fields.ISSUED_TO, license.issuedTo());
@ -77,7 +76,6 @@ public class ClusterInfoRenderer extends AbstractRenderer<ClusterInfoMarvelDoc>
static final XContentBuilderString STATUS = new XContentBuilderString("status");
static final XContentBuilderString UID = new XContentBuilderString("uid");
static final XContentBuilderString TYPE = new XContentBuilderString("type");
static final XContentBuilderString FEATURE = new XContentBuilderString("feature");
static final XContentBuilderString ISSUE_DATE_IN_MILLIS = new XContentBuilderString("issue_date_in_millis");
static final XContentBuilderString ISSUE_DATE = new XContentBuilderString("issue_date");
static final XContentBuilderString EXPIRY_DATE_IN_MILLIS = new XContentBuilderString("expiry_date_in_millis");

View File

@ -6,110 +6,45 @@
package org.elasticsearch.marvel.license;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
import org.elasticsearch.common.joda.Joda;
import org.elasticsearch.common.logging.support.LoggerMessageFormat;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.license.plugin.core.LicensesManagerService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.marvel.MarvelPlugin;
import org.elasticsearch.marvel.agent.settings.MarvelSettings;
import org.elasticsearch.marvel.mode.Mode;
import java.util.*;
public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
public class LicenseService extends AbstractLifecycleComponent<LicenseService> implements Licensee {
public static final String FEATURE_NAME = MarvelPlugin.NAME;
private static final LicensesService.TrialLicenseOptions TRIAL_LICENSE_OPTIONS =
new LicensesService.TrialLicenseOptions(TimeValue.timeValueHours(30 * 24), 1000);
private static final FormatDateTimeFormatter DATE_FORMATTER = Joda.forPattern("EEEE, MMMMM dd, yyyy", Locale.ROOT);
static final TimeValue GRACE_PERIOD = days(7);
private final LicensesManagerService managerService;
private final LicensesClientService clientService;
private final LicenseeRegistry clientService;
private final MarvelSettings marvelSettings;
private final Collection<LicensesService.ExpirationCallback> expirationLoggers;
private final LicensesClientService.AcknowledgementCallback acknowledgementCallback;
private volatile Mode mode;
private volatile boolean enabled;
private volatile LicenseState state;
private volatile long expiryDate;
@Inject
public LicenseService(Settings settings, LicensesClientService clientService, LicensesManagerService managerService, MarvelSettings marvelSettings) {
public LicenseService(Settings settings, LicenseeRegistry clientService, LicensesManagerService managerService, MarvelSettings marvelSettings) {
super(settings);
this.managerService = managerService;
this.clientService = clientService;
this.marvelSettings = marvelSettings;
this.mode = Mode.LITE;
this.expirationLoggers = Arrays.asList(
new LicensesService.ExpirationCallback.Pre(days(7), days(30), days(1)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Marvel license will expire on [{}].\n" +
"# Have a new license? please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesService.ExpirationCallback.Pre(days(0), days(7), minutes(10)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Marvel license will expire on [{}].\n" +
"# Have a new license? please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesService.ExpirationCallback.Post(days(0), GRACE_PERIOD, minutes(10)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
long endOfGracePeriod = license.expiryDate() + GRACE_PERIOD.getMillis();
logger.error("\n" +
"#\n" +
"# MARVEL LICENSE HAS EXPIRED ON [{}].\n" +
"# MARVEL WILL STOP COLLECTING DATA ON [{}].\n" +
"# HAVE A NEW LICENSE? PLEASE UPDATE IT. OTHERWISE, PLEASE REACH OUT TO YOUR SUPPORT CONTACT.\n" +
"#", DATE_FORMATTER.printer().print(endOfGracePeriod), DATE_FORMATTER.printer().print(license.expiryDate()));
}
}
);
this.acknowledgementCallback = new LicensesClientService.AcknowledgementCallback() {
@Override
public List<String> acknowledge(License currentLicense, License newLicense) {
switch (newLicense.type()) {
case "trial":
case "gold":
case "platinum":
return Collections.emptyList();
default: // "basic" - we also fall back to basic for an unknown type
return Collections.singletonList(LoggerMessageFormat.format(
"Marvel: Multi-cluster support is disabled for clusters with [{}] licenses.\n" +
"If you are running multiple customers, users won't be able to access this\n" +
"all the clusters with [{}] licenses from a single Marvel instance. To access them\n" +
"a dedicated and separated marvel instance will be required for each cluster",
newLicense.type(), newLicense.type()));
}
}
};
}
@Override
protected void doStart() throws ElasticsearchException {
clientService.register(FEATURE_NAME, TRIAL_LICENSE_OPTIONS, expirationLoggers, acknowledgementCallback, new InternalListener(this));
clientService.register(this);
}
@Override
@ -120,14 +55,6 @@ public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
protected void doClose() throws ElasticsearchException {
}
static TimeValue days(int days) {
return TimeValue.timeValueHours(days * 24);
}
static TimeValue minutes(int minutes) {
return TimeValue.timeValueMinutes(minutes);
}
/**
* @return the current marvel's operating mode
*/
@ -138,18 +65,20 @@ public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
/**
* @return all registered licenses
*/
public List<License> licenses() {
return managerService.getLicenses();
public License license() {
return managerService.getLicense();
}
/**
* @return true if the marvel license is enabled
*/
public boolean enabled() {
return enabled;
return state == LicenseState.ENABLED || state == LicenseState.GRACE_PERIOD;
}
/**
* TODO: remove licensing grace period, just check for state == LicensesClientService.LicenseState.GRACE_PERIOD instead
*
* @return true if marvel is running within the "grace period", ie when the license
* is expired but a given extra delay is not yet elapsed
*/
@ -164,30 +93,57 @@ public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
return expiryDate;
}
class InternalListener implements LicensesClientService.Listener {
@Override
public String id() {
return FEATURE_NAME;
}
private final LicenseService service;
@Override
public String[] expirationMessages() {
// TODO add messages to be logged around license expiry
return Strings.EMPTY_ARRAY;
}
public InternalListener(LicenseService service) {
this.service = service;
@Override
public String[] acknowledgmentMessages(License currentLicense, License newLicense) {
switch (newLicense.operationMode()) {
case BASIC:
if (currentLicense != null) {
switch (currentLicense.operationMode()) {
case TRIAL:
case GOLD:
case PLATINUM:
return new String[] {
LoggerMessageFormat.format(
"Multi-cluster support is disabled for clusters with [{}] licenses.\n" +
"If you are running multiple customers, users won't be able to access this\n" +
"all the clusters with [{}] licenses from a single Marvel instance. To access them\n" +
"a dedicated and separated marvel instance will be required for each cluster",
newLicense.type(), newLicense.type())
};
}
}
}
return Strings.EMPTY_ARRAY;
}
@Override
public void onEnabled(License license) {
try {
service.enabled = true;
service.expiryDate = license.expiryDate();
service.mode = Mode.fromName(license.type());
} catch (IllegalArgumentException e) {
service.mode = Mode.LITE;
@Override
public void onChange(License license, LicenseState state) {
synchronized (this) {
this.state = state;
if (license != null) {
try {
mode = Mode.fromName(license.type());
} catch (IllegalArgumentException e) {
mode = Mode.LITE;
}
expiryDate = license.expiryDate();
} else {
mode = Mode.LITE;
}
if (state == LicenseState.DISABLED) {
mode = Mode.LITE;
}
}
@Override
public void onDisabled(License license) {
service.enabled = false;
service.expiryDate = license.expiryDate();
service.mode = Mode.LITE;
}
}
}

View File

@ -16,6 +16,8 @@ public enum Mode {
/**
* Marvel runs in downgraded mode
*
* TODO: do we really need mode?
*/
TRIAL(0),
@ -55,9 +57,13 @@ public enum Mode {
public static Mode fromName(String name) {
switch (name.toLowerCase(Locale.ROOT)) {
case "trial": return TRIAL;
case "lite": return LITE;
case "standard" : return STANDARD;
case "trial":
return LITE;
case "basic":
case "gold" :
case "silver":
case "platinum":
return STANDARD;
default:
throw new ElasticsearchException("unknown marvel mode name [" + name + "]");
}

View File

@ -33,7 +33,7 @@ public class MarvelInternalUserHolder {
// and full access to .marvel-* and .marvel-data indices
.add(Privilege.Index.ALL, MarvelSettings.MARVEL_INDICES_PREFIX + "*")
// note, we don't need _licenses permission as we're taking the licenses
// note, we don't need _license permission as we're taking the licenses
// directly form the license service.
.build();

View File

@ -17,12 +17,11 @@ import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesManagerService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.license.plugin.action.delete.DeleteLicenseRequest;
import org.elasticsearch.license.plugin.action.put.PutLicenseRequest;
import org.elasticsearch.license.plugin.core.*;
import org.elasticsearch.marvel.MarvelPlugin;
import org.elasticsearch.marvel.agent.settings.MarvelSettings;
import org.elasticsearch.marvel.license.LicenseService;
import org.elasticsearch.marvel.shield.MarvelShieldIntegration;
import org.elasticsearch.marvel.shield.SecuredClient;
import org.elasticsearch.marvel.test.MarvelIntegTestCase;
@ -88,15 +87,13 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
private static License createTestingLicense(long issueDate, long expiryDate) {
return License.builder()
.feature(LicenseService.FEATURE_NAME)
.expiryDate(expiryDate)
.issueDate(issueDate)
.issuedTo("AbstractCollectorTestCase")
.issuer("test")
.maxNodes(Integer.MAX_VALUE)
.signature("_signature")
.type("standard")
.subscriptionType("all_is_good")
.type("basic")
.uid(String.valueOf(RandomizedTest.systemPropertyAsInt(SysGlobals.CHILDVM_SYSPROP_JVM_ID, 0)) + System.identityHashCode(AbstractCollectorTestCase.class))
.build();
}
@ -107,7 +104,7 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
final License license = createTestingLicense(issueDate, expiryDate);
for (LicenseServiceForCollectors service : internalCluster().getInstances(LicenseServiceForCollectors.class)) {
service.enable(license);
service.onChange(license, LicenseState.ENABLED);
}
for (LicensesManagerServiceForCollectors service : internalCluster().getInstances(LicensesManagerServiceForCollectors.class)) {
service.update(license);
@ -120,7 +117,7 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
final License license = createTestingLicense(issueDate, expiryDate);
for (LicenseServiceForCollectors service : internalCluster().getInstances(LicenseServiceForCollectors.class)) {
service.disable(license);
service.onChange(license, LicenseState.GRACE_PERIOD);
}
for (LicensesManagerServiceForCollectors service : internalCluster().getInstances(LicensesManagerServiceForCollectors.class)) {
service.update(license);
@ -133,7 +130,7 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
final License license = createTestingLicense(issueDate, expiryDate);
for (LicenseServiceForCollectors service : internalCluster().getInstances(LicenseServiceForCollectors.class)) {
service.disable(license);
service.onChange(license, LicenseState.DISABLED);
}
for (LicensesManagerServiceForCollectors service : internalCluster().getInstances(LicensesManagerServiceForCollectors.class)) {
service.update(license);
@ -146,7 +143,7 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
final License license = createTestingLicense(issueDate, expiryDate);
for (LicenseServiceForCollectors service : internalCluster().getInstances(LicenseServiceForCollectors.class)) {
service.disable(license);
service.onChange(license, LicenseState.DISABLED);
}
for (LicensesManagerServiceForCollectors service : internalCluster().getInstances(LicensesManagerServiceForCollectors.class)) {
service.update(license);
@ -205,7 +202,7 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
@Override
protected void configure() {
bind(LicenseServiceForCollectors.class).asEagerSingleton();
bind(LicensesClientService.class).to(LicenseServiceForCollectors.class);
bind(LicenseeRegistry.class).to(LicenseServiceForCollectors.class);
bind(LicensesManagerServiceForCollectors.class).asEagerSingleton();
bind(LicensesManagerService.class).to(LicensesManagerServiceForCollectors.class);
}
@ -213,9 +210,9 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
}
}
public static class LicenseServiceForCollectors extends AbstractComponent implements LicensesClientService {
public static class LicenseServiceForCollectors extends AbstractComponent implements LicenseeRegistry {
private final List<Listener> listeners = new ArrayList<>();
private final List<Licensee> licensees = new ArrayList<>();
@Inject
public LicenseServiceForCollectors(Settings settings) {
@ -223,19 +220,13 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
}
@Override
public void register(String feature, TrialLicenseOptions trialLicenseOptions, Collection<ExpirationCallback> expirationCallbacks, AcknowledgementCallback acknowledgementCallback, Listener listener) {
listeners.add(listener);
public void register(Licensee licensee) {
licensees.add(licensee);
}
public void enable(License license) {
for (Listener listener : listeners) {
listener.onEnabled(license);
}
}
public void disable(License license) {
for (Listener listener : listeners) {
listener.onDisabled(license);
public void onChange(License license, LicenseState state) {
for (Licensee licensee : licensees) {
licensee.onChange(license, state);
}
}
}
@ -245,21 +236,24 @@ public class AbstractCollectorTestCase extends MarvelIntegTestCase {
private final Map<String, License> licenses = Collections.synchronizedMap(new HashMap<String, License>());
@Override
public void registerLicenses(LicensesService.PutLicenseRequestHolder requestHolder, ActionListener<LicensesService.LicensesUpdateResponse> listener) {
public void registerLicense(PutLicenseRequest request, ActionListener<LicensesService.LicensesUpdateResponse> listener) {
}
@Override
public void removeLicenses(LicensesService.DeleteLicenseRequestHolder requestHolder, ActionListener<ClusterStateUpdateResponse> listener) {
public void removeLicense(DeleteLicenseRequest request, ActionListener<ClusterStateUpdateResponse> listener) {
}
@Override
public Set<String> enabledFeatures() {
public List<String> licenseesWithState(LicenseState state) {
return null;
}
@Override
public List<License> getLicenses() {
return new ArrayList<>(licenses.values());
public License getLicense() {
// TODO: we only take the first of the licenses that are updated
// FIXME
Iterator<License> iterator = licenses.values().iterator();
return iterator.hasNext() ? iterator.next() : null;
}
public void update(License license) {

View File

@ -93,7 +93,6 @@ public class ClusterInfoIT extends MarvelIntegTestCase {
String recalculated = ClusterInfoRenderer.hash(status, uid, type, String.valueOf(expiryDate), clusterUUID);
assertThat(hkey, equalTo(recalculated));
assertThat((String) license.get(ClusterInfoRenderer.Fields.FEATURE.underscore().toString()), not(isEmptyOrNullString()));
assertThat((String) license.get(ClusterInfoRenderer.Fields.ISSUER.underscore().toString()), not(isEmptyOrNullString()));
assertThat((String) license.get(ClusterInfoRenderer.Fields.ISSUED_TO.underscore().toString()), not(isEmptyOrNullString()));
assertThat((Long) license.get(ClusterInfoRenderer.Fields.ISSUE_DATE_IN_MILLIS.underscore().toString()), greaterThan(0L));

View File

@ -13,8 +13,9 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.marvel.MarvelPlugin;
import org.elasticsearch.marvel.mode.Mode;
import org.elasticsearch.marvel.test.MarvelIntegTestCase;
@ -106,26 +107,24 @@ public class LicenseIntegrationTests extends MarvelIntegTestCase {
@Override
protected void configure() {
bind(MockLicenseService.class).asEagerSingleton();
bind(LicensesClientService.class).to(MockLicenseService.class);
bind(LicenseeRegistry.class).to(MockLicenseService.class);
}
}
public static class MockLicenseService extends AbstractComponent implements LicensesClientService {
public static class MockLicenseService extends AbstractComponent implements LicenseeRegistry {
static final License DUMMY_LICENSE = License.builder()
.feature(LicenseService.FEATURE_NAME)
.expiryDate(System.currentTimeMillis())
.issueDate(System.currentTimeMillis())
.issuedTo("LicensingTests")
.issuer("test")
.maxNodes(Integer.MAX_VALUE)
.signature("_signature")
.type("standard")
.subscriptionType("all_is_good")
.type("basic")
.uid(String.valueOf(RandomizedTest.systemPropertyAsInt(SysGlobals.CHILDVM_SYSPROP_JVM_ID, 0)) + System.identityHashCode(LicenseIntegrationTests.class))
.build();
private final List<Listener> listeners = new ArrayList<>();
private final List<Licensee> licensees = new ArrayList<>();
@Inject
public MockLicenseService(Settings settings) {
@ -134,22 +133,20 @@ public class LicenseIntegrationTests extends MarvelIntegTestCase {
}
@Override
public void register(String s, LicensesService.TrialLicenseOptions trialLicenseOptions, Collection<LicensesService.ExpirationCallback> collection, AcknowledgementCallback acknowledgementCallback, Listener listener) {
listeners.add(listener);
public void register(Licensee licensee) {
licensees.add(licensee);
enable();
}
public void enable() {
// enabled all listeners (incl. shield)
for (Listener listener : listeners) {
listener.onEnabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
licensee.onChange(DUMMY_LICENSE, randomBoolean() ? LicenseState.GRACE_PERIOD : LicenseState.ENABLED);
}
}
public void disable() {
// only disable watcher listener (we need shield to work)
for (Listener listener : listeners) {
listener.onDisabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
licensee.onChange(DUMMY_LICENSE, LicenseState.DISABLED);
}
}
}

View File

@ -22,7 +22,7 @@ To install or update the license use the following REST API:
[source,shell]
-----------------------------------------------------------------------
curl -XPUT -u admin 'http://<host>:<port>/_licenses' -d @license.json
curl -XPUT -u admin 'http://<host>:<port>/_license' -d @license.json
-----------------------------------------------------------------------
Where:
@ -42,7 +42,7 @@ You can list all currently installed licenses by executing the following REST AP
[source,shell]
-----------------------------------------------------
curl -XGET -u admin:password 'http://<host>:<port>/_licenses'
curl -XGET -u admin:password 'http://<host>:<port>/_license'
-----------------------------------------------------
The response of this command will be a JSON listing all available licenses. In the case of Shield, the following

View File

@ -16,6 +16,7 @@ import org.elasticsearch.action.support.ActionFilterChain;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.LicenseUtils;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.action.interceptor.RequestInterceptor;
@ -62,13 +63,8 @@ public class ShieldActionFilter extends AbstractComponent implements ActionFilte
this.actionMapper = actionMapper;
licenseEventsNotifier.register(new LicenseEventsNotifier.Listener() {
@Override
public void enabled() {
licenseEnabled = true;
}
@Override
public void disabled() {
licenseEnabled = false;
public void notify(LicenseState state) {
licenseEnabled = state != LicenseState.DISABLED;
}
});
this.requestInterceptors = requestInterceptors;

View File

@ -5,6 +5,8 @@
*/
package org.elasticsearch.shield.license;
import org.elasticsearch.license.plugin.core.LicenseState;
import java.util.HashSet;
import java.util.Set;
@ -26,22 +28,14 @@ public class LicenseEventsNotifier {
listeners.add(listener);
}
protected void notifyEnabled() {
protected void notify(LicenseState state) {
for (Listener listener : listeners) {
listener.enabled();
}
}
protected void notifyDisabled() {
for (Listener listener : listeners) {
listener.disabled();
listener.notify(state);
}
}
public static interface Listener {
void enabled();
void disabled();
void notify(LicenseState state);
}
}

View File

@ -6,99 +6,95 @@
package org.elasticsearch.shield.license;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
import org.elasticsearch.common.joda.Joda;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.shield.ShieldPlugin;
import java.util.*;
/**
*
*/
public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
public class LicenseService extends AbstractLifecycleComponent<LicenseService> implements Licensee {
public static final String FEATURE_NAME = ShieldPlugin.NAME;
private static final LicensesClientService.TrialLicenseOptions TRIAL_LICENSE_OPTIONS =
new LicensesClientService.TrialLicenseOptions(TimeValue.timeValueHours(30 * 24), 1000);
private static final FormatDateTimeFormatter DATE_FORMATTER = Joda.forPattern("EEEE, MMMMM dd, yyyy", Locale.ROOT);
private final LicensesClientService licensesClientService;
private final LicenseeRegistry licenseeRegistry;
private final LicenseEventsNotifier notifier;
private final Collection<LicensesClientService.ExpirationCallback> expirationLoggers;
private final LicensesClientService.AcknowledgementCallback acknowledgementCallback;
private boolean enabled = false;
private volatile LicenseState state = LicenseState.DISABLED;
@Inject
public LicenseService(Settings settings, LicensesClientService licensesClientService, LicenseEventsNotifier notifier) {
public LicenseService(Settings settings, LicenseeRegistry licenseeRegistry, LicenseEventsNotifier notifier) {
super(settings);
this.licensesClientService = licensesClientService;
this.licenseeRegistry = licenseeRegistry;
this.notifier = notifier;
this.expirationLoggers = Arrays.asList(
new LicensesClientService.ExpirationCallback.Pre(days(7), days(30), days(1)) {
@Override
public void on(License license, LicensesClientService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Shield license will expire on [{}]. Cluster health, cluster stats and indices stats operations are\n" +
"# blocked on Shield license expiration. All data operations (read and write) continue to work. If you\n" +
"# have a new license, please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesClientService.ExpirationCallback.Pre(days(0), days(7), minutes(10)) {
@Override
public void on(License license, LicensesClientService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Shield license will expire on [{}]. Cluster health, cluster stats and indices stats operations are\n" +
"# blocked on Shield license expiration. All data operations (read and write) continue to work. If you\n" +
"# have a new license, please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesClientService.ExpirationCallback.Post(days(0), null, minutes(10)) {
@Override
public void on(License license, LicensesClientService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# SHIELD LICENSE EXPIRED ON [{}]! CLUSTER HEALTH, CLUSTER STATS AND INDICES STATS OPERATIONS ARE\n" +
"# NOW BLOCKED. ALL DATA OPERATIONS (READ AND WRITE) CONTINUE TO WORK. IF YOU HAVE A NEW LICENSE, PLEASE\n" +
"# UPDATE IT. OTHERWISE, PLEASE REACH OUT TO YOUR SUPPORT CONTACT.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
}
);
this.acknowledgementCallback = new LicensesClientService.AcknowledgementCallback() {
@Override
public List<String> acknowledge(License currentLicense, License newLicense) {
// TODO: add messages to be acknowledged when installing newLicense from currentLicense
// NOTE: currentLicense can be null, as a license registration can happen before
// a trial license could be generated
return Collections.emptyList();
}
}
@Override
public String id() {
return FEATURE_NAME;
}
@Override
public String[] expirationMessages() {
return new String[] {
"Cluster health, cluster stats and indices stats operations are blocked",
"All data operations (read and write) continue to work"
};
}
public synchronized boolean enabled() {
return enabled;
@Override
public String[] acknowledgmentMessages(License currentLicense, License newLicense) {
switch (newLicense.operationMode()) {
case BASIC:
if (currentLicense != null) {
switch (currentLicense.operationMode()) {
case TRIAL:
case GOLD:
case PLATINUM:
return new String[] { "The following Shield functionality will be disabled: authentication, authorization, ip filtering, auditing, SSL will be disabled on node restart. Please restart your node after applying the license." };
}
}
break;
case GOLD:
if (currentLicense != null) {
switch (currentLicense.operationMode()) {
case TRIAL:
case BASIC:
case PLATINUM:
return new String[] {
"Field and document level access control will be disabled"
};
}
}
break;
}
return Strings.EMPTY_ARRAY;
}
@Override
public void onChange(License license, LicenseState state) {
synchronized (this) {
this.state = state;
notifier.notify(state);
}
}
public LicenseState state() {
return state;
}
@Override
protected void doStart() throws ElasticsearchException {
if (settings.getGroups("tribe", true).isEmpty()) {
licensesClientService.register(FEATURE_NAME, TRIAL_LICENSE_OPTIONS, expirationLoggers, acknowledgementCallback, new InternalListener());
licenseeRegistry.register(this);
} else {
//TODO currently we disable licensing on tribe node. remove this once es core supports merging cluster
new InternalListener().onEnabled(null);
onChange(null, LicenseState.ENABLED);
}
}
@ -109,34 +105,4 @@ public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
@Override
protected void doClose() throws ElasticsearchException {
}
static TimeValue days(int days) {
return TimeValue.timeValueHours(days * 24);
}
static TimeValue minutes(int minutes) {
return TimeValue.timeValueMinutes(minutes);
}
class InternalListener implements LicensesClientService.Listener {
@Override
public void onEnabled(License license) {
synchronized (LicenseService.this) {
logger.info("enabling license for [{}]", FEATURE_NAME);
enabled = true;
notifier.notifyEnabled();
}
}
@Override
public void onDisabled(License license) {
synchronized (LicenseService.this) {
logger.info("DISABLING LICENSE FOR [{}]", FEATURE_NAME);
enabled = false;
notifier.notifyDisabled();
}
}
}
}

View File

@ -12,6 +12,7 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.internal.Nullable;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.rest.*;
import org.elasticsearch.shield.ShieldBuild;
import org.elasticsearch.shield.ShieldPlugin;
@ -71,7 +72,7 @@ public class RestShieldInfoAction extends BaseRestHandler {
private Status resolveStatus() {
if (shieldEnabled) {
if (licenseService.enabled()) {
if (licenseService.state() != LicenseState.DISABLED) {
return Status.ENABLED;
}
return Status.UNLICENSED;

View File

@ -19,7 +19,9 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.shield.license.LicenseService;
@ -169,13 +171,13 @@ public class LicensingTests extends ShieldIntegTestCase {
}
public static void disableLicensing() {
for (InternalLicensesClientService service : internalCluster().getInstances(InternalLicensesClientService.class)) {
for (InternalLicenseeRegistry service : internalCluster().getInstances(InternalLicenseeRegistry.class)) {
service.disable();
}
}
public static void enableLicensing() {
for (InternalLicensesClientService service : internalCluster().getInstances(InternalLicensesClientService.class)) {
for (InternalLicenseeRegistry service : internalCluster().getInstances(InternalLicenseeRegistry.class)) {
service.enable();
}
}
@ -203,49 +205,47 @@ public class LicensingTests extends ShieldIntegTestCase {
public static class InternalLicenseModule extends AbstractModule {
@Override
protected void configure() {
bind(InternalLicensesClientService.class).asEagerSingleton();
bind(LicensesClientService.class).to(InternalLicensesClientService.class);
bind(InternalLicenseeRegistry.class).asEagerSingleton();
bind(LicenseeRegistry.class).to(InternalLicenseeRegistry.class);
}
}
public static class InternalLicensesClientService extends AbstractComponent implements LicensesClientService {
public static class InternalLicenseeRegistry extends AbstractComponent implements LicenseeRegistry {
private final List<Listener> listeners = new ArrayList<>();
private final List<Licensee> licensees = new ArrayList<>();
static final License DUMMY_LICENSE = License.builder()
.feature(LicenseService.FEATURE_NAME)
.expiryDate(System.currentTimeMillis())
.issueDate(System.currentTimeMillis())
.issuedTo("LicensingTests")
.issuer("test")
.maxNodes(Integer.MAX_VALUE)
.signature("_signature")
.type("test_license_for_shield")
.subscriptionType("all_is_good")
.type("basic")
.uid(String.valueOf(randomLong()) + System.identityHashCode(LicensingTests.class))
.build();
@Inject
public InternalLicensesClientService(Settings settings) {
public InternalLicenseeRegistry(Settings settings) {
super(settings);
enable();
}
@Override
public void register(String s, LicensesClientService.TrialLicenseOptions trialLicenseOptions, Collection<LicensesClientService.ExpirationCallback> collection, AcknowledgementCallback acknowledgementCallback, Listener listener) {
listeners.add(listener);
public void register(Licensee licensee) {
licensees.add(licensee);
enable();
}
void enable() {
for (Listener listener : listeners) {
listener.onEnabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
licensee.onChange(DUMMY_LICENSE, LicenseState.ENABLED);
}
}
void disable() {
for (Listener listener : listeners) {
listener.onDisabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
licensee.onChange(DUMMY_LICENSE, LicenseState.DISABLED);
}
}
}

View File

@ -11,6 +11,7 @@ import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.action.support.ActionFilterChain;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.action.interceptor.RequestInterceptor;
import org.elasticsearch.shield.audit.AuditTrail;
@ -112,7 +113,7 @@ public class ShieldActionFilterTests extends ESTestCase {
private class MockLicenseEventsNotifier extends LicenseEventsNotifier {
@Override
public void register(MockLicenseEventsNotifier.Listener listener) {
listener.enabled();
listener.notify(LicenseState.ENABLED);
}
}
}

View File

@ -6,93 +6,70 @@
package org.elasticsearch.watcher.license;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.joda.FormatDateTimeFormatter;
import org.elasticsearch.common.joda.Joda;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.watcher.WatcherPlugin;
import java.util.*;
/**
*
*/
public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
public class LicenseService extends AbstractLifecycleComponent<LicenseService> implements Licensee {
public static final String FEATURE_NAME = WatcherPlugin.NAME;
private static final LicensesClientService.TrialLicenseOptions TRIAL_LICENSE_OPTIONS =
new LicensesClientService.TrialLicenseOptions(TimeValue.timeValueHours(30 * 24), 1000);
private static final FormatDateTimeFormatter DATE_FORMATTER = Joda.forPattern("EEEE, MMMMM dd, yyyy", Locale.ROOT);
private final LicensesClientService clientService;
private final Collection<LicensesService.ExpirationCallback> expirationLoggers;
private final LicensesClientService.AcknowledgementCallback acknowledgementCallback;
private volatile boolean enabled;
private final LicenseeRegistry clientService;
private volatile LicenseState state;
@Inject
public LicenseService(Settings settings, LicensesClientService clientService) {
public LicenseService(Settings settings, LicenseeRegistry clientService) {
super(settings);
this.clientService = clientService;
this.expirationLoggers = Arrays.asList(
new LicensesService.ExpirationCallback.Pre(days(7), days(30), days(1)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Watcher license will expire on [{}]. All configured actions on\n" +
"# all registered watches are throttled (not executed) on Watcher license expiration. \n" +
"# Watches will continue be evaluated and watch history will continue being recorded.\n" +
"# Have a new license? please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesService.ExpirationCallback.Pre(days(0), days(7), minutes(10)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# Watcher license will expire on [{}]. All configured actions on\n" +
"# all registered watches are throttled (not executed) on Watcher license expiration. \n" +
"# Watches will continue be evaluated and watch history will continue being recorded.\n" +
"# Have a new license? please update it. Otherwise, please reach out to your support contact.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
},
new LicensesService.ExpirationCallback.Post(days(0), null, minutes(10)) {
@Override
public void on(License license, LicensesService.ExpirationStatus status) {
logger.error("\n" +
"#\n" +
"# WATCHER LICENSE WAS EXPIRED ON [{}]. ALL CONFIGURED ACTIONS ON\n" +
"# ALL REGISTERED WATCHES ARE THROTTLED (NOT EXECUTED) ON WATCHER LICENSE EXPIRATION. \n" +
"# WATCHES WILL CONTINUE BE EVALUATED AND WATCH HISTORY WILL CONTINUE BEING RECORDED.\n" +
"# HAVE A NEW LICENSE? PLEASE UPDATE IT. OTHERWISE, PLEASE REACH OUT TO YOUR SUPPORT CONTACT.\n" +
"#", DATE_FORMATTER.printer().print(license.expiryDate()));
}
@Override
public String id() {
return FEATURE_NAME;
}
@Override
public String[] expirationMessages() {
// TODO add messages to be logged around license expiry
return new String[0];
}
@Override
public String[] acknowledgmentMessages(License currentLicense, License newLicense) {
switch (newLicense.operationMode()) {
case BASIC:
if (currentLicense != null) {
switch (currentLicense.operationMode()) {
case TRIAL:
case GOLD:
case PLATINUM:
return new String[] { "Watcher will be disabled" };
}
}
);
this.acknowledgementCallback = new LicensesClientService.AcknowledgementCallback() {
@Override
public List<String> acknowledge(License currentLicense, License newLicense) {
// TODO: add messages to be acknowledged when installing newLicense from currentLicense
// NOTE: currentLicense can be null, as a license registration can happen before
// a trial license could be generated
return Collections.emptyList();
}
};
break;
}
return Strings.EMPTY_ARRAY;
}
@Override
public void onChange(License license, LicenseState state) {
synchronized (this) {
this.state = state;
}
}
@Override
protected void doStart() throws ElasticsearchException {
clientService.register(FEATURE_NAME, TRIAL_LICENSE_OPTIONS, expirationLoggers, acknowledgementCallback, new InternalListener(this));
clientService.register(this);
}
@Override
@ -104,33 +81,6 @@ public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
}
public boolean enabled() {
return enabled;
}
static TimeValue days(int days) {
return TimeValue.timeValueHours(days * 24);
}
static TimeValue minutes(int minutes) {
return TimeValue.timeValueMinutes(minutes);
}
class InternalListener implements LicensesClientService.Listener {
private final LicenseService service;
public InternalListener(LicenseService service) {
this.service = service;
}
@Override
public void onEnabled(License license) {
service.enabled = true;
}
@Override
public void onDisabled(License license) {
service.enabled = false;
}
return state != LicenseState.DISABLED;
}
}

View File

@ -14,8 +14,9 @@ import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.core.License;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.license.plugin.core.LicenseState;
import org.elasticsearch.license.plugin.core.Licensee;
import org.elasticsearch.license.plugin.core.LicenseeRegistry;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.watcher.actions.ActionStatus;
@ -46,15 +47,13 @@ import static org.hamcrest.Matchers.*;
public class LicenseIntegrationTests extends AbstractWatcherIntegrationTestCase {
static final License DUMMY_LICENSE = License.builder()
.feature(LicenseService.FEATURE_NAME)
.expiryDate(System.currentTimeMillis())
.issueDate(System.currentTimeMillis())
.issuedTo("LicensingTests")
.issuer("test")
.maxNodes(Integer.MAX_VALUE)
.signature("_signature")
.type("test_license_for_watcher")
.subscriptionType("all_is_good")
.type("basic")
.uid(String.valueOf(RandomizedTest.systemPropertyAsInt(SysGlobals.CHILDVM_SYSPROP_JVM_ID, 0)) + System.identityHashCode(LicenseIntegrationTests.class))
.build();
@ -301,13 +300,13 @@ public class LicenseIntegrationTests extends AbstractWatcherIntegrationTestCase
@Override
protected void configure() {
bind(MockLicenseService.class).asEagerSingleton();
bind(LicensesClientService.class).to(MockLicenseService.class);
bind(LicenseeRegistry.class).to(MockLicenseService.class);
}
}
public static class MockLicenseService extends AbstractComponent implements LicensesClientService {
public static class MockLicenseService extends AbstractComponent implements LicenseeRegistry {
private final List<Listener> listeners = new ArrayList<>();
private final List<Licensee> licensees = new ArrayList<>();
@Inject
public MockLicenseService(Settings settings) {
@ -316,23 +315,23 @@ public class LicenseIntegrationTests extends AbstractWatcherIntegrationTestCase
}
@Override
public void register(String s, LicensesService.TrialLicenseOptions trialLicenseOptions, Collection<LicensesService.ExpirationCallback> collection, AcknowledgementCallback acknowledgementCallback, Listener listener) {
listeners.add(listener);
public void register(Licensee licensee) {
licensees.add(licensee);
enable();
}
public void enable() {
// enabled all listeners (incl. shield)
for (Listener listener : listeners) {
listener.onEnabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
licensee.onChange(DUMMY_LICENSE, LicenseState.ENABLED);
}
}
public void disable() {
// only disable watcher listener (we need shield to work)
for (Listener listener : listeners) {
if (listener instanceof LicenseService.InternalListener) {
listener.onDisabled(DUMMY_LICENSE);
for (Licensee licensee : licensees) {
if (licensee instanceof LicenseService) {
licensee.onChange(DUMMY_LICENSE, LicenseState.DISABLED);
}
}
}