From b0ff5227b00b0da85c04646ed5f425a74c11c41e Mon Sep 17 00:00:00 2001 From: Ryan Brandt Date: Tue, 8 Dec 2015 08:29:44 -0700 Subject: [PATCH] Add alarm count resource Add alarm counts resource at /v2.0/alarms/count Add alarm counts spec Add tempest tests for alarm count Change-Id: I84f762ddf438ff87bc8e5daa04c970f5d484a61b --- .../files/monasca-api/python/api-config.conf | 1 + docs/monasca-api-spec.md | 119 +++++- etc/api-config.conf | 1 + .../api/domain/model/alarm/AlarmCount.java | 63 ++++ .../api/domain/model/alarm/AlarmRepo.java | 11 +- .../hibernate/AlarmSqlRepoImpl.java | 11 + .../persistence/mysql/AlarmMySqlRepoImpl.java | 195 +++++++++- .../monasca/api/resource/AlarmResource.java | 85 ++++- .../main/java/monasca/api/resource/Links.java | 22 +- monasca_api/api/alarms_api_v2.py | 10 +- monasca_api/api/server.py | 7 +- .../common/repositories/alarms_repository.py | 6 +- .../repositories/mysql/alarms_repository.py | 127 ++++++- monasca_api/v2/reference/alarms.py | 98 ++++- monasca_api/v2/reference/helpers.py | 16 + .../services/monasca_client.py | 9 +- .../tests/api/test_alarms_count.py | 345 ++++++++++++++++++ 17 files changed, 1088 insertions(+), 38 deletions(-) create mode 100644 java/src/main/java/monasca/api/domain/model/alarm/AlarmCount.java create mode 100644 monasca_tempest_tests/tests/api/test_alarms_count.py diff --git a/devstack/files/monasca-api/python/api-config.conf b/devstack/files/monasca-api/python/api-config.conf index 28a0629a2..299bde4de 100644 --- a/devstack/files/monasca-api/python/api-config.conf +++ b/devstack/files/monasca-api/python/api-config.conf @@ -18,6 +18,7 @@ metrics_statistics = monasca_api.v2.reference.metrics:MetricsStatistics metrics_names = monasca_api.v2.reference.metrics:MetricsNames alarm_definitions = monasca_api.v2.reference.alarm_definitions:AlarmDefinitions alarms = monasca_api.v2.reference.alarms:Alarms +alarms_count = monasca_api.v2.reference.alarms:AlarmsCount alarms_state_history = monasca_api.v2.reference.alarms:AlarmsStateHistory notification_methods = monasca_api.v2.reference.notifications:Notifications diff --git a/docs/monasca-api-spec.md b/docs/monasca-api-spec.md index f8363120e..80cbc0947 100644 --- a/docs/monasca-api-spec.md +++ b/docs/monasca-api-spec.md @@ -247,18 +247,19 @@ Document Version: v2.0 - [Status Code](#status-code-16) - [Response Body](#response-body-18) - [Response Examples](#response-examples-15) - - [List Alarms State History](#list-alarms-state-history) - - [GET /v2.0/alarms/state-history](#get-v20alarmsstate-history) + - [Get Alarm Counts](#get-alarm-counts) + - [GET /v2.0/alarms/count](#get-v20alarmscount) - [Headers](#headers-19) - [Path Parameters](#path-parameters-19) - [Query Parameters](#query-parameters-19) - [Request Body](#request-body-19) + - [Request Examples](#request-examples-18) - [Response](#response-19) - [Status Code](#status-code-17) - [Response Body](#response-body-19) - - [Response Examples](#response-examples-16) - - [Get Alarm](#get-alarm) - - [GET /v2.0/alarms/{alarm_id}](#get-v20alarmsalarm_id) + - [Response Example](#response-example) + - [List Alarms State History](#list-alarms-state-history) + - [GET /v2.0/alarms/state-history](#get-v20alarmsstate-history) - [Headers](#headers-20) - [Path Parameters](#path-parameters-20) - [Query Parameters](#query-parameters-20) @@ -266,20 +267,19 @@ Document Version: v2.0 - [Response](#response-20) - [Status Code](#status-code-18) - [Response Body](#response-body-20) - - [Response Examples](#response-examples-17) - - [Update Alarm](#update-alarm) - - [PUT /v2.0/alarms/{alarm_id}](#put-v20alarmsalarm_id) + - [Response Examples](#response-examples-16) + - [Get Alarm](#get-alarm) + - [GET /v2.0/alarms/{alarm_id}](#get-v20alarmsalarm_id) - [Headers](#headers-21) - [Path Parameters](#path-parameters-21) - [Query Parameters](#query-parameters-21) - [Request Body](#request-body-21) - - [Request Examples](#request-examples-18) - [Response](#response-21) - [Status Code](#status-code-19) - [Response Body](#response-body-21) - - [Response Examples](#response-examples-18) - - [Patch Alarm](#patch-alarm) - - [PATCH /v2.0/alarms/{alarm_id}](#patch-v20alarmsalarm_id) + - [Response Examples](#response-examples-17) + - [Update Alarm](#update-alarm) + - [PUT /v2.0/alarms/{alarm_id}](#put-v20alarmsalarm_id) - [Headers](#headers-22) - [Path Parameters](#path-parameters-22) - [Query Parameters](#query-parameters-22) @@ -288,9 +288,9 @@ Document Version: v2.0 - [Response](#response-22) - [Status Code](#status-code-20) - [Response Body](#response-body-22) - - [Response Examples](#response-examples-19) - - [Delete Alarm](#delete-alarm) - - [DELETE /v2.0/alarms/{alarm_id}](#delete-v20alarmsalarm_id) + - [Response Examples](#response-examples-18) + - [Patch Alarm](#patch-alarm) + - [PATCH /v2.0/alarms/{alarm_id}](#patch-v20alarmsalarm_id) - [Headers](#headers-23) - [Path Parameters](#path-parameters-23) - [Query Parameters](#query-parameters-23) @@ -299,16 +299,27 @@ Document Version: v2.0 - [Response](#response-23) - [Status Code](#status-code-21) - [Response Body](#response-body-23) - - [List Alarm State History](#list-alarm-state-history) - - [GET /v2.0/alarms/{alarm_id}/state-history](#get-v20alarmsalarm_idstate-history) + - [Response Examples](#response-examples-19) + - [Delete Alarm](#delete-alarm) + - [DELETE /v2.0/alarms/{alarm_id}](#delete-v20alarmsalarm_id) - [Headers](#headers-24) - [Path Parameters](#path-parameters-24) - [Query Parameters](#query-parameters-24) - [Request Body](#request-body-24) - - [Request Data](#request-data) + - [Request Examples](#request-examples-21) - [Response](#response-24) - [Status Code](#status-code-22) - [Response Body](#response-body-24) + - [List Alarm State History](#list-alarm-state-history) + - [GET /v2.0/alarms/{alarm_id}/state-history](#get-v20alarmsalarm_idstate-history) + - [Headers](#headers-25) + - [Path Parameters](#path-parameters-25) + - [Query Parameters](#query-parameters-25) + - [Request Body](#request-body-25) + - [Request Data](#request-data) + - [Response](#response-25) + - [Status Code](#status-code-23) + - [Response Body](#response-body-25) - [Response Examples](#response-examples-20) - [License](#license) @@ -2333,6 +2344,78 @@ Returns a JSON object with a 'links' array of links and an 'elements' array of a ``` ___ +##Get Alarm Counts +Get the number of alarms that match the criteria. + +###GET /v2.0/alarms/count + +####Headers +* X-Auth-Token (string, required) - Keystone auth token +* Content-Type (string, required) - application/json +* Accept (string) - application/json + +####Path Parameters +None + +####Query Parameters +* alarm_definition_id (string, optional) - Alarm definition ID to filter by. +* metric_name (string(255), optional) - Name of metric to filter by. +* metric_dimensions ({string(255): string(255)}, optional) - Dimensions of metrics to filter by specified as a comma separated array of (key, value) pairs as `key1:value1,key1:value1,...` +* state (string, optional) - State of alarm to filter by, either `OK`, `ALARM` or `UNDETERMINED`. +* 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 (integer, optional) +* limit (integer, optional) +* group_by (string, optional) – a list of fields to group the results by as ```field1,field2,…``` +The group_by field is limited to `alarm_definition_id`, `name`, `state`, `severity`, `link`, `lifecycle_state`, `metric_name`, `dimension_name`, `dimension_value`. +If any of the fields `metric_name`, `dimension_name`, or `dimension_value` are specified, the sum of the resulting counts is not guaranteed to equal the total number of alarms in the system. Alarms with multiple metrics may be included in multiple counts when grouped by any of these three fields. + +####Request Body +None + +####Request Examples +``` +GET /v2.0/alarms/count?metric_name=cpu.system_perc&metric_dimensions=hostname:devstack&group_by=state,lifecycle_state +HTTP/1.1 Host: 192.168.10.4:8080 +Content-Type: application/json +X-Auth-Token: 2b8882ba2ec44295bf300aecb2caa4f7 +Cache-Control: no-cache +``` + +###Response +####Status Code +* 200 OK + +####Response Body +Returns a JSON object containing the following fields: +* links ([link]) - Links to alarms count resource +* columns ([string]) - List of the column names, in the order they were returned +* counts ([array[]]) - A two dimensional array of the counts returned + +####Response Example +``` +{ + "links": [ + { + "rel": "self", + "href": "http://192.168.10.4:8080/v2.0/alarms/count?name=cpu.system_perc&dimensions=hostname%3Adevstack&group_by=state,lifecycle_state" + } + ], + "columns": ["count", "state", "lifecycle_state"], + "counts": [ + [124, "ALARM", "ACKNOWLEDGED"], + [12, "ALARM", "RESOLVED"], + [235, "OK", "OPEN"], + [61, "OK", "RESOLVED"], + [13, "UNDETERMINED", "ACKNOWLEDGED"], + [1, "UNDETERMINED", "OPEN"], + [2, "UNDETERMINED", "RESOLVED"], + ] + } +``` +___ + ## List Alarms State History List alarm state history for alarms. diff --git a/etc/api-config.conf b/etc/api-config.conf index 7a1b92ead..681f5cb92 100755 --- a/etc/api-config.conf +++ b/etc/api-config.conf @@ -18,6 +18,7 @@ metrics_statistics = monasca_api.v2.reference.metrics:MetricsStatistics metrics_names = monasca_api.v2.reference.metrics:MetricsNames alarm_definitions = monasca_api.v2.reference.alarm_definitions:AlarmDefinitions alarms = monasca_api.v2.reference.alarms:Alarms +alarms_count = monasca_api.v2.reference.alarms:AlarmsCount alarms_state_history = monasca_api.v2.reference.alarms:AlarmsStateHistory notification_methods = monasca_api.v2.reference.notifications:Notifications diff --git a/java/src/main/java/monasca/api/domain/model/alarm/AlarmCount.java b/java/src/main/java/monasca/api/domain/model/alarm/AlarmCount.java new file mode 100644 index 000000000..05cbd9661 --- /dev/null +++ b/java/src/main/java/monasca/api/domain/model/alarm/AlarmCount.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 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 + * + * 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 monasca.api.domain.model.alarm; + +import java.util.ArrayList; +import java.util.List; + +import monasca.api.domain.model.common.Link; +import monasca.api.domain.model.common.Linked; +import monasca.common.model.domain.common.AbstractEntity; + +public class AlarmCount extends AbstractEntity implements Linked { + private List links; + private List columns; + private List> counts; + + public AlarmCount() {} + + public AlarmCount(List columns, List> counts) { + this.columns = new ArrayList<>(); + this.columns.add("count"); + if (columns != null) { + this.columns.addAll(columns); + } + this.counts = new ArrayList<>(); + this.counts.addAll(counts); + } + + public void setColumns(List columns) { + this.columns = columns; + } + + public List getColumns() { + return this.columns; + } + + public void setCounts(List> counts) { + this.counts = counts; + } + + public List> getCounts() { + return this.counts; + } + + public void setLinks(List links) { + this.links = links; + } + + public List getLinks() { + return this.links; + } +} 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 14a96758c..0e355117f 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Hewlett-Packard Development Company, L.P. + * Copyright (c) 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 a copy of the License at @@ -55,4 +55,13 @@ public interface AlarmRepo { * Alarm Definition Id */ Map> findAlarmSubExpressionsForAlarmDefinition(String alarmDefinitionId); + + /** + * Gets the count(s) of the alarms matching the parameters + * @return 2 dimensional list of the counts with their group tags + */ + AlarmCount getAlarmsCount(String tenantId, String alarmDefId, String metricName, + Map metricDimensions, AlarmState state, + String lifecycleState, String link, DateTime stateUpdatedStart, + List groupBy, String offset, int limit); } 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 328ba6b9c..b1ec2d99f 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 @@ -38,7 +38,9 @@ import org.slf4j.LoggerFactory; import monasca.api.domain.exception.EntityNotFoundException; import monasca.api.domain.model.alarm.Alarm; +import monasca.api.domain.model.alarm.AlarmCount; import monasca.api.domain.model.alarm.AlarmRepo; +import monasca.api.resource.exception.Exceptions; import monasca.common.hibernate.db.AlarmDb; import monasca.common.hibernate.db.SubAlarmDb; import monasca.common.hibernate.type.BinaryId; @@ -523,4 +525,13 @@ public class AlarmSqlRepoImpl return subAlarms; } + + @Override + public AlarmCount getAlarmsCount(String tenantId, String alarmDefId, String metricName, + Map metricDimensions, AlarmState state, + String lifecycleState, String link, DateTime stateUpdatedStart, + List groupBy, String offset, int limit) { + // Not Implemented + return null; + } } 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 a07847e5c..1c920038b 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 @@ -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,8 +13,12 @@ */ package monasca.api.infrastructure.persistence.mysql; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; + import monasca.api.domain.exception.EntityNotFoundException; import monasca.api.domain.model.alarm.Alarm; +import monasca.api.domain.model.alarm.AlarmCount; import monasca.api.domain.model.alarm.AlarmRepo; import monasca.api.infrastructure.persistence.DimensionQueries; import monasca.api.infrastructure.persistence.PersistUtils; @@ -51,6 +55,8 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { 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, " @@ -400,4 +406,191 @@ public class AlarmMySqlRepoImpl implements AlarmRepo { return subAlarms; } } + + @Override + public AlarmCount getAlarmsCount(String tenantId, String alarmDefId, String metricName, + Map metricDimensions, AlarmState state, + String lifecycleState, String link, DateTime stateUpdatedStart, + List groupBy, String offset, int limit) { + final String SELECT_CLAUSE = "SELECT count(*) as count%1$s " + + " FROM alarm AS a " + + " INNER JOIN alarm_definition as ad on ad.id = a.alarm_definition_id "; + + StringBuilder queryBuilder = new StringBuilder(); + + String groupByStr = ""; + String metricSelect; + if (groupBy != null) { + groupByStr = COMMA_JOINER.join(groupBy); + queryBuilder.append(String.format(SELECT_CLAUSE, ',' + groupByStr)); + + if (groupBy.contains("metric_name") || groupBy.contains("dimension_name") || groupBy + .contains("dimension_value")) { + metricSelect = " INNER JOIN (SELECT distinct am.alarm_id%1$s " + + "FROM metric_definition AS md " + + "JOIN metric_definition_dimensions AS mdd on md.id = mdd.metric_definition_id " + + "JOIN metric_dimension AS mdim ON mdd.metric_dimension_set_id = mdim.dimension_set_id " + + "JOIN alarm_metric AS am ON am.metric_definition_dimensions_id = mdd.id)" + + "AS metrics ON a.id = metrics.alarm_id "; + String subSelect = ""; + if (groupBy.contains("metric_name")) { + subSelect = subSelect + ",md.name AS metric_name"; + } + if (groupBy.contains("dimension_name")) { + subSelect = subSelect + ",mdim.name AS dimension_name"; + } + if (groupBy.contains("dimension_value")) { + subSelect = subSelect + ",mdim.value AS dimension_value"; + } + + queryBuilder.append(String.format(metricSelect, subSelect)); + } + } else { + queryBuilder.append(String.format(SELECT_CLAUSE, groupByStr)); + } + + + queryBuilder.append(" INNER JOIN (SELECT a.id " + + "FROM alarm AS a, alarm_definition AS ad " + + "WHERE ad.id = a.alarm_definition_id " + + " AND ad.deleted_at IS NULL " + + " AND ad.tenant_id = :tenantId "); + + if (alarmDefId != null) { + queryBuilder.append(" AND ad.id = :alarmDefId "); + } + + if (metricName != null) { + + queryBuilder.append(" AND a.id IN (SELECT distinct a.id FROM alarm AS a " + + "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 (SELECT distinct id FROM metric_definition " + + " WHERE name = :metricName) AS md " + + " ON md.id = mdd.metric_definition_id "); + + buildJoinClauseFor(metricDimensions, queryBuilder); + + queryBuilder.append(")"); + + } else if (metricDimensions != null) { + + queryBuilder.append(" AND a.id IN (SELECT distinct a.id FROM alarm AS a " + + "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 "); + + buildJoinClauseFor(metricDimensions, queryBuilder); + + queryBuilder.append(")"); + + } + + if (state != null) { + queryBuilder.append(" AND a.state = :state"); + } + + if (lifecycleState != null) { + queryBuilder.append(" AND a.lifecycle_state = :lifecycleState"); + } + + if (link != null) { + queryBuilder.append(" AND a.link = :link"); + } + + if (stateUpdatedStart != null) { + queryBuilder.append(" AND a.state_updated_at >= :stateUpdatedStart"); + } + + queryBuilder.append(") AS alarm_id_list ON alarm_id_list.id = a.id "); + + if (groupBy != null) { + queryBuilder.append(" GROUP BY "); + queryBuilder.append(groupByStr); + } + + queryBuilder.append(" ORDER BY "); + if (!Strings.isNullOrEmpty(groupByStr)) { + queryBuilder.append(groupByStr); + } else { + queryBuilder.append(" a.id "); + } + + queryBuilder.append(" LIMIT :limit"); + + if (offset != null) { + queryBuilder.append(String.format(" OFFSET %1$s ", offset)); + } + + + + try (Handle h = db.open()) { + + final Query> q = h.createQuery(queryBuilder.toString()).bind("tenantId", tenantId); + + if (alarmDefId != null) { + q.bind("alarmDefId", alarmDefId); + } + + if (metricName != null) { + q.bind("metricName", metricName); + } + + if (state != null) { + q.bind("state", state.name()); + } + + if (lifecycleState != null) { + q.bind("lifecycleState", lifecycleState); + } + + if (link != null) { + q.bind("link", link); + } + + if (stateUpdatedStart != null) { + q.bind("stateUpdatedStart", stateUpdatedStart.toString()); + } + + q.bind("limit", limit + 1); + + DimensionQueries.bindDimensionsToQuery(q, metricDimensions); + + final List> rows = q.list(); + + return createAlarmCounts(groupBy, rows); + + } + } + + private AlarmCount createAlarmCounts(List groupBy, List> rows) { + List> counts = new ArrayList<>(); + + // if no results, return 0 and fill columns with null + if (rows.size() == 0) { + List countsAndTags = new ArrayList<>(); + countsAndTags.add(0); + if (groupBy != null) { + for (final String columnName : groupBy) { + countsAndTags.add(null); + } + } + counts.add(countsAndTags); + return new AlarmCount(groupBy, counts); + } + + for (final Map row : rows) { + List countAndTags = new ArrayList<>(); + countAndTags.add(row.get("count")); + if (groupBy != null && !groupBy.isEmpty()) { + for (final String columnName : groupBy) { + countAndTags.add(row.get(columnName)); + } + } + counts.add(countAndTags); + } + + return new AlarmCount(groupBy, counts); + } } diff --git a/java/src/main/java/monasca/api/resource/AlarmResource.java b/java/src/main/java/monasca/api/resource/AlarmResource.java index 9ae24e2ad..925fe7dcc 100644 --- a/java/src/main/java/monasca/api/resource/AlarmResource.java +++ b/java/src/main/java/monasca/api/resource/AlarmResource.java @@ -1,11 +1,11 @@ /* - * 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 - * + * * 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 @@ -13,7 +13,9 @@ */ package monasca.api.resource; +import com.google.common.base.Splitter; import com.google.common.base.Strings; +import com.google.common.collect.Lists; import com.codahale.metrics.annotation.Timed; import com.fasterxml.jackson.databind.JsonMappingException; @@ -21,6 +23,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; import org.hibernate.validator.constraints.NotEmpty; import org.joda.time.DateTime; +import java.util.Arrays; import java.util.List; import java.util.Map; @@ -44,11 +47,13 @@ import monasca.api.app.command.UpdateAlarmCommand; import monasca.api.app.validation.MetricNameValidation; import monasca.api.app.validation.Validation; import monasca.api.domain.model.alarm.Alarm; +import monasca.api.domain.model.alarm.AlarmCount; import monasca.api.domain.model.alarm.AlarmRepo; import monasca.api.domain.model.alarmstatehistory.AlarmStateHistory; import monasca.api.domain.model.alarmstatehistory.AlarmStateHistoryRepo; import monasca.api.infrastructure.persistence.PersistUtils; import monasca.api.resource.annotation.PATCH; +import monasca.api.resource.exception.Exceptions; import monasca.common.model.alarm.AlarmState; /** @@ -61,6 +66,12 @@ public class AlarmResource { private final PersistUtils persistUtils; private final AlarmStateHistoryRepo stateHistoryRepo; + private final static List ALLOWED_GROUP_BY = Arrays.asList("alarm_definition_id", "name", + "state", "severity", "link", + "lifecycle_state", + "metric_name", "dimension_name", + "dimension_value"); + @Inject public AlarmResource(AlarmService service, AlarmRepo repo, AlarmStateHistoryRepo stateHistoryRepo, @@ -91,7 +102,7 @@ public class AlarmResource { private Alarm fixAlarmLinks(UriInfo uriInfo, Alarm alarm) { Links.hydrate(alarm.getAlarmDefinition(), uriInfo, - AlarmDefinitionResource.ALARM_DEFINITIONS_PATH); + AlarmDefinitionResource.ALARM_DEFINITIONS_PATH); return Links.hydrate(alarm, uriInfo, true); } @@ -106,9 +117,9 @@ public class AlarmResource { throws Exception { final int paging_limit = this.persistUtils.getLimit(limit); final List resource = stateHistoryRepo.findById(tenantId, - alarmId, - offset, - paging_limit + alarmId, + offset, + paging_limit ); return Links.paginate(paging_limit, resource, uriInfo); } @@ -220,4 +231,62 @@ public class AlarmResource { return fixAlarmLinks(uriInfo, service.update(tenantId, alarmId, command)); } + + @GET + @Timed + @Path("/count") + @Produces(MediaType.APPLICATION_JSON) + public Object getCount(@Context UriInfo uriInfo, + @HeaderParam("X-Tenant-Id") String tenantId, @PathParam("alarm_id") String alarmId, + @QueryParam("alarm_definition_id") String alarmDefId, + @QueryParam("metric_name") String metricName, + @QueryParam("metric_dimensions") String metricDimensionsStr, + @QueryParam("state") AlarmState state, + @QueryParam("lifecycle_state") String lifecycleState, + @QueryParam("link") String link, + @QueryParam("state_updated_start_time") String stateUpdatedStartStr, + @QueryParam("group_by") String groupByStr, + @QueryParam("offset") String offset, + @QueryParam("limit") String limit) + throws Exception { + Map metricDimensions = + Strings.isNullOrEmpty(metricDimensionsStr) ? null : Validation + .parseAndValidateDimensions(metricDimensionsStr); + MetricNameValidation.validate(metricName, false); + DateTime stateUpdatedStart = + Validation.parseAndValidateDate(stateUpdatedStartStr, + "state_updated_start_time", false); + List groupBy = (Strings.isNullOrEmpty(groupByStr)) ? null : parseAndValidateGroupBy( + groupByStr); + + if (offset != null) { + Validation.parseAndValidateNumber(offset, "offset"); + } + + final int paging_limit = this.persistUtils.getLimit(limit); + final AlarmCount resource = repo.getAlarmsCount(tenantId, + alarmDefId, + metricName, + metricDimensions, + state, + lifecycleState, + link, + stateUpdatedStart, + groupBy, + offset, + paging_limit); + Links.paginateAlarmCount(resource, paging_limit, uriInfo); + return resource; + } + + private List parseAndValidateGroupBy(String groupByStr) { + List groupBy = null; + if (!Strings.isNullOrEmpty(groupByStr)) { + groupBy = Lists.newArrayList(Splitter.on(',').omitEmptyStrings().trimResults().split(groupByStr)); + if (!ALLOWED_GROUP_BY.containsAll(groupBy)) { + throw Exceptions.unprocessableEntity("Unprocessable Entity", "Invalid group_by field"); + } + } + return groupBy; + } } diff --git a/java/src/main/java/monasca/api/resource/Links.java b/java/src/main/java/monasca/api/resource/Links.java index ed0f53919..d92f10bad 100644 --- a/java/src/main/java/monasca/api/resource/Links.java +++ b/java/src/main/java/monasca/api/resource/Links.java @@ -1,11 +1,11 @@ /* - * 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 - * + * * 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 @@ -23,6 +23,7 @@ import javax.ws.rs.core.UriInfo; import com.google.common.base.Preconditions; import monasca.api.ApiConfig; +import monasca.api.domain.model.alarm.AlarmCount; import monasca.api.domain.model.common.Paged; import monasca.api.domain.model.measurement.Measurements; import monasca.api.domain.model.statistic.Statistics; @@ -348,4 +349,17 @@ public final class Links { return nextLink; } + public static void paginateAlarmCount(AlarmCount alarmCount, int limit, UriInfo uriInfo) + throws UnsupportedEncodingException { + List links = new ArrayList<>(); + links.add(getSelfLink(uriInfo)); + if (alarmCount.getCounts().size() > limit) { + alarmCount.getCounts().remove(alarmCount.getCounts().size()-1); + String offset = String.valueOf(limit); + links.add(getNextLink(offset, uriInfo)); + } + + alarmCount.setLinks(links); + } + } diff --git a/monasca_api/api/alarms_api_v2.py b/monasca_api/api/alarms_api_v2.py index 101f10c43..186e30313 100644 --- a/monasca_api/api/alarms_api_v2.py +++ b/monasca_api/api/alarms_api_v2.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014-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 @@ -35,6 +35,14 @@ class AlarmsV2API(object): res.status = '501 Not Implemented' +class AlarmsCountV2API(object): + def __init__(self): + super(AlarmsCountV2API, self).__init__() + + def on_get(self, req, res): + res.status = "501 Not Implemented" + + class AlarmsStateHistoryV2API(object): def __init__(self): super(AlarmsStateHistoryV2API, self).__init__() diff --git a/monasca_api/api/server.py b/monasca_api/api/server.py index a029924f2..7a29c8780 100755 --- a/monasca_api/api/server.py +++ b/monasca_api/api/server.py @@ -1,5 +1,5 @@ # Copyright 2014 IBM Corp -# Copyright 2015 Hewlett-Packard +# 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 @@ -38,6 +38,8 @@ dispatcher_opts = [cfg.StrOpt('versions', default=None, help='Alarm definitions'), cfg.StrOpt('alarms', default=None, help='Alarms'), + cfg.StrOpt('alarms_count', default=None, + help='Alarms Count'), cfg.StrOpt('alarms_state_history', default=None, help='Alarms state history'), cfg.StrOpt('notification_methods', default=None, @@ -91,6 +93,9 @@ def launch(conf, config_file="/etc/monasca/api-config.conf"): app.add_route("/v2.0/alarms", alarms) app.add_route("/v2.0/alarms/{alarm_id}", alarms) + alarm_count = simport.load(cfg.CONF.dispatcher.alarms_count)() + app.add_route("/v2.0/alarms/count/", alarm_count) + alarms_state_history = simport.load( cfg.CONF.dispatcher.alarms_state_history)() app.add_route("/v2.0/alarms/state-history", alarms_state_history) diff --git a/monasca_api/common/repositories/alarms_repository.py b/monasca_api/common/repositories/alarms_repository.py index e73274749..683fca283 100644 --- a/monasca_api/common/repositories/alarms_repository.py +++ b/monasca_api/common/repositories/alarms_repository.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014-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 @@ -47,3 +47,7 @@ class AlarmsRepository(object): @abc.abstractmethod def get_alarms(self, tenant_id, query_parms, offset, limit): pass + + @abc.abstractmethod + def get_alarms_count(self, tenant_id, query_parms, offset, limit): + pass diff --git a/monasca_api/common/repositories/mysql/alarms_repository.py b/monasca_api/common/repositories/mysql/alarms_repository.py index 375078967..20d4cdc1c 100644 --- a/monasca_api/common/repositories/mysql/alarms_repository.py +++ b/monasca_api/common/repositories/mysql/alarms_repository.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014-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 @@ -296,3 +296,128 @@ class AlarmsRepository(mysql_repository.MySQLRepository, query = select_clause + where_clause + order_by_clause + limit_clause return self._execute_query(query, parms) + + @mysql_repository.mysql_try_catch_block + def get_alarms_count(self, tenant_id, query_parms, offset, limit): + + select_clause = """select count(*) as count{} + from alarm as a + join alarm_definition as ad on ad.id = a.alarm_definition_id + """ + + if 'group_by' in query_parms: + group_by_str = ",".join(query_parms['group_by']) + + metric_group_by = {'metric_name', + 'dimension_name', + 'dimension_value'}.intersection(set(query_parms['group_by'])) + if metric_group_by: + metric_select = """ + join ( select distinct am.alarm_id{} + from metric_definition as md + join metric_definition_dimensions as mdd on md.id = mdd.metric_definition_id + join metric_dimension as mdim on mdd.metric_dimension_set_id = mdim.dimension_set_id + join alarm_metric as am on am.metric_definition_dimensions_id = mdd.id + ) as metrics on a.id = metrics.alarm_id """ + sub_select_clause = "" + if 'metric_name' in metric_group_by: + sub_select_clause += ', md.name as metric_name' + if 'dimension_name' in metric_group_by: + sub_select_clause += ', mdim.name as dimension_name' + if 'dimension_value' in metric_group_by: + sub_select_clause += ', mdim.value as dimension_value' + + select_clause += metric_select.format(sub_select_clause) + + else: + group_by_str = "" + + parms = [] + + where_clause = " where ad.tenant_id = %s " + parms.append(tenant_id) + + if 'alarm_definition_id' in query_parms: + parms.append(query_parms['alarm_definition_id']) + where_clause += " and ad.id = %s " + + if 'state' in query_parms: + parms.append(query_parms['state'].encode('utf8')) + where_clause += " and a.state = %s " + + if 'lifecycle_state' in query_parms: + parms.append(query_parms['lifecycle_state'].encode('utf8')) + where_clause += " and a.lifecycle_state = %s " + + if 'link' in query_parms: + parms.append(query_parms['link'].encode('utf8')) + where_clause += " and a.link = %s " + + if 'state_updated_start_time' in query_parms: + parms.append(query_parms['state_updated_start_time'] + .encode("utf8")) + where_clause += " and state_updated_at >= %s " + + if 'metric_name' in query_parms: + sub_select_clause = """ + and a.id in (select distinct a.id from alarm as a + 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 (select distinct id from + metric_definition + where name = %s) as md + on md.id = mdd.metric_definition_id) + """ + parms.append(query_parms['metric_name'].encode('utf8')) + where_clause += sub_select_clause + + if 'metric_dimensions' in query_parms: + sub_select_clause = """ + and a.id in (select distinct a.id from alarm as a + 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 + """ + sub_select_parms = [] + i = 0 + for metric_dimension in query_parms['metric_dimensions']: + parsed_dimension = metric_dimension.split(':') + sub_select_clause += """ + inner join (select distinct dimension_set_id + from metric_dimension + where name = %s and value = %s) as md{} + on md{}.dimension_set_id = mdd.metric_dimension_set_id + """.format(i, i) + i += 1 + sub_select_parms += [parsed_dimension[0].encode('utf8'), + parsed_dimension[1].encode('utf8')] + + sub_select_clause += ")" + parms += sub_select_parms + where_clause += sub_select_clause + + if group_by_str: + group_order_by_clause = " group by {} order by {} ".format(group_by_str, group_by_str) + else: + group_order_by_clause = "" + + if limit: + limit_clause = " limit %s " + parms.append(limit + 1) + else: + limit_clause = "" + + if offset: + offset_clause = " offset {} ".format(offset) + else: + offset_clause = "" + + select_group_by = ',' + group_by_str if group_by_str else "" + select_clause = select_clause.format(select_group_by) + + query = select_clause + where_clause + group_order_by_clause + limit_clause + offset_clause + + return self._execute_query(query, parms) diff --git a/monasca_api/v2/reference/alarms.py b/monasca_api/v2/reference/alarms.py index d886ffc24..e086bc90e 100644 --- a/monasca_api/v2/reference/alarms.py +++ b/monasca_api/v2/reference/alarms.py @@ -1,4 +1,4 @@ -# Copyright 2014 Hewlett-Packard +# Copyright 2014-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 @@ -330,6 +330,102 @@ class Alarms(alarms_api_v2.AlarmsV2API, return helpers.paginate(result, req_uri, limit) +class AlarmsCount(alarms_api_v2.AlarmsCountV2API, alarming.Alarming): + + def __init__(self): + try: + super(AlarmsCount, self).__init__() + self._region = cfg.CONF.region + self._default_authorized_roles = ( + cfg.CONF.security.default_authorized_roles) + self._alarms_repo = simport.load( + cfg.CONF.repositories.alarms_driver)() + + except Exception as ex: + LOG.exception(ex) + raise exceptions.RepositoryException(ex) + + def on_get(self, req, res): + helpers.validate_authorization(req, self._default_authorized_roles) + tenant_id = helpers.get_tenant_id(req) + query_parms = falcon.uri.parse_query_string(req.query_string) + if 'group_by' in query_parms: + if not isinstance(query_parms['group_by'], list): + query_parms['group_by'] = [query_parms['group_by']] + self._validate_group_by(query_parms['group_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: + try: + offset = int(offset) + except Exception: + raise HTTPUnprocessableEntityError("Unprocessable Entity", + "Offset must be a valid integer, was {}".format(offset)) + + limit = helpers.get_limit(req) + + result = self._alarms_count(req.uri, tenant_id, query_parms, offset, limit) + + res.body = helpers.dumpit_utf8(result) + res.status = falcon.HTTP_200 + + @resource.resource_try_catch_block + def _alarms_count(self, req_uri, tenant_id, query_parms, offset, limit): + + count_data = self._alarms_repo.get_alarms_count(tenant_id, query_parms, offset, limit) + group_by = query_parms['group_by'] if 'group_by' in query_parms else [] + + # result = count_data + result = { + 'links': [ + { + 'rel': 'self', + 'href': req_uri + } + ], + 'columns': ['count'] + } + + if len(count_data) == 0 or count_data[0]['count'] == 0: + count = [0] + if 'group_by' in query_parms: + for field in query_parms['group_by']: + result['columns'].append(field) + count.append(None) + result['counts'] = [count] + return result + + if len(count_data) > limit: + result['links'].append({'rel': 'next', + 'href': helpers.create_alarms_count_next_link(req_uri, offset, limit)}) + count_data = count_data[:limit] + + result['columns'].extend(group_by) + + result['counts'] = [] + for row in count_data: + count_result = [row['count']] + for field in group_by: + count_result.append(row[field]) + result['counts'].append(count_result) + + return result + + def _validate_group_by(self, group_by): + allowed_values = {'alarm_definition_id', 'name', 'state', 'severity', + 'link', 'lifecycle_state', 'metric_name', + 'dimension_name', 'dimension_value'} + if not set(group_by).issubset(allowed_values): + raise HTTPUnprocessableEntityError( + "Unprocessable Entity", + "One or more group-by values from {} are not in {}".format(group_by, allowed_values)) + + class AlarmsStateHistory(alarms_api_v2.AlarmsStateHistoryV2API, alarming.Alarming): def __init__(self): diff --git a/monasca_api/v2/reference/helpers.py b/monasca_api/v2/reference/helpers.py index 0f0e599ab..cca8c5acd 100644 --- a/monasca_api/v2/reference/helpers.py +++ b/monasca_api/v2/reference/helpers.py @@ -1,5 +1,6 @@ # Copyright 2014 Hewlett-Packard # Copyright 2015 Cray Inc. All Rights Reserved. +# Copyright 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 @@ -472,6 +473,21 @@ def paginate_statistics(statistic, uri, limit): return resource +def create_alarms_count_next_link(uri, offset, limit): + if offset is None: + offset = 0 + parsed_url = urlparse.urlparse(uri) + base_url = build_base_uri(parsed_url) + new_query_params = [u'offset=' + urlparse.quote(str(offset + limit))] + _get_old_query_params_except_offset(new_query_params, parsed_url) + + next_link = base_url + if new_query_params: + next_link += '?' + '&'.join(new_query_params) + + return next_link + + def build_base_uri(parsed_uri): return parsed_uri.scheme + '://' + parsed_uri.netloc + parsed_uri.path diff --git a/monasca_tempest_tests/services/monasca_client.py b/monasca_tempest_tests/services/monasca_client.py index 503e4ac36..bc41d050d 100644 --- a/monasca_tempest_tests/services/monasca_client.py +++ b/monasca_tempest_tests/services/monasca_client.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 @@ -260,6 +260,13 @@ class MonascaClient(rest_client.RestClient): resp, response_body = self.patch(uri, json.dumps(request_body)) return resp, json.loads(response_body) + def count_alarms(self, query_params=None): + uri = 'alarms/count' + if query_params is not None: + uri += query_params + resp, response_body = self.get(uri) + return resp, json.loads(response_body) + def list_alarms_state_history(self, query_params=None): uri = 'alarms/state-history' if query_params is not None: diff --git a/monasca_tempest_tests/tests/api/test_alarms_count.py b/monasca_tempest_tests/tests/api/test_alarms_count.py new file mode 100644 index 000000000..b9e5e7698 --- /dev/null +++ b/monasca_tempest_tests/tests/api/test_alarms_count.py @@ -0,0 +1,345 @@ +# (C) Copyright 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 +# 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. + +import time + +from monasca_tempest_tests.tests.api import base +from monasca_tempest_tests.tests.api import helpers +from tempest.common.utils import data_utils +from tempest import test +from tempest_lib import exceptions + + +GROUP_BY_ALLOWED_PARAMS = {'alarm_definition_id', 'name', 'state', 'severity', + 'link', 'lifecycle_state', 'metric_name', + 'dimension_name', 'dimension_value'} + +class TestAlarmsCount(base.BaseMonascaTest): + + @classmethod + def resource_setup(cls): + super(TestAlarmsCount, cls).resource_setup() + + num_hosts = 20 + + alarm_definitions = [] + expected_alarm_counts = [] + metrics_to_send = [] + + # OK, LOW + expression = "max(test_metric_01) > 10" + name = data_utils.rand_name('test-counts-01') + alarm_definitions.append(helpers.create_alarm_definition( + name=name, + expression=expression, + severity='LOW', + match_by=['hostname', 'unique'])) + for i in xrange(100): + metrics_to_send.append(helpers.create_metric( + name='test_metric_01', + dimensions={'hostname': 'test_' + str(i % num_hosts), + 'unique': str(i)}, + value=1 + )) + expected_alarm_counts.append(100) + + # ALARM, MEDIUM + expression = "max(test_metric_02) > 10" + name = data_utils.rand_name('test-counts-02') + alarm_definitions.append(helpers.create_alarm_definition( + name=name, + expression=expression, + severity='MEDIUM', + match_by=['hostname', 'unique'])) + for i in xrange(75): + metrics_to_send.append(helpers.create_metric( + name='test_metric_02', + dimensions={'hostname': 'test_' + str(i % num_hosts), + 'unique': str(i)}, + value=11 + )) + # append again to move from undetermined to alarm + metrics_to_send.append(helpers.create_metric( + name='test_metric_02', + dimensions={'hostname': 'test_' + str(i % num_hosts), + 'unique': str(i)}, + value=11 + )) + expected_alarm_counts.append(75) + + # OK, HIGH, shared dimension + expression = "max(test_metric_03) > 100" + name = data_utils.rand_name('test_counts-03') + alarm_definitions.append(helpers.create_alarm_definition( + name=name, + expression=expression, + severity='HIGH', + match_by=['hostname', 'unique'])) + for i in xrange(50): + metrics_to_send.append(helpers.create_metric( + name='test_metric_03', + dimensions={'hostname': 'test_' + str(i % num_hosts), + 'unique': str(i), + 'height': '55'}, + value=i + )) + expected_alarm_counts.append(50) + + # UNDERTERMINED, CRITICAL + expression = "max(test_metric_undet) > 100" + name = data_utils.rand_name('test-counts-04') + alarm_definitions.append(helpers.create_alarm_definition( + name=name, + expression=expression, + severity='CRITICAL', + match_by=['hostname', 'unique'])) + for i in xrange(25): + metrics_to_send.append(helpers.create_metric( + name='test_metric_undet', + dimensions={'hostname': 'test_' + str(i % num_hosts), + 'unique': str(i)}, + value=1 + )) + expected_alarm_counts.append(25) + + # create alarm definitions + cls.alarm_definition_ids = [] + for definition in alarm_definitions: + resp, response_body = cls.monasca_client.create_alarm_definitions( + definition) + if resp.status == 201: + cls.alarm_definition_ids.append(response_body['id']) + else: + msg = "Failed to create alarm_definition during setup: {} {}".format(resp.status, response_body) + assert False, msg + + # create alarms + for metric in metrics_to_send: + metric['timestamp'] = int(time.time() * 1000) + cls.monasca_client.create_metrics(metric) + # ensure metric timestamps are unique + time.sleep(0.01) + + # check that alarms exist + time_out = time.time() + 70 + while time.time() < time_out: + setup_complete = True + alarm_count = 0 + for i in xrange(len(cls.alarm_definition_ids)): + resp, response_body = cls.monasca_client.list_alarms( + '?alarm_definition_id=' + cls.alarm_definition_ids[i]) + if resp.status != 200: + msg = "Error listing alarms: {} {}".format(resp.status, response_body) + assert False, msg + if len(response_body['elements']) < expected_alarm_counts[i]: + setup_complete = False + alarm_count += len(response_body['elements']) + break + + if setup_complete: + # allow alarm transitions to occur + # time.sleep(15) + return + + msg = "Failed to create all specified alarms during setup, alarm_count was {}".format(alarm_count) + assert False, msg + + + @classmethod + def resource_cleanup(cls): + for definition_id in cls.alarm_definition_ids: + cls.monasca_client.delete_alarm_definition(definition_id) + + def _verify_counts_format(self, response_body, group_by=None, expected_length=None): + expected_keys = ['links', 'counts', 'columns'] + for key in expected_keys: + self.assertIn(key, response_body) + self.assertIsInstance(response_body[key], list) + + expected_columns = ['count'] + if isinstance(group_by, list): + expected_columns.extend(group_by) + self.assertEqual(expected_columns, response_body['columns']) + + if expected_length is not None: + self.assertEqual(expected_length, len(response_body['counts'])) + else: + expected_length = len(response_body['counts']) + + for i in xrange(expected_length): + self.assertEqual(len(expected_columns), len(response_body['counts'][i])) + + # test with no params + @test.attr(type='gate') + def test_count(self): + resp, response_body = self.monasca_client.count_alarms() + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body) + self.assertEqual(250, response_body['counts'][0][0]) + + # test with each group_by parameter singularly + @test.attr(type='gate') + def test_group_by_singular(self): + resp, response_body = self.monasca_client.list_alarms("?state=ALARM") + self.assertEqual(200, resp.status) + alarm_state_count = len(response_body['elements']) + resp, response_body = self.monasca_client.list_alarms("?state=UNDETERMINED") + self.assertEqual(200, resp.status) + undet_state_count = len(response_body['elements']) + + resp, response_body = self.monasca_client.count_alarms("?group_by=state") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, group_by=['state']) + + self.assertEquals('ALARM', response_body['counts'][0][1]) + self.assertEqual(alarm_state_count, response_body['counts'][0][0]) + self.assertEquals('UNDETERMINED', response_body['counts'][-1][1]) + self.assertEqual(undet_state_count, response_body['counts'][-1][0]) + + resp, response_body = self.monasca_client.count_alarms("?group_by=name") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, group_by=['name'], expected_length=4) + + # test with group by a parameter that is not allowed + @test.attr(type="gate") + @test.attr(type=['negative']) + def test_group_by_not_allowed(self): + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.count_alarms, "?group_by=not_allowed") + + # test with a few group_by fields + @test.attr(type='gate') + def test_group_by_multiple(self): + resp, response_body = self.monasca_client.list_alarms() + alarm_low_count = 0 + for alarm in response_body['elements']: + if alarm['state'] is 'ALARM' and alarm['severity'] is 'LOW': + alarm_low_count += 1 + + + resp, response_body = self.monasca_client.count_alarms("?group_by=state,severity") + self._verify_counts_format(response_body, group_by=['state', 'severity']) + + + # test with filter parameters + @test.attr(type='gate') + def test_filter_params(self): + resp, response_body = self.monasca_client.list_alarms("?severity=LOW") + self.assertEqual(200, resp.status) + expected_count = len(response_body['elements']) + + resp, response_body = self.monasca_client.count_alarms("?severity=LOW") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, expected_length=1) + self.assertEqual(expected_count, response_body['counts'][0][0]) + + # test with multiple metric dimensions + @test.attr(type='gate') + def test_filter_multiple_dimensions(self): + resp, response_body = self.monasca_client.list_alarms("?metric_dimensions=hostname:test_1,unique:1") + self.assertEqual(200, resp.status) + expected_count = len(response_body['elements']) + + resp, response_body = self.monasca_client.count_alarms("?metric_dimensions=hostname:test_1,unique:1") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, expected_length=1) + self.assertEqual(expected_count, response_body['counts'][0][0]) + + # test with filter and group_by parameters + @test.attr(type='gate') + def test_filter_and_group_by_params(self): + resp, response_body = self.monasca_client.list_alarms("?state=ALARM") + self.assertEqual(200, resp.status) + expected_count = 0 + for element in response_body['elements']: + if element['alarm_definition']['severity'] == 'MEDIUM': + expected_count += 1 + + resp, response_body = self.monasca_client.count_alarms("?state=ALARM&group_by=severity") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, group_by=['severity']) + self.assertEqual(expected_count, response_body['counts'][0][0]) + + @test.attr(type='gate') + def test_with_all_group_by_params(self): + resp, response_body = self.monasca_client.list_alarms() + self.assertEqual(200, resp.status) + expected_num_count = len(response_body['elements']) + + query_params = "?group_by=" + ','.join(GROUP_BY_ALLOWED_PARAMS) + resp, response_body = self.monasca_client.count_alarms(query_params) + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, group_by=list(GROUP_BY_ALLOWED_PARAMS)) + + # Expect duplicates + msg = "Not enough distinct counts. Expected at least {}, found {}".format(expected_num_count, + len(response_body['counts'])) + assert expected_num_count <= len(response_body['counts']), msg + + @test.attr(type='gate') + def test_limit(self): + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value']) + assert len(response_body['counts']) > 1, "Too few counts to test limit, found 1" + + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value&limit=1") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value'], + expected_length=1) + + + + @test.attr(type='gate') + def test_offset(self): + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value']) + expected_counts = len(response_body['counts']) - 1 + + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value&offset=1") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value'], + expected_length=expected_counts) + + @test.attr(type='gate') + @test.attr(type=['negative']) + def test_invalid_offset(self): + self.assertRaises(exceptions.UnprocessableEntity, + self.monasca_client.count_alarms, "?group_by=metric_name&offset=not_an_int") + + @test.attr(type='gate') + def test_limit_and_offset(self): + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value']) + expected_first_result = response_body['counts'][1] + + resp, response_body = self.monasca_client.count_alarms( + "?group_by=metric_name,dimension_name,dimension_value&offset=1&limit=5") + self.assertEqual(200, resp.status) + self._verify_counts_format(response_body, + group_by=['metric_name', 'dimension_name', 'dimension_value'], + expected_length=5) + self.assertEqual(expected_first_result, response_body['counts'][0])