From 0195c9162d3410bb897f06b11f92adca34e5284e Mon Sep 17 00:00:00 2001 From: Ryan Brandt Date: Tue, 15 Dec 2015 15:20:00 -0700 Subject: [PATCH] Allow alarm definition and alarm lists to be sorted Change offsets to integers instead of ids Change-Id: I58712dfe2ae8e6c40f52eb4753fc39e6cbfdd3b9 --- docs/monasca-api-spec.md | 8 +- .../api/app/AlarmDefinitionService.java | 2 +- .../api/app/validation/Validation.java | 26 ++++++- .../api/domain/model/alarm/AlarmRepo.java | 3 +- .../alarmdefinition/AlarmDefinitionRepo.java | 4 +- .../hibernate/AlarmDefinitionSqlRepoImpl.java | 7 +- .../hibernate/AlarmSqlRepoImpl.java | 5 ++ .../mysql/AlarmDefinitionMySqlRepoImpl.java | 31 +++++--- .../persistence/mysql/AlarmMySqlRepoImpl.java | 44 ++++++----- .../api/resource/AlarmDefinitionResource.java | 22 ++++-- .../monasca/api/resource/AlarmResource.java | 26 +++++-- .../main/java/monasca/api/resource/Links.java | 38 +++++++++ .../AlarmDefinitionSqlRepositoryImplTest.java | 18 +++-- .../hibernate/AlarmSqlRepositoryImplTest.java | 39 ++++++---- ...larmDefinitionMySqlRepositoryImplTest.java | 26 ++++--- .../mysql/AlarmMySqlRepositoryImplTest.java | 46 ++++++----- .../resource/AlarmDefinitionResourceTest.java | 17 ++-- .../alarm_definitions_repository.py | 6 +- .../mysql/alarm_definitions_repository.py | 26 ++++--- .../repositories/mysql/alarms_repository.py | 24 ++++-- monasca_api/v2/common/exceptions.py | 2 +- monasca_api/v2/common/validation.py | 23 +++++- monasca_api/v2/reference/alarm_definitions.py | 25 +++++- monasca_api/v2/reference/alarming.py | 2 +- monasca_api/v2/reference/alarms.py | 15 ++++ .../tests/api/test_alarm_definitions.py | 78 ++++++++++++------- .../tests/api/test_alarms.py | 77 +++++++++++++++--- 27 files changed, 467 insertions(+), 173 deletions(-) diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index 80cbc0947..da910fefa 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -1786,8 +1786,10 @@ None. #### Query Parameters * name (string(255), optional) - Name of alarm to filter by. * dimensions (string, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1, ...` -* offset (string, optional) +* offset (integer, optional) * limit (integer, optional) +* sort_by (string, optional) - Comma separated list of fields to sort by, defaults to 'id', 'created_at'. Fields may be followed by 'asc' or 'desc' to set the direction, ex 'severity desc' +Allowed fields for sort_by are: 'id', 'name', 'severity', 'updated_at', 'created_at' #### Request Body None. @@ -2254,8 +2256,10 @@ None. * lifecycle_state (string(50), optional) - Lifecycle state to filter by. * link (string(512), optional) - Link to filter by. * state_updated_start_time (string, optional) - The start time in ISO 8601 combined date and time format in UTC. -* offset (string, optional) +* offset (integer, optional) * limit (integer, optional) +* sort_by (string, optional) - Comma separated list of fields to sort by, defaults to 'alarm_id'. Fields may be followed by 'asc' or 'desc' to set the direction, ex 'severity desc' +Allowed fields for sort_by are: 'alarm_id', 'alarm_definition_id', 'state', 'severity', 'lifecycle_state', 'link', 'state_updated_timestamp', 'updated_timestamp', 'created_timestamp' #### Request Body None. diff --git a/java/src/main/java/monasca/api/app/AlarmDefinitionService.java b/java/src/main/java/monasca/api/app/AlarmDefinitionService.java index 11569e56f..94b33bf9f 100644 --- a/java/src/main/java/monasca/api/app/AlarmDefinitionService.java +++ b/java/src/main/java/monasca/api/app/AlarmDefinitionService.java @@ -152,7 +152,7 @@ public class AlarmDefinitionService { // Have to get information about the Alarms before they are deleted. They will be deleted // by the database as a cascade delete from the Alarm Definition delete - final List alarms = alarmRepo.find(tenantId, alarmDefId, null, null, null, null, null, null, null, 1, false); + final List alarms = alarmRepo.find(tenantId, alarmDefId, null, null, null, null, null, null, null, null, 1, false); final Map> alarmSubExpressions = alarmRepo.findAlarmSubExpressionsForAlarmDefinition(alarmDefId); diff --git a/java/src/main/java/monasca/api/app/validation/Validation.java b/java/src/main/java/monasca/api/app/validation/Validation.java index d5c0a8160..355d78625 100644 --- a/java/src/main/java/monasca/api/app/validation/Validation.java +++ b/java/src/main/java/monasca/api/app/validation/Validation.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -13,9 +13,11 @@ */ package monasca.api.app.validation; +import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; import com.fasterxml.jackson.databind.JsonMappingException; @@ -220,4 +222,26 @@ public final class Validation { public static boolean isCrossProjectRequest(String crossTenantId, String tenantId) { return !Strings.isNullOrEmpty(crossTenantId) && !crossTenantId.equals(tenantId); } + + public static List parseAndValidateSortBy(String sortBy, final List allowed_sort_by) { + List sortByList = new ArrayList<>(); + if (sortBy != null && !sortBy.isEmpty()) { + List fieldList = Lists + .newArrayList(Splitter.on(',').omitEmptyStrings().trimResults().split(sortBy)); + for (String sortByField: fieldList) { + List field = Lists.newArrayList(Splitter.on(' ').omitEmptyStrings().trimResults().split(sortByField)); + if (field.size() > 2) { + throw Exceptions.unprocessableEntity(String.format("Invalid sort_by format %s", sortByField)); + } + if (!allowed_sort_by.contains(field.get(0))) { + throw Exceptions.unprocessableEntity(String.format("Sort_by field %s must be one of %s", field.get(0), allowed_sort_by)); + } + if (field.size() > 1 && !field.get(1).equals("desc") && !field.get(1).equals("asc")) { + throw Exceptions.unprocessableEntity(String.format("Sort_by value %s must be 'asc' or 'desc'", field.get(1))); + } + sortByList.add(Joiner.on(' ').join(field)); + } + } + return sortByList; + } } diff --git a/java/src/main/java/monasca/api/domain/model/alarm/AlarmRepo.java b/java/src/main/java/monasca/api/domain/model/alarm/AlarmRepo.java index 0e355117f..2a001e78f 100644 --- a/java/src/main/java/monasca/api/domain/model/alarm/AlarmRepo.java +++ b/java/src/main/java/monasca/api/domain/model/alarm/AlarmRepo.java @@ -32,7 +32,8 @@ public interface AlarmRepo { * Returns alarms for the given criteria. */ List find(String tenantId, String alarmDefId, String metricName, Map metricDimensions, AlarmState state, String lifecycleState, String link, DateTime stateUpdatedStart, String offset, int limit, boolean enforceLimit); + String> metricDimensions, AlarmState state, String lifecycleState, String link, DateTime stateUpdatedStart, + List sort_by, String offset, int limit, boolean enforceLimit); /** * @throws EntityNotFoundException if an alarm cannot be found for the {@code id} diff --git a/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinitionRepo.java b/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinitionRepo.java index 02783fca7..a4c631346 100644 --- a/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinitionRepo.java +++ b/java/src/main/java/monasca/api/domain/model/alarmdefinition/AlarmDefinitionRepo.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -48,7 +48,7 @@ public interface AlarmDefinitionRepo { * Returns alarms for the given criteria. */ List find(String tenantId, String name, Map dimensions, - String offset, int limit); + List sortBy, String offset, int limit); /** * @throws EntityNotFoundException if an alarm cannot be found for the {@code alarmDefId} diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java index 8be38b915..5e2269966 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepoImpl.java @@ -1,5 +1,6 @@ /* * Copyright 2015 FUJITSU LIMITED + * Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -50,6 +51,7 @@ import monasca.api.domain.exception.EntityNotFoundException; import monasca.api.domain.model.alarmdefinition.AlarmDefinition; import monasca.api.domain.model.alarmdefinition.AlarmDefinitionRepo; import monasca.api.infrastructure.persistence.SubAlarmDefinitionQueries; +import monasca.api.resource.exception.Exceptions; import monasca.common.hibernate.db.AlarmActionDb; import monasca.common.hibernate.db.AlarmDb; import monasca.common.hibernate.db.AlarmDefinitionDb; @@ -225,8 +227,11 @@ public class AlarmDefinitionSqlRepoImpl @Override @SuppressWarnings("unchecked") - public List find(String tenantId, String name, Map dimensions, String offset, int limit) { + public List find(String tenantId, String name, Map dimensions, List sortBy, String offset, int limit) { logger.trace(ORM_LOG_MARKER, "find(...) entering..."); + if (sortBy != null && !sortBy.isEmpty()) { + throw Exceptions.unprocessableEntity("Sort_by is not implemented for the hibernate database type"); + } Session session = null; List resultSet = Lists.newArrayList(); diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepoImpl.java index b1ec2d99f..21624046b 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepoImpl.java @@ -1,5 +1,6 @@ /* * Copyright 2015 FUJITSU LIMITED + * Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -150,8 +151,12 @@ public class AlarmSqlRepoImpl public List find(String tenantId, String alarmDefId, String metricName, Map metricDimensions, AlarmState state, String lifecycleState, String link, DateTime stateUpdatedStart, + List sortBy, String offset, int limit, boolean enforceLimit) { logger.trace(ORM_LOG_MARKER, "find(...) entering"); + if (sortBy != null && !sortBy.isEmpty()) { + throw Exceptions.unprocessableEntity("Sort_by is not implemented for the hibernate database type"); + } List alarms = this.findInternal(tenantId, alarmDefId, metricName, metricDimensions, state, lifecycleState, link, stateUpdatedStart, offset, (3 * limit / 2), enforceLimit); diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java index f5b9ebb07..8cf9dbc6a 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepoImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -139,7 +139,7 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { @SuppressWarnings("unchecked") @Override public List find(String tenantId, String name, - Map dimensions, String offset, int limit) { + Map dimensions, List sortBy, String offset, int limit) { try (Handle h = db.open()) { @@ -155,10 +155,9 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { + " FROM alarm_definition AS ad " + " LEFT OUTER JOIN sub_alarm_definition AS sad ON ad.id = sad.alarm_definition_id " + " LEFT OUTER JOIN sub_alarm_definition_dimension AS dim ON sad.id = dim.sub_alarm_definition_id %1$s " - + " WHERE ad.tenant_id = :tenantId AND ad.deleted_at IS NULL %2$s %3$s) AS t " + + " WHERE ad.tenant_id = :tenantId AND ad.deleted_at IS NULL %2$s) AS t " + "LEFT OUTER JOIN alarm_action AS aa ON t.id = aa.alarm_definition_id " - + "GROUP BY t.id ORDER BY t.id, t.created_at"; - + + "GROUP BY t.id %3$s %4$s %5$s"; StringBuilder sbWhere = new StringBuilder(); @@ -166,8 +165,14 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { sbWhere.append(" and ad.name = :name"); } - if (offset != null) { - sbWhere.append(" and ad.id > :offset"); + String orderByPart = ""; + if (sortBy != null && !sortBy.isEmpty()) { + orderByPart = " order by " + COMMA_JOINER.join(sortBy); + if (!orderByPart.contains("id")) { + orderByPart = orderByPart + ",id"; + } + } else { + orderByPart = " order by id "; } String limitPart = ""; @@ -175,8 +180,14 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { limitPart = " limit :limit"; } + String offsetPart = ""; + if (offset != null) { + offsetPart = " offset " + offset + ' '; + } + String sql = String.format(query, - SubAlarmDefinitionQueries.buildJoinClauseFor(dimensions), sbWhere, limitPart); + SubAlarmDefinitionQueries.buildJoinClauseFor(dimensions), sbWhere, orderByPart, + limitPart, offsetPart); Query q = h.createQuery(sql); @@ -186,10 +197,6 @@ public class AlarmDefinitionMySqlRepoImpl implements AlarmDefinitionRepo { q.bind("name", name); } - if (offset != null) { - q.bind("offset", offset); - } - if (limit > 0) { q.bind("limit", limit + 1); } diff --git a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java index 1c920038b..64b1fb908 100644 --- a/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java +++ b/java/src/main/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepoImpl.java @@ -50,16 +50,15 @@ import javax.inject.Named; */ public class AlarmMySqlRepoImpl implements AlarmRepo { + private static final Joiner COMMA_JOINER = Joiner.on(','); private static final Logger logger = LoggerFactory.getLogger(AlarmMySqlRepoImpl.class); private final DBI db; private final PersistUtils persistUtils; - private static final Joiner COMMA_JOINER = Joiner.on(','); - private static final String FIND_ALARM_BY_ID_SQL = "select ad.id as alarm_definition_id, ad.severity, ad.name as alarm_definition_name, " - + "a.id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + + "a.id as alarm_id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + "a.updated_at as updated_timestamp, a.created_at as created_timestamp, " + "md.name as metric_name, mdg.dimensions as metric_dimensions from alarm as a " + "inner join alarm_definition ad on ad.id = a.alarm_definition_id " @@ -72,18 +71,18 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { private static final String FIND_ALARMS_SQL = "select ad.id as alarm_definition_id, ad.severity, ad.name as alarm_definition_name, " - + "a.id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + + "a.id as alarm_id, a.state, a.lifecycle_state, a.link, a.state_updated_at as state_updated_timestamp, " + "a.updated_at as updated_timestamp, a.created_at as created_timestamp, " + "md.name as metric_name, group_concat(mdim.name, '=', mdim.value order by mdim.name) as metric_dimensions " + "from alarm as a " - + "inner join %s as alarm_id_list on alarm_id_list.id = a.id " + + "inner join %1$s as alarm_id_list on alarm_id_list.id = a.id " + "inner join alarm_definition ad on ad.id = a.alarm_definition_id " + "inner join alarm_metric as am on am.alarm_id = a.id " + "inner join metric_definition_dimensions as mdd on mdd.id = am.metric_definition_dimensions_id " + "inner join metric_definition as md on md.id = mdd.metric_definition_id " + "left outer join metric_dimension as mdim on mdim.dimension_set_id = mdd.metric_dimension_set_id " + "group by a.id, md.name, mdim.dimension_set_id " - + "order by a.id ASC"; + + "%2$s"; @Inject public AlarmMySqlRepoImpl(@Named("mysql") DBI db, PersistUtils persistUtils) { @@ -122,8 +121,8 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { @Override public List find(String tenantId, String alarmDefId, String metricName, Map metricDimensions, AlarmState state, - String lifecycleState, String link, DateTime stateUpdatedStart, String offset, - int limit, boolean enforceLimit) { + String lifecycleState, String link, DateTime stateUpdatedStart, + List sortBy, String offset, int limit, boolean enforceLimit) { StringBuilder sbWhere = @@ -180,19 +179,32 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { sbWhere.append(" and a.state_updated_at >= :stateUpdatedStart"); } - if (offset != null) { - sbWhere.append(" and a.id > :offset"); + StringBuilder sortByClause = new StringBuilder(); + if (sortBy != null && !sortBy.isEmpty()) { + sortByClause.append(" order by "); + sortByClause.append(COMMA_JOINER.join(sortBy)); + // if alarm_id is not in the list, add it + if (sortByClause.indexOf("alarm_id") == -1) { + sortByClause.append(",alarm_id ASC"); + } + sortByClause.append(' '); + } else { + sortByClause.append(" order by alarm_id ASC "); } - sbWhere.append(" order by a.id ASC "); - if (enforceLimit && limit > 0) { sbWhere.append(" limit :limit"); } + if (offset != null) { + sbWhere.append(" offset "); + sbWhere.append(offset); + sbWhere.append(' '); + } + sbWhere.append(")"); - String sql = String.format(FIND_ALARMS_SQL, sbWhere); + String sql = String.format(FIND_ALARMS_SQL, sbWhere, sortByClause); try (Handle h = db.open()) { @@ -222,10 +234,6 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { q.bind("stateUpdatedStart", stateUpdatedStart.toString()); } - if (offset != null) { - q.bind("offset", offset); - } - if (enforceLimit && limit > 0) { q.bind("limit", limit + 1); } @@ -267,7 +275,7 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { final List alarms = new LinkedList<>(); List alarmedMetrics = null; for (final Map row : rows) { - final String alarmId = (String) row.get("id"); + final String alarmId = (String) row.get("alarm_id"); if (!alarmId.equals(previousAlarmId)) { alarmedMetrics = new ArrayList<>(); alarm = diff --git a/java/src/main/java/monasca/api/resource/AlarmDefinitionResource.java b/java/src/main/java/monasca/api/resource/AlarmDefinitionResource.java index 11a619aef..ef6e41b0a 100644 --- a/java/src/main/java/monasca/api/resource/AlarmDefinitionResource.java +++ b/java/src/main/java/monasca/api/resource/AlarmDefinitionResource.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -20,6 +20,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import java.io.UnsupportedEncodingException; import java.net.URI; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -62,6 +63,8 @@ public class AlarmDefinitionResource { private final PersistUtils persistUtils; public final static String ALARM_DEFINITIONS = "alarm-definitions"; public final static String ALARM_DEFINITIONS_PATH = "/v2.0/" + ALARM_DEFINITIONS; + private final static List ALLOWED_SORT_BY = Arrays.asList("id", "name", "severity", + "updated_at", "created_at"); @Inject public AlarmDefinitionResource(AlarmDefinitionService service, @@ -93,20 +96,27 @@ public class AlarmDefinitionResource { public Object list(@Context UriInfo uriInfo, @HeaderParam("X-Tenant-Id") String tenantId, @QueryParam("name") String name, @QueryParam("dimensions") String dimensionsStr, + @QueryParam("sort_by") String sortByStr, @QueryParam("offset") String offset, @QueryParam("limit") String limit) throws UnsupportedEncodingException { Map dimensions = Strings.isNullOrEmpty(dimensionsStr) ? null : Validation .parseAndValidateDimensions(dimensionsStr); + List sortByList = Validation.parseAndValidateSortBy(sortByStr, ALLOWED_SORT_BY); + if (!Strings.isNullOrEmpty(offset)) { + Validation.parseAndValidateNumber(offset, "offset"); + } + final int paging_limit = this.persistUtils.getLimit(limit); final List resources = repo.find(tenantId, - name, - dimensions, - offset, - paging_limit + name, + dimensions, + sortByList, + offset, + paging_limit ); - return Links.paginate(paging_limit, Links.hydrate(resources, uriInfo), uriInfo); + return Links.paginateAlarming(paging_limit, Links.hydrate(resources, uriInfo), uriInfo); } @GET diff --git a/java/src/main/java/monasca/api/resource/AlarmResource.java b/java/src/main/java/monasca/api/resource/AlarmResource.java index 925fe7dcc..1555a0fab 100644 --- a/java/src/main/java/monasca/api/resource/AlarmResource.java +++ b/java/src/main/java/monasca/api/resource/AlarmResource.java @@ -71,6 +71,10 @@ public class AlarmResource { "lifecycle_state", "metric_name", "dimension_name", "dimension_value"); + private final static List ALLOWED_SORT_BY = Arrays.asList("alarm_id", "alarm_definition_id", "state", + "severity", "lifecycle_state", "link", + "state_updated_timestamp", "updated_timestamp", + "created_timestamp"); @Inject public AlarmResource(AlarmService service, AlarmRepo repo, @@ -153,11 +157,11 @@ public class AlarmResource { final int paging_limit = this.persistUtils.getLimit(limit); final List resources = stateHistoryRepo.find(tenantId, - dimensions, - startTime, - endTime, - offset, - paging_limit + dimensions, + startTime, + endTime, + offset, + paging_limit ); return Links.paginate(paging_limit, resources, uriInfo); } @@ -173,6 +177,7 @@ public class AlarmResource { @QueryParam("lifecycle_state") String lifecycleState, @QueryParam("link") String link, @QueryParam("state_updated_start_time") String stateUpdatedStartStr, + @QueryParam("sort_by") String sortBy, @QueryParam("offset") String offset, @QueryParam("limit") String limit) throws Exception { @@ -185,9 +190,14 @@ public class AlarmResource { Validation.parseAndValidateDate(stateUpdatedStartStr, "state_updated_start_time", false); + List sortByList = Validation.parseAndValidateSortBy(sortBy, ALLOWED_SORT_BY); + if (!Strings.isNullOrEmpty(offset)) { + Validation.parseAndValidateNumber(offset, "offset"); + } + final int paging_limit = this.persistUtils.getLimit(limit); final List alarms = repo.find(tenantId, alarmDefId, metricName, metricDimensions, state, - lifecycleState, link, stateUpdatedStart, + lifecycleState, link, stateUpdatedStart, sortByList, offset, paging_limit, true); for (final Alarm alarm : alarms) { Links.hydrate( @@ -196,9 +206,11 @@ public class AlarmResource { AlarmDefinitionResource.ALARM_DEFINITIONS_PATH ); } - return Links.paginate(paging_limit, Links.hydrate(alarms, uriInfo), uriInfo); + return Links.paginateAlarming(paging_limit, Links.hydrate(alarms, uriInfo), uriInfo); } + + @PATCH @Timed @Path("/{alarm_id}") diff --git a/java/src/main/java/monasca/api/resource/Links.java b/java/src/main/java/monasca/api/resource/Links.java index d92f10bad..febdee42e 100644 --- a/java/src/main/java/monasca/api/resource/Links.java +++ b/java/src/main/java/monasca/api/resource/Links.java @@ -197,6 +197,44 @@ public final class Links { } + public static Object paginateAlarming(int limit, List elements, UriInfo uriInfo) + throws UnsupportedEncodingException { + + // Check for paging turned off. Happens if maxQueryLimit is not set or is set to zero. + if (limit == 0) { + Paged paged = new Paged(); + paged.elements = elements != null ? elements : new ArrayList<>(); + return paged; + } + + Paged paged = new Paged(); + + paged.links.add(getSelfLink(uriInfo)); + + if (elements != null) { + + if (elements.size() > limit) { + + String offset = String.valueOf(limit); + + paged.links.add(getNextLink(offset, uriInfo)); + + // Truncate the list. Normally this will just truncate one extra element. + elements = elements.subList(0, limit); + } + + paged.elements = elements; + + } else { + + paged.elements = new ArrayList<>(); + + } + + return paged; + + } + public static Object paginateMeasurements(int limit, List elements, UriInfo uriInfo) throws UnsupportedEncodingException { diff --git a/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepositoryImplTest.java b/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepositoryImplTest.java index 51c044542..7983c7a3d 100644 --- a/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepositoryImplTest.java +++ b/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmDefinitionSqlRepositoryImplTest.java @@ -1,5 +1,6 @@ /* * Copyright 2015 FUJITSU LIMITED + * Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -49,6 +50,8 @@ import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import javax.ws.rs.WebApplicationException; + @Test(groups = "orm") public class AlarmDefinitionSqlRepositoryImplTest { @@ -340,33 +343,38 @@ public class AlarmDefinitionSqlRepositoryImplTest { fail(); } catch (EntityNotFoundException expected) { } - assertEquals(Arrays.asList(alarmDef_234), repo.find("bob", null, null, null, 1)); + assertEquals(Arrays.asList(alarmDef_234), repo.find("bob", null, null, null, null, 1)); } public void shouldFindByDimension() { final Map dimensions = new HashMap<>(); dimensions.put("image_id", "888"); - List result = repo.find("bob", null, dimensions, null, 1); + List result = repo.find("bob", null, dimensions, null, null, 1); assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), result); dimensions.clear(); dimensions.put("device", "1"); - assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", null, dimensions, null, 1)); + assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", null, dimensions, null, null, 1)); dimensions.clear(); dimensions.put("Not real", "AA"); - assertEquals(0, repo.find("bob", null, dimensions, null, 1).size()); + assertEquals(0, repo.find("bob", null, dimensions, null, null, 1).size()); } public void shouldFindByName() { final Map dimensions = new HashMap<>(); dimensions.put("image_id", "888"); - List result = repo.find("bob", "90% CPU", dimensions, null, 1); + List result = repo.find("bob", "90% CPU", dimensions, null, null, 1); assertEquals(Arrays.asList(alarmDef_123), result); } + + @Test(groups = "orm", expectedExceptions = WebApplicationException.class) + public void shouldFindThrowException() { + repo.find("bob", null, null, Arrays.asList("severity", "state"), null, 1); + } } diff --git a/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepositoryImplTest.java b/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepositoryImplTest.java index 5bbd87392..f7d9e801f 100644 --- a/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepositoryImplTest.java +++ b/java/src/test/java/monasca/api/infrastructure/persistence/hibernate/AlarmSqlRepositoryImplTest.java @@ -1,5 +1,6 @@ /* * Copyright 2015 FUJITSU LIMITED + * Copyright 2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -24,6 +25,7 @@ import java.util.List; import java.util.Map; import javax.annotation.Nullable; +import javax.ws.rs.WebApplicationException; import com.google.common.base.Optional; import com.google.common.base.Predicate; @@ -346,55 +348,60 @@ public class AlarmSqlRepositoryImplTest { @Test(groups = "orm") public void shouldFind() { - checkList(repo.find("Not a tenant id", null, null, null, null, null, null, null, null, 1, false)); + checkList(repo.find("Not a tenant id", null, null, null, null, null, null, null, null, null, 1, false)); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); - checkList(repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, null, null, null, null, null, 1, false), compoundAlarm); + checkList(repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, null, null, null, null, null, 1, false), compoundAlarm); + checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); + checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", ImmutableMap.builder().put("flavor_id", "222").build(), null, null, null, - null, null, 1, false), alarm1, alarm3); + null, null, null, 1, false), alarm1, alarm3); checkList( repo.find(TENANT_ID, null, "cpu.idle_perc", ImmutableMap.builder().put("service", "monitoring").put("hostname", "roland") - .build(), null, null, null, null, null, 1, false), compoundAlarm); + .build(), null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, null, null, AlarmState.UNDETERMINED, null, null, null, null, 1, false), alarm2, compoundAlarm); + checkList(repo.find(TENANT_ID, null, null, null, AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), alarm2, compoundAlarm); checkList( repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", ImmutableMap.builder() - .put("service", "monitoring").build(), null, null, null, null, null, 1, false), alarm1, alarm2); + .put("service", "monitoring").build(), null, null, null, null, null, null, 1, false), alarm1, alarm2); - checkList(repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", null, null, null, null, null, null, 1, false), alarm1, + checkList(repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3); checkList( - repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, AlarmState.UNDETERMINED, null, null, null, null, 1, false), + repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, AlarmState.UNDETERMINED, null, null, null, null, 1, false), compoundAlarm); + checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), compoundAlarm); checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", ImmutableMap.builder().put("service", "monitoring").build(), - AlarmState.UNDETERMINED, null, null, null, null, 1, false), alarm2, compoundAlarm); + AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), alarm2, compoundAlarm); checkList( repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", ImmutableMap.builder() - .put("service", "monitoring").build(), AlarmState.UNDETERMINED, null, null, null, null, 1, false), alarm2); + .put("service", "monitoring").build(), AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), alarm2); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, DateTime.now(UTC_TIMEZONE), null, 0, false)); + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, DateTime.now(UTC_TIMEZONE), null, null, 0, false)); //checkList(repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-15T00:00:00Z"), null, 0, false), // compoundAlarm); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-14T00:00:00Z"), null, 1, false), + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-14T00:00:00Z"), null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); } + @Test(groups = "orm", expectedExceptions = WebApplicationException.class) + public void shouldFindThrowException() { + repo.find(TENANT_ID, null, null, null, null, null, null, null, Arrays.asList("severity", "state"), null, 1, true); + } + @Test(groups = "orm") public void shouldFindById() { diff --git a/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepositoryImplTest.java b/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepositoryImplTest.java index e99642dad..b636e0d52 100644 --- a/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepositoryImplTest.java +++ b/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmDefinitionMySqlRepositoryImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -245,36 +245,42 @@ public class AlarmDefinitionMySqlRepositoryImplTest { } public void shouldFind() { - assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), repo.find("bob", null, null, null, 1)); + assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), repo.find("bob", null, null, null, null, 1)); // Make sure it still finds AlarmDefinitions with no notifications handle.execute("delete from alarm_action"); alarmDef_123.setAlarmActions(new ArrayList(0)); alarmDef_234.setAlarmActions(new ArrayList(0)); - assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), repo.find("bob", null, null, null, 1)); + assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), repo.find("bob", null, null, null, null, 1)); - assertEquals(0, repo.find("bill", null, null, null, 1).size()); + assertEquals(0, repo.find("bill", null, null, null, null, 1).size()); + + assertEquals(Arrays.asList(alarmDef_234, alarmDef_123), + repo.find("bob", null, null, Arrays.asList("name"), null, 1)); + + assertEquals(Arrays.asList(alarmDef_234, alarmDef_123), + repo.find("bob", null, null, Arrays.asList("id desc"), null, 1)); } public void shouldFindByDimension() { final Map dimensions = new HashMap<>(); dimensions.put("image_id", "888"); assertEquals(Arrays.asList(alarmDef_123, alarmDef_234), - repo.find("bob", null, dimensions, null, 1)); + repo.find("bob", null, dimensions, null, null, 1)); dimensions.clear(); dimensions.put("device", "1"); - assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", null, dimensions, null, 1)); + assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", null, dimensions, null, null, 1)); dimensions.clear(); dimensions.put("Not real", "AA"); - assertEquals(0, repo.find("bob", null, dimensions, null, 1).size()); + assertEquals(0, repo.find("bob", null, dimensions, null, null, 1).size()); } public void shouldFindByName() { - assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", "90% CPU", null, null, 1)); + assertEquals(Arrays.asList(alarmDef_123), repo.find("bob", "90% CPU", null, null, null, 1)); - assertEquals(0, repo.find("bob", "Does not exist", null, null, 1).size()); + assertEquals(0, repo.find("bob", "Does not exist", null, null, null, 1).size()); } public void shouldDeleteById() { @@ -285,6 +291,6 @@ public class AlarmDefinitionMySqlRepositoryImplTest { fail(); } catch (EntityNotFoundException expected) { } - assertEquals(Arrays.asList(alarmDef_234), repo.find("bob", null, null, null, 1)); + assertEquals(Arrays.asList(alarmDef_234), repo.find("bob", null, null, null, null, 1)); } } diff --git a/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepositoryImplTest.java b/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepositoryImplTest.java index 4b5abd493..4db788286 100644 --- a/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepositoryImplTest.java +++ b/java/src/test/java/monasca/api/infrastructure/persistence/mysql/AlarmMySqlRepositoryImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -166,7 +166,7 @@ public class AlarmMySqlRepositoryImplTest { handle .execute( "insert into alarm_definition (id, tenant_id, name, severity, expression, match_by, actions_enabled, created_at, updated_at, deleted_at) " - + "values ('234', 'bob', '50% CPU', 'LOW', 'avg(cpu.sys_mem{service=monitoring}) > 20 and avg(cpu.idle_perc{service=monitoring}) < 10', 'hostname,region', 1, NOW(), NOW(), NULL)"); + + "values ('234', 'bob', '50% CPU', 'HIGH', 'avg(cpu.sys_mem{service=monitoring}) > 20 and avg(cpu.idle_perc{service=monitoring}) < 10', 'hostname,region', 1, NOW(), NOW(), NULL)"); handle .execute("insert into alarm (id, alarm_definition_id, state, created_at, updated_at, state_updated_at) values ('234111', '234', 'UNDETERMINED', '"+timestamp4.toString().replace('Z', ' ')+"', '"+timestamp4.toString().replace('Z', ' ')+"', '"+timestamp4.toString().replace('Z', ' ')+"')"); handle @@ -202,7 +202,7 @@ public class AlarmMySqlRepositoryImplTest { .execute("insert into metric_dimension (dimension_set_id, name, value) values (22, 'extra', 'vivi')"); compoundAlarm = - new Alarm("234111", "234", "50% CPU", "LOW", buildAlarmMetrics( + new Alarm("234111", "234", "50% CPU", "HIGH", buildAlarmMetrics( buildMetricDefinition("cpu.sys_mem", "service", "monitoring", "hostname", "roland", "region", "colorado"), buildMetricDefinition("cpu.idle_perc", "service", "monitoring", "hostname", "roland", @@ -271,58 +271,64 @@ public class AlarmMySqlRepositoryImplTest { @Test(groups = "database") public void shouldFind() { - checkList(repo.find("Not a tenant id", null, null, null, null, null, null, null, null, 1, false)); + checkList(repo.find("Not a tenant id", null, null, null, null, null, null, null, null, null, 1, false)); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); - checkList(repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, null, null, null, null, null, 1, false), compoundAlarm); + checkList(repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, null, null, null, null, null, 1, false), compoundAlarm); + checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); + checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); checkList( repo.find(TENANT_ID, null, "cpu.idle_perc", - ImmutableMap.builder().put("flavor_id", "222").build(), null, null, null, null, null, 1, false), alarm1, + ImmutableMap.builder().put("flavor_id", "222").build(), null, null, null, null, null, null, 1, false), alarm1, alarm3); checkList( repo.find(TENANT_ID, null, "cpu.idle_perc", ImmutableMap.builder().put("service", "monitoring") - .put("hostname", "roland").build(), null, null, null, null, null, 1, false), compoundAlarm); + .put("hostname", "roland").build(), null, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, null, null, AlarmState.UNDETERMINED, null, null, null, null, 1, false), + checkList(repo.find(TENANT_ID, null, null, null, AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), alarm2, compoundAlarm); checkList( repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", ImmutableMap - .builder().put("service", "monitoring").build(), null, null, null, null, null, 1, false), alarm1, alarm2); + .builder().put("service", "monitoring").build(), null, null, null, null, null, null, 1, false), alarm1, alarm2); checkList( - repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", null, null, null, null, null, null, 1, false), + repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", null, null, null, null, null, null, null, 1, false), alarm1, alarm2, alarm3); checkList(repo.find(TENANT_ID, compoundAlarm.getAlarmDefinition().getId(), null, null, - AlarmState.UNDETERMINED, null, null, null, null, 1, false), compoundAlarm); + AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), compoundAlarm); - checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, AlarmState.UNDETERMINED, null, null, null, null, 1, false), + checkList(repo.find(TENANT_ID, null, "cpu.sys_mem", null, AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), compoundAlarm); checkList(repo.find(TENANT_ID, null, "cpu.idle_perc", ImmutableMap.builder() - .put("service", "monitoring").build(), AlarmState.UNDETERMINED, null, null, null, null, 1,false), alarm2, compoundAlarm); + .put("service", "monitoring").build(), AlarmState.UNDETERMINED, null, null, null, null, null, 1,false), alarm2, compoundAlarm); checkList(repo.find(TENANT_ID, alarm1.getAlarmDefinition().getId(), "cpu.idle_perc", ImmutableMap.builder().put("service", "monitoring").build(), - AlarmState.UNDETERMINED, null, null, null, null, 1, false), alarm2); + AlarmState.UNDETERMINED, null, null, null, null, null, 1, false), alarm2); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, DateTime.now(DateTimeZone.forID("UTC")), null, 0, false)); + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, DateTime.now(DateTimeZone.forID("UTC")), null, null, 0, false)); - checkList(repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-15T00:00:00Z"), null, 0, false), compoundAlarm); + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-15T00:00:00Z"), null, null, 0, false), compoundAlarm); checkList( - repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-14T00:00:00Z"), null, + repo.find(TENANT_ID, null, null, null, null, null, null, ISO_8601_FORMATTER.parseDateTime("2015-03-14T00:00:00Z"), null, null, 1, false), alarm1, alarm2, alarm3, compoundAlarm); + + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, Arrays.asList("state","severity"), null, 1, false), + alarm1, alarm2, compoundAlarm, alarm3); + + checkList(repo.find(TENANT_ID, null, null, null, null, null, null, null, Arrays.asList("state desc","severity"), null, 1, false), + compoundAlarm, alarm3, alarm2, alarm1); } private DateTime getAlarmStateUpdatedDate(final String alarmId) { diff --git a/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java b/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java index f2b4e268d..3b1a1e490 100644 --- a/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java +++ b/java/src/test/java/monasca/api/resource/AlarmDefinitionResourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014 Hewlett-Packard Development Company, L.P. + * Copyright (c) 2014,2016 Hewlett Packard Enterprise Development Company, L.P. * * 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 @@ -16,6 +16,7 @@ package monasca.api.resource; import static org.mockito.Matchers.any; import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyList; import static org.mockito.Matchers.anyMap; import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.anyString; @@ -85,7 +86,8 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { repo = mock(AlarmDefinitionRepo.class); when(repo.findById(eq("abc"), eq("123"))).thenReturn(alarm); - when(repo.find(anyString(), anyString(), (Map) anyMap(), anyString(), anyInt())).thenReturn( + when(repo.find(anyString(), anyString(), (Map) anyMap(), (List) anyList(), + anyString(), anyInt())).thenReturn( Arrays.asList(alarmItem)); addResources(new AlarmDefinitionResource(service, repo, new PersistUtils())); @@ -275,7 +277,8 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { assertEquals(alarms, Arrays.asList(alarmItem)); - verify(repo).find(eq("abc"), anyString(), (Map) anyMap(), anyString(), anyInt()); + verify(repo).find(eq("abc"), anyString(), (Map) anyMap(), (List) anyList(), + anyString(), anyInt()); } @SuppressWarnings("unchecked") @@ -306,8 +309,8 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { List alarms = Arrays.asList(ad); assertEquals(alarms, Arrays.asList(alarmItem)); - verify(repo).find(eq("abc"), eq("foo bar baz"), (Map) anyMap(), anyString(), - anyInt()); + verify(repo).find(eq("abc"), eq("foo bar baz"), (Map) anyMap(), (List) anyList(), + anyString(), anyInt()); } public void shouldGet() { @@ -352,13 +355,13 @@ public class AlarmDefinitionResourceTest extends AbstractMonApiResourceTest { public void should500OnInternalException() { doThrow(new RuntimeException("")).when(repo).find(anyString(), anyString(), - (Map) anyObject(), anyString(), anyInt()); + (Map) anyObject(), (List) anyList(), anyString(), anyInt()); try { client().resource("/v2.0/alarm-definitions").header("X-Tenant-Id", "abc").get(List.class); fail(); } catch (Exception e) { - assertTrue(e.getMessage().contains("500")); + assertTrue(e.getMessage().contains("500"), e.getMessage()); } } diff --git a/monasca_api/common/repositories/alarm_definitions_repository.py b/monasca_api/common/repositories/alarm_definitions_repository.py index 75a4df87d..fd35a257d 100644 --- a/monasca_api/common/repositories/alarm_definitions_repository.py +++ b/monasca_api/common/repositories/alarm_definitions_repository.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014,2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -50,8 +50,8 @@ class AlarmDefinitionsRepository(object): pass @abc.abstractmethod - def get_alarm_definitions(self, tenant_id, name, dimensions, offset, - limit): + def get_alarm_definitions(self, tenant_id, name, dimensions, sort_by, + offset, limit): pass @abc.abstractmethod diff --git a/monasca_api/common/repositories/mysql/alarm_definitions_repository.py b/monasca_api/common/repositories/mysql/alarm_definitions_repository.py index e96981e79..bccd304ca 100644 --- a/monasca_api/common/repositories/mysql/alarm_definitions_repository.py +++ b/monasca_api/common/repositories/mysql/alarm_definitions_repository.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014,2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -76,8 +76,8 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, raise exceptions.DoesNotExistException @mysql_repository.mysql_try_catch_block - def get_alarm_definitions(self, tenant_id, name, dimensions, offset, - limit): + def get_alarm_definitions(self, tenant_id, name, dimensions, + sort_by, offset, limit): parms = [tenant_id] @@ -89,14 +89,20 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, where_clause += " and ad.name = %s " parms.append(name.encode('utf8')) - order_by_clause = " order by ad.id, ad.created_at " + if sort_by is not None: + order_by_clause = " order by " + ','.join(sort_by) + if 'id' not in sort_by: + order_by_clause += ",ad.id " + else: + order_by_clause += " " + else: + order_by_clause = " order by ad.id " + + limit_offset_clause = " limit %s " + parms.append(limit + 1) if offset: - where_clause += " and ad.id > %s " - parms.append(offset.encode('utf8')) - - limit_clause = " limit %s " - parms.append(limit + 1) + limit_offset_clause += ' offset {}'.format(offset) if dimensions: inner_join = """ inner join sub_alarm_definition as sad @@ -119,7 +125,7 @@ class AlarmDefinitionsRepository(mysql_repository.MySQLRepository, select_clause += inner_join parms = inner_join_parms + parms - query = select_clause + where_clause + order_by_clause + limit_clause + query = select_clause + where_clause + order_by_clause + limit_offset_clause return self._execute_query(query, parms) diff --git a/monasca_api/common/repositories/mysql/alarms_repository.py b/monasca_api/common/repositories/mysql/alarms_repository.py index 20d4cdc1c..f3b1ebcb9 100644 --- a/monasca_api/common/repositories/mysql/alarms_repository.py +++ b/monasca_api/common/repositories/mysql/alarms_repository.py @@ -214,14 +214,8 @@ class AlarmsRepository(mysql_repository.MySQLRepository, select_clause = AlarmsRepository.base_query - order_by_clause = " order by a.id " - where_clause = " where ad.tenant_id = %s " - if offset: - where_clause += " and a.id > %s" - parms.append(offset.encode('utf8')) - if 'alarm_definition_id' in query_parms: parms.append(query_parms['alarm_definition_id']) where_clause += " and ad.id = %s " @@ -287,13 +281,29 @@ class AlarmsRepository(mysql_repository.MySQLRepository, parms += sub_select_parms where_clause += sub_select_clause + if 'sort_by' in query_parms: + order_by_clause = " order by " + ','.join(query_parms['sort_by']) + if 'alarm_id' not in query_parms['sort_by']: + order_by_clause += ",alarm_id " + else: + order_by_clause += " " + else: + order_by_clause = " order by a.id " + + if offset: + offset_clause = " offset {}".format(offset) + else: + offset_clause = "" + if limit: limit_clause = " limit %s " parms.append(limit + 1) else: limit_clause = "" - query = select_clause + where_clause + order_by_clause + limit_clause + query = select_clause + where_clause + order_by_clause + limit_clause + offset_clause + + LOG.debug("Query: {}".format(query)) return self._execute_query(query, parms) diff --git a/monasca_api/v2/common/exceptions.py b/monasca_api/v2/common/exceptions.py index de939017b..47d908f82 100644 --- a/monasca_api/v2/common/exceptions.py +++ b/monasca_api/v2/common/exceptions.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP # # 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 diff --git a/monasca_api/v2/common/validation.py b/monasca_api/v2/common/validation.py index 857e6d148..57226d661 100644 --- a/monasca_api/v2/common/validation.py +++ b/monasca_api/v2/common/validation.py @@ -1,4 +1,4 @@ -# Copyright 2015 Hewlett-Packard +# Copyright 2015,2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -12,9 +12,11 @@ # License for the specific language governing permissions and limitations # under the License. +from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError + import re -invalid_chars = "<>={}(),\"\\\\;&" +invalid_chars = "<>={}(),'\"\\\\;&" restricted_chars = re.compile('[' + invalid_chars + ']') @@ -34,3 +36,20 @@ def dimension_value(value): assert isinstance(value, (str, unicode)), "Dimension value must be a string" assert len(value) <= 255, "Dimension value must be 255 characters or less" assert not restricted_chars.search(value), "Invalid characters in dimension value " + value + + +def validate_sort_by(sort_by_list, allowed_sort_by): + for sort_by_field in sort_by_list: + sort_by_values = sort_by_field.split() + if len(sort_by_values) > 2: + raise HTTPUnprocessableEntityError("Unprocessable Entity", + "Invalid sort_by {}".format(sort_by_field)) + if sort_by_values[0] not in allowed_sort_by: + raise HTTPUnprocessableEntityError("Unprocessable Entity", + "sort_by field {} must be one of [{}]".format( + sort_by_values[0], + ','.join(list(allowed_sort_by)))) + if len(sort_by_values) > 1 and sort_by_values[1] not in ['asc', 'desc']: + raise HTTPUnprocessableEntityError("Unprocessable Entity", + "sort_by value {} must be 'asc' or 'desc'".format( + sort_by_values[1])) diff --git a/monasca_api/v2/reference/alarm_definitions.py b/monasca_api/v2/reference/alarm_definitions.py index f66f54d46..4e0a5049d 100644 --- a/monasca_api/v2/reference/alarm_definitions.py +++ b/monasca_api/v2/reference/alarm_definitions.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014,2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 @@ -85,11 +85,27 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, tenant_id = helpers.get_tenant_id(req) name = helpers.get_query_name(req) dimensions = helpers.get_query_dimensions(req) + sort_by = helpers.get_query_param(req, 'sort_by', default_val=None) + if sort_by is not None: + if isinstance(sort_by, basestring): + sort_by = [sort_by] + + allowed_sort_by = {'id', 'name', 'severity', + 'updated_at', 'created_at'} + + validation.validate_sort_by(sort_by, allowed_sort_by) + offset = helpers.get_query_param(req, 'offset') + if offset is not None and not isinstance(offset, int): + try: + offset = int(offset) + except Exception: + raise HTTPUnprocessableEntityError('Unprocessable Entity', + 'Offset value {} must be an integer'.format(offset)) limit = helpers.get_limit(req) result = self._alarm_definition_list(tenant_id, name, dimensions, - req.uri, offset, limit) + req.uri, sort_by, offset, limit) res.body = helpers.dumpit_utf8(result) res.status = falcon.HTTP_200 @@ -203,6 +219,7 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, definitions = self._alarm_definitions_repo.get_alarm_definitions(tenant_id=tenant_id, name=name, dimensions=None, + sort_by=None, offset=None, limit=0) if definitions: @@ -280,12 +297,12 @@ class AlarmDefinitions(alarm_definitions_api_v2.AlarmDefinitionsV2API, alarm_metric_rows, sub_alarm_rows) @resource.resource_try_catch_block - def _alarm_definition_list(self, tenant_id, name, dimensions, req_uri, + def _alarm_definition_list(self, tenant_id, name, dimensions, req_uri, sort_by, offset, limit): alarm_definition_rows = ( self._alarm_definitions_repo.get_alarm_definitions(tenant_id, name, - dimensions, + dimensions, sort_by, offset, limit)) result = [] diff --git a/monasca_api/v2/reference/alarming.py b/monasca_api/v2/reference/alarming.py index efda51cab..ba89f6423 100644 --- a/monasca_api/v2/reference/alarming.py +++ b/monasca_api/v2/reference/alarming.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014,2016 Hewlett Packard Enterprise Development Company, L.P. # # 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 diff --git a/monasca_api/v2/reference/alarms.py b/monasca_api/v2/reference/alarms.py index e086bc90e..1fa874f4a 100644 --- a/monasca_api/v2/reference/alarms.py +++ b/monasca_api/v2/reference/alarms.py @@ -23,6 +23,7 @@ from monasca_api.api import alarms_api_v2 from monasca_api.common.repositories import exceptions from monasca_api.v2.common.exceptions import HTTPUnprocessableEntityError from monasca_api.v2.common.schemas import alarm_update_schema as schema_alarm +from monasca_api.v2.common import validation from monasca_api.v2.reference import alarming from monasca_api.v2.reference import helpers from monasca_api.v2.reference import resource @@ -116,12 +117,26 @@ class Alarms(alarms_api_v2.AlarmsV2API, if alarm_id is None: query_parms = falcon.uri.parse_query_string(req.query_string) + if 'sort_by' in query_parms: + if isinstance(query_parms['sort_by'], basestring): + query_parms['sort_by'] = [query_parms['sort_by']] + + allowed_sort_by = {'alarm_id', 'alarm_definition_id', 'state', 'severity', 'lifecycle_state', 'link', + 'state_updated_timestamp', 'updated_timestamp', 'created_timestamp'} + validation.validate_sort_by(query_parms['sort_by'], allowed_sort_by) # ensure metric_dimensions is a list if 'metric_dimensions' in query_parms and isinstance(query_parms['metric_dimensions'], str): query_parms['metric_dimensions'] = query_parms['metric_dimensions'].split(',') offset = helpers.get_query_param(req, 'offset') + if offset is not None and not isinstance(offset, int): + try: + offset = int(offset) + except Exception as ex: + LOG.exception(ex) + raise HTTPUnprocessableEntityError("Unprocessable Entity", + "Offset value {} must be an integer".format(offset)) limit = helpers.get_limit(req) diff --git a/monasca_tempest_tests/tests/api/test_alarm_definitions.py b/monasca_tempest_tests/tests/api/test_alarm_definitions.py index a9f926a70..f9d589690 100644 --- a/monasca_tempest_tests/tests/api/test_alarm_definitions.py +++ b/monasca_tempest_tests/tests/api/test_alarm_definitions.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP # # 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 @@ -326,6 +326,48 @@ class TestAlarmDefinitions(base.BaseMonascaTest): links = response_body['links'] self._verify_list_alarm_definitions_links(links) + @test.attr(type='gate') + def test_list_alarm_definitions_sort_by(self): + alarm_definitions = [] + alarm_definitions.append(helpers.create_alarm_definition( + name='alarm def sort by 01', + expression='test_metric_01 > 1', + severity='HIGH' + )) + alarm_definitions.append(helpers.create_alarm_definition( + name='alarm def sort by 04', + expression='test_metric_04 > 1', + severity='LOW' + )) + alarm_definitions.append(helpers.create_alarm_definition( + name='alarm def sort by 02', + expression='test_metric_02 > 1', + severity='CRITICAL' + )) + alarm_definitions.append(helpers.create_alarm_definition( + name='alarm def sort by 03', + expression='test_metric_03 > 1', + severity='MEDIUM' + )) + for definition in alarm_definitions: + self.monasca_client.create_alarm_definitions(definition) + + resp, response_body = self.monasca_client.list_alarm_definitions('?sort_by=severity') + self.assertEqual(200, resp.status) + + prev_severity = 'CRITICAL' + for alarm_definition in response_body['elements']: + assert prev_severity <= alarm_definition['severity'],\ + "Severity {} came after {}".format(alarm_definition['severity'], prev_severity) + prev_severity = alarm_definition['severity'] + + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_list_alarm_definitions_invalid_sort_by(self): + query_parms = '?sort_by=random' + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.list_alarm_definitions, query_parms) + @test.attr(type="gate") def test_list_alarm_definitions_with_offset_limit(self): helpers.delete_alarm_definitions(self.monasca_client) @@ -347,31 +389,15 @@ class TestAlarmDefinitions(base.BaseMonascaTest): self.assertEqual(first_element, elements[0]) self.assertEqual(last_element, elements[1]) - timeout = time.time() + 60 * 1 # 1 minute timeout - for limit in xrange(1, 3): - next_element = elements[limit - 1] - while True: - if time.time() < timeout: - query_parms = '?offset=' + str(next_element['id']) + \ - '&limit=' + str(limit) - resp, response_body = self.monasca_client.\ - list_alarm_definitions(query_parms) - self.assertEqual(200, resp.status) - new_elements = response_body['elements'] - if len(new_elements) > limit - 1: - self.assertEqual(limit, len(new_elements)) - next_element = new_elements[limit - 1] - elif 0 < len(new_elements) <= limit - 1: - self.assertEqual(last_element, new_elements[0]) - break - else: - self.assertEqual(last_element, next_element) - break - else: - msg = "Failed " \ - "test_list_alarm_definitions_with_offset_limit: " \ - "one minute timeout" - raise exceptions.TimeoutException(msg) + for offset in xrange(0, 2): + for limit in xrange(1, 3 - offset): + query_parms = '?offset=' + str(offset) + '&limit=' + str(limit) + resp, response_body = self.monasca_client.list_alarm_definitions(query_parms) + self.assertEqual(200, resp.status) + new_elements = response_body['elements'] + self.assertEqual(limit, len(new_elements)) + self.assertEqual(elements[offset], new_elements[0]) + self.assertEqual(elements[offset+limit-1], new_elements[-1]) # Get diff --git a/monasca_tempest_tests/tests/api/test_alarms.py b/monasca_tempest_tests/tests/api/test_alarms.py index d15a8e6db..e09b32658 100644 --- a/monasca_tempest_tests/tests/api/test_alarms.py +++ b/monasca_tempest_tests/tests/api/test_alarms.py @@ -1,4 +1,4 @@ -# (C) Copyright 2015 Hewlett Packard Enterprise Development Company LP +# (C) Copyright 2015,2016 Hewlett Packard Enterprise Development Company LP # # 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 @@ -176,21 +176,18 @@ class TestAlarms(base.BaseMonascaTest): @test.attr(type="gate") def test_list_alarms_by_offset_limit(self): - helpers.delete_alarm_definitions(self.monasca_client) - self._create_alarms_for_test_alarms(num=2) - resp, response_body = self.monasca_client.list_alarms() + definition_ids, expected_metric = self._create_alarms_for_test_alarms(num=2) + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name']) self._verify_list_alarms_elements(resp, response_body, expect_num_elements=2) elements = response_body['elements'] - first_element = elements[0] - next_element = elements[1] - id_first_element = first_element['id'] - query_parms = '?offset=' + str(id_first_element) + '&limit=1' + second_element = elements[1] + query_parms = '?metric_name=' + expected_metric['name'] + '&offset=1&limit=1' resp, response_body1 = self.monasca_client.list_alarms(query_parms) elements = response_body1['elements'] self.assertEqual(1, len(elements)) - self.assertEqual(elements[0]['id'], next_element['id']) - self.assertEqual(elements[0], next_element) + self.assertEqual(elements[0]['id'], second_element['id']) + self.assertEqual(elements[0], second_element) @test.attr(type="gate") def test_get_alarm(self): @@ -207,6 +204,66 @@ class TestAlarms(base.BaseMonascaTest): metric = element['metrics'][0] self._verify_metric_in_alarm(metric, expected_metric) + @test.attr(type="gate") + def test_list_alarms_sort_by(self): + alarm_definition_ids, expected_metric = self._create_alarms_for_test_alarms(num=3) + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name']) + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=3) + + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name'] + + '&sort_by=created_timestamp') + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=3) + + elements = response_body['elements'] + last_timestamp = elements[0]['created_timestamp'] + for element in elements: + assert element['state'] >= last_timestamp,\ + "Created_timestamps are not in sorted order {} came before {}".format(last_timestamp, + element['created_timestamp']) + last_timestamp = element['created_timestamp'] + + @test.attr(type='gate') + def test_list_alarms_sort_by_asc_desc(self): + alarm_definition_ids, expected_metric = self._create_alarms_for_test_alarms(num=3) + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name']) + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=3) + + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name'] + + '&sort_by=created_timestamp') + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=3) + + elements = response_body['elements'] + last_timestamp = elements[0]['created_timestamp'] + for element in elements: + assert element['state'] >= last_timestamp,\ + "Created_timestamps are not in ascending order {} came before {}".format(last_timestamp, + element['created_timestamp']) + last_timestamp = element['created_timestamp'] + + resp, response_body = self.monasca_client.list_alarms('?metric_name=' + expected_metric['name'] + + '&sort_by=created_timestamp') + self._verify_list_alarms_elements(resp, response_body, + expect_num_elements=3) + + elements = response_body['elements'] + last_timestamp = elements[0]['created_timestamp'] + for element in elements: + assert element['state'] >= last_timestamp,\ + "Created_timestamps are not in descending order {} came before {}".format(last_timestamp, + element['created_timestamp']) + last_timestamp = element['created_timestamp'] + + + @test.attr(type="gate") + def test_list_alarms_invalid_sort_by(self): + query_parms = '?sort_by=not_valid_field' + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.list_alarms, query_parms) + @test.attr(type="gate") @test.attr(type=['negative']) def test_get_alarm_with_invalid_id(self):