├── .ruff.toml
├── LICENSE
├── docs
├── sc
├── changelog.md
├── CONTRIBUTING.md
├── overrides
│ └── main.html
├── assets
│ ├── rpm.png
│ ├── temboard-logo.png
│ ├── temboard-logo-256.png
│ ├── temboard-logo-64.png
│ ├── temboard-logo-slogan.png
│ ├── temboard-logo-blanc-256.png
│ ├── rtd-material.js
│ └── temboard.css
├── screenshots
│ ├── login.png
│ ├── add-member.png
│ ├── add-user.png
│ ├── statements.png
│ ├── add-instance.png
│ ├── architecture.png
│ ├── alerting_check.png
│ ├── alerting_checks.png
│ ├── alerting_edit.png
│ ├── edit-user-admin.png
│ ├── instance-list.png
│ ├── discover-instance.png
│ ├── maintenance_hints.png
│ ├── alerting_dashboard.png
│ ├── instance-dashboard.png
│ ├── maintenance_schemas.png
│ ├── maintenance_tables.png
│ ├── maintenance_databases.png
│ ├── alerting_notification_test.png
│ ├── maintenance_schedule_vacuum.png
│ ├── alerting_notification_set_user.png
│ └── alerting_notification_set_instance.png
├── temboard-howto-statements.md
├── postgres_upgrade.md
├── compatibility_guide.md
├── quickstart.md
├── temboard-howto-maintenance.md
└── index.md
├── README.md
├── ui
├── .husky
│ ├── .gitignore
│ └── pre-commit
├── .npmrc
├── temboardui
│ ├── cli
│ │ ├── __init__.py
│ │ ├── serve.py
│ │ ├── web.py
│ │ ├── generate_key.py
│ │ ├── migratedb.py
│ │ └── routes.py
│ ├── plugins
│ │ ├── __init__.py
│ │ ├── monitoring
│ │ │ ├── model
│ │ │ │ └── __init__.py
│ │ │ ├── handlers
│ │ │ │ ├── __init__.py
│ │ │ │ └── monitoring.py
│ │ │ ├── routes.py
│ │ │ ├── templates
│ │ │ │ ├── alerting.checks.html
│ │ │ │ ├── index.html
│ │ │ │ └── alerting.check.html
│ │ │ └── pivot.py
│ │ ├── dashboard
│ │ │ ├── __init__.py
│ │ │ └── routes.py
│ │ ├── pgconf
│ │ │ ├── routes.py
│ │ │ └── __init__.py
│ │ ├── statements
│ │ │ └── templates
│ │ │ │ └── index.html
│ │ ├── activity
│ │ │ └── __init__.py
│ │ └── maintenance
│ │ │ ├── routes.py
│ │ │ └── __init__.py
│ ├── web
│ │ ├── __init__.py
│ │ └── routes
│ │ │ ├── __init__.py
│ │ │ ├── instance.py
│ │ │ ├── core.py
│ │ │ └── home.py
│ ├── handlers
│ │ ├── __init__.py
│ │ ├── settings
│ │ │ ├── __init__.py
│ │ │ └── metadata.py
│ │ └── notification.py
│ ├── model
│ │ ├── queries
│ │ │ ├── roles-count.sql
│ │ │ ├── instances-count.sql
│ │ │ ├── apikeys-delete.sql
│ │ │ ├── apikeys-purge.sql
│ │ │ ├── group-get.sql
│ │ │ ├── apikeys-insert.sql
│ │ │ ├── apikeys-select-active.sql
│ │ │ ├── apikeys-select-secret.sql
│ │ │ ├── instance-disable-plugin.sql
│ │ │ ├── instance-enable-plugin.sql
│ │ │ ├── group-delete-membership.sql
│ │ │ ├── group-insert-member.sql
│ │ │ ├── roles-select-environments.sql
│ │ │ ├── group-select-membership.sql
│ │ │ ├── environments-all.sql
│ │ │ ├── instances-all.sql
│ │ │ ├── monitoring-purge-hosts.sql
│ │ │ ├── environments-get.sql
│ │ │ ├── environment-memberships.sql
│ │ │ ├── instance-has-dba.sql
│ │ │ ├── roles-all.sql
│ │ │ ├── roles-get.sql
│ │ │ ├── roles-select-instances.sql
│ │ │ ├── monitoring-purge-instances.sql
│ │ │ ├── instance-select-email-and-phone-for-notify.sql
│ │ │ ├── instances-insert.sql
│ │ │ ├── instance-get.sql
│ │ │ └── instances-copy-as-csv.sql
│ │ └── versions
│ │ │ ├── 011_drop-agent_key.sql
│ │ │ ├── 013_alerting-index.sql
│ │ │ ├── 000_init.sql
│ │ │ ├── 007_drop-alembic.sql
│ │ │ ├── 009_apikey.sql
│ │ │ ├── 010_discover.sql
│ │ │ └── 008_monitoring-archive-wait-lock.sql
│ ├── static
│ │ └── src
│ │ │ ├── public
│ │ │ └── images
│ │ │ │ ├── heron.png
│ │ │ │ ├── favicon.ico
│ │ │ │ ├── heron-w.png
│ │ │ │ ├── sort_asc.png
│ │ │ │ ├── sort_both.png
│ │ │ │ ├── sort_desc.png
│ │ │ │ ├── temboard-logo.png
│ │ │ │ ├── sort_asc_disabled.png
│ │ │ │ ├── sort_desc_disabled.png
│ │ │ │ ├── temboard-150x32-w.png
│ │ │ │ └── ring-alt.svg
│ │ │ ├── reset-password.js
│ │ │ ├── explain.js
│ │ │ ├── settings.about.js
│ │ │ ├── activity.js
│ │ │ ├── dashboard.js
│ │ │ ├── pgconf.js
│ │ │ ├── instance.about.js
│ │ │ ├── maintenance.js
│ │ │ ├── reset-password-form.js
│ │ │ ├── settings.members.js
│ │ │ ├── alerting.checks.js
│ │ │ ├── maintenance.database.js
│ │ │ ├── maintenance.table.js
│ │ │ ├── maintenance.schema.js
│ │ │ ├── notifications.js
│ │ │ ├── daterangepicker-override.scss
│ │ │ ├── home.js
│ │ │ ├── monitoring.js
│ │ │ ├── statements.js
│ │ │ ├── alerting.check.js
│ │ │ ├── login.js
│ │ │ ├── components
│ │ │ ├── configuration
│ │ │ │ └── SettingSwitch.vue
│ │ │ ├── settings
│ │ │ │ └── InstanceDetails.vue
│ │ │ ├── ModalDialog.vue
│ │ │ ├── Error.vue
│ │ │ └── home
│ │ │ │ └── Sparkline.vue
│ │ │ ├── _bootstrap-variables-override.scss
│ │ │ ├── utils
│ │ │ ├── state.js
│ │ │ └── duration.js
│ │ │ ├── settings.notifications.js
│ │ │ ├── settings.environments.js
│ │ │ └── settings.users.js
│ ├── templates
│ │ ├── flask
│ │ │ ├── reset-password-form.html
│ │ │ ├── reset-password.html
│ │ │ ├── explain.html
│ │ │ ├── error.html
│ │ │ ├── 401.html
│ │ │ ├── 404.html
│ │ │ ├── home.html
│ │ │ ├── configuration.html
│ │ │ ├── instance-about.html
│ │ │ ├── about.html
│ │ │ ├── activity.html
│ │ │ ├── maintenance
│ │ │ │ ├── index.html
│ │ │ │ ├── database.html
│ │ │ │ ├── schema.html
│ │ │ │ ├── table.html
│ │ │ │ └── breadcrumb.html
│ │ │ ├── settings
│ │ │ │ ├── members.html
│ │ │ │ └── menu.html
│ │ │ ├── 403.html
│ │ │ ├── dashboard.html
│ │ │ └── login.html
│ │ ├── error.html
│ │ ├── unauthorized.html
│ │ └── notifications.html
│ ├── errors.py
│ ├── __init__.py
│ ├── version.py
│ └── core.py
├── packaging
│ ├── .gitignore
│ ├── nfpm
│ │ ├── Makefile
│ │ ├── mkchanges.sh
│ │ ├── docker-compose.yml
│ │ └── nfpm.yaml
│ ├── temboard.service
│ └── docker
│ │ ├── entrypoint.sh
│ │ └── Dockerfile
├── .dockerignore
├── tests
│ └── unit
│ │ ├── pytest.ini
│ │ ├── test_pivot.py
│ │ ├── test_main.py
│ │ └── test_model.py
├── share
│ ├── sql
│ │ ├── pg_stat_statements-create-extension.sql
│ │ └── upgrade-4-5.sql
│ ├── postinst.sh
│ ├── preun.sh
│ ├── temboard_CHANGEME.pem
│ ├── temboard_ca_certs_CHANGEME.pem
│ ├── purge.sh
│ ├── temboard_CHANGEME.key
│ └── temboard.conf
├── vendor.in
├── .prettierignore
├── postcss.config.js
├── .prettierrc.json
├── vendor.txt
├── LICENSE
├── package.json
└── pyproject.toml
├── dev
├── prometheus
│ ├── import
│ │ └── .gitkeep
│ ├── targets
│ │ └── .gitkeep
│ └── prometheus.yml
├── postgres-setup-primary.sql
├── requirements.txt
├── git-blame-ignore-revs
├── grafana
│ ├── entrypoint.sh
│ └── rootfs
│ │ └── etc
│ │ └── grafana
│ │ └── provisioning
│ │ ├── datasources
│ │ ├── loki.yaml
│ │ └── prometheus.yaml
│ │ └── dashboards
│ │ └── perf.yaml
├── bin
│ ├── mkenv
│ ├── checkdocker
│ ├── mktargets
│ └── switchover.sh
├── postgres-setup-replication.sh
├── loki
│ └── loki-config.yaml
├── agent-entrypoint.sh
├── signing-public.pem
├── lnav
│ └── formats
│ │ └── postgresql_log.json
└── docker-compose.massagent.yml
├── agent
├── temboardagent
│ ├── cli
│ │ ├── __init__.py
│ │ ├── web.py
│ │ ├── discover.py
│ │ ├── runscript.py
│ │ ├── runtask.py
│ │ ├── serve.py
│ │ ├── tasks.py
│ │ ├── routes.py
│ │ └── fetch_key.py
│ ├── plugins
│ │ ├── __init__.py
│ │ ├── pgconf
│ │ │ ├── types.py
│ │ │ └── __init__.py
│ │ └── monitoring
│ │ │ └── output.py
│ ├── web
│ │ └── __init__.py
│ ├── core.py
│ ├── version.py
│ ├── queries
│ │ ├── __init__.py
│ │ ├── status.sql
│ │ ├── discover.sql
│ │ ├── discover-settings.sql
│ │ ├── monitoring-bgwriter.sql
│ │ ├── pgconf-settings.sql
│ │ ├── activity-sessions.sql
│ │ └── activity-locks.sql
│ ├── errors.py
│ ├── __init__.py
│ ├── __main__.py
│ └── command.py
├── vendor.in
├── packaging
│ ├── docker
│ │ ├── .gitignore
│ │ ├── sudoers
│ │ ├── Dockerfile.dev
│ │ └── Dockerfile
│ ├── nfpm
│ │ ├── .env
│ │ ├── Makefile
│ │ ├── mkchanges.sh
│ │ ├── docker-compose.yml
│ │ └── nfpm.yaml
│ └── temboard-agent@.service
├── .dockerignore
├── tests
│ └── unit
│ │ ├── pytest.ini
│ │ ├── test_core.py
│ │ └── test_postgres.py
├── vendor.txt
├── README.md
├── share
│ ├── restart-all.sh
│ ├── preun.sh
│ ├── purge.sh
│ ├── temboard-agent_CHANGEME.pem
│ ├── temboard-agent_ca_certs_CHANGEME.pem
│ └── temboard-agent_CHANGEME.key
├── LICENSE
└── pyproject.toml
├── toolkit
├── temboardtoolkit
│ ├── __init__.py
│ ├── tasklist
│ │ └── __init__.py
│ ├── errors.py
│ ├── logfmt.py
│ └── versions.py
├── pyproject.toml
└── tests
│ ├── test_perf.py
│ ├── test_queries.py
│ ├── test_versions.py
│ └── test_utils.py
├── tests
├── fixtures
│ └── __init__.py
├── monitoring-files
├── statements-files
├── psql
├── pytest.ini
├── test_60_alerting.py
├── pytest-ci
└── install-all.sh
├── .config
├── .readthedocs.yaml
└── temboard.conf
├── .editorconfig
├── .github
└── ISSUE_TEMPLATE
│ └── bug_report.md
├── .gitignore
└── pyproject.toml
/.ruff.toml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | ui/LICENSE
--------------------------------------------------------------------------------
/docs/sc:
--------------------------------------------------------------------------------
1 | screenshots
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ui/README.md
--------------------------------------------------------------------------------
/ui/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/dev/prometheus/import/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dev/prometheus/targets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/changelog.md:
--------------------------------------------------------------------------------
1 | ../CHANGELOG.md
--------------------------------------------------------------------------------
/ui/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/temboardui/web/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dev/postgres-setup-primary.sql:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/docs/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | ../CONTRIBUTING.md
--------------------------------------------------------------------------------
/toolkit/temboardtoolkit/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/packaging/.gitignore:
--------------------------------------------------------------------------------
1 | build-*/
2 |
--------------------------------------------------------------------------------
/ui/temboardui/handlers/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/temboardui/web/routes/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent/temboardagent/plugins/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent/vendor.in:
--------------------------------------------------------------------------------
1 | bottle >= 0.13, <0.14
2 |
--------------------------------------------------------------------------------
/ui/temboardui/handlers/settings/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent/packaging/docker/.gitignore:
--------------------------------------------------------------------------------
1 | histfile
2 |
--------------------------------------------------------------------------------
/toolkit/temboardtoolkit/tasklist/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/model/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/agent/packaging/nfpm/.env:
--------------------------------------------------------------------------------
1 | COMPOSE_PROJECT_NAME=tbapkg
2 |
--------------------------------------------------------------------------------
/docs/overrides/main.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
--------------------------------------------------------------------------------
/agent/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/*bookworm*.deb
3 | !packaging/docker/
4 |
--------------------------------------------------------------------------------
/ui/.dockerignore:
--------------------------------------------------------------------------------
1 | *
2 | !dist/*bookworm*.deb
3 | !packaging/docker/*.sh
4 |
--------------------------------------------------------------------------------
/docs/assets/rpm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/rpm.png
--------------------------------------------------------------------------------
/docs/screenshots/login.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/login.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/roles-count.sql:
--------------------------------------------------------------------------------
1 | SELECT COUNT(1) AS "count"
2 | FROM application.roles;
3 |
--------------------------------------------------------------------------------
/ui/tests/unit/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -vvv --showlocals
3 | cache_dir = .pytest_cache
4 |
--------------------------------------------------------------------------------
/ui/share/sql/pg_stat_statements-create-extension.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
2 |
--------------------------------------------------------------------------------
/ui/vendor.in:
--------------------------------------------------------------------------------
1 | flask
2 | jinja2
3 | python-dateutil>=1.5
4 | sqlalchemy>=1.3.2,<2
5 | tornado>=6.0.2,<6.5
6 |
--------------------------------------------------------------------------------
/agent/temboardagent/web/__init__.py:
--------------------------------------------------------------------------------
1 | from .service import HTTPDService
2 |
3 | __all__ = ["HTTPDService"]
4 |
--------------------------------------------------------------------------------
/docs/assets/temboard-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/temboard-logo.png
--------------------------------------------------------------------------------
/docs/screenshots/add-member.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/add-member.png
--------------------------------------------------------------------------------
/docs/screenshots/add-user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/add-user.png
--------------------------------------------------------------------------------
/docs/screenshots/statements.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/statements.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instances-count.sql:
--------------------------------------------------------------------------------
1 | SELECT COUNT(1) AS "count"
2 | FROM application.instances;
3 |
--------------------------------------------------------------------------------
/agent/temboardagent/core.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit import taskmanager
2 |
3 | workers = taskmanager.WorkerSet()
4 |
--------------------------------------------------------------------------------
/agent/tests/unit/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -vvv --showlocals --fulltrace
3 | cache_dir = .pytest_cache
4 |
--------------------------------------------------------------------------------
/docs/assets/temboard-logo-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/temboard-logo-256.png
--------------------------------------------------------------------------------
/docs/assets/temboard-logo-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/temboard-logo-64.png
--------------------------------------------------------------------------------
/docs/screenshots/add-instance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/add-instance.png
--------------------------------------------------------------------------------
/docs/screenshots/architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/architecture.png
--------------------------------------------------------------------------------
/agent/packaging/docker/sudoers:
--------------------------------------------------------------------------------
1 | # Edit with /usr/sbin/visudo -sf
2 | Defaults !env_reset
3 | Defaults always_set_home
4 |
--------------------------------------------------------------------------------
/agent/temboardagent/version.py:
--------------------------------------------------------------------------------
1 | from importlib.metadata import version
2 |
3 | __version__ = version("temboard-agent")
4 |
--------------------------------------------------------------------------------
/docs/assets/temboard-logo-slogan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/temboard-logo-slogan.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_check.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_check.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_checks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_checks.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_edit.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_edit.png
--------------------------------------------------------------------------------
/docs/screenshots/edit-user-admin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/edit-user-admin.png
--------------------------------------------------------------------------------
/docs/screenshots/instance-list.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/instance-list.png
--------------------------------------------------------------------------------
/docs/screenshots/discover-instance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/discover-instance.png
--------------------------------------------------------------------------------
/docs/screenshots/maintenance_hints.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/maintenance_hints.png
--------------------------------------------------------------------------------
/ui/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.yml
3 | **/*.yaml
4 | **/*.html
5 | temboardui/_vendor
6 | temboardui/static/src/public
7 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/__init__.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit.queries import QueryFiler
2 |
3 | QUERIES = QueryFiler(__path__[0])
4 |
--------------------------------------------------------------------------------
/docs/assets/temboard-logo-blanc-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/assets/temboard-logo-blanc-256.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_dashboard.png
--------------------------------------------------------------------------------
/docs/screenshots/instance-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/instance-dashboard.png
--------------------------------------------------------------------------------
/docs/screenshots/maintenance_schemas.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/maintenance_schemas.png
--------------------------------------------------------------------------------
/docs/screenshots/maintenance_tables.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/maintenance_tables.png
--------------------------------------------------------------------------------
/docs/screenshots/maintenance_databases.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/maintenance_databases.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/apikeys-delete.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM "application"."apikeys"
2 | WHERE "id" = :id
3 | RETURNING "id", "comment";
4 |
--------------------------------------------------------------------------------
/agent/packaging/docker/Dockerfile.dev:
--------------------------------------------------------------------------------
1 | FROM dalibo/temboard-agent:snapshot
2 |
3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/bin/
4 |
--------------------------------------------------------------------------------
/ui/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | cd ui
5 | npx lint-staged
6 | ruff check
7 | ruff format --check
8 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/apikeys-purge.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM "application"."apikeys"
2 | WHERE "edate" < NOW()
3 | RETURNING "id", "comment";
4 |
--------------------------------------------------------------------------------
/docs/screenshots/alerting_notification_test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_notification_test.png
--------------------------------------------------------------------------------
/docs/screenshots/maintenance_schedule_vacuum.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/maintenance_schedule_vacuum.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/heron.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/heron.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_notification_set_user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_notification_set_user.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/group-get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | id,
3 | name,
4 | description
5 | FROM application.groups
6 | WHERE name = :name
7 | LIMIT 2;
8 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/favicon.ico
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/heron-w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/heron-w.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/sort_asc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/sort_asc.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/apikeys-insert.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO "application"."apikeys"("secret", "comment")
2 | VALUES (:secret, :comment)
3 | RETURNING *;
4 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/011_drop-agent_key.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 |
3 | ALTER TABLE application.instances DROP COLUMN IF EXISTS agent_key;
4 |
5 | COMMIT;
6 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/sort_both.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/sort_both.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/sort_desc.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/sort_desc.png
--------------------------------------------------------------------------------
/docs/screenshots/alerting_notification_set_instance.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/docs/screenshots/alerting_notification_set_instance.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/temboard-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/temboard-logo.png
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/apikeys-select-active.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | id, secret, comment, cdate, edate
3 | FROM "application"."apikeys"
4 | WHERE "edate" > NOW();
5 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/apikeys-select-secret.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | id, secret, comment, cdate, edate
3 | FROM "application"."apikeys"
4 | WHERE "secret" = :secret;
5 |
--------------------------------------------------------------------------------
/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | map: true,
3 | plugins: {
4 | autoprefixer: {
5 | browserlist: ["last 2 versions"],
6 | },
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instance-disable-plugin.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM application.plugins WHERE agent_address = :address AND agent_port = :port AND plugin_name = :name;
2 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/sort_asc_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/sort_asc_disabled.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/sort_desc_disabled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/sort_desc_disabled.png
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/temboard-150x32-w.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dalibo/temboard/HEAD/ui/temboardui/static/src/public/images/temboard-150x32-w.png
--------------------------------------------------------------------------------
/agent/packaging/nfpm/Makefile:
--------------------------------------------------------------------------------
1 | DISTDIR=../../dist
2 |
3 | default:
4 |
5 | build-%:
6 | mkdir -p $(DISTDIR)
7 | env -u COMPOSE_FILE docker compose run --rm -e CLEAN $*
8 |
--------------------------------------------------------------------------------
/ui/packaging/nfpm/Makefile:
--------------------------------------------------------------------------------
1 | DISTDIR=../../dist
2 |
3 | default:
4 |
5 | build-%:
6 | mkdir -p $(DISTDIR)
7 | env -u COMPOSE_FILE docker compose run -e CLEAN -e CI --rm $*
8 |
--------------------------------------------------------------------------------
/tests/fixtures/__init__.py:
--------------------------------------------------------------------------------
1 | from fixtures.agent import * # noqa: F401, F403
2 | from fixtures.postgres import * # noqa: F401, F403
3 | from fixtures.ui import * # noqa: F401, F403
4 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/reset-password.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import ResetPassword from "./views/ResetPassword.vue";
4 |
5 | createApp(ResetPassword).mount("#app");
6 |
--------------------------------------------------------------------------------
/agent/temboardagent/plugins/pgconf/types.py:
--------------------------------------------------------------------------------
1 | T_PGSETTINGS_CATEGORY = b"(^.{1,128}$)"
2 | T_FILE_VERSION = rb"([0-9]{4}\-[0-9]{2}\-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})"
3 | T_NEW_VERSION = bool
4 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instance-enable-plugin.sql:
--------------------------------------------------------------------------------
1 | INSERT INTO application.plugins (agent_address, agent_port, plugin_name)
2 | VALUES (:agent_address, :agent_port, :name)
3 | RETURNING *;
4 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/group-delete-membership.sql:
--------------------------------------------------------------------------------
1 | DELETE FROM application.memberships AS ms
2 | USING application.groups AS g
3 | WHERE g.name = :group AND ms.group_id = g.id AND ms.role_name = :role;
4 |
--------------------------------------------------------------------------------
/agent/vendor.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile --python-version=3.9 agent/vendor.in -o agent/vendor.txt
3 | bottle==0.13.4
4 | # via -r agent/vendor.in
5 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/013_alerting-index.sql:
--------------------------------------------------------------------------------
1 | SET search_path TO monitoring, public;
2 | CREATE INDEX IF NOT EXISTS idx_state_changes_key
3 | ON monitoring.state_changes (check_id, key, datetime DESC);
4 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/explain.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import Explain from "./views/Explain.vue";
4 |
5 | createApp({
6 | components: {
7 | explain: Explain,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/settings.about.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import About from "./views/About.vue";
4 |
5 | createApp({
6 | components: {
7 | about: About,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/agent/README.md:
--------------------------------------------------------------------------------
1 | # temBoard Agent
2 |
3 | This project contains the service monitoring and managing one PostgreSQL instance.
4 |
5 | Please refer to [temBoard](https://labs.dalibo.com/temboard) main project to see the full picture.
6 |
--------------------------------------------------------------------------------
/docs/temboard-howto-statements.md:
--------------------------------------------------------------------------------
1 | The *statements* feature gives user performance statistics of the SQL queries executed on the instance.
2 | It relies on the `pg_stat_statements` extension.
3 |
4 | 
5 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/activity.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import Activity from "./views/Activity.vue";
4 |
5 | createApp({
6 | components: {
7 | activity: Activity,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/dashboard.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import Dashboard from "./views/Dashboard.vue";
4 |
5 | createApp({
6 | components: {
7 | dashboard: Dashboard,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/packaging/nfpm/mkchanges.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | srcdir=$(readlink -m "$0/..")
4 | DEB=$1
5 | CODENAME=$2
6 | CHANGES=${DEB/.deb/_${CODENAME}.changes}
7 | CODENAME=$CODENAME "$srcdir/simplechanges" $DEB > $CHANGES
8 | debsign $CHANGES
9 |
--------------------------------------------------------------------------------
/agent/packaging/nfpm/mkchanges.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | srcdir=$(readlink -m "$0/..")
4 | DEB=$1
5 | CODENAME=$2
6 | CHANGES=${DEB/.deb/_${CODENAME}.changes}
7 | CODENAME=$CODENAME "$srcdir/simplechanges" "$DEB" > "$CHANGES"
8 | debsign $CHANGES
9 |
--------------------------------------------------------------------------------
/agent/tests/unit/test_core.py:
--------------------------------------------------------------------------------
1 | def test_version():
2 | from packaging.version import parse
3 | from temboardagent.version import __version__
4 |
5 | version = parse(__version__)
6 |
7 | assert "Version" == version.__class__.__name__
8 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/group-insert-member.sql:
--------------------------------------------------------------------------------
1 | WITH "group" AS (
2 | SELECT id, description FROM application.groups WHERE name = :group
3 | )
4 | INSERT INTO application.memberships (role_name, group_id)
5 | SELECT :role, id FROM "group" AS g;
6 |
--------------------------------------------------------------------------------
/ui/share/postinst.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | if ! readlink /proc/1/exe | grep -q systemd ; then
4 | echo You must restart manually temboard service. >&2
5 | exit 0
6 | fi
7 |
8 | systemctl daemon-reload
9 | systemctl try-restart temboard
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/pgconf.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import Configuration from "./views/Configuration.vue";
4 |
5 | createApp({
6 | components: {
7 | configuration: Configuration,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/000_init.sql:
--------------------------------------------------------------------------------
1 | CREATE SCHEMA IF NOT EXISTS application;
2 | CREATE TABLE IF NOT EXISTS application.schema_migration_log (
3 | version TEXT UNIQUE NOT NULL PRIMARY KEY,
4 | comment TEXT,
5 | cdate TIMESTAMP DEFAULT NOW()
6 | );
7 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/instance.about.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import InstanceAbout from "./views/InstanceAbout.vue";
4 |
5 | createApp({
6 | components: {
7 | InstanceAbout: InstanceAbout,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from os.path import realpath
2 |
3 | from ....web.tornado import Blueprint, TemplateRenderer
4 |
5 | blueprint = Blueprint()
6 | render_template = TemplateRenderer(realpath(__file__ + "/../../templates"))
7 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/maintenance.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import MaintenanceIndex from "./views/maintenance/Index.vue";
4 |
5 | createApp({
6 | components: {
7 | maintenanceindex: MaintenanceIndex,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/reset-password-form.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import ResetPasswordForm from "./views/ResetPasswordForm.vue";
4 |
5 | createApp(ResetPasswordForm, {
6 | token: document.getElementById("app").dataset.token,
7 | }).mount("#app");
8 |
--------------------------------------------------------------------------------
/agent/share/restart-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | if ! readlink /proc/1/exe | grep -q systemd ; then
4 | echo You must restart manually temboard-agent services. >&2
5 | exit 0
6 | fi
7 |
8 | systemctl daemon-reload
9 |
10 | systemctl try-restart temboard-agent@*
11 |
--------------------------------------------------------------------------------
/dev/requirements.txt:
--------------------------------------------------------------------------------
1 | check-manifest
2 | hupper
3 | pytest
4 | pytest-mock
5 | pytest-tornado
6 | pytz # for dev/perfui/
7 | mock
8 | docutils
9 | pep440deb
10 | wheel
11 | ruff==0.14.4
12 | # For integration tests
13 | httpx
14 | selenium
15 | sh<2
16 | tenacity<8.4
17 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/settings.members.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import MembersPage from "./components/settings/MembersPage.vue";
4 |
5 | createApp({
6 | components: {
7 | memberspage: MembersPage,
8 | },
9 | }).mount("#members-app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/roles-select-environments.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | e.id,
3 | e.name
4 | FROM application.environments AS e
5 | JOIN application.groups AS g ON g.id = e.dba_group_id
6 | JOIN application.memberships AS ms ON ms.group_id = g.id
7 | WHERE ms.role_name = :role_name;
8 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/007_drop-alembic.sql:
--------------------------------------------------------------------------------
1 | DROP TABLE IF EXISTS "application"."alembic_version";
2 | CREATE TABLE IF NOT EXISTS "application"."schema_migration_log" (
3 | version TEXT UNIQUE NOT NULL PRIMARY KEY,
4 | comment TEXT,
5 | cdate TIMESTAMP DEFAULT NOW()
6 | );
7 |
--------------------------------------------------------------------------------
/dev/git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Format with ruff
2 | 6d6a1edb1ff01f64e9b622134e94a9404a801a59
3 | d60e1e9077f94c10cb9c5849a0e933ef13c01f08
4 | fc61a4d1ea20c8f8a0b60eed785a1e7554ead89e
5 | c85b2e01c6b526a3308d47bbeb3a308872667182
6 | # Sort imports
7 | 4b23838e985f7c7f1de24fedd6f8170f20f62447
8 |
--------------------------------------------------------------------------------
/docs/assets/rtd-material.js:
--------------------------------------------------------------------------------
1 | document.addEventListener('readystatechange', function() {
2 | if ('complete' != document.readyState) {
3 | return
4 | }
5 |
6 | var rtd = document.querySelector('div.injected')
7 | if (null !== rtd) {
8 | rtd.remove()
9 | }
10 | })
11 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/alerting.checks.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import AlertingChecks from "./views/AlertingChecks.vue";
4 |
5 | createApp({
6 | el: "#app",
7 | components: {
8 | "alerting-checks": AlertingChecks,
9 | },
10 | }).mount("#app");
11 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/maintenance.database.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import MaintenanceDatabase from "./views/maintenance/Database.vue";
4 |
5 | createApp({
6 | components: {
7 | maintenancedatabase: MaintenanceDatabase,
8 | },
9 | }).mount("#app");
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/maintenance.table.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import MaintenanceTable from "./views/maintenance/Table.vue";
4 |
5 | createApp({
6 | el: "#app",
7 | components: {
8 | maintenancetable: MaintenanceTable,
9 | },
10 | }).mount("#app");
11 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/maintenance.schema.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 |
3 | import MaintenanceSchema from "./views/maintenance/Schema.vue";
4 |
5 | createApp({
6 | el: "#app",
7 | components: {
8 | maintenanceschema: MaintenanceSchema,
9 | },
10 | }).mount("#app");
11 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/notifications.js:
--------------------------------------------------------------------------------
1 | import DataTablesLib from "datatables.net-bs5";
2 | import DataTable from "datatables.net-vue3";
3 |
4 | DataTable.use(DataTablesLib);
5 |
6 | new DataTablesLib("#tableNotifications", {
7 | order: [[0, "desc"]],
8 | pageLength: 25,
9 | });
10 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/group-select-membership.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | ms.role_name AS username,
3 | g.name,
4 | g.description AS profile
5 | FROM application.groups AS g
6 | JOIN application.memberships AS ms ON ms.group_id = g.id
7 | WHERE g.name = :group AND ms.role_name = :role
8 | ORDER BY 1, 2;
9 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/009_apikey.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE "application"."apikeys" (
2 | id SERIAL PRIMARY KEY,
3 | secret TEXT NOT NULL UNIQUE,
4 | comment TEXT,
5 | cdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
6 | edate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() + '6 months'::interval
7 | );
8 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/routes.py:
--------------------------------------------------------------------------------
1 | # Flask routes
2 | from flask import current_app
3 |
4 | from ...web.flask import apikey_allowed, instance_proxy
5 |
6 |
7 | @instance_proxy.route("/monitoring/metrics")
8 | @apikey_allowed
9 | def get_metrics():
10 | return current_app.instance.proxy()
11 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/status.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | pg_is_in_recovery() AS is_standby,
3 | (
4 | SELECT setting
5 | FROM pg_settings
6 | WHERE name = 'primary_conninfo') AS primary_conninfo,
7 | (
8 | SELECT bool_or(pending_restart)
9 | FROM pg_settings
10 | ) AS pending_restart
11 | ;
12 |
--------------------------------------------------------------------------------
/tests/monitoring-files:
--------------------------------------------------------------------------------
1 | # Sources files requiring monitoring tests on CI
2 | .circleci/
3 | agent/temboardagent/plugins/monitoring/
4 | tests/fixtures/
5 | tests/monitoring-files
6 | tests/test_40_monitoring.py
7 | ui/temboardui/plugins/monitoring/
8 | ui/temboardui/toolkit/taskmanager/
9 | pyproject.toml
10 |
--------------------------------------------------------------------------------
/tests/statements-files:
--------------------------------------------------------------------------------
1 | # Sources files requiring statements tests on CI
2 | .circleci/
3 | agent/temboardagent/plugins/statements/
4 | tests/fixtures/
5 | tests/statements-files
6 | tests/test_50_statements.py
7 | ui/temboardui/plugins/statements/
8 | ui/temboardui/toolkit/taskmanager/
9 | pyproject.toml
10 |
--------------------------------------------------------------------------------
/toolkit/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "temboard-toolkit"
3 | version = "0.0.0"
4 | classifiers = ["Private :: Do Not Upload"]
5 |
6 | [build-system]
7 | requires = ["uv_build"]
8 | build-backend = "uv_build"
9 |
10 | [tool.uv.build-backend]
11 | module-name = "temboardtoolkit"
12 | module-root = ""
13 |
--------------------------------------------------------------------------------
/agent/tests/unit/test_postgres.py:
--------------------------------------------------------------------------------
1 | def test_pickle():
2 | from pickle import dumps as pickle
3 | from pickle import loads as unpickle
4 |
5 | from temboardagent.postgres import Postgres
6 |
7 | orig = Postgres(host="myhost")
8 | copy = unpickle(pickle(orig))
9 | assert "myhost" == copy.host
10 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/dashboard/__init__.py:
--------------------------------------------------------------------------------
1 | from ...web.flask import instance_proxy
2 |
3 |
4 | class DashboardPlugin:
5 | def __init__(self, app, **kw):
6 | self.app = app
7 |
8 | def load(self):
9 | __import__(__name__ + ".routes")
10 | instance_proxy.generic_proxy("/dashboard")
11 |
--------------------------------------------------------------------------------
/agent/temboardagent/errors.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit.errors import UserError
2 |
3 | __all__ = ["UserError"]
4 |
5 |
6 | class NotificationError(Exception):
7 | """Notification errors"""
8 |
9 | def __init__(self, message):
10 | Exception.__init__(self, message)
11 | self.message = str(message)
12 |
--------------------------------------------------------------------------------
/toolkit/temboardtoolkit/errors.py:
--------------------------------------------------------------------------------
1 | class TemboardError(Exception):
2 | """An internal temBoard error."""
3 |
4 |
5 | class UserError(Exception):
6 | def __init__(self, message, retcode=1):
7 | super().__init__(message)
8 | self.retcode = retcode
9 |
10 |
11 | class StorageEngineError(Exception):
12 | pass
13 |
--------------------------------------------------------------------------------
/agent/temboardagent/plugins/pgconf/__init__.py:
--------------------------------------------------------------------------------
1 | from bottle import default_app
2 |
3 |
4 | class PgConfPlugin:
5 | PG_MIN_VERSION = (90500, 9.5)
6 |
7 | def __init__(self, app, **kw):
8 | self.app = app
9 |
10 | def load(self):
11 | from .routes import bottle
12 |
13 | default_app().mount("/pgconf/", bottle)
14 |
--------------------------------------------------------------------------------
/dev/grafana/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eux
2 |
3 | # Copie avant de changer parternité et droits.
4 | cp -arv /usr/local/lib/grafana/rootfs/* /
5 |
6 | chown -R root:root /etc/grafana
7 | chmod -R a+r /etc/grafana
8 | chown -R grafana: /var/lib/grafana
9 | chown -R grafana: /usr/share/grafana
10 |
11 | exec su -s "$SHELL" grafana -c /run.sh
12 |
--------------------------------------------------------------------------------
/tests/psql:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | #
3 | # Helper to enter monitored psql.
4 | #
5 | # Keep it sync with conftest.py
6 | #
7 |
8 | # shellcheck disable=2086
9 | unset ${!PG*}
10 |
11 | export PGHOST=$PWD/tests/workdir/run/postgresql/
12 | export PGPASSWORD=S3cret_postgres
13 | export PGPORT=55432
14 | export PGUSER=postgres
15 |
16 | exec "psql" "$@"
17 |
--------------------------------------------------------------------------------
/ui/packaging/temboard.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=temBoard Server
3 | After=network.target
4 |
5 | [Service]
6 | Type=simple
7 | User=temboard
8 | Group=temboard
9 | Environment="SYSTEMD=1"
10 | ExecStart=/usr/bin/temboard -c /etc/temboard/temboard.conf serve
11 | ExecReload=/bin/kill -HUP $MAINPID
12 |
13 | [Install]
14 | WantedBy=multi-user.target
15 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/environments-all.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | e.id,
3 | e.name,
4 | e.description,
5 | e.color,
6 | e.dba_group_id,
7 | g.id AS g_id,
8 | g.name AS g_name,
9 | g.description AS g_description
10 | FROM application.environments AS e
11 | JOIN application.groups AS g ON g.id = e.dba_group_id
12 | ORDER BY 2
13 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/discover.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | version() AS version,
3 | (SELECT split_part(version(), ' ', 1) || ' ' || setting FROM pg_catalog.pg_settings WHERE name = 'server_version') AS version_summary,
4 | (SELECT setting::BIGINT FROM pg_catalog.pg_settings WHERE name = 'server_version_num') AS version_num,
5 | pg_postmaster_start_time() as start_time
6 | ;
7 |
--------------------------------------------------------------------------------
/dev/prometheus/prometheus.yml:
--------------------------------------------------------------------------------
1 | scrape_configs:
2 | - job_name: temboard
3 | scrape_interval: 60s
4 | scheme: https
5 | authorization:
6 | type: Bearer
7 | credentials: UNSECURE_DEV_APIKEY
8 | tls_config:
9 | insecure_skip_verify: true
10 | file_sd_configs:
11 | - files:
12 | - /targets/temboard-dev.yaml
13 | - /targets/custom.yaml
14 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/reset-password-form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 |
5 |
6 |
7 |
8 |
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/tests/pytest.ini:
--------------------------------------------------------------------------------
1 | [pytest]
2 | addopts = -v --strict-markers -p no:anyio -p no:cov -p no:logging -p no:mock -p no:tornado
3 | cache_dir = .pytest_cache
4 | markers =
5 | slowmonitoring: (slow) tests of monitoring plugin
6 | slowstatements: (slow) tests of statements plugin
7 |
8 | junit_suite_name = e2e
9 | junit_duration_report = call
10 | junit_logging = all
11 |
--------------------------------------------------------------------------------
/agent/temboardagent/plugins/monitoring/output.py:
--------------------------------------------------------------------------------
1 | def remove_passwords(instances):
2 | clean_instances = []
3 | for instance in instances:
4 | clean_instance = {}
5 | for k in instance.keys():
6 | if k != "password":
7 | clean_instance[k] = instance[k]
8 | clean_instances.append(clean_instance)
9 | return clean_instances
10 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/reset-password.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}temBoard / Reset password{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
10 |
11 | {% endblock %}
12 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instances-all.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | agent_address,
3 | agent_port,
4 | hostname,
5 | pg_port,
6 | notify,
7 | comment,
8 | discover,
9 | discover_date,
10 | discover_etag,
11 | environment_id,
12 | e.id AS e_id,
13 | e.name AS e_name
14 | FROM application.instances AS i
15 | JOIN application.environments AS e ON e.id = i.environment_id
16 | ORDER BY 1, 2;
17 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/monitoring-purge-hosts.sql:
--------------------------------------------------------------------------------
1 | WITH deleted AS (
2 | SELECT
3 | h.host_id,
4 | h.hostname
5 | FROM monitoring.hosts AS h
6 | LEFT OUTER JOIN monitoring.instances AS i USING (host_id)
7 | WHERE i.instance_id IS NULL
8 | )
9 | DELETE FROM monitoring.hosts AS h
10 | USING deleted
11 | WHERE h.host_id = deleted.host_id
12 | RETURNING deleted.hostname;
13 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/environments-get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | e.id AS e_id,
3 | e.name AS e_name,
4 | e.description AS e_description,
5 | e.color,
6 | e.dba_group_id,
7 | g.id AS g_id,
8 | g.name AS g_name,
9 | g.description AS g_description
10 | FROM application.environments AS e
11 | JOIN application.groups AS g ON g.id = e.dba_group_id
12 | WHERE e.name = :name
13 |
--------------------------------------------------------------------------------
/ui/temboardui/errors.py:
--------------------------------------------------------------------------------
1 | class CLIError(Exception):
2 | """CLI errors"""
3 |
4 | def __init__(self, message):
5 | Exception.__init__(self, message)
6 | self.message = str(message)
7 |
8 |
9 | class TemboardUIError(Exception):
10 | def __init__(self, code, message):
11 | Exception.__init__(self, message)
12 | self.code = int(code)
13 | self.message = str(message)
14 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/environment-memberships.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | r.role_name AS username,
3 | g.name AS groupname,
4 | g.description AS profile
5 | FROM application.environments AS e
6 | JOIN application.groups AS g ON g.id = e.dba_group_id
7 | JOIN application.memberships AS ms ON ms.group_id = g.id
8 | JOIN application.roles AS r on r.role_name = ms.role_name
9 | WHERE e.name = :name
10 | ORDER BY 1, 2;
11 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/pgconf/routes.py:
--------------------------------------------------------------------------------
1 | from flask import current_app, render_template
2 |
3 | from ...web.flask import instance_routes
4 |
5 |
6 | @instance_routes.route("/pgconf/configuration", methods=["GET"])
7 | def pgconf_configuration():
8 | current_app.instance.check_active_plugin("pgconf")
9 | current_app.instance.fetch_status()
10 |
11 | return render_template("configuration.html", plugin="pgconf")
12 |
--------------------------------------------------------------------------------
/ui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 120,
3 | "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-jinja-template"],
4 | "importOrder": ["", "^[./]"],
5 | "importOrderSeparation": true,
6 | "importOrderSortSpecifiers": true,
7 | "overrides": [
8 | {
9 | "files": "*.html",
10 | "options": {
11 | "parser": "jinja-template"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/explain.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}temBoard / PEV2{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('explain.js') %}{{ link|safe }}{% endfor %}
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/ui/tests/unit/test_pivot.py:
--------------------------------------------------------------------------------
1 | from io import StringIO
2 |
3 |
4 | def test_pivot():
5 | from temboardui.plugins.monitoring.pivot import pivot_timeserie
6 |
7 | in_ = StringIO("i,k,v\n1,a,1\n1,b,2\n2,a,3\n4,b,5\n100,z,100\n")
8 | expected = "i,a,b,z\n1,1,2,\n2,3,,\n4,,5,\n100,,,100\n"
9 | out_ = StringIO()
10 | pivot_timeserie(in_, index="i", key="k", value="v", output=out_)
11 | assert out_.getvalue() == expected
12 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instance-has-dba.sql:
--------------------------------------------------------------------------------
1 | SELECT EXISTS (
2 | SELECT COUNT(1)
3 | FROM application.instances AS i
4 | JOIN application.environments AS e ON e.id = i.environment_id
5 | JOIN application.groups AS g ON g.id = e.dba_group_id
6 | JOIN application.memberships AS ms ON ms.group_id = g.id
7 | WHERE i.agent_address = :agent_address AND i.agent_port = :agent_port
8 | AND ms.role_name = :role_name
9 | ) AS has_dba;
10 |
--------------------------------------------------------------------------------
/dev/bin/mkenv:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | #
3 | # Generate environment variables for development.
4 | #
5 |
6 | # Configure a routable UI address to configure agent-to-UI communication from
7 | # Docker container.
8 |
9 | mapfile -t hostaddr < <(hostname --all-ip-addresses | tr ' ' '\n')
10 | test -n "${hostaddr[*]}"
11 | TEMBOARD_UI_URL="http://${hostaddr[0]}:8888"
12 |
13 | cat <<-EOF
14 | # Generated by $0 at $(date).
15 | TEMBOARD_UI_URL=${TEMBOARD_UI_URL@Q}
16 | EOF
17 |
--------------------------------------------------------------------------------
/dev/postgres-setup-replication.sh:
--------------------------------------------------------------------------------
1 | mkdir -p "/var/lib/postgresql/archive"
2 |
3 | docker_process_sql <> "$PGDATA/pg_hba.conf" <
7 |
8 |
9 |
10 |
11 |
14 |
15 | {% end %}
16 |
--------------------------------------------------------------------------------
/toolkit/tests/test_perf.py:
--------------------------------------------------------------------------------
1 | def test_format():
2 | from temboardtoolkit.perf import PerfCounters
3 |
4 | p = PerfCounters()
5 |
6 | p["a"] = 1
7 | p["bc"] = 1
8 | p["ba"] = 1
9 |
10 | assert "a=1 ba=1 bc=1" in str(p)
11 |
12 |
13 | def test_stat():
14 | from temboardtoolkit.perf import PerfCounters
15 |
16 | p = PerfCounters()
17 |
18 | p.snapshot()
19 |
20 | assert "io_rchar" in p
21 | assert "io_wchar" in p
22 | assert 0 != p["load1"]
23 |
--------------------------------------------------------------------------------
/ui/share/sql/upgrade-4-5.sql:
--------------------------------------------------------------------------------
1 | BEGIN;
2 |
3 | CREATE TABLE IF NOT EXISTS monitoring.collector_status (
4 | instance_id INTEGER PRIMARY KEY REFERENCES monitoring.instances(instance_id) ON DELETE CASCADE,
5 | last_pull TIMESTAMP WITHOUT TIME ZONE,
6 | last_push TIMESTAMP WITHOUT TIME ZONE,
7 | last_insert TIMESTAMP WITHOUT TIME ZONE,
8 | status CHAR(12) CHECK (status = 'OK' OR status = 'FAIL')
9 | );
10 |
11 | GRANT ALL ON TABLE monitoring.collector_status TO temboard;
12 |
13 | COMMIT;
14 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/roles-all.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | r.role_name,
3 | role_email,
4 | role_phone,
5 | is_active,
6 | is_admin,
7 | g.id AS g_id,
8 | g.name AS g_name,
9 | e.id AS e_id,
10 | e.name AS e_name
11 | FROM application.roles AS r
12 | LEFT OUTER JOIN application.memberships AS ms ON ms.role_name = r.role_name
13 | LEFT OUTER JOIN application.groups AS g ON ms.group_id = g.id
14 | LEFT OUTER JOIN application.environments AS e ON e.dba_group_id = g.id
15 | ORDER BY 1, 9;
16 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/roles-get.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | r.role_name,
3 | role_email,
4 | role_phone,
5 | is_active,
6 | is_admin,
7 | g.id AS g_id,
8 | g.name,
9 | e.id AS e_id,
10 | e.name AS e_name
11 | FROM application.roles AS r
12 | LEFT OUTER JOIN application.memberships AS ms ON ms.role_name = r.role_name
13 | LEFT OUTER JOIN application.groups AS g ON g.id = ms.group_id
14 | LEFT OUTER JOIN application.environments AS e ON e.dba_group_id = g.id
15 | WHERE r.role_name = :name;
16 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/discover-settings.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | "name", "vartype", "unit", "setting"
3 | FROM pg_catalog.pg_settings
4 | WHERE "name" IN (
5 | 'config_file'
6 | ,'cluster_name'
7 | ,'data_directory'
8 | ,'external_pid_file'
9 | ,'hba_file'
10 | ,'ident_file'
11 | ,'lc_collate'
12 | ,'lc_ctype'
13 | ,'listen_addresses'
14 | ,'max_connections'
15 | ,'port'
16 | ,'server_encoding'
17 | ,'syslog_ident'
18 | ,'unix_socket_directories'
19 | ) OR context = 'internal';
20 |
--------------------------------------------------------------------------------
/ui/temboardui/handlers/notification.py:
--------------------------------------------------------------------------------
1 | from ..web.tornado import app, render_template
2 |
3 |
4 | @app.instance_route(r"/notifications")
5 | def notifications(request):
6 | notifications = request.instance.get("/notifications")
7 | request.instance.fetch_status()
8 | return render_template(
9 | "notifications.html",
10 | instance=request.instance,
11 | notifications=notifications,
12 | plugin="notifications",
13 | role=request.current_user,
14 | )
15 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/daterangepicker-override.scss:
--------------------------------------------------------------------------------
1 | $daterangepicker-active-bg-color: $primary;
2 | $daterangepicker-ranges-hover-bg-color: $primary;
3 | $daterangepicker-control-active-border-color: $primary;
4 | $daterangepicker-ranges-color: $primary;
5 | $daterangepicker-cell-hover-bg-color: lighten($primary, 50%);
6 | $daterangepicker-in-range-bg-color: lighten($primary, 60%);
7 | $daterangepicker-z-index: $zindex-dropdown !default;
8 |
9 | .daterangepicker {
10 | z-index: 1151 !important;
11 | }
12 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/web.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit import services
2 | from temboardtoolkit.app import SubCommand
3 |
4 | from .app import app
5 |
6 |
7 | @app.command
8 | class Web(SubCommand):
9 | """Standalone web server
10 |
11 | For testing purpose only. Some feature won't work without combined task
12 | manager processes. See serve command for production.
13 |
14 | """
15 |
16 | is_service = True
17 |
18 | def main(self, args):
19 | return services.run(self.app.httpd)
20 |
--------------------------------------------------------------------------------
/dev/grafana/rootfs/etc/grafana/provisioning/datasources/loki.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | datasources:
4 | - "id": 1
5 | "uid": "j1tzJKb7k"
6 | "orgId": 1
7 | "name": "Loki"
8 | "type": "loki"
9 | "typeName": "Loki"
10 | "typeLogoUrl": "public/app/plugins/datasource/loki/img/loki_icon.svg"
11 | "access": "proxy"
12 | "url": "http://loki:3100"
13 | "password": ""
14 | "user": ""
15 | "database": ""
16 | "basicAuth": false
17 | "isDefault": false
18 | "jsonData": {}
19 | "readOnly": false
20 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/error.html:
--------------------------------------------------------------------------------
1 | {% extends base.html %}
2 |
3 | {% block title %}temBoard{% end %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Error
10 |
{{error}}
11 |
12 |
13 |
14 |
15 |
16 | {% end %}
17 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/error.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Error{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Internal Error
10 |
The following unhandled error occured:
11 |
{{ message }}
12 |
13 |
14 |
15 | {% endblock %}
16 |
--------------------------------------------------------------------------------
/toolkit/temboardtoolkit/logfmt.py:
--------------------------------------------------------------------------------
1 | def format(**kw):
2 | return " ".join([format_attribute(*i) for i in sorted(flatten(**kw))])
3 |
4 |
5 | def format_attribute(key, value):
6 | if " " in str(value):
7 | value = '"%s"' % value
8 | return "%s=%s" % (key, value)
9 |
10 |
11 | def flatten(**kw):
12 | for k, v in kw.items():
13 | if isinstance(v, dict):
14 | for kk, vv in flatten(**v):
15 | yield "%s.%s" % (k, kk), vv
16 | else:
17 | yield k, v
18 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/serve.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit import services
2 | from temboardtoolkit.app import SubCommand
3 |
4 | from ..model import check_schema
5 | from .app import app
6 |
7 |
8 | @app.command
9 | class Serve(SubCommand):
10 | """Combined web server and background workers."""
11 |
12 | is_service = True
13 |
14 | def main(self, args):
15 | check_schema()
16 | self.app.config.load_signing_key()
17 |
18 | services.run(self.app.webservice, self.app.scheduler, self.app.worker_pool)
19 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/roles-select-instances.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | i.agent_address,
3 | i.agent_port,
4 | i.hostname,
5 | i.pg_port,
6 | i.notify,
7 | i.comment,
8 | i.cdate,
9 | i.discover,
10 | i.discover_date,
11 | i.discover_etag,
12 | i.environment_id
13 | FROM application.instances AS i
14 | JOIN application.environment AS e ON e.id = i.environment_id
15 | JOIN application.groups AS g ON g.id = e.group_id
16 | JOIN application.memberships AS m ON m.group_id = g.id
17 | WHERE m.role_name = :role_name;
18 |
--------------------------------------------------------------------------------
/agent/packaging/temboard-agent@.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=PostgreSQL Remote Control Agent %I
3 | After=network.target postgresql@%i.service
4 | AssertPathExists=/etc/temboard-agent/%I/temboard-agent.conf
5 |
6 | [Service]
7 | Type=simple
8 | User=postgres
9 | Group=postgres
10 | Environment="SYSTEMD=1"
11 | ExecStart=/usr/bin/temboard-agent -c /etc/temboard-agent/%I/temboard-agent.conf serve
12 | # Increase OOM Score to ensure agent is killed before Postgres.
13 | OOMScoreAdjust=15
14 |
15 | [Install]
16 | WantedBy=multi-user.target
17 |
--------------------------------------------------------------------------------
/.config/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | build:
4 | os: ubuntu-22.04
5 | tools:
6 | python: "3.9"
7 | jobs:
8 | pre_create_environment:
9 | - asdf plugin add uv
10 | - asdf install uv latest
11 | - asdf global uv latest
12 | create_environment:
13 | - uv venv "${READTHEDOCS_VIRTUALENV_PATH}"
14 | install:
15 | - UV_PROJECT_ENVIRONMENT="${READTHEDOCS_VIRTUALENV_PATH}" uv sync --frozen --no-default-groups --group=docs
16 |
17 | mkdocs:
18 | configuration: mkdocs.yml
19 | fail_on_warning: true
20 |
--------------------------------------------------------------------------------
/dev/bin/checkdocker:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | # Usage: dev/bin/checkdocker
3 |
4 | max="$1"
5 | current="$(docker version --format "{{ .Server.Version }}")"
6 | lowest="$(echo -e "$max\n$current" | sort --version-sort | head -1)"
7 | if [ "$lowest" == "$max" ]; then
8 | echo "Unsupported Docker version $current. Please fix develop for this version of Docker." >&2
9 | exit 1
10 | fi
11 | if ! docker compose version >/dev/null; then
12 | echo "docker compose not found. Please install docker compose v2." >&2
13 | exit 1
14 | fi
15 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/401.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Authentication required{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Authentication required
10 |
Go to login page to authenticate.
11 |
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Not Found{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Page not Found
10 |
The page you are looking for might have been removed or had its name changed.
11 |
12 |
13 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}temBoard{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('home.js') %}
7 | {{ link | safe}}
8 | {% endfor %}
9 | {% endblock %}
10 |
11 | {% block content %}
12 |
13 |
16 |
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/toolkit/tests/test_queries.py:
--------------------------------------------------------------------------------
1 | def test_filter_pg_version():
2 | from temboardtoolkit.queries import filter_pragma_version
3 |
4 | assert filter_pragma_version("pouet", pg_version=150000)
5 |
6 | sql = "pouet -- pragma:pg_version_max 140000"
7 | assert not filter_pragma_version(sql, pg_version=150000)
8 | assert filter_pragma_version(sql, pg_version=100000)
9 |
10 | sql = "pouet -- pragma:pg_version_min 140000"
11 | assert not filter_pragma_version(sql, pg_version=100000)
12 | assert filter_pragma_version(sql, pg_version=150000)
13 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/monitoring-purge-instances.sql:
--------------------------------------------------------------------------------
1 | WITH deleted AS (
2 | SELECT
3 | i.instance_id,
4 | h.hostname,
5 | i.port
6 | FROM monitoring.instances AS i
7 | JOIN monitoring.hosts AS h USING (host_id)
8 | LEFT OUTER JOIN application.instances AS agent
9 | ON agent.hostname = h.hostname AND agent.pg_port = i.port
10 | WHERE agent.agent_address IS NULL
11 | )
12 | DELETE FROM monitoring.instances AS i
13 | USING deleted
14 | WHERE i.instance_id = deleted.instance_id
15 | RETURNING deleted.hostname, deleted.port;
16 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/configuration.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{super()}} / Configuration{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('home.js') %}
7 | {{ link | safe}}
8 | {% endfor %}
9 | {% endblock %}
10 |
11 | {% block content %}
12 |
13 |
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/instance-about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ About{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/home.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createRouter, createWebHistory } from "vue-router";
3 |
4 | import Home from "./views/Home.vue";
5 |
6 | const NotFound = { template: "" };
7 | const router = createRouter({
8 | history: createWebHistory(),
9 | // at least one route is required, we use a fake one
10 | routes: [{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }],
11 | });
12 |
13 | createApp({
14 | components: {
15 | home: Home,
16 | },
17 | })
18 | .use(router)
19 | .mount("#app");
20 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}temBoard / About{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('settings.about.js') %}
7 | {{ link | safe}}
8 | {% endfor %}
9 | {% endblock %}
10 |
11 | {% block content %}
12 |
13 |
19 |
20 | {% endblock %}
21 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/discover.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import sys
3 |
4 | from temboardtoolkit.app import SubCommand
5 |
6 | from .app import app
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | @app.command
12 | class Discover(SubCommand):
13 | """Introspect system and PostgreSQL instance."""
14 |
15 | def main(self, args):
16 | self.app.discover.refresh()
17 | self.app.discover.write()
18 | self.app.discover.write(sys.stdout)
19 | logger.debug("Discover etag is %s.", self.app.discover.etag)
20 | return 0
21 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/unauthorized.html:
--------------------------------------------------------------------------------
1 | {% extends base.html %}
2 |
3 | {% block title %}temBoard{% end %}
4 |
5 | {% block content %}
6 |
16 | {% end %}
17 |
--------------------------------------------------------------------------------
/ui/share/preun.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 | mode="$1"
4 |
5 | # rpm uses an int. deb uses a keyword.
6 | case "$mode" in
7 | remove|0)
8 | if ! readlink /proc/1/exe | grep -q systemd ; then
9 | echo You must disable manually temboard service. >&2
10 | exit 0
11 | fi
12 |
13 | systemctl disable --now temboard
14 | if systemctl -q is-failed temboard ; then
15 | systemctl reset-failed temboard
16 | fi
17 | exit 0
18 | ;;
19 | upgrade|1)
20 | exit 0
21 | ;;
22 | *)
23 | echo "Unknown scriptlet execution mode: '$mode'. Skipping" >&2
24 | exit 0
25 | ;;
26 | esac
27 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/monitoring.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createRouter, createWebHistory } from "vue-router";
3 |
4 | import Monitoring from "./views/Monitoring.vue";
5 |
6 | const NotFound = { template: "" };
7 | const router = createRouter({
8 | history: createWebHistory(),
9 | // at least one route is required, we use a fake one
10 | routes: [{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }],
11 | });
12 |
13 | createApp({
14 | components: {
15 | monitoring: Monitoring,
16 | },
17 | })
18 | .use(router)
19 | .mount("#app");
20 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/statements.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createRouter, createWebHistory } from "vue-router";
3 |
4 | import Statements from "./views/Statements.vue";
5 |
6 | const NotFound = { template: "" };
7 | const router = createRouter({
8 | history: createWebHistory(),
9 | // at least one route is required, we use a fake one
10 | routes: [{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }],
11 | });
12 |
13 | createApp({
14 | components: {
15 | statements: Statements,
16 | },
17 | })
18 | .use(router)
19 | .mount("#app");
20 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/activity.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 | {% block title %}temBoard / {{instance}} / Activity{% endblock %}
3 |
4 | {% block head %}
5 | {% for link in vitejs.css_links_for('activity.js') %}{{ link | safe }}{% endfor %}
6 | {% endblock %}
7 |
8 | {% block content %}
9 |
12 |
13 |
14 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/ui/temboardui/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 |
4 | if True: # Vendoring
5 | parent_dir = os.path.abspath(os.path.dirname(__file__))
6 | vendor_dir = os.path.join(parent_dir, "_vendor")
7 | sys.path[:0] = [vendor_dir]
8 |
9 | import warnings
10 |
11 | import temboardtoolkit.log
12 |
13 | from .version import __version__ # noqa
14 |
15 | if "DEBUG" not in os.environ and "CI" not in os.environ:
16 | warnings.filterwarnings("ignore")
17 |
18 | # Configure toolkit root logger name before importing toolkit's getLogger.
19 | temboardtoolkit.log.LastnameFilter.root = __name__
20 |
--------------------------------------------------------------------------------
/agent/temboardagent/__init__.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | import warnings
4 |
5 | if True: # Vendoring
6 | parent_dir = os.path.abspath(os.path.dirname(__file__))
7 | vendor_dir = os.path.join(parent_dir, "_vendor")
8 | sys.path[:0] = [vendor_dir]
9 |
10 | import temboardtoolkit.log
11 |
12 | from .version import __version__ # noqa
13 |
14 | if "DEBUG" not in os.environ and "CI" not in os.environ:
15 | warnings.filterwarnings("ignore")
16 |
17 |
18 | # Configure toolkit root logger name before importing toolkit's getLogger.
19 | temboardtoolkit.log.LastnameFilter.root = __name__
20 |
--------------------------------------------------------------------------------
/dev/grafana/rootfs/etc/grafana/provisioning/dashboards/perf.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | providers:
4 | - name: "temBoard Performance Provisionned"
5 | type: file
6 | folder: ''
7 | allowUiUpdates: true
8 | options:
9 | path: /usr/share/temboard/perf.json
10 | - name: "Postgres from temBoard agent"
11 | type: file
12 | folder: ''
13 | allowUiUpdates: true
14 | options:
15 | path: /usr/share/temboard/postgres.json
16 | - name: "temBoard Prometheus"
17 | type: file
18 | folder: ''
19 | allowUiUpdates: true
20 | options:
21 | path: /usr/share/temboard/temboard-prometheus.json
22 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/alerting.check.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import { createRouter, createWebHistory } from "vue-router";
3 |
4 | import AlertingCheck from "./views/AlertingCheck.vue";
5 |
6 | const NotFound = { template: "" };
7 | const router = createRouter({
8 | history: createWebHistory(),
9 | // at least one route is required, we use a fake one
10 | routes: [{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFound }],
11 | });
12 |
13 | createApp({
14 | components: {
15 | alertingCheck: AlertingCheck,
16 | },
17 | })
18 | .use(router)
19 | .mount("#app");
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | # Unix-style newlines
8 | end_of_line = lf
9 | # Remove any whitespace characters preceding newline characters
10 | trim_trailing_whitespace = true
11 | # Newline ending every file
12 | insert_final_newline = true
13 | # Sync it with default ruff value.
14 | max_line_length = 88
15 |
16 | # 2 space indentation for UI
17 | [*.{css,html,js,json,scss,yml,yaml,sql,vue}]
18 | indent_size = 2
19 |
20 | [{*.sh,Makefile}]
21 | indent_size = 8
22 | indent_style = tab
23 |
24 | [dev/lnav/**.json]
25 | indent_size = 4
26 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/web.py:
--------------------------------------------------------------------------------
1 | from temboardtoolkit import services
2 | from temboardtoolkit.app import SubCommand
3 |
4 | from ..model import check_schema
5 | from .app import app
6 |
7 |
8 | @app.command
9 | class web(SubCommand):
10 | """Standalone web server.
11 |
12 | For testing purpose only. Some feature won't work without combined task
13 | manager processes. See serve command for production.
14 |
15 | """
16 |
17 | is_service = True
18 |
19 | def main(self, args):
20 | check_schema()
21 | self.app.config.load_signing_key()
22 | services.run(self.app.webservice)
23 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/maintenance/index.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Maintenance{% endblock %}
4 |
5 | {% block content %}
6 |
7 | {% include "maintenance/breadcrumb.html" %}
8 |
9 |
16 |
17 |
18 |
19 | {% endblock %}
20 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/pgconf/__init__.py:
--------------------------------------------------------------------------------
1 | from ...web.flask import instance_proxy
2 |
3 |
4 | class PGConfPlugin:
5 | def __init__(self, app):
6 | self.app = app
7 |
8 | def load(self):
9 | __import__(__name__ + ".routes")
10 | instance_proxy.generic_proxy("/pgconf/configuration")
11 | instance_proxy.generic_proxy("/pgconf/configuration", method="POST")
12 | instance_proxy.generic_proxy("/pgconf/configuration/categories")
13 | instance_proxy.generic_proxy("/pgconf/configuration/status")
14 | instance_proxy.generic_proxy("/pgconf/configuration/category/")
15 |
--------------------------------------------------------------------------------
/dev/bin/mktargets:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | # shellcheck source=/dev/null
4 | . .env
5 |
6 | target="${TEMBOARD_UI_URL#https://}"
7 |
8 | cat <<-EOF
9 | - targets:
10 | - $target
11 | labels:
12 | __metrics_path__: /proxy/0.0.0.0/2345/monitoring/metrics
13 | instance: "postgres0.dev:5432"
14 | # For PostgreSQL Database dashboard #9628
15 | kubernetes_namespace: nok8s
16 | release: norelease
17 | - targets:
18 | - $target
19 | labels:
20 | __metrics_path__: /proxy/0.0.0.0/2346/monitoring/metrics
21 | instance: "postgres1.dev:5432"
22 | kubernetes_namespace: nok8s
23 | release: norelease
24 | EOF
25 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/settings/members.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}temBoard / Settings / Environments / {{ environment }} / Members{% endblock %}
4 | {% block head %}
5 | {% for link in vitejs.css_links_for('settings.members.js') %}{{ link|safe }}{% endfor %}
6 | {% endblock %}
7 |
8 | {% block sidebar %}
9 | {% include "settings/menu.html" %}
10 | {% endblock %}
11 |
12 | {% block content %}
13 |
14 |
15 |
16 |
17 | {% endblock %}
18 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/public/images/ring-alt.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/403.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Restricted area{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
8 |
9 |
Restricted area
10 |
You are not allowed to access this page.
11 | Go back to home page
12 | or ask your temBoard administrator to grant access to you.
13 |
14 |
15 |
16 | {% endblock %}
17 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instance-select-email-and-phone-for-notify.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | array_agg(DISTINCT r.role_email) FILTER (WHERE r.role_email IS NOT NULL AND r.role_email <> '') AS emails,
3 | array_agg(DISTINCT r.role_phone) FILTER (WHERE r.role_phone IS NOT NULL AND r.role_phone <> '') AS phones
4 | FROM application.instances AS i
5 | JOIN application.environments AS e ON i.environment_id = e.id
6 | JOIN application.memberships AS m ON e.dba_group_id = m.group_id
7 | JOIN application.roles AS r ON m.role_name = r.role_name
8 | WHERE i.agent_address = :agent_address AND i.agent_port = :agent_port
9 | AND r.is_active AND i.notify;
10 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/login.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 |
3 | function login(e) {
4 | e.preventDefault();
5 | $.ajax({
6 | url: "/json/login",
7 | type: "post",
8 | data: JSON.stringify({
9 | username: $("#inputUsername").val(),
10 | password: $("#inputPassword").val(),
11 | }),
12 | headers: {
13 | "Content-Type": "application/json",
14 | },
15 | success: function (data) {
16 | window.location.href = document.referrer ? document.referrer : "/";
17 | },
18 | error: function (xhr) {
19 | console.log("error", xhr);
20 | showError(xhr);
21 | },
22 | });
23 | }
24 |
25 | window.login = login;
26 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/components/configuration/SettingSwitch.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/dev/loki/loki-config.yaml:
--------------------------------------------------------------------------------
1 | auth_enabled: false
2 |
3 | server:
4 | http_listen_port: 3100
5 | grpc_listen_port: 9096
6 |
7 | common:
8 | path_prefix: /tmp/loki
9 | storage:
10 | filesystem:
11 | chunks_directory: /tmp/loki/chunks
12 | rules_directory: /tmp/loki/rules
13 | replication_factor: 1
14 | ring:
15 | instance_addr: 127.0.0.1
16 | kvstore:
17 | store: inmemory
18 |
19 | schema_config:
20 | configs:
21 | - from: 2020-10-24
22 | store: boltdb-shipper
23 | object_store: filesystem
24 | schema: v11
25 | index:
26 | prefix: index_
27 | period: 24h
28 |
29 | ruler:
30 | alertmanager_url: http://localhost:9093
31 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instances-insert.sql:
--------------------------------------------------------------------------------
1 | WITH environment AS (
2 | SELECT id FROM application.environments WHERE name = :environment
3 | )
4 | INSERT INTO application.instances (agent_address, agent_port, hostname, pg_port, notify, comment, discover, discover_date, discover_etag, environment_id)
5 | SELECT
6 | :agent_address AS agent_address,
7 | :agent_port AS agent_port,
8 | :hostname AS hostname,
9 | :pg_port AS pg_port,
10 | :notify AS notify,
11 | :comment AS "comment",
12 | :discover AS discover,
13 | :discover_date AS discover_date,
14 | :discover_etag AS discover_etag,
15 | environment.id AS environment_id
16 | FROM environment
17 | RETURNING *;
18 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/components/settings/InstanceDetails.vue:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
:
8 |
9 | CPU - GB memory
10 | serving .
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/ui/packaging/docker/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | export PGHOST=${PGHOST-localhost}
4 | export PGPORT=${PGPORT-5432}
5 | export PGUSER=${PGUSER-postgres}
6 | PGPASSWORD=${PGPASSWORD-}
7 | export PGDATABASE=${PGDATABASE-$PGUSER}
8 |
9 | wait-for-it "${PGHOST}:${PGPORT}"
10 | if [ ! -f /etc/temboard/temboard.conf ] ; then
11 | if ! DEBUG=y PGPASSWORD="$PGPASSWORD" /usr/share/temboard/auto_configure.sh; then
12 | cat /var/log/temboard-auto-configure.log >&2
13 | exit 1
14 | fi
15 | fi
16 |
17 | # Clean PG* used for setup. libpq for temBoard is configured only by config
18 | # file.
19 | # shellcheck disable=2086
20 | unset ${!PG*}
21 |
22 | set -x
23 | exec sudo -EHu temboard "${@-temboard}"
24 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/runscript.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from temboardtoolkit.app import SubCommand
4 |
5 | from .app import app
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @app.command
11 | class RunScript(SubCommand):
12 | """Run a Python script."""
13 |
14 | def define_arguments(self, parser):
15 | parser.add_argument("script", metavar="PATH", help="Path to script file.")
16 |
17 | super().define_arguments(parser)
18 |
19 | def main(self, args):
20 | with open(args.script) as fo:
21 | source = fo.read()
22 | code = compile(source, args.script, "exec")
23 |
24 | g = globals()
25 | g["app"] = self.app
26 |
27 | exec(code, g, g)
28 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends ../../../templates/base.html %}
2 |
3 | {% block title %}temBoard / {{instance}} / Monitoring{% end %}
4 |
5 | {% block head %}
6 |
7 | {% end %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
21 |
22 | {% end %}
23 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/monitoring-bgwriter.sql:
--------------------------------------------------------------------------------
1 | WITH backends AS (
2 | SELECT SUM(writes) AS buffers_backend,
3 | SUM(fsyncs) AS buffers_backend_fsync
4 | FROM pg_stat_io
5 | WHERE backend_type = 'client backend'
6 | )
7 | SELECT cp.num_timed AS checkpoints_timed,
8 | cp.num_requested AS checkpoints_req,
9 | cp.write_time AS checkpoint_write_time,
10 | cp.sync_time AS checkpoint_sync_time,
11 | cp.buffers_written AS buffers_checkpoint,
12 | bg.buffers_clean,
13 | bg.maxwritten_clean,
14 | backends.buffers_backend,
15 | backends.buffers_backend_fsync,
16 | bg.buffers_alloc,
17 | bg.stats_reset
18 | FROM pg_stat_bgwriter AS bg,
19 | pg_stat_checkpointer AS cp,
20 | backends;
21 |
--------------------------------------------------------------------------------
/dev/agent-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eux
2 |
3 | chown -R "$(stat -c "%u:%g" "$0")" /usr/local/src/temboard/agent
4 |
5 | if ! [ -f /etc/temboard-agent/postgres0/signing-public.pem ] ; then
6 | # Copy dev signing key to prefetch UI key. Don't bother whether we are
7 | # managing first or second instance.
8 | mkdir -p /etc/temboard-agent/postgres{0,1}
9 | cp -fv /usr/local/src/temboard/dev/signing-public.pem /etc/temboard-agent/postgres0
10 | cp -fv /usr/local/src/temboard/dev/signing-public.pem /etc/temboard-agent/postgres1
11 | fi
12 |
13 | # Presere uv PATH when sudoing as Postgres.
14 | sed -i /secure_path/d /etc/sudoers
15 | visudo -cf /etc/sudoers
16 |
17 | uv sync
18 | exec uv run /usr/local/src/temboard/agent/packaging/docker/entrypoint.sh "$@"
19 |
--------------------------------------------------------------------------------
/agent/share/preun.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -eu
3 | mode="$1"
4 |
5 | # rpm uses an int. deb uses a keyword.
6 | case "$mode" in
7 | 0|remove)
8 | if ! readlink /proc/1/exe | grep -q systemd ; then
9 | echo You must disable manually temboard-agent services. >&2
10 | exit 0
11 | fi
12 |
13 | while read -r service ; do
14 | systemctl disable --now "$service"
15 | if systemctl -q is-failed "$service" ; then
16 | systemctl reset-failed "$service"
17 | fi
18 | done < <(systemctl list-units --full --no-legend --no-pager --plain --type service temboard-agent@* | grep -Po '^[^.]+')
19 | exit 0
20 | ;;
21 | 1|upgrade)
22 | exit 0
23 | ;;
24 | *)
25 | echo "Unknown scriptlet execution mode: '$mode'. Skipping" >&2
26 | exit 0
27 | ;;
28 | esac
29 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/pgconf-settings.sql:
--------------------------------------------------------------------------------
1 | SELECT
2 | category, "s"."name",
3 | "s".setting,
4 | current_setting("name") AS current_setting,
5 | unit,
6 | vartype,
7 | min_val, max_val, enumvals,
8 | context,
9 | short_desc || ' ' || coalesce(extra_desc, '') AS "desc",
10 | boot_val, reset_val,
11 | pending_restart,
12 | COALESCE(json_agg(
13 | json_build_object(
14 | 'file', f.sourcefile,
15 | 'line', f.sourceline,
16 | 'seqno', f.seqno,
17 | 'setting', f.setting,
18 | 'error', f.error,
19 | 'applied', f.applied
20 | )
21 | ) FILTER (WHERE f.sourcefile IS NOT NULL), '[]'::JSON) AS sources
22 | FROM pg_settings AS "s"
23 | LEFT OUTER JOIN pg_file_settings AS "f" USING("name")
24 | GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
25 | ORDER BY 1, 2;
26 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/statements/templates/index.html:
--------------------------------------------------------------------------------
1 | {% extends ../../../templates/base.html %}
2 |
3 | {% block title %}temBoard / {{instance}} / Statements{% end %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('statements.js') %}{% raw link %}{% end %}
7 |
8 | {% end %}
9 |
10 | {% block content %}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
22 | {% end %}
23 |
--------------------------------------------------------------------------------
/agent/temboardagent/__main__.py:
--------------------------------------------------------------------------------
1 | import sys
2 |
3 | from bottle import default_app
4 |
5 | from .cli.app import app
6 | from .web.app import create_app
7 |
8 |
9 | def main():
10 | default_app.push(create_app(app))
11 |
12 | # Import core HTTP routes
13 | __import__(__package__ + ".web.core")
14 |
15 | # Import commands
16 | __import__(__package__ + ".cli.discover")
17 | __import__(__package__ + ".cli.fetch_key")
18 | __import__(__package__ + ".cli.register")
19 | __import__(__package__ + ".cli.routes")
20 | __import__(__package__ + ".cli.runscript")
21 | __import__(__package__ + ".cli.serve")
22 | __import__(__package__ + ".cli.tasks")
23 | __import__(__package__ + ".cli.web")
24 | return app()
25 |
26 |
27 | if "__main__" == __name__:
28 | sys.exit(main())
29 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/maintenance/database.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Maintenance / {{database}}{% endblock %}
4 |
5 | {% block content %}
6 |
7 | {% include "maintenance/breadcrumb.html" %}
8 |
9 |
10 |
11 |
20 |
21 |
22 |
23 | {% endblock %}
24 |
--------------------------------------------------------------------------------
/ui/tests/unit/test_main.py:
--------------------------------------------------------------------------------
1 | def test_pgvar_map():
2 | from temboardui.cli.app import map_pgvars
3 |
4 | env = dict(
5 | PGHOST="localhost",
6 | PGPORT="5433",
7 | PGUSER="temboard",
8 | PGPASSWORD="étagère",
9 | PGDATABASE="temboarddb",
10 | )
11 | mapped = map_pgvars(env)
12 | assert "localhost" == mapped["TEMBOARD_REPOSITORY_HOST"]
13 | assert "5433" == mapped["TEMBOARD_REPOSITORY_PORT"]
14 | assert "temboard" == mapped["TEMBOARD_REPOSITORY_USER"]
15 | assert "étagère" == mapped["TEMBOARD_REPOSITORY_PASSWORD"]
16 | assert "temboarddb" == mapped["TEMBOARD_REPOSITORY_DBNAME"]
17 |
18 | env = dict(PGHOST="pg", TEMBOARD_REPOSITORY_HOST="temboard")
19 | mapped = map_pgvars(env)
20 | assert "temboard" == mapped["TEMBOARD_REPOSITORY_HOST"]
21 |
--------------------------------------------------------------------------------
/ui/temboardui/web/routes/instance.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from flask import current_app as app
4 | from flask import g, render_template
5 |
6 | from ..flask import instance_routes
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | @instance_routes.route("/about")
12 | def instance_about():
13 | app.instance.fetch_status()
14 | return render_template(
15 | "instance-about.html",
16 | instance_name=g.instance.__str__(),
17 | pg_data=g.instance.pg_data,
18 | pg_version_summary=g.instance.pg_version_summary,
19 | discover=g.instance.discover,
20 | environment=g.instance.environment.name,
21 | )
22 |
23 |
24 | @app.route("/explain")
25 | def explain():
26 | return render_template(
27 | "explain.html", nav=True, role=g.current_user, vitejs=app.vitejs
28 | )
29 |
--------------------------------------------------------------------------------
/dev/signing-public.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA4TVNVsTR3IaL9hdV69XY
3 | 6FEFnZpxfdJ5HuKtku+Gy+ezw4swTNLolSXn1OYDlLd688aE0p3xER+dtim5bWxj
4 | p2iTnN2hS8w6hPMA3o5noBG1UWJy5jjKeVF40h92MAB6KU4SA84IJNz+zBQTIFkx
5 | AqRaOJUPqyJwXeHhkiH3p5zuPHw+FPZZkNcmL4FuoPjZ5KGvPalUvmUM7HZevSdf
6 | 4Ycsn5xEHB8mutpwheNhRJDRuUfzZxoj6Iru7hPxWOSrQdgkIxYymZV0YrRGB0OI
7 | AV5Go1MwWCAyK8D55JO9F3fZMwyVbSHAdOEcvCVqf8YBrjT3BzvgxM1qDx+k5Mlb
8 | 3RcMED9EXNGwKUHKxsc9jEFunNeM6t8mpndki2gdxrmZsovQ2MDAmGcFNTffSO5h
9 | S5hm4nw44Vv2SG7FBOp29WmGbOdBnNRwAt1f5VvvyYLwwAX3bk8ljIYplMBWJN5D
10 | sl0kT9YPYphbWtWESLX9X/LgpgD7t299m4Fi0OEyamL0jF4pRgvTha3vUIEtcgaJ
11 | 3WNGVFQRBHgu8lIUmI6hO7swCMnIG+CcjCRo0ez/e0JyK6ONVFuuwt38FSam+Nrk
12 | vDqvhmKaNFwJpkWuSW3p+j5fjWVsf39arIodLU4XxNr32LaqKncld24bRYhg35CG
13 | /saax0girQznwhje0AH0tLMCAwEAAQ==
14 | -----END PUBLIC KEY-----
15 |
--------------------------------------------------------------------------------
/agent/share/purge.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 |
3 | if [ -n "${DEBUG-}" ] ; then
4 | set -x
5 | fi
6 |
7 | ETCDIR=${ETCDIR-/etc/temboard-agent}
8 | VARDIR=${VARDIR-/var/lib/temboard-agent}
9 | LOGDIR=${LOGDIR-/var/log/temboard-agent}
10 |
11 | instance_path="$1"
12 | instance_name="${instance_path//\//-}"
13 |
14 | if type -p systemctl >/dev/null && systemctl is-system-running && [ -w /etc/systemd/system ]; then
15 | echo "Stopping and disabling systemd service." >&2
16 | systemctl disable --now "temboard-agent@$instance_name" || true
17 | fi
18 |
19 | echo "Cleaning files and directories..." >&2
20 | rm -rvf \
21 | "${ETCDIR:?}/$instance_path/" \
22 | "${LOGDIR:?}/$instance_path.log" \
23 | "${VARDIR:?}/$instance_path/" \
24 | "/etc/systemd/system/temboard-agent@${instance_path}.service.d" \
25 | ;
26 |
27 | echo "temBoard agent ${instance_name} stopped and cleaned." >&2
28 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/dashboard.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Dashboard{% endblock %}
4 |
5 | {% block content %}
6 |
7 |
20 |
21 |
22 |
23 |
24 |
25 | {% endblock %}
26 |
--------------------------------------------------------------------------------
/ui/vendor.txt:
--------------------------------------------------------------------------------
1 | # This file was autogenerated by uv via the following command:
2 | # uv pip compile --python-version=3.9 ui/vendor.in -o ui/vendor.txt
3 | blinker==1.9.0
4 | # via flask
5 | click==8.1.8
6 | # via flask
7 | flask==3.1.2
8 | # via -r ui/vendor.in
9 | greenlet==3.2.4
10 | # via sqlalchemy
11 | importlib-metadata==8.7.0
12 | # via flask
13 | itsdangerous==2.2.0
14 | # via flask
15 | jinja2==3.1.6
16 | # via
17 | # -r ui/vendor.in
18 | # flask
19 | markupsafe==3.0.3
20 | # via
21 | # flask
22 | # jinja2
23 | # werkzeug
24 | python-dateutil==2.9.0.post0
25 | # via -r ui/vendor.in
26 | six==1.17.0
27 | # via python-dateutil
28 | sqlalchemy==1.4.54
29 | # via -r ui/vendor.in
30 | tornado==6.4.2
31 | # via -r ui/vendor.in
32 | werkzeug==3.1.3
33 | # via flask
34 | zipp==3.23.0
35 | # via importlib-metadata
36 |
--------------------------------------------------------------------------------
/docs/assets/temboard.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-primary-fg-color: #d65916;
3 | --md-primary-fg-color--dark: #622a11;
4 | --md-accent-fg-color: #d65916;
5 | }
6 |
7 | .md-grid {
8 | max-width: 1200px;
9 | }
10 |
11 | .md-sidebar[hidden] + .md-content {
12 | max-width: 990px;
13 | margin: 0 auto;
14 | }
15 |
16 | .md-container .md-main h2.doc-hide {
17 | display: none;
18 | }
19 |
20 | .md-container .md-main img.logo {
21 | display: block;
22 | margin: 3rem auto;
23 | max-width: 75%;
24 | }
25 |
26 | .display-none
27 | {
28 | display: none !important;
29 | }
30 |
31 | .md-container .md-main article > p img {
32 | display: block;
33 | max-width: 90%;
34 | margin: 1em auto;
35 | }
36 |
37 | /* Hide RTD injected code. */
38 | div.injected {
39 | display: none !important;
40 | }
41 |
42 | .md-header__option {
43 | height: 2.4rem;
44 | line-height: 2.4rem;
45 | }
46 |
--------------------------------------------------------------------------------
/agent/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016, Dalibo
2 |
3 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
4 |
5 | IN NO EVENT SHALL DALIBO BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF DALIBO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 |
7 | DALIBO SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND DALIBO HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
8 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/_bootstrap-variables-override.scss:
--------------------------------------------------------------------------------
1 | $primary: #3c424c;
2 | $undef: #ccc;
3 | $cat1: #78b8fa;
4 | $cat2: #fa9e78;
5 | $navbar-inverse-bg: $primary;
6 | $link-color: lighten(saturate($primary, 15%), 15%);
7 |
8 | @import "../node_modules/bootstrap/scss/functions";
9 | @import "../node_modules/bootstrap/scss/variables";
10 |
11 | $input-border-focus: $link-color;
12 | $grid-gutter-width: 20px;
13 | $spacer: $grid-gutter-width;
14 | $small-font-size: 85%;
15 |
16 | // Additional xxl grid tiers
17 | $grid-breakpoints: (
18 | xs: 0,
19 | sm: 576px,
20 | md: 768px,
21 | lg: 992px,
22 | xl: 1200px,
23 | xxl: 1500px,
24 | ) !default;
25 |
26 | $custom-colors: (
27 | "ok": $success,
28 | "critical": $danger,
29 | "undef": $undef,
30 | "cat1": $cat1,
31 | "cat2": $cat2,
32 | );
33 | $theme-colors: map-merge($theme-colors, $custom-colors);
34 | $popover-font-size: 1rem !default;
35 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/010_discover.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "application"."instances"
2 | ADD COLUMN "cdate" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
3 | ADD COLUMN "discover" JSONB,
4 | ADD COLUMN "discover_date" TIMESTAMP WITH TIME ZONE,
5 | ADD COLUMN "discover_etag" TEXT;
6 |
7 | UPDATE "application"."instances"
8 | SET "discover" = json_build_object(
9 | 'system', json_build_object(
10 | 'cpu_count', "cpu",
11 | 'memory', "memory_size",
12 | 'fqdn', "hostname"
13 | ),
14 | 'postgres', json_build_object(
15 | 'version', "pg_version",
16 | 'version_summary', "pg_version_summary",
17 | 'port', "pg_port",
18 | 'data_directory', "pg_data"
19 | ),
20 | 'temboard', json_build_object()
21 | );
22 |
23 | ALTER TABLE "application"."instances"
24 | DROP COLUMN "cpu",
25 | DROP COLUMN "memory_size",
26 | DROP COLUMN "pg_data",
27 | DROP COLUMN "pg_version",
28 | DROP COLUMN "pg_version_summary";
29 |
--------------------------------------------------------------------------------
/ui/packaging/nfpm/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | rhel10: &rhel
3 | image: dalibo/buildpack-pkg:rockylinux10
4 | volumes:
5 | - ../..:/workspace
6 | working_dir: /workspace
7 | entrypoint: ["/usr/bin/tini", "--"]
8 | command: /workspace/packaging/nfpm/mkrpm.sh
9 |
10 | rhel9:
11 | <<: *rhel
12 | image: dalibo/buildpack-pkg:rockylinux9
13 |
14 | rhel8:
15 | <<: *rhel
16 | image: dalibo/buildpack-pkg:rockylinux8
17 |
18 | trixie: &mkdeb
19 | <<: *rhel
20 | image: dalibo/buildpack-pkg:trixie
21 | command: /workspace/packaging/nfpm/mkdeb.sh
22 |
23 | bookworm:
24 | <<: *mkdeb
25 | image: dalibo/buildpack-pkg:bookworm
26 |
27 | bullseye:
28 | <<: *mkdeb
29 | image: dalibo/buildpack-pkg:bullseye
30 |
31 | jammy:
32 | <<: *mkdeb
33 | image: dalibo/buildpack-pkg:jammy
34 |
35 | noble:
36 | <<: *mkdeb
37 | image: dalibo/buildpack-pkg:noble
38 |
--------------------------------------------------------------------------------
/agent/packaging/nfpm/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | rhel10: &rhel
3 | image: dalibo/buildpack-pkg:rockylinux10
4 | volumes:
5 | - ../..:/workspace
6 | working_dir: /workspace
7 | entrypoint: ["/usr/bin/tini", "--"]
8 | command: /workspace/packaging/nfpm/mkrpm.sh
9 |
10 | rhel9:
11 | <<: *rhel
12 | image: dalibo/buildpack-pkg:rockylinux9
13 |
14 | rhel8:
15 | <<: *rhel
16 | image: dalibo/buildpack-pkg:rockylinux8
17 |
18 | trixie: &mkdeb
19 | <<: *rhel
20 | image: dalibo/buildpack-pkg:trixie
21 | command: /workspace/packaging/nfpm/mkdeb.sh
22 |
23 | bookworm:
24 | <<: *mkdeb
25 | image: dalibo/buildpack-pkg:bookworm
26 |
27 | bullseye:
28 | <<: *mkdeb
29 | image: dalibo/buildpack-pkg:bullseye
30 |
31 | jammy:
32 | <<: *mkdeb
33 | image: dalibo/buildpack-pkg:jammy
34 |
35 | noble:
36 | <<: *mkdeb
37 | image: dalibo/buildpack-pkg:noble
38 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/templates/alerting.check.html:
--------------------------------------------------------------------------------
1 | {% extends ../../../templates/base.html %}
2 |
3 | {% block title %}temBoard / {{instance}} / Monitoring{% end %}
4 |
5 | {% block head %}
6 |
7 | {% end %}
8 |
9 | {% block content %}
10 |
11 |
14 |
15 |
16 |
17 |
28 | {% end %}
29 |
--------------------------------------------------------------------------------
/ui/temboardui/web/routes/core.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 | from flask import g, jsonify, redirect
3 |
4 | from ...model.orm import Instance
5 | from ..flask import admin_required, anonymous_allowed
6 |
7 |
8 | @app.route("/")
9 | @anonymous_allowed
10 | def index():
11 | if g.current_user:
12 | return redirect("/home")
13 | return redirect("/login")
14 |
15 |
16 | @app.route("/json/instances/home")
17 | def get_instance_home():
18 | """Data for InstanceCards.vue component."""
19 | return jsonify(
20 | [
21 | {k: getattr(row, k) for k in row.keys()}
22 | for row in g.db_session.execute(
23 | Instance.select_for_home(g.current_user.role_name)
24 | )
25 | ]
26 | )
27 |
28 |
29 | @app.route("/json/plugins")
30 | @admin_required
31 | def get_plugins():
32 | """List plugins."""
33 | return jsonify(sorted(app.temboard.config.temboard.plugins))
34 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/utils/state.js:
--------------------------------------------------------------------------------
1 | function stateBorderClass(state) {
2 | const classes = ["border"];
3 | if (state != "OK" && state != "UNDEF") {
4 | classes.push("border-" + state.toLowerCase());
5 | }
6 | if (state == "CRITICAL") {
7 | classes.push("border-2");
8 | }
9 | return classes;
10 | }
11 |
12 | function stateBgClass(state) {
13 | const classes = [];
14 | if (state == "UNDEF") {
15 | classes.push("text-bg-light");
16 | } else if (state == "OK") {
17 | classes.push("text-success");
18 | } else {
19 | classes.push("text-bg-" + state.toLowerCase());
20 | }
21 | return classes;
22 | }
23 |
24 | function stateIcon(state) {
25 | if (state == "WARNING") {
26 | return "fa-warning";
27 | }
28 | if (state == "CRITICAL") {
29 | return "fa-burst";
30 | }
31 | if (state == "OK") {
32 | return "fa-check";
33 | }
34 | return "";
35 | }
36 |
37 | export { stateBgClass, stateBorderClass, stateIcon };
38 |
--------------------------------------------------------------------------------
/ui/packaging/nfpm/nfpm.yaml:
--------------------------------------------------------------------------------
1 | name: temboard
2 | arch: amd64
3 | # Accept PEP440
4 | version_schema: none
5 | version: ${VERSION}
6 | release: ${RELEASE}
7 | depends:
8 | - "${PYTHONPKG}"
9 | maintainer: Dalibo Labs
10 | description: PostgreSQL Remote Control UI
11 | homepage: https://labs.dalibo.com/temboard/
12 | license: PostgreSQL
13 | contents:
14 | - type: tree
15 | src: packaging/nfpm/build/destdir/
16 | dst: /
17 | - type: tree
18 | src: share/
19 | dst: /usr/share/temboard
20 | - type: tree
21 | src: packaging/temboard.service
22 | dst: /usr/lib/systemd/system/temboard.service
23 | scripts:
24 | postinstall: share/postinst.sh
25 | preremove: share/preun.sh
26 | overrides:
27 | deb:
28 | depends:
29 | - ssl-cert
30 | - python3-cryptography
31 | - "python3-psycopg2 (>>2.8)"
32 | rpm:
33 | depends:
34 | - openssl
35 | - ${PYTHONPKG}-psycopg2
36 | - ${PYTHONPKG}-cryptography
37 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/runtask.py:
--------------------------------------------------------------------------------
1 | # Note that our home-made background task implementation, we use a different
2 | # semantic that state of the art background task implemtation:
3 | #
4 | # a task function is called a worker
5 | # a worker process is called workerpool
6 | # a message is called a task
7 | # the message broker is called taskmanager
8 | #
9 |
10 | import logging.config
11 |
12 | from temboardtoolkit.app import SubCommand
13 | from temboardtoolkit.taskmanager import RunTaskMixin
14 |
15 | from .app import app
16 |
17 | logger = logging.getLogger(__name__)
18 |
19 |
20 | @app.command
21 | class RunTask(RunTaskMixin, SubCommand):
22 | """Run a task foreground."""
23 |
24 | def main(self, args):
25 | workers = self.iter_workers()
26 |
27 | if "?" == args.worker_name:
28 | self.print_workers(workers)
29 | else:
30 | worker, worker_args = self.compute_worker_args(workers, args)
31 | worker(*worker_args)
32 | return 0
33 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instance-get.sql:
--------------------------------------------------------------------------------
1 | SELECT DISTINCT
2 | i.agent_address,
3 | i.agent_port,
4 | i.hostname,
5 | i.pg_port,
6 | i.notify,
7 | i.comment,
8 | i.discover,
9 | i.discover_date,
10 | i.discover_etag,
11 | i.environment_id,
12 | -- Instance.plugins eager load.
13 | p.agent_address AS p_agent_address,
14 | p.agent_port AS p_agent_port,
15 | p.plugin_name AS p_name,
16 | -- Instance.environment eager load.
17 | e.id AS e_id,
18 | e.name AS e_name,
19 | e.description AS e_description,
20 | e.color AS e_color,
21 | e.dba_group_id AS e_dba_group_id,
22 | -- Environment.dba_group eager load.
23 | g.id AS g_id,
24 | g.name AS g_name,
25 | g.description AS g_description
26 | FROM application.instances AS i
27 | LEFT OUTER JOIN application.plugins AS p ON p.agent_address = i.agent_address AND p.agent_port = i.agent_port
28 | JOIN application.environments AS e ON i.environment_id = e.id
29 | JOIN application.groups AS g ON g.id = e.dba_group_id
30 | WHERE i.agent_address = :address AND i.agent_port = :port;
31 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ''
5 | labels: ''
6 | assignees: ''
7 |
8 | ---
9 |
10 |
32 |
33 |
--------------------------------------------------------------------------------
/agent/packaging/nfpm/nfpm.yaml:
--------------------------------------------------------------------------------
1 | # template file for nfpm configuration
2 | name: temboard-agent
3 | arch: all
4 | # Accept PEP440
5 | version_schema: none
6 | version: ${VERSION}
7 | release: ${RELEASE}
8 | maintainer: Dalibo Labs
9 | description: PostgreSQL Remote Control Agent
10 | homepage: https://labs.dalibo.com/temboard/
11 | license: PostgreSQL
12 | depends:
13 | - ${PYTHONPKG}
14 | contents:
15 | - type: tree
16 | src: packaging/nfpm/build/destdir/
17 | dst: /
18 | - type: tree
19 | src: share/
20 | dst: /usr/share/temboard-agent
21 | - type: tree
22 | src: packaging/temboard-agent@.service
23 | dst: /usr/lib/systemd/system/temboard-agent@.service
24 | scripts:
25 | postinstall: share/restart-all.sh
26 | preremove: share/preun.sh
27 | overrides:
28 | deb:
29 | depends:
30 | - python3-cryptography
31 | - python3-psycopg2 (>= 2.7)
32 | - ssl-cert
33 | rpm:
34 | depends:
35 | - openssl
36 | - ${PYTHONPKG}-cryptography
37 | - ${PYTHONPKG}-psycopg2
38 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/settings/menu.html:
--------------------------------------------------------------------------------
1 |
33 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/notifications.html:
--------------------------------------------------------------------------------
1 | {% extends base.html %}
2 |
3 | {% block title %}temBoard / {{instance}} / Notifications{% end %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('settings.users.js') %}{% raw link %}{% end %}
7 | {% end %}
8 |
9 | {% block content %}
10 |
11 |
12 |
13 |
14 |
15 |
16 | | Date |
17 | Username |
18 | Content |
19 |
20 |
21 |
22 | {% for notification in notifications %}
23 |
24 | | {{notification['date']}} |
25 | {{notification['username']}} |
26 | {{notification['message']}} |
27 |
28 | {% end %}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {% end %}
36 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/dashboard/routes.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from flask import current_app, render_template
4 |
5 | from ...agentclient import TemboardAgentClient
6 | from ...web.flask import instance_routes
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | @instance_routes.route("/dashboard")
12 | def dashboard():
13 | current_app.instance.check_active_plugin("dashboard")
14 |
15 | try:
16 | config = current_app.instance.request("/dashboard/config").json()
17 | except TemboardAgentClient.Error as e:
18 | if 404 != e.code:
19 | raise
20 | logger.debug("Fallback dashboard config.")
21 | config = {"history_length": 150, "scheduler_interval": 2}
22 |
23 | history = current_app.instance.request("/dashboard/history").json()
24 |
25 | current_app.instance.fetch_status()
26 | dashboard = history[-1] if history else {}
27 | return render_template(
28 | "dashboard.html",
29 | plugin="dashboard",
30 | config=config,
31 | dashboard=dashboard,
32 | history=history or "",
33 | )
34 |
--------------------------------------------------------------------------------
/ui/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2020, Dalibo
2 |
3 | Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies.
4 |
5 | IN NO EVENT SHALL DALIBO BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF DALIBO HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 |
7 | DALIBO SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND DALIBO HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
8 |
9 | A modified version of code chunks of PoWA (powa-archivist and powa-web) is embedded in temBoard with copyright (c) Dalibo, The PoWA-team under the terms of the PostgreSQL License.
10 |
--------------------------------------------------------------------------------
/ui/temboardui/model/versions/008_monitoring-archive-wait-lock.sql:
--------------------------------------------------------------------------------
1 | CREATE OR REPLACE FUNCTION monitoring.archive_current_metrics(table_name TEXT, record_type TEXT, query TEXT)
2 | RETURNS TABLE(tblname TEXT, nb_rows INTEGER)
3 | LANGUAGE plpgsql
4 | AS $$
5 | DECLARE
6 | v_table_current TEXT;
7 | v_table_history TEXT;
8 | v_query TEXT;
9 | i INTEGER;
10 | BEGIN
11 | v_table_current := table_name || '_current';
12 | v_table_history := table_name || '_history';
13 | -- Lock _current table to prevent concurrent updates
14 | EXECUTE 'LOCK TABLE ' || v_table_current || ' IN SHARE MODE';
15 | v_query := replace(query, '#history_table#', v_table_history);
16 | v_query := replace(v_query, '#current_table#', v_table_current);
17 | v_query := replace(v_query, '#record_type#', record_type);
18 | -- Move data into _history table
19 | EXECUTE v_query;
20 | GET DIAGNOSTICS i = ROW_COUNT;
21 | -- Truncate _current table
22 | EXECUTE 'TRUNCATE '||v_table_current;
23 | -- Return each history table name and the number of rows inserted
24 | RETURN QUERY SELECT v_table_history, i;
25 | END;
26 | $$;
27 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/activity/__init__.py:
--------------------------------------------------------------------------------
1 | from flask import current_app, jsonify
2 | from flask import render_template as flask_render_template
3 |
4 | from ...web.flask import instance_proxy, instance_routes
5 |
6 |
7 | class ActivityPlugin:
8 | def __init__(self, app, **kw):
9 | self.app = app
10 |
11 | def load(self):
12 | instance_proxy.generic_proxy("/activity/kill", method="POST")
13 |
14 |
15 | @instance_routes.route("/activity")
16 | def activity():
17 | current_app.instance.check_active_plugin("activity")
18 | current_app.instance.fetch_status()
19 | return flask_render_template("activity.html", plugin="activity")
20 |
21 |
22 | @instance_proxy.route("/activity")
23 | def activity_proxy():
24 | # DEPRECATED: move to new agent endpoints for activity.
25 | return jsonify(
26 | dict(
27 | blocking=current_app.instance.request("/activity/blocking").json(),
28 | running=current_app.instance.request("/activity").json(),
29 | waiting=current_app.instance.request("/activity/waiting").json(),
30 | )
31 | )
32 |
--------------------------------------------------------------------------------
/tests/test_60_alerting.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | def test_quick_alerting(browser, browse_alerting):
5 | # Quick smoketest of status page.
6 | browser.select("#checks-container")
7 |
8 |
9 | @pytest.mark.slowmonitoring
10 | def test_alerts(browser, browse_alerting):
11 | checks = [
12 | "btree_bloat",
13 | "cpu_core",
14 | "heap_bloat",
15 | "hitreadratio_db",
16 | "load1",
17 | "memory_usage",
18 | "rollback_db",
19 | "sessions_usage",
20 | "temp_files_size_delta",
21 | "waiting_sessions_db",
22 | "wal_files_archive",
23 | "wal_files_total",
24 | ]
25 |
26 | wanted = {"text-success", "text-bg-warning", "text-bg-critical"}
27 |
28 | for check in checks:
29 | el = browser.refresh_until(f"#status-{check} .badge")
30 | classes = set(el.get_attribute("class").split())
31 | assert wanted & classes
32 |
33 |
34 | @pytest.fixture
35 | def browse_alerting(browse_instance, browser):
36 | """Go to Monitoring tab of current instance."""
37 | browser.select("div.sidebar a.alerting").click()
38 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/maintenance/schema.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Maintenance / {{database}} / {{schema}}{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('maintenance.schema.js') %}{{ link | safe }}{% endfor %}
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 | {% include "maintenance/breadcrumb.html" %}
12 |
13 |
14 |
25 |
26 |
27 |
28 | {% endblock %}
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python runtime files
2 | *.egg-info/
3 | *.pyc
4 | .venv/
5 |
6 | dev/downloads/
7 | dev/bin/prometheus
8 | dev/bin/promtool
9 |
10 | # Buildtime files
11 | agent/temboardagent/_vendor/
12 | ui/temboardui/_vendor/
13 | build/
14 | dist/
15 |
16 | # ViteJS assets
17 | ui/temboardui/static/*.css
18 | ui/temboardui/static/css/
19 | ui/temboardui/static/images/
20 | ui/temboardui/static/*.js
21 | ui/temboardui/static/js/
22 | ui/temboardui/static/fontawesome-webfont*
23 | ui/temboardui/static/manifest.json
24 | ui/temboardui/static/.vite/
25 |
26 | # From dev compose project
27 | .env
28 | dev/agent-bash_history
29 | dev/agent-psql_history
30 | dev/pip-cache
31 | dev/prometheus/targets/*.yaml
32 |
33 | # Test runtime files
34 | # from pytest-log
35 | test_log.log
36 | tests/downloads/
37 | tests/logs/
38 | tests/screenshots/
39 | tests/workdir/
40 | tests/results.xml
41 |
42 | # Nodejs buildtime files
43 | node_modules/
44 |
45 | # temBoard runtime files
46 | dev/temboard/
47 |
48 | # Help developer customize their development environment
49 | docker-compose.override.yml
50 | temboard.conf.d/
51 | *.log
52 | dev/prometheus/import/*.txt
53 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/serve.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os
3 |
4 | from temboardtoolkit import services
5 | from temboardtoolkit.app import SubCommand
6 |
7 | from ..plugins.monitoring import db
8 | from .app import app
9 |
10 | logger = logging.getLogger(__name__)
11 |
12 |
13 | @app.command
14 | class Serve(SubCommand):
15 | """Combined web and worker services."""
16 |
17 | is_service = True
18 |
19 | def main(self, args):
20 | # Purge all legacy data queues
21 | home = self.app.config.temboard.home
22 | if os.path.exists(home):
23 | [
24 | os.remove(os.path.join(home, f))
25 | for f in os.listdir(home)
26 | if f.endswith(".q")
27 | ]
28 |
29 | if "monitoring" in self.app.config.temboard.plugins:
30 | logger.info("Resetting monitoring data.")
31 | db.bootstrap(self.app.config.temboard.home, "monitoring.db")
32 |
33 | self.app.config.load_signing_key()
34 | self.app.discover.refresh()
35 | self.app.discover.write()
36 |
37 | return services.run(self.app.httpd, self.app.scheduler, self.app.worker_pool)
38 |
--------------------------------------------------------------------------------
/ui/temboardui/handlers/settings/metadata.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from temboardui.web.tornado import admin_required, app, render_template
4 |
5 | from ...version import inspect_versions
6 |
7 | logger = logging.getLogger(__name__)
8 |
9 |
10 | @app.route(r"/settings/metadata")
11 | @admin_required
12 | def metadata(request):
13 | versions_info = inspect_versions()
14 | infos = {
15 | "Version": "%(temboard)s (%(temboardbin)s)" % versions_info,
16 | "Uptime": app.start_time,
17 | "OS": "%(distname)s %(distversion)s" % versions_info,
18 | "Python": "%(python)s (%(pythonbin)s)" % versions_info,
19 | "cryptography": "%(cryptography)s" % versions_info,
20 | "Tornado": "%(tornado)s" % versions_info,
21 | "libpq": "%(libpq)s" % versions_info,
22 | "psycopg2": "%(psycopg2)s" % versions_info,
23 | "SQLAlchemy": "%(sqlalchemy)s" % versions_info,
24 | }
25 | temboard_version = "%(temboard)s" % versions_info
26 |
27 | return render_template(
28 | "settings/metadata.html",
29 | role=request.current_user,
30 | infos=infos,
31 | temboard_version=temboard_version,
32 | )
33 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/activity-sessions.sql:
--------------------------------------------------------------------------------
1 | WITH blockages AS (
2 | SELECT
3 | pid AS waiting_pid,
4 | -- Requires pg_blocking_pids from PostgreSQL 9.6+
5 | unnest(pg_blocking_pids(pid)) AS blocking_pid
6 | FROM pg_stat_activity
7 | )
8 | SELECT
9 | sessions.pid AS pid,
10 | sessions.client_addr AS client_addr,
11 | sessions.usename AS "username",
12 | sessions.application_name AS application_name,
13 | sessions.datname AS database,
14 | sessions."state" AS "state",
15 | sessions.backend_start AS backend_start,
16 | sessions.query_start AS query_start,
17 | round(EXTRACT(epoch FROM (NOW() - sessions.query_start))::numeric, 2)::FLOAT AS duration,
18 | sessions.query AS query,
19 | waiting.waiting_pid IS NOT NULL AS waiting,
20 | blocking.blocking_pid IS NOT NULL AS blocking
21 | FROM pg_catalog.pg_stat_activity AS sessions
22 | LEFT OUTER JOIN blockages AS waiting ON waiting.waiting_pid = pid
23 | LEFT OUTER JOIN blockages AS blocking ON blocking.blocking_pid = pid
24 | WHERE sessions.pid <> pg_backend_pid()
25 | AND sessions.backend_type = 'client backend' -- pragma:pg_version_min 100000
26 | ORDER BY EXTRACT(epoch FROM (NOW() - sessions.query_start)) DESC
27 |
--------------------------------------------------------------------------------
/docs/postgres_upgrade.md:
--------------------------------------------------------------------------------
1 | This section explains how to upgrade a PostgreSQL instance monitored by a temBoard agent.
2 |
3 | To upgrade to PostgreSQL instance hosting the *repository* database,
4 | please go to the [Server Upgrade] section.
5 |
6 | [Server Upgrade]: server_upgrade.md
7 |
8 |
9 | ## Minor Upgrade
10 |
11 | PostgreSQL minor upgrades (e.g. from `10.3` to `10.4`) are very important
12 | but they include only backward-compatible changes.
13 |
14 | Therefore, you can simply upgrade your instance using the usual update process,
15 | depending on which distribution you are using.
16 |
17 | You dont need to restart temBoard agent nor edit its configuration.
18 |
19 |
20 | ## Major Upgrade
21 |
22 | PostgreSQL major upgrades (e.g. from 13 to 14) will bring new features and improvements.
23 |
24 | If you choose to migrate your data from your current instance to a new one listening to another port (or on another host),
25 | then you can simply add a temboard-agent to this new instance.
26 | However you will lose the history of the former one.
27 |
28 | If you choose the *in-place upgrade*, you can keep the same agent.
29 | You don't need to restart agent nor edit its configuration.
30 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/maintenance/table.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Maintenance / {{database}} / {{schema}} / {{table}}{% endblock %}
4 |
5 | {% block head %}
6 | {% for link in vitejs.css_links_for('maintenance.table.js') %}{{ link | safe }}{% endfor %}
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
11 | {% include "maintenance/breadcrumb.html" %}
12 |
13 |
14 |
26 |
27 |
28 |
29 | {% endblock %}
30 |
--------------------------------------------------------------------------------
/ui/share/temboard_CHANGEME.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDETCCAfkCFCfsZSk1gg872dzAFpy/8yk3B2GCMA0GCSqGSIb3DQEBCwUAMEUx
3 | CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwNDI2MTcwNzI3WhcNMjUwNDI1MTcw
5 | NzI3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
6 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
7 | AQ8AMIIBCgKCAQEAsJYf3R8GOrtF0wn6cxLn4OxnClccKzvVV25zQIR+QM0SeIFk
8 | lxs7zxc5bT2TsVmr2dGnHhqrgueQL/UFfwovZUpkktZnUaNm6El2XSSIQJ+X0m6i
9 | 8K6NKn7Yjgqkj36tMyMNerGcXjfA9+QfXjnkTIXty19GqYtzslYFZRWXDfoKLhQf
10 | qjRdIMPsxeXzrROYfAuT1dxBULfLH+oa2RWL/gaVt+L7+ld06r/a6dFGCuHebbTN
11 | FaqqdmMwnMBCqYkrgPPhsysJF07ENGBIeFzYX54gOtpG+Ya5ayWvQsQLSlGlhYkC
12 | tNqFcxT5jtWvXHVDSkaQ4g8F+2SJApUeQNqHHQIDAQABMA0GCSqGSIb3DQEBCwUA
13 | A4IBAQBhT5DnbJFDrt9fCP3bZbD3X9iAv3YA8dEp46BfyTd4jh8HALiHttmUEjG+
14 | ImB7y/U/84vrMRgdAfVpkogMIO9fHG9joLQxuCtKkeikKI5BBw+BM5wZyC8m3bpW
15 | 9wvAsle5Suc0scx6Mpwiu5/n+nYEj25UE8vkCZ7R/wRH1/xLfk1tWFYhyvz/exUm
16 | KmcdbUWxSkxrWCEeHHTIwSlf/yty5VNz4OzzSy2919kkcs9iB5vjgKp0zbC7SDXA
17 | Inm78yDQpcZXfhnt7cRTu2d4Vm+wsdpOX5nf7qBuGAT/VvqJg9qaLqNVqNNtcj/x
18 | e8dTtauaSnZ4MvZvU8rwFZ4ATsTA
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/agent/share/temboard-agent_CHANGEME.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDETCCAfkCFGOIzLQKtvCulsO6905Nd3d95aOOMA0GCSqGSIb3DQEBCwUAMEUx
3 | CzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl
4 | cm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMjIwNDI2MTcwNzI3WhcNMjUwNDI1MTcw
5 | NzI3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UE
6 | CgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOC
7 | AQ8AMIIBCgKCAQEA0HwkI8cz7KnBf+2arktqO3TvaVi8+307rJGkkfmeDZYnjuWv
8 | BiQSF7N97thMPLaB7W2J2nzrIJCKTTtnagqVOn/TLOboSxDAzMWaJh9jfXqOwgX1
9 | e4khjw7gXemOQR/tCPVX00fIorKtX0vqS1oBDlbpW/Yxxu2gI/Lwld+787957iy5
10 | uZq8kIqa19tOW+0Kp8288nQbqnCrzb3yOFS57T6UdRHqtwY12vuZV/orjwL6ToVR
11 | zJXGt9Nqqa6dU4toXCEan3AcOQQ8oYmRaNaW0HzZI+jXJs8Y+GS+K1FtFibA2eYU
12 | Rql2DpiHcsg+l6UKQwM340nVGJLbMF6ZbJWrrwIDAQABMA0GCSqGSIb3DQEBCwUA
13 | A4IBAQBu7zj8LR/XMMS+pI/fbDxd5BkXTwAOZ12aDfA+UpEHv6+94+NhFy6XCTKX
14 | JJDYavNdijTtTZjmjdfq6Ryza2R2xhj7DZmgi8COg31foRU+TE6/th4EUxb8bQwg
15 | E/tvMAhNF55RZitIOyFSkylXAK4sT0XqbGlRTo343ME/YzSTqtCMv28iUhLHuDRn
16 | KZCS+6dk930dnCuUvT2gnsd6+MgS61jhgYIYLeyhO6MdDs4VFiH8z0mdudPbcTru
17 | YuEjVwTw9g/3dAuq59oJduRn1l7RLCSMG7ylpQotnjpD+gwDMM4+sQzbvdX4/VP7
18 | ukZMXodW9KeqWGMbUAdBnO99+/qm
19 | -----END CERTIFICATE-----
20 |
--------------------------------------------------------------------------------
/docs/compatibility_guide.md:
--------------------------------------------------------------------------------
1 | temBoard is designed to interact with many components: systemd, PostgreSQL,
2 | agents, etc.
3 |
4 | As we are trying to find the right balance between innovation and backward
5 | compatibility, we define a comprehensive list of platforms and software that
6 | we support for each version.
7 |
8 | Temboard 10 is the current release.
9 |
10 | Each minor release (e.g. `10.1`, `10.2`, etc.) is compatible with the following
11 | components:
12 |
13 | | | |
14 | | -------------- | -------------------------------------------|
15 | | Linux | Debian 12 and 13, Ubuntu 22.04 and 24.04, RHEL 8, 9 and 10 |
16 | | Python | 3.9+ |
17 | | Postgres | 13 to 18 |
18 | | Temboard Agent | 9.x and 10.x |
19 |
20 | Additional notes:
21 |
22 | * On RHEL8, the default python version is 3.6. Be sure to install `python39`.
23 | * RHEL7 et Debian 11 (bullseye) are unsupported
24 | * All PostgreSQL versions below 13.x are obsolete and unsupported
25 | * It is highly recommended to upgrade PostgreSQL to the latest minor version of your
26 | current major version
27 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/settings.notifications.js:
--------------------------------------------------------------------------------
1 | import $ from "jquery";
2 |
3 | $("#sendEmailForm").on("submit", function (e) {
4 | e.preventDefault();
5 | sendEmail();
6 | });
7 |
8 | $("#sendSmsForm").on("submit", function (e) {
9 | e.preventDefault();
10 | sendSms();
11 | });
12 |
13 | function sendEmail() {
14 | clearError();
15 | $.ajax({
16 | url: "/json/test_email",
17 | type: "post",
18 | contentType: "application/json",
19 | data: JSON.stringify({
20 | email: $("#inputTestEmail").val(),
21 | }),
22 | success: function (data) {
23 | var msg = "Test email successfully sent";
24 | msg += "\nIf the email is not received, please have a look at your SMTP server logs";
25 | alert(msg);
26 | },
27 | error: function (xhr) {
28 | console.log("error", xhr);
29 | showError(xhr);
30 | },
31 | });
32 | }
33 |
34 | function sendSms() {
35 | clearError();
36 | $.ajax({
37 | url: "/json/test_sms",
38 | type: "post",
39 | contentType: "application/json",
40 | data: JSON.stringify({
41 | phone: $("#inputTestPhone").val(),
42 | }),
43 | success: function (data) {
44 | alert("Test SMS sent");
45 | },
46 | error: showError,
47 | });
48 | }
49 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/utils/duration.js:
--------------------------------------------------------------------------------
1 | import _ from "lodash";
2 |
3 | const durationSecond = 1000;
4 | const durationMinute = durationSecond * 60;
5 | const durationHour = durationMinute * 60;
6 | const durationDay = durationHour * 24;
7 |
8 | function formatDuration(ms, rounded) {
9 | let results = [];
10 | const µs = Math.round((ms - Math.floor(ms)) * 1000);
11 | ms = Math.floor(ms);
12 | const d = Math.trunc(ms / durationDay);
13 | results.push(d + " d");
14 | ms = ms - d * durationDay;
15 | const h = Math.trunc(ms / durationHour);
16 | results.push(h + " h");
17 | ms = ms - h * durationHour;
18 | const m = Math.trunc(ms / durationMinute);
19 | results.push(m + " min");
20 | ms = ms - m * durationMinute;
21 | const s = Math.trunc(ms / durationSecond);
22 | results.push(s + " s");
23 | ms = ms - s * durationSecond;
24 | results.push(ms + " ms");
25 | results.push(µs + " µs");
26 |
27 | results = _.dropWhile(results, (o) => parseInt(o) == 0);
28 |
29 | if (rounded) {
30 | // Only keep the first or two firsts values
31 | const n = parseInt(results[0]) < 3 && parseInt(results[1]) !== 0 ? 2 : 1;
32 | results = results.slice(0, n);
33 | }
34 | return results.length ? results.join(" ") : "0 ms";
35 | }
36 |
37 | export { formatDuration };
38 |
--------------------------------------------------------------------------------
/dev/grafana/rootfs/etc/grafana/provisioning/datasources/prometheus.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: 1
2 |
3 | datasources:
4 | - "uid": "01lWBBhnk"
5 | "orgId": 1
6 | "name": "Prometheus"
7 | "type": "prometheus"
8 | "typeName": "Prometheus"
9 | "typeLogoUrl": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg"
10 | "access": "proxy"
11 | "url": "http://prometheus:9090/"
12 | "password": ""
13 | "user": ""
14 | "database": ""
15 | "basicAuth": false
16 | "isDefault": true
17 | "jsonData":
18 | "httpMethod": "POST"
19 | "timeInterval": "60s"
20 | "readOnly": false
21 |
22 | - "uid": "Kme6NfMSz"
23 | "orgId": 1
24 | "name": "temBoard prometheus"
25 | "type": "prometheus"
26 | "typeName": "Prometheus"
27 | "typeLogoUrl": "public/app/plugins/datasource/prometheus/img/prometheus_logo.svg"
28 | "access": "proxy"
29 | # This IP is the default docker0 host IP.
30 | # Binding to 0.0.0.0 on docker host exposes to container through this IP at least.
31 | # You may need to update if docker0 host IP is different.
32 | # And then recreate Grafana container.
33 | "url": "http://172.17.0.1:8890/"
34 | "user": ""
35 | "database": ""
36 | "basicAuth": false
37 | "isDefault": false
38 | "jsonData":
39 | "httpMethod": "POST"
40 | "readOnly": false
41 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/components/ModalDialog.vue:
--------------------------------------------------------------------------------
1 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/ui/share/temboard_ca_certs_CHANGEME.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDXTCCAkWgAwIBAgIJALXncntr5W52MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMDk0MzA3WhcNMjExMDIyMDk0MzA3WjBF
5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7 | CgKCAQEAsJYf3R8GOrtF0wn6cxLn4OxnClccKzvVV25zQIR+QM0SeIFklxs7zxc5
8 | bT2TsVmr2dGnHhqrgueQL/UFfwovZUpkktZnUaNm6El2XSSIQJ+X0m6i8K6NKn7Y
9 | jgqkj36tMyMNerGcXjfA9+QfXjnkTIXty19GqYtzslYFZRWXDfoKLhQfqjRdIMPs
10 | xeXzrROYfAuT1dxBULfLH+oa2RWL/gaVt+L7+ld06r/a6dFGCuHebbTNFaqqdmMw
11 | nMBCqYkrgPPhsysJF07ENGBIeFzYX54gOtpG+Ya5ayWvQsQLSlGlhYkCtNqFcxT5
12 | jtWvXHVDSkaQ4g8F+2SJApUeQNqHHQIDAQABo1AwTjAdBgNVHQ4EFgQUtlIGs0QI
13 | XlQtg7bkbzBH35j57howHwYDVR0jBBgwFoAUtlIGs0QIXlQtg7bkbzBH35j57how
14 | DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAgxWfdbITjrWOMJEzhY3q
15 | KKz+B/N4QjUq9hpceQtmyT21ntbqkto1yaeksws9Z/81JBYkowW5U6J6DzOziIpI
16 | PptEz845AKa8rljPv7o8Tu/Z+9MdP4Pdl3eVayMIF/WR7drR6vCIRycw25yiY0UT
17 | RvgsGcW6Tf7sz8LWGzlvwcgATXLu5hwGC0iEWRfGESd0wsMEu0FcGDnJceScOVBu
18 | ssp2k7K4g1V+bHEQftTlQ/wYfFgy/o4BoVV0rQx4jlSq9hTx9nTQcbBLtw9stHkr
19 | gNoaIx+Fdqm/OT/h1WDmJkb/grd75uMTZKhJKKMxcvYxtYxpdtWTDSoPqgnVXFBc
20 | hQ==
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/agent/share/temboard-agent_ca_certs_CHANGEME.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDYDCCAkigAwIBAgIJALYNYoXM3oe6MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
4 | aWRnaXRzIFB0eSBMdGQwHhcNMTgwNjI2MTQzNzA1WhcNMjEwNjI1MTQzNzA1WjBF
5 | MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
6 | ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
7 | CgKCAQEA0HwkI8cz7KnBf+2arktqO3TvaVi8+307rJGkkfmeDZYnjuWvBiQSF7N9
8 | 7thMPLaB7W2J2nzrIJCKTTtnagqVOn/TLOboSxDAzMWaJh9jfXqOwgX1e4khjw7g
9 | XemOQR/tCPVX00fIorKtX0vqS1oBDlbpW/Yxxu2gI/Lwld+787957iy5uZq8kIqa
10 | 19tOW+0Kp8288nQbqnCrzb3yOFS57T6UdRHqtwY12vuZV/orjwL6ToVRzJXGt9Nq
11 | qa6dU4toXCEan3AcOQQ8oYmRaNaW0HzZI+jXJs8Y+GS+K1FtFibA2eYURql2DpiH
12 | csg+l6UKQwM340nVGJLbMF6ZbJWrrwIDAQABo1MwUTAdBgNVHQ4EFgQUN/ZOXDwy
13 | QBxz7r5DQGjOUAxFFGIwHwYDVR0jBBgwFoAUN/ZOXDwyQBxz7r5DQGjOUAxFFGIw
14 | DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAXOoFGi8FpSyXQkwx
15 | VZ6rSQRk8dCorwP3STQsw9p+pXU543y+uZgm1MRJa7LrVAn/5dkBMlEk8LGUzIZt
16 | ScQHWgRn3AtiR5Fz3EOsT24+/JyBqVmIz6WbeeyXAsmoNY3aU5RiRudOGplUubz2
17 | c+pbQCaLsj47Xgu2bgIsJsDzOEy9E5AXcfukBSJIZEO2SzeNGRtSRQpe0pHp5VC1
18 | bj14HVlAD73xarRwatByMj9Nd2PLBVYZcXCWbBYnELG3NBvXzptaK2yCeB8l7H37
19 | V3rS0q+gSDcJMxyFyG4jVCcI/AeUHndxRozDgLWE6y2nxSHHQpcZ7fy24IZfTKMc
20 | DmdQAw==
21 | -----END CERTIFICATE-----
22 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/monitoring/pivot.py:
--------------------------------------------------------------------------------
1 | import csv
2 | import operator
3 |
4 |
5 | def get_csv_data(fd):
6 | # CSV parser as generator
7 | yield from csv.DictReader(fd)
8 |
9 |
10 | def pivot_timeserie(fd, index, key, value, output):
11 | # Simple pivot table implementation.
12 | # Beware, input data *MUST* be ordered by index value.
13 | fd.seek(0)
14 | keys = {}
15 | p = 1
16 | # We need to get the keys first
17 | for r in get_csv_data(fd):
18 | if r[key] not in keys:
19 | keys[r[key]] = p
20 | p += 1
21 | sk = sorted(list(keys.items()), key=operator.itemgetter(1))
22 | # CSV Header
23 | line = [index] + [x[0] for x in sk]
24 | p_index = ""
25 | fd.seek(0)
26 | for r in get_csv_data(fd):
27 | if r[index] != p_index:
28 | # As data are ordered if we meet a new index value then the current
29 | # line is complete.
30 | output.write(",".join(line) + "\n")
31 | # And start a new line
32 | line = [""] * (len(keys) + 1)
33 | line[0] = r[index]
34 | # Append value to the current line
35 | line[keys[r[key]]] = r[value]
36 | # Keep a track of the index value
37 | p_index = r[index]
38 | # Write the last line
39 | output.write(",".join(line) + "\n")
40 |
--------------------------------------------------------------------------------
/agent/temboardagent/command.py:
--------------------------------------------------------------------------------
1 | import errno
2 | from shlex import split as shlex_split
3 | from subprocess import PIPE, CalledProcessError, Popen, check_call
4 |
5 |
6 | def exec_command(command_args, **kwargs):
7 | """
8 | Execute a system command with Popen.
9 | """
10 | kwargs.setdefault("stdout", PIPE)
11 | kwargs.setdefault("stderr", PIPE)
12 | kwargs.setdefault("stdin", PIPE)
13 | kwargs.setdefault("close_fds", True)
14 | try:
15 | process = Popen(command_args, **kwargs)
16 | except OSError as err:
17 | return (err.errno, None, err.strerror)
18 |
19 | (stdout, stderrout) = process.communicate()
20 |
21 | return (process.returncode, stdout, stderrout)
22 |
23 |
24 | def exec_script(script_args, **kwargs):
25 | """
26 | Execute an external script.
27 | """
28 | kwargs.setdefault("stderr", PIPE)
29 | kwargs.setdefault("stdout", PIPE)
30 | try:
31 | check_call(script_args, **kwargs)
32 | except CalledProcessError as err:
33 | return (err.returncode, None, err.output)
34 | except OSError as err:
35 | if err.errno == errno.EPIPE:
36 | pass
37 |
38 | return (0, None, None)
39 |
40 |
41 | def oneline_cmd_to_array(command_line):
42 | """
43 | Split a command line using shlex module.
44 | """
45 | return shlex_split(command_line)
46 |
--------------------------------------------------------------------------------
/agent/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "temboard-agent"
3 | version = "10.0.0"
4 | description = "PostgreSQL Remote Control Agent"
5 | readme = "README.md"
6 | requires-python = ">=3.9,<3.14"
7 | license = "PostgreSQL"
8 | authors = [
9 | {name = "Dalibo", email = "contact@dalibo.com"},
10 | ]
11 | classifiers = ["Private :: Do Not Upload"]
12 | dependencies = [
13 | "cryptography",
14 | ]
15 |
16 | [project.urls]
17 | Homepage = "https://labs.dalibo.com/temboard"
18 | Documentation = "https://temboard.readthedocs.io"
19 | Repository = "https://github.com/dalibo/temboard"
20 | Changelog = "https://github.com/dalibo/temboard/blob/master/CHANGELOG.md"
21 |
22 | [project.scripts]
23 | temboard-agent = "temboardagent.__main__:main"
24 |
25 | [project.entry-points."temboardagent.plugins"]
26 | activity = "temboardagent.plugins.activity:ActivityPlugin"
27 | dashboard = "temboardagent.plugins.dashboard:DashboardPlugin"
28 | maintenance = "temboardagent.plugins.maintenance:MaintenancePlugin"
29 | monitoring = "temboardagent.plugins.monitoring:MonitoringPlugin"
30 | pgconf = "temboardagent.plugins.pgconf:PgConfPlugin"
31 | statements = "temboardagent.plugins.statements:StatementsPlugin"
32 |
33 | [build-system]
34 | requires = ["uv_build>=0.9.10,<0.10.0"]
35 | build-backend = "uv_build"
36 |
37 | [tool.uv.build-backend]
38 | module-name = "temboardagent"
39 | module-root = ""
40 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/tasks.py:
--------------------------------------------------------------------------------
1 | # Note that our home-made background task implementation uses a different
2 | # semantic that state of the art background task implemtation:
3 | #
4 | # a task function is called a worker
5 | # a worker process is called workerpool
6 | # a message is called a task
7 | # the message broker is called taskmanager
8 | #
9 |
10 | import logging.config
11 |
12 | from temboardtoolkit.app import SubCommand
13 | from temboardtoolkit.errors import UserError
14 | from temboardtoolkit.taskmanager import FlushTasksMixin, RunTaskMixin
15 |
16 | from .app import app
17 |
18 | logger = logging.getLogger(__name__)
19 |
20 |
21 | @app.command
22 | class Tasks(SubCommand):
23 | """Manage background tasks."""
24 |
25 | def main(self, args):
26 | raise UserError("Missing sub-command. See --help for details.")
27 |
28 |
29 | @Tasks.command
30 | class Flush(FlushTasksMixin, SubCommand):
31 | """Flush all tasks."""
32 |
33 |
34 | @Tasks.command
35 | class Run(RunTaskMixin, SubCommand):
36 | """Run a task foreground."""
37 |
38 | def main(self, args):
39 | workers = self.iter_workers()
40 |
41 | if "?" == args.worker_name:
42 | self.print_workers(workers)
43 | else:
44 | worker, worker_args = self.compute_worker_args(workers, args)
45 | worker(*worker_args)
46 | return 0
47 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/login.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}{{ super() }}/ Login{% endblock %}
4 |
5 | {% block content %}
6 |
31 |
32 | {% endblock %}
33 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "temboard",
3 | "private": true,
4 | "type": "module",
5 | "engines": {
6 | "node": ">=20.0.0"
7 | },
8 | "scripts": {
9 | "dev": "vite",
10 | "build": "vite build",
11 | "install-husky": "cd .. && husky install ui/.husky"
12 | },
13 | "dependencies": {
14 | "@fortawesome/fontawesome-free": "^7.1.0",
15 | "@vueuse/components": "^10.11.0",
16 | "@vueuse/core": "^10.9.0",
17 | "bootstrap": "^5.3.3",
18 | "bootstrap-vue-next": "^0.16.6",
19 | "datatables.net-bs5": "^2.0.8",
20 | "datatables.net-buttons-bs5": "^3.0.2",
21 | "datatables.net-vue3": "^3.0.1",
22 | "daterangepicker": "^2.1.27",
23 | "dygraphs": "^2.2.1",
24 | "filesize": "^10.1.2",
25 | "highlight.js": "^11.9.0",
26 | "jquery": "^3.7.1",
27 | "lodash": "^4.17.21",
28 | "moment": "^2.30.1",
29 | "pev2": "^1.12.1",
30 | "vue": "^3.4.21",
31 | "vue-multiselect": "^3.0.0",
32 | "vue-router": "^4.4.0"
33 | },
34 | "devDependencies": {
35 | "@trivago/prettier-plugin-sort-imports": "^4.3.0",
36 | "@vitejs/plugin-vue": "^5.0.5",
37 | "autoprefixer": "^10.4.19",
38 | "husky": "^8.0.3",
39 | "lint-staged": "^14.0.1",
40 | "postcss": "^8.4.38",
41 | "prettier": "3.3.3",
42 | "prettier-plugin-jinja-template": "1.4.1",
43 | "sass": "^1.77.6",
44 | "vite": "^5.3.1"
45 | },
46 | "lint-staged": {
47 | "*.{js,css,vue}": "prettier --write"
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/toolkit/tests/test_versions.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from textwrap import dedent
3 |
4 |
5 | def test_os_release():
6 | from temboardtoolkit.versions import parse_lsb_release
7 |
8 | distinfos = parse_lsb_release(
9 | dedent("""\
10 | PRETTY_NAME="Debian GNU/Linux 10 (buster)"
11 | NAME="Debian GNU/Linux"
12 | VERSION_ID="10"
13 | VERSION="10 (buster)"
14 | VERSION_CODENAME=buster
15 | ID=debian
16 | HOME_URL="https://www.debian.org/"
17 | SUPPORT_URL="https://www.debian.org/support"
18 | BUG_REPORT_URL="https://bugs.debian.org/"
19 | """).splitlines(True)
20 | )
21 |
22 | assert "10" == distinfos["VERSION_ID"]
23 | assert "10 (buster)" == distinfos["VERSION"]
24 |
25 |
26 | def test_format_pq_version():
27 | from temboardtoolkit.versions import format_pq_version
28 |
29 | assert "14.1" == format_pq_version(140001)
30 | assert "13.5" == format_pq_version(130005)
31 | assert "12.9" == format_pq_version(120009)
32 | assert "11.14" == format_pq_version(110014)
33 | assert "10.19" == format_pq_version(100019)
34 | assert "9.6.24" == format_pq_version(90624)
35 |
36 |
37 | def test_read_libpq_version_from_ctypes(mocker):
38 | __import__("psycopg2.extensions")
39 | # Remove __libpq_version__ if any.
40 | mocker.patch.dict(sys.modules, [("psycopg2.extensions", object())])
41 |
42 | from temboardtoolkit.versions import read_libpq_version
43 |
44 | assert read_libpq_version() > 90000
45 |
--------------------------------------------------------------------------------
/tests/pytest-ci:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | #
3 | # Wrapper around pytest to configure marker expr from git diff.
4 | #
5 |
6 | main() {
7 | upstream="$1"; shift
8 | marker_expr=()
9 |
10 | if [ "$CIRCLE_BRANCH" = "$upstream" ] ; then
11 | log "Run all tests on $upstream."
12 | else
13 | if ! git diff --name-only "origin/$upstream"..HEAD | grep -f tests/monitoring-files ; then
14 | log "Skipping slow monitoring tests."
15 | marker_expr+=("not slowmonitoring")
16 | fi
17 |
18 | if ! git diff --name-only "origin/$upstream"..HEAD | grep -f tests/statements-files ; then
19 | log "Skipping slow statements tests."
20 | marker_expr+=("not slowstatements")
21 | fi
22 |
23 | if [ "${#marker_expr[@]}" -eq 0 ] ; then
24 | log "Running all test for modifications."
25 | fi
26 | fi
27 |
28 | marker_expr_s="$(join_array " and " "${marker_expr[@]-}")"
29 |
30 | if [ "${marker_expr_s}" ] ; then
31 | set -- "-m" "$marker_expr_s" "$@"
32 | fi
33 |
34 | args=("$@") # Hack for CentOS 7 bash.
35 | log "Running pytest" "${args[@]@Q}"
36 |
37 | exec pytest "$@"
38 | }
39 |
40 |
41 | log() {
42 | echo "$*" >&2
43 | }
44 |
45 |
46 | # From https://stackoverflow.com/questions/1527049/how-can-i-join-elements-of-an-array-in-bash
47 | join_array() {
48 | local d="${1-}" f="${2-}"
49 | if shift 2; then
50 | printf %s "$f" "${@/#/$d}"
51 | fi
52 | }
53 |
54 |
55 | main "$@"
56 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/maintenance/routes.py:
--------------------------------------------------------------------------------
1 | from flask import current_app, render_template
2 |
3 | from ...web.flask import instance_routes
4 |
5 | PLUGIN_NAME = "maintenance"
6 |
7 |
8 | @instance_routes.route("/maintenance")
9 | def maintenance():
10 | current_app.instance.check_active_plugin(PLUGIN_NAME)
11 | current_app.instance.fetch_status()
12 | return render_template("maintenance/index.html", plugin=PLUGIN_NAME)
13 |
14 |
15 | @instance_routes.route("/maintenance//schema//table/")
16 | def table(database, schema, table):
17 | current_app.instance.check_active_plugin(PLUGIN_NAME)
18 | current_app.instance.fetch_status()
19 | return render_template(
20 | "maintenance/table.html",
21 | plugin=PLUGIN_NAME,
22 | database=database,
23 | schema=schema,
24 | table=table,
25 | )
26 |
27 |
28 | @instance_routes.route("/maintenance//schema/")
29 | def schema(database, schema):
30 | current_app.instance.check_active_plugin(PLUGIN_NAME)
31 | current_app.instance.fetch_status()
32 | return render_template(
33 | "maintenance/schema.html", plugin=PLUGIN_NAME, database=database, schema=schema
34 | )
35 |
36 |
37 | @instance_routes.route("/maintenance/")
38 | def database(database):
39 | current_app.instance.check_active_plugin(PLUGIN_NAME)
40 | current_app.instance.fetch_status()
41 | return render_template(
42 | "maintenance/database.html", plugin=PLUGIN_NAME, database=database
43 | )
44 |
--------------------------------------------------------------------------------
/ui/temboardui/version.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from importlib.metadata import version
3 | from platform import python_version
4 |
5 | __version__ = version("temboard")
6 |
7 |
8 | # This output is parsed by tests/conftest.py::pytest_report_header.
9 | VERSION_FMT = """\
10 | temBoard %(temboard)s (%(temboardbin)s)
11 | System %(distname)s %(distversion)s
12 | Python %(python)s (%(pythonbin)s)
13 | cryptography %(cryptography)s
14 | Tornado %(tornado)s
15 | Flask %(flask)s
16 | libpq %(libpq)s
17 | psycopg2 %(psycopg2)s
18 | SQLAlchemy %(sqlalchemy)s
19 | """
20 |
21 |
22 | def format_version():
23 | return VERSION_FMT % inspect_versions()
24 |
25 |
26 | def inspect_versions():
27 | import cryptography
28 | import flask
29 | import psycopg2
30 | import sqlalchemy
31 | import tornado
32 | from temboardtoolkit.versions import (
33 | format_pq_version,
34 | read_distinfo,
35 | read_libpq_version,
36 | )
37 |
38 | distinfos = read_distinfo()
39 |
40 | return dict(
41 | cryptography=cryptography.__version__,
42 | distname=distinfos["NAME"],
43 | distversion=distinfos.get("VERSION", "n/a"),
44 | flask=flask.__version__,
45 | libpq=format_pq_version(read_libpq_version()),
46 | psycopg2=psycopg2.__version__,
47 | python=python_version(),
48 | pythonbin=sys.executable,
49 | sqlalchemy=sqlalchemy.__version__,
50 | temboard=__version__,
51 | temboardbin=sys.argv[0],
52 | tornado=tornado.version,
53 | )
54 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/routes.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from bottle import default_app
4 | from temboardtoolkit.app import SubCommand
5 |
6 | from .app import app
7 |
8 | logger = logging.getLogger(__name__)
9 |
10 |
11 | @app.command
12 | class Routes(SubCommand):
13 | """Show HTTP routing table."""
14 |
15 | def main(self, args):
16 | for path, method, callback in sorted(iter_bottle_routes()):
17 | doc = callback.__doc__ or ""
18 | try:
19 | title = doc.splitlines()[0]
20 | except IndexError:
21 | title = ""
22 | print(" %6.6s %-64s %s" % (method, path, title))
23 |
24 | return 0
25 |
26 |
27 | def iter_bottle_routes(bottle=None):
28 | if bottle is None:
29 | bottle = default_app()
30 | for route in bottle.routes:
31 | if "PROXY" == route.method: # Gateway to a plugin.
32 | prefix = route.config["mountpoint.prefix"].rstrip("/")
33 | subapp = route.config["mountpoint.target"]
34 | dynamic = "<:re:" in route.rule
35 | for rule, method, callback in iter_bottle_routes(subapp):
36 | # Show root for static PROXY only.
37 | if dynamic:
38 | if "/" == rule:
39 | continue
40 | elif "/" != rule:
41 | continue
42 | yield prefix + ("" if "/" == rule else rule), method, callback
43 | else:
44 | yield route.rule, route.method, route.callback
45 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/settings.environments.js:
--------------------------------------------------------------------------------
1 | import { Tooltip } from "bootstrap";
2 | import DataTablesLib from "datatables.net-bs5";
3 | import "datatables.net-buttons-bs5";
4 | import DataTable from "datatables.net-vue3";
5 | import { createApp } from "vue";
6 |
7 | import DeleteDialog from "./components/DeleteDialog.vue";
8 | import EnvironmentDialog from "./components/EnvironmentDialog.vue";
9 |
10 | DataTable.use(DataTablesLib);
11 |
12 | createApp({
13 | components: {
14 | deletedialog: DeleteDialog,
15 | environmentdialog: EnvironmentDialog,
16 | },
17 | created() {
18 | this.$nextTick(() => {
19 | new DataTablesLib("#tableEnvironments", {
20 | stateSave: true,
21 | autoWidth: false,
22 | // name, description, actions
23 | columns: [{ width: "32rem" }, { width: "auto" }, { width: "8rem", orderable: false }],
24 | layout: {
25 | topStart: "search",
26 | topEnd: {
27 | buttons: [
28 | {
29 | text: "New environment",
30 | className: "btn btn-sm btn-success",
31 | attr: {
32 | "data-bs-toggle": "modal",
33 | "data-bs-target": "#modalEditEnvironment",
34 | "data-testid": "new",
35 | },
36 | },
37 | ],
38 | },
39 | },
40 | });
41 |
42 | document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => new Tooltip(el));
43 | });
44 | },
45 | }).mount("#vue-app");
46 |
--------------------------------------------------------------------------------
/ui/temboardui/core.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from temboardtoolkit.taskmanager import WorkerSet
4 | from temboardtoolkit.utils import utcnow
5 |
6 | from .agentclient import TemboardAgentClient
7 | from .model import Session, worker_engine
8 | from .model.orm import Instance
9 |
10 | logger = logging.getLogger(__name__)
11 | workers = WorkerSet()
12 |
13 |
14 | @workers.register(pool_size=1)
15 | def refresh_discover(app, address, port):
16 | session = Session(bind=worker_engine(app.config.repository))
17 | instance = Instance.get(address, port).with_session(session).one()
18 | client = TemboardAgentClient.factory(app.config, address, port)
19 | try:
20 | logger.info("Discovering %s.", instance)
21 | response = client.get("/discover")
22 | response.raise_for_status()
23 | except (OSError, ConnectionError, client.Error) as e:
24 | logger.error("Failed to discover %s: %s", instance, e)
25 | logger.error("Agent or host may be down or misconfigured.")
26 | return
27 |
28 | data = response.json()
29 |
30 | discover_etag = response.headers.get("ETag")
31 | if discover_etag == instance.discover_etag:
32 | logger.info("Discover data up to date for %s.", instance)
33 | if session.is_modified(instance):
34 | session.commit()
35 | return
36 |
37 | instance.discover = data
38 | instance.discover_etag = discover_etag
39 | instance.discover_date = utcnow()
40 | session.commit()
41 |
42 | logger.info("Updated discover data for %s.", instance)
43 |
--------------------------------------------------------------------------------
/ui/packaging/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm-slim AS copier
2 | # RUN --mount syntax does not accept arg. Thus first stage copy with arg in
3 | # static path for future RUN --mount.
4 | ARG VERSION
5 | COPY dist/temboard_${VERSION}-0dlb1bookworm1_amd64.deb /tmp/temboard.deb
6 |
7 | FROM debian:bookworm-slim
8 |
9 | ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it
10 | RUN set -ex; \
11 | chmod 0755 /usr/local/bin/wait-for-it; \
12 | :
13 |
14 | RUN set -ex; \
15 | apt-get update -y ; \
16 | mkdir -p /usr/share/man/man1 /usr/share/man/man7 ; \
17 | apt-get install -y --no-install-recommends \
18 | openssl \
19 | postgresql-client \
20 | ssl-cert \
21 | sudo \
22 | ; \
23 | apt-get clean ; \
24 | rm -rf /var/lib/apt/lists/* /usr/share/man/man*/*; \
25 | :
26 |
27 | ADD packaging/docker/entrypoint.sh /usr/local/bin/docker-entrypoint.sh
28 |
29 | ARG VERSION
30 | RUN --mount=type=bind,from=copier,source=/tmp/temboard.deb,target=/tmp/temboard.deb \
31 | set -ex; \
32 | apt-get update -y ; \
33 | mkdir -p /usr/share/man/man1 /usr/share/man/man7 ; \
34 | apt-get install -y --no-install-recommends /tmp/temboard.deb ; \
35 | apt-get clean ; \
36 | rm -rf /var/lib/apt/lists/* /usr/share/man/man*/*; \
37 | temboard --version ; \
38 | :
39 |
40 | VOLUME /var/lib/temboard
41 | VOLUME /etc/temboard
42 |
43 | WORKDIR /var/lib/temboard
44 | ENTRYPOINT ["docker-entrypoint.sh"]
45 | CMD ["temboard"]
46 |
47 | EXPOSE 8888
48 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/settings.users.js:
--------------------------------------------------------------------------------
1 | import DataTablesLib from "datatables.net-bs5";
2 | import "datatables.net-buttons-bs5";
3 | import DataTable from "datatables.net-vue3";
4 | import { createApp } from "vue";
5 |
6 | import DeleteDialog from "./components/DeleteDialog.vue";
7 | import EditUserDialog from "./components/settings/EditUserDialog.vue";
8 |
9 | DataTable.use(DataTablesLib);
10 |
11 | createApp({
12 | components: {
13 | edituserdialog: EditUserDialog,
14 | deletedialog: DeleteDialog,
15 | },
16 | created() {
17 | this.$nextTick(() => {
18 | new DataTablesLib("#tableUsers", {
19 | pageLength: 50,
20 | stateSave: true,
21 | columns: [
22 | { width: "auto" }, // Username
23 | { width: "auto" }, // Email
24 | { width: "12rem" }, // Phone
25 | { width: "6rem" }, // Active
26 | { width: "6rem" }, // Admin
27 | { width: "auto" }, // Environments
28 | { width: "6rem", orderable: false }, // Actions
29 | ],
30 | layout: {
31 | topStart: "search",
32 | topEnd: {
33 | buttons: [
34 | {
35 | text: "New user",
36 | className: "btn btn-sm btn-success",
37 | attr: {
38 | id: "buttonNewUser",
39 | "data-bs-toggle": "modal",
40 | "data-bs-target": "#modalEditUser",
41 | },
42 | },
43 | ],
44 | },
45 | },
46 | });
47 | });
48 | },
49 | }).mount("#vueapp");
50 |
--------------------------------------------------------------------------------
/tests/install-all.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eux
2 | #
3 | # Install UI & agent from packages.
4 | #
5 |
6 | main() {
7 | local disttag pkgtype
8 |
9 | disttag="$(guess_disttag)"
10 | pkgtype="$(guess_pkg_type "$disttag")"
11 |
12 | mapfile -t pkgs < <(readlink -e agent/dist/temboard-agent*"$disttag"*".$pkgtype" ui/dist/temboard*"$disttag"*".$pkgtype")
13 | test "${#pkgs[@]}" -eq 2
14 |
15 | "install_$pkgtype" "${pkgs[@]}"
16 |
17 | temboard --version
18 | temboard-agent --version
19 | }
20 |
21 | guess_disttag() {
22 | . /etc/os-release
23 |
24 | case "$ID" in
25 | centos)
26 | echo "el${VERSION_ID}"
27 | ;;
28 | rocky)
29 | echo "${PLATFORM_ID#platform:}"
30 | ;;
31 | debian)
32 | # Debian testing does not have VERSION* vars.
33 | if ! grep -Po '(.+(?=/))' /etc/debian_version ; then
34 | echo "$VERSION_CODENAME"
35 | fi
36 | ;;
37 | *)
38 | echo "Unsupported distribution $ID." >&2
39 | return 1
40 | ;;
41 | esac
42 | }
43 |
44 | guess_pkg_type() {
45 | local disttag="$1"
46 |
47 | case "$disttag" in
48 | trixie|bookworm|bullseye)
49 | echo deb
50 | ;;
51 | el*)
52 | echo rpm
53 | ;;
54 | *)
55 | echo "Unsupported distribution tag: $disttag." >&2
56 | return 1
57 | ;;
58 | esac
59 | }
60 |
61 |
62 | install_rpm() {
63 | "$(type -p retry)" yum --quiet --assumeyes --disablerepo='pgdg*' --disablerepo='powertools*' --disablerepo='extras' install "$@"
64 | }
65 |
66 |
67 | install_deb() {
68 | apt-get --assume-yes --quiet update
69 | apt install --no-install-recommends --quiet --yes "$@"
70 | }
71 |
72 |
73 | main "$@"
74 |
--------------------------------------------------------------------------------
/ui/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "temboard"
3 | version = "10.0.0"
4 | description = "temBoard User Interface."
5 | readme = "README.md"
6 | requires-python = ">=3.9,<3.14"
7 | license = "PostgreSQL"
8 | authors = [
9 | {name = "Dalibo", email = "contact@dalibo.com"},
10 | ]
11 | classifiers = ["Private :: Do Not Upload"]
12 | dependencies = [
13 | "cryptography",
14 | # There is no hard dependency on psycopg2 to allow using
15 | # psycopg2-binary instead. psycopg2 is not provided by psycopg2-binary
16 | # and there is no way to state an OR dependency in Python. It's up to
17 | # the user or package manager to ensure psycopg2 dependency. See
18 | # documentation.
19 | ]
20 |
21 | [project.urls]
22 | Homepage = "https://labs.dalibo.com/temboard"
23 | Documentation = "https://temboard.readthedocs.io"
24 | Repository = "https://github.com/dalibo/temboard"
25 | Changelog = "https://github.com/dalibo/temboard/blob/master/CHANGELOG.md"
26 |
27 | [project.scripts]
28 | temboard = "temboardui.__main__:main"
29 |
30 | [project.entry-points."temboardui.plugins"]
31 | activity = "temboardui.plugins.activity:ActivityPlugin"
32 | dashboard = "temboardui.plugins.dashboard:DashboardPlugin"
33 | maintenance = "temboardui.plugins.maintenance:MaintenancePlugin"
34 | monitoring = "temboardui.plugins.monitoring:MonitoringPlugin"
35 | pgconf = "temboardui.plugins.pgconf:PGConfPlugin"
36 | statements = "temboardui.plugins.statements:StatementsPlugin"
37 |
38 | [build-system]
39 | requires = ["uv_build>=0.9.10,<0.10"]
40 | build-backend = "uv_build"
41 |
42 | [tool.uv.build-backend]
43 | module-name = "temboardui"
44 | module-root = ""
45 |
--------------------------------------------------------------------------------
/ui/temboardui/web/routes/home.py:
--------------------------------------------------------------------------------
1 | from flask import current_app as app
2 | from flask import g, render_template, request
3 |
4 | from ...model import orm
5 | from ...version import inspect_versions
6 |
7 |
8 | @app.route("/home")
9 | def home():
10 | role = g.current_user
11 | environments = [
12 | e.name for e in role.select_environments().with_session(g.db_session).all()
13 | ]
14 | return render_template("home.html", environments=environments)
15 |
16 |
17 | @app.route("/about")
18 | def about():
19 | versions_info = inspect_versions()
20 | instances = g.db_session.scalar(orm.Instance.count())
21 | roles = g.db_session.scalar(orm.Role.count())
22 | infos = {
23 | "Browser": request.headers.get("User-Agent", "Unknown"),
24 | "Version": "%(temboard)s (%(temboardbin)s)" % versions_info,
25 | "Uptime": app.start_time,
26 | "OS": "%(distname)s %(distversion)s" % versions_info,
27 | "Python": "%(python)s (%(pythonbin)s)" % versions_info,
28 | "cryptography": versions_info["cryptography"],
29 | "Tornado": versions_info["tornado"],
30 | "libpq": versions_info["libpq"],
31 | "psycopg2": versions_info["psycopg2"],
32 | "SQLAlchemy": versions_info["sqlalchemy"],
33 | "Instances": instances,
34 | "Users": roles,
35 | "SMTP": app.temboard.config.notifications.smtp_host is not None,
36 | "Twilio": app.temboard.config.notifications.twilio_account_sid is not None,
37 | }
38 | temboard_version = versions_info["temboard"]
39 |
40 | return render_template("about.html", infos=infos, temboard_version=temboard_version)
41 |
--------------------------------------------------------------------------------
/ui/share/purge.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash -eu
2 | #
3 | # purge.sh purges all temboard files and configuration, except database.
4 |
5 | ETCDIR=${ETCDIR-/etc/temboard}
6 | VARDIR=${VARDIR-/var/lib/temboard}
7 | LOGDIR=${LOGDIR-/var/log/temboard}
8 | LOGFILE=${LOGFILE-/var/log/temboard-purge.log}
9 | SYSUSER=${SYSUSER-temboard}
10 |
11 | catchall() {
12 | # shellcheck disable=SC2181
13 | if [ $? -gt 0 ] ; then
14 | fatal "Failure. See ${LOGFILE} for details."
15 | else
16 | rm -f "${LOGFILE}"
17 | fi
18 | trap - INT EXIT TERM
19 | }
20 |
21 | fatal() {
22 | echo -e "\\e[1;31m$*\\e[0m" | tee -a /dev/fd/3 >&2
23 | exit 1
24 | }
25 |
26 | log() {
27 | echo "$@" | tee -a /dev/fd/3 >&2
28 | }
29 |
30 | if [ -n "${DEBUG-}" ] ; then
31 | exec 3>/dev/null
32 | else
33 | exec 3>&2 2>"${LOGFILE}" 1>&2
34 | chmod 0600 "${LOGFILE}"
35 | trap 'catchall' INT EXIT TERM
36 | fi
37 |
38 | # Now, log everything.
39 | set -x
40 |
41 | if systemctl is-system-running && systemctl cat temboard &>/dev/null ; then
42 | systemctl disable --now temboard
43 | systemctl reset-failed temboard || true
44 | fi
45 |
46 | if getent passwd "$SYSUSER" && [ "$(whoami)" != "$SYSUSER" ]; then
47 | userdel "$SYSUSER"
48 | fi
49 |
50 | if [ -d "${PGHOST-/var/run/postgresql/}" ] ; then
51 | # If local, sudo to PGUSER.
52 | run_as_postgres=(sudo -nEHu "${PGUSER-postgres}")
53 | else
54 | run_as_postgres=(env)
55 | fi
56 |
57 | "${run_as_postgres[@]}" dropdb --if-exists "${TEMBOARD_DATABASE-temboard}"
58 | "${run_as_postgres[@]}" dropuser --if-exists temboard || :
59 |
60 | rm -rf "${ETCDIR}" "${VARDIR}" "${LOGDIR}" "/etc/pki/tls/*/temboard-auto.*"
61 |
62 | log "temBoard UI unconfigured."
63 |
--------------------------------------------------------------------------------
/ui/temboardui/model/queries/instances-copy-as-csv.sql:
--------------------------------------------------------------------------------
1 | COPY (
2 | WITH
3 | inventory AS (SELECT DISTINCT
4 | i.hostname AS "Hostname",
5 | port AS "Port",
6 | discover->'postgres'->'data_directory'#>>'{}' AS "PGDATA",
7 | discover->'postgres'->'version_summary'#>>'{}' AS "Version",
8 | e.name AS "Environment",
9 | app.agent_address AS "Agent Address",
10 | app.agent_port AS "Agent Port",
11 | string_agg(DISTINCT plugins.plugin_name, ',') AS "Plugins",
12 | app.comment AS "Comment",
13 | string_agg(DISTINCT db.dbname, ',') AS "Databases"
14 | FROM monitoring.hosts AS i
15 | LEFT OUTER JOIN monitoring.instances AS monit
16 | ON monit.host_id = i.host_id
17 | LEFT OUTER JOIN application.instances AS app
18 | ON app.hostname = i.hostname
19 | LEFT OUTER JOIN application.plugins
20 | ON plugins.agent_address = app.agent_address
21 | AND plugins.agent_port = app.agent_port
22 | LEFT OUTER JOIN application.environments AS e
23 | ON e.id = app.environment_id
24 | LEFT OUTER JOIN monitoring.metric_db_size_current AS db
25 | ON db.instance_id = monit.instance_id
26 | AND db.dbname NOT IN ('postgres', 'template1')
27 | AND datetime > (now() - interval '5 minutes')
28 | GROUP BY 1, 2, 3, 4, 5, 6, 7
29 | ORDER BY 5, 1, 2
30 | )
31 | SELECT * FROM inventory
32 | WHERE concat_ws(
33 | ' ',
34 | "Hostname", "Port", "PGDATA", "Version", "Environment",
35 | "Agent Address", "Agent Port"
36 | ) LIKE %s
37 | ) TO STDOUT WITH (
38 | DELIMITER ';',
39 | ENCODING 'UTF-8',
40 | FORCE_QUOTE *,
41 | FORMAT CSV,
42 | HEADER
43 | ) ;
44 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/generate_key.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os.path
3 | from subprocess import check_call
4 |
5 | from cryptography.hazmat.primitives import serialization
6 | from temboardtoolkit.app import SubCommand
7 | from temboardtoolkit.errors import UserError
8 | from temboardtoolkit.signing import load_private_key
9 |
10 | from .app import app
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | @app.command
16 | class GenerateKey(SubCommand):
17 | """Generate signing key."""
18 |
19 | name = "generate-key"
20 |
21 | def define_arguments(self, parser):
22 | parser.add_argument(
23 | "--force",
24 | action="store_true",
25 | default=False,
26 | help="Force overwriting existing files.",
27 | )
28 |
29 | def main(self, args):
30 | priv = self.app.config.temboard.signing_private_key
31 | if os.path.exists(priv) and not args.force:
32 | raise UserError("%s exists. Use --force to overwrite." % priv)
33 | logger.info("Generating RSA key with openssl at %s.", priv)
34 | check_call(["openssl", "genrsa", "-out", priv, "4096"])
35 |
36 | with open(priv, "rb") as fo:
37 | privkey = load_private_key(fo.read())
38 |
39 | pub = self.app.config.temboard.signing_public_key
40 | logger.info("Exporting public key at %s.", pub)
41 | pem = privkey.public_key().public_bytes(
42 | encoding=serialization.Encoding.PEM,
43 | format=serialization.PublicFormat.SubjectPublicKeyInfo,
44 | )
45 | with open(pub, "wb") as fo:
46 | fo.write(pem)
47 |
48 | return 0
49 |
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | ---
2 | hide:
3 | - navigation
4 | ---
5 |
6 | Quickstart
7 |
8 | We provide a `docker-compose.yml` file to give temBoard a try.
9 |
10 | You'll need docker compose 1.10+ and docker engine 1.10+.
11 |
12 | ``` console
13 | wget https://raw.githubusercontent.com/dalibo/temboard/master/docker/docker-compose.yml
14 | docker compose up
15 | ```
16 |
17 | `docker compose` will launch:
18 |
19 | - a PostgreSQL instance for temBoard own data
20 | - the temBoard UI
21 | - 4 PostgreSQL instances (with different versions), exposed on ports 5432, 5433, 5434 and 5435,
22 | - a temBoard agent for each instance.
23 |
24 | temBoard UI is available on with `admin` / `admin`
25 | credentials.
26 |
27 | You can access the PostgreSQL instances. For example to run some pgbench tasks:
28 |
29 | ``` console
30 | $ export PGHOST=0.0.0.0 PGPORT=5432 PGUSER=postgres PGPASSWORD=postgres
31 | $ psql -c 'CREATE EXTENSION IF NOT EXISTS pg_stat_statements'
32 | $ createdb pgbench
33 | $ pgbench -i pgbench
34 | $ pgbench -c 8 -T 60 pgbench
35 | ```
36 |
37 | !!! danger
38 |
39 | **DO NOT USE THIS IN PRODUCTION !**
40 |
41 | temBoard docker images are designed for *testing* and *demo*.
42 |
43 | - The SSL certificate is *self-signed*.
44 | - Default passwords are dumb and public.
45 | - temBoard agent is designed to run on same host as PostgreSQL which is incompatible with Docker service-minded architecture.
46 | - temBoard agent image requires *access to docker socket* to restart PostgreSQL, which you do not want in production.
47 |
48 | **To deploy temBoard in a production environment, follow [installation documentation](server_install.md).**
49 |
--------------------------------------------------------------------------------
/toolkit/temboardtoolkit/versions.py:
--------------------------------------------------------------------------------
1 | # Various functions to expose components version
2 |
3 | import ctypes
4 | import re
5 |
6 |
7 | def read_distinfo(): # pragma: nocover
8 | with open("/etc/os-release") as fo:
9 | distinfos = parse_lsb_release(fo)
10 | return distinfos
11 |
12 |
13 | def parse_lsb_release(lines):
14 | _assignement_re = re.compile(r"""(?P[A-Z_]+)="(?P[^"]+)"$""")
15 | infos = dict()
16 | for line in lines:
17 | m = _assignement_re.match(line)
18 | if not m:
19 | continue
20 | infos[m.group("variable")] = m.group("value")
21 | return infos
22 |
23 |
24 | def load_libpq():
25 | __import__("psycopg2")
26 |
27 | # Search for libpq.so path in loaded libraries.
28 | with open("/proc/self/maps") as fo:
29 | for line in fo:
30 | values = line.split()
31 | path = values[-1]
32 | if "/libpq" in path:
33 | break
34 | else: # pragma: nocover
35 | raise Exception("libpq.so not loaded")
36 |
37 | return ctypes.cdll.LoadLibrary(path)
38 |
39 |
40 | def read_libpq_version():
41 | # Search libpq version bound to this process.
42 |
43 | try:
44 | # For psycopg2 2.7+
45 | from psycopg2.extensions import libpq_version
46 |
47 | return libpq_version()
48 | except ImportError:
49 | libpq = load_libpq()
50 | return libpq.PQlibVersion()
51 |
52 |
53 | def format_pq_version(version):
54 | pqnums = [version / 10000, version % 100]
55 | if version <= 100000:
56 | pqnums[1:1] = [(version % 10000) / 100]
57 | return ".".join(str(int(n)) for n in pqnums)
58 |
--------------------------------------------------------------------------------
/toolkit/tests/test_utils.py:
--------------------------------------------------------------------------------
1 | def test_dict_factory():
2 | from temboardtoolkit.utils import dict_factory
3 |
4 | my = dict_factory()
5 | assert isinstance(my, dict)
6 | assert 0 == len(my)
7 |
8 | my = dict_factory(dict(a=True))
9 | assert isinstance(my, dict)
10 | assert 1 == len(my)
11 | assert my["a"] is True
12 |
13 | my = dict_factory([("a", True)])
14 | assert isinstance(my, dict)
15 | assert 1 == len(my)
16 | assert my["a"] is True
17 |
18 | original = dict(a=True)
19 | my = dict_factory(original)
20 | original["a"] = False
21 | assert my["a"] is False
22 |
23 |
24 | def test_dotdict():
25 | from temboardtoolkit.utils import DotDict
26 |
27 | my = DotDict(dict(a=1, b=dict(c=2)))
28 |
29 | assert 1 == my.a
30 | assert 1 == my["a"]
31 | assert 2 == my.b.c
32 | assert 2 == my["b"].c
33 |
34 | keys = list(iter(my))
35 | assert "a" in keys
36 | assert "b" in keys
37 |
38 | my.a = 3
39 | my.b.c = 4
40 |
41 | assert 3 == my.a
42 | assert 4 == my["b"]["c"]
43 |
44 | my["a"] = 5
45 | assert 5 == my.a
46 |
47 | d = my.setdefault("d", dict(e=True))
48 | assert d.e is True
49 |
50 |
51 | def test_pickle_dotdict():
52 | from pickle import dumps as pickle
53 | from pickle import loads as unpickle
54 |
55 | from temboardtoolkit.utils import DotDict
56 |
57 | orig = DotDict(dict(a=1, b=dict(c=2)))
58 | copy = unpickle(pickle(orig))
59 | assert 2 == copy.b.c
60 |
61 |
62 | def test_ensure_str():
63 | from temboardtoolkit.taskmanager import ensure_str
64 |
65 | assert type(ensure_str("toto")) is str
66 | assert type(ensure_str(b"toto")) is str
67 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/components/Error.vue:
--------------------------------------------------------------------------------
1 |
43 |
44 |
45 |
46 |
47 | Error {{ code }}
48 |
49 |
50 |
51 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/agent/packaging/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM debian:bookworm-slim AS copier
2 | # RUN --mount syntax does not accept arg. Thus first stage copy with arg in
3 | # static path for future RUN --mount.
4 | # Ensure package is not ignored by .dockerignore.
5 | ARG VERSION
6 | COPY dist/temboard-agent_${VERSION}-0dlb1bookworm1_all.deb /tmp/temboard-agent.deb
7 |
8 | FROM debian:bookworm-slim
9 |
10 | ADD https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh /usr/local/bin/wait-for-it
11 | RUN set -ex; \
12 | chmod 0755 /usr/local/bin/wait-for-it; \
13 | apt-get update -y ; \
14 | mkdir -p /usr/share/man/man1 /usr/share/man/man7 ; \
15 | apt-get install -y --no-install-recommends \
16 | iproute2 \
17 | openssl \
18 | postgresql-client \
19 | ssl-cert \
20 | sudo \
21 | ; \
22 | apt-get clean ; \
23 | rm -rf /var/lib/apt/lists/* /usr/share/man/man*/*.7*; \
24 | :
25 |
26 | ADD packaging/docker/sudoers /etc/sudoers.d/temboard-agent
27 | ADD packaging/docker/entrypoint.sh /usr/local/lib/docker-entrypoint.sh
28 |
29 | RUN --mount=type=bind,from=copier,source=/tmp/temboard-agent.deb,target=/tmp/temboard-agent.deb \
30 | set -ex; \
31 | apt-get update -y ; \
32 | mkdir -p /usr/share/man/man1 /usr/share/man/man7 ; \
33 | apt-get install -y --no-install-recommends /tmp/temboard-agent.deb tini ; \
34 | apt-get clean ; \
35 | rm -rf /var/lib/apt/lists/* /usr/share/man/man*/*.7*; \
36 | temboard-agent --version ; \
37 | :
38 |
39 | VOLUME /etc/temboard-agent
40 | VOLUME /var/lib/temboard-agent
41 | WORKDIR /var/lib/temboard-agent
42 | ENTRYPOINT ["/usr/bin/tini", "/usr/local/lib/docker-entrypoint.sh"]
43 | CMD ["temboard-agent"]
44 |
--------------------------------------------------------------------------------
/ui/temboardui/templates/flask/maintenance/breadcrumb.html:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/ui/tests/unit/test_model.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 |
4 | @pytest.fixture
5 | def engine(mocker):
6 | engine = mocker.Mock(name="engine")
7 | conn = mocker.MagicMock(name="conn")
8 | engine.pool._invoke_creator.return_value = conn
9 | engine.conn = conn
10 | cur = conn.cursor.return_value.__enter__.return_value
11 | cur.fetchone.return_value = ("PostgreSQL 15 Debian",)
12 | engine.cur = cur
13 | return engine
14 |
15 |
16 | def test_check_connectivity_ok(engine, mocker):
17 | sleep = mocker.patch("temboardui.model.sleep")
18 | sleep.side_effect = Exception("Must not sleep")
19 | from temboardui.model import check_connectivity
20 |
21 | check_connectivity(engine)
22 |
23 | assert engine.conn.close.called is True
24 |
25 |
26 | def test_check_connectivity_sleep(engine, mocker):
27 | sleep = mocker.patch("temboardui.model.sleep")
28 | from temboardui.model import check_connectivity
29 |
30 | engine.pool._invoke_creator.side_effect = [Exception(), engine.conn]
31 |
32 | check_connectivity(engine)
33 |
34 | assert sleep.called is True
35 |
36 |
37 | def test_check_connectivity_fail(engine, mocker):
38 | sleep = mocker.patch("temboardui.model.sleep")
39 | from temboardui.model import check_connectivity
40 |
41 | engine.pool._invoke_creator.side_effect = Exception()
42 |
43 | with pytest.raises(Exception):
44 | check_connectivity(engine)
45 |
46 | assert sleep.called is True
47 |
48 |
49 | def test_configure(mocker):
50 | mod = "temboardui.model"
51 | Session = mocker.patch(mod + ".Session")
52 |
53 | from temboardui.model import configure
54 |
55 | configure(dsn="sqlite://") # LOL
56 | assert Session.configure.called is True
57 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | # This is a virtual project to manage a single development env
2 | #
3 | # Never build nor release temboard-workspace project.
4 | #
5 |
6 | [project]
7 | requires-python = ">=3.9,<3.14"
8 | name = "temboard-workspace"
9 | version = "0.0.0" # never change this.
10 | classifiers = ["Private :: Do Not Upload"]
11 |
12 | [dependency-groups]
13 | dev = [
14 | "hupper",
15 | "pep440deb",
16 | "pytest",
17 | "psycopg2-binary",
18 | "pytz>=2025.2", # for dev/perfui/
19 | # Installed as editable according to uv settings below.
20 | "temboard",
21 | "temboard-agent",
22 | "temboard-toolkit",
23 | "wheel>=0.45.1",
24 | ]
25 | docs = [
26 | "mkdocs-git-revision-date-localized-plugin",
27 | "mkdocs-glightbox",
28 | "mkdocs-material",
29 | ]
30 | lint = [
31 | "docutils",
32 | "ruff",
33 | ]
34 | e2e = [
35 | "httpx",
36 | "pytest",
37 | "pytest-mock",
38 | "pytest-tornado",
39 | "selenium",
40 | "sh<2",
41 | "tenacity<8.4",
42 | ]
43 |
44 | [tool.uv]
45 | default-groups = "all"
46 |
47 | [tool.uv.workspace]
48 | members = ["agent", "ui", "toolkit"]
49 |
50 | [tool.uv.sources]
51 | temboard = { workspace = true }
52 | temboard-agent = { workspace = true }
53 | temboard-toolkit = { workspace = true }
54 |
55 | [tool.ruff]
56 | target-version = "py39"
57 | force-exclude = true
58 | extend-exclude = ["_vendor"]
59 |
60 | [tool.ruff.lint]
61 | extend-select = [
62 | "I", # Sort imports
63 | ]
64 | ignore = [
65 | "E721", # Do not compare types, use `isinstance()`
66 | ]
67 |
68 | [tool.ruff.lint.isort]
69 | split-on-trailing-comma = false
70 |
71 | [tool.ruff.format]
72 | skip-magic-trailing-comma = true
73 | line-ending = "lf"
74 | docstring-code-format = true
75 |
--------------------------------------------------------------------------------
/dev/lnav/formats/postgresql_log.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://lnav.org/schemas/format-v1.schema.json",
3 | "postgresql_log": {
4 | "description": "Format file generated from regex101 entry -- https://regex101.com/r/2ofpAY",
5 | "regex": {
6 | "std": {
7 | "pattern": "(?.+) \\[(?\\d+)\\]:(?: \\[(?\\d+)-\\d+\\])? (?:(?:app=(?[^,]*)|db=(?[^,]*)|client=(?[^,]*)|user=(?[^, ]*)),?)+ (?[A-Z]+): (?.+)"
8 | }
9 | },
10 | "value": {
11 | "level": {
12 | "kind": "string",
13 | "identifier": true
14 | },
15 | "user": {
16 | "kind": "string",
17 | "identifier": true
18 | },
19 | "client": {
20 | "kind": "string",
21 | "collate": "ipaddress"
22 | },
23 | "app": {
24 | "kind": "string"
25 | },
26 | "db": {
27 | "kind": "string",
28 | "identifier": true
29 | },
30 | "pid": {
31 | "kind": "string",
32 | "identifier": true
33 | },
34 | "line": {
35 | "kind": "integer"
36 | }
37 | },
38 | "sample": [
39 | {
40 | "line": "2024-06-25 14:54:20 UTC [551]: user=,db=,app=,client= LOG: starting PostgreSQL 13.15 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-20), 64-bit\n2024-06-25 14:54:20 UTC [551]: user=,db=,app=,client= LOG: listening on IPv4 address \"127.0.0.1\", port 55432"
41 | }
42 | ]
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/migratedb.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from contextlib import closing
3 |
4 | from psycopg2 import connect
5 | from temboardtoolkit.app import SubCommand
6 | from temboardtoolkit.errors import UserError
7 |
8 | from ..model import format_dsn
9 | from ..model.migrator import Migrator
10 | from .app import app
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | @app.command
16 | class MigrateDB(SubCommand):
17 | """Manage temBoard own database."""
18 |
19 | def main(self, args):
20 | raise UserError("Missing sub-command. See --help for details.")
21 |
22 | def init_migrator(self):
23 | migrator = Migrator()
24 | migrator.inspect_available_versions()
25 | return migrator
26 |
27 | def make_conn(self):
28 | return closing(connect(format_dsn(self.app.config.repository)))
29 |
30 |
31 | @MigrateDB.command
32 | class Check(SubCommand):
33 | """Check schema synchronisation status only."""
34 |
35 | def main(self, args):
36 | migrator = self.parent.init_migrator()
37 |
38 | with self.parent.make_conn() as conn:
39 | migrator.inspect_current_version(conn)
40 | migrator.check()
41 |
42 |
43 | @MigrateDB.command
44 | class Upgrade(SubCommand):
45 | """Upgrade temBoard database to latest revision."""
46 |
47 | def main(self, args):
48 | migrator = self.parent.init_migrator()
49 |
50 | with self.parent.make_conn() as conn:
51 | migrator.inspect_current_version(conn)
52 | for version in migrator.missing_versions:
53 | logger.info("Upgrading database to version %s.", version)
54 | migrator.apply(conn, version)
55 |
56 | logger.info("Database up to date.")
57 |
--------------------------------------------------------------------------------
/ui/temboardui/cli/routes.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from itertools import chain
3 |
4 | from flask import current_app
5 | from temboardtoolkit.app import SubCommand
6 |
7 | from .app import app
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | @app.command
13 | class Routes(SubCommand):
14 | """List HTTP routes map."""
15 |
16 | def define_arguments(self, parser):
17 | parser.add_argument(
18 | "--sort",
19 | action="store_true",
20 | default=False,
21 | help="Sort routes alphabetically",
22 | )
23 |
24 | def main(self, args):
25 | rules = self.app.tornado_app.wildcard_router.rules
26 | routes = chain(iter_tornado_routes(rules), iter_flask_routes())
27 |
28 | if args.sort:
29 | logger.debug("Sorting routes alphabetically.")
30 | routes = sorted(routes, key=lambda x: (x[1], x[0]))
31 | else:
32 | logger.debug("Listing routes by matching order.")
33 |
34 | for method, path in routes:
35 | print(" %6.6s %-64s" % (method, path))
36 |
37 | return 0
38 |
39 |
40 | def iter_flask_routes():
41 | for rule in current_app.url_map.iter_rules():
42 | for method in rule.methods:
43 | if method in ("OPTIONS", "HEAD"):
44 | continue
45 | yield method, rule.rule
46 |
47 |
48 | def iter_tornado_routes(rules):
49 | for rule in rules:
50 | if "fallback" in rule.target_kwargs:
51 | # Skip fallback to Flask routes.
52 | continue
53 |
54 | # Fallback for static handlers
55 | methods = rule.target_kwargs.get("methods", ["GET"])
56 | path = rule.matcher.regex.pattern
57 | for method in methods:
58 | yield method, path
59 |
--------------------------------------------------------------------------------
/ui/share/temboard_CHANGEME.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCwlh/dHwY6u0XT
3 | CfpzEufg7GcKVxwrO9VXbnNAhH5AzRJ4gWSXGzvPFzltPZOxWavZ0aceGquC55Av
4 | 9QV/Ci9lSmSS1mdRo2boSXZdJIhAn5fSbqLwro0qftiOCqSPfq0zIw16sZxeN8D3
5 | 5B9eOeRMhe3LX0api3OyVgVlFZcN+gouFB+qNF0gw+zF5fOtE5h8C5PV3EFQt8sf
6 | 6hrZFYv+BpW34vv6V3Tqv9rp0UYK4d5ttM0Vqqp2YzCcwEKpiSuA8+GzKwkXTsQ0
7 | YEh4XNhfniA62kb5hrlrJa9CxAtKUaWFiQK02oVzFPmO1a9cdUNKRpDiDwX7ZIkC
8 | lR5A2ocdAgMBAAECggEATD1UnnxRjTPjjp0FQ3+LG2IVjrJTWBsqHehy3A0YEVQ4
9 | wExlKJQ6e0u0oIRwaqajepR4yZeMKyVc7EemStYT5nB7AaaNzwJ0YJ+u+cHXDceZ
10 | neHkeXNzQUCosJLJv6ZakvR0Ul+yej8qEhysqlrmRV+hbifBc1vg5MMc6yqqH/73
11 | nU17a6VCZwNFfaWfv34x/SEZIlGr1NzfIrqs/42KgriifTp5dKR0C47d94+ykpit
12 | PIgPmYtXDvVdzkuxWlRsXhGjBw6/bvDW7yQaQb5V3GnDr4vZQK5+5MtcGDD1zL7y
13 | ztdqx42OmHsCv6loZbfdneRdkBHcWgnwKKRKCgJ0xQKBgQDYRsnYcPWmDG/5GUQZ
14 | Q7s2U0Fk0MHvdDv8W041xU3TW6yCw+vvWDpKALTrhCkCwWLBRPyrrPMImOt/G5Bs
15 | VnlYnop4IoMz91+7MSj0TJKAjlOcTql41EZ+SUwynEIElgJO5RMNKKiTAHZtiRfH
16 | VldVpgws3QbQnHBCuBdghRlBawKBgQDRBSDHmRIAdiTPRI6yKsFCEucrak0ezRsC
17 | FIcNQVtUyIEFkqsNi8bB6I9JaovFXGBhhTWjAB6K5AXuithZyPG1Dy3Oa7LNgB/p
18 | PzEgrINsSQwx7AgxVC7IOdUQTRvVmNNmdj4xj2m5SPoiEdiT8/u3bk4QojYHylPl
19 | bD5Ucc4TlwKBgHOZ8s3EIylsQHWvMZ0nfOveuHeAtgid4mpTC4zmN2EgASesPXIj
20 | gJSJqCqy65DTeUvA6iWBPi58PnQkcZ/W4OmjZHQuTl76fKr77d4XB7+7U6mayi8R
21 | l9RsrVPn/cmhvP4ap4JDF0fr2WFXA+TCm8/l/2ADjF75H3AqIiSvP/6dAoGAPJdV
22 | 90ZiN4wIA6WGEBPgzfyY0rcQABvI9oNo2ujVRvCQpkLsHxMj3NZDoy6lseKjdeGd
23 | uNCyCeUr6wiIyw47MxdhWfNSc8vudDkDTstzlZJfXKFlhpc2sIhDQWR46yRQM+WX
24 | Bdri9Pk4uWOe+tTBZV0ueBftvbhjNaB5ORV8faUCgYEAtYpg3CFTAJmTuZB34BPv
25 | qjk/f3Uc4YuAFmxhrvYrGpJuIRyTHm8VKmDMa2A+sbXHZQgVgKH/bNcTukytFAeU
26 | nzwlIYGmbD9/daqOoybz2SRxB+oBFJxXRcYy8cwOW5HkTZKbkczg07oG18vTJFmW
27 | x6yBaegnkVr9QAb/2o1Of14=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/docs/temboard-howto-maintenance.md:
--------------------------------------------------------------------------------
1 | *maintenance* feature gives user an overview on the
2 | **databases**, **schemas**, **tables** or **indexes** respective **size**.
3 |
4 | It's very useful to get information about **bloat** and **toast**.
5 | This can help users determine potential issues and understand or prevent performance issues due to
6 | unaccordingly used space.
7 |
8 | The *maintenance* feature also provides easy access to administrative actions such as **VACUUM**,
9 | **ANALYZE** or **REINDEX** in order to fix space or performances problems.
10 |
11 |
12 | !!! note
13 |
14 | Please beware that the values for bloat, toast, etc… are estimated.
15 | They may not perfectly reflect the reality especially if there hasn't been
16 | any analyze performed recently.
17 |
18 |
19 | ## Views
20 |
21 | The maintenance plugin lets you navigate through databases, schemas and tables
22 | of the instance.
23 |
24 | 
25 |
26 | 
27 |
28 | The page for one table shows more detailed information.
29 |
30 | 
31 |
32 | ## Hints
33 |
34 | Once in a while temBoard may be able to provide some hints on actions to
35 | perform on tables or indexes.
36 |
37 | 
38 |
39 |
40 | ## Actions
41 |
42 | If you're logged onto the agent, you may then be able to perform actions by
43 | clicking on the dedicated buttons.
44 |
45 | temBoard can let you launch different actions such as **ANALYZE**, **VACUUM** or
46 | **REINDEX**
47 |
48 | You can also choose either to launch the action immediately or in the future
49 | at the date and time you decide.
50 |
51 | 
52 |
--------------------------------------------------------------------------------
/agent/share/temboard-agent_CHANGEME.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDQfCQjxzPsqcF/
3 | 7ZquS2o7dO9pWLz7fTuskaSR+Z4NlieO5a8GJBIXs33u2Ew8toHtbYnafOsgkIpN
4 | O2dqCpU6f9Ms5uhLEMDMxZomH2N9eo7CBfV7iSGPDuBd6Y5BH+0I9VfTR8iisq1f
5 | S+pLWgEOVulb9jHG7aAj8vCV37vzv3nuLLm5mryQiprX205b7QqnzbzydBuqcKvN
6 | vfI4VLntPpR1Eeq3BjXa+5lX+iuPAvpOhVHMlca302qprp1Ti2hcIRqfcBw5BDyh
7 | iZFo1pbQfNkj6Ncmzxj4ZL4rUW0WJsDZ5hRGqXYOmIdyyD6XpQpDAzfjSdUYktsw
8 | XplslauvAgMBAAECggEAcKLRJ59E11SzXPkyu321DCBWBFVT7I8xQT+CaPcyQ+be
9 | wl4m3Ct6tuFbZUHolliIA41NkAQzR+mvPqCcc3b6Ppu2rKin0X5gm4EakgQdTTp8
10 | jCiKAs8ImXCRGUvIdjjYaCGc7GH47KWZ80VhdIpQzb144B03sWDKRwHGp0I0GjBb
11 | khdCqB6QsPJhxbh07M8qoAaq1TZDisa9EFznQ1ay0jy0PuZyry01M5dukM/suq+r
12 | US02vUpoFp3LpSjh+ydyrhgQH1o4SWLRiKgtHwTFuhGaMwqCQDXh+Td9hq2q7EXk
13 | Ixg+EWCJAt60TBy3Kx/9zDgC9uvY1WZmPOeGMCZIoQKBgQD08FEzMJzmktl3Vkge
14 | iFvhwCRD+FOcVipYCji7StwDjTIAha+O9Y1fVPeOyVXSsCyEo8R7ulCE/nQK2ARf
15 | 7RQq0gOyREjGgGeKLOs1jVwCrOSl0iP1NpF7fUqArzeanjFRTvJDBXHBePittF/E
16 | k0a3+o4R1aN+SxtMHDkBOk8VUQKBgQDZ5mO1VWF2WnKg2wSpCB9s6wlRNjdtGCVy
17 | A6NaT33jWPlkg9gkOv+G3M15RYimyxFFJ0Z64V1F0s8sUYySwoSH3LUYie49kV98
18 | mfyE+WBiracqQVvNyFueg5oUUJLYIVlyvvEtXPZ4voypDo/mozyV+IHhNS+XhpDf
19 | 3eou57xw/wKBgQDbd+Ep/ur3ZqlYVoU0ZnX9p05XYNB6CnLShAYlO4Q74m3lLeQK
20 | MldEDjvrQteVeqnJB3xsaJrxL5YGiVwSH7msTJVnS+vxgOhFVM5EI69H7mbJdasm
21 | coiUn8T73QPzlL8X3acRCnXNJ3mbGz2cQ2JgQy69KDHgXafN4JPrV7W4oQKBgDyT
22 | QsbHXJfVXyZ+nJYNDwdtc6KjCteGLeq7Pi8+CAYq1vHtgSnZSO4J9gkvnmSX8U4j
23 | NAG3IwHlL/jnFsg50TQf1CxlM9jj0ALIoB2rYfMsyVsC3m2ftHClrzDUkW4KH165
24 | 3Dw7Kr24Y0wgIzr/yDj848Zizb83BpFllNPDUmyrAoGAO6PSTycIUnXvyB6h7bkx
25 | rkKQbWvIfZCq1CjBPXwHw3GeF988ymd3whGcRCyI+6EXDbj8LRO6m6J6xHTZqNrX
26 | QkTgVo8ERr++lVa7+z0A66AfzhhEAGGKiJit0BbUAXV68c88huNhzhY+6g9A1NEv
27 | k1WXeEYMCZJMKVUgIzsyR/I=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/ui/temboardui/static/src/components/home/Sparkline.vue:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
68 |
69 |
--------------------------------------------------------------------------------
/agent/temboardagent/queries/activity-locks.sql:
--------------------------------------------------------------------------------
1 | WITH blockages AS (
2 | SELECT
3 | pid AS waiting_pid,
4 | -- Requires pg_blocking_pids from PostgreSQL 9.6+
5 | unnest(pg_blocking_pids(pid)) AS blocking_pid
6 | FROM pg_catalog.pg_stat_activity
7 | )
8 | SELECT
9 | lock.pid,
10 | lock.locktype,
11 | dat.datname AS "database",
12 | nsp.nspname AS "schema",
13 | rel.relname AS "relation",
14 | lock.mode,
15 | lock."granted",
16 | waiting.blocking_pid AS blocking_pid,
17 | min(lock."waitstart"), -- pragma:pg_version_min 140000
18 | array_remove(array_agg(blocking.waiting_pid), NULL) AS waiting_pids
19 | FROM pg_catalog.pg_locks AS lock
20 | LEFT OUTER JOIN pg_catalog.pg_database AS dat ON dat.oid = lock.database
21 | LEFT OUTER JOIN pg_catalog.pg_class AS rel ON rel.oid = lock.relation
22 | LEFT OUTER JOIN pg_catalog.pg_namespace AS nsp ON nsp.oid = rel.relnamespace
23 | LEFT OUTER JOIN blockages AS waiting ON waiting.waiting_pid = pid
24 | LEFT OUTER JOIN blockages AS blocking ON blocking.blocking_pid = pid
25 | LEFT OUTER JOIN pg_catalog.pg_locks AS waiting_lock
26 | ON lock."granted" AND waiting_lock.pid = blocking.waiting_pid
27 | AND waiting_lock.locktype = lock.locktype
28 | AND CASE lock.locktype
29 | WHEN 'advisory' THEN waiting_lock.objid = lock.objid AND waiting_lock.objsubid = lock.objsubid
30 | WHEN 'relation' THEN waiting_lock.database = lock.database AND waiting_lock.relation = lock.relation
31 | WHEN 'transactionid' THEN waiting_lock.transactionid = lock.transactionid
32 | WHEN 'virtualxid' THEN waiting_lock.virtualxid = lock.virtualxid
33 | END
34 | WHERE NOT lock."granted"
35 | OR (blocking.blocking_pid IS NOT NULL AND waiting_lock.pid IS NOT NULL AND "lock"."mode" LIKE '%Exclusive%')
36 | GROUP BY 1, 2, 3, 4, 5, 6, 7, 8
37 | ORDER BY min("lock"."waitstart") ASC -- pragma:pg_version_min 140000
38 |
--------------------------------------------------------------------------------
/agent/temboardagent/cli/fetch_key.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import os.path
3 | from urllib.parse import urlparse
4 |
5 | from temboardtoolkit.app import SubCommand
6 | from temboardtoolkit.errors import UserError
7 | from temboardtoolkit.http import TemboardClient
8 | from temboardtoolkit.signing import load_public_key
9 |
10 | from .app import app
11 |
12 | logger = logging.getLogger(__name__)
13 |
14 |
15 | @app.command
16 | class FetchKey(SubCommand):
17 | """Fetch signing public key."""
18 |
19 | name = "fetch-key"
20 |
21 | def define_arguments(self, parser):
22 | parser.add_argument(
23 | "--force",
24 | action="store_true",
25 | default=False,
26 | help="Force overwriting existing files.",
27 | )
28 |
29 | def main(self, args):
30 | pub = self.app.config.temboard.signing_public_key
31 | if os.path.exists(pub) and not args.force:
32 | raise UserError("%s exists. Use --force to overwrite." % pub)
33 |
34 | ui_url_raw = self.app.config.temboard.ui_url.rstrip("/")
35 | ui_url = urlparse(ui_url_raw)
36 | ui_client = TemboardClient.factory(
37 | self.app.config,
38 | scheme=ui_url.scheme,
39 | host=ui_url.hostname,
40 | port=ui_url.port,
41 | )
42 |
43 | logger.info("Requesting public key from %s.", ui_url_raw)
44 | response = ui_client.get("/signing.key")
45 | response.raise_for_status()
46 | pem = response.read()
47 |
48 | logger.info("Validating PEM data.")
49 | load_public_key(pem)
50 |
51 | with open(pub, "wb") as fo:
52 | fo.write(pem)
53 |
54 | logger.info("%s updated from %s.", pub, ui_url_raw)
55 | logger.info("Please reload agent service if running.")
56 |
57 | return 0
58 |
--------------------------------------------------------------------------------
/ui/temboardui/plugins/maintenance/__init__.py:
--------------------------------------------------------------------------------
1 | from ...web.flask import instance_proxy
2 |
3 |
4 | class MaintenancePlugin:
5 | def __init__(self, app):
6 | self.app = app
7 |
8 | def load(self):
9 | __import__(__name__ + ".routes")
10 | instance_proxy.generic_proxy("/maintenance")
11 | instance_proxy.generic_proxy("/maintenance/")
12 | instance_proxy.generic_proxy("/maintenance/", method="POST")
13 | instance_proxy.generic_proxy(
14 | "/maintenance//",
15 | method="DELETE",
16 | )
17 | instance_proxy.generic_proxy(
18 | "/maintenance//",
19 | method="POST",
20 | )
21 | instance_proxy.generic_proxy(
22 | "/maintenance///scheduled"
23 | )
24 | instance_proxy.generic_proxy("/maintenance//schema/")
25 | instance_proxy.generic_proxy(
26 | "/maintenance//schema///scheduled"
27 | )
28 | instance_proxy.generic_proxy(
29 | "/maintenance//schema//table/"
30 | )
31 | instance_proxy.generic_proxy(
32 | "/maintenance//schema///