Fluke MET/BASE as a migration source¶
Migrating legacy data from MET/BASE (MET/TRACK) into calServer V2 through a supplied source container.
Overview¶
The V2 first migration reads MET/BASE through its database server (SQL Anywhere, port 2638). There are two ways to get there:
| Route | When |
|---|---|
| Direct to the running MET/BASE on the customer network | The server is reachable and may be read during the migration |
| Source container holding a copy of the database file | The source system is unreachable, must not be touched, or the migration needs to be repeatable |
Both end in the same configuration (METTRACK_DB_*) and the same procedure.
The source container is just a different address.
The container is not a service
It runs during a migration only and does not come back by itself after a host restart. It is not reachable from outside and gets no browser address. Once the data is migrated, it is switched off.
Always a copy, never the original
The server opens the database read-write — the image has no read-only mode. A database that was not shut down cleanly is recovered on first open, which modifies it. Put a copy into the data directory and nothing else.
No ongoing replication
The container does not replace the SymmetricDS replication that calServer V1 offered. V2 reads the source once and never writes back. Sites that need an ongoing multi-location sync stay on V1 for that.
Prerequisites¶
- A copy of the MET/BASE database file (
.db) together with its transaction log (.log), shut down cleanly. A file copied while the server was running is recovered on first open, which takes time and can fail. - An amd64 host. The image is 32-bit x86 (
FROM 32bit/ubuntu:16.04); it does not start on arm64. - The password of the MET/BASE user (default
mt). It belongs in the password manager, not in the repository. - Access to the private registry (
DOCKERHUB_USERNAME,DOCKERHUB_PULL_TOKEN), because the image carries a licensed SQL Anywhere runtime. - Disk space for file, log and the recovery on first open. Budget twice the file size.
Docker Compose¶
1. Place the file. By default under metbase-data/ next to the compose
files:
2. Extend .env:
This means the deployment .env next to the Compose files (template:
docker/.env), not laravel/.env. METBASE_ENABLED, METBASE_DATA_PATH
and METBASE_IMAGE_TAG are read by Compose, not by the API. What the API
itself reads — control channel, source directory, settle time, idle stop —
is documented in laravel/.env.example; on Compose deploy.sh sets those
values, and you only set them by hand without Compose and without the chart.
METBASE_ENABLED=true
METBASE_DATA_PATH=./metbase-data
METBASE_DB_NAME=metbase.db
METTRACK_DB_DATABASE=metbase
METTRACK_DB_USERNAME=mt
METTRACK_DB_PASSWORD=<from the password manager>
METBASE_DB_NAME is a file name, not a path: the directory is hardcoded to
/calhelp inside the image, and that is where METBASE_DATA_PATH is mounted.
The ASA database name follows from the file name — metbase.db is served as
database metbase, which is why METTRACK_DB_DATABASE no longer carries its
mtrack default.
Leave METTRACK_DB_HOST empty. deploy.sh sets it to calserver-metbase and
writes it into .env; when you switch the container off again it clears the
value. An explicitly configured host wins — that is the direct route to a real
MET/BASE.
3. Deploy and wait for the server to come up:
On first start SQL Anywhere brings the database up. On a grown MET/BASE that takes minutes. The container is only healthy once the port answers.
A running container is not a running database
The server starts as a daemon and the container then parks itself in an
endless loop. If the database dies, the container stays up and looks
healthy. Trust the health check
(docker inspect --format '{{.State.Health.Status}}' calserver-metbase),
not docker ps.
4. Check the source and run the migration:
docker exec calserver-api-v2 php artisan mettrack:import --dry-run
docker exec calserver-api-v2 php artisan mettrack:import
Or through the interface: Administration → Data migration.
5. Switch it off afterwards. Remove METBASE_ENABLED=true from .env (or
set it to false) and deploy again:
docker stop closes the database cleanly through dbstop, and it is given 120
seconds to do so. docker rm -f would kill it, and the next start pays for the
recovery.
The data directory stays. Delete it only once the migration has been accepted — until then it is the only way to repeat the run.
Kubernetes and OpenShift¶
The source container is part of the Helm chart and disabled there as well.
1. Create and fill the volume. Create a PersistentVolumeClaim
(metbase-source) and copy the file into it, for example through a short-lived
helper pod:
oc cp metbase.db metbase-loader:/calhelp/metbase.db
oc cp metbase.log metbase-loader:/calhelp/metbase.log
/calhelp is hardcoded in the image and therefore also the mount path in the
chart. A different path means the server finds no database.
Fill first, enable second
A claim created by the chart is empty on first start. The container then
finds no database and enters a restart loop. This is why
v2.metbase.persistence.existingClaim is the intended route.
2. Update the release:
helm upgrade --install calserver calserver/calserver -f values.yaml \
--set v2.metbase.enabled=true \
--set v2.metbase.persistence.existingClaim=metbase-source \
--set v2.metbase.databaseFile=metbase.db \
--set v2.mettrack.database=metbase \
--set v2.mettrack.password=<from the password manager>
Leave v2.mettrack.host empty: the chart fills in the Service name itself.
3. Run the migration:
oc exec deploy/calserver-api-v2 -- php artisan mettrack:import --dry-run
oc exec deploy/calserver-api-v2 -- php artisan mettrack:import
4. Switch it off afterwards:
helm upgrade --install calserver calserver/calserver -f values.yaml \
--set v2.metbase.enabled=false
The claim stays and has to be deleted separately.
OpenShift specifics¶
- Leave
v2.metbase.podSecurityContextandv2.metbase.containerSecurityContextatenabled: false. The SCC assigns UID andfsGroup, and it is that assignedfsGroupwhich makes the volume holding the MET/BASE file writable for the runtime. The image creates/calhelpwith mode 0777 and is prepared for this. - The node selector defaults to
kubernetes.io/arch: amd64. The image is 32-bit x86 and does not start on arm64. - The pod runs as exactly one instance and is stopped before a new one starts
(
Recreate). The runtime locks the database file exclusively; a second instance could not open it. - With network policies enabled the pod accepts connections from the same
release only. If you need an additional toolbox pod, add it under
v2.metbase.networkPolicy.extraFrom.
The "MET/BASE source" page in the UI¶
Administration → Interfaces → MET/BASE carries the source container on three tabs:
| Tab | Contents |
|---|---|
| State | Whether the container runs, which file is set, since when — plus the start and stop buttons. |
| Source | What lies in the source directory, with "Check" and "Use" per file. |
| Check | Connection probe, login probe and diagnostics report. |
Until August 2026 this lived on the Data Migration page
The container used to be a section under Administration → Data
Migration. That did not match its own permission: that page requires
data_migration_admin, so anyone holding only metbase_source_admin never
got there. It now sits where the other foreign systems sit. A pointer
remains on the Data Migration page.
The MET/TRACK server login did not move. Signing in to the source belongs to the import and stays under Data Migration: it is stored per user, expires after two hours, and applies to an external MET/TRACK without any container as well. The interface page checks credentials, it stores none.
Who sees it. The separate permission metbase_source_admin
(Administration → Roles). Deliberately not part of data_migration_admin:
providing the server and importing the data are two acts. Whoever is to do
both needs both. No role carries it out of the box; the superadmin reaches it
through the RBAC bypass.
What it shows, and what that means.
| Display | Meaning |
|---|---|
| Not configured | This installation has no source container. The normal case. |
| Not deployed | The control channel answers but does not know the service: METBASE_ENABLED / v2.metbase.enabled is missing. |
| Off | Deployed, not running. That is how it should sit between migrations. |
| Starting | Running, not answering yet. A database that was not shut down cleanly is recovered on first open, which takes minutes. |
| Running | Running and answering on the ASA port. |
| Running, not answering | The container runs, the database server is dead. Exactly the case the port check exists for. |
| Shutting down | dbstop is closing the database. On a grown MET/BASE that takes minutes. |
| Control channel disturbed | The channel itself does not answer; the error text is shown next to it. |
What feeds it.
- On Compose,
deploy.shsets everything needed as soon asMETBASE_ENABLED=trueis in the.env. The state comes from the updater sidecar; without it the section shows the file list and reports the state as not configured. - On Kubernetes there are two switches, both off:
v2.metbase.source.mountmounts the source directory read-only into the API pod (needsReadWriteMany, since two pods hold the same claim), andv2.metbase.control.enabledcreates theRolethat lets the API pod read the state of this one deployment.
The source directory is mounted read-only. That is not caution but the division of labour: the database server is the only writer on those files.
Starting and stopping¶
Both buttons work in the background: the section confirms that the job was taken and then polls until it is done.
- Starting can take a while. If the image is not on the host yet, the start pulls roughly 500 MB of SAP runtime from the private registry — without valid registry credentials it fails, and the reason then appears in the section, not only in the log.
- Stopping asks first. On SIGTERM
dbstopcloses the database cleanly; on a grown MET/BASE that takes minutes, and killing it means the next start pays for the recovery. A running import breaks off.
On Kubernetes switching is its own toggle: v2.metbase.control.startStop
widens the Role by patch on the scale subresource of this one deployment
(replicas: 1 / 0), never on the deployment itself. Without it the section
shows the state but the buttons stay ineffective.
Choosing the database file¶
Where several .db files sit in the source directory — two customers one after
another, or a fresh copy beside the old one — each row offers Check and
Use.
Check says whether the file will do, and changes nothing:
| Check | Why |
|---|---|
| A bare file name, no path | The value ends up on the entrypoint's command line; /calhelp is hardcoded in the image |
Extension .db |
The server opens the database and finds the .log itself |
| Present in the directory and not empty | Otherwise the server finds nothing at start |
| Header is not ZIP, gzip, 7z, RAR, PDF or SQLite | The most common mistake is a database delivered "packed" |
Untouched for at least METBASE_SETTLE_SECONDS |
A running copy looks complete in any file listing |
| Free space against file size | Recovering an unclean database needs about its own size again — tight is a warning, too little is a refusal |
There is deliberately no positive check for the ASA format: the file format is undocumented, and a guessed signature would either reject valid files or catch nothing.
Use checks first, then asks, then stops the container. DB_NAME is
read exactly once, at start — which is why the swap cannot happen while the
server runs, and why starting again is a separate click. If the server was
running, an import in progress breaks off.
On Kubernetes the selection needs v2.metbase.control.selectFile. The file
name lives in a ConfigMap rather than in the deployment, so the API pod needs
the right to change a ConfigMap instead of a right on the deployment — which
would also permit swapping the image. A helm upgrade resets that ConfigMap
to v2.metbase.databaseFile; where that matters, put the value you want into
the release.
The handover folder _metbase¶
So far the operator puts the database file into a directory that belongs to the
source container alone. Where the lab already writes into the inbox over SMB
or SFTP, that second path is not needed: the reserved folder _metbase inside
the inbox is the handover folder.
lab ──SMB/SFTP──▶ {inbox}/_metbase/ ──mount──▶ /calhelp (source container)
│
└──mount ro──▶ API (list, check, select)
The folder is reserved, like _archive. It cannot be registered as an
inbox slot, nothing can be uploaded or moved into it, and it does not appear in
the folder list. Not tidiness: the inbox processing runs over the registered
slots, and a slot named _metbase would run it over a customer database that a
database server holds open.
The backup leaves it out. backup.exclude_patterns carries
*/inbox/_metbase/* by default. Without it a 12 GB source database would land
in every backup — every one, because restic does deduplicate but the first
backup is paid in full. The file is source material, not operating state, and it
does not change after it is dropped.
Where the inbox lives depends on DMS_STORAGE_MODE:
| Mode | Inbox | Usable as a handover folder |
|---|---|---|
v1-tree |
{filemanager}/inbox — the very tree the lab sees over the share |
yes |
| otherwise | storage/app/inbox inside the API container |
only if that directory is reachable from outside |
The folder is not created automatically. Once:
Compose. Point METBASE_DATA_PATH at the same folder the inbox uses; the
source container then mounts it as /calhelp and the API sees it read-only:
Kubernetes. Use the same claim as the inbox and set
v2.metbase.persistence.subPath to _metbase:
helm upgrade --install calserver calserver/calserver \
--set v2.metbase.enabled=true \
--set v2.metbase.persistence.existingClaim=calinbox-pvc \
--set v2.metbase.persistence.subPath=_metbase \
--set v2.metbase.source.mount=true
The claim has to be ReadWriteMany: the source container and the API pod hold
it at the same time. The SMB CSI PV from
CALINBOX-SMB-OPENSHIFT
is; a ReadWriteOnce claim leaves the API pod stuck in Multi-Attach.
And one operational requirement that is not a switch: the inbox is reachable
for labs — that is its purpose. The handover folder, however, is the source of a
migration that writes into the production database. The share therefore needs
its own ACL on _metbase: whoever accompanies the migration may write there,
not everyone who delivers documents. That is set by the share's administrator,
not by calServer.
It switches itself off¶
The container cannot be forgotten. Once it has run
METBASE_IDLE_STOP_HOURS hours (default 12, in the chart
v2.metbase.idleStopHours) without the installation using the source, the
scheduled task "Stop the MET/BASE source" shuts it down. It lives under
Administration → Scheduled Tasks and only appears where a control channel is
set up.
What counts as use is an opened database connection — from the UI, from an
import window, or from mettrack:import on the CLI. An open browser tab does
not. Where neither a use nor a start time can be established, the container
stays up: shutting down a server nobody can say how long has been running would
be a guess.
METBASE_IDLE_STOP_HOURS=0 disables the task. Whoever does that owns switching
the container off again.
Two probes, and why there are two¶
The Check tab has two buttons, and the difference is the point:
- Test connection asks whether anything answers at the configured address — first at the port, then with the server configuration.
- Test login asks whether the server accepts these credentials. They are checked and not stored.
The reason for the split is the driver: FreeTDS reports a stopped container, a
wrong password and a mismatched protocol version with the same words,
Adaptive Server connection failed. Without the split you restart the
container although the password is wrong. The UI therefore names the classified
cause and the next step next to the result.
The diagnostics report beside them joins what the individual probes each know on their own. The finding it exists for: "the container runs, but the configured file is not in the source directory" — neither the state nor the file list can say that alone.
Troubleshooting¶
| Symptom | Cause | Remedy |
|---|---|---|
| Pod restarts continuously, log reports a missing database | Volume empty or databaseFile wrong |
Put the file into the volume, check the name |
| Startup aborts after a few minutes | Recovery takes longer than the startup probe allows | Raise v2.metbase.startupProbe.failureThreshold (steps of 10 seconds) |
| Migration reports a connection error instead of "no source configured" | METTRACK_DB_HOST still points at the disabled container |
Run deploy.sh again; it clears the value |
| Authentication fails | Wrong source user or password | Check METTRACK_DB_USERNAME / METTRACK_DB_PASSWORD against the password manager |
| Characters arrive garbled | Source character set differs | Adjust METTRACK_DB_CHARSET |
| The "MET/BASE source" section is missing | Role without metbase_source_admin |
Grant the permission in Administration → Roles |
| Section shows the state but no files | Source directory not mounted | Compose: run deploy.sh again. Kubernetes: v2.metbase.source.mount=true (needs ReadWriteMany) |
| Section shows files but "not configured" as state | No control channel | Compose: set up the updater sidecar (UPDATER_TOKEN). Kubernetes: v2.metbase.control.enabled=true |
| "The configured file is not in the directory" | DB_NAME and file name differ |
Point METBASE_DB_NAME / v2.metbase.databaseFile at the real file name |
| The buttons are greyed out | No control channel, or an operation is already running | The line next to the buttons names the reason |
| Start reports "pull access denied" | Registry credentials missing | Set DOCKERHUB_USERNAME/DOCKERHUB_PULL_TOKEN in the .env; on Kubernetes v2.metbase.image.pullSecrets |
| On Kubernetes nothing happens on start (403) | Only the read permission is set | v2.metbase.control.startStop=true |
| The container was off in the morning | Idle stop | Intended. Widen the window via METBASE_IDLE_STOP_HOURS or disable the task |
| "The file was written just now" | The copy is still running, or the timestamp is fresh | Wait for the copy; lower METBASE_SETTLE_SECONDS if need be |
| "That is not a database, it looks like …" | An archive or PDF stored under .db |
Store the database unpacked |
After a helm upgrade the container opens the old file again |
The ConfigMap was reset to the chart value | Set v2.metbase.databaseFile in the release |
_metbase does not appear in the folder list |
Reserved, by design | Nothing. The file list in the "MET/BASE source" section shows its contents |
| "The folder is reserved" when creating an inbox slot | _archive or _metbase as the slot name |
Pick another name |
| The backup grows by several GB | The handover folder is not below inbox/_metbase |
Add the pattern to backup.exclude_patterns |
What gets migrated¶
Which fields the chain MET/BASE → calServer → MET/TEAM carries, and where they diverge, is documented for developers under Field chain MET/BASE → calServer → MET/TEAM.
See also¶
- Fluke MET/TEAM — ongoing integration with the MSSQL successor
- MET/TEAM cutover V1 to V2
- Backup and restore