Checkpoint Lab — Database and Instance Migration: OrientDB Sunset, H2/PostgreSQL Paths, and Migrator Workflows
Run the migration as an operational change, not a command demo. You will define source and target contracts, predict state changes, pass compatibility gates, migrate synthetic configuration/content, validate target security and bytes, simulate a failed cutover check, and demonstrate rollback to the untouched source.
Learning objectives
- Write explicit source/target migration contracts and RPO/RTO/cutover assumptions before execution.
- Predict database, repository, blob, security, migration-state, and client-endpoint changes before they happen.
- Execute a faithful side-by-side fixture migration with resumable-style transfer evidence and separate generated metadata.
- Validate repositories, roles, assets, checksums, target-only derived state, and client routing before cutover acceptance.
- Demonstrate rollback by proving the source remains unchanged and documenting how target-only writes would be handled.
1. Scenario and migration contract
You operate a synthetic legacy-like source and need a modern side-by-side target. The mandatory lab does not run an actual OrientDB binary or require PostgreSQL. Instead, it models the current Instance Migrator responsibilities so every learner can practice source/target contracts, exclusions, transfer state, and cutover validation. An optional extension maps the same evidence to real disposable Nexus instances.
source:
nexus: "3.70.5-fixture"
database: "OrientDB-fixture"
endpoint: "http://127.0.0.1:18081"
target:
nexus: "3.95.2-01"
java: 21
database: "PostgreSQL-fixture"
endpoint: "http://127.0.0.1:28081"
migrator:
model: "Instance Migrator semantics"
realToolJavaRequirement: 25
cutover:
sourceOfTruthBefore: source
publicationFreezeRequired: true
rollbackSourcePreserved: true
2. Write predictions before execution
- Target repository definitions and roles will appear after configuration migration.
- Hosted artifact bytes will be transferred and retain the source SHA-256.
- Proxy cached content is not treated as authoritative hosted content to migrate; proxy behavior is revalidated from configuration/upstream.
- Generated format metadata may be recreated and therefore need not be byte-identical.
- Source bytes and source metadata will remain unchanged throughout the drill.
- No target endpoint will be accepted until the validation report passes.
3. Build source and empty target fixtures
from pathlib import Path
import hashlib, json, shutil
root=Path('ch26-checkpoint')
if root.exists(): shutil.rmtree(root)
src=root/'source'; tgt=root/'target'; logs=root/'migrator-state'
for p in [src/'db',src/'hosted',src/'proxy-cache',tgt/'db',tgt/'hosted',logs]: p.mkdir(parents=True,exist_ok=True)
artifacts={
'releases/com/example/app/1.0.0/app-1.0.0.jar': b'APP RELEASE 1\n',
'releases/com/example/lib/3.2.1/lib-3.2.1.jar': b'LIB RELEASE 321\n',
}
rows=[]
for path,data in artifacts.items():
p=src/'hosted'/path; p.parent.mkdir(parents=True,exist_ok=True); p.write_bytes(data)
rows.append({'path':path,'sha256':hashlib.sha256(data).hexdigest()})
(src/'proxy-cache/public/example.bin').parent.mkdir(parents=True,exist_ok=True)
(src/'proxy-cache/public/example.bin').write_bytes(b'REFETCHABLE CACHE\n')
config={
'repositories':[
{'name':'ch26-releases','type':'hosted','format':'maven2','blobStore':'ch26-target-file'},
{'name':'ch26-central','type':'proxy','format':'maven2','remoteUrl':'https://repo1.maven.org/maven2/'}],
'roles':[{'id':'ch26-reader','privileges':['nx-repository-view-maven2-ch26-releases-read']}],
'assets':rows,
'manualFollowup':['external LDAP connection fixture','realm ordering fixture']
}
(src/'db/config.json').write_text(json.dumps(config,indent=2)+'\n')
print('SOURCE READY',len(rows),'hosted assets')
4. Fingerprint the untouched source
from pathlib import Path
import hashlib, json
root=Path('ch26-checkpoint'); src=root/'source'
f=[]
for p in sorted(src.rglob('*')):
if p.is_file(): f.append({'path':p.relative_to(src).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()})
(root/'source-fingerprint.json').write_text(json.dumps(f,indent=2)+'\n')
print('source fingerprint entries:',len(f))
5. Compatibility and target-preparation gate
from pathlib import Path
import json
root=Path('ch26-checkpoint')
gates={
'sourceVersionExactly3705': True,
'targetVersionAtLeast3902': True,
'targetDatabaseSupported': True,
'realInstanceMigratorJava25AvailableIfUsingLiveTool': True,
'targetBlobStoresPreCreatedWithExpectedNames': True,
'defaultTargetRepositoriesRemovedOrAccountedFor': True,
'cipherPlanDefinedForSecrets': True,
'backupAndRollbackCheckpointVerified': True,
'publicationFreezePlanWritten': True,
}
assert all(gates.values()), [k for k,v in gates.items() if not v]
(root/'gates.json').write_text(json.dumps(gates,indent=2)+'\n')
print('GATES: PASS')
The exact target-preparation steps are version/workflow-specific. Current OrientDB→self-hosted PostgreSQL guidance includes pre-creating matching blob stores and avoiding conflicting default repository definitions; it also documents special handling for migrator cipher/user configuration and external identity items.
6. Migrate supported configuration
from pathlib import Path
import json
root=Path('ch26-checkpoint'); src=root/'source'; tgt=root/'target'
config=json.loads((src/'db/config.json').read_text())
target_config={
'repositories':config['repositories'],
'roles':config['roles'],
'assets':[],
'generatedMetadata':[],
'manualFollowup':config['manualFollowup'],
'migration':{'configuration':'complete','assetTransfer':'pending'}
}
(tgt/'db/config.json').write_text(json.dumps(target_config,indent=2)+'\n')
print('configuration migrated; hosted assets pending')
7. Transfer hosted assets and record per-asset state
from pathlib import Path
import json, shutil, hashlib
root=Path('ch26-checkpoint'); src=root/'source'; tgt=root/'target'; logs=root/'migrator-state'
config=json.loads((tgt/'db/config.json').read_text()); transfers=[]
for a in json.loads((src/'db/config.json').read_text())['assets']:
sp=src/'hosted'/a['path']; tp=tgt/'hosted'/a['path']; tp.parent.mkdir(parents=True,exist_ok=True)
shutil.copy2(sp,tp); actual=hashlib.sha256(tp.read_bytes()).hexdigest()
status='success' if actual==a['sha256'] else 'failed'
transfers.append({'path':a['path'],'expectedSha256':a['sha256'],'actualSha256':actual,'status':status})
if status=='success': config['assets'].append(a)
(logs/'asset-transfers.json').write_text(json.dumps(transfers,indent=2)+'\n')
config['migration']['assetTransfer']='complete' if all(x['status']=='success' for x in transfers) else 'failed'
(tgt/'db/config.json').write_text(json.dumps(config,indent=2)+'\n')
print('asset transfer:',config['migration']['assetTransfer'])
8. Regenerate target-specific derived metadata
from pathlib import Path
import json
root=Path('ch26-checkpoint'); tgt=root/'target'
config=json.loads((tgt/'db/config.json').read_text())
config['generatedMetadata']=['browse-index:rebuilt','search-index:rebuilt','maven-metadata:regenerated-fixture']
(tgt/'db/config.json').write_text(json.dumps(config,indent=2)+'\n')
print('derived metadata rebuilt')
This represents current migration behavior where some format-specific metadata is regenerated on the target. The generated strings are evidence of target derivation, not copied source binaries.
9. Inject one target validation failure
Simulate a missing external LDAP connection—a documented category that may require manual target configuration in current target-specific migration guidance:
from pathlib import Path
import json
root=Path('ch26-checkpoint'); tgt=root/'target'
config=json.loads((tgt/'db/config.json').read_text())
validation={'repositories':True,'roles':True,'assets':True,'externalLdapConnectionConfigured':False}
(root/'validation-1.json').write_text(json.dumps(validation,indent=2)+'\n')
print('CUTOVER:', 'PASS' if all(validation.values()) else 'BLOCKED')
Expected result: CUTOVER: BLOCKED. This is not a reason
to bypass LDAP or grant anonymous access. Configure the external
identity integration deliberately, then test again.
10. Correct only the missing target-side dependency
from pathlib import Path
import json
root=Path('ch26-checkpoint')
validation=json.loads((root/'validation-1.json').read_text())
validation['externalLdapConnectionConfigured']=True
validation['realmOrderReviewed']=True
(root/'validation-2.json').write_text(json.dumps(validation,indent=2)+'\n')
print('CUTOVER:', 'PASS' if all(validation.values()) else 'BLOCKED')
11. Independent byte-level validation
from pathlib import Path
import hashlib, json
root=Path('ch26-checkpoint'); src=root/'source'; tgt=root/'target'
src_cfg=json.loads((src/'db/config.json').read_text()); tgt_cfg=json.loads((tgt/'db/config.json').read_text())
assert len(src_cfg['repositories'])==len(tgt_cfg['repositories'])
assert len(src_cfg['roles'])==len(tgt_cfg['roles'])
assert len(src_cfg['assets'])==len(tgt_cfg['assets'])
for a in src_cfg['assets']:
sp=src/'hosted'/a['path']; tp=tgt/'hosted'/a['path']
assert tp.exists()
assert hashlib.sha256(sp.read_bytes()).hexdigest()==a['sha256']
assert hashlib.sha256(tp.read_bytes()).hexdigest()==a['sha256']
print('CONTENT VALIDATION: PASS')
12. Prove rollback source is untouched
from pathlib import Path
import hashlib, json
root=Path('ch26-checkpoint'); src=root/'source'
before=json.loads((root/'source-fingerprint.json').read_text())
after=[]
for p in sorted(src.rglob('*')):
if p.is_file(): after.append({'path':p.relative_to(src).as_posix(),'sha256':hashlib.sha256(p.read_bytes()).hexdigest()})
assert before==after
print('SOURCE ROLLBACK CHECKPOINT: UNCHANGED')
13. Client/cutover acceptance
For a live disposable extension, add controlled requests:
- read a known hosted release through the target URL with the least-privilege reader;
- attempt a forbidden write with that reader and require denial;
- publish a new synthetic version only after formal cutover acceptance with a dedicated publisher;
- resolve through the proxy/group endpoint and inspect upstream/cache behavior;
- capture logs/task status and target version/database evidence.
Do not point a production client fleet at the target until these acceptance gates pass.
14. Demonstrate rollback decision
Before any target-only publication, rollback is straightforward: restore traffic/configuration to the still-validated source and leave the failed target isolated for evidence. If target-only writes have already happened, the rollback plan must list each coordinate/digest that would be lost and how it will be replayed from trusted CI/build evidence.
{
"rollback": {
"sourceStillValid": true,
"targetOnlyWrites": [],
"action": "route clients back to source; preserve target for diagnosis"
}
}
15. Evidence packet
source-fingerprint.jsongates.jsonmigrator-state/asset-transfers.json- source/target repository and role comparison
- hosted asset SHA-256 comparison
validation-1.jsonshowing blocked cutovervalidation-2.jsonshowing corrected dependency- post-migration generated metadata/task evidence
- client authorization/read test output (live extension)
- rollback decision and target-only write inventory
16. Cleanup / rollback
After preserving sanitized evidence, delete only the local
ch26-checkpoint fixture. For optional live targets,
keep the old source until the organization’s rollback window expires
and the target has passed operational acceptance. Decommission the
legacy source through a separate approved change; never delete it
merely because the migration tool reported success.
17. Knowledge check
Why did the first cutover validation intentionally fail?
To prove that migration acceptance includes dependencies such as external identity configuration, not just repository and asset transfer.
Why is the source fingerprint important?
It independently proves the rollback source remained unchanged while the target was built and tested.
Why is proxy cached content not used as the primary migration success metric?
Proxy cache is re-fetchable/derived from upstream behavior; hosted content is authoritative internal state. Proxy configuration and behavior still require validation.
When should the legacy source be decommissioned?
Only after target acceptance, the defined rollback window, and an approved decommission change—not immediately after the migrator exits.
What does Chapter 27 add after this checkpoint?
Upgrade planning across Nexus versions, Java/runtime transitions, breaking changes, release notes, compatibility checks, and rollback strategy after the database/instance state is on a supported platform.
18. Chapter summary and bridge to Chapter 27
You have now treated migration as a version-constrained platform change: inventory source state; choose Database Migrator or Instance Migrator from topology; preserve a coherent recovery checkpoint; satisfy PostgreSQL and version gates; move metadata/content through supported semantics; validate repositories, security, derived metadata, actual bytes, and clients; block cutover on unresolved dependencies; and retain a demonstrable rollback source. Chapter 27 builds on this supported state to plan Nexus and Java upgrades without combining too many irreversible changes at once.
Official references and version notes
- Sonatype: What to Choose — Instance Migrator or Database Migrator — current division between live source→target migration and offline database transformation.
- Sonatype: Instance Migrator — supported OrientDB source/target versions, Java requirement, resumability, logging, and asset migration behavior.
- Sonatype: OrientDB to Self-Hosted PostgreSQL Migration — target preparation, migrator cipher, migrated/excluded state, and known limitations.
- Sonatype: Migrating to a New Database — Database Migrator scenarios, offline requirements, H2→PostgreSQL sequence, and post-migration tasks.
- Sonatype: Upgrading to Nexus Repository 3.71.0 and Beyond — OrientDB 3.70.x migration gate, H2/PostgreSQL paths, and Java transition.
- Sonatype: Nexus Repository Upgrade Paths — current version sequencing and breaking-version gates.
- Sonatype: Reverting Back to OrientDB — why the old database is a recovery point only when matching blob state is restored.
-
Sonatype: Install Nexus Repository with PostgreSQL
— database ownership, schema privileges, and
pg_trgmrequirement. - Sonatype: Nexus Repository Database — H2 production limitations, PostgreSQL recommendation, and OrientDB sunset state.
- Sonatype: Nexus Repository 3.70.x Downloads with OrientDB — final OrientDB line and matching migration downloads.
- Sonatype: Download — current Nexus/Database Migrator/Instance Migrator download references.
- Sonatype: System Requirements — current Java/PostgreSQL requirements and supported database expectations.
- Sonatype: Nexus Repository 3.95.x Release Notes — dated current release line used as the modern target reference.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.