Repository navigation
Hyper V Discovery Developer Guide
How the virtual estate (#199 Parts A and B) is built, and the rules a change must keep. The user page is Hyper-V and Failover Cluster discovery; the reasoning is in the design.
In one breath: a read-only PowerShell collector pushes a report per scope. One service (VirtualEstateService) applies it to ten virt_* tables, under the inventory ingest's rules: observed time, complete/partial sections, stale/replay/conflict, a savepoint per section, removal is not deletion. Identity changes are raised for a person. Each connection is a pushed source on job health. The tables are named for the estate, not Hyper-V, so vCenter, Proxmox and Cloud Director can move onto them, and Parts B and C are built once.
ποΈ schema Β· π§ rules Β· π REST Β· π₯οΈ UI Β· π©Ί job health Β· πͺ collector Β· π§ͺ tests
| π¨ | File | What it does there |
|---|---|---|
| ποΈ |
database/freeitsm.sql, includes/db_verify_schema.php, includes/db_verify_indexes.php, api/system/db_verify.php, includes/db_verify_modules.php
|
Ten virt_* tables; children cascade from virt_connections and virt_vms. virt_ maps to Assets |
| π§ | includes/services/virtual_estate.php |
submit(), the six apply_*() sections, decideReview(), mintKey(), freshness()
|
| π |
api/v1/resources/virtual_estate.php, api/v1/lib/routes.php, api/v1/lib/permissions.php, api/v1/spec.json
|
POST /virtual-estate/{id}/reports (only the connection's api_key_id), GET /virtual-estate/{id}; permission virtual_estate
|
| π₯οΈ |
api/assets/hyperv_connections.php, asset-management/settings/hyperv.js, asset-management/settings/index.php, asset-management/settings/manifest.php
|
Assets β Settings β Hyper-V (Cap::ASSETS_HYPERV, sensitive: it mints keys) |
| π₯οΈ |
api/assets/get_servers.php, asset-management/servers/index.php
|
Hyper-V rows (scoped to the analyst's companies), the reports panel, hosts and clusters |
| π©Ί |
includes/job_registry.php (jobRegistryAll()), includes/job_health.php, includes/job_runs.php, api/reporting/job_health.php
|
One virt_push_<id> entry per active connection; trigger push
|
| πͺ | scripts/Invoke-HyperVInventory.ps1 |
The collector |
| π§ͺ | tests/virtual-estate.php |
47 checks - every scenario in criterion 7, REST key gating, job health |
| π§ͺ |
tests/hyperv-collector.php, tests/hyperv-collector/Mock-HyperV.ps1
|
The REAL collector against mocked cmdlets, its reports through the REAL ingest, and sending to the local API (Windows only) |
| π§ͺ |
tests/virtual-estate-load.php, tests/fixtures/virtual-estate/synthetic.php
|
Sizing on the synthetic reference estates; the fixture generator |
| π | samples/hyperv/ |
Fully synthetic example reports and their generator |
| Table | Identity (external_id) |
Notes |
|---|---|---|
virt_connections |
- | One company (tenant_id); api_key_id is the only key accepted; interval, grace, retention |
virt_clusters |
Get-Cluster Id |
quorum type, witness |
virt_hosts |
hardware UUID, or name:<host>
|
identity_kind source / name; cluster_id; node_state
|
virt_cluster_roles |
Get-ClusterGroup Id |
owner host, preferred owners |
virt_vms |
Get-VM Id |
host, cluster, is_clustered (NULL = unknown), role, possible_owners_json (host row ids) |
virt_vm_disks, virt_vm_networks
|
per VM | replaced as a set by a complete section; observed_at guards against older reports |
virt_placements |
- | one open row per VM; a host change closes it and opens another |
virt_reports |
- | every report received and what each section came to |
virt_identity_reviews |
- | open β same / different / superseded |
Every discovered row carries first_seen_at, last_seen_observed_at, not_seen_since and not_confirmed_since.
submit(connection, report)
envelope(): schema_version 1, observed_at with a timezone (not in the future), scope, sections
scope_key = hash of the scope; fingerprint = hash of scope + sections + unreadable
newest applied report for this scope: newer -> stale | same time, same hash, applied -> replayed
same time, different hash -> 409 observation_conflict
same time, same hash, partial -> apply again (retries the failed sections)
JobRun::start('virt_push_<id>', 'push')
one transaction; per section a SAVEPOINT: clusters, hosts, cluster_roles, vms, vm_disks, vm_networks
markUnconfirmed() raiseIdentityReviews() purge() commit
record the report; connection's last_report_at, last_outcome, last_summary
200 applied | 207 partial | 422 failed
-
Upsert by
(connection_id, external_id), reading the connection's rows once per section (FOR UPDATE). An object last observed after this report is skipped (stale_objects), so an older report sent under a different scope can never wind it back. -
Complete marks not-seen, within scope only.
markNotSeen()limits itself to what the report's scope covers - its clusters, and the hosts in them or named as standalone - and skips anything on an unreadable host or cluster (onUnreadable()). -
Unreadable is "not confirmed", never "gone" (
markUnconfirmed()). - Hosts are referenced by NAME inside a report (a VM's host, a role's owner, possible owners), resolved per report against the hosts just applied. That is what lets a host rename keep its VMs.
-
Hardware UUIDs are checked: all-zero, all-F, malformed, or the same UUID on two hosts in one report β
name:identity. -
A bad item fails its section, rolled back to the savepoint, not skipped. Unknown VMs in
vm_disks/vm_networksfail the section rather than dropping data.
raiseIdentityReviews() pairs a row that is not seen with a live row of the same kind, same name (case-insensitive), first seen at or after the old one was last seen. For hosts, it also pairs two name-identity hosts in the same cluster. Raised once per pair.
decideReview(same) keeps the old row - its id, first seen and placements - and gives it the new identity and the new row's current facts. It moves the new row's disks, adapters, placements and anything pointing at it, then deletes the new row. It refuses (review_changed, case closed as superseded) if the old row is seen again or the new one goes before the decision.
jobRegistryAll($conn) = the scheduled jobs + one entry per active connection: key virt_push_<id>, 'pushed' => true, interval and grace from the connection, its own name (name_params). Runs are recorded by submit() with trigger push, and the freshness rule counts push beside cron and web. An unreadable host makes the run partial.
If the connections table cannot be read, health shows a Pushed sources: unknown entry, never silence.
Windows PowerShell 5.1 syntax throughout (no ??, no ternary). It only calls Get-* and CIM reads. Per cluster it reads Get-Cluster, Get-ClusterQuorum, Get-ClusterNode and Get-ClusterGroup. For each "Virtual Machine" resource it reads VmID (Get-ClusterParameter) and its possible owners (Get-ClusterOwnerNode). Per host it reads the UUID (Win32_ComputerSystemProduct), the OS, Get-VMHost, Get-VM, disks (Get-VMHardDiskDrive + Get-VHD) and adapters (+ VLAN). A host or cluster that throws goes into unreadable. A VM detail that throws is null.
-TestAccess records every read through Try-Read and prints the table, exiting 2 if anything was refused. -Redact uses an HMAC keyed on %ProgramData%\FreeITSM\redact.key: names become host-1a2b3c4d, ids become stable fake GUIDs, error text is replaced outright.
Reports go to <ApiUrl>/api/v1/index.php?path=/virtual-estate/<id>/reports with Authorization: Bearer, which works without URL rewriting.
includes/services/virtual_estate_cmdb.php (VirtualEstateCmdbService), with api/assets/hyperv_cmdb.php (the panel) and api/cmdb/discovery_source.php (the CI's Discovered card).
| Table | What it is |
|---|---|
virt_cmdb_mappings |
per connection: classes per kind, the four relationship types, sync_names, enabled
|
cmdb_object_sources |
bindings as periods - one live per CI and per object, start_source / end_reason (unbound, ci_deleted, rebound). Same shape as asset_ci_links
|
cmdb_relationship_sources |
the relationships the sync created - the only ones it ever changes or removes |
virt_cmdb_decisions |
create_new, leave_unbound, ci_deleted - what the sync must not decide alone |
status() decides each object: bound β keep (rename if allowed); open identity review as the new object β waiting; a decision β as decided; a same-name unbound CI of the mapped class in the company β match (never bound by name); otherwise create.
sync(): one transaction, a savepoint per CI (a class requiring properties fails that object only), then syncEdges(). It wants a set of edges from the virt rows and diffs it against the edges it owns. It does not adopt an identical edge a person drew, and it keeps owned edges of objects that are not seen. Workflow events fire after the commit. VirtualEstateService::submit() calls it after the report's own commit, so a CMDB problem never loses a report.
Hooks: CmdbService::deleteObject() β ciDeleted(); the connection delete in api/assets/hyperv_connections.php β connectionDeleted() (live bindings end as connection_deleted, rows kept - forCi() LEFT JOINs the connection; mapping, decisions and cmdb_relationship_sources deleted, as none has a foreign key); VirtualEstateService::rebind() β rebound() (the old row keeps its binding; a binding only the new row had moves; if both had one, the new one ends as rebound); purge() skips bound objects via boundIdsSql(); cmdbMemberState() β memberState() and cmdbPossibleOwners() β possibleOwnerCis() feed Part C.
π§ͺ tests/virtual-estate-cmdb.php - 32 checks, from the preview to unbinding.
-
TRAP:
Ris an alias. The redaction helper was first calledR.ris PowerShell's alias forInvoke-History, and an alias beats a function, so everyR 'host' $nameranInvoke-History. It isMasknow. CheckGet-Alias <name>before naming a short function. -
TRAP: PowerShell scoping is dynamic. A mocked cmdlet that read
$uuidsaw the collector's$uuidvariable, not the mock's own table. The mock's tables are named$Mock*for that reason. -
TRAP: comma binds tighter than
+.@('10.0.0.' + $n, 'fe80::1')is ONE string. Parenthesise. -
TRAP:
jobRegistry()is the scheduled jobs only. Anything listing or looking up jobs must usejobRegistryAll($conn). -
TRAP: Part B binds CIs to virt row ids.
decideReview()keeps the old row for that reason, andpurge()skips a bound object. - TRAP: a person who deletes a relationship the sync owns sees it come back on the next report - discovery is the authority for its own edges. Unbinding the object is how to stop that.
-
TRAP:
api/system/db_verify.php$primaryKeys. A new table whose key is notidmust be listed there, or Database Verification fails to create it.virt_cmdb_mappings,cmdb_relationship_sourcesandvirt_cmdb_decisionsare. -
TRAP: the merge guard reads every
host_idcolumn as an asset reference, because Software's is one.virt_vms.host_idandvirt_placements.host_idare inAssetMergeService::NOT_REFERENCES. -
Size. On this dev box: 800 VMs β 1 s, 8,000 VMs β 9 s in one 7.5 MB report (
tests/virtual-estate-load.php). Recommend-ReportPer Cluster.
The collector's reads have only met mocked cmdlets. The property names it relies on - IsClustered, MemoryAssigned, OwnerGroup.Id, the VmID parameter, Win32_ComputerSystemProduct.UUID - are Microsoft's documented ones. Whether they hold on every Windows Server version, and whether the narrower permissions are enough, can only be confirmed on a real estate. The user page says so.
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
-
Architecture
- β³ π§Ή Root folder tidy (URL routing)
- β³ π Database Integrity (FKs & constraints)
- β³ π οΈ Database Verification β Developer Guide
- β³ π§© Service Layer (UI + API share code)
- β³ πͺ€ Code traps (the TRAP: comments)
- β³ π Commenting for the Engineering wiki
- β³ π οΈ Engineering wiki scanner - Developer Guide
- β³ π Service-Layer progress
- π€ Contributing (read before a pull request)
- π§ͺ Developer tests
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- π Date & Time Formats
- π± Currencies
- π§ The sidebar (optional module rail)
- π§© Preference templates
- π Display order (modules, Watchtower cards) β Developer Guide
- Theming & Dark Mode
- ποΈ Recent β getting back to what you were doing
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
-
MobileβFriendly
- β³ π« Mobile: Tickets
- β³ π» Mobile: Assets
- β³ π Mobile: Calendar
- β³ π Mobile: Knowledge
- β³ π¦ Mobile: Service Status
- β³ πΌ Mobile: Watchtower
- β³ π§© Mobile: Problem Management
- β³ π Mobile: Change Management
- β³ πΏ Mobile: Software
- β³ β Mobile: Tasks
- β³ π Mobile: Forms
- β³ π Mobile: Contracts
- β³ π Mobile: Domains
- β³ π Mobile: People
- β³ π Mobile: Projects
- β³ ποΈ Mobile: Files
- β³ π Mobile: LMS
- β³ πΊοΈ Mobile: CMDB
- β³ πΊοΈ Mobile: Network Mapper
- β³ π§ Mobile: Process Mapper
- β³ βοΈ Mobile: Workflow
- β³ π₯οΈ Mobile: System
- β³ π Mobile: Reporting
- β³ π Mobile: System Wiki
- β³ π Mobile: Self-Service Portal
- β³ π§° Mobile: Techniques & Tricks
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- β³ π‘οΈ CSRF protection (S4) β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- π CardDAV contact sync
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π Proposal: custom fields, inventory import and change evidence (#194)
- β³ π οΈ Custom fields, evidence + inventory - review (#195-#197)
- β³ π οΈ Long forms in a task's Work view - review (#198)
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π REST API: Domains
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ π·οΈ REST API: Cost centres
- β³ π§βπΌ REST API: Analysts
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ π οΈ Inbox.js map β Developer Guide
- β³ π οΈ Tickets settings β Developer Guide
- β³ π Ticket workspace - design (PR #206)
- β³ π Rota copy and paste β Developer Deep Dive
- β³ β Checklists & SOPs
- β³ βοΈ Mandatory fields
- β³ π§Ύ Custom ticket fields
- β³ π·οΈ Ticket categories
- β³ π₯ Assigning tickets to a team, and escalation
- β³ π’ One board across every company
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ π Attachment size limits and large emails
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
-
β³
βοΈ Telegram channel - β³ β CSAT company scope and filters β Developer Guide
- β³ π₯ Microsoft Teams channel
- β³ π¨οΈ Mattermost channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ β Record previews
- β³ π Ticket notes: internal or shared
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ π¨ Telling the analyst a ticket is theirs
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π’ Ticket numbering
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ π Confidential tickets
- β³ π₯ Portal managers
- β³ π Who has seen a ticket
- β³ π Reading long tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ ποΈ The folder pane
- β³ π½ Just my tickets, or no closed ones
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- β³ π Case reports - a ticket and its work as one PDF
- β³ π Scheduled work in your own calendar
- Problem Management
- Tasks
- π Projects
-
Assets
- β³ π’ Moving an asset between companies
- β³ π Shared asset locations
- β³ π§βπΌ Assigning assets to analysts
- β³ π Warranty and lease alerts
- β³ π Leases
- β³ π Saved table views
- β³ π¨οΈ Recording anything, and importing it
- β³ π§© Asset type layouts
- β³ π Inventory freshness
- β³ π Linked configuration item
- β³ π Connected assets
- β³ π Asset review
- β³ π·οΈ QR asset labels
- β³ π Who holds what, and handover documents
- β³ π₯οΈ The inventory agent (PowerShell)
- β³ ποΈ Proxmox VE servers
- β³ βοΈ VMware Cloud Director servers
- β³ πͺ Hyper-V and Failover Clusters
- β³ π Linking equipment to tickets
- β³ βοΈ Follow-up tasks on a ticket
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
-
Forms
- β³ π¨ The form designer β Developer Guide
- β³ π Layout & the grid β Developer Guide
- β³ ποΈ Collections β grouping submissions
- β³ π Submissions as PDFs
- β³ β‘ What happens next β a form's own actions
- β³ π οΈ Sections & conditional logic β Developer Guide
- β³ π οΈ Lookup fields β Developer Guide
- β³ π‘οΈ Catalogue request approvals
- People
- Domains
- ποΈ Files
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- π‘ Zabbix (monitoring)
- π¬ Microsoft Teams alerts
- π 3CX (phone system)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ π’ One board across every company
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)
- What this is
-
π Bugs resolved
- β³ π³ The Docker image was missing PHP extensions
- β³ π A failed DNS lookup read as a missing record
- β³ π A link could change data
- β³ π Access outlived its removal
- β³ π Knowledge articles could run scripts
- β³ π Knowledge writes ignored permissions
- β³ π Company boundaries (Oct 2026)
- β³ π Smaller gaps (Oct 2026)
- β³ π Escaped text could leave its attribute
- β³ π Task descriptions were not cleaned
- β³ π Task documents ignored company scope
- β³ π Repeating tasks ignored company scope
- β³ π¬ #183 Mailbox checks overlapped
- β³ π #188 Transfer lock shown off when unknown
- β³ β #184 Subtask names and Overdue
- β³ π§© #186 Teams: the buttons did nothing
- β³ π Text in a change could run script
- β³ π AI settings changed without the AI permission
- β³ πͺ #178 Deleting an analyst failed
- β³ π« A team with no departments showed 0 tickets
- β³ π Projects: adding a phase did nothing on Save
- β³ π Leavers could still sign in
- β³ πΌοΈ Logo and courses broke on Apache with PHP-FPM
- β³ π’ Chat tickets ignored your ticket numbering
- β³ π Dates shown as a dash, or in server time
- β³ π Assets β Users showed people from other companies
- β³ π Restricted analysts could read other modules' data
- β³ πΌοΈ Replies with a picture in the thread failed to send
- β³ π Reply attachments never reached the customer
- β³ π οΈ Outbound email attachments β Developer Guide
- β³ π A global SSO provider was missing from the portal
- β³ π Behind a proxy, the SSO redirect said http
- β³ βοΈ The portal tagline moved when you saved it
- β³ π¨ The portal settings screen forgot what you saved
- β³ π‘οΈ The approvals inbox said "Error" and nothing else
- β³ π A table's answers were missing from the PDF
- β³ β A single-select column let you tick every option
- β³ π The portal ignored a form's field widths
- β³ π The tasks board stopped taking clicks
- β³ ποΈ #121 The index list is out of date after upgrading
- β³ π #133 The calendar subscription was empty
- β³ π #131 Tasks always reopened on the board
- β³ π₯ #129 Every page returned HTTP 500 after upgrading
- β³ π³ #127 A PHP warning above the System page
- β³ π #126 Notes stamped with the server's clock
- β³ π Storing every date in UTC
- β³ πͺ The portal was down for everyone signed in
- β³ βοΈ #120 Workflow notes could never be written
- β³ βοΈ #123 Three errors when running Database Verification
- β³ π #122 The description box was a stub in the corner
- β³ π£ Demo data deleted real accounts
- β³ π #117 Sign-in redirected to the wrong address
- β³ π¨ #108 The priority dot was invisible
- β³ β±οΈ #116 Time logged from the right-click menu
- β³ π #114 API keys refused by our own guard
- β³ ποΈ #110 Assigning a task told nobody
- β³ πͺ #107 Signed out while still working
- β³ π #103 "Share with Requester" reached nobody
- β³ π #102 Search found nothing for hyphens
- β³ πͺ #101 Source code editor opened behind
- β³ βοΈ #88 Subtasks could not be ticked off
- β³ π» #84 Asset deep link selected nothing
- β³ π« #79 A new ticket arrived with no status
- β³ π§ #79 A ticket from email did not say so
- β³ π #78 Bell opened to nothing
- β³ π¬ #77 Mail only collected from Inbox
- β³ π #74 The default password could not be changed
- β³ π¦ #70 Renaming an impact level
- β³ π€ #67 App-only mailboxes could not send
- β³ π #45 Verify only ever worked for Microsoft
- β³ π #45 IMAP reported as not authenticated
- β³ βοΈ An email template stopped escaping itself
- β³ π The portal dashboard showed the wrong time
- β³ π’ The folder said 99 and the list showed 96