Chapter 26Lesson 05300–420 min

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.

Checkpoint labSide-by-sideValidationCutoverRollback

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.
Dated baseline (27 August 2026). Modern examples use Nexus Repository 3.95.2-01 with Java 21 as the target/reference line. Legacy OrientDB migration examples intentionally pin the source to the final supported OrientDB line, 3.70.5, and do not pretend that 3.95.x can open an OrientDB data directory.
Tool boundary. Database Migrator changes the database backing a self-hosted deployment and requires Nexus to be shut down; it does not move repository blob content. Instance Migrator is a separate live source→target tool that migrates configuration plus hosted assets and tracks migration state. Choose the tool from the migration topology, not from familiarity with a command.
Recovery checkpoint first. Before any migration, take and verify a supported backup/recovery set for the source database/configuration and blob stores. Keep the source untouched until the target passes validation. Do not copy database files across incompatible Nexus versions, rewrite rows, edit blob metadata, or downgrade an already-upgraded data directory as a rollback shortcut.

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

  1. Target repository definitions and roles will appear after configuration migration.
  2. Hosted artifact bytes will be transferred and retain the source SHA-256.
  3. Proxy cached content is not treated as authoritative hosted content to migrate; proxy behavior is revalidated from configuration/upstream.
  4. Generated format metadata may be recreated and therefore need not be byte-identical.
  5. Source bytes and source metadata will remain unchanged throughout the drill.
  6. 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.json
  • gates.json
  • migrator-state/asset-transfers.json
  • source/target repository and role comparison
  • hosted asset SHA-256 comparison
  • validation-1.json showing blocked cutover
  • validation-2.json showing 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?

Why is the source fingerprint important?

Why is proxy cached content not used as the primary migration success metric?

When should the legacy source be decommissioned?

What does Chapter 27 add after this checkpoint?

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

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.