Maintenance & modifications
Every change to a pool, whether the client, a staff member or the system asks for it, goes through one path: a change row, a guarded job, an AWS or licence-provider call, and an applied row. Billable changes move the billing timeline; sites and licence tiers are logged but never billed.
States
stateDiagram-v2 [*] --> queued: order (card + standing checks) queued --> provisioning: job starts provisioning --> active: step 20 activate provisioning --> failed: max exceptions failed --> provisioning: resume command failed --> terminating: abort command active --> suspended: WHMCS suspend / staff suspended --> active: unsuspend active --> cancellation_pending: cancel request cancellation_pending --> active: cancel_termination (inside window) cancellation_pending --> terminating: due date / abort window passed terminating --> closing_account: instance gone, final invoice closing_account --> closed: CloseAccount succeeded closed --> [*]
Instance state (running, stopped, rebooting) is separate from these lifecycle states and is read live from EC2; the cached copy in dedicated_instances.instance_state only serves list pages.
Sites on the pool
From the app
"Create Site" offers Rochen One (shared) or any of the client's pools. With a pool chosen, the order carries the new product Rochen One Dedicated Site; RochenOne::store() routes it to Dedicated::storeSite(): parent_id = pool, server = the pool's own tblservers row, the effective limits and disk_size quota in custom fields (no configurable options; the tier picker only fills the fields), no container option updates and no rochen_one_logs row. The existing deploy then creates the cPanel account, DNS, quota, LVE limits, Softaculous and mail on the pool's server.
Resource limits, free
A Dedicated Site keeps the Server Resources picker (Tier 1–8 presets) and gains a custom form for speed, pmem, io, iops, ep and nproc; either way the values are applied with the existing applyLVELimits on the pool and written to the site's custom fields. The pool page shows quotas and limits summed against the pool's allocation and instance size and warns when oversubscribed; a single quota larger than the pool's volume is refused. Changes are logged on the site's history and never billed. The client has no WHM or root access.
Delete
Deleting an app site on a pool removes the cPanel account on the pool's server and terminates the row; no container maths, no log row. A site_removed change row triggers a re-tier check.
Manage like Shared
Every site page a shared site has (details, resources, disk, PHP version, monitoring, delete) works for a pool site unchanged; only the price labels disappear.
Licence tiers
RetierDedicatedLicencesJob counts the live Dedicated Site services on the pool (parent_id = pool), applies the rules in config/dedicated.php (cPanel by accounts; Imunify360 by accounts; LiteSpeed by instance size and accounts; CloudLinux, KernelCare, JetBackup, MailChannels fixed), and changes the provider package or key limit when the tier differs from dedicated_instances.licences.
| Trigger | Direction | What happens |
|---|---|---|
Site created, or the daily dedicated:retier-licences reconcile finds a mismatch | up | at once: Manage2 change package / CLN update key limit; tracking options and licences json updated; change_licence row (not billable) |
| Site deleted | down | only after 24 hours below the threshold, to avoid flapping |
| Template applied (resize) | either | LiteSpeed re-evaluated for the new instance size |
| Provider call fails | — | row failed, retried next day; Maintenance ticket after 3 failures. LiteSpeed changes open a ticket until an API is confirmed |
| Daily reconcile finds cPanel accounts on the server that are not app rows | — | Maintenance ticket; tiers follow the app rows |
Endpoints
| Call | Payload | Handled by | Notes |
|---|---|---|---|
PATCH /v1/hosting/{id} | {template} or {diskSize: [{device, size, iops, throughput, mountPoint}]} | Dedicated::updateHosting() | same payloads the AWS server pages already send, so servers/[id]/resources and /disk work unchanged |
PATCH /v1/hosting-configuration/{id} | {command: reboot_server | start_server | stop_server} | DedicatedConfigurationController | synchronous EC2 calls; each writes a change row |
PATCH /v1/hosting-configuration/{id} | {command: cancel_termination} | same | allowed only while cancellation_pending |
GET /v1/hosting-configuration/{id}?command=state | — | existing | live EC2 state, polled by the details card |
GET …?command=provisioning | — | new | plan progress while queued / provisioning / terminating |
GET …?command=history | — | new | the change log, newest first, paginated |
GET …?command=licences | — | new | current tier per software, the account count that drives it, and when it was last checked |
GET …?command=sites | — | new | the sites on the pool with their limits |
HostingPolicy refuses update and upgrade while DedicatedInstance::inFlightFor() or DedicatedServerChange::inFlightFor() is true, the same rule the migration flow uses. Every endpoint is added to public/docs/api.html.
Jobs, guards, rollback
All jobs are unique per hosting on the dedicated queue with the lock dedicated:change:{hosting}. Each starts by moving its change row to applying and ends with applied, failed or rolled_back.
| Job | Guards | Steps | Rollback |
|---|---|---|---|
ResizeDedicatedInstanceJob | can_scale · template belongs to the pool's region group · differs from current · no change in flight · instance running or stopped | stop → wait stopped → ModifyInstanceAttribute type → start → wait running → change applied (new billing span), Templates option updated → LiteSpeed re-tier queued | modify fails → start with the old type, change rolled_back; start fails → change failed, Maintenance ticket, client mail |
ResizeDedicatedDiskJob | grow only · 6 h since the last applied resize_disk · gp3 ratio: throughput ≤ IOPS ÷ 4, IOPS ≤ 500 × GB · within the region's min/max | ModifyVolume → wait past modifying → SSM command: pvresize, lvextend, xfs_growfs /home → change applied, options updated | ModifyVolume failed → nothing changed, failed. Filesystem grow failed → the volume did grow, so the change is applied with an error and a ticket "filesystem not grown" |
| reboot · start · stop | can_reboot · instance state allows it | one EC2 call, synchronous, change row written | none needed |
RetierDedicatedLicencesJob | live count available · tier differs · 24 h hysteresis on the way down | provider change package or key limit → licences json + tracking options → change_licence row (not billable) | provider call failed → row failed, retried; nothing else changed |
RotateDedicatedKeyJob (staff only, dedicated:rotate-key) | instance running | new key pair → SSM command replaces authorized_keys → old key pair deleted → refs updated | SSM command failed → new key deleted, old kept |
Change-log rules
One row per intent
Created with status = requested and the actor (client, admin, system) before any job is dispatched, so a request that never ran is still visible.
Whole configuration
to holds the full resulting configuration, never a delta, so any row can be read on its own and the billing routine needs no replay.
Immutable once applied
applied_at is written once. Mistakes are corrected with a new reconcile row, which is how a manual AWS-side change gets recorded too.
Billable flag
Only create, resize_instance, resize_disk and terminate move the billing timeline. Reboots, key rotations and licence changes are logged but do not start a span.
Audit
Every applied or failed change also writes a tbllogs row; dedicated:show {hosting} prints configuration, plan and log for support.
Drift check
dedicated:reconcile (weekly) compares instance type, volume size, IOPS and throughput against EC2 and opens a Maintenance ticket on mismatch, replacing the legacy matchInstances mails.
Suspend, stop, reboot
| Action | Who | Instance | Service status | Licences | Billing |
|---|---|---|---|---|---|
| Suspend | WHMCS overdue automation or staff | stopped | Suspended | untouched | continues (type and volumes reserved) |
| Unsuspend | WHMCS on payment, or staff | started | Active | untouched | continues |
| Stop | client (can_reboot) | stopped | Active | untouched | continues; the app says so next to the button |
| Start · Reboot | client | started / rebooted | Active | untouched | continues |
Limits and monitoring (proposals)
Instance-size ceiling
Accounts with fewer than 3 paid months, or any overdue history, may run one pool up to m7i.xlarge; established accounts up to m7i.4xlarge; larger sizes or more pools need a staff approval recorded on the client. The store and the resize page hide sizes above the ceiling and say why. Thresholds to agree with Chris and Ben.
New pool alert
Every deployment mails devops with client, account age, fraud score, template, region and hostname, so new accounts get a human look early, as Chris asked.
Transfer and abuse alerts
dedicated:log-data-transfer opens a Maintenance ticket when a pool younger than 30 days passes 1 TB outbound in 7 days, or any pool passes 5 TB in a month; Zabbix keeps the usual host alerts. Staff can suspend from WHMCS, which stops the instance.
Credentials
Each pool's secrets live in SSM for the instance and as one item in the 1Password inbox vault for staff; the deployer can write there and nowhere else.
WHMCS admin
WHMCS still calls the aws module for product 464 (suspend, unsuspend, terminate, the Start/Stop buttons, and a template change on every service save). A minimal guard keeps those calls from touching the legacy pipeline:
aws_isDedicated($serviceId) — true when a dedicated_instances row existsaws_CreateAccount, aws_SuspendAccount, aws_UnsuspendAccount, aws_StartServer, aws_StopServer, aws_AdminServicesTerminate, aws_AdminServicesTabFieldsSavededicated_server_changes row (actor = admin, status = requested) and return success; they never write to the aws queuededicated:apply-requested-changes every minute, dispatching the matching jobconfigdedicatedtemplates.php edits dedicated_templates; service configuration itself is edited in the app or through the API, not in the WHMCS service tabIn the app
Server page
Reuses the AWS server cards: details with live state and Start/Stop, resources with the template picker, one card per volume, Zabbix graphs. Adds a provisioning card (step list while queued, provisioning or terminating), the Sites and Software cards, and a History card fed by ?command=history, styled like Rochen One's history.
Resize pages
servers/[id]/resources and /disk work as they do for MCS; their guard accepts product.line === "dedicated". The review block states that a resize restarts the server and when the new rate starts.
Sites and software cards
New on the pool page: the sites on the pool with a "Create Site" shortcut, and the included software with the current tier of each and the site count that set it. Nothing to buy.
No access page
The client has no root, SSH or WHM. The pool's hosting row has null credentials, like the Shared container; the root password and WHM token live once, on the pool's tblservers row, WHMCS-encrypted. Support reaches the server over SSM or with that credential; the API never exposes it.