def testCreateModelForImportModel(self, createDatasourceAdapterMock, ctxMock, repositoryMock, quotaRepositoryMock, _engineMock): nativeMetric = { "type": "metric", "region": "us-west-2", "namespace": "AWS/EC2", "datasource": "cloudwatch", "metric": "CPUUtilization", "dimensions": { "InstanceId": "i-ab15a19d" } } metricSpec = { "region": nativeMetric["region"], "namespace": nativeMetric["namespace"], "metric": nativeMetric["metric"], "dimensions": nativeMetric["dimensions"] } metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) (createDatasourceAdapterMock.return_value.getInstanceNameForModelSpec. return_value) = metricAdapter.getCanonicalResourceName() quotaRepositoryMock.getInstanceCount.return_value = 0 result = models_api.ModelHandler.createModel(nativeMetric) self.assertIs(result, repositoryMock.getMetric.return_value) repositoryMock.getMetric.assert_called_once_with( ctxMock.connFactory.return_value.__enter__.return_value, createDatasourceAdapterMock.return_value.importModel.return_value)
def getMetricData(self, metricSpec, start, end): # pylint: disable=R0201 """ Retrieve metric data for the given time range :param metricSpec: metric specification for Cloudwatch-based model :type metricSpec: dict (see monitorMetric()) :param start: UTC start time of the metric data range. The start value is inclusive: results include datapoints with the time stamp specified. If set to None, the implementation will choose the start time automatically based on Cloudwatch metric data expiration policy (14 days at the time of this writing) :type start: datetime.datetime :param end: UTC end time of the metric data range. The end value is exclusive; results will include datapoints predating the time stamp specified. If set to None, will use the current UTC time as end :type start: datetime.datetime :returns: A two-tuple (<data-sequence>, <next-start-time>). <data-sequence> is a possibly empty sequence of data points sorted by timestamp in ascending order. Each data point is a two-tuple of (<datetime timestamp>, <value>). <next-start-time> is a datetime.datetime object indicating the UTC start time to use in next call to this method. :rtype: tuple """ metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) return metricAdapter.getMetricData(start, end)
def getMetricResourceStatus(self, metricSpec): # pylint: disable=R0201 """ Query AWS for the status of the metric's resource :returns: AWS/resource-specific status string if supported and available or None if not :rtype: string or NoneType """ metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) return metricAdapter.getResourceStatus()
def getInstanceNameForModelSpec(self, spec): """ Get canonical instance name from a model spec :param modelSpec: Datasource-specific model specification :type modelSpec: JSONifiable dict :returns: Canonical instance name :rtype: str """ metricSpec = spec["metricSpec"] metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) return metricAdapter.getCanonicalResourceName()
def testCreateModelForImportModel(self, createDatasourceAdapterMock, ctxMock, repositoryMock, quotaRepositoryMock, _engineMock): nativeMetric = { "type": "metric", "region": "us-west-2", "namespace": "AWS/EC2", "datasource": "cloudwatch", "metric": "CPUUtilization", "dimensions": { "InstanceId": "i-ab15a19d" } } metricSpec = { "region": nativeMetric["region"], "namespace": nativeMetric["namespace"], "metric": nativeMetric["metric"], "dimensions": nativeMetric["dimensions"] } metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) (createDatasourceAdapterMock .return_value .getInstanceNameForModelSpec .return_value) = metricAdapter.getCanonicalResourceName() quotaRepositoryMock.getInstanceCount.return_value = 0 result = models_api.ModelHandler.createModel(nativeMetric) self.assertIs(result, repositoryMock.getMetric.return_value) repositoryMock.getMetric.assert_called_once_with( ctxMock.connFactory.return_value.__enter__.return_value, createDatasourceAdapterMock.return_value.importModel.return_value)
def _getMetricStatistics(self, metricSpec): # pylint: disable=R0201 """ Retrieve metric data statistics :param metricSpec: metric specification for Cloudwatch-based model :type metricSpec: dict (see monitorMetric()) :param start: UTC start time of the metric data range. The start value is inclusive: results include datapoints with the time stamp specified. If set to None, the implementation will choose the start time automatically based on Cloudwatch metric data expiration policy (14 days at the time of this writing) :type start: datetime.datetime :param end: UTC end time of the metric data range. The end value is exclusive; results will include datapoints predating the time stamp specified. If set to None, will use the current UTC time :type start: datetime.datetime :returns: a dictionary with the metric's statistics :rtype: dict; {"min": <min-value>, "max": <max-value>} """ metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) return metricAdapter.getMetricStatistics(start=None, end=None)
def monitorMetric(self, modelSpec): """ Start monitoring a metric; create a "cloudwatch model" DAO object for the given model specification. :param modelSpec: model specification for Cloudwatch-based model :type modelSpec: dict :: { "datasource": "cloudwatch", "metricSpec": { "region": "us-west-2", "namespace": "AWS/EC2", "metric": "CPUUtilization", "dimensions": { "InstanceId": "i-12d67826" } }, # optional "modelParams": { "min": 0, # optional "max": 100 # optional } } :returns: datasource-specific unique model identifier :raises htm.it.app.exceptions.ObjectNotFoundError: if referenced metric doesn't exist :raises htm.it.app.exceptions.MetricNotSupportedError: if requested metric isn't supported :raises htm.it.app.exceptions.MetricAlreadyMonitored: if the metric is already being monitored """ metricSpec = modelSpec["metricSpec"] metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) # NOTE: getResourceName may be slow (AWS query) # TODO MER-3430: would be handy to use caching to speed things up a lot resourceName = metricAdapter.getResourceName() canonicalResourceName = self.getInstanceNameForModelSpec(modelSpec) resourceLocation = metricAdapter.getResourceLocation() metricName = metricAdapter.getMetricName() metricPeriod = metricAdapter.getMetricPeriod() metricDescription = metricAdapter.getMetricSummary() nameColumnValue = self._composeMetricNameColumnValue( metricName=metricName, metricNamespace=metricSpec["namespace"]) # Determine if the model should be started. This will happen if the # nativeMetric input includes both "min" and "max" or we have default values # for both "min" and "max" defaultMin = metricAdapter.getMetricDefaultMin() defaultMax = metricAdapter.getMetricDefaultMax() if defaultMin is None or defaultMax is None: defaultMin = defaultMax = None # Get user-provided min/max, if any modelParams = modelSpec.get("modelParams", dict()) explicitMin = modelParams.get("min") explicitMax = modelParams.get("max") if (explicitMin is None) != (explicitMax is None): raise ValueError( "min and max params must both be None or non-None; modelSpec=%r" % (modelSpec,)) minVal = explicitMin if explicitMin is not None else defaultMin maxVal = explicitMax if explicitMax is not None else defaultMax stats = {"min": minVal, "max": maxVal} swarmParams = scalar_metric_utils.generateSwarmParams(stats) # Perform the start-monitoring operation atomically/reliably @repository.retryOnTransientErrors def startMonitoringWithRetries(): """ :returns: metricId """ with self.connectionFactory() as conn: with conn.begin(): repository.lockOperationExclusive(conn, repository.OperationLock.METRICS) # Check if the metric is already monitored matchingMetrics = repository.getCloudwatchMetricsForNameAndServer( conn, nameColumnValue, canonicalResourceName, fields=[schema.metric.c.uid, schema.metric.c.parameters]) for m in matchingMetrics: parameters = htmengine.utils.jsonDecode(m.parameters) if (parameters["metricSpec"]["dimensions"] == metricSpec["dimensions"]): msg = ("monitorMetric: Cloudwatch modelId=%s is already " "monitoring metric=%s on resource=%s; model=%r" % (m.uid, nameColumnValue, canonicalResourceName, m)) self._log.warning(msg) raise htm.it.app.exceptions.MetricAlreadyMonitored(msg, uid=m.uid) # Add a metric row for the requested metric metricDict = repository.addMetric( conn, name=nameColumnValue, description=metricDescription, server=canonicalResourceName, location=resourceLocation, poll_interval=metricPeriod, status=MetricStatus.UNMONITORED, datasource=self._DATASOURCE, parameters=htmengine.utils.jsonEncode(modelSpec), tag_name=resourceName) metricId = metricDict["uid"] self._log.info("monitorMetric: metric=%s, stats=%r", metricId, stats) # Start monitoring scalar_metric_utils.startMonitoring( conn=conn, metricId=metricId, swarmParams=swarmParams, logger=self._log) return metricId return startMonitoringWithRetries()
def monitorMetric(self, modelSpec): """ Start monitoring a metric; create a "cloudwatch model" DAO object for the given model specification. :param modelSpec: model specification for Cloudwatch-based model :type modelSpec: dict :: { "datasource": "cloudwatch", "metricSpec": { "region": "us-west-2", "namespace": "AWS/EC2", "metric": "CPUUtilization", "dimensions": { "InstanceId": "i-12d67826" } }, # optional "modelParams": { "min": 0, # optional "max": 100 # optional } } :returns: datasource-specific unique model identifier :raises htm.it.app.exceptions.ObjectNotFoundError: if referenced metric doesn't exist :raises htm.it.app.exceptions.MetricNotSupportedError: if requested metric isn't supported :raises htm.it.app.exceptions.MetricAlreadyMonitored: if the metric is already being monitored """ metricSpec = modelSpec["metricSpec"] metricAdapter = AWSResourceAdapterBase.createMetricAdapter(metricSpec) # NOTE: getResourceName may be slow (AWS query) # TODO MER-3430: would be handy to use caching to speed things up a lot resourceName = metricAdapter.getResourceName() canonicalResourceName = self.getInstanceNameForModelSpec(modelSpec) resourceLocation = metricAdapter.getResourceLocation() metricName = metricAdapter.getMetricName() metricPeriod = metricAdapter.getMetricPeriod() metricDescription = metricAdapter.getMetricSummary() nameColumnValue = self._composeMetricNameColumnValue( metricName=metricName, metricNamespace=metricSpec["namespace"]) # Determine if the model should be started. This will happen if the # nativeMetric input includes both "min" and "max" or we have default values # for both "min" and "max" defaultMin = metricAdapter.getMetricDefaultMin() defaultMax = metricAdapter.getMetricDefaultMax() if defaultMin is None or defaultMax is None: defaultMin = defaultMax = None # Get user-provided min/max, if any modelParams = modelSpec.get("modelParams", dict()) explicitMin = modelParams.get("min") explicitMax = modelParams.get("max") if (explicitMin is None) != (explicitMax is None): raise ValueError( "min and max params must both be None or non-None; modelSpec=%r" % (modelSpec, )) minVal = explicitMin if explicitMin is not None else defaultMin maxVal = explicitMax if explicitMax is not None else defaultMax stats = {"min": minVal, "max": maxVal} swarmParams = scalar_metric_utils.generateSwarmParams(stats) # Perform the start-monitoring operation atomically/reliably @repository.retryOnTransientErrors def startMonitoringWithRetries(): """ :returns: metricId """ with self.connectionFactory() as conn: with conn.begin(): repository.lockOperationExclusive( conn, repository.OperationLock.METRICS) # Check if the metric is already monitored matchingMetrics = repository.getCloudwatchMetricsForNameAndServer( conn, nameColumnValue, canonicalResourceName, fields=[ schema.metric.c.uid, schema.metric.c.parameters ]) for m in matchingMetrics: parameters = htmengine.utils.jsonDecode(m.parameters) if (parameters["metricSpec"]["dimensions"] == metricSpec["dimensions"]): msg = ( "monitorMetric: Cloudwatch modelId=%s is already " "monitoring metric=%s on resource=%s; model=%r" % (m.uid, nameColumnValue, canonicalResourceName, m)) self._log.warning(msg) raise htm.it.app.exceptions.MetricAlreadyMonitored( msg, uid=m.uid) # Add a metric row for the requested metric metricDict = repository.addMetric( conn, name=nameColumnValue, description=metricDescription, server=canonicalResourceName, location=resourceLocation, poll_interval=metricPeriod, status=MetricStatus.UNMONITORED, datasource=self._DATASOURCE, parameters=htmengine.utils.jsonEncode(modelSpec), tag_name=resourceName) metricId = metricDict["uid"] self._log.info("monitorMetric: metric=%s, stats=%r", metricId, stats) # Start monitoring scalar_metric_utils.startMonitoring( conn=conn, metricId=metricId, swarmParams=swarmParams, logger=self._log) return metricId return startMonitoringWithRetries()