Example #1
0
    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*", "*YOMP*"], "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 = YOMP.app.adapters.datasource.createDatasourceAdapter(
            aggSpec["datasource"])
        instances = adapter.getMatchingResources(aggSpec)
        if len(instances) > MAX_INSTANCES_PER_AUTOSTACK:
            raise YOMP.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
Example #2
0
  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*", "*YOMP*"], "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 = YOMP.app.adapters.datasource.createDatasourceAdapter(
      aggSpec["datasource"])
    instances = adapter.getMatchingResources(aggSpec)
    if len(instances) > MAX_INSTANCES_PER_AUTOSTACK:
      raise YOMP.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
Example #3
0
    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 YOMP.app.exceptions.ObjectNotFoundError: if referenced autostack
      doesn't exist

    :raises YOMP.app.exceptions.MetricNotSupportedError: if requested metric
      isn't supported

    :raises YOMP.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 YOMP.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()
Example #4
0
  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 YOMP.app.exceptions.ObjectNotFoundError: if referenced autostack
      doesn't exist

    :raises YOMP.app.exceptions.MetricNotSupportedError: if requested metric
      isn't supported

    :raises YOMP.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 YOMP.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()
Example #5
0
  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))
Example #6
0
  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([])