diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cdbaac8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = tab +indent_size = 4 + +[*.py] +indent_style = space +indent_size = 4 diff --git a/.gitignore b/.gitignore index f1f5093..5fe2c95 100644 --- a/.gitignore +++ b/.gitignore @@ -15,11 +15,11 @@ htmlcov/ # Python egg metadata, regenerated from source files by setuptools. /*.egg-info -/*.egg +/*.eggs # Virtualenv env/ -venv/ +.venv/ # OSX .DS_Store diff --git a/.travis.yml b/.travis.yml index cdc154c..ddd8478 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,21 @@ -sudo: false +--- + +sudo: required +dist: xenial + language: python python: - '2.7' - - '3.3' - - '3.4' -install: - - pip install coveralls nose flake8 - - python setup.py install + - '3.6' + - '3.7' + +branches: + only: + - develop + - master + script: - - flake8 . - - python setup.py nosetests + - make test + after_success: coveralls diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ca18b9 --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +PATH := $(PWD)/.venv/bin:$(shell printenv PATH) +SHELL := env PATH=$(PATH) /bin/bash +VENV_DIR=.venv + +.PHONY: clean +## Destroy docker instances, remove virtualenv, molecule temp, .pyc files +clean: + rm -rf .venv + +.PHONY: deps +## Create virtualenv, install dependencies +deps: + test -d ${VENV_DIR} || virtualenv ${VENV_DIR} + ${VENV_DIR}/bin/pip install -r requirements/main.txt + virtualenv --relocatable ${VENV_DIR} + python setup.py install + +.PHONY: help +help: + @awk -v skip=1 \ + '/^##/ { sub(/^[#[:blank:]]*/, "", $$0); doc_h=$$0; doc=""; skip=0; next } \ + skip { next } \ + /^#/ { doc=doc "\n" substr($$0, 2); next } \ + /:/ { sub(/:.*/, "", $$0); \ + printf "\033[34m%-30s\033[0m\033[1m%s\033[0m %s\n\n", $$0, doc_h, doc; skip=1 }' \ + $(MAKEFILE_LIST) + +.PHONY: test +## Run tests +test: deps + flake8 newrelic_api + python setup.py nosetests diff --git a/README.rst b/README.rst index 12ae74b..e58be87 100644 --- a/README.rst +++ b/README.rst @@ -5,11 +5,11 @@ :target: https://coveralls.io/r/ambitioninc/newrelic-api?branch=develop .. image:: https://pypip.in/v/newrelic-api/badge.png - :target: https://crate.io/packages/newrelic-api/ + :target: https://pypi.python.org/pypi/newrelic-api :alt: Latest PyPI version .. image:: https://pypip.in/d/newrelic-api/badge.png - :target: https://crate.io/packages/newrelic-api/ + :target: https://pypi.python.org/pypi/newrelic-api :alt: Number of PyPI downloads newrelic-api: A Python interface to New Relic's API diff --git a/docs/Makefile b/docs/Makefile index 264bfde..771f554 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -50,7 +50,7 @@ clean: rm -rf $(BUILDDIR)/* html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + $(SPHINXBUILD) -b html -W $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/docs/examples.rst b/docs/examples.rst index 87bfb5d..74e6fa9 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -10,7 +10,7 @@ Applications & AlertPolicies Example ------------------------------------ **Scenario:** Say we want to move our application, 'Marketing Website' from -the default Alert Policy to a second Alert Policy, 'Marketing Policy'. +the default Alert Policy, 'Marketing Policy', to a second Alert Policy, 'Marketing Policy2'. First we need to get the ID for our application and the alert policy that we want to add it to: @@ -29,6 +29,7 @@ want to add it to: filter_enabled=True )['alert_policies'][0] + Next, we need to determine if our application is already in the alert_policy. Since each alert_policy dictionary in the the AlertPolicies ``.list()`` method response has an inner dictionary ``links`` with a key ``applications`` @@ -45,14 +46,24 @@ policy for 'Marketing Policy'. .. code-block:: python - if not app_in_policy: + if app_in_policy: app_ids = marketing_policy['links']['applications'] app_ids.append(website_app_id) - new_alert_policy = marketing_policy.copy() - new_alert_policy['links']['applications'] = app_ids + + new_alert_policy = AlertPolicies().list( + filter_name='Marketing Policy2', + filter_type=['application'], + filter_enabled=True + )['alert_policies'][0] + + new_alert_policy_wrapper = {"alert_policy": {"test":"test"}} + new_alert_policy_wrapper['alert_policy'] = new_alert_policy + new_alert_policy_wrapper['alert_policy']['links']['applications'] = app_ids + AlertPolicies().update( - id=new_alert_policy['id'], - policy_update=new_alert_policy + id=new_alert_policy_wrapper['alert_policy']['id'], + policy_update=new_alert_policy_wrapper ) + diff --git a/docs/index.rst b/docs/index.rst index 7d917a0..6c7804c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -14,8 +14,10 @@ Currently Supported Resources * Applications (:doc:`API Reference `) * Application Hosts (:doc:`API Reference `) * Application Instances (:doc:`API Reference `) +* Browser Applications (:doc:`API Reference `) * Components (:doc:`API Reference `) * Key Transactions (:doc:`API Reference `) +* Labels (:doc:`API Reference `) * Notification Channels (:doc:`API Reference `) * Plugins (:doc:`API Reference `) * Servers (:doc:`API Reference `) diff --git a/docs/ref/browser_applications.rst b/docs/ref/browser_applications.rst new file mode 100644 index 0000000..6acb1d7 --- /dev/null +++ b/docs/ref/browser_applications.rst @@ -0,0 +1,14 @@ +.. _ref-browser_applications: + +Browser Applications +==================== + +newrelic_api.browser_applications +--------------------------------- + +.. automodule:: newrelic_api.browser_applications +.. autoclass:: newrelic_api.browser_applications.BrowserApplications + :members: + :undoc-members: + + .. automethod:: __init__ diff --git a/docs/ref/labels.rst b/docs/ref/labels.rst new file mode 100644 index 0000000..a1909d8 --- /dev/null +++ b/docs/ref/labels.rst @@ -0,0 +1,14 @@ +.. _ref-labels: + +Labels +====== + +newrelic_api.labels +------------------- + +.. automodule:: newrelic_api.labels +.. autoclass:: newrelic_api.labels.Labels + :members: + :undoc-members: + + .. automethod:: __init__ diff --git a/docs/toc.rst b/docs/toc.rst index 9937936..ce22380 100644 --- a/docs/toc.rst +++ b/docs/toc.rst @@ -12,12 +12,13 @@ Table of Contents ref/applications ref/application_hosts ref/application_instances + ref/browser_applications ref/components ref/key_transactions + ref/labels ref/notification_channels ref/plugins ref/servers - ref/servers ref/users ref/base ref/exceptions diff --git a/newrelic_api/__init__.py b/newrelic_api/__init__.py index 80e60f8..279197c 100644 --- a/newrelic_api/__init__.py +++ b/newrelic_api/__init__.py @@ -2,10 +2,14 @@ from .version import __version__ from .alert_policies import AlertPolicies +from .alert_conditions import AlertConditions +from .alert_conditions_infra import AlertConditionsInfra +from .alert_conditions_nrql import AlertConditionsNRQL from .applications import Applications from .application_hosts import ApplicationHosts from .application_instances import ApplicationInstances from .components import Components +from .dashboards import Dashboards from .key_transactions import KeyTransactions from .notification_channels import NotificationChannels from .plugins import Plugins diff --git a/newrelic_api/alert_conditions.py b/newrelic_api/alert_conditions.py new file mode 100644 index 0000000..531184d --- /dev/null +++ b/newrelic_api/alert_conditions.py @@ -0,0 +1,362 @@ +from .base import Resource +from newrelic_api.exceptions import NoEntityException, ConfigurationException + + +class AlertConditions(Resource): + """ + An interface for interacting with the NewRelic Alert Conditions API. + """ + def list(self, policy_id, page=None): + """ + This API endpoint returns a paginated list of alert conditions associated with the + given policy_id. + + This API endpoint returns a paginated list of the alert conditions + associated with your New Relic account. Alert conditions can be filtered + by their name, list of IDs, type (application, key_transaction, or + server) or whether or not policies are archived (defaults to filtering + archived policies). + + :type policy_id: int + :param policy_id: Alert policy id + + :type page: int + :param page: Pagination index + + :rtype: dict + :return: The JSON response of the API, with an additional 'pages' key + if there are paginated results + + :: + + { + "conditions": [ + { + "id": "integer", + "type": "string", + "condition_scope": "string", + "name": "string", + "enabled": "boolean", + "entities": [ + "integer" + ], + "metric": "string", + "runbook_url": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "user_defined": { + "metric": "string", + "value_function": "string" + } + } + ] + } + + """ + filters = [ + 'policy_id={0}'.format(policy_id), + 'page={0}'.format(page) if page else None + ] + + return self._get( + url='{0}alerts_conditions.json'.format(self.URL), + headers=self.headers, + params=self.build_param_string(filters) + ) + + def update( + self, alert_condition_id, policy_id, + type=None, + condition_scope=None, + name=None, + entities=None, + metric=None, + runbook_url=None, + terms=None, + user_defined=None, + enabled=None): + """ + Updates any of the optional parameters of the alert condition + + :type alert_condition_id: int + :param alert_condition_id: Alerts condition id to update + + :type policy_id: int + :param policy_id: Alert policy id where target alert condition belongs to + + :type type: str + :param type: The type of the condition, can be apm_app_metric, + apm_kt_metric, servers_metric, browser_metric, mobile_metric + + :type condition_scope: str + :param condition_scope: The scope of the condition, can be instance or application + + :type name: str + :param name: The name of the server + + :type entities: list[str] + :param name: entity ids to which the alert condition is applied + + :type : str + :param metric: The target metric + + :type : str + :param runbook_url: The url of the runbook + + :type terms: list[hash] + :param terms: list of hashes containing threshold config for the alert + + :type user_defined: hash + :param user_defined: hash containing threshold user_defined for the alert + required if metric is set to user_defined + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :raises: This will raise a + :class:`NewRelicAPIServerException` + if target alert condition is not included in target policy + + :raises: This will raise a + :class:`ConfigurationException` + if metric is set as user_defined but user_defined config is not passed + + :: + + { + "condition": { + "id": "integer", + "type": "string", + "condition_scope": "string", + "name": "string", + "enabled": "boolean", + "entities": [ + "integer" + ], + "metric": "string", + "runbook_url": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "user_defined": { + "metric": "string", + "value_function": "string" + } + } + } + + """ + conditions_dict = self.list(policy_id) + target_condition = None + for condition in conditions_dict['conditions']: + if int(condition['id']) == alert_condition_id: + target_condition = condition + break + + if target_condition is None: + raise NoEntityException( + 'Target alert condition is not included in that policy.' + 'policy_id: {}, alert_condition_id {}'.format(policy_id, alert_condition_id) + ) + + data = { + 'condition': { + 'type': type or target_condition['type'], + 'name': name or target_condition['name'], + 'entities': entities or target_condition['entities'], + 'condition_scope': condition_scope or target_condition['condition_scope'], + 'terms': terms or target_condition['terms'], + 'metric': metric or target_condition['metric'], + 'runbook_url': runbook_url or target_condition['runbook_url'], + } + } + + if enabled is not None: + data['condition']['enabled'] = str(enabled).lower() + + if data['condition']['metric'] == 'user_defined': + if user_defined: + data['condition']['user_defined'] = user_defined + elif 'user_defined' in target_condition: + data['condition']['user_defined'] = target_condition['user_defined'] + else: + raise ConfigurationException( + 'Metric is set as user_defined but no user_defined config specified' + ) + + return self._put( + url='{0}alerts_conditions/{1}.json'.format(self.URL, alert_condition_id), + headers=self.headers, + data=data + ) + + def create( + self, policy_id, + type, + condition_scope, + name, + entities, + metric, + terms, + runbook_url=None, + user_defined=None, + enabled=True): + """ + Creates an alert condition + + :type policy_id: int + :param policy_id: Alert policy id where target alert condition belongs to + + :type type: str + :param type: The type of the condition, can be apm_app_metric, + apm_kt_metric, servers_metric, browser_metric, mobile_metric + + :type condition_scope: str + :param condition_scope: The scope of the condition, can be instance or application + + :type name: str + :param name: The name of the server + + :type entities: list[str] + :param name: entity ids to which the alert condition is applied + + :type : str + :param metric: The target metric + + :type : str + :param runbook_url: The url of the runbook + + :type terms: list[hash] + :param terms: list of hashes containing threshold config for the alert + + :type user_defined: hash + :param user_defined: hash containing threshold user_defined for the alert + required if metric is set to user_defined + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "condition": { + "id": "integer", + "type": "string", + "condition_scope": "string", + "name": "string", + "enabled": "boolean", + "entities": [ + "integer" + ], + "metric": "string", + "runbook_url": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "user_defined": { + "metric": "string", + "value_function": "string" + } + } + } + + """ + + data = { + 'condition': { + 'type': type, + 'name': name, + 'enabled': enabled, + 'entities': entities, + 'condition_scope': condition_scope, + 'terms': terms, + 'metric': metric, + 'runbook_url': runbook_url, + } + } + + if metric == 'user_defined': + if user_defined: + data['condition']['user_defined'] = user_defined + else: + raise ConfigurationException( + 'Metric is set as user_defined but no user_defined config specified' + ) + + return self._post( + url='{0}alerts_conditions/policies/{1}.json'.format(self.URL, policy_id), + headers=self.headers, + data=data + ) + + def delete(self, alert_condition_id): + """ + This API endpoint allows you to delete an alert condition + + :type alert_condition_id: integer + :param alert_condition_id: Alert Condition ID + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "condition": { + "id": "integer", + "type": "string", + "condition_scope": "string", + "name": "string", + "enabled": "boolean", + "entities": [ + "integer" + ], + "metric": "string", + "runbook_url": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "user_defined": { + "metric": "string", + "value_function": "string" + } + } + } + + """ + + return self._delete( + url='{0}alerts_conditions/{1}.json'.format(self.URL, alert_condition_id), + headers=self.headers + ) diff --git a/newrelic_api/alert_conditions_infra.py b/newrelic_api/alert_conditions_infra.py new file mode 100644 index 0000000..ae3756b --- /dev/null +++ b/newrelic_api/alert_conditions_infra.py @@ -0,0 +1,251 @@ +from .base import Resource + + +class AlertConditionsInfra(Resource): + """ + Point to the NR Infra API + """ + URL = 'https://infra-api.newrelic.com/v2/' + + """ + An interface for interacting with the NewRelic Alert Conditions Infra API. + """ + def list(self, policy_id, limit=None, offset=None): + """ + This API endpoint returns a paginated list of alert conditions for infrastucture + metrics associated with the given policy_id. + + :type policy_id: int + :param policy_id: Alert policy id + + :type limit: string + :param limit: Max amount of results to return + + :type offset: string + :param offset: Starting record to return + + :rtype: dict + :return: The JSON response of the API, with an additional 'pages' key + if there are paginated results + + :: + + { + "data": [ + { + "id": "integer", + "policy_id": "integer", + "type": "string", + "name": "string", + "enabled": "boolean", + "where_clause": "string", + "comparison": "string", + "filter": "hash", + "critical_threshold": "hash", + "process_where_clause": "string", + "created_at_epoch_millis": "time", + "updated_at_epoch_millis": "time" + } + ], + "meta": { + "limit": "integer", + "offset": "integer", + "total": "integer" + } + } + + """ + + filters = [ + 'policy_id={0}'.format(policy_id), + 'limit={0}'.format(limit) if limit else '50', + 'offset={0}'.format(offset) if offset else '0' + ] + + return self._get( + url='{0}alerts/conditions'.format(self.URL), + headers=self.headers, + params=self.build_param_string(filters) + ) + + def show(self, alert_condition_infra_id): + """ + This API endpoint returns an alert condition for infrastucture, identified by its + ID. + + :type alert_condition_infra_id: int + :param alert_condition_infra_id: Alert Condition Infra ID + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "data": { + "id": "integer", + "policy_id": "integer", + "type": "string", + "name": "string", + "enabled": "boolean", + "where_clause": "string", + "comparison": "string", + "filter": "hash", + "critical_threshold": "hash", + "event_type": "string", + "process_where_clause": "string", + "created_at_epoch_millis": "time", + "updated_at_epoch_millis": "time" + } + } + + """ + return self._get( + url='{0}alerts/conditions/{1}'.format(self.URL, alert_condition_infra_id), + headers=self.headers, + ) + + def create(self, policy_id, name, condition_type, alert_condition_configuration, enabled=True): + """ + This API endpoint allows you to create an alert condition for infrastucture + + :type policy_id: int + :param policy_id: Alert policy id + + :type name: str + :param name: The name of the alert condition + + :type condition_type: str + :param condition_type: The type of the alert condition can be + infra_process_running, infra_metric or infra_host_not_reporting + + :type alert_condition_configuration: hash + :param alert_condition_configuration: hash containing config for the alert + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "data": { + "id": "integer", + "policy_id": "integer", + "type": "string", + "name": "string", + "enabled": "boolean", + "where_clause": "string", + "comparison": "string", + "filter": "hash", + "critical_threshold": "hash", + "event_type": "string", + "process_where_clause": "string", + "created_at_epoch_millis": "time", + "updated_at_epoch_millis": "time" + } + } + + """ + + data = { + "data": alert_condition_configuration + } + + data['data']['type'] = condition_type + data['data']['policy_id'] = policy_id + data['data']['name'] = name + data['data']['enabled'] = enabled + + return self._post( + url='{0}alerts/conditions'.format(self.URL), + headers=self.headers, + data=data + ) + + def update(self, alert_condition_infra_id, policy_id, + name, condition_type, alert_condition_configuration, enabled=True): + """ + This API endpoint allows you to update an alert condition for infrastucture + + :type alert_condition_infra_id: int + :param alert_condition_infra_id: Alert Condition Infra ID + + :type policy_id: int + :param policy_id: Alert policy id + + :type name: str + :param name: The name of the alert condition + + :type condition_type: str + :param condition_type: The type of the alert condition can be + infra_process_running, infra_metric or infra_host_not_reporting + + :type alert_condition_configuration: hash + :param alert_condition_configuration: hash containing config for the alert + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "data": { + "id": "integer", + "policy_id": "integer", + "type": "string", + "name": "string", + "enabled": "boolean", + "where_clause": "string", + "comparison": "string", + "filter": "hash", + "critical_threshold": "hash", + "event_type": "string", + "process_where_clause": "string", + "created_at_epoch_millis": "time", + "updated_at_epoch_millis": "time" + } + } + + """ + + data = { + "data": alert_condition_configuration + } + + data['data']['type'] = condition_type + data['data']['policy_id'] = policy_id + data['data']['name'] = name + data['data']['enabled'] = enabled + + return self._put( + url='{0}alerts/conditions/{1}'.format(self.URL, alert_condition_infra_id), + headers=self.headers, + data=data + ) + + def delete(self, alert_condition_infra_id): + """ + This API endpoint allows you to delete an alert condition for infrastucture + + :type alert_condition_infra_id: integer + :param alert_condition_infra_id: Alert Condition Infra ID + + :rtype: dict + :return: The JSON response of the API + + :: + + {} + + """ + + return self._delete( + url='{0}alerts/conditions/{1}'.format(self.URL, alert_condition_infra_id), + headers=self.headers + ) diff --git a/newrelic_api/alert_conditions_nrql.py b/newrelic_api/alert_conditions_nrql.py new file mode 100644 index 0000000..f176999 --- /dev/null +++ b/newrelic_api/alert_conditions_nrql.py @@ -0,0 +1,394 @@ +from .base import Resource +from newrelic_api.exceptions import NoEntityException, ConfigurationException + + +class AlertConditionsNRQL(Resource): + """ + An interface for interacting with the NewRelic Alert Conditions NRQL API. + """ + def list(self, policy_id, page=None): + """ + This API endpoint returns a paginated list of alert conditions NRQL associated with the + given policy_id. + + This API endpoint returns a paginated list of the alert conditions NRQL + associated with your New Relic account. + + :type policy_id: int + :param policy_id: Alert policy id + + :type page: int + :param page: Pagination index + + :rtype: dict + :return: The JSON response of the API, with an additional 'pages' key + if there are paginated results + + :: + { + "nrql_conditions": [ + { + "type": "string", + "id": "integer", + "name": "string", + "runbook_url": "string", + "enabled": "boolean", + "expected_groups": "integer", + "ignore_overlap": "boolean", + "value_function": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "nrql": { + "query": "string", + "since_value": "string" + } + } + ] + } + """ + filters = [ + 'policy_id={0}'.format(policy_id), + 'page={0}'.format(page) if page else None + ] + + return self._get( + url='{0}alerts_nrql_conditions.json'.format(self.URL), + headers=self.headers, + params=self.build_param_string(filters) + ) + + def update( # noqa: C901 + self, alert_condition_nrql_id, policy_id, name=None, threshold_type=None, query=None, + since_value=None, terms=None, expected_groups=None, value_function=None, + runbook_url=None, ignore_overlap=None, enabled=True): + """ + Updates any of the optional parameters of the alert condition nrql + + :type alert_condition_nrql_id: int + :param alert_condition_nrql_id: Alerts condition NRQL id to update + + :type policy_id: int + :param policy_id: Alert policy id where target alert condition belongs to + + :type condition_scope: str + :param condition_scope: The scope of the condition, can be instance or application + + :type name: str + :param name: The name of the alert + + :type threshold_type: str + :param threshold_type: The tthreshold_typeype of the condition, can be static or outlier + + :type query: str + :param query: nrql query for the alerts + + :type since_value: str + :param since_value: since value for the alert + + :type terms: list[hash] + :param terms: list of hashes containing threshold config for the alert + + :type expected_groups: int + :param expected_groups: expected groups setting for outlier alerts + + :type value_function: str + :param type: value function for static alerts + + :type runbook_url: str + :param runbook_url: The url of the runbook + + :type ignore_overlap: bool + :param ignore_overlap: Whether to ignore overlaps for outlier alerts + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :raises: This will raise a + :class:`NewRelicAPIServerException` + if target alert condition is not included in target policy + + :raises: This will raise a + :class:`ConfigurationException` + if metric is set as user_defined but user_defined config is not passed + :: + { + "nrql_condition": { + "name": "string", + "runbook_url": "string", + "enabled": "boolean", + "expected_groups": "integer", + "ignore_overlap": "boolean", + "value_function": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "nrql": { + "query": "string", + "since_value": "string" + } + } + } + """ + + conditions_nrql_dict = self.list(policy_id) + target_condition_nrql = None + for condition in conditions_nrql_dict['nrql_conditions']: + if int(condition['id']) == alert_condition_nrql_id: + target_condition_nrql = condition + break + + if target_condition_nrql is None: + raise NoEntityException( + 'Target alert condition nrql is not included in that policy.' + 'policy_id: {}, alert_condition_nrql_id {}'.format( + policy_id, + alert_condition_nrql_id + ) + ) + + data = { + 'nrql_condition': { + 'type': threshold_type or target_condition_nrql['type'], + 'enabled': target_condition_nrql['enabled'], + 'name': name or target_condition_nrql['name'], + 'terms': terms or target_condition_nrql['terms'], + 'nrql': { + 'query': query or target_condition_nrql['nrql']['query'], + 'since_value': since_value or target_condition_nrql['nrql']['since_value'], + } + } + } + + if enabled is not None: + data['nrql_condition']['enabled'] = str(enabled).lower() + + if runbook_url is not None: + data['nrql_condition']['runbook_url'] = runbook_url + elif 'runbook_url' in target_condition_nrql: + data['nrql_condition']['runbook_url'] = target_condition_nrql['runbook_url'] + + if expected_groups is not None: + data['nrql_condition']['expected_groups'] = expected_groups + elif 'expected_groups' in target_condition_nrql: + data['nrql_condition']['expected_groups'] = target_condition_nrql['expected_groups'] + + if ignore_overlap is not None: + data['nrql_condition']['ignore_overlap'] = ignore_overlap + elif 'ignore_overlap' in target_condition_nrql: + data['nrql_condition']['ignore_overlap'] = target_condition_nrql['ignore_overlap'] + + if value_function is not None: + data['nrql_condition']['value_function'] = value_function + elif 'value_function' in target_condition_nrql: + data['nrql_condition']['value_function'] = target_condition_nrql['value_function'] + + if data['nrql_condition']['type'] == 'static': + if 'value_function' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as static but no value_function config specified' + ) + data['nrql_condition'].pop('expected_groups', None) + data['nrql_condition'].pop('ignore_overlap', None) + + elif data['nrql_condition']['type'] == 'outlier': + if 'expected_groups' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as outlier but expected_groups config is not specified' + ) + if 'ignore_overlap' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as outlier but ignore_overlap config is not specified' + ) + data['nrql_condition'].pop('value_function', None) + + return self._put( + url='{0}alerts_nrql_conditions/{1}.json'.format(self.URL, alert_condition_nrql_id), + headers=self.headers, + data=data + ) + + def create( + self, policy_id, name, threshold_type, query, since_value, terms, + expected_groups=None, value_function=None, runbook_url=None, + ignore_overlap=None, enabled=True): + """ + Creates an alert condition nrql + + :type policy_id: int + :param policy_id: Alert policy id where target alert condition nrql belongs to + + :type name: str + :param name: The name of the alert + + :type threshold_type: str + :param type: The threshold_type of the condition, can be static or outlier + + :type query: str + :param query: nrql query for the alerts + + :type since_value: str + :param since_value: since value for the alert + + :type terms: list[hash] + :param terms: list of hashes containing threshold config for the alert + + :type expected_groups: int + :param expected_groups: expected groups setting for outlier alerts + + :type value_function: str + :param type: value function for static alerts + + :type runbook_url: str + :param runbook_url: The url of the runbook + + :type ignore_overlap: bool + :param ignore_overlap: Whether to ignore overlaps for outlier alerts + + :type enabled: bool + :param enabled: Whether to enable that alert condition + + :rtype: dict + :return: The JSON response of the API + + :raises: This will raise a + :class:`NewRelicAPIServerException` + if target alert condition is not included in target policy + + :raises: This will raise a + :class:`ConfigurationException` + if metric is set as user_defined but user_defined config is not passed + :: + { + "nrql_condition": { + "name": "string", + "runbook_url": "string", + "enabled": "boolean", + "expected_groups": "integer", + "ignore_overlap": "boolean", + "value_function": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "nrql": { + "query": "string", + "since_value": "string" + } + } + } + """ + + data = { + 'nrql_condition': { + 'type': threshold_type, + 'name': name, + 'enabled': enabled, + 'terms': terms, + 'nrql': { + 'query': query, + 'since_value': since_value + } + } + } + + if runbook_url is not None: + data['nrql_condition']['runbook_url'] = runbook_url + + if expected_groups is not None: + data['nrql_condition']['expected_groups'] = expected_groups + + if ignore_overlap is not None: + data['nrql_condition']['ignore_overlap'] = ignore_overlap + + if value_function is not None: + data['nrql_condition']['value_function'] = value_function + + if data['nrql_condition']['type'] == 'static': + if 'value_function' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as static but no value_function config specified' + ) + data['nrql_condition'].pop('expected_groups', None) + data['nrql_condition'].pop('ignore_overlap', None) + + elif data['nrql_condition']['type'] == 'outlier': + if 'expected_groups' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as outlier but expected_groups config is not specified' + ) + if 'ignore_overlap' not in data['nrql_condition']: + raise ConfigurationException( + 'Alert is set as outlier but ignore_overlap config is not specified' + ) + data['nrql_condition'].pop('value_function', None) + + return self._post( + url='{0}alerts_nrql_conditions/policies/{1}.json'.format(self.URL, policy_id), + headers=self.headers, + data=data + ) + + def delete(self, alert_condition_nrql_id): + """ + This API endpoint allows you to delete an alert condition nrql + + :type alert_condition_nrql_id: integer + :param alert_condition_nrql_id: Alert Condition ID + + :rtype: dict + :return: The JSON response of the API + + :: + { + "nrql_condition": { + "type": "string", + "id": "integer", + "name": "string", + "runbook_url": "string", + "enabled": "boolean", + "expected_groups": "integer", + "ignore_overlap": "boolean", + "value_function": "string", + "terms": [ + { + "duration": "string", + "operator": "string", + "priority": "string", + "threshold": "string", + "time_function": "string" + } + ], + "nrql": { + "query": "string", + "since_value": "string" + } + } + } + + """ + + return self._delete( + url='{0}alerts_nrql_conditions/{1}.json'.format(self.URL, alert_condition_nrql_id), + headers=self.headers + ) diff --git a/newrelic_api/alert_policies.py b/newrelic_api/alert_policies.py index b894915..49e8526 100644 --- a/newrelic_api/alert_policies.py +++ b/newrelic_api/alert_policies.py @@ -5,28 +5,15 @@ class AlertPolicies(Resource): """ An interface for interacting with the NewRelic Alert Policies API. """ - def list( - self, filter_name=None, filter_type=None, filter_ids=None, - filter_enabled=None, page=None): + def list(self, filter_name=None, page=None): """ This API endpoint returns a paginated list of the alert policies associated with your New Relic account. Alert policies can be filtered - by their name, list of IDs, type (application, key_transaction, or - server) or whether or not policies are archived (defaults to filtering - archived policies). + by their name with exact match. :type filter_name: str :param filter_name: Filter by name - :type filter_type: list of str - :param filter_type: Filter by policy types. - - :type filter_ids: list of int - :param filter_ids: Filter by policy IDs - - :type filter_enabled: bool - :param filter_enabled: Select only enabled/disabled policies (default: both) - :type page: int :param page: Pagination index @@ -37,71 +24,38 @@ def list( :: { - "alert_policies": [ + "policies": [ { "id": "integer", - "type": "string", + "incident_preference": "string", "name": "string", - "enabled": "boolean", - "conditions": [ - { - "id": "integer", - "type": "string", - "severity": "string", - "threshold": "float", - "trigger_minutes": "integer", - "enabled": "boolean" - } - ], - "links": { - "notification_channels": [ - "integer" - ], - "applications": [ - "integer" - ], - "key_transactions": [ - "integer" - ], - "servers": [ - "integer" - ] - } - } - ], - "pages": { - "last": { - "url": "https://api.newrelic.com/v2/alert_policies.json?page=2", - "rel": "last" + "created_at": "integer", }, - "next": { - "url": "https://api.newrelic.com/v2/alert_policies.json?page=2", - "rel": "next" - } - } + ] } """ filters = [ 'filter[name]={0}'.format(filter_name) if filter_name else None, - 'filter[type]={0}'.format(','.join(filter_type)) if filter_type else None, - 'filter[ids]={0}'.format(','.join([str(app_id) for app_id in filter_ids])) if filter_ids else None, - 'filter[enabled]={0}'.format(filter_enabled) if filter_enabled in [True, False] else None, 'page={0}'.format(page) if page else None ] return self._get( - url='{0}alert_policies.json'.format(self.URL), + url='{0}alerts_policies.json'.format(self.URL), headers=self.headers, params=self.build_param_string(filters) ) - def show(self, id): + def create(self, name, incident_preference): """ - This API endpoint returns a single alert policy, identified by ID. + This API endpoint allows you to create an alert policy - :type id: int - :param id: Alert policy ID + :type name: str + :param name: The name of the policy + + :type incident_preference: str + :param incident_preference: Can be PER_POLICY, PER_CONDITION or + PER_CONDITION_AND_TARGET :rtype: dict :return: The JSON response of the API @@ -109,90 +63,80 @@ def show(self, id): :: { - "alert_policy": { + "policy": { + "created_at": "time", "id": "integer", - "type": "string", + "incident_preference": "string", "name": "string", - "enabled": "boolean", - "conditions": [ - { - "id": "integer", - "type": "string", - "severity": "string", - "threshold": "float", - "trigger_minutes": "integer", - "enabled": "boolean" - } - ], - "links": { - "notification_channels": [ - "integer" - ], - "applications": [ - "integer" - ], - "key_transactions": [ - "integer" - ], - "servers": [ - "integer" - ] - } + "updated_at": "time" } } """ - return self._get( - url='{0}alert_policies/{1}.json'.format(self.URL, id), + + data = { + "policy": { + "name": name, + "incident_preference": incident_preference + } + } + + return self._post( + url='{0}alerts_policies.json'.format(self.URL), headers=self.headers, + data=data ) - def update(self, id, policy_update): + def update(self, id, name, incident_preference): """ - This API endpoint allows you to update your alert policies. + This API endpoint allows you to update an alert policy + + :type id: integer + :param id: The id of the policy + + :type name: str + :param name: The name of the policy + + :type incident_preference: str + :param incident_preference: Can be PER_POLICY, PER_CONDITION or + PER_CONDITION_AND_TARGET - The input is expected to be in **JSON** format in the body - parameters of the PUT request. The exact schema is defined below. Any - extra parameters passed in the body **will be ignored** .:: + :rtype: dict + :return: The JSON response of the API + + :: { - "alert_policy": { - "name": str, - "enabled": bool, - "conditions": [ - { - "id": int, - "threshold": float, - "trigger_minutes": int, - "enabled": bool - } - ], - "links": { - "notification_channels": [ - int - ], - "applications": [ - int - ], - "key_transactions": [ - "int - ], - "servers": [ - int - ] - } + "policy": { + "created_at": "time", + "id": "integer", + "incident_preference": "string", + "name": "string", + "updated_at": "time" } } - **NOTE:** When updating alertable and notification channel links, the - list sent replaces the existing list. Invalid values will be ignored - but an empty array will result in alertables/channels being reset. + """ - :type id: int - :param id: Alert policy ID + data = { + "policy": { + "name": name, + "incident_preference": incident_preference + } + } - :type policy_update: dict - :param policy_update: The json of the policy to update + return self._put( + url='{0}alerts_policies/{1}.json'.format(self.URL, id), + headers=self.headers, + data=data + ) + + def delete(self, id): + """ + This API endpoint allows you to delete an alert policy + + :type id: integer + :param id: The id of the policy :rtype: dict :return: The JSON response of the API @@ -200,41 +144,91 @@ def update(self, id, policy_update): :: { - "alert_policy": { + "policy": { + "created_at": "time", "id": "integer", - "type": "string", + "incident_preference": "string", "name": "string", - "enabled": "boolean", - "conditions": [ - { - "id": "integer", - "type": "string", - "severity": "string", - "threshold": "float", - "trigger_minutes": "integer", - "enabled": "boolean" - } - ], - "links": { - "notification_channels": [ - "integer" - ], - "applications": [ - "integer" - ], - "key_transactions": [ - "integer" - ], - "servers": [ - "integer" - ] - } + "updated_at": "time" } } """ + + return self._delete( + url='{0}alerts_policies/{1}.json'.format(self.URL, id), + headers=self.headers + ) + + def associate_with_notification_channel(self, id, channel_id): + """ + This API endpoint allows you to associate an alert policy with an + notification channel + + :type id: integer + :param id: The id of the policy + + :type channel_id: integer + :param channel_id: The id of the notification channel + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "policy": { + "channel_ids": "list", + "id": "integer" + } + } + + """ + return self._put( - url='{0}alert_policies/{1}.json'.format(self.URL, id), - headers=self.headers, - data=policy_update + url='{0}alerts_policy_channels.json?policy_id={1}&channel_ids={2}'.format( + self.URL, + id, + channel_id + ), + headers=self.headers + ) + + def dissociate_from_notification_channel(self, id, channel_id): + """ + This API endpoint allows you to dissociate an alert policy from an + notification channel + + :type id: integer + :param id: The id of the policy + + :type channel_id: integer + :param channel_id: The id of the notification channel + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "channel":{ + "configuration": "hash", + "type": "string", + "id": "integer", + "links":{ + "policy_ids": "list" + }, + "name": "string" + } + } + + """ + + return self._delete( + url='{0}alerts_policy_channels.json?policy_id={1}&channel_id={2}'.format( + self.URL, + id, + channel_id + ), + headers=self.headers ) diff --git a/newrelic_api/base.py b/newrelic_api/base.py index 4f77fa8..871581b 100644 --- a/newrelic_api/base.py +++ b/newrelic_api/base.py @@ -69,6 +69,7 @@ def _put(self, *args, **kwargs): response = requests.put(*args, **kwargs) if not response.ok: raise NewRelicAPIServerException('{}: {}'.format(response.status_code, response.text)) + return response.json() def _post(self, *args, **kwargs): @@ -87,6 +88,7 @@ def _post(self, *args, **kwargs): response = requests.post(*args, **kwargs) if not response.ok: raise NewRelicAPIServerException('{}: {}'.format(response.status_code, response.text)) + return response.json() def _delete(self, *args, **kwargs): @@ -103,7 +105,11 @@ def _delete(self, *args, **kwargs): response = requests.delete(*args, **kwargs) if not response.ok: raise NewRelicAPIServerException('{}: {}'.format(response.status_code, response.text)) - return response.json() + + if response.text: + return response.json() + + return {} def build_param_string(self, params): """ diff --git a/newrelic_api/browser_applications.py b/newrelic_api/browser_applications.py index 5dea992..4bde4c5 100644 --- a/newrelic_api/browser_applications.py +++ b/newrelic_api/browser_applications.py @@ -8,10 +8,10 @@ class BrowserApplications(Resource): def list(self, filter_name=None, filter_ids=None, page=None): """ This API endpoint returns a list of the Browser Applications associated - with your New Relic account. + with your New Relic account. Browser Applications can be filtered by their name, or by the - application IDs. + application IDs. :type filter_name: str :param filter_name: Filter by application name diff --git a/newrelic_api/dashboards.py b/newrelic_api/dashboards.py new file mode 100644 index 0000000..b3c58c7 --- /dev/null +++ b/newrelic_api/dashboards.py @@ -0,0 +1,272 @@ +from .base import Resource + + +class Dashboards(Resource): + """ + An interface for interacting with the NewRelic dashboard API. + """ + def list(self, filter_title=None, filter_ids=None, page=None): + """ + :type filter_title: str + :param filter_title: Filter by dashboard title + + :type filter_ids: list of ints + :param filter_ids: Filter by dashboard ids + + :type page: int + :param page: Pagination index + + :rtype: dict + :return: The JSON response of the API, with an additional 'page' key + if there are paginated results + + :: + + { + "dashboards": [ + { + "id": "integer", + "title": "string", + "description": "string", + "icon": "string", + "created_at": "time", + "updated_at": "time", + "visibility": "string", + "editable": "string", + "ui_url": "string", + "api_url": "string", + "owner_email": "string", + "filter": { + "event_types": ["string"], + "attributes": ["string"] + } + } + ], + "pages": { + "last": { + "url": "https://api.newrelic.com/v2/dashboards.json?page=1&per_page=100", + "rel": "last" + }, + "next": { + "url": "https://api.newrelic.com/v2/dashboards.json?page=1&per_page=100", + "rel": "next" + } + } + } + """ + filters = [ + 'filter[title]={0}'.format(filter_title) if filter_title else None, + 'filter[ids]={0}'.format(','.join([str(dash_id) for dash_id in filter_ids])) if filter_ids else None, + 'page={0}'.format(page) if page else None + ] + return self._get( + url='{0}dashboards.json'.format(self.URL), + headers=self.headers, + params=self.build_param_string(filters) + ) + + def show(self, id): + """ + This API endpoint returns a single Dashboard, identified by its ID. + + :type id: int + :param id: Dashboard ID + + :rtype: dict + :return: The JSON response of the API + + :: + { + "dashboard": { + "id": "integer", + "title": "string", + "description": "string", + "icon": "string", + "created_at": "string", + "updated_at": "string", + "visibility": "string", + "editable": "string", + "ui_url": "string", + "api_url": "string", + "owner_email": "string", + "metadata": { + "version": "integer" + }, + "widgets": [ + { + "visualization": "string", + "layout": { + "width": "integer", + "height": "integer", + "row": "integer", + "column": "integer" + }, + "widget_id": "integer", + "account_id": "integer", + "data": [ + { + "nrql": "string" + } + ], + "presentation": { + "title": "string", + "notes": "string" + } + } + ], + "filter": { + "event_types": ["string"], + "attributes": ["string"] + } + } + } + """ + return self._get( + url='{0}dashboards/{1}.json'.format(self.URL, id), + headers=self.headers, + ) + + def delete(self, id): + """ + This API endpoint deletes a dashboard and all its widgets. + + :type id: int + :param id: Dashboard ID + + :rtype: dict + :return: The JSON response of the API + + :: + { + "dashboard": { + "id": "integer" + } + } + """ + return self._delete( + url='{0}dashboards/{1}.json'.format(self.URL, id), + headers=self.headers, + ) + + def create(self, dashboard_data): + """ + This API endpoint creates a dashboard and all defined widgets. + + :type dashboard: dict + :param dashboard: Dashboard Dictionary + + :rtype dict + :return: The JSON response of the API + + :: + { + "dashboard": { + "id": "integer", + "title": "string", + "description": "string", + "icon": "string", + "created_at": "time", + "updated_at": "time", + "visibility": "string", + "editable": "string", + "ui_url": "string", + "api_url": "string", + "owner_email": "string", + "metadata": { + "version": "integer" + }, + "widgets": [ + { + "visualization": "string", + "layout": { + "width": "integer", + "height": "integer", + "row": "integer", + "column": "integer" + }, + "widget_id": "integer", + "account_id": "integer", + "data": [ + "nrql": "string" + ], + "presentation": { + "title": "string", + "notes": "string" + } + } + ], + "filter": { + "event_types": ["string"], + "attributes": ["string"] + } + } + } + """ + return self._post( + url='{0}dashboards.json'.format(self.URL), + headers=self.headers, + data=dashboard_data, + ) + + def update(self, id, dashboard_data): + """ + This API endpoint updates a dashboard and all defined widgets. + + :type id: int + :param id: Dashboard ID + + :type dashboard: dict + :param dashboard: Dashboard Dictionary + + :rtype dict + :return: The JSON response of the API + + :: + { + "dashboard": { + "id": "integer", + "title": "string", + "description": "string", + "icon": "string", + "created_at": "time", + "updated_at": "time", + "visibility": "string", + "editable": "string", + "ui_url": "string", + "api_url": "string", + "owner_email": "string", + "metadata": { + "version": "integer" + }, + "widgets": [ + { + "visualization": "string", + "layout": { + "width": "integer", + "height": "integer", + "row": "integer", + "column": "integer" + }, + "widget_id": "integer", + "account_id": "integer", + "data": [ + "nrql": "string" + ], + "presentation": { + "title": "string", + "notes": "string" + } + } + ], + "filter": { + "event_types": ["string"], + "attributes": ["string"] + } + } + } + """ + return self._put( + url='{0}dashboards/{1}.json'.format(self.URL, id), + headers=self.headers, + data=dashboard_data, + ) diff --git a/newrelic_api/exceptions.py b/newrelic_api/exceptions.py index 6bfc3a8..ab9dc31 100644 --- a/newrelic_api/exceptions.py +++ b/newrelic_api/exceptions.py @@ -10,3 +10,10 @@ class NewRelicAPIServerException(Exception): An exception for New Relic server errors """ message = 'There was an error from New Relic' + + +class NoEntityException(Exception): + """ + An exception for operation to no existed entities + """ + message = 'No entity exists' diff --git a/newrelic_api/labels.py b/newrelic_api/labels.py index 9cc7cc3..084fe84 100644 --- a/newrelic_api/labels.py +++ b/newrelic_api/labels.py @@ -62,7 +62,7 @@ def list(self, page=None): def create(self, name, category, applications=None, servers=None): """ This API endpoint will create a new label with the provided name and - category + category :type name: str :param name: The name of the label @@ -119,7 +119,7 @@ def create(self, name, category, applications=None, servers=None): def delete(self, key): """ When applications are provided, this endpoint will remove those - applications from the label. + applications from the label. When no applications are provided, this endpoint will remove the label. diff --git a/newrelic_api/notification_channels.py b/newrelic_api/notification_channels.py index 2ec97f3..14ec5ea 100644 --- a/newrelic_api/notification_channels.py +++ b/newrelic_api/notification_channels.py @@ -5,19 +5,11 @@ class NotificationChannels(Resource): """ An interface for interacting with the NewRelic Notification Channels API. """ - def list(self, filter_type=None, filter_ids=None, page=None): + def list(self, page=None): """ This API endpoint returns a paginated list of the notification channels associated with your New Relic account. - Notification channels can be filtered by their type or a list of IDs. - - :type filter_type: list of str - :param filter_type: Filter by notification channel types - - :type filter_ids: list of int - :param filter_ids: Filter by notification channel ids - :type page: int :param page: Pagination index @@ -26,28 +18,88 @@ def list(self, filter_type=None, filter_ids=None, page=None): if there are paginated results """ filters = [ - 'filter[type]={0}'.format(','.join(filter_type)) if filter_type else None, - 'filter[ids]={0}'.format(','.join([str(app_id) for app_id in filter_ids])) if filter_ids else None, 'page={0}'.format(page) if page else None ] return self._get( - url='{0}notification_channels.json'.format(self.URL), + url='{0}alerts_channels.json'.format(self.URL), headers=self.headers, params=self.build_param_string(filters) ) - def show(self, id): + def create(self, name, type, configuration): """ - This API endpoint returns a single notification channel, identified by - ID. + This API endpoint allows you to create a notification channel, see + New Relic API docs for details of types and configuration - :type id: int - :param id: notification channel ID + :type name: str + :param name: The name of the channel + + :type type: str + :param type: Type of notification, eg. email, user, webhook + + :type configuration: hash + :param configuration: Configuration for notification :rtype: dict :return: The JSON response of the API + + :: + + { + "channels": { + "id": "integer", + "name": "string", + "type": "string", + "configuration": { }, + "links": { + "policy_ids": [] + } + } + } + """ - return self._get( - url='{0}notification_channels/{1}.json'.format(self.URL, id), + + data = { + "channel": { + "name": name, + "type": type, + "configuration": configuration + } + } + + return self._post( + url='{0}alerts_channels.json'.format(self.URL), headers=self.headers, + data=data + ) + + def delete(self, id): + """ + This API endpoint allows you to delete a notification channel + + :type id: integer + :param id: The id of the channel + + :rtype: dict + :return: The JSON response of the API + + :: + + { + "channels": { + "id": "integer", + "name": "string", + "type": "string", + "configuration": { }, + "links": { + "policy_ids": [] + } + } + } + + """ + + return self._delete( + url='{0}alerts_channels/{1}.json'.format(self.URL, id), + headers=self.headers ) diff --git a/newrelic_api/tests/alert_conditions_infra_tests.py b/newrelic_api/tests/alert_conditions_infra_tests.py new file mode 100644 index 0000000..820349c --- /dev/null +++ b/newrelic_api/tests/alert_conditions_infra_tests.py @@ -0,0 +1,168 @@ +from unittest import TestCase + +from mock import patch, Mock +import requests + +from newrelic_api.alert_conditions_infra import AlertConditionsInfra + + +class NRAlertConditionsInfraInfraTests(TestCase): + def setUp(self): + super(NRAlertConditionsInfraInfraTests, self).setUp() + self.alert_conditions_infra = AlertConditionsInfra(api_key='dummy_key') + + self.list_success_response = { + "meta": { + "total": 1, + "limit": 50, + "offset": 0 + }, + "data": [ + { + "comparison": "above", + "select_value": "cpuPercent", + "created_at_epoch_millis": 1532946280004, + "name": "CPU usage alert", + "enabled": "true", + "updated_at_epoch_millis": 1532947363110, + "event_type": "SystemSample", + "critical_threshold": { + "duration_minutes": 1, + "value": 50, + "time_function": "all" + }, + "type": "infra_metric", + "id": 100, + "policy_id": 1 + } + ], + "links": {} + } + + self.single_success_response = { + "data": { + "comparison": "above", + "select_value": "cpuPercent", + "created_at_epoch_millis": 1532946280004, + "name": "CPU usage alert", + "enabled": "true", + "updated_at_epoch_millis": 1532947363110, + "event_type": "SystemSample", + "critical_threshold": { + "duration_minutes": 1, + "value": 50, + "time_function": "all" + }, + "type": "infra_metric", + "id": 100, + "policy_id": 1 + } + } + + @patch.object(requests, 'get') + def test_list_success(self, mock_get): + """ + Test alert conditions .list() + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.list_success_response + mock_get.return_value = mock_response + + # Call the method + response = self.alert_conditions_infra.list(policy_id=1) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + def test_list_failure(self, mock_get): + """ + Test alert conditions .list() failure case + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_response + + with self.assertRaises(ValueError): + # Call the method + self.alert_conditions_infra.list(policy_id=1) + + @patch.object(requests, 'get') + def test_show_success(self, mock_get): + """ + Test alert conditions .show() + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.single_success_response + mock_get.return_value = mock_response + + # Call the method + response = self.alert_conditions_infra.show( + alert_condition_infra_id=100 + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'post') + def test_create_success(self, mock_post): + """ + Test alerts_conditions .update() success + """ + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_post.return_value = mock_update_response + + # Call the method + response = self.alert_conditions_infra.create( + policy_id=1, name='New Name', condition_type='infra_metric', + alert_condition_configuration=self.single_success_response['data'] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'put') + def test_update_success(self, mock_put): + """ + Test alerts_conditions .update() success + """ + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_put.return_value = mock_update_response + + # Call the method + response = self.alert_conditions_infra.update( + alert_condition_infra_id=100, policy_id=1, name='New Name', + condition_type='infra_metric', + alert_condition_configuration=self.single_success_response['data'] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'put') + def test_update_failure(self, mock_put): + """ + Test alerts_conditions .update() failure + """ + mock_update_response = Mock(name='response') + mock_update_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_put.return_value = mock_update_response + + # Call the method + with self.assertRaises(ValueError): + self.alert_conditions_infra.update( + alert_condition_infra_id=100, policy_id=1, name='New Name', + condition_type='infra_metric', + alert_condition_configuration=self.single_success_response['data'] + ) + + @patch.object(requests, 'delete') + def test_delete_success(self, mock_delete): + """ + Test alert policies .delete() success + """ + + self.alert_conditions_infra.delete(alert_condition_infra_id=100) + + mock_delete.assert_called_once_with( + url='https://infra-api.newrelic.com/v2/alerts/conditions/100', + headers=self.alert_conditions_infra.headers + ) diff --git a/newrelic_api/tests/alert_conditions_nrql_tests.py b/newrelic_api/tests/alert_conditions_nrql_tests.py new file mode 100644 index 0000000..fe72c19 --- /dev/null +++ b/newrelic_api/tests/alert_conditions_nrql_tests.py @@ -0,0 +1,204 @@ +from unittest import TestCase + +from mock import patch, Mock +import requests + +from newrelic_api.alert_conditions_nrql import AlertConditionsNRQL +from newrelic_api.exceptions import NoEntityException + + +class NRAlertConditionsNRQLTests(TestCase): + def setUp(self): + super(NRAlertConditionsNRQLTests, self).setUp() + self.alert_conditions_nrql = AlertConditionsNRQL(api_key='dummy_key') + + self.list_success_response = { + "nrql_conditions": [ + { + "type": "static", + "id": 100, + "name": "5xx alert", + "enabled": True, + "value_function": "single_value", + "terms": [ + { + "duration": "15", + "operator": "above", + "priority": "critical", + "threshold": "10", + "time_function": "all" + } + ], + "nrql": { + "query": "SELECT something WHERE something = 'somevalue'", + "since_value": "3" + } + } + ] + } + + self.single_success_response = { + "nrql_condition": { + "type": "static", + "id": 100, + "name": "5xx alert", + "enabled": True, + "value_function": "single_value", + "terms": [ + { + "duration": "15", + "operator": "above", + "priority": "critical", + "threshold": "10", + "time_function": "all" + } + ], + "nrql": { + "query": "SELECT something WHERE something = 'somevalue'", + "since_value": "3" + } + } + } + + @patch.object(requests, 'get') + def test_list_success(self, mock_get): + """ + Test alert conditions nrql .list() + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.list_success_response + mock_get.return_value = mock_response + + # Call the method + response = self.alert_conditions_nrql.list(policy_id=1) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + def test_list_failure(self, mock_get): + """ + Test alert conditions nrql .list() failure case + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_response + + with self.assertRaises(ValueError): + # Call the method + self.alert_conditions_nrql.list(policy_id=1) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_success(self, mock_put, mock_get): + """ + Test alert_conditions_nrql .update() success + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + # Call the method + response = self.alert_conditions_nrql.update( + alert_condition_nrql_id=100, + policy_id=1, + name='New Name', + threshold_type='static', + query="SELECT something WHERE something = 'somevalue'", + since_value='3', + runbook_url='http://example.com/', + value_function='single_value', + terms=[{ + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + }] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_failure(self, mock_put, mock_get): + """ + Test alert_conditions_nrql .update() failure + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + # Call the method + with self.assertRaises(ValueError): + self.alert_conditions_nrql.update( + alert_condition_nrql_id=100, + policy_id=1 + ) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_no_alert_condition(self, mock_put, mock_get): + """ + Test alert_conditions_nrql .update() success + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + with self.assertRaises(NoEntityException): + # Call the method with non existing alert_condition_nrql_id + self.alert_conditions_nrql.update( + alert_condition_nrql_id=9999, + policy_id=1 + ) + + @patch.object(requests, 'post') + def test_create_success(self, mock_post): + """ + Test alert_conditions_nrql .update() success + """ + mock_create_response = Mock(name='response') + mock_create_response.json.return_value = self.single_success_response + mock_post.return_value = mock_create_response + + # Call the method + response = self.alert_conditions_nrql.create( + policy_id=1, + name='New Name', + threshold_type='static', + query="SELECT something WHERE something = 'somevalue'", + since_value='3', + runbook_url='http://example.com/', + value_function='single_value', + terms=[{ + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + }] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'delete') + def test_delete_success(self, mock_delete): + """ + Test alert policies .delete() success + """ + + self.alert_conditions_nrql.delete(alert_condition_nrql_id=100) + + mock_delete.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_nrql_conditions/100.json', + headers=self.alert_conditions_nrql.headers + ) diff --git a/newrelic_api/tests/alert_conditions_tests.py b/newrelic_api/tests/alert_conditions_tests.py new file mode 100644 index 0000000..b78619a --- /dev/null +++ b/newrelic_api/tests/alert_conditions_tests.py @@ -0,0 +1,217 @@ +from unittest import TestCase + +from mock import patch, Mock +import requests + +from newrelic_api.alert_conditions import AlertConditions +from newrelic_api.exceptions import NoEntityException, ConfigurationException + + +class NRAlertConditionsTests(TestCase): + def setUp(self): + super(NRAlertConditionsTests, self).setUp() + self.alert_conditions = AlertConditions(api_key='dummy_key') + + self.list_success_response = { + "conditions": [ + { + "id": "100", + "type": "servers_metric", + "name": "CPU usage alert", + "condition_scope": "application", + "enabled": True, + "entities": [ + "1234567" + ], + "metric": "cpu_percentage", + "terms": [ + { + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + } + ], + "runbook_url": "http://example.com/" + } + ] + } + + self.single_success_response = { + "condition": { + "id": "100", + "type": "servers_metric", + "name": "CPU usage alert", + "condition_scope": "application", + "enabled": True, + "entities": [ + "1234567" + ], + "metric": "cpu_percentage", + "terms": [ + { + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + } + ], + "runbook_url": "http://example.com/" + } + } + + @patch.object(requests, 'get') + def test_list_success(self, mock_get): + """ + Test alert conditions .list() + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.list_success_response + mock_get.return_value = mock_response + + # Call the method + response = self.alert_conditions.list(policy_id=1) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + def test_list_failure(self, mock_get): + """ + Test alert conditions .list() failure case + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_response + + with self.assertRaises(ValueError): + # Call the method + self.alert_conditions.list(policy_id=1) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_success(self, mock_put, mock_get): + """ + Test alerts_conditions .update() success + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + response = self.alert_conditions.update( + alert_condition_id=100, + policy_id=1, + name='New Name', + type='servers_metric', + condition_scope='application', + entities=['1234567'], + metric='cpu_percentage', + runbook_url='http://example.com/', + terms=[{ + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + }] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_failure(self, mock_put, mock_get): + """ + Test alerts_conditions .update() failure + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + with self.assertRaises(NoEntityException): + self.alert_conditions.update( + alert_condition_id=1000, + policy_id=1 + ) + + with self.assertRaises(ConfigurationException): + self.alert_conditions.update( + alert_condition_id=100, + policy_id=1, + metric='user_defined' + ) + + with self.assertRaises(ValueError): + self.alert_conditions.update( + alert_condition_id=100, + policy_id=1 + ) + + @patch.object(requests, 'get') + @patch.object(requests, 'put') + def test_update_no_alert_condition(self, mock_put, mock_get): + """ + Test alerts_conditions .update() success + """ + mock_list_response = Mock(name='response') + mock_list_response.json.return_value = self.list_success_response + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_get.return_value = mock_list_response + mock_put.return_value = mock_update_response + + with self.assertRaises(NoEntityException): + # Call the method with non existing alert_condition_id + self.alert_conditions.update( + alert_condition_id=9999, + policy_id=1 + ) + + @patch.object(requests, 'post') + def test_create_success(self, mock_post): + """ + Test alerts_conditions .update() success + """ + mock_create_response = Mock(name='response') + mock_create_response.json.return_value = self.single_success_response + mock_post.return_value = mock_create_response + + # Call the method + response = self.alert_conditions.create( + policy_id=1, + name='New Name', + type='servers_metric', + condition_scope='application', + entities=['1234567'], + metric='cpu_percentage', + runbook_url='http://example.com/', + terms=[{ + "duration": "5", + "operator": "above", + "priority": "above", + "threshold": "90", + "time_function": "all" + }] + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'delete') + def test_delete_success(self, mock_delete): + """ + Test alert policies .delete() success + """ + + self.alert_conditions.delete(alert_condition_id=100) + + mock_delete.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_conditions/100.json', + headers=self.alert_conditions.headers + ) diff --git a/newrelic_api/tests/alert_policies_tests.py b/newrelic_api/tests/alert_policies_tests.py index 7fa1182..1591398 100644 --- a/newrelic_api/tests/alert_policies_tests.py +++ b/newrelic_api/tests/alert_policies_tests.py @@ -1,7 +1,7 @@ -import json from unittest import TestCase from mock import patch, Mock +import json import requests from newrelic_api.alert_policies import AlertPolicies @@ -13,103 +13,30 @@ def setUp(self): self.policies = AlertPolicies(api_key='dummy_key') self.policies_list_response = { - "alert_policies": [ + "policies": [ { "id": 12345, - "type": "server", + "incident_preference": "PER_CONDITION_AND_TARGET", "name": "Default Server Policy", - "enabled": True, - "conditions": [ - { - "id": 347535, - "type": "disk_io", - "severity": "caution", - "threshold": 70, - "trigger_minutes": 20, - "enabled": True - }, - { - "id": 347536, - "type": "disk_io", - "severity": "critical", - "threshold": 90, - "trigger_minutes": 15, - "enabled": True - }, - { - "id": 347537, - "type": "fullest_disk", - "severity": "caution", - "threshold": 70, - "trigger_minutes": 10, - "enabled": True - }, - { - "id": 347538, - "type": "fullest_disk", - "severity": "critical", - "threshold": 90, - "trigger_minutes": 5, - "enabled": True - }, - { - "id": 347539, - "type": "memory", - "severity": "caution", - "threshold": 80, - "trigger_minutes": 10, - "enabled": True - }, - { - "id": 347540, - "type": "memory", - "severity": "critical", - "threshold": 95, - "trigger_minutes": 5, - "enabled": True - }, - { - "id": 347541, - "type": "cpu", - "severity": "caution", - "threshold": 60, - "trigger_minutes": 20, - "enabled": True - }, - { - "id": 347542, - "type": "cpu", - "severity": "critical", - "threshold": 90, - "trigger_minutes": 15, - "enabled": True - }, - { - "id": 347543, - "type": "server_downtime", - "severity": "downtime", - "trigger_minutes": 5, - "enabled": True - } - ], - "links": { - "notification_channels": [ - 333444 - ], - "servers": [ - 1234567, - 2345678, - 3456789, - 4567890, - 5678901, - 6789012 - ] - } + "created_at": 123456789012, } ] } - self.policy_show_response = { - 'alert_policy': self.policies_list_response['alert_policies'][0] + self.policy_single_response = { + "policy": self.policies_list_response['policies'][0] + } + self.channel_single_response = { + "channel": { + "id": 111222, + "type": "user", + "name": "Some User", + "links": { + "policy_ids": [] + }, + "configuration": { + "user": 222333 + } + } } @patch.object(requests, 'get') @@ -127,7 +54,7 @@ def test_list_success(self, mock_get): self.assertIsInstance(response, dict) @patch.object(requests, 'get') - def test_list_success_with_ids(self, mock_get): + def test_list_success_with_name(self, mock_get): """ Test alert policies .list() with filter_ids """ @@ -136,13 +63,13 @@ def test_list_success_with_ids(self, mock_get): mock_get.return_value = mock_response # Call the method - response = self.policies.list(filter_ids=[12345]) + response = self.policies.list(filter_name='Default Server Policy') self.assertIsInstance(response, dict) mock_get.assert_called_once_with( - url='https://api.newrelic.com/v2/alert_policies.json', + url='https://api.newrelic.com/v2/alerts_policies.json', headers=self.policies.headers, - params='filter[ids]=12345' + params='filter[name]=Default Server Policy' ) @patch.object(requests, 'get') @@ -158,42 +85,98 @@ def test_list_failure(self, mock_get): # Call the method self.policies.list() - @patch.object(requests, 'get') - def test_show_success(self, mock_get): + @patch.object(requests, 'post') + def test_create_success(self, mock_post): """ - Test alert policies .show() success + Test alert policies .create() calls put with correct parameters """ - mock_response = Mock(name='response') - mock_response.json.return_value = self.policy_show_response - mock_get.return_value = mock_response + self.policies.create( + name=self.policy_single_response['policy']['name'], + incident_preference=self.policy_single_response['policy']['incident_preference'] + ) - # Call the method - response = self.policies.show(id=333112) + mock_post.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_policies.json', + headers=self.policies.headers, + data=json.dumps({ + "policy": { + "name": self.policy_single_response['policy']['name'], + "incident_preference": self.policy_single_response['policy']['incident_preference'] + } + }) + ) - self.assertIsInstance(response, dict) + @patch.object(requests, 'put') + def test_update_success(self, mock_put): + """ + Test alert policies .update() calls put with correct parameters + """ + self.policies.update( + id=self.policy_single_response['policy']['id'], + name=self.policy_single_response['policy']['name'], + incident_preference=self.policy_single_response['policy']['incident_preference'] + ) - @patch.object(requests, 'get') - def test_show_failure(self, mock_get): + mock_put.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_policies/{0}.json'.format( + self.policy_single_response['policy']['id'] + ), + headers=self.policies.headers, + data=json.dumps({ + "policy": { + "name": self.policy_single_response['policy']['name'], + "incident_preference": self.policy_single_response['policy']['incident_preference'] + } + }) + ) + + @patch.object(requests, 'delete') + def test_delete_success(self, mock_delete): """ - Test alert policies .show() failure + Test alert policies .delete() success """ - mock_response = Mock(name='response') - mock_response.json.side_effect = ValueError('No JSON object could be decoded') - mock_get.return_value = mock_response - with self.assertRaises(ValueError): - # Call the method - self.policies.show(id=333114) + self.policies.delete(id=self.policy_single_response['policy']['id']) + + mock_delete.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_policies/{0}.json'.format( + self.policy_single_response['policy']['id'] + ), + headers=self.policies.headers + ) @patch.object(requests, 'put') - def test_update(self, mock_put): + def test_associate_with_notification_channel_success(self, mock_put): """ - Test alert policies .update() calls put with correct parameters + Test alert policies .associate_with_notification_channel() calls put with correct parameters """ - self.policies.update(id=333114, policy_update=self.policy_show_response) + self.policies.associate_with_notification_channel( + id=self.policy_single_response['policy']['id'], + channel_id=self.channel_single_response['channel']['id'], + ) mock_put.assert_called_once_with( - url='https://api.newrelic.com/v2/alert_policies/333114.json', - headers=self.policies.headers, - data=json.dumps(self.policy_show_response) + url='https://api.newrelic.com/v2/alerts_policy_channels.json?policy_id={0}&channel_ids={1}'.format( + self.policy_single_response['policy']['id'], + self.channel_single_response['channel']['id'] + ), + headers=self.policies.headers + ) + + @patch.object(requests, 'put') + def test_dissociate_from_notification_channel(self, mock_put): + """ + Test alert policies .associate_with_notification_channel() calls put with correct parameters + """ + self.policies.associate_with_notification_channel( + id=self.policy_single_response['policy']['id'], + channel_id=self.channel_single_response['channel']['id'], + ) + + mock_put.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_policy_channels.json?policy_id={0}&channel_ids={1}'.format( + self.policy_single_response['policy']['id'], + self.channel_single_response['channel']['id'] + ), + headers=self.policies.headers ) diff --git a/newrelic_api/tests/base_tests.py b/newrelic_api/tests/base_tests.py index fcbcbda..6137d76 100644 --- a/newrelic_api/tests/base_tests.py +++ b/newrelic_api/tests/base_tests.py @@ -1,5 +1,4 @@ import os -from datetime import time from unittest import TestCase from mock import patch, call, Mock @@ -250,7 +249,6 @@ def test_build_param_string(self): 0, [], {}, - time(0) ] param_str = resource.build_param_string(test_params) diff --git a/newrelic_api/tests/dashboards_test.py b/newrelic_api/tests/dashboards_test.py new file mode 100644 index 0000000..4e86df6 --- /dev/null +++ b/newrelic_api/tests/dashboards_test.py @@ -0,0 +1,317 @@ +from unittest import TestCase + +from mock import patch, Mock +import requests + +from newrelic_api.dashboards import Dashboards +from newrelic_api.exceptions import NewRelicAPIServerException + + +class NRDashboardsTests(TestCase): + def setUp(self): + super(NRDashboardsTests, self).setUp() + self.dashboards = Dashboards(api_key='dummy_key') + + self.list_success_response = { + 'dashboards': [ + { + "id": 123456, + "title": "test-dashboard", + "description": "Test Dashboard", + "icon": "line-chart", + "created_at": "2018-09-06T12:13:14Z", + "updated_at": "2018-09-07T13:14:15Z", + "visibility": "owner", + "editable": "editable_by_owner", + "ui_url": "https://insights.newrelic.com/accounts/234567/dashboards/123456", + "api_url": "https://api.newrelic.com/v2/dashboards/123456", + "owner_email": "user@company.com", + "filter": { + "event_types": ["SystemSample"], + "attributes": ["environment"] + } + } + ] + } + + self.single_success_response = { + 'dashboard': { + "id": 123456, + "title": "test-dashboard", + "description": "Test Dashboard", + "icon": "line-chart", + "created_by": "2018-09-06T12:13:14Z", + "updated_by": "2018-09-07T13:14:15Z", + "visibility": "owner", + "editable": "editable_by_owner", + "ui_url": "https://insights.newrelic.com/accounts/234567/dashboards/123456", + "api_url": "https://api.newrelic.com/v2/dashboards/123456", + "owner_email": "user@company.com", + "metadata": { + "version": 1 + }, + "widgets": [ + { + "visualization": "faceted_line_chart", + "layout": { + "width": 1, + "height": 1, + "row": 1, + "column": 1 + }, + "widget_id": 654321, + "account_id": 234567, + "data": [ + { + "nrql": "SELECT average(cpuPercent) from SystemSample FACET role" + } + ], + "presentation": { + "title": "CPU Utilization", + "notes": "" + } + } + ], + "filter": { + "event_types": ["SystemSample"], + "attributes": ["environment"] + } + } + } + + self.delete_success_response = { + 'dashboard': { + 'id': 123456 + } + } + + @patch.object(requests, 'get') + def test_list_success(self, mock_get): + """ + Tests dashboards .list() success + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.list_success_response + mock_get.return_value = mock_response + + response = self.dashboards.list() + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + def test_list_failure(self, mock_get): + """ + Tests dashboards .list() failure + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_response + + with self.assertRaises(ValueError): + self.dashboards.list() + + @patch.object(requests, 'get') + def test_show_success(self, mock_get): + """ + Tests dashboards .show() success + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.single_success_response + mock_get.return_value = mock_response + + response = self.dashboards.show(123456) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'get') + def test_show_failure(self, mock_get): + """ + Tests dashboards .show() failure + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = ValueError('No JSON object could be decoded') + mock_get.return_value = mock_response + + with self.assertRaises(ValueError): + self.dashboards.show(123456) + + @patch.object(requests, 'delete') + def test_delete_success(self, mock_delete): + """ + Tests dashboards .delete() success + """ + mock_response = Mock(name='response') + mock_response.json.return_value = self.delete_success_response + mock_delete.return_value = mock_response + + response = self.dashboards.delete(123456) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'delete') + def test_delete_failure(self, mock_delete): + """ + Tests dashboards .delete() failure + """ + mock_response = Mock(name='response') + mock_response.json.side_effect = NewRelicAPIServerException("New Relic API Exception") + mock_delete.return_value = mock_response + + with self.assertRaises(NewRelicAPIServerException): + self.dashboards.delete(123456) + + @patch.object(requests, 'post') + def test_create_success(self, mock_post): + """ + Test dashboards .create() success + """ + mock_create_response = Mock(name='response') + mock_create_response.json.return_value = self.single_success_response + mock_post.return_value = mock_create_response + + response = self.dashboards.create( + { + "dashboard": { + "title": "test-dashboard", + "description": "Test Dashboard", + "icon": "line-chart", + "visibility": "owner", + "metadata": { + "version": 1 + }, + "widgets": [ + { + "visualization": "faceted_line_chart", + "layout": { + "width": 1, + "height": 1, + "row": 1, + "column": 1 + }, + "account_id": 234567, + "data": [ + { + "nrql": "SELECT average(cpuPercent) FROM SystemSample FACET role" + } + ], + "presentation": { + "title": "CPU Utilization", + "notes": "" + } + } + ], + "filter": { + "event_types": ["SystemSample"], + "attributes": ["environment"] + } + } + } + ) + + self.assertIsInstance(response, dict) + + @patch.object(requests, 'post') + def test_create_failure(self, mock_post): + """ + Test dashboards .create() failure + """ + + @patch.object(requests, 'put') + def test_update_success(self, mock_put): + """ + Test dashboards .update() success + """ + mock_update_response = Mock(name='response') + mock_update_response.json.return_value = self.single_success_response + mock_put.return_value = mock_update_response + + response = self.dashboards.update( + 123456, + { + "dashboard": { + "title": "test-dashboard", + "description": "Test Dashboard", + "icon": "line-chart", + "visibility": "owner", + "metadata": { + "version": 1 + }, + "widgets": [ + { + "visualization": "faceted_line_chart", + "layout": { + "width": 1, + "height": 1, + "row": 1, + "column": 1 + }, + "account_id": 234567, + "data": [ + { + "nrql": "SELECT average(cpuPercent) FROM SystemSample FACET role" + } + ], + "presentation": { + "title": "CPU Utilization", + "notes": "" + } + } + ], + "filter": { + "event_types": ["SystemSample"], + "attributes": ["environment"] + } + } + } + ) + self.assertIsInstance(response, dict) + + @patch.object(requests, 'put') + def test_update_failure(self, mock_put): + """ + Test dashboards .update() failure + """ + mock_update_response = Mock(name='response') + mock_update_response.json.side_effect = NewRelicAPIServerException('No JSON object could be decoded') + mock_put.return_value = mock_update_response + + with self.assertRaises(NewRelicAPIServerException): + self.dashboards.update( + 123456, + { + "dashboard": { + "title": "test-dashboard", + "description": "Test Dashboard", + "icon": "line-chart", + "visibility": "owner", + "metadata": { + "version": 1 + }, + "widgets": [ + { + "visualization": "faceted_line_chart", + "layout": { + "width": 1, + "height": 1, + "row": 1, + "column": 1 + }, + "account_id": 234567, + "data": [ + { + "nrql": "SELECT average(cpuPercent) FROM SystemSample FACET role" + } + ], + "presentation": { + "title": "CPU Utilization", + "notes": "" + } + } + ], + "filter": { + "event_types": ["SystemSample"], + "attributes": ["environment"] + } + } + } + ) diff --git a/newrelic_api/tests/notification_channels_tests.py b/newrelic_api/tests/notification_channels_tests.py index 84d7a89..fe0b732 100644 --- a/newrelic_api/tests/notification_channels_tests.py +++ b/newrelic_api/tests/notification_channels_tests.py @@ -1,6 +1,7 @@ from unittest import TestCase -from mock import patch +from mock import patch, Mock +import json import requests from newrelic_api.notification_channels import NotificationChannels @@ -12,57 +13,75 @@ def setUp(self): self.channels = NotificationChannels(api_key='dummy_key') self.list_response = { - "notification_channels": [ + "channels": [ { "id": 111222, "type": "user", - "downtime_only": False, - "mobile_alerts": False, - "email_alerts": True, + "name": "Some User", "links": { + "policy_ids": [] + }, + "configuration": { "user": 222333 } } ] } - self.show_response = { - 'notification_channel': self.list_response['notification_channels'][0] + self.single_response = { + 'channels': self.list_response['channels'][0] } @patch.object(requests, 'get') - def test_list(self, mock_get): + def test_list_success(self, mock_get): """ Test notification channels .list() """ - self.channels.list(filter_type=['user'], page=0) + mock_response = Mock(name='response') + mock_response.json.return_value = self.list_response + mock_get.return_value = mock_response + response = self.channels.list() + + self.assertIsInstance(response, dict) mock_get.assert_called_once_with( - url='https://api.newrelic.com/v2/notification_channels.json', + url='https://api.newrelic.com/v2/alerts_channels.json', headers=self.channels.headers, - params='filter[type]=user' + params='' ) @patch.object(requests, 'get') - def test_list_with_filter_ids(self, mock_get): + def test_list_success_with_pagination(self, mock_get): """ - Test notification channels .list() with filter_ids + Test notification channels .list() with page parameter """ - self.channels.list(filter_type=['user'], filter_ids=[111222], page=0) + self.channels.list(page=2) mock_get.assert_called_once_with( - url='https://api.newrelic.com/v2/notification_channels.json', + url='https://api.newrelic.com/v2/alerts_channels.json', headers=self.channels.headers, - params='filter[type]=user&filter[ids]=111222' + params='page=2' ) - @patch.object(requests, 'get') - def test_show(self, mock_get): + @patch.object(requests, 'post') + def test_create_success(self, mock_post): """ - Test notification channels .show() + Test notification channels .create() calls put with correct parameters """ - self.channels.show(id=11122) - mock_get.assert_called_once_with( - url='https://api.newrelic.com/v2/notification_channels/11122.json', + self.channels.create( + name=self.single_response['channels']['name'], + type=self.single_response['channels']['type'], + configuration=self.single_response['channels']['configuration'] + ) + + mock_post.assert_called_once_with( + url='https://api.newrelic.com/v2/alerts_channels.json', headers=self.channels.headers, + data=json.dumps({ + "channel": { + "name": self.single_response['channels']['name'], + "type": self.single_response['channels']['type'], + "configuration": self.single_response['channels']['configuration'] + } + }) ) diff --git a/newrelic_api/version.py b/newrelic_api/version.py index 8a81504..6843bf6 100644 --- a/newrelic_api/version.py +++ b/newrelic_api/version.py @@ -1 +1 @@ -__version__ = '1.0.4' +__version__ = '1.0.6-dev' diff --git a/requirements/main.txt b/requirements/main.txt new file mode 100644 index 0000000..fa07c11 --- /dev/null +++ b/requirements/main.txt @@ -0,0 +1,4 @@ +coverage==4.5.1 +flake8==3.5.0 +nose==1.3.7 +requests==2.19.1 diff --git a/setup.cfg b/setup.cfg index 86fd585..a93c166 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [nosetests] with-coverage = 1 cover-branches = 1 -cover-min-percentage = 100 +cover-min-percentage = 85 cover-package = newrelic_api [flake8]