matomo-bootstrap 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matomo_bootstrap-1.0.0/LICENSE +7 -0
- matomo_bootstrap-1.0.0/PKG-INFO +199 -0
- matomo_bootstrap-1.0.0/README.md +183 -0
- matomo_bootstrap-1.0.0/pyproject.toml +34 -0
- matomo_bootstrap-1.0.0/setup.cfg +4 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/__init__.py +8 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/__main__.py +32 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/cli.py +50 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/config.py +75 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/errors.py +10 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/health.py +17 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/http.py +60 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/installers/__init__.py +1 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/installers/base.py +11 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/installers/web.py +236 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/matomo_api.py +125 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap/service.py +33 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/PKG-INFO +199 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/SOURCES.txt +21 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/dependency_links.txt +1 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/entry_points.txt +2 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/requires.txt +6 -0
- matomo_bootstrap-1.0.0/src/matomo_bootstrap.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2025 Kevin Veen-Birkenbach
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matomo-bootstrap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Headless bootstrap tooling for Matomo (installation + API token provisioning)
|
|
5
|
+
Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kevinveenbirkenbach/matomo-bootstrap
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: playwright>=1.40.0
|
|
12
|
+
Provides-Extra: e2e
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: ruff; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# matomo-bootstrap
|
|
18
|
+
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Headless bootstrap tooling for **Matomo**
|
|
22
|
+
Automates **installation** (via recorded Playwright flow) and **API token provisioning** for fresh Matomo instances.
|
|
23
|
+
|
|
24
|
+
This tool is designed for **CI, containers, and reproducible environments**, where no interactive browser access is available.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* 🚀 **Fully headless Matomo installation**
|
|
31
|
+
|
|
32
|
+
* Drives the official Matomo web installer using **Playwright**
|
|
33
|
+
* Automatically skips installation if Matomo is already installed
|
|
34
|
+
* 🔐 **API token provisioning**
|
|
35
|
+
|
|
36
|
+
* Creates an *app-specific token* via authenticated Matomo session
|
|
37
|
+
* Compatible with Matomo 5.3.x Docker images
|
|
38
|
+
* 🧪 **E2E-tested**
|
|
39
|
+
|
|
40
|
+
* Docker-based end-to-end tests included
|
|
41
|
+
* ❄️ **First-class Nix support**
|
|
42
|
+
|
|
43
|
+
* Flake-based packaging
|
|
44
|
+
* Reproducible CLI and dev environments
|
|
45
|
+
* 🐍 **Standard Python CLI**
|
|
46
|
+
|
|
47
|
+
* Installable via `pip`
|
|
48
|
+
* Clean stdout (token only), logs on stderr
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
* A running Matomo instance (e.g. Docker)
|
|
55
|
+
* For fresh installs:
|
|
56
|
+
|
|
57
|
+
* Chromium (managed automatically by Playwright)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
### Using **Nix** (recommended)
|
|
64
|
+
|
|
65
|
+
If you use **Nix** with flakes:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Install Playwright’s Chromium browser (one-time):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap#matomo-bootstrap-playwright-install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This installs Chromium into the user cache used by Playwright.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Using **Python / pip**
|
|
82
|
+
|
|
83
|
+
Requires **Python ≥ 3.10**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install matomo-bootstrap
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Install Chromium for Playwright:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m playwright install chromium
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
### CLI
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
matomo-bootstrap \
|
|
103
|
+
--base-url http://127.0.0.1:8080 \
|
|
104
|
+
--admin-user administrator \
|
|
105
|
+
--admin-password AdminSecret123! \
|
|
106
|
+
--admin-email administrator@example.org
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
On success, the command prints **only the API token** to stdout:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
6c7a8c2b0e9e4a3c8e1d0c4e8a6b9f21
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Environment Variables
|
|
118
|
+
|
|
119
|
+
All options can be provided via environment variables:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
export MATOMO_URL=http://127.0.0.1:8080
|
|
123
|
+
export MATOMO_ADMIN_USER=administrator
|
|
124
|
+
export MATOMO_ADMIN_PASSWORD=AdminSecret123!
|
|
125
|
+
export MATOMO_ADMIN_EMAIL=administrator@example.org
|
|
126
|
+
export MATOMO_TOKEN_DESCRIPTION=my-ci-token
|
|
127
|
+
|
|
128
|
+
matomo-bootstrap
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Debug Mode
|
|
134
|
+
|
|
135
|
+
Enable verbose logs (stderr only):
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
matomo-bootstrap --debug
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## How It Works
|
|
144
|
+
|
|
145
|
+
1. **Reachability check**
|
|
146
|
+
|
|
147
|
+
* Waits until Matomo responds over HTTP (any status)
|
|
148
|
+
2. **Installation (if needed)**
|
|
149
|
+
|
|
150
|
+
* Uses a recorded Playwright flow to complete the Matomo web installer
|
|
151
|
+
3. **Authentication**
|
|
152
|
+
|
|
153
|
+
* Logs in using the `Login.logme` controller
|
|
154
|
+
4. **Token creation**
|
|
155
|
+
|
|
156
|
+
* Calls `UsersManager.createAppSpecificTokenAuth`
|
|
157
|
+
5. **Output**
|
|
158
|
+
|
|
159
|
+
* Prints the token to stdout (safe for scripting)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## End-to-End Tests
|
|
164
|
+
|
|
165
|
+
Run the full E2E cycle locally:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
make e2e
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This will:
|
|
172
|
+
|
|
173
|
+
1. Start Matomo + MariaDB via Docker
|
|
174
|
+
2. Install Matomo headlessly
|
|
175
|
+
3. Create an API token
|
|
176
|
+
4. Validate the token via Matomo API
|
|
177
|
+
5. Tear everything down again
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Project Status
|
|
182
|
+
|
|
183
|
+
* ✔ Stable for CI / automation
|
|
184
|
+
* ✔ Tested against Matomo 5.3.x Docker images
|
|
185
|
+
* ⚠ Installer flow is UI-recorded (robust, but may need updates for future Matomo UI changes)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Author
|
|
190
|
+
|
|
191
|
+
**Kevin Veen-Birkenbach**
|
|
192
|
+
🌐 [https://www.veen.world/](https://www.veen.world/)
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT License
|
|
199
|
+
See [LICENSE](LICENSE)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# matomo-bootstrap
|
|
2
|
+
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
Headless bootstrap tooling for **Matomo**
|
|
6
|
+
Automates **installation** (via recorded Playwright flow) and **API token provisioning** for fresh Matomo instances.
|
|
7
|
+
|
|
8
|
+
This tool is designed for **CI, containers, and reproducible environments**, where no interactive browser access is available.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
* 🚀 **Fully headless Matomo installation**
|
|
15
|
+
|
|
16
|
+
* Drives the official Matomo web installer using **Playwright**
|
|
17
|
+
* Automatically skips installation if Matomo is already installed
|
|
18
|
+
* 🔐 **API token provisioning**
|
|
19
|
+
|
|
20
|
+
* Creates an *app-specific token* via authenticated Matomo session
|
|
21
|
+
* Compatible with Matomo 5.3.x Docker images
|
|
22
|
+
* 🧪 **E2E-tested**
|
|
23
|
+
|
|
24
|
+
* Docker-based end-to-end tests included
|
|
25
|
+
* ❄️ **First-class Nix support**
|
|
26
|
+
|
|
27
|
+
* Flake-based packaging
|
|
28
|
+
* Reproducible CLI and dev environments
|
|
29
|
+
* 🐍 **Standard Python CLI**
|
|
30
|
+
|
|
31
|
+
* Installable via `pip`
|
|
32
|
+
* Clean stdout (token only), logs on stderr
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Requirements
|
|
37
|
+
|
|
38
|
+
* A running Matomo instance (e.g. Docker)
|
|
39
|
+
* For fresh installs:
|
|
40
|
+
|
|
41
|
+
* Chromium (managed automatically by Playwright)
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### Using **Nix** (recommended)
|
|
48
|
+
|
|
49
|
+
If you use **Nix** with flakes:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Install Playwright’s Chromium browser (one-time):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap#matomo-bootstrap-playwright-install
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This installs Chromium into the user cache used by Playwright.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
### Using **Python / pip**
|
|
66
|
+
|
|
67
|
+
Requires **Python ≥ 3.10**
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install matomo-bootstrap
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Install Chromium for Playwright:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
python -m playwright install chromium
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
### CLI
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
matomo-bootstrap \
|
|
87
|
+
--base-url http://127.0.0.1:8080 \
|
|
88
|
+
--admin-user administrator \
|
|
89
|
+
--admin-password AdminSecret123! \
|
|
90
|
+
--admin-email administrator@example.org
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
On success, the command prints **only the API token** to stdout:
|
|
94
|
+
|
|
95
|
+
```text
|
|
96
|
+
6c7a8c2b0e9e4a3c8e1d0c4e8a6b9f21
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### Environment Variables
|
|
102
|
+
|
|
103
|
+
All options can be provided via environment variables:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
export MATOMO_URL=http://127.0.0.1:8080
|
|
107
|
+
export MATOMO_ADMIN_USER=administrator
|
|
108
|
+
export MATOMO_ADMIN_PASSWORD=AdminSecret123!
|
|
109
|
+
export MATOMO_ADMIN_EMAIL=administrator@example.org
|
|
110
|
+
export MATOMO_TOKEN_DESCRIPTION=my-ci-token
|
|
111
|
+
|
|
112
|
+
matomo-bootstrap
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Debug Mode
|
|
118
|
+
|
|
119
|
+
Enable verbose logs (stderr only):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
matomo-bootstrap --debug
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## How It Works
|
|
128
|
+
|
|
129
|
+
1. **Reachability check**
|
|
130
|
+
|
|
131
|
+
* Waits until Matomo responds over HTTP (any status)
|
|
132
|
+
2. **Installation (if needed)**
|
|
133
|
+
|
|
134
|
+
* Uses a recorded Playwright flow to complete the Matomo web installer
|
|
135
|
+
3. **Authentication**
|
|
136
|
+
|
|
137
|
+
* Logs in using the `Login.logme` controller
|
|
138
|
+
4. **Token creation**
|
|
139
|
+
|
|
140
|
+
* Calls `UsersManager.createAppSpecificTokenAuth`
|
|
141
|
+
5. **Output**
|
|
142
|
+
|
|
143
|
+
* Prints the token to stdout (safe for scripting)
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## End-to-End Tests
|
|
148
|
+
|
|
149
|
+
Run the full E2E cycle locally:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
make e2e
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
This will:
|
|
156
|
+
|
|
157
|
+
1. Start Matomo + MariaDB via Docker
|
|
158
|
+
2. Install Matomo headlessly
|
|
159
|
+
3. Create an API token
|
|
160
|
+
4. Validate the token via Matomo API
|
|
161
|
+
5. Tear everything down again
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Project Status
|
|
166
|
+
|
|
167
|
+
* ✔ Stable for CI / automation
|
|
168
|
+
* ✔ Tested against Matomo 5.3.x Docker images
|
|
169
|
+
* ⚠ Installer flow is UI-recorded (robust, but may need updates for future Matomo UI changes)
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Author
|
|
174
|
+
|
|
175
|
+
**Kevin Veen-Birkenbach**
|
|
176
|
+
🌐 [https://www.veen.world/](https://www.veen.world/)
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT License
|
|
183
|
+
See [LICENSE](LICENSE)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "matomo-bootstrap"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Headless bootstrap tooling for Matomo (installation + API token provisioning)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [{ name = "Kevin Veen-Birkenbach", email = "kevin@veen.world" }]
|
|
12
|
+
license = { text = "MIT" }
|
|
13
|
+
urls = { Homepage = "https://github.com/kevinveenbirkenbach/matomo-bootstrap" }
|
|
14
|
+
|
|
15
|
+
dependencies = [
|
|
16
|
+
"playwright>=1.40.0",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
# Provides a stable CLI name for Nix + pip installs:
|
|
20
|
+
[project.scripts]
|
|
21
|
+
matomo-bootstrap = "matomo_bootstrap.__main__:main"
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
e2e = []
|
|
25
|
+
|
|
26
|
+
dev = [
|
|
27
|
+
"ruff",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
package-dir = { "" = "src" }
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from .cli import parse_args
|
|
6
|
+
from .config import config_from_env_and_args
|
|
7
|
+
from .errors import BootstrapError
|
|
8
|
+
from .service import run
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> int:
|
|
12
|
+
args = parse_args()
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
config = config_from_env_and_args(args)
|
|
16
|
+
token = run(config)
|
|
17
|
+
print(token)
|
|
18
|
+
return 0
|
|
19
|
+
except ValueError as exc:
|
|
20
|
+
# config validation errors
|
|
21
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
22
|
+
return 2
|
|
23
|
+
except BootstrapError as exc:
|
|
24
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
25
|
+
return 2
|
|
26
|
+
except Exception as exc:
|
|
27
|
+
print(f"[FATAL] {type(exc).__name__}: {exc}", file=sys.stderr)
|
|
28
|
+
return 3
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def parse_args() -> argparse.Namespace:
|
|
6
|
+
p = argparse.ArgumentParser(
|
|
7
|
+
description="Headless bootstrap tool for Matomo (installation + API token provisioning)"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
p.add_argument(
|
|
11
|
+
"--base-url",
|
|
12
|
+
default=os.environ.get("MATOMO_URL"),
|
|
13
|
+
help="Matomo base URL (or MATOMO_URL env)",
|
|
14
|
+
)
|
|
15
|
+
p.add_argument(
|
|
16
|
+
"--admin-user",
|
|
17
|
+
default=os.environ.get("MATOMO_ADMIN_USER"),
|
|
18
|
+
help="Admin login (or MATOMO_ADMIN_USER env)",
|
|
19
|
+
)
|
|
20
|
+
p.add_argument(
|
|
21
|
+
"--admin-password",
|
|
22
|
+
default=os.environ.get("MATOMO_ADMIN_PASSWORD"),
|
|
23
|
+
help="Admin password (or MATOMO_ADMIN_PASSWORD env)",
|
|
24
|
+
)
|
|
25
|
+
p.add_argument(
|
|
26
|
+
"--admin-email",
|
|
27
|
+
default=os.environ.get("MATOMO_ADMIN_EMAIL"),
|
|
28
|
+
help="Admin email (or MATOMO_ADMIN_EMAIL env)",
|
|
29
|
+
)
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"--token-description",
|
|
32
|
+
default=os.environ.get("MATOMO_TOKEN_DESCRIPTION", "matomo-bootstrap"),
|
|
33
|
+
help="App token description",
|
|
34
|
+
)
|
|
35
|
+
p.add_argument(
|
|
36
|
+
"--timeout",
|
|
37
|
+
type=int,
|
|
38
|
+
default=int(os.environ.get("MATOMO_TIMEOUT", "20")),
|
|
39
|
+
help="Network timeout in seconds (or MATOMO_TIMEOUT env)",
|
|
40
|
+
)
|
|
41
|
+
p.add_argument("--debug", action="store_true", help="Enable debug logs on stderr")
|
|
42
|
+
|
|
43
|
+
# Optional (future use)
|
|
44
|
+
p.add_argument(
|
|
45
|
+
"--matomo-container-name",
|
|
46
|
+
default=os.environ.get("MATOMO_CONTAINER_NAME"),
|
|
47
|
+
help="Matomo container name (optional; also MATOMO_CONTAINER_NAME env)",
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return p.parse_args()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(frozen=True)
|
|
8
|
+
class Config:
|
|
9
|
+
base_url: str
|
|
10
|
+
admin_user: str
|
|
11
|
+
admin_password: str
|
|
12
|
+
admin_email: str
|
|
13
|
+
token_description: str = "matomo-bootstrap"
|
|
14
|
+
timeout: int = 20
|
|
15
|
+
debug: bool = False
|
|
16
|
+
matomo_container_name: str | None = (
|
|
17
|
+
None # optional, for future console installer usage
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def config_from_env_and_args(args) -> Config:
|
|
22
|
+
"""
|
|
23
|
+
Build a Config object from CLI args (preferred) and environment variables (fallback).
|
|
24
|
+
"""
|
|
25
|
+
base_url = getattr(args, "base_url", None) or os.environ.get("MATOMO_URL")
|
|
26
|
+
admin_user = getattr(args, "admin_user", None) or os.environ.get(
|
|
27
|
+
"MATOMO_ADMIN_USER"
|
|
28
|
+
)
|
|
29
|
+
admin_password = getattr(args, "admin_password", None) or os.environ.get(
|
|
30
|
+
"MATOMO_ADMIN_PASSWORD"
|
|
31
|
+
)
|
|
32
|
+
admin_email = getattr(args, "admin_email", None) or os.environ.get(
|
|
33
|
+
"MATOMO_ADMIN_EMAIL"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
token_description = (
|
|
37
|
+
getattr(args, "token_description", None)
|
|
38
|
+
or os.environ.get("MATOMO_TOKEN_DESCRIPTION")
|
|
39
|
+
or "matomo-bootstrap"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
timeout = int(
|
|
43
|
+
getattr(args, "timeout", None) or os.environ.get("MATOMO_TIMEOUT") or "20"
|
|
44
|
+
)
|
|
45
|
+
debug = bool(getattr(args, "debug", False))
|
|
46
|
+
|
|
47
|
+
matomo_container_name = (
|
|
48
|
+
getattr(args, "matomo_container_name", None)
|
|
49
|
+
or os.environ.get("MATOMO_CONTAINER_NAME")
|
|
50
|
+
or None
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
missing: list[str] = []
|
|
54
|
+
if not base_url:
|
|
55
|
+
missing.append("--base-url (or MATOMO_URL)")
|
|
56
|
+
if not admin_user:
|
|
57
|
+
missing.append("--admin-user (or MATOMO_ADMIN_USER)")
|
|
58
|
+
if not admin_password:
|
|
59
|
+
missing.append("--admin-password (or MATOMO_ADMIN_PASSWORD)")
|
|
60
|
+
if not admin_email:
|
|
61
|
+
missing.append("--admin-email (or MATOMO_ADMIN_EMAIL)")
|
|
62
|
+
|
|
63
|
+
if missing:
|
|
64
|
+
raise ValueError("missing required values: " + ", ".join(missing))
|
|
65
|
+
|
|
66
|
+
return Config(
|
|
67
|
+
base_url=str(base_url),
|
|
68
|
+
admin_user=str(admin_user),
|
|
69
|
+
admin_password=str(admin_password),
|
|
70
|
+
admin_email=str(admin_email),
|
|
71
|
+
token_description=str(token_description),
|
|
72
|
+
timeout=timeout,
|
|
73
|
+
debug=debug,
|
|
74
|
+
matomo_container_name=matomo_container_name,
|
|
75
|
+
)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
class BootstrapError(RuntimeError):
|
|
2
|
+
"""Base error for matomo-bootstrap."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class MatomoNotReadyError(BootstrapError):
|
|
6
|
+
"""Matomo is not reachable or not initialized."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokenCreationError(BootstrapError):
|
|
10
|
+
"""Failed to create API token."""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.request
|
|
4
|
+
|
|
5
|
+
from .errors import MatomoNotReadyError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def assert_matomo_ready(base_url: str, timeout: int = 10) -> None:
|
|
9
|
+
try:
|
|
10
|
+
with urllib.request.urlopen(base_url, timeout=timeout) as resp:
|
|
11
|
+
html = resp.read().decode("utf-8", errors="replace")
|
|
12
|
+
except Exception as exc:
|
|
13
|
+
raise MatomoNotReadyError(f"Matomo not reachable: {exc}") from exc
|
|
14
|
+
|
|
15
|
+
lower = html.lower()
|
|
16
|
+
if "matomo" not in lower and "piwik" not in lower:
|
|
17
|
+
raise MatomoNotReadyError("Matomo UI not detected at base URL")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import http.cookiejar
|
|
4
|
+
import sys
|
|
5
|
+
import urllib.error
|
|
6
|
+
import urllib.parse
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Dict, Tuple
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HttpClient:
|
|
12
|
+
def __init__(self, base_url: str, timeout: int = 20, debug: bool = False):
|
|
13
|
+
self.base_url = base_url.rstrip("/")
|
|
14
|
+
self.timeout = timeout
|
|
15
|
+
self.debug = debug
|
|
16
|
+
|
|
17
|
+
self.cookies = http.cookiejar.CookieJar()
|
|
18
|
+
self.opener = urllib.request.build_opener(
|
|
19
|
+
urllib.request.HTTPCookieProcessor(self.cookies)
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
def _dbg(self, msg: str) -> None:
|
|
23
|
+
if self.debug:
|
|
24
|
+
print(msg, file=sys.stderr)
|
|
25
|
+
|
|
26
|
+
def _open(self, req: urllib.request.Request) -> Tuple[int, str]:
|
|
27
|
+
try:
|
|
28
|
+
with self.opener.open(req, timeout=self.timeout) as resp:
|
|
29
|
+
body = resp.read().decode("utf-8", errors="replace")
|
|
30
|
+
return resp.status, body
|
|
31
|
+
except urllib.error.HTTPError as exc:
|
|
32
|
+
# urllib raises HTTPError for 4xx/5xx but it still contains status + body
|
|
33
|
+
try:
|
|
34
|
+
body = exc.read().decode("utf-8", errors="replace")
|
|
35
|
+
except Exception:
|
|
36
|
+
body = str(exc)
|
|
37
|
+
return exc.code, body
|
|
38
|
+
|
|
39
|
+
def get(self, path: str, params: Dict[str, str]) -> Tuple[int, str]:
|
|
40
|
+
qs = urllib.parse.urlencode(params)
|
|
41
|
+
if path == "/":
|
|
42
|
+
url = f"{self.base_url}/"
|
|
43
|
+
else:
|
|
44
|
+
url = f"{self.base_url}{path}"
|
|
45
|
+
if qs:
|
|
46
|
+
url = f"{url}?{qs}"
|
|
47
|
+
|
|
48
|
+
self._dbg(f"[HTTP] GET {url}")
|
|
49
|
+
|
|
50
|
+
req = urllib.request.Request(url, method="GET")
|
|
51
|
+
return self._open(req)
|
|
52
|
+
|
|
53
|
+
def post(self, path: str, data: Dict[str, str]) -> Tuple[int, str]:
|
|
54
|
+
url = self.base_url + path
|
|
55
|
+
encoded = urllib.parse.urlencode(data).encode()
|
|
56
|
+
|
|
57
|
+
self._dbg(f"[HTTP] POST {url} keys={list(data.keys())}")
|
|
58
|
+
|
|
59
|
+
req = urllib.request.Request(url, data=encoded, method="POST")
|
|
60
|
+
return self._open(req)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__all__ = []
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
|
|
9
|
+
from .base import Installer
|
|
10
|
+
from ..config import Config
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Optional knobs (mostly for debugging / CI stability)
|
|
14
|
+
PLAYWRIGHT_HEADLESS = os.environ.get("MATOMO_PLAYWRIGHT_HEADLESS", "1").strip() not in (
|
|
15
|
+
"0",
|
|
16
|
+
"false",
|
|
17
|
+
"False",
|
|
18
|
+
)
|
|
19
|
+
PLAYWRIGHT_SLOWMO_MS = int(os.environ.get("MATOMO_PLAYWRIGHT_SLOWMO_MS", "0"))
|
|
20
|
+
PLAYWRIGHT_NAV_TIMEOUT_MS = int(
|
|
21
|
+
os.environ.get("MATOMO_PLAYWRIGHT_NAV_TIMEOUT_MS", "60000")
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
# Values used by the installer flow (recorded)
|
|
25
|
+
DEFAULT_SITE_NAME = os.environ.get("MATOMO_SITE_NAME", "localhost")
|
|
26
|
+
DEFAULT_SITE_URL = os.environ.get("MATOMO_SITE_URL", "http://localhost")
|
|
27
|
+
DEFAULT_TIMEZONE = os.environ.get("MATOMO_TIMEZONE", "Germany - Berlin")
|
|
28
|
+
DEFAULT_ECOMMERCE = os.environ.get("MATOMO_ECOMMERCE", "Ecommerce enabled")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _log(msg: str) -> None:
|
|
32
|
+
# IMPORTANT: logs must not pollute stdout (tests expect only token on stdout)
|
|
33
|
+
print(msg, file=sys.stderr)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def wait_http(url: str, timeout: int = 180) -> None:
|
|
37
|
+
"""
|
|
38
|
+
Consider Matomo 'reachable' as soon as the HTTP server answers - even with 500.
|
|
39
|
+
urllib raises HTTPError for 4xx/5xx, so we must treat that as reachability too.
|
|
40
|
+
"""
|
|
41
|
+
_log(f"[install] Waiting for Matomo HTTP at {url} ...")
|
|
42
|
+
last_err: Exception | None = None
|
|
43
|
+
|
|
44
|
+
for i in range(timeout):
|
|
45
|
+
try:
|
|
46
|
+
with urllib.request.urlopen(url, timeout=2) as resp:
|
|
47
|
+
_ = resp.read(128)
|
|
48
|
+
_log("[install] Matomo HTTP reachable (2xx/3xx).")
|
|
49
|
+
return
|
|
50
|
+
except urllib.error.HTTPError as exc:
|
|
51
|
+
_log(f"[install] Matomo HTTP reachable (HTTP {exc.code}).")
|
|
52
|
+
return
|
|
53
|
+
except Exception as exc:
|
|
54
|
+
last_err = exc
|
|
55
|
+
if i % 5 == 0:
|
|
56
|
+
_log(
|
|
57
|
+
f"[install] still waiting ({i}/{timeout}) … ({type(exc).__name__})"
|
|
58
|
+
)
|
|
59
|
+
time.sleep(1)
|
|
60
|
+
|
|
61
|
+
raise RuntimeError(
|
|
62
|
+
f"Matomo did not become reachable after {timeout}s: {url} ({last_err})"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def is_installed(url: str) -> bool:
|
|
67
|
+
"""
|
|
68
|
+
Heuristic:
|
|
69
|
+
- installed instances typically render login module links
|
|
70
|
+
- installer renders 'installation' wizard content
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
with urllib.request.urlopen(url, timeout=5) as resp:
|
|
74
|
+
html = resp.read().decode(errors="ignore").lower()
|
|
75
|
+
return (
|
|
76
|
+
("module=login" in html)
|
|
77
|
+
or ("matomo › login" in html)
|
|
78
|
+
or ("matomo/login" in html)
|
|
79
|
+
)
|
|
80
|
+
except urllib.error.HTTPError as exc:
|
|
81
|
+
try:
|
|
82
|
+
html = exc.read().decode(errors="ignore").lower()
|
|
83
|
+
return (
|
|
84
|
+
("module=login" in html)
|
|
85
|
+
or ("matomo › login" in html)
|
|
86
|
+
or ("matomo/login" in html)
|
|
87
|
+
)
|
|
88
|
+
except Exception:
|
|
89
|
+
return False
|
|
90
|
+
except Exception:
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class WebInstaller(Installer):
|
|
95
|
+
def ensure_installed(self, config: Config) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Ensure Matomo is installed. NO-OP if already installed.
|
|
98
|
+
Uses Playwright to drive the web installer (recorded flow).
|
|
99
|
+
"""
|
|
100
|
+
base_url = config.base_url
|
|
101
|
+
|
|
102
|
+
wait_http(base_url)
|
|
103
|
+
|
|
104
|
+
if is_installed(base_url):
|
|
105
|
+
if config.debug:
|
|
106
|
+
_log("[install] Matomo already looks installed. Skipping installer.")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
from playwright.sync_api import sync_playwright
|
|
110
|
+
|
|
111
|
+
_log("[install] Running Matomo web installer via Playwright (recorded flow)...")
|
|
112
|
+
|
|
113
|
+
with sync_playwright() as p:
|
|
114
|
+
browser = p.chromium.launch(
|
|
115
|
+
headless=PLAYWRIGHT_HEADLESS,
|
|
116
|
+
slow_mo=PLAYWRIGHT_SLOWMO_MS if PLAYWRIGHT_SLOWMO_MS > 0 else None,
|
|
117
|
+
)
|
|
118
|
+
context = browser.new_context()
|
|
119
|
+
page = context.new_page()
|
|
120
|
+
page.set_default_navigation_timeout(PLAYWRIGHT_NAV_TIMEOUT_MS)
|
|
121
|
+
page.set_default_timeout(PLAYWRIGHT_NAV_TIMEOUT_MS)
|
|
122
|
+
|
|
123
|
+
def _dbg(msg: str) -> None:
|
|
124
|
+
if config.debug:
|
|
125
|
+
_log(f"[install] {msg}")
|
|
126
|
+
|
|
127
|
+
def click_next() -> None:
|
|
128
|
+
"""
|
|
129
|
+
Matomo installer mixes link/button variants and sometimes includes '»'.
|
|
130
|
+
We try common variants in a robust order.
|
|
131
|
+
"""
|
|
132
|
+
candidates = [
|
|
133
|
+
("link", "Next »"),
|
|
134
|
+
("button", "Next »"),
|
|
135
|
+
("link", "Next"),
|
|
136
|
+
("button", "Next"),
|
|
137
|
+
("link", "Continue"),
|
|
138
|
+
("button", "Continue"),
|
|
139
|
+
("link", "Proceed"),
|
|
140
|
+
("button", "Proceed"),
|
|
141
|
+
("link", "Start Installation"),
|
|
142
|
+
("button", "Start Installation"),
|
|
143
|
+
("link", "Weiter"),
|
|
144
|
+
("button", "Weiter"),
|
|
145
|
+
("link", "Fortfahren"),
|
|
146
|
+
("button", "Fortfahren"),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
for role, name in candidates:
|
|
150
|
+
loc = page.get_by_role(role, name=name)
|
|
151
|
+
if loc.count() > 0:
|
|
152
|
+
_dbg(f"click_next(): {role} '{name}'")
|
|
153
|
+
loc.first.click()
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
loc = page.get_by_text("Next", exact=False)
|
|
157
|
+
if loc.count() > 0:
|
|
158
|
+
_dbg("click_next(): fallback text 'Next'")
|
|
159
|
+
loc.first.click()
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
raise RuntimeError(
|
|
163
|
+
"Could not find a Next/Continue control in the installer UI."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
page.goto(base_url, wait_until="domcontentloaded")
|
|
167
|
+
|
|
168
|
+
def superuser_form_visible() -> bool:
|
|
169
|
+
return page.locator("#login-0").count() > 0
|
|
170
|
+
|
|
171
|
+
for _ in range(12):
|
|
172
|
+
if superuser_form_visible():
|
|
173
|
+
break
|
|
174
|
+
click_next()
|
|
175
|
+
page.wait_for_load_state("domcontentloaded")
|
|
176
|
+
page.wait_for_timeout(200)
|
|
177
|
+
else:
|
|
178
|
+
raise RuntimeError(
|
|
179
|
+
"Installer did not reach superuser step (login-0 not found)."
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
page.locator("#login-0").click()
|
|
183
|
+
page.locator("#login-0").fill(config.admin_user)
|
|
184
|
+
|
|
185
|
+
page.locator("#password-0").click()
|
|
186
|
+
page.locator("#password-0").fill(config.admin_password)
|
|
187
|
+
|
|
188
|
+
if page.locator("#password_bis-0").count() > 0:
|
|
189
|
+
page.locator("#password_bis-0").click()
|
|
190
|
+
page.locator("#password_bis-0").fill(config.admin_password)
|
|
191
|
+
|
|
192
|
+
page.locator("#email-0").click()
|
|
193
|
+
page.locator("#email-0").fill(config.admin_email)
|
|
194
|
+
|
|
195
|
+
if page.get_by_role("button", name="Next »").count() > 0:
|
|
196
|
+
page.get_by_role("button", name="Next »").click()
|
|
197
|
+
else:
|
|
198
|
+
click_next()
|
|
199
|
+
|
|
200
|
+
if page.locator("#siteName-0").count() > 0:
|
|
201
|
+
page.locator("#siteName-0").click()
|
|
202
|
+
page.locator("#siteName-0").fill(DEFAULT_SITE_NAME)
|
|
203
|
+
|
|
204
|
+
if page.locator("#url-0").count() > 0:
|
|
205
|
+
page.locator("#url-0").click()
|
|
206
|
+
page.locator("#url-0").fill(DEFAULT_SITE_URL)
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
page.get_by_role("combobox").first.click()
|
|
210
|
+
page.get_by_role("listbox").get_by_text(DEFAULT_TIMEZONE).click()
|
|
211
|
+
except Exception:
|
|
212
|
+
_dbg("Timezone selection skipped (not found / changed UI).")
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
page.get_by_role("combobox").nth(2).click()
|
|
216
|
+
page.get_by_role("listbox").get_by_text(DEFAULT_ECOMMERCE).click()
|
|
217
|
+
except Exception:
|
|
218
|
+
_dbg("Ecommerce selection skipped (not found / changed UI).")
|
|
219
|
+
|
|
220
|
+
click_next()
|
|
221
|
+
page.wait_for_load_state("domcontentloaded")
|
|
222
|
+
|
|
223
|
+
if page.get_by_role("link", name="Next »").count() > 0:
|
|
224
|
+
page.get_by_role("link", name="Next »").click()
|
|
225
|
+
|
|
226
|
+
if page.get_by_role("button", name="Continue to Matomo »").count() > 0:
|
|
227
|
+
page.get_by_role("button", name="Continue to Matomo »").click()
|
|
228
|
+
|
|
229
|
+
context.close()
|
|
230
|
+
browser.close()
|
|
231
|
+
|
|
232
|
+
time.sleep(1)
|
|
233
|
+
if not is_installed(base_url):
|
|
234
|
+
raise RuntimeError("[install] Installer did not reach installed state.")
|
|
235
|
+
|
|
236
|
+
_log("[install] Installation finished.")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import urllib.error
|
|
8
|
+
|
|
9
|
+
from .errors import MatomoNotReadyError, TokenCreationError
|
|
10
|
+
from .http import HttpClient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _md5(text: str) -> str:
|
|
14
|
+
return hashlib.md5(text.encode("utf-8")).hexdigest()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _try_json(body: str) -> object:
|
|
18
|
+
try:
|
|
19
|
+
return json.loads(body)
|
|
20
|
+
except json.JSONDecodeError as exc:
|
|
21
|
+
raise TokenCreationError(f"Invalid JSON from Matomo API: {body[:400]}") from exc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _dbg(msg: str, enabled: bool) -> None:
|
|
25
|
+
if enabled:
|
|
26
|
+
# Keep stdout clean (tests expect only token on stdout).
|
|
27
|
+
print(msg, file=sys.stderr)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MatomoApi:
|
|
31
|
+
def __init__(self, *, client: HttpClient, debug: bool = False):
|
|
32
|
+
self.client = client
|
|
33
|
+
self.debug = debug
|
|
34
|
+
|
|
35
|
+
def assert_ready(self, timeout: int = 10) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Minimal readiness check: Matomo UI should be reachable and look like Matomo.
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
status, body = self.client.get("/", {})
|
|
41
|
+
except Exception as exc: # pragma: no cover
|
|
42
|
+
raise MatomoNotReadyError(f"Matomo not reachable: {exc}") from exc
|
|
43
|
+
|
|
44
|
+
_dbg(f"[ready] GET / -> HTTP {status}", self.debug)
|
|
45
|
+
|
|
46
|
+
html = (body or "").lower()
|
|
47
|
+
if "matomo" not in html and "piwik" not in html:
|
|
48
|
+
raise MatomoNotReadyError("Matomo UI not detected at base URL")
|
|
49
|
+
|
|
50
|
+
def login_via_logme(self, admin_user: str, admin_password: str) -> None:
|
|
51
|
+
"""
|
|
52
|
+
Create an authenticated Matomo session (cookie jar) using Login controller.
|
|
53
|
+
Matomo accepts md5 hashed password in `password` parameter for action=logme.
|
|
54
|
+
"""
|
|
55
|
+
md5_password = _md5(admin_password)
|
|
56
|
+
try:
|
|
57
|
+
status, body = self.client.get(
|
|
58
|
+
"/index.php",
|
|
59
|
+
{
|
|
60
|
+
"module": "Login",
|
|
61
|
+
"action": "logme",
|
|
62
|
+
"login": admin_user,
|
|
63
|
+
"password": md5_password,
|
|
64
|
+
},
|
|
65
|
+
)
|
|
66
|
+
_dbg(f"[auth] logme HTTP {status} body[:120]={body[:120]!r}", self.debug)
|
|
67
|
+
except urllib.error.HTTPError as exc:
|
|
68
|
+
# Even 4xx/5xx can still set cookies; continue and let the API call validate.
|
|
69
|
+
try:
|
|
70
|
+
err_body = exc.read().decode("utf-8", errors="replace")
|
|
71
|
+
except Exception:
|
|
72
|
+
err_body = ""
|
|
73
|
+
_dbg(
|
|
74
|
+
f"[auth] logme HTTPError {exc.code} body[:120]={err_body[:120]!r}",
|
|
75
|
+
self.debug,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
def create_app_specific_token(
|
|
79
|
+
self,
|
|
80
|
+
*,
|
|
81
|
+
admin_user: str,
|
|
82
|
+
admin_password: str,
|
|
83
|
+
description: str,
|
|
84
|
+
) -> str:
|
|
85
|
+
"""
|
|
86
|
+
Create an app-specific token using an authenticated session (cookies),
|
|
87
|
+
not UsersManager.getTokenAuth (not available in Matomo 5.3.x images).
|
|
88
|
+
"""
|
|
89
|
+
env_token = os.environ.get("MATOMO_BOOTSTRAP_TOKEN_AUTH")
|
|
90
|
+
if env_token:
|
|
91
|
+
_dbg(
|
|
92
|
+
"[auth] Using MATOMO_BOOTSTRAP_TOKEN_AUTH from environment.", self.debug
|
|
93
|
+
)
|
|
94
|
+
return env_token
|
|
95
|
+
|
|
96
|
+
self.login_via_logme(admin_user, admin_password)
|
|
97
|
+
|
|
98
|
+
status, body = self.client.post(
|
|
99
|
+
"/index.php",
|
|
100
|
+
{
|
|
101
|
+
"module": "API",
|
|
102
|
+
"method": "UsersManager.createAppSpecificTokenAuth",
|
|
103
|
+
"userLogin": admin_user,
|
|
104
|
+
"passwordConfirmation": admin_password,
|
|
105
|
+
"description": description,
|
|
106
|
+
"format": "json",
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
_dbg(
|
|
111
|
+
f"[auth] createAppSpecificTokenAuth HTTP {status} body[:200]={body[:200]!r}",
|
|
112
|
+
self.debug,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if status != 200:
|
|
116
|
+
raise TokenCreationError(
|
|
117
|
+
f"HTTP {status} during token creation: {body[:400]}"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
data = _try_json(body)
|
|
121
|
+
token = data.get("value") if isinstance(data, dict) else None
|
|
122
|
+
if not token:
|
|
123
|
+
raise TokenCreationError(f"Unexpected response from token creation: {data}")
|
|
124
|
+
|
|
125
|
+
return str(token)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from .config import Config
|
|
4
|
+
from .http import HttpClient
|
|
5
|
+
from .matomo_api import MatomoApi
|
|
6
|
+
from .installers.web import WebInstaller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run(config: Config) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Orchestrate:
|
|
12
|
+
1) Ensure Matomo is installed (NO-OP if installed)
|
|
13
|
+
2) Ensure Matomo is reachable/ready
|
|
14
|
+
3) Create an app-specific token using an authenticated session
|
|
15
|
+
"""
|
|
16
|
+
installer = WebInstaller()
|
|
17
|
+
installer.ensure_installed(config)
|
|
18
|
+
|
|
19
|
+
client = HttpClient(
|
|
20
|
+
base_url=config.base_url,
|
|
21
|
+
timeout=config.timeout,
|
|
22
|
+
debug=config.debug,
|
|
23
|
+
)
|
|
24
|
+
api = MatomoApi(client=client, debug=config.debug)
|
|
25
|
+
|
|
26
|
+
api.assert_ready(timeout=config.timeout)
|
|
27
|
+
|
|
28
|
+
token = api.create_app_specific_token(
|
|
29
|
+
admin_user=config.admin_user,
|
|
30
|
+
admin_password=config.admin_password,
|
|
31
|
+
description=config.token_description,
|
|
32
|
+
)
|
|
33
|
+
return token
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: matomo-bootstrap
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Headless bootstrap tooling for Matomo (installation + API token provisioning)
|
|
5
|
+
Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/kevinveenbirkenbach/matomo-bootstrap
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: playwright>=1.40.0
|
|
12
|
+
Provides-Extra: e2e
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: ruff; extra == "dev"
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# matomo-bootstrap
|
|
18
|
+
[](https://github.com/sponsors/kevinveenbirkenbach) [](https://www.patreon.com/c/kevinveenbirkenbach) [](https://buymeacoffee.com/kevinveenbirkenbach) [](https://s.veen.world/paypaldonate)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
Headless bootstrap tooling for **Matomo**
|
|
22
|
+
Automates **installation** (via recorded Playwright flow) and **API token provisioning** for fresh Matomo instances.
|
|
23
|
+
|
|
24
|
+
This tool is designed for **CI, containers, and reproducible environments**, where no interactive browser access is available.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
* 🚀 **Fully headless Matomo installation**
|
|
31
|
+
|
|
32
|
+
* Drives the official Matomo web installer using **Playwright**
|
|
33
|
+
* Automatically skips installation if Matomo is already installed
|
|
34
|
+
* 🔐 **API token provisioning**
|
|
35
|
+
|
|
36
|
+
* Creates an *app-specific token* via authenticated Matomo session
|
|
37
|
+
* Compatible with Matomo 5.3.x Docker images
|
|
38
|
+
* 🧪 **E2E-tested**
|
|
39
|
+
|
|
40
|
+
* Docker-based end-to-end tests included
|
|
41
|
+
* ❄️ **First-class Nix support**
|
|
42
|
+
|
|
43
|
+
* Flake-based packaging
|
|
44
|
+
* Reproducible CLI and dev environments
|
|
45
|
+
* 🐍 **Standard Python CLI**
|
|
46
|
+
|
|
47
|
+
* Installable via `pip`
|
|
48
|
+
* Clean stdout (token only), logs on stderr
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Requirements
|
|
53
|
+
|
|
54
|
+
* A running Matomo instance (e.g. Docker)
|
|
55
|
+
* For fresh installs:
|
|
56
|
+
|
|
57
|
+
* Chromium (managed automatically by Playwright)
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Installation
|
|
62
|
+
|
|
63
|
+
### Using **Nix** (recommended)
|
|
64
|
+
|
|
65
|
+
If you use **Nix** with flakes:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Install Playwright’s Chromium browser (one-time):
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
nix run github:kevinveenbirkenbach/matomo-bootstrap#matomo-bootstrap-playwright-install
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
This installs Chromium into the user cache used by Playwright.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
### Using **Python / pip**
|
|
82
|
+
|
|
83
|
+
Requires **Python ≥ 3.10**
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
pip install matomo-bootstrap
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Install Chromium for Playwright:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
python -m playwright install chromium
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
### CLI
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
matomo-bootstrap \
|
|
103
|
+
--base-url http://127.0.0.1:8080 \
|
|
104
|
+
--admin-user administrator \
|
|
105
|
+
--admin-password AdminSecret123! \
|
|
106
|
+
--admin-email administrator@example.org
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
On success, the command prints **only the API token** to stdout:
|
|
110
|
+
|
|
111
|
+
```text
|
|
112
|
+
6c7a8c2b0e9e4a3c8e1d0c4e8a6b9f21
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
### Environment Variables
|
|
118
|
+
|
|
119
|
+
All options can be provided via environment variables:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
export MATOMO_URL=http://127.0.0.1:8080
|
|
123
|
+
export MATOMO_ADMIN_USER=administrator
|
|
124
|
+
export MATOMO_ADMIN_PASSWORD=AdminSecret123!
|
|
125
|
+
export MATOMO_ADMIN_EMAIL=administrator@example.org
|
|
126
|
+
export MATOMO_TOKEN_DESCRIPTION=my-ci-token
|
|
127
|
+
|
|
128
|
+
matomo-bootstrap
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
### Debug Mode
|
|
134
|
+
|
|
135
|
+
Enable verbose logs (stderr only):
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
matomo-bootstrap --debug
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## How It Works
|
|
144
|
+
|
|
145
|
+
1. **Reachability check**
|
|
146
|
+
|
|
147
|
+
* Waits until Matomo responds over HTTP (any status)
|
|
148
|
+
2. **Installation (if needed)**
|
|
149
|
+
|
|
150
|
+
* Uses a recorded Playwright flow to complete the Matomo web installer
|
|
151
|
+
3. **Authentication**
|
|
152
|
+
|
|
153
|
+
* Logs in using the `Login.logme` controller
|
|
154
|
+
4. **Token creation**
|
|
155
|
+
|
|
156
|
+
* Calls `UsersManager.createAppSpecificTokenAuth`
|
|
157
|
+
5. **Output**
|
|
158
|
+
|
|
159
|
+
* Prints the token to stdout (safe for scripting)
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## End-to-End Tests
|
|
164
|
+
|
|
165
|
+
Run the full E2E cycle locally:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
make e2e
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
This will:
|
|
172
|
+
|
|
173
|
+
1. Start Matomo + MariaDB via Docker
|
|
174
|
+
2. Install Matomo headlessly
|
|
175
|
+
3. Create an API token
|
|
176
|
+
4. Validate the token via Matomo API
|
|
177
|
+
5. Tear everything down again
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## Project Status
|
|
182
|
+
|
|
183
|
+
* ✔ Stable for CI / automation
|
|
184
|
+
* ✔ Tested against Matomo 5.3.x Docker images
|
|
185
|
+
* ⚠ Installer flow is UI-recorded (robust, but may need updates for future Matomo UI changes)
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Author
|
|
190
|
+
|
|
191
|
+
**Kevin Veen-Birkenbach**
|
|
192
|
+
🌐 [https://www.veen.world/](https://www.veen.world/)
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT License
|
|
199
|
+
See [LICENSE](LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/matomo_bootstrap/__init__.py
|
|
5
|
+
src/matomo_bootstrap/__main__.py
|
|
6
|
+
src/matomo_bootstrap/cli.py
|
|
7
|
+
src/matomo_bootstrap/config.py
|
|
8
|
+
src/matomo_bootstrap/errors.py
|
|
9
|
+
src/matomo_bootstrap/health.py
|
|
10
|
+
src/matomo_bootstrap/http.py
|
|
11
|
+
src/matomo_bootstrap/matomo_api.py
|
|
12
|
+
src/matomo_bootstrap/service.py
|
|
13
|
+
src/matomo_bootstrap.egg-info/PKG-INFO
|
|
14
|
+
src/matomo_bootstrap.egg-info/SOURCES.txt
|
|
15
|
+
src/matomo_bootstrap.egg-info/dependency_links.txt
|
|
16
|
+
src/matomo_bootstrap.egg-info/entry_points.txt
|
|
17
|
+
src/matomo_bootstrap.egg-info/requires.txt
|
|
18
|
+
src/matomo_bootstrap.egg-info/top_level.txt
|
|
19
|
+
src/matomo_bootstrap/installers/__init__.py
|
|
20
|
+
src/matomo_bootstrap/installers/base.py
|
|
21
|
+
src/matomo_bootstrap/installers/web.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
matomo_bootstrap
|