diff --git a/changes/23787-script-name b/changes/23787-script-name new file mode 100644 index 000000000000..af50855badf3 --- /dev/null +++ b/changes/23787-script-name @@ -0,0 +1,2 @@ +- Fixes a bug where the name of the setup experience script was not showing up in the activity for + that script execution. \ No newline at end of file diff --git a/ee/server/service/setup_experience.go b/ee/server/service/setup_experience.go index 077630d3c46a..7bece3103e03 100644 --- a/ee/server/service/setup_experience.go +++ b/ee/server/service/setup_experience.go @@ -220,9 +220,10 @@ func (svc *Service) SetupExperienceNextStep(ctx context.Context, hostUUID string return false, ctxerr.Errorf(ctx, "setup experience script missing content id: %d", *script.SetupExperienceScriptID) } req := &fleet.HostScriptRequestPayload{ - HostID: host.ID, - ScriptName: script.Name, - ScriptContentID: *script.ScriptContentID, + HostID: host.ID, + ScriptName: script.Name, + ScriptContentID: *script.ScriptContentID, + SetupExperienceScriptID: script.SetupExperienceScriptID, } res, err := svc.ds.NewHostScriptExecutionRequest(ctx, req) if err != nil { diff --git a/handbook/company/why-this-way.md b/handbook/company/why-this-way.md index b9e3c72b071f..c520a7dfa6c6 100644 --- a/handbook/company/why-this-way.md +++ b/handbook/company/why-this-way.md @@ -283,7 +283,7 @@ For example, here is the [philosophy behind Fleet's bug report template](https:/ ## Why don't we sell like everyone else? Many companies encourage salespeople to ["spray and pray"](https://www.linkedin.com/posts/amstech_the-rampant-abuse-of-linkedin-connections-activity-7178412289413246978-Ci0I?utm_source=share&utm_medium=member_ios) email blasts, and to do whatever it takes to close deals. This can sometimes be temporarily effective. But Fleet takes a [🟠longer-term](https://fleetdm.com/handbook/company#ownership) approach: -- **No spam.** Fleet is deliberate and thoughtful in the way we do outreach, whether that's for community-building, education, or [🧊 conversation-starting](https://github.com/fleetdm/confidential/blob/main/cold-outbound-strategy.md). +- **No spam.** Fleet is deliberate and thoughtful in the way we do outreach, whether that's for community-building, education, or [🧊 conversation-starting](https://docs.google.com/document/d/1IbucpsZZ0qbJQRPRtm9e2kMcSBDTXjixAMVOWyTu_pA/edit?tab=t.0). - **Be a helper.** We focus on [🔴being helpers](https://fleetdm.com/handbook/company#empathy). Always be depositing value. This is how we create a virtuous cycle. (That doesn't mean sharing a random article; it means genuinely hearing, doing whatever it takes to fully understand, and offering only advice or links that we would actually want.) We are genuinely curious and desperate to help, because creating real value for people is the way we win. - **Engineers first.** We always talk to engineers first, and learn how it's going. Security and IT engineers are the people closest to the work, and the people best positioned to know what their organizations need. - **Fewer words. Fewer pings.** People are busy. We don't waste their time. Avoid dumping work on prospect's plates at all costs. Light touches, no asks. Every notification from Fleet is a ping they have to deal with. We don't overload people with words and links. We [🟢keep things simple](https://fleetdm.com/handbook/company#results) and [write briefly](http://www.paulgraham.com/writing44.html). diff --git a/handbook/customer-success/README.md b/handbook/customer-success/README.md index 4f0646c1fed1..b39c9e2d17e5 100644 --- a/handbook/customer-success/README.md +++ b/handbook/customer-success/README.md @@ -45,6 +45,19 @@ Before a routine customer call, the CSM prepares an agenda including the followi 6. Provide updates to open bug reports + ### Invite new customer DRI + +Sometimes there is a change in the champion within the customer's organization. +1. Get an introduction to the new DRIs including names, roles, contact information. +2. Make sure they're in the Slack channel. +3. Invite them to the *Success* meetings. +4. In the first meeting understand their proficiency level of osquery. + 1. Make sure the meeting time is still convenient for their team. + 2. Understand their needs and goals for visibility. + 3. Offer training to get them up to speed. + 4. Provide a white glove experience. + + ### Generate an expansion opportunity in Salesforce [Customer Success Managers (CSMs)](https://fleetdm.com/handbook/customer-success#team) are responsible for developing customer expansion opportunities that are not being worked on in conjunction with an Account Executive (AE). An AE may be assigned by the [Chief Revenue Officer (CRO)](https://fleetdm.com/handbook/sales#team) for large-scale expansion opportunities such as bringing on a new Fleet use case or bringing on a new group of hosts to an existing Fleet use case. CSMs manage expansion opportunities for things like host count increases for customer growth and price increases on renewals. Discuss examples of these scenarios with your manager to learn more. Moving forward, CSM's are responsible for keeping the stage, next steps, and date of next steps fields updated as the opportunity progresses through the sales cycle. Take the steps below when creating an expansion opportunity in Salesforce: diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md index aefd4cd24578..d76516ca4374 100644 --- a/handbook/digital-experience/README.md +++ b/handbook/digital-experience/README.md @@ -207,6 +207,11 @@ To update the host count on a user's subscription: 7. Let the person who created the request know what actions were taken so they can communicate them to the customer. +### Change customer credit card number + +You can help a Premium license dispenser customers change their credit card by directing them to their [account dashboard](https://fleetdm.com/customers/dashboard). On that page, the customer can update their billing card by clicking the pencil icon next to their billing information. + + ### Cancel a Fleet Premium subscription Use the following steps to cancel a Fleet Premium subscription: diff --git a/handbook/finance/README.md b/handbook/finance/README.md index fc59bbeb3471..4730cc33fd09 100644 --- a/handbook/finance/README.md +++ b/handbook/finance/README.md @@ -1,23 +1,28 @@ # Finance + This handbook page details processes specific to working [with](#contact-us) and [within](#responsibilities) this department. ## Team + | Role | Contributor(s) | |:------------------------------|:-----------------------------------------------------------------------------------------------------------| | Finance Engineer | [Isabell Reedy](https://www.linkedin.com/in/isabell-reedy-202aa3123/) _([@ireedy](https://github.com/ireedy))_ ## Contact us + - To **make a request** of this department, [create an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-finance&projects=&template=custom-request.md) and a team member will get back to you within one business day (If urgent, mention a [team member](#team) in [#g-finance](https://fleetdm.slack.com/archives/C047N5L6EGH). - Please **use issue comments and GitHub mentions** to communicate follow-ups or answer questions related to your request. - Any Fleet team member can [view the kanban board](https://app.zenhub.com/workspaces/-g-finance-63f3dc3cc931f6247fcf55a9/board?sprints=none) for this department, including pending tasks and the status of new requests. ## Responsibilities + The Finance department is directly responsible for accounts receivable including invoicing, accounts payable including commision calculations, exspense reporting including Brex memos and maintaining accurate spend projections in "🧮The numbers", sales taxes, payroll taxes, corporate income/franchise taxes, and financial operations including bank accounts and cash flow management. ### Run payroll + Many of these processes are automated, but it's vital to check Gusto and Plane manually for accuracy. - Salaried fleeties are automated in Gusto and Plane. @@ -76,12 +81,14 @@ To complete payroll for an international contractor, use the following steps: ### Reconcile monthly recurring expenses + Recurring monthly or annual expenses, such as the tools we use throughout Fleet, are tracked as recurring, non-personnel expenses in ["🧮 The Numbers"](https://docs.google.com/spreadsheets/d/1X-brkmUK7_Rgp7aq42drNcUg8ZipzEiS153uKZSabWc/edit#gid=2112277278) _(¶confidential Google Sheet)_, along with their payment source. Reconciliation of recurring expenses happens monthly. > Use this spreadsheet as the source of truth. Always make changes to it first before adding or removing a recurring expense. Only track significant expenses. (Other things besides amount can make a payment significant; like it being an individualized expense, for example.) ### Register Fleet as an employer with a new state + Fleet must register as an employer in any state where we hire new teammates. To do this, complete the following steps in Gusto: 1. After a new teammate completes their Gusto profile, the Finance department will be prompted to approve it for payroll. Sign in to your Gusto admin account and begin the approval process. 2. Select "yes" when prompted to file a new hire report and complete the approval process. @@ -93,6 +100,7 @@ Fleet must register as an employer in any state where we hire new teammates. To ### Process an email from a state agency + From time to time, you may get notices via email (or in the mail) from state agencies regarding Fleet's withholding and/or unemployment tax accounts. You can resolve some of these notices on your own by verifying and/or updating the settings in your Gusto account. If the notice is regarding an upcoming change to your deposit schedule or unemployment tax rate, make the required change in Gusto, such as: @@ -193,6 +201,7 @@ Use the following steps to update the [💸Finance department KPIs](https://docs ### Create an invoice + To create a new invoice for a Fleet customer, follow these steps: 1. Go to the [invoice folder in Google Drive](https://drive.google.com/drive/folders/11limC_KQYNYQPApPoXN0CplHo_5Qgi2b?usp=drive_link). 2. Create a copy of the invoice template, and title the copy `[invoice number] Fleet invoice - [customer name]`. @@ -226,7 +235,17 @@ Thanks, > Certain vendors require invoices submitted via a payment portal (such as Coupa). Once you've generated the invoice using the steps above, upload it to the relevant payment portal and email the billing contact to let them know you've submitted the invoice. +### Provide payment information to a customer + +For customers with large deployments, Fleet accepts payment via wire transfer or electronic debit (ACH/SWIFT). + +Payment information for customers within the United States is on Fleet's invoices. Typically, payment information does not need to be sent separately. + +For Fleet customers outside of the United States or instances where a customer is requesting payment information prior to invoicing, provide remittance information to customers by exporting ["💸 Paying Fleet"](https://docs.google.com/document/d/1KP_-x9c1x3sS1X9Q8Wlib2H7tq69xRONn1KMA3nVFQc/edit) into a PDF, then sending that to the prospect. + + ### Communicate the status of customer financial actions + This reporting is performed to update the status of open or upcoming customer actions regarding the financial health of the opportunity. To complete the report: 1. Check [SVB](https://connect.svb.com/#/) and [Brex](https://accounts.brex.com/login) for any recently received payments from customers and record them in SFDC. 2. Go to this [report folder](https://fleetdm.lightning.force.com/lightning/r/Folder/00lUG000000DstpYAC/view?queryScope=userFolders) in SFDC. The three reports will provide the data used in the report. @@ -255,7 +274,13 @@ Thanks, 6. Review the [billing cycles](https://fleetdm.lightning.force.com/lightning/r/Report/00OUG000000yGjR2AU/view) report in SFDC for customers on multiyear deals. For any customers due for invoicing within the next week, create an issue on the Finance board. +### Obtain a copy of Fleet's W-9 + +A recent signed copy of Fleet's W-9 form can be found in [this confidential PDF in Google Drive](https://drive.google.com/file/d/1ugXazEBk1oVm_LqGbYNsIFECcv5jXLA9/view?usp=drivesdk). + + ### Run US commission payroll + 1. Update individual teammates commission calculators (linked from [main commission calculator](https://docs.google.com/spreadsheets/d/1PuqUbfPGos87TfcHWgUd05TRJgQLlBmhyz1euj79m2A/edit?usp=sharing)) with new revenue from any deals that are closed-won (have a subscription agreement signed by both parties) and have a **close date** within the previous month. - Verify closed-won deal numbers with CRO to ensure any agreed upon exceptions are captured (eg: CRO approves an AE to receive commission on a renewal deal due to cross-sell). 2. In the "Monthly commission payroll party" meeting, present the commission calculations for Fleeties receiving commission for approval. @@ -265,6 +290,7 @@ Thanks, 4. Once commission payroll has been run, update the [main commission calculator](https://docs.google.com/spreadsheets/d/1PuqUbfPGos87TfcHWgUd05TRJgQLlBmhyz1euj79m2A/edit?usp=sharing) to mark the commission as paid. ### Run international commission payroll + 1. Follow the steps in [run US commission payroll](https://fleetdm.com/handbook/finance#run-us-commission-payroll) to have the commission amounts approved by the CRO. 2. After the amounts are approved in the "Monthly commission payroll party", navigate to Help > Ask a question in Plane to request a commission payment for the teammate. 3. Send a message using the following template @@ -283,6 +309,7 @@ Thanks, ### Run quarterly or annual employee bonus payroll + 1. Update individual teammate bonus calculator (linked from [main commission calculator](https://docs.google.com/spreadsheets/d/1PuqUbfPGos87TfcHWgUd05TRJgQLlBmhyz1euj79m2A/edit?usp=sharing)) with relevant metrics. - Bonus plans will have details specified on how to measure success, with most drawing from the [KPI spreadsheet](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit?usp=sharing) or from linked SFDC reports. If unsure where to pull achievement metrics from, contact teammate's manager to clarify. 2. In the "Monthly commission payroll party" meeting, present the bonus calculations for Fleeties receiving bonus for approval. @@ -293,6 +320,7 @@ Thanks, ### Process monthly accounting + Create a [new montly accounting issue](https://github.com/fleetdm/confidential/issues/new/choose) for the current month and year named "Closing out YYYY-MM" in GitHub and complete all of the tasks in the issue. (This uses the [monthly accounting issue template](https://github.com/fleetdm/confidential/blob/main/.github/ISSUE_TEMPLATE/5-monthly-accounting.md). - **SLA:** The monthly accounting issue should be completed and closed before the 7th of the month. @@ -301,6 +329,7 @@ Create a [new montly accounting issue](https://github.com/fleetdm/confidential/i ### Respond to low credit alert + Fleet admins will receive an email alert when the usage of company cards for the month is aproaching the company credit limit. To avoid the limit being exceeded, a Brex admin will follow these steps: 1. Sign in to Fleet's Brex account. 2. On the landing page, use the "Move money" button to "Add funds to your Brex business accounts". @@ -310,6 +339,7 @@ Fleet admins will receive an email alert when the usage of company cards for the No further action needs to be taken, the amount available for use will increase without disruption to regular processes. ### Check franchise tax status + No later than the second month of every quarter, we check [Delaware divison of corporations](https://icis.corp.delaware.gov) to ensure that Fleet has paid the quarterly franchise tax amounts to remain in good standing with the state of Delaware. - Go to the [DCIS - eCorp website](https://icis.corp.delaware.gov/ecorp/logintax.aspx?FilingType=FranchiseTax) and use the details in 1Password to look up Fleet's status. - If no outstanding amounts: the tax has been paid. @@ -317,6 +347,7 @@ No later than the second month of every quarter, we check [Delaware divison of c ### Check finances for quirks + Every quarter, we check Quickbooks Online (QBO) for discrepancies and follow up on quirks. 1. Check to make sure [bookkeeping quirks](https://docs.google.com/spreadsheets/d/1nuUPMZb1z_lrbaQEcgjnxppnYv_GWOTTo4FMqLOlsWg/edit?usp=sharing) are all accounted for and resolved or in progress toward resolution. 2. Check balance sheet and profit and loss statements (P&Ls) in QBO against the latest [monthly workbooks](https://drive.google.com/drive/folders/1ben-xJgL5MlMJhIl2OeQpDjbk-pF6eJM) in Google Drive. Ensure reports are in the "accural" accounting method. @@ -325,6 +356,7 @@ Every quarter, we check Quickbooks Online (QBO) for discrepancies and follow up ### Report quarterly numbers in Chronograph + Follow these steps to perform quarterly reporting for Fleet's investors: 1. Login to Chronograph and upload our profit and loss statement (P&L), balance sheet and cash flow statements for CRV (all in one book saved in [Google Drive](https://drive.google.com/drive/folders/1ben-xJgL5MlMJhIl2OeQpDjbk-pF6eJM). 2. Provide updated metrics for the following items using Fleet's [KPI spreadsheet](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit#gid=0). @@ -345,6 +377,7 @@ Follow these steps to perform quarterly reporting for Fleet's investors: ### Deliver annual report for venture line + Within 60 days of the end of the year, follow these steps: 1. Provide Silicon Valley Bank (SVB) with our balance sheet and profit and loss statement (P&L, sometimes called a cashflow statement) for the past twelve months. 2. Provide SVB with our board-approved annual operating budgets and projections (on a quarterly granularity) for the new year. @@ -352,6 +385,7 @@ Within 60 days of the end of the year, follow these steps: ### Process a new vendor invoice + Fleet pays its vendors in less than 15 business days in most cases. All invoices and tax documents should be submitted to the Finance department using the [appropriate Fleet email address (confidential Google Doc)](https://docs.google.com/document/d/1tE-NpNfw1icmU2MjYuBRib0VWBPVAdmq4NiCrpuI0F0/edit#heading=h.wqalwz1je6rq). - After making sure the invoice received from a new vendor is valid, add the new vendor to the recurring expenses section of ["The numbers"](https://docs.google.com/spreadsheets/d/1X-brkmUK7_Rgp7aq42drNcUg8ZipzEiS153uKZSabWc/edit#gid=2112277278) before paying the invoice. - If we have not paid this vendor before, make sure we have received the required W-9 or W-8 form from the vendor. **Accounting cannot process a payment without these tax forms for compliance reasons.** @@ -360,6 +394,7 @@ Fleet pays its vendors in less than 15 business days in most cases. All invoices ### Process a request to cancel a vendor + - Make the cancellation notification in accordance with the contract terms between Fleet and the vendor, typically these notifications are made via email and may have a specific address that notice must be sent to. If the vendor has an autorenew contract with Fleet there will often be a window of time in which Fleet can cancel, if notification is made after this time period Fleet may be obligated to pay for the subsequent year even if we don't use the vendor during the next contract term. - Once cancelled, update the recurring expenses section of [The Numbers](https://docs.google.com/spreadsheets/d/1X-brkmUK7_Rgp7aq42drNcUg8ZipzEiS153uKZSabWc/edit#gid=2112277278) to reflect the cancellation by changing the projected monthly burn in column G to $0 and adding "CANCELLED" in front of the vendor's name in column C. diff --git a/handbook/sales/README.md b/handbook/sales/README.md index a1cde2cf6744..ce0cd254c1ee 100644 --- a/handbook/sales/README.md +++ b/handbook/sales/README.md @@ -25,11 +25,25 @@ This handbook page details processes specific to working [with](#contact-us) and The Sales department is directly responsible for attaining the revenue goals of Fleet and helping to deliver upon our customers' objectives. +### Set up a Fleet trial + +You can set up a Fleet Managed Cloud environment for a prospect with >300 hosts, or you can help them generate a trial license key to configure on their own self-managed Fleet server. + +- **To set up a new Fleet Managed Cloud environment** for a user: First, [create a "New customer environment" issue](https://fleetdm.com/docs/configuration/fleet-server-configuration#license-key). Then, once the environment is set up, you'll get a notification and you can let the user know. +- **To set up only a trial license key** for a user's self-managed Fleet server: Point the user towards fleetdm.com/start, where they can sign up and choose to "Run your own trial with Docker". On that page, they'll see a license key located in the `fleectl preview` CLI instructions, and they can configure this by copying and pasting it as the [`FLEET_LICENSE_KEY`](https://fleetdm.com/docs/configuration/fleet-server-configuration#license-key) environment variable on the server(s) where Fleet is deployed. + +### Demo Fleet to a prospect + +To run a demo for a prospect, follow the relevant steps in ["Why Fleet?"](https://docs.google.com/document/d/1E0VU4AcB6UTVRd4JKD45Saxh9Gz-mkO3LnGSTBDLEZo/edit#heading=h.vfxwnwufxzzi) + +### Introduce Fleet's CEO + +To get the CEO's attention and introduce him to an account, follow the relevant steps in ["Why Fleet?"](https://docs.google.com/document/d/1E0VU4AcB6UTVRd4JKD45Saxh9Gz-mkO3LnGSTBDLEZo/edit#heading=h.vfxwnwufxzzi) + + ### Track an objection -We often hear objections to using Fleet that are important to track, understand, and solve for. To track an objection: -1. Navigate to the ["Understanding objections document" (Confidential Google Doc)](https://docs.google.com/document/d/1UFjHaIBdoSGDiqNqwgxRdwRz9Wn9SqP7h-g2OM8Runk/edit). -2. Copy the template at the top of the page and paste it at the top of the "Objections" section completing all TODOs. +To track an objection you heard from a prospect, follow the relevant steps in ["Why Fleet?"](https://docs.google.com/document/d/1E0VU4AcB6UTVRd4JKD45Saxh9Gz-mkO3LnGSTBDLEZo/edit#heading=h.vfxwnwufxzzi) ### Change a contact's organization in Salesforce @@ -38,10 +52,6 @@ Use the following steps to change a contact's organization in Salesforce: - If the contact's organization in Salesforce is incorrect but their new organization is unknown, navigate to the contact in Salesforce and change the "Account name" to "?" and save. - If the contact's organization in Salesforce is incorrect and we know where they're moving to, navigate to the contact in Salesforce, change the "Account name" to the contact's new organization, and save. -### Onboard a new sales team member - -Once the standard Fleetie onboarding issue is complete, create a new ["Sales team onboarding"](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-sales&projects=&template=sales-team-onboarding.md&title=Sales%20onboarding%3A_____________) issue and complete it. - ### Send a quote @@ -55,18 +65,15 @@ The Fleet owner of the opportunity (usually AE or CSM) will prepare a quote and/ - Before sending to prospect, work with the Finance team to verify if sales tax needs to be charged and, if so, how much. -### Obtain a copy of Fleet's W-9 - -A recent signed copy of Fleet's W-9 form can be found in [this confidential PDF in Google Drive](https://drive.google.com/file/d/1ugXazEBk1oVm_LqGbYNsIFECcv5jXLA9/view?usp=drivesdk). - +### Schedule a Solutions Consultant for prospect meeting -### Provide payment information to a prospect - -For customers with large deployments, Fleet accepts payment via wire transfer or electronic debit (ACH/SWIFT). - -Payment information for customers within the United States is on Fleet's invoices. Typically, payment information does not need to be sent separately. +To schedule an [ad hoc meeting](https://www.vocabulary.com/dictionary/ad%20hoc) with a Fleet prospect, the Account Executive (AE) will [open an issue](https://github.com/fleetdm/confidential/issues/new?assignees=&labels=%23g-sales%2C%23solutions-consultant%2C%3Adiscovery%2C%3Ademo%2C%3Ascoping%2C%3Atech-eval&projects=&template=custom-request.md&title=prospect+name+-+prep+%28date%29+-+discovery%2Cdemo%2Cscoping+%28date%29). + - Use [this calendly link](https://calendly.com/fleetdm/talk-to-a-solutions-consultant) to obtain SC availability. + - The AE will populate this issue with the appropriate dates for an internal prep meeting as well as the dates for the external prospect meeting. + - Do not assign the issue. The Director of Solutions Consulting will assign the issue. + - Ensure that the product category is defined ("Endpoint ops", "Device management", or "Vulnerability management") in the description of the issue. -For Fleet customers outside of the United States or instances where a customer is requesting payment information prior to invoicing, provide remittance information to customers by exporting ["💸 Paying Fleet"](https://docs.google.com/document/d/1KP_-x9c1x3sS1X9Q8Wlib2H7tq69xRONn1KMA3nVFQc/edit) into a PDF, then sending that to the prospect. + ### Send an NDA to a customer @@ -248,11 +196,6 @@ Temp Transfer to: Temp technical DRI To close a deal with a new customer (non-self-service), create and complete a GitHub issue using the ["Sale" issue template](https://github.com/fleetdm/confidential/issues/new?assignees=alexmitchelliii&labels=%23g-sales&projects=&template=3-sale.md&title=New+customer%3A+_____________). -### Change customer credit card number - -You can help a Premium license dispenser customers change their credit card by directing them to their [account dashboard](https://fleetdm.com/customers/dashboard). On that page, the customer can update their billing card by clicking the pencil icon next to their billing information. - - ### Process a security questionnaire - The AE will [use the handbook](https://fleetdm.com/handbook/company/communications#vendor-questionnaires) to answer most of the questions with links to appropriate sections in the handbook. After this first pass has been completed, and if there are outstanding questions, the AE will [assign the issue to Digital Experience (#g-digital-experience)](https://fleetdm.com/handbook/digital-experience#contact-us) with a requested timeline for completion defined. diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index cdbf3b440b02..bfe732426e87 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -312,7 +312,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint JSON_OBJECT( 'host_id', hsr.host_id, 'host_display_name', COALESCE(hdn.display_name, ''), - 'script_name', COALESCE(scr.name, ''), + 'script_name', COALESCE(ses.name, COALESCE(scr.name, '')), 'script_execution_id', hsr.execution_id, 'async', NOT hsr.sync_request, 'policy_id', hsr.policy_id, @@ -330,6 +330,8 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint scripts scr ON scr.id = hsr.script_id LEFT OUTER JOIN host_software_installs hsi ON hsi.execution_id = hsr.execution_id + LEFT OUTER JOIN + setup_experience_scripts ses ON ses.id = hsr.setup_experience_script_id WHERE hsr.host_id = :host_id AND hsr.exit_code IS NULL AND diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go index 2e19fddccfac..ce3a33815b92 100644 --- a/server/datastore/mysql/activities_test.go +++ b/server/datastore/mysql/activities_test.go @@ -541,6 +541,15 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { h2SelfService, err := ds.InsertSoftwareInstallRequest(noUserCtx, h2.ID, sw1Meta.InstallerID, true, nil) require.NoError(t, err) + setupExpScript := &fleet.Script{Name: "setup_experience_script", ScriptContents: "setup_experience"} + err = ds.SetSetupExperienceScript(ctx, setupExpScript) + require.NoError(t, err) + ses, err := ds.GetSetupExperienceScript(ctx, h2.TeamID) + require.NoError(t, err) + hsr, err = ds.NewHostScriptExecutionRequest(ctx, &fleet.HostScriptRequestPayload{HostID: h2.ID, ScriptContents: "setup_experience", SetupExperienceScriptID: &ses.ID}) + require.NoError(t, err) + h2SetupExp := hsr.ExecutionID + // create pending install and uninstall requests for h3 that will be deleted _, err = ds.InsertSoftwareInstallRequest(ctx, h3.ID, sw3Meta.InstallerID, false, nil) require.NoError(t, err) @@ -560,7 +569,8 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2SelfService) endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_software_installs", "execution_id", h2Bar) endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2A, h2F) - SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2) + endTime = SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_vpp_software_installs", "command_uuid", vppCommand1, vppCommand2) + SetOrderedCreatedAtTimestamps(t, ds, endTime, "host_script_results", "execution_id", h2SetupExp) execIDsWithUser := map[string]bool{ h1A: true, @@ -576,11 +586,13 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { h2Bar: true, vppCommand1: true, vppCommand2: false, + h2SetupExp: false, } execIDsScriptName := map[string]string{ - h1A: scr1.Name, - h1B: scr2.Name, - h2A: scr1.Name, + h1A: scr1.Name, + h1B: scr2.Name, + h2A: scr1.Name, + h2SetupExp: setupExpScript.Name, } execIDsSoftwareTitle := map[string]string{ h1Fleet: "foo", @@ -641,10 +653,10 @@ func testListHostUpcomingActivities(t *testing.T, ds *Datastore) { wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true, TotalResults: 8}, }, { - opts: fleet.ListOptions{PerPage: 4}, + opts: fleet.ListOptions{PerPage: 5}, hostID: h2.ID, - wantExecs: []string{h2SelfService, h2Bar, h2A, vppCommand2}, - wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 4}, + wantExecs: []string{h2SelfService, h2Bar, h2A, vppCommand2, h2SetupExp}, + wantMeta: &fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: false, TotalResults: 5}, }, { opts: fleet.ListOptions{}, diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go index d09b61645583..b6e4ff057673 100644 --- a/server/datastore/mysql/scripts.go +++ b/server/datastore/mysql/scripts.go @@ -39,8 +39,8 @@ func (ds *Datastore) NewHostScriptExecutionRequest(ctx context.Context, request func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, request *fleet.HostScriptRequestPayload) (*fleet.HostScriptResult, error) { const ( - insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request) VALUES (?, ?, ?, '', ?, ?, ?, ?)` - getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?` + insStmt = `INSERT INTO host_script_results (host_id, execution_id, script_content_id, output, script_id, policy_id, user_id, sync_request, setup_experience_script_id) VALUES (?, ?, ?, '', ?, ?, ?, ?, ?)` + getStmt = `SELECT hsr.id, hsr.host_id, hsr.execution_id, hsr.created_at, hsr.script_id, hsr.policy_id, hsr.user_id, hsr.sync_request, sc.contents as script_contents, hsr.setup_experience_script_id FROM host_script_results hsr JOIN script_contents sc WHERE sc.id = hsr.script_content_id AND hsr.id = ?` ) execID := uuid.New().String() @@ -52,6 +52,7 @@ func newHostScriptExecutionRequest(ctx context.Context, tx sqlx.ExtContext, requ request.PolicyID, request.UserID, request.SyncRequest, + request.SetupExperienceScriptID, ) if err != nil { return nil, ctxerr.Wrap(ctx, err, "new host script execution request") @@ -269,7 +270,8 @@ func (ds *Datastore) getHostScriptExecutionResultDB(ctx context.Context, q sqlx. hsr.created_at, hsr.user_id, hsr.sync_request, - hsr.host_deleted_at + hsr.host_deleted_at, + hsr.setup_experience_script_id FROM host_script_results hsr JOIN diff --git a/server/datastore/mysql/setup_experience.go b/server/datastore/mysql/setup_experience.go index 328cbb12aec0..fe69a14aef3e 100644 --- a/server/datastore/mysql/setup_experience.go +++ b/server/datastore/mysql/setup_experience.go @@ -404,6 +404,32 @@ WHERE return &script, nil } +func (ds *Datastore) GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*fleet.Script, error) { + query := ` +SELECT + id, + team_id, + name, + script_content_id, + created_at, + updated_at +FROM + setup_experience_scripts +WHERE + id = ? +` + + var script fleet.Script + if err := sqlx.GetContext(ctx, ds.reader(ctx), &script, query, scriptID); err != nil { + if err == sql.ErrNoRows { + return nil, ctxerr.Wrap(ctx, notFound("SetupExperienceScript"), "get setup experience script by id") + } + return nil, ctxerr.Wrap(ctx, err, "get setup experience script by id") + } + + return &script, nil +} + func (ds *Datastore) SetSetupExperienceScript(ctx context.Context, script *fleet.Script) error { err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error { var err error diff --git a/server/datastore/mysql/setup_experience_test.go b/server/datastore/mysql/setup_experience_test.go index fa48a4107c96..f1f387fe1abf 100644 --- a/server/datastore/mysql/setup_experience_test.go +++ b/server/datastore/mysql/setup_experience_test.go @@ -29,6 +29,7 @@ func TestSetupExperience(t *testing.T) { {"ListSetupExperienceStatusResults", testSetupExperienceStatusResults}, {"SetupExperienceScriptCRUD", testSetupExperienceScriptCRUD}, {"TestHostInSetupExperience", testHostInSetupExperience}, + {"TestGetSetupExperienceScriptByID", testGetSetupExperienceScriptByID}, } for _, c := range cases { @@ -675,7 +676,6 @@ func testSetSetupExperienceTitles(t *testing.T, ds *Datastore) { assert.False(t, *titles[0].SoftwarePackage.InstallDuringSetup) assert.False(t, *titles[1].SoftwarePackage.InstallDuringSetup) assert.False(t, *titles[2].AppStoreApp.InstallDuringSetup) - } func testSetupExperienceStatusResults(t *testing.T, ds *Datastore) { @@ -909,3 +909,28 @@ func testHostInSetupExperience(t *testing.T, ds *Datastore) { require.NoError(t, err) require.False(t, inSetupExperience) } + +func testGetSetupExperienceScriptByID(t *testing.T, ds *Datastore) { + ctx := context.Background() + + script := &fleet.Script{ + Name: "setup_experience_script", + ScriptContents: "echo hello", + } + + err := ds.SetSetupExperienceScript(ctx, script) + require.NoError(t, err) + + scriptByTeamID, err := ds.GetSetupExperienceScript(ctx, nil) + require.NoError(t, err) + + gotScript, err := ds.GetSetupExperienceScriptByID(ctx, scriptByTeamID.ID) + require.NoError(t, err) + + require.Equal(t, script.Name, gotScript.Name) + require.NotZero(t, gotScript.ScriptContentID) + + b, err := ds.GetAnyScriptContents(ctx, gotScript.ScriptContentID) + require.NoError(t, err) + require.Equal(t, script.ScriptContents, string(b)) +} diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go index b1bcc4b7b246..496c707b86bf 100644 --- a/server/fleet/datastore.go +++ b/server/fleet/datastore.go @@ -1795,14 +1795,46 @@ type Datastore interface { // Setup Experience // + // ListSetupExperienceResultsByHostUUID lists the setup experience results for a host by its UUID. ListSetupExperienceResultsByHostUUID(ctx context.Context, hostUUID string) ([]*SetupExperienceStatusResult, error) + + // UpdateSetupExperienceStatusResult updates the given setup experience status result. UpdateSetupExperienceStatusResult(ctx context.Context, status *SetupExperienceStatusResult) error + + // EnqueueSetupExperienceItems enqueues the relevant setup experience items (software and + // script) for a given host. It first clears out any pre-existing setup experience items that + // were previously enqueued for the host (since the setup experience only happens once during + // the initial device setup). It then adds any software and script that have been configured for + // this team to the host's queue and sets their status to pending. If any items were enqueued, + // it returns true, otherwise it returns false. EnqueueSetupExperienceItems(ctx context.Context, hostUUID string, teamID uint) (bool, error) + + // GetSetupExperienceScript gets the setup experience script for a team. There can only be 1 + // setup experience script per team. GetSetupExperienceScript(ctx context.Context, teamID *uint) (*Script, error) + + // GetSetupExperienceScriptByID gets the setup experience script by its ID. + GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*Script, error) + + // SetSetupExperienceScript sets the setup experience script to the given script. SetSetupExperienceScript(ctx context.Context, script *Script) error + + // DeleteSetupExperienceScript deletes the setup experience script for the given team. DeleteSetupExperienceScript(ctx context.Context, teamID *uint) error + + // MaybeUpdateSetupExperienceScriptStatus updates the status of the setup experience script for + // the given host if the script result row exists. If there was an update, it returns true. + // Otherwise, it returns false. MaybeUpdateSetupExperienceScriptStatus(ctx context.Context, hostUUID string, executionID string, status SetupExperienceStatusResultStatus) (bool, error) + + // MaybeUpdateSetupExperienceSoftwareInstallStatus updates the status of the setup experience + // software installer for the given host if the software installer result row exists. If there + // was an update, it returns true. Otherwise, it returns false. MaybeUpdateSetupExperienceSoftwareInstallStatus(ctx context.Context, hostUUID string, executionID string, status SetupExperienceStatusResultStatus) (bool, error) + + // MaybeUpdateSetupExperienceVPPStatus updates the status of the setup experience + // VPP app for the given host if the VPP app installer row exists. If there was an update, it + // returns true. Otherwise, it returns false. MaybeUpdateSetupExperienceVPPStatus(ctx context.Context, hostUUID string, commandUUID string, status SetupExperienceStatusResultStatus) (bool, error) // Fleet-maintained apps diff --git a/server/fleet/scripts.go b/server/fleet/scripts.go index 28bba6ad8cf7..8b011642b10b 100644 --- a/server/fleet/scripts.go +++ b/server/fleet/scripts.go @@ -149,6 +149,9 @@ type HostScriptRequestPayload struct { // SyncRequest is filled automatically based on the endpoint used to create // the execution request (synchronous or asynchronous). SyncRequest bool `json:"-"` + // SetupExperienceScriptID is the ID of the setup experience script related to this request + // payload, if such a script exists. + SetupExperienceScriptID *uint `json:"-"` } func (r HostScriptRequestPayload) ValidateParams(waitForResult time.Duration) error { @@ -251,6 +254,10 @@ type HostScriptResult struct { // results can still be returned to see activity details after the host got // deleted. HostDeletedAt *time.Time `json:"-" db:"host_deleted_at"` + + // SetupExperienceScriptID is the ID of the setup experience script, if this script execution + // was part of setup experience. + SetupExperienceScriptID *uint `json:"-" db:"setup_experience_script_id"` } func (hsr HostScriptResult) AuthzType() string { diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go index d793428ad754..51bda642d301 100644 --- a/server/mock/datastore_mock.go +++ b/server/mock/datastore_mock.go @@ -1141,6 +1141,8 @@ type EnqueueSetupExperienceItemsFunc func(ctx context.Context, hostUUID string, type GetSetupExperienceScriptFunc func(ctx context.Context, teamID *uint) (*fleet.Script, error) +type GetSetupExperienceScriptByIDFunc func(ctx context.Context, scriptID uint) (*fleet.Script, error) + type SetSetupExperienceScriptFunc func(ctx context.Context, script *fleet.Script) error type DeleteSetupExperienceScriptFunc func(ctx context.Context, teamID *uint) error @@ -2844,6 +2846,9 @@ type DataStore struct { GetSetupExperienceScriptFunc GetSetupExperienceScriptFunc GetSetupExperienceScriptFuncInvoked bool + GetSetupExperienceScriptByIDFunc GetSetupExperienceScriptByIDFunc + GetSetupExperienceScriptByIDFuncInvoked bool + SetSetupExperienceScriptFunc SetSetupExperienceScriptFunc SetSetupExperienceScriptFuncInvoked bool @@ -6800,6 +6805,13 @@ func (s *DataStore) GetSetupExperienceScript(ctx context.Context, teamID *uint) return s.GetSetupExperienceScriptFunc(ctx, teamID) } +func (s *DataStore) GetSetupExperienceScriptByID(ctx context.Context, scriptID uint) (*fleet.Script, error) { + s.mu.Lock() + s.GetSetupExperienceScriptByIDFuncInvoked = true + s.mu.Unlock() + return s.GetSetupExperienceScriptByIDFunc(ctx, scriptID) +} + func (s *DataStore) SetSetupExperienceScript(ctx context.Context, script *fleet.Script) error { s.mu.Lock() s.SetSetupExperienceScriptFuncInvoked = true diff --git a/server/service/integration_mdm_dep_test.go b/server/service/integration_mdm_dep_test.go index 53046106af53..626f0d191f65 100644 --- a/server/service/integration_mdm_dep_test.go +++ b/server/service/integration_mdm_dep_test.go @@ -1962,6 +1962,7 @@ func (s *integrationMDMTestSuite) createTeamDeviceForSetupExperienceWithProfileS require.Len(t, listHostsRes.Hosts, 1) require.Equal(t, listHostsRes.Hosts[0].HardwareSerial, teamDevice.SerialNumber) enrolledHost := listHostsRes.Hosts[0].Host + enrolledHost.TeamID = &tm.ID // transfer it to the team s.Do("POST", "/api/v1/fleet/hosts/transfer", @@ -2086,6 +2087,57 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) + // The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull + // it out manually + results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID) + require.Len(t, results, 2) + require.NoError(t, err) + var installUUID string + for _, r := range results { + if r.HostSoftwareInstallsExecutionID != nil { + installUUID = *r.HostSoftwareInstallsExecutionID + } + } + + require.NotEmpty(t, installUUID) + + // Need to get the software title to get the package name + var getSoftwareTitleResp getSoftwareTitleResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *statusResp.Results.Software[0].SoftwareTitleID), nil, http.StatusOK, &getSoftwareTitleResp, "team_id", fmt.Sprintf("%d", *enrolledHost.TeamID)) + require.NotNil(t, getSoftwareTitleResp.SoftwareTitle) + require.NotNil(t, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage) + + debugPrintActivities := func(activities []*fleet.Activity) []string { + var res []string + for _, activity := range activities { + res = append(res, fmt.Sprintf("%+v", activity)) + } + return res + } + + // Check upcoming activities: we should only have the software upcoming because we don't run the + // script until after the software is done + var hostActivitiesResp listHostUpcomingActivitiesResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", enrolledHost.ID), + nil, http.StatusOK, &hostActivitiesResp) + + expectedActivityDetail := fmt.Sprintf(` + { + "status": "pending_install", + "host_id": %d, + "policy_id": null, + "policy_name": null, + "install_uuid": "%s", + "self_service": false, + "software_title": "%s", + "software_package": "%s", + "host_display_name": "%s" + } + `, enrolledHost.ID, installUUID, getSoftwareTitleResp.SoftwareTitle.Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, enrolledHost.DisplayName()) + require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", debugPrintActivities(hostActivitiesResp.Activities)) + require.NotNil(t, hostActivitiesResp.Activities[0].Details) + require.JSONEq(t, expectedActivityDetail, string(*hostActivitiesResp.Activities[0].Details)) + // no MDM command got enqueued due to the /status call (device not released yet) cmd, err = mdmDevice.Idle() require.NoError(t, err) @@ -2103,20 +2155,6 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu require.Equal(t, "script.sh", statusResp.Results.Script.Name) require.Equal(t, fleet.SetupExperienceStatusPending, statusResp.Results.Script.Status) - // The /setup_experience/status endpoint doesn't return the various IDs for executions, so pull - // it out manually - results, err := s.ds.ListSetupExperienceResultsByHostUUID(ctx, enrolledHost.UUID) - require.Len(t, results, 2) - require.NoError(t, err) - var installUUID string - for _, r := range results { - if r.HostSoftwareInstallsExecutionID != nil { - installUUID = *r.HostSoftwareInstallsExecutionID - } - } - - require.NotEmpty(t, installUUID) - // record a result for software installation s.Do("POST", "/api/fleet/orbit/software_install/result", json.RawMessage(fmt.Sprintf(`{ @@ -2147,12 +2185,10 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu // Software is installed, now we should run the script statusResp = getOrbitSetupExperienceStatusResponse{} s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) - // Software is now running, script is still pending require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) require.NotZero(t, *statusResp.Results.Software[0].SoftwareTitleID) - require.NotNil(t, statusResp.Results.Script) require.Equal(t, "script.sh", statusResp.Results.Script.Name) require.Equal(t, fleet.SetupExperienceStatusRunning, statusResp.Results.Script.Status) @@ -2168,6 +2204,48 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu } } + // Validate past activity for software install + // For some reason the display name that's included in the `enrolledHost` is _slightly_ + // different than the expected value in the activities. Pulling the host directly gets the + // correct display name. + var getHostResp getHostResponse + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", enrolledHost.ID), nil, http.StatusOK, &getHostResp) + + expectedActivityDetail = fmt.Sprintf(` +{ + "host_id": %d, + "host_display_name": "%s", + "software_title": "%s", + "software_package": "%s", + "self_service": false, + "install_uuid": "%s", + "status": "installed", + "policy_id": null, + "policy_name": null +} + `, enrolledHost.ID, getHostResp.Host.DisplayName, statusResp.Results.Software[0].Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, installUUID) + + s.lastActivityMatches(fleet.ActivityTypeInstalledSoftware{}.ActivityName(), expectedActivityDetail, 0) + + // Validate upcoming activity for the script + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/activities/upcoming", enrolledHost.ID), + nil, http.StatusOK, &hostActivitiesResp) + + expectedActivityDetail = fmt.Sprintf(` +{ + "async": true, + "host_id": %d, + "policy_id": null, + "policy_name": null, + "script_name": "%s", + "host_display_name": "%s", + "script_execution_id": "%s" +} + `, enrolledHost.ID, statusResp.Results.Script.Name, enrolledHost.DisplayName(), execID) + require.Len(t, hostActivitiesResp.Activities, 1, "got activities: %v", debugPrintActivities(hostActivitiesResp.Activities)) + require.NotNil(t, hostActivitiesResp.Activities[0].Details) + require.JSONEq(t, expectedActivityDetail, string(*hostActivitiesResp.Activities[0].Details)) + // record a result for script execution var scriptResp orbitPostScriptResultResponse s.DoJSON("POST", "/api/fleet/orbit/scripts/result", @@ -2178,7 +2256,6 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu // release of the device, as all setup experience steps are now complete. statusResp = getOrbitSetupExperienceStatusResponse{} s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp) - // Software is now running, script is still pending require.Equal(t, "DummyApp.app", statusResp.Results.Software[0].Name) require.Equal(t, fleet.SetupExperienceStatusSuccess, statusResp.Results.Software[0].Status) require.NotNil(t, statusResp.Results.Software[0].SoftwareTitleID) @@ -2212,6 +2289,21 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu } require.Equal(t, 1, deviceConfiguredCount) require.Equal(t, 0, otherCount) + + // Validate activity for script run + expectedActivityDetail = fmt.Sprintf(` +{ + "async": true, + "host_id": %d, + "policy_id": null, + "policy_name": null, + "script_name": "%s", + "host_display_name": "%s", + "script_execution_id": "%s" +} + `, enrolledHost.ID, statusResp.Results.Script.Name, getHostResp.Host.DisplayName, execID) + + s.lastActivityMatches(fleet.ActivityTypeRanScript{}.ActivityName(), expectedActivityDetail, 0) } func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptForceRelease() { @@ -2370,7 +2462,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptFo require.Equal(t, 0, otherCount) } -func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM() { +func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingItFromABM() { t := s.T() s.enableABM(t.Name()) ctx := context.Background() @@ -2544,7 +2636,6 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM( {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "deleted", OpDate: time.Now()}, } - t.Log("RUN AFTER DELETED") s.runDEPSchedule() a := checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) @@ -2560,7 +2651,6 @@ func (s *integrationMDMTestSuite) TestReenrollingADEDeviceAfterRemovingtFromABM( {SerialNumber: mdmDevice.SerialNumber, Model: "MacBook Pro", OS: "osx", OpType: "added", OpDate: time.Now(), ProfileUUID: a[mdmDevice.SerialNumber].ProfileUUID}, } - t.Log("RUN AFTER RE-ADDED") s.runDEPSchedule() a = checkHostDEPAssignProfileResponses([]string{mdmDevice.SerialNumber}, profileAssignmentReqs[0].ProfileUUID, fleet.DEPAssignProfileResponseSuccess) diff --git a/server/service/orbit.go b/server/service/orbit.go index 5ad9b0dc3bb0..69dd27a88276 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -837,11 +837,20 @@ func (svc *Service) SaveHostScriptResult(ctx context.Context, result *fleet.Host } } var scriptName string - if hsr.ScriptID != nil { + + switch { + case hsr.ScriptID != nil: scr, err := svc.ds.Script(ctx, *hsr.ScriptID) if err != nil { return ctxerr.Wrap(ctx, err, "get saved script") } + scriptName = scr.Name + case hsr.SetupExperienceScriptID != nil: + scr, err := svc.ds.GetSetupExperienceScriptByID(ctx, *hsr.SetupExperienceScriptID) + if err != nil { + return ctxerr.Wrap(ctx, err, "get setup experience script") + } + scriptName = scr.Name } diff --git a/website/config/custom.js b/website/config/custom.js index f791a82e6f37..4e0331a1ff37 100644 --- a/website/config/custom.js +++ b/website/config/custom.js @@ -301,7 +301,6 @@ module.exports.custom = { // "Secret handbook" // Standard operating procedures (SOP), etc that would be public handbook content except for that it's confidential. 'README.md': ['mikermcneil'],// « about this repo - 'cold-outbound-strategy.md': ['mikermcneil', 'sampfluger88'],// « Cold outbound strategy (see fleetdm.com/handbook/company/why-this-way for our vision of a better way to sell) // GitHub issue templates '.github/ISSUE_TEMPLATE': ['mikermcneil', 'sampfluger88', 'lukeheath'],// FUTURE: Bust out individual maintainership for issue templates once relevant DRIs are GitHub, markdown, and content design-certified