diff --git a/ansible/playbooks/win_update.yml b/ansible/playbooks/win_update.yml index ae4cec9..04c2ae9 100644 --- a/ansible/playbooks/win_update.yml +++ b/ansible/playbooks/win_update.yml @@ -1,6 +1,6 @@ --- -- hosts: "{{ target_host }}" - gather_facts: true +- hosts: "meta-os_windows" + gather_facts: false vars_files: - ../vars/common.yml - ../vars/connection.yml diff --git a/ansible/roles/win_update/tasks/update.yml b/ansible/roles/win_update/tasks/update.yml index 0a502a6..bfeda45 100644 --- a/ansible/roles/win_update/tasks/update.yml +++ b/ansible/roles/win_update/tasks/update.yml @@ -1,30 +1,65 @@ --- -- name: Update VM - tags: update_vm +- name: Install client VM updates and reboot as much as needed block: - - name: Clear update cache - win_shell: | - UsoClient RefreshSettings - UsoClient ScanInstallWait - - - name: Update Windows + - name: Install Windows updates (looped) + tags: update_windows ansible.windows.win_updates: - category_names: '*' - skip_optional: true + category_names: + - CriticalUpdates + - DefinitionUpdates + - SecurityUpdates + - UpdateRollups reboot: true - reboot_timeout: 3600 - ignore_errors: true + register: update_result + retries: 5 + delay: 30 + until: update_result.reboot_required == false - - name: Update Windows (again) + - name: Wait for host to come back online + tags: update_windows + ansible.builtin.wait_for_connection: + timeout: 600 + + - name: Update all choco packages + win_command: choco upgrade all -y + tags: update_choco + + - name: Update Start Menu Shortcuts in XML on remote Windows machines + tags: update_start_menu + block: + - name: Copy Python script to target machine + ansible.windows.win_template: + src: "{{ script_name }}" + dest: "{{ script_path }}" + force: true + + - name: Copy current XML to backup location + win_copy: + remote_src: true + src: "{{ start_menu_xml }}" + dest: "{{ backup_xml }}" + + - name: Run Python script on remote machine + win_command: python "{{ script_path }}.xml" + when: "inventory_hostname in groups['meta-type_client']|default([])" + +- name: Install Server VM updates and reboot as much as needed + tags: update_windows + block: + - name: Install Windows updates (looped) ansible.windows.win_updates: - category_names: '*' + category_names: + - CriticalUpdates + - DefinitionUpdates + - SecurityUpdates + - UpdateRollups reboot: true - reboot_timeout: 3600 - ignore_errors: true + register: update_result + retries: 5 + delay: 30 + until: update_result.reboot_required == false - - name: Logging action - vars: - ansible_connection: local - ansible.builtin.shell: - cmd: echo "Update {{ os_project }}-{{ ansible_hostname }} $(date '+%Y-%m-%d %H:%M:%S')" >> /deployments/{{ os_project }}-{{ win_project }}/action.log - delegate_to: localhost + - name: Wait for host to come back online + ansible.builtin.wait_for_connection: + timeout: 600 + when: "inventory_hostname in groups['meta-type_server']" diff --git a/ansible/roles/win_update/templates/newXML.py b/ansible/roles/win_update/templates/newXML.py new file mode 100644 index 0000000..3140631 --- /dev/null +++ b/ansible/roles/win_update/templates/newXML.py @@ -0,0 +1,146 @@ +import os +import shutil +import re +import xml.etree.ElementTree as ET + +original_xml = "{{ start_menu_xml }}" +backup_xml = "{{ backup_xml }}" + +# This script assumes backup and copying already handled by Ansible + +START_MENU_PATHS = { + "%ALLUSERSPROFILE%\\Microsoft\\Windows\\Start Menu\\Programs": os.path.expandvars(r"%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs") +} + +def extract_version(filename): + match = re.search(r'(\d+\.\d+\.\d+|\d+)', filename) + if match: + return tuple(map(int, match.group(0).split('.'))) + return (0,) + +def adjust_app_name(app_name): + return f"{app_name} " if len(app_name) == 1 else app_name + +def remove_bracketed_text(filename): + return re.sub(r"[\(\[].*?[\)\]]", "", filename).strip() + +def folder_has_standalone_version(folder_name): + return bool(re.search(r'\b\d+(?:\.\d+){1,3}\b', folder_name)) + +def clean_folder_name(folder_name): + return re.sub(r'\b\d+(?:\.\d+){1,3}\b', '', folder_name).strip() + +def remove_version_number(app_name): + if re.search(r'\d+\.\d+(\.\d+)*$', app_name): + return re.sub(r'(\s*\d+\.\d+(\.\d+)*\s*)$', '', app_name).strip() + return app_name + +def is_extra_word_present(filename, base_name): + name_without_ext = os.path.splitext(filename)[0].lower() + return name_without_ext != base_name.lower() + +def version_sort_key(filename, base_name): + version = extract_version(filename) + cleaned_name = remove_bracketed_text(filename) + padded_version = version + (0,) * (3 - len(version)) + return ( + is_extra_word_present(filename, base_name), + len(cleaned_name), + -padded_version[0], + -padded_version[1] if len(padded_version) > 1 else 0, + -padded_version[2] if len(padded_version) > 2 else 0 + ) + +def get_clean_base_name(app_name): + return re.sub(r"(\s*\d+(\.\d+)*\s*)+$", "", app_name).strip() + +def find_best_match(app_name, available_files): + base_name = get_clean_base_name(app_name) + adjusted_name = adjust_app_name(base_name) + start_matches = [file for file in available_files if file.lower().startswith(adjusted_name.lower())] + exact_match = [f for f in start_matches if os.path.splitext(f)[0].lower() == adjusted_name.lower()] + if exact_match: + print(f"Exact match for {app_name}: {exact_match[0]}") + return exact_match[0] + best = min(start_matches, key=lambda f: version_sort_key(f, adjusted_name)) if start_matches else None + print(f"Best match for {app_name}: {best}") + return best + +def convert_to_env_path(abs_path): + for env_path, abs_root in START_MENU_PATHS.items(): + if abs_path.startswith(abs_root): + return abs_path.replace(abs_root, env_path) + return abs_path + +def find_shortcut_paths(app_names): + app_paths = {} + all_lnk_files = {} + + for _, start_menu in START_MENU_PATHS.items(): + for root, _, files in os.walk(start_menu): + for file in files: + if file.endswith(".lnk"): + full_path = os.path.join(root, file) + all_lnk_files[file] = full_path + + for app_name in app_names: + best_match = find_best_match(app_name, all_lnk_files.keys()) + if best_match: + env_path = convert_to_env_path(all_lnk_files[best_match]) + app_paths[app_name] = env_path + print(f"Mapped {app_name} to {env_path}") + else: + print(f"No match found for {app_name}") + + return app_paths + +def extract_app_names_and_paths(xml_file): + tree = ET.parse(xml_file) + root = tree.getroot() + apps = {} + + for tile in root.findall(".//{*}DesktopApplicationTile"): + link_path = tile.get("DesktopApplicationLinkPath") + if link_path: + folder_name = os.path.basename(os.path.dirname(link_path)) + lnk_name = os.path.splitext(os.path.basename(link_path))[0] + + if folder_has_standalone_version(folder_name): + app_name = clean_folder_name(folder_name) + else: + app_name = remove_bracketed_text(lnk_name) + + app_name = remove_version_number(app_name) + app_name = adjust_app_name(app_name) + apps[app_name] = link_path + print(f"Extracted {app_name} from {link_path}") + + return apps + +def update_xml_paths(xml_file, old_new_paths): + with open(xml_file, "r", encoding="utf-8") as file: + content = file.read() + + for old_path, new_path in old_new_paths: + content = content.replace(old_path, new_path) + print(f"Replaced {old_path} with {new_path}") + + with open(xml_file, "w", encoding="utf-8") as file: + file.write(content) + + print("Start menu XML updated successfully.") + +if __name__ == "__main__": + xml_file = original_xml + extracted_apps_with_paths = extract_app_names_and_paths(xml_file) + app_shortcuts = find_shortcut_paths(extracted_apps_with_paths.keys()) + + old_new_paths = [] + for app_name, old_path in extracted_apps_with_paths.items(): + new_path = app_shortcuts.get(app_name) + if new_path: + old_new_paths.append((old_path, new_path)) + else: + print(f"Warning: No new path found for {app_name}. Keeping old path.") + + update_xml_paths(xml_file, old_new_paths) diff --git a/ansible/roles/win_update/vars/main.yml b/ansible/roles/win_update/vars/main.yml index 47f4512..aba6bed 100644 --- a/ansible/roles/win_update/vars/main.yml +++ b/ansible/roles/win_update/vars/main.yml @@ -1,6 +1,5 @@ --- -services: - - wuauserv - - bits - - cryptSvc - - msiserver +start_menu_xml: "C:\\eidf\\{{ win_project }}.xml" +backup_xml: "C:\\eidf\\{{ win_project }}-old.xml" +script_name: newXML.py +script_path: "C:\\eidf\\{{ script_name }}" \ No newline at end of file