· 6 years ago · Apr 01, 2020, 08:58 AM
1<#
2.Synopsis
3 Icinga 2 PowerShell Module - the most flexible and easy way to configure and install Icinga 2 Agents on Windows.
4.DESCRIPTION
5 More Information on https://github.com/Icinga/icinga2-powershell-module
6.EXAMPLE
7 exit $icinga = Icinga2AgentModule `
8 -AgentName 'windows-host-name' `
9 -Ticket '3459843583450834508634856383459' `
10 -ParentZone 'icinga-master' `
11 -ParentEndpoints 'icinga2a', 'icinga2b' `
12 -CAServer 'icinga-master' `
13 -RunInstaller;
14 .NOTES
15
16#>
17function Icinga2AgentModule {
18
19 #
20 # Setup parameters which can be accessed
21 # with -<ParamName>
22 #
23 [CmdletBinding()]
24 param(
25
26 # This is in general the name of your Windows host. It will have to match with your Icinga configuration, as it is part of the Icinga 2 Ticket and Certificate handling to ensure a valid certificate is generated
27 [string]$AgentName,
28 # The Ticket you will receive from your Icinga 2 CA. In combination with the Icinga Director, it will tell you which Ticket you will require for your host
29 [string]$Ticket,
30 # You can either leave this parameter or add it to allow the module to install or update the Icinga 2 Agent on your system
31 [string]$InstallAgentVersion,
32 # Instead of setting the Agent Name with -AgentName, the PowerShell module is capable of retreiving the information automaticly from Windows. Please note this is not the FQDN
33 [switch]$FetchAgentName = $FALSE,
34 # Like -FetchAgentName, this argument will ensure the hostname is set inside the script, will however include the domain to provide the FQDN internally.
35 [switch]$FetchAgentFQDN = $FALSE,
36 # Allows to transform the hostname to either lower or upper case if required. 0: Do nothing 1: To lower case 2: To upper case
37 [int]$TransformHostname = 0,
38
39 # This variable allows to specify on which port the Icinga 2 Agent will listen on
40 [int]$AgentListenPort = 5665,
41 # Each Icinga 2 Agent is in general forwarding it's check results to a parent master or satellite zone. Here you will have to specify the name of the parent zone
42 [string]$ParentZone,#
43 # Icinga 2 internals to make it configurable if the Agent is accepting configuration from the Icinga config master.
44 [bool]$AcceptConfig = $TRUE,
45 # This argument will define if the Icinga 2 debug log will be enabled or disabled.
46 [switch]$IcingaEnableDebugLog = $FALSE,
47 # Allows to specify if the PowerShell Module will add a firewall rule, allowing Icinga 2 masters or Satellites to connect to the Icinga 2 Agent on the defined port
48 [switch]$AgentAddFirewallRule = $FALSE,
49 # This parameter requires an array of string values, to which endpoints the Agent should in general connect to. If you are only having one endpoint, only add one. You will have to specify all endpoints the Agent requires to connect to
50 [array]$ParentEndpoints,
51 # While -ParentEndpoints will define the name of endpoints by an array, this parameter will allow to assign IP address and port configuration, allowing the Icinga 2 Agent to directly connect to parent Icinga 2 instances. To specify IP address and port, you will have to seperate these entries by using ';' without blank spaces. The order of the config has to match the assignment of -ParentEndpoints. You can specify the IP address only without a port definition by just leaving the last part. If you wish to not specify a config for a specific endpoint, simply add an empty string to the correct location.
52 [array]$EndpointsConfig,
53 # Allows to specify global zones, which will be added into the icinga2.conf. Note: In case no global zone will be defined, director-global will be added by default. If you specify zones by yourself, please ensure to add director-global as this is not done automaticly when adding custom global-zones.
54 [array]$GlobalZones = @( 'director-global' ),
55
56 # Agent installation / update
57 <# This argument will allow to override the user the Icinga 2 service is running with. Windows provides some basic users already which can be configured:
58
59 LocalSystem
60 NT AUTHORITY\NetworkService (Icinga default)
61 NT AUTHORITY\LocalService
62 If you require an own user, you can add that one as well for the argument. If a password is required for the user to login, seperate username and password with a ':'.
63
64 Example: jdoe:mysecretpassword
65
66 Furthermore you can also use domains in combination and pass them over.
67
68 Example: icinga\jdoe:mysecretpassword[string]$IcingaServiceUser,
69 #>
70 [string]$IcingaServiceUser,
71 #With this parameter you can define a download Url or local directory from which the module will download/install a specific Icinga 2 Agent MSI Installer package. Please ensure to only define the base download Url / Directory, as the Module will generate the MSI file name based on your operating system architecture and the version to install. The Icinga 2 MSI Installer name is internally build as follows: Icinga2-v[InstallAgentVersion]-[OSArchitecture].msi
72
73 # Full example: Icinga2-v2.8.0-x86_64.msi
74 [string]$DownloadUrl = 'https://packages.icinga.com/windows/',
75 # Allows to specify in which directory the Icinga 2 Agent will be installed into. In case of an Agent update you can specify with this argument a new directory the new Agent will be installed into. The old directory will be removed caused by the required uninstaller process.
76 [string]$AgentInstallDirectory,
77 # In case the Icinga 2 Agent is already installed on the system, this parameter will allow you to configure if you wish to upgrade / downgrade to a specified version with the -InstallAgentVersion parameter as well. If none of both parameters is defined, the module will not update or downgrade the agent.
78 # If argument -AgentInstallDirectory is not specified, the Icinga 2 Agent will be installed into the same directory as before. In case defined, the PowerShell Module will use the new directory as installation target.
79 [switch]$AllowUpdates = $FALSE,
80 # To ensure downloaded packages are build by the Icinga Team and not compromised by third parties, you will be able to provide an array of SHA1 hashes here. In case you have defined any hashses, the module will not continue with updating / installing the Agent in case the SHA1 hash of the downloaded MSI package is not matching one of the provided hashes of this parameter.
81 [array]$InstallerHashes,
82 # In case the Icinga Agent will accept configuration from the parent Icinga 2 system, it will possibly write data to /var/lib/icinga2/api/* By adding this parameter to your script call, all content inside the api directory will be flushed once a change is detected by the module which requires a restart of the Icinga 2 Agent
83 [switch]$FlushApiDirectory = $FALSE,
84
85 # Here you can provide a string to the Icinga 2 CA or any other CA responsible to generate the required certificates for the SSL communication between the Icinga 2 Agent and it's parent
86 [string]$CAServer,
87 # TODO
88 [string]$CACertificatePath,
89 # Here you can specify a custom port in case your CA Server is not listening on 5665
90 [int]$CAPort = 5665,
91 # The module will generate the certificates in general only if one of the required files is missing. By adding this parameter to your call, the module will force the re-creation of the certificates.
92 [switch]$ForceCertificateGeneration = $FALSE,
93 # This option will allow the validation of the trusted-master.crt generated during certificate generation, to ensure we are connected to the correct endpoint to prevent possible man-in-the-middle attacks.
94 [string]$CAFingerprint,
95 # Use this switch to enable the CAProxy feature Introduced with Icinga 2.8
96 [switch]$CAProxy = $FALSE,
97
98 # Director communication
99 #This argument will tell the PowerShell where the Icinga Director can be found. Please specify the entire path to the Icinga Director! Example: https://example.com/icingaweb2/director/
100 [string]$DirectorUrl,
101 #To fetch the Ticket for a host, creating host objects or deploying the configuration you will have to authenticate against the Icinga Director. This parameter allows to set the User we shall use to login.
102 [string]$DirectorUser,
103 # To fetch the Ticket for a host, creating host objects or deploying the configuration you will have to authenticate against the Icinga Director. This parameter allows to set the Password we shall use to login.
104 [string]$DirectorPassword,
105 # TODO
106 [string]$DirectorDomain,
107 # API key for specific host templates, allowing the configuration and creation of host objects within the Icinga Director without password authentication. This is the API token assigned to a host template. Hosts created with this token, will automaticly receive the Host-Template assigned to the API key. Furthermore this token allows to access the Icinga Director Self-Service API to fetch basic arguments for the module.
108 # Note: This argument requires Icinga Director API Version 1.4.0 or higher
109 [string]$DirectorAuthToken,
110 # This argument allows you to parse either a valid JSON-String or an hashtable / array, containing all informations for the host object to create. Please note that using arrays or hashtable objects for this argument will require PowerShell version 3 and above.
111 [System.Object]$DirectorHostObject,
112 # If you add this parameter to your script call, the PowerShell module will tell the Icinga Director to deploy outstanding configurations. This parameter can be used in combination with -DirectorHostObject, to create objects and deploy them right away. This argument requires the user and password argument and will not work with the Self Service api.
113 # Caution: If set, all outstanding deployments inside the Icinga Director will be deployed. Use with caution!!!
114 [switch]$DirectorDeployConfig = $FALSE,
115
116 # NSClient Installer
117 [switch]$InstallNSClient = $FALSE,
118 [switch]$NSClientAddDefaults = $FALSE,
119 [switch]$NSClientEnableFirewall = $FALSE,
120 [switch]$NSClientEnableService = $FALSE,
121 [string]$NSClientDirectory,
122 [string]$NSClientInstallerPath,
123
124 # Uninstaller arguments
125 # This argument is only used by the function 'uninstall' and will remove the remaining content from 'C:\Program Data\icinga2' to prepare a clean setup of the Icinga 2 infrastrucure.
126 [switch]$FullUninstallation = $FALSE,
127 # When this argument is set, the installed NSClient++ will be removed from the system as well. This argument is only used by calling the function 'uninstall'
128 [switch]$RemoveNSClient = $FALSE,
129
130 # Dump Icinga Config
131 [switch]$DumpIcingaConfig = $FALSE,
132 # Dump Icinga Objects
133 [switch]$DumpIcingaObjects = $FALSE,
134
135 #Internal handling
136 # This argument allows to shorten the entire call of the module, not requiring to define a custom variable and executing the installation function of the monitoring components.
137 [switch]$RunInstaller = $FALSE,
138 # This argument allows to shorten the entire call of the module, not requiring to define a custom variable and executing the uninstallation function of the monitoring components.
139 [switch]$RunUninstaller = $FALSE,
140 #In certain cases it could be required to ingore SSL certificate validations from the Icinga Web 2 installation (for example in case self-signed certificates are used). By default the PowerShell Module is validating SSL certificates and throws an error if the validation fails.
141 #In case self-signed certificates are used and not included to the local certificate store of the Windows machine, the module will fail. By providing this argument, validation will always be valid and the script will execute as if the certificate was valid.
142 [switch]$IgnoreSSLErrors = $FALSE,
143
144 [switch]$DebugMode = $FALSE,
145 # Specify a path to either a directory or a file to write all output from the PowerShell module into a file for later debugging. In case a directory is specified, the script will automaticly create a new file with a unique name into it. If a file is specified which is not yet present, it will be created.
146 [string]$ModuleLogFile
147 );
148
149 #
150 # Initialise our installer object
151 # and generate our config objects
152 #
153 $installer = New-Object -TypeName PSObject;
154 $installer | Add-Member -membertype NoteProperty -name 'properties' -value @{}
155 $installer | Add-Member -membertype NoteProperty -name 'cfg' -value @{
156 agent_name = $AgentName;
157 ticket = $Ticket;
158 agent_version = $InstallAgentVersion;
159 fetch_agent_name = $FetchAgentName;
160 fetch_agent_fqdn = $FetchAgentFQDN;
161 transform_hostname = $TransformHostname;
162 agent_listen_port = $AgentListenPort;
163 parent_zone = $ParentZone;
164 accept_config = $AcceptConfig;
165 icinga_enable_debug_log = $IcingaEnableDebugLog;
166 agent_add_firewall_rule = $AgentAddFirewallRule;
167 parent_endpoints = $ParentEndpoints;
168 endpoints_config = $EndpointsConfig;
169 global_zones = $GlobalZones;
170 icinga_service_user = $IcingaServiceUser;
171 download_url = $DownloadUrl;
172 agent_install_directory = $AgentInstallDirectory;
173 allow_updates = $AllowUpdates;
174 installer_hashes = $InstallerHashes;
175 flush_api_directory = $FlushApiDirectory;
176 ca_server = $CAServer;
177 ca_certificate_path = $CACertificatePath;
178 ca_port = $CAPort;
179 force_cert = $ForceCertificateGeneration;
180 ca_fingerprint = $CAFingerprint;
181 caproxy = $CAProxy;
182 director_url = $DirectorUrl;
183 director_user = $DirectorUser;
184 director_password = $DirectorPassword;
185 director_domain = $DirectorDomain;
186 director_auth_token = $DirectorAuthToken;
187 director_host_object = $DirectorHostObject;
188 director_deploy_config = $DirectorDeployConfig;
189 install_nsclient = $InstallNSClient;
190 nsclient_add_defaults = $NSClientAddDefaults;
191 nsclient_firewall = $NSClientEnableFirewall;
192 nsclient_service = $NSClientEnableService;
193 nsclient_directory = $NSClientDirectory;
194 nsclient_installer_path = $NSClientInstallerPath;
195 full_uninstallation = $FullUninstallation;
196 remove_nsclient = $RemoveNSClient;
197 ignore_ssl_errors = $IgnoreSSLErrors;
198 debug_mode = $DebugMode;
199 module_log_file = $ModuleLogFile;
200 }
201
202 #
203 # Access default script config parameters
204 # by using this function. These variables
205 # are set during the initial call of
206 # the script with the parameters
207 #
208 $installer | Add-Member -membertype ScriptMethod -name 'config' -value {
209 param([string] $key);
210 return $this.cfg[$key];
211 }
212
213 #
214 # Override the given arguments of the PowerShell script with
215 # custom values or edited values
216 #
217 $installer | Add-Member -membertype ScriptMethod -name 'overrideConfig' -value {
218 param([string] $key, $value);
219 $this.cfg[$key] = $value;
220 }
221
222 #
223 # Convert a boolean value $TRUE $FALSE
224 # to a string value
225 #
226 $installer | Add-Member -membertype ScriptMethod -name 'convertBoolToString' -value {
227 param([bool]$key);
228 if ($key) {
229 return 'true';
230 }
231 return 'false';
232 }
233
234 #
235 # Convert a boolean value $TRUE $FALSE
236 # to a int value
237 #
238 $installer | Add-Member -membertype ScriptMethod -name 'convertBoolToInt' -value {
239 param([bool]$key);
240 if ($key) {
241 return 1;
242 }
243 return 0;
244 }
245
246 #
247 # Global variables can be accessed
248 # by using this function. Example:
249 # $this.getProperty('agent_version)
250 #
251 $installer | Add-Member -membertype ScriptMethod -name 'getProperty' -value {
252 param([string] $key);
253
254 # Initialse some variables first
255 # will only be called once
256 if (-Not $this.properties.Get_Item('initialized')) {
257 $this.init();
258 }
259
260 return $this.properties.Get_Item($key);
261 }
262
263 #
264 # Set the value of a global variable
265 # to ensure later usage. Example
266 # $this.setProperty('agent_version', '2.4.10')
267 #
268 $installer | Add-Member -membertype ScriptMethod -name 'setProperty' -value {
269 param([string]$key, $value);
270
271 # Initialse some variables first
272 # will only be called once
273 if (-Not $this.properties.Get_Item('initialized')) {
274 $this.properties.Set_Item('initialized', $TRUE);
275 $this.init();
276 }
277
278 $this.properties.Set_Item($key, $value);
279 }
280
281 #
282 # This function will dump all global
283 # variables of the script for debugging
284 # purposes
285 #
286 $installer | Add-Member -membertype ScriptMethod -name 'dumpProperties' -value {
287 [string]$dumpData = $this.properties | Out-String;
288 $this.debug('Dumping properties...');
289 $this.debug($dumpData);
290 }
291
292 #
293 # Dump all configured arguments for easier debugging
294 #
295 $installer | Add-Member -membertype ScriptMethod -name 'dumpConfig' -value {
296 [string]$dumpData = $this.cfg | Out-String;
297 $this.debug('Dumping config...');
298 $this.debug($dumpData);
299 }
300
301 #
302 # Write all output from consoles to a logfile
303 #
304 $installer | Add-Member -membertype ScriptMethod -name 'writeLogFile' -value {
305 param([string]$severity, [string]$content);
306
307 # If no logfile is specified, do nothing
308 if (-Not $this.config('module_log_file')) {
309 return;
310 }
311
312 # Store our logfile into a variable
313 $logFile = $this.config('module_log_file');
314
315 # Have we specified a directory to write into or a file already?
316 try {
317 # Check if we are a directory or a file
318 # Will return false for files or non-existing files
319 $directory = (Get-Item $logFile) -is [System.IO.DirectoryInfo];
320 } catch {
321 # Nothing to catch. Simply get rid of error messages from aboves function in case of error
322 # Will return false anyways on error
323 }
324
325 # If we are a directory, add a file we can write to
326 if ($directory) {
327 $logFile = Join-Path -Path $logFile -ChildPath 'icinga2agent_psmodule.log';
328 }
329
330 # Format a timestamp to get to know the exact date and time. Example: 2017-13-07 22:09:13.263.263
331 $timestamp = Get-Date -Format "yyyy-dd-MM HH:mm:ss.fff";
332 $content = [string]::Format('{0} [{1}]: {2}', $timestamp, $severity, $content);
333
334 # Write the content to our logfile
335 Add-Content -Path $logFile -Value $content;
336 }
337
338 #
339 # This function will print messages as errors, but add them internally to
340 # an exception list. These will re-printed at the end to summarize possible
341 # issues during the run
342 #
343 $installer | Add-Member -membertype ScriptMethod -name 'exception' -value {
344 param([string]$message, [string[]]$args);
345 [array]$exceptions = $this.getProperty('exception_messages');
346 if ($exceptions -eq $null) {
347 $exceptions = @();
348 }
349 $exceptions += $message;
350 $this.setProperty('exception_messages', $exceptions);
351 write-host 'Fatal:' $message -ForegroundColor red;
352 $this.writeLogFile('fatal', $message);
353 }
354
355 #
356 # Get the current exit code of the script. Return 0 for no errors and 1 for
357 # possible errors, including a summary of what went wrong
358 #
359 $installer | Add-Member -membertype ScriptMethod -name 'getScriptExitCode' -value {
360 [array]$exceptions = $this.getProperty('exception_messages');
361
362 $this.dumpProperties();
363 $this.dumpConfig();
364
365 if ($exceptions -eq $null) {
366 return 0;
367 }
368
369 $this.writeLogFile('fatal', '##################################################################');
370 $message = '######## The script encountered several errors during run ########';
371 $this.writeLogFile('fatal', $message);
372 $this.writeLogFile('fatal', '##################################################################');
373 write-host $message -ForegroundColor red;
374 foreach ($err in $exceptions) {
375 write-host 'Fatal:' $err -ForegroundColor red;
376 $this.writeLogFile('fatal', $err);
377 }
378
379 return 1;
380 }
381
382 #
383 # Print the relevant exception
384 # By reading the relevant info
385 # from the stack
386 #
387 $installer | Add-Member -membertype ScriptMethod -name 'printLastException' -value {
388 $this.exception($_.Exception.Message);
389 }
390
391 #
392 # this function will print an info message
393 # or throw an exception, based on the
394 # provided exitcode
395 # (0 = ok, anything else => exception)
396 #
397 $installer | Add-Member -membertype ScriptMethod -name 'printAndAssertResultBasedOnExitCode' -value {
398 param([string]$result, [string]$exitcode);
399 if ($exitcode -ne 0) {
400 throw $result;
401 } else {
402 $this.info($result);
403 }
404 }
405
406 #
407 # Return an error message with red text
408 #
409 $installer | Add-Member -membertype ScriptMethod -name 'error' -value {
410 param([string] $message, [array] $args);
411 Write-Host 'Error:' $message -ForegroundColor red;
412 $this.writeLogFile('error', $message);
413 }
414
415 #
416 # Return a warning message with yellow text
417 #
418 $installer | Add-Member -membertype ScriptMethod -name 'warn' -value {
419 param([string] $message, [array] $args);
420 Write-Host 'Warning:' $message -ForegroundColor yellow;
421 $this.writeLogFile('warning', $message);
422 }
423
424 #
425 # Return a info message with green text
426 #
427 $installer | Add-Member -membertype ScriptMethod -name 'info' -value {
428 param([string] $message, [array] $args);
429 Write-Host 'Notice:' $message -ForegroundColor green;
430 $this.writeLogFile('info', $message);
431 }
432
433 #
434 # Return a output message with wrhite text
435 #
436 $installer | Add-Member -membertype ScriptMethod -name 'output' -value {
437 param([string] $message, [array] $args);
438 Write-Host '' $message -ForegroundColor white;
439 $this.writeLogFile('', $message);
440 }
441
442 #
443 # Return a debug message with blue text
444 # in case debug mode is enabled
445 #
446 $installer | Add-Member -membertype ScriptMethod -name 'debug' -value {
447 param([string] $message, [array] $args);
448 if ($this.config('debug_mode')) {
449 Write-Host 'Debug:' $message -ForegroundColor blue;
450 $this.writeLogFile('debug', $message);
451 }
452 }
453
454 #
455 # Initialise certain parts of the
456 # script first
457 #
458 $installer | Add-Member -membertype ScriptMethod -name 'init' -value {
459 $this.setProperty('initialized', $TRUE);
460 # Set the default config dir
461 $this.setProperty('config_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\etc\icinga2\'));
462 $this.setProperty('api_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\api'));
463 $this.setProperty('cert_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs'));
464 $this.setProperty('icinga_ticket', $this.config('ticket'));
465 $this.setProperty('local_hostname', $this.config('agent_name'));
466 # Ensure we generate the required configuration content
467 $this.generateConfigContent();
468 }
469
470 #
471 # We require to run this script as admin. Generate the required function here
472 # We might run this script from a non-privileged user. Ensure we have admin
473 # rights first. Otherwise abort the script.
474 #
475 $installer | Add-Member -membertype ScriptMethod -name 'isAdmin' -value {
476 $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent();
477 $principal = New-Object System.Security.Principal.WindowsPrincipal($identity);
478
479 if (-not $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)) {
480 throw 'You require to run this script as administrator.';
481 return $FALSE;
482 }
483 return $TRUE;
484 }
485
486 #
487 # In case we want to define endpoint configuration (address / port)
488 # we will require to fetch data correctly from a given array
489 #
490 $installer | Add-Member -membertype ScriptMethod -name 'getEndpointConfigurationByArrayIndex' -value {
491 param([int] $currentIndex);
492
493 # Load the config into a local variable for quicker access
494 [array]$endpoint_config = $this.config('endpoints_config');
495
496 # In case no endpoint config is given, we should do nothing
497 if ($endpoint_config -eq $NULL) {
498 return '';
499 }
500
501 [string]$configArgument = $endpoint_config[$currentIndex];
502 [string]$config_string = '';
503 [array]$configObject = '';
504
505 if ($configArgument -ne '') {
506 $configObject = $configArgument.Split(';');
507 } else {
508 return '';
509 }
510
511 # Write the host data from the first array position
512 if ($configObject[0]) {
513 $config_string += [string]::Format(' host = "{0}"', $configObject[0]);
514 }
515
516 # Write the port data from the second array position
517 if ($configObject[1]) {
518 $config_string += [string]::Format('{0} port = {1}', "`n", $configObject[1]);
519 }
520
521 # Return the host and possible port configuration for this endpoint
522 return $config_string;
523 }
524
525 #
526 # Build endpoint hosts and objects based
527 # on configuration
528 #
529 $installer | Add-Member -membertype ScriptMethod -name 'generateEndpointNodes' -value {
530
531 if ($this.config('parent_endpoints')) {
532 [string]$endpoint_objects = '';
533 [string]$endpoint_nodes = '';
534 [int]$endpoint_index = 0;
535
536 foreach ($endpoint in $this.config('parent_endpoints')) {
537 $endpoint_objects += [string]::Format('object Endpoint "{0}" {1}{2}', $endpoint, '{', "`n");
538 $endpoint_objects += $this.getEndpointConfigurationByArrayIndex($endpoint_index);
539 $endpoint_objects += [string]::Format('{0}{1}{2}', "`n", '}', "`n");
540 $endpoint_nodes += [string]::Format('"{0}", ', $endpoint);
541 $endpoint_index += 1;
542 }
543 # Remove the last blank and , from the string
544 if (-Not $endpoint_nodes.length -eq 0) {
545 $endpoint_nodes = $endpoint_nodes.Remove($endpoint_nodes.length - 2, 2);
546 }
547 $this.setProperty('endpoint_nodes', $endpoint_nodes);
548 $this.setProperty('endpoint_objects', $endpoint_objects);
549 $this.setProperty('generate_config', 'true');
550 } else {
551 $this.setProperty('generate_config', 'false');
552 }
553 }
554
555 #
556 # Generate global zones by configuration
557 #
558 $installer | Add-Member -membertype ScriptMethod -name 'generateGlobalZones' -value {
559
560 # Load all configured global zones
561 [array]$global_zones = $this.config('global_zones');
562 [string]$zones = '';
563
564 # In case no zones are given, simply add director-global
565 if ($global_zones -eq $NULL) {
566 $this.setProperty('global_zones', $zones);
567 return;
568 }
569
570 # Loop through all given zones and add them to our configuration
571 foreach ($zone in $global_zones) {
572 if ($zone -ne '') {
573 $zones += [string]::Format('object Zone "{0}" {1}{2} global = true{3}{4}{5}', $zone, '{', "`n", "`n", '}', "`n");
574 }
575 }
576 $this.setProperty('global_zones', $zones);
577 }
578
579 #
580 # Generate default config values
581 #
582 $installer | Add-Member -membertype ScriptMethod -name 'generateConfigContent' -value {
583 $this.generateEndpointNodes();
584 $this.generateGlobalZones();
585 }
586
587 #
588 # This function will ensure we create a
589 # Web Client object we can use entirely
590 # inside the module to achieve our requirements
591 #
592 $installer | Add-Member -membertype ScriptMethod -name 'createWebClientInstance' -value {
593 param([string]$header, [bool]$directorHeader = $FALSE);
594
595 [System.Object]$webClient = New-Object System.Net.WebClient;
596 if ($this.config('director_user') -And $this.config('director_password')) {
597 [string]$domain = $null;
598 if ($this.config('director_domain')) {
599 $domain = $this.config('director_domain');
600 }
601 $webClient.Credentials = New-Object System.Net.NetworkCredential($this.config('director_user'), $this.config('director_password'), $domain);
602 }
603 $webClient.Headers.add('accept', $header);
604 if ($directorHeader) {
605 $webClient.Headers.add('X-Director-Accept', 'text/plain');
606 }
607
608 return $webClient;
609 }
610
611 #
612 # Handle HTTP Requests properly to receive proper status codes in return
613 #
614 $installer | Add-Member -membertype ScriptMethod -name 'createHTTPRequest' -value {
615 param([string]$url, [string]$body, [string]$method, [string]$header, [bool]$directorHeader, [bool]$printExceptionMessage);
616
617 $httpRequest = [System.Net.HttpWebRequest]::Create($url);
618 $httpRequest.Method = $method;
619 $httpRequest.Accept = $header;
620 $httpRequest.ContentType = 'application/json; charset=utf-8';
621 if ($directorHeader) {
622 $httpRequest.Headers.Add('X-Director-Accept: text/plain');
623 }
624 $httpRequest.TimeOut = 6000;
625
626 # If we are using self-signed certificates for example, the HTTP request will
627 # fail caused by the SSL certificate. With this we can allow even faulty
628 # certificates. This should be used with caution
629 if ($this.config('ignore_ssl_errors')) {
630 [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
631 }
632
633 if ($this.config('director_user') -And $this.config('director_password')) {
634 [string]$credentials = [System.Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes([string]::Format('{0}:{1}', $this.config('director_user'), $this.config('director_password'))));
635 $httpRequest.Headers.add([string]::Format('Authorization: Basic {0}', $credentials));
636 }
637
638 # Only send data in case we want to send some data
639 if ($body -ne '') {
640 $transmitBytes = [System.Text.Encoding]::UTF8.GetBytes($body);
641 $httpRequest.ContentLength = $transmitBytes.Length;
642 [System.IO.Stream]$httpOutput = [System.IO.Stream]$httpRequest.GetRequestStream()
643 $httpOutput.Write($transmitBytes, 0, $transmitBytes.Length)
644 $httpOutput.Close()
645 }
646
647 try {
648
649 return $this.readResponseStream($httpRequest.GetResponse());
650
651 } catch [System.Net.WebException] {
652 if ($printExceptionMessage) {
653 # Print an exception message and the possible body in case we received one
654 # to make troubleshooting easier
655 [string]$errorResponse = $this.readResponseStream($_.Exception.Response);
656 $this.error($_.Exception.Message);
657 if ($errorResponse -ne '') {
658 $this.error($errorResponse);
659 }
660 }
661
662 $exceptionMessage = $_.Exception.Response;
663 if ($exceptionMessage.StatusCode) {
664 return [int][System.Net.HttpStatusCode]$exceptionMessage.StatusCode;
665 } else {
666 return 900;
667 }
668 }
669
670 return '';
671 }
672
673 #
674 # Read the content of a response and return it's value as a string
675 #
676 $installer | Add-Member -membertype ScriptMethod -name 'readResponseStream' -value {
677 param([System.Object]$response);
678
679 if ($response) {
680 $responseStream = $response.getResponseStream();
681 $streamReader = New-Object IO.StreamReader($responseStream);
682 $result = $streamReader.ReadToEnd();
683 $response.close()
684 $streamReader.close()
685
686 return $result;
687 }
688
689 $this.exception('Could not retreive response from remote server. Response is null. This might be caused by SSL errors. Please try using -IgnoreSSLErrors as argument and try again.');
690 return 'No response from remote server';
691 }
692
693 #
694 # Check if the provided result is an HTTP Response code
695 #
696 $installer | Add-Member -membertype ScriptMethod -name 'isHTTPResponseCode' -value {
697 param([string]$httpResult);
698
699 if ($httpResult.length -eq 3) {
700 return $TRUE;
701 }
702
703 return $FALSE;
704 }
705
706 #
707 # Do we require to update the Agent?
708 # Might be disabled by user or current version
709 # is already installed
710 #
711 $installer | Add-Member -membertype ScriptMethod -name 'requireAgentUpdate' -value {
712 if (-Not $this.config('allow_updates') -Or -Not $this.config('agent_version')) {
713 $this.warn('Icinga 2 Agent update installation disabled.');
714 return $FALSE;
715 }
716
717 if ($this.getProperty('agent_version') -eq $this.config('agent_version')) {
718 $this.info('Icinga 2 Agent up-to-date. No update required.');
719 return $FALSE;
720 }
721
722 $this.info([string]::Format('Current Icinga 2 Agent Version ({0}) is not matching server version ({1}). Downloading new version...'), $this.getProperty('agent_version'), $this.config('agent_version'));
723
724 return $TRUE;
725 }
726
727 #
728 # We could try to install the Agent from a local directory
729 #
730 $installer | Add-Member -membertype ScriptMethod -name 'isDownloadPathLocal' -value {
731 if ($this.config('download_url') -And (Test-Path ($this.config('download_url')))) {
732 return $TRUE;
733 }
734 return $FALSE;
735 }
736
737 #
738 # Download the Icinga 2 Agent Installer from out defined source
739 #
740 $installer | Add-Member -membertype ScriptMethod -name 'downloadInstaller' -value {
741 if (-Not $this.config('agent_version')) {
742 return;
743 }
744
745 if ($this.isDownloadPathLocal()) {
746 $this.info('Installing Icinga 2 Agent from local directory');
747 } else {
748 $url = $this.config('download_url') + $this.getProperty('install_msi_package');
749 $this.info([string]::Format('Downloading Icinga 2 Agent Binary from "{0}"', $url));
750
751 Try {
752 [System.Object]$client = New-Object System.Net.WebClient;
753 $client.DownloadFile($url, $this.getInstallerPath());
754
755 if (-Not $this.installerExists()) {
756 $this.exception([string]::Format('Unable to locate downloaded Icinga 2 Agent installer file from {0}. Download destination: {1}', $url, $this.getInstallerPath()));
757 }
758 } catch {
759 $this.exception([string]::Format('Unable to download Icinga 2 Agent from {0}. Please ensure the link does exist and access is possible. Error: {1}', $url, $_.Exception.Message));
760 }
761 }
762 }
763
764 #
765 # In case we provide a list of hashes to very against
766 # we check them to ensure the package we downloaded
767 # for the Agent installation is allowed to be installed
768 #
769 $installer | Add-Member -membertype ScriptMethod -name 'verifyInstallerChecksumAndThrowException' -value {
770 if (-Not $this.config('installer_hashes')) {
771 $this.warn("Icinga 2 Agent Installer verification disabled.");
772 return;
773 }
774
775 [string]$installerHash = $this.getInstallerFileHash($this.getInstallerPath());
776 foreach($hash in $this.config('installer_hashes')) {
777 if ($hash -eq $installerHash) {
778 $this.info('Icinga 2 Agent hash verification successfull.');
779 return;
780 }
781 }
782
783 throw 'Failed to verify against any provided installer hash.';
784 return;
785 }
786
787 #
788 # Get the SHA1 hash from our uninstaller file
789 # Own function required because Get-FileHash is not
790 # supported by PowerShell Version 2
791 #
792 $installer | Add-Member -membertype ScriptMethod -name 'getInstallerFileHash' -value {
793 param([string]$filename);
794
795 [System.Object]$fileInput = New-Object System.IO.FileStream($filename,[System.IO.FileMode]::Open);
796 [System.Object]$hash = New-Object System.Text.StringBuilder;
797 [System.Security.Cryptography.HashAlgorithm]::Create('SHA1').ComputeHash($fileInput) |
798 ForEach-Object {
799 [Void]$hash.Append($_.ToString("x2"));
800 }
801 $fileInput.Close();
802 return $hash.ToString().ToUpper();
803 }
804
805 #
806 # Returns the full path to our installer package
807 #
808 $installer | Add-Member -membertype ScriptMethod -name 'getInstallerPath' -value {
809 if (-Not $this.config('download_url') -Or -Not $this.getProperty('install_msi_package')) {
810 return '';
811 }
812 [string]$installerPath = '';
813 if (Test-Path ($this.config('download_url'))) {
814 $installerPath = Join-Path -Path $this.config('download_url') -ChildPath $this.getProperty('install_msi_package');
815 } else {
816 $installerPath = [string]::Format('{0}/{1}', $this.config('download_url'), $this.getProperty('install_msi_package'));
817 }
818
819 if ($this.isDownloadPathLocal()) {
820 if (Test-Path $installerPath) {
821 return $installerPath;
822 } else {
823 $this.exception([string]::Format('Failed to locate local Icinga 2 Agent installer at {0}', $installerPath));
824 return '';
825 }
826 } else {
827 return (Join-Path -Path $Env:temp -ChildPath $this.getProperty('install_msi_package'));
828 }
829 }
830
831 #
832 # Verify that the installer package we downloaded
833 # does exist in first place
834 #
835 $installer | Add-Member -membertype ScriptMethod -name 'installerExists' -value {
836 if ($this.getInstallerPath() -And (Test-Path $this.getInstallerPath())) {
837 return $TRUE;
838 }
839 return $FALSE;
840 }
841
842 #
843 # Get all arguments for the Icinga 2 Agent installer package
844 #
845 $installer | Add-Member -membertype ScriptMethod -name 'getIcingaAgentInstallerArguments' -value {
846 # Initialise some basic variables
847 [string]$arguments = '';
848 [string]$installerLocation = '';
849
850 # By default, install the Icinga 2 Agent again in the pre-installed directory
851 # before the update. Will only apply during updates / downgrades of the Agent
852 if ($this.getProperty('cur_install_dir')) {
853 # In case we perform an architecture change, we should use the new default location as source in case
854 # we have installed the Agent into Program Files (x86) for example but are now using a x64 Agent
855 # which should be installed into Program Files instead
856 if ($this.getProperty('agent_architecture_change') -And $this.getProperty('agent_migration_target')) {
857 $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.getProperty('agent_migration_target'));
858 } else {
859 $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.getProperty('cur_install_dir'));
860 }
861 }
862
863 # However, if we specified a custom directory over the argument, always use that
864 # one as installer target directory
865 if ($this.config('agent_install_directory')) {
866 $installerLocation = [string]::Format(' INSTALL_ROOT="{0}"', $this.config('agent_install_directory'));
867 $this.setProperty('cur_install_dir', $this.config('agent_install_directory'));
868 }
869
870 $arguments += $installerLocation;
871
872 return $arguments;
873 }
874
875 #
876 # Do we require to migrate data from previous Icinga 2 Agent Directory
877 #
878 $installer | Add-Member -membertype ScriptMethod -name 'checkForIcingaMigrationRequirement' -value {
879 if ($this.getProperty('cur_install_dir')) {
880 [string]$installDir = $this.getProperty('cur_install_dir');
881 # Just in case we installed an x86 Agent on the System, we will require to migrate to x64 on a x64 system.
882 if (${Env:ProgramFiles(x86)} -And $installDir.contains(${Env:ProgramFiles(x86)}) -And $this.getProperty('system_architecture') -eq 'x86_64') {
883 [string]$migrationPath = $installDir.Replace(${Env:ProgramFiles(x86)}, ${Env:ProgramFiles});
884 $this.setProperty('agent_architecture_change', $TRUE);
885 $this.setProperty('require_migration', $TRUE);
886 $this.setProperty('agent_migration_source', $installDir);
887 $this.setProperty('agent_migration_target', $migrationPath);
888 $this.setProperty('cur_install_dir', $migrationPath);
889 $this.warn('Detected architecture change. Current installed Agent version is x86, while new installed version will be x64. Possible data will be migrated.');
890 } else {
891 $this.setProperty('agent_migration_source', $this.getProperty('cur_install_dir'));
892 }
893 }
894
895 if ($this.config('agent_install_directory')) {
896 [string]$currentInstallDir = $this.cutLastSlashFromDirectoryPath($this.getProperty('cur_install_dir'));
897 [string]$intendedInstallDir = $this.cutLastSlashFromDirectoryPath($this.config('agent_install_directory'));
898
899 if ($currentInstallDir -ne $intendedInstallDir) {
900 $this.setProperty('agent_migration_target', $this.config('agent_install_directory'));
901 $this.setProperty('require_migration', $TRUE);
902 }
903 }
904 }
905
906 #
907 # To ensure we handle path strings correctly, we always require to cut the last \
908 #
909 $installer | Add-Member -membertype ScriptMethod -name 'cutLastSlashFromDirectoryPath' -value {
910 param([string]$path);
911
912 if (-Not $path -Or $path -eq '') {
913 return $path;
914 }
915
916 if ($path[$path.Length - 1] -eq '\') {
917 $path = $path.Substring(0, $path.Length - 1);
918 }
919
920 return $path;
921 }
922
923 #
924 # Install the Icinga 2 agent from the provided installation package
925 #
926 $installer | Add-Member -membertype ScriptMethod -name 'installAgent' -value {
927 $this.downloadInstaller();
928 if (-Not $this.installerExists()) {
929 $this.exception('Failed to setup Icinga 2 Agent. Installer package not found.');
930 return;
931 }
932 $this.verifyInstallerChecksumAndThrowException();
933 $this.info('Installing Icinga 2 Agent');
934
935 # Start the installer process
936 $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $this.getInstallerPath(), $this.getIcingaAgentInstallerArguments()));
937
938 # Exit Code 0 means the Agent was installed successfully
939 # Otherwise we require to throw an error
940 if ($result.Get_Item('exitcode') -ne 0) {
941 $this.exception('Failed to install Icinga 2 Agent. ' + $result.Get_Item('message'));
942 } else {
943 $this.info('Icinga 2 Agent installed.');
944 }
945
946 # Update the Icinga 2 Agent Directories in case of a version change
947 # Required by updating from older versions to 2.8.0. and newer
948 $return = $this.isAgentInstalled();
949
950 $this.setProperty('require_restart', 'true');
951 }
952
953 #
954 # Updates the Agent in case allowed and required.
955 # Removes previous version of Icinga 2 Agent first
956 #
957 $installer | Add-Member -membertype ScriptMethod -name 'updateAgent' -value {
958 $this.downloadInstaller();
959 if (-Not $this.installerExists()) {
960 $this.exception('Failed to update Icinga 2 Agent. Installer package not found.');
961 return;
962 }
963 $this.verifyInstallerChecksumAndThrowException()
964 if (-Not $this.getProperty('uninstall_id')) {
965 $this.exception('Failed to update Icinga 2 Agent. Uninstaller is not specified.');
966 return;
967 }
968
969 $this.info('Removing previous Icinga 2 Agent version');
970 # Start the uninstaller process
971 $result = $this.startProcess('MsiExec.exe', $TRUE, $this.getProperty('uninstall_id') +' /q');
972
973 # Exit Code 0 means the Agent was removed successfully
974 # Otherwise we require to throw an error
975 if ($result.Get_Item('exitcode') -ne 0) {
976 $this.exception('Failed to remove Icinga 2 Agent. ' + $result.Get_Item('message'));
977 } else {
978 $this.info('Icinga 2 Agent successfully removed.');
979 }
980
981 $this.checkForIcingaMigrationRequirement();
982 $this.applyPossibleAgentMigration();
983
984 $this.info('Installing new Icinga 2 Agent version');
985 # Start the installer process
986 $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $this.getInstallerPath(), $this.getIcingaAgentInstallerArguments()));
987
988 # Exit Code 0 means the Agent was removed successfully
989 # Otherwise we require to throw an error
990 if ($result.Get_Item('exitcode') -ne 0) {
991 $this.exception([string]::Format('Failed to install new Icinga 2 Agent. {0}', $result.Get_Item('message')));
992 } else {
993 $this.info('Icinga 2 Agent successfully updated.');
994 }
995
996 # Update the Icinga 2 Agent Directories in case of a version change
997 # Required by updating from older versions to 2.8.0. and newer
998 $return = $this.isAgentInstalled();
999 $this.setProperty('require_restart', 'true');
1000 }
1001
1002 #
1003 # Migrate a folder and it's content from a previous Agent installation to
1004 # a new target destination
1005 #
1006 $installer | Add-Member -membertype ScriptMethod -name 'doMigrateIcingaDirectory' -value {
1007 param([string]$sourcePath, [string]$targetPath, [string]$directory);
1008
1009 if (Test-Path (Join-Path -Path $sourcePath -ChildPath $directory)) {
1010 [string]$source = Join-Path -Path $sourcePath -ChildPath $directory;
1011 [string]$target = Join-Path -Path $targetPath -ChildPath $directory;
1012 $this.info([string]::Format('Migrating content from "{0}" to "{1}"', $source, $target));
1013 $result = Copy-Item $source $target -Recurse;
1014 }
1015 }
1016
1017 #
1018 # Copy a single file from it's source location to our target location
1019 #
1020 $installer | Add-Member -membertype ScriptMethod -name 'doMigrateIcingaFile' -value {
1021 param([string]$sourcePath, [string]$targetPath, [string]$file);
1022 $this.info([string]::Format('Migrating file from "{0}" to "{1}\{2}"', $sourcePath, $targetPath, $_));
1023 Copy-Item $sourcePath $targetPath;
1024 }
1025
1026 #
1027 # This function will determine if we require to migrate content from a previous
1028 # Icinga 2 Agent installation to the new location
1029 #
1030 $installer | Add-Member -membertype ScriptMethod -name 'applyPossibleAgentMigration' -value {
1031 if (-Not $this.getProperty('require_migration') -Or $this.getProperty('require_migration') -eq $FALSE) {
1032 $this.info('No migration of Icinga 2 Agent data required.')
1033 return;
1034 }
1035
1036 $this.info([string]::Format('Icinga 2 Agent installation location changed from {0} to {1}. Migrating possible content...', $this.getProperty('agent_migration_source'), $this.getProperty('agent_migration_target')));
1037
1038 if ($this.getProperty('agent_migration_source') -And (Test-Path ($this.getProperty('agent_migration_source')))) {
1039 # Load Directories and Remove \ at the end of the path if present to ensure we have the same path base
1040 [string]$sourcePath = $this.cutLastSlashFromDirectoryPath($this.getProperty('agent_migration_source'));
1041 [string]$targetPath = $this.cutLastSlashFromDirectoryPath($this.getProperty('agent_migration_target'));
1042
1043 # Get all objects within our source root and copy it to our target destination
1044 $result = Get-ChildItem -Path $sourcePath |
1045 ForEach-Object {
1046 if ($_.PSIsContainer) {
1047 $this.doMigrateIcingaDirectory($sourcePath, $targetPath, $_);
1048 } else {
1049 $this.doMigrateIcingaFile($_.FullName, $targetPath, $_);
1050 }
1051 }
1052 $this.info([string]::Format('Migration of source folder applied. Please remove content from previous directory {0} if no longer required.', $sourcePath));
1053 } else {
1054 $this.info('No data for migration found. Setup is clean.');
1055 }
1056 }
1057
1058 #
1059 # We might have installed the Icinga 2 Agent
1060 # already. In case we do, get all data to
1061 # ensure we access the Agent correctly
1062 #
1063 $installer | Add-Member -membertype ScriptMethod -name 'isAgentInstalled' -value {
1064 [string]$architecture = '';
1065 if ([IntPtr]::Size -eq 4) {
1066 $architecture = "x86";
1067 $regPath = 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*';
1068 } else {
1069 $architecture = "x86_64";
1070 $regPath = @('HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*');
1071 }
1072
1073 # Try locating current Icinga 2 Agent installation
1074 $localData = Get-ItemProperty $regPath |
1075 .{
1076 process {
1077 if ($_.DisplayName) {
1078 $_;
1079 }
1080 }
1081 } |
1082 Where-Object {
1083 $_.DisplayName -eq 'Icinga 2';
1084 } |
1085 Select-Object -Property InstallLocation, UninstallString, DisplayVersion;
1086
1087 if ($localData.UninstallString) {
1088 $this.setProperty('uninstall_id', $localData.UninstallString.Replace("MsiExec.exe ", ""));
1089 }
1090 $this.setProperty('cur_install_dir', $localData.InstallLocation);
1091 $this.setProperty('agent_version', $localData.DisplayVersion);
1092 $this.setProperty('install_msi_package', 'Icinga2-v' + $this.config('agent_version') + '-' + $architecture + '.msi');
1093 $this.setProperty('system_architecture', $architecture);
1094 $this.setIcinga2AgentVersion($localData.DisplayVersion);
1095
1096 if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
1097 $this.setProperty('cert_dir', (Join-Path -Path $this.getProperty('config_dir') -ChildPath 'pki'));
1098 if ($this.getProperty('use_new_cert_dir')) {
1099 $this.setProperty('require_cert_migration', $TRUE);
1100 $this.info('You are downgrading from a newer Icinga 2 Version to a older one. This will require a certificate migration.');
1101 }
1102 } else {
1103 $this.setProperty('cert_dir', (Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs'));
1104 $this.setProperty('use_new_cert_dir', $TRUE);
1105 }
1106
1107 $this.info([string]::Format('Using Icinga version "{0}", setting certificate directory to "{1}"',
1108 $localData.DisplayVersion,
1109 $this.getProperty('cert_dir')
1110 )
1111 );
1112
1113 if ($localData.InstallLocation) {
1114 $this.info([string]::Format('Found Icinga 2 Agent version {0} installed at "{1}"', $localData.DisplayVersion, $localData.InstallLocation));
1115 return $TRUE;
1116 } else {
1117 $this.warn('Icinga 2 Agent does not seem to be installed on the system');
1118 # Set Default value for install dir
1119 $this.setProperty('cur_install_dir', (Join-Path $Env:ProgramFiles -ChildPath 'ICINGA2'));
1120 }
1121 return $FALSE;
1122 }
1123
1124 #
1125 # Ensure we are able to install a firewall rule for the Icinga 2 Agent,
1126 # allowing masters and satellites to connect to our local agent
1127 #
1128 $installer | Add-Member -membertype ScriptMethod -name 'installIcingaAgentFirewallRule' -value {
1129 if ($this.config('agent_add_firewall_rule') -eq $FALSE) {
1130 $this.warn('Icinga 2 Agent Firewall Rule will not be installed.');
1131 return;
1132 }
1133
1134 $this.info([string]::Format('Trying to install Icinga 2 Agent Firewall Rule for port {0}', $this.config('agent_listen_port')));
1135
1136 $result = $this.startProcess('netsh', $FALSE, 'advfirewall firewall show rule name="Icinga 2 Agent Inbound by PS-Module"');
1137 if ($result.Get_Item('exitcode') -eq 0) {
1138 # Firewall rule is already defined -> delete it and add it again
1139
1140 $this.info('Icinga 2 Agent Firewall Rule already installed. Trying to remove it to add it again...');
1141 $result = $this.startProcess('netsh', $TRUE, 'advfirewall firewall delete rule name="Icinga 2 Agent Inbound by PS-Module"');
1142
1143 if ($result.Get_Item('exitcode') -ne 0) {
1144 $this.error([string]::Format('Failed to remove Icinga 2 Agent Firewall rule before adding it again: {0}', $result.Get_Item('message')));
1145 return;
1146 } else {
1147 $this.info('Icinga 2 Agent Firewall Rule has been removed. Re-Adding now...');
1148 }
1149 }
1150
1151 [string]$binaryPath = Join-Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
1152 [string]$argument = 'advfirewall firewall add rule'
1153 $argument += [string]::Format(' dir=in action=allow program="{0}"', $binaryPath);
1154 $argument += ' name="Icinga 2 Agent Inbound by PS-Module"';
1155 $argument += ' description="Inbound Firewall Rule to allow Icinga 2 masters/satellites to connect to the Icinga 2 Agent installed on this system."';
1156 $argument += ' enable=yes';
1157 $argument += ' remoteip=any';
1158 $argument += ' localip=any';
1159 $argument += [string]::Format(' localport={0}', $this.config('agent_listen_port'));
1160 $argument += ' protocol=tcp';
1161
1162 $result = $this.startProcess('netsh', $FALSE, $argument);
1163 if ($result.Get_Item('exitcode') -ne 0) {
1164 # Firewall rule was not added -> print error
1165 $this.error([string]::Format('Failed to install Icinga 2 Agent Firewall: {0}', $result.Get_Item('message')));
1166 return;
1167 }
1168
1169 $this.info([string]::Format('Icinga 2 Agent Firewall Rule successfully installed for port {0}', $this.config('agent_listen_port')));
1170 }
1171
1172 #
1173 # Get the default path or our custom path for the NSClient++
1174 #
1175 $installer | Add-Member -membertype ScriptMethod -name 'getNSClientDefaultExecutablePath' -value {
1176
1177 if ($this.config('nsclient_directory')) {
1178 return (Join-Path -Path $this.config('nsclient_directory') -ChildPath 'nscp.exe');
1179 }
1180
1181 if (Test-Path ('C:\Program Files\NSClient++\nscp.exe')) {
1182 return 'C:\Program Files\NSClient++\nscp.exe';
1183 }
1184
1185 if (Test-Path ('C:\Program Files (x86)\NSClient++\nscp.exe')) {
1186 return 'C:\Program Files (x86)\NSClient++\nscp.exe';
1187 }
1188
1189 return '';
1190 }
1191
1192 #
1193 # In case have the Agent already installed
1194 # We might use a different installation path
1195 # for the Agent. This function will return
1196 # the correct, valid installation path
1197 #
1198 $installer | Add-Member -membertype ScriptMethod -name 'getInstallPath' -value {
1199 [string]$agentPath = '';
1200 if ($this.getProperty('cur_install_dir')) {
1201 $agentPath = $this.getProperty('cur_install_dir');
1202 }
1203 return $agentPath;
1204 }
1205
1206 #
1207 # In case we installed the agent freshly we
1208 # require to change configuration once we
1209 # would like to use the Director properly
1210 # This function will simply do a backup
1211 # of the icinga2.conf, ensuring we can
1212 # use them later again
1213 #
1214 $installer | Add-Member -membertype ScriptMethod -name 'backupDefaultConfig' -value {
1215 [string]$configFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icinga2.conf';
1216 [string]$configBackupFile = $configFile + 'director.bak';
1217
1218 # Check if a config and backup file already exists
1219 # Only procceed with backup of the current config if no backup was found
1220 if (Test-Path $configFile) {
1221 if (-Not (Test-Path $configBackupFile)) {
1222 Rename-Item $configFile $configBackupFile;
1223 $this.info('Icinga 2 configuration backup successfull');
1224 } else {
1225 $this.warn('Default icinga2.conf backup detected. Skipping backup');
1226 }
1227 }
1228 }
1229
1230 #
1231 # Allow us to restart the Icinga 2 Agent
1232 #
1233 $installer | Add-Member -membertype ScriptMethod -name 'cleanupAgentInstaller' -value {
1234 if (-Not ($this.isDownloadPathLocal())) {
1235 if ($this.getInstallerPath() -And (Test-Path $this.getInstallerPath())) {
1236 $this.info('Removing downloaded Icinga 2 Agent installer');
1237 Remove-Item $this.getInstallerPath() | Out-Null;
1238 }
1239 }
1240 }
1241
1242 #
1243 # Get Api directory if Icinga 2
1244 #
1245 $installer | Add-Member -membertype ScriptMethod -name 'getApiDirectory' -value {
1246 return $this.getProperty('api_dir');
1247 }
1248
1249 #
1250 # Should we remove the Api directory content
1251 # from the Agent? Can be defined by setting the
1252 # -RemoveApiDirectory argument of the function builder
1253 #
1254 $installer | Add-Member -membertype ScriptMethod -name 'shouldFlushIcingaApiDirectory' -value {
1255 return $this.config('flush_api_directory');
1256 }
1257
1258 #
1259 # Flush all content from the Icinga 2 Agent
1260 # Api directory, but keep the folder structure
1261 #
1262 $installer | Add-Member -membertype ScriptMethod -name 'flushIcingaApiDirectory' -value {
1263 if ((Test-Path $this.getApiDirectory()) -And $this.shouldFlushIcingaApiDirectory()) {
1264 $this.info([string]::Format('Flushing content of "{0}"', $this.getApiDirectory()));
1265 $this.stopIcingaService();
1266 [System.Object]$folder = New-Object -ComObject Scripting.FileSystemObject;
1267 $folder.DeleteFolder($this.getApiDirectory());
1268 $this.setProperty('require_restart', 'true');
1269 }
1270 }
1271
1272 #
1273 # Modify the user the Icinga services is running with
1274 #
1275 $installer | Add-Member -membertype ScriptMethod -name 'modifyIcingaServiceUser' -value {
1276
1277 # If no user is specified -> do nothing
1278 if ($this.config('icinga_service_user') -eq '') {
1279 return;
1280 }
1281
1282 [System.Object]$currentUser = Get-WMIObject win32_service -Filter "Name='icinga2'";
1283 [string]$credentials = $this.config('icinga_service_user');
1284 [string]$newUser = '';
1285 [string]$password = '';
1286
1287 if ($currentUser -eq $null) {
1288 $this.warn('Unable to modify Icinga service user: Service not found.');
1289 return;
1290 }
1291
1292 # Check if we defined user name and password (':' cannot appear within a username)
1293 # If so split them into seperate variables, otherwise simply use the string as user
1294 if ($credentials.Contains(':')) {
1295 [int]$delimeter = $credentials.IndexOf(':');
1296 $newUser = $credentials.Substring(0, $delimeter);
1297 $password = [string]::Format(' password= {0}', $credentials.Substring($delimeter + 1, $credentials.Length - 1 - $delimeter));
1298 } else {
1299 $newUser = $credentials;
1300 }
1301
1302 # If the user's are identical -> do nothing
1303 if ($currentUser.StartName -eq $newUser) {
1304 $this.info('Icinga user was not modified. Source and target service user are identical.');
1305 return;
1306 }
1307
1308 # Try to update the service name and return an error in case of a failure
1309 # In the error case we do not have to deal with cleanup, as no change was made anyway
1310 $this.info([string]::Format('Updating Icinga 2 service user to {0}', $newUser));
1311 $result = $this.startProcess('sc.exe', $TRUE, [string]::Format('config icinga2 obj= "{0}"{1}', $newUser, $password));
1312
1313 if ($result.Get_Item('exitcode') -ne 0) {
1314 $this.error($result.Get_Item('message'));
1315 return;
1316 }
1317
1318 # Just write the success message
1319 $this.info($result.Get_Item('message'));
1320
1321 # Try to restart the service
1322 $result = $this.restartService('icinga2');
1323
1324 # In case of an error try to rollback to the previous assigned user of the service
1325 # If this fails aswell, set the user to 'NT AUTHORITY\NetworkService' and restart the service to
1326 # ensure that the agent is atleast running and collecting some data.
1327 # Of course we throw plenty of errors to notify the user about problems
1328 if ($result.Get_Item('exitcode') -ne 0) {
1329 $this.error($result.Get_Item('message'));
1330 $this.info([string]::Format('Reseting user to previous working user {0}', $currentUser.StartName));
1331 $result = $this.startProcess('sc.exe', $TRUE, [string]::Format('config icinga2 obj= "{0}"{1}', $currentUser.StartName, $password));
1332 $result = $this.restartService('icinga2');
1333 if ($result.Get_Item('exitcode') -ne 0) {
1334 $this.error([string]::Format('Failed to reset Icinga 2 service user to the previous user "{0}". Setting user to "NT AUTHORITY\NetworkService" now to ensure the service integrity', $currentUser.StartName));
1335 $result = $this.startProcess('sc.exe', $TRUE, 'config icinga2 obj= "NT AUTHORITY\NetworkService" password=dummy');
1336 $this.info($result.Get_Item('message'));
1337 $result = $this.restartService('icinga2');
1338 if ($result.Get_Item('exitcode') -eq 0) {
1339 $this.info('Reseting Icinga 2 service user to "NT AUTHORITY\NetworkService" successfull.');
1340 return;
1341 } else {
1342 $this.error([string]::Format('Failed to rollback Icinga 2 service user to "NT AUTHORITY\NetworkService": {0}', $result.Get_Item('message')));
1343 return;
1344 }
1345 }
1346 }
1347
1348 $this.info('Icinga 2 service is running');
1349 }
1350
1351 #
1352 # Function to make restart of services easier
1353 #
1354 $installer | Add-Member -membertype ScriptMethod -name 'restartService' -value {
1355 param([string]$service);
1356
1357 $this.info([string]::Format('Restarting service {0}', $service));
1358
1359 # Stop the current service
1360 $result = $this.startProcess("sc.exe", $TRUE, "stop $service");
1361
1362 # Wait until the service is stopped
1363 $serviceResult = $this.waitForServiceToReachState($service, 'Stopped');
1364
1365 # Start the service again
1366 $result = $this.startProcess("sc.exe", $TRUE, "start $service");
1367
1368 # Wait until the service is started
1369 if ($this.waitForServiceToReachState($service, 'Running') -eq $FALSE) {
1370 $result.Set_Item('message', [string]::Format('Failed to restart service {0}.', $service));
1371 $result.Set_Item('exitcode', '1');
1372 }
1373
1374 return $result;
1375 }
1376
1377 #
1378 # Function to stop the Icinga 2 service
1379 #
1380 $installer | Add-Member -membertype ScriptMethod -name 'stopIcingaService' -value {
1381 # Stop the Icinga 2 Service
1382 $this.info('Stopping the Icinga 2 Service...')
1383 $result = $this.startProcess("sc.exe", $TRUE, "stop icinga2");
1384
1385 # Wait until the service is stopped
1386 $serviceResult = $this.waitForServiceToReachState('icinga2', 'Stopped');
1387 $this.info('Icinga 2 service has been stopped.')
1388 }
1389
1390 #
1391 # This function will wait for a specific service until it reaches
1392 # the defined state. Will break after 20 seconds with an error message
1393 #
1394 $installer | Add-Member -membertype ScriptMethod -name 'waitForServiceToReachState' -value {
1395 param([string]$service, [string]$state);
1396
1397 [int]$counter = 0;
1398
1399 # Wait until the service reached the desired state
1400 while ($TRUE) {
1401
1402 # Get the current state of the service
1403 $serviceState = (Get-WMIObject win32_service -Filter "Name='$service'").State;
1404 if ($serviceState -eq $state) {
1405 break;
1406 }
1407
1408 # Sleep a little to prevent crushing the CPU
1409 Start-Sleep -Milliseconds 100;
1410 $counter += 1;
1411
1412 # After 20 seconds break with an error. It look's like the service does not respond
1413 if ($counter -gt 200) {
1414 $this.error([string]::Format('Timeout reached while waiting for "{0}" to reach state "{1}". Service is not responding.', $service, $state));
1415 return $FALSE;
1416 }
1417 }
1418
1419 # Wait one second and check the status again to ensure it remains within it's state
1420 Start-Sleep -Seconds 1;
1421
1422 if ($state -ne (Get-WMIObject win32_service -Filter "Name='$service'").State) {
1423 return $FALSE;
1424 }
1425
1426 return $TRUE;
1427 }
1428
1429 #
1430 # Function to start processes and wait for their exit
1431 # Will return a dictionary with results (message, error, exitcode)
1432 #
1433 $installer | Add-Member -membertype ScriptMethod -name 'startProcess' -value {
1434 param([string]$executable, [bool]$flushNewLines, [string]$arguments);
1435
1436 $processData = New-Object System.Diagnostics.ProcessStartInfo;
1437 $processData.FileName = $executable;
1438 $processData.RedirectStandardError = $true;
1439 $processData.RedirectStandardOutput = $true;
1440 $processData.UseShellExecute = $false;
1441 $processData.Arguments = $arguments;
1442 $process = New-Object System.Diagnostics.Process;
1443 $process.StartInfo = $processData;
1444 $process.Start() | Out-Null;
1445 $stdout = $process.StandardOutput.ReadToEnd();
1446 $stderr = $process.StandardError.ReadToEnd();
1447 $process.WaitForExit();
1448
1449 if ($flushNewLines) {
1450 $stdout = $stdout.Replace("`n", '').Replace("`r", '');
1451 $stderr = $stderr.Replace("`n", '').Replace("`r", '');
1452 } else {
1453 if ($stdout.Contains("`n")) {
1454 $stdout = $stdout.Substring(0, $stdout.LastIndexOf("`n"));
1455 }
1456 }
1457
1458 $result = @{};
1459 $result.Add('message', $stdout);
1460 $result.Add('error', $stderr);
1461 $result.Add('exitcode', $process.ExitCode);
1462
1463 return $result;
1464 }
1465
1466 #
1467 # Restart the Icinga 2 service and get the
1468 # result if the restart failed or everything
1469 # worked as expected
1470 #
1471 $installer | Add-Member -membertype ScriptMethod -name 'restartAgent' -value {
1472 $result = $this.restartService('icinga2');
1473
1474 if ($result.Get_Item('exitcode') -eq 0) {
1475 $this.info('Icinga 2 Agent successfully restarted.');
1476 $this.setProperty('require_restart', '');
1477 } else {
1478 $this.error($result.Get_Item('message'));
1479 }
1480 }
1481
1482 #
1483 # This function will determine if and how we create the
1484 # API-Listener configuration
1485 #
1486 $installer | Add-Member -membertype ScriptMethod -name 'getApiListenerConfiguration' -value {
1487 if (-Not $this.hasCertificates() -And -Not $this.getProperty('certs_created')) {
1488 $this.warn('Configuring Icinga 2 Agent without ApiListener, as certificates have not been generated.');
1489 return [string]::Format('{0}/* ApiListener has not been configured, as certificates have not been generated. */', "`n`n");
1490 }
1491
1492 [string]$apiListenerConfig = '';
1493 [string]$certificateConfig = '';
1494 # Icinga 2 Agent Versions below 2.8.0 will require cert_path, key_path and ca_path
1495 if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
1496 $certificateConfig = '
1497 cert_path = SysconfDir + "/icinga2/pki/' + $this.getProperty('local_hostname') + '.crt"
1498 key_path = SysconfDir + "/icinga2/pki/' + $this.getProperty('local_hostname') + '.key"
1499 ca_path = SysconfDir + "/icinga2/pki/ca.crt"';
1500 }
1501
1502 $apiListenerConfig = '
1503object ApiListener "api" {' + $certificateConfig + '
1504 accept_commands = true
1505 accept_config = ' + $this.convertBoolToString($this.config('accept_config')) + '
1506 bind_host = "::"
1507 bind_port = ' + [int]$this.config('agent_listen_port') + '
1508}';
1509
1510 return $apiListenerConfig;
1511 }
1512
1513 #
1514 # Generate the new configuration for Icinga 2
1515 #
1516 $installer | Add-Member -membertype ScriptMethod -name 'generateIcingaConfiguration' -value {
1517 if ($this.getProperty('generate_config') -eq 'true') {
1518
1519 $this.checkConfigInputParametersAndThrowException();
1520
1521 [string]$icingaCurrentConfig = '';
1522 if (Test-Path $this.getIcingaConfigFile()) {
1523 $icingaCurrentConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
1524 }
1525
1526 [string]$icingaNewConfig =
1527'/**
1528 * Icinga 2 Config - Proposed by Icinga 2 PowerShell Module
1529 */
1530
1531/* Define our includes to run the agent properly. */
1532include "constants.conf"
1533include <itl>
1534include <plugins>
1535include <nscp>
1536include <windows-plugins>
1537
1538/* Required for Icinga 2.8.0 and above */
1539const NodeName = "' + $this.getProperty('local_hostname') + '"
1540
1541/* Define our block required to enable or disable Icinga 2 debug log
1542 * Enable or disable it by using the PowerShell Module with
1543 * argument -IcingaEnableDebugLog or by switching
1544 * PowerShellIcinga2EnableDebug to true or false manually.
1545 * true: Debug log is active
1546 * false: Debug log is deactivated
1547 * IMPORTANT: ";" after true or false has to remain to allow the
1548 * PowerShell Module to switch this feature on or off.
1549 */
1550const PowerShellIcinga2EnableDebug = false;
1551if (PowerShellIcinga2EnableDebug) {
1552 object FileLogger "debug-file" {
1553 severity = "debug"
1554 path = LocalStateDir + "/log/icinga2/debug.log"
1555 }
1556}
1557
1558/* Try to define a constant for our NSClient++ installation
1559 * IMPORTANT: If the NSClient++ is installed newly to the system, the
1560 * Icinga Service has to be restarted in order to set this variable
1561 * correctly. If the NSClient++ is installed over the PowerShell Module,
1562 * the Icinga 2 Service is restarted automaticly.
1563 */
1564if (!globals.contains("NscpPath")) {
1565 NscpPath = dirname(msi_get_component_path("{5C45463A-4AE9-4325-96DB-6E239C034F93}"))
1566}
1567
1568/* Enable our default main logger feature to write log output. */
1569object FileLogger "main-log" {
1570 severity = "information"
1571 path = LocalStateDir + "/log/icinga2/icinga2.log"
1572}
1573
1574/* All informations required to correctly connect to our parent Icinga 2 nodes. */
1575object Endpoint "' + $this.getProperty('local_hostname') + '" {}
1576' + $this.getProperty('endpoint_objects') + '
1577/* Define the zone and its containing endpoints we should communicate with. */
1578object Zone "' + $this.config('parent_zone') + '" {
1579 endpoints = [ ' + $this.getProperty('endpoint_nodes') +' ]
1580}
1581
1582/* All of our global zones, check commands and other configuration are synced into.
1583 * Director global zone must be defined in case the Icinga Director is beeing used.
1584 * Default value for this is "director-global".
1585 * All additional zones can be configured with -GlobalZones argument.
1586 * IMPORTANT: If -GlobalZones argument is used, the Icinga Director global zones has
1587 * to be defined as well within the argument array.
1588 */
1589' + $this.getProperty('global_zones') + '
1590/* Define a zone for our current agent and set our parent zone for proper communication. */
1591object Zone "' + $this.getProperty('local_hostname') + '" {
1592 parent = "' + $this.config('parent_zone') + '"
1593 endpoints = [ "' + $this.getProperty('local_hostname') + '" ]
1594}
1595
1596/* Configure all settings we require for our API listener to properly work.
1597 * This will include the certificates, if we accept configurations which
1598 * can be changed with argument -AcceptConfig and the bind informations.
1599 * The bind_port can be modified with argument -AgentListenPort.
1600 */' + $this.getApiListenerConfiguration();
1601
1602 $this.setProperty('new_icinga_config', $icingaNewConfig);
1603 $this.setProperty('old_icinga_config', $icingaCurrentConfig);
1604 }
1605 }
1606
1607 #
1608 # Generate a hash for old and new config
1609 # and determine if the configuration has changed
1610 #
1611 $installer | Add-Member -membertype ScriptMethod -name 'hasConfigChanged' -value {
1612
1613 if ($this.getProperty('generate_config') -eq 'false') {
1614 return $FALSE;
1615 }
1616 if (-Not $this.getProperty('new_icinga_config')) {
1617 throw 'New Icinga 2 configuration not generated. Please call "generateIcingaConfiguration" before.';
1618 }
1619
1620 [string]$oldConfigHash = $this.getHashFromString($this.getProperty('old_icinga_config'));
1621 [string]$newConfigHash = $this.getHashFromString($this.getProperty('new_icinga_config'));
1622
1623 $this.debug([string]::Format('Old Config Hash: "{0}" New Hash: "{1}"', $oldConfigHash, $newConfigHash));
1624
1625 if ($oldConfigHash -eq $newConfigHash) {
1626 return $FALSE;
1627 }
1628
1629 return $TRUE;
1630 }
1631
1632 #
1633 # Generate a SHA1 Hash from a provided string
1634 #
1635 $installer | Add-Member -membertype ScriptMethod -name 'getHashFromString' -value {
1636 param([string]$text);
1637 [System.Object]$algorithm = New-Object System.Security.Cryptography.SHA1CryptoServiceProvider;
1638 $hash = [System.Text.Encoding]::UTF8.GetBytes($text);
1639 $hashInBytes = $algorithm.ComputeHash($hash);
1640 [string]$result = '';
1641 foreach($byte in $hashInBytes) {
1642 $result += $byte.ToString();
1643 }
1644 return $result;
1645 }
1646
1647 #
1648 # Return the path to the Icinga 2 config file
1649 #
1650 $installer | Add-Member -membertype ScriptMethod -name 'getIcingaConfigFile' -value {
1651 return (Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icinga2.conf');
1652 }
1653
1654 #
1655 # Create Icinga 2 configuration file based
1656 # on Director settings
1657 #
1658 $installer | Add-Member -membertype ScriptMethod -name 'writeConfig' -value {
1659 param([string]$configData);
1660
1661 if (-Not (Test-Path $this.getProperty('config_dir'))) {
1662 $this.warn('Unable to write Icinga 2 configuration. The required directory was not found. Possibly the Icinga 2 Agent is not installed.');
1663 return;
1664 }
1665
1666 # Write new configuration to file
1667 $this.info([string]::Format('Writing icinga2.conf to "{0}"', $this.getProperty('config_dir')));
1668 [System.IO.File]::WriteAllText($this.getIcingaConfigFile(), $configData);
1669 $this.setProperty('require_restart', 'true');
1670 }
1671
1672 #
1673 # Write old coniguration again
1674 # just in case we received errors
1675 #
1676 $installer | Add-Member -membertype ScriptMethod -name 'rollbackConfig' -value {
1677 # Write new configuration to file
1678 $this.info([string]::Format('Rolling back previous icinga2.conf to "{0}"', $this.getProperty('config_dir')));
1679 [System.IO.File]::WriteAllText($this.getIcingaConfigFile(), $this.getProperty('old_icinga_config'));
1680 $this.setProperty('require_restart', 'true');
1681 }
1682
1683 #
1684 # Provide a result of an operation (string) and
1685 # the intended match value. In case every was
1686 # ok, the function will return an info message
1687 # with the result. Otherwise it will thrown an
1688 # exception
1689 #
1690 $installer | Add-Member -membertype ScriptMethod -name 'printResultOkOrException' -value {
1691 param([string]$result, [string]$expected);
1692 if ($result -And $expected) {
1693 if (-Not ($result -Like $expected)) {
1694 throw $result;
1695 } else {
1696 $this.info($result);
1697 }
1698 } elseif ($result) {
1699 $this.info($result);
1700 }
1701 }
1702
1703 #
1704 # Create Host-Certificates for Icinga 2
1705 #
1706 $installer | Add-Member -membertype ScriptMethod -name 'createHostCertificates' -value {
1707 param([string]$hostname, [string]$certDir);
1708
1709 $this.info('Generating Host certificates required by Icinga 2');
1710 [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
1711 $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki new-cert --cn {0} --key {1}{0}.key --cert {1}{0}.crt',
1712 $hostname,
1713 $certDir
1714 )
1715 );
1716
1717 if ($result.Get_Item('exitcode') -ne 0) {
1718 throw $result.Get_Item('message');
1719 }
1720 $this.info($result.Get_Item('message'));
1721 }
1722
1723 #
1724 # Fix certificate naming for upper / lower case changes
1725 #
1726 $installer | Add-Member -membertype ScriptMethod -name 'fixCertificateNames' -value {
1727 param([string]$hostname, [string]$certDir);
1728 # Rename the certificates to apply possible upper / lower case naming changes
1729 # which is not done by Windows by default
1730 Move-Item (Join-Path -Path $certDir -ChildPath ($hostname + '.key')) (Join-Path -Path $certDir -ChildPath ($hostname + '.key'))
1731 Move-Item (Join-Path -Path $certDir -ChildPath ($hostname + '.crt')) (Join-Path -Path $certDir -ChildPath ($hostname + '.crt'))
1732 }
1733
1734 #
1735 # Generate the Icinga 2 SSL certificate to ensure the communication between the
1736 # Agent and the Master can be established in first place
1737 #
1738 $installer | Add-Member -membertype ScriptMethod -name 'generateCertificates' -value {
1739
1740 [string]$icingaCertDir = Join-Path $this.getProperty('cert_dir') -ChildPath '\';
1741 [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
1742 [string]$agentName = $this.getProperty('local_hostname');
1743
1744 if (-Not (Test-Path $icingaBinary)) {
1745 $this.warn('Unable to generate Icinga 2 certificates. Icinga 2 executable not found. It looks like the Icinga 2 Agent is not installed.');
1746 return;
1747 }
1748
1749 if (-Not $this.getProperty('local_hostname')) {
1750 $this.info('Skipping function for generating certificates, as hostname is not specified within the module.');
1751 return;
1752 }
1753
1754 # Handling for Icinga 2.8.0 and above: CA-Proxy support
1755 if ($this.config('caproxy')) {
1756 if (-Not $this.validateVersions('2.8.0', $this.getProperty('icinga2_agent_version'))) {
1757 throw 'The argument "-CAProxy" is only supported by Icinga Version 2.8.0 and above.';
1758 return;
1759 }
1760
1761 if (-Not $this.config('ca_certificate_path')) {
1762 throw 'You will require to specify a source path of your CA certificate with -CACertificatePath in order to use CA proxy certificate generation.';
1763 }
1764
1765 # Generate the certificate
1766 [string]$caDestPath = (Join-Path $icingaCertDir -ChildPath '\ca.crt');
1767 $this.createHostCertificates($agentName, $icingaCertDir);
1768 $this.fixCertificateNames($agentName, $icingaCertDir);
1769 $this.setProperty('require_restart', 'true');
1770 $this.info('Your host certificate has been generated. Please review the request on your Icinga CA with "icinga2 ca list" and sign it with "icinga2 ca sign <request_id>".');
1771 $this.info([string]::Format('Trying to copy your specified CA certificate "{0}" to "{1}".',
1772 $this.config('ca_certificate_path'),
1773 $caDestPath
1774 ));
1775 if (-Not (Test-Path $this.config('ca_certificate_path'))) {
1776 throw [string]::Format('Failed to copy your CA certificate from "{0}" to "{1}". Your source destination does not exist.',
1777 $this.config('ca_certificate_path'),
1778 $caDestPath
1779 );
1780 return;
1781 }
1782 Copy-Item $this.config('ca_certificate_path') $caDestPath;
1783 $this.setProperty('certs_created', $TRUE);
1784 return;
1785 }
1786
1787 if ($this.config('ca_server') -And $this.getProperty('icinga_ticket')) {
1788 # Generate the certificate
1789 $this.createHostCertificates($agentName, $icingaCertDir);
1790
1791 # Save Certificate
1792 $this.info("Storing Icinga 2 certificates");
1793 $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki save-cert --key {0}{1}.key --trustedcert {0}trusted-master.crt --host {2}',
1794 $icingaCertDir,
1795 $agentName,
1796 $this.config('ca_server')
1797 )
1798 );
1799 if ($result.Get_Item('exitcode') -ne 0) {
1800 throw $result.Get_Item('message');
1801 }
1802 $this.info($result.Get_Item('message'));
1803
1804 # Validate if set against a given fingerprint for the CA
1805 if (-Not $this.validateCertificate([string]::Format('{0}trusted-master.crt', $icingaCertDir))) {
1806 throw 'Failed to validate against CA authority';
1807 }
1808
1809 # Request certificate
1810 $this.info("Requesting Icinga 2 certificates");
1811 $result = $this.startProcess($icingaBinary, $FALSE, [string]::Format('pki request --host {0} --port {1} --ticket {2} --key {3}{4}.key --cert {3}{4}.crt --trustedcert {3}trusted-master.crt --ca {3}ca.crt',
1812 $this.config('ca_server'),
1813 $this.config('ca_port'),
1814 $this.getProperty('icinga_ticket'),
1815 $icingaCertDir,
1816 $agentName
1817 )
1818 );
1819 if ($result.Get_Item('exitcode') -ne 0) {
1820 if ($this.getProperty('agent_name_change')) {
1821 $this.exception('You have changed the naming of the Agent (upper / lower case) and therefor your certificates are no longer valid. Certificate generation failed because of a possible wrong ticket. Please ensure to set the "hostname" within the Icinga 2 configuration correctly and re-run this script.');
1822 }
1823 throw $result.Get_Item('message');
1824 }
1825 $this.info($result.Get_Item('message'));
1826 $this.fixCertificateNames($agentName, $icingaCertDir);
1827 $this.setProperty('require_restart', 'true');
1828 $this.setProperty('certs_created', $TRUE);
1829 } else {
1830 $this.info('Skipping certificate generation. One or more of the following arguments is not set: -CAServer <server> -Ticket <ticket>');
1831 }
1832 }
1833
1834 #
1835 # Validate against a given fingerprint if we are connected to the correct CA
1836 #
1837 $installer | Add-Member -membertype ScriptMethod -name 'validateCertificate' -value {
1838 param([string] $certificate);
1839
1840 [System.Object]$certFingerprint = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2;
1841 $certFingerprint.Import($certificate);
1842 $this.info([string]::Format('Certificate fingerprint: "{0}"', $certFingerprint.Thumbprint));
1843
1844 if ($this.config('ca_fingerprint')) {
1845 if (-Not ($this.config('ca_fingerprint') -eq $certFingerprint.Thumbprint)) {
1846 $this.error([string]::Format('CA fingerprint does not match! Expected: "{0}", given: "{1}"',
1847 $certFingerprint.Thumbprint,
1848 $this.config('ca_fingerprint')
1849 )
1850 );
1851 return $FALSE;
1852 } else {
1853 $this.info('CA fingerprint validation successfull');
1854 return $TRUE;
1855 }
1856 }
1857
1858 $this.warn('CA fingerprint validation disabled');
1859 return $TRUE;
1860 }
1861
1862 #
1863 # In case we migrate from an Icinga 2 Version with the new certificate path to
1864 # a version with the old one, we require to migrate the certificates
1865 #
1866 $installer | Add-Member -membertype ScriptMethod -name 'migrateCertificates' -value {
1867 if (-Not $this.getProperty('require_cert_migration')) {
1868 return;
1869 }
1870
1871 [string]$agentName = $this.getProperty('local_hostname');
1872
1873 [string]$caPath = Join-Path -Path $Env:ProgramData -ChildPath 'icinga2\var\lib\icinga2\certs\ca.crt';
1874 [string]$newCA = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'pki\ca.crt';
1875 [string]$certPath = Join-Path -Path $Env:ProgramData -ChildPath ([string]::Format('icinga2\var\lib\icinga2\certs\{0}.crt', $agentName));
1876 [string]$newCertPath = Join-Path -Path $this.getProperty('config_dir') -ChildPath ([string]::Format('pki\{0}.crt', $agentName));
1877 [string]$keyPath = Join-Path -Path $Env:ProgramData -ChildPath ([string]::Format('icinga2\var\lib\icinga2\certs\{0}.key', $agentName));
1878 [string]$newKeyPath = Join-Path -Path $this.getProperty('config_dir') -ChildPath ([string]::Format('pki\{0}.key', $agentName));
1879
1880 if (Test-Path $caPath) {
1881 Copy-Item $caPath $newCA;
1882 $this.info([string]::Format('Migrating ca.crt from "{0}" to "{1}".', $caPath, $newCA));
1883 }
1884
1885 if (Test-Path $certPath) {
1886 Copy-Item $certPath $newCertPath;
1887 $this.info([string]::Format('Migrating {0}.crt from "{1}" to "{2}".', $agentName, $certPath, $newCertPath));
1888 }
1889
1890 if (Test-Path $keyPath) {
1891 Copy-Item $keyPath $newKeyPath;
1892 $this.info([string]::Format('Migrating {0}.crt from "{1}" to "{2}".', $agentName, $keyPath, $newKeyPath));
1893 }
1894 }
1895
1896 #
1897 # Check the Icinga install directory and determine
1898 # if the certificates are both available for the
1899 # Agent. If not, return FALSE
1900 #
1901 $installer | Add-Member -membertype ScriptMethod -name 'hasCertificates' -value {
1902 [string]$icingaCertDir = Join-Path -Path $this.getProperty('cert_dir') -ChildPath '\';
1903 [string]$agentName = $this.getProperty('local_hostname');
1904 [bool]$filesExist = $FALSE;
1905 # First check if the files in generell exist
1906 if (
1907 ((Test-Path ((Join-Path -Path $icingaCertDir -ChildPath $agentName) + '.key'))) `
1908 -And (Test-Path ((Join-Path -Path $icingaCertDir -ChildPath $agentName) + '.crt')) `
1909 -And (Test-Path (Join-Path -Path $icingaCertDir -ChildPath 'ca.crt'))
1910 ) {
1911 $filesExist = $TRUE;
1912 }
1913
1914 # In case they do, check if the characters (upper / lowercase) are matching as well
1915 if ($filesExist -eq $TRUE) {
1916
1917 [string]$hostCRT = [string]::Format('{0}.crt', $agentName);
1918 [string]$hostKEY = [string]::Format('{0}.key', $agentName);
1919
1920 # Get all files inside your certificate directory
1921 $certificates = Get-ChildItem -Path $icingaCertDir;
1922 # Now loop each file and match their name with our hostname
1923 foreach ($cert in $certificates) {
1924 if ($cert.Name.toLower() -eq $hostCRT.toLower() -Or $cert.Name.toLower() -eq $hostKEY.toLower()) {
1925 $file = $cert.Name.Replace('.key', '').Replace('.crt', '');
1926 if (-Not ($file -clike $agentName)) {
1927 $this.warn([string]::Format('Certificate file {0} is not matching the hostname {1}. Certificate generation is required.', $cert.Name, $agentName));
1928 $this.setProperty('agent_name_change', $true);
1929 return $FALSE;
1930 }
1931 }
1932 }
1933 }
1934
1935 return $filesExist;
1936 }
1937
1938 #
1939 # Have we passed an argument to force
1940 # the creation of the certificates?
1941 #
1942 $installer | Add-Member -membertype ScriptMethod -name 'forceCertificateGeneration' -value {
1943 return $this.config('force_cert');
1944 }
1945
1946 #
1947 # Is the current Agent the version
1948 # we would like to install?
1949 #
1950 $installer | Add-Member -membertype ScriptMethod -name 'isAgentUpToDate' -value {
1951 if ($this.canInstallAgent() -And $this.getProperty('agent_version') -eq $this.config('agent_version')) {
1952 return $TRUE;
1953 }
1954
1955 return $FALSE;
1956 }
1957
1958 #
1959 # Print a message telling us the installed
1960 # and intended version of the Agent
1961 #
1962 $installer | Add-Member -membertype ScriptMethod -name 'printAgentUpdateMessage' -value {
1963 $this.info([string]::Format('Current Icinga 2 Agent Version ({0}) is not matching intended version ({1}). Downloading new version...',
1964 $this.getProperty('agent_version'),
1965 $this.config('agent_version')
1966 )
1967 );
1968 }
1969
1970 #
1971 # Do we allow Agent updates / downgrades?
1972 #
1973 $installer | Add-Member -membertype ScriptMethod -name 'allowAgentUpdates' -value {
1974 return $this.config('allow_updates');
1975 }
1976
1977 #
1978 # Have we specified a version to install the Agent?
1979 #
1980 $installer | Add-Member -membertype ScriptMethod -name 'canInstallAgent' -value {
1981 if ($this.config('download_url') -And $this.config('agent_version')) {
1982 return $TRUE;
1983 }
1984
1985 if (-Not $this.config('download_url') -And -Not $this.config('agent_version')) {
1986 $this.warn('Icinga 2 Agent will not be installed. Arguments -DownloadUrl and -InstallAgentVersion both not defined.');
1987 return $FALSE;
1988 }
1989
1990 if (-Not $this.config('agent_version')) {
1991 $this.warn('Icinga 2 Agent will not be installed. Argument -InstallAgentVersion is not defined.');
1992 return $FALSE;
1993 }
1994
1995 if (-Not $this.config('download_url')) {
1996 $this.warn('Icinga 2 Agent will not be installed. Argument -DownloadUrl is not defined.');
1997 return $FALSE;
1998 }
1999
2000 return $FALSE;
2001 }
2002
2003 #
2004 # Check if all required arguments for writing a valid
2005 # configuration are set
2006 #
2007 $installer | Add-Member -membertype ScriptMethod -name 'checkConfigInputParametersAndThrowException' -value {
2008 if (-Not $this.getProperty('local_hostname')) {
2009 throw 'Argument -AgentName <name> required for config generation.';
2010 }
2011 if (-Not $this.config('parent_zone')) {
2012 throw 'Argument -ParentZone <name> required for config generation.';
2013 }
2014 if (-Not $this.getProperty('endpoint_nodes') -Or -Not $this.getProperty('endpoint_objects')) {
2015 throw 'Argument -Endpoints <name> requires atleast one defined endpoint.';
2016 }
2017 }
2018
2019 #
2020 # Execute a check with Icinga2 daemon -C
2021 # to ensure the configuration is valid
2022 #
2023 $installer | Add-Member -membertype ScriptMethod -name 'isIcingaConfigValid' -value {
2024 param([bool] $checkInternal = $TRUE);
2025 if (-Not $this.config('parent_zone') -And $checkInternal) {
2026 throw 'Parent Zone not defined. Please specify it with -ParentZone <name>';
2027 }
2028 $icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
2029
2030 if (Test-Path $icingaBinary) {
2031 $result = $this.startProcess($icingaBinary, $FALSE, 'daemon -C');
2032 if ($result.Get_Item('exitcode') -ne 0) {
2033 $this.error($result.Get_Item('message'));
2034 return $FALSE;
2035 }
2036 } else {
2037 $this.warn('Icinga 2 config validation not possible. Icinga 2 executable not found. Possibly the Agent is not installed.');
2038 }
2039 return $TRUE;
2040 }
2041
2042 #
2043 # Returns true or false, depending
2044 # if any changes were made requiring
2045 # the Icinga 2 Agent to become restarted
2046 #
2047 $installer | Add-Member -membertype ScriptMethod -name 'madeChanges' -value {
2048 return $this.getProperty('require_restart');
2049 }
2050
2051 #
2052 # Apply possible configuration changes to
2053 # our Icinga 2 Agent
2054 #
2055 $installer | Add-Member -membertype ScriptMethod -name 'applyPossibleConfigChanges' -value {
2056 if ($this.hasConfigChanged() -And $this.getProperty('generate_config') -eq 'true') {
2057 $this.backupDefaultConfig();
2058 $this.writeConfig($this.getProperty('new_icinga_config'));
2059
2060 # Check if the config is valid and rollback otherwise
2061 if (-Not $this.isIcingaConfigValid()) {
2062 $this.error('Icinga 2 config validation failed. Rolling back to previous version.');
2063 if (-Not $this.hasCertificates()) {
2064 $this.error('Icinga 2 certificates not found. Please generate the certificates over this module or add them manually.');
2065 }
2066 $this.rollbackConfig();
2067 if ($this.isIcingaConfigValid($FALSE)) {
2068 $this.info('Rollback of Icinga 2 configuration successfull.');
2069 } else {
2070 throw 'Icinga 2 config rollback failed. Please check the icinga2.log';
2071 }
2072 } else {
2073 $this.info('Icinga 2 configuration check successfull.');
2074 }
2075 } else {
2076 # Throw an exception in case we use a parent zone which is a global zone
2077 foreach ($zone in $this.config('global_zones')) {
2078 if ($zone -eq $this.config('parent_zone')) {
2079 $this.exception([string]::Format('The zone specified for the Icinga 2 Agent to connect to is set to "{0}". This is a global zone which cannot be used. Please review either your arguments used for this module or the Host-Template within the Icinga Director to use the correct zone for this Agent.', $this.config('parent_zone')));
2080 }
2081 }
2082 # In case no parent endpoints are configured, print a warning as we can't write valid Icinga 2 configuration
2083 if (-Not $this.config('parent_endpoints')) {
2084 $this.warn('No parent endpoints have been defined within the module call. Either specify them by using the "-ParentEndpoints" argument or ensure you configured your Icinga Director properly in case you are using the Self-Service API. Icinga2.conf has not been generated.');
2085 }
2086 $this.info('icinga2.conf did not change or required parameters not set. Nothing to do');
2087 }
2088 }
2089
2090 #
2091 # Enable or disable the Icinga 2 debug log
2092 #
2093 $installer | Add-Member -membertype ScriptMethod -name 'switchIcingaDebugLog' -value {
2094 # In case the config is not valid -> do nothing
2095 if (-Not $this.isIcingaConfigValid($FALSE)) {
2096 throw 'Unable to process Icinga 2 debug configuration. The icinga2.conf is corrupt! Please check the icinga2.log';
2097 }
2098
2099 # If there is no config file defined -> do nothing
2100 if (-Not (Test-Path $this.getIcingaConfigFile())) {
2101 return;
2102 }
2103
2104 [string]$icingaCurrentConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
2105 [string]$newIcingaConfig = '';
2106
2107 if ($this.config('icinga_enable_debug_log')) {
2108 $this.info('Trying to enable debug log for Icinga 2...');
2109 if ($icingaCurrentConfig.Contains('const PowerShellIcinga2EnableDebug = false;')) {
2110 $newIcingaConfig = $icingaCurrentConfig.Replace('const PowerShellIcinga2EnableDebug = false;', 'const PowerShellIcinga2EnableDebug = true;');
2111 $this.info('Icinga 2 debug log has been enabled');
2112 } else {
2113 $this.info('Icinga 2 debug log is already enabled or configuration not found');
2114 }
2115 } else {
2116 $this.info('Trying to disable debug log for Icinga 2...');
2117 if ($icingaCurrentConfig.Contains('const PowerShellIcinga2EnableDebug = true;')) {
2118 $newIcingaConfig = $icingaCurrentConfig.Replace('const PowerShellIcinga2EnableDebug = true;', 'const PowerShellIcinga2EnableDebug = false;');
2119 $this.info('Icinga 2 debug log has been disabled');
2120 } else {
2121 $this.info('Icinga 2 debug log is not enabled or configuration not found');
2122 }
2123 }
2124
2125 # In case we made a modification to the configuration -> write it
2126 if ($newIcingaConfig -ne '') {
2127 $this.writeConfig($newIcingaConfig);
2128 # Validate the config if it is valid
2129 if (-Not $this.isIcingaConfigValid($FALSE)) {
2130 # if not write the old configuration again
2131 $this.writeConfig($icingaCurrentConfig);
2132 if (-Not $this.isIcingaConfigValid($FALSE)) {
2133 throw 'Critical exception: Something went wrong while processing debug configuration. The Icinga 2 config is corrupt! Please check the icinga2.log';
2134 }
2135 }
2136 }
2137 }
2138
2139 #
2140 # Ensure we get the hostname or FQDN
2141 # from the PowerShell to make things more
2142 # easier
2143 #
2144 $installer | Add-Member -membertype ScriptMethod -name 'fetchHostnameOrFQDN' -value {
2145 if ($this.config('fetch_agent_fqdn') -And (Get-WmiObject win32_computersystem).Domain) {
2146 [string]$hostname = [string]::Format('{0}.{1}',
2147 (Get-WmiObject win32_computersystem).DNSHostName,
2148 (Get-WmiObject win32_computersystem).Domain
2149 );
2150 $this.setProperty('local_hostname', $hostname);
2151 $this.info([string]::Format('Setting internal Agent Name to "{0}"', $this.getProperty('local_hostname')));
2152 } elseif ($this.config('fetch_agent_name')) {
2153 [string]$hostname = (Get-WmiObject win32_computersystem).DNSHostName;
2154 $this.setProperty('local_hostname', $hostname);
2155 $this.info([string]::Format('Setting internal Agent Name to "{0}"', $this.getProperty('local_hostname')));
2156 }
2157
2158 # Add additional variables to our config for more user-friendly usage
2159 [string]$host_fqdn = [string]::Format('{0}.{1}',
2160 (Get-WmiObject win32_computersystem).DNSHostName,
2161 (Get-WmiObject win32_computersystem).Domain
2162 );
2163 [string]$hostname = (Get-WmiObject win32_computersystem).DNSHostName;
2164
2165 $this.setProperty('fqdn', $host_fqdn);
2166 $this.setProperty('hostname', $hostname);
2167
2168 if (-Not $this.getProperty('local_hostname')) {
2169 $this.warn('You have not specified an Agent Name or turned on to auto fetch this information.');
2170 }
2171 }
2172
2173 #
2174 # Retreive the current IP-Address of the Host
2175 #
2176 $installer | Add-Member -membertype ScriptMethod -name 'fetchHostIPAddress' -value {
2177
2178 # First try to lookup the IP by the FQDN
2179 if ($this.doLookupIPAddressesForHostname($this.getProperty('fqdn'))) {
2180 return;
2181 }
2182
2183 # Now take a look for the given hostname
2184 if ($this.doLookupIPAddressesForHostname($this.getProperty('hostname'))) {
2185 return;
2186 }
2187
2188 # If still nothing is found, look on the entire host
2189 if ($this.doLookupIPAddressesForHostname("")) {
2190 return;
2191 }
2192
2193 $this.exception('Failed to lookup any IP-Address for this host');
2194 }
2195
2196 #
2197 # This function will try to locate the IPv4 address used
2198 # for communicating with the network
2199 #
2200 $installer | Add-Member -membertype ScriptMethod -name 'lookupPrimaryIPv4Address' -value {
2201 # First execute nslookup for your FQDN and hostname to check if this
2202 # host is registered and receive it's IP address
2203 [System.Collections.Hashtable]$fqdnLookup = $this.startProcess('nslookup.exe', $TRUE, $this.getProperty('fqdn'));
2204 [System.Collections.Hashtable]$hostnameLookup = $this.startProcess('nslookup.exe', $TRUE, $this.getProperty('hostname'));
2205
2206 # Now get the message of our result we should work with (nslookup output)
2207 [string]$fqdnLookup = $fqdnLookup.Get_Item('message');
2208 [string]$hostnameLookup = $hostnameLookup.Get_Item('message');
2209 # Get our basic IP first
2210 [string]$usedIP = $this.getProperty('ipaddress');
2211
2212 # First try to lookup the basic address. If it is not contained, look further
2213 if ($this.isIPv4AddressInsideLookup($fqdnLookup, $hostnameLookup, $usedIP) -eq $FALSE) {
2214 [int]$ipCount = $this.getProperty('ipv4_count');
2215 [bool]$found = $FALSE;
2216 # Loop through all found IPv4 IP's and try to locate the correct one
2217 for ($index = 0; $index -lt $ipCount; $index++) {
2218 $usedIP = $this.getProperty([string]::Format('ipaddress[{0}]', $index));
2219 if ($this.isIPv4AddressInsideLookup($fqdnLookup, $hostnameLookup, $usedIP)) {
2220 # Swap IP values once we found a match and exit this loop
2221 $this.setProperty([string]::Format('ipaddress[{0}]', $index), $this.getProperty('ipaddress'));
2222 $this.setProperty('ipaddress', $usedIP);
2223 $found = $TRUE;
2224 break;
2225 }
2226 }
2227
2228 if ($found -eq $FALSE) {
2229 $this.warn([string]::Format('Failed to lookup primary IP for this host. Unable to match nslookup against any IPv4 addresses on this system. Using {0} as default now. Access it with &ipaddress& for all JSON requests.',
2230 $this.getProperty('ipaddress')
2231 )
2232 );
2233 return;
2234 }
2235 }
2236
2237 $this.info([string]::Format('Setting IP {0} as primary IP for this host for all requests. Access it with &ipaddress& for all JSON requests.',
2238 $usedIP
2239 )
2240 );
2241 }
2242
2243 #
2244 # Check if inside our lookup the IP-Address is found
2245 #
2246 $installer | Add-Member -membertype ScriptMethod -name 'isIPv4AddressInsideLookup' -value {
2247 param([string]$fqdnLookup, [string]$hostnameLookup, [string]$ipv4Address);
2248
2249 if ($fqdnLookup.Contains($ipv4Address) -Or $hostnameLookup.Contains($ipv4Address)) {
2250 return $TRUE;
2251 }
2252
2253 return $FALSE;
2254 }
2255
2256 #
2257 # Add all found IP-Addresses to our property array
2258 #
2259 $installer | Add-Member -membertype ScriptMethod -name 'doLookupIPAddressesForHostname' -value {
2260 param([string]$hostname);
2261
2262 $this.info([string]::Format('Trying to fetch Host IP-Address for hostname: {0}', $hostname));
2263 try {
2264 [array]$ipAddressArray = [Net.DNS]::GetHostEntry($hostname).AddressList;
2265 $this.addHostIPAddressToProperties($ipAddressArray);
2266 return $TRUE;
2267 } catch {
2268 # Write an error in case something went wrong
2269 $this.warn([string]::Format('Failed to lookup IP-Address with DNS-Lookup for "{0}": {1}',
2270 $hostname,
2271 $_.Exception.Message
2272 )
2273 );
2274 }
2275 return $FALSE;
2276 }
2277
2278 #
2279 # Add all found IP-Addresses to our property array
2280 #
2281 $installer | Add-Member -membertype ScriptMethod -name 'addHostIPAddressToProperties' -value {
2282 param($ipArray);
2283
2284 [int]$ipV4Index = 0;
2285 [int]$ipV6Index = 0;
2286
2287 foreach ($address in $ipArray) {
2288 # Split config attributes for IPv4 and IPv6 into different values
2289 if ($address.AddressFamily -eq 'InterNetwork') { #IPv4
2290 # If the first entry of our default ipaddress is empty -> add it
2291 if ($this.getProperty('ipaddress') -eq $null) {
2292 $this.setProperty('ipaddress', $address);
2293 }
2294 # Now add the IP's with an array like construct
2295 $this.setProperty([string]::Format('ipaddress[{0}]', $ipV4Index), $address);
2296 $ipV4Index += 1;
2297 } else { #IPv6
2298 # If the first entry of our default ipaddress is empty -> add it
2299 if ($this.getProperty('ipaddressV6') -eq $null) {
2300 $this.setProperty('ipaddressV6', $address);
2301 }
2302 # Now add the IP's with an array like construct
2303 $this.setProperty([string]::Format('ipaddressV6[{0}]', $ipV6Index), $address);
2304 $ipV6Index += 1;
2305 }
2306 }
2307 $this.setProperty('ipv4_count', $ipV4Index);
2308 $this.setProperty('ipv6_count', $ipV6Index);
2309 }
2310
2311 #
2312 # Transform the hostname to upper or lower case if required
2313 # 0: Do nothing (default)
2314 # 1: Transform to lower case
2315 # 2: Transform to upper case
2316 #
2317 $installer | Add-Member -MemberType ScriptMethod -name 'doTransformHostname' -Value {
2318 [string]$hostname = $this.getProperty('local_hostname');
2319 [int]$type = $this.config('transform_hostname');
2320 switch ($type) {
2321 1 { $hostname = $hostname.ToLower(); }
2322 2 { $hostname = $hostname.ToUpper(); }
2323 Default {} # Do nothing by default
2324 }
2325
2326 if ($hostname -cne $this.getProperty('local_hostname')) {
2327 $this.info([string]::Format('Transforming Agent Name to {0}', $hostname));
2328 }
2329
2330 $this.setProperty('local_hostname', $hostname);
2331 }
2332
2333 #
2334 # Allow the replacing of placeholders within a JSON-String
2335 #
2336 $installer | Add-Member -MemberType ScriptMethod -name 'doReplaceJSONPlaceholders' -Value {
2337 param([string]$jsonString);
2338
2339 # Replace the encoded & with the original symbol at first
2340 $jsonString = $jsonString.Replace('\u0026', '&');
2341
2342 # &hostname& => hostname
2343 $jsonString = $jsonString.Replace('&hostname&', $this.getProperty('hostname'));
2344
2345 # &hostname.lowerCase& => hostname to lower
2346 $jsonString = $jsonString.Replace('&hostname.lowerCase&', $this.getProperty('hostname').ToLower());
2347
2348 # &hostname.upperCase& => hostname to upper
2349 $jsonString = $jsonString.Replace('&hostname.upperCase&', $this.getProperty('hostname').ToUpper());
2350
2351 # &fqdn& => fqdn
2352 $jsonString = $jsonString.Replace('&fqdn&', $this.getProperty('fqdn'));
2353
2354 # &fqdn.lowerCase& => fqdn to lower
2355 $jsonString = $jsonString.Replace('&fqdn.lowerCase&', $this.getProperty('fqdn').ToLower());
2356
2357 # &fqdn.upperCase& => fqdn to upper
2358 $jsonString = $jsonString.Replace('&fqdn.upperCase&', $this.getProperty('fqdn').ToUpper());
2359
2360 # hostname_placeholder => current hostname (either FQDN, hostname, with plain, upper or lower case)
2361 $jsonString = $jsonString.Replace('&hostname_placeholder&', $this.getProperty('local_hostname'));
2362
2363 # Try to replace our IP-Address
2364 if ($jsonString.Contains('&ipaddressV6')) {
2365 $jsonString = $this.doReplaceJSONIPAddress($jsonString, 'ipaddressV6');
2366 } elseif ($jsonString.Contains('&ipaddress')) {
2367 $jsonString = $this.doReplaceJSONIPAddress($jsonString, 'ipaddress');
2368 }
2369
2370 # Encode the & again to receive a proper JSON
2371 $jsonString = $jsonString.Replace('&', '\u0026');
2372
2373 return $jsonString;
2374 }
2375
2376 #
2377 # Allow the replacing of added IPv4 and IPv6 address
2378 #
2379 $installer | Add-Member -MemberType ScriptMethod -name 'doReplaceJSONIPAddress' -Value {
2380 param([string]$jsonString, [string]$ipType);
2381
2382 # Add our & delimeter to begin with
2383 [string]$ipSearchPattern = '&' + $ipType;
2384
2385 # Now locate the string and cut everything away until only our & tag for the string shall be remaining, including the array placeholder
2386 [string]$ipAddressEnd = $jsonString.Substring($jsonString.IndexOf($ipSearchPattern) + $ipType.Length + 1, $jsonString.Length - $jsonString.IndexOf($ipSearchPattern) - $ipType.Length - 1);
2387 # Ensure we still got an ending &, otherwise throw an error
2388 if ($ipAddressEnd.Contains('&')) {
2389 # Now cut everything until the first & we found
2390 $ipAddressEnd = $ipAddressEnd.Substring(0, $ipAddressEnd.IndexOf('&'));
2391 # Build together our IP-Address string, which could be for example ipaddress[1]
2392 [string]$ipAddressString = $ipType + $ipAddressEnd;
2393
2394 # Now replace this finding with our config attribute
2395 $jsonString = $jsonString.Replace('&' + $ipAddressString + '&', $this.getProperty($ipAddressString));
2396 } else {
2397 # If something goes wrong we require to notify our user
2398 $this.error([string]::Format('Failed to replace IP-Address placeholder. Invalid format for IP-Type {0}',
2399 $ipType
2400 )
2401 );
2402 }
2403
2404 # Return our new JSON-String
2405 return $jsonString;
2406 }
2407
2408 #
2409 # This function will allow us to create a
2410 # host object directly inside the Icinga Director
2411 # with a provided JSON string
2412 #
2413 $installer | Add-Member -membertype ScriptMethod -name 'createHostInsideIcingaDirector' -value {
2414
2415 if ($this.config('director_url') -And $this.getProperty('local_hostname')) {
2416 if ($this.getProperty('use_self_service_api')) {
2417
2418 if ($this.getProperty('icinga_host_exist')) {
2419 $this.info('Host is already registered within Icinga Director.');
2420 return;
2421 }
2422
2423 if ($this.getProperty('no_valid_api_token')) {
2424 $this.info('Skipping host creation over Icinga Director Self-Service API, as no valid token has been specified.');
2425 return;
2426 }
2427
2428 # If not, try to create the host and fetch the API key
2429 [string]$apiKey = $this.config('director_auth_token');
2430 [string]$url = [string]::Format('{0}self-service/register-host?name={1}&key={2}',
2431 $this.config('director_url'),
2432 $this.getProperty('local_hostname'),
2433 $apiKey
2434 );
2435 [string]$json = '';
2436 # If no JSON Object is defined (should be default), we shall create one
2437 if (-Not $this.config('director_host_object')) {
2438 [string]$hostname = $this.getProperty('local_hostname');
2439 $json = [string]::Format('{ "address": "{0}", "display_name": "{0}" }',
2440 $hostname
2441 );
2442 } else {
2443 # Otherwise use the specified one and replace the host object placeholders
2444 $json = $this.doReplaceJSONPlaceholders($this.config('director_host_object'));
2445 }
2446
2447 $this.info([string]::Format('Creating host "{0}" over API token inside Icinga Director.', $this.getProperty('local_hostname')));
2448
2449 [string]$httpResponse = $this.createHTTPRequest($url, $json, 'POST', 'application/json', $TRUE, $TRUE);
2450
2451 if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
2452 $this.setProperty('director_host_token', $httpResponse);
2453 $this.writeHostAPIKeyToDisk();
2454 [string]$response = $this.fetchIcingaDirectorSelfServiceAPIConfig($httpResponse, $FALSE);
2455 if ($response -ne '200') {
2456 $this.error([string]::Format('Failed to fetch config arguments of Icinga Director Self-Service API after adding new host to Icinga Director. Response was "{0}"', $httpResponse));
2457 } else {
2458 $this.info('Successfully fetched configuration for this host over Self-Service API.')
2459 }
2460 } else {
2461 if ($httpResponse -eq '400') {
2462 throw [string]::Format("Received response 400 from Icinga Director. In general this means you tried to re-create an existing host inside the Icinga Director with a host template API key, but the host itself has already a key assigned. Please drop the API key for the host '{0}' and re-run this script to claim ownership. This error usually occures in case the host token was removed manually from the host.", $this.getProperty('local_hostname'));
2463 } else {
2464 $this.warn([string]::Format('Failed to create host. Response code {0}', $httpResponse));
2465 }
2466 }
2467 } elseif ($this.config('director_host_object')) {
2468 # Setup the url we need to call
2469 [string]$url = $this.config('director_url') + 'host';
2470 # Replace the host object placeholders
2471 [string]$host_object_json = $this.doReplaceJSONPlaceholders($this.config('director_host_object'));
2472 # Create the host object inside the director
2473 [string]$httpResponse = $this.createHTTPRequest($url, $host_object_json, 'PUT', 'application/json', $FALSE, $this.config('debug_mode'));
2474
2475 if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
2476 $this.info([string]::Format('Placed query for creating host "{0}" inside Icinga Director. Config: {1}',
2477 $this.getProperty('local_hostname'),
2478 $httpResponse
2479 )
2480 );
2481 } else {
2482 if ($httpResponse -eq '422') {
2483 $this.warn([string]::Format('Failed to create host "{0}" inside Icinga Director. The host seems to already exist.', $this.getProperty('local_hostname')));
2484 } else {
2485 $this.error([string]::Format('Failed to create host "{0}" inside Icinga Director. Error response {1}',
2486 $this.getProperty('local_hostname'),
2487 $httpResponse
2488 )
2489 );
2490 }
2491 }
2492 # Shall we deploy the config for the generated host?
2493 if ($this.config('director_deploy_config')) {
2494 $url = $this.config('director_url') + 'config/deploy';
2495 [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $TRUE);
2496 $this.info([string]::Format('Deploying configuration from Icinga Director to Icinga. Result: {0}', $httpResponse));
2497 }
2498 }
2499 }
2500 }
2501
2502 #
2503 # Write Host API-Key for future usage
2504 #
2505 $installer | Add-Member -membertype ScriptMethod -name 'writeHostAPIKeyToDisk' -value {
2506 if (Test-Path ($this.getProperty('config_dir'))) {
2507 [string]$apiFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icingadirector.token';
2508 $this.info([string]::Format('Writing host API-Key "{0}" to "{1}"',
2509 $this.getProperty('director_host_token'),
2510 $apiFile
2511 )
2512 );
2513 [System.IO.File]::WriteAllText($apiFile, $this.getProperty('director_host_token'));
2514 }
2515 }
2516
2517 #
2518 # Read Host API-Key from disk for usage
2519 #
2520 $installer | Add-Member -membertype ScriptMethod -name 'readHostAPIKeyFromDisk' -value {
2521 [string]$apiFile = Join-Path -Path $this.getProperty('config_dir') -ChildPath 'icingadirector.token';
2522 if (Test-Path ($apiFile)) {
2523 [string]$hostToken = [System.IO.File]::ReadAllText($apiFile);
2524 $this.setProperty('director_host_token', $hostToken);
2525 $this.info([string]::Format('Reading host api token "{0}" from "{1}"',
2526 $hostToken,
2527 $apiFile
2528 )
2529 );
2530 } else {
2531 $this.setProperty('director_host_token', '');
2532 }
2533 }
2534
2535 #
2536 # Get the API Version from the Icinga Director. In case we are using
2537 # an older Version of the Director, we wont get this version
2538 #
2539 $installer | Add-Member -membertype ScriptMethod -name 'getIcingaDirectorVersion' -value {
2540 if ($this.config('director_url')) {
2541 # Do a legacy call to the Icinga Director and get a JSON-Value
2542 # Older versions of the Director do not support plain/text and
2543 # would result in making this request quite useless
2544
2545 [string]$url = $this.config('director_url') + 'self-service/api-version';
2546 [string]$versionString = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $this.config('debug_mode'));
2547
2548 if ($this.isHTTPResponseCode($versionString) -eq $FALSE) {
2549 # Remove all characters we do not need inside the string
2550 [string]$versionString = $versionString.Replace('"', '').Replace("`r", '').Replace("`n", '');
2551 [array]$version = $versionString.Split('.');
2552 $this.setProperty('icinga_director_api_version', $versionString);
2553 return;
2554 } else {
2555 if ($versionString -eq '900') {
2556 throw 'Failed to query Icinga Director API. Received error code 900. Please enable debug mode with -DebugMode for the script run to reteive additional information regarding this error.';
2557 }
2558 $this.warn('You seem to use an older Version of the Icinga Director, as no API version could be retreived.');
2559 $this.setProperty('icinga_director_api_version', '0.0.0');
2560 return;
2561 }
2562 }
2563 $this.setProperty('icinga_director_api_version', 'false');
2564 }
2565
2566 #
2567 # Set Icinga 2 Agent Version based no the installed Agent
2568 #
2569 $installer | Add-Member -membertype ScriptMethod -name 'setIcinga2AgentVersion' -value {
2570 param([string]$versionString)
2571
2572 if (-Not $versionString) {
2573 return;
2574 }
2575
2576 $this.setProperty('icinga2_agent_version', $versionString.Split('.'));
2577 }
2578
2579 #
2580 # Compare Version-Strings and check if we are running a higher or lower version
2581 #
2582 $installer | Add-Member -membertype ScriptMethod -name 'validateVersions' -value {
2583 param([string]$requiredVersion, [array]$providedVersion);
2584
2585 if (-Not $requiredVersion -Or -Not $providedVersion) {
2586 return $FALSE;
2587 }
2588
2589 [array]$requiredVersion = $requiredVersion.Split('.');
2590 $currentVersion = $providedVersion;
2591
2592 if ([int]$requiredVersion[0] -gt [int]$currentVersion[0]) {
2593 return $FALSE;
2594 }
2595
2596 if ([int]$requiredVersion[1] -gt [int]$currentVersion[1]) {
2597 return $FALSE;
2598 }
2599
2600 if ([int]$requiredVersion[1] -ge [int]$currentVersion[1] -And [int]$requiredVersion[2] -gt [int]$currentVersion[2]) {
2601 return $FALSE;
2602 }
2603
2604 return $TRUE;
2605 }
2606
2607 #
2608 # Match the Icinga Director API Version against a provided string
2609 #
2610 $installer | Add-Member -membertype ScriptMethod -name 'requireIcingaDirectorAPIVersion' -value {
2611 param([string]$version, [string]$functionName);
2612
2613 # Director URL not specified
2614 if ($this.getProperty('icinga_director_api_version') -eq 'false') {
2615 return $FALSE;
2616 }
2617
2618 if ($this.getProperty('icinga_director_api_version') -eq '0.0.0') {
2619 $this.error([string]::Format('The feature "{0}" requires Icinga Director API-Version {1}. Your Icinga Director version does not support the API.',
2620 $functionName,
2621 $version
2622 )
2623 );
2624 return $FALSE;
2625 }
2626
2627 [bool]$versionValid = $this.validateVersions($version, $this.getProperty('icinga_director_api_version').Split('.'));
2628
2629 if ($versionValid -eq $FALSE) {
2630 $this.error([string]::Format('The feature "{0}" requires Icinga Director API-Version {1}. Got version {2}',
2631 $functionName,
2632 $version,
2633 $this.getProperty('icinga_director_api_version')
2634 )
2635 );
2636 return $FALSE;
2637 }
2638
2639 return $TRUE;
2640 }
2641
2642 #
2643 # This function will convert a [hashtable] or [array] object to string
2644 # with function ConvertTo-Json for argument -DirectorHostObject.
2645 # It will however only process those if the PowerShell Version is 3
2646 # and above, because Version 2 is not providing the required
2647 # functionality. In that case the module will throw an exception
2648 #
2649 $installer | Add-Member -membertype ScriptMethod -name 'convertDirectorHostObjectArgument' -value {
2650
2651 # First add the value to an object we can work with
2652 [System.Object]$json = $this.config('director_host_object');
2653
2654 # Prevent processing of empty data
2655 if ($json -eq $null -Or $json -eq '') {
2656 return;
2657 }
2658
2659 # In case the argument is already a string -> nothing to do
2660 if ($json.GetType() -eq [string]) {
2661 # Do nothing
2662 return;
2663 } elseif ($json.GetType() -eq [hashtable] -Or $json.GetType() -eq [array]) {
2664 # Check which PowerShell Version we are using and throw an error in case our Version does not support the argument
2665 if ($PSVersionTable.PSVersion.Major -lt 3) {
2666 [string]$errorMessage = 'You are trying to pass an object of type [hashtable] or [array] for argument "-DirectorHostObject", but are using ' +
2667 'PowerShell Version 2 or lower. Passing hashtables through this argument is possible, but it requires to be ' +
2668 'converted with function ConvertTo-Json, which is available on PowerShell Version 3 and above only. ' +
2669 'You can still process JSON-Values with this module, even on PowerShell Version 2, but you will have to pass the ' +
2670 'JSON as string instead of an object. This module will now exit with an error code. For further details, please ' +
2671 'read the documentation for the "-DirectorHostObject" argument. ' +
2672 'Documentation: https://github.com/Icinga/icinga2-powershell-module/blob/master/doc/10-Basic-Arguments.md';
2673 $this.exception($errorMessage);
2674 throw 'PowerShell Version exception.';
2675 }
2676
2677 # If our PowerShell Version is supporting the function, convert it to a valid string
2678 $this.overrideConfig('director_host_object', (ConvertTo-Json -Compress $json));
2679 }
2680 }
2681
2682 #
2683 # This function will connect to the Icinga Director Self-Service API
2684 # and try to fetch the configuration for our host or the global
2685 # configuraton, depending if the Host-Token does exist and is valid
2686 # or in case it does not exist or is invalid if the API-tiken is
2687 # specified
2688 #
2689 $installer | Add-Member -membertype ScriptMethod -name 'connectToIcingaDirectorSelfServiceAPI' -value {
2690 if (-Not $this.config('director_url')) {
2691 return;
2692 }
2693
2694 if ($this.config('director_user') -And $this.config('director_password')) {
2695 $this.info('User and Password for Icinga Director have been specified, Self-Service API will not be used.');
2696 $this.setProperty('use_password_auth', $TRUE);
2697 return;
2698 }
2699
2700 $this.setProperty('icinga_host_exist', $FALSE);
2701
2702 [string]$response = $this.fetchIcingaDirectorSelfServiceAPIConfig($this.getProperty('director_host_token'), $FALSE);
2703 switch ($response) {
2704 '200' {
2705 $this.info('Connected successfully to Icinga Director Self-Service API over stored host token.');
2706 $this.setProperty('icinga_host_exist', $TRUE);
2707 $this.setProperty('use_self_service_api', $TRUE);
2708 return;
2709 };
2710 '404' {
2711 $this.warn('The local host token could not be found inside the Icinga Director.');
2712 };
2713 '500' {
2714 $this.warn('An internal server error occured while processing your local host token against the Icinga Director Self-Service API.');
2715 };
2716 }
2717
2718 if ($this.config('director_auth_token') -eq '' -And $this.getProperty('director_host_token')) {
2719 $this.error('No template API token has been specified and the host token seems no longer valid.')
2720 $this.setProperty('no_valid_api_token', $TRUE);
2721 return;
2722 }
2723
2724 # In case no host-token is set or no longer valid, use our API token if
2725 # specified to fetch the global configuration from the API
2726 $response = $this.fetchIcingaDirectorSelfServiceAPIConfig($this.config('director_auth_token'), $FALSE);
2727 switch ($response) {
2728 '200' {
2729 $this.info('Connected successfully to Icinga Director Self-Service API over API token.');
2730 $this.setProperty('use_self_service_api', $TRUE);
2731 return;
2732 };
2733 '404' {
2734 $this.warn('Failed to query Icinga Director Self-Service API.');
2735 };
2736 '500' {
2737 $this.warn('An internal server error occured while processing your API token against the Icinga Director Self-Service API.');
2738 };
2739 '900' {
2740 # Nothing to do
2741 return;
2742 };
2743 }
2744
2745 if ($this.getProperty('director_host_token') -Or $this.config('director_auth_token')) {
2746 $this.error(
2747 [string]::Format('Failed to connect to Icinga Director Self-Service API. Tokens were specified but informations could not be fetched. Please review your tokens: Host: "{0}", API: "{1}".',
2748 $this.getProperty('director_host_token'),
2749 $this.config('director_auth_token')
2750 ));
2751 $this.setProperty('no_valid_api_token', $TRUE);
2752 }
2753 }
2754
2755 #
2756 # This function will try to call the Icinga Director API
2757 # with either a host-token or our API-token and retreive
2758 # our arguments for processing with the configuration of
2759 # our Icinga 2 Agent Setup
2760 #
2761 $installer | Add-Member -membertype ScriptMethod -name 'fetchIcingaDirectorSelfServiceAPIConfig' -value {
2762 param([string]$token, [bool]$writeError);
2763 if (-Not $this.config('director_url') -Or $token -eq '') {
2764 return '900';
2765 }
2766
2767 if (-Not $this.requireIcingaDirectorAPIVersion('1.4.0', '[Function::fetchIcingaDirectorSelfServiceAPIConfig]')) {
2768 return '900';
2769 }
2770
2771 [string]$url = [string]::Format('{0}self-service/powershell-parameters?key={1}',
2772 $this.config('director_url'),
2773 $token
2774 );
2775 [string]$argumentString = $this.createHTTPRequest($url, '', 'POST', 'application/json', $TRUE, $this.config('debug_mode'));
2776
2777 if ($this.isHTTPResponseCode($argumentString) -eq $FALSE) {
2778 # First split the entire result based in new-lines into an array
2779 [array]$arguments = $argumentString.Split("`n");
2780
2781 # Now loop all elements and construct a dictionary for all values
2782 foreach ($item in $arguments) {
2783 if ($item.Contains(':')) {
2784 $this.debug([string]::Format('Processing Director API config argument "{0}"', $item));
2785 [int]$argumentPos = $item.IndexOf(":");
2786 [string]$argument = $item.Substring(0, $argumentPos);
2787 if (($argumentPos + 2) -le $item.Length) {
2788 [string]$value = $item.Substring($argumentPos + 2, $item.Length - 2 - $argumentPos);
2789 $value = $value.Replace("`r", '');
2790 $value = $value.Replace("`n", '');
2791
2792 if ($value.Contains( '!')) {
2793 [array]$valueArray = $value.Split('!');
2794 $this.overrideConfig($argument, $valueArray);
2795 } else {
2796 if ($value.toLower() -eq 'true') {
2797 $this.overrideConfig($argument, $TRUE);
2798 } elseif ($value.toLower() -eq 'false') {
2799 $this.overrideConfig($argument, $FALSE);
2800 } else {
2801 $this.overrideConfig($argument, $value);
2802 }
2803 }
2804 } else {
2805 $this.debug([string]::Format('Got key argument "{0}" without a value.', $argument));
2806 }
2807 }
2808 }
2809 } else {
2810 if ($writeError) {
2811 $this.error([string]::Format('Received "{0}" from Icinga Director. Possibly your API token is no longer valid or the object does not exist.', $argumentString));
2812 }
2813 return $argumentString;
2814 }
2815
2816 # Ensure we generate the required configuration content
2817 $this.generateConfigContent();
2818 return '200';
2819 }
2820
2821 #
2822 # This function will communicate directly with
2823 # the Icinga Director and ensuring that we get
2824 # some of the possible required informations
2825 #
2826 $installer | Add-Member -membertype ScriptMethod -name 'fetchTicketFromIcingaDirector' -value {
2827
2828 if ($this.getProperty('director_host_token') -And -Not $this.getProperty('use_password_auth')) {
2829 if ($this.getProperty('no_valid_api_token')) {
2830 $this.info('Skipping fetching of SSL ticket, as no valid API token has been specified.');
2831 return;
2832 }
2833 if ($this.requireIcingaDirectorAPIVersion('1.4.0', '[Function::fetchTicketFromIcingaDirector]')) {
2834 [string]$url = [string]::Format('{0}self-service/ticket?key={1}',
2835 $this.config('director_url'),
2836 $this.getProperty('director_host_token')
2837 );
2838 [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $TRUE, $TRUE);
2839 if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
2840 $this.setProperty('icinga_ticket', $httpResponse);
2841 $this.info([string]::Format('Fetched ticket "{0}" from Icinga Director', $httpResponse));
2842 } else {
2843 $this.error([string]::Format('Failed to fetch Ticket from Icinga Director. Error response {0}', $httpResponse));
2844 }
2845 }
2846 } else {
2847 if ($this.config('director_url') -And $this.getProperty('local_hostname')) {
2848 [string]$url = $this.config('director_url') + 'host/ticket?name=' + $this.getProperty('local_hostname');
2849 [string]$httpResponse = $this.createHTTPRequest($url, '', 'POST', 'application/json', $FALSE, $TRUE);
2850
2851 if ($this.isHTTPResponseCode($httpResponse) -eq $FALSE) {
2852 # Lookup all " inside the return string
2853 $quotes = Select-String -InputObject $httpResponse -Pattern '"' -AllMatches;
2854
2855 # If we only got two ", we should have received a valid ticket
2856 # Otherwise we need to throw an error
2857 if ($quotes.Matches.Count -ne 2) {
2858 throw [string]::Format('Failed to fetch ticket for host "{0}". Got "{1}" as ticket.',
2859 $this.getProperty('local_hostname'),
2860 $httpResponse
2861 );
2862 } else {
2863 $httpResponse = $httpResponse.subString(1, $httpResponse.length - 3);
2864 $this.info([string]::Format('Fetched ticket "{0}" for host "{1}".',
2865 $httpResponse,
2866 $this.getProperty('local_hostname')
2867 )
2868 );
2869 $this.setProperty('icinga_ticket', $httpResponse);
2870 }
2871 } else {
2872 if ($httpResponse -eq '404') {
2873 $this.error('Unable to fetch host ticket from Icinga Director. The Host object could not be found. Ensure the object is already present or created by specifying the -DirectorHostObject argument of this script.');
2874 } else {
2875 $this.error([string]::Format('Failed to fetch Ticket from Icinga Director. Error response {0}', $httpResponse));
2876 }
2877 }
2878 }
2879 }
2880 }
2881
2882 #
2883 # Check if NSClient is installed on the system
2884 #
2885 $installer | Add-Member -membertype ScriptMethod -name 'isNSClientInstalled' -value {
2886 $nsclient = Get-WmiObject -Class Win32_Product |
2887 Where-Object {
2888 $_.Name -match 'NSClient*';
2889 }
2890
2891 if ($nsclient -eq $null) {
2892 return $FALSE;
2893 }
2894
2895 return $TRUE;
2896 }
2897
2898 #
2899 # Shall we install the NSClient as well on the system?
2900 # All possible actions are handeled here
2901 #
2902 $installer | Add-Member -membertype ScriptMethod -name 'installNSClient' -value {
2903
2904 if ($this.config('install_nsclient')) {
2905
2906 [string]$installerPath = $this.getNSClientInstallerPath();
2907 $this.info([string]::Format('Trying to install and configure NSClient++ from "{0}"', $installerPath));
2908
2909 # First check if the package does exist
2910 if (Test-Path ($installerPath)) {
2911
2912 if ($this.isNSClientInstalled() -eq $FALSE) {
2913 # Get all required arguments for installing the NSClient unattended
2914 [string]$NSClientArguments = $this.getNSClientInstallerArguments();
2915
2916 # Start the installer process
2917 $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('/quiet /i "{0}" {1}', $installerPath, $NSClientArguments));
2918
2919 # Exit Code 0 means the NSClient was installed successfully
2920 # Otherwise we require to throw an error
2921 if ($result.Get_Item('exitcode') -ne 0) {
2922 $this.exception([string]::Format('Failed to install NSClient++. {0}', $result.Get_Item('message')));
2923 } else {
2924 $this.info('NSClient++ successfully installed.');
2925
2926 # To tell Icinga 2 we installed the NSClient and to make
2927 # the NSCPPath variable available, we require to restart Icinga 2
2928 $this.setProperty('require_restart', 'true');
2929 }
2930 } else {
2931 $this.info('NSClient++ is already installed on the system.');
2932 }
2933
2934 # If defined remove the Firewall Rule to secure the system
2935 # By default the NSClient is only called from the Icinga 2 Agent locally
2936 $this.removeNSClientFirewallRule();
2937 # Remove the service if we only call the NSClient locally
2938 $this.removeNSClientService();
2939 # Add the default NSClient config if we want to do more
2940 $this.addNSClientDefaultConfig();
2941 } else {
2942 $this.error([string]::Format('Failed to locate NSClient++ Installer at "{0}"', $installerPath));
2943 }
2944 } else {
2945 $this.info('NSClient++ will not be installed on the system.');
2946 }
2947 }
2948
2949 #
2950 # Determine the location of the NSClient installer
2951 # By default we are using the shipped NSClient from the Icinga 2 Agent
2952 #
2953 $installer | Add-Member -membertype ScriptMethod -name 'getNSClientInstallerPath' -value {
2954
2955 if ($this.config('nsclient_installer_path') -ne '') {
2956
2957 # Check of the installer is a local path
2958 # If so, use this as installer source
2959 if (Test-Path ($this.config('nsclient_installer_path'))) {
2960 return $this.config('nsclient_installer_path');
2961 }
2962
2963 $this.info([string]::Format('Trying to download NSClient++ from "{0}"', $this.config('nsclient_installer_path')));
2964 [System.Object]$client = New-Object System.Net.WebClient;
2965 $client.DownloadFile($this.config('nsclient_installer_path'), (Join-Path -Path $Env:temp -ChildPath 'NSCP.msi'));
2966
2967 return (Join-Path -Path $Env:temp -ChildPath 'NSCP.msi');
2968 } else {
2969 # Icinga is shipping a NSClient Version after installation
2970 # Install this version if defined
2971 return (Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\NSCP.msi');
2972 }
2973
2974 return '';
2975 }
2976
2977 #
2978 # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
2979 # we do not require an open Firewall Rule to allow traffic.
2980 #
2981 $installer | Add-Member -membertype ScriptMethod -name 'getNSClientInstallerArguments' -value {
2982 [string]$NSClientArguments = '';
2983
2984 if ($this.config('nsclient_directory')) {
2985 $NSClientArguments += [string]::Format(' INSTALLLOCATION={0}', $this.config('nsclient_directory'));
2986 }
2987
2988 return $NSClientArguments;
2989 }
2990
2991 #
2992 # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
2993 # we do not require an open Firewall Rule to allow traffic.
2994 #
2995 $installer | Add-Member -membertype ScriptMethod -name 'removeNSClientFirewallRule' -value {
2996 if ($this.config('nsclient_firewall') -eq $FALSE) {
2997
2998 $result = $this.startProcess('netsh', $FALSE, 'advfirewall firewall show rule name="NSClient++ Monitoring Agent"');
2999 if ($result.Get_Item('exitcode') -ne 0) {
3000 # Firewall rule was not found. Nothing to do
3001 $this.info('NSClient++ Firewall Rule is not installed');
3002 return;
3003 }
3004
3005 $this.info('Trying to remove NSClient++ Firewall Rule');
3006
3007 $result = $this.startProcess('netsh', $TRUE, 'advfirewall firewall delete rule name="NSClient++ Monitoring Agent"');
3008
3009 if ($result.Get_Item('exitcode') -ne 0) {
3010 $this.error([string]::Format('Failed to remove NSClient++ Firewall rule: {0}', $result.Get_Item('message')));
3011 } else {
3012 $this.info('NSClient++ Firewall Rule has been successfully removed');
3013 }
3014 }
3015 }
3016
3017 #
3018 # If we only want to use the NSClient++ to be called from the Icinga 2 Agent
3019 # we do not require a running NSClient++ Service
3020 #
3021 $installer | Add-Member -membertype ScriptMethod -name 'removeNSClientService' -value {
3022 if ($this.config('nsclient_service') -eq $FALSE) {
3023 $NSClientService = Get-WmiObject -Class Win32_Service -Filter "Name='nscp'";
3024 if ($NSClientService -ne $null) {
3025 $this.info('Trying to remove NSClient++ service');
3026 # Before we remove the service, stop it (to prevent ghosts)
3027 Stop-Service 'nscp';
3028 # Now remove it
3029 $result = $NSClientService.delete();
3030 if ($result.ReturnValue -eq 0) {
3031 $this.info('NSClient++ Service has been removed');
3032 } else {
3033 $this.error('Failed to remove NSClient++ service');
3034 }
3035 } else {
3036 $this.info('NSClient++ Service is not installed')
3037 }
3038 }
3039 }
3040
3041 #
3042 # In case we want to do more with the NSClient, we can auto-generate
3043 # all NSClient++ config attributes
3044 #
3045 $installer | Add-Member -membertype ScriptMethod -name 'addNSClientDefaultConfig' -value {
3046 if ($this.config('nsclient_add_defaults')) {
3047 [string]$NSClientBinary = $this.getNSClientDefaultExecutablePath();
3048
3049 if ($NSClientBinary -eq '') {
3050 $this.error('Unable to generate NSClient++ default config. Executable nscp.exe could not be found ' +
3051 'on default locations or the specified custom location. If you installed the NSClient on a ' +
3052 'custom location, please specify the path with -NSClientDirectory');
3053 return;
3054 }
3055
3056 if (Test-Path ($NSClientBinary)) {
3057 $this.info('Generating all default NSClient++ config values');
3058 $result = $this.startProcess($NSClientBinary, $TRUE, 'settings --generate --add-defaults --load-all');
3059 if ($result.Get_Item('exitcode') -ne 0) {
3060 $this.error($result.Get_Item('message'));
3061 }
3062 } else {
3063 $this.error([string]::Format('Failed to generate NSClient++ defaults config. Path to executable is not valid: {0}', $NSClientBinary));
3064 }
3065 }
3066 }
3067
3068 #
3069 # Deprecated function
3070 #
3071 $installer | Add-Member -membertype ScriptMethod -name 'installIcinga2Agent' -value {
3072 $this.warn('The function "installIcinga2Agent" is deprecated and will be removed soon. Please use "install" instead.')
3073 return $this.install();
3074 }
3075 $installer | Add-Member -membertype ScriptMethod -name 'installMonitoringComponents' -value {
3076 $this.warn('The function "installMonitoringComponents" is deprecated and will be removed soon. Please use "install" instead.')
3077 return $this.install();
3078 }
3079
3080 #
3081 # This function will try to load all
3082 # data from the system and setup the
3083 # entire Agent without user interaction
3084 # including download and update if
3085 # specified. Returnd 0 or 1 as exit code
3086 #
3087 $installer | Add-Member -membertype ScriptMethod -name 'install' -value {
3088 try {
3089 if (-Not $this.isAdmin()) {
3090 return 1;
3091 }
3092
3093 # Write an output to the logfile only, ensuring we always get a proper 'start entry' for the user
3094 $this.info('Started script run...');
3095 # Get the current API-Version from the Icinga Director
3096 $this.getIcingaDirectorVersion();
3097 # Convert our DirectorHostObject argument from Object to String if required
3098 $this.convertDirectorHostObjectArgument();
3099 # Read the Host-API Key in case it exists
3100 $this.readHostAPIKeyFromDisk();
3101 # Establish connection to Icinga Director Self-Service API if required
3102 # and fetch basic / host configuration if tokens are set
3103 $this.connectToIcingaDirectorSelfServiceAPI();
3104 # Get host name or FQDN if required
3105 $this.fetchHostnameOrFQDN();
3106 # Get IP-Address of host
3107 $this.fetchHostIPAddress();
3108 # Try to locate the primary IP Address
3109 $this.lookupPrimaryIPv4Address();
3110 # Transform the hostname if required
3111 $this.doTransformHostname();
3112 # Before we continue, flush the API Directory if specified. This will require
3113 # us to stop the Icinga 2 Agent, but should prevent any false positive in
3114 # case dependencies within the API Director are no longer pressent and will
3115 # ensure a possible config rollback is working as intended as well
3116 $this.flushIcingaApiDirectory();
3117
3118 # Try to locate the current
3119 # Installation data from the Agent
3120 if ($this.isAgentInstalled()) {
3121 if (-Not $this.isAgentUpToDate()) {
3122 if ($this.allowAgentUpdates()) {
3123 $this.printAgentUpdateMessage();
3124 $this.updateAgent();
3125 $this.cleanupAgentInstaller();
3126 }
3127 } else {
3128 $this.info('Icinga 2 Agent is up-to-date. Nothing to do.');
3129 }
3130 } else {
3131 if ($this.canInstallAgent()) {
3132 $this.installAgent();
3133 $this.cleanupAgentInstaller();
3134 } else {
3135 $this.warn('Icinga 2 Agent is not installed and not allowed of beeing installed.');
3136 }
3137 }
3138
3139 # Try to create a host object inside the Icinga Director
3140 $this.createHostInsideIcingaDirector();
3141 # First check if we should get some parameters from the Icinga Director
3142 $this.fetchTicketFromIcingaDirector();
3143
3144 # In case we downgrade from Icinga 2.8.0 or above to a older version (like Icinga 2.7.2)
3145 $this.migrateCertificates();
3146 if (-Not $this.hasCertificates() -Or $this.forceCertificateGeneration()) {
3147 $this.generateCertificates();
3148 } else {
3149 $this.info('Icinga 2 certificates already exist. Nothing to do.');
3150 }
3151
3152 $this.generateIcingaConfiguration();
3153 $this.applyPossibleConfigChanges();
3154 $this.switchIcingaDebugLog();
3155 $this.installIcingaAgentFirewallRule();
3156 $this.installNSClient();
3157
3158 if ($this.madeChanges()) {
3159 $this.restartAgent();
3160 } else {
3161 $this.info('No changes detected.');
3162 }
3163
3164 # We modify the service user at the very last to ensure
3165 # the user we defined for logging in is valid
3166 $this.modifyIcingaServiceUser();
3167 return $this.getScriptExitCode();
3168 } catch {
3169 $this.printLastException();
3170 [void]$this.getScriptExitCode();
3171 return 1;
3172 }
3173 }
3174
3175 #
3176 # Deprecated function
3177 #
3178 $installer | Add-Member -membertype ScriptMethod -name 'uninstallIcinga2Agent' -value {
3179 $this.warn('The function "uninstallIcinga2Agent" is deprecated and will be removed soon. Please use "uninstall" instead.')
3180 return $this.uninstall();
3181 }
3182 $installer | Add-Member -membertype ScriptMethod -name 'uninstallMonitoringComponents' -value {
3183 $this.warn('The function "uninstallMonitoringComponents" is deprecated and will be removed soon. Please use "uninstall" instead.')
3184 return $this.uninstall();
3185 }
3186
3187 #
3188 # Removes the Icinga 2 Agent from the system
3189 #
3190 $installer | Add-Member -membertype ScriptMethod -name 'uninstall' -value {
3191 $this.info('Trying to locate Icinga 2 Agent...');
3192
3193 if ($this.isAgentInstalled()) {
3194 $this.info('Removing Icinga 2 Agent from the system');
3195 $result = $this.startProcess('MsiExec.exe', $TRUE, [string]::Format('{0} /q', $this.getProperty('uninstall_id')));
3196
3197 if ($result.Get_Item('exitcode') -ne 0) {
3198 $this.error($result.Get_Item('message'));
3199 return [int]$result.Get_Item('exitcode');
3200 }
3201
3202 $this.info('Icinga 2 Agent successfully removed.');
3203 }
3204
3205 if ($this.config('full_uninstallation')) {
3206 $this.info('Flushing Icinga 2 program data directory...');
3207 if (Test-Path ((Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'))) {
3208 try {
3209 [System.Object]$folder = New-Object -ComObject Scripting.FileSystemObject;
3210 [void]$folder.DeleteFolder((Join-Path -Path $Env:ProgramData -ChildPath 'icinga2'));
3211 $this.info('Remaining Icinga 2 configuration successfully removed.');
3212 } catch {
3213 $this.exception([string]::Format('Failed to delete Icinga 2 Program Data Directory: {0}', $_.Exception.Message));
3214 }
3215 } else {
3216 $this.warn('Icinga 2 Agent program directory not present.');
3217 }
3218 }
3219
3220 if ($this.config('remove_nsclient')) {
3221 $this.info('Trying to remove installed NSClient++...');
3222
3223 $nsclient = Get-WmiObject -Class Win32_Product |
3224 Where-Object {
3225 $_.Name -match 'NSClient*';
3226 }
3227
3228 if ($nsclient -ne $null) {
3229 $this.info('Removing installed NSClient++...');
3230 [void]$nsclient.Uninstall();
3231 $this.info('NSClient++ has been successfully removed.');
3232 } else {
3233 $this.warn('NSClient++ could not be located on the system. Nothing to remove.');
3234 }
3235 }
3236
3237 return $this.getScriptExitCode();
3238 }
3239
3240 #
3241 # Locate the current installation of Icinga 2 and dump the icinga2.conf to the window
3242 #
3243 $installer | Add-Member -membertype ScriptMethod -name 'dumpIcinga2Conf' -value {
3244 if (-Not $this.isAgentInstalled()) {
3245 $this.info('Icinga 2 Agent is not installed on the system. No configuration to dump.');
3246 return $this.getScriptExitCode();
3247 }
3248
3249 [string]$icingaConfig = '';
3250 if (Test-Path $this.getIcingaConfigFile()) {
3251 $icingaConfig = [System.IO.File]::ReadAllText($this.getIcingaConfigFile());
3252 $this.info([string]::Format('Dumping content of the Icinga 2 configuration from "{0}".', $this.getIcingaConfigFile()));
3253 $this.output($icingaConfig);
3254
3255 } else {
3256 $this.exception([string]::Format('Failed to lookup Icinga 2 configuration at "{0}". File does not exist.', $this.getIcingaConfigFile()));
3257 }
3258 }
3259
3260 #
3261 # Locate the current installation of Icinga 2 and dump all Icinga 2 objects
3262 #
3263 $installer | Add-Member -membertype ScriptMethod -name 'dumpIcinga2Objects' -value {
3264 if (-Not $this.isAgentInstalled()) {
3265 $this.info('Icinga 2 Agent is not installed on the system. No objects to dump.');
3266 return $this.getScriptExitCode();
3267 }
3268
3269 [string]$icingaBinary = Join-Path -Path $this.getInstallPath() -ChildPath 'sbin\icinga2.exe';
3270
3271 if (-Not (Test-Path $icingaBinary)) {
3272 $this.exception([string]::Format('Failed to query Icinga 2 objects. Executable at "{0}" does not exist.', $icingaBinary));
3273 return $this.getScriptExitCode();
3274 }
3275
3276 $result = $this.startProcess($icingaBinary, $FALSE, 'object list');
3277 if ($result.Get_Item('exitcode') -ne 0) {
3278 $this.exception($result.Get_Item('message'));
3279 } else {
3280 $this.info('Dumping all objects from Icinga 2');
3281 $this.output($result.Get_Item('message'));
3282 }
3283 }
3284
3285 # Make the installation / uninstallation of the script easier and shorter
3286 [int]$installerExitCode = 0;
3287 [int]$uninstallerExitCode = 0;
3288 [int]$dumpConfigExitCode = 0;
3289 [int]$dumpObjectsExitCode = 0;
3290 # If flag RunUninstaller is set, do the uninstallation of the components
3291 if ($RunUninstaller) {
3292 $uninstallerExitCode = $installer.uninstall();
3293 }
3294 # If flag RunInstaller is set, do the installation of the components
3295 if ($RunInstaller) {
3296 $installerExitCode = $installer.install();
3297 }
3298 # If flag DumpIcingaConfig is set, print the current Icinga 2 configuration
3299 if ($DumpIcingaConfig) {
3300 $dumpConfigExitCode = $installer.dumpIcinga2Conf();
3301 }
3302 if ($DumpIcingaObjects) {
3303 $dumpObjectsExitCode = $installer.dumpIcinga2Objects();
3304 }
3305 if ($RunInstaller -Or $RunUninstaller -Or $DumpIcingaConfig -Or $DumpIcingaObjects) {
3306 if ($installerExitCode -ne 0 -Or $uninstallerExitCode -ne 0 -Or $dumpConfigExitCode -ne 0 -Or $dumpObjectsExitCode -ne 0) {
3307 return 1;
3308 }
3309 }
3310
3311 # Otherwise handle everything as before
3312 return $installer;
3313}
3314
3315
3316exit Icinga2AgentModule `
3317 -AgentName 'ocecc01-2100.univoip.net' `
3318 -Ticket '3f9f6ad2a53c549a23fba238053a23ec89979ab4' `
3319 -ParentZone 'icinga2-s-crane.univoip.net' `
3320 -ParentEndpoints 'icinga2-s-crane.univoip.net' `
3321 -CAServer 'icinga.univoip.net' `
3322 -RunInstaller