/*******************************************************************************
* Copyright (c) 2011, 2016 Eurotech and/or its affiliates
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Eurotech
*******************************************************************************/
package org.eclipse.kura.core.configuration;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.eclipse.kura.KuraErrorCode;
import org.eclipse.kura.KuraException;
import org.eclipse.kura.cloud.CloudService;
import org.eclipse.kura.cloud.Cloudlet;
import org.eclipse.kura.cloud.CloudletTopic;
import org.eclipse.kura.configuration.ComponentConfiguration;
import org.eclipse.kura.configuration.ConfigurationService;
import org.eclipse.kura.core.configuration.util.XmlUtil;
import org.eclipse.kura.message.KuraPayload;
import org.eclipse.kura.message.KuraRequestPayload;
import org.eclipse.kura.message.KuraResponsePayload;
import org.eclipse.kura.system.SystemService;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CloudConfigurationHandler extends Cloudlet {
private static Logger s_logger = LoggerFactory.getLogger(CloudConfigurationHandler.class);
public static final String APP_ID = "CONF-V1";
/* GET or PUT */
public static final String RESOURCE_CONFIGURATIONS = "configurations";
/* GET */
public static final String RESOURCE_SNAPSHOTS = "snapshots";
/* EXEC */
public static final String RESOURCE_SNAPSHOT = "snapshot";
public static final String RESOURCE_ROLLBACK = "rollback";
private SystemService m_systemService;
private ConfigurationService m_configurationService;
private ComponentContext m_ctx;
private ScheduledExecutorService m_executor;
protected void setConfigurationService(ConfigurationService configurationService) {
this.m_configurationService = configurationService;
}
protected void unsetConfigurationService(ConfigurationService configurationService) {
this.m_configurationService = null;
}
protected void setSystemService(SystemService systemService) {
this.m_systemService = systemService;
}
protected void unsetSystemService(SystemService systemService) {
this.m_systemService = null;
}
// The dependency on the CloudService is optional so we might be activated
// before we have the CloudService.
@Override
public void setCloudService(CloudService cloudService) {
super.setCloudService(cloudService);
super.activate(this.m_ctx);
}
@Override
public void unsetCloudService(CloudService cloudService) {
super.deactivate(this.m_ctx);
super.unsetCloudService(cloudService);
}
public CloudConfigurationHandler() {
super(APP_ID);
}
@Override
protected void activate(ComponentContext componentContext) {
this.m_ctx = componentContext;
this.m_executor = Executors.newSingleThreadScheduledExecutor();
}
@Override
protected void deactivate(ComponentContext componentContext) {
this.m_executor.shutdownNow();
}
@Override
protected void doGet(CloudletTopic reqTopic, KuraRequestPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String resources[] = reqTopic.getResources();
if (resources == null || resources.length == 0) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected one resource but found {}", resources != null ? resources.length : "none");
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
if (resources[0].equals(RESOURCE_CONFIGURATIONS)) {
doGetConfigurations(reqTopic, reqPayload, respPayload);
} else if (resources[0].equals(RESOURCE_SNAPSHOTS)) {
doGetSnapshots(reqTopic, reqPayload, respPayload);
} else {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Cannot find resource with name: {}", resources[0]);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_NOTFOUND);
return;
}
}
@Override
protected void doPut(CloudletTopic reqTopic, KuraRequestPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String resources[] = reqTopic.getResources();
if (resources == null || resources.length == 0) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected one resource but found {}", resources != null ? resources.length : "none");
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
if (resources[0].equals(RESOURCE_CONFIGURATIONS)) {
doPutConfigurations(reqTopic, reqPayload, respPayload);
} else {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Cannot find resource with name: {}", resources[0]);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_NOTFOUND);
return;
}
}
@Override
protected void doExec(CloudletTopic reqTopic, KuraRequestPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String[] resources = reqTopic.getResources();
if (resources == null || resources.length == 0) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected one resource but found {}", resources != null ? resources.length : "none");
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
if (resources[0].equals(RESOURCE_SNAPSHOT)) {
doExecSnapshot(reqTopic, reqPayload, respPayload);
} else if (resources[0].equals(RESOURCE_ROLLBACK)) {
doExecRollback(reqTopic, reqPayload, respPayload);
} else {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Cannot find resource with name: {}", resources[0]);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_NOTFOUND);
return;
}
}
private void doGetSnapshots(CloudletTopic reqTopic, KuraPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String[] resources = reqTopic.getResources();
if (resources.length > 2) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected one or two resource(s) but found {}", resources.length);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
String snapshotId = resources.length == 2 ? resources[1] : null;
if (snapshotId != null) {
long sid = Long.parseLong(snapshotId);
XmlComponentConfigurations xmlConfigs = ((ConfigurationServiceImpl) this.m_configurationService)
.loadEncryptedSnapshotFileContent(sid);
//
// marshall the response
List<ComponentConfigurationImpl> configs = xmlConfigs.getConfigurations();
for (ComponentConfigurationImpl config : configs) {
if (config != null) {
try {
Map<String, Object> decryptedProperties = ((ConfigurationServiceImpl) this.m_configurationService)
.decryptPasswords(config);
config.setProperties(decryptedProperties);
} catch (Throwable t) {
s_logger.warn("Error during snapshot password decryption");
}
}
}
byte[] body = toResponseBody(xmlConfigs);
//
// Build payload
respPayload.setBody(body);
} else {
// get the list of snapshot IDs and put them into a response object
Set<Long> sids = null;
try {
sids = this.m_configurationService.getSnapshots();
} catch (KuraException e) {
s_logger.error("Error listing snapshots: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_SNAPSHOT_LISTING, e);
}
List<Long> snapshotIds = new ArrayList<Long>(sids);
XmlSnapshotIdResult xmlResult = new XmlSnapshotIdResult();
xmlResult.setSnapshotIds(snapshotIds);
//
// marshall the response
byte[] body = toResponseBody(xmlResult);
//
// Build payload
respPayload.setBody(body);
}
}
private void doGetConfigurations(CloudletTopic reqTopic, KuraPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String[] resources = reqTopic.getResources();
if (resources.length > 2) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected at most two resource(s) but found {}", resources.length);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
String pid = resources.length == 2 ? resources[1] : null;
//
// get current configuration with descriptors
List<ComponentConfigurationImpl> configs = new ArrayList<ComponentConfigurationImpl>();
try {
if (pid == null) {
List<String> pidsToIgnore = this.m_systemService.getDeviceManagementServiceIgnore();
// the configuration for all components has been requested
Set<String> componentPids = this.m_configurationService.getConfigurableComponentPids();
for (String componentPid : componentPids) {
boolean skip = false;
if (pidsToIgnore != null && !pidsToIgnore.isEmpty()) {
for (String pidToIgnore : pidsToIgnore) {
if (componentPid.equals(pidToIgnore)) {
skip = true;
break;
}
}
}
if (skip) {
continue;
}
ComponentConfiguration cc = this.m_configurationService.getComponentConfiguration(componentPid);
// TODO: define a validate method for ComponentConfiguration
if (cc == null) {
s_logger.error("null ComponentConfiguration");
continue;
}
if (cc.getPid() == null || cc.getPid().isEmpty()) {
s_logger.error("null or empty ComponentConfiguration PID");
continue;
}
if (cc.getDefinition() == null) {
s_logger.error("null OCD for ComponentConfiguration PID {}", cc.getPid());
continue;
}
if (cc.getDefinition().getId() == null || cc.getDefinition().getId().isEmpty()) {
s_logger.error("null or empty OCD ID for ComponentConfiguration PID {}. OCD ID: {}",
cc.getPid(), cc.getDefinition().getId());
continue;
}
configs.add((ComponentConfigurationImpl) cc);
}
} else {
// the configuration for a specific component has been requested.
ComponentConfiguration cc = this.m_configurationService.getComponentConfiguration(pid);
if (cc != null) {
configs.add((ComponentConfigurationImpl) cc);
}
}
} catch (KuraException e) {
s_logger.error("Error getting component configurations: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_ERROR, e, "Error getting component configurations");
}
XmlComponentConfigurations xmlConfigs = new XmlComponentConfigurations();
xmlConfigs.setConfigurations(configs);
//
// marshall
byte[] body = toResponseBody(xmlConfigs);
//
// Build response payload
respPayload.setBody(body);
}
private void doPutConfigurations(CloudletTopic reqTopic, KuraPayload reqPayload, KuraResponsePayload respPayload) {
String[] resources = reqTopic.getResources();
if (resources.length > 2) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected at most two resource(s) but found {}", resources.length);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
String pid = resources.length == 2 ? resources[1] : null;
XmlComponentConfigurations xmlConfigs = null;
try {
// unmarshall the response
if (reqPayload.getBody() == null || reqPayload.getBody().length == 0) {
throw new IllegalArgumentException("body");
}
String s = new String(reqPayload.getBody(), "UTF-8");
s_logger.info("Received new Configuration");
StringReader sr = new StringReader(s);
xmlConfigs = XmlUtil.unmarshal(sr, XmlComponentConfigurations.class);
} catch (Exception e) {
s_logger.error("Error unmarshalling the request body: {}", e);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
respPayload.setException(e);
return;
}
this.m_executor.schedule(new UpdateConfigurationsCallable(pid, xmlConfigs, this.m_configurationService), 1000,
TimeUnit.MILLISECONDS);
}
private void doExecRollback(CloudletTopic reqTopic, KuraPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String[] resources = reqTopic.getResources();
if (resources.length > 2) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected at most two resource(s) but found {}",
resources != null ? resources.length : "none");
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
String snapshotId = resources.length == 2 ? resources[1] : null;
Long sid;
try {
sid = snapshotId != null ? Long.parseLong(snapshotId) : null;
} catch (NumberFormatException e) {
s_logger.error("Bad numeric numeric format for snapshot ID: {}", snapshotId);
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
this.m_executor.schedule(new RollbackCallable(sid, this.m_configurationService), 1000, TimeUnit.MILLISECONDS);
}
private void doExecSnapshot(CloudletTopic reqTopic, KuraPayload reqPayload, KuraResponsePayload respPayload)
throws KuraException {
String[] resources = reqTopic.getResources();
if (resources.length > 1) {
s_logger.error("Bad request topic: {}", reqTopic.toString());
s_logger.error("Expected one resource(s) but found {}", resources != null ? resources.length : "none");
respPayload.setResponseCode(KuraResponsePayload.RESPONSE_CODE_BAD_REQUEST);
return;
}
// take a new snapshot and get the id
long snapshotId;
try {
snapshotId = this.m_configurationService.snapshot();
} catch (KuraException e) {
s_logger.error("Error taking snapshot: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_SNAPSHOT_TAKING, e);
}
List<Long> snapshotIds = new ArrayList<Long>();
snapshotIds.add(snapshotId);
XmlSnapshotIdResult xmlResult = new XmlSnapshotIdResult();
xmlResult.setSnapshotIds(snapshotIds);
byte[] body = toResponseBody(xmlResult);
respPayload.setBody(body);
}
private static byte[] toResponseBody(Object o) throws KuraException {
//
// marshall the response
StringWriter sw = new StringWriter();
try {
XmlUtil.marshal(o, sw);
} catch (Exception e) {
s_logger.error("Error marshalling snapshots: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_SNAPSHOT_LOADING, e);
}
byte[] body = null;
try {
body = sw.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
s_logger.error("Error encoding response body: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_SNAPSHOT_LOADING, e);
}
return body;
}
}
class UpdateConfigurationsCallable implements Callable<Void> {
private static Logger s_logger = LoggerFactory.getLogger(UpdateConfigurationsCallable.class);
private final String m_pid;
private final XmlComponentConfigurations m_xmlConfigurations;
private final ConfigurationService m_configurationService;
public UpdateConfigurationsCallable(String pid, XmlComponentConfigurations xmlConfigurations,
ConfigurationService configurationService) {
this.m_pid = pid;
this.m_xmlConfigurations = xmlConfigurations;
this.m_configurationService = configurationService;
}
@Override
public Void call() throws Exception {
s_logger.info("Updating configurations");
Thread.currentThread().setName(getClass().getSimpleName());
//
// update the configuration
try {
List<ComponentConfigurationImpl> configImpls = this.m_xmlConfigurations != null
? this.m_xmlConfigurations.getConfigurations() : null;
if (configImpls == null) {
return null;
}
List<ComponentConfiguration> configs = new ArrayList<ComponentConfiguration>();
configs.addAll(configImpls);
if (this.m_pid == null) {
// update all the configurations provided
this.m_configurationService.updateConfigurations(configs);
} else {
// update only the configuration with the provided id
for (ComponentConfiguration config : configs) {
if (this.m_pid.equals(config.getPid())) {
this.m_configurationService.updateConfiguration(this.m_pid,
config.getConfigurationProperties());
}
}
}
} catch (KuraException e) {
s_logger.error("Error updating configurations: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_UPDATE, e);
}
return null;
}
}
class RollbackCallable implements Callable<Void> {
private static Logger s_logger = LoggerFactory.getLogger(RollbackCallable.class);
private final Long m_snapshotId;
private final ConfigurationService m_configurationService;
public RollbackCallable(Long snapshotId, ConfigurationService configurationService) {
super();
this.m_snapshotId = snapshotId;
this.m_configurationService = configurationService;
}
@Override
public Void call() throws Exception {
Thread.currentThread().setName(getClass().getSimpleName());
// rollback to the specified snapshot if any
try {
if (this.m_snapshotId == null) {
this.m_configurationService.rollback();
} else {
this.m_configurationService.rollback(this.m_snapshotId);
}
} catch (KuraException e) {
s_logger.error("Error rolling back to snapshot: {}", e);
throw new KuraException(KuraErrorCode.CONFIGURATION_ROLLBACK, e);
}
return null;
}
}