diff --git a/bin/fk-lab-fixture.php b/bin/fk-lab-fixture.php index 810c032c5c..341c659c05 100644 --- a/bin/fk-lab-fixture.php +++ b/bin/fk-lab-fixture.php @@ -260,7 +260,6 @@ function ex(\PDO $pdo, string $sql): int 'snapinJobs' => 'sjHostID', 'tasks' => 'taskHostID', 'hostAutoLogOut' => 'haloHostID', - 'hostScreenSettings' => 'hssHostID', 'groupMembers' => 'gmHostID', 'snapinAssoc' => 'saHostID', 'printerAssoc' => 'paHostID', diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 0646248589..1a55cf06b5 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -175,6 +175,7 @@ 'WindowedStats' => 'Audit', 'Retention' => 'Audit', 'Blame' => 'Audit', + 'FOGLdap' => 'Net', 'FOGFTP' => 'Net', 'FOGSSH' => 'Net', 'FOGURLRequests' => 'Net', @@ -200,6 +201,50 @@ // and the task-completion report. Util is for things belonging to no // subsystem at all. 'SecureBootState' => 'Boot', + // Agent, not Boot: fog-agent is the management client, not the netboot + // path. Both extend FOGBase directly -- Enrollment is the policy for who + // gets a client certificate, Principal is the pure verifier that turns a + // presented certificate back into a host -- so ancestry cannot place + // them, and they are one subsystem the way the Boot classes are. + 'Enrollment' => 'Agent', + 'Principal' => 'Agent', + 'Token' => 'Agent', + 'State' => 'Agent', + 'Snapins' => 'Agent', + 'SoftwareSet' => 'Agent', + // The two writers for what an agent reports about its own host + // (design 0006). Named *Facts rather than Inventory and Software + // because both of those already name an Items class -- these write + // those rows, they are not those rows. + 'InventoryFacts' => 'Agent', + 'SoftwareFacts' => 'Agent', + // The writer for what an agent reports about who is logged on (design + // 0008). Named UserSessions rather than UserTracking because that name + // is already an Items class for the legacy event table -- this writes + // hostUserSession rows, it is not either of those rows. + 'DirectoryFacts' => 'Agent', + 'DirectoryPlacement' => 'Agent', + // The join half of directory membership (design 0009 section 6): the + // one class that decides whether a credential leaves this server. + 'DirectoryJoin' => 'Agent', + // The writer for the links a host is on, and the class that asks an + // awake agent to broadcast a wake for a sleeping neighbor (design + // 0011). Network and Wake would both be far too general as Items + // names; these write hostNetwork and agentWake rows. + 'NetworkFacts' => 'Agent', + 'WakeRelay' => 'Agent', + // The writer for what an agent reports about its installed printers + // (design 0010). Same naming reason: Printer is already an Items + // class for the assignable printer -- this writes hostPrinter and + // hostSpooler rows, it is not that row. + 'PrinterFacts' => 'Agent', + 'PrinterSet' => 'Agent', + // The second writer for hosts.hostSbState (design 0012). Named + // SecureBootFacts and not SecureBootState because THAT name is the Boot + // class holding the six state names -- this reports observations into + // that vocabulary, it does not define it. + 'SecureBootFacts' => 'Agent', + 'UserSessions' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/bin/schema-manifest.php b/bin/schema-manifest.php index 99d8105bb8..2dad7ddc3e 100644 --- a/bin/schema-manifest.php +++ b/bin/schema-manifest.php @@ -419,15 +419,29 @@ function render($value, $indent = 1) $A[$t][$i] = $to; } } - // Tables the NEW side declares it dropped on purpose. Keyed lowercase so - // the lookup matches the comparison, which is case-insensitive because - // MySQL's own table-name casing depends on the server's filesystem. + // Tables and columns the NEW side declares it dropped on purpose. Keyed + // lowercase so the lookup matches the comparison, which is + // case-insensitive because MySQL's own table-name casing depends on the + // server's filesystem. + // + // An entry with a `column` retires that one column and leaves the table + // alone; without one it retires the whole table. Both exist because a + // rebuild drops both kinds, and the alternative to declaring a dropped + // column is a permanent difference in this output -- which trains + // whoever reads it to skim past differences, and the next real one goes + // with it. $retired = []; + $retiredCols = []; foreach ((array)($b['retired'] ?? []) as $r) { $t = strtolower($r['table'] ?? ''); if (!$t) { continue; } + $c = strtolower((string)($r['column'] ?? '')); + if ('' !== $c) { + $retiredCols[$t . '.' . $c] = (string)($r['reason'] ?? ''); + continue; + } $retired[$t] = (string)($r['reason'] ?? ''); } @@ -451,7 +465,21 @@ function render($value, $indent = 1) } $gone = array_diff($cols, $B[$table]); $added = array_diff($B[$table], $cols); - foreach ($gone as $c) { + foreach ($gone as $i => $c) { + if (isset($retiredCols[$table . '.' . $c])) { + // Reported, not silenced, exactly as for a retired table. + printf( + "RETIRED COLUMN %s.%s -- %s\n", + $table, + $c, + $retiredCols[$table . '.' . $c] ?: 'no reason recorded' + ); + // Dropped from $gone as well, so the rename heuristic below + // does not then read a deliberate drop plus an unrelated + // addition as one renamed column. + unset($gone[$i]); + continue; + } printf("MISSING COLUMN %s.%s\n", $table, $c); $found++; } diff --git a/bin/upgrade-rehearsal.php b/bin/upgrade-rehearsal.php index ac2b86c33c..def400cd34 100644 --- a/bin/upgrade-rehearsal.php +++ b/bin/upgrade-rehearsal.php @@ -595,7 +595,6 @@ function seedRow($label, $table, array $values) seedRow('snapinAssoc <- host and snapin', 'snapinAssoc', ['saHostID' => 900001, 'saSnapinID' => 900001]); seedRow('printerAssoc <- host and printer', 'printerAssoc', ['paHostID' => 900001, 'paPrinterID' => 900001]); seedRow('inventory <- host', 'inventory', ['iHostID' => 900001]); - seedRow('greenFog <- host', 'greenFog', ['gfHostID' => 900001, 'gfHour' => 1, 'gfMin' => 0]); seedRow('powerManagement <- host', 'powerManagement', ['pmHostID' => 900001]); seedRow('imageGroupAssoc <- image and storage group', 'imageGroupAssoc', ['igaImageID' => 900001, 'igaStorageGroupID' => 900001]); seedRow('snapinGroupAssoc <- snapin and storage group', 'snapinGroupAssoc', ['sgaSnapinID' => 900001, 'sgaStorageGroupID' => 900001]); @@ -1067,8 +1066,8 @@ function seedRow($label, $table, array $values) $tables = ['hosts', 'hostMAC', 'groupMembers', 'snapinAssoc', 'printerAssoc', 'moduleStatusByHost', 'inventory', 'tasks', 'scheduledTasks', 'images', 'nfsGroups', 'nfsGroupMembers', 'users', 'snapinJobs', 'snapinTasks', - 'multicastSessions', 'multicastSessionsAssoc', 'greenFog', - 'hostScreenSettings', 'hostAutoLogOut', 'powerManagement', 'taskLog', + 'multicastSessions', 'multicastSessionsAssoc', + 'hostAutoLogOut', 'powerManagement', 'taskLog', 'sites', 'siteHostMembers', 'siteUserMembers', 'siteGroupMembers']; printf("census %s (schema %d)\n", $db, $runner->version()); foreach ($tables as $t) { diff --git a/docs/adr/0023-activity-is-a-view-and-retention-is-a-registry.md b/docs/adr/0023-activity-is-a-view-and-retention-is-a-registry.md index 755b8621d0..23447fa62e 100644 --- a/docs/adr/0023-activity-is-a-view-and-retention-is-a-registry.md +++ b/docs/adr/0023-activity-is-a-view-and-retention-is-a-registry.md @@ -358,7 +358,12 @@ either companion ADR. 6 and 7 are gated, deliberately. - A `report.view` holder stops getting a movement log for named employees as a side effect. Signed off and shipped as item 1; a role holding `report.view` loses user tracking on deploy and is re-granted deliberately. -- Retention is one mechanism for four tables rather than four mechanisms. +- Retention is one mechanism for five tables rather than five mechanisms. + (Four when this was written. `hostUserSession` joined in schema 422 -- + design 0008's agent-reported user sessions -- which is the registry + working as intended: a new table that accumulates rows per host declares + its own setting and date column rather than growing unbounded while + somebody notices later.) - Upgraded servers keep every existing `userTracking` row until an administrator decides otherwise, and are told that they hold them. - `history`'s existing prose rows render in the new viewer with empty subject diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index 94396e9e59..8618b6e445 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**108 of the map's 123 relationships are declared.** The other 15 are not +**122 of the map's 137 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index c0f33dc825..7e27b24709 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -224,7 +224,7 @@ today and the class where PHP already agrees. ### 1:1 and 1:N satellites — CASCADE. Agreed. -`inventory`, `hostScreenSettings`, `hostAutoLogOut`, `powerManagement`, +`inventory`, `hostAutoLogOut`, `powerManagement`, `greenFog`, `apiTokens`, `userAuths`, `nfsGroupMembers`, and the plugins' `LDAPGroups`, `OIDCGroups`, `oidcIdentity`. @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 123 relationships live in them. +the survey and 25 of the map's 137 relationships live in them. ### Direction is the whole rule @@ -877,10 +877,8 @@ snapinAssoc saHostID -> hosts saSnapinID -> snapins printerAssoc paHostID -> hosts paPrinterID -> printers moduleStatusByHost msHostID -> hosts msModuleID -> modules inventory iHostID -> hosts -hostScreenSettings hssHostID -> hosts hostAutoLogOut haloHostID -> hosts powerManagement pmHostID -> hosts -greenFog gfHostID -> hosts ``` All CASCADE, and **nothing an admin can see changes**: `deletemass()` diff --git a/docs/development/group-split.md b/docs/development/group-split.md index 6116f0130e..b76aa59925 100644 --- a/docs/development/group-split.md +++ b/docs/development/group-split.md @@ -46,7 +46,6 @@ grep -n 'function ' packages/web/src/Items/Group.php | `removeSnapin()` | 293 | `deletemass` | over membership | | `setSnapinOrder()` | 316 | `saSequence` | `new Host()` per member | | `addModule()` | 346 | `moduleStatusByHost` | one per host × module | -| `setDisp()` | 401 | delete-all + insert | one per host | | `setAlo()` | 436 | delete-all + insert | one per host | | `addImage()` | 501 | `hosts.hostImage` | one `UPDATE ... IN` | | `setAD()` | 1100 | five `hosts` columns | one `UPDATE ... IN` | diff --git a/docs/release/1.6.0-release-notes.DRAFT.md b/docs/release/1.6.0-release-notes.DRAFT.md index 61e31db068..89b77de06a 100644 --- a/docs/release/1.6.0-release-notes.DRAFT.md +++ b/docs/release/1.6.0-release-notes.DRAFT.md @@ -220,6 +220,64 @@ pre-upgrade *dump* — it is not a schema rollback. ## Breaking changes +- **Display Manager is removed, and its per-host screen settings are deleted.** + The module reset a client's resolution to a fixed size at logoff and at + startup. That is the wrong layer now: Windows has honored each monitor's + own preferred mode for a decade, a fixed resolution pushed over the top of + it is wrong on every machine whose panel is not the size the setting names, + and a laptop that docks changes its answer twice a day. + + The upgrade drops the `hostScreenSettings` table, the `displaymanager` + module and its per-host answers, and the four `FOG_CLIENT_DISPLAYMANAGER_*` + settings. **The per-host width, height and refresh values are not + recoverable after the upgrade** — take the backup the warning above asks + for if you want them. The `hostscreensetting` API route goes with them, so + tooling that reads it must stop. + + Nothing replaces it. The new agent does not implement it and will not. + +- **Directory Cleaner, User Cleanup, Client Updater and Green FOG are removed, + and their tables with them.** All four were already inert: the server had + stopped listing them among its client modules, so a legacy client that asked + for one got "unknown module" back, no endpoint served them, and their + `FOG_CLIENT_*_ENABLED` settings rendered as checkboxes nothing read. What + survived was stored opinion — a `modules` row each and a per-host on/off + answer for every host you own. + + The upgrade drops the `dirCleaner`, `userCleanup`, `clientUpdates` and + `greenFog` tables, the three module rows and their per-host answers, and the + `FOG_CLIENT_DIRECTORYCLEANER_ENABLED`, `FOG_CLIENT_USERCLEANUP_ENABLED` and + `FOG_CLIENT_CLIENTUPDATER_ENABLED` settings. Green FOG lost its module back + in 1.6's earlier schema work; this finishes the job by dropping the table + that was left behind. + + Three of the four tables are empty on any server that ran these modules, + because 1.6 had already removed every page that wrote them. `userCleanup` is + the exception: it holds the names FOG seeds on every install — `admin`, + `guest`, `administrator`, `HelpAssistant`, `ASPNET`, `SUPPORT_`. **If you + added your own names to that list they are not recoverable after the + upgrade.** + + Nothing replaces Client Updater as a module: the new agent updates through + the MSI at a stable URL plus a snapin filtered on the agent version each host + already reports. For deleting paths or stale profiles across a fleet, use a + snapin — unlike these modules, a snapin returns an exit code and its output, + so you can see what happened. Green FOG's scheduling is Power Management. + +- **Auto Log Out is kept, and gains a warning you can set.** It is the one + legacy client module the new agent reimplements rather than drops: the + per-host time, the global `FOG_CLIENT_AUTOLOGOFF_MIN` default and the + five-minute floor all behave exactly as they did. New alongside them is + **FOG_CLIENT_AUTOLOGOFF_WARN** — how many seconds before the log out the + user is told, on the same Auto Log Out settings tab, defaulting to 60. Set + it to 0 to log the user out with no warning. + + The warning is a message box in the user's own session rather than the old + client's countdown window with a background image, because the agent is a + service and a service has had no visible desktop since Vista. + `FOG_CLIENT_AUTOLOGOFF_BGIMAGE` is removed; nothing has read it since the + legacy client stopped shipping. + - **The `site` plugin's own tables are retired** once its data has moved into core's `sites` table (see the one-way-upgrade warning above). Custom tooling querying the old plugin tables directly must repoint at the core tables. If @@ -312,7 +370,7 @@ pre-upgrade *dump* — it is not a schema rollback. - **The group page's push-to-all controls are gone.** Setting an image, kernel, kernel arguments, init, primary disk, product key, BIOS/EFI exit - type, AD details, printer level, screen resolution, auto-logout or hostname + type, AD details, printer level, auto-logout or hostname enforcement from a group applied the value **once**, to whichever hosts were members at that moment. A host added afterward did not get it; a host removed kept it. That was always true and was never visible. @@ -414,8 +472,8 @@ pre-upgrade *dump* — it is not a schema rollback. The fields it covers are the ones the group page pushed: image, kernel, kernel arguments, primary disk, init, BIOS and EFI exit type, product key, - printer management level, the AD settings, hostname enforcement, auto-logout - and screen resolution. + printer management level, the AD settings, hostname enforcement and + auto-logout. **For plugin authors:** `HOST_MASSEDIT_FIELDS` and `HOST_MASSEDIT_APPLY` let a plugin put its own field in that form with the same three states, and diff --git a/lib/common/functions.sh b/lib/common/functions.sh index 71f81089a7..389298b64c 100755 --- a/lib/common/functions.sh +++ b/lib/common/functions.sh @@ -5749,6 +5749,14 @@ _installNodeCertSigner() { echo "PKI_SB_CA_CERT=${sbca}" echo "PKI_SB_CA_KEY=$(_pkiZoneDir secureboot)/ca/.fogSBCA.key" fi + # The agent zone, when it exists (createAgentIntermediateCA runs + # from createSSLCA, so a master that has run this installer once has + # it). Same gate as the Secure Boot pair: a path that is not there is + # not a capability. + if [[ -f "$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" ]]; then + echo "PKI_AGENT_CA_CERT=$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" + echo "PKI_AGENT_CA_KEY=$(_pkiZoneDir agent)/ca/.fogAgentCA.key" + fi echo "PKI_STAGING=${stagedir}" } > "$conf" chown root:root "$conf" >>$error_log 2>&1 @@ -5892,6 +5900,14 @@ _installPkiAdminHelper() { echo "PKI_SB_CA_KEY=$(_pkiZoneDir secureboot)/ca/.fogSBCA.key" fi echo "PKI_SETTINGS=${fogprogramdir}/.fogsettings" + # The agent zone, when it exists (createAgentIntermediateCA runs + # from createSSLCA, so a master that has run this installer once has + # it). Same gate as the Secure Boot pair: a path that is not there is + # not a capability. + if [[ -f "$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" ]]; then + echo "PKI_AGENT_CA_CERT=$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" + echo "PKI_AGENT_CA_KEY=$(_pkiZoneDir agent)/ca/.fogAgentCA.key" + fi echo "PKI_STAGING=${stagedir}" } > "$conf" chown root:root "$conf" >>$error_log 2>&1 @@ -8132,6 +8148,12 @@ emitNginxPhpBody() { # PHP_AUTH_USER/PHP_AUTH_PW were never populated and basic auth # could not succeed. echo " fastcgi_param HTTP_AUTHORIZATION \$http_authorization;" >> "$1" + # fog-agent authenticates with a client certificate; PHP needs the + # verdict and the certificate itself (URL-escaped: the raw form has + # newlines, which a fastcgi param cannot carry). Empty for everyone + # else, and Agent\Principal treats empty as "no certificate". + echo " fastcgi_param SSL_CLIENT_VERIFY \$ssl_client_verify;" >> "$1" + echo " fastcgi_param SSL_CLIENT_CERT \$ssl_client_escaped_cert;" >> "$1" echo " fastcgi_buffers 16 16k;" >> "$1" echo " fastcgi_buffer_size 32k;" >> "$1" } @@ -8589,7 +8611,7 @@ _customPkiPair() { _pkiZoneDir() { local root case "$1" in - root|web|client|secureboot) root="$(_pkiRootDir)" ;; + root|web|client|secureboot|agent) root="$(_pkiRootDir)" ;; *) return 0 ;; esac echo "${root}/$1" @@ -9427,6 +9449,39 @@ EOF chmod 0644 "${outdir}/${certfile}" >>$error_log 2>&1 return $st } +# The agent zone: an intermediate that issues CLIENT certificates to +# fog-agent installs, through fog-sign-node-cert's agent type. Its own zone +# rather than a use of the Web CA for two reasons the web zone's own notes +# make clear: the Web CA may be one the admin brought (a public CA issues no +# client certificates at all), and an EKU on a CA bounds what it can issue, +# so clientAuth here means nothing from this zone can ever pose as a server +# however its leaf is written. Always minted under the FOG root -- it is what +# the vhost will be told to trust for client certificates, and it must be +# something this server holds. +createAgentIntermediateCA() { + local agentdir cadir + agentdir="$(_pkiZoneDir agent)" + cadir="${agentdir}/ca" + mkdir -p "$cadir" >>$error_log 2>&1 + chmod 0700 "$cadir" >>$error_log 2>&1 + PKI_agent_ca_key="${cadir}/.fogAgentCA.key" + PKI_agent_ca_cert="${cadir}/.fogAgentCA.pem" + if [[ ! -f ${PKI_agent_ca_cert} ]]; then + dots "Creating FOG Agent CA" + _issueIntermediateCA "FOG Agent CA" "$cadir" ".fogAgentCA.key" ".fogAgentCA.pem" \ + "extendedKeyUsage = clientAuth" "FOG Agent" + errorStat $? + fi + # The trust file for VERIFYING agent certificates: the agent CA and the + # root it chains to, public halves only, world-readable. Three readers: + # the vhost (ssl_client_certificate / SSLCACertificateFile), PHP + # (Agent\Principal re-verifies against it, see that class for why), and + # the copy published under management/other for the same reason + # ca.cert.pem is. Rewritten every run so it follows a re-minted CA. + PKI_agent_ca_bundle="${agentdir}/agent-ca-bundle.pem" + cat "${PKI_agent_ca_cert}" "${PKI_root_ca_cert}" > "${PKI_agent_ca_bundle}" 2>>$error_log + chmod 0644 "${PKI_agent_ca_bundle}" >>$error_log 2>&1 +} # Did ${PKI_root_ca_cert} actually issue ${PKI_web_ca_cert}? # # The one question that separates a FOG-generated Web CA from one imported with @@ -10731,6 +10786,10 @@ EOF PKI_web_trust_chain="${PKI_root_ca_cert}" fi fi + # Outside the web-zone branch on purpose: an install that brought its own + # Web CA still needs an agent CA, and it is issued by the FOG root, which + # every master holds whatever signs its web leaf. + createAgentIntermediateCA _resolveWebLeafPaths _createWebLeaf _writeWebChainFiles @@ -10770,6 +10829,13 @@ EOF # certificate. cp -f "${PKI_root_ca_cert}" $webdirdest/management/other/ca.cert.pem >>$error_log 2>&1 openssl x509 -outform der -in $webdirdest/management/other/ca.cert.pem -out $webdirdest/management/other/ca.cert.der >>$error_log 2>&1 + # What Agent\Principal verifies client certificates against (see + # createAgentIntermediateCA). A storage node mints no agent CA and + # publishes nothing here, and the router's agent gate then refuses + # every certificate, which is right: a node is not an agent server. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + cp -f "${PKI_agent_ca_bundle}" $webdirdest/management/other/agent-ca-bundle.pem >>$error_log 2>&1 + fi errorStat $? dots "Resetting SSL Permissions" chown -R $apacheuser:$apacheuser $webdirdest/management/other >>$error_log 2>&1 @@ -10909,6 +10975,21 @@ EOF # it and the root. echo " ssl_certificate ${sslfullchain:-${PKI_web_vhost_cert}};" >> "$etcconf" echo " ssl_certificate_key ${PKI_web_vhost_key};" >> "$etcconf" + # fog-agent client certificates. `optional`, and at + # server scope because nginx allows nothing finer: + # a browser is never asked for one it does not have + # -- the request names only the FOG Agent CA, which + # no browser holds a certificate from -- and a + # request with no certificate reaches PHP with an + # empty verdict, where the router's agent gate + # decides. Verification against the agent bundle + # (agent CA + root) and depth 2 for exactly that + # chain. Absent on a node, which mints no agent CA. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " ssl_client_certificate ${PKI_agent_ca_bundle};" >> "$etcconf" + echo " ssl_verify_client optional;" >> "$etcconf" + echo " ssl_verify_depth 2;" >> "$etcconf" + fi echo " ssl_session_timeout 1d;" >> "$etcconf" # Zone name is FOG-specific on purpose. Alpine's stock # nginx.conf already declares `shared:SSL:2m` in the @@ -11086,6 +11167,21 @@ EOF # it and the root. echo " ssl_certificate ${sslfullchain:-${PKI_web_vhost_cert}};" >> "$etcconf" echo " ssl_certificate_key ${PKI_web_vhost_key};" >> "$etcconf" + # fog-agent client certificates. `optional`, and at + # server scope because nginx allows nothing finer: + # a browser is never asked for one it does not have + # -- the request names only the FOG Agent CA, which + # no browser holds a certificate from -- and a + # request with no certificate reaches PHP with an + # empty verdict, where the router's agent gate + # decides. Verification against the agent bundle + # (agent CA + root) and depth 2 for exactly that + # chain. Absent on a node, which mints no agent CA. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " ssl_client_certificate ${PKI_agent_ca_bundle};" >> "$etcconf" + echo " ssl_verify_client optional;" >> "$etcconf" + echo " ssl_verify_depth 2;" >> "$etcconf" + fi echo " ssl_session_timeout 1d;" >> "$etcconf" # Zone name is FOG-specific on purpose. Alpine's stock # nginx.conf already declares `shared:SSL:2m` in the @@ -11311,7 +11407,27 @@ EOF # supports 2.4.6, which would silently serve only the first # certificate -- the exact failure this is here to fix. [[ -n $sslchainonly ]] && echo " SSLCertificateChainFile $sslchainonly" >> "$etcconf" - echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + # fog-agent client certificates. Apache verifies them + # against SSLCACertificateFile, so with an agent CA + # present that file is the agent bundle (agent CA + + # root) rather than the web trust chain -- the + # directive governs client verification only, the + # server's own chain is SSLCertificateChainFile above. + # `optional` at vhost scope: a Location-scoped + # requirement means renegotiation, which TLS 1.3 has + # not got and Go's client does not do. The env vars + # are exported only under /agent/, the one place PHP + # reads them. Agent\Principal re-verifies regardless. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " SSLCACertificateFile ${PKI_agent_ca_bundle}" >> "$etcconf" + echo " SSLVerifyClient optional" >> "$etcconf" + echo " SSLVerifyDepth 2" >> "$etcconf" + echo " " >> "$etcconf" + echo " SSLOptions +StdEnvVars +ExportCertData" >> "$etcconf" + echo " " >> "$etcconf" + else + echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + fi echo " " >> "$etcconf" echo " Protocols h2 http/1.1" >> "$etcconf" echo " " >> "$etcconf" @@ -11438,7 +11554,27 @@ EOF # supports 2.4.6, which would silently serve only the first # certificate -- the exact failure this is here to fix. [[ -n $sslchainonly ]] && echo " SSLCertificateChainFile $sslchainonly" >> "$etcconf" - echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + # fog-agent client certificates. Apache verifies them + # against SSLCACertificateFile, so with an agent CA + # present that file is the agent bundle (agent CA + + # root) rather than the web trust chain -- the + # directive governs client verification only, the + # server's own chain is SSLCertificateChainFile above. + # `optional` at vhost scope: a Location-scoped + # requirement means renegotiation, which TLS 1.3 has + # not got and Go's client does not do. The env vars + # are exported only under /agent/, the one place PHP + # reads them. Agent\Principal re-verifies regardless. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " SSLCACertificateFile ${PKI_agent_ca_bundle}" >> "$etcconf" + echo " SSLVerifyClient optional" >> "$etcconf" + echo " SSLVerifyDepth 2" >> "$etcconf" + echo " " >> "$etcconf" + echo " SSLOptions +StdEnvVars +ExportCertData" >> "$etcconf" + echo " " >> "$etcconf" + else + echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + fi echo " " >> "$etcconf" echo " Protocols h2 http/1.1" >> "$etcconf" echo " " >> "$etcconf" diff --git a/packages/pki/fog-sign-node-cert b/packages/pki/fog-sign-node-cert index cd2750d5d9..4f1626db37 100755 --- a/packages/pki/fog-sign-node-cert +++ b/packages/pki/fog-sign-node-cert @@ -45,14 +45,14 @@ CONF="/opt/fog/.fog-pki" die() { echo "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "fog-sign-node-cert must run as root" -[[ $# -eq 2 ]] || die "usage: fog-sign-node-cert " +[[ $# -eq 2 ]] || die "usage: fog-sign-node-cert " type="$1" reqid="$2" # Validated before use, not after. A request id reaching the filesystem # unchecked is a path traversal into whatever the web user can write. -[[ $type =~ ^(web|signing)$ ]] || die "unknown certificate type" +[[ $type =~ ^(web|signing|agent)$ ]] || die "unknown certificate type" [[ $reqid =~ ^[a-f0-9]{32}$ ]] || die "malformed request id" [[ -r $CONF ]] || die "missing $CONF -- re-run the FOG installer" @@ -68,6 +68,59 @@ out="${PKI_STAGING}/${reqid}.pem" chainout="${PKI_STAGING}/${reqid}.chain" [[ -f $csr ]] || die "no request at ${csr}" + +# The agent zone issues CLIENT certificates to fog-agent installs +# (FOG\Agent\Enrollment stages the request). No names: the certificate +# identifies a host record, not an address, and the only thing the endpoint +# hands over is the host id, in its own file, matched against a fixed +# pattern here before it goes anywhere near a subject line. The key it is +# signed with is the agent intermediate, never the web CA -- the web CA may +# be one the admin brought, and a public CA does not issue client +# certificates -- and clientAuth alone on the EKU means nothing issued here +# can ever pose as a server. +if [[ $type == agent ]]; then + hostfile="${PKI_STAGING}/${reqid}.agent" + [[ -f $hostfile ]] || die "no host id at ${hostfile}" + hostid=$(head -n1 "$hostfile" | tr -d '[:space:]') + [[ $hostid =~ ^[0-9]{1,10}$ ]] || die "malformed host id" + cacert="${PKI_AGENT_CA_CERT:-}" + cakey="${PKI_AGENT_CA_KEY:-}" + [[ -n $cacert && -f $cacert ]] || die "the agent CA is not present on this server -- re-run the FOG installer" + [[ -n $cakey && -f $cakey ]] || die "the agent CA private key is not on this server (expected at ${cakey:-})" + tmpext=$(mktemp) || die "could not create a temporary file" + trap 'rm -f "$tmpext"' EXIT + { + echo "[ v3_agent ]" + echo "basicConstraints = critical,CA:FALSE" + echo "keyUsage = critical,digitalSignature" + echo "extendedKeyUsage = clientAuth" + echo "subjectKeyIdentifier = hash" + echo "authorityKeyIdentifier = keyid" + } > "$tmpext" + rm -f "$out" "$chainout" + # One year. The agent renews over its own mTLS session well before then; + # a shorter life bounds what a copied key is worth. + if ! openssl x509 -req -in "$csr" -CA "$cacert" -CAkey "$cakey" \ + -CAcreateserial -sha256 -days 365 \ + -extensions v3_agent -extfile "$tmpext" \ + -subj "/CN=fog-agent host ${hostid}/O=FOG Project/OU=FOG Agent" \ + -out "$out" 2>/dev/null; then + die "signing failed" + fi + # What the agent presents beneath its leaf: the intermediate. The root + # is what the server's vhost trusts and need not travel. + cat "$cacert" > "$chainout" + anchor="${PKI_ROOT_CERT:-}" + [[ -n $anchor && -f $anchor ]] || anchor="$cacert" + if ! openssl verify -purpose sslclient -CAfile "$anchor" -untrusted "$cacert" "$out" >/dev/null 2>&1; then + rm -f "$out" "$chainout" + die "the issued certificate does not verify against ${anchor}" + fi + chmod 0644 "$out" "$chainout" + echo "OK" + exit 0 +fi + [[ -f $san ]] || die "no name list at ${san}" # anchor: what the issued certificate must verify against, and what the node is diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index d721ab40ce..d40d3a842a 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -103,10 +103,8 @@ // ---- satellite: rows wholly owned by one parent ---------------------- ['child' => 'inventory', 'column' => 'iHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], - ['child' => 'hostScreenSettings', 'column' => 'hssHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'hostAutoLogOut', 'column' => 'haloHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'powerManagement', 'column' => 'pmHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], - ['child' => 'greenFog', 'column' => 'gfHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'apiTokens', 'column' => 'atUserID', 'parent' => 'users', 'pcolumn' => 'uId', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 2], ['child' => 'userAuths', 'column' => 'uaUserID', 'parent' => 'users', 'pcolumn' => 'uId', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 2], // Group 7, added with the table itself (schema 391/392). A NUMBER, not a @@ -299,6 +297,15 @@ ['child' => 'groupSnapinAssoc', 'column' => 'gsaSnapinID', 'parent' => 'snapins', 'pcolumn' => 'sID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], ['child' => 'groupPrinterAssoc', 'column' => 'gpaGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], ['child' => 'groupPrinterAssoc', 'column' => 'gpaPrinterID', 'parent' => 'printers', 'pcolumn' => 'pID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], + // Group 13 -- fog-agent software (design 0003, schema 418). An + // assignment or a status row is meaningless without both its host (or + // group) and its software entry. + ['child' => 'softwareAssoc', 'column' => 'swaHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareAssoc', 'column' => 'swaSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'groupSoftwareAssoc', 'column' => 'gswaGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'groupSoftwareAssoc', 'column' => 'gswaSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareStatus', 'column' => 'sstHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareStatus', 'column' => 'sstSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], // ADR 0038 decision 3, revised. Group 10, created empty by step 407 so // there is nothing to sweep before the flip. // @@ -321,6 +328,38 @@ // against a reused group id would silently start shutting down every // host that inherited the number. ['child' => 'groupPowerManagement', 'column' => 'gpmGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 11], + // FOG Agent enrollment. Group 12, created empty by step 416 so there is + // nothing to sweep before the flip. + // + // `satellite`: an enrollment row is the agent's standing with ONE host -- + // pending, issued or denied -- and means nothing once that host is gone. + // Every row gets a host, because an unknown machine is given a pending + // host at enrollment time, the same way iPXE registration does it. + // CASCADE rather than RESTRICT because deleting the pending host IS how + // an admin forgets a machine; the agent then comes back as unknown and + // waits for a fresh decision. A denied row going with its host is the + // same outcome, and the decision itself is in auditLog. + ['child' => 'agentEnrollment', 'column' => 'aeHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 12], + // The facts an agent reports about its own host (design 0006). Both are + // satellites in the same sense inventory is: the rows describe one host + // and mean nothing without it. CASCADE, so deleting a host takes its + // reported software history with it -- the same call FOG already makes + // for that host's inventory row, and the reason the fleet report reads + // "which hosts have X" rather than "which machines ever did". + ['child' => 'hostSoftware', 'column' => 'hsHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostUserSession', 'column' => 'husHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostFactState', 'column' => 'hfsHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostDirectory', 'column' => 'hdHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostPrinter', 'column' => 'hpHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostSpooler', 'column' => 'hspHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostNetwork', 'column' => 'hnHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + // Both ends of a wake relay are hosts, and BOTH cascade. A deleted + // target has nothing left to wake; a deleted sender cannot be asked. + // Leaving either behind would leave a row naming a host id that has + // since been reused, which is how an admin ends up reading that a + // machine relayed a wake it has never heard of. + ['child' => 'agentWake', 'column' => 'awTargetID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'agentWake', 'column' => 'awSenderID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'ldapUserGrant', 'column' => 'lugTargetID', 'parent' => '(lugTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ['child' => 'oidcUserGrant', 'column' => 'ougTargetID', 'parent' => '(ougTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ]; diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 036c6f9eb7..1b925cf27e 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -70,18 +70,129 @@ 'retired' => [ [ 'table' => 'imagingLog', - 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging' - . ' run now, so the table was retired rather than ported', + 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging run now, so the table was retired rather than ported', + ], + [ + 'table' => 'hostScreenSettings', + 'reason' => 'design 0014 -- Display Manager is removed, so the per-host width, height and refresh have nothing that reads or applies them. Schema step 431 drops the table with the module', + ], + [ + 'table' => 'dirCleaner', + 'reason' => 'the Directory Cleaner module is removed -- the rebuilt agent does not delete paths a list names, and a snapin already ships a script with an exit code and captured output. Schema step 433 drops the table with the module', + ], + [ + 'table' => 'userCleanup', + 'reason' => 'the User Cleanup module is removed -- nothing reads the profile names it held. Its six rows were FOG\'s own install seed (admin, guest, administrator, HelpAssistant, ASPNET, SUPPORT_), never operator data. Schema step 433 drops the table with the module', + ], + [ + 'table' => 'clientUpdates', + 'reason' => 'the Client Updater module is removed -- it served .NET client binaries, and the rebuild updates through the MSI at a stable URL plus a snapin filtered on the reported agentVersion, deliberately not a self-replacing updater. Schema step 433 drops the table with the module', + ], + [ + 'table' => 'greenFog', + 'reason' => 'Green FOG was superseded by Power Management (design 0004). Schema step 375 removed the module, its settings and its per-host rows but left the table behind; step 433 finishes that removal', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon1', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon2', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon3', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon4', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon5', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon2', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon3', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon4', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon5', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', ], [ 'table' => 'virus', - 'reason' => 'GH-328 -- the ClamAV scan is removed. 1.6 never' - . ' carried service/av.php across from 1.5, so nothing on' - . ' this branch has ever written the table and no model,' - . ' manager, report or page reads it', + 'reason' => 'GH-328 -- the ClamAV scan is removed. 1.6 never carried service/av.php across from 1.5, so nothing on this branch has ever written the table and no model, manager, report or page reads it', ], ], 'tables' => [ + 'agentEnrollment' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentEnrollment` ( `aeID` int(11) NOT NULL AUTO_INCREMENT, `aeHostID` int(11) NOT NULL DEFAULT 0, `aeFingerprint` varchar(64) NOT NULL DEFAULT \'\', `aeCSR` text NOT NULL, `aeIdentity` text NOT NULL DEFAULT \'\', `aeHostname` varchar(191) NOT NULL DEFAULT \'\', `aeOS` varchar(20) NOT NULL DEFAULT \'\', `aeArch` varchar(20) NOT NULL DEFAULT \'\', `aeAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `aeRemoteIP` varchar(45) NOT NULL DEFAULT \'\', `aeReason` varchar(32) NOT NULL DEFAULT \'\', `aeState` varchar(16) NOT NULL DEFAULT \'pending\', `aeCert` text NOT NULL DEFAULT \'\', `aeCreated` datetime DEFAULT NULL, `aeUpdated` datetime DEFAULT NULL, `aeDecided` datetime DEFAULT NULL, `aeDecidedBy` varchar(191) NOT NULL DEFAULT \'\', `aeDecidedVia` varchar(16) NOT NULL DEFAULT \'\', PRIMARY KEY (`aeID`), UNIQUE KEY `aeFingerprint` (`aeFingerprint`), KEY `aeState` (`aeState`), KEY `aeHostID` (`aeHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'aeID' => 'int(11) NOT NULL', + 'aeHostID' => 'int(11) NOT NULL DEFAULT 0', + 'aeFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'aeCSR' => 'text NOT NULL', + 'aeIdentity' => 'text NOT NULL DEFAULT \'\'', + 'aeHostname' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'aeOS' => 'varchar(20) NOT NULL DEFAULT \'\'', + 'aeArch' => 'varchar(20) NOT NULL DEFAULT \'\'', + 'aeAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', + 'aeRemoteIP' => 'varchar(45) NOT NULL DEFAULT \'\'', + 'aeReason' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'aeState' => 'varchar(16) NOT NULL DEFAULT \'pending\'', + 'aeCert' => 'text NOT NULL DEFAULT \'\'', + 'aeCreated' => 'datetime DEFAULT NULL', + 'aeUpdated' => 'datetime DEFAULT NULL', + 'aeDecided' => 'datetime DEFAULT NULL', + 'aeDecidedBy' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'aeDecidedVia' => 'varchar(16) NOT NULL DEFAULT \'\'', + ], + ], + 'agentEnrollToken' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentEnrollToken` ( `atID` int(11) NOT NULL AUTO_INCREMENT, `atName` varchar(191) NOT NULL DEFAULT \'\', `atHash` varchar(64) NOT NULL DEFAULT \'\', `atUses` int(11) NOT NULL DEFAULT 1, `atExpires` datetime DEFAULT NULL, `atCreatedBy` varchar(191) NOT NULL DEFAULT \'\', `atCreated` datetime DEFAULT NULL, PRIMARY KEY (`atID`), UNIQUE KEY `atHash` (`atHash`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'atID' => 'int(11) NOT NULL', + 'atName' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'atHash' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'atUses' => 'int(11) NOT NULL DEFAULT 1', + 'atExpires' => 'datetime DEFAULT NULL', + 'atCreatedBy' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'atCreated' => 'datetime DEFAULT NULL', + ], + ], + 'agentWake' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentWake` ( `awID` int(11) NOT NULL AUTO_INCREMENT, `awTargetID` int(11) NOT NULL, `awSenderID` int(11) NOT NULL, `awRequestedAt` datetime DEFAULT NULL, `awExpiresAt` datetime DEFAULT NULL, `awStatus` varchar(16) NOT NULL DEFAULT \'pending\', `awPackets` int(11) NOT NULL DEFAULT 0, `awDetail` varchar(255) NOT NULL DEFAULT \'\', `awReportedAt` datetime DEFAULT NULL, `awRequestedBy` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`awID`), KEY `awSenderStatus` (`awSenderID`,`awStatus`), KEY `awTargetID` (`awTargetID`), KEY `awExpiresAt` (`awExpiresAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'awID' => 'int(11) NOT NULL', + 'awTargetID' => 'int(11) NOT NULL', + 'awSenderID' => 'int(11) NOT NULL', + 'awRequestedAt' => 'datetime DEFAULT NULL', + 'awExpiresAt' => 'datetime DEFAULT NULL', + 'awStatus' => 'varchar(16) NOT NULL DEFAULT \'pending\'', + 'awPackets' => 'int(11) NOT NULL DEFAULT 0', + 'awDetail' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'awReportedAt' => 'datetime DEFAULT NULL', + 'awRequestedBy' => 'varchar(255) NOT NULL DEFAULT \'\'', + ], + ], 'apiTokens' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `apiTokens` ( `atID` int(11) NOT NULL AUTO_INCREMENT, `atUserID` int(11) NOT NULL DEFAULT 0, `atName` varchar(255) NOT NULL DEFAULT \'\', `atHash` char(64) NOT NULL DEFAULT \'\', `atEnabled` tinyint(1) NOT NULL DEFAULT 1, `atCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `atCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `atLastUsed` datetime DEFAULT NULL, PRIMARY KEY (`atID`), UNIQUE KEY `atHash` (`atHash`), KEY `atUserID` (`atUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -119,7 +230,7 @@ ], ], 'auditLog' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `auditLog` ( `alID` int(11) NOT NULL AUTO_INCREMENT, `alCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `alCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `alIP` varchar(45) NOT NULL DEFAULT \'\', `alAuthSource` varchar(64) NOT NULL DEFAULT \'\', `alType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectID` int(11) NOT NULL DEFAULT 0, `alSubjectLabel` varchar(255) NOT NULL DEFAULT \'\', `alPermission` varchar(128) NOT NULL DEFAULT \'\', `alOutcome` enum(\'unknown\',\'allowed\',\'denied\',\'failed\',\'partial\') NOT NULL DEFAULT \'unknown\', `alCorrelationID` varchar(32) NOT NULL DEFAULT \'\', `alAffectedCount` int(11) NOT NULL DEFAULT 0, `alRenderable` tinyint(1) unsigned NOT NULL DEFAULT 1, `alText` longtext NOT NULL DEFAULT \'\', `alActedAs` varchar(255) NOT NULL DEFAULT \'\', `alSpanID` varchar(32) NOT NULL DEFAULT \'\', PRIMARY KEY (`alID`), KEY `alCreatedTime` (`alCreatedTime`), KEY `alCreatedBy` (`alCreatedBy`), KEY `alCorrelationID` (`alCorrelationID`), KEY `alOutcome` (`alOutcome`), KEY `alSubject` (`alSubjectType`,`alSubjectID`), KEY `alSpanID` (`alSpanID`), KEY `alActedAs` (`alActedAs`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `auditLog` ( `alID` int(11) NOT NULL AUTO_INCREMENT, `alCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `alCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `alIP` varchar(45) NOT NULL DEFAULT \'\', `alAuthSource` varchar(64) NOT NULL DEFAULT \'\', `alType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectID` int(11) NOT NULL DEFAULT 0, `alSubjectLabel` varchar(255) NOT NULL DEFAULT \'\', `alPermission` varchar(128) NOT NULL DEFAULT \'\', `alOutcome` enum(\'unknown\',\'allowed\',\'denied\',\'failed\',\'partial\') NOT NULL DEFAULT \'unknown\', `alCorrelationID` varchar(32) NOT NULL DEFAULT \'\', `alAffectedCount` int(11) NOT NULL DEFAULT 0, `alRenderable` tinyint(1) unsigned NOT NULL DEFAULT 1, `alText` longtext NOT NULL DEFAULT \'\', `alActedAs` varchar(255) NOT NULL DEFAULT \'\', `alSpanID` varchar(32) NOT NULL DEFAULT \'\', PRIMARY KEY (`alID`), KEY `alCreatedTime` (`alCreatedTime`), KEY `alCreatedBy` (`alCreatedBy`), KEY `alCorrelationID` (`alCorrelationID`), KEY `alOutcome` (`alOutcome`), KEY `alSubject` (`alSubjectType`,`alSubjectID`), KEY `alActedAs` (`alActedAs`), KEY `alSpanID` (`alSpanID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'alID' => 'int(11) NOT NULL', 'alCreatedTime' => 'datetime NOT NULL DEFAULT current_timestamp()', @@ -155,23 +266,6 @@ 'bfPinned' => 'tinyint(1) NOT NULL DEFAULT 0', ], ], - 'clientUpdates' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `clientUpdates` ( `cuID` int(11) NOT NULL AUTO_INCREMENT, `cuName` varchar(200) NOT NULL DEFAULT \'\', `cuMD5` varchar(100) NOT NULL DEFAULT \'\', `cuType` varchar(30) NOT NULL DEFAULT \'\', `cuFile` longblob NOT NULL DEFAULT \'\', PRIMARY KEY (`cuID`), KEY `new_index` (`cuName`), KEY `new_index1` (`cuType`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'cuID' => 'int(11) NOT NULL', - 'cuName' => 'varchar(200) NOT NULL DEFAULT \'\'', - 'cuMD5' => 'varchar(100) NOT NULL DEFAULT \'\'', - 'cuType' => 'varchar(30) NOT NULL DEFAULT \'\'', - 'cuFile' => 'longblob NOT NULL DEFAULT \'\'', - ], - ], - 'dirCleaner' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `dirCleaner` ( `dcID` int(11) NOT NULL AUTO_INCREMENT, `dcPath` longtext NOT NULL DEFAULT \'\', PRIMARY KEY (`dcID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'dcID' => 'int(11) NOT NULL', - 'dcPath' => 'longtext NOT NULL DEFAULT \'\'', - ], - ], 'dmidecodeKeys' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `dmidecodeKeys` ( `dkID` int(11) NOT NULL AUTO_INCREMENT, `dkName` varchar(255) NOT NULL, PRIMARY KEY (`dkID`), UNIQUE KEY `name` (`dkName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -180,7 +274,7 @@ ], ], 'fileDeleteQueue' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `fileDeleteQueue` ( `fdqID` int(11) NOT NULL AUTO_INCREMENT, `fdqPathName` varchar(255) NOT NULL, `fdqStorageGroupID` int(11) NOT NULL, `fdqCreateDate` datetime DEFAULT current_timestamp(), `fdqCompletedDate` datetime DEFAULT NULL, `fdqCreateBy` varchar(40) DEFAULT NULL, `fdqState` int(11) NOT NULL DEFAULT 0, `fdqPathType` varchar(255) NOT NULL, PRIMARY KEY (`fdqID`), KEY `idx_fdqCreateDate` (`fdqCreateDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `fileDeleteQueue` ( `fdqID` int(11) NOT NULL AUTO_INCREMENT, `fdqPathName` varchar(255) NOT NULL, `fdqStorageGroupID` int(11) NOT NULL, `fdqCreateDate` datetime DEFAULT current_timestamp(), `fdqCompletedDate` datetime DEFAULT NULL, `fdqCreateBy` varchar(40) DEFAULT NULL, `fdqState` int(11) NOT NULL DEFAULT 0, `fdqPathType` varchar(255) NOT NULL, PRIMARY KEY (`fdqID`), KEY `idx_fdqCreateDate` (`fdqCreateDate`), KEY `fk_fileDeleteQueue_fdqStorageGroupID` (`fdqStorageGroupID`), KEY `fk_fileDeleteQueue_fdqState` (`fdqState`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'fdqID' => 'int(11) NOT NULL', 'fdqPathName' => 'varchar(255) NOT NULL', @@ -202,17 +296,6 @@ 'settingCategory' => 'longtext NOT NULL DEFAULT \'\'', ], ], - 'greenFog' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `greenFog` ( `gfID` int(11) NOT NULL AUTO_INCREMENT, `gfHostID` int(11) NOT NULL, `gfHour` int(11) NOT NULL DEFAULT 0, `gfMin` int(11) NOT NULL DEFAULT 0, `gfAction` varchar(2) NOT NULL DEFAULT \'\', `gfDays` varchar(25) NOT NULL DEFAULT \'\', PRIMARY KEY (`gfID`), KEY `new_index` (`gfHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'gfID' => 'int(11) NOT NULL', - 'gfHostID' => 'int(11) NOT NULL', - 'gfHour' => 'int(11) NOT NULL DEFAULT 0', - 'gfMin' => 'int(11) NOT NULL DEFAULT 0', - 'gfAction' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'gfDays' => 'varchar(25) NOT NULL DEFAULT \'\'', - ], - ], 'groupMembers' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `groupMembers` ( `gmID` int(11) NOT NULL AUTO_INCREMENT, `gmHostID` int(11) NOT NULL, `gmGroupID` int(11) NOT NULL, PRIMARY KEY (`gmID`), UNIQUE KEY `gmHostID` (`gmHostID`,`gmGroupID`), UNIQUE KEY `gmGroupID` (`gmHostID`,`gmGroupID`), KEY `new_index` (`gmHostID`), KEY `new_index1` (`gmGroupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -276,6 +359,15 @@ 'gsaSequence' => 'int(11) NOT NULL DEFAULT 0', ], ], + 'groupSoftwareAssoc' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `groupSoftwareAssoc` ( `gswaID` int(11) NOT NULL AUTO_INCREMENT, `gswaGroupID` int(11) NOT NULL, `gswaSoftwareID` int(11) NOT NULL, `gswaSequence` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`gswaID`), UNIQUE KEY `gswaGroupSoftware` (`gswaGroupID`,`gswaSoftwareID`), KEY `gswaSoftwareID` (`gswaSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'gswaID' => 'int(11) NOT NULL', + 'gswaGroupID' => 'int(11) NOT NULL', + 'gswaSoftwareID' => 'int(11) NOT NULL', + 'gswaSequence' => 'int(11) NOT NULL DEFAULT 0', + ], + ], 'history' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `history` ( `hID` int(11) NOT NULL AUTO_INCREMENT, `hText` text NOT NULL, `hUser` varchar(200) NOT NULL DEFAULT \'\', `hTime` timestamp NOT NULL DEFAULT current_timestamp(), `hIP` varchar(50) NOT NULL DEFAULT \'\', `hType` varchar(16) NOT NULL DEFAULT \'\', `hSubjectType` varchar(64) NOT NULL DEFAULT \'\', `hSubjectID` int(11) DEFAULT NULL, `hSubjectLabel` varchar(200) NOT NULL DEFAULT \'\', PRIMARY KEY (`hID`), KEY `hTime` (`hTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -305,6 +397,35 @@ 'haloTime' => 'varchar(10) NOT NULL', ], ], + 'hostDirectory' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostDirectory` ( `hdID` int(11) NOT NULL AUTO_INCREMENT, `hdHostID` int(11) NOT NULL, `hdJoined` tinyint(1) NOT NULL DEFAULT 0, `hdKind` varchar(32) NOT NULL DEFAULT \'\', `hdDomain` varchar(255) NOT NULL DEFAULT \'\', `hdNetbios` varchar(64) NOT NULL DEFAULT \'\', `hdComputerDN` varchar(1024) NOT NULL DEFAULT \'\', `hdMachineAccount` varchar(255) NOT NULL DEFAULT \'\', `hdSite` varchar(255) NOT NULL DEFAULT \'\', `hdObservedAt` datetime DEFAULT NULL, `hdPlacementAt` datetime DEFAULT NULL, `hdPlacementError` varchar(255) NOT NULL DEFAULT \'\', `hdJoinAt` datetime DEFAULT NULL, `hdJoinError` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`hdID`), UNIQUE KEY `hdHostID` (`hdHostID`), KEY `hdDomain` (`hdDomain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hdID' => 'int(11) NOT NULL', + 'hdHostID' => 'int(11) NOT NULL', + 'hdJoined' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hdKind' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'hdDomain' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hdNetbios' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'hdComputerDN' => 'varchar(1024) NOT NULL DEFAULT \'\'', + 'hdMachineAccount' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hdSite' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hdObservedAt' => 'datetime DEFAULT NULL', + 'hdPlacementAt' => 'datetime DEFAULT NULL', + 'hdPlacementError' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hdJoinAt' => 'datetime DEFAULT NULL', + 'hdJoinError' => 'varchar(255) NOT NULL DEFAULT \'\'', + ], + ], + 'hostFactState' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostFactState` ( `hfsID` int(11) NOT NULL AUTO_INCREMENT, `hfsHostID` int(11) NOT NULL, `hfsKind` varchar(16) NOT NULL, `hfsHash` varchar(64) NOT NULL DEFAULT \'\', `hfsUpdated` datetime DEFAULT NULL, PRIMARY KEY (`hfsID`), UNIQUE KEY `hfsHostKind` (`hfsHostID`,`hfsKind`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hfsID' => 'int(11) NOT NULL', + 'hfsHostID' => 'int(11) NOT NULL', + 'hfsKind' => 'varchar(16) NOT NULL', + 'hfsHash' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'hfsUpdated' => 'datetime DEFAULT NULL', + ], + ], 'hostMAC' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hostMAC` ( `hmID` int(11) NOT NULL AUTO_INCREMENT, `hmHostID` int(11) NOT NULL, `hmMAC` varchar(59) NOT NULL, `hmDesc` longtext NOT NULL DEFAULT \'\', `hmPrimary` tinyint(1) NOT NULL DEFAULT 0, `hmPending` tinyint(1) NOT NULL DEFAULT 0, `hmIgnoreClient` tinyint(1) NOT NULL DEFAULT 0, `hmIgnoreImaging` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`hmID`), UNIQUE KEY `hmHostID` (`hmMAC`), UNIQUE KEY `hmMAC` (`hmMAC`), UNIQUE KEY `hmMAC_2` (`hmMAC`), KEY `idxHostID` (`hmHostID`), KEY `idxMac` (`hmMAC`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -318,8 +439,37 @@ 'hmIgnoreImaging' => 'tinyint(1) NOT NULL DEFAULT 0', ], ], + 'hostNetwork' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostNetwork` ( `hnID` int(11) NOT NULL AUTO_INCREMENT, `hnHostID` int(11) NOT NULL, `hnName` varchar(255) NOT NULL DEFAULT \'\', `hnMAC` varchar(17) NOT NULL DEFAULT \'\', `hnIPv4` varchar(15) NOT NULL DEFAULT \'\', `hnPrefix` tinyint(3) unsigned NOT NULL DEFAULT 0, `hnNetwork` varchar(15) NOT NULL DEFAULT \'\', `hnBroadcast` varchar(15) NOT NULL DEFAULT \'\', `hnUp` tinyint(1) NOT NULL DEFAULT 0, `hnWireless` tinyint(1) NOT NULL DEFAULT 0, `hnObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hnID`), UNIQUE KEY `hnHostAddress` (`hnHostID`,`hnName`,`hnIPv4`), KEY `hnLink` (`hnNetwork`,`hnPrefix`), KEY `hnMAC` (`hnMAC`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hnID' => 'int(11) NOT NULL', + 'hnHostID' => 'int(11) NOT NULL', + 'hnName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hnMAC' => 'varchar(17) NOT NULL DEFAULT \'\'', + 'hnIPv4' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnPrefix' => 'tinyint(3) unsigned NOT NULL DEFAULT 0', + 'hnNetwork' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnBroadcast' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnUp' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hnWireless' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hnObservedAt' => 'datetime DEFAULT NULL', + ], + ], + 'hostPrinter' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostPrinter` ( `hpID` int(11) NOT NULL AUTO_INCREMENT, `hpHostID` int(11) NOT NULL, `hpName` varchar(255) NOT NULL DEFAULT \'\', `hpURI` varchar(1024) NOT NULL DEFAULT \'\', `hpDriver` varchar(255) NOT NULL DEFAULT \'\', `hpDefault` tinyint(1) NOT NULL DEFAULT 0, `hpShared` tinyint(1) NOT NULL DEFAULT 0, `hpObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hpID`), UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), KEY `hpName` (`hpName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hpID' => 'int(11) NOT NULL', + 'hpHostID' => 'int(11) NOT NULL', + 'hpName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', + 'hpDriver' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpDefault' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpShared' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpObservedAt' => 'datetime DEFAULT NULL', + ], + ], 'hosts' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT \'\', `hostAgentNotAfter` datetime DEFAULT NULL, `hostAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `hostAgentCheckin` datetime DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`), KEY `fk_hosts_hostImage` (`hostImage`), KEY `fk_hosts_hostArchID` (`hostArchID`), KEY `hostAgentFingerprint` (`hostAgentFingerprint`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'hostID' => 'int(11) NOT NULL', 'hostName' => 'varchar(16) NOT NULL', @@ -362,23 +512,57 @@ 'hostSbEnrolled' => 'datetime DEFAULT NULL', 'hostSbEnrollCert' => 'varchar(95) DEFAULT NULL', 'hostSbEnrollVia' => 'varchar(16) DEFAULT NULL', - ], - ], - 'hostScreenSettings' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hostScreenSettings` ( `hssID` int(11) NOT NULL AUTO_INCREMENT, `hssHostID` int(11) NOT NULL, `hssWidth` int(11) NOT NULL DEFAULT 0, `hssHeight` int(11) NOT NULL DEFAULT 0, `hssRefresh` int(11) NOT NULL DEFAULT 0, `hssOrientation` int(11) NOT NULL DEFAULT 0, `hssOther1` int(11) NOT NULL DEFAULT 0, `hssOther2` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`hssID`), UNIQUE KEY `hssHostID` (`hssHostID`), KEY `new_index` (`hssHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'hssID' => 'int(11) NOT NULL', - 'hssHostID' => 'int(11) NOT NULL', - 'hssWidth' => 'int(11) NOT NULL DEFAULT 0', - 'hssHeight' => 'int(11) NOT NULL DEFAULT 0', - 'hssRefresh' => 'int(11) NOT NULL DEFAULT 0', - 'hssOrientation' => 'int(11) NOT NULL DEFAULT 0', - 'hssOther1' => 'int(11) NOT NULL DEFAULT 0', - 'hssOther2' => 'int(11) NOT NULL DEFAULT 0', + 'hostAgentFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'hostAgentNotAfter' => 'datetime DEFAULT NULL', + 'hostAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', + 'hostAgentCheckin' => 'datetime DEFAULT NULL', + ], + ], + 'hostSoftware' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostSoftware` ( `hsID` int(11) NOT NULL AUTO_INCREMENT, `hsHostID` int(11) NOT NULL, `hsName` varchar(255) NOT NULL, `hsVersion` varchar(128) NOT NULL DEFAULT \'\', `hsPublisher` varchar(255) NOT NULL DEFAULT \'\', `hsSource` varchar(16) NOT NULL DEFAULT \'\', `hsArch` varchar(16) NOT NULL DEFAULT \'\', `hsInstallDate` date DEFAULT NULL, `hsFirstSeen` datetime DEFAULT NULL, `hsLastSeen` datetime DEFAULT NULL, `hsRemovedAt` datetime DEFAULT NULL, PRIMARY KEY (`hsID`), UNIQUE KEY `hsHostNameSrcVer` (`hsHostID`,`hsName`,`hsSource`,`hsVersion`), KEY `hsName` (`hsName`), KEY `hsHostRemoved` (`hsHostID`,`hsRemovedAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hsID' => 'int(11) NOT NULL', + 'hsHostID' => 'int(11) NOT NULL', + 'hsName' => 'varchar(255) NOT NULL', + 'hsVersion' => 'varchar(128) NOT NULL DEFAULT \'\'', + 'hsPublisher' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hsSource' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'hsArch' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'hsInstallDate' => 'date DEFAULT NULL', + 'hsFirstSeen' => 'datetime DEFAULT NULL', + 'hsLastSeen' => 'datetime DEFAULT NULL', + 'hsRemovedAt' => 'datetime DEFAULT NULL', + ], + ], + 'hostSpooler' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostSpooler` ( `hspID` int(11) NOT NULL AUTO_INCREMENT, `hspHostID` int(11) NOT NULL, `hspSubsystem` varchar(16) NOT NULL DEFAULT \'\', `hspObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hspID`), UNIQUE KEY `hspHostID` (`hspHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hspID' => 'int(11) NOT NULL', + 'hspHostID' => 'int(11) NOT NULL', + 'hspSubsystem' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'hspObservedAt' => 'datetime DEFAULT NULL', + ], + ], + 'hostUserSession' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostUserSession` ( `husID` int(11) NOT NULL AUTO_INCREMENT, `husHostID` int(11) NOT NULL, `husSessionKey` varchar(191) NOT NULL DEFAULT \'\', `husUserName` varchar(255) NOT NULL DEFAULT \'\', `husDomain` varchar(255) NOT NULL DEFAULT \'\', `husUserSID` varchar(191) NOT NULL DEFAULT \'\', `husType` varchar(32) NOT NULL DEFAULT \'\', `husState` varchar(32) NOT NULL DEFAULT \'\', `husRemoteHost` varchar(255) NOT NULL DEFAULT \'\', `husStartedAt` datetime NOT NULL, `husEndedAt` datetime DEFAULT NULL, `husEndReason` varchar(32) NOT NULL DEFAULT \'\', `husLastSeen` datetime DEFAULT NULL, PRIMARY KEY (`husID`), UNIQUE KEY `husHostKeyStart` (`husHostID`,`husSessionKey`,`husStartedAt`), KEY `husHostOpen` (`husHostID`,`husEndedAt`), KEY `husUserName` (`husUserName`), KEY `husStartedAt` (`husStartedAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'husID' => 'int(11) NOT NULL', + 'husHostID' => 'int(11) NOT NULL', + 'husSessionKey' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'husUserName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'husDomain' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'husUserSID' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'husType' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'husState' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'husRemoteHost' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'husStartedAt' => 'datetime NOT NULL', + 'husEndedAt' => 'datetime DEFAULT NULL', + 'husEndReason' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'husLastSeen' => 'datetime DEFAULT NULL', ], ], 'imageGroupAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `imageGroupAssoc` ( `igaID` mediumint(9) NOT NULL AUTO_INCREMENT, `igaImageID` int(11) NOT NULL, `igaStorageGroupID` int(11) NOT NULL, `igaPrimary` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`igaID`), UNIQUE KEY `igaImageID` (`igaImageID`,`igaStorageGroupID`), UNIQUE KEY `igaImageID_2` (`igaImageID`,`igaStorageGroupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `imageGroupAssoc` ( `igaID` mediumint(9) NOT NULL AUTO_INCREMENT, `igaImageID` int(11) NOT NULL, `igaStorageGroupID` int(11) NOT NULL, `igaPrimary` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`igaID`), UNIQUE KEY `igaImageID` (`igaImageID`,`igaStorageGroupID`), UNIQUE KEY `igaImageID_2` (`igaImageID`,`igaStorageGroupID`), KEY `fk_imageGroupAssoc_igaStorageGroupID` (`igaStorageGroupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'igaID' => 'mediumint(9) NOT NULL', 'igaImageID' => 'int(11) NOT NULL', @@ -395,7 +579,7 @@ ], ], 'images' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `images` ( `imageID` int(11) NOT NULL AUTO_INCREMENT, `imageName` varchar(40) NOT NULL, `imageDesc` longtext NOT NULL DEFAULT \'\', `imagePath` longtext NOT NULL, `imageProtect` mediumint(9) NOT NULL DEFAULT 0, `imageMagnetUri` longtext NOT NULL DEFAULT \'\', `imageDateTime` timestamp NOT NULL DEFAULT current_timestamp(), `imageCreateBy` varchar(50) NOT NULL DEFAULT \'\', `imageBuilding` int(11) NOT NULL DEFAULT 0, `imageSize` varchar(255) NOT NULL DEFAULT \'\', `imageTypeID` mediumint(9) NOT NULL, `imagePartitionTypeID` mediumint(9) NOT NULL, `imageOSID` mediumint(9) DEFAULT NULL, `imageFormat` char(1) DEFAULT NULL, `imageLastDeploy` datetime DEFAULT NULL, `imageCompress` int(11) DEFAULT NULL, `imageEnabled` tinyint(1) NOT NULL DEFAULT 1, `imageReplicate` tinyint(1) NOT NULL DEFAULT 1, `imageServerSize` bigint(20) unsigned NOT NULL DEFAULT 0, `imageSectorSize` int(11) DEFAULT NULL, `imageArchID` mediumint(9) DEFAULT NULL, PRIMARY KEY (`imageID`), UNIQUE KEY `imageName` (`imageName`), KEY `new_index` (`imageName`), KEY `new_index1` (`imageBuilding`), KEY `new_index2` (`imageTypeID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `images` ( `imageID` int(11) NOT NULL AUTO_INCREMENT, `imageName` varchar(40) NOT NULL, `imageDesc` longtext NOT NULL DEFAULT \'\', `imagePath` longtext NOT NULL, `imageProtect` mediumint(9) NOT NULL DEFAULT 0, `imageMagnetUri` longtext NOT NULL DEFAULT \'\', `imageDateTime` timestamp NOT NULL DEFAULT current_timestamp(), `imageCreateBy` varchar(50) NOT NULL DEFAULT \'\', `imageBuilding` int(11) NOT NULL DEFAULT 0, `imageSize` varchar(255) NOT NULL DEFAULT \'\', `imageTypeID` mediumint(9) NOT NULL, `imagePartitionTypeID` mediumint(9) NOT NULL, `imageOSID` mediumint(9) DEFAULT NULL, `imageFormat` char(1) DEFAULT NULL, `imageLastDeploy` datetime DEFAULT NULL, `imageCompress` int(11) DEFAULT NULL, `imageEnabled` tinyint(1) NOT NULL DEFAULT 1, `imageReplicate` tinyint(1) NOT NULL DEFAULT 1, `imageServerSize` bigint(20) unsigned NOT NULL DEFAULT 0, `imageSectorSize` int(11) DEFAULT NULL, `imageArchID` mediumint(9) DEFAULT NULL, PRIMARY KEY (`imageID`), UNIQUE KEY `imageName` (`imageName`), KEY `new_index` (`imageName`), KEY `new_index1` (`imageBuilding`), KEY `new_index2` (`imageTypeID`), KEY `fk_images_imageOSID` (`imageOSID`), KEY `fk_images_imagePartitionTypeID` (`imagePartitionTypeID`), KEY `fk_images_imageArchID` (`imageArchID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'imageID' => 'int(11) NOT NULL', 'imageName' => 'varchar(40) NOT NULL', @@ -509,7 +693,7 @@ ], ], 'multicastSessions' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `multicastSessions` ( `msID` int(11) NOT NULL AUTO_INCREMENT, `msName` varchar(250) NOT NULL DEFAULT \'\', `msBasePort` int(11) NOT NULL DEFAULT 0, `msLogPath` longtext NOT NULL DEFAULT \'\', `msImage` longtext NOT NULL DEFAULT \'\', `msClients` int(11) NOT NULL DEFAULT 0, `msSessClients` int(11) NOT NULL DEFAULT 0, `msInterface` varchar(250) NOT NULL DEFAULT \'\', `msStartDateTime` datetime DEFAULT NULL, `msPercent` int(11) NOT NULL DEFAULT 0, `msState` int(11) DEFAULT NULL, `msCompleteDateTime` datetime DEFAULT NULL, `msIsDD` int(11) NOT NULL DEFAULT 0, `msNFSGroupID` int(11) NOT NULL, `msShutdown` tinyint(1) NOT NULL DEFAULT 0, `msMaxwait` int(11) NOT NULL DEFAULT 0, `msAnon5` varchar(250) NOT NULL DEFAULT \'\', `msSenderPID` int(11) NOT NULL DEFAULT 0, `msSenderNode` int(11) DEFAULT NULL, `msSenderStart` datetime DEFAULT NULL, PRIMARY KEY (`msID`), KEY `new_index` (`msNFSGroupID`), KEY `idx_msStartDateTime` (`msStartDateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `multicastSessions` ( `msID` int(11) NOT NULL AUTO_INCREMENT, `msName` varchar(250) NOT NULL DEFAULT \'\', `msBasePort` int(11) NOT NULL DEFAULT 0, `msLogPath` longtext NOT NULL DEFAULT \'\', `msImage` longtext NOT NULL DEFAULT \'\', `msClients` int(11) NOT NULL DEFAULT 0, `msSessClients` int(11) NOT NULL DEFAULT 0, `msInterface` varchar(250) NOT NULL DEFAULT \'\', `msStartDateTime` datetime DEFAULT NULL, `msPercent` int(11) NOT NULL DEFAULT 0, `msState` int(11) DEFAULT NULL, `msCompleteDateTime` datetime DEFAULT NULL, `msIsDD` int(11) NOT NULL DEFAULT 0, `msNFSGroupID` int(11) NOT NULL, `msShutdown` tinyint(1) NOT NULL DEFAULT 0, `msMaxwait` int(11) NOT NULL DEFAULT 0, `msAnon5` varchar(250) NOT NULL DEFAULT \'\', `msSenderPID` int(11) NOT NULL DEFAULT 0, `msSenderNode` int(11) DEFAULT NULL, `msSenderStart` datetime DEFAULT NULL, PRIMARY KEY (`msID`), KEY `new_index` (`msNFSGroupID`), KEY `idx_msStartDateTime` (`msStartDateTime`), KEY `fk_multicastSessions_msSenderNode` (`msSenderNode`), KEY `fk_multicastSessions_msState` (`msState`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'msID' => 'int(11) NOT NULL', 'msName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -642,21 +826,18 @@ ], ], 'printerAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `printerAssoc` ( `paID` int(11) NOT NULL AUTO_INCREMENT, `paHostID` int(11) NOT NULL, `paPrinterID` int(11) NOT NULL, `paIsDefault` varchar(2) NOT NULL DEFAULT \'\', `paAnon1` varchar(2) NOT NULL DEFAULT \'\', `paAnon2` varchar(2) NOT NULL DEFAULT \'\', `paAnon3` varchar(2) NOT NULL DEFAULT \'\', `paAnon4` varchar(2) NOT NULL DEFAULT \'\', `paAnon5` varchar(2) NOT NULL DEFAULT \'\', PRIMARY KEY (`paID`), UNIQUE KEY `paHostID` (`paHostID`,`paPrinterID`), KEY `new_index1` (`paHostID`), KEY `new_index2` (`paPrinterID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `printerAssoc` ( `paID` int(11) NOT NULL AUTO_INCREMENT, `paHostID` int(11) NOT NULL, `paPrinterID` int(11) NOT NULL, `paIsDefault` tinyint(1) NOT NULL DEFAULT 0, `paAppliedAt` datetime DEFAULT NULL, `paError` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`paID`), UNIQUE KEY `paHostID` (`paHostID`,`paPrinterID`), KEY `new_index1` (`paHostID`), KEY `new_index2` (`paPrinterID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'paID' => 'int(11) NOT NULL', 'paHostID' => 'int(11) NOT NULL', 'paPrinterID' => 'int(11) NOT NULL', - 'paIsDefault' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon1' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon2' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon3' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon4' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon5' => 'varchar(2) NOT NULL DEFAULT \'\'', + 'paIsDefault' => 'tinyint(1) NOT NULL DEFAULT 0', + 'paAppliedAt' => 'datetime DEFAULT NULL', + 'paError' => 'varchar(255) NOT NULL DEFAULT \'\'', ], ], 'printers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pAnon2` varchar(10) NOT NULL DEFAULT \'\', `pAnon3` varchar(10) NOT NULL DEFAULT \'\', `pAnon4` varchar(10) NOT NULL DEFAULT \'\', `pAnon5` varchar(10) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, `pURI` varchar(1024) NOT NULL DEFAULT \'\', PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'pID' => 'int(11) NOT NULL', 'pPort' => 'longtext NOT NULL DEFAULT \'\'', @@ -666,11 +847,8 @@ 'pConfig' => 'varchar(10) NOT NULL DEFAULT \'\'', 'pConfigFile' => 'varchar(255) NOT NULL DEFAULT \'\'', 'pIP' => 'varchar(255) NOT NULL DEFAULT \'\'', - 'pAnon2' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon3' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon4' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon5' => 'varchar(10) NOT NULL DEFAULT \'\'', 'pDesc' => 'longtext DEFAULT NULL', + 'pURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', ], ], 'pxeMenu' => [ @@ -706,7 +884,7 @@ ], ], 'roleUserAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserAssoc` ( `ruaID` int(11) NOT NULL AUTO_INCREMENT, `ruaName` varchar(60) NOT NULL DEFAULT \'\', `ruaRoleID` int(11) NOT NULL, `ruaUserID` int(11) NOT NULL, PRIMARY KEY (`ruaID`), UNIQUE KEY `ruaRoleUser` (`ruaRoleID`,`ruaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserAssoc` ( `ruaID` int(11) NOT NULL AUTO_INCREMENT, `ruaName` varchar(60) NOT NULL DEFAULT \'\', `ruaRoleID` int(11) NOT NULL, `ruaUserID` int(11) NOT NULL, PRIMARY KEY (`ruaID`), UNIQUE KEY `ruaRoleUser` (`ruaRoleID`,`ruaUserID`), KEY `fk_roleUserAssoc_ruaUserID` (`ruaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ruaID' => 'int(11) NOT NULL', 'ruaName' => 'varchar(60) NOT NULL DEFAULT \'\'', @@ -715,7 +893,7 @@ ], ], 'roleUserGroupAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserGroupAssoc` ( `rugID` int(11) NOT NULL AUTO_INCREMENT, `rugName` varchar(60) NOT NULL DEFAULT \'\', `rugGroupID` int(11) NOT NULL, `rugRoleID` int(11) NOT NULL, PRIMARY KEY (`rugID`), UNIQUE KEY `rugGroupRole` (`rugGroupID`,`rugRoleID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserGroupAssoc` ( `rugID` int(11) NOT NULL AUTO_INCREMENT, `rugName` varchar(60) NOT NULL DEFAULT \'\', `rugGroupID` int(11) NOT NULL, `rugRoleID` int(11) NOT NULL, PRIMARY KEY (`rugID`), UNIQUE KEY `rugGroupRole` (`rugGroupID`,`rugRoleID`), KEY `fk_roleUserGroupAssoc_rugRoleID` (`rugRoleID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'rugID' => 'int(11) NOT NULL', 'rugName' => 'varchar(60) NOT NULL DEFAULT \'\'', @@ -761,7 +939,7 @@ ], ], 'scheduledTasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `scheduledTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stName` varchar(240) NOT NULL DEFAULT \'\', `stDesc` longtext NOT NULL DEFAULT \'\', `stType` varchar(24) NOT NULL, `stTaskTypeID` mediumint(9) NOT NULL, `stMinute` varchar(240) NOT NULL DEFAULT \'\', `stHour` varchar(240) NOT NULL DEFAULT \'\', `stDOM` varchar(240) NOT NULL DEFAULT \'\', `stMonth` varchar(240) NOT NULL DEFAULT \'\', `stDOW` varchar(240) NOT NULL DEFAULT \'\', `stIsGroup` varchar(2) NOT NULL DEFAULT \'0\', `stGroupHostID` int(11) NOT NULL, `stImageID` int(11) DEFAULT NULL, `stShutDown` varchar(2) NOT NULL DEFAULT \'\', `stOther1` varchar(240) NOT NULL DEFAULT \'\', `stOther2` varchar(240) NOT NULL DEFAULT \'\', `stOther3` varchar(240) NOT NULL DEFAULT \'\', `stOther4` varchar(240) NOT NULL DEFAULT \'\', `stOther5` varchar(240) NOT NULL DEFAULT \'\', `stDateTime` bigint(20) unsigned NOT NULL DEFAULT 0, `stActive` varchar(2) NOT NULL DEFAULT \'1\', PRIMARY KEY (`stID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `scheduledTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stName` varchar(240) NOT NULL DEFAULT \'\', `stDesc` longtext NOT NULL DEFAULT \'\', `stType` varchar(24) NOT NULL, `stTaskTypeID` mediumint(9) NOT NULL, `stMinute` varchar(240) NOT NULL DEFAULT \'\', `stHour` varchar(240) NOT NULL DEFAULT \'\', `stDOM` varchar(240) NOT NULL DEFAULT \'\', `stMonth` varchar(240) NOT NULL DEFAULT \'\', `stDOW` varchar(240) NOT NULL DEFAULT \'\', `stIsGroup` varchar(2) NOT NULL DEFAULT \'0\', `stGroupHostID` int(11) NOT NULL, `stImageID` int(11) DEFAULT NULL, `stShutDown` varchar(2) NOT NULL DEFAULT \'\', `stOther1` varchar(240) NOT NULL DEFAULT \'\', `stOther2` varchar(240) NOT NULL DEFAULT \'\', `stOther3` varchar(240) NOT NULL DEFAULT \'\', `stOther4` varchar(240) NOT NULL DEFAULT \'\', `stOther5` varchar(240) NOT NULL DEFAULT \'\', `stDateTime` bigint(20) unsigned NOT NULL DEFAULT 0, `stActive` varchar(2) NOT NULL DEFAULT \'1\', PRIMARY KEY (`stID`), KEY `fk_scheduledTasks_stTaskTypeID` (`stTaskTypeID`), KEY `fk_scheduledTasks_stImageID` (`stImageID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'stID' => 'int(11) NOT NULL', 'stName' => 'varchar(240) NOT NULL DEFAULT \'\'', @@ -875,7 +1053,7 @@ ], ], 'snapinJobs' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapinJobs` ( `sjID` int(11) NOT NULL AUTO_INCREMENT, `sjHostID` int(11) NOT NULL, `sjStateID` int(11) NOT NULL, `sjAbortOnFail` tinyint(1) NOT NULL DEFAULT 0, `sjCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`sjID`), KEY `new_index` (`sjHostID`), KEY `idx_sjCreateTime` (`sjCreateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapinJobs` ( `sjID` int(11) NOT NULL AUTO_INCREMENT, `sjHostID` int(11) NOT NULL, `sjStateID` int(11) NOT NULL, `sjAbortOnFail` tinyint(1) NOT NULL DEFAULT 0, `sjCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`sjID`), KEY `new_index` (`sjHostID`), KEY `idx_sjCreateTime` (`sjCreateTime`), KEY `fk_snapinJobs_sjStateID` (`sjStateID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'sjID' => 'int(11) NOT NULL', 'sjHostID' => 'int(11) NOT NULL', @@ -885,7 +1063,7 @@ ], ], 'snapins' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapins` ( `sID` int(11) NOT NULL AUTO_INCREMENT, `sName` varchar(200) NOT NULL, `sDesc` longtext NOT NULL DEFAULT \'\', `sFilePath` longtext NOT NULL, `sArgs` longtext NOT NULL DEFAULT \'\', `sCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `sCreator` varchar(200) NOT NULL DEFAULT \'\', `sReboot` varchar(1) NOT NULL DEFAULT \'\', `sRunWith` varchar(245) NOT NULL DEFAULT \'\', `sRunWithArgs` varchar(200) NOT NULL DEFAULT \'\', `sAnon3` varchar(45) NOT NULL DEFAULT \'\', `snapinProtect` mediumint(9) NOT NULL DEFAULT 0, `sEnabled` tinyint(1) NOT NULL DEFAULT 1, `sReplicate` tinyint(1) NOT NULL DEFAULT 1, `sShutdown` tinyint(1) NOT NULL DEFAULT 0, `sHideLog` tinyint(1) NOT NULL DEFAULT 0, `sTimeout` int(11) NOT NULL DEFAULT 0, `sPackType` tinyint(1) NOT NULL DEFAULT 0, `sHash` varchar(255) NOT NULL DEFAULT \'\', `sSize` bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (`sID`), UNIQUE KEY `sName` (`sName`), KEY `new_index` (`sName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapins` ( `sID` int(11) NOT NULL AUTO_INCREMENT, `sName` varchar(200) NOT NULL, `sDesc` longtext NOT NULL DEFAULT \'\', `sFilePath` longtext NOT NULL, `sArgs` longtext NOT NULL DEFAULT \'\', `sCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `sCreator` varchar(200) NOT NULL DEFAULT \'\', `sReboot` varchar(1) NOT NULL DEFAULT \'\', `sRunWith` varchar(245) NOT NULL DEFAULT \'\', `sRunWithArgs` varchar(200) NOT NULL DEFAULT \'\', `sAnon3` varchar(45) NOT NULL DEFAULT \'\', `snapinProtect` mediumint(9) NOT NULL DEFAULT 0, `sEnabled` tinyint(1) NOT NULL DEFAULT 1, `sReplicate` tinyint(1) NOT NULL DEFAULT 1, `sShutdown` tinyint(1) NOT NULL DEFAULT 0, `sHideLog` tinyint(1) NOT NULL DEFAULT 0, `sTimeout` int(11) NOT NULL DEFAULT 0, `sPackType` tinyint(1) NOT NULL DEFAULT 0, `sHash` varchar(255) NOT NULL DEFAULT \'\', `sSize` bigint(20) NOT NULL DEFAULT 0, `sReturnCodes` text DEFAULT NULL, PRIMARY KEY (`sID`), UNIQUE KEY `sName` (`sName`), KEY `new_index` (`sName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'sID' => 'int(11) NOT NULL', 'sName' => 'varchar(200) NOT NULL', @@ -907,10 +1085,11 @@ 'sPackType' => 'tinyint(1) NOT NULL DEFAULT 0', 'sHash' => 'varchar(255) NOT NULL DEFAULT \'\'', 'sSize' => 'bigint(20) NOT NULL DEFAULT 0', + 'sReturnCodes' => 'text DEFAULT NULL', ], ], 'snapinTasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stReturnDetails` varchar(250) NOT NULL DEFAULT \'\', PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stStatus` varchar(16) NOT NULL DEFAULT \'\', `stReturnDetails` text NOT NULL DEFAULT \'\', PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'stID' => 'int(11) NOT NULL', 'stJobID' => 'int(11) NOT NULL', @@ -920,7 +1099,49 @@ 'stSnapinID' => 'int(11) NOT NULL', 'stSequence' => 'int(11) NOT NULL DEFAULT 0', 'stReturnCode' => 'int(11) NOT NULL DEFAULT 0', - 'stReturnDetails' => 'varchar(250) NOT NULL DEFAULT \'\'', + 'stStatus' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'stReturnDetails' => 'text NOT NULL DEFAULT \'\'', + ], + ], + 'software' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `software` ( `swID` int(11) NOT NULL AUTO_INCREMENT, `swName` varchar(200) NOT NULL, `swDesc` longtext NOT NULL DEFAULT \'\', `swBackend` varchar(16) NOT NULL DEFAULT \'choco\', `swPackage` varchar(255) NOT NULL, `swVersion` varchar(64) NOT NULL DEFAULT \'\', `swState` varchar(8) NOT NULL DEFAULT \'present\', `swSource` varchar(255) NOT NULL DEFAULT \'\', `swArgs` varchar(255) NOT NULL DEFAULT \'\', `swTimeout` int(11) NOT NULL DEFAULT 900, `swReturnCodes` text DEFAULT NULL, `swEnabled` tinyint(1) NOT NULL DEFAULT 1, `swCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `swCreator` varchar(50) NOT NULL DEFAULT \'\', PRIMARY KEY (`swID`), UNIQUE KEY `swName` (`swName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'swID' => 'int(11) NOT NULL', + 'swName' => 'varchar(200) NOT NULL', + 'swDesc' => 'longtext NOT NULL DEFAULT \'\'', + 'swBackend' => 'varchar(16) NOT NULL DEFAULT \'choco\'', + 'swPackage' => 'varchar(255) NOT NULL', + 'swVersion' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'swState' => 'varchar(8) NOT NULL DEFAULT \'present\'', + 'swSource' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'swArgs' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'swTimeout' => 'int(11) NOT NULL DEFAULT 900', + 'swReturnCodes' => 'text DEFAULT NULL', + 'swEnabled' => 'tinyint(1) NOT NULL DEFAULT 1', + 'swCreateDate' => 'timestamp NOT NULL DEFAULT current_timestamp()', + 'swCreator' => 'varchar(50) NOT NULL DEFAULT \'\'', + ], + ], + 'softwareAssoc' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `softwareAssoc` ( `swaID` int(11) NOT NULL AUTO_INCREMENT, `swaHostID` int(11) NOT NULL, `swaSoftwareID` int(11) NOT NULL, `swaSequence` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`swaID`), UNIQUE KEY `swaHostSoftware` (`swaHostID`,`swaSoftwareID`), KEY `swaSoftwareID` (`swaSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'swaID' => 'int(11) NOT NULL', + 'swaHostID' => 'int(11) NOT NULL', + 'swaSoftwareID' => 'int(11) NOT NULL', + 'swaSequence' => 'int(11) NOT NULL DEFAULT 0', + ], + ], + 'softwareStatus' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `softwareStatus` ( `sstID` int(11) NOT NULL AUTO_INCREMENT, `sstHostID` int(11) NOT NULL, `sstSoftwareID` int(11) NOT NULL, `sstInstalledVersion` varchar(64) NOT NULL DEFAULT \'\', `sstStatus` varchar(16) NOT NULL DEFAULT \'\', `sstReturnCode` int(11) NOT NULL DEFAULT 0, `sstDetails` text NOT NULL DEFAULT \'\', `sstChecked` datetime DEFAULT NULL, PRIMARY KEY (`sstID`), UNIQUE KEY `sstHostSoftware` (`sstHostID`,`sstSoftwareID`), KEY `sstSoftwareID` (`sstSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'sstID' => 'int(11) NOT NULL', + 'sstHostID' => 'int(11) NOT NULL', + 'sstSoftwareID' => 'int(11) NOT NULL', + 'sstInstalledVersion' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'sstStatus' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'sstReturnCode' => 'int(11) NOT NULL DEFAULT 0', + 'sstDetails' => 'text NOT NULL DEFAULT \'\'', + 'sstChecked' => 'datetime DEFAULT NULL', ], ], 'storageEpoch' => [ @@ -959,7 +1180,7 @@ ], ], 'tasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `tasks` ( `taskID` int(11) NOT NULL AUTO_INCREMENT, `taskName` varchar(250) NOT NULL DEFAULT \'\', `taskCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), `taskCheckIn` datetime DEFAULT NULL, `taskHostID` int(11) NOT NULL, `taskImageID` int(11) DEFAULT NULL, `taskStateID` int(11) NOT NULL, `taskIsDebug` mediumint(9) NOT NULL DEFAULT 0, `taskCreateBy` varchar(200) NOT NULL DEFAULT \'\', `taskForce` varchar(1) NOT NULL DEFAULT \'\', `taskScheduledStartTime` datetime DEFAULT NULL, `taskTypeID` mediumint(9) NOT NULL, `taskPCT` int(10) unsigned zerofill NOT NULL DEFAULT 0000000000, `taskBPM` varchar(250) NOT NULL DEFAULT \'\', `taskTimeElapsed` varchar(250) NOT NULL DEFAULT \'\', `taskTimeRemaining` varchar(250) NOT NULL DEFAULT \'\', `taskDataCopied` varchar(250) NOT NULL DEFAULT \'\', `taskPercentText` varchar(250) NOT NULL DEFAULT \'\', `taskDataTotal` varchar(250) NOT NULL DEFAULT \'\', `taskNFSGroupID` int(11) DEFAULT NULL, `taskNFSMemberID` int(11) DEFAULT NULL, `taskNFSFailures` char(1) NOT NULL DEFAULT \'\', `taskLastMemberID` int(11) DEFAULT NULL, `taskWOL` tinyint(1) NOT NULL DEFAULT 0, `taskPassreset` varchar(250) NOT NULL DEFAULT \'\', `taskShutdown` char(1) NOT NULL DEFAULT \'\', `taskBypassBitlocker` tinyint(1) NOT NULL DEFAULT 0, `taskStateChangedTime` datetime DEFAULT NULL, PRIMARY KEY (`taskID`), KEY `new_index` (`taskHostID`), KEY `new_index1` (`taskCheckIn`), KEY `new_index2` (`taskStateID`), KEY `new_index3` (`taskForce`), KEY `new_index4` (`taskTypeID`), KEY `new_index5` (`taskNFSGroupID`), KEY `new_index6` (`taskNFSMemberID`), KEY `new_index7` (`taskNFSFailures`), KEY `new_index8` (`taskLastMemberID`), KEY `idx_taskCreateTime` (`taskCreateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `tasks` ( `taskID` int(11) NOT NULL AUTO_INCREMENT, `taskName` varchar(250) NOT NULL DEFAULT \'\', `taskCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), `taskCheckIn` datetime DEFAULT NULL, `taskHostID` int(11) NOT NULL, `taskImageID` int(11) DEFAULT NULL, `taskStateID` int(11) NOT NULL, `taskIsDebug` mediumint(9) NOT NULL DEFAULT 0, `taskCreateBy` varchar(200) NOT NULL DEFAULT \'\', `taskForce` varchar(1) NOT NULL DEFAULT \'\', `taskScheduledStartTime` datetime DEFAULT NULL, `taskTypeID` mediumint(9) NOT NULL, `taskPCT` int(10) unsigned zerofill NOT NULL DEFAULT 0000000000, `taskBPM` varchar(250) NOT NULL DEFAULT \'\', `taskTimeElapsed` varchar(250) NOT NULL DEFAULT \'\', `taskTimeRemaining` varchar(250) NOT NULL DEFAULT \'\', `taskDataCopied` varchar(250) NOT NULL DEFAULT \'\', `taskPercentText` varchar(250) NOT NULL DEFAULT \'\', `taskDataTotal` varchar(250) NOT NULL DEFAULT \'\', `taskNFSGroupID` int(11) DEFAULT NULL, `taskNFSMemberID` int(11) DEFAULT NULL, `taskNFSFailures` char(1) NOT NULL DEFAULT \'\', `taskLastMemberID` int(11) DEFAULT NULL, `taskWOL` tinyint(1) NOT NULL DEFAULT 0, `taskPassreset` varchar(250) NOT NULL DEFAULT \'\', `taskShutdown` char(1) NOT NULL DEFAULT \'\', `taskBypassBitlocker` tinyint(1) NOT NULL DEFAULT 0, `taskStateChangedTime` datetime DEFAULT NULL, PRIMARY KEY (`taskID`), KEY `new_index` (`taskHostID`), KEY `new_index1` (`taskCheckIn`), KEY `new_index2` (`taskStateID`), KEY `new_index3` (`taskForce`), KEY `new_index4` (`taskTypeID`), KEY `new_index5` (`taskNFSGroupID`), KEY `new_index6` (`taskNFSMemberID`), KEY `new_index7` (`taskNFSFailures`), KEY `new_index8` (`taskLastMemberID`), KEY `idx_taskCreateTime` (`taskCreateTime`), KEY `fk_tasks_taskImageID` (`taskImageID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'taskID' => 'int(11) NOT NULL', 'taskName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -1017,7 +1238,7 @@ ], ], 'userAuths' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `userAuths` ( `uaID` int(11) NOT NULL AUTO_INCREMENT, `uaUserID` int(11) NOT NULL, `uaExpireDate` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `uaIsExpired` int(11) NOT NULL DEFAULT 0, `uaSelectorHash` varchar(255) NOT NULL DEFAULT \'\', `uaPasswordHash` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`uaID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `userAuths` ( `uaID` int(11) NOT NULL AUTO_INCREMENT, `uaUserID` int(11) NOT NULL, `uaExpireDate` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `uaIsExpired` int(11) NOT NULL DEFAULT 0, `uaSelectorHash` varchar(255) NOT NULL DEFAULT \'\', `uaPasswordHash` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`uaID`), KEY `fk_userAuths_uaUserID` (`uaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'uaID' => 'int(11) NOT NULL', 'uaUserID' => 'int(11) NOT NULL', @@ -1027,15 +1248,8 @@ 'uaPasswordHash' => 'varchar(255) NOT NULL DEFAULT \'\'', ], ], - 'userCleanup' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `userCleanup` ( `ucID` int(11) NOT NULL AUTO_INCREMENT, `ucName` varchar(254) NOT NULL DEFAULT \'\', PRIMARY KEY (`ucID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'ucID' => 'int(11) NOT NULL', - 'ucName' => 'varchar(254) NOT NULL DEFAULT \'\'', - ], - ], 'userGroupMembers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `userGroupMembers` ( `ugmID` int(11) NOT NULL AUTO_INCREMENT, `ugmName` varchar(60) NOT NULL DEFAULT \'\', `ugmGroupID` int(11) NOT NULL, `ugmUserID` int(11) NOT NULL, PRIMARY KEY (`ugmID`), UNIQUE KEY `ugmGroupUser` (`ugmGroupID`,`ugmUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `userGroupMembers` ( `ugmID` int(11) NOT NULL AUTO_INCREMENT, `ugmName` varchar(60) NOT NULL DEFAULT \'\', `ugmGroupID` int(11) NOT NULL, `ugmUserID` int(11) NOT NULL, PRIMARY KEY (`ugmID`), UNIQUE KEY `ugmGroupUser` (`ugmGroupID`,`ugmUserID`), KEY `fk_userGroupMembers_ugmUserID` (`ugmUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ugmID' => 'int(11) NOT NULL', 'ugmName' => 'varchar(60) NOT NULL DEFAULT \'\'', diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 4065bda047..c41cc22746 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -10772,3 +10772,854 @@ function () { . "WHERE `settingKey`='FOG_MEMTEST_KERNEL' " . "AND `settingValue`='memtest.bin'", ]; + +// 416 +$this->schema[] = [ + // fog-agent enrollment (docs/design in the fog-agent repo, section 4; + // wire contract in its docs/design/protocol-v1.md). + // + // An agent that has no certificate yet presents its firmware identity, + // its MACs and a CSR. Nothing is issued until one of three approvals + // happens: an admin clicks Approve, a valid enrollment token was + // presented, or this server itself imaged the host within + // FOG_AGENT_ENROLL_DEPLOY_WINDOW hours. Until then the request waits + // here, verbatim, so that what gets signed on approval is exactly what + // was presented and not a re-read of anything the agent could change in + // between. + // + // One row per KEY (aeFingerprint is the sha256 of the SubjectPublicKeyInfo), + // not per request: an agent repeats the identical request every few + // minutes while it waits, and the repeat refreshes the row rather than + // adding one. A denied key stays denied across repeats for the same + // reason. + // + // aeHostID 0 until the request is bound to a host. Set at approval, + // or immediately when the identity resolved to a host and + // the request is merely waiting for a click. + // aeIdentity the SMBIOS tuple, smbios version and MAC list as the + // agent sent them, JSON. Kept raw: canonicalization is + // SmbiosIdentity's job at read time, the same as for boot. + // aeReason why it is waiting: unknown-host, known-host-no-agent, + // rebind, identity-conflict. Shown to the admin. + // aeState pending, issued, denied. + // aeCert the issued leaf plus its chain, PEM. Filled at approval + // so the agent's next poll can collect it; cleared once + // collected so a database read does not hand out a + // certificate twice. + "CREATE TABLE IF NOT EXISTS `agentEnrollment` ( " + . "`aeID` int(11) NOT NULL AUTO_INCREMENT, " + . "`aeHostID` int(11) NOT NULL DEFAULT 0, " + . "`aeFingerprint` varchar(64) NOT NULL DEFAULT '', " + . "`aeCSR` text NOT NULL, " + . "`aeIdentity` text NOT NULL DEFAULT '', " + . "`aeHostname` varchar(191) NOT NULL DEFAULT '', " + . "`aeOS` varchar(20) NOT NULL DEFAULT '', " + . "`aeArch` varchar(20) NOT NULL DEFAULT '', " + . "`aeAgentVersion` varchar(50) NOT NULL DEFAULT '', " + . "`aeRemoteIP` varchar(45) NOT NULL DEFAULT '', " + . "`aeReason` varchar(32) NOT NULL DEFAULT '', " + . "`aeState` varchar(16) NOT NULL DEFAULT 'pending', " + . "`aeCert` text NOT NULL DEFAULT '', " + . "`aeCreated` datetime DEFAULT NULL, " + . "`aeUpdated` datetime DEFAULT NULL, " + . "`aeDecided` datetime DEFAULT NULL, " + . "`aeDecidedBy` varchar(191) NOT NULL DEFAULT '', " + . "`aeDecidedVia` varchar(16) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`aeID`), " + . "UNIQUE KEY `aeFingerprint` (`aeFingerprint`), " + . "KEY `aeState` (`aeState`), " + . "KEY `aeHostID` (`aeHostID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Enrollment tokens: an admin's pre-approval, minted in the UI and baked + // into an installer or an image's bootstrap file. Only the sha256 of the + // token is stored, so a database read does not yield a usable token. + // atUses counts down; -1 means unlimited until atExpires. + "CREATE TABLE IF NOT EXISTS `agentEnrollToken` ( " + . "`atID` int(11) NOT NULL AUTO_INCREMENT, " + . "`atName` varchar(191) NOT NULL DEFAULT '', " + . "`atHash` varchar(64) NOT NULL DEFAULT '', " + . "`atUses` int(11) NOT NULL DEFAULT 1, " + . "`atExpires` datetime DEFAULT NULL, " + . "`atCreatedBy` varchar(191) NOT NULL DEFAULT '', " + . "`atCreated` datetime DEFAULT NULL, " + . "PRIMARY KEY (`atID`), " + . "UNIQUE KEY `atHash` (`atHash`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // What the host knows about its agent. The fingerprint is the binding: + // a client certificate whose key does not hash to this value is not this + // host's agent, whatever its subject says. Same shape as the Secure Boot + // enrollment columns above it in the host table. + "ALTER TABLE `hosts` " + . "ADD COLUMN `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT '', " + . "ADD COLUMN `hostAgentNotAfter` datetime DEFAULT NULL, " + . "ADD COLUMN `hostAgentVersion` varchar(50) NOT NULL DEFAULT '', " + . "ADD COLUMN `hostAgentCheckin` datetime DEFAULT NULL, " + . "ADD KEY `hostAgentFingerprint` (`hostAgentFingerprint`)", + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_AGENT_ENROLL_DEPLOY_WINDOW','Hours after this server completes " + . "a deploy to a host during which an agent presenting that host''s " + . "firmware identity is enrolled without an admin approving it. The " + . "deploy was the approval. 0 disables the shortcut and every " + . "enrollment waits for a click or a token.','24','General Settings')", +]; +// 417 +$this->schema[] = [ + // fog-agent snapins (design 0001 section 7, protocol-v1 "Snapins"). + // A snapin's return-code table: one `code=class` per line, class one + // of success, reboot, retry, failed. Empty means the installer + // defaults (0 and 1707 success, 3010 and 1641 reboot, 1618 retry). + // The server reads a task's exit code against it for the agent and + // the legacy client alike, so an MSI that answers 3010 is a success + // that needs a reboot instead of a failed job. + "ALTER TABLE `snapins` ADD COLUMN IF NOT EXISTS `sReturnCodes` text NULL", + // What a run came to: success, reboot, retry, failed, or when the + // payload never ran, hash_mismatch, timeout, cannot_run. Beside the + // raw exit code, which stays the program's own. + "ALTER TABLE `snapinTasks` ADD COLUMN IF NOT EXISTS `stStatus` varchar(16) NOT NULL DEFAULT '' AFTER `stReturnCode`", + // Installers put the useful line well past 250 characters; the agent + // reports the last 4 KB of output. + "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL", +]; +// 418 +$this->schema[] = [ + // fog-agent software management (design 0003). Software is desired + // state, not a task: a package id plus a version policy, held on the + // host by a package manager (Chocolatey first) and reported back with + // the version the host actually has. Snapins stay as they are. + "CREATE TABLE IF NOT EXISTS `software` ( " + . "`swID` int(11) NOT NULL AUTO_INCREMENT, " + . "`swName` varchar(200) NOT NULL, " + . "`swDesc` longtext NOT NULL DEFAULT '', " + // Which package manager knows the package: choco now, others later + // behind the same interface. + . "`swBackend` varchar(16) NOT NULL DEFAULT 'choco', " + . "`swPackage` varchar(255) NOT NULL, " + // '' any version, 'latest' tracks the source, else an exact pin. + . "`swVersion` varchar(64) NOT NULL DEFAULT '', " + . "`swState` varchar(8) NOT NULL DEFAULT 'present', " + . "`swSource` varchar(255) NOT NULL DEFAULT '', " + . "`swArgs` varchar(255) NOT NULL DEFAULT '', " + . "`swTimeout` int(11) NOT NULL DEFAULT 900, " + // The same code=class table snapins carry (schema 417). + . "`swReturnCodes` text NULL, " + . "`swEnabled` tinyint(1) NOT NULL DEFAULT 1, " + . "`swCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), " + . "`swCreator` varchar(50) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`swID`), " + . "UNIQUE KEY `swName` (`swName`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Host-direct assignment, ordered like snapinAssoc. + "CREATE TABLE IF NOT EXISTS `softwareAssoc` ( " + . "`swaID` int(11) NOT NULL AUTO_INCREMENT, " + . "`swaHostID` int(11) NOT NULL, " + . "`swaSoftwareID` int(11) NOT NULL, " + . "`swaSequence` int(11) NOT NULL DEFAULT 0, " + . "PRIMARY KEY (`swaID`), " + . "UNIQUE KEY `swaHostSoftware` (`swaHostID`,`swaSoftwareID`), " + . "KEY `swaSoftwareID` (`swaSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Group grant, the ADR 0038 shape: a fact about the group, resolved + // per host after its direct assignments and deduplicated. + "CREATE TABLE IF NOT EXISTS `groupSoftwareAssoc` ( " + . "`gswaID` int(11) NOT NULL AUTO_INCREMENT, " + . "`gswaGroupID` int(11) NOT NULL, " + . "`gswaSoftwareID` int(11) NOT NULL, " + . "`gswaSequence` int(11) NOT NULL DEFAULT 0, " + . "PRIMARY KEY (`gswaID`), " + . "UNIQUE KEY `gswaGroupSoftware` (`gswaGroupID`,`gswaSoftwareID`), " + . "KEY `gswaSoftwareID` (`gswaSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // What each host last reported per entry: one row, refreshed in + // place, so the host's Software tab is a current picture rather than + // a history. + "CREATE TABLE IF NOT EXISTS `softwareStatus` ( " + . "`sstID` int(11) NOT NULL AUTO_INCREMENT, " + . "`sstHostID` int(11) NOT NULL, " + . "`sstSoftwareID` int(11) NOT NULL, " + . "`sstInstalledVersion` varchar(64) NOT NULL DEFAULT '', " + // converged, installed, upgraded, removed, failed, retry, reboot, + // timeout, cannot_run. + . "`sstStatus` varchar(16) NOT NULL DEFAULT '', " + . "`sstReturnCode` int(11) NOT NULL DEFAULT 0, " + . "`sstDetails` text NOT NULL DEFAULT '', " + . "`sstChecked` datetime DEFAULT NULL, " + . "PRIMARY KEY (`sstID`), " + . "UNIQUE KEY `sstHostSoftware` (`sstHostID`,`sstSoftwareID`), " + . "KEY `sstSoftwareID` (`sstSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // 417 widened stReturnDetails without a default, which a strict server + // refuses on an INSERT that omits it (the legacy client's does). + "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL DEFAULT ''", + // How often a host re-checks its software set when nothing changed on + // the server. Six hours: often enough to catch a removed package the + // same working day, rare enough that choco is not a poll-loop cost. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_SOFTWARE_DRIFT_INTERVAL','Seconds between a host''s checks of " + . "its software set when the set has not changed. The check runs " + . "choco against every entry, so keep it hours, not minutes.'," + . "'21600','FOG Client')", +]; +// 419 +$this->schema[] = [ + // 418 tried to seed the software module row at id 13, which step 223 + // had already given to powermanagement, so INSERT IGNORE dropped it on + // every server that has that step. Seed by short name instead and let + // the id be whatever is free; nothing keys on the number. + "INSERT INTO `modules` (`name`, `short_name`, `description`) " + . "SELECT 'Software', 'software', 'This setting will enable or disable " + . "the software management module on this specific host. If the module " + . "is globally disabled, this setting is ignored.' " + . "FROM DUAL WHERE NOT EXISTS " + . "(SELECT 1 FROM `modules` WHERE `short_name` = 'software')", + // The global switch every module has (FOGBase::getGlobalModuleStatus). + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_CLIENT_SOFTWARE_ENABLED','This setting defines if the agent''s " + . "software management module should be enabled on client computers. " + . "It holds each host to its assigned software through the host''s " + . "package manager. (Valid values: 0 or 1).','1','FOG Client')", + // The two TEXT defaults 418 carries in its final form but not on a + // server that ran it before they were added: a strict server refuses + // an INSERT that omits a NOT NULL column with no default, and the + // legacy client's snapin close-out omits stReturnDetails. + "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL DEFAULT ''", + "ALTER TABLE `softwareStatus` MODIFY COLUMN `sstDetails` text NOT NULL DEFAULT ''", +]; +// 420 +$this->schema[] = [ + // Chocolatey bootstrap (fog-agent design 0003 section 8). Off by + // default: the agent runs the fetched script as SYSTEM, so an admin + // opts in by naming the script, the community one or a copy they host. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_SOFTWARE_CHOCO_BOOTSTRAP_URL','URL of Chocolatey''s install " + . "script for hosts that have software assigned but no Chocolatey. " + . "Empty leaves such hosts reporting cannot run. The public script is " + . "https://community.chocolatey.org/install.ps1; a copy on a server " + . "you control works too. The agent runs it as SYSTEM.','','FOG Client')", + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_SOFTWARE_CHOCO_NUPKG_URL','Optional URL of the chocolatey " + . ".nupkg the bootstrap script installs from (its chocolateyDownloadUrl), " + . "for hosts with no route to the community feed. Empty uses the " + . "script''s default.','','FOG Client')", +]; +// 421 +$this->schema[] = [ + // fog-agent inventory and installed-software reporting (design 0006). + // What the agent last reported per host, per fact kind: hsSource is + // part of identity because an OS package list enumerates the same + // name under more than one manager, and hsVersion because two + // versions can be installed at once and an upgrade is one version + // removed, another added -- the history a report wants. hsRemovedAt + // NULL is "installed now"; a row is never deleted, only closed out. + "CREATE TABLE IF NOT EXISTS `hostSoftware` ( " + . "`hsID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hsHostID` int(11) NOT NULL, " + . "`hsName` varchar(255) NOT NULL, " + . "`hsVersion` varchar(128) NOT NULL DEFAULT '', " + . "`hsPublisher` varchar(255) NOT NULL DEFAULT '', " + . "`hsSource` varchar(16) NOT NULL DEFAULT '', " + . "`hsArch` varchar(16) NOT NULL DEFAULT '', " + . "`hsInstallDate` date DEFAULT NULL, " + . "`hsFirstSeen` datetime DEFAULT NULL, " + . "`hsLastSeen` datetime DEFAULT NULL, " + . "`hsRemovedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hsID`), " + . "UNIQUE KEY `hsHostNameSrcVer` (`hsHostID`,`hsName`,`hsSource`,`hsVersion`), " + . "KEY `hsName` (`hsName`), " + . "KEY `hsHostRemoved` (`hsHostID`,`hsRemovedAt`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // What the server last heard from a host, per fact kind (inventory, + // software): the hash it holds and when. Doubles as "when did we last + // hear facts from this host". A missing row is the want_* signal -- + // the agent has never successfully reported that kind. + "CREATE TABLE IF NOT EXISTS `hostFactState` ( " + . "`hfsID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hfsHostID` int(11) NOT NULL, " + . "`hfsKind` varchar(16) NOT NULL, " + . "`hfsHash` varchar(64) NOT NULL DEFAULT '', " + . "`hfsUpdated` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hfsID`), " + . "UNIQUE KEY `hfsHostKind` (`hfsHostID`,`hfsKind`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // The one gate for both: an installed-program list is mildly + // sensitive, so a site can turn collection off. Hardware inventory + // and software share it for v1 (design 0006 section 2, "Gate"). + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_AGENT_INVENTORY_ENABLED','This setting defines if the agent " + . "collects and reports hardware inventory and installed software for " + . "its host. When disabled, the server never requests a report and " + . "ignores one if it arrives. (Valid values: 0 or 1).','1','FOG Client')", +]; + +// 422 +$this->schema[] = [ + // fog-agent user tracking as sessions (design 0008). One row per logon + // with two ends, replacing the login/logout EVENT pairs in + // `userTracking` -- which is not touched, and keeps working for 1.5 + // clients and the Activity page. + // + // The event log cannot answer "who is logged in now": a logout needs a + // network round trip at the moment the machine is going away, so six of + // eleven sessions on the lab server have no logout at all. Here the open + // set is re-reported and whatever stops being reported is closed -- + // marked `inferred`, dated to husLastSeen, because "we never found out" + // and "logged out at 11:54" are different facts. + // + // husSessionKey plus husStartedAt is identity: a second logon by the + // same user is a distinct session, not an ambiguous second event. + // husUserName and husDomain are separate and unmangled, unlike the + // legacy table, which lowercases and strips the domain and so merges + // CORP\jsmith with LAB\jsmith. + "CREATE TABLE IF NOT EXISTS `hostUserSession` ( " + . "`husID` int(11) NOT NULL AUTO_INCREMENT, " + . "`husHostID` int(11) NOT NULL, " + . "`husSessionKey` varchar(191) NOT NULL DEFAULT '', " + . "`husUserName` varchar(255) NOT NULL DEFAULT '', " + . "`husDomain` varchar(255) NOT NULL DEFAULT '', " + . "`husUserSID` varchar(191) NOT NULL DEFAULT '', " + . "`husType` varchar(32) NOT NULL DEFAULT '', " + . "`husState` varchar(32) NOT NULL DEFAULT '', " + . "`husRemoteHost` varchar(255) NOT NULL DEFAULT '', " + . "`husStartedAt` datetime NOT NULL, " + . "`husEndedAt` datetime DEFAULT NULL, " + . "`husEndReason` varchar(32) NOT NULL DEFAULT '', " + . "`husLastSeen` datetime DEFAULT NULL, " + . "PRIMARY KEY (`husID`), " + . "UNIQUE KEY `husHostKeyStart` (`husHostID`,`husSessionKey`,`husStartedAt`), " + . "KEY `husHostOpen` (`husHostID`,`husEndedAt`), " + . "KEY `husUserName` (`husUserName`), " + . "KEY `husStartedAt` (`husStartedAt`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Whether to keep feeding the legacy table as well. On by default so a + // fleet migrating to fog-agent sees no gap in the Activity page it + // already uses; off for an estate that has fully migrated and does not + // want the duplicate rows. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_USERTRACKING_COMPAT_WRITE','This setting defines if agent-reported " + . "user sessions are also written to the legacy user tracking table, so " + . "the Activity page keeps showing them while an estate migrates. Turn it " + . "off once every client is a fog-agent. (Valid values: 0 or 1).','1','FOG Client')", + // Sessions age out on their own clock: they are far coarser than the + // event rows (one per logon, not two), so an estate may want to keep + // them longer than the legacy table's. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_HOSTUSERSESSION_RETENTION_DAYS','This setting defines how many " + . "days of agent-reported user sessions to keep. Zero keeps them " + . "forever. (Valid values: a whole number of days).','365','FOG Audit')", +]; + +// 423 +$this->schema[] = [ + // What directory each host is ACTUALLY a member of, and + // where its computer object ACTUALLY sits (design 0009). + // + // The hosts table's hostADDomain and hostADOU are intent -- what an + // admin typed into a form -- and nothing anywhere has ever recorded the + // other half. So FOG cannot answer "which of my machines are not where + // I think they are", and the legacy client made that worse by never + // comparing the OU at all: it short-circuits on "already joined to the + // target domain" and reads the OU only as lpAccountOU at the initial + // join, so editing a host's OU does nothing, forever, silently. + // + // One row per host, not a history: this is current state, so it is + // replaced in place and needs no retention entry. hdComputerDN is the + // load-bearing column -- a server-side LDAP Modify DN needs the exact + // object, and having the machine report its own DN means the server + // never has to search by name and guess between duplicates. + // + // hdObservedAt is when this membership was last REPORTED, not when it + // was last confirmed true. The agent hash-gates the block, so an + // unchanged membership is never sent and this column would otherwise + // claim a freshness nobody checked. "Is this still true" is answered by + // the host's own hostAgentCheckin, which the report shows beside it. + "CREATE TABLE IF NOT EXISTS `hostDirectory` ( " + . "`hdID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hdHostID` int(11) NOT NULL, " + . "`hdJoined` tinyint(1) NOT NULL DEFAULT 0, " + . "`hdKind` varchar(32) NOT NULL DEFAULT '', " + . "`hdDomain` varchar(255) NOT NULL DEFAULT '', " + . "`hdNetbios` varchar(64) NOT NULL DEFAULT '', " + . "`hdComputerDN` varchar(1024) NOT NULL DEFAULT '', " + . "`hdMachineAccount` varchar(255) NOT NULL DEFAULT '', " + . "`hdSite` varchar(255) NOT NULL DEFAULT '', " + . "`hdObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hdID`), " + . "UNIQUE KEY `hdHostID` (`hdHostID`), " + . "KEY `hdDomain` (`hdDomain`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", +]; + +// 424 +$this->schema[] = [ + // Design 0009 section 5: FOG moves a computer object between OUs itself, + // with one LDAP Modify DN, instead of asking the machine to leave the + // domain and rejoin -- which is what an admin is reduced to today, and + // which costs the object its password and, if it is recreated, its SID. + // + // Proven against a real DC before these columns existed: a service + // account delegated create/delete-child of computer objects on ONE + // subtree moved an object between two OUs under it, and was refused + // ("Insufficient access") when it tried to move the same object out of + // that subtree. That refusal is the whole security argument for giving + // FOG an account of its own rather than a domain admin. + // + // Named for the ATTEMPT, not the move. This stamp is written whenever + // placement consults the directory about a host -- which is also how a + // host that cannot report its own DN is kept off an every-poll LDAP + // search -- so a name like hdMovedAt would say a move happened on every + // occasion nothing did. + "ALTER TABLE `hostDirectory` " + . "ADD COLUMN `hdPlacementAt` datetime DEFAULT NULL, " + . "ADD COLUMN `hdPlacementError` varchar(255) NOT NULL DEFAULT ''", + // Off until it is configured. Placement WRITES to the directory, so it + // must never begin working because someone upgraded. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_PLACEMENT_ENABLED','This setting defines if FOG moves " + . "a host\\'s computer object into the OU set on the host, using the " + . "directory account below. Off by default: this writes to your " + . "directory. (Valid values: 0 or 1).','0','FOG Directory')", + // ldaps:// or an ldap:// that will be promoted with StartTLS. A bind + // carries the credential, and Active Directory refuses a simple bind on + // a cleartext connection anyway ("Strong(er) authentication required"). + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_LDAP_URI','This setting defines the directory server " + . "FOG connects to when placing computer objects, as a URI. Use " + . "ldaps://dc.example.com -- a host name, not an address, or the " + . "server\\'s certificate cannot be verified. (Valid values: an LDAP " + . "URI).','','FOG Directory')", + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_BIND_DN','This setting defines the account FOG binds " + . "as to move computer objects, as a userPrincipalName or a full DN. It " + . "needs create-child and delete-child of computer objects on the " + . "subtree holding them, and nothing else. (Valid values: a UPN or " + . "DN).','','FOG Directory')", + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_BIND_PASSWORD','This setting defines the password for " + . "the directory account above. Stored encrypted. (Valid values: a " + . "password).','','FOG Directory')", + // The search base for the fallback when a host could not report its own + // DN -- no Linux join tool exposes one, so the server looks the object + // up by its machine account name instead. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_BASE_DN','This setting defines where FOG searches for " + . "a computer object when the host could not report its own " + . "distinguished name, which is normal on Linux. (Valid values: a base " + . "DN such as DC=example,DC=com).','','FOG Directory')", + // A private CA is the norm for a directory, and refusing to verify the + // certificate would make the TLS above decorative. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_DIRECTORY_CA_CERT','This setting defines the path to the CA " + . "certificate that signed your directory server\\'s certificate. Leave " + . "empty to use the system trust store. (Valid values: a file " + . "path).','','FOG Directory')", +]; + +// 425 +$this->schema[] = [ + // Step 424 described FOG_DIRECTORY_BIND_PASSWORD as "Stored encrypted". + // It is not, and saying so is worse than saying nothing: an admin who + // believes a secret is encrypted at rest will treat a database dump as + // safe to hand over. + // + // FOG has no key store. aesencrypt() takes a key and PUTS IT IN THE + // CIPHERTEXT (`iv|data|key`), and aesdecrypt() returns anything without a + // `|` unchanged -- which is why the LDAP plugin's bind password has always + // been stored exactly as typed. This setting behaves the same way, so the + // description now says what is true and points at the mitigation that is + // real: delegate the account narrowly, so what the row is worth to an + // attacker is bounded by what the account can do. + "UPDATE `globalSettings` SET `settingDesc` = 'This setting defines the " + . "password for the directory account above. Stored in the database as " + . "typed -- FOG has no key store, so treat a database dump as disclosing " + . "it, and delegate the account to one subtree rather than granting it " + . "domain rights. (Valid values: a password).' " + . "WHERE `settingKey` = 'FOG_DIRECTORY_BIND_PASSWORD'", +]; + +// 426 +$this->schema[] = [ + // Design 0010: FOG has never recorded what printers a machine actually + // has. Both legacy platform managers had a GetPrinters() and neither + // ever transmitted the result -- all three call sites are local + // decisions inside PrinterManager.cs -- so "did the printer I assigned + // actually install?" has had no answer since the feature shipped. + // + // Two tables rather than one. hostSpooler is the per-host anchor: which + // print subsystem the machine runs, and when it last said so. It exists + // separately from hostPrinter because a machine with CUPS and no queues + // has REPORTED, and a report that could only see hostPrinter rows would + // show that host as never having answered -- which is precisely the + // invisible-absence failure design 0010 section 6 is built to avoid. + // + // hostFactState already records the same "when did this host last + // report kind X", but that table is the poll's hash cache. A report + // built on it would couple an admin-facing page to the protocol's + // internal bookkeeping, and would break the next time the gate changes. + "CREATE TABLE IF NOT EXISTS `hostSpooler` ( " + . "`hspID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hspHostID` int(11) NOT NULL, " + . "`hspSubsystem` varchar(16) NOT NULL DEFAULT '', " + . "`hspObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hspID`), " + . "UNIQUE KEY `hspHostID` (`hspHostID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // One row per queue observed on a host. + // + // hpURI is the load-bearing column and the whole of design 0010 section + // 2: both spoolers already describe a printer as a device URI plus a + // driver, so recording the URI is what lets a Windows row and a CUPS row + // for the same physical device be recognized as the same device. FOG's + // pConfig could never do that -- it named a code path, not a printer. + // + // hpDriver empty is a real value meaning driverless (IPP Everywhere), + // which FOG's existing model has no way to express at all. + "CREATE TABLE IF NOT EXISTS `hostPrinter` ( " + . "`hpID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hpHostID` int(11) NOT NULL, " + . "`hpName` varchar(255) NOT NULL DEFAULT '', " + . "`hpURI` varchar(1024) NOT NULL DEFAULT '', " + . "`hpDriver` varchar(255) NOT NULL DEFAULT '', " + . "`hpDefault` tinyint(1) NOT NULL DEFAULT 0, " + . "`hpShared` tinyint(1) NOT NULL DEFAULT 0, " + . "`hpObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hpID`), " + . "UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), " + . "KEY `hpName` (`hpName`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Where a failed install gets to live. Today a printer that will not + // install produces nothing an admin can see: the client retries the same + // thing every poll, forever, silently. paAppliedAt is named for the + // ATTEMPT, not the success -- the hdPlacementAt lesson from step 424. + "ALTER TABLE `printerAssoc` " + . "ADD COLUMN `paAppliedAt` datetime DEFAULT NULL, " + . "ADD COLUMN `paError` varchar(255) NOT NULL DEFAULT ''", + // paIsDefault is a varchar(2) holding a boolean; groupPrinterAssoc's + // gpaIsDefault is a tinyint(1) holding the same idea. Same concept, two + // types, because they were added years apart. + // + // The UPDATE has to come first. The column holds '' on every row nobody + // ever set, and MariaDB in strict mode refuses to convert '' to an + // integer -- so a bare MODIFY fails the upgrade on essentially every + // existing install rather than on none of them. + "UPDATE `printerAssoc` SET `paIsDefault`='0' " + . "WHERE `paIsDefault` NOT IN ('0','1')", + "ALTER TABLE `printerAssoc` " + . "MODIFY COLUMN `paIsDefault` tinyint(1) NOT NULL DEFAULT 0", + // The pre-allocated spare columns. `plugins` had the same pAnon1-pAnon5 + // and they were renamed into real columns (pIcon, pRunfile, pLocation) + // through schema-expected.php's `renames` block; the printer ones were + // never claimed by anything, in ten years. Verified across the whole + // tree before dropping: the only readers were the two Items field maps + // and four hidden DataTables export columns, all updated in this change. + "ALTER TABLE `printerAssoc` " + . "DROP COLUMN `paAnon1`, DROP COLUMN `paAnon2`, DROP COLUMN `paAnon3`, " + . "DROP COLUMN `paAnon4`, DROP COLUMN `paAnon5`", + "ALTER TABLE `printers` " + . "DROP COLUMN `pAnon2`, DROP COLUMN `pAnon3`, DROP COLUMN `pAnon4`, " + . "DROP COLUMN `pAnon5`", +]; + +// 427 +$this->schema[] = [ + // Design 0010 section 2: a printer is a device URI and a driver, which + // is how both spoolers already describe one. This is the column that + // makes a printer row portable -- the same physical device is a TCP/IP + // port on Windows and a socket:// device URI on CUPS, and until they are + // written the same way nothing can tell they are the same printer. + // + // It is also the only way to express a DRIVERLESS printer (IPP + // Everywhere), where the device describes its own capabilities and no + // driver file exists. FOG's model has pDefFile and pModel and no way to + // say "neither", which for a lot of estates is now the common case. + // + // DELIBERATELY NOT BACKFILLED, and this is a change from what design + // 0010 section 7 first proposed. Deriving pConfig/pIP/pPort into a + // stored URI once, on upgrade, bakes the derivation in: pPort is a + // longtext that has held whatever an admin typed for a decade, so some + // rows will derive wrong, and a stored wrong answer has to be found and + // corrected by hand on every install. Items\Printer::uri() derives on + // read instead, so this column holds only what an admin explicitly set, + // an empty one keeps following the type-specific fields, and fixing the + // derivation fixes every printer at once. + "ALTER TABLE `printers` " + . "ADD COLUMN `pURI` varchar(1024) NOT NULL DEFAULT ''", +]; + +// 428 +$this->schema[] = [ + // Design 0009 section 6: the agent joins a machine to the domain the + // host record asks for, and what happened is recorded here. + // + // These two columns are the whole reason the join is safe to automate. + // A join that fails on a bad password is a FAILED AUTHENTICATION + // against somebody's domain controller, and without a stamp to hold a + // cooldown against it is one per host per poll -- which is how a + // service account with a lockout policy gets locked out, taking every + // other host's join with it. `hdJoinAt` is what + // Agent\DirectoryJoin::RETRY_AFTER reads. + // + // Named for the ATTEMPT, like hdPlacementAt beside it: this is stamped + // whenever the agent acted, so a name like hdJoinedAt would claim a + // join happened on every occasion one did not. + // + // Deliberately separate from hdPlacementAt/hdPlacementError rather than + // reusing them. They are different operations by different actors -- + // the machine joins, the server moves -- and a report that showed one + // error against both would be a report that lies about which half is + // broken. + "ALTER TABLE `hostDirectory` " + . "ADD COLUMN `hdJoinAt` datetime DEFAULT NULL, " + . "ADD COLUMN `hdJoinError` varchar(255) NOT NULL DEFAULT ''", +]; + +// 429 +$this->schema[] = [ + // Design 0011 section 3: which links a host is actually on. + // + // FOG has never recorded a host's interfaces. `hosts.hostIP` is + // whatever the host last resolved to -- one address, no prefix, no + // notion of which of several interfaces it came from -- so "which + // machines share a link with host 41" has not been a question this + // server could answer, and that question is the entire basis of the + // wake relay: a magic packet is a link-layer broadcast, and FOG can + // only send one from a machine it owns. + // + // hnNetwork is the address masked to hnPrefix, stored rather than + // computed. Two hosts are on the same link when both columns match, + // which is an index lookup; the honest alternative, + // `INET_ATON(hnIPv4) & mask`, is a full scan on every wake. + // + // One row per host per interface ADDRESS, not per interface: an + // interface with two addresses is on two links and can broadcast on + // both. Replaced in place, not a history -- this is current state. + // + // hnObservedAt is when the interfaces were last REPORTED, not when + // they were last confirmed. The agent hash-gates the block, so an + // unchanged set is never sent; "is this still true" is answered by + // the host's own hostAgentCheckin, which is also what says whether + // the machine is awake enough to relay anything. + "CREATE TABLE IF NOT EXISTS `hostNetwork` ( " + . "`hnID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hnHostID` int(11) NOT NULL, " + . "`hnName` varchar(255) NOT NULL DEFAULT '', " + . "`hnMAC` varchar(17) NOT NULL DEFAULT '', " + . "`hnIPv4` varchar(15) NOT NULL DEFAULT '', " + . "`hnPrefix` tinyint(3) unsigned NOT NULL DEFAULT 0, " + . "`hnNetwork` varchar(15) NOT NULL DEFAULT '', " + . "`hnBroadcast` varchar(15) NOT NULL DEFAULT '', " + . "`hnUp` tinyint(1) NOT NULL DEFAULT 0, " + . "`hnWireless` tinyint(1) NOT NULL DEFAULT 0, " + . "`hnObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hnID`), " + . "UNIQUE KEY `hnHostAddress` (`hnHostID`,`hnName`,`hnIPv4`), " + . "KEY `hnLink` (`hnNetwork`,`hnPrefix`), " + . "KEY `hnMAC` (`hnMAC`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", +]; + +// 430 +$this->schema[] = [ + // Design 0011: one row per (machine to wake, machine asked to send it). + // + // A wake ordered now cannot be relayed now -- the neighboring agent + // finds out when it next polls -- so the request has to be written + // down somewhere, and FOG has nowhere. That is what this table is. + // + // Fanning out to SEVERAL senders is deliberate: a magic packet is one + // UDP datagram, sending three costs nothing, and the alternative is a + // wake that silently does nothing because the single chosen sender + // went to sleep between the poll and the send. + // + // It is also the first time FOG can say anything at all about whether + // a wake happened. The existing path is fire and forget: a machine + // that stays asleep is indistinguishable from a packet that never + // left the building. Here "three machines were asked and all three + // said they sent it" is a row an admin can read. + // + // awExpiresAt is what keeps a wake from being a standing instruction. + // A machine that comes back a week later must not be told to broadcast + // for a wake somebody ordered last Tuesday. + "CREATE TABLE IF NOT EXISTS `agentWake` ( " + . "`awID` int(11) NOT NULL AUTO_INCREMENT, " + . "`awTargetID` int(11) NOT NULL, " + . "`awSenderID` int(11) NOT NULL, " + . "`awRequestedAt` datetime DEFAULT NULL, " + . "`awExpiresAt` datetime DEFAULT NULL, " + . "`awStatus` varchar(16) NOT NULL DEFAULT 'pending', " + . "`awPackets` int(11) NOT NULL DEFAULT 0, " + . "`awDetail` varchar(255) NOT NULL DEFAULT '', " + . "`awReportedAt` datetime DEFAULT NULL, " + . "`awRequestedBy` varchar(255) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`awID`), " + . "KEY `awSenderStatus` (`awSenderID`,`awStatus`), " + . "KEY `awTargetID` (`awTargetID`), " + . "KEY `awExpiresAt` (`awExpiresAt`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Off by default. This asks one customer machine to put traffic on the + // network on behalf of another, which is a thing an estate owner opts + // into rather than discovers after an upgrade. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_AGENT_WAKE_RELAY_ENABLED','This setting defines if FOG may ask " + . "an enrolled agent to send a Wake-on-LAN packet for another FOG host " + . "on the same subnet. This reaches subnets that have no FOG server or " + . "storage node on them, which cannot be woken otherwise. Off by " + . "default. (Valid values: 0 or 1).','0','FOG Agent')", +]; + +// 431 +$this->schema[] = [ + // Display Manager goes, and its table with it. + // + // The module reset a client's screen resolution to a fixed size at + // logoff and at startup. That was a reasonable thing for a lab in 2010 + // and it is the wrong layer now: Windows has honored the per-monitor + // EDID-preferred mode by itself for a decade, a fixed resolution pushed + // over the top of it is wrong on every machine whose panel is not the + // size the setting names, and a laptop that docks changes its answer + // twice a day. Nothing in the rebuilt agent implements it and nothing + // will; the answer to "my screens are wrong" is not a FOG setting. + // + // This is the greenfog removal (step 375) repeated with one difference: + // Display Manager owns a table, and the table goes too. + // + // That is a deliberate, irreversible loss of the per-host `hssWidth`, + // `hssHeight` and `hssRefresh` an admin configured. Keeping the rows + // was considered and rejected. Every consumer is removed in this same + // commit -- the client endpoint, the host card, the mass-edit field, + // Host::getDispVals()/setDisp(), Group::setDisp() -- so keeping the + // table would mean keeping `HostScreenSetting`, its manager, its + // `hostscreensetting` REST route, its Authorization mapping and its + // foreign key alive to serve data that nothing writes and nothing + // honors. Step 375 named that failure exactly: a setting that lies is + // worse than no setting, and an API route reporting a resolution the + // fleet does not apply is a setting that lies. + // + // Ordered so nothing ever references something already gone: the + // per-host module answers first, then the module row, then the four + // globalSettings rows, then the table. msModuleID is a VARCHAR and step + // 34 seeded these with the short name before a later step rewrote them + // to the numeric id, so a server upgraded across that boundary can hold + // either spelling and both are matched -- the same care step 375 took. + // + // The seed steps that created all of this are deliberately NOT edited. + // schema.php is a replay log; step 326 set that precedent and step 375 + // followed it. A fresh install creates these and removes them one step + // later, which costs nothing and keeps the history readable. + "DELETE FROM `moduleStatusByHost` " + . "WHERE `msModuleID` IN ('3', 'displaymanager')", + "DELETE FROM `modules` WHERE `short_name` = 'displaymanager'", + "DELETE FROM `globalSettings` WHERE `settingKey` IN (" + . "'FOG_CLIENT_DISPLAYMANAGER_ENABLED'," + . "'FOG_CLIENT_DISPLAYMANAGER_X'," + . "'FOG_CLIENT_DISPLAYMANAGER_Y'," + . "'FOG_CLIENT_DISPLAYMANAGER_R')", + Schema::dropTable('hostScreenSettings'), +]; + +// 432 +$this->schema[] = [ + // Auto Log Out gets a configurable warning, and loses a setting that + // has never done anything. + // + // FOG_CLIENT_AUTOLOGOFF_WARN is how long the user is told before they + // are logged off. The legacy .NET client baked that countdown in; the + // rebuilt agent (design 0014) takes it from here, and 0 means log the + // user off with no warning at all -- legal, and what a kiosk wants. + // Sixty seconds is the default because it is long enough to notice and + // short enough that the machine is actually freed. + // + // FOG_CLIENT_AUTOLOGOFF_BGIMAGE goes. It named the 300x300 background + // of the .NET client's countdown window, and there is no countdown + // window any more: the agent is a service in session 0, which has had + // no visible desktop since Vista, so it warns through WTSSendMessage -- + // rendered by winlogon inside the user's own session, and not something + // an image can be attached to. Nothing has read this setting since the + // legacy client stopped shipping, and the FOG Configuration page never + // rendered it, so it is a row that promises a thing FOG cannot do. That + // is the same defect step 375 removed FOG_CLIENT_GREENFOG_ENABLED for + // and step 326 removed FOG_PLUGINSYS_DIR for: a setting that lies is + // worse than no setting. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_CLIENT_AUTOLOGOFF_WARN','This setting defines how many seconds " + . "before an automatic log out the user is warned. 0 logs the user out " + . "with no warning. The warning is a message box shown in the users own " + . "session; moving the mouse or pressing a key cancels the log out.'," + . "'60','FOG Client - Auto Log Off')", + "DELETE FROM `globalSettings` " + . "WHERE `settingKey` = 'FOG_CLIENT_AUTOLOGOFF_BGIMAGE'", +]; + +// 433 +$this->schema[] = [ + // The last four legacy client modules go, and their tables with them. + // + // Directory Cleaner deleted the contents of paths a `dirCleaner` row + // named. User Cleanup deleted the Windows profiles a `userCleanup` row + // named. Client Updater served .NET client binaries out of + // `clientUpdates` so the old service could replace itself. Green FOG + // ran scheduled power actions; step 375 removed its module, settings + // and per-host rows but left `greenFog` standing. + // + // None of the four is reimplemented and none will be. Power Management + // (design 0004) already does what Green FOG did. The rebuilt agent + // updates through the MSI at a stable URL plus a snapin filtered on the + // agentVersion it already reports -- a deliberate decision against a + // self-replacing updater, so Client Updater is not a gap but a closed + // question. Deleting paths and profiles across a fleet is what snapins + // are for: a snapin ships a script and returns an exit code and its + // output, where these modules returned nothing an admin could read. + // + // They were already inert. getGlobalModuleStatus() has not listed any + // of them for some time, so ServiceModule::send() answered `#!um` to a + // legacy client that asked, no service/ endpoint serves them, and the + // three surviving FOG_CLIENT_*_ENABLED settings were rendered as + // checkboxes nothing read. What survived was rows: a `modules` row + // apiece and 86 per-host answers each on the lab, which is a fleet's + // worth of stored opinion about modules that cannot run. That is the + // FOG_PLUGINSYS_DIR and greenfog defect (steps 326 and 375) a third + // time. + // + // NOTHING RECOVERABLE IS LOST. `dirCleaner`, `clientUpdates` and + // `greenFog` are empty on a server that ever ran the modules, because + // 1.6 removed every page that wrote them. `userCleanup` holds rows, but + // they are the seed step 9 inserts on every install -- admin, guest, + // administrator, HelpAssistant, ASPNET, SUPPORT_ -- and not operator + // data. An install that added its own names loses those names, which is + // the same trade Display Manager took in step 431. + // + // Ordered so nothing references something already gone: per-host + // answers, then the module rows, then the settings, then the tables. + // msModuleID is a VARCHAR that held the short name before a later step + // rewrote it to the numeric id, so both spellings are matched -- the + // care steps 375 and 431 took. + // + // The seed steps are deliberately NOT edited. schema.php is a replay + // log; step 326 set that precedent, 375 and 431 followed it. + "DELETE FROM `moduleStatusByHost` WHERE `msModuleID` IN (" + . "'1', '2', '7', 'dircleanup', 'usercleanup', 'clientupdater')", + "DELETE FROM `modules` WHERE `short_name` IN (" + . "'dircleanup', 'usercleanup', 'clientupdater')", + "DELETE FROM `globalSettings` WHERE `settingKey` IN (" + . "'FOG_CLIENT_DIRECTORYCLEANER_ENABLED'," + . "'FOG_CLIENT_USERCLEANUP_ENABLED'," + . "'FOG_CLIENT_CLIENTUPDATER_ENABLED')", + Schema::dropTable('dirCleaner'), + Schema::dropTable('userCleanup'), + Schema::dropTable('clientUpdates'), + Schema::dropTable('greenFog'), +]; diff --git a/packages/web/commons/text.php b/packages/web/commons/text.php index 81ee17b60d..682fe0fe1a 100644 --- a/packages/web/commons/text.php +++ b/packages/web/commons/text.php @@ -33,6 +33,7 @@ $foglang['Storage'] = _('Storage'); $foglang['Snapin'] = _('Snapin'); $foglang['Snapins'] = _('Snapins'); +$foglang['Software'] = _('Software'); $foglang['Remove'] = _('Remove'); $foglang['Removed'] = _('Removed'); $foglang['Enabled'] = _('Enabled'); @@ -141,10 +142,8 @@ $foglang['HostRegistration'] = _('Host Registration'); $foglang['SnapinClient'] = _('Snapin Client'); $foglang['TaskReboot'] = _('Task Reboot'); -$foglang['UserCleanup'] = _('User Cleanup'); $foglang['UserTracker'] = _('User Tracker'); $foglang['SelManager'] = _('%s Manager'); -$foglang['DirectoryCleaner'] = _('Directory Cleaner'); $foglang['MACAddrList'] = _('MAC Address List'); $foglang['FOGSettings'] = _('FOG Settings'); $foglang['ServerShell'] = _('Server Shell'); diff --git a/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js new file mode 100644 index 0000000000..7c9bdf0398 --- /dev/null +++ b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js @@ -0,0 +1,410 @@ +/** + * Agent activity: one grid, grouped by host, expanded in place. + * + * Read only by construction, not by omission: `auditlog` has no create, + * update or delete route anywhere in FOG (ADR 0021 Decision 8), so there is + * nothing here to wire a row action to. The page declares that once, in + * AgentActivityManagement::$selectable, and the toolbar follows. + * + * WHY NOT A CHILD ROW. The first version of this file opened a nested + * DataTable per host through `row.child()`. A DataTables row has ONE child + * slot; registerTable() turns Responsive on for every grid, and Responsive + * owns that slot. So the nested table was never built -- clicking expand + * rendered Responsive's hidden-column list instead, on a live install, at a + * width where nothing was hidden to begin with. Adding rows to the grid + * itself has no such conflict, and it is what makes an expanded host look + * like the rest of FOG instead of a grid inside a grid with its own + * scrollbar and pager. + * + * WHY EVERY GROUP KEEPS ONE ROW. Collapse is a search filter over the event + * rows, and RowGroup draws its headers from the rows that SURVIVE the + * filter: a group left with none renders no header, and the host vanishes + * from the page. So each host's newest event is seeded into the table and + * is never filtered. It anchors the header, and it is the row worth reading + * when everything is collapsed -- what each agent last did. + * + * WHY THE SEED IS SEPARATE FROM THE EXPANSION. An agent writes a row per + * changed fact per host and FOG_AUDIT_RETENTION_DAYS defaults to 0 (keep + * forever), so the flat event set is unbounded -- it cannot be loaded + * client side, which is equally why rowGroup over a `serverSide` grid is + * not an option (it would group within one page). The seed is bounded by + * the fleet, each expansion by ROWS_PER_HOST. Nothing here loads a set + * bounded by neither. + */ +(function($) { + var $table = $('#agentactivity-table'); + + if (!$table.length) { + return; + } + + // How many rows one expansion pulls. A host that has been enrolled for a + // year has thousands, and putting all of them into the browser to answer + // "what has this machine been doing" is the cost the summary exists to + // avoid. Past this the header says so and offers the host's own page. + var ROWS_PER_HOST = 500; + + // hostName -> true while that host's events are showing. Keyed by name + // because that is what RowGroup groups on and what startRender is handed; + // the id travels on the row data for the fetch. + var expanded = {}; + // hostName -> true once its rows are in the table, so a collapse and a + // second expand do not re-fetch what is already loaded. + var loaded = {}; + // hostName -> true while a fetch is in flight, so a double click on a + // header cannot start two. + var loading = {}; + // hostName -> total the server reported, when it is more than we asked + // for. Read by the header to say what is not being shown. + var truncated = {}; + + var outcomeClass = { + allowed: 'text-bg-success', + denied: 'text-bg-danger', + failed: 'text-bg-warning', + partial: 'text-bg-warning', + unknown: 'text-bg-secondary' + }; + + // Every column escapes. An audit row carries subject labels and detail + // text that came from a machine on the network, so its contents are + // hostile by definition -- and DataTables writes cell data as HTML unless + // a column supplies its own render. + function escaped(field) { + return { + data: field, + render: function(d, t) { + // display only: the Buttons CSV/copy exports ask for other types and + // escaping those would put & into the exported file. + return t === 'display' ? $.escapeHtml(d === null ? '' : String(d)) : d; + } + }; + } + + function outcomeColumn() { + return { + data: 'outcome', + render: function(d, t) { + var v = d === null ? '' : String(d); + if (t !== 'display') { + return v; + } + if ('' === v) { + return ''; + } + return '' + $.escapeHtml(v) + ''; + } + }; + } + + // The seed rows and the fetched rows are the same shape by the time they + // reach the table, so one column set serves both and an expanded group is + // continuous with its anchor rather than a differently-shaped insert. + function seedRow(host) { + return { + hostID: host.hostID, + hostName: host.hostName, + events: host.events, + createdTime: host.lastTime, + type: host.lastType, + text: host.lastText, + outcome: host.lastOutcome, + // What the hidden host column SORTS by -- see groupSortColumn(). + groupSort: String(host.lastTime) + '|' + String(host.hostID), + // Marks the row that must never be filtered out. Without it a + // collapsed group loses every row, and with them its header. + anchor: true + }; + } + + // `seed` is a row this file already built, NOT the raw summary object -- + // so the sort key is COPIED from it rather than recomputed. Recomputing it + // from seed.lastTime read undefined (a seed row carries createdTime), every + // event of a host sorted under "undefined|" instead of beside its + // anchor, and the group split in two with its header drawn twice. + function eventRow(seed, row) { + return { + hostID: seed.hostID, + hostName: seed.hostName, + events: seed.events, + createdTime: row.createdTime, + type: row.type, + text: row.text, + outcome: row.outcome, + // Identical for every row of one host, which is what keeps the group + // contiguous once the table is ordered by it. + groupSort: seed.groupSort, + anchor: false + }; + } + + // The hidden Host column: displayed as the host name if it were ever + // shown, but SORTED by the group's own recency. + // + // RowGroup starts a new group -- and draws another header -- every time + // its dataSrc changes down the ordered rows, so a group is only whole if + // the table is ordered by it. Ordering by time alone is not enough: one + // host's older events fall past the next host's newest one and its header + // is drawn twice. That was measured, not predicted. + // + // Sorting alphabetically by name would fix contiguity and lose the order + // that matters, which is "which agents have done something lately". So + // every row of a host sorts on that host's LAST activity plus its id -- + // one value per group, so groups stay whole, ordered by recency, with the + // id breaking a tie between two hosts last seen in the same second. + function groupSortColumn() { + return { + data: 'hostName', + visible: false, + className: 'noVis', + render: function(d, t, row) { + if (t === 'sort' || t === 'type') { + return row.groupSort; + } + return t === 'display' + ? $.escapeHtml(d === null ? '' : String(d)) + : d; + } + }; + } + + // Collapse hides a group's event rows and leaves its anchor. Registered + // once and scoped to this table by node identity -- ext.search is global, + // so an unscoped filter would silently apply to every grid on the page. + var tableNode = $table.get(0); + + $.fn.dataTable.ext.search.push(function(settings, data, dataIndex, row) { + if (settings.nTable !== tableNode) { + return true; + } + return row.anchor === true || expanded[row.hostName] === true; + }); + + // The group header. It carries what used to be four columns of a summary + // grid -- the host, its event count, and now the expand control -- which + // is what frees the columns below to be the events themselves. + function groupHeader(rows, name) { + var d = rows.data()[0] || {}, + open = expanded[name] === true, + count = parseInt(d.events, 10) || 0, + note = ''; + + if (truncated[name]) { + // Said on the header rather than in a row: it is a fact about the + // group, and a row saying it would sort and filter like an event. + note = ' ' + + $.escapeHtml('showing the newest ' + ROWS_PER_HOST) + ''; + } + + return $('') + .addClass('agentactivity-group') + .attr('data-host', name) + .append( + $('') + .attr('colspan', 5) + .html( + '' + + '' + $.escapeHtml(name) + '' + + ' ' + + $.escapeHtml(String(count)) + '' + + note + ) + ); + } + + var table = $table.registerTable(null, { + // Newest activity first. Ordering by the time column also keeps each + // host's rows in the order its agent wrote them, since RowGroup orders + // groups by the first ordering column it is grouped on and rows within + // a group by whatever follows. + // Groups first, then time within a group. Both descending: the most + // recently active host heads the page, and its newest event heads it. + order: [ + [4, 'desc'], + [0, 'desc'] + ], + columns: [ + escaped('createdTime'), + escaped('type'), + escaped('text'), + outcomeColumn(), + groupSortColumn() + ], + rowGroup: { + dataSrc: 'hostName', + startRender: groupHeader + }, + processing: true, + // NO PAGING, and that is the whole reason this page reads correctly. + // + // Paging counts ROWS; this page's unit is HOSTS. Expanding one host with + // 83 events at 25 rows a page filled pages one to three with that host + // and pushed every other host onto page four -- and RowGroup redraws a + // group's header on each page the group spans, so the same host appeared + // four times, expanded, which is what it looked like to the person + // reading it. Neither is a bug in RowGroup: both are what paging by row + // means when the thing being grouped is bigger than a page. + // + // There is no page length that fixes it, because the number of rows an + // expansion adds is a property of the host, not of the setting. Turning + // paging off removes the unit mismatch instead of tuning it: collapsed, + // the grid is one row per host; expanded, it gets longer and you scroll. + // The seed is bounded by the fleet (MAX_HOSTS) and each expansion by + // ROWS_PER_HOST, so "no paging" is not "no limit". + // + // Scroller is not the alternative -- registerTable() excludes any + // rowGroup table from it, because its virtual row-height math cannot + // reconcile injected header rows. + paging: false, + // ...and with it the "entries per page" control, which paging is the + // only thing that gives a value. Left in, it renders as an empty box + // beside its own label in both themes -- not a contrast problem, a + // control with nothing to say. dom keeps `l` for every other grid, so + // this is the switch that removes it here. + lengthChange: false, + // Client side: the seed is one row per host, which is bounded by the + // fleet, and rowGroup cannot group a server-side grid beyond one page. + serverSide: false, + // Nothing here is selectable -- auditlog has no write route at all (ADR + // 0021 Decision 8) -- and registerTable() drops Select All / Deselect + // All when it sees this. + select: false, + ajax: { + url: '../management/index.php?node=agentactivity&sub=getList', + type: 'post', + dataSrc: function(json) { + var out = []; + $.each(json.data || [], function(i, host) { + out.push(seedRow(host)); + }); + return out; + } + } + }); + + // Groups start collapsed, which is the whole point: the page opens as a + // list of hosts and what each last did, and you go looking from there. + // Nothing to do to arrange it -- `expanded` starts empty and the filter + // above keeps every non-anchor row out until a header is clicked. + + // Every reload starts over, because a reload throws the rows away. + // + // The toolbar's Refresh is `dt.clear().draw(); dt.ajax.reload();` -- it + // empties the table and re-fetches the seed. Without this the maps below + // survived that: a host still marked `loaded` was never re-fetched, so its + // rows were gone and clicking its header did nothing at all. It read as + // the expander breaking permanently after one press of Refresh. + // + // xhr.dt fires when the new seed lands, including the first load, where + // resetting empty maps costs nothing. + $table.on('xhr.dt', function() { + expanded = {}; + loaded = {}; + loading = {}; + truncated = {}; + }); + + function loadHost(name, hostID, done) { + if (loading[name]) { + return; + } + loading[name] = true; + + $.ajax({ + url: '../management/index.php?node=agentactivity&sub=getHostActivity&id=' + + encodeURIComponent(hostID), + type: 'post', + dataType: 'json', + // Route::listem() reads DataTables' own paging parameters, so the cap + // is expressed the way that endpoint already understands rather than + // by teaching it a second one. + data: {start: 0, length: ROWS_PER_HOST}, + success: function(json) { + var rows = (json && json.data) || [], + // recordsFiltered, NOT recordsTotal. listem()'s recordsTotal is + // every row in auditLog -- 1435 on the lab install -- so testing + // against it declared a 134-event host truncated at 500. + total = (json && json.recordsFiltered) || rows.length, + seed = table.rows().data().toArray().filter(function(r) { + return r.hostName === name && r.anchor; + })[0], + add = []; + + if (total > rows.length) { + truncated[name] = true; + } + + $.each(rows, function(i, row) { + // The newest row is already on screen as the anchor. Adding it + // again would show one event twice under its own header. + if (seed && String(row.createdTime) === String(seed.createdTime) + && String(row.type) === String(seed.type)) { + return; + } + // A host with no anchor cannot be reached from the UI (there is + // no header to click), but the key still has to be per-host so a + // programmatic load cannot merge two hosts into one group. + add.push(eventRow( + seed || { + hostID: hostID, + hostName: name, + events: rows.length, + groupSort: String(row.createdTime) + '|' + String(hostID) + }, + row + )); + }); + + if (add.length) { + table.rows.add(add); + } + loaded[name] = true; + }, + complete: function() { + loading[name] = false; + done(); + } + }); + } + + // Delegated to the table: RowGroup redraws its headers on every draw, so + // a handler bound to the header elements themselves would be lost the + // first time anything sorted, searched or added a row. + $table.on('click', '.agentactivity-group', function(e) { + var name = $(this).attr('data-host'), + row = table.rows().data().toArray().filter(function(r) { + return r.hostName === name && r.anchor; + })[0]; + + e.preventDefault(); + + if (expanded[name]) { + expanded[name] = false; + table.draw(false); + return; + } + + expanded[name] = true; + + if (loaded[name] || !row) { + table.draw(false); + return; + } + + // Drawn before the fetch as well as after: the chevron turns over + // immediately, so a slow endpoint reads as loading rather than as a + // click that did nothing. + table.draw(false); + loadHost(name, row.hostID, function() { + table.draw(false); + }); + }); +})(jQuery); diff --git a/packages/web/management/js/fog/fog.common.js b/packages/web/management/js/fog/fog.common.js index 3f985d604a..3718c9a983 100644 --- a/packages/web/management/js/fog/fog.common.js +++ b/packages/web/management/js/fog/fog.common.js @@ -3728,8 +3728,20 @@ function fogSizeScroller(dt, release) { if (!init || !init.scroller) { return; // only Scroller-enabled tables } - var container = dt.table().container(), - body = $('div.dt-scroll-body', container); + // container() is null for a table whose wrapper is not in the document + // yet. registerTable() calls this on a setTimeout(0) right after init, and + // a grid built into a node that is still being attached -- a DataTables + // child row is the case that found this -- gets here before its wrapper + // lands. Guarded for the same reason init is above: `outer` below falls + // back to `container`, so a null one reached getBoundingClientRect() and + // threw an uncaught TypeError out of the timeout. Skipping is right rather + // than merely safe -- there is nothing to measure against yet, and + // whatever attaches the node re-runs the sizing pass once it has. + var container = dt.table().container(); + if (!container) { + return; + } + var body = $('div.dt-scroll-body', container); if (!body.length || !body.is(':visible')) { return; // not rendered, or in a hidden tab } @@ -4411,6 +4423,23 @@ $.fn.registerTable = function(onSelect, opts) { } delete opts.extraButtons; + // A table that does not select does not get the buttons that select. + // + // `select: false` used to turn selection off and leave Select All and + // Deselect All sitting in the toolbar, where they were enabled, clickable + // and did nothing -- on 33 tables, including every report pane and the + // read-only event logs. The buttons live in `defaults.buttons` while the + // opt-out arrives in `opts`, so nothing connected the two. + // + // Dropped rather than disabled: a permanently grayed-out control still asks + // the reader what would enable it. The PHP half of the same statement is + // FOGPage::$selectable, which suppresses "Delete selected". + if (opts.select === false) { + defaults.buttons = defaults.buttons.filter(function(b) { + return !b || (b.extend !== 'selectAll' && b.extend !== 'selectNone'); + }); + } + // Column resizing is on for every table. Pulled off opts before they reach // DataTables, which has no such option and would only carry it around. var columnResize = opts.columnResize !== false; @@ -5316,6 +5345,7 @@ function reinitialize() { setupPasswordReveal(); setupUniversalSearch(); setupInfoCard(); + setupInfoCardActions(); }; /** @@ -5383,6 +5413,99 @@ function setupInfoCard() { }); } + +/** + * The info card's one-click task buttons. + * + * Straight to the create endpoint. The options form at + * ?node={node}&sub=deploy is skipped deliberately -- Deploy, Capture and + * Multi-Cast need nothing off it, and fetching it only to post it back + * unchanged is the click this exists to remove. Everything that form's POST + * is checked for -- a pending host, an assigned and enabled image, a + * protected image on a capture, one image across a multicast -- is checked + * in deployPost(), not in the form, so nothing is skipped but the rendering. + * + * scheduleType is the one field that must be sent: validateScheduleType() + * throws on an absent value rather than defaulting, so an empty body would + * come back "Invalid scheduling type". + * + * Every button confirms first. The list grid's equivalents do not, but there + * you have ticked a row to get them; here you arrive on this page just by + * clicking a host name, and one stray click would deploy over a running + * machine -- or, from a group, over all of them. The text is built server + * side (FOGPageRender::renderQuickTaskActions) because it is translated. + * + * The confirmation is the page's own modal rather than window.confirm(): the + * browser dialog cannot be styled, ignores the dark theme, and prefixes the + * page URL, so it reads as something outside the application. Every button + * shares the one modal, which carries the clicked button's data-confirm as + * its body and remembers which button opened it. + */ +function setupInfoCardActions() { + // Guards the window between the click and the server's answer. Per button + // rather than one flag for the card: Deploy and Capture are different + // taskings and there is no reason firing one should block the other. The + // button stays enabled underneath so nothing has to re-enable it on a + // refusal; this is only about the impatient double-click, which would + // otherwise be two identical taskings. + var running = {}; + + // Which button opened the modal. Cleared on every open so a dismissed + // confirmation cannot be committed by the next one. + var pending = null; + + function fire(btn) { + var node = btn.data('node'), + id = btn.data('id'), + type = btn.data('type'), + key = node + ':' + id + ':' + type; + if (running[key]) { + return; + } + running[key] = true; + $.apiCall( + 'post', + '../management/index.php?node=' + encodeURIComponent(node) + + '&sub=deploy&id=' + encodeURIComponent(id) + + '&type=' + encodeURIComponent(type), + {scheduleType: 'instant'}, + function() { + // Both outcomes land here and both only need the lock released. + // apiCall has already drawn the toast, and there is nothing on + // this page to repaint: the card's Last Deployed is the date the + // last task FINISHED, which a task just queued has not. + running[key] = false; + } + ); + } + + // Delegated and namespaced: the card and its modal are torn down and + // rebuilt with the page on every AJAX nav, so a direct binding would be + // lost on the first one and doPageLoad() would stack a new one per visit. + // Both handlers share the one namespace so the .off() clears the pair. + $(document) + .off('click.fogQuickTask') + .on('click.fogQuickTask', '.fog-quicktask', function(e) { + e.preventDefault(); + pending = $(this); + // .text(), never .html(): data-confirm carries a host or group name, + // which is admin-supplied. + $('#quicktask-confirm-text').text(pending.data('confirm')); + $('#quicktask-confirm-modal').modal('show'); + }) + .on('click.fogQuickTask', '#quicktask-confirm-go', function(e) { + e.preventDefault(); + var btn = pending; + // Taken before the request so a second click during the hide + // animation has nothing left to commit. + pending = null; + $('#quicktask-confirm-modal').modal('hide'); + if (btn) { + fire(btn); + } + }); +} + // Select2 builds its search inputs with neither id/name nor a label, tripping // the browser's "form field should have an id or name" autofill advisory and the // "no label associated with a form field" accessibility advisory. There are two diff --git a/packages/web/management/js/fog/group/fog.group.edit.js b/packages/web/management/js/fog/group/fog.group.edit.js index 52fb6e7e98..3a46d4e529 100644 --- a/packages/web/management/js/fog/group/fog.group.edit.js +++ b/packages/web/management/js/fog/group/fog.group.edit.js @@ -395,6 +395,119 @@ loadGroupSnapinOrder(); + // --------------------------------------------------------------- + // SOFTWARE TAB + // Association goes through Group::addSoftware(), which writes one grant + // row on the group, mirroring the snapin tab above. + var groupSoftwareTable = $.registerAssociationTab({ + slug: 'group-software', + item: 'software', + sub: 'getSoftwareList', + afterCommit: loadGroupSoftwareOrder + }); + $.registerCreateAndAssociate('group-software', groupSoftwareTable); + + // --------------------------------------------------------------- + // GROUP SOFTWARE ORDER (the software this group grants) + var groupSoftwareOrderList = $('#group-software-order-list'), + groupSoftwareOrderSaveBtn = $('#group-software-order-save'); + + function updateGroupSoftwareOrderPositions() { + groupSoftwareOrderList.children('li').each(function(i) { + $(this).find('.software-order-pos').text((i + 1) + '. '); + }); + } + + function renderGroupSoftwareOrder(items) { + groupSoftwareOrderList.empty(); + if (!items || items.length === 0) { + groupSoftwareOrderList.append( + $('
  • ', {'class': 'list-group-item text-muted'}) + .text('No software is granted by this group.') + ); + groupSoftwareOrderSaveBtn.prop('disabled', true); + return; + } + groupSoftwareOrderSaveBtn.prop('disabled', false); + $.each(items, function(i, item) { + var controls = $('', {'class': 'float-end'}) + .append( + $('