diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9917ac0..6b01fab 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,39 +1,42 @@ -name: Docker Build and Push +name: Docker on: push: branches: [ master, release/* ] + tags: [ '**' ] + +env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} jobs: build: + name: Build & Push runs-on: ubuntu-latest env: - REPOSITORY_URL: docker.pkg.github.com - IMAGE_NAME: ${{ github.repository }}/alerta-cli - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + REPOSITORY_URL: ghcr.io + IMAGE_NAME: ${{ github.repository_owner }}/alerta-cli steps: - name: Checkout - uses: actions/checkout@v2 - - name: Variables - id: vars - run: echo "::set-output name=SHORT_COMMIT_ID::$(git rev-parse --short HEAD)" + uses: actions/checkout@v4 - name: Build image id: docker-build run: >- docker build -t $IMAGE_NAME - -t $REPOSITORY_URL/$IMAGE_NAME:${{ steps.vars.outputs.SHORT_COMMIT_ID }} + -t $REPOSITORY_URL/$IMAGE_NAME:$(cat VERSION) + -t $REPOSITORY_URL/$IMAGE_NAME:$(git rev-parse --short HEAD) -t $REPOSITORY_URL/$IMAGE_NAME:latest . - name: Docker Login - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - run: docker login $REPOSITORY_URL --username "$DOCKER_USERNAME" --password "$DOCKER_PASSWORD" + uses: docker/login-action@v2 + with: + registry: ${{ env.REPOSITORY_URL }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Publish Image id: docker-push - run: docker push $REPOSITORY_URL/$IMAGE_NAME + run: docker push --all-tags $REPOSITORY_URL/$IMAGE_NAME - - uses: act10ns/slack@v1 + - uses: act10ns/slack@v2 with: status: ${{ job.status }} steps: ${{ toJson(steps) }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..8234696 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Release + +on: + push: + tags: [ 'v*' ] + +env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + +jobs: + test: + name: Test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Install dependencies + id: install-deps + run: | + python3 -m pip install --upgrade pip + pip install flake8 pytest + pip install -r requirements.txt + pip install -r requirements-dev.txt + pip install . + - name: Pre-commit hooks + id: hooks + run: | + pre-commit run -a --show-diff-on-failure + - name: Test with pytest + id: test + run: | + pytest --cov=alertaclient tests/unit + - uses: act10ns/slack@v2 + with: + status: ${{ job.status }} + steps: ${{ toJson(steps) }} + if: failure() + + release: + name: Publish + needs: test + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + - name: Build + id: build + run: | + python3 -m pip install --upgrade build + python3 -m build + zip alerta-api.zip -r dist/* + tar cvfz alerta-api.tar.gz dist/* + - name: Release + id: create-release + uses: softprops/action-gh-release@v2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + tag_name: ${{ github.ref }} + name: Release ${{ github.ref }} + draft: false + prerelease: ${{ contains(github.ref_name, '-') }} + files: | + ./alerta-api.zip + ./alerta-api.tar.gz + + - uses: act10ns/slack@v2 + with: + status: ${{ job.status }} + steps: ${{ toJson(steps) }} + + - name: Test Install + run: | + python3 -m pip install --upgrade alerta + python3 -m pip freeze diff --git a/.github/workflows/github-ci.yml b/.github/workflows/tests.yml similarity index 52% rename from .github/workflows/github-ci.yml rename to .github/workflows/tests.yml index 4903c09..bdecb5d 100644 --- a/.github/workflows/github-ci.yml +++ b/.github/workflows/tests.yml @@ -1,27 +1,26 @@ -name: CI Tests +name: Tests on: push: - branches: [ master, release/* ] pull_request: branches: [ master ] +env: + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + REPOSITORY_URL: docker.pkg.github.com + jobs: test: runs-on: ubuntu-latest - env: - REPOSITORY_URL: docker.pkg.github.com - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: ['3.10', '3.11', '3.12'] steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -41,30 +40,17 @@ jobs: run: | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics flake8 . --count --exit-zero --max-complexity=50 --max-line-length=127 --statistics - - name: Type Check - id: types - run: | - python -m mypy alertaclient/ - name: Test with pytest id: unit-test - env: - DATABASE_URL: postgres://postgres:postgres@localhost:5432/alerta run: | pytest --cov=alertaclient tests/unit - - name: Docker Login - env: - DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} - DOCKER_PASSWORD: ${{ secrets.GITHUB_TOKEN }} - run: | - docker login $REPOSITORY_URL --username "$DOCKER_USERNAME" --password "$DOCKER_PASSWORD" - name: Integration Test id: integration-test run: | - docker-compose -f docker-compose.ci.yaml build sut - docker-compose -f docker-compose.ci.yaml up --exit-code-from sut - docker-compose -f docker-compose.ci.yaml rm --stop --force - - - uses: act10ns/slack@v1 + docker compose -f docker-compose.ci.yaml build sut + docker compose -f docker-compose.ci.yaml up --exit-code-from sut + docker compose -f docker-compose.ci.yaml rm --stop --force + - uses: act10ns/slack@v2 with: status: ${{ job.status }} steps: ${{ toJson(steps) }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cb5d3c6..1b8b58c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,10 +1,10 @@ repos: -- repo: https://github.com/pre-commit/mirrors-autopep8 - rev: v1.5.1 +- repo: https://github.com/hhatto/autopep8 + rev: v2.0.4 hooks: - id: autopep8 - repo: https://github.com/pre-commit/pre-commit-hooks.git - rev: v2.5.0 + rev: v4.5.0 hooks: - id: check-added-large-files - id: check-ast @@ -17,25 +17,25 @@ repos: - id: debug-statements - id: double-quote-string-fixer - id: end-of-file-fixer - - id: flake8 - id: fix-encoding-pragma args: ['--remove'] - id: pretty-format-json args: ['--autofix'] - id: name-tests-test args: ['--django'] + exclude: ^tests/helpers/ - id: requirements-txt-fixer - id: trailing-whitespace +- repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 - repo: https://github.com/asottile/pyupgrade - rev: v1.27.0 + rev: v3.21.2 hooks: - id: pyupgrade args: ['--py3-plus'] -- repo: https://github.com/asottile/seed-isort-config - rev: v1.9.4 - hooks: - - id: seed-isort-config -- repo: https://github.com/pre-commit/mirrors-isort - rev: v4.3.21 +- repo: https://github.com/PyCQA/isort + rev: 7.0.0 hooks: - id: isort diff --git a/CHANGELOG.md b/CHANGELOG.md index 268a01b..e221bfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1 +1,37 @@ -## Unreleased +## v8.5.2 (2023-03-18) + +### Refactor + +- convert formatted strings to f-strings (#272) + +## v8.5.1 (2021-11-21) + +### Feat + +- Add a --alert flag to alert keys to alert on expired and expiring key (#274) +- Add option to use custom value when creating API key (#270) + +### Refactor + +- convert formatted strings to f-strings (#272) +- assign api key directly (#271) + +## v8.5.0 (2021-04-18) + +### Fix + +- improve alert note command (#263) +- consistent use of ID as metavar (#262) +- add scopes cmd and minor fixes (#257) +- **build**: run tests against correct branch + +### Feat + +- add examples for group cmd (#261) +- add and remove users to/from groups (#260) +- add option to list users to group cmd (#259) +- add option to list groups to user cmd (#258) +- add alerts command for list alert attributes (#256) +- show user details (#255) +- add option to show decoded auth token claims (#254) +- **auth**: add HMAC authentication as config option (#248) diff --git a/Makefile b/Makefile index 07b8395..d3b23f7 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,15 @@ #!make VENV=venv -PYTHON=$(VENV)/bin/python +PYTHON=$(VENV)/bin/python3 PIP=$(VENV)/bin/pip --disable-pip-version-check -PYLINT=$(VENV)/bin/pylint +FLAKE8=$(VENV)/bin/flake8 MYPY=$(VENV)/bin/mypy -BLACK=$(VENV)/bin/black TOX=$(VENV)/bin/tox PYTEST=$(VENV)/bin/pytest +DOCKER_COMPOSE=docker compose PRE_COMMIT=$(VENV)/bin/pre-commit +BUILD=$(VENV)/bin/build WHEEL=$(VENV)/bin/wheel TWINE=$(VENV)/bin/twine GIT=git @@ -18,60 +19,58 @@ GIT=git -include .env .env.local .env.*.local ifndef PROJECT -$(error PROJECT is not set) + $(error PROJECT is not set) endif +PYPI_REPOSITORY ?= pypi VERSION=$(shell cut -d "'" -f 2 $(PROJECT)/version.py) -PKG_SDIST=dist/*-$(VERSION).tar.gz -PKG_WHEEL=dist/*-$(VERSION)-*.whl - all: help -$(PIP): +$(VENV): python3 -m venv $(VENV) -$(PYLINT): $(PIP) - $(PIP) install pylint +$(FLAKE8): $(VENV) + $(PIP) install flake8 -$(MYPY): $(PIP) +$(MYPY): $(VENV) $(PIP) install mypy -$(BLACK): $(PIP) - $(PIP) install black - -$(TOX): $(PIP) +$(TOX): $(VENV) $(PIP) install tox -$(PYTEST): $(PIP) - $(PIP) install pytest +$(PYTEST): $(VENV) + $(PIP) install pytest pytest-cov -$(PRE_COMMIT): $(PIP) +$(PRE_COMMIT): $(VENV) $(PIP) install pre-commit + $(PRE_COMMIT) install + +$(BUILD): $(VENV) + $(PIP) install --upgrade build -$(WHEEL): $(PIP) - $(PIP) install wheel +$(WHEEL): $(VENV) + $(PIP) install --upgrade wheel -$(TWINE): $(PIP) - $(PIP) install wheel twine +$(TWINE): $(VENV) + $(PIP) install --upgrade wheel twine ifdef TOXENV -toxparams?=-e $(TOXENV) + toxparams?=-e $(TOXENV) endif -## format - Code formatter. -format: $(BLACK) - $(BLACK) -l120 -S -v $(PROJECT) - -## lint - Lint and type checking. -lint: $(PYLINT) $(MYPY) $(BLACK) - $(PYLINT) --rcfile pylintrc $(PROJECT) - $(MYPY) $(PROJECT)/ - $(BLACK) -l120 -S --check -v $(PROJECT) || true +## install - Install dependencies. +install: $(VENV) + $(PIP) install -r requirements.txt ## hooks - Run pre-commit hooks. hooks: $(PRE_COMMIT) - $(PRE_COMMIT) run -a + $(PRE_COMMIT) run --all-files --show-diff-on-failure + +## lint - Lint and type checking. +lint: $(FLAKE8) $(MYPY) + $(FLAKE8) $(PROJECT)/ + $(MYPY) $(PROJECT)/ ## test - Run all tests. test: test.unit test.integration @@ -82,10 +81,11 @@ test.unit: $(TOX) $(PYTEST) ## test.integration - Run integration tests. test.integration: - docker-compose -f docker-compose.ci.yaml rm --stop --force - docker-compose -f docker-compose.ci.yaml build sut - docker-compose -f docker-compose.ci.yaml up --exit-code-from sut - docker-compose -f docker-compose.ci.yaml rm --stop --force + $(DOCKER_COMPOSE) -f docker-compose.ci.yaml rm --stop --force + $(DOCKER_COMPOSE) -f docker-compose.ci.yaml pull + $(DOCKER_COMPOSE) -f docker-compose.ci.yaml build sut + $(DOCKER_COMPOSE) -f docker-compose.ci.yaml up --exit-code-from sut + $(DOCKER_COMPOSE) -f docker-compose.ci.yaml rm --stop --force ## run - Run application. run: @@ -97,16 +97,13 @@ tag: $(GIT) push --tags ## build - Build package. -build: $(PIP) $(WHEEL) $(PKG_SDIST) $(PKG_WHEEL) - -$(PKG_SDIST): - $(PYTHON) setup.py sdist -$(PKG_WHEEL): - $(PYTHON) setup.py bdist_wheel +build: $(BUILD) + $(PYTHON) -m build ## upload - Upload package to PyPI. upload: $(TWINE) - $(TWINE) upload dist/* + $(TWINE) check dist/* + $(TWINE) upload --repository $(PYPI_REPOSITORY) --verbose dist/* ## clean - Clean source. clean: diff --git a/README.md b/README.md index 0eb90d6..8279418 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,7 @@ License ------- Alerta monitoring system and console - Copyright 2012-2020 Nick Satterly + Copyright 2012-2024 Nick Satterly Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/VERSION b/VERSION index 2bf50aa..7dbcd5d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -8.3.0 +8.5.3 diff --git a/alertaclient/__init__.py b/alertaclient/__init__.py index 193f832..e69de29 100644 --- a/alertaclient/__init__.py +++ b/alertaclient/__init__.py @@ -1,26 +0,0 @@ -import sys - -if sys.version_info < (3,): - raise ImportError( - """You are running Alerta 6.0 on Python 2 - -Alerta 6.0 and above are no longer compatible with Python 2. - -Make sure you have pip >= 9.0 to avoid this kind of issue, -as well as setuptools >= 24.2: - - $ pip install pip setuptools --upgrade - -Your choices: - -- Upgrade to Python 3. - -- Install an older version of Alerta: - - $ pip install 'alerta<6.0' - -See the following URL for more up-to-date information: - -https://github.com/alerta/alerta/wiki/Python-3 - -""") diff --git a/alertaclient/api.py b/alertaclient/api.py index ea0f9e6..a6dda7c 100644 --- a/alertaclient/api.py +++ b/alertaclient/api.py @@ -32,8 +32,8 @@ class Client: DEFAULT_ENDPOINT = 'http://localhost:8080' - def __init__(self, endpoint=None, key=None, secret=None, token=None, username=None, password=None, timeout=5.0, ssl_verify=True, - ssl_cert=None, ssl_key=None, headers=None, debug=False): + def __init__(self, endpoint=None, key=None, secret=None, token=None, username=None, password=None, timeout=5.0, + ssl_verify=True, ssl_cert=None, ssl_key=None, headers=None, debug=False): self.endpoint = endpoint or os.environ.get('ALERTA_ENDPOINT', self.DEFAULT_ENDPOINT) if debug: @@ -69,6 +69,11 @@ def send_alert(self, resource, event, **kwargs): alert = Alert.parse(r['alert']) if 'alert' in r else None return r.get('id', '-'), alert, r.get('message', None) + def send_alerts(self, data: list): + r = self.http.post('/alerts', data) + alerts = [Alert.parse(alert) for alert in r['alerts']] if 'alerts' in r else None + return [alert.id for alert in alerts], alerts, r.get('message', None) + def get_alert(self, id): return Alert.parse(self.http.get('/alert/%s' % id)['alert']) @@ -103,6 +108,9 @@ def update_attributes(self, id, attributes): def delete_alert(self, id): return self.http.delete('/alert/%s' % id) + def delete_alerts(self, ids): + return self.http.delete('/alerts?%s' % '&'.join([f'id={id}'for id in ids])) + def search(self, query=None, page=1, page_size=None): return self.get_alerts(query, page, page_size) @@ -150,25 +158,26 @@ def alert_note(self, id, text): data = { 'text': text } - r = self.http.put('/alert/{}/note'.format(id), data) + r = self.http.put(f'/alert/{id}/note', data) return Note.parse(r['note']) def get_alert_notes(self, id, page=1, page_size=None): - r = self.http.get('/alert/{}/notes'.format(id), page=page, page_size=page_size) + r = self.http.get(f'/alert/{id}/notes', page=page, page_size=page_size) return [Note.parse(n) for n in r['notes']] def update_alert_note(self, id, note_id, text): data = { 'text': text, } - r = self.http.put('/alert/{}/note/{}'.format(id, note_id), data) + r = self.http.put(f'/alert/{id}/note/{note_id}', data) return Note.parse(r['note']) def delete_alert_note(self, id, note_id): - return self.http.delete('/alert/{}/note/{}'.format(id, note_id)) + return self.http.delete(f'/alert/{id}/note/{note_id}') # Blackouts - def create_blackout(self, environment, service=None, resource=None, event=None, group=None, tags=None, customer=None, start=None, duration=None, text=None): + def create_blackout(self, environment, service=None, resource=None, event=None, group=None, tags=None, + origin=None, customer=None, start=None, duration=None, text=None): data = { 'environment': environment, 'service': service or list(), @@ -176,6 +185,7 @@ def create_blackout(self, environment, service=None, resource=None, event=None, 'event': event, 'group': group, 'tags': tags or list(), + 'origin': origin, 'customer': customer, 'startTime': start, 'duration': duration, @@ -200,12 +210,13 @@ def update_blackout(self, id, **kwargs): 'event': kwargs.get('event'), 'group': kwargs.get('group'), 'tags': kwargs.get('tags'), + 'origin': kwargs.get('origin'), 'startTime': kwargs.get('startTime'), 'endTime': kwargs.get('endTime'), 'text': kwargs.get('text'), } - r = self.http.put('/blackout/{}'.format(id), data) + r = self.http.put(f'/blackout/{id}', data) return Blackout.parse(r['blackout']) def delete_blackout(self, id): @@ -232,7 +243,7 @@ def update_customer(self, id, **kwargs): 'match': kwargs.get('match'), 'customer': kwargs.get('customer') } - r = self.http.put('/customer/{}'.format(id), data) + r = self.http.put(f'/customer/{id}', data) return Customer.parse(r['customer']) def delete_customer(self, id): @@ -262,12 +273,13 @@ def delete_heartbeat(self, id): return self.http.delete('/heartbeat/%s' % id) # API Keys - def create_key(self, username, scopes=None, expires=None, text='', customer=None): + def create_key(self, username, scopes=None, expires=None, text='', customer=None, **kwargs): data = { 'user': username, 'scopes': scopes or list(), 'text': text, - 'customer': customer + 'customer': customer, + 'key': kwargs.get('key') } if expires: data['expireTime'] = DateTime.iso8601(expires) @@ -288,7 +300,7 @@ def update_key(self, id, **kwargs): 'expireTime': kwargs.get('expireTime'), 'customer': kwargs.get('customer') } - r = self.http.put('/key/{}'.format(id), data) + r = self.http.put(f'/key/{id}', data) return ApiKey.parse(r['key']) def delete_key(self, id): @@ -315,7 +327,7 @@ def update_perm(self, id, **kwargs): 'match': kwargs.get('match'), # role 'scopes': kwargs.get('scopes') } - r = self.http.put('/perm/{}'.format(id), data) + r = self.http.put(f'/perm/{id}', data) return Permission.parse(r['permission']) def delete_perm(self, id): @@ -351,11 +363,11 @@ def create_user(self, name, email, password, status, roles=None, attributes=None r = self.http.post('/user', data) return User.parse(r['user']) - def get_user(self): + def get_user(self, id): return User.parse(self.http.get('/user/%s' % id)['user']) def get_user_groups(self, id): - r = self.http.get('/user/{}/groups'.format(id)) + r = self.http.get(f'/user/{id}/groups') return [Group.parse(g) for g in r['groups']] def get_me(self): @@ -379,7 +391,7 @@ def update_user(self, id, **kwargs): 'text': kwargs.get('text'), 'email_verified': kwargs.get('email_verified') } - r = self.http.put('/user/{}'.format(id), data) + r = self.http.put(f'/user/{id}', data) return User.parse(r['user']) def update_me(self, **kwargs): @@ -448,7 +460,7 @@ def get_group(self): return Group.parse(self.http.get('/group/%s' % id)['group']) def get_group_users(self, id): - r = self.http.get('/group/{}/users'.format(id)) + r = self.http.get(f'/group/{id}/users') return [User.parse(u) for u in r['users']] def get_users_groups(self, query=None): @@ -460,14 +472,14 @@ def update_group(self, id, **kwargs): 'name': kwargs.get('name'), 'text': kwargs.get('text') } - r = self.http.put('/group/{}'.format(id), data) + r = self.http.put(f'/group/{id}', data) return Group.parse(r['group']) def add_user_to_group(self, group_id, user_id): - return self.http.put('/group/{}/user/{}'.format(group_id, user_id)) + return self.http.put(f'/group/{group_id}/user/{user_id}') def remove_user_from_group(self, group_id, user_id): - return self.http.delete('/group/{}/user/{}'.format(group_id, user_id)) + return self.http.delete(f'/group/{group_id}/user/{user_id}') def delete_group(self, id): return self.http.delete('/group/%s' % id) @@ -488,6 +500,15 @@ def housekeeping(self, expired_delete_hours=None, info_delete_hours=None): if response.status_code != 200: raise UnknownError(response.text) + def escalate(self): + self.http.session.get(self.http.endpoint + '/escalate', auth=self.http.auth, timeout=self.http.timeout) + + def reactivate_notification_rules(self): + self.http.session.get(self.http.endpoint + '/notificationrules/reactivate', auth=self.http.auth, timeout=self.http.timeout) + + def fire_delayed_notifications(self): + self.http.session.get(self.http.endpoint + '/notificationdelay/fire', auth=self.http.auth, timeout=self.http.timeout) + class ApiKeyAuth(AuthBase): @@ -496,7 +517,7 @@ def __init__(self, api_key=None, auth_token=None): self.auth_token = auth_token def __call__(self, r): - r.headers['Authorization'] = 'Key {}'.format(self.api_key) + r.headers['Authorization'] = f'Key {self.api_key}' return r @@ -506,7 +527,7 @@ def __init__(self, auth_token=None): self.auth_token = auth_token def __call__(self, r): - r.headers['Authorization'] = 'Bearer {}'.format(self.auth_token) + r.headers['Authorization'] = f'Bearer {self.auth_token}' return r @@ -604,7 +625,7 @@ def delete(self, path): def _handle_error(self, response): if self.debug: - print('\nbody: {}'.format(response.text)) + print(f'\nbody: {response.text}') resp = response.json() status = resp.get('status', None) if status == 'ok': diff --git a/alertaclient/auth/utils.py b/alertaclient/auth/utils.py index 2fa85e1..d966cf5 100644 --- a/alertaclient/auth/utils.py +++ b/alertaclient/auth/utils.py @@ -29,7 +29,7 @@ def save_token(endpoint, username, token): try: info = netrc(NETRC_FILE) except Exception as e: - raise ConfigurationError('{}'.format(e)) + raise ConfigurationError(f'{e}') info.hosts[machine(endpoint)] = (username, None, token) with open(NETRC_FILE, 'w') as f: f.write(dump_netrc(info)) @@ -39,13 +39,13 @@ def clear_token(endpoint): try: info = netrc(NETRC_FILE) except Exception as e: - raise ConfigurationError('{}'.format(e)) + raise ConfigurationError(f'{e}') try: del info.hosts[machine(endpoint)] with open(NETRC_FILE, 'w') as f: f.write(dump_netrc(info)) except KeyError as e: - raise ConfigurationError('No credentials stored for {}'.format(e)) + raise ConfigurationError(f'No credentials stored for {e}') # See https://bugs.python.org/issue30806 diff --git a/alertaclient/cli.py b/alertaclient/cli.py index 663f2e7..9d03530 100644 --- a/alertaclient/cli.py +++ b/alertaclient/cli.py @@ -63,6 +63,7 @@ def cli(ctx, config_file, profile, endpoint_url, output, color, debug): ctx.obj['client'] = Client( endpoint=endpoint, key=config.options['key'], + secret=config.options['secret'], token=get_token(endpoint), username=config.options.get('username', None), password=config.options.get('password', None), diff --git a/alertaclient/commands/cmd_ack.py b/alertaclient/commands/cmd_ack.py index 8f198f2..1797020 100644 --- a/alertaclient/commands/cmd_ack.py +++ b/alertaclient/commands/cmd_ack.py @@ -4,7 +4,7 @@ @click.command('ack', short_help='Acknowledge alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Message associated with status change') @@ -22,4 +22,4 @@ def cli(obj, ids, query, filters, text): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - action_progressbar(client, action='ack', ids=ids, label='Acking {} alerts'.format(total), text=text) + action_progressbar(client, action='ack', ids=ids, label=f'Acking {total} alerts', text=text) diff --git a/alertaclient/commands/cmd_action.py b/alertaclient/commands/cmd_action.py index 662e52e..be4b29b 100644 --- a/alertaclient/commands/cmd_action.py +++ b/alertaclient/commands/cmd_action.py @@ -5,7 +5,7 @@ @click.command('action', short_help='Action alerts') @click.option('--action', '-a', metavar='ACTION', help='Custom action (user-defined)') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Message associated with action') @@ -23,5 +23,5 @@ def cli(obj, action, ids, query, filters, text): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - label = 'Action ({}) {} alerts'.format(action, total) + label = f'Action ({action}) {total} alerts' action_progressbar(client, action=action, ids=ids, label=label, text=text) diff --git a/alertaclient/commands/cmd_alerts.py b/alertaclient/commands/cmd_alerts.py new file mode 100644 index 0000000..7041b2b --- /dev/null +++ b/alertaclient/commands/cmd_alerts.py @@ -0,0 +1,47 @@ +import json + +import click +from tabulate import tabulate + + +@click.command('alerts', short_help='List environments, services, groups and tags') +@click.option('--environments', '-E', is_flag=True, help='List alert environments.') +@click.option('--services', '-S', is_flag=True, help='List alert services.') +@click.option('--groups', '-g', is_flag=True, help='List alert groups.') +@click.option('--tags', '-T', is_flag=True, help='List alert tags.') +@click.pass_obj +def cli(obj, environments, services, groups, tags): + """List alert environments, services, groups and tags.""" + + client = obj['client'] + + if environments: + if obj['output'] == 'json': + r = client.http.get('/environments') + click.echo(json.dumps(r['environments'], sort_keys=True, indent=4, ensure_ascii=False)) + else: + headers = {'environment': 'ENVIRONMENT', 'count': 'COUNT', 'severityCounts': 'SEVERITY COUNTS', 'statusCounts': 'STATUS COUNTS'} + click.echo(tabulate(client.get_environments(), headers=headers, tablefmt=obj['output'])) + elif services: + if obj['output'] == 'json': + r = client.http.get('/services') + click.echo(json.dumps(r['services'], sort_keys=True, indent=4, ensure_ascii=False)) + else: + headers = {'environment': 'ENVIRONMENT', 'service': 'SERVICE', 'count': 'COUNT', 'severityCounts': 'SEVERITY COUNTS', 'statusCounts': 'STATUS COUNTS'} + click.echo(tabulate(client.get_services(), headers=headers, tablefmt=obj['output'])) + elif groups: + if obj['output'] == 'json': + r = client.http.get('/alerts/groups') + click.echo(json.dumps(r['groups'], sort_keys=True, indent=4, ensure_ascii=False)) + else: + headers = {'environment': 'ENVIRONMENT', 'group': 'GROUP', 'count': 'COUNT', 'severityCounts': 'SEVERITY COUNTS', 'statusCounts': 'STATUS COUNTS'} + click.echo(tabulate(client.get_groups(), headers=headers, tablefmt=obj['output'])) + elif tags: + if obj['output'] == 'json': + r = client.http.get('/alerts/tags') + click.echo(json.dumps(r['tags'], sort_keys=True, indent=4, ensure_ascii=False)) + else: + headers = {'environment': 'ENVIRONMENT', 'tag': 'TAG', 'count': 'COUNT', 'severityCounts': 'SEVERITY COUNTS', 'statusCounts': 'STATUS COUNTS'} + click.echo(tabulate(client.get_tags(), headers=headers, tablefmt=obj['output'])) + else: + raise click.UsageError('Must choose an alert attribute to list.') diff --git a/alertaclient/commands/cmd_blackout.py b/alertaclient/commands/cmd_blackout.py index c083b71..697a4df 100644 --- a/alertaclient/commands/cmd_blackout.py +++ b/alertaclient/commands/cmd_blackout.py @@ -10,13 +10,14 @@ @click.option('--event', '-e', metavar='EVENT', help='Event name') @click.option('--group', '-g', metavar='GROUP', help='Group event by type eg. OS, Performance') @click.option('--tag', '-T', 'tags', multiple=True, metavar='TAG', help='List of tags eg. London, os:linux, AWS/EC2') +@click.option('--origin', '-O', metavar='ORIGIN', help='Origin of alert in form app/host') @click.option('--customer', metavar='STRING', help='Customer (Admin only)') @click.option('--start', metavar='DATETIME', help='Start time in ISO8601 eg. 2018-02-01T12:00:00.000Z') @click.option('--duration', metavar='SECONDS', type=int, help='Blackout period in seconds') @click.option('--text', help='Reason for blackout') -@click.option('--delete', '-D', help='Delete blackout using ID') +@click.option('--delete', '-D', metavar='ID', help='Delete blackout using ID') @click.pass_obj -def cli(obj, environment, service, resource, event, group, tags, customer, start, duration, text, delete): +def cli(obj, environment, service, resource, event, group, tags, origin, customer, start, duration, text, delete): """Suppress alerts for specified duration based on alert attributes.""" client = obj['client'] if delete: @@ -32,12 +33,13 @@ def cli(obj, environment, service, resource, event, group, tags, customer, start event=event, group=group, tags=tags, + origin=origin, customer=customer, start=start, duration=duration, text=text ) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(blackout.id) diff --git a/alertaclient/commands/cmd_blackouts.py b/alertaclient/commands/cmd_blackouts.py index 70c2af2..edbb77e 100644 --- a/alertaclient/commands/cmd_blackouts.py +++ b/alertaclient/commands/cmd_blackouts.py @@ -18,15 +18,15 @@ def cli(obj, purge): timezone = obj['timezone'] headers = { 'id': 'ID', 'priority': 'P', 'environment': 'ENVIRONMENT', 'service': 'SERVICE', 'resource': 'RESOURCE', - 'event': 'EVENT', 'group': 'GROUP', 'tags': 'TAGS', 'customer': 'CUSTOMER', 'startTime': 'START', 'endTime': 'END', - 'duration': 'DURATION', 'user': 'USER', 'createTime': 'CREATED', 'text': 'COMMENT', - 'status': 'STATUS', 'remaining': 'REMAINING' + 'event': 'EVENT', 'group': 'GROUP', 'tags': 'TAGS', 'origin': 'ORIGIN', 'customer': 'CUSTOMER', + 'startTime': 'START', 'endTime': 'END', 'duration': 'DURATION', 'user': 'USER', + 'createTime': 'CREATED', 'text': 'COMMENT', 'status': 'STATUS', 'remaining': 'REMAINING' } blackouts = client.get_blackouts() click.echo(tabulate([b.tabular(timezone) for b in blackouts], headers=headers, tablefmt=obj['output'])) expired = [b for b in blackouts if b.status == 'expired'] if purge: - with click.progressbar(expired, label='Purging {} blackouts'.format(len(expired))) as bar: + with click.progressbar(expired, label=f'Purging {len(expired)} blackouts') as bar: for b in bar: client.delete_blackout(b.id) diff --git a/alertaclient/commands/cmd_close.py b/alertaclient/commands/cmd_close.py index 7c40866..8b7ac78 100644 --- a/alertaclient/commands/cmd_close.py +++ b/alertaclient/commands/cmd_close.py @@ -4,7 +4,7 @@ @click.command('ack', short_help='Close alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Message associated with status change') @@ -22,4 +22,4 @@ def cli(obj, ids, query, filters, text): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - action_progressbar(client, action='close', ids=ids, label='Closing {} alerts'.format(total), text=text) + action_progressbar(client, action='close', ids=ids, label=f'Closing {total} alerts', text=text) diff --git a/alertaclient/commands/cmd_config.py b/alertaclient/commands/cmd_config.py index f458601..04b7c4e 100644 --- a/alertaclient/commands/cmd_config.py +++ b/alertaclient/commands/cmd_config.py @@ -8,4 +8,4 @@ def cli(obj): for k, v in obj.items(): if isinstance(v, list): v = ', '.join(v) - click.echo('{:20}: {}'.format(k, v)) + click.echo(f'{k:20}: {v}') diff --git a/alertaclient/commands/cmd_customer.py b/alertaclient/commands/cmd_customer.py index 33942f3..51f7540 100644 --- a/alertaclient/commands/cmd_customer.py +++ b/alertaclient/commands/cmd_customer.py @@ -21,6 +21,6 @@ def cli(obj, customer, match, delete): try: customer = client.create_customer(customer, match) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(customer.id) diff --git a/alertaclient/commands/cmd_delete.py b/alertaclient/commands/cmd_delete.py index 2991fde..c3ca575 100644 --- a/alertaclient/commands/cmd_delete.py +++ b/alertaclient/commands/cmd_delete.py @@ -4,7 +4,7 @@ @click.command('delete', short_help='Delete alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.pass_obj @@ -23,6 +23,6 @@ def cli(obj, ids, query, filters): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - with click.progressbar(ids, label='Deleting {} alerts'.format(total)) as bar: + with click.progressbar(ids, label=f'Deleting {total} alerts') as bar: for id in bar: client.delete_alert(id) diff --git a/alertaclient/commands/cmd_escalate.py b/alertaclient/commands/cmd_escalate.py new file mode 100644 index 0000000..7fb3a8d --- /dev/null +++ b/alertaclient/commands/cmd_escalate.py @@ -0,0 +1,9 @@ +import click + + +@click.command('escalate', short_help='Escalate alerts using escaltion rules') +@click.pass_obj +def cli(obj): + """Trigger escalation of alerts""" + client = obj['client'] + client.escalate() diff --git a/alertaclient/commands/cmd_group.py b/alertaclient/commands/cmd_group.py index e2e3396..63ec9a4 100644 --- a/alertaclient/commands/cmd_group.py +++ b/alertaclient/commands/cmd_group.py @@ -1,22 +1,61 @@ import sys import click +from tabulate import tabulate @click.command('group', short_help='Create user group') +@click.option('--id', '-i', metavar='ID', help='Group ID') @click.option('--name', help='Group name') @click.option('--text', help='Description of user group') -@click.option('--delete', '-D', metavar='ID', help='Delete user group using ID') +@click.option('--user', '-U', help='Add user to group') +@click.option('--users', is_flag=True, metavar='ID', help='Get list of group users') +@click.option('--delete', '-D', metavar='ID', help='Delete user group, or remove user from group') @click.pass_obj -def cli(obj, name, text, delete): - """Create or delete a user group.""" +def cli(obj, id, name, text, user, users, delete): + """ + Create or delete a user group, add and remove users from groups. + + EXAMPLES + + Create a group. + + $ alerta group --name --text + + Add a user to a group. + + $ alerta group -id --user + + List users in a group. + + $ alerta group --users + + Delete a user from a group. + + $ alerta group -id -D + + Delete a group. + + $ alerta group -D + """ client = obj['client'] - if delete: + if id and user: + client.add_user_to_group(id, user) + elif id and delete: + client.remove_user_from_group(id, delete) + elif users: + group_users = client.get_group_users(id) + timezone = obj['timezone'] + headers = {'id': 'ID', 'name': 'USER', 'email': 'EMAIL', 'roles': 'ROLES', 'status': 'STATUS', + 'text': 'TEXT', 'createTime': 'CREATED', 'updateTime': 'LAST UPDATED', + 'lastLogin': 'LAST LOGIN', 'email_verified': 'VERIFIED'} + click.echo(tabulate([gu.tabular(timezone) for gu in group_users], headers=headers, tablefmt=obj['output'])) + elif delete: client.delete_group(delete) else: try: group = client.create_group(name, text) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(group.id) diff --git a/alertaclient/commands/cmd_heartbeat.py b/alertaclient/commands/cmd_heartbeat.py index 3c6760a..901c948 100644 --- a/alertaclient/commands/cmd_heartbeat.py +++ b/alertaclient/commands/cmd_heartbeat.py @@ -52,6 +52,6 @@ def cli(obj, origin, environment, severity, service, group, tags, timeout, custo try: heartbeat = client.heartbeat(origin=origin, tags=tags, attributes=attributes, timeout=timeout, customer=customer) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(heartbeat.id) diff --git a/alertaclient/commands/cmd_heartbeats.py b/alertaclient/commands/cmd_heartbeats.py index 079f738..9003b89 100644 --- a/alertaclient/commands/cmd_heartbeats.py +++ b/alertaclient/commands/cmd_heartbeats.py @@ -33,7 +33,7 @@ def cli(obj, alert, severity, timeout, purge): ) if obj['output'] == 'json': - r = client.http.get('/heartbeats') + r = client.http.get('/heartbeats?page-size=ALL') heartbeats = [Heartbeat.parse(hb) for hb in r['heartbeats']] click.echo(json.dumps(r['heartbeats'], sort_keys=True, indent=4, ensure_ascii=False)) else: @@ -48,70 +48,86 @@ def cli(obj, alert, severity, timeout, purge): not_ok = [hb for hb in heartbeats if hb.status != 'ok'] if purge: - with click.progressbar(not_ok, label='Purging {} heartbeats'.format(len(not_ok))) as bar: + with click.progressbar(not_ok, label=f'Purging {len(not_ok)} heartbeats') as bar: for b in bar: client.delete_heartbeat(b.id) if alert: - with click.progressbar(heartbeats, label='Alerting {} heartbeats'.format(len(heartbeats))) as bar: - for b in bar: + with click.progressbar(heartbeats, label=f'Checking {len(heartbeats)} heartbeats') as bar: + alerts = client.get_alerts(query=[('event', '~Heartbeat')], page_size='ALL') + new_alerts = [] - want_environment = b.attributes.pop('environment', 'Production') + for b in bar: + want_environment = 'Heartbeats' want_severity = b.attributes.pop('severity', severity) want_service = b.attributes.pop('service', ['Alerta']) want_group = b.attributes.pop('group', 'System') + state_map = { + 'expired': { + 'event': 'HeartbeatFail', + 'value': f'{b.since}', + 'text': f'Heartbeat not received in {b.timeout} seconds', + 'severity': want_severity + }, + 'slow': { + 'event': 'HeartbeatSlow', + 'value': f'{b.latency}ms', + 'text': f'Heartbeat took more than {b.max_latency}ms to be processed', + 'severity': want_severity + }, + 'ok': { + 'event': 'HeartbeatOK', + 'value': '', + 'text': 'Heartbeat OK', + 'severity': default_normal_severity + } + } - if b.status == 'expired': # aka. "stale" - client.send_alert( - resource=b.origin, - event='HeartbeatFail', - environment=want_environment, - severity=want_severity, - correlate=['HeartbeatFail', 'HeartbeatSlow', 'HeartbeatOK'], - service=want_service, - group=want_group, - value='{}'.format(b.since), - text='Heartbeat not received in {} seconds'.format(b.timeout), - tags=b.tags, - attributes=b.attributes, - origin=origin(), - type='heartbeatAlert', - timeout=timeout, - customer=b.customer - ) - elif b.status == 'slow': - client.send_alert( - resource=b.origin, - event='HeartbeatSlow', - environment=want_environment, - severity=want_severity, - correlate=['HeartbeatFail', 'HeartbeatSlow', 'HeartbeatOK'], - service=want_service, - group=want_group, - value='{}ms'.format(b.latency), - text='Heartbeat took more than {}ms to be processed'.format(b.max_latency), - tags=b.tags, - attributes=b.attributes, - origin=origin(), - type='heartbeatAlert', - timeout=timeout, - customer=b.customer - ) - else: - client.send_alert( - resource=b.origin, - event='HeartbeatOK', - environment=want_environment, - severity=default_normal_severity, - correlate=['HeartbeatFail', 'HeartbeatSlow', 'HeartbeatOK'], - service=want_service, - group=want_group, - value='', - text='Heartbeat OK', - tags=b.tags, - attributes=b.attributes, - origin=origin(), - type='heartbeatAlert', - timeout=timeout, - customer=b.customer + state = state_map[b.status] + found_alert = None + for a in alerts: + if a.environment == want_environment and a.resource == b.origin: + found_alert = alerts.pop(alerts.index(a)) + break + + if found_alert is None or state['event'] != found_alert.event: + new_alerts.append( + { + 'resource': b.origin, + 'event': state['event'], + 'environment': want_environment, + 'severity': state['severity'], + 'correlate': ['HeartbeatFail', 'HeartbeatSlow', 'HeartbeatOK'], + 'service': want_service, + 'group': want_group, + 'value': state['value'], + 'text': state['text'], + 'tags': b.tags, + 'attributes': b.attributes, + 'origin': origin(), + 'type': 'heartbeatAlert', + 'timeout': timeout, + 'customer': b.customer + } ) + + number_of_co = 50 + number_of_alerts = len(new_alerts) + number_of_sends = number_of_alerts // number_of_co + alert_groups = [new_alerts[i * number_of_co:(i + 1) * number_of_co] for i in range(number_of_sends)] + if number_of_alerts % number_of_co: + alert_groups.append(new_alerts[number_of_co * number_of_sends:number_of_co * number_of_sends + number_of_alerts % number_of_co]) + + with click.progressbar(alert_groups, label=f'Alerting {len(new_alerts)} heartbeats') as bar: + for b in bar: + client.send_alerts(b) + + alerts = [alert.id for alert in alerts] + number_of_deletes = len(alerts) + number_of_delete_sends = number_of_deletes // number_of_co + delete_groups = [alerts[i * number_of_co:(i + 1) * number_of_co] for i in range(number_of_delete_sends)] + if number_of_deletes % number_of_co: + delete_groups.append(alerts[number_of_co * number_of_delete_sends:number_of_co * number_of_delete_sends + number_of_deletes % number_of_co]) + with click.progressbar(delete_groups, label=f'Removing {len(alerts)} old alerts') as bar: + for delete_group in bar: + client.delete_alerts(delete_group) diff --git a/alertaclient/commands/cmd_history.py b/alertaclient/commands/cmd_history.py index a454fdd..694dd4c 100644 --- a/alertaclient/commands/cmd_history.py +++ b/alertaclient/commands/cmd_history.py @@ -7,7 +7,7 @@ @click.command('history', short_help='Show alert history') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.pass_obj diff --git a/alertaclient/commands/cmd_key.py b/alertaclient/commands/cmd_key.py index 58819e9..5889b1d 100644 --- a/alertaclient/commands/cmd_key.py +++ b/alertaclient/commands/cmd_key.py @@ -5,6 +5,7 @@ @click.command('key', short_help='Create API key') +@click.option('--api-key', '-K', help='User-defined API Key. [default: random string]') @click.option('--username', '-u', help='User (Admin only)') @click.option('--scope', 'scopes', multiple=True, help='List of permissions eg. admin:keys, write:alerts') @click.option('--duration', metavar='SECONDS', type=int, help='Duration API key is valid') @@ -12,7 +13,7 @@ @click.option('--customer', metavar='STRING', help='Customer') @click.option('--delete', '-D', metavar='ID', help='Delete API key using ID or KEY') @click.pass_obj -def cli(obj, username, scopes, duration, text, customer, delete): +def cli(obj, api_key, username, scopes, duration, text, customer, delete): """Create or delete an API key.""" client = obj['client'] if delete: @@ -20,8 +21,8 @@ def cli(obj, username, scopes, duration, text, customer, delete): else: try: expires = datetime.utcnow() + timedelta(seconds=duration) if duration else None - key = client.create_key(username, scopes, expires, text, customer) + key = client.create_key(username, scopes, expires, text, customer, key=api_key) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(key.key) diff --git a/alertaclient/commands/cmd_keys.py b/alertaclient/commands/cmd_keys.py index d740eb1..bdcf18b 100644 --- a/alertaclient/commands/cmd_keys.py +++ b/alertaclient/commands/cmd_keys.py @@ -1,12 +1,19 @@ import json +from datetime import datetime, timedelta import click from tabulate import tabulate +from alertaclient.utils import origin + @click.command('keys', short_help='List API keys') +@click.option('--alert', is_flag=True, help='Alert on expiring and expired keys') +@click.option('--maxage', metavar='DAYS', default=7, type=int, help='Max remaining days before alerting') +@click.option('--timeout', metavar='SECONDS', default=86400, type=int, help='Seconds before expired key alerts will be expired') +@click.option('--severity', '-s', metavar='SEVERITY', default='warning', help='Severity for expiring and expired alerts') @click.pass_obj -def cli(obj): +def cli(obj, alert, maxage, severity, timeout): """List API keys.""" client = obj['client'] @@ -20,3 +27,62 @@ def cli(obj): 'expireTime': 'EXPIRES', 'count': 'COUNT', 'lastUsedTime': 'LAST USED', 'customer': 'CUSTOMER' } click.echo(tabulate([k.tabular(timezone) for k in client.get_keys()], headers=headers, tablefmt=obj['output'])) + + if alert: + keys = r['keys'] + service = ['Alerta'] + group = 'System' + environment = 'Production' + with click.progressbar(keys, label=f'Analysing {len(keys)} keys') as bar: + for b in bar: + if b['status'] == 'expired': + client.send_alert( + resource=b['id'], + event='ApiKeyExpired', + environment=environment, + severity=severity, + correlate=['ApiKeyExpired', 'ApiKeyExpiring', 'ApiKeyOK'], + service=service, + group=group, + value='Expired', + text=f"Key expired on {b['expireTime']}", + origin=origin(), + type='apiKeyAlert', + timeout=timeout, + customer=b['customer'] + ) + elif b['status'] == 'active': + expiration = datetime.fromisoformat(b['expireTime'].split('.')[0]) + remaining_validity = expiration - datetime.now() + if remaining_validity < timedelta(days=maxage): + client.send_alert( + resource=b['id'], + event='ApiKeyExpiring', + environment=environment, + severity=severity, + correlate=['ApiKeyExpired', 'ApiKeyExpiring', 'ApiKeyOK'], + service=service, + group=group, + value=str(remaining_validity), + text=f"Key is active and expires on {b['expireTime']}", + origin=origin(), + type='apiKeyAlert', + timeout=timeout, + customer=b['customer'] + ) + else: + client.send_alert( + resource=b['id'], + event='ApiKeyOK', + environment=environment, + severity='ok', + correlate=['ApiKeyExpired', 'ApiKeyExpiring', 'ApiKeyOK'], + service=service, + group=group, + value=str(remaining_validity), + text=f"Key is active and expires on {b['expireTime']}", + origin=origin(), + type='apiKeyAlert', + timeout=timeout, + customer=b['customer'] + ) diff --git a/alertaclient/commands/cmd_login.py b/alertaclient/commands/cmd_login.py index 74191f6..ddbeac3 100644 --- a/alertaclient/commands/cmd_login.py +++ b/alertaclient/commands/cmd_login.py @@ -37,7 +37,7 @@ def cli(obj, username): password = click.prompt('Password', hide_input=True) token = client.login(username, password)['token'] else: - click.echo('ERROR: unknown provider {provider}'.format(provider=provider), err=True) + click.echo(f'ERROR: unknown provider {provider}', err=True) sys.exit(1) except Exception as e: raise AuthError(e) @@ -46,7 +46,7 @@ def cli(obj, username): preferred_username = jwt.parse(token)['preferred_username'] if preferred_username: save_token(client.endpoint, preferred_username, token) - click.echo('Logged in as {}'.format(preferred_username)) + click.echo(f'Logged in as {preferred_username}') else: click.echo('Failed to login.') sys.exit(1) diff --git a/alertaclient/commands/cmd_me.py b/alertaclient/commands/cmd_me.py index 09e7640..e073f73 100644 --- a/alertaclient/commands/cmd_me.py +++ b/alertaclient/commands/cmd_me.py @@ -20,6 +20,6 @@ def cli(obj, name, email, password, status, text): try: user = client.update_me(name=name, email=email, password=password, status=status, attributes=None, text=text) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(user.id) diff --git a/alertaclient/commands/cmd_note.py b/alertaclient/commands/cmd_note.py index 7b77212..f341163 100644 --- a/alertaclient/commands/cmd_note.py +++ b/alertaclient/commands/cmd_note.py @@ -4,15 +4,30 @@ @click.command('note', short_help='Add note') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of note IDs') -@click.option('--alert-ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--alert-ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Note or message') -@click.option('--delete', '-D', metavar='ID', nargs=2, help='Delete note parent ID and note ID') +@click.option('--delete', '-D', metavar='ID', nargs=2, help='Delete note, using alert ID and note ID') @click.pass_obj -def cli(obj, ids, alert_ids, query, filters, text, delete): - """Add or delete note to alerts.""" +def cli(obj, alert_ids, query, filters, text, delete): + """ + Add or delete note to alerts. + + EXAMPLES + + Add a note to an alert. + + $ alerta note --alert-ids --text + + List notes for an alert. + + $ alerta notes --alert-ids + + Delete a note for an alert. + + $ alerta note -D + """ client = obj['client'] if delete: client.delete_alert_note(*delete) @@ -27,6 +42,6 @@ def cli(obj, ids, alert_ids, query, filters, text, delete): total, _, _ = client.get_count(query) alert_ids = [a.id for a in client.get_alerts(query)] - with click.progressbar(alert_ids, label='Add note to {} alerts'.format(total)) as bar: + with click.progressbar(alert_ids, label=f'Add note to {total} alerts') as bar: for id in bar: client.alert_note(id, text=text) diff --git a/alertaclient/commands/cmd_notes.py b/alertaclient/commands/cmd_notes.py index 7435f67..fc57943 100644 --- a/alertaclient/commands/cmd_notes.py +++ b/alertaclient/commands/cmd_notes.py @@ -5,14 +5,14 @@ @click.command('notes', short_help='List notes') -@click.option('--alert-id', '-i', metavar='UUID', help='alert IDs (can use short 8-char id)') +@click.option('--alert-id', '-i', metavar='ID', help='alert IDs (can use short 8-char id)') @click.pass_obj def cli(obj, alert_id): """List notes.""" client = obj['client'] if alert_id: if obj['output'] == 'json': - r = client.http.get('/alert/{}/notes'.format(alert_id)) + r = client.http.get(f'/alert/{alert_id}/notes') click.echo(json.dumps(r['notes'], sort_keys=True, indent=4, ensure_ascii=False)) else: timezone = obj['timezone'] diff --git a/alertaclient/commands/cmd_notfication_delay.py b/alertaclient/commands/cmd_notfication_delay.py new file mode 100644 index 0000000..f6e5a58 --- /dev/null +++ b/alertaclient/commands/cmd_notfication_delay.py @@ -0,0 +1,9 @@ +import click + + +@click.command('fire_delayed_notifications', short_help='Fire delayed notifications') +@click.pass_obj +def cli(obj): + """Firing delayed notification rules""" + client = obj['client'] + client.fire_delayed_notifications() diff --git a/alertaclient/commands/cmd_perm.py b/alertaclient/commands/cmd_perm.py index 361acee..236ee59 100644 --- a/alertaclient/commands/cmd_perm.py +++ b/alertaclient/commands/cmd_perm.py @@ -21,6 +21,6 @@ def cli(obj, role, scopes, delete): try: perm = client.create_perm(role, scopes) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(perm.id) diff --git a/alertaclient/commands/cmd_query.py b/alertaclient/commands/cmd_query.py index 9ba95e4..f794cee 100644 --- a/alertaclient/commands/cmd_query.py +++ b/alertaclient/commands/cmd_query.py @@ -17,7 +17,7 @@ @click.command('query', short_help='Search for alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--oneline', 'display', flag_value='oneline', default=True, help='Show alerts using table format') @@ -72,21 +72,21 @@ def cli(obj, ids, query, filters, display, from_date=None): alert.group, alert.event, alert.value or 'n/a'), fg=color['fg']) - click.secho(' |{}'.format(alert.text), fg=color['fg']) + click.secho(f' |{alert.text}', fg=color['fg']) if display == 'full': click.secho(' severity | {} -> {}'.format(alert.previous_severity, alert.severity), fg=color['fg']) - click.secho(' trend | {}'.format(alert.trend_indication), fg=color['fg']) - click.secho(' status | {}'.format(alert.status), fg=color['fg']) - click.secho(' resource | {}'.format(alert.resource), fg=color['fg']) - click.secho(' group | {}'.format(alert.group), fg=color['fg']) - click.secho(' event | {}'.format(alert.event), fg=color['fg']) - click.secho(' value | {}'.format(alert.value), fg=color['fg']) + click.secho(f' trend | {alert.trend_indication}', fg=color['fg']) + click.secho(f' status | {alert.status}', fg=color['fg']) + click.secho(f' resource | {alert.resource}', fg=color['fg']) + click.secho(f' group | {alert.group}', fg=color['fg']) + click.secho(f' event | {alert.event}', fg=color['fg']) + click.secho(f' value | {alert.value}', fg=color['fg']) click.secho(' tags | {}'.format(' '.join(alert.tags)), fg=color['fg']) for key, value in alert.attributes.items(): - click.secho(' {} | {}'.format(key.ljust(10), value), fg=color['fg']) + click.secho(f' {key.ljust(10)} | {value}', fg=color['fg']) latency = alert.receive_time - alert.create_time @@ -96,18 +96,18 @@ def cli(obj, ids, query, filters, display, from_date=None): DateTime.localtime(alert.receive_time, timezone)), fg=color['fg']) click.secho(' last received | {}'.format( DateTime.localtime(alert.last_receive_time, timezone)), fg=color['fg']) - click.secho(' latency | {}ms'.format(latency.microseconds / 1000), fg=color['fg']) - click.secho(' timeout | {}s'.format(alert.timeout), fg=color['fg']) + click.secho(f' latency | {latency.microseconds / 1000}ms', fg=color['fg']) + click.secho(f' timeout | {alert.timeout}s', fg=color['fg']) - click.secho(' alert id | {}'.format(alert.id), fg=color['fg']) - click.secho(' last recv id | {}'.format(alert.last_receive_id), fg=color['fg']) - click.secho(' customer | {}'.format(alert.customer), fg=color['fg']) - click.secho(' environment | {}'.format(alert.environment), fg=color['fg']) + click.secho(f' alert id | {alert.id}', fg=color['fg']) + click.secho(f' last recv id | {alert.last_receive_id}', fg=color['fg']) + click.secho(f' customer | {alert.customer}', fg=color['fg']) + click.secho(f' environment | {alert.environment}', fg=color['fg']) click.secho(' service | {}'.format(','.join(alert.service)), fg=color['fg']) - click.secho(' resource | {}'.format(alert.resource), fg=color['fg']) - click.secho(' type | {}'.format(alert.event_type), fg=color['fg']) - click.secho(' repeat | {}'.format(alert.repeat), fg=color['fg']) - click.secho(' origin | {}'.format(alert.origin), fg=color['fg']) + click.secho(f' resource | {alert.resource}', fg=color['fg']) + click.secho(f' type | {alert.event_type}', fg=color['fg']) + click.secho(f' repeat | {alert.repeat}', fg=color['fg']) + click.secho(f' origin | {alert.origin}', fg=color['fg']) click.secho(' correlate | {}'.format(','.join(alert.correlate)), fg=color['fg']) return auto_refresh, last_time diff --git a/alertaclient/commands/cmd_raw.py b/alertaclient/commands/cmd_raw.py index c5b74d8..e48ca92 100644 --- a/alertaclient/commands/cmd_raw.py +++ b/alertaclient/commands/cmd_raw.py @@ -5,7 +5,7 @@ @click.command('raw', short_help='Show alert raw data') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.pass_obj diff --git a/alertaclient/commands/cmd_reactivate_notification_rules.py b/alertaclient/commands/cmd_reactivate_notification_rules.py new file mode 100644 index 0000000..10a45db --- /dev/null +++ b/alertaclient/commands/cmd_reactivate_notification_rules.py @@ -0,0 +1,9 @@ +import click + + +@click.command('reactivate_notitification_rules', short_help='Reactivate inactive notification rules after reactivate time is up') +@click.pass_obj +def cli(obj): + """Trigger reactivation of notification rules""" + client = obj['client'] + client.reactivate_notification_rules() diff --git a/alertaclient/commands/cmd_revoke.py b/alertaclient/commands/cmd_revoke.py index 346edd7..8adb128 100644 --- a/alertaclient/commands/cmd_revoke.py +++ b/alertaclient/commands/cmd_revoke.py @@ -4,7 +4,7 @@ @click.command('revoke', short_help='Revoke API key') -@click.option('--api-key', '-K', required=True, help='API Key or UUID') +@click.option('--api-key', '-K', required=True, help='API Key or ID') @click.pass_context def cli(ctx, api_key): ctx.invoke(revoke, delete=api_key) diff --git a/alertaclient/commands/cmd_scopes.py b/alertaclient/commands/cmd_scopes.py new file mode 100644 index 0000000..280d3f3 --- /dev/null +++ b/alertaclient/commands/cmd_scopes.py @@ -0,0 +1,18 @@ +import json + +import click +from tabulate import tabulate + + +@click.command('scopes', short_help='List scopes') +@click.pass_obj +def cli(obj): + """List scopes.""" + client = obj['client'] + + if obj['output'] == 'json': + r = client.http.get('/scopes') + click.echo(json.dumps(r['scopes'], sort_keys=True, indent=4, ensure_ascii=False)) + else: + headers = {'scope': 'SCOPE'} + click.echo(tabulate([s.tabular() for s in client.get_scopes()], headers=headers, tablefmt=obj['output'])) diff --git a/alertaclient/commands/cmd_send.py b/alertaclient/commands/cmd_send.py index 00ba2a3..bf01a2c 100644 --- a/alertaclient/commands/cmd_send.py +++ b/alertaclient/commands/cmd_send.py @@ -48,15 +48,15 @@ def send_alert(resource, event, **kwargs): customer=kwargs.get('customer') ) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) if alert: if alert.repeat: - message = '{} duplicates'.format(alert.duplicate_count) + message = f'{alert.duplicate_count} duplicates' else: - message = '{} -> {}'.format(alert.previous_severity, alert.severity) - click.echo('{} ({})'.format(id, message)) + message = f'{alert.previous_severity} -> {alert.severity}' + click.echo(f'{id} ({message})') # read raw data from file or stdin if raw_data and raw_data.startswith('@') or raw_data == '-': @@ -71,7 +71,7 @@ def send_alert(resource, event, **kwargs): try: payload = json.loads(line) except Exception as e: - click.echo("ERROR: JSON parse failure - input must be in 'json_lines' format: {}".format(e), err=True) + click.echo(f"ERROR: JSON parse failure - input must be in 'json_lines' format: {e}", err=True) sys.exit(1) send_alert(**payload) sys.exit(0) diff --git a/alertaclient/commands/cmd_shelve.py b/alertaclient/commands/cmd_shelve.py index 5bc25c3..20a4e40 100644 --- a/alertaclient/commands/cmd_shelve.py +++ b/alertaclient/commands/cmd_shelve.py @@ -4,7 +4,7 @@ @click.command('shelve', short_help='Shelve alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--timeout', metavar='SECONDS', type=int, help='Seconds before alert auto-unshelved.', default=7200, show_default=True) @@ -24,4 +24,4 @@ def cli(obj, ids, query, filters, timeout, text): ids = [a.id for a in client.get_alerts(query)] action_progressbar(client, action='shelve', ids=ids, - label='Shelving {} alerts'.format(total), text=text, timeout=timeout) + label=f'Shelving {total} alerts', text=text, timeout=timeout) diff --git a/alertaclient/commands/cmd_signup.py b/alertaclient/commands/cmd_signup.py index dfa736d..cf6be9c 100644 --- a/alertaclient/commands/cmd_signup.py +++ b/alertaclient/commands/cmd_signup.py @@ -22,7 +22,7 @@ def cli(obj, name, email, password, status, text): try: r = client.signup(name=name, email=email, password=password, status=status, attributes=None, text=text) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) if 'token' in r: click.echo('Signed Up.') diff --git a/alertaclient/commands/cmd_tag.py b/alertaclient/commands/cmd_tag.py index a10e010..f98c678 100644 --- a/alertaclient/commands/cmd_tag.py +++ b/alertaclient/commands/cmd_tag.py @@ -4,7 +4,7 @@ @click.command('tag', short_help='Tag alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--tag', '-T', 'tags', required=True, multiple=True, help='List of tags') @@ -22,6 +22,6 @@ def cli(obj, ids, query, filters, tags): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - with click.progressbar(ids, label='Tagging {} alerts'.format(total)) as bar: + with click.progressbar(ids, label=f'Tagging {total} alerts') as bar: for id in bar: client.tag_alert(id, tags) diff --git a/alertaclient/commands/cmd_token.py b/alertaclient/commands/cmd_token.py index a99e651..044bbae 100644 --- a/alertaclient/commands/cmd_token.py +++ b/alertaclient/commands/cmd_token.py @@ -1,11 +1,21 @@ import click +from alertaclient.auth.token import Jwt from alertaclient.auth.utils import get_token @click.command('token', short_help='Display current auth token') +@click.option('--decode', '-D', is_flag=True, help='Decode auth token.') @click.pass_obj -def cli(obj): - """Display the auth token for logged in user.""" +def cli(obj, decode): + """Display the auth token for logged in user, with option to decode it.""" client = obj['client'] - click.echo(get_token(client.endpoint)) + token = get_token(client.endpoint) + if decode: + jwt = Jwt() + for k, v in jwt.parse(token).items(): + if isinstance(v, list): + v = ', '.join(v) + click.echo(f'{k:20}: {v}') + else: + click.echo(token) diff --git a/alertaclient/commands/cmd_unack.py b/alertaclient/commands/cmd_unack.py index 4242432..64b25d5 100644 --- a/alertaclient/commands/cmd_unack.py +++ b/alertaclient/commands/cmd_unack.py @@ -4,7 +4,7 @@ @click.command('unack', short_help='Un-acknowledge alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Message associated with status change') @@ -22,4 +22,4 @@ def cli(obj, ids, query, filters, text): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - action_progressbar(client, action='unack', ids=ids, label='Un-acking {} alerts'.format(total), text=text) + action_progressbar(client, action='unack', ids=ids, label=f'Un-acking {total} alerts', text=text) diff --git a/alertaclient/commands/cmd_unshelve.py b/alertaclient/commands/cmd_unshelve.py index ac23f4c..278a8e7 100644 --- a/alertaclient/commands/cmd_unshelve.py +++ b/alertaclient/commands/cmd_unshelve.py @@ -4,7 +4,7 @@ @click.command('unshelve', short_help='Un-shelve alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--text', help='Message associated with status change') @@ -22,4 +22,4 @@ def cli(obj, ids, query, filters, text): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - action_progressbar(client, 'unshelve', ids, label='Un-shelving {} alerts'.format(total), text=text) + action_progressbar(client, 'unshelve', ids, label=f'Un-shelving {total} alerts', text=text) diff --git a/alertaclient/commands/cmd_untag.py b/alertaclient/commands/cmd_untag.py index 579ce33..d2026cd 100644 --- a/alertaclient/commands/cmd_untag.py +++ b/alertaclient/commands/cmd_untag.py @@ -4,7 +4,7 @@ @click.command('untag', short_help='Untag alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--tag', '-T', 'tags', required=True, multiple=True, help='List of tags') @@ -22,6 +22,6 @@ def cli(obj, ids, query, filters, tags): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - with click.progressbar(ids, label='Untagging {} alerts'.format(total)) as bar: + with click.progressbar(ids, label=f'Untagging {total} alerts') as bar: for id in bar: client.untag_alert(id, tags) diff --git a/alertaclient/commands/cmd_update.py b/alertaclient/commands/cmd_update.py index 71f68ed..90c0155 100644 --- a/alertaclient/commands/cmd_update.py +++ b/alertaclient/commands/cmd_update.py @@ -4,7 +4,7 @@ @click.command('update', short_help='Update alert attributes') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--attributes', '-A', metavar='KEY=VALUE', multiple=True, required=True, help='List of attributes eg. priority=high') @@ -22,6 +22,6 @@ def cli(obj, ids, query, filters, attributes): total, _, _ = client.get_count(query) ids = [a.id for a in client.get_alerts(query)] - with click.progressbar(ids, label='Updating {} alerts'.format(total)) as bar: + with click.progressbar(ids, label=f'Updating {total} alerts') as bar: for id in bar: client.update_attributes(id, dict(a.split('=') for a in attributes)) diff --git a/alertaclient/commands/cmd_user.py b/alertaclient/commands/cmd_user.py index 889a931..491853e 100644 --- a/alertaclient/commands/cmd_user.py +++ b/alertaclient/commands/cmd_user.py @@ -1,6 +1,7 @@ import sys import click +from tabulate import tabulate class CommandWithOptionalPassword(click.Command): @@ -19,7 +20,7 @@ def parse_args(self, ctx, args): @click.command('user', cls=CommandWithOptionalPassword, short_help='Update user') -@click.option('--id', '-i', metavar='UUID', help='User ID') +@click.option('--id', '-i', metavar='ID', help='User ID') @click.option('--name', help='Name of user') @click.option('--email', help='Email address (login username)') @click.option('--password', help='Password (will prompt if not supplied)') @@ -27,26 +28,37 @@ def parse_args(self, ctx, args): @click.option('--role', 'roles', multiple=True, help='List of roles') @click.option('--text', help='Description of user') @click.option('--email-verified/--email-not-verified', default=None, help='Email address verified flag') -@click.option('--delete', '-D', metavar='UUID', help='Delete user using ID') +@click.option('--groups', is_flag=True, help='Get list of user groups') +@click.option('--delete', '-D', metavar='ID', help='Delete user using ID') @click.pass_obj -def cli(obj, id, name, email, password, status, roles, text, email_verified, delete): - """Create user or update user details, including password reset.""" +def cli(obj, id, name, email, password, status, roles, text, email_verified, groups, delete): + """Create user, show or update user details, including password reset, list user groups and delete user.""" client = obj['client'] - if delete: + if groups: + user_groups = client.get_user_groups(id) + headers = {'id': 'ID', 'name': 'USER', 'text': 'TEXT', 'count': 'COUNT'} + click.echo(tabulate([ug.tabular() for ug in user_groups], headers=headers, tablefmt=obj['output'])) + elif delete: client.delete_user(delete) elif id: if not any([name, email, password, status, roles, text, (email_verified is not None)]): - click.echo('Nothing to update.') - sys.exit(1) - try: - user = client.update_user( - id, name=name, email=email, password=password, status=status, - roles=roles, attributes=None, text=text, email_verified=email_verified - ) - except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) - sys.exit(1) - click.echo(user.id) + user = client.get_user(id) + timezone = obj['timezone'] + headers = {'id': 'ID', 'name': 'USER', 'email': 'EMAIL', 'roles': 'ROLES', 'status': 'STATUS', + 'text': 'TEXT', + 'createTime': 'CREATED', 'updateTime': 'LAST UPDATED', 'lastLogin': 'LAST LOGIN', + 'email_verified': 'VERIFIED'} + click.echo(tabulate([user.tabular(timezone)], headers=headers, tablefmt=obj['output'])) + else: + try: + user = client.update_user( + id, name=name, email=email, password=password, status=status, + roles=roles, attributes=None, text=text, email_verified=email_verified + ) + except Exception as e: + click.echo(f'ERROR: {e}', err=True) + sys.exit(1) + click.echo(user.id) else: if not email: raise click.UsageError('Need "--email" to create user.') @@ -58,6 +70,6 @@ def cli(obj, id, name, email, password, status, roles, text, email_verified, del roles=roles, attributes=None, text=text, email_verified=email_verified ) except Exception as e: - click.echo('ERROR: {}'.format(e), err=True) + click.echo(f'ERROR: {e}', err=True) sys.exit(1) click.echo(user.id) diff --git a/alertaclient/commands/cmd_version.py b/alertaclient/commands/cmd_version.py index b576353..60f639e 100644 --- a/alertaclient/commands/cmd_version.py +++ b/alertaclient/commands/cmd_version.py @@ -11,7 +11,7 @@ def cli(ctx, obj): """Show Alerta server and client versions.""" client = obj['client'] click.echo('alerta {}'.format(client.mgmt_status()['version'])) - click.echo('alerta client {}'.format(client_version)) - click.echo('requests {}'.format(requests_version)) - click.echo('click {}'.format(click.__version__)) + click.echo(f'alerta client {client_version}') + click.echo(f'requests {requests_version}') + click.echo(f'click {click.__version__}') ctx.exit() diff --git a/alertaclient/commands/cmd_watch.py b/alertaclient/commands/cmd_watch.py index f9c8e5d..ffe8c9a 100644 --- a/alertaclient/commands/cmd_watch.py +++ b/alertaclient/commands/cmd_watch.py @@ -7,7 +7,7 @@ @click.command('watch', short_help='Watch alerts') -@click.option('--ids', '-i', metavar='UUID', multiple=True, help='List of alert IDs (can use short 8-char id)') +@click.option('--ids', '-i', metavar='ID', multiple=True, help='List of alert IDs (can use short 8-char id)') @click.option('--query', '-q', 'query', metavar='QUERY', help='severity:"warning" AND resource:web') @click.option('--filter', '-f', 'filters', metavar='FILTER', multiple=True, help='KEY=VALUE eg. serverity=warning resource=web') @click.option('--details', is_flag=True, help='Compact output with details') diff --git a/alertaclient/commands/cmd_whoami.py b/alertaclient/commands/cmd_whoami.py index 7feef78..f3861ac 100644 --- a/alertaclient/commands/cmd_whoami.py +++ b/alertaclient/commands/cmd_whoami.py @@ -12,6 +12,6 @@ def cli(obj, show_userinfo): for k, v in userinfo.items(): if isinstance(v, list): v = ', '.join(v) - click.echo('{:20}: {}'.format(k, v)) + click.echo(f'{k:20}: {v}') else: click.echo(userinfo['preferred_username']) diff --git a/alertaclient/config.py b/alertaclient/config.py index d88f5d1..43953d1 100644 --- a/alertaclient/config.py +++ b/alertaclient/config.py @@ -11,6 +11,7 @@ 'profile': None, 'endpoint': 'http://localhost:8080', 'key': '', + 'secret': None, 'client_id': None, 'username': None, 'password': None, @@ -63,8 +64,8 @@ def get_remote_config(self, endpoint=None): r.raise_for_status() remote_config = r.json() except requests.RequestException as e: - raise ClientException('Failed to get config from {}. Reason: {}'.format(config_url, e)) + raise ClientException(f'Failed to get config from {config_url}. Reason: {e}') except json.decoder.JSONDecodeError: - raise ClientException('Failed to get config from {}: Reason: not a JSON object'.format(config_url)) + raise ClientException(f'Failed to get config from {config_url}: Reason: not a JSON object') self.options = {**remote_config, **self.options} diff --git a/alertaclient/models/blackout.py b/alertaclient/models/blackout.py index d1b3716..856f0ce 100644 --- a/alertaclient/models/blackout.py +++ b/alertaclient/models/blackout.py @@ -24,6 +24,7 @@ def __init__(self, environment, **kwargs): self.event = kwargs.get('event', None) self.group = kwargs.get('group', None) self.tags = kwargs.get('tags', None) or list() + self.origin = kwargs.get('origin', None) self.customer = kwargs.get('customer', None) self.start_time = start_time self.end_time = end_time @@ -47,6 +48,8 @@ def __init__(self, environment, **kwargs): self.priority = 6 elif self.tags: self.priority = 7 + if self.origin: + self.priority = 8 now = datetime.utcnow() if self.start_time <= now and self.end_time > now: @@ -71,6 +74,8 @@ def __repr__(self): more += 'group=%r, ' % self.group if self.tags: more += 'tags=%r, ' % self.tags + if self.origin: + more += 'origin=%r, ' % self.origin if self.customer: more += 'customer=%r, ' % self.customer @@ -100,6 +105,7 @@ def parse(cls, json): event=json.get('event', None), group=json.get('group', None), tags=json.get('tags', list()), + origin=json.get('origin', None), customer=json.get('customer', None), start_time=DateTime.parse(json.get('startTime')), end_time=DateTime.parse(json.get('endTime')), @@ -119,12 +125,13 @@ def tabular(self, timezone=None): 'event': self.event, 'group': self.group, 'tags': ','.join(self.tags), + 'origin': self.origin, 'customer': self.customer, 'startTime': DateTime.localtime(self.start_time, timezone), 'endTime': DateTime.localtime(self.end_time, timezone), - 'duration': '{}s'.format(self.duration), + 'duration': f'{self.duration}s', 'status': self.status, - 'remaining': '{}s'.format(self.remaining), + 'remaining': f'{self.remaining}s', 'user': self.user, 'createTime': DateTime.localtime(self.create_time, timezone), 'text': self.text diff --git a/alertaclient/models/enums.py b/alertaclient/models/enums.py index bb7b596..5eff97b 100644 --- a/alertaclient/models/enums.py +++ b/alertaclient/models/enums.py @@ -1,13 +1,14 @@ from enum import Enum -class Scope(str, Enum): +class Scope(str): read = 'read' write = 'write' admin = 'admin' read_alerts = 'read:alerts' write_alerts = 'write:alerts' + delete_alerts = 'delete:alerts' admin_alerts = 'admin:alerts' read_blackouts = 'read:blackouts' write_blackouts = 'write:blackouts' @@ -54,10 +55,15 @@ def from_str(action: str, resource: str = None): :return: Scope """ if resource: - return Scope('{}:{}'.format(action, resource)) + return Scope(f'{action}:{resource}') else: return Scope(action) + def tabular(self): + return { + 'scope': self + } + ADMIN_SCOPES = [Scope.admin, Scope.read, Scope.write] diff --git a/alertaclient/models/heartbeat.py b/alertaclient/models/heartbeat.py index 4b8f56f..f61292b 100644 --- a/alertaclient/models/heartbeat.py +++ b/alertaclient/models/heartbeat.py @@ -1,3 +1,4 @@ +import re from datetime import datetime, timedelta from alertaclient.utils import DateTime @@ -46,6 +47,14 @@ def parse(cls, json): if not isinstance(json.get('timeout', 0), int): raise ValueError('timeout must be an integer') + create_time = json.get('createTime') + if create_time is not None: + create_time = re.sub(r'(\d{3})(\d*)(Z)', r'\1\3', create_time) + + receiveTime = json.get('receiveTime') + if receiveTime is not None: + receiveTime = re.sub(r'(\d{3})(\d*)(Z)', r'\1\3', receiveTime) + return Heartbeat( id=json.get('id', None), origin=json.get('origin', None), @@ -53,10 +62,10 @@ def parse(cls, json): tags=json.get('tags', list()), attributes=json.get('attributes', dict()), event_type=json.get('type', None), - create_time=DateTime.parse(json.get('createTime')), + create_time=DateTime.parse(create_time), timeout=json.get('timeout', None), max_latency=json.get('maxLatency', None) or DEFAULT_MAX_LATENCY, - receive_time=DateTime.parse(json.get('receiveTime')), + receive_time=DateTime.parse(receiveTime), customer=json.get('customer', None) ) @@ -70,8 +79,8 @@ def tabular(self, timezone=None): 'createTime': DateTime.localtime(self.create_time, timezone), 'receiveTime': DateTime.localtime(self.receive_time, timezone), 'since': self.since, - 'timeout': '{}s'.format(self.timeout), - 'latency': '{:.0f}ms'.format(self.latency), - 'maxLatency': '{}ms'.format(self.max_latency), + 'timeout': f'{self.timeout}s', + 'latency': f'{self.latency:.0f}ms', + 'maxLatency': f'{self.max_latency}ms', 'status': self.status } diff --git a/alertaclient/models/note.py b/alertaclient/models/note.py index 8718949..3979eef 100644 --- a/alertaclient/models/note.py +++ b/alertaclient/models/note.py @@ -40,12 +40,12 @@ def tabular(self, timezone=None): note = { 'id': self.id, 'text': self.text, + 'createTime': DateTime.localtime(self.create_time, timezone), 'user': self.user, # 'attributes': self.attributes, 'type': self.note_type, - 'createTime': DateTime.localtime(self.create_time, timezone), - 'updateTime': DateTime.localtime(self.update_time, timezone), 'related': self.alert, + 'updateTime': DateTime.localtime(self.update_time, timezone), 'customer': self.customer } return note diff --git a/alertaclient/top.py b/alertaclient/top.py index c3756fe..702dc51 100644 --- a/alertaclient/top.py +++ b/alertaclient/top.py @@ -84,7 +84,7 @@ def update(self): # draw header self._addstr(0, 0, self.client.endpoint, curses.A_BOLD) - self._addstr(0, 'C', 'alerta {}'.format(version), curses.A_BOLD) + self._addstr(0, 'C', f'alerta {version}', curses.A_BOLD) self._addstr(0, 'R', '{}'.format(now.strftime('%H:%M:%S %d/%m/%y')), curses.A_BOLD) # TODO - draw bars diff --git a/alertaclient/utils.py b/alertaclient/utils.py index 12549d7..9577073 100644 --- a/alertaclient/utils.py +++ b/alertaclient/utils.py @@ -50,7 +50,7 @@ def action_progressbar(client, action, ids, label, text=None, timeout=None): def show_skipped(id): if not id and skipped: - return '(skipped {})'.format(skipped) + return f'(skipped {skipped})' with click.progressbar(ids, label=label, show_eta=True, item_show_func=show_skipped) as bar: for id in bar: @@ -62,4 +62,4 @@ def show_skipped(id): def origin(): prog = os.path.basename(sys.argv[0]) - return '{}/{}'.format(prog, platform.uname()[1]) + return f'{prog}/{platform.uname()[1]}' diff --git a/alertaclient/version.py b/alertaclient/version.py index e93ed63..c0b8631 100644 --- a/alertaclient/version.py +++ b/alertaclient/version.py @@ -1 +1 @@ -__version__ = '8.3.0' +__version__ = '8.5.3' diff --git a/docker-compose.ci.yaml b/docker-compose.ci.yaml index 077fd0f..e20abd2 100644 --- a/docker-compose.ci.yaml +++ b/docker-compose.ci.yaml @@ -1,8 +1,8 @@ version: '3.7' services: - api: - image: docker.pkg.github.com/alerta/alerta/alerta-api:latest + alerta: + image: alerta/alerta-web ports: - "8080:8080" depends_on: @@ -14,34 +14,17 @@ services: - ADMIN_USERS=admin@alerta.io,devops@alerta.io #default password: alerta - ADMIN_KEY=demo-key # assigned to first user in ADMIN_USERS list # - PLUGINS=reject,blackout,normalise,enhance - networks: - net: - aliases: - - api db: - image: postgres:9.6 - volumes: - - /var/lib/postgresql/data + image: postgres:14 environment: - POSTGRES_DB=monitoring - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres restart: always - networks: - net: - aliases: - - db sut: build: . depends_on: - - api - command: ["./wait-for-it.sh", "api:8080", "-t", "60", "--", "pytest", "tests/integration/"] - networks: - net: - aliases: - - sut - -networks: - net: {} + - alerta + command: ["./wait-for-it.sh", "alerta:8080", "-t", "60", "--", "pytest", "tests/integration/"] diff --git a/examples/alerta.conf b/examples/alerta.conf index 30257fa..2f75a0b 100644 --- a/examples/alerta.conf +++ b/examples/alerta.conf @@ -19,3 +19,8 @@ key = demo-key [profile development] endpoint = http://localhost:8080 key = demo-key + +[profile hmac-auth] +endpoint = http://localhost:9080/api +key = access-key +secret = secret-key diff --git a/requirements-dev.txt b/requirements-dev.txt index 52efccd..5fb0f7f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,7 @@ -mypy==0.790 -pre-commit==2.9.2 -pylint==2.6.0 -pytest-cov +mypy==1.10.1 +pre-commit==3.3.3 +pylint==3.2.5 pytest>=5.4.3 +pytest-cov python-dotenv requests_mock -twine -wheel diff --git a/requirements.txt b/requirements.txt index 013b89e..ef5ebf5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -Click==7.1.2 -pytz==2020.4 -PyYAML==5.3.1 -requests==2.25.0 -requests-hawk==1.0.1 -tabulate==0.8.7 +Click==8.1.7 +pytz==2024.1 +PyYAML==6.0.1 +requests==2.32.3 +requests-hawk==1.2.1 +tabulate==0.9.0 diff --git a/setup.py b/setup.py index 19df3fb..9a71f13 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ def read(filename): url='https://github.com/guardian/python-alerta', license='Apache License 2.0', author='Nick Satterly', - author_email='nick.satterly@gmail.com', + author_email='nfsatterly@gmail.com', packages=setuptools.find_packages(exclude=['tests']), install_requires=[ 'Click', @@ -42,12 +42,12 @@ def read(filename): 'Intended Audience :: System Administrators', 'Intended Audience :: Telecommunications Industry', 'License :: OSI Approved :: Apache Software License', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.6', 'Topic :: System :: Monitoring', 'Topic :: Software Development :: Libraries :: Python Modules' ], - python_requires='>=3.6' + python_requires='>=3.8' ) diff --git a/tests/integration/test_alerts.py b/tests/integration/test_alerts.py index 7c1a439..4b4989c 100644 --- a/tests/integration/test_alerts.py +++ b/tests/integration/test_alerts.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_alert(self): id, alert, message = self.client.send_alert( diff --git a/tests/integration/test_blackouts.py b/tests/integration/test_blackouts.py index 5414b94..517eb28 100644 --- a/tests/integration/test_blackouts.py +++ b/tests/integration/test_blackouts.py @@ -6,11 +6,12 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_blackout(self): blackout = self.client.create_blackout( - environment='Production', service=['Web', 'App'], resource='web01', event='node_down', group='Network', tags=['london', 'linux'] + environment='Production', service=['Web', 'App'], resource='web01', event='node_down', group='Network', + tags=['london', 'linux'], origin='foo/bar' ) blackout_id = blackout.id @@ -18,14 +19,17 @@ def test_blackout(self): self.assertEqual(blackout.service, ['Web', 'App']) self.assertIn('london', blackout.tags) self.assertIn('linux', blackout.tags) + self.assertEqual(blackout.origin, 'foo/bar') - blackout = self.client.update_blackout(blackout_id, environment='Development', group='Network', text='updated blackout') + blackout = self.client.update_blackout(blackout_id, environment='Development', group='Network', + origin='foo/quux', text='updated blackout') self.assertEqual(blackout.environment, 'Development') self.assertEqual(blackout.group, 'Network') + self.assertEqual(blackout.origin, 'foo/quux') self.assertEqual(blackout.text, 'updated blackout') blackout = self.client.create_blackout( - environment='Production', service=['Core'], group='Network' + environment='Production', service=['Core'], group='Network', origin='foo/baz' ) blackouts = self.client.get_blackouts() diff --git a/tests/integration/test_customers.py b/tests/integration/test_customers.py index aa713a2..819f4b3 100644 --- a/tests/integration/test_customers.py +++ b/tests/integration/test_customers.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_customer(self): customer = self.client.create_customer(customer='ACME Corp.', match='example.com') diff --git a/tests/integration/test_groups.py b/tests/integration/test_groups.py index 7c52988..6f091c3 100644 --- a/tests/integration/test_groups.py +++ b/tests/integration/test_groups.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_group(self): group = self.client.create_group(name='myGroup', text='test group') diff --git a/tests/integration/test_heartbeats.py b/tests/integration/test_heartbeats.py index 1e69bae..153d756 100644 --- a/tests/integration/test_heartbeats.py +++ b/tests/integration/test_heartbeats.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_heartbeat(self): hb = self.client.heartbeat(origin='app/web01', timeout=10, tags=['london', 'linux']) diff --git a/tests/integration/test_history.py b/tests/integration/test_history.py index 962ef51..7177c3f 100644 --- a/tests/integration/test_history.py +++ b/tests/integration/test_history.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_alert(self): id, alert, message = self.client.send_alert( diff --git a/tests/integration/test_keys.py b/tests/integration/test_keys.py index d0bc75d..5aa1b08 100644 --- a/tests/integration/test_keys.py +++ b/tests/integration/test_keys.py @@ -7,7 +7,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_key(self): api_key = self.client.create_key( @@ -23,8 +23,9 @@ def test_key(self): self.assertEqual(api_key.text, 'Updated Ops API Key') api_key = self.client.create_key( - username='key@alerta.io', scopes=[Scope.admin], text='Admin API Key' + username='key@alerta.io', scopes=[Scope.admin], text='Admin API Key', key='admin-key' ) + self.assertEqual(api_key.key, 'admin-key') api_keys = self.client.get_keys(query=[('user', 'key@alerta.io')]) self.assertEqual(len(api_keys), 2) diff --git a/tests/integration/test_notes.py b/tests/integration/test_notes.py index f71b6ae..12962ea 100644 --- a/tests/integration/test_notes.py +++ b/tests/integration/test_notes.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_notes(self): # add tests here when /notes endpoints are created diff --git a/tests/integration/test_permissions.py b/tests/integration/test_permissions.py index 647ef73..4b358d9 100644 --- a/tests/integration/test_permissions.py +++ b/tests/integration/test_permissions.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_permission(self): perm = self.client.create_perm(role='websys', scopes=['admin:users', 'admin:keys', 'write']) diff --git a/tests/integration/test_users.py b/tests/integration/test_users.py index 22713eb..f951111 100644 --- a/tests/integration/test_users.py +++ b/tests/integration/test_users.py @@ -6,7 +6,7 @@ class AlertTestCase(unittest.TestCase): def setUp(self): - self.client = Client(endpoint='http://api:8080', key='demo-key') + self.client = Client(endpoint='http://alerta:8080/api', key='demo-key') def test_user(self): users = self.client.get_users() diff --git a/tests/unit/test_blackouts.py b/tests/unit/test_blackouts.py index 2a841b3..4962ef8 100644 --- a/tests/unit/test_blackouts.py +++ b/tests/unit/test_blackouts.py @@ -13,31 +13,33 @@ def setUp(self): self.blackout = """ { "blackout": { - "createTime": "2018-08-26T20:45:04.622Z", + "createTime": "2021-04-14T20:36:06.453Z", "customer": null, "duration": 3600, - "endTime": "2018-08-26T21:45:04.622Z", + "endTime": "2021-04-14T21:36:06.453Z", "environment": "Production", - "event": null, - "group": null, - "href": "http://localhost:8080/blackout/e18a4be8-60d7-4ce2-9b3d-f18d814f7b85", - "id": "e18a4be8-60d7-4ce2-9b3d-f18d814f7b85", - "priority": 3, - "remaining": 3599, - "resource": null, + "event": "node_down", + "group": "Network", + "href": "http://local.alerta.io:8080/blackout/5ed223a3-27dc-4c4c-97d1-504f107d8a1a", + "id": "5ed223a3-27dc-4c4c-97d1-504f107d8a1a", + "origin": "foo/xyz", + "priority": 8, + "remaining": 3600, + "resource": "web01", "service": [ - "Network" + "Web", + "App" ], - "startTime": "2018-08-26T20:45:04.622Z", + "startTime": "2021-04-14T20:36:06.453Z", "status": "active", "tags": [ - "london", - "linux" + "london", + "linux" ], "text": "Network outage in Bracknell", - "user": "admin@alerta.io" + "user": "admin@alerta.dev" }, - "id": "e18a4be8-60d7-4ce2-9b3d-f18d814f7b85", + "id": "5ed223a3-27dc-4c4c-97d1-504f107d8a1a", "status": "ok" } """ @@ -45,10 +47,13 @@ def setUp(self): @requests_mock.mock() def test_blackout(self, m): m.post('http://localhost:8080/blackout', text=self.blackout) - alert = self.client.create_blackout(environment='Production', service=[ - 'Web', 'App'], resource='web01', event='node_down', group='Network', tags=['london', 'linux']) + alert = self.client.create_blackout(environment='Production', service=['Web', 'App'], resource='web01', + event='node_down', group='Network', tags=['london', 'linux'], + origin='foo/xyz', text='Network outage in Bracknell') self.assertEqual(alert.environment, 'Production') - self.assertEqual(alert.service, ['Network']) + self.assertEqual(alert.service, ['Web', 'App']) + self.assertEqual(alert.group, 'Network') self.assertIn('london', alert.tags) + self.assertEqual(alert.origin, 'foo/xyz') self.assertEqual(alert.text, 'Network outage in Bracknell') - self.assertEqual(alert.user, 'admin@alerta.io') + self.assertEqual(alert.user, 'admin@alerta.dev') diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 3ec72ab..dfd0a60 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -84,40 +84,38 @@ def test_heartbeat_cmd(self, m): @requests_mock.mock() def test_heartbeats_cmd(self, m): - heartbeats_response = """ - { - "heartbeats": [ - { - "attributes": { - "environment": "Infrastructure", - "severity": "major", - "service": ["Internal"], - "group": "Heartbeats", - "region": "EU" - }, - "createTime": "2020-03-10T20:25:54.541Z", - "customer": null, - "href": "http://127.0.0.1/heartbeat/52c202e8-d949-45ed-91e0-cdad4f37de73", - "id": "52c202e8-d949-45ed-91e0-cdad4f37de73", - "latency": 0, - "maxLatency": 2000, - "origin": "monitoring-01", - "receiveTime": "2020-03-10T20:25:54.541Z", - "since": 204, - "status": "expired", - "tags": [], - "timeout": 90, - "type": "Heartbeat" - } - ], - "status": "ok", - "total": 1 + heartbeats_response = { + 'heartbeats': [ + { + 'attributes': { + 'environment': 'Infrastructure', + 'severity': 'major', + 'service': ['Internal'], + 'group': 'Heartbeats', + 'region': 'EU' + }, + 'createTime': '2020-03-10T20:25:54.541Z', + 'customer': None, + 'href': 'http://127.0.0.1/heartbeat/52c202e8-d949-45ed-91e0-cdad4f37de73', + 'id': '52c202e8-d949-45ed-91e0-cdad4f37de73', + 'latency': 0, + 'maxLatency': 2000, + 'origin': 'monitoring-01', + 'receiveTime': '2020-03-10T20:25:54.541Z', + 'since': 204, + 'status': 'expired', + 'tags': [], + 'timeout': 90, + 'type': 'Heartbeat' + } + ], + 'status': 'ok', + 'total': 1 } - """ heartbeat_alert_response = """ { - "alert": { + "alerts": [{ "attributes": {}, "correlate": [ "HeartbeatFail", @@ -166,25 +164,31 @@ def test_heartbeats_cmd(self, m): "type": "heartbeatAlert", "updateTime": "2020-03-10T21:55:07.916Z", "value": "22ms" - }, - "id": "6cfbc30f-c2d6-4edf-b672-841070995206", + }], + "status": "ok" + } + """ + + empty_alerts_response = """ + { + "alerts":[], "status": "ok" } """ - m.get('/heartbeats', text=heartbeats_response) - m.post('/alert', text=heartbeat_alert_response) + m.get('/heartbeats', json=heartbeats_response) + m.get('/alerts', text=empty_alerts_response) + m.post('/alerts', text=heartbeat_alert_response) result = self.runner.invoke(heartbeats_cmd, ['--alert'], obj=self.obj) self.assertEqual(result.exit_code, 0, result.exception) self.assertIn('monitoring-01', result.output) - history = m.request_history - data = history[1].json() - self.assertEqual(data['environment'], 'Infrastructure') + data = m.last_request.json()[0] + self.assertEqual(data['environment'], 'Heartbeats') self.assertEqual(data['severity'], 'major') self.assertEqual(data['service'], ['Internal']) self.assertEqual(data['group'], 'Heartbeats') - self.assertEqual(data['attributes'], {'region': 'EU'}) + self.assertEqual(data['attributes'], {'environment': 'Infrastructure', 'region': 'EU'}) @requests_mock.mock() def test_whoami_cmd(self, m): diff --git a/tests/unit/test_keys.py b/tests/unit/test_keys.py index dbaa3fd..2951a66 100644 --- a/tests/unit/test_keys.py +++ b/tests/unit/test_keys.py @@ -28,7 +28,7 @@ def setUp(self): "type": "read-write", "user": "johndoe@example.com" }, - "key": "BpSG0Ck5JCqk5TJiuBSLAWuTs03QKc_527T5cDtw", + "key": "demo-key", "status": "ok" } """ @@ -36,7 +36,7 @@ def setUp(self): @requests_mock.mock() def test_key(self, m): m.post('http://localhost:8080/key', text=self.key) - api_key = self.client.create_key(username='johndoe@example.com', scopes=[ - 'write:alerts', 'admin:keys'], text='Ops API Key') + api_key = self.client.create_key(username='johndoe@example.com', scopes=['write:alerts', 'admin:keys'], + text='Ops API Key', key='demo-key') self.assertEqual(api_key.user, 'johndoe@example.com') self.assertEqual(sorted(api_key.scopes), sorted(['write:alerts', 'admin:keys']))