/*
* Copyright 2014 SeaClouds
* Contact: SeaClouds
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.seaclouds.policy;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.io.BaseEncoding;
import com.google.common.net.HttpHeaders;
import com.google.common.net.MediaType;
import com.google.common.reflect.TypeToken;
import eu.atos.sla.parser.data.wsag.Agreement;
import eu.seaclouds.policy.utils.SeaCloudsPolicyRequestComposer;
import it.polimi.tower4clouds.rules.Action;
import it.polimi.tower4clouds.rules.MonitoringRule;
import it.polimi.tower4clouds.rules.MonitoringRules;
import it.polimi.tower4clouds.rules.Parameter;
import org.apache.brooklyn.api.catalog.Catalog;
import org.apache.brooklyn.api.effector.Effector;
import org.apache.brooklyn.api.entity.EntityLocal;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.sensor.AttributeSensor;
import org.apache.brooklyn.config.ConfigKey;
import org.apache.brooklyn.core.config.ConfigKeys;
import org.apache.brooklyn.core.effector.EffectorBody;
import org.apache.brooklyn.core.effector.Effectors;
import org.apache.brooklyn.core.entity.EntityInternal;
import org.apache.brooklyn.core.entity.StartableApplication;
import org.apache.brooklyn.core.entity.trait.Startable;
import org.apache.brooklyn.core.location.Locations;
import org.apache.brooklyn.core.policy.AbstractPolicy;
import org.apache.brooklyn.core.sensor.BasicAttributeSensor;
import org.apache.brooklyn.core.sensor.Sensors;
import org.apache.brooklyn.entity.software.base.SoftwareProcess;
import org.apache.brooklyn.entity.stock.EffectorStartableImpl;
import org.apache.brooklyn.util.collections.MutableMap;
import org.apache.brooklyn.util.core.config.ConfigBag;
import org.apache.brooklyn.util.core.flags.SetFromFlag;
import org.apache.brooklyn.util.exceptions.Exceptions;
import org.apache.brooklyn.util.http.HttpTool;
import org.apache.brooklyn.util.http.HttpToolResponse;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Catalog(name = "SeaClouds Management", description = "Policy that configures Tower4Clouds, InfluxDB Observers and SLA for a SeaClouds Application")
public class SeaCloudsManagementPolicy extends AbstractPolicy {
private static final Logger LOG = LoggerFactory.getLogger(SeaCloudsManagementPolicy.class);
@SetFromFlag("slaAgreement")
public static final ConfigKey<String> SLA_AGREEMENT = ConfigKeys.newStringConfigKey(
"sla.agreement", "SLA Agreement (Base64-encoded)");
@SetFromFlag("slaEndpoint")
public static final ConfigKey<String> SLA_ENDPOINT = ConfigKeys.newStringConfigKey(
"sla.endpoint", "SLA Core Endpoint (http(s)://url:port)");
@SetFromFlag("slaUsername")
public static final ConfigKey<String> SLA_USERNAME = ConfigKeys.newStringConfigKey(
"sla.username", "SLA Core Username");
@SetFromFlag("slaPasword")
public static final ConfigKey<String> SLA_PASSWORD = ConfigKeys.newStringConfigKey(
"sla.password", "SLA Core Password");
@SetFromFlag("t4cRules")
public static final ConfigKey<String> T4C_RULES = ConfigKeys.newStringConfigKey(
"t4c.rules", "T4C Rules (Base64-encoded)");
@SetFromFlag("t4cEndpoint")
public static final ConfigKey<String> T4C_ENDPOINT = ConfigKeys.newStringConfigKey(
"t4c.endpoint", "Tower4Clouds Endpoint (http(s)://url:port)");
@SetFromFlag("t4cUsername")
public static final ConfigKey<String> T4C_USERNAME = ConfigKeys.newStringConfigKey(
"t4c.username", "Tower4Clouds Username");
@SetFromFlag("t4cPassword")
public static final ConfigKey<String> T4C_PASSWORD = ConfigKeys.newStringConfigKey(
"t4c.password", "Tower4Clouds Password");
@SetFromFlag("influxdbEndpoint")
public static final ConfigKey<String> INFLUXDB_ENDPOINT = ConfigKeys.newStringConfigKey(
"influxdb.endpoint", "InfluxDB Endpoint (http(s)://url:port)");
@SetFromFlag("influxdbUsername")
public static final ConfigKey<String> INFLUXDB_USERNAME = ConfigKeys.newStringConfigKey(
"influxdb.username", "InfluxDB Username", "root");
@SetFromFlag("influxdbPassword")
public static final ConfigKey<String> INFLUXDB_PASSWORD = ConfigKeys.newStringConfigKey(
"influxdb.password", "InfluxDB Password", "root");
@SetFromFlag("influxdbDatabase")
public static final ConfigKey<String> INFLUXDB_DATABASE = ConfigKeys.newStringConfigKey(
"influxdb.database", "InfluxDB Database Name", "tower4clouds");
@SetFromFlag("grafanaEndpoint")
public static final ConfigKey<String> GRAFANA_ENDPOINT = ConfigKeys.newStringConfigKey(
"grafana.endpoint", "InfluxDB Endpoint (http(s)://url:port)");
@SetFromFlag("grafanaUsername")
public static final ConfigKey<String> GRAFANA_USERNAME = ConfigKeys.newStringConfigKey(
"grafana.username", "Grafana Username", "admin");
@SetFromFlag("grafanaPassword")
public static final ConfigKey<String> GRAFANA_PASSWORD = ConfigKeys.newStringConfigKey(
"grafana.password", "Grafana Password", "admin");
static final AttributeSensor<String> SLA_ID = Sensors.newStringSensor("sla.id", "Service Level Agreement ID");
static final AttributeSensor<List<String>> T4C_IDS = new BasicAttributeSensor<List<String>>(new TypeToken<List<String>>() {
}, "t4c.ids", "Tower4Clouds MonitoringRules IDs");
private MonitoringRules monitoringRules;
private Agreement agreement;
private String grafanaDashboardSlug;
private String observerInitializerPayload;
private String grafanaInitializerPayload;
//TODO: This method is boilerplate code also contained in eu.seaclouds.platform.dashboard.util.ObjectMapperHelpers
private <T> T xmlToObject(String xml, Class<T> type) throws JAXBException {
JAXBContext jaxbContext = JAXBContext.newInstance(type);
Unmarshaller jaxbUnmarshaller = jaxbContext.createUnmarshaller();
T obj = (T) jaxbUnmarshaller.unmarshal(new StringReader(xml));
return obj;
}
// Helper method to extract metric names from Monitoring Rules
public List<String> extractMetricNames() {
List<String> result = new ArrayList<>();
for (MonitoringRule rule : monitoringRules.getMonitoringRules()) {
for (Action action : rule.getActions().getActions()) {
if (action.getName().equalsIgnoreCase("OutputMetric")) {
for (Parameter parameter : action.getParameters()) {
if (parameter.getName().equalsIgnoreCase("metric")) {
result.add(parameter.getValue());
}
}
}
}
}
return result;
}
private void parseMonitoringRules() {
String decodedRules = new String(BaseEncoding.base64().decode(getConfig(T4C_RULES)));
try {
monitoringRules = xmlToObject(decodedRules, MonitoringRules.class);
} catch (JAXBException e) {
Exceptions.propagateIfFatal(e);
LOG.warn("Cannot unmarshal the content of t4c.rule into MonitoringRules");
}
}
private void parseAgreements() {
String decodedRules = new String(BaseEncoding.base64().decode(getConfig(SLA_AGREEMENT)));
try {
agreement = xmlToObject(decodedRules, Agreement.class);
} catch (JAXBException e) {
Exceptions.propagateIfFatal(e);
LOG.warn("Cannot unmarshal the content of sla.agreement into an Agreement");
}
}
private void setupObserverInitializerPayload() {
observerInitializerPayload = SeaCloudsPolicyRequestComposer
.setupObserverInitializerPayload(this);
}
private void setupGrafanaInitializerPayload() {
grafanaInitializerPayload =
SeaCloudsPolicyRequestComposer.setupGrafanaInitializerPayload(this, entity.getDisplayName());
}
@Override
public void setEntity(EntityLocal entity) {
super.setEntity(entity);
if (!entity.getApplication().equals(entity)) {
throw new RuntimeException("SeaCloudsInitializerPolicy must be attached to an application");
}
parseMonitoringRules();
parseAgreements();
setupObserverInitializerPayload();
setupGrafanaInitializerPayload();
// Overriding the default START/STOP behaivour to include the aditional tasks required by
// SeaClouds while keeping the original behaviour. It allows to initialize and clean-up SeaClouds
// components when an application is added/removed.
((EntityInternal) entity).getMutableEntityType().addSensor(SLA_ID);
((EntityInternal) entity).getMutableEntityType().addSensor(T4C_IDS);
((EntityInternal) entity).getMutableEntityType().addEffector(newStartEffector());
((EntityInternal) entity).getMutableEntityType().addEffector(newStopEffector());
}
private Effector<Void> newStartEffector() {
return Effectors.effector(Startable.START)
.impl(new EffectorBody<Void>() {
@Override
public Void call(ConfigBag parameters) {
LOG.info("Starting SeaCloudsInitializerPolicy " + "for " + entity.getId());
installSLA();
installMonitoringRules();
notifyRulesReady();
installInfluxDbObservers();
installGrafanaDashboards();
// Rewire the original behaviour
Collection<? extends Location> locations = null;
Object locationRaw = parameters.getStringKey(EffectorStartableImpl.StartParameters.LOCATIONS.getName());
locations = Locations.coerceToCollectionOfLocationsManaged(getManagementContext(), locationRaw);
((StartableApplication) entity).start(locations);
return null;
}
})
.build();
}
private Effector<Void> newStopEffector() {
return Effectors.effector(Startable.STOP)
.parameter(SoftwareProcess.StopSoftwareParameters.STOP_PROCESS_MODE)
.parameter(SoftwareProcess.StopSoftwareParameters.STOP_MACHINE_MODE)
.impl(new EffectorBody<Void>() {
@Override
public Void call(ConfigBag parameters) {
LOG.info("Stopping SeaCloudsInitializerPolicy " + "for " + entity.getId());
removeSlaAgreement();
removeMonitoringRules();
removeInfluxDbObservers();
removeGrafanaDashboard();
// Rewire the original behaviour
((StartableApplication) entity).stop();
return null;
}
})
.build();
}
private void installSLA() {
LOG.info("SeaCloudsInitializerPolicy is installing SLA Agreements for " + entity.getId());
HttpToolResponse response = post(getConfig(SLA_ENDPOINT) + "/seaclouds/agreements", MediaType.APPLICATION_XML_UTF_8.toString(),
getConfig(SLA_USERNAME), getConfig(SLA_PASSWORD), BaseEncoding.base64().decode(getConfig(SLA_AGREEMENT)));
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong during the SLA Agreements installation. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
} else {
entity.sensors().set(SLA_ID, agreement.getAgreementId());
}
}
private void notifyRulesReady() {
LOG.info("SeaCloudsInitializerPolicy is starting to enforce SLA Agreements for " + entity.getId());
HttpToolResponse response = post(getConfig(SLA_ENDPOINT) + "/seaclouds/commands/rulesready?agreementId=" +
agreement.getAgreementId(), MediaType.APPLICATION_XML_UTF_8.toString(),
getConfig(SLA_USERNAME), getConfig(SLA_PASSWORD), "".getBytes());
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong during the SLA Agreements installation. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
}
private void installMonitoringRules() {
LOG.info("SeaCloudsInitializerPolicy is installing T4C Monitoring Rules for " + entity.getId());
HttpToolResponse response = post(getConfig(T4C_ENDPOINT) + "/v1/monitoring-rules", MediaType.APPLICATION_XML_UTF_8.toString(),
getConfig(T4C_USERNAME), getConfig(T4C_PASSWORD), BaseEncoding.base64().decode(getConfig(T4C_RULES)));
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while the monitoring rules installation. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
} else {
List<String> ruleIds = new ArrayList<>();
for (MonitoringRule rule : monitoringRules.getMonitoringRules()) {
ruleIds.add(rule.getId());
}
entity.sensors().set(T4C_IDS, ruleIds);
}
}
private void installInfluxDbObservers() {
LOG.info("SeaCloudsInitializerPolicy is installing InfluxDB Observers for " + entity.getId());
for (String metricName : extractMetricNames()) {
HttpToolResponse response = post(getConfig(T4C_ENDPOINT) + "/v1/metrics/" + metricName + "/observers",
MediaType.JSON_UTF_8.toString(), getConfig(T4C_USERNAME),
getConfig(T4C_PASSWORD), observerInitializerPayload.getBytes());
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while attaching the observers to the Rules. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
}
}
private void installGrafanaDashboards() {
try {
HttpToolResponse response = post(getConfig(GRAFANA_ENDPOINT) + "/api/dashboards/db",
MediaType.JSON_UTF_8.toString(), getConfig(GRAFANA_USERNAME),
getConfig(GRAFANA_PASSWORD), grafanaInitializerPayload.getBytes());
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while installing Grafana Dashboards. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
// Retrieving unique slug to be able to remove the Dashboard later
ObjectMapper mapper = new ObjectMapper();
JsonNode json = mapper.readTree(response.getContent());
grafanaDashboardSlug = json.get("slug").asText();
} catch (Exception e) {
Exceptions.propagateIfFatal(e);
LOG.warn("Error installingGrafanaDashboards, using " + getConfig(GRAFANA_ENDPOINT) + "/api/dashboards/db {}", e);
}
}
private void removeGrafanaDashboard() {
LOG.info("SeaCloudsInitializerPolicy is removing Grafana Dashboard for " + entity.getId());
HttpToolResponse response = delete(getConfig(GRAFANA_ENDPOINT) + "/api/dashboards/db/" + grafanaDashboardSlug,
MediaType.JSON_UTF_8.toString(), getConfig(GRAFANA_USERNAME), getConfig(GRAFANA_PASSWORD));
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while removing Grafana Dashboards. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
}
private void removeSlaAgreement() {
LOG.info("SeaCloudsInitializerPolicy is removing SLA Agreeement for " + entity.getId());
HttpToolResponse response = delete(getConfig(SLA_ENDPOINT) + "/agreements/" + agreement.getAgreementId(),
MediaType.APPLICATION_XML_UTF_8.toString(), getConfig(SLA_USERNAME), getConfig(SLA_PASSWORD));
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while removing the SLA Agreements. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
}
private void removeMonitoringRules() {
LOG.info("SeaCloudsInitializerPolicy is removing T4C Monitoring Rules for " + entity.getId());
for (MonitoringRule rule : monitoringRules.getMonitoringRules()) {
HttpToolResponse response = delete(getConfig(T4C_ENDPOINT) + "/v1/monitoring-rules/" + rule.getId(),
MediaType.JSON_UTF_8.toString(), getConfig(T4C_USERNAME), getConfig(T4C_PASSWORD));
if (!HttpTool.isStatusCodeHealthy(response.getResponseCode())) {
throw new RuntimeException("Something went wrong while removing the Monitoring Rules. " +
"Invalid response code, " + response.getResponseCode() + ":" + response.getContentAsString());
}
}
}
private void removeInfluxDbObservers() {
//TODO
LOG.info("SeaCloudsInitializerPolicy is removing InfluxDB Observers for " + entity.getId());
// Removing the rules also removes the observers, this is just a info msg.
}
private HttpToolResponse delete(String url, String mediaType, String username, String password) {
URI apiEndpoint = URI.create(url);
// the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
if (username == null || password == null) {
return HttpTool.httpDelete(HttpTool.httpClientBuilder().build(), apiEndpoint,
MutableMap.of(HttpHeaders.CONTENT_TYPE, mediaType, HttpHeaders.ACCEPT, mediaType));
} else {
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
// the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
return HttpTool.httpDelete(HttpTool.httpClientBuilder().uri(apiEndpoint).credentials(credentials).build(),
apiEndpoint,
MutableMap.of(HttpHeaders.CONTENT_TYPE, mediaType, HttpHeaders.ACCEPT, mediaType,
HttpHeaders.AUTHORIZATION, HttpTool.toBasicAuthorizationValue(credentials)));
}
}
private HttpToolResponse post(String url, String mediaType, String username, String password, byte[] payload) {
URI apiEndpoint = URI.create(url);
// the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
if (username == null || password == null) {
return HttpTool.httpPost(HttpTool.httpClientBuilder().build(), apiEndpoint,
MutableMap.of(HttpHeaders.CONTENT_TYPE, mediaType, HttpHeaders.ACCEPT, mediaType), payload);
} else {
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(username, password);
// the uri is required by the HttpClientBuilder in order to set the AuthScope of the credentials
return HttpTool.httpPost(HttpTool.httpClientBuilder().uri(apiEndpoint).credentials(credentials).build(),
apiEndpoint, MutableMap.of(HttpHeaders.CONTENT_TYPE, mediaType, HttpHeaders.ACCEPT, mediaType,
HttpHeaders.AUTHORIZATION, HttpTool.toBasicAuthorizationValue(credentials)), payload);
}
}
}