From 8f9b885113fde4ccf3380872825f61c9e5a03d50 Mon Sep 17 00:00:00 2001 From: jhawkesworth Date: Wed, 30 Aug 2017 00:19:18 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + lib/ansible/modules/windows/win_toast.ps1 | 91 +++++++++++++++ lib/ansible/modules/windows/win_toast.py | 93 +++++++++++++++ test/integration/targets/win_toast/aliases | 1 + .../targets/win_toast/tasks/main.yml | 13 +++ .../targets/win_toast/tasks/setup.yml | 27 +++++ .../targets/win_toast/tasks/tests.yml | 106 ++++++++++++++++++ 7 files changed, 332 insertions(+) create mode 100644 lib/ansible/modules/windows/win_toast.ps1 create mode 100644 lib/ansible/modules/windows/win_toast.py create mode 100644 test/integration/targets/win_toast/aliases create mode 100644 test/integration/targets/win_toast/tasks/main.yml create mode 100644 test/integration/targets/win_toast/tasks/setup.yml create mode 100644 test/integration/targets/win_toast/tasks/tests.yml diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e7ef765c2..1794b35e7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -568,6 +568,7 @@ Ansible Changes By Release * win_rabbitmq_plugin * win_route * win_security_policy + * win_toast * win_user_right * win_wait_for * win_wakeonlan diff --git a/lib/ansible/modules/windows/win_toast.ps1 b/lib/ansible/modules/windows/win_toast.ps1 new file mode 100644 index 00000000000..db7202882ae --- /dev/null +++ b/lib/ansible/modules/windows/win_toast.ps1 @@ -0,0 +1,91 @@ +#!powershell +# This file is part of Ansible + +# Copyright (c) 2017, Jon Hawkesworth (@jhawkesworth) +# 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 diff --git a/lib/ansible/modules/windows/win_toast.py b/lib/ansible/modules/windows/win_toast.py new file mode 100644 index 00000000000..c56c32f0a15 --- /dev/null +++ b/lib/ansible/modules/windows/win_toast.py @@ -0,0 +1,93 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2017, Jon Hawkesworth (@jhawkesworth) +# 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 +''' diff --git a/test/integration/targets/win_toast/aliases b/test/integration/targets/win_toast/aliases new file mode 100644 index 00000000000..ee0ed5974e9 --- /dev/null +++ b/test/integration/targets/win_toast/aliases @@ -0,0 +1 @@ +windows/ci/group2 diff --git a/test/integration/targets/win_toast/tasks/main.yml b/test/integration/targets/win_toast/tasks/main.yml new file mode 100644 index 00000000000..b4d49e6781c --- /dev/null +++ b/test/integration/targets/win_toast/tasks/main.yml @@ -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 diff --git a/test/integration/targets/win_toast/tasks/setup.yml b/test/integration/targets/win_toast/tasks/setup.yml new file mode 100644 index 00000000000..1fe3a22da19 --- /dev/null +++ b/test/integration/targets/win_toast/tasks/setup.yml @@ -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 diff --git a/test/integration/targets/win_toast/tasks/tests.yml b/test/integration/targets/win_toast/tasks/tests.yml new file mode 100644 index 00000000000..009212f7567 --- /dev/null +++ b/test/integration/targets/win_toast/tasks/tests.yml @@ -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