System Pulse Role Usage Guide
Requirements
None
Prerequisites
- Microsoft SQL must be installed on the multipurpose SQL server, and the System Pulse database must be created with the necessary permissions and configuration. This can be done using the Microsoft SQL role.
- The customer must provide a copy of the System Pulse MSI.
- The LDAP bind account must already exist in Active Directory. This role does not create it — every directory object in the environment is provisioned by this repo's AD lifecycle playbooks instead. Add the account to
ad_service_accountsin the System Pulse group'sgroup_varsand runplaybooks/create-ad-users.ymlbefore installing, then pointsystem_pulse_ldap_bind_user/system_pulse_ldap_bind_passwordat it. It is a plain domain user — browsing AD and verifying usernames needs no permissions beyond the default domain-user directory read access. - The following variables must be defined. The variables should be all uppercase when defined as environment variables, but all lowercase when defined in
host_varsorgroup_varsfiles or an ansible vault. Use a secure password generator to create strong passwords.
# REQUIRED — spadmin password for the SQL database connection string
# Can alternatively be set as the Ansible variable microsoft_sql_spadmin_password
export MICROSOFT_SQL_SPADMIN_PASSWORD='USE_PASSWORD_GENERATOR'
# REQUIRED — LDAP bind credentials for System Pulse
# Can alternatively be set as Ansible variables (see Ansible Variables tab)
export SYSTEM_PULSE_LDAP_BIND_USER='DOMAIN_USER'
export SYSTEM_PULSE_LDAP_BIND_PASSWORD='PASSWORD_OF_DOMAIN_USER'
# REQUIRED — SQL server hostname for the connectionStrings.config
system_pulse_sql_hostname: 'SQL_SERVER_HOSTNAME'
# REQUIRED — spadmin password for the SQL database connection string
# Can alternatively be set as the environment variable MICROSOFT_SQL_SPADMIN_PASSWORD
microsoft_sql_spadmin_password: 'USE_PASSWORD_GENERATOR'
# OPTIONAL — SQL user for the connectionStrings.config (default: spadmin)
system_pulse_sql_user: 'spadmin'
# REQUIRED — LDAP bind credentials for System Pulse
# Can alternatively be set as environment variables (see Environment Variables tab)
system_pulse_ldap_bind_user: 'DOMAIN_USER'
system_pulse_ldap_bind_password: 'PASSWORD_OF_DOMAIN_USER'
Azure SQL Database
When the System Pulse database lives on Azure SQL Database (SaaS) instead of a SQL Server VM, set:
This changes two things:
- ASP.NET SQL provider install scripts are patched. The Configuration Editor's database initialization (
-UPGRADE) runs the stockInstall*.sqlscripts fromC:\Windows\Microsoft.NET\Framework{,64}\v4.0.30319\. Those scriptsCREATE DATABASE, querymaster.dbo.sysdatabases, andUSE [aspnetdb]— none of which Azure SQL Database supports — so initialization fails. The role backs up the stock scripts (to<name>.orig) and replaces them with Azure SQL-compatible versions shipped inside the role (files/aspnet_azure_sql/), so no GitHub access is needed at run time. The role'sCLAUDE.mddocuments how the patched scripts were derived from ASP.NET-Azure-SQL-Install and how to regenerate them for a new System Pulse version. Encrypt=Trueis added to the connection strings (also controllable independently viasystem_pulse_sql_encrypt).
Additional Azure SQL requirements:
- The
SystemPulsedatabase must already exist — the patched scripts (and Azure SQL generally) cannotCREATE DATABASEfrom a user connection. - Azure SQL Database has no Windows integrated authentication, so use a contained SQL user (e.g. provisioned with
playbooks/ire/configure-azure-sql-db-user.yml) and setsystem_pulse_sql_user/system_pulse_sql_passwordaccordingly. - Set
system_pulse_sql_hostnameto the full server endpoint, e.g.tcp:myserver.database.windows.net,1433.
Tags
A tag must be specified when running this role. Running without --tags or with --tags all is not supported and will result in an error.
The role's tasks are grouped into two phases: install, everything that puts software and state on the box (Windows features, the MSI, the database, the services), and configure, everything that writes the web app's configSource files (connection strings, LDAP membership, SMTP, communication security, Forms authentication, appSettings). install still runs both phases end to end, so nothing changes for a normal deploy; the split exists so a configuration-only change to a running server can be applied with --tags configure, touching no installer, no database and no service.
| Tag | Includes | Description |
|---|---|---|
install |
prerequisites, dependencies, patch_aspnet_sql_scripts, install_system_pulse, initialize_database, start_services, configure_system_pulse, configure_services |
Runs the full deploy — both the install and the configuration phase. |
prerequisites |
Runs only the prerequisites tasks (resolve install user/password, validate required variables). | |
dependencies |
Runs only the dependencies tasks (IIS features and configuration). | |
patch_aspnet_sql_scripts |
Runs only the Azure SQL script patch tasks (skipped unless system_pulse_azure_sql is true). |
|
install_system_pulse |
Runs only the MSI installation tasks. | |
initialize_database |
Runs only the headless database initialization/upgrade tasks (skipped when system_pulse_initialize_database is false). |
|
start_services |
Starts the System Pulse Windows services (EpicSystemPulseTaskScheduler, EpicSystemPulseAsyncDataFiler). |
|
configure |
configure_system_pulse, configure_services |
Runs the configuration phase alone against an already-installed server. |
configure_system_pulse |
Runs only the web app's configuration tasks (connection strings, LDAP membership, SMTP, appSettings, communication security, Forms authentication). | |
configure_services |
Runs only the Windows services' configuration tasks (their own Config\ connection strings, membership, SMTP, appSettings), restarting the services on change. |
|
resources |
resource_groups, dashboard_tabs |
Applies in-application configuration to a running System Pulse over HTTP. Opt-in and data-driven — a no-op when no inputs are set — so it is deliberately not part of install. |
resource_groups |
Assigns monitored host resources to resource groups (system_pulse_resource_groups). |
|
dashboard_tabs |
Applies home page dashboard tab settings (system_pulse_dashboard_tabs). |
Usage
- Create a file in
host_varsfor each host System Pulse will be installed on if it doesn't already exist. The file should be named after the host's inventory name (e.g.host_vars/abcsp01.yml). - A System Pulse MSI is required to install System Pulse. The playbook supports delivering it via HTTP(S) (e.g. storage account, S3 bucket, caddy file server, etc.) or by uploading it from the Ansible control node.
- When uploading files from the Ansible control node, place the MSI in a "shared files" directory on the Ansible control node (usually a location on the container's persistent storage in /home/ansible/source/files) and set
shared_files_directoryingroup_varsto the path of the directory. If thesystem_pulse_install_source_dirvariable is defined inhost_vars, it will overrideshared_files_directoryfor that host. - If the value of
shared_files_directoryorsystem_pulse_install_source_diris a URL, the playbook will attempt to download the MSI from that location. If it's a local file path, the playbook will attempt to upload the MSI from the Ansible control node.system_pulse_install_source_fileshould be set to the name of the MSI file (e.g.Epic May 2026 System Pulse Fix Package 2.msi).
- When uploading files from the Ansible control node, place the MSI in a "shared files" directory on the Ansible control node (usually a location on the container's persistent storage in /home/ansible/source/files) and set
- Update each host's
host_varswith the necessary variables using the sample variables below. - Run the
playbooks/deploy-system-pulse.ymlplaybook with the appropriate tag.
Database initialization
The database is initialized (or upgraded) automatically and non-interactively as part of --tags install (system_pulse_initialize_database defaults to true). The Configuration Editor's Database Installation/Upgrade Wizard is a GUI with no silent switch, so the role instead drives the same API the wizard uses (Epic.EnterpriseMonitoring.DatabaseUpdater in Epic.EnterpriseMonitoring.ConfigurationUpdater.dll): it compiles a small console driver into the ConfigurationEditor directory at run time, runs CanUpdateDatabase → UpdateDatabase → GrantExecuteOnProccedures, and removes the driver. The task is idempotent — it reports changed=false when the database schema version already matches the installed release.
This requires system_pulse_role_edit_password (the wizard's required "role edit password" on new installs) — store it in an Ansible vault. The role also creates the SP_User database principal (as a database role) if it is missing; the Microsoft SQL role normally pre-creates it on SQL Server VMs.
Manual fallback
Set system_pulse_initialize_database: false to skip the automatic initialization. The database must then be initialized manually by logging in to the server and running:
"C:\Program Files (x86)\Epic\System Pulse\ConfigurationEditor\SystemPulseConfigurationEditor.exe" -UPGRADE
Setting system_pulse_auto_launch_config_editor: true in host_vars will automatically schedule the Configuration Editor to launch on the next login for the user running the playbook.
Communication Security
system_pulse_communication_security defaults to full, matching the Configuration Editor's "Communication Security → Full" mode. configure_system_pulse writes the WCF serviceModel.services.config and serviceModel.behaviors.config (the DataFiler / DataProvider service endpoints and behaviors) plus urlSchemeSwitcher.config (mode="On", HTTPS enforced on login/admin/dashboard/API paths). The binding configurations these reference (No_Security, TransportSecurity, TransportWithMessageCredential) are defined statically in web.config. These files contain no secrets and are not encrypted. Set the variable to custom or off to leave them untouched and manage them yourself.
Forms authentication
configure_system_pulse writes authentication.config with Forms authentication, exactly as the Configuration Editor does (its App_Data\authentication.xml template): login at Login\Login.aspx against the AD membership provider from membership.config, defaultUrl Default.aspx, requireSSL on the auth cookie (system_pulse_forms_require_ssl, default true) and a 30-day ticket (system_pulse_forms_timeout_minutes, default 43200). The MSI ships an empty <authentication/> stub — without this file, System Pulse redirects unauthenticated browsers to the ASP.NET default login URL login.aspx, which does not exist in the app, and the login flow dead-ends in an MVC "controller not found" error. Identity-requiring pages include the Admin pages such as Client Token Authentication Settings (Admin/ApiAuth.aspx), which the kuiper role's System Pulse client registration logs into and writes.
Encrypted config files
configure_system_pulse writes the web app's configSource section files — connectionStrings.config, membership.config, and (when SMTP is configured) smtp.config — then DPAPI-encrypts connectionStrings.config and membership.config in place under the LocalMachine scope, wrapping the section in configProtectionProvider="DataProtectionConfigurationProvider". This keeps the SQL passwords and the LDAP bind password off disk in plaintext; ASP.NET decrypts them transparently at runtime. smtp.config stays plaintext — it carries only the from-address, host, port, and SSL flag, no credentials. The step is idempotent (a file already carrying configProtectionProvider is left as-is).
SMTP
Set both system_pulse_smtp_from_email and system_pulse_smtp_server to write smtp.config (otherwise it is skipped). system_pulse_smtp_port (default 25) and system_pulse_smtp_ssl (default false) control the transport. The file is written as a standard <smtp> mailSettings section and, unlike the credential-bearing config files, is not encrypted.
Windows services
The EpicSystemPulseTaskScheduler and EpicSystemPulseAsyncDataFiler services install with StartType=Automatic but are stopped after the MSI — the Configuration Editor normally starts them on its first run. Because the install initializes the database headlessly, the role starts them itself (start_services, after initialize_database). Override the list with system_pulse_services if needed.
Web appSettings
configure_system_pulse seeds the web app's App_Data\Config\appSettings.config from Epic's Configuration Editor template (ConfigurationEditor\App_Data\appSettings.xml, the <web> section) — the same appSettings the Configuration Editor GUI writes. A fresh MSI install ships that file with only SystemPulsePaused, but the web app requires the full set: notably Content-Security-Policy, which Global.SetContentSecurityPolicy reads and passes straight to Response.AddHeader on every request — if the key is absent the app throws ArgumentNullException and returns HTTP 500 on every page. Reading the template that ships in the install keeps the values matched to the installed System Pulse version; the existing SystemPulsePaused value is preserved.
LDAP bind account creation
The role no longer creates the bind account. Add it to ad_service_accounts in the System Pulse group's group_vars and run playbooks/create-ad-users.yml before installing. The account is a plain domain user — browsing AD and verifying usernames needs no permissions beyond default domain-user directory read access. Point system_pulse_ldap_bind_user / system_pulse_ldap_bind_password at it; on AADDS the built-in Users container is read-only, so give the entry a path under a writable OU.
Service configuration files
The two Windows services do not read the web app's configuration: their own executables point configSource at TaskScheduler\Config\ and AsyncDataFiler\Config\, which the MSI ships as empty stubs and only the Configuration Editor GUI normally fills. Without a connection string both services crash on startup (DBConnectionException: The connection string was missing, name OperationalDB) and the service recovery settings restart them in a loop — Get-Service can show Running while the Application event log fills with "Host Died" entries.
configure_services writes the same connection strings, membership, and SMTP files as the web phase into both service Config\ directories, seeds each service's appSettings.config, DPAPI-encrypts the secret-bearing files, and restarts the services when anything changed.
The appSettings seed is not simply the service's own template section: the Configuration Editor also copies a fixed block of released <web> values into both services so the web app and the services agree on alert message templates, error email, database timeouts and PHI restrictions (26 keys, plus 3 more for the task scheduler). The role reproduces that block, reading the values from the installed template so they stay version-matched. The result was verified key-for-key against a server built entirely by hand with the Configuration Editor: 39 keys for the AsyncDataFiler, 91 for the TaskScheduler, 97 for the web app.
Encrypted sections must be single-line
The ConfigurationManager reader these two services use rejects a DPAPI-protected section containing anything besides the EncryptedData node — including the whitespace of a pretty-printed wrapper — with ConfigurationErrorsException: The section is marked as being protected, but it does not have the correct format. ASP.NET tolerates either form, so a pretty-printed wrapper leaves the web app working while both services fail every database read. The role writes the wrapper on one line with no byte-order mark.
External URL
system_pulse_external_url (default empty) sets the GUI's "External URL", and the role writes it to both ExternalApplicationUrl and ApplicationUrl in the web app and both services. The services resolve their relative service URLs against ApplicationUrl; with the key absent they fall back to https://localhost, which fails TLS validation against a server certificate that has no localhost SAN — the AsyncDataFiler then crash-loops with "Could not establish trust relationship for the SSL/TLS secure channel with authority 'localhost'" in the event log. Set it to the URL users reach the site on, e.g. https://systempulse.example.org/SystemPulse, and make sure that name is covered by the server certificate's SANs.
This is the one value the role deliberately writes differently from the Configuration Editor, which puts http://localhost:80/SystemPulse/ in ApplicationUrl. That URL only resolves while the plaintext DataFiler endpoints exist, and Communication Security Full removes them, so a GUI-built server logs a steady stream of 404s against it.
Communication Security and the Kuiper DataProvider warning
With system_pulse_communication_security: full, Kuiper's Infrastructure Settings → System Pulse page reports "Unable to ping DataProviderService.svc … An error occurred when verifying security for the message" when you save. This is expected: Full mode binds the SOAP DataProvider endpoints with TransportWithMessageCredential (paired with the DataProviderService-UsernameAuthenticationRequired appSettings key), and Kuiper's ping sends no message credential. A System Pulse server built entirely with Epic's Configuration Editor produces the identical warning, so it is Epic behaviour rather than a gap in this role. Saving past it is Epic's own flow, and the data-filing path (DataFilerService.svc) validates clean. Do not resolve the warning by downgrading those endpoints to TransportSecurity — that removes client authentication from the DataProvider service.
Resource groups and dashboard tabs (--tags resources)
The resources tags configure the application itself rather than the host: which resource group each monitored server belongs to, and which home page tab shows it. They talk to the running web app over HTTP, so the site must be up, and they are opt-in — with no inputs set, the phase does nothing. All of them need a base URL in system_pulse_api_url (defaults to system_pulse_external_url). dashboard_tabs additionally needs a non-MFA AD web login in system_pulse_admin_username / system_pulse_admin_password; resource_groups needs no credentials, because the endpoints it uses accept anonymous callers. Note the two differ in where the HTTP calls originate: resource_groups runs them from the Ansible control node, which must therefore resolve system_pulse_api_url and trust the certificate's issuing CA, while dashboard_tabs runs them on the target host. The phase is --check-aware: check mode performs the same reads and reports what would change, without writing anything.
The System Pulse database is read-only to us
Every change here goes through System Pulse's own web surface. Never write the SystemPulse database directly — that bypasses the application's logic and its audit trail. Reading it (schema, stored procedure text, confirming what a call persisted) is fine.
The login name is a sAMAccountName
The membership provider authenticates the sAMAccountName, which is the UPN prefix truncated to 20 characters. A too-long name is rejected with no error text on the page — for example the account IRE_Entra_Domain_Admin@… must be given as AADDS\IRE_Entra_Domain_Adm.
Resource groups come from system_pulse_resource_groups, a map of group name to the registered host resource names in it:
system_pulse_resource_groups:
Hyperspace Web: [azhsw001]
Authentication Hyperspace Web: [azahsw001]
Interconnect: [azicfg001]
There is no separate "create group" step — a group exists exactly while some resource names it, so assigning the members is creating the group. The role reads the current assignment from System Pulse's DashboardService.svc OData feed and writes changes through the same DataService.asmx/RegisterResource call the monitoring agents use, which preserves every other property of the resource. A host that has not registered with System Pulse yet (its agent has never filed data) is skipped with a warning, and hosts absent from the variable are never touched. A host may appear in only one group — listing it under two is an error, since a resource holds a single group.
Dashboard tabs come from system_pulse_dashboard_tabs, a list matched on each tab's name. Only the keys you set change; anything else is left as stored, and a tab that does not exist is created:
system_pulse_dashboard_tabs:
- name: IRE
index: 0 # first slot in the tab strip
default: true # the landing page for every user
use_group_filter: true
windows_group_filters: # one entry per column, max three
- [Hyperspace Web, Authentication Hyperspace Web]
- [Interconnect]
- [Kuiper, System Pulse]
index and default work on Epic's own tabs too — unlike the group filter, they are among the few fields System Pulse stores for a built-in tab. index is applied with the admin page's own reorder call, so the remaining tabs renumber around the moved one. default is exclusive: at most one entry may set it, requesting it clears whichever tab currently holds it, and the role asserts exactly one holder afterwards. A tab moved or un-defaulted by hand is put back on the next run.
windows_group_filters describes the tab's columns, of which System Pulse stores three: a plain entry is its own column, and a nested list puts those groups in one column. Asking for more than three columns is an error rather than a silent repack.
Two mechanisms decide what a tab shows; the role uses the group filter
"Use group list" (system_pulse_dashboard_tabs) — a group filter held on the tab record itself, which tracks group membership dynamically, so a newly registered host appears without a re-run. It works on custom tabs only. This is what the role automates.
"Select in tab" — the pencil in each tab's header, "Configure the resources that are displayed on this dashboard". It stores a system-wide selection of individual resources and is the only mechanism that can scope one of Epic's own tabs. The role does not automate it: it is reachable only by scraping ViewState out of Monitor.aspx (which exposes no API at all), it stores resource ids rather than groups, and its resource tree is cached in the IIS application pool. A tab_resources tag that drove it was removed in favour of the approach below.
Epic's admin page names them exactly that way, as its "Resource Selection" radio.
Epic's own tabs cannot carry a group filter — mirror them instead
On a built-in tab System Pulse stores only the order, hidden and default flags, and discards a group filter while still reporting success. This is structural: the data-access method for a built-in tab takes only (tabID, index, isHidden, isDefault), and the table its overrides land in has no filter columns. Epic's admin page hides those controls when you edit a built-in tab. The role fails with an explanatory error rather than report a change that did not happen.
To scope a built-in tab's grid, mirror it: define a new tab carrying that tab's skin plus the group filter you want, and set hidden: true on the built-in original. The skin is what renders Epic's per-application metric columns, so the mirror looks the same — verified against the built-in HSW tab, where a custom tab with skin: Web rendered identically apart from the pencil the built-in tab has and a filtered tab does not. Leave edit_permission unset: Epic's GUI forces it to None whenever a group filter is on.
Other behaviour worth knowing: skins, edit permissions and group names are validated against the values System Pulse offers before anything is written (an unknown value would save cleanly and then match nothing); HasCache/HasWindows are derived by the server from which filters you set, so a Windows-only filter yields a Windows-only tab; and removal is not implemented — dropping an entry leaves the tab in place, so delete a stale tab through the admin page.
The IRE dev environment is a worked example: one IRE overview tab plus HSW (IRE) and Interconnect (IRE) mirrors, with the built-in HSW and Interconnect hidden. See group_vars/ire_copier_azure_app_systempulse/vars.yml.