Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Agent MSI support #212

Merged
merged 35 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f7f4e23
first draft
amitkanfer Nov 19, 2023
0a38799
6
amitkanfer Nov 19, 2023
98a893b
6
amitkanfer Nov 19, 2023
67cd709
Preventing agent upgrade via MSI
amitkanfer Dec 1, 2023
947738e
Update src/installer/BeatPackageCompiler/BeatPackageCompiler.cs
amitkanfer Dec 1, 2023
46c394a
Update README.md
amitkanfer Dec 3, 2023
093e120
Update README.md
amitkanfer Dec 3, 2023
1c32ac2
Fixing uninstall flow and install cleanup flow
amitkanfer Dec 3, 2023
0a40567
Merge branch 'agent_support' of https://github.com/elastic/elastic-st…
amitkanfer Dec 3, 2023
add4b02
rolling back if agent install command fails
amitkanfer Dec 3, 2023
04c589b
redirecting stdout to MSI log
amitkanfer Dec 4, 2023
818cb23
Failing the uninstall flow in case agent uninstall command fails
amitkanfer Dec 5, 2023
8bc764a
Don't attempt calling agent install if the file doesn't exist
amitkanfer Dec 5, 2023
33e978a
Merge branch 'main' into agent_support
amitkanfer Dec 11, 2023
5753acf
adding BK logic (#219)
amitkanfer Dec 12, 2023
f2f637a
Merge branch 'main' into agent_support
amitkanfer Dec 12, 2023
c6df1eb
Update build.ps1
amitkanfer Dec 12, 2023
798ff24
Clean up path length
dliappis Jan 15, 2024
0b933a9
Fix 798ff24da8f65b5b49154cd48ef04f167362b11f
dliappis Jan 15, 2024
50b68bd
Typo
dliappis Jan 15, 2024
566b9e4
Build from MANIFEST_URL and don't sign
dliappis Jan 15, 2024
87fefb7
Update README.md
amitkanfer Jan 16, 2024
0c68b23
Update README.md
amitkanfer Jan 16, 2024
f9f9370
Update README.md
amitkanfer Jan 16, 2024
dab7370
Update README.md
amitkanfer Jan 16, 2024
a3cbdc5
Redirecting stderr and removing PATH manipulation for Agent MSI
amitkanfer Jan 16, 2024
b53ea24
Remove cron schedule
dliappis Jan 17, 2024
fd4793d
Trigger 7.17 beats DRA using schedule
dliappis Jan 18, 2024
41444d5
Making sure Agent MSI runs as an administrator
amitkanfer Jan 18, 2024
7832f16
Merge branch 'agent_support' of https://github.com/elastic/elastic-st…
amitkanfer Jan 18, 2024
a245c94
Add MSI tests for Agent Pipeline (#220)
strawgate Jan 22, 2024
07d0d86
Enabling agent tests
amitkanfer Jan 26, 2024
622fd2b
Disable "Default" test case as it doesn't exist anymore. Don't fail b…
strawgate Jan 29, 2024
00dc987
Default mode clean-up
strawgate Jan 29, 2024
7c6a596
Update build.ps1
strawgate Jan 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,28 @@ Update version in `Directory.Build.props` in the branch for the related minor ve
- Add a new daily schedule for the new minor branch
- Remove the daily schedule for the previous minor branch
ex: https://github.com/elastic/elastic-stack-installers/pull/156 and https://github.com/elastic/elastic-stack-installers/pull/172

---
## Agent

In case of problems during install / uninstall of agent, please refer to the [Capturing Logs](https://github.com/elastic/elastic-stack-installers/blob/agent_support/README.md#capturing-logs) section which will enable troubleshooting.

### Install
During the install flow, The MSI installer will unpack the contents of the MSI to a temp folder and then will call the `elastic-agent install` in order to:
1. copy the files to the final destination at `c:\Program Files\Elastic\Agent`
2. register the agent as a windows service
3. enroll the agent into fleet

In order to complete step 3 above, the MSI installer shall receive command line arguments, passed with AGENTARGS command line switch followed by `"`, for example:
```
elastic-agent.msi AGENTARGS="--url=<fleet_url_with_port> --enrollment-token=<token>"
```

Note that the MSI will call the `elastic-agent install` command with `-f` (force) to avoid user interaction.

### Uninstall
Similarly to the install flow (described above), the MSI will call the `elastic-agent uninstall` command, and it's possible to pass arguements using `AGENTARGS`. One common use case is uninstalling an agent which has tamper protection enabled.

### Upgrade
The Agnet MSI doesn't support upgrade. Since the agents are fleet managed, upgrades shall be done using fleet (UI / API).

11 changes: 11 additions & 0 deletions src/CustomAction.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0.30319"/>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
<supportedRuntime version="v2.0.50727"/>
<supportedRuntime version="v2.0.50215"/>
<supportedRuntime version="v1.1.4322"/>
</startup>
</configuration>
10 changes: 10 additions & 0 deletions src/config/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,13 @@ products:
published_binaries: [ winlogbeat.exe ]
mutable_dirs: [ ]
service: true

elastic-agent:
display_name: Elastic Agent
description: With Elastic Agent you can collect all forms of data from anywhere with a single unified agent per host
published_name: Elastic Agent
published_url: https://www.elastic.co/elastic-agent
published_binaries: [ elastic-agent.exe ]
mutable_dirs: [ ]
is_agent: true
service: false
82 changes: 82 additions & 0 deletions src/installer/BeatPackageCompiler/AgentCustomAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Diagnostics;
using System.IO;
using Microsoft.Deployment.WindowsInstaller;

namespace Elastic.PackageCompiler.Beats
{
public class AgentCustomAction
{
[CustomAction]
public static ActionResult InstallAction(Session session)
{
try
{
// If there are no install args, we stop here
// // (the MSI just copied the files, there will be no agent-install)
if (string.IsNullOrEmpty(session["INSTALLARGS"]))
return ActionResult.Success;

string install_args = session["INSTALLARGS"];
string install_folder = Path.Combine(session["INSTALLDIR"], session["exe_folder"]);
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.WorkingDirectory = install_folder;
process.StartInfo.FileName = "elastic-agent.exe";
process.StartInfo.Arguments = "install -f " + install_args;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we consider always adding --delay-enroll so that installation success doesn't depend on fleet service reachability + valid API token?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one of the tasks was to make sure the service was up and running right after the installation flow... Don't you think the admin would want to know if the token is wrong when rolling out agent installations?

process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();

session.Log("Agent install return code:" + process.ExitCode);

if (process.ExitCode == 0)
{
// If agent got installed properly, we can go ahead and remove all the files installed by the MSI (best effort)
new DirectoryInfo(install_folder).Delete(true);
}

return process.ExitCode == 0 ? ActionResult.Success : ActionResult.Failure;
}
catch
{
return ActionResult.Failure;
}
}

[CustomAction]
public static ActionResult UpgradeAction(Session session)
{
session.Log("Detected an agent upgrade via MSI, which is not supported. Aborting.");
return ActionResult.Failure;
}

[CustomAction]
public static ActionResult UnInstallAction(Session session)
{
try
{
// If there are no (un)install args, we stop here
if (string.IsNullOrEmpty(session["INSTALLARGS"]))
return ActionResult.Success;

string install_args = session["INSTALLARGS"];
System.Diagnostics.Process process = new System.Diagnostics.Process();
process.StartInfo.FileName = @"c:\\Program Files\\Elastic\\Agent\\elastic-agent.exe";
process.StartInfo.Arguments = "uninstall -f " + install_args;
process.StartInfo.CreateNoWindow = true;
process.Start();
process.WaitForExit();

session.Log("Agent uninstall return code:" + process.ExitCode);
}
catch (Exception ex)
{
// IMPORTANT! Uninstall will be done as best effort..
// We don't want to fail the MSI uninstall in case there is an issue with the agent uninstall command.
session.Log(ex.ToString());
}

return ActionResult.Success;
}
}
}
149 changes: 91 additions & 58 deletions src/installer/BeatPackageCompiler/BeatPackageCompiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ static void Main(string[] args)

var companyName = MagicStrings.Elastic;
var productSetName = MagicStrings.Beats.Name;
var displayName = MagicStrings.Beats.Name + " " + ap.TargetName;

// A product can define a display name to be used.
// At the time of writing this line, elastic-agent is the only product that used it
var displayName = !string.IsNullOrEmpty(pc.DisplayName) ? pc.DisplayName : MagicStrings.Beats.Name + " " + ap.TargetName;
var exeName = ap.CanonicalTargetName + MagicStrings.Ext.DotExe;

// Generate UUID v5 from product properties.
Expand Down Expand Up @@ -156,7 +159,9 @@ static void Main(string[] args)
bool exclude =
// .exe must be excluded for service configuration to work
(pc.IsWindowsService && itm.EndsWith(exeName, StringComparison.OrdinalIgnoreCase))
|| (isConfigFile);

// beats config file is handled further down
|| (!pc.IsAgent && isConfigFile);

// this is an "include" filter
return ! exclude;
Expand All @@ -165,65 +170,38 @@ static void Main(string[] args)

packageContents.Add(pc.IsWindowsService ? service : null);

// Add a note to the final screen and a checkbox to open the directory of .example.yml file
var beatConfigExampleFileName = ap.CanonicalTargetName + ".example" + MagicStrings.Ext.DotYml;
var beatConfigExampleFileId = beatConfigExampleFileName + "_" + (uint) beatConfigExampleFileName.GetHashCode32();

project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALTEXT",
$"NOTE: Only Administrators can modify configuration files! We put an example configuration file " +
$"in the data directory caled {ap.CanonicalTargetName}.example.yml. Please copy this example file to " +
$"{ap.CanonicalTargetName}.yml and make changes according to your environment. Once {ap.CanonicalTargetName}.yml " +
$"is created, you can configure {ap.CanonicalTargetName} from your favorite shell (in an elevated prompt) " +
$"and then start {serviceDisplayName} Windows service.\r\n"));

project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALCHECKBOX", "1"));
project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT",
$"Open {ap.CanonicalTargetName} data directory in Windows Explorer"));
// For agent, the MSI installer copies the contents of the MSI to a temp folder
// and then shall call the 'elastic-agent install' command.
// When uninstalling, the 'elastic-agent uninstall' command.
if (pc.IsAgent)
{
// Passing the agent executable path to the action handler which will run it post installation
project.AddProperty(new Property("exe_folder", Path.Combine(ap.Version, ap.CanonicalTargetName)));
project.AddAction(new ManagedAction(AgentCustomAction.InstallAction, Return.check, When.After, Step.InstallFinalize, Condition.NOT_Installed));

// We'll open the folder for now
// TODO: select file in explorer window
project.AddProperty(new Property(
"WixShellExecTarget",
$"[$Component.{beatConfigExampleFileId}]"));
// https://stackoverflow.com/questions/320921/how-to-add-a-wix-custom-action-that-happens-only-on-uninstall-via-msi
project.AddAction(new ManagedAction(AgentCustomAction.UnInstallAction, Return.check, When.After, Step.InstallFinalize, Condition.BeingUninstalledAndNotBeingUpgraded));

project.AddWixFragment("Wix/Product",
XElement.Parse(@"
<CustomAction
Id=""CA_SelectExampleYamlInExplorer""
BinaryKey = ""WixCA""
DllEntry = ""WixShellExec""
Impersonate = ""yes""
/>"),
XElement.Parse(@"
<UI>
<Publish
Dialog=""ExitDialog""
Control=""Finish""
Event=""DoAction""
Value=""CA_SelectExampleYamlInExplorer"">WIXUI_EXITDIALOGOPTIONALCHECKBOX=1 and NOT Installed
</Publish>
</UI>"));

var dataContents = new DirectoryInfo(opts.PackageInDir)
.GetFiles(MagicStrings.Files.AllDotYml, SearchOption.TopDirectoryOnly)
.Select(fi =>
{
// rename main config file to hide it from MSI engine and keep customizations
if (string.Compare(
fi.Name,
ap.CanonicalTargetName + MagicStrings.Ext.DotYml,
StringComparison.OrdinalIgnoreCase) == 0)
{
var wf = new WixSharp.File(fi.FullName);
wf.Attributes.Add("Name", beatConfigExampleFileName);
wf.Id = new Id(beatConfigExampleFileId);
return wf;
}
return null;
})
.ToList<WixEntity>();
// Upgrade custom action. Found that "AppSearch" is the first step after WIX_UPGRADE_DETECTED is set
project.AddAction(new ManagedAction(AgentCustomAction.UpgradeAction, Return.check, When.Before, Step.AppSearch, "WIX_UPGRADE_DETECTED AND NOT (REMOVE=\"ALL\")"));
}

packageContents.AddRange(dataContents);
if (!pc.IsAgent)
{
// Add a note to the final screen and a checkbox to open the directory of .example.yml file
var beatConfigExampleFileName = ap.CanonicalTargetName.Replace("-", "_") + ".example" + MagicStrings.Ext.DotYml;
var beatConfigExampleFileId = beatConfigExampleFileName + "_" + (uint) beatConfigExampleFileName.GetHashCode32();

project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALTEXT",
$"NOTE: Only Administrators can modify configuration files! We put an example configuration file " +
$"in the data directory named {beatConfigExampleFileName}. Please copy this example file to " +
$"{ap.CanonicalTargetName}.yml and make changes according to your environment. Once {ap.CanonicalTargetName}.yml " +
$"is created, you can configure {ap.CanonicalTargetName} from your favorite shell (in an elevated prompt) " +
$"and then start {serviceDisplayName} Windows service.\r\n"));

HandleOpenExplorer(ap, project, beatConfigExampleFileId);
RenameConfigFile(opts, ap, packageContents, beatConfigExampleFileName, beatConfigExampleFileId);
}

// Drop CLI shim on disk
var cliShimScriptPath = Path.Combine(
Expand Down Expand Up @@ -280,5 +258,60 @@ static void Main(string[] args)
else
Compiler.BuildMsi(project);
}

private static void HandleOpenExplorer(ArtifactPackage ap, Project project, string beatConfigExampleFileId)
{
project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALCHECKBOX", "1"));
project.AddProperty(new Property("WIXUI_EXITDIALOGOPTIONALCHECKBOXTEXT",
$"Open {ap.CanonicalTargetName} data directory in Windows Explorer"));

// We'll open the folder for now
// TODO: select file in explorer window
project.AddProperty(new Property(
"WixShellExecTarget",
$"[$Component.{beatConfigExampleFileId}]"));

project.AddWixFragment("Wix/Product",
XElement.Parse(@"
<CustomAction
Id=""CA_SelectExampleYamlInExplorer""
BinaryKey = ""WixCA""
DllEntry = ""WixShellExec""
Impersonate = ""yes""
/>"),
XElement.Parse(@"
<UI>
<Publish
Dialog=""ExitDialog""
Control=""Finish""
Event=""DoAction""
Value=""CA_SelectExampleYamlInExplorer"">WIXUI_EXITDIALOGOPTIONALCHECKBOX=1 and NOT Installed
</Publish>
</UI>"));
}

private static void RenameConfigFile(CmdLineOptions opts, ArtifactPackage ap, List<WixEntity> packageContents, string beatConfigExampleFileName, string beatConfigExampleFileId)
{
var dataContents = new DirectoryInfo(opts.PackageInDir)
.GetFiles(MagicStrings.Files.AllDotYml, SearchOption.TopDirectoryOnly)
.Select(fi =>
{
// rename main config file to hide it from MSI engine and keep customizations
if (string.Compare(
fi.Name,
ap.CanonicalTargetName + MagicStrings.Ext.DotYml,
StringComparison.OrdinalIgnoreCase) == 0)
{
var wf = new WixSharp.File(fi.FullName);
wf.Attributes.Add("Name", beatConfigExampleFileName);
wf.Id = new Id(beatConfigExampleFileId);
return wf;
}
return null;
})
.ToList<WixEntity>();

packageContents.AddRange(dataContents);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@
"commandName": "Project",
"commandLineArgs": "--package=filebeat-8.11.0-windows-x86_64 -v --keep-temp-files",
"workingDirectory": "$(SolutionDir)"
},
"Agent 8.11.1": {
"commandName": "Project",
"commandLineArgs": "--package=elastic-agent-8.11.1-windows-x86_64 -v --keep-temp-files",
"workingDirectory": "$(SolutionDir)"
},
"Agent 8.10.4": {
"commandName": "Project",
"commandLineArgs": "--package=elastic-agent-8.10.4-windows-x86_64 -v --keep-temp-files",
"workingDirectory": "$(SolutionDir)"
}
}
}
Binary file added src/installer/resources/elastic-agent.ico
Binary file not shown.
6 changes: 6 additions & 0 deletions src/shared/ArtifactPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ static bool FromFilenameOrUrl(string fileName, string url, out ArtifactPackage a
Architecture = rxGroups["arch"].Value.ToLower();
IsSnapshot = !rxGroups["snapshot"].Value.IsEmpty();

// HACK
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a better way to do this, or will this live on forever :)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a very good question :)

if (TargetName == "agent")
{
TargetName = "elastic-agent";
}

IsOss = TargetName.EndsWith(
MagicStrings.Files.DashOssSuffix,
StringComparison.OrdinalIgnoreCase);
Expand Down
6 changes: 6 additions & 0 deletions src/shared/ProductConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ public sealed class ProductConfig
[YamlMember("description")]
public string Description { get; set; } = "(add 'description' field to config.yaml)";

[YamlMember("display_name")]
public string DisplayName { get; set; } = "";

[YamlMember("is_agent")]
public bool IsAgent { get; set; } = false;

[YamlMember("published_name")]
public string PublishedName { get; set; } = "Elastic Beats";

Expand Down