/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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 org.apache.brooklyn.rest.resources;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.core.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import org.apache.brooklyn.api.entity.EntitySpec;
import org.apache.brooklyn.api.location.Location;
import org.apache.brooklyn.api.location.LocationSpec;
import org.apache.brooklyn.api.location.NoMachinesAvailableException;
import org.apache.brooklyn.core.mgmt.internal.LocalUsageManager;
import org.apache.brooklyn.core.mgmt.internal.ManagementContextInternal;
import org.apache.brooklyn.core.test.entity.TestApplication;
import org.apache.brooklyn.entity.software.base.SoftwareProcessEntityTest;
import org.apache.brooklyn.location.localhost.LocalhostMachineProvisioningLocation;
import org.apache.brooklyn.location.ssh.SshMachineLocation;
import org.apache.brooklyn.rest.domain.ApplicationSpec;
import org.apache.brooklyn.rest.domain.Status;
import org.apache.brooklyn.rest.domain.TaskSummary;
import org.apache.brooklyn.rest.domain.UsageStatistic;
import org.apache.brooklyn.rest.domain.UsageStatistics;
import org.apache.brooklyn.rest.testing.BrooklynRestResourceTest;
import org.apache.brooklyn.rest.testing.mocks.RestMockSimpleEntity;
import org.apache.brooklyn.util.repeat.Repeater;
import org.apache.brooklyn.util.time.Time;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.GenericType;
public class UsageResourceTest extends BrooklynRestResourceTest {
@SuppressWarnings("unused")
private static final Logger LOG = LoggerFactory.getLogger(UsageResourceTest.class);
private static final long TIMEOUT_MS = 10*1000;
private Calendar testStartTime;
private final ApplicationSpec simpleSpec = ApplicationSpec.builder().name("simple-app").
entities(ImmutableSet.of(new org.apache.brooklyn.rest.domain.EntitySpec("simple-ent", RestMockSimpleEntity.class.getName()))).
locations(ImmutableSet.of("localhost")).
build();
@BeforeMethod(alwaysRun=true)
public void setUpMethod() {
((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.APPLICATION_USAGE_KEY);
((ManagementContextInternal)getManagementContext()).getStorage().remove(LocalUsageManager.LOCATION_USAGE_KEY);
testStartTime = new GregorianCalendar();
}
@Test
public void testListApplicationUsages() throws Exception {
// Create an app
Calendar preStart = new GregorianCalendar();
String appId = createApp(simpleSpec);
Calendar postStart = new GregorianCalendar();
// We will retrieve usage from one millisecond after start; this guarantees to not be
// told about both STARTING+RUNNING, which could otherwise happen if they are in the
// same milliscond.
Calendar afterPostStart = Time.newCalendarFromMillisSinceEpochUtc(postStart.getTime().getTime()+1);
// Check that app's usage is returned
ClientResponse response = client().resource("/v1/usage/applications").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
UsageStatistics usage = Iterables.getOnlyElement(usages);
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
// check app ignored if endCalendar before app started
response = client().resource("/v1/usage/applications?start="+0+"&end="+(preStart.getTime().getTime()-1)).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
assertTrue(Iterables.isEmpty(usages), "usages="+usages);
// Wait, so that definitely asking about things that have happened (not things in the future,
// or events that are happening this exact same millisecond)
waitForFuture(afterPostStart.getTime().getTime());
// Check app start + end date truncated, even if running for longer (i.e. only tell us about this time window).
// Note that start==end means we get a snapshot of the apps in use at that exact time.
//
// The start/end times in UsageStatistic are in String format, and are rounded down to the nearest second.
// The comparison does use the milliseconds passed in the REST call though.
// The rounding down result should be the same as roundDown(afterPostStart), because that is the time-window
// we asked for.
response = client().resource("/v1/usage/applications?start="+afterPostStart.getTime().getTime()+"&end="+afterPostStart.getTime().getTime()).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
usage = Iterables.getOnlyElement(usages);
assertAppUsage(usage, appId, ImmutableList.of(Status.RUNNING), roundDown(preStart), postStart);
assertAppUsageTimesTruncated(usage, roundDown(afterPostStart), roundDown(afterPostStart));
// Delete the app
Calendar preDelete = new GregorianCalendar();
deleteApp(appId);
Calendar postDelete = new GregorianCalendar();
// Deleted app still returned, if in time range
response = client().resource("/v1/usage/applications").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
usage = Iterables.getOnlyElement(usages);
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING, Status.DESTROYED), roundDown(preStart), postDelete);
assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
long afterPostDelete = postDelete.getTime().getTime()+1;
waitForFuture(afterPostDelete);
response = client().resource("/v1/usage/applications?start=" + afterPostDelete).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
assertTrue(Iterables.isEmpty(usages), "usages="+usages);
}
@Test
public void testGetApplicationUsagesForNonExistantApp() throws Exception {
ClientResponse response = client().resource("/v1/usage/applications/wrongid").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
}
@Test
public void testGetApplicationUsage() throws Exception {
// Create an app
Calendar preStart = new GregorianCalendar();
String appId = createApp(simpleSpec);
Calendar postStart = new GregorianCalendar();
// Normal request returns all
ClientResponse response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
UsageStatistics usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
// Time-constrained requests
response = client().resource("/v1/usage/applications/" + appId + "?start=1970-01-01T00:00:00-0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00+0100").get(ClientResponse.class);
assertTrue(response.getStatus() >= 400, "end defaults to NOW, so future start should fail, instead got code "+response.getStatus());
response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00%2B0100&end=9999-01-02T00:00:00%2B0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertTrue(usage.getStatistics().isEmpty());
response = client().resource("/v1/usage/applications/" + appId + "?end=9999-01-01T00:00:00+0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
response = client().resource("/v1/usage/applications/" + appId + "?start=9999-01-01T00:00:00+0100&end=9999-02-01T00:00:00+0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertTrue(usage.getStatistics().isEmpty());
response = client().resource("/v1/usage/applications/" + appId + "?start=1970-01-01T00:00:00-0100&end=9999-01-01T00:00:00+0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING), roundDown(preStart), postStart);
response = client().resource("/v1/usage/applications/" + appId + "?end=1970-01-01T00:00:00-0100").get(ClientResponse.class);
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertTrue(usage.getStatistics().isEmpty());
// Delete the app
Calendar preDelete = new GregorianCalendar();
deleteApp(appId);
Calendar postDelete = new GregorianCalendar();
// Deleted app still returned, if in time range
response = client().resource("/v1/usage/applications/" + appId).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertAppUsage(usage, appId, ImmutableList.of(Status.STARTING, Status.RUNNING, Status.DESTROYED), roundDown(preStart), postDelete);
assertAppUsage(ImmutableList.copyOf(usage.getStatistics()).subList(2, 3), appId, ImmutableList.of(Status.DESTROYED), roundDown(preDelete), postDelete);
// Deleted app not returned if terminated before time range begins
long afterPostDelete = postDelete.getTime().getTime()+1;
waitForFuture(afterPostDelete);
response = client().resource("/v1/usage/applications/" + appId +"?start=" + afterPostDelete).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertTrue(usage.getStatistics().isEmpty(), "usages="+usage);
}
@Test
public void testGetMachineUsagesForNonExistantMachine() throws Exception {
ClientResponse response = client().resource("/v1/usage/machines/wrongid").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
}
@Test
public void testGetMachineUsagesInitiallyEmpty() throws Exception {
// All machines: empty
ClientResponse response = client().resource("/v1/usage/machines").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
assertTrue(Iterables.isEmpty(usages));
// Specific machine that does not exist: get 404
response = client().resource("/v1/usage/machines/machineIdThatDoesNotExist").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.NOT_FOUND.getStatusCode());
}
@Test
public void testListAndGetMachineUsage() throws Exception {
Location location = getManagementContext().getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
TestApplication app = getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(org.apache.brooklyn.api.entity.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
Calendar preStart = new GregorianCalendar();
app.start(ImmutableList.of(location));
Calendar postStart = new GregorianCalendar();
Location machine = Iterables.getOnlyElement(entity.getLocations());
// All machines
ClientResponse response = client().resource("/v1/usage/machines").get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
UsageStatistics usage = Iterables.getOnlyElement(usages);
assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
// Specific machine
response = client().resource("/v1/usage/machines/"+machine.getId()).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usage = response.getEntity(new GenericType<UsageStatistics>() {});
assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
}
@Test
public void testListMachinesUsageForApp() throws Exception {
Location location = getManagementContext().getLocationManager().createLocation(LocationSpec.create(DynamicLocalhostMachineProvisioningLocation.class));
TestApplication app = getManagementContext().getEntityManager().createEntity(EntitySpec.create(TestApplication.class));
SoftwareProcessEntityTest.MyService entity = app.createAndManageChild(org.apache.brooklyn.api.entity.EntitySpec.create(SoftwareProcessEntityTest.MyService.class));
String appId = app.getId();
Calendar preStart = new GregorianCalendar();
app.start(ImmutableList.of(location));
Calendar postStart = new GregorianCalendar();
Location machine = Iterables.getOnlyElement(entity.getLocations());
// For running machine
ClientResponse response = client().resource("/v1/usage/machines?application="+appId).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
Iterable<UsageStatistics> usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
UsageStatistics usage = Iterables.getOnlyElement(usages);
assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED), roundDown(preStart), postStart);
// Stop the machine
Calendar preStop = new GregorianCalendar();
app.stop();
Calendar postStop = new GregorianCalendar();
// Deleted machine still returned, if in time range
response = client().resource("/v1/usage/machines?application=" + appId).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
usage = Iterables.getOnlyElement(usages);
assertMachineUsage(usage, app.getId(), machine.getId(), ImmutableList.of(Status.ACCEPTED, Status.DESTROYED), roundDown(preStart), postStop);
assertMachineUsage(ImmutableList.copyOf(usage.getStatistics()).subList(1,2), appId, machine.getId(), ImmutableList.of(Status.DESTROYED), roundDown(preStop), postStop);
// Terminated machines ignored if terminated since start-time
long futureTime = postStop.getTime().getTime()+1;
waitForFuture(futureTime);
response = client().resource("/v1/usage/applications?start=" + futureTime).get(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.OK.getStatusCode());
usages = response.getEntity(new GenericType<List<UsageStatistics>>() {});
assertTrue(Iterables.isEmpty(usages), "usages="+usages);
}
private String createApp(ApplicationSpec spec) {
ClientResponse response = clientDeploy(spec);
assertEquals(response.getStatus(), Response.Status.CREATED.getStatusCode());
TaskSummary createTask = response.getEntity(TaskSummary.class);
waitForTask(createTask.getId());
return createTask.getEntityId();
}
private void deleteApp(String appId) {
ClientResponse response = client().resource("/v1/applications/"+appId)
.delete(ClientResponse.class);
assertEquals(response.getStatus(), Response.Status.ACCEPTED.getStatusCode());
TaskSummary deletionTask = response.getEntity(TaskSummary.class);
waitForTask(deletionTask.getId());
}
private void assertCalendarOrders(Object context, Calendar... Calendars) {
if (Calendars.length <= 1) return;
long[] times = new long[Calendars.length];
for (int i = 0; i < times.length; i++) {
times[i] = millisSinceStart(Calendars[i]);
}
String err = "context="+context+"; Calendars="+Arrays.toString(Calendars) + "; CalendarsSanitized="+Arrays.toString(times);
Calendar Calendar = Calendars[0];
for (int i = 1; i < Calendars.length; i++) {
assertTrue(Calendar.getTime().getTime() <= Calendars[i].getTime().getTime(), err);
}
}
private void waitForTask(final String taskId) {
boolean success = Repeater.create()
.repeat(new Runnable() { public void run() {}})
.until(new Callable<Boolean>() {
@Override public Boolean call() {
ClientResponse response = client().resource("/v1/activities/"+taskId).get(ClientResponse.class);
if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) {
return true;
}
TaskSummary summary = response.getEntity(TaskSummary.class);
return summary != null && summary.getEndTimeUtc() != null;
}})
.every(10L, TimeUnit.MILLISECONDS)
.limitTimeTo(TIMEOUT_MS, TimeUnit.MILLISECONDS)
.run();
assertTrue(success, "task "+taskId+" not finished");
}
private long millisSinceStart(Calendar time) {
return time.getTime().getTime() - testStartTime.getTime().getTime();
}
private Calendar roundDown(Calendar calendar) {
long time = calendar.getTime().getTime();
long timeDown = ((long)(time / 1000)) * 1000;
return Time.newCalendarFromMillisSinceEpochUtc(timeDown);
}
@SuppressWarnings("unused")
private Calendar roundUp(Calendar calendar) {
long time = calendar.getTime().getTime();
long timeDown = ((long)(time / 1000)) * 1000;
long timeUp = (time == timeDown) ? time : timeDown + 1000;
return Time.newCalendarFromMillisSinceEpochUtc(timeUp);
}
private void assertMachineUsage(UsageStatistics usage, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
assertUsage(usage.getStatistics(), appId, machineId, states, pre, post, false);
}
private void assertMachineUsage(Iterable<UsageStatistic> usages, String appId, String machineId, List<Status> states, Calendar pre, Calendar post) throws Exception {
assertUsage(usages, appId, machineId, states, pre, post, false);
}
private void assertAppUsage(UsageStatistics usage, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
assertUsage(usage.getStatistics(), appId, appId, states, pre, post, false);
}
private void assertAppUsage(Iterable<UsageStatistic> usages, String appId, List<Status> states, Calendar pre, Calendar post) throws Exception {
assertUsage(usages, appId, appId, states, pre, post, false);
}
private void assertUsage(Iterable<UsageStatistic> usages, String appId, String id, List<Status> states, Calendar pre, Calendar post, boolean allowGaps) throws Exception {
String errMsg = "usages="+usages;
Calendar now = new GregorianCalendar();
Calendar lowerBound = pre;
Calendar strictStart = null;
assertEquals(Iterables.size(usages), states.size(), errMsg);
for (int i = 0; i < Iterables.size(usages); i++) {
UsageStatistic usage = Iterables.get(usages, i);
Calendar usageStart = Time.parseCalendar(usage.getStart());
Calendar usageEnd = Time.parseCalendar(usage.getEnd());
assertEquals(usage.getId(), id, errMsg);
assertEquals(usage.getApplicationId(), appId, errMsg);
assertEquals(usage.getStatus(), states.get(i), errMsg);
assertCalendarOrders(usages, lowerBound, usageStart, post);
assertCalendarOrders(usages, usageEnd, now);
if (strictStart != null) {
assertEquals(usageStart, strictStart, errMsg);
}
if (!allowGaps) {
strictStart = usageEnd;
}
lowerBound = usageEnd;
}
}
private void assertAppUsageTimesTruncated(UsageStatistics usages, Calendar strictStart, Calendar strictEnd) throws Exception {
String errMsg = "strictStart="+Time.makeDateString(strictStart)+"; strictEnd="+Time.makeDateString(strictEnd)+";usages="+usages;
Calendar usageStart = Time.parseCalendar(Iterables.getFirst(usages.getStatistics(), null).getStart());
Calendar usageEnd = Time.parseCalendar(Iterables.getLast(usages.getStatistics()).getStart());
// time zones might be different - so must convert to date
assertEquals(usageStart.getTime(), strictStart.getTime(), "usageStart="+Time.makeDateString(usageStart)+";"+errMsg);
assertEquals(usageEnd.getTime(), strictEnd.getTime(), errMsg);
}
public static class DynamicLocalhostMachineProvisioningLocation extends LocalhostMachineProvisioningLocation {
@Override
public SshMachineLocation obtain(Map<?, ?> flags) throws NoMachinesAvailableException {
return super.obtain(flags);
}
@Override
public void release(SshMachineLocation machine) {
super.release(machine);
super.machines.remove(machine);
getManagementContext().getLocationManager().unmanage(machine);
}
}
private void waitForFuture(long futureTime) throws InterruptedException {
long now;
while ((now = System.currentTimeMillis()) < futureTime) {
Thread.sleep(futureTime - now);
}
}
}