def createAutostack(self, stackSpec): """ Create an "autostack" :param stackSpec: specification for an Autostack :type stackSpec: dict :: { "name": "all_web_servers", # Autostack name "aggSpec": { # aggregation spec "datasource": "cloudwatch", "region": "us-west-2", "resourceType": "AWS::EC2::Instance" "filters": { # resourceType-specific filter "tag:Name":["*test*", "*htm-it*"], "tag:Description":["Blah", "foo"] }, } } :returns: "autostack" """ if "name" not in stackSpec or stackSpec["name"].strip() == "": raise ValueError("Must provide a valid name for the Autostack.") aggSpec = stackSpec["aggSpec"] filters = aggSpec["filters"] self._validateFilters(filters) region = aggSpec["region"] # Enforce the instances per AutoStack limit adapter = htm-it.app.adapters.datasource.createDatasourceAdapter( aggSpec["datasource"]) instances = adapter.getMatchingResources(aggSpec) if len(instances) > MAX_INSTANCES_PER_AUTOSTACK: raise htm-it.app.exceptions.TooManyInstancesError( "The filters specified match %i instances but the limit per " "AutoStack is %i." % (len(instances), MAX_INSTANCES_PER_AUTOSTACK)) name = stackSpec["name"] with self.connectionFactory() as conn: autostackDict = repository.addAutostack( conn, name=name, region=region, filters=htmengine.utils.jsonEncode(filters)) autostackObj = repository.getAutostack(conn, autostackDict["uid"]) return autostackObj
def POST(self, autostackId, data=None): # pylint: disable=C0103,R0201 """ Create one or more Autostack Metric(s) :: POST /_autostacks/{autostackId}/metrics [ { "namespace": "AWS/EC2", "metric": "CPUUtilization" }, ... ] Request body is a list of items, each of which are a subset of the standard cloudwatch native metric, specifying only: :param namespace: AWS Namespace :type namespace: str :param metric: AWS Metric name :type str: `datasource`, `region`, and `dimensions` normally required when creating models are not necessary. """ try: self.addStandardHeaders() with web.ctx.connFactory() as conn: autostackRow = repository.getAutostack(conn, autostackId) data = data or utils.jsonDecode(web.data()) for nativeMetric in data: try: if nativeMetric["namespace"] == "Autostacks": slaveDatasource = "autostack" else: slaveDatasource = "cloudwatch" # only support cloudwatch for now modelParams = {} if "min" and "max" in nativeMetric: modelParams["min"] = nativeMetric["min"] modelParams["max"] = nativeMetric["max"] modelSpec = { "datasource": "autostack", "metricSpec": { "autostackId": autostackRow.uid, "slaveDatasource": slaveDatasource, "slaveMetric": nativeMetric }, "modelParams": modelParams } metricId = (createAutostackDatasourceAdapter() .monitorMetric(modelSpec)) with web.ctx.connFactory() as conn: metricRow = repository.getMetric(conn, metricId) metricDict = convertMetricRowToMetricDict(metricRow) except KeyError: raise web.badrequest("Missing details in request") except ValueError: response = {"result": "failure"} raise web.badrequest(utils.jsonEncode(response)) response = {"result": "success", "metric": metricDict} raise web.created(utils.jsonEncode(response)) except ObjectNotFoundError: raise web.notfound("Autostack not found: Autostack ID: %s" % autostackId) except (web.HTTPError) as ex: if bool(re.match(r"([45][0-9][0-9])\s?", web.ctx.status)): # Log 400-599 status codes as errors, ignoring 200-399 log.error(str(ex) or repr(ex)) raise except Exception as ex: log.exception("POST Failed") raise web.internalerror(str(ex) or repr(ex))
def GET(self, autostackId=None): # pylint: disable=C0103 """ Get instances for known Autostack: :: GET /_autostacks/{autostackId}/instances Preview Autostack instances: :: GET /_autostacks/preview_instances?region={region}&filters={filters} :param region: AWS Region Name :type region: str :param filters: AWS Tag value pattern :type value: str (JSON object) Example query params: :: region=us-west-2&filters={"tag:Name":["jenkins-master"]} :return: List of instance details. See AutostackInstancesHandler.formatInstance() for implementation. Example return value: :: [ { "instanceID": "i-12345678", "state": "stopped", "regionName": "us-west-2", "instanceType": "m1.medium", "launchTime": "2013-09-24T02:02:48Z", "tags": { "Type": "Jenkins", "Description": "Jenkins Master", "Name": "jenkins-master" } }, { "instanceID": "i-12345678", "state": "running", "regionName": "us-west-2", "instanceType": "m1.large", "launchTime": "2013-12-19T12:02:31Z", "tags": { "Type": "Jenkins", "Name": "jenkins-master", "Description": "Jenkin Master(Python 2.7)" } } ] """ self.addStandardHeaders() aggSpec = { "datasource": "cloudwatch", # only support EC2 for now "region": None, # to be filled below "resourceType": "AWS::EC2::Instance", # only support EC2 for now "filters": None # to be filled below } adapter = createCloudwatchDatasourceAdapter() if autostackId is not None: try: with web.ctx.connFactory() as conn: autostackRow = repository.getAutostack(conn, autostackId) except ObjectNotFoundError: raise web.notfound("Autostack not found: Autostack ID: %s" % autostackId) except web.HTTPError as ex: if bool(re.match(r"([45][0-9][0-9])\s?", web.ctx.status)): # Log 400-599 status codes as errors, ignoring 200-399 log.error(str(ex) or repr(ex)) raise except Exception as ex: raise web.internalerror(str(ex) or repr(ex)) aggSpec["region"] = autostackRow.region aggSpec["filters"] = autostackRow.filters result = adapter.getMatchingResources(aggSpec) else: data = web.input(region=None, filters=None) if not data.region: raise InvalidRequestResponse({"result":"Invalid region"}) if not data.filters: raise InvalidRequestResponse({"result":"Invalid filters"}) try: aggSpec["region"] = data.region aggSpec["filters"] = utils.jsonDecode(data.filters) result = adapter.getMatchingResources(aggSpec) except boto.exception.EC2ResponseError as responseError: raise InvalidRequestResponse({"result": responseError.message}) if result: return utils.jsonEncode([self.formatInstance(instance) for instance in result]) return utils.jsonEncode([])
def monitorMetric(self, modelSpec): """ Start monitoring a metric; create a model linked to an existing Autostack :param modelSpec: model specification for an Autostack-based model :type modelSpec: dict :: { "datasource": "autostack", "metricSpec": { # TODO [MER-3533]: This should be autostack name instead "autostackId": "a858c6990a444cd8a07466ec7f3cae16", "slaveDatasource": "cloudwatch", "slaveMetric": { # specific to slaveDatasource "namespace": "AWS/EC2", "metric": "CPUUtilization" }, "period": 300 # aggregation period; seconds }, "modelParams": { # optional; specific to slave metric "min": 0, # optional "max": 100 # optional } } :returns: datasource-specific unique model identifier :raises htm-it.app.exceptions.ObjectNotFoundError: if referenced autostack 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"] autostackId = metricSpec["autostackId"] with self.connectionFactory() as conn: autostack = repository.getAutostack(conn, autostackId) slaveDatasource = metricSpec["slaveDatasource"] slaveMetric = metricSpec["slaveMetric"] canonicalResourceName = self.getInstanceNameForModelSpec(modelSpec) metricAdapter = AutostackMetricAdapterBase.getMetricAdapter(slaveDatasource) nameColumnValue = metricAdapter.getMetricName(slaveMetric) metricDescription = metricAdapter.getMetricDescription(slaveMetric, autostack) queryParams = metricAdapter.getQueryParams(nameColumnValue) defaultMin = queryParams["min"] defaultMax = queryParams["max"] defaultPeriod = queryParams["period"] modelParams = modelSpec.get("modelParams", dict()) explicitMin = modelParams.get("min") explicitMax = modelParams.get("max") explicitPeriod = metricSpec.get("period") 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 period = explicitPeriod if explicitPeriod is not None else defaultPeriod stats = {"min": minVal, "max": maxVal} if minVal is None or maxVal is None: raise ValueError("Expected min and max to be set") swarmParams = scalar_metric_utils.generateSwarmParams(stats) @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.getAutostackMetricsWithMetricName( conn, autostackId, nameColumnValue, fields=[schema.metric.c.uid]) matchingMetric = next(iter(matchingMetrics), None) if matchingMetric: msg = ("monitorMetric: Autostack modelId=%s is already " "monitoring metric=%s on resource=%s; model=%r" % (matchingMetric.uid, nameColumnValue, canonicalResourceName, matchingMetric)) self._log.warning(msg) raise htm-it.app.exceptions.MetricAlreadyMonitored( msg, uid=matchingMetric.uid) # Add a metric row for the requested metric metricDict = repository.addMetric( conn, datasource=self._DATASOURCE, name=nameColumnValue, description=metricDescription, server=canonicalResourceName, location=autostack.region, tag_name=autostack.name, parameters=htmengine.utils.jsonEncode(modelSpec), poll_interval=period, status=MetricStatus.UNMONITORED) metricId = metricDict["uid"] repository.addMetricToAutostack(conn, autostackId, metricId) # Start monitoring scalar_metric_utils.startMonitoring( conn=conn, metricId=metricId, swarmParams=swarmParams, logger=self._log) self._log.info("monitorMetric: monitoring metric=%s, stats=%r", metricId, stats) return metricId return startMonitoringWithRetries()