Windows: A module for creating Toast notifications on Modern Windows versions. (#26675)
* replace duff commit version of win_toast * change expire_mins to expire_secs and add example showing use of async * fix metadata version to keep sanity --test validate-modules happy * code review fixes and change expire_secs to expire_seconds * add first pass integration tests for win_toast * win_toast no longer fails if there are no logged in users to notify (it sets a toast_sent false if this happens) * yaml lint clean up of setup.yml in win_toast integration tests * improve exception and stack trace if the notifier cannot be created, following feedback from dag * removed unwanted 'echo' input parameters from return vals; added to CHANGELOG.md, removed _seconds units from module params; updated tests to match
This commit is contained in:
parent
4ba7d05e9d
commit
8f9b885113
7 changed files with 332 additions and 0 deletions
|
@ -568,6 +568,7 @@ Ansible Changes By Release
|
||||||
* win_rabbitmq_plugin
|
* win_rabbitmq_plugin
|
||||||
* win_route
|
* win_route
|
||||||
* win_security_policy
|
* win_security_policy
|
||||||
|
* win_toast
|
||||||
* win_user_right
|
* win_user_right
|
||||||
* win_wait_for
|
* win_wait_for
|
||||||
* win_wakeonlan
|
* win_wakeonlan
|
||||||
|
|
91
lib/ansible/modules/windows/win_toast.ps1
Normal file
91
lib/ansible/modules/windows/win_toast.ps1
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
#!powershell
|
||||||
|
# This file is part of Ansible
|
||||||
|
|
||||||
|
# Copyright (c) 2017, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
#Requires -Module Ansible.ModuleUtils.Legacy.psm1
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# version check
|
||||||
|
$osversion = [Environment]::OSVersion
|
||||||
|
$lowest_version = 10
|
||||||
|
if ($osversion.Version.Major -lt $lowest_version ) {
|
||||||
|
Fail-Json $result "Sorry, this version of windows, $osversion, does not support Toast notifications. Toast notifications are available from version $lowest_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
$stopwatch = [system.diagnostics.stopwatch]::startNew()
|
||||||
|
$now = [DateTime]::Now
|
||||||
|
$default_title = "Notification: " + $now.ToShortTimeString()
|
||||||
|
|
||||||
|
$params = Parse-Args $args -supports_check_mode $true
|
||||||
|
$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false
|
||||||
|
|
||||||
|
$expire_seconds = Get-AnsibleParam -obj $params -name "expire" -type "int" -default 45
|
||||||
|
$group = Get-AnsibleParam -obj $params -name "group" -type "str" -default "Powershell"
|
||||||
|
$msg = Get-AnsibleParam -obj $params -name "msg" -type "str" -default "Hello world!"
|
||||||
|
$popup = Get-AnsibleParam -obj $params -name "popup" -type "bool" -default $true
|
||||||
|
$tag = Get-AnsibleParam -obj $params -name "tag" -type "str" -default "Ansible"
|
||||||
|
$title = Get-AnsibleParam -obj $params -name "title" -type "str" -default $default_title
|
||||||
|
|
||||||
|
$timespan = New-TimeSpan -Seconds $expire_seconds
|
||||||
|
$expire_at = $now + $timespan
|
||||||
|
$expire_at_utc = $($expire_at.ToUniversalTime()|Out-String).Trim()
|
||||||
|
|
||||||
|
$result = @{
|
||||||
|
changed = $false
|
||||||
|
expire_at = $expire_at.ToString()
|
||||||
|
expire_at_utc = $expire_at_utc
|
||||||
|
toast_sent = $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# If no logged in users, there is no notifications service,
|
||||||
|
# and no-one to read the message, so exit but do not fail
|
||||||
|
# if there are no logged in users to notify.
|
||||||
|
|
||||||
|
if ((get-process -name explorer -EA silentlyContinue).Count -gt 0){
|
||||||
|
|
||||||
|
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null
|
||||||
|
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText01)
|
||||||
|
|
||||||
|
#Convert to .NET type for XML manipulation
|
||||||
|
$toastXml = [xml] $template.GetXml()
|
||||||
|
$toastXml.GetElementsByTagName("text").AppendChild($toastXml.CreateTextNode($title)) > $null
|
||||||
|
# TODO add subtitle
|
||||||
|
|
||||||
|
#Convert back to WinRT type
|
||||||
|
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
|
||||||
|
$xml.LoadXml($toastXml.OuterXml)
|
||||||
|
|
||||||
|
$toast = [Windows.UI.Notifications.ToastNotification]::new($xml)
|
||||||
|
$toast.Tag = $tag
|
||||||
|
$toast.Group = $group
|
||||||
|
$toast.ExpirationTime = $expire_at
|
||||||
|
$toast.SuppressPopup = -not $popup
|
||||||
|
|
||||||
|
try {
|
||||||
|
$notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($msg)
|
||||||
|
if (-not $check_mode) {
|
||||||
|
$notifier.Show($toast)
|
||||||
|
$result.toast_sent = $true
|
||||||
|
Start-Sleep -Seconds $expire_seconds
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$excep = $_
|
||||||
|
$result.exception = $excep.ScriptStackTrace
|
||||||
|
Fail-Json -obj $result -message "Failed to create toast notifier: $($excep.Exception.Message)"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$result.toast_sent = $false
|
||||||
|
$result.no_toast_sent_reason = 'No logged in users to notifiy'
|
||||||
|
}
|
||||||
|
|
||||||
|
$endsend_at = Get-Date| Out-String
|
||||||
|
$stopwatch.Stop()
|
||||||
|
|
||||||
|
$result.time_taken = $stopwatch.Elapsed.TotalSeconds
|
||||||
|
$result.sent_localtime = $endsend_at.Trim()
|
||||||
|
|
||||||
|
Exit-Json $result
|
93
lib/ansible/modules/windows/win_toast.py
Normal file
93
lib/ansible/modules/windows/win_toast.py
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# (c) 2017, Jon Hawkesworth (@jhawkesworth) <figs@unity.demon.co.uk>
|
||||||
|
# Copyright (c) 2017 Ansible Project
|
||||||
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||||
|
|
||||||
|
# this is a windows documentation stub. actual code lives in the .ps1
|
||||||
|
# file of the same name
|
||||||
|
|
||||||
|
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||||
|
'status': ['preview'],
|
||||||
|
'supported_by': 'community'}
|
||||||
|
|
||||||
|
DOCUMENTATION = r'''
|
||||||
|
---
|
||||||
|
module: win_toast
|
||||||
|
version_added: "2.4"
|
||||||
|
short_description: Sends Toast windows notification to logged in users on Windows 10 or later hosts
|
||||||
|
description:
|
||||||
|
- Sends alerts which appear in the Action Center area of the windows desktop.
|
||||||
|
options:
|
||||||
|
expire:
|
||||||
|
description:
|
||||||
|
- How long in seconds before the notification expires.
|
||||||
|
default: 45
|
||||||
|
group:
|
||||||
|
description:
|
||||||
|
- Which notification group to add the notification to.
|
||||||
|
default: Powershell
|
||||||
|
msg:
|
||||||
|
description:
|
||||||
|
- The message to appear inside the notification. May include \n to format the message to appear within the Action Center.
|
||||||
|
default: 'Hello, World!'
|
||||||
|
popup:
|
||||||
|
description:
|
||||||
|
- If false, the notification will not pop up and will only appear in the Action Center.
|
||||||
|
type: bool
|
||||||
|
default: yes
|
||||||
|
tag:
|
||||||
|
description:
|
||||||
|
- The tag to add to the notification.
|
||||||
|
default: Ansible
|
||||||
|
title:
|
||||||
|
description:
|
||||||
|
- The notification title, which appears in the pop up..
|
||||||
|
default: Notification HH:mm
|
||||||
|
author:
|
||||||
|
- Jon Hawkesworth (@jhawkesworth)
|
||||||
|
notes:
|
||||||
|
- This module must run on a windows 10 or Server 2016 host, so ensure your play targets windows hosts, or delegates to a windows host.
|
||||||
|
- The module does not fail if there are no logged in users to notify.
|
||||||
|
- Messages are only sent to the local host where the module is run.
|
||||||
|
- You must run this module with async, otherwise it will hang until the expire period has passed.
|
||||||
|
'''
|
||||||
|
|
||||||
|
EXAMPLES = r'''
|
||||||
|
- name: Warn logged in users of impending upgrade (note use of async to stop the module from waiting until notification expires).
|
||||||
|
win_toast:
|
||||||
|
expire: 60
|
||||||
|
title: System Upgrade Notification
|
||||||
|
msg: Automated upgrade about to start. Please save your work and log off before {{ deployment_start_time }}
|
||||||
|
async: 60
|
||||||
|
poll: 0
|
||||||
|
'''
|
||||||
|
|
||||||
|
RETURN = r'''
|
||||||
|
expire_at_utc:
|
||||||
|
description: Calculated utc date time when the notification expires.
|
||||||
|
returned: allways
|
||||||
|
type: string
|
||||||
|
sample: 07 July 2017 04:50:54
|
||||||
|
no_toast_sent_reason:
|
||||||
|
description: Text containing the reason why a notification was not sent.
|
||||||
|
returned: when no logged in users are detected
|
||||||
|
type: string
|
||||||
|
sample: No logged in users to notify
|
||||||
|
sent_localtime:
|
||||||
|
description: local date time when the notification was sent.
|
||||||
|
returned: allways
|
||||||
|
type: string
|
||||||
|
sample: 07 July 2017 05:45:54
|
||||||
|
time_taken:
|
||||||
|
description: How long the module took to run on the remote windows host in seconds.
|
||||||
|
returned: allways
|
||||||
|
type: float
|
||||||
|
sample: 0.3706631999999997
|
||||||
|
toast_sent:
|
||||||
|
description: Whether the module was able to send a toast notification or not.
|
||||||
|
returned: allways
|
||||||
|
type: boolean
|
||||||
|
sample: false
|
||||||
|
'''
|
1
test/integration/targets/win_toast/aliases
Normal file
1
test/integration/targets/win_toast/aliases
Normal file
|
@ -0,0 +1 @@
|
||||||
|
windows/ci/group2
|
13
test/integration/targets/win_toast/tasks/main.yml
Normal file
13
test/integration/targets/win_toast/tasks/main.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
- name: Set up tests
|
||||||
|
include_tasks: setup.yml
|
||||||
|
|
||||||
|
- name: Test in normal mode
|
||||||
|
include_tasks: tests.yml
|
||||||
|
vars:
|
||||||
|
in_check_mode: no
|
||||||
|
|
||||||
|
- name: Test in check mode
|
||||||
|
include_tasks: tests.yml
|
||||||
|
vars:
|
||||||
|
in_check_mode: yes
|
||||||
|
check_mode: yes
|
27
test/integration/targets/win_toast/tasks/setup.yml
Normal file
27
test/integration/targets/win_toast/tasks/setup.yml
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
- name: Get OS version
|
||||||
|
win_shell: '[Environment]::OSVersion.Version.Major'
|
||||||
|
register: os_version
|
||||||
|
|
||||||
|
- name: Get logged in user count (using explorer exe as a proxy)
|
||||||
|
win_shell: (get-process -name explorer -EA silentlyContinue).Count
|
||||||
|
register: user_count
|
||||||
|
|
||||||
|
- name: debug os_version
|
||||||
|
debug:
|
||||||
|
var: os_version
|
||||||
|
verbosity: 2
|
||||||
|
|
||||||
|
- name: debug user_count
|
||||||
|
debug:
|
||||||
|
var: user_count
|
||||||
|
verbosity: 2
|
||||||
|
|
||||||
|
- name: Set fact if toast cannot be made
|
||||||
|
set_fact:
|
||||||
|
can_toast: False
|
||||||
|
when: os_version.stdout|int < 10
|
||||||
|
|
||||||
|
- name: Set fact if toast can be made
|
||||||
|
set_fact:
|
||||||
|
can_toast: True
|
||||||
|
when: os_version.stdout|int >= 10
|
106
test/integration/targets/win_toast/tasks/tests.yml
Normal file
106
test/integration/targets/win_toast/tasks/tests.yml
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
- name: Warn user
|
||||||
|
win_toast:
|
||||||
|
expire_seconds: 10
|
||||||
|
msg: Keep calm and carry on.
|
||||||
|
register: msg_result
|
||||||
|
ignore_errors: True
|
||||||
|
|
||||||
|
- name: Test msg_result when can_toast is true (normal mode, users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result|failed
|
||||||
|
- msg_result.time_taken > 10
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int > 0
|
||||||
|
|
||||||
|
- name: Test msg_result when can_toast is true (normal mode, no users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result|failed
|
||||||
|
- msg_result.time_taken > 0.1
|
||||||
|
- msg_result.toast_sent == False
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int == 0
|
||||||
|
|
||||||
|
- name: Test msg_result when can_toast is true (check mode, users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result|failed
|
||||||
|
- msg_result.time_taken > 0.1
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == True
|
||||||
|
|
||||||
|
- name: Test msg_result when can_toast is true (check mode, no users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result|failed
|
||||||
|
- msg_result.time_taken > 0.1
|
||||||
|
- msg_result.toast_sent == False
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == True
|
||||||
|
- user_count.stdout|int == 0
|
||||||
|
|
||||||
|
- name: Test msg_result when can_toast is false
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- msg_result|failed
|
||||||
|
when: can_toast == False
|
||||||
|
|
||||||
|
- name: Warn user again
|
||||||
|
win_toast:
|
||||||
|
expire_seconds: 10
|
||||||
|
msg: Keep calm and carry on.
|
||||||
|
register: msg_result2
|
||||||
|
ignore_errors: True
|
||||||
|
|
||||||
|
- name: Test msg_result2 when can_toast is true (normal mode, users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result2|failed
|
||||||
|
- msg_result2.time_taken > 10
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int > 0
|
||||||
|
|
||||||
|
- name: Test msg_result2 when can_toast is true (normal mode, no users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result2|failed
|
||||||
|
- msg_result2.time_taken > 0.1
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int == 0
|
||||||
|
|
||||||
|
- name: Test msg_result2 when can_toast is true (check mode, users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result2|failed
|
||||||
|
- msg_result2.time_taken > 0.1
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int > 0
|
||||||
|
|
||||||
|
- name: Test msg_result2 when can_toast is true (check mode, no users)
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- not msg_result2|failed
|
||||||
|
- msg_result2.time_taken > 0.1
|
||||||
|
when:
|
||||||
|
- can_toast == True
|
||||||
|
- in_check_mode == False
|
||||||
|
- user_count.stdout|int == 0
|
||||||
|
|
||||||
|
- name: Test msg_result2 when can_toast is false
|
||||||
|
assert:
|
||||||
|
that:
|
||||||
|
- msg_result2|failed
|
||||||
|
when: can_toast == False
|
Loading…
Reference in a new issue