├── .travis.yml ├── LICENSE ├── README.md ├── demos ├── demo_nim_check.yml ├── demo_nim_flrtvc.yml ├── demo_nim_install.yml ├── demo_suma.yml ├── demo_update-all-hosts.yml └── demo_yum.yml ├── docs ├── Ansible_Automate_infrastructure_updates_in_NIM_environment_v1.0.pdf └── Ansible_Automate_vios_update_in_NIM_environment_v0.9.pdf ├── library ├── aix_flrtvc.py ├── aix_nim.py ├── aix_nim_updateios.py ├── aix_nim_upgradeios.py ├── aix_nim_vios_alt_disk.py ├── aix_nim_vios_hc.py ├── aix_nim_viosupgrade.py ├── aix_suma.py └── suma.py ├── meta └── main.yml ├── playbook_aix_flrtvc.yml ├── playbook_aix_nim_check.yml ├── playbook_aix_nim_reboot.yml ├── playbook_aix_nim_vios_altdisk.yml ├── playbook_aix_nim_vios_hc.yml ├── playbook_aix_nim_vios_update.yml ├── playbook_aix_nim_vios_upgrade.yml ├── playbook_aix_nim_viosupgrade.yml ├── playbook_aix_suma.yml ├── playbook_aix_suma_nim.yml ├── playbook_aix_suma_targets_all.yml ├── playbook_aix_suma_targets_list.yml ├── playbook_aix_suma_targets_range.yml └── playbook_aix_suma_targets_star.yml /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "2.7" 5 | 6 | # command to install dependencies 7 | install: 8 | - pip install ansible 9 | #- pip install pylint 10 | - pip install flake8 11 | - pip install pycodestyle 12 | - ansible --version 13 | 14 | # command to run tests 15 | script: 16 | # With pylint ignore 17 | # logging-format-interpolation: Use % formatting in logging functions 18 | # global-statement : Using the global statement 19 | #- pylint library/* -d logging-format-interpolation -d global-statement 20 | 21 | # With flake ignore 22 | # F403: import *' used; unable to detect undefined names 23 | # F405: may be undefined, or defined from star imports 24 | # W503: line break before binary operator (PEP8 advices to put logical operator ahead) 25 | #- flake8 library/* --max-line-length=100 --ignore F403,F405,W503 26 | - flake8 library/* --max-line-length=100 27 | 28 | # pycodestyle (PEP8) 29 | - pycodestyle library/* --max-line-length=100 30 | 31 | # checking yaml syntax is not relevant since there are just examples. 32 | #- ansible-playbook playbook_aix_flrtvc.yml --syntax-check 33 | #- ansible-playbook playbook_aix_suma_targets_all.yml --syntax-check 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Updates in a NIM environment 2 | 3 | # AIX for Ansible 4 | 5 | ## Requirements 6 | 7 | ### Platforms 8 | 9 | - AIX 6.1 10 | - AIX 7.1 11 | - AIX 7.2 12 | 13 | ### Ansible 14 | 15 | - Requires Ansible 1.2 or newer 16 | 17 | ## Resources 18 | 19 | ### SUMA 20 | 21 | Creates a task to automate the download of technology levels (TL) and service packs (SP) from a fix server. 22 | 23 | Must be described in yaml format with the follwoing parameters: 24 | 25 | ```yaml 26 | aix_suma: 27 | oslevel: required; specifies the OS level to update to; 28 | "latest" indicates the latest level of the higher TL among 29 | the target; based on the fix server, aix_suma will determine 30 | the actual oslevel necessary to update the targets 31 | and create the corresponding NIM resource on the NIM server; 32 | xxxx-xx(-00-0000): sepcifies a TL; 33 | xxxx-xx-xx-xxxx or xxxx-xx-xx: specifies a SP. 34 | location: required; if it is an absolute path, specifies the directory where the 35 | packages will be downloaded on the NIM server; 36 | if it is a filename, specifies the lpp_source_name 37 | targets: required; specifies the NIM clients to update; 38 | "foo*": all the NIM clients with name starting by "foo"; 39 | "foo[2:4]": designates the NIM clients among foo2, foo3 and foo4; 40 | "*" or "ALL": all the NIM clients. 41 | action: required; specifies to action to be performed; 42 | possible values: "download" to download all the fixes and create 43 | the associated NIM resources; 44 | or "preview" to execute all the checks without 45 | downloading the fixes. 46 | ``` 47 | 48 | ### NIM 49 | 50 | Creates a task to update targets. 51 | 52 | Must be described in yaml format with the following parameters: 53 | 54 | ```yaml 55 | aix_nim: 56 | lpp_source: indicates the lpp_source to apply to the targets; 57 | "latest_tl", "latest_sp", "next_tl" and "next_sp" can be specified; 58 | based on the NIM server resources, aix_nim will determine 59 | the actual oslevel necessary to update the targets. 60 | targets: specifies the NIM clients to update; 61 | "foo*" designates all the NIM clients with name starting by "foo"; 62 | "foo[2:4]" designates the NIM clients among foo2, foo3 and foo4; 63 | "*" or "ALL" designates all the NIM clients. 64 | async: boolean; 65 | if set to "False" (default), a NIM client will be completely 66 | installed before starting the installation of another NIM client; 67 | if "lpp_source" is set to "latest_xx" or "next_xx", this parameter 68 | is set to "false". 69 | action: required; specifies the action to perform; 70 | possible values: "update", "check" or "reboot"; 71 | "update" performs an updates of the targets; 72 | requires "lpp_source" and "targets" to be specified; 73 | "check" displays the oslevel of the targets and their NIM status; 74 | requires "targets" to be specified; 75 | "reboot" reboots the targets. "targets" must be specified. 76 | 77 | ``` 78 | 79 | ### FLRTVC 80 | 81 | Creates a task to check targets vulnerability against available fixes, and apply them necessary fixes 82 | 83 | Must be described in yaml format with the following parameters: 84 | 85 | ```yaml 86 | aix_flrtvc: 87 | targets: required; specifies the NIM clients to update; 88 | "foo*" designates all the NIM clients with name starting by "foo"; 89 | "foo[2:4]" designates the NIM clients among foo2, foo3 and foo4; 90 | "*" or "ALL" designates all tne NIM clients. 91 | path: Working directory used for temporary files; 92 | it will contain FLRTVC reports; 93 | if not specified "/tmp/ansible" is used. 94 | apar: type of apar to check or download; 95 | "sec" security fixes; 96 | "hiper" corrections to High Impact PERvasive threats; 97 | "all" default value; both "sec" fixes and "hiper" fixes. 98 | filesets: only fixes on the filesets specified will be checked and updated. 99 | csv: path to a file containing the description of the "sec" and "hiper" fixes; 100 | this file is usually transferred form the fix server; 101 | this rather big transfer can be avoided by specifying 102 | an already transferred file. 103 | check_only: boolean; 104 | if set to "True", only checks if fixes are already applied 105 | on the targets. 106 | download_only: boolean; 107 | if set to "True", performs "check_only" and downloads the fixes 108 | (no update of the targets). 109 | clean: boolean; 110 | if set to "True", remove the working directory at the end of execution; 111 | (default "False") 112 | force: boolean; 113 | if set to "True", remove currently installed ifix before running flrtvc; 114 | (default "False") 115 | 116 | ``` 117 | 118 | ### UPDATEIOS 119 | 120 | Updates the Virtual I/O Server. 121 | 122 | Must be described in yaml format with the following parameters: 123 | 124 | ```yaml 125 | aix_nim_updateios: 126 | targets: required; a list of VIOS to act upon depending on the "action" specified; 127 | to perform an update on dual VIOS, specify the list as a tuple 128 | with the following format : "(gdrh9v1, gdrh9v2) (gdrh10v1, gdrh10v2)”; 129 | to specify a single VIOS, use the following format : "(gdrh11v0)". 130 | lpp_source: the resource that will provide the installation images; 131 | required in case of "install". 132 | filesets: a list of filesets to act upon on each of the targets 133 | depending on the "action" specified. 134 | installp_bundle: the resource that lists the filesets to act upon on each of the targets 135 | depending on the "action" specified; 136 | "filesets" and "installp_bundle" are mutually exclusive. 137 | accept_licenses: specify whether the software licenses should be automatically accepted 138 | during the installation; 139 | default value: "yes". 140 | action: required; the operation to perform on the VIOS; 141 | possible values are : "install", "commit", "reject", "cleanup" and "remove"; 142 | "reject" is not supported by the latest version of updateios. 143 | preview: specify that only a preview operation will be performed 144 | (the action itself will not be performed); 145 | default value: "yes". 146 | time_limit: when this parameter is specified, before starting the updateios action 147 | specified on a new VIOS in the "targets" list, the actual date is compared 148 | to this parameter value; if it is greater then the task is stopped; 149 | the format is mm/dd/yyyy hh:mm 150 | vios_status: specify the result of previous operation. This allows to combine severals 151 | tasks that depend on the result of previous operation. 152 | vars: specify playbook's variables to use (log_file for example); 153 | if myvars is the playbook hash, use vars: "{{ myvars }}" 154 | nim_node: allows to pass along NIM node info from a task to another so that it 155 | discovers NIM info only one time for all tasks; 156 | if you use: "register: backup_result", you can specify the following 157 | nim_node: "{{ backup_result.nim_node }}" 158 | ``` 159 | 160 | ### VIOS HEALTH CHECK 161 | 162 | Performs a health check of VIOS before updating. 163 | 164 | Requires vioshc.py as a prerequisite. 165 | vioshc.py is available on https://github.com/aixoss/vios-health-checker. 166 | 167 | Must be described in yaml format with the following parameters: 168 | 169 | ```yaml 170 | aix_nim_vios_hc: 171 | targets: required; a list of VIOS to act upon depending on the "action" specified; 172 | to perform a health check on dual VIOS, specify the list as a tuple 173 | with the following format : "(gdrh9v1, gdrh9v2) (gdrh10v1, gdrh10v2)”; 174 | to specify a single VIOS, use the following format : "(gdrh11v0)". 175 | action: required; the operation to perform on the VIOS; 176 | must be set to "health_check". 177 | vars: specify playbook's variables to use (log_file for example); 178 | if myvars is the playbook hash, use vars: "{{ myvars }}" 179 | 180 | ``` 181 | 182 | ### ALTERNATE DISK COPY on a VIOS 183 | 184 | Performs alternate disk copy on a VIOS (before update). 185 | 186 | Must be described in yaml format with the following parameters: 187 | 188 | ```yaml 189 | aix_nim_vios_alt_disk: 190 | targets: required; a list of VIOS to act upon depending on the "action" specified; 191 | use a tuple format with the 1st element the VIOS and the 2nd element 192 | the disk used for the alternate disk copy; 193 | for a dual VIOS, the format will look like : "(vios1,disk1,vios2,disk2)"; 194 | for a single VIOS, the format will look like : "(vios1,disk1)". 195 | action: required; the operation to perform on the VIOS; 196 | 2 possible values : "alt_disk_copy" and "alt_disk_clean". 197 | disk_size_policy: specify how the choose the alternate disk if not specified; 198 | 4 possible values : "nearest" (default), "lower", "upper", "minimize". 199 | time_limit: when this parameter is specified, before starting the altternate disk action 200 | specified on a new VIOS in the "targets" list, the actual date is compared 201 | to this parameter value; if it is greater then the task is stopped 202 | the format is mm/dd/yyyy hh:mm 203 | force: when set to "yes", any existing altinst_rootvg is cleaned before looking for 204 | an alternate disk for the copy operation. 205 | vios_status: specify the result of previous operation. This allows to combine severals 206 | tasks that depend on the result of previous operation. 207 | vars: specify playbook's variables to use (log_file for example); 208 | if myvars is the playbook hash, use vars: "{{ myvars }}" 209 | nim_node: allows to pass along NIM node info from a task to another so that it 210 | discovers NIM info only one time for all tasks; 211 | if you use: "register: backup_result", you can specify the following 212 | nim_node: "{{ backup_result.nim_node }}" 213 | 214 | ``` 215 | 216 | ### UPGRADEIOS 217 | 218 | Upgrades the Virtual I/O Server using NIM and viosbr. 219 | 220 | Must be described in yaml format with the following parameters: 221 | 222 | ```yaml 223 | aix_nim_upgradeios: 224 | targets: required; a list of VIOS to act upon depending on the "action" specified; 225 | to perform an action on dual VIOS, specify the list as a tuple 226 | with the following format : "(gdrh9v1, gdrh9v2) (gdrh10v1, gdrh10v2)”; 227 | to specify a single VIOS, use the following format : "(gdrh11v0)". 228 | action: required; the operation to perform on the VIOS; 229 | possible values are : "backup", "view_backup", "upgrade_restore", "all". 230 | email: email address to set in the NIM master's /etc/niminfo file if not already 231 | set with: export NIM_MASTER_UID= 232 | location: existing directory to store the ios_backup on the NIM master; 233 | required for if "action" is "backup". 234 | backup_prefix: prefix of the ios_backup NIM resource; the name of the target VIOS is 235 | added to this prefix; 236 | default value: "ios_backup_". 237 | force: when set to "yes", any existing ios_backup NIM resource for each target 238 | is removed before performing the backup creation; supported for "backup" 239 | action; 240 | default value: "no". 241 | boot_client: specify whether the clients of the target VIOS should be booted after the 242 | upgrade and restore operation; can be used for "upgrade_restore" and 243 | "all" actions; 244 | default value: "no". 245 | resolv_conf: specify the NIM resource to use for the VIOS installation; 246 | required for "upgrade_restore" and "all" actions; 247 | spot_prefix: prefix of the Shared product Object Tree (SPOT) NIM resource to use for 248 | the VIOS installation; the NIM name of the target VIOS is added to find 249 | the actual NIM resource, like: "_"; this resource 250 | must exists prior the playbook execution; 251 | required for "upgrade_restore" action; 252 | mksysb_prefix: prefix of the mksysb NIM resource to use for the VIOS installation; the 253 | NIM name of the target VIOS is added to this prefix to find the actual 254 | NIM resource, like: "_"; 255 | this resource must exists prior the playbook execution; 256 | required for "upgrade_restore" and "all" actions; 257 | bosinst_data_prefix: prefix of the bosinst_data NIM resource that contains the BOS 258 | installation program to use; the NIM name of the target VIOS is added to 259 | this prefix to find the actual NIM resource, like: 260 | "_"; this resource must exists prior the 261 | playbook execution; 262 | required for "upgrade_restore" and "all" actions; 263 | time_limit: when this parameter is specified, before starting the upgradeios action 264 | specified on a new VIOS in the "targets" list, the actual date is 265 | compared to this parameter value; if it is greater then the task stops; 266 | the format is mm/dd/yyyy hh:mm 267 | vios_status: specify the result of previous operation. This allows to combine 268 | severals tasks that depend on the result of previous operation. 269 | vars: specify playbook's variables to use (log_file for example); 270 | if myvars is the playbook hash, use vars: "{{ myvars }}" 271 | nim_node: allows to pass along NIM node info from a task to another so that it 272 | discovers NIM info only one time for all tasks; 273 | if you use: "register: backup_result", you can specify the following 274 | nim_node: "{{ backup_result.nim_node }}" 275 | ``` 276 | -------------------------------------------------------------------------------- /demos/demo_nim_check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "NIM check on AIX playbook" 3 | hosts: nim 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: "AIX NIM" 9 | aix_nim: 10 | action: 'check' 11 | description: 'NIM check' 12 | register: nim_output 13 | - debug: var=nim_output 14 | 15 | -------------------------------------------------------------------------------- /demos/demo_nim_flrtvc.yml: -------------------------------------------------------------------------------- 1 | - name: "NIM FLRTVC on AIX playbook" 2 | hosts: nim 3 | gather_facts: no 4 | tasks: 5 | - name: "FLRTVC" 6 | aix_flrtvc: 7 | targets: 'quimby01' 8 | path: /export/nim/ansible 9 | verbose: yes 10 | apar: all 11 | force: no 12 | clean: False 13 | download_only: no 14 | register: result 15 | - debug: var=result 16 | -------------------------------------------------------------------------------- /demos/demo_nim_install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "NIM BOS install using mksysb image" 3 | hosts: nim 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: Install using group resource 9 | aix_nim: 10 | action: 'bos_inst' 11 | targets: 'quimby03' 12 | group: 'basic_res_grp' 13 | description: 'NIM -o bos_inst -a source=mksysb' 14 | register: nim_output 15 | - debug: var=nim_output 16 | -------------------------------------------------------------------------------- /demos/demo_suma.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA on AIX" 3 | hosts: all 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: Check oslevel of system 9 | shell: "oslevel -s" 10 | register: output 11 | - debug: var=output 12 | 13 | # - name: Expand /usr filesystem 14 | # shell: "chfs -a size=+5G /usr" 15 | # register: output 16 | # - debug: var=output 17 | 18 | # - name: Verify log directory exists 19 | # file: 20 | # path: /var/adm/ansible 21 | # state: directory 22 | # register: output 23 | # - debug: var=output 24 | 25 | - name: Check for, and install, system updates 26 | suma: 27 | oslevel: 'latest' 28 | location: '/usr/sys/inst.images' 29 | action: download 30 | install_updates: true 31 | ignore_errors: True 32 | register: output 33 | - debug: var=output 34 | 35 | # - name: Expand /opt filesystem 36 | # shell: "chfs -a size=+500M /opt" 37 | # register: output 38 | # - debug: var=output 39 | 40 | # - name: Perform YuM check-updates 41 | # yum: 42 | # name: '*' 43 | # state: latest 44 | # register: output 45 | # - debug: var=output 46 | 47 | - name: Check for new oslevel 48 | shell: "oslevel -s" 49 | register: output 50 | - debug: var=output 51 | 52 | # - name: Restart with newest kernel 53 | # shell: "sleep 5 && reboot" 54 | # async: 1 55 | # poll: 0 56 | # register: output 57 | # - debug: var=output 58 | -------------------------------------------------------------------------------- /demos/demo_update-all-hosts.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "Update all hosts - x86, power" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | - name: Check oslevel of AIX system 9 | shell: "oslevel -s" 10 | when: ansible_distribution == 'AIX' 11 | register: output 12 | - debug: var=output 13 | 14 | 15 | - name: Check for, and install, system updates 16 | suma: 17 | oslevel: 'latest' 18 | # download_dir: '/usr/sys/inst.images' 19 | download_only: False 20 | when: ansible_distribution == 'AIX' 21 | ignore_errors: True 22 | register: output 23 | - debug: var=output 24 | 25 | 26 | - name: Check for new oslevel 27 | shell: "oslevel -s" 28 | when: ansible_distribution == 'AIX' 29 | register: output 30 | - debug: var=output 31 | 32 | 33 | - name: Perform YuM check-updates 34 | yum: 35 | name: '*' 36 | state: latest 37 | when: ansible_distribution == 'CentOS' 38 | register: output 39 | - debug: var=output 40 | 41 | 42 | - name: Restart with newest kernel 43 | shell: "sleep 5 && reboot" 44 | async: 1 45 | poll: 0 46 | register: output 47 | - debug: var=output 48 | -------------------------------------------------------------------------------- /demos/demo_yum.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "YuM Check-Update" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | - name: Upgrade packages, excluding kernel related packages 9 | yum: 10 | name: '*' 11 | state: latest 12 | exclude: kernel* 13 | register: output 14 | - debug: var=output 15 | -------------------------------------------------------------------------------- /docs/Ansible_Automate_infrastructure_updates_in_NIM_environment_v1.0.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aixoss/ansible-playbooks/15541820de57b58b6fe2216fceacb5ffd575b9f2/docs/Ansible_Automate_infrastructure_updates_in_NIM_environment_v1.0.pdf -------------------------------------------------------------------------------- /docs/Ansible_Automate_vios_update_in_NIM_environment_v0.9.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aixoss/ansible-playbooks/15541820de57b58b6fe2216fceacb5ffd575b9f2/docs/Ansible_Automate_vios_update_in_NIM_environment_v0.9.pdf -------------------------------------------------------------------------------- /library/aix_nim_updateios.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2017, International Business Machines Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ############################################################################ 18 | """AIX VIOS NIM Update: tools to update a list of one or a pair of VIOSes""" 19 | 20 | import os 21 | import re 22 | import subprocess 23 | import threading 24 | import logging 25 | import time 26 | 27 | # Ansible module 'boilerplate' 28 | # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin 29 | from ansible.module_utils.basic import AnsibleModule 30 | 31 | DOCUMENTATION = """ 32 | --- 33 | module: aix_nim_updateios 34 | authors: Alain Poncet, Patrice Jacquin, Vianney Robin 35 | short_description: Perform a VIOS update with NIM 36 | """ 37 | 38 | 39 | # ---------------------------------------------------------------- 40 | # ---------------------------------------------------------------- 41 | def exec_cmd(cmd, module, exit_on_error=False, debug_data=True, shell=False): 42 | """ 43 | Execute the given command 44 | 45 | Note: If executed in thread, fail_json does not exit the parent 46 | 47 | args: 48 | - cmd array of the command parameters 49 | - module the module variable 50 | - exit_on_error use fail_json if true and cmd return !0 51 | - debug_data prints some trace in DEBUG_DATA if set 52 | - shell execute cmd through the shell if set (vulnerable to shell 53 | injection when cmd is from user inputs). If cmd is a string 54 | string, the string specifies the command to execute through 55 | the shell. If cmd is a list, the first item specifies the 56 | command, and other items are arguments to the shell itself. 57 | return 58 | - ret return code of the command 59 | - output output and stderr of the command 60 | - errout command stderr 61 | """ 62 | 63 | global DEBUG_DATA 64 | global CHANGED 65 | global OUTPUT 66 | 67 | ret = 0 68 | output = '' 69 | errout = '' 70 | 71 | th_id = threading.current_thread().ident 72 | stderr_file = '/tmp/ansible_updateios_cmd_stderr_{}'.format(th_id) 73 | 74 | logging.debug('command:{}'.format(cmd)) 75 | if debug_data is True: 76 | DEBUG_DATA.append('exec_cmd:{}'.format(cmd)) 77 | try: 78 | myfile = open(stderr_file, 'w') 79 | output = subprocess.check_output(cmd, stderr=myfile, shell=shell) 80 | myfile.close() 81 | s = re.search(r'rc=([-\d]+)$', output) 82 | if s: 83 | ret = int(s.group(1)) 84 | output = re.sub(r'rc=[-\d]+\n$', '', output) # remove the rc of c_rsh with echo $? 85 | 86 | except subprocess.CalledProcessError as exc: 87 | myfile.close() 88 | errout = re.sub(r'rc=[-\d]+\n$', '', exc.output) # remove the rc of c_rsh with echo $? 89 | ret = exc.returncode 90 | 91 | except OSError as exc: 92 | myfile.close() 93 | errout = re.sub(r'rc=[-\d]+\n$', '', exc.args[1]) # remove the rc of c_rsh with echo $? 94 | ret = exc.args[0] 95 | 96 | except IOError as exc: 97 | # generic exception 98 | myfile.close() 99 | msg = 'Command: {} Exception: {}'.format(cmd, exc) 100 | ret = 1 101 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 102 | 103 | # check for error message 104 | if os.path.getsize(stderr_file) > 0: 105 | myfile = open(stderr_file, 'r') 106 | errout += ''.join(myfile) 107 | myfile.close() 108 | os.remove(stderr_file) 109 | 110 | if debug_data is True: 111 | DEBUG_DATA.append('exec_cmd rc:{}, output:{} errout:{}' 112 | .format(ret, output, errout)) 113 | logging.debug('retrun rc:{}, output:{} errout:{}' 114 | .format(ret, output, errout)) 115 | 116 | if ret != 0 and exit_on_error is True: 117 | msg = 'Command: {} RetCode:{} ... stdout:{} stderr:{}'\ 118 | .format(cmd, ret, output, errout) 119 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 120 | 121 | return (ret, output, errout) 122 | 123 | 124 | # ---------------------------------------------------------------- 125 | # ---------------------------------------------------------------- 126 | def get_nim_clients_info(module, lpar_type): 127 | """ 128 | Get the list of the lpar (standalones or vios) defined on the nim master, and get their 129 | cstate. 130 | 131 | return the list of the name of the lpar objects defined on the 132 | nim master and their associated cstate value 133 | """ 134 | global CHANGED 135 | global OUTPUT 136 | std_out = '' 137 | info_hash = {} 138 | 139 | cmd = 'LC_ALL=C lsnim -t {} -l'.format(lpar_type) 140 | (ret, std_out, std_err) = exec_cmd(cmd, module, shell=True) 141 | if ret != 0: 142 | msg = 'Cannot list NIM {} objects: {}'.format(lpar_type, std_err) 143 | logging.error(msg) 144 | module.fail_json(changed=CHANGED, msg=msg, meta=OUTPUT) 145 | 146 | # lpar name and associated Cstate 147 | obj_key = "" 148 | for line in std_out.split('\n'): 149 | line = line.rstrip() 150 | match_key = re.match(r"^(\S+):", line) 151 | if match_key: 152 | obj_key = match_key.group(1) 153 | info_hash[obj_key] = {} 154 | continue 155 | 156 | match_cstate = re.match(r"^\s+Cstate\s+=\s+(.*)$", line) 157 | if match_cstate: 158 | cstate = match_cstate.group(1) 159 | info_hash[obj_key]['cstate'] = cstate 160 | continue 161 | 162 | # For VIOS store the management profile 163 | if lpar_type == 'vios': 164 | # Not used in this module so far 165 | # match_mgmtprof = re.match(r"^\s+mgmt_profile1\s+=\s+(.*)$", line) 166 | # if match_mgmtprof: 167 | # mgmt_elts = match_mgmtprof.group(1).split() 168 | # if len(mgmt_elts) == 3: 169 | # info_hash[obj_key]['mgmt_hmc_id'] = mgmt_elts[0] 170 | # info_hash[obj_key]['mgmt_vios_id'] = mgmt_elts[1] 171 | # info_hash[obj_key]['mgmt_cec_serial'] = mgmt_elts[2] 172 | # else: 173 | # logging.warning('WARNING: VIOS {} management profile has not 3 elements: {}'. 174 | # format(obj_key, match_mgmtprof.group(1))) 175 | # continue 176 | 177 | # Get VIOS interface info in case we need c_rsh 178 | match_if = re.match(r"^\s+if1\s+=\s+\S+\s+(\S+)\s+.*$", line) 179 | if match_if: 180 | info_hash[obj_key]['vios_ip'] = match_if.group(1) 181 | continue 182 | 183 | return info_hash 184 | 185 | 186 | # ---------------------------------------------------------------- 187 | # ---------------------------------------------------------------- 188 | def build_nim_node(module): 189 | """ 190 | build the nim node containing the nim vios and hmcinfo. 191 | 192 | arguments: 193 | None 194 | 195 | return: 196 | None 197 | """ 198 | 199 | global NIM_NODE 200 | 201 | # ========================================================================= 202 | # Build vios info list 203 | # ========================================================================= 204 | nim_vios = {} 205 | nim_vios = get_nim_clients_info(module, 'vios') 206 | 207 | NIM_NODE['nim_vios'] = nim_vios 208 | logging.debug('NIM VIOS: {}'.format(nim_vios)) 209 | 210 | 211 | # ---------------------------------------------------------------- 212 | # ---------------------------------------------------------------- 213 | def check_lpp_source(module, lpp_source): 214 | """ 215 | Check to make sure lpp_source exists 216 | - module the module variable 217 | - lpp_source lpp_source param provided by module 218 | In case lpp_source does not exist fail the module 219 | return 220 | - exists True 221 | """ 222 | global OUTPUT 223 | global CHANGED 224 | 225 | # find location of lpp_source 226 | cmd = ['lsnim', '-a', 'location', lpp_source] 227 | (ret, std_out, std_err) = exec_cmd(cmd, module) 228 | if ret != 0: 229 | msg = 'Cannot find location of lpp_source {}, lsnim returns: {}'\ 230 | .format(lpp_source, std_err) 231 | logging.error(msg) 232 | OUTPUT.append(msg) 233 | module.fail_json(changed=CHANGED, msg=msg, meta=OUTPUT) 234 | location = std_out.split()[3] 235 | 236 | # check to make sure path exists 237 | cmd = ['/bin/find', location] 238 | (ret, std_out, std_err) = exec_cmd(cmd, module) 239 | if ret != 0: 240 | msg = 'Cannot find location of lpp_source {}: {}'\ 241 | .format(lpp_source, std_err) 242 | logging.error(msg) 243 | OUTPUT.append(msg) 244 | module.fail_json(changed=CHANGED, msg=msg, meta=OUTPUT) 245 | 246 | return True 247 | 248 | 249 | # ---------------------------------------------------------------- 250 | # ---------------------------------------------------------------- 251 | def check_vios_targets(module, targets): 252 | """ 253 | check the list of the vios targets. 254 | 255 | a target name could be of the following form: 256 | (vios1, vios2) (vios3) 257 | 258 | arguments: 259 | module (hash): the Ansible module 260 | targets (str): list of tuple of NIM name of vios machine 261 | 262 | return: the list of the existing vios tuple matching the target list 263 | """ 264 | global NIM_NODE 265 | 266 | vios_list = {} 267 | vios_list_tuples_res = [] 268 | vios_list_tuples = targets.replace(" ", "").replace("),(", ")(").split('(') 269 | 270 | # =========================================== 271 | # Build targets list 272 | # =========================================== 273 | for vios_tuple in vios_list_tuples[1:]: 274 | logging.debug('Checking vios_tuple: {}'.format(vios_tuple)) 275 | 276 | tuple_elts = list(vios_tuple[:-1].split(',')) 277 | tuple_len = len(tuple_elts) 278 | 279 | if tuple_len != 1 and tuple_len != 2: 280 | OUTPUT.append('Malformed VIOS targets {}. Tuple {} should be a 1 or 2 elements.' 281 | .format(targets, tuple_elts)) 282 | logging.error('Malformed VIOS targets {}. Tuple {} should be a 1 or 2 elements.' 283 | .format(targets, tuple_elts)) 284 | return None 285 | 286 | # check vios not already exists in the target list 287 | if tuple_elts[0] in vios_list or (tuple_len == 2 288 | and (tuple_elts[1] in vios_list or tuple_elts[0] == tuple_elts[1])): 289 | OUTPUT.append('Malformed VIOS targets {}. Duplicated VIOS' 290 | .format(targets)) 291 | logging.error('Malformed VIOS targets {}. Duplicated VIOS' 292 | .format(targets)) 293 | return None 294 | 295 | # check vios is knowed by the NIM master - if not ignore it 296 | if tuple_elts[0] not in NIM_NODE['nim_vios']: 297 | msg = "VIOS {} is not client of the NIM master, will be ignored"\ 298 | .format(tuple_elts[0]) 299 | OUTPUT.append(msg) 300 | logging.warn(msg) 301 | continue 302 | if tuple_len == 2 and tuple_elts[1] not in NIM_NODE['nim_vios']: 303 | msg = "VIOS {} is not client of the NIM master, will be ignored"\ 304 | .format(tuple_elts[1]) 305 | OUTPUT.append(msg) 306 | logging.warn(msg) 307 | continue 308 | 309 | # check vios connectivity 310 | res = 0 311 | for elem in tuple_elts: 312 | cmd = ['/usr/lpp/bos.sysmgt/nim/methods/c_rsh', elem, 313 | '"/usr/bin/ls /dev/null; echo rc=$?"'] 314 | (ret, std_out, std_err) = exec_cmd(cmd, module) 315 | if ret != 0: 316 | res = 1 317 | msg = 'skipping {}: cannot reach {} with c_rsh: {}, {}, {}'\ 318 | .format(vios_tuple, elem, res, std_out, std_err) 319 | logging.info(msg) 320 | continue 321 | if res != 0: 322 | continue 323 | 324 | if tuple_len == 2: 325 | vios_list[tuple_elts[0]] = tuple_elts[1] 326 | vios_list[tuple_elts[1]] = tuple_elts[0] 327 | # vios_list = vios_list.extend([tuple_elts[0], tuple_elts[1]]) 328 | my_tuple = (tuple_elts[0], tuple_elts[1]) 329 | vios_list_tuples_res.append(tuple(my_tuple)) 330 | else: 331 | vios_list[tuple_elts[0]] = tuple_elts[0] 332 | # vios_list.append(tuple_elts[0]) 333 | my_tuple = (tuple_elts[0],) 334 | vios_list_tuples_res.append(tuple(my_tuple)) 335 | 336 | return vios_list_tuples_res 337 | 338 | 339 | # ---------------------------------------------------------------- 340 | # ---------------------------------------------------------------- 341 | def get_vios_ssp_status(module, target_tuple, vios_key, update_op_tab): 342 | """ 343 | Check the SSP status of the VIOS tuple 344 | Update IOS can only be performed when both VIOSes in the tuple 345 | refer to the same cluster and have the same SSP status 346 | return 347 | 0 if OK 348 | 1 else 349 | """ 350 | 351 | global NIM_NODE 352 | 353 | ssp_name = '' 354 | vios_name = '' 355 | vios_ssp_status = '' 356 | err_label = 'FAILURE-SSP' 357 | cluster_found = False 358 | tuple_len = len(target_tuple) 359 | 360 | for vios in target_tuple: 361 | NIM_NODE['nim_vios'][vios]['ssp_status'] = 'none' 362 | 363 | # get the SSP status 364 | for vios in target_tuple: 365 | cmd = ['/usr/lpp/bos.sysmgt/nim/methods/c_rsh', 366 | NIM_NODE['nim_vios'][vios]['vios_ip'], 367 | '"LC_ALL=C /usr/ios/cli/ioscli cluster -list &&' 368 | ' /usr/ios/cli/ioscli cluster -status -fmt : ; echo rc=$?"'] 369 | 370 | (ret, std_out, std_err) = exec_cmd(cmd, module) 371 | if ret != 0: 372 | std_out = std_out.rstrip() 373 | if std_out.find('Cluster does not exist') != -1: 374 | logging.debug('There is no cluster or the node {} is DOWN' 375 | .format(vios)) 376 | NIM_NODE['nim_vios'][vios]['vios_ssp_status'] = 'DOWN' 377 | if tuple_len == 1: 378 | return 0 379 | else: 380 | continue 381 | else: 382 | update_op_tab[vios_key] = err_label 383 | OUTPUT.append(' Failed to get the SSP status for {}: {} {}' 384 | .format(vios, std_out, std_err)) 385 | logging.error('Failed to get the SSP status for {}: {} {} {}' 386 | .format(vios, ret, std_out, std_err)) 387 | return 1 388 | cluster_found = True 389 | 390 | # check that the VIOSes belong to the same cluster and have the same satus 391 | # or there is no SSP 392 | # stdout is like: 393 | # gdr_ssp3:OK:castor_gdr_vios3:8284-22A0221FD4BV:17:OK:OK 394 | # gdr_ssp3:OK:castor_gdr_vios2:8284-22A0221FD4BV:16:OK:OK 395 | # or 396 | # Cluster does not exist. 397 | # 398 | for line in std_out.split('\n'): 399 | line = line.rstrip() 400 | match_key = re.match(r"^(\S+):(\S+):(\S+):\S+:\S+:(\S+):.*", line) 401 | if not match_key: 402 | logging.debug('cluster line: "{}" does not match'.format(line)) 403 | continue 404 | 405 | if match_key.group(3) not in target_tuple: 406 | continue 407 | 408 | cur_ssp_name = match_key.group(1) 409 | # cur_ssp_satus = match_key.group(2) 410 | cur_vios_name = match_key.group(3) 411 | cur_vios_ssp_status = match_key.group(4) 412 | 413 | NIM_NODE['nim_vios'][cur_vios_name]['vios_ssp_status'] = cur_vios_ssp_status 414 | NIM_NODE['nim_vios'][cur_vios_name]['ssp_name'] = cur_ssp_name 415 | # single VIOS case 416 | if tuple_len == 1: 417 | if cur_vios_ssp_status == 'OK': 418 | err_msg = 'SSP is active for the single VIOS: {}.'\ 419 | ' VIOS cannot be updated'\ 420 | .format(cur_vios_name) 421 | OUTPUT.append(' ' + err_msg) 422 | logging.error(err_msg) 423 | update_op_tab[vios_key] = err_label 424 | return 1 425 | return 0 426 | 427 | # first VIOS in the pair 428 | if ssp_name == '': 429 | ssp_name = cur_ssp_name 430 | vios_name = cur_vios_name 431 | vios_ssp_status = cur_vios_ssp_status 432 | continue 433 | 434 | # both VIOSes found 435 | if vios_ssp_status != cur_vios_ssp_status: 436 | err_msg = '{} cannot be updated: SSP status differ: {}:{}, {}:{}'\ 437 | .format(vios_key, vios_name, vios_ssp_status, 438 | cur_vios_name, cur_vios_ssp_status) 439 | OUTPUT.append(' ' + err_msg) 440 | logging.error(err_msg) 441 | update_op_tab[vios_key] = err_label 442 | return 1 443 | elif ssp_name != cur_ssp_name and cur_vios_ssp_status == 'OK': 444 | err_msg = '{} cannot be updated: both VIOSes must belong to the same SSP'\ 445 | .format(vios_key) 446 | OUTPUT.append(' ' + err_msg) 447 | logging.error(err_msg) 448 | update_op_tab[vios_key] = err_label 449 | return 1 450 | return 0 451 | 452 | if cluster_found is True: 453 | err_msg = '{} cannot be updated: only one VIOS belongs to an SSP'.format(vios_key) 454 | OUTPUT.append(' ' + err_msg) 455 | logging.error(err_msg) 456 | update_op_tab[vios_key] = err_label 457 | return 1 458 | return 0 459 | 460 | 461 | # ---------------------------------------------------------------- 462 | # ---------------------------------------------------------------- 463 | def ssp_stop_start(module, target_tuple, vios, action): 464 | """ 465 | stop/start the SSP for a VIOS 466 | return 467 | 0 if OK 468 | 1 else 469 | """ 470 | 471 | global NIM_NODE 472 | global OUTPUT 473 | 474 | logging.debug("ssp_start_stop {},{},{}".format(target_tuple, vios, action)) 475 | # if action is start SSP, find the first node running SSP 476 | node = vios 477 | if action == "start": 478 | logging.debug("search the vios runing ssp") 479 | for cur_node in target_tuple: 480 | logging.debug("vios:{} ssp status is {}". 481 | format(cur_node, NIM_NODE['nim_vios'][cur_node]['vios_ssp_status'])) 482 | 483 | if NIM_NODE['nim_vios'][cur_node]['vios_ssp_status'] == "OK": 484 | node = cur_node 485 | break 486 | 487 | cmd = ['/usr/lpp/bos.sysmgt/nim/methods/c_rsh', 488 | NIM_NODE['nim_vios'][node]['vios_ip'], 489 | '"/usr/sbin/clctrl -{} -n {} -m {}; echo rc=$?"' 490 | .format(action, NIM_NODE['nim_vios'][vios]['ssp_name'], vios)] 491 | 492 | (ret, std_out, std_err) = exec_cmd(cmd, module) 493 | if ret != 0: 494 | msg = 'Failed to {} SSP cluster on {}: {}'\ 495 | .format(action, NIM_NODE['nim_vios'][vios]['ssp_name'], vios, std_err) 496 | OUTPUT.append(' ' + msg) 497 | logging.error(msg) 498 | return 1 499 | 500 | if action == "stop": 501 | NIM_NODE['nim_vios'][vios]['vios_ssp_status'] = 'DOWN' 502 | else: 503 | NIM_NODE['nim_vios'][vios]['vios_ssp_status'] = 'OK' 504 | 505 | msg = '{} SSP cluster {} on {} succeeded'\ 506 | .format(action, NIM_NODE['nim_vios'][vios]['ssp_name'], vios) 507 | OUTPUT.append(' ' + msg) 508 | logging.info(msg) 509 | return 0 510 | 511 | 512 | # ---------------------------------------------------------------- 513 | # ---------------------------------------------------------------- 514 | def get_updateios_cmd(module): 515 | """ 516 | Assemble the updateios command 517 | - module the module variable 518 | return 519 | - cmd array of the command parameters 520 | """ 521 | global OUTPUT 522 | global CHANGED 523 | 524 | cmd = ['nim', '-o', 'updateios'] 525 | 526 | # lpp source 527 | if module.params['lpp_source']: 528 | if check_lpp_source(module, module.params['lpp_source']): 529 | cmd += ['-a', 'lpp_source=%s' % (module.params['lpp_source'])] 530 | 531 | # accept licenses 532 | if module.params['accept_licenses']: 533 | cmd += ['-a', 'accept_licenses=%s' % (module.params['accept_licenses'])] 534 | else: # default 535 | cmd += ['-a', 'accept_licenses=yes'] 536 | 537 | # updateios flags 538 | cmd += ['-a', 'updateios_flags=-%s' % (module.params['action'])] 539 | 540 | if module.params['action'] == "remove": 541 | if module.params['filesets']: 542 | cmd += ['-a', 'filesets=%s' % (module.params['filesets'])] 543 | elif module.params['installp_bundle']: 544 | cmd += ['-a', 'installp_bundle=%s' % (module.params['installp_bundle'])] 545 | else: 546 | msg = '"filesets" parameter or "installp_bundle" parameter'\ 547 | ' is mandatory with the "remove" action' 548 | logging.error('{}'.format(msg)) 549 | OUTPUT.append('{}'.format(msg)) 550 | module.fail_json(changed=CHANGED, msg=msg, meta=OUTPUT) 551 | else: 552 | if module.params['filesets'] or module.params['installp_bundle']: 553 | logging.info('Discarding attribute filesets {} and installp_bundle {}' 554 | .format(module.params['filesets'], module.params['installp_bundle'])) 555 | OUTPUT.append('Discarding installp_bundle or filesets') 556 | 557 | # preview mode 558 | if module.params['preview']: 559 | cmd += ['-a', 'preview=%s' % (module.params['preview'])] 560 | else: # default 561 | cmd += ['-a', 'preview=yes'] 562 | 563 | return cmd 564 | 565 | 566 | # ---------------------------------------------------------------- 567 | # ---------------------------------------------------------------- 568 | def nim_updateios(module, targets_list, vios_status, update_op_tab, time_limit): 569 | """ 570 | Execute the updateios command 571 | - module the Ansible module 572 | return 573 | - ret return code of nim updateios command 574 | """ 575 | global CHANGED 576 | global OUTPUT 577 | global NIM_NODE 578 | 579 | # build the updateios command from the playbook parameters 580 | updateios_cmd = get_updateios_cmd(module) 581 | 582 | vios_key = [] 583 | for target_tuple in targets_list: 584 | OUTPUT.append('Processing tuple: {}'.format(target_tuple)) 585 | logging.debug('Processing target_tuple: {}'.format(target_tuple)) 586 | 587 | tup_len = len(target_tuple) 588 | vios1 = target_tuple[0] 589 | if tup_len == 2: 590 | vios2 = target_tuple[1] 591 | vios_key = "{}-{}".format(vios1, vios2) 592 | else: 593 | vios_key = vios1 594 | 595 | logging.debug('vios_key: {}'.format(vios_key)) 596 | 597 | # if health check status is known, check the vios tuple has passed 598 | # the health check successfuly 599 | if vios_status is not None: 600 | if vios_key not in vios_status: 601 | update_op_tab[vios_key] = "FAILURE-NO-PREV-STATUS" 602 | OUTPUT.append(" {} vioses skipped (no previous status found)" 603 | .format(vios_key)) 604 | logging.warn("{} vioses skipped (no previous status found)" 605 | .format(vios_key)) 606 | continue 607 | 608 | elif vios_status[vios_key] != 'SUCCESS-ALTDC': 609 | update_op_tab[vios_key] = vios_status[vios_key] 610 | OUTPUT.append(" {} vioses skipped (vios_status: {})" 611 | .format(vios_key, vios_status[vios_key])) 612 | logging.warn("{} vioses skipped (vios_status: {})" 613 | .format(vios_key, vios_status[vios_key])) 614 | continue 615 | 616 | # check if there is time to handle this tuple 617 | if not (time_limit is None) and time.localtime(time.time()) >= time_limit: 618 | time_limit_str = time.strftime("%m/%d/%Y %H:%M", time_limit) 619 | OUTPUT.append(" Time limit {} reached, no further operation" 620 | .format(time_limit_str)) 621 | logging.info('Time limit {} reached, no further operation' 622 | .format(time_limit_str)) 623 | return 0 624 | 625 | # check if SSP is defined for this VIOSes tuple. 626 | ret = get_vios_ssp_status(module, target_tuple, vios_key, update_op_tab) 627 | if ret == 1: 628 | OUTPUT.append(" {} vioses skipped (bad SSP status)".format(vios_key)) 629 | logging.warn('Update operation for {} vioses skipped due to bad SSP status' 630 | .format(vios_key)) 631 | logging.info('Update operation can only be done when both of the VIOSes have' 632 | ' the same SSP status (or for a single VIOS, when the SSP status' 633 | ' is inactive) and belong to the same SSP') 634 | continue 635 | 636 | # TBC - Begin: Uncomment for testing without effective update operation 637 | # OUTPUT.append('Warning: testing without effective update operation') 638 | # OUTPUT.append('NIM Command: {} '.format(updateios_cmd)) 639 | # ret = 0 640 | # std_out = 'NIM Command: {} '.format(updateios_cmd) 641 | # update_op_tab[vios_key] = "SUCCESS-UPDT" 642 | # continue 643 | # TBC - End 644 | 645 | update_op_tab[vios_key] = "SUCCESS-UPDT" 646 | 647 | for vios in target_tuple: 648 | # Commit applied lpps if necessay 649 | if module.params['preview'] and module.params['preview'] == 'no': 650 | OUTPUT.append(' Commit all applied lpps before the update on {}' 651 | .format(vios)) 652 | logging.info('Commit all applied lpps before the update on {}' 653 | .format(vios)) 654 | 655 | cmd_commit = 'LC_ALL=C /usr/sbin/nim -o updateios '\ 656 | '-a updateios_flags=-commit -a filesets=all {} 2>&1'\ 657 | .format(vios) 658 | logging.debug('NIM - Command:{}'.format(cmd_commit)) 659 | 660 | (ret, std_out, std_err) = exec_cmd(cmd_commit, module, shell=True) 661 | 662 | if ret != 0: 663 | if std_err.find('There are no uncommitted updates') == -1: 664 | msg = 'Failed to commit lpps on {}'.format(vios) 665 | logging.warn('{}, {} returned {} {}'.format(msg, cmd_commit, ret, std_err)) 666 | OUTPUT.append(' ' + msg) 667 | else: 668 | OUTPUT.append(' Nothing to commit on {}'.format(vios)) 669 | else: 670 | logging.debug('All applied updates are now committed: {}' 671 | .format(std_out)) 672 | OUTPUT.append(' All applied updates are now committed') 673 | CHANGED = True 674 | 675 | OUTPUT.append(' Updating VIOS: {}'.format(vios)) 676 | 677 | # set the error label to be used in sub routines 678 | err_label = "FAILURE-UPDT1" 679 | if vios != vios1: 680 | err_label = "FAILURE-UPDT2" 681 | 682 | # if needed stop the SSP for the VIOS 683 | restart_needed = False 684 | if NIM_NODE['nim_vios'][vios]['vios_ssp_status'] == 'OK': 685 | ret = ssp_stop_start(module, target_tuple, vios, 'stop') 686 | if ret == 1: 687 | logging.error('SSP stop operation failure for VIOS {}' 688 | .format(vios)) 689 | update_op_tab[vios_key] = err_label 690 | logging.info('VIOS update status for {}: {}' 691 | .format(vios_key, update_op_tab[vios_key])) 692 | break # cannot continue 693 | else: 694 | restart_needed = True 695 | logging.info(' {}: {}'.format(vios_key, update_op_tab[vios_key])) 696 | 697 | skip_next_target = False 698 | 699 | cmd = updateios_cmd + [vios] 700 | (ret, std_out, std_err) = exec_cmd(cmd, module) 701 | 702 | if ret != 0: 703 | logging.error('NIM Command: {} failed rc:{} stdout:{} stderr:{}' 704 | .format(cmd, ret, std_out, std_err)) 705 | OUTPUT.append(' Failed to update VIOS {} with NIM: {} failed: {}' 706 | .format(vios, cmd, std_err)) 707 | update_op_tab[vios_key] = err_label 708 | # in case of failure try to restart the SSP if needed 709 | skip_next_target = True 710 | else: 711 | logging.info('VIOS {} successfully updated'.format(vios)) 712 | OUTPUT.append(" VIOS {} successfully updated".format(vios)) 713 | CHANGED = True 714 | 715 | # if needed restart the SSP for the VIOS 716 | if restart_needed: 717 | ret = ssp_stop_start(module, target_tuple, vios, 'start') 718 | if ret == 1: 719 | logging.error('SSP start operation failure for VIOS {}' 720 | .format(vios)) 721 | update_op_tab[vios_key] = err_label 722 | logging.info('VIOS update status for {}: {}' 723 | .format(vios_key, update_op_tab[vios_key])) 724 | break # cannot continue 725 | 726 | logging.info(' {}: {}'.format(vios_key, update_op_tab[vios_key])) 727 | 728 | if skip_next_target: 729 | break 730 | 731 | return 0 732 | 733 | 734 | ################################################################################### 735 | 736 | if __name__ == '__main__': 737 | DEBUG_DATA = [] 738 | OUTPUT = [] 739 | NIM_NODE = {} 740 | CHANGED = False 741 | VARS = {} 742 | 743 | MODULE = AnsibleModule( 744 | argument_spec=dict( 745 | description=dict(required=False, type='str'), 746 | targets=dict(required=True, type='str'), 747 | filesets=dict(required=False, type='str'), 748 | installp_bundle=dict(required=False, type='str'), 749 | lpp_source=dict(required=False, type='str'), 750 | accept_licenses=dict(required=False, type='str'), 751 | action=dict(choices=['install', 'commit', 'reject', 'cleanup', 'remove'], 752 | required=True, type='str'), 753 | preview=dict(required=False, type='str'), 754 | time_limit=dict(required=False, type='str'), 755 | vars=dict(required=False, type='dict'), 756 | vios_status=dict(required=False, type='dict'), 757 | nim_node=dict(required=False, type='dict') 758 | ), 759 | required_if=[ 760 | ['action', 'install', ['lpp_source']], 761 | ], 762 | mutually_exclusive=[ 763 | ['filesets', 'installp_bundle'], 764 | ], 765 | ) 766 | 767 | # ========================================================================= 768 | # Get Module params 769 | # ========================================================================= 770 | targets_update_status = {} 771 | vios_status = {} 772 | targets = MODULE.params['targets'] 773 | 774 | if MODULE.params['vios_status']: 775 | vios_status = MODULE.params['vios_status'] 776 | else: 777 | vios_status = None 778 | 779 | # build a time structure for time_limit attribute, 780 | time_limit = None 781 | if MODULE.params['time_limit']: 782 | match_key = re.match(r"^\s*\d{2}/\d{2}/\d{4} \S*\d{2}:\d{2}\s*$", 783 | MODULE.params['time_limit']) 784 | if match_key: 785 | time_limit = time.strptime(MODULE.params['time_limit'], '%m/%d/%Y %H:%M') 786 | else: 787 | msg = 'Malformed time limit "{}", please use mm/dd/yyyy hh:mm format.'\ 788 | .format(MODULE.params['time_limit']) 789 | MODULE.fail_json(msg=msg) 790 | 791 | # Handle playbook variables 792 | LOGNAME = '/tmp/ansible_updateios_debug.log' 793 | if MODULE.params['vars']: 794 | VARS = MODULE.params['vars'] 795 | if VARS is not None and 'log_file' not in VARS: 796 | VARS['log_file'] = LOGNAME 797 | 798 | # Open log file 799 | DEBUG_DATA.append('Log file: {}'.format(VARS['log_file'])) 800 | LOGFRMT = '[%(asctime)s] %(levelname)s: [%(funcName)s:%(thread)d] %(message)s' 801 | logging.basicConfig(filename="{}".format(VARS['log_file']), format=LOGFRMT, level=logging.DEBUG) 802 | 803 | logging.debug('*** START NIM UPDATE VIOS OPERATION ***') 804 | 805 | OUTPUT.append('Updateios operation for {}'.format(MODULE.params['targets'])) 806 | logging.info('Action {} for {} targets'.format(MODULE.params['action'], targets)) 807 | 808 | # ========================================================================= 809 | # build nim node info 810 | # ========================================================================= 811 | if MODULE.params['nim_node']: 812 | NIM_NODE = MODULE.params['nim_node'] 813 | else: 814 | build_nim_node(MODULE) 815 | 816 | # ========================================================================= 817 | # Perfom checks 818 | # ========================================================================= 819 | ret = check_vios_targets(MODULE, targets) 820 | if (not ret) or (ret is None): 821 | OUTPUT.append('Empty target list') 822 | logging.warn('Warning: Empty target list: "{}"'.format(targets)) 823 | else: 824 | targets_list = ret 825 | OUTPUT.append('Targets list:{}'.format(targets_list)) 826 | logging.debug('Target list: {}'.format(targets_list)) 827 | 828 | # ========================================================================= 829 | # Perfom the update 830 | # ========================================================================= 831 | ret = nim_updateios(MODULE, targets_list, vios_status, 832 | targets_update_status, time_limit) 833 | 834 | if targets_update_status: 835 | OUTPUT.append('NIM updateios operation status:') 836 | logging.info('NIM updateios operation status:') 837 | for vios_key in targets_update_status: 838 | OUTPUT.append(" {} : {}".format(vios_key, targets_update_status[vios_key])) 839 | logging.info(' {} : {}'.format(vios_key, targets_update_status[vios_key])) 840 | logging.info('NIM updateios operation result: {}'.format(targets_update_status)) 841 | else: 842 | logging.error('NIM updateios operation: status table is empty') 843 | OUTPUT.append('NIM updateios operation: Error getting the status') 844 | targets_update_status = vios_status 845 | 846 | # ========================================================================= 847 | # Exit 848 | # ========================================================================= 849 | MODULE.exit_json( 850 | changed=CHANGED, 851 | msg="NIM updateios operation completed successfully", 852 | targets=MODULE.params['targets'], 853 | debug_output=DEBUG_DATA, 854 | output=OUTPUT, 855 | status=targets_update_status) 856 | -------------------------------------------------------------------------------- /library/aix_nim_vios_hc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2017, International Business Machines Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ###################################################################### 18 | """AIX VIOS Health Check: check the pair of VIOS can be updated""" 19 | 20 | import os 21 | import stat 22 | import re 23 | import subprocess 24 | import threading 25 | import logging 26 | # Ansible module 'boilerplate' 27 | from ansible.module_utils.basic import AnsibleModule 28 | 29 | 30 | DOCUMENTATION = """ 31 | --- 32 | module: aix_nim_vios_hc 33 | short_description: "Check the pair of VIOS can be updated" 34 | author: "Patrice Jacquin, Alain Poncet, Vianney Robin" 35 | version_added: "1.0.0" 36 | requirements: [ AIX ] 37 | """ 38 | 39 | 40 | # ---------------------------------------------------------------- 41 | # ---------------------------------------------------------------- 42 | def exec_cmd(cmd, module, exit_on_error=False, debug_data=True, shell=False): 43 | """ 44 | Execute the given command 45 | 46 | Note: If executed in thread, fail_json does not exit the parent 47 | 48 | args: 49 | - cmd array of the command parameters 50 | - module the module variable 51 | - exit_on_error execption is raised if true and cmd return !0 52 | - debug_data prints some trace in DEBUG_DATA if set 53 | - shell execute cmd through the shell if set (vulnerable to shell 54 | injection when cmd is from user inputs). If cmd is a string 55 | string, the string specifies the command to execute through 56 | the shell. If cmd is a list, the first item specifies the 57 | command, and other items are arguments to the shell itself. 58 | return 59 | - ret return code of the command 60 | - output output of the command 61 | - errout command stderr 62 | """ 63 | 64 | global DEBUG_DATA 65 | global CHANGED 66 | global OUTPUT 67 | 68 | ret = 0 69 | output = '' 70 | errout = '' 71 | th_id = threading.current_thread().ident 72 | stderr_file = '/tmp/ansible_vios_check_cmd_stderr_{}'.format(th_id) 73 | 74 | logging.debug('exec command:{}'.format(cmd)) 75 | if debug_data is True: 76 | DEBUG_DATA.append('exec command:{}'.format(cmd)) 77 | try: 78 | myfile = open(stderr_file, 'w') 79 | output = subprocess.check_output(cmd, stderr=myfile, shell=shell) 80 | myfile.close() 81 | s = re.search(r'rc=([-\d]+)$', output) 82 | if s: 83 | ret = int(s.group(1)) 84 | output = re.sub(r'rc=[-\d]+\n$', '', output) # remove the rc of c_rsh with echo $? 85 | 86 | except subprocess.CalledProcessError as exc: 87 | myfile.close() 88 | errout = re.sub(r'rc=[-\d]+\n$', '', exc.output) # remove the rc of c_rsh with echo $? 89 | ret = exc.returncode 90 | 91 | except OSError as exc: 92 | myfile.close 93 | errout = re.sub(r'rc=[-\d]+\n$', '', exc.args[1]) # remove the rc of c_rsh with echo $? 94 | ret = exc.args[0] 95 | 96 | except IOError as exc: 97 | # uncatched exception 98 | myfile.close 99 | msg = 'Command: {} Exception: {}'.format(cmd, exc) 100 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 101 | 102 | # check for error message 103 | if os.path.getsize(stderr_file) > 0: 104 | myfile = open(stderr_file, 'r') 105 | errout += ''.join(myfile) 106 | myfile.close() 107 | os.remove(stderr_file) 108 | 109 | if ret != 0 and exit_on_error is True: 110 | msg = 'Error executing command {} RetCode:{} ... stdout:{} stderr:{}'\ 111 | .format(cmd, ret, output, errout) 112 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 113 | 114 | msg = 'exec command rc:{}, output:{}, stderr:{}'\ 115 | .format(ret, output, errout) 116 | if debug_data is True: 117 | DEBUG_DATA.append(msg) 118 | logging.debug(msg) 119 | 120 | return (ret, output, errout) 121 | 122 | 123 | # ---------------------------------------------------------------- 124 | # ---------------------------------------------------------------- 125 | def get_hmc_info(module): 126 | """ 127 | Get the hmc info on the nim master 128 | 129 | fill the hmc_dic passed in parameter 130 | 131 | return a dic with hmc info 132 | """ 133 | std_out = '' 134 | info_hash = {} 135 | 136 | cmd = 'LC_ALL=C lsnim -t hmc -l' 137 | (ret, std_out, std_err) = exec_cmd(cmd, module, shell=True) 138 | if ret != 0: 139 | msg = 'Failed to get HMC NIM info, lsnim returns: {}'.format(std_err) 140 | logging.error(msg) 141 | OUTPUT.append(msg) 142 | return info_hash 143 | 144 | obj_key = '' 145 | for line in std_out.split('\n'): 146 | line = line.rstrip() 147 | match_key = re.match(r"^(\S+):", line) 148 | # HMC name 149 | if match_key: 150 | obj_key = match_key.group(1) 151 | info_hash[obj_key] = {} 152 | continue 153 | 154 | match_cstate = re.match(r"^\s+Cstate\s+=\s+(.*)$", line) 155 | if match_cstate: 156 | cstate = match_cstate.group(1) 157 | info_hash[obj_key]['cstate'] = cstate 158 | continue 159 | 160 | match_key = re.match(r"^\s+passwd_file\s+=\s+(.*)$", line) 161 | if match_key: 162 | info_hash[obj_key]['passwd_file'] = match_key.group(1) 163 | continue 164 | 165 | match_key = re.match(r"^\s+login\s+=\s+(.*)$", line) 166 | if match_key: 167 | info_hash[obj_key]['login'] = match_key.group(1) 168 | continue 169 | 170 | match_key = re.match(r"^\s+if1\s*=\s*\S+\s*(\S*)\s*.*$", line) 171 | if match_key: 172 | info_hash[obj_key]['ip'] = match_key.group(1) 173 | continue 174 | 175 | return info_hash 176 | 177 | 178 | # ---------------------------------------------------------------- 179 | # ---------------------------------------------------------------- 180 | def get_nim_cecs_info(module): 181 | """ 182 | Get the list of the cec defined on the nim master and 183 | get their serial number. 184 | 185 | return the list of the name of the cec objects defined on the 186 | nim master and their associated CEC serial number value 187 | """ 188 | std_out = '' 189 | info_hash = {} 190 | 191 | cmd = 'LC_ALL=C lsnim -t cec -l' 192 | (ret, std_out, std_err) = exec_cmd(cmd, module, shell=True) 193 | if ret != 0: 194 | msg = 'Failed to get CEC NIM info, lsnim returns: {}'.format(std_err) 195 | logging.error(msg) 196 | OUTPUT.append(msg) 197 | return info_hash 198 | 199 | # lpar name and associated Cstate 200 | obj_key = "" 201 | for line in std_out.split('\n'): 202 | line = line.rstrip() 203 | match_key = re.match(r"^(\S+):", line) 204 | if match_key: 205 | obj_key = match_key.group(1) 206 | info_hash[obj_key] = {} 207 | continue 208 | 209 | match_serial = re.match(r"^\s+serial\s+=\s+(.*)$", line) 210 | if match_serial: 211 | info_hash[obj_key]['serial'] = match_serial.group(1) 212 | continue 213 | 214 | return info_hash 215 | 216 | 217 | # ---------------------------------------------------------------- 218 | # ---------------------------------------------------------------- 219 | def get_nim_clients_info(module, lpar_type): 220 | """ 221 | Get the list of the lpar (standalones or vios) defined on the nim master, and get their 222 | cstate. 223 | 224 | return the list of the name of the lpar objects defined on the 225 | nim master and their associated cstate value 226 | """ 227 | std_out = '' 228 | info_hash = {} 229 | 230 | cmd = 'LC_ALL=C lsnim -t {} -l'.format(lpar_type) 231 | (ret, std_out, std_err) = exec_cmd(cmd, module, shell=True) 232 | if ret != 0: 233 | msg = 'Failed to get NIM clients info, lsnim returns: {}'.format(std_err) 234 | logging.error(msg) 235 | OUTPUT.append(msg) 236 | return info_hash 237 | 238 | # lpar name and associated Cstate 239 | obj_key = "" 240 | for line in std_out.split('\n'): 241 | line = line.rstrip() 242 | match_key = re.match(r"^(\S+):", line) 243 | if match_key: 244 | obj_key = match_key.group(1) 245 | info_hash[obj_key] = {} 246 | continue 247 | 248 | match_cstate = re.match(r"^\s+Cstate\s+=\s+(.*)$", line) 249 | if match_cstate: 250 | info_hash[obj_key]['cstate'] = match_cstate.group(1) 251 | continue 252 | 253 | # For VIOS store the management profile 254 | if lpar_type == 'vios': 255 | match_mgmtprof = re.match(r"^\s+mgmt_profile1\s+=\s+(.*)$", line) 256 | if match_mgmtprof: 257 | mgmt_elts = match_mgmtprof.group(1).split() 258 | if len(mgmt_elts) == 3: 259 | info_hash[obj_key]['mgmt_hmc_id'] = mgmt_elts[0] 260 | info_hash[obj_key]['mgmt_vios_id'] = mgmt_elts[1] 261 | info_hash[obj_key]['mgmt_cec'] = mgmt_elts[2] 262 | 263 | match_if = re.match(r"^\s+if1\s+=\s+\S+\s+(\S+)\s+.*$", line) 264 | if match_if: 265 | info_hash[obj_key]['vios_ip'] = match_if.group(1) 266 | 267 | return info_hash 268 | 269 | 270 | # ---------------------------------------------------------------- 271 | # ---------------------------------------------------------------- 272 | def build_nim_node(module): 273 | """ 274 | build the nim node containing the nim vios and hmcinfo. 275 | 276 | arguments: 277 | None 278 | 279 | return: 280 | None 281 | """ 282 | 283 | global NIM_NODE 284 | 285 | # ========================================================================= 286 | # Build hmc info list 287 | # ========================================================================= 288 | nim_hmc = {} 289 | nim_hmc = get_hmc_info(module) 290 | 291 | NIM_NODE['nim_hmc'] = nim_hmc 292 | logging.debug('NIM HMC: {}'.format(nim_hmc)) 293 | 294 | # ========================================================================= 295 | # Build CEC list 296 | # ========================================================================= 297 | nim_cec = {} 298 | nim_cec = get_nim_cecs_info(module) 299 | 300 | # ========================================================================= 301 | # Build vios info list 302 | # ========================================================================= 303 | nim_vios = {} 304 | nim_vios = get_nim_clients_info(module, 'vios') 305 | 306 | # ========================================================================= 307 | # Complete the CEC serial in nim_vios dict 308 | # ========================================================================= 309 | for key in nim_vios: 310 | if nim_vios[key]['mgmt_cec'] in nim_cec: 311 | nim_vios[key]['mgmt_cec_serial'] = nim_cec[nim_vios[key]['mgmt_cec']]['serial'] 312 | 313 | NIM_NODE['nim_vios'] = nim_vios 314 | logging.debug('NIM VIOS: {}'.format(nim_vios)) 315 | 316 | 317 | # ---------------------------------------------------------------- 318 | # ---------------------------------------------------------------- 319 | def check_vios_targets(module, targets): 320 | """ 321 | check the list of the vios targets. 322 | check that each target can be reached. 323 | 324 | a target name could be of the following form: 325 | (vios1, vios2) (vios3) 326 | 327 | arguments: 328 | targets (str): list of tuple of NIM name of vios machine 329 | 330 | return: the list of the existing vios tuple matching the target list 331 | """ 332 | global NIM_NODE 333 | 334 | vios_list = {} 335 | vios_list_tuples_res = [] 336 | vios_list_tuples = targets.replace(" ", "").replace("),(", ")(").split('(') 337 | 338 | # =========================================== 339 | # Build targets list 340 | # =========================================== 341 | for vios_tuple in vios_list_tuples[1:]: 342 | 343 | logging.debug('vios_tuple: {}'.format(vios_tuple)) 344 | 345 | tuple_elts = list(vios_tuple[:-1].split(',')) 346 | tuple_len = len(tuple_elts) 347 | 348 | if tuple_len != 1 and tuple_len != 2: 349 | logging.error('Malformed VIOS targets {}. Tuple {} should be a 2 or 4 elements.' 350 | .format(targets, tuple_elts)) 351 | return None 352 | 353 | # check vios not already exists in the target list 354 | if tuple_elts[0] in vios_list or \ 355 | (tuple_len == 2 and (tuple_elts[1] in vios_list or 356 | tuple_elts[0] == tuple_elts[1])): 357 | logging.error('Malformed VIOS targets {}. Duplicated VIOS' 358 | .format(targets)) 359 | return None 360 | 361 | # check vios is known by the NIM master - if not ignore it 362 | if tuple_elts[0] not in NIM_NODE['nim_vios'] or \ 363 | (tuple_len == 2 and tuple_elts[1] not in NIM_NODE['nim_vios']): 364 | logging.info('skipping {} as VIOS not known by the NIM master.' 365 | .format(vios_tuple)) 366 | continue 367 | 368 | # check vios connectivity 369 | res = 0 370 | for elem in tuple_elts: 371 | cmd = ['/usr/lpp/bos.sysmgt/nim/methods/c_rsh', elem, 372 | '"/usr/bin/ls /dev/null; echo rc=$?"'] 373 | (ret, std_out, std_err) = exec_cmd(cmd, module) 374 | if ret != 0: 375 | res = 1 376 | msg = 'skipping {}: cannot reach {} with c_rsh: {}, {}, {}'\ 377 | .format(vios_tuple, elem, res, std_out, std_err) 378 | logging.info(msg) 379 | continue 380 | if res != 0: 381 | continue 382 | 383 | if tuple_len == 2: 384 | vios_list[tuple_elts[0]] = tuple_elts[1] 385 | vios_list[tuple_elts[1]] = tuple_elts[0] 386 | # vios_list = vios_list.extend([tuple_elts[0], tuple_elts[1]]) 387 | my_tuple = (tuple_elts[0], tuple_elts[1]) 388 | vios_list_tuples_res.append(tuple(my_tuple)) 389 | else: 390 | vios_list[tuple_elts[0]] = tuple_elts[0] 391 | # vios_list.append(tuple_elts[0]) 392 | my_tuple = (tuple_elts[0],) 393 | vios_list_tuples_res.append(tuple(my_tuple)) 394 | 395 | return vios_list_tuples_res 396 | 397 | 398 | # ---------------------------------------------------------------- 399 | # ---------------------------------------------------------------- 400 | def vios_health(module, mgmt_sys_uuid, hmc_ip, vios_uuids): 401 | """ 402 | Check the "health" of the given VIOSES 403 | 404 | return: True if ok, 405 | False else 406 | """ 407 | global NIM_NODE 408 | 409 | logging.debug('hmc_ip: {} vios_uuids: {}'.format(hmc_ip, vios_uuids)) 410 | 411 | # build the vioshc cmde 412 | cmd = ['LC_ALL=C /usr/sbin/vioshc.py', '-i', hmc_ip, '-m', mgmt_sys_uuid] 413 | for vios in vios_uuids: 414 | cmd.extend(['-U', vios]) 415 | if VERBOSITY != 0: 416 | vstr = "-v" 417 | verbose = 1 418 | while verbose < VERBOSITY: 419 | vstr += "v" 420 | verbose += 1 421 | cmd.extend([vstr]) 422 | 423 | if VERBOSITY >= 3: 424 | cmd.extend(['-D']) 425 | 426 | (ret, std_out, std_err) = exec_cmd(' '.join(cmd), module, shell=True) 427 | if ret != 0: 428 | OUTPUT.append(' VIOS Health check failed, vioshc returns: {}' 429 | .format(std_err)) 430 | logging.error('VIOS Health check failed, vioshc returns: {} {}' 431 | .format(ret, std_err)) 432 | OUTPUT.append(' VIOS can NOT be updated') 433 | logging.info('vioses {} can NOT be updated'.format(vios_uuids)) 434 | ret = 1 435 | elif re.search(r'Pass rate of 100%', std_out, re.M): 436 | OUTPUT.append(' VIOS Health check passed') 437 | logging.info('vioses {} can be updated'.format(vios_uuids)) 438 | ret = 0 439 | else: 440 | OUTPUT.append(' VIOS can NOT be updated') 441 | logging.info('vioses {} can NOT be updated'.format(vios_uuids)) 442 | ret = 1 443 | 444 | return ret 445 | 446 | 447 | # ---------------------------------------------------------------- 448 | # ---------------------------------------------------------------- 449 | def vios_health_init(module, hmc_id, hmc_ip): 450 | """ 451 | Check the "health" of the given VIOSES for a rolling update point of view 452 | 453 | This operation uses the vioshc.py script to evaluate the capacity of the 454 | pair of the VIOSes to support the rolling update operation: 455 | - check they manage the same LPARs, 456 | - ... 457 | 458 | return: True if ok, 459 | False else 460 | """ 461 | global NIM_NODE 462 | global CHANGED 463 | global OUTPUT 464 | 465 | logging.debug('hmc_id: {}, hmc_ip: {}'.format(hmc_id, hmc_ip)) 466 | 467 | ret = 0 468 | # if needed, call the /usr/sbin/vioshc.py script a first time to 469 | # collect UUIDs 470 | cmd = ['LC_ALL=C /usr/sbin/vioshc.py', '-i', hmc_ip, '-l', 'a'] 471 | if VERBOSITY != 0: 472 | vstr = "-v" 473 | verbose = 1 474 | while verbose < VERBOSITY: 475 | vstr += "v" 476 | verbose += 1 477 | cmd.extend([vstr]) 478 | if VERBOSITY >= 3: 479 | cmd.extend(['-D']) 480 | 481 | (ret, std_out, std_err) = exec_cmd(' '.join(cmd), module, shell=True) 482 | if ret != 0: 483 | OUTPUT.append(' Failed to get the VIOS information, vioshc returns: {}' 484 | .format(std_err)) 485 | logging.error('Failed to get the VIOS information, vioshc returns: {} {}' 486 | .format(ret, std_err)) 487 | msg = 'Health init check failed. vioshc command error. rc:{}, stdout: {} stderr: {}'\ 488 | .format(ret, std_out, std_err) 489 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 490 | 491 | # Parse the output and store the UUIDs 492 | data_start = 0 493 | vios_section = 0 494 | cec_uuid = '' 495 | cec_serial = '' 496 | for line in std_out.split('\n'): 497 | line = line.rstrip() 498 | # TBC - remove? 499 | logging.debug('--------line {}'.format(line)) 500 | if vios_section == 0: 501 | # skip the header 502 | match_key = re.match(r"^-+\s+-+$", line) 503 | if match_key: 504 | data_start = 1 505 | continue 506 | if data_start == 0: 507 | continue 508 | 509 | # New managed system section 510 | match_key = re.match(r"^(\S+)\s+(\S+)$", line) 511 | if match_key: 512 | cec_uuid = match_key.group(1) 513 | cec_serial = match_key.group(2) 514 | 515 | logging.debug('New managed system section:{},{}' 516 | .format(cec_uuid, cec_serial)) 517 | continue 518 | 519 | # New vios section 520 | match_key = re.match(r"^\s+-+\s+-+$", line) 521 | if match_key: 522 | vios_section = 1 523 | continue 524 | 525 | # skip all header and empty lines until the vios section 526 | continue 527 | 528 | # new vios partition 529 | match_key = re.match(r"^\s+(\S+)\s+(\S+)$", line) 530 | if match_key: 531 | vios_uuid = match_key.group(1) 532 | vios_part_id = match_key.group(2) 533 | logging.debug('new vios partitionsection:{},{}' 534 | .format(vios_uuid, vios_part_id)) 535 | 536 | # retrieve the vios with the vios_part_id and the cec_serial value 537 | # and store the UUIDs in the dictionaries 538 | for vios_key in NIM_NODE['nim_vios']: 539 | if NIM_NODE['nim_vios'][vios_key]['mgmt_vios_id'] == vios_part_id \ 540 | and NIM_NODE['nim_vios'][vios_key]['mgmt_cec_serial'] == cec_serial: 541 | NIM_NODE['nim_vios'][vios_key]['vios_uuid'] = vios_uuid 542 | NIM_NODE['nim_vios'][vios_key]['cec_uuid'] = cec_uuid 543 | break 544 | continue 545 | 546 | # skip vios line where lparid is not found. 547 | match_key = re.match(r"^\s+(\S+)\s+none$", line) 548 | if match_key: 549 | continue 550 | 551 | # skip empty line after vios section. stop the vios section 552 | match_key = re.match(r"^$", line) 553 | if match_key: 554 | vios_section = 0 555 | continue 556 | 557 | OUTPUT.append(' Bad command output for the hmc: {}'.format(hmc_id)) 558 | logging.error('vioshc command, bad output line: {}'.format(line)) 559 | msg = 'Health init check failed. Bad vioshc.py command output for the {} hmc - output: {}'\ 560 | .format(hmc_id, line) 561 | module.fail_json(changed=CHANGED, msg=msg, output=OUTPUT) 562 | 563 | logging.debug('vioshc output: {}'.format(line)) 564 | return ret 565 | 566 | 567 | # ---------------------------------------------------------------- 568 | # ---------------------------------------------------------------- 569 | def health_check(module, targets): 570 | """ 571 | Healt assessment of the VIOSes targets to ensure they can be support 572 | a rolling update operation. 573 | 574 | For each VIOS tuple, 575 | - call /usr/sbin/vioshc.py a first time to collect the VIOS UUIDs 576 | - call it a second time to check the healthiness 577 | 578 | return: True if ok, 579 | False else 580 | """ 581 | global NIM_NODE 582 | 583 | logging.debug('targets: {}'.format(targets)) 584 | 585 | health_tab = {} 586 | vios_key = [] 587 | for target_tuple in targets: 588 | OUTPUT.append('Checking: {}'.format(target_tuple)) 589 | logging.debug('target_tuple: {}'.format(target_tuple)) 590 | 591 | tup_len = len(target_tuple) 592 | vios1 = target_tuple[0] 593 | if tup_len == 2: 594 | vios2 = target_tuple[1] 595 | vios_key = "{}-{}".format(vios1, vios2) 596 | else: 597 | vios_key = vios1 598 | 599 | logging.debug('vios1: {}'.format(vios1)) 600 | # cec_serial = NIM_NODE['nim_vios'][vios1]['mgmt_cec_serial'] 601 | hmc_id = NIM_NODE['nim_vios'][vios1]['mgmt_hmc_id'] 602 | 603 | if hmc_id not in NIM_NODE['nim_hmc']: 604 | OUTPUT.append(' VIOS {} refers to an inexistant hmc {}' 605 | .format(vios1, hmc_id)) 606 | logging.warn("VIOS {} refers to an inexistant hmc {}" 607 | .format(vios1, hmc_id)) 608 | health_tab[vios_key] = 'FAILURE-HC' 609 | continue 610 | 611 | hmc_ip = NIM_NODE['nim_hmc'][hmc_id]['ip'] 612 | 613 | vios_uuid = [] 614 | 615 | # if needed call vios_health_init to get the UUIDs value 616 | if 'vios_uuid' not in NIM_NODE['nim_vios'][vios1] \ 617 | or tup_len == 2 and 'vios_uuid' not in NIM_NODE['nim_vios'][vios2]: 618 | OUTPUT.append(' Getting VIOS UUID') 619 | 620 | ret = vios_health_init(module, hmc_id, hmc_ip) 621 | if ret != 0: 622 | OUTPUT.append(' Unable to get UUIDs of {} and {}, ret: {}' 623 | .format(vios1, vios2, ret)) 624 | logging.warn("Unable to get UUIDs of {} and {}, ret: {}" 625 | .format(vios1, vios2, ret)) 626 | health_tab[vios_key] = 'FAILURE-HC' 627 | continue 628 | 629 | if 'vios_uuid' not in NIM_NODE['nim_vios'][vios1] \ 630 | or tup_len == 2 and 'vios_uuid' not in NIM_NODE['nim_vios'][vios2]: 631 | # vios uuid's not found 632 | OUTPUT.append(' One VIOS UUID not found') 633 | logging.warn("Unable to find one vios_uuid in NIM_NODE") 634 | health_tab[vios_key] = 'FAILURE-HC' 635 | 636 | else: 637 | # run the vios_health check for the vios tuple 638 | vios_uuid.append(NIM_NODE['nim_vios'][vios1]['vios_uuid']) 639 | if tup_len == 2: 640 | vios_uuid.append(NIM_NODE['nim_vios'][vios2]['vios_uuid']) 641 | 642 | mgmt_uuid = NIM_NODE['nim_vios'][vios1]['cec_uuid'] 643 | 644 | OUTPUT.append(' Checking if we can update the VIOS') 645 | ret = vios_health(module, mgmt_uuid, hmc_ip, vios_uuid) 646 | 647 | if ret == 0: 648 | OUTPUT.append(' Health check succeeded') 649 | logging.info("Health check succeeded for {}".format(vios_key)) 650 | health_tab[vios_key] = 'SUCCESS-HC' 651 | else: 652 | OUTPUT.append(' Health check failed') 653 | logging.info("Health check failed for {}".format(vios_key)) 654 | health_tab[vios_key] = 'FAILURE-HC' 655 | 656 | logging.debug('health_tab: {}'. format(health_tab)) 657 | return health_tab 658 | 659 | 660 | ################################################################################ 661 | 662 | if __name__ == '__main__': 663 | 664 | DEBUG_DATA = [] 665 | OUTPUT = [] 666 | PARAMS = {} 667 | NIM_NODE = {} 668 | CHANGED = False 669 | targets_list = [] 670 | VARS = {} 671 | VERBOSITY = 0 672 | 673 | module = AnsibleModule( 674 | argument_spec=dict( 675 | description=dict(required=False, type='str'), 676 | targets=dict(required=True, type='str'), 677 | action=dict(required=True, choices=['health_check'], type='str'), 678 | vars=dict(required=False, type='dict'), 679 | ), 680 | supports_check_mode=True 681 | ) 682 | 683 | # ========================================================================= 684 | # Get Module params 685 | # ========================================================================= 686 | action = module.params['action'] 687 | targets = module.params['targets'] 688 | VERBOSITY = module._verbosity 689 | 690 | if module.params['description']: 691 | description = module.params['description'] 692 | else: 693 | description = "Perform a VIOS Health Check operation: {} request".format(action) 694 | 695 | PARAMS['action'] = action 696 | PARAMS['targets'] = targets 697 | PARAMS['Description'] = description 698 | 699 | # Handle playbook variables 700 | if module.params['vars']: 701 | VARS = module.params['vars'] 702 | if VARS is not None and 'log_file' not in VARS: 703 | VARS['log_file'] = '/tmp/ansible_vios_check_debug.log' 704 | 705 | # Open log file 706 | logging.basicConfig( 707 | filename="{}".format(VARS['log_file']), 708 | format='[%(asctime)s] %(levelname)s: [%(funcName)s:%(thread)d] %(message)s', 709 | level=logging.DEBUG) 710 | 711 | logging.debug('*** START VIOS {} ***'.format(action.upper())) 712 | 713 | OUTPUT.append('VIOS Health Check operation for {}'.format(targets)) 714 | logging.info('action {} for {} targets'.format(action, targets)) 715 | logging.info('VERBOSITY is set to {}'.format(VERBOSITY)) 716 | 717 | targets_health_status = {} 718 | 719 | # ========================================================================= 720 | # build nim node info 721 | # ========================================================================= 722 | build_nim_node(module) 723 | 724 | ret = check_vios_targets(module, targets) 725 | if (ret is None) or (not ret): 726 | OUTPUT.append(' Warning: Empty target list') 727 | logging.warn('Empty target list: "{}"'.format(targets)) 728 | else: 729 | targets_list = ret 730 | OUTPUT.append(' Targets list: {}'.format(targets_list)) 731 | logging.debug('Targets list: {}'.format(targets_list)) 732 | 733 | # =============================================== 734 | # Check vioshc script is present, else install it 735 | # =============================================== 736 | logging.debug('Check vioshc script: /usr/sbin/vioshc.py') 737 | 738 | vioshcpath = os.path.abspath(os.path.join(os.sep, 'usr', 'sbin')) 739 | vioshcfile = os.path.join(vioshcpath, 'vioshc.py') 740 | 741 | if not os.path.exists(vioshcfile): 742 | OUTPUT.append('Cannot find {}'.format(vioshcfile)) 743 | logging.error('Cannot find {}'.format(vioshcfile)) 744 | module.fail_json(msg="Cannot find {}".format(vioshcfile)) 745 | 746 | st = os.stat(vioshcfile) 747 | if not st.st_mode & stat.S_IEXEC: 748 | OUTPUT.append('Bad credentials for {}'.format(vioshcfile)) 749 | logging.error('Bad credentials for {}'.format(vioshcfile)) 750 | module.fail_json(msg="Bad credentials for {}".format(vioshcfile)) 751 | 752 | targets_health_status = health_check(module, targets_list) 753 | 754 | OUTPUT.append('VIOS Health Check status:') 755 | logging.info('VIOS Health Check status:') 756 | for vios_key in targets_health_status.keys(): 757 | OUTPUT.append(" {} : {}".format(vios_key, targets_health_status[vios_key])) 758 | logging.info(' {} : {}'.format(vios_key, targets_health_status[vios_key])) 759 | 760 | # ========================================================================== 761 | # Exit 762 | # ========================================================================== 763 | module.exit_json( 764 | changed=CHANGED, 765 | msg="VIOS Health Check completed successfully", 766 | targets=targets_list, 767 | nim_node=NIM_NODE, 768 | status=targets_health_status, 769 | debug_output=DEBUG_DATA, 770 | output=OUTPUT) 771 | -------------------------------------------------------------------------------- /library/aix_nim_viosupgrade.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2018, International Business Machines Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | 17 | ############################################################################ 18 | """AIX NIM viosupgrade: tool to upgrade VIOSes in NIM environment""" 19 | 20 | import logging 21 | import csv 22 | import distutils.util 23 | 24 | # Ansible module 'boilerplate' 25 | from ansible.module_utils.basic import AnsibleModule 26 | 27 | 28 | DOCUMENTATION = """ 29 | --- 30 | module: nim_upgradeios 31 | authors: Vianney Robin, Alain Poncet, Pascal Oliva 32 | short_description: Perform a upgrade with the viosupgrade tool 33 | """ 34 | 35 | # TODO: ----------------------------------------------------------------------------- 36 | # TODO: Later, check SSP support (option -c of viosupgrade) 37 | # TODO: Later, check mirrored rootvg support for upgrade & upgrade all in one 38 | # TODO: Check if all debug section (TBC) are commented before commit 39 | # TODO: Check flake8 complaints 40 | # TODO: Add this module usage in README.md file 41 | # TODO: ----------------------------------------------------------------------------- 42 | # TODO: Add message in OUTPUT 43 | # TODO: Can we tune more precisly CHANGED (stderr parsing/analysis)? 44 | # TODO: Skip operation if vios_status is defined and not SUCCESS, set the vios_status after operation 45 | # TODO: Do we support viosupgrade without NIM environment (executed on the VIOS itself)? in another module? 46 | # TODO: a time_limit could be used in get_status to loop for a period of time (might want to add parameter for sleep period) 47 | # TODO: nim_migvios_setup not supported yet? 48 | # TODO: ----------------------------------------------------------------------------- 49 | 50 | 51 | # ---------------------------------------------------------------- 52 | # ---------------------------------------------------------------- 53 | # TODO: test viosupgrade_query 54 | def viosupgrade_query(module): 55 | """ 56 | Query to get the status of the upgrade for each target 57 | runs: viosupgrade -q { [-n hostname | -f filename] } 58 | 59 | The caller must ensure either the filename or the target list is set. 60 | 61 | args: 62 | module the Ansible module 63 | 64 | module.param used: 65 | target_file_name (optional) filename with targets info 66 | targets (required if not target_file_name) 67 | 68 | return: 69 | ret the number of error 70 | """ 71 | ret = 0 72 | 73 | if module.param['target_file_name']: 74 | cmd = '/usr/sbin/viosupgrade -q -f {}'\ 75 | .format(module.param['target_file_name']) 76 | (ret, stdout, stderr) = module.run_command(cmd) 77 | 78 | logging.info("[STDOUT] {}".format(stdout)) 79 | if ret == 0: 80 | logging.info("[STDERR] {}".format(stderr)) 81 | else: 82 | logging.error("command {} failed: {}".format(stderr)) 83 | ret = 1 84 | else: 85 | for target in module.param['targets']: 86 | cmd = '/usr/sbin/viosupgrade -q -n {}'.format(target) 87 | (rc, stdout, stderr) = module.run_command(cmd) 88 | 89 | logging.info("[STDOUT] {}".format(stdout)) 90 | if rc == 0: 91 | logging.info("[STDERR] {}".format(stderr)) 92 | else: 93 | logging.error("command {} failed: {}".format(stderr)) 94 | ret += 1 95 | return ret 96 | 97 | 98 | # ---------------------------------------------------------------- 99 | # ---------------------------------------------------------------- 100 | # TODO: test viosupgrade_file 101 | def viosupgrade_file(module, filename): 102 | """ 103 | Upgrade each VIOS specified in the provided file 104 | runs: viosupgrade -t {bosinst | altdisk} -f [filename] [-v] 105 | 106 | args: 107 | module the Ansible module 108 | filename filename with info for VIOS upgrade 109 | 110 | module.param used: 111 | targets 112 | 113 | return: 114 | ret return code of the command 115 | """ 116 | global CHANGED 117 | ret = 0 118 | 119 | # build the command 120 | cmd = '/usr/sbin/viosupgrade' 121 | if 'altdisk_install' in module.param['action']: 122 | cmd += ' -t altdisk' 123 | elif 'bos_install' in module.param['action']: 124 | cmd += ' -t bosinst' 125 | cmd += ' -f' + module.param['target_file_name'] 126 | if module.param['validate_input_data']: 127 | cmd += ' -v' 128 | 129 | # run the command 130 | (ret, stdout, stderr) = module.run_command(cmd) 131 | 132 | CHANGED=True # don't really know 133 | logging.info("[STDOUT] {}".format(stdout)) 134 | if ret == 0: 135 | logging.info("[STDERR] {}".format(stderr)) 136 | else: 137 | logging.error("command {} failed: {}".format(stderr)) 138 | 139 | return ret 140 | 141 | 142 | # ---------------------------------------------------------------- 143 | # ---------------------------------------------------------------- 144 | # TODO: test viosupgrade_list 145 | def viosupgrade_list(module, targets): 146 | """ 147 | Upgrade each VIOS specified in the provided file 148 | runs one of: 149 | viosupgrade -t bosinst -n hostname -m mksysb_name -p spotname 150 | {-a rootvg_vg_clone_disk | -r rootvg_inst_disk | -s} 151 | [-b backupFile] [-c] [-v] 152 | viosupgrade -t altdisk -n hostname -m mksysb_name 153 | -a rootvg_vg_clone_disk [-b backup_file] [-c] [-v] 154 | 155 | args: 156 | module the Ansible module 157 | 158 | module.param used: 159 | target_file_name (optional) filename with targets info 160 | targets (required if not target_file_name) 161 | 162 | return: 163 | ret the number of error 164 | """ 165 | global CHANGED 166 | ret = 0 167 | 168 | for target in targets: 169 | # build the command 170 | cmd = '/usr/sbin/viosupgrade' 171 | if 'altdisk_install' in module.param['action']: 172 | cmd += ' -t altdisk' 173 | elif 'bos_install' in module.param['action']: 174 | cmd += ' -t bosinst' 175 | 176 | # TODO: check if NIM object name is supported, otherwise get it from lsnim 177 | # NIM object name can be different from hostname 178 | # 3rd field of following cmd result 'lsnim -Z -a 'if1' ' (separated with ':') 179 | cmd += ' -n ' + target 180 | 181 | if target in module.param['mksysb_name']: 182 | cmd += ' -m ' + module.param['mksysb_name'][target] 183 | elif 'all' in module.param['mksysb_name']: 184 | cmd += ' -m ' + module.param['mksysb_name']['all'] 185 | 186 | if target in module.param['spot_name']: 187 | cmd += ' -p ' + module.param['spot_name'][target] 188 | elif 'all' in module.param['spot_name']: 189 | cmd += ' -p ' + module.param['spot_name']['all'] 190 | 191 | if target in module.param['rootvg_clone_disk']: 192 | cmd += ' -a ' + module.param['rootvg_clone_disk'][target] 193 | elif 'all' in module.param['rootvg_clone_disk']: 194 | cmd += ' -a ' + module.param['rootvg_clone_disk']['all'] 195 | 196 | if target in module.param['rootvg_install_disk']: 197 | cmd += ' -r ' + module.param['rootvg_install_disk'][target] 198 | elif 'all' in module.param['rootvg_install_disk']: 199 | cmd += ' -r ' + module.param['rootvg_install_disk']['all'] 200 | 201 | if target in module.param['skip_rootvg_cloning']: 202 | if distutils.util.strtobool(module.param['skip_rootvg_cloning'][target]): 203 | cmd += ' -s' 204 | elif 'all' in module.param['skip_rootvg_cloning']: 205 | if distutils.util.strtobool(module.param['skip_rootvg_cloning']['all']): 206 | cmd += ' -s' 207 | 208 | if target in module.param['backup_file']: 209 | cmd += ' -b ' + module.param['backup_file'][target] 210 | elif 'all' in module.param['backup_file']: 211 | cmd += ' -b ' + module.param['backup_file']['all'] 212 | 213 | if target in module.param['cluster_exists']: 214 | if distutils.util.strtobool(module.param['cluster_exists'][target]): 215 | cmd += ' -c' 216 | elif 'all' in module.param['cluster_exists']: 217 | if distutils.util.strtobool(module.param['cluster_exists']['all']): 218 | cmd += ' -c' 219 | 220 | if target in module.param['validate_input_data']: 221 | if distutils.util.strtobool(module.param['validate_input_data'][target]): 222 | cmd += ' -v' 223 | elif 'all' in module.param['validate_input_data']: 224 | if distutils.util.strtobool(module.param['validate_input_data']['all']): 225 | cmd += ' -v' 226 | 227 | supported_res = ['res_resolv_conf', 'res_script', 'res_fb_script', 228 | 'res_file_res', 'res_image_data', 'res_log'] 229 | for res in supported_res: 230 | if target in module.param[res]: 231 | cmd += ' -e {}:{}'.format(res, module.param[res][target]) 232 | elif 'all' in module.param[res]: 233 | cmd += ' -e {}:{}'.format(res, module.param[res]['all']) 234 | 235 | # run the command 236 | (rc, stdout, stderr) = module.run_command(cmd) 237 | 238 | CHANGED=True # don't really know 239 | logging.info("[STDOUT] {}".format(stdout)) 240 | if rc == 0: 241 | logging.info("[STDERR] {}".format(stderr)) 242 | else: 243 | logging.error("command {} failed: {}".format(stderr)) 244 | ret += 1 245 | 246 | return ret 247 | 248 | 249 | ################################################################################### 250 | 251 | if __name__ == '__main__': 252 | DEBUG_DATA = [] 253 | OUTPUT = [] 254 | CHANGED = False 255 | VARS = {} 256 | 257 | MODULE = AnsibleModule( 258 | # TODO: remove not needed attributes 259 | argument_spec=dict( 260 | description=dict(required=False, type='str'), 261 | 262 | # IBM automation generic attributes 263 | action=dict(required=True, type='str', 264 | choices=['altdisk_install', 'bos_install', 'get_status']), 265 | vars=dict(required=False, type='dict'), 266 | vios_status=dict(required=False, type='dict'), 267 | # not used so far, can be used to get if1 for hostname resolution 268 | # nim_node=dict(required=False, type='dict'), 269 | 270 | # nim_migvios_setup not supported yet? 271 | # nim_migvios_setup [ -a [ mk_resource={yes|no}] [ file_system=fs_name ] 272 | # [ volume_group=vg_name ] [ disk=disk_name ] 273 | # [device=device ] 274 | # ] [ -B ] [ -F ] [ -S ] [ -v ] 275 | 276 | # mutually exclisive 277 | targets=dict(required=False, type='list'), 278 | target_file_name=dict(required=False, type='str'), 279 | 280 | # following attributes are dictionaries with 281 | # key: 'all' or hostname and value: a string 282 | # example: 283 | # mksysb_name={"tgt1": "hdisk1", "tgt2": "hdisk1"} 284 | # mksysb_name={"all": "hdisk1"} 285 | mksysb_name=dict(required=False, type='dict'), 286 | spot_name=dict(required=False, type='dict'), 287 | backup_file=dict(required=False, type='dict'), 288 | rootvg_clone_disk=dict(required=False, type='dict'), 289 | rootvg_install_disk=dict(required=False, type='dict'), 290 | # Resources (-e option): 291 | res_resolv_conf=dict(required=False, type='dict'), 292 | res_script=dict(required=False, type='dict'), 293 | res_fb_script=dict(required=False, type='dict'), 294 | res_file_res=dict(required=False, type='dict'), 295 | res_image_data=dict(required=False, type='dict'), 296 | res_log=dict(required=False, type='dict'), 297 | 298 | # dictionaries with key: 'all' or hostname and value: bool 299 | cluster_exists=dict(required=False, type='dict'), 300 | validate_input_data=dict(required=False, type='dict'), 301 | skip_rootvg_cloning=dict(required=False, type='dict'), 302 | ), 303 | mutually_exclusive=[['targets', 'target_file_name']], 304 | required_one_of=[['targets', 'target_file_name']], 305 | # TODO: VRO determine mandatory attributes 306 | required_if=[], 307 | ) 308 | 309 | # ========================================================================= 310 | # Get Module params 311 | # ========================================================================= 312 | MODULE.status = {} 313 | MODULE.targets = [] 314 | MODULE.nim_node = {} 315 | nb_error = 0 316 | 317 | # Handle playbook variables 318 | LOGNAME = '/tmp/ansible_upgradeios_debug.log' 319 | if MODULE.params['vars']: 320 | VARS = MODULE.params['vars'] 321 | if VARS is not None and 'log_file' not in VARS: 322 | VARS['log_file'] = LOGNAME 323 | 324 | # Open log file 325 | OUTPUT.append('Log file: {}'.format(VARS['log_file'])) 326 | LOGFRMT = '[%(asctime)s] %(levelname)s: [%(funcName)s:%(thread)d] %(message)s' 327 | logging.basicConfig(filename='{}'.format(VARS['log_file']), format=LOGFRMT, level=logging.DEBUG) 328 | 329 | logging.debug('*** START NIM VIOSUPGRADE OPERATION ***') 330 | 331 | OUTPUT.append('VIOSUpgrade operation for {}'.format(MODULE.params['targets'])) 332 | logging.info('Action {} for {} targets' 333 | .format(MODULE.params['action'], MODULE.params['targets'])) 334 | 335 | # build NIM node info (if needed) 336 | if MODULE.params['nim_node']: 337 | MODULE.nim_node = MODULE.params['nim_node'] 338 | # TODO: remove this, not needed, except maybe for hostname 339 | # if 'nim_vios' not in MODULE.nim_node: 340 | # MODULE.nim_node['nim_vios'] = get_nim_clients_info(MODULE, 'vios') 341 | # logging.debug('NIM VIOS: {}'.format(MODULE.nim_node['nim_vios'])) 342 | 343 | if MODULE.params['target_file_name']: 344 | try: 345 | myfile = open(MODULE.params['target_file_name'], 'r') 346 | csvreader = csv.reader(myfile, delimiter=':') 347 | for line in csvreader: 348 | MODULE.targets.append(line[0].strip()) 349 | myfile.close() 350 | except IOError as e: 351 | msg = 'Failed to parse file {}: {}.'.format(e.filename, e.strerror) 352 | logging.error(msg) 353 | MODULE.fail_json(changed=CHANGED, msg=msg, output=OUTPUT, 354 | debug_output=DEBUG_DATA, status=MODULE.status) 355 | else: 356 | MODULE.params['target_file_name'] = "" 357 | MODULE.targets = MODULE.params['targets'] 358 | 359 | if not MODULE.targets: 360 | msg = 'Empty target list' 361 | OUTPUT.append(msg) 362 | logging.warn(msg + ': {}'.format(MODULE.params['targets'])) 363 | MODULE.exit_json( 364 | changed=False, 365 | msg=msg, 366 | nim_node=MODULE.nim_node, 367 | debug_output=DEBUG_DATA, 368 | output=OUTPUT, 369 | status=MODULE.status) 370 | 371 | OUTPUT.append('Targets list:{}'.format(MODULE.targets)) 372 | logging.debug('Target list: {}'.format(MODULE.targets)) 373 | 374 | if MODULE.params['target_file_name']: 375 | if MODULE.params['action'] != 'get_status': 376 | viosupgrade_file(MODULE, MODULE.params['target_file_name']) 377 | 378 | if 'get_status' in MODULE.params['action']: 379 | viosupgrade_query(MODULE) 380 | 381 | elif MODULE.params['targets']: 382 | if MODULE.params['action'] != 'get_status': 383 | viosupgrade_list(MODULE, MODULE.params['targets']) 384 | 385 | if 'get_status' in MODULE.params['action']: 386 | viosupgrade_query(MODULE) 387 | else: 388 | # should not happen 389 | msg = 'Please speficy one of "targets" or "target_file_name" parameters.' 390 | logging.error(msg) 391 | MODULE.fail_json(changed=CHANGED, msg=msg, output=OUTPUT, 392 | debug_output=DEBUG_DATA, status=MODULE.status) 393 | 394 | # # Prints status for each targets 395 | # msg = 'VIOSUpgrade {} operation status:'.format(MODULE.params['action']) 396 | # if MODULE.status: 397 | # OUTPUT.append(msg) 398 | # logging.info(msg) 399 | # for vios_key in MODULE.status: 400 | # OUTPUT.append(' {} : {}'.format(vios_key, MODULE.status[vios_key])) 401 | # logging.info(' {} : {}'.format(vios_key, MODULE.status[vios_key])) 402 | # if not re.match(r"^SUCCESS", MODULE.status[vios_key]): 403 | # nb_error += 1 404 | # else: 405 | # logging.error(msg + ' MODULE.status table is empty') 406 | # OUTPUT.append(msg + ' Error getting the status') 407 | # MODULE.status = MODULE.params['vios_status'] # can be None 408 | 409 | # # Prints a global result statement 410 | # if nb_error == 0: 411 | # msg = 'VIOSUpgrade {} operation succeeded'\ 412 | # .format(MODULE.params['action']) 413 | # OUTPUT.append(msg) 414 | # logging.info(msg) 415 | # else: 416 | # msg = 'VIOSUpgrade {} operation failed: {} errors'\ 417 | # .format(MODULE.params['action'], nb_error) 418 | # OUTPUT.append(msg) 419 | # logging.error(msg) 420 | 421 | # # ========================================================================= 422 | # # Exit 423 | # # ========================================================================= 424 | # if nb_error == 0: 425 | # MODULE.exit_json( 426 | # changed=CHANGED, 427 | # msg=msg, 428 | # targets=MODULE.targets, 429 | # nim_node=MODULE.nim_node, 430 | # debug_output=DEBUG_DATA, 431 | # output=OUTPUT, 432 | # status=MODULE.status) 433 | 434 | MODULE.fail_json( 435 | changed=CHANGED, 436 | msg=msg, 437 | targets=MODULE.targets, 438 | nim_node=MODULE.nim_node, 439 | debug_output=DEBUG_DATA, 440 | output=OUTPUT, 441 | status=MODULE.status) 442 | -------------------------------------------------------------------------------- /library/aix_suma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2016, International Business Machines Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | ###################################################################### 19 | """AIX SUMA: download fixes, SP or TL on a NIM server""" 20 | 21 | import os 22 | import re 23 | import glob 24 | import shutil 25 | import subprocess 26 | import threading 27 | import logging 28 | # Ansible module 'boilerplate' 29 | # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin 30 | from ansible.module_utils.basic import AnsibleModule 31 | 32 | 33 | DOCUMENTATION = """ 34 | ------ 35 | module: aix_suma 36 | author: "Cyril Bouhallier, Patrice Jacquin" 37 | version_added: "1.0.0" 38 | requirements: [ AIX ] 39 | """ 40 | 41 | 42 | # ---------------------------------------------------------------- 43 | # ---------------------------------------------------------------- 44 | def min_oslevel(dic): 45 | """ 46 | Find the minimun value of a dictionnary. 47 | 48 | arguments: 49 | dict - Dictionnary {machine: oslevel} 50 | return: 51 | minimun oslevel from the dictionnary 52 | """ 53 | oslevel_min = None 54 | 55 | for key, value in iter(dic.items()): 56 | if oslevel_min is None or value < oslevel_min: 57 | oslevel_min = value 58 | 59 | return oslevel_min 60 | 61 | 62 | # ---------------------------------------------------------------- 63 | # ---------------------------------------------------------------- 64 | def max_oslevel(dic): 65 | """ 66 | Find the maximum value of a the oslevel dictionary. 67 | 68 | arguments: 69 | dic - Dictionnary {client: oslevel} 70 | return: 71 | maximum oslevel from the dictionnary 72 | """ 73 | oslevel_max = None 74 | 75 | for key, value in iter(dic.items()): 76 | if oslevel_max is None or value > oslevel_max: 77 | oslevel_max = value 78 | 79 | return oslevel_max 80 | 81 | 82 | # ---------------------------------------------------------------- 83 | # ---------------------------------------------------------------- 84 | def run_cmd(machine, result): 85 | """Run command function, command to be 'threaded'. 86 | 87 | The thread then store the outpout in the dedicated slot of the result 88 | dictionnary. 89 | 90 | arguments: 91 | machine (str): The name machine 92 | result (dict): The result of the command 93 | """ 94 | if machine == 'master': 95 | cmd = ['/usr/bin/oslevel', '-s'] 96 | else: 97 | cmd = ['/usr/lpp/bos.sysmgt/nim/methods/c_rsh', 98 | machine, 99 | '"/usr/bin/oslevel -s"'] 100 | 101 | proc = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, 102 | stderr=subprocess.PIPE) 103 | 104 | # return stdout only ... stripped! 105 | result[machine] = proc.communicate()[0].rstrip() 106 | 107 | 108 | # ---------------------------------------------------------------- 109 | # ---------------------------------------------------------------- 110 | def expand_targets(targets_list, nim_clients): 111 | """ 112 | Expand the list of the targets. 113 | 114 | a taget name could be of the following form: 115 | target* all the NIM client machines whose name starts 116 | with 'target' 117 | target[n1:n2] where n1 and n2 are numeric: target to target 118 | * or ALL all the NIM client machines 119 | client_name the NIM client named 'client_name' 120 | master the NIM master 121 | 122 | sample: target[1:5] target12 other_target* 123 | 124 | arguments: 125 | machine (str): The name machine 126 | result (dict): The result of the command 127 | 128 | return: the list of the existing machines matching the target list 129 | """ 130 | clients = [] 131 | if len(targets_list) == 0: 132 | return clients 133 | 134 | for target in targets_list: 135 | 136 | # ----------------------------------------------------------- 137 | # Build target(s) from: range i.e. quimby[7:12] 138 | # ----------------------------------------------------------- 139 | rmatch = re.match(r"(\w+)\[(\d+):(\d+)\]", target) 140 | if rmatch: 141 | 142 | name = rmatch.group(1) 143 | start = rmatch.group(2) 144 | end = rmatch.group(3) 145 | 146 | for i in range(int(start), int(end) + 1): 147 | # target_results.append("{0}{1:02}".format(name, i)) 148 | curr_name = name + str(i) 149 | if curr_name in nim_clients: 150 | clients.append(curr_name) 151 | 152 | continue 153 | 154 | # ----------------------------------------------------------- 155 | # Build target(s) from: val*. i.e. quimby* 156 | # ----------------------------------------------------------- 157 | rmatch = re.match(r"(\w+)\*$", target) 158 | if rmatch: 159 | 160 | name = rmatch.group(1) 161 | 162 | for curr_name in nim_clients: 163 | if re.match(r"^%s\.*" % name, curr_name): 164 | clients.append(curr_name) 165 | 166 | continue 167 | 168 | # ----------------------------------------------------------- 169 | # Build target(s) from: all or * 170 | # ----------------------------------------------------------- 171 | if target.upper() == 'ALL' or target == '*': 172 | clients = nim_clients 173 | continue 174 | 175 | # ----------------------------------------------------------- 176 | # Build target(s) from: quimby05 quimby08 quimby12 177 | # ----------------------------------------------------------- 178 | if (target in nim_clients) or (target == 'master'): 179 | clients.append(target) 180 | 181 | return list(set(clients)) 182 | 183 | 184 | # ---------------------------------------------------------------- 185 | # ---------------------------------------------------------------- 186 | def exec_cmd(cmd, shell=False): 187 | """Execute a command. 188 | 189 | arguments: 190 | cmd (str): The command to be executed 191 | shell (bool): execute cmd through the shell if set (vulnerable to shell 192 | injection when cmd is from user inputs). If cmd is a string 193 | string, the string specifies the command to execute through 194 | the shell. If cmd is a list, the first item specifies the 195 | command, and other items are arguments to the shell itself. 196 | 197 | return: 198 | ret code: 0 - OK 199 | 1 - CalledProcessError exception 200 | 2 - other exception 201 | both stdout and stderr of the command 202 | """ 203 | out = '' 204 | 205 | logging.debug("exec command:{}".format(cmd)) 206 | try: 207 | out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=shell) 208 | 209 | except subprocess.CalledProcessError as exc: 210 | logging.debug("exec command rc:{} out:{}" 211 | .format(exc.returncode, exc.output)) 212 | return exc.returncode, exc.output 213 | 214 | except OSError as exc: 215 | logging.debug("exec command rc:{} out:{}" 216 | .format(exc.args[0], exc.args)) 217 | return exc.args[0], exc.args 218 | 219 | except Exception as exc: 220 | msg = "Command: {} Exception:{} =>Data:{}"\ 221 | .format(cmd, exc, out) 222 | logging.debug("exec command rc:2 out:{}".format(msg)) 223 | return 2, msg 224 | 225 | logging.debug("exec command rc:0 out:{}".format(out)) 226 | 227 | return 0, out 228 | 229 | 230 | # ---------------------------------------------------------------- 231 | # ---------------------------------------------------------------- 232 | def get_nim_clients(module): 233 | """ 234 | Get the list of the standalones defined on the NIM master. 235 | 236 | return the list of the name of the standlone objects defined on the 237 | NIM master. 238 | """ 239 | std_out = '' 240 | std_err = '' 241 | clients_list = [] 242 | 243 | cmd = ['lsnim', '-t', 'standalone'] 244 | 245 | try: 246 | proc = subprocess.Popen(cmd, shell=False, stdin=None, 247 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) 248 | (std_out, std_err) = proc.communicate() 249 | except Exception as excep: 250 | msg = "Command: {} Exception.Args{} =>Data:{} ... Error :{}"\ 251 | .format(cmd, excep.args, std_out, std_err) 252 | SUMA_ERROR.append(msg) 253 | logging.error(msg) 254 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 255 | 256 | # nim_clients list 257 | for line in std_out.rstrip().split('\n'): 258 | clients_list.append(line.split()[0]) 259 | 260 | return clients_list 261 | 262 | 263 | # ---------------------------------------------------------------- 264 | # ---------------------------------------------------------------- 265 | def get_nim_lpp_source(): 266 | """ 267 | Get the list of the lpp_source defined on the NIM master. 268 | 269 | arguments: 270 | None 271 | 272 | return: 273 | ret code: 0 - OK 274 | 1 - CalledProcessError exception 275 | 2 - other exception 276 | lpp_source_list: dictionary key, value 277 | key = lpp source name 278 | value = lpp source location 279 | """ 280 | std_out = '' 281 | lpp_source_list = {} 282 | 283 | cmd = 'LC_ALL=C lsnim -t lpp_source -l' 284 | 285 | logging.debug("SUMA command:{}".format(cmd)) 286 | 287 | ret, std_out = exec_cmd(cmd, shell=True) 288 | if ret != 0: 289 | logging.error("SUMA command error rc:{}, error: {}" 290 | .format(ret, std_out)) 291 | return ret, std_out 292 | 293 | # lpp_source list 294 | for line in std_out.rstrip().split('\n'): 295 | match_key = re.match(r"^(\S+):", line) 296 | if match_key: 297 | obj_key = match_key.group(1) 298 | else: 299 | match_loc = re.match(r"^\s+location\s+=\s+(\S+)$", line) 300 | if match_loc: 301 | loc = match_loc.group(1) 302 | lpp_source_list[obj_key] = loc 303 | 304 | return 0, lpp_source_list 305 | 306 | 307 | # ---------------------------------------------------------------- 308 | # ---------------------------------------------------------------- 309 | def compute_rq_type(oslevel, empty_list): 310 | """Compute rq_type. 311 | 312 | return: 313 | Latest when oslevel is blank or latest (not case sensitive) 314 | Latest when oslevel is a TL (6 digits) and target list is empty 315 | TL when oslevel is xxxx-xx(-00-0000) 316 | SP when oslevel is xxxx-xx-xx(-xxxx) 317 | ERROR when oslevel is not recognized 318 | """ 319 | if oslevel is None or not oslevel.strip() or oslevel.upper() == 'LATEST': 320 | return 'Latest' 321 | if re.match(r"^([0-9]{4}-[0-9]{2})$", oslevel) and empty_list: 322 | return 'Latest' 323 | if re.match(r"^([0-9]{4}-[0-9]{2})(|-00|-00-0000)$", oslevel): 324 | return 'TL' 325 | if re.match(r"^([0-9]{4}-[0-9]{2}-[0-9]{2})(|-[0-9]{4})$", oslevel): 326 | return 'SP' 327 | 328 | return 'ERROR' 329 | 330 | 331 | # ---------------------------------------------------------------- 332 | # ---------------------------------------------------------------- 333 | def compute_rq_name(rq_type, oslevel, clients_target_oslevel): 334 | """ 335 | Compute rq_name. 336 | if oslevel is a complete SP (12 digits) then return RqName = oslevel 337 | if oslevel is an incomplete SP (8 digits) or equal Latest then execute 338 | a metadata suma request to find the complete SP level (12 digits) 339 | Compute the suma rq_name 340 | - for Latest: return a SP value in the form xxxx-xx-xx-xxxx 341 | - for TL: return the TL value in the form xxxx-xx 342 | - for SP: return the SP value in the form xxxx-xx-xx-xxxx 343 | 344 | arguments: 345 | rq_type 346 | oslevel requested oslevel 347 | clients_target__oslevel oslevel of each selected client 348 | 349 | return: 350 | return code : 0 - OK 351 | 1 - CalledProcessError exception 352 | 2 - other exception 353 | rq_name value or stderr in case of error 354 | """ 355 | metadata_dir = "/tmp/ansible/metadata" # get env variable for that 356 | rq_name = '' 357 | if rq_type == 'Latest': 358 | if not clients_target_oslevel: 359 | if clients_target_oslevel == 'Latest': 360 | logging.error('Error: target oslevel cannot be "Latest"' 361 | 'check you can get the oslevel on targets') 362 | return 2 363 | metadata_filter_ml = oslevel[:7] 364 | if len(metadata_filter_ml) == 4: 365 | metadata_filter_ml += "-00" 366 | else: 367 | # search first the bigest technical level from client list 368 | tl_max = re.match( 369 | r"^([0-9]{4}-[0-9]{2})(|-[0-9]{2}|-[0-9]{2}-[0-9]{4})$", 370 | max_oslevel(clients_target_oslevel)).group(1) 371 | 372 | # search also the lowest technical level from client list 373 | tl_min = re.match( 374 | r"^([0-9]{4}-[0-9]{2})(|-[0-9]{2}|-[0-9]{2}-[0-9]{4})$", 375 | min_oslevel(clients_target_oslevel)).group(1) 376 | 377 | # warn the user if bigest and lowest tl do not belong 378 | # to the same release 379 | if re.match(r"^([0-9]{4})", tl_min).group(1) \ 380 | != re.match(r"^([0-9]{4})", tl_max).group(1): 381 | logging.warning("Error: Release level mismatch, " 382 | "only AIX {} SP/TL will be downloaded\n\n" 383 | .format(tl_max[:2])) 384 | 385 | # tl_max is used to get metadata then to get latest SP 386 | metadata_filter_ml = tl_max 387 | 388 | if not metadata_filter_ml: 389 | logging.error( 390 | 'Error: cannot discover filter ml based on the list of targets') 391 | raise Exception( 392 | 'Error: cannot discover filter ml based on the list of targets') 393 | 394 | if not os.path.exists(metadata_dir): 395 | os.makedirs(metadata_dir) 396 | 397 | # Build suma command to get metadata 398 | cmd = 'LC_ALL=C /usr/sbin/suma -x -a Action=Metadata '\ 399 | '-a RqType=Latest -a FilterML={} -a DLTarget={} -a DisplayName="{}"'\ 400 | .format(metadata_filter_ml, metadata_dir, PARAMS['Description']) 401 | 402 | logging.debug("SUMA command:{}".format(cmd)) 403 | 404 | ret, stdout = exec_cmd(cmd, shell=True) 405 | if ret != 0: 406 | logging.error("SUMA command error rc:{}, error: {}" 407 | .format(ret, stdout)) 408 | return ret, stdout 409 | 410 | logging.debug("SUMA command rc:{}".format(ret)) 411 | 412 | # find latest SP build number for the highest TL 413 | sp_version = None 414 | file_name = metadata_dir + "/installp/ppc/" \ 415 | + metadata_filter_ml + "*.xml" 416 | logging.debug("searched files: {}".format(file_name)) 417 | files = glob.glob(file_name) 418 | logging.debug("found files: {}".format(files)) 419 | for cur_file in files: 420 | logging.debug("open file: {}".format(cur_file)) 421 | fic = open(cur_file, "r") 422 | for line in fic: 423 | logging.debug("line: {}".format(line)) 424 | match_item = re.match( 425 | r"^$", 426 | line) 427 | if match_item: 428 | version = match_item.group(1) 429 | if sp_version is None or version > sp_version: 430 | sp_version = version 431 | break 432 | 433 | rq_name = sp_version 434 | shutil.rmtree(metadata_dir) 435 | 436 | elif rq_type == 'TL': 437 | # target verstion = TL part of the requested version 438 | rq_name = re.match(r"^([0-9]{4}-[0-9]{2})(|-00|-00-0000)$", 439 | oslevel).group(1) 440 | 441 | elif rq_type == 'SP': 442 | if re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}$", oslevel): 443 | rq_name = oslevel 444 | elif re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", oslevel): 445 | metadata_filter_ml = re.match(r"^([0-9]{4}-[0-9]{2})-[0-9]{2}$", 446 | oslevel).group(1) 447 | 448 | if not os.path.exists(metadata_dir): 449 | os.makedirs(metadata_dir) 450 | 451 | # ================================================================= 452 | # Build suma command to get metadata 453 | # ================================================================= 454 | cmd = 'LC_ALL=C /usr/sbin/suma -x -a Action=Metadata '\ 455 | '-a RqType=Latest -a FilterML={} -a DLTarget={} -a DisplayName="{}"'\ 456 | .format(metadata_filter_ml, metadata_dir, PARAMS['Description']) 457 | 458 | logging.debug("suma command: {}".format(cmd)) 459 | 460 | ret, stdout = exec_cmd(cmd, shell=True) 461 | if ret != 0: 462 | logging.error("SUMA command error rc:{}, error: {}" 463 | .format(ret, stdout)) 464 | return ret, stdout 465 | 466 | # find SP build number 467 | sp_version = None 468 | cur_file = metadata_dir + "/installp/ppc/" + oslevel + ".xml" 469 | fic = open(cur_file, "r") 470 | for line in fic: 471 | match_item = re.match( 472 | r"^$", 473 | line) 474 | if match_item: 475 | sp_version = match_item.group(1) 476 | break 477 | 478 | rq_name = sp_version 479 | shutil.rmtree(metadata_dir) 480 | 481 | return 0, rq_name 482 | 483 | 484 | # ---------------------------------------------------------------- 485 | # ---------------------------------------------------------------- 486 | def compute_filter_ml(clients_target_oslevel, rq_name): 487 | 488 | """ 489 | Compute the suma filter ML. 490 | returns the TL part of rq_name if there is no target machine. 491 | returns the lowest Technical Level from the target client list 492 | (clients_oslevel) that is at the same release as the 493 | requested target os_level (rq_name). 494 | """ 495 | minimum_oslevel = None 496 | filter_ml = None 497 | if not clients_target_oslevel: 498 | filter_ml = rq_name[:7] 499 | if len(filter_ml) == 4: 500 | filter_ml += "-00" 501 | else: 502 | for key, value in iter(clients_target_oslevel.items()): 503 | if re.match(r"^([0-9]{4})", value).group(1) == rq_name[:4] \ 504 | and re.match(r"^([0-9]{4}-[0-9]{2}-[0-9]{2})", value).group(1) < rq_name[:10] \ 505 | and (minimum_oslevel is None or value < minimum_oslevel): 506 | minimum_oslevel = value 507 | 508 | if minimum_oslevel is not None: 509 | filter_ml = minimum_oslevel[:7] 510 | 511 | return filter_ml 512 | 513 | 514 | # ---------------------------------------------------------------- 515 | # ---------------------------------------------------------------- 516 | def compute_lpp_source_name(location, rq_name): 517 | """ 518 | Compute lpp source name based on the location. 519 | return: the name of the lpp_source 520 | 521 | When no location is specified or the location is a path the 522 | lpp_source_name is the -lpp_source 523 | else le lpp_source_name is the location value 524 | if location contains a relative path it will be considered as a 525 | lpp_source, and will not be find in lpp source list 526 | because "/" is a wrong caracter fo lpp_source name 527 | """ 528 | lpp_src = '' 529 | oslevel = rq_name 530 | if not location or not location.strip() or location[0] == '/': 531 | if re.match(r"^([0-9]{4}-[0-9]{2})$", oslevel): 532 | oslevel = oslevel + '-00-0000' 533 | lpp_src = "{}-lpp_source".format(oslevel) 534 | else: 535 | lpp_src = location.rstrip('/') 536 | 537 | return lpp_src 538 | 539 | 540 | # ---------------------------------------------------------------- 541 | # ---------------------------------------------------------------- 542 | def compute_dl_target(location, lpp_source, nim_lpp_sources): 543 | """ 544 | Compute suma DL target based on lpp source name. 545 | 546 | When the location is empty, set the location path to 547 | /usr/sys/inst.images 548 | Check if a lpp_source NIM resource already exist and check the path is 549 | the same 550 | When the location is not a path, check that a NIM lpp_source 551 | corresponding to the location value exists, and returns the 552 | location path of this NIM ressource. 553 | 554 | return: 555 | return code : 0 - OK 556 | 1 - if error 557 | dl_target value or msg in case of error 558 | """ 559 | if not location or not location.strip(): 560 | loc = "/usr/sys/inst.images" 561 | else: 562 | loc = location.rstrip('/') 563 | 564 | if loc[0] == '/': 565 | dl_target = "{}/{}".format(loc, lpp_source) 566 | if lpp_source in nim_lpp_sources \ 567 | and nim_lpp_sources[lpp_source] != dl_target: 568 | return 1, "SUMA Error: lpp source location mismatch. It already " \ 569 | "exists a lpp source '{}' with a location different as '{}'" \ 570 | .format(lpp_source, dl_target) 571 | else: 572 | if loc not in nim_lpp_sources: 573 | return 1, "SUMA Error: lpp_source: '{}' does not exist" \ 574 | .format(loc) 575 | 576 | dl_target = nim_lpp_sources[loc] 577 | 578 | return 0, dl_target 579 | 580 | 581 | # ---------------------------------------------------------------- 582 | # ---------------------------------------------------------------- 583 | def suma_command(module, action): 584 | """ 585 | Run a suma command. 586 | 587 | parameters 588 | action preview or download 589 | 590 | return: 591 | ret suma command return code 592 | stdout suma command output 593 | """ 594 | rq_type = PARAMS['RqType'] 595 | if rq_type == 'Latest': 596 | rq_type = 'SP' 597 | 598 | suma_cmd = 'LC_ALL=C /usr/sbin/suma -x -a RqType={} -a Action={} '\ 599 | '-a FilterML={} -a DLTarget={} -a RqName={} -a DisplayName="{}"'\ 600 | .format(rq_type, action, 601 | PARAMS['FilterMl'], PARAMS['DLTarget'], 602 | PARAMS['RqName'], PARAMS['Description']) 603 | 604 | logging.debug("SUMA - Command:{}".format(suma_cmd)) 605 | SUMA_OUTPUT.append("SUMA - Command:{}".format(suma_cmd)) 606 | 607 | ret, stdout = exec_cmd(suma_cmd, shell=True) 608 | if ret != 0: 609 | logging.error("Error: suma {} command failed with return code {}" 610 | .format(action, ret)) 611 | SUMA_ERROR.append("SUMA Command: {} => Error :{}".format(suma_cmd, stdout.split('\n'))) 612 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 613 | 614 | return ret, stdout 615 | 616 | 617 | # ---------------------------------------------------------------- 618 | # ---------------------------------------------------------------- 619 | def nim_command(module): 620 | """ 621 | Run a 'nim -o define' command 622 | 623 | parameters 624 | action 625 | 626 | return: 627 | ret NIM command return code 628 | stdout NIM command output 629 | """ 630 | nim_cmd = 'LC_ALL=C /usr/sbin/nim -o define -t lpp_source -a server=master '\ 631 | '-a location={} -a packages=all -a comments={} {}'\ 632 | .format(PARAMS['DLTarget'], PARAMS['Comments'], PARAMS['LppSource']) 633 | 634 | logging.info("NIM - Command:{}".format(nim_cmd)) 635 | SUMA_OUTPUT.append("NIM command:{}".format(nim_cmd)) 636 | 637 | ret, stdout = exec_cmd(nim_cmd, shell=True) 638 | 639 | if ret != 0: 640 | msg = "NIM Command: {}".format(nim_cmd) 641 | logging.error(msg) 642 | SUMA_ERROR.append(msg) 643 | msg = "NIM operation failed - rc:{}".format(ret) 644 | logging.error(msg) 645 | SUMA_ERROR.append(msg) 646 | logging.error("{}".format(stdout)) 647 | SUMA_ERROR.append("{}".format(stdout)) 648 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 649 | 650 | return ret, stdout 651 | 652 | 653 | # ---------------------------------------------------------------- 654 | # ---------------------------------------------------------------- 655 | def suma_list(module): 656 | """ 657 | List all SUMA tasks or the task associated with the given task ID 658 | """ 659 | task = PARAMS['task_id'] 660 | if task is None or task.strip() == '': 661 | task = '' 662 | cmde = "/usr/sbin/suma -l {}".format(task) 663 | ret, stdout, stderr = module.run_command(cmde) 664 | 665 | if ret != 0: 666 | msg = "SUMA Error: list command: '{}' failed with return code {}" \ 667 | .format(cmde, ret) 668 | logging.error(msg) 669 | SUMA_ERROR.append(msg) 670 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 671 | 672 | SUMA_OUTPUT.append('List SUMA tasks:') 673 | SUMA_OUTPUT.append(stdout.split('\n')) 674 | 675 | 676 | # ---------------------------------------------------------------- 677 | # ---------------------------------------------------------------- 678 | def check_time(val, mini, maxi): 679 | """ 680 | Check a value is equal to '*' or is a numeric value in the 681 | [mini, maxi] range 682 | """ 683 | if val == '*': 684 | return True 685 | 686 | if val.isdigit() and mini <= int(val) and maxi >= int(val): 687 | return True 688 | 689 | return False 690 | 691 | 692 | # ---------------------------------------------------------------- 693 | # ---------------------------------------------------------------- 694 | def suma_edit(module): 695 | """ 696 | Edit a SUMA task associated with the given task ID 697 | 698 | Depending on the shed_time parameter value, the task wil be scheduled, 699 | unscheduled or saved 700 | """ 701 | cmde = '/usr/sbin/suma' 702 | if PARAMS['sched_time'] is None: 703 | # save 704 | cmde += ' w' 705 | 706 | elif not PARAMS['sched_time'].strip(): 707 | # unschedule 708 | cmde += ' u' 709 | 710 | else: 711 | # schedule 712 | minute, hour, day, month, weekday = PARAMS['sched_time'].split(' ') 713 | 714 | if check_time(minute, 0, 59) and check_time(hour, 0, 23) \ 715 | and check_time(day, 1, 31) and check_time(month, 1, 12) \ 716 | and check_time(weekday, 0, 6): 717 | 718 | cmde += ' -s "{}"'.format(PARAMS['sched_time']) 719 | else: 720 | msg = "Error: SUMA edit command: '{}' Bad schedule time".format(cmde) 721 | logging.error(msg) 722 | SUMA_ERROR.append(msg) 723 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 724 | 725 | cmde += ' {}'.format(PARAMS['task_id']) 726 | ret, stdout, stderr = module.run_command(cmde) 727 | 728 | if ret != 0: 729 | msg = "SUMA Error: edit command: '{}' failed with return code {}" \ 730 | .format(cmde, ret) 731 | logging.error(msg) 732 | SUMA_ERROR.append(msg) 733 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 734 | 735 | SUMA_OUTPUT.append("Edit SUMA task {}".format(PARAMS['task_id'])) 736 | SUMA_OUTPUT.append(stdout.split('\n')) 737 | 738 | 739 | # ---------------------------------------------------------------- 740 | # ---------------------------------------------------------------- 741 | def suma_unschedule(module): 742 | """ 743 | Unschedule a SUMA task associated with the given task ID 744 | """ 745 | cmde = "/usr/sbin/suma -u {}".format(PARAMS['task_id']) 746 | ret, stdout, stderr = module.run_command(cmde) 747 | 748 | if ret != 0: 749 | msg = "SUMA Error: unschedule command: '{}' failed with return code {}" \ 750 | .format(cmde, ret) 751 | logging.error(msg) 752 | SUMA_ERROR.append(msg) 753 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 754 | 755 | SUMA_OUTPUT.append("Unschedule suma task: {}".format(PARAMS['task_id'])) 756 | SUMA_OUTPUT.append(stdout.split('\n')) 757 | 758 | 759 | # ---------------------------------------------------------------- 760 | # ---------------------------------------------------------------- 761 | def suma_delete(module): 762 | """ 763 | Delete the SUMA task associated with the given task ID 764 | """ 765 | cmde = "/usr/sbin/suma -d {}".format(PARAMS['task_id']) 766 | ret, stdout, stderr = module.run_command(cmde) 767 | 768 | if ret != 0: 769 | msg = "SUMA Error: delete command: '{}' failed with return code {}" \ 770 | .format(cmde, ret) 771 | logging.error(msg) 772 | SUMA_ERROR.append(msg) 773 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 774 | 775 | SUMA_OUTPUT.append("Delete SUMA task {}".format(PARAMS['task_id'])) 776 | SUMA_OUTPUT.append(stdout.split('\n')) 777 | 778 | 779 | # ---------------------------------------------------------------- 780 | # ---------------------------------------------------------------- 781 | def suma_config(module): 782 | """ 783 | List the SUMA global configuration settings 784 | """ 785 | cmde = '/usr/sbin/suma -c' 786 | ret, stdout, stderr = module.run_command(cmde) 787 | 788 | if ret != 0: 789 | msg = "SUMA Error: config command: '{}' failed with return code {}" \ 790 | .format(cmde, ret) 791 | logging.error(msg) 792 | SUMA_ERROR.append(msg) 793 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 794 | 795 | SUMA_OUTPUT.append('SUMA global configuration settings:') 796 | SUMA_OUTPUT.append(stdout.split('\n')) 797 | 798 | 799 | # ---------------------------------------------------------------- 800 | # ---------------------------------------------------------------- 801 | def suma_default(module): 802 | """ 803 | List default SUMA tasks 804 | """ 805 | cmde = '/usr/sbin/suma -D' 806 | ret, stdout, stderr = module.run_command(cmde) 807 | 808 | if ret != 0: 809 | msg = "SUMA Error: default command: '{}' failed with return code {}" \ 810 | .format(cmde, ret) 811 | logging.error(msg) 812 | SUMA_ERROR.append(msg) 813 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 814 | 815 | SUMA_OUTPUT.append('SUMA default task:') 816 | SUMA_OUTPUT.append(stdout.split('\n')) 817 | 818 | 819 | # ---------------------------------------------------------------- 820 | # ---------------------------------------------------------------- 821 | def suma_down_prev(module): 822 | """ 823 | Dowload (or preview) action 824 | """ 825 | 826 | global SUMA_CHANGED 827 | global PARAMS 828 | targets_list = [] 829 | empty_list = False 830 | if PARAMS['targets'] != '': 831 | targets_list = PARAMS['targets'].split(' ') 832 | else: 833 | empty_list = True 834 | req_oslevel = PARAMS['req_oslevel'] 835 | if req_oslevel is None \ 836 | or not req_oslevel.strip() \ 837 | or req_oslevel.upper() == 'LATEST': 838 | req_oslevel = 'Latest' 839 | PARAMS['req_oslevel'] = req_oslevel 840 | 841 | if not targets_list: 842 | if req_oslevel == 'Latest': 843 | msg = 'Oslevel target could not be empty or equal "Latest" when' \ 844 | ' target machine list is empty' 845 | logging.error(msg) 846 | SUMA_ERROR.append(msg) 847 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 848 | elif re.match(r"^([0-9]{4}-[0-9]{2})(-00|-00-0000)$", req_oslevel): 849 | msg = 'When no Service Pack is provided , a target machine list is required' 850 | logging.error(msg) 851 | SUMA_ERROR.append(msg) 852 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 853 | else: 854 | if re.match(r"^([0-9]{4})(|-00|-00-00|-00-00-0000)$", req_oslevel): 855 | msg = 'Specify a non 0 value for the Technical Level or the Service Pack' 856 | logging.error(msg) 857 | SUMA_ERROR.append(msg) 858 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 859 | 860 | # ========================================================================= 861 | # build NIM lpp_source list 862 | # ========================================================================= 863 | nim_lpp_sources = {} 864 | ret, nim_lpp_sources = get_nim_lpp_source() 865 | if ret != 0: 866 | msg = "SUMA Error: Getting the lpp_source list - rc:{}, error:{}" \ 867 | .format(ret, nim_lpp_sources) 868 | logging.error(msg) 869 | SUMA_ERROR.append(msg) 870 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 871 | 872 | logging.debug("lpp source list: {}".format(nim_lpp_sources)) 873 | 874 | # =========================================== 875 | # Build nim_clients list 876 | # =========================================== 877 | nim_clients = [] 878 | nim_clients = get_nim_clients(module) 879 | nim_clients.append('master') 880 | 881 | logging.debug("NIM Clients: {}".format(nim_clients)) 882 | 883 | # =========================================== 884 | # Build targets list 885 | # =========================================== 886 | target_clients = [] 887 | target_clients = expand_targets(targets_list, nim_clients) 888 | PARAMS['target_clients'] = target_clients 889 | 890 | logging.info("SUMA - Target list: {}".format(len(targets_list))) 891 | logging.info("SUMA - Target clients: {}".format(len(target_clients))) 892 | 893 | if len(targets_list) != 0 and len(target_clients) == 0: 894 | # the tagets_list doesn't match any NIM clients 895 | msg = "SUMA Error: The target patern '{}' does not match any NIM client" \ 896 | .format(PARAMS['targets']) 897 | logging.error(msg) 898 | SUMA_ERROR.append(msg) 899 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 900 | 901 | # ========================================================================= 902 | # Launch threads to collect information on targeted nim clients 903 | # ========================================================================= 904 | threads = [] 905 | clients_oslevel = {} 906 | 907 | for machine in target_clients: 908 | process = threading.Thread(target=run_cmd, 909 | args=(machine, clients_oslevel)) 910 | process.start() 911 | threads.append(process) 912 | 913 | for process in threads: 914 | process.join() 915 | 916 | logging.debug("oslevel unclean dict: {}".format(clients_oslevel)) 917 | 918 | # ========================================================================= 919 | # Delete empty value of dictionnary 920 | # ========================================================================= 921 | removed_oslevel = [] 922 | 923 | for key in [k for (k, v) in clients_oslevel.items() if not v]: 924 | removed_oslevel.append(key) 925 | del clients_oslevel[key] 926 | 927 | # Check we have at least one oslevel when a target is specified 928 | if len(targets_list) != 0 and len(clients_oslevel) == 0: 929 | msg = "SUMA Error: Cannot retrieve oslevel for any NIM client of the target list" 930 | logging.error(msg) 931 | SUMA_ERROR.append(msg) 932 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 933 | 934 | logging.debug("oslevel cleaned dict: {}".format(clients_oslevel)) 935 | 936 | if len(removed_oslevel) != 0: 937 | msg = "SUMA - unavailable client list: {}".format(removed_oslevel) 938 | SUMA_ERROR.append(msg) 939 | SUMA_OUTPUT.append(msg) 940 | logging.warn(msg) 941 | 942 | # ========================================================================= 943 | # compute SUMA request type based on oslevel property 944 | # ========================================================================= 945 | rq_type = compute_rq_type(PARAMS['req_oslevel'], empty_list) 946 | if rq_type == 'ERROR': 947 | msg = "SUMA Error: Invalid oslevel: '{}'".format(PARAMS['req_oslevel']) 948 | logging.error(msg) 949 | SUMA_ERROR.append(msg) 950 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 951 | 952 | PARAMS['RqType'] = rq_type 953 | 954 | logging.debug("SUMA req Type: {}".format(rq_type)) 955 | 956 | # ========================================================================= 957 | # compute SUMA request name based on metadata info 958 | # ========================================================================= 959 | ret, rq_name = compute_rq_name(rq_type, PARAMS['req_oslevel'], clients_oslevel) 960 | if ret != 0: 961 | msg = "SUMA Error: compute_rq_name - rc:{}, error:{}" \ 962 | .format(ret, rq_name) 963 | logging.error(msg) 964 | SUMA_OUTPUT.append(msg) 965 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 966 | 967 | PARAMS['RqName'] = rq_name 968 | 969 | logging.debug("Suma req Name: {}".format(rq_name)) 970 | 971 | # ========================================================================= 972 | # Compute the filter_ml i.e. the min oslevel from the clients_oslevel 973 | # ========================================================================= 974 | filter_ml = compute_filter_ml(clients_oslevel, rq_name) 975 | PARAMS['FilterMl'] = filter_ml 976 | 977 | logging.debug("{} <= Min Oslevel".format(filter_ml)) 978 | 979 | if filter_ml is None: 980 | # no technical level found for the target machines 981 | msg = "SUMA Error: There is no target machine matching the requested oslevel {}." \ 982 | .format(rq_name[:10]) 983 | logging.error(msg) 984 | SUMA_ERROR.append(msg) 985 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 986 | 987 | # ========================================================================= 988 | # metadata does not match any fixes 989 | # ========================================================================= 990 | if not rq_name or not rq_name.strip(): 991 | msg = "SUMA - Error: oslevel {} doesn't match any fixes" \ 992 | .format(PARAMS['req_oslevel']) 993 | logging.error(msg) 994 | SUMA_ERROR.append(msg) 995 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 996 | 997 | logging.debug("Suma req Name: {}".format(rq_name)) 998 | 999 | # ========================================================================= 1000 | # compute lpp source name based on request name 1001 | # ========================================================================= 1002 | lpp_source = compute_lpp_source_name(PARAMS['location'], rq_name) 1003 | PARAMS['LppSource'] = lpp_source 1004 | 1005 | logging.debug("Lpp source name: {}".format(lpp_source)) 1006 | 1007 | # ========================================================================= 1008 | # compute suma dl target based on lpp source name 1009 | # ========================================================================= 1010 | ret, dl_target = compute_dl_target(PARAMS['location'], lpp_source, 1011 | nim_lpp_sources) 1012 | if ret != 0: 1013 | msg = "SUMA Error: compute_dl_target - {}".format(dl_target) 1014 | logging.error(msg) 1015 | SUMA_ERROR.append(msg) 1016 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 1017 | 1018 | PARAMS['DLTarget'] = dl_target 1019 | 1020 | logging.debug("DL target: {}".format(dl_target)) 1021 | 1022 | # display user message 1023 | logging.info("The builded lpp_source will be: {}.".format(lpp_source)) 1024 | logging.info("The lpp_source location will be: {}.".format(dl_target)) 1025 | logging.info("The lpp_source will be available to update machines from {}-00 to {}." 1026 | .format(filter_ml, rq_name)) 1027 | if rq_type == 'Latest': 1028 | logging.info("{} is the Latest SP of TL {}." 1029 | .format(rq_name, filter_ml)) 1030 | 1031 | PARAMS['Comments'] = '"Updates from {} to {}, built by Ansible'\ 1032 | 'Aix Automate infrastructure updates tools"'\ 1033 | .format(filter_ml, rq_name) 1034 | 1035 | # ======================================================================== 1036 | # Make lpp_source_dir='/usr/sys/inst.images/{}-lpp_source'.format(rq_name) 1037 | # ======================================================================== 1038 | if not os.path.exists(dl_target): 1039 | os.makedirs(dl_target) 1040 | 1041 | logging.debug("mkdir command:{}".format(dl_target)) 1042 | 1043 | # ======================================================================== 1044 | # SUMA command for preview 1045 | # ======================================================================== 1046 | ret, stdout = suma_command(module, 'Preview') 1047 | logging.debug("SUMA preview stdout:{}".format(stdout)) 1048 | 1049 | # parse output to see if there is something to download 1050 | downloaded = 0 1051 | failed = 0 1052 | skipped = 0 1053 | for line in stdout.rstrip().split('\n'): 1054 | line = line.rstrip() 1055 | matched = re.match(r"^\s+(\d+)\s+downloaded$", line) 1056 | if matched: 1057 | downloaded = int(matched.group(1)) 1058 | continue 1059 | matched = re.match(r"^\s+(\d+)\s+failed$", line) 1060 | if matched: 1061 | failed = int(matched.group(1)) 1062 | continue 1063 | matched = re.match(r"^\s+(\d+)\s+skipped$", line) 1064 | if matched: 1065 | skipped = int(matched.group(1)) 1066 | 1067 | msg = "Preview summary : {} to download, {} failed, {} skipped"\ 1068 | .format(downloaded, failed, skipped) 1069 | logging.info(msg) 1070 | SUMA_OUTPUT.append(msg) 1071 | 1072 | # ======================================================================== 1073 | # If action is preview or nothing is available to download, we are done 1074 | # else dowload what is found and create associated NIM objects 1075 | # ======================================================================== 1076 | if PARAMS['action'] == 'download': 1077 | if downloaded != 0: 1078 | 1079 | # ================================================================ 1080 | # SUMA command for download 1081 | # ================================================================ 1082 | ret, stdout = suma_command(module, 'Download') 1083 | logging.debug("SUMA dowload stdout:{}".format(stdout)) 1084 | 1085 | # parse output to see if there is something downloaded 1086 | downloaded = 0 1087 | failed = 0 1088 | skipped = 0 1089 | for line in stdout.rstrip().split('\n'): 1090 | line = line.rstrip() 1091 | matched = re.match(r"^\s+(\d+)\s+downloaded$", line) 1092 | if matched: 1093 | downloaded = int(matched.group(1)) 1094 | continue 1095 | matched = re.match(r"^\s+(\d+)\s+failed$", line) 1096 | if matched: 1097 | failed = int(matched.group(1)) 1098 | continue 1099 | matched = re.match(r"^\s+(\d+)\s+skipped$", line) 1100 | if matched: 1101 | skipped = int(matched.group(1)) 1102 | 1103 | msg = "Download summary : {} downloaded, {} failed, {} skipped"\ 1104 | .format(downloaded, failed, skipped) 1105 | logging.info(msg) 1106 | SUMA_OUTPUT.append(msg) 1107 | 1108 | if downloaded != 0: 1109 | SUMA_CHANGED = True 1110 | 1111 | # ==================================================================== 1112 | # Create the associated NIM resource if necessary 1113 | # ==================================================================== 1114 | if lpp_source not in nim_lpp_sources: 1115 | 1116 | # ================================================================ 1117 | # nim -o define command 1118 | # ================================================================ 1119 | ret, stdout = nim_command(module) 1120 | 1121 | SUMA_CHANGED = True 1122 | 1123 | logging.info("NIM operation succeeded - output:{}".format(stdout)) 1124 | SUMA_OUTPUT.append("NIM operation succeeded - output:{}" 1125 | .format(stdout)) 1126 | 1127 | 1128 | ############################################################################## 1129 | 1130 | if __name__ == '__main__': 1131 | 1132 | SUMA_CHANGED = False 1133 | SUMA_OUTPUT = [] 1134 | SUMA_ERROR = [] 1135 | PARAMS = {} 1136 | 1137 | module = AnsibleModule( 1138 | argument_spec=dict( 1139 | oslevel=dict(required=False, type='str'), 1140 | location=dict(required=False, type='str'), 1141 | targets=dict(required=False, type='str'), 1142 | task_id=dict(required=False, type='str'), 1143 | sched_time=dict(required=False, type='str'), 1144 | action=dict(required=False, 1145 | choices=['download', 'preview', 'list', 'edit', 1146 | 'unschedule', 'delete', 'config', 'default'], 1147 | type='str', default='preview'), 1148 | description=dict(required=False, type='str'), 1149 | ), 1150 | required_if=[ 1151 | ['action', 'edit', ['task_id']], 1152 | ['action', 'delete', ['task_id']], 1153 | ['action', 'unschedule', ['task_id']], 1154 | ['action', 'preview', ['location', 'oslevel']], 1155 | ['action', 'download', ['location', 'oslevel']], 1156 | ], 1157 | supports_check_mode=True 1158 | ) 1159 | 1160 | SUMA_CHANGED = False 1161 | 1162 | # Open log file 1163 | logging.basicConfig( 1164 | filename='/tmp/ansible_suma_debug.log', 1165 | format='[%(asctime)s] %(levelname)s: [%(funcName)s:%(thread)d] %(message)s', 1166 | level=logging.DEBUG) 1167 | logging.debug('*** START ***') 1168 | 1169 | # ======================================================================== 1170 | # Get Module params 1171 | # ======================================================================== 1172 | req_oslevel = module.params['oslevel'] 1173 | location = module.params['location'] 1174 | if location.upper() == 'DEFAULT': 1175 | location = '' 1176 | targets = '' 1177 | if 'targets' in module.params.keys(): 1178 | targets = module.params['targets'] 1179 | if targets is None: 1180 | targets = '' 1181 | 1182 | task_id = module.params['task_id'] 1183 | sched_time = module.params['sched_time'] 1184 | action = module.params['action'] 1185 | 1186 | if module.params['description']: 1187 | description = module.params['description'] 1188 | else: 1189 | description = "{} request for oslevel {}".format(action, req_oslevel) 1190 | 1191 | PARAMS['Description'] = description 1192 | PARAMS['action'] = action 1193 | PARAMS['LppSource'] = '' 1194 | PARAMS['target_clients'] = () 1195 | PARAMS['targets'] = targets 1196 | 1197 | # ======================================================================== 1198 | # switch action 1199 | # ======================================================================== 1200 | if action == 'list': 1201 | PARAMS['task_id'] = task_id 1202 | suma_list(module) 1203 | 1204 | elif action == 'edit': 1205 | PARAMS['task_id'] = task_id 1206 | PARAMS['sched_time'] = sched_time 1207 | suma_edit(module) 1208 | 1209 | elif action == 'unschedule': 1210 | PARAMS['task_id'] = task_id 1211 | suma_unschedule(module) 1212 | 1213 | elif action == 'delete': 1214 | PARAMS['task_id'] = task_id 1215 | suma_delete(module) 1216 | 1217 | elif action == 'config': 1218 | suma_config(module) 1219 | 1220 | elif action == 'default': 1221 | suma_default(module) 1222 | 1223 | elif action == 'download' or action == 'preview': 1224 | PARAMS['req_oslevel'] = req_oslevel 1225 | PARAMS['location'] = location 1226 | PARAMS['targets'] = targets 1227 | suma_down_prev(module) 1228 | 1229 | # ======================================================================== 1230 | # Exit 1231 | # ======================================================================== 1232 | module.exit_json( 1233 | changed=SUMA_CHANGED, 1234 | msg="Suma {} completed successfully".format(action), 1235 | suma_output=SUMA_OUTPUT, 1236 | lpp_source_name=PARAMS['LppSource'], 1237 | target_list=" ".join(PARAMS['target_clients'])) 1238 | -------------------------------------------------------------------------------- /library/suma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # 3 | # Copyright 2020, International Business Machines Corporation 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | ###################################################################### 19 | """SUMA: download fixes, SP or TL on an AIX server""" 20 | 21 | import os 22 | import re 23 | import glob 24 | import shutil 25 | import subprocess 26 | import threading 27 | import logging 28 | # Ansible module 'boilerplate' 29 | # pylint: disable=wildcard-import,unused-wildcard-import,redefined-builtin 30 | from ansible.module_utils.basic import AnsibleModule 31 | 32 | 33 | DOCUMENTATION = """ 34 | ------ 35 | module: suma 36 | version_added: "1.0.0" 37 | requirements: [ AIX ] 38 | """ 39 | 40 | # ---------------------------------------------------------------- 41 | # ---------------------------------------------------------------- 42 | def exec_cmd(cmd, shell=False): 43 | """Execute a command. 44 | 45 | arguments: 46 | cmd (str): The command to be executed 47 | shell (bool): execute cmd through the shell if set (vulnerable to shell 48 | injection when cmd is from user inputs). If the cmd is a 49 | string, the string specifies the command to execute through 50 | the shell. If cmd is a list, the first item specifies the 51 | command, and other items are arguments to the shell itself. 52 | 53 | return: 54 | ret code: 0 - OK 55 | 1 - CalledProcessError exception 56 | 2 - other exception 57 | both stdout and stderr of the command 58 | """ 59 | out = '' 60 | 61 | logging.debug("exec command:{}".format(cmd)) 62 | try: 63 | out = subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=shell) 64 | 65 | except subprocess.CalledProcessError as exc: 66 | logging.debug("exec command rc:{} out:{}" 67 | .format(exc.returncode, exc.output)) 68 | return exc.returncode, exc.output 69 | 70 | except OSError as exc: 71 | logging.debug("exec command rc:{} out:{}" 72 | .format(exc.args[0], exc.args)) 73 | return exc.args[0], exc.args 74 | 75 | except Exception as exc: 76 | msg = "Command: {} Exception:{} =>Data:{}"\ 77 | .format(cmd, exc, out) 78 | logging.debug("exec command rc:2 out:{}".format(msg)) 79 | return 2, msg 80 | 81 | logging.debug("exec command rc:0 out:{}".format(out)) 82 | 83 | return 0, out 84 | 85 | # ---------------------------------------------------------------- 86 | # ---------------------------------------------------------------- 87 | def compute_rq_type(oslevel): 88 | """Compute rq_type. 89 | 90 | return: 91 | Latest when oslevel is blank or latest (not case sensitive) 92 | Latest when oslevel is a TL (6 digits) 93 | TL when oslevel is xxxx-xx(-00-0000) 94 | SP when oslevel is xxxx-xx-xx(-xxxx) 95 | ERROR when oslevel is not recognized 96 | """ 97 | if oslevel is None or not oslevel.strip() or oslevel.upper() == 'LATEST': 98 | return 'Latest' 99 | if re.match(r"^([0-9]{4}-[0-9]{2})$", oslevel): 100 | return 'Latest' 101 | if re.match(r"^([0-9]{4}-[0-9]{2})(|-00|-00-0000)$", oslevel): 102 | return 'TL' 103 | if re.match(r"^([0-9]{4}-[0-9]{2}-[0-9]{2})(|-[0-9]{4})$", oslevel): 104 | return 'SP' 105 | 106 | return 'ERROR' 107 | 108 | # ---------------------------------------------------------------- 109 | # ---------------------------------------------------------------- 110 | def compute_rq_name(rq_type, oslevel): 111 | """ 112 | Compute rq_name. 113 | if oslevel is a complete SP (12 digits) then return RqName = oslevel 114 | if oslevel is an incomplete SP (8 digits) or equal Latest then execute 115 | a metadata suma request to find the complete SP level (12 digits) 116 | Compute the suma rq_name 117 | - for Latest: return a SP value in the form xxxx-xx-xx-xxxx 118 | - for TL: return the TL value in the form xxxx-xx 119 | - for SP: return the SP value in the form xxxx-xx-xx-xxxx 120 | 121 | arguments: 122 | rq_type 123 | oslevel requested oslevel 124 | 125 | return: 126 | return code : 0 - OK 127 | 1 - CalledProcessError exception 128 | 2 - other exception 129 | rq_name value or stderr in case of error 130 | """ 131 | global LOGDIR 132 | 133 | metadata_dir = LOGDIR + "/metadata" # get env variable for that 134 | rq_name = '' 135 | if rq_type == 'Latest': 136 | metadata_filter_ml = oslevel[:7] 137 | if len(metadata_filter_ml) == 4: 138 | metadata_filter_ml += "-00" 139 | 140 | if not metadata_filter_ml: 141 | logging.error( 142 | 'Error: cannot discover filter ml based on the target client') 143 | raise Exception( 144 | 'Error: cannot discover filter ml based on the target client') 145 | 146 | if not os.path.exists(metadata_dir): 147 | os.makedirs(metadata_dir) 148 | 149 | # Build suma command to get metadata 150 | cmd = 'LC_ALL=C /usr/sbin/suma -x -a Action=Metadata '\ 151 | '-a RqType=Latest -a DLTarget={} -a DisplayName="{}"'\ 152 | .format(metadata_dir, PARAMS['Description']) 153 | 154 | logging.debug("SUMA command:{}".format(cmd)) 155 | 156 | ret, stdout = exec_cmd(cmd, shell=True) 157 | if ret != 0: 158 | logging.error("SUMA command error rc:{}, error: {}" 159 | .format(ret, stdout)) 160 | return ret, stdout 161 | 162 | logging.debug("SUMA command rc:{}".format(ret)) 163 | 164 | # find latest SP build number for the highest TL 165 | sp_version = None 166 | file_name = metadata_dir + "/installp/ppc/" + "*.xml" 167 | logging.debug("searched files: {}".format(file_name)) 168 | files = glob.glob(file_name) 169 | logging.debug("found files: {}".format(files)) 170 | for cur_file in files: 171 | logging.debug("open file: {}".format(cur_file)) 172 | fic = open(cur_file, "r") 173 | for line in fic: 174 | logging.debug("line: {}".format(line)) 175 | match_item = re.match( 176 | r"^$", 177 | line) 178 | if match_item: 179 | version = match_item.group(1) 180 | if sp_version is None or version > sp_version: 181 | sp_version = version 182 | break 183 | 184 | rq_name = sp_version 185 | shutil.rmtree(metadata_dir) 186 | 187 | elif rq_type == 'TL': 188 | # target version = TL part of the requested version 189 | rq_name = re.match(r"^([0-9]{4}-[0-9]{2})(|-00|-00-0000)$", 190 | oslevel).group(1) 191 | 192 | elif rq_type == 'SP': 193 | if re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}$", oslevel): 194 | rq_name = oslevel 195 | elif re.match(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$", oslevel): 196 | metadata_filter_ml = re.match(r"^([0-9]{4}-[0-9]{2})-[0-9]{2}$", 197 | oslevel).group(1) 198 | 199 | if not os.path.exists(metadata_dir): 200 | os.makedirs(metadata_dir) 201 | 202 | # ================================================================= 203 | # Build suma command to get metadata 204 | # ================================================================= 205 | cmd = 'LC_ALL=C /usr/sbin/suma -x -a Action=Metadata '\ 206 | '-a RqType=Latest -a DLTarget={} -a DisplayName="{}"'\ 207 | .format(metadata_dir, PARAMS['Description']) 208 | 209 | logging.debug("suma command: {}".format(cmd)) 210 | 211 | ret, stdout = exec_cmd(cmd, shell=True) 212 | if ret != 0: 213 | logging.error("SUMA command error rc:{}, error: {}" 214 | .format(ret, stdout)) 215 | return ret, stdout 216 | 217 | # find SP build number 218 | sp_version = None 219 | cur_file = metadata_dir + "/installp/ppc/" + oslevel + ".xml" 220 | fic = open(cur_file, "r") 221 | for line in fic: 222 | match_item = re.match( 223 | r"^$", 224 | line) 225 | if match_item: 226 | sp_version = match_item.group(1) 227 | break 228 | 229 | rq_name = sp_version 230 | shutil.rmtree(metadata_dir) 231 | 232 | return 0, rq_name 233 | 234 | # ---------------------------------------------------------------- 235 | # ---------------------------------------------------------------- 236 | def compute_filter_ml(rq_name): 237 | 238 | """ 239 | Compute the suma filter ML. 240 | returns the TL part of rq_name. 241 | """ 242 | filter_ml = rq_name[:7] 243 | if len(filter_ml) == 4: 244 | filter_ml += "-00" 245 | 246 | return filter_ml 247 | 248 | # ---------------------------------------------------------------- 249 | # ---------------------------------------------------------------- 250 | def compute_dl_target(location): 251 | """ 252 | When the location is empty, set the location path to 253 | /usr/sys/inst.images 254 | 255 | return: 256 | return code : 0 - OK 257 | 1 - if error 258 | dl_target value or msg in case of error 259 | """ 260 | if not location or not location.strip(): 261 | loc = "/usr/sys/inst.images" 262 | else: 263 | loc = location.rstrip('/') 264 | 265 | 266 | dl_target = loc 267 | 268 | return 0, dl_target 269 | 270 | # ---------------------------------------------------------------- 271 | # ---------------------------------------------------------------- 272 | def suma_command(module, action): 273 | """ 274 | Run a suma command. 275 | 276 | parameters 277 | action preview, download or install 278 | 279 | return: 280 | ret suma command return code 281 | stdout suma command output 282 | """ 283 | rq_type = PARAMS['RqType'] 284 | if rq_type == 'Latest': 285 | suma_cmd = 'LC_ALL=C /usr/sbin/suma -x -a RqType={} -a Action={} '\ 286 | '-a DLTarget={} -a DisplayName="{}"'\ 287 | .format(rq_type, action, PARAMS['DLTarget'], PARAMS['Description']) 288 | else: 289 | suma_cmd = 'LC_ALL=C /usr/sbin/suma -x -a RqType={} -a Action={} '\ 290 | '-a DLTarget={} -a RqName={} -a DisplayName="{}"'\ 291 | .format(rq_type, action, PARAMS['DLTarget'], PARAMS['RqName'], PARAMS['Description']) 292 | 293 | logging.debug("SUMA - Command:{}".format(suma_cmd)) 294 | SUMA_OUTPUT.append("SUMA - Command:{}".format(suma_cmd)) 295 | 296 | ret, stdout = exec_cmd(suma_cmd, shell=True) 297 | if ret != 0: 298 | logging.error("Error: suma {} command failed with return code {}" 299 | .format(action, ret)) 300 | SUMA_ERROR.append("SUMA Command: {} => Error :{}".format(suma_cmd, stdout.split('\n'))) 301 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 302 | 303 | return ret, stdout 304 | 305 | # ---------------------------------------------------------------- 306 | # ---------------------------------------------------------------- 307 | def suma_list(module): 308 | """ 309 | List all SUMA tasks or the task associated with the given task ID 310 | """ 311 | task = PARAMS['task_id'] 312 | if task is None or task.strip() == '': 313 | task = '' 314 | cmde = "/usr/sbin/suma -l {}".format(task) 315 | ret, stdout, stderr = module.run_command(cmde) 316 | 317 | if ret != 0: 318 | msg = "SUMA Error: list command: '{}' failed with return code {}" \ 319 | .format(cmde, ret) 320 | logging.error(msg) 321 | SUMA_ERROR.append(msg) 322 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 323 | 324 | SUMA_OUTPUT.append('List SUMA tasks:') 325 | SUMA_OUTPUT.append(stdout.split('\n')) 326 | 327 | # ---------------------------------------------------------------- 328 | # ---------------------------------------------------------------- 329 | def check_time(val, mini, maxi): 330 | """ 331 | Check a value is equal to '*' or is a numeric value in the 332 | [mini, maxi] range 333 | """ 334 | if val == '*': 335 | return True 336 | 337 | if val.isdigit() and mini <= int(val) and maxi >= int(val): 338 | return True 339 | 340 | return False 341 | 342 | # ---------------------------------------------------------------- 343 | # ---------------------------------------------------------------- 344 | def suma_edit(module): 345 | """ 346 | Edit a SUMA task associated with the given task ID 347 | 348 | Depending on the shed_time parameter value, the task wil be scheduled, 349 | unscheduled or saved 350 | """ 351 | cmde = '/usr/sbin/suma' 352 | if PARAMS['sched_time'] is None: 353 | # save 354 | cmde += ' w' 355 | 356 | elif not PARAMS['sched_time'].strip(): 357 | # unschedule 358 | cmde += ' u' 359 | 360 | else: 361 | # schedule 362 | minute, hour, day, month, weekday = PARAMS['sched_time'].split(' ') 363 | 364 | if check_time(minute, 0, 59) and check_time(hour, 0, 23) \ 365 | and check_time(day, 1, 31) and check_time(month, 1, 12) \ 366 | and check_time(weekday, 0, 6): 367 | 368 | cmde += ' -s "{}"'.format(PARAMS['sched_time']) 369 | else: 370 | msg = "Error: SUMA edit command: '{}' Bad schedule time".format(cmde) 371 | logging.error(msg) 372 | SUMA_ERROR.append(msg) 373 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 374 | 375 | cmde += ' {}'.format(PARAMS['task_id']) 376 | ret, stdout, stderr = module.run_command(cmde) 377 | 378 | if ret != 0: 379 | msg = "SUMA Error: edit command: '{}' failed with return code {}" \ 380 | .format(cmde, ret) 381 | logging.error(msg) 382 | SUMA_ERROR.append(msg) 383 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 384 | 385 | SUMA_OUTPUT.append("Edit SUMA task {}".format(PARAMS['task_id'])) 386 | SUMA_OUTPUT.append(stdout.split('\n')) 387 | 388 | # ---------------------------------------------------------------- 389 | # ---------------------------------------------------------------- 390 | def suma_unschedule(module): 391 | """ 392 | Unschedule a SUMA task associated with the given task ID 393 | """ 394 | cmde = "/usr/sbin/suma -u {}".format(PARAMS['task_id']) 395 | ret, stdout, stderr = module.run_command(cmde) 396 | 397 | if ret != 0: 398 | msg = "SUMA Error: unschedule command: '{}' failed with return code {}" \ 399 | .format(cmde, ret) 400 | logging.error(msg) 401 | SUMA_ERROR.append(msg) 402 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 403 | 404 | SUMA_OUTPUT.append("Unschedule suma task: {}".format(PARAMS['task_id'])) 405 | SUMA_OUTPUT.append(stdout.split('\n')) 406 | 407 | # ---------------------------------------------------------------- 408 | # ---------------------------------------------------------------- 409 | def suma_delete(module): 410 | """ 411 | Delete the SUMA task associated with the given task ID 412 | """ 413 | cmde = "/usr/sbin/suma -d {}".format(PARAMS['task_id']) 414 | ret, stdout, stderr = module.run_command(cmde) 415 | 416 | if ret != 0: 417 | msg = "SUMA Error: delete command: '{}' failed with return code {}" \ 418 | .format(cmde, ret) 419 | logging.error(msg) 420 | SUMA_ERROR.append(msg) 421 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 422 | 423 | SUMA_OUTPUT.append("Delete SUMA task {}".format(PARAMS['task_id'])) 424 | SUMA_OUTPUT.append(stdout.split('\n')) 425 | 426 | # ---------------------------------------------------------------- 427 | # ---------------------------------------------------------------- 428 | def suma_config(module): 429 | """ 430 | List the SUMA global configuration settings 431 | """ 432 | cmde = '/usr/sbin/suma -c' 433 | ret, stdout, stderr = module.run_command(cmde) 434 | 435 | if ret != 0: 436 | msg = "SUMA Error: config command: '{}' failed with return code {}" \ 437 | .format(cmde, ret) 438 | logging.error(msg) 439 | SUMA_ERROR.append(msg) 440 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 441 | 442 | SUMA_OUTPUT.append('SUMA global configuration settings:') 443 | SUMA_OUTPUT.append(stdout.split('\n')) 444 | 445 | # ---------------------------------------------------------------- 446 | # ---------------------------------------------------------------- 447 | def suma_default(module): 448 | """ 449 | List default SUMA tasks 450 | """ 451 | cmde = '/usr/sbin/suma -D' 452 | ret, stdout, stderr = module.run_command(cmde) 453 | 454 | if ret != 0: 455 | msg = "SUMA Error: default command: '{}' failed with return code {}" \ 456 | .format(cmde, ret) 457 | logging.error(msg) 458 | SUMA_ERROR.append(msg) 459 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 460 | 461 | SUMA_OUTPUT.append('SUMA default task:') 462 | SUMA_OUTPUT.append(stdout.split('\n')) 463 | 464 | # ---------------------------------------------------------------- 465 | # ---------------------------------------------------------------- 466 | def suma_download(module): 467 | """ 468 | Download / Install (or preview) action 469 | """ 470 | 471 | global SUMA_CHANGED 472 | global PARAMS 473 | global LOGDIR 474 | 475 | req_oslevel = PARAMS['req_oslevel'] 476 | if req_oslevel is None \ 477 | or not req_oslevel.strip() \ 478 | or req_oslevel.upper() == 'LATEST': 479 | req_oslevel = 'Latest' 480 | PARAMS['req_oslevel'] = req_oslevel 481 | 482 | if re.match(r"^([0-9]{4})(|-00|-00-00|-00-00-0000)$", req_oslevel): 483 | msg = 'Specify a non 0 value for the Technical Level or the Service Pack' 484 | logging.error(msg) 485 | SUMA_ERROR.append(msg) 486 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 487 | 488 | 489 | # ========================================================================= 490 | # compute SUMA request type based on oslevel property 491 | # ========================================================================= 492 | rq_type = compute_rq_type(PARAMS['req_oslevel']) 493 | if rq_type == 'ERROR': 494 | msg = "SUMA Error: Invalid oslevel: '{}'".format(PARAMS['req_oslevel']) 495 | logging.error(msg) 496 | SUMA_ERROR.append(msg) 497 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 498 | 499 | PARAMS['RqType'] = rq_type 500 | 501 | logging.debug("SUMA req Type: {}".format(rq_type)) 502 | 503 | # ========================================================================= 504 | # compute SUMA request name based on metadata info 505 | # ========================================================================= 506 | # ret, rq_name = compute_rq_name(rq_type, PARAMS['req_oslevel']) 507 | # if ret != 0: 508 | # msg = "SUMA Error: compute_rq_name - rc:{}, error:{}" \ 509 | # .format(ret, rq_name) 510 | # logging.error(msg) 511 | # SUMA_OUTPUT.append(msg) 512 | # module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 513 | 514 | rq_name = PARAMS['req_oslevel'] 515 | PARAMS['RqName'] = rq_name 516 | 517 | logging.debug("Suma req Name: {}".format(rq_name)) 518 | 519 | # ========================================================================= 520 | # Compute the filter_ml i.e. the min oslevel 521 | # ========================================================================= 522 | filter_ml = compute_filter_ml(rq_name) 523 | 524 | logging.debug("{} <= Min Oslevel".format(filter_ml)) 525 | 526 | if filter_ml is None: 527 | # no technical level found for the target machines 528 | msg = "SUMA Error: There is no target machine matching the requested oslevel {}." \ 529 | .format(rq_name[:10]) 530 | logging.error(msg) 531 | SUMA_ERROR.append(msg) 532 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 533 | 534 | # ========================================================================= 535 | # metadata does not match any fixes 536 | # ========================================================================= 537 | if not rq_name or not rq_name.strip(): 538 | msg = "SUMA - Error: oslevel {} doesn't match any fixes" \ 539 | .format(PARAMS['req_oslevel']) 540 | logging.error(msg) 541 | SUMA_ERROR.append(msg) 542 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 543 | 544 | logging.debug("Suma req Name: {}".format(rq_name)) 545 | 546 | # ========================================================================= 547 | # compute suma dl target 548 | # ========================================================================= 549 | ret, dl_target = compute_dl_target(PARAMS['download_dir']) 550 | if ret != 0: 551 | msg = "SUMA Error: compute_dl_target - {}".format(dl_target) 552 | logging.error(msg) 553 | SUMA_ERROR.append(msg) 554 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 555 | 556 | PARAMS['DLTarget'] = dl_target 557 | 558 | # display user message 559 | logging.debug("DL target: {}".format(dl_target)) 560 | logging.info("The download location will be: {}.".format(dl_target)) 561 | 562 | if rq_type == 'Latest': 563 | logging.info("{} is the Latest SP of TL {}." 564 | .format(rq_name, filter_ml)) 565 | 566 | PARAMS['Comments'] = '"Packages for updates from {} to {}"'\ 567 | .format(filter_ml, rq_name) 568 | 569 | # ======================================================================== 570 | # Create download path/dir 571 | # ======================================================================== 572 | if not os.path.exists(dl_target): 573 | os.makedirs(dl_target) 574 | 575 | logging.debug("mkdir command:{}".format(dl_target)) 576 | 577 | # ======================================================================== 578 | # SUMA command for preview 579 | # ======================================================================== 580 | ret, stdout = suma_command(module, 'Preview') 581 | logging.debug("SUMA preview stdout:{}".format(stdout)) 582 | 583 | # parse output to see if there is something to download 584 | downloaded = 0 585 | failed = 0 586 | skipped = 0 587 | for line in stdout.rstrip().split('\n'): 588 | line = line.rstrip() 589 | matched = re.match(r"^\s+(\d+)\s+downloaded$", line) 590 | if matched: 591 | downloaded = int(matched.group(1)) 592 | continue 593 | matched = re.match(r"^\s+(\d+)\s+failed$", line) 594 | if matched: 595 | failed = int(matched.group(1)) 596 | continue 597 | matched = re.match(r"^\s+(\d+)\s+skipped$", line) 598 | if matched: 599 | skipped = int(matched.group(1)) 600 | 601 | msg = "Preview summary : {} to download, {} failed, {} skipped"\ 602 | .format(downloaded, failed, skipped) 603 | logging.info(msg) 604 | SUMA_OUTPUT.append(msg) 605 | 606 | # ======================================================================== 607 | # If action is preview or nothing is available to download, we are done 608 | # else dowload what is found (and install if necessary) 609 | # ======================================================================== 610 | if PARAMS['action'] == 'download': 611 | if downloaded != 0: 612 | 613 | # ================================================================ 614 | # SUMA command for download 615 | # ================================================================ 616 | ret, stdout = suma_command(module, 'Download') 617 | logging.debug("SUMA dowload stdout:{}".format(stdout)) 618 | 619 | # parse output to see if there is something downloaded 620 | downloaded = 0 621 | failed = 0 622 | skipped = 0 623 | for line in stdout.rstrip().split('\n'): 624 | line = line.rstrip() 625 | matched = re.match(r"^\s+(\d+)\s+downloaded$", line) 626 | if matched: 627 | downloaded = int(matched.group(1)) 628 | continue 629 | matched = re.match(r"^\s+(\d+)\s+failed$", line) 630 | if matched: 631 | failed = int(matched.group(1)) 632 | continue 633 | matched = re.match(r"^\s+(\d+)\s+skipped$", line) 634 | if matched: 635 | skipped = int(matched.group(1)) 636 | 637 | msg = "Download summary : {} downloaded, {} failed, {} skipped"\ 638 | .format(downloaded, failed, skipped) 639 | logging.info(msg) 640 | SUMA_OUTPUT.append(msg) 641 | 642 | if downloaded != 0: 643 | SUMA_CHANGED = True 644 | # =========================================================== 645 | # Install updates 646 | # =========================================================== 647 | if not PARAMS['download_only']: 648 | cmde = "LC_ALL=C /usr/sbin/install_all_updates -Yd {}".format(PARAMS['DLTarget']) 649 | logging.debug("SUMA command:{}".format(cmde)) 650 | SUMA_OUTPUT.append("SUMA command:{}".format(cmde)) 651 | ret, stdout = exec_cmd(cmde, shell=True) 652 | 653 | if ret != 0: 654 | msg = "SUMA Error: install_all_updates command failed with return code {}. Review {}/suma_debug.log for status." \ 655 | .format(ret, LOGDIR) 656 | logging.error(msg) 657 | SUMA_ERROR.append(msg) 658 | module.fail_json(msg=SUMA_ERROR, suma_output=SUMA_OUTPUT) 659 | 660 | ############################################################################## 661 | 662 | if __name__ == '__main__': 663 | 664 | SUMA_CHANGED = False 665 | SUMA_OUTPUT = [] 666 | SUMA_ERROR = [] 667 | PARAMS = {} 668 | LOGDIR = "/var/adm/ansible" 669 | 670 | module = AnsibleModule( 671 | argument_spec=dict( 672 | oslevel=dict(required=False, type='str'), 673 | action=dict(required=False, 674 | choices=['download', 'preview', 'list', 'edit', 675 | 'unschedule', 'delete', 'config', 'default'], 676 | type='str', default='download'), 677 | download_dir=dict(required=False, type='str'), 678 | download_only=dict(required=False, type='bool', default=False), 679 | task_id=dict(required=False, type='str'), 680 | sched_time=dict(required=False, type='str'), 681 | description=dict(required=False, type='str'), 682 | ), 683 | required_if=[ 684 | ['action', 'edit', ['task_id']], 685 | ['action', 'delete', ['task_id']], 686 | ['action', 'download', ['oslevel']], 687 | ['action', 'preview', ['oslevel']], 688 | ['action', 'unschedule', ['task_id']], 689 | ], 690 | supports_check_mode=True 691 | ) 692 | 693 | SUMA_CHANGED = False 694 | 695 | # Open log file 696 | if not os.path.exists(LOGDIR): 697 | os.makedirs(LOGDIR) 698 | logging.basicConfig( 699 | filename=LOGDIR + "/suma_debug.log", 700 | format='[%(asctime)s] %(levelname)s: [%(funcName)s:%(thread)d] %(message)s', 701 | level=logging.DEBUG) 702 | logging.debug('*** START ***') 703 | 704 | # ======================================================================== 705 | # Get Module params 706 | # ======================================================================== 707 | req_oslevel = module.params['oslevel'] 708 | 709 | if module.params['action']: 710 | action = module.params['action'] 711 | else: 712 | action = "download" 713 | 714 | download_dir = module.params['download_dir'] 715 | if download_dir and download_dir.upper() == 'DEFAULT': 716 | download_dir = '' 717 | 718 | download_only = module.params['download_only'] 719 | 720 | task_id = module.params['task_id'] 721 | 722 | sched_time = module.params['sched_time'] 723 | 724 | if module.params['description']: 725 | description = module.params['description'] 726 | else: 727 | description = "{} request for oslevel {}".format(action, req_oslevel) 728 | 729 | PARAMS['action'] = action 730 | PARAMS['download_only'] = download_only 731 | PARAMS['Description'] = description 732 | 733 | # ======================================================================== 734 | # switch action 735 | # ======================================================================== 736 | if action == 'list': 737 | PARAMS['task_id'] = task_id 738 | suma_list(module) 739 | 740 | elif action == 'edit': 741 | PARAMS['task_id'] = task_id 742 | PARAMS['sched_time'] = sched_time 743 | suma_edit(module) 744 | 745 | elif action == 'unschedule': 746 | PARAMS['task_id'] = task_id 747 | suma_unschedule(module) 748 | 749 | elif action == 'delete': 750 | PARAMS['task_id'] = task_id 751 | suma_delete(module) 752 | 753 | elif action == 'config': 754 | suma_config(module) 755 | 756 | elif action == 'default': 757 | suma_default(module) 758 | 759 | elif action == 'download' or action == 'preview': 760 | PARAMS['req_oslevel'] = req_oslevel 761 | PARAMS['download_dir'] = download_dir 762 | suma_download(module) 763 | 764 | 765 | # ======================================================================== 766 | # Exit 767 | # ======================================================================== 768 | module.exit_json( 769 | changed=SUMA_CHANGED, 770 | msg="Suma {} completed successfully".format(action), 771 | suma_output=SUMA_OUTPUT) 772 | -------------------------------------------------------------------------------- /meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | galaxy_info: 3 | author: Paul Finley 4 | description: Playbooks and modules that can be used to manage configurations of and deployments to AIX systems. 5 | company: IBM 6 | 7 | license: Apache License 2.0 8 | 9 | min_ansible_version: 1.2 10 | 11 | platforms: 12 | - name: AIX 13 | versions: 14 | - 6.1 15 | - 7.1 16 | - 7.2 17 | 18 | galaxy_tags: 19 | - aix 20 | - ansible 21 | - automation 22 | - patching 23 | - nim 24 | - suma 25 | 26 | dependencies: [] 27 | -------------------------------------------------------------------------------- /playbook_aix_flrtvc.yml: -------------------------------------------------------------------------------- 1 | - name: "FLRTVC on AIX playbook" 2 | hosts: fattony01 3 | gather_facts: no 4 | tasks: 5 | - name: "FLRTVC" 6 | aix_flrtvc: 7 | targets: quimby01 8 | #quimby-vios* master 9 | path: /flrtvc-ansible 10 | #verbose: yes 11 | #apar: hiper 12 | #csv: /apar.csv 13 | #filesets: devices 14 | #check_only: no 15 | #download_only: no 16 | #clean: no 17 | #force: no 18 | register: result 19 | - debug: var=result 20 | -------------------------------------------------------------------------------- /playbook_aix_nim_check.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "NIM check on AIX playbook" 3 | hosts: all 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: "AIX NIM" 9 | aix_nim: 10 | action: 'check' 11 | description: 'NIM check' 12 | 13 | register: nim_output 14 | 15 | - debug: var=nim_output 16 | 17 | -------------------------------------------------------------------------------- /playbook_aix_nim_reboot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "NIM reboot on AIX" 3 | hosts: all 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: "AIX NIM" 9 | aix_nim: 10 | action: 'reboot' 11 | targets: "quimby0[3:5]" 12 | description: 'NIM reboot' 13 | 14 | register: nim_output 15 | 16 | - debug: var=nim_output 17 | -------------------------------------------------------------------------------- /playbook_aix_nim_vios_altdisk.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "VIOS alternate disk copy on AIX" 3 | hosts: all 4 | gather_facts: no 5 | vars: 6 | log_file: "/tmp/ansible_vios_alt_disk_debug.log" 7 | 8 | tasks: 9 | - name: "AIX VIOS ALT DISK COPY" 10 | aix_nim_vios_alt_disk: 11 | description: 'Perform an alternate disk copy on VIOS' 12 | targets: "(gdrh9v1,hdisk1,gdrh9v2,hdisk2)" 13 | action: alt_disk_copy 14 | vars: "{{ vars }}" 15 | #time_limit: "mm/dd/yyyy hh:mm" 16 | 17 | register: altdisk_copy_result 18 | 19 | - debug: var=altdisk_copy_result 20 | 21 | - name: "AIX VIOS ALT DISK CLEANUP" 22 | aix_nim_vios_alt_disk: 23 | description: 'Remove the altinst_rootvg from the alternate disk' 24 | targets: "(gdrh9v1,hdisk1,gdrh9v2,)" 25 | action: alt_disk_clean 26 | vars: "{{ vars }}" 27 | vios_status: "{{ altdisk_copy_result.status }}" 28 | 29 | register: altdisk_result 30 | 31 | - debug: var=altdisk_result 32 | -------------------------------------------------------------------------------- /playbook_aix_nim_vios_hc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "VIOS health check on AIX" 3 | hosts: all 4 | gather_facts: no 5 | 6 | tasks: 7 | 8 | - name: "AIX HEALTH CHECK" 9 | aix_nim_vios_hc: 10 | description: 'playbook_aix_vios_health_check' 11 | targets: "(gdrh9v1,gdrh9v2) (gdrh10v1,gdrh10v2)" 12 | action: health_check 13 | 14 | register: hc_output 15 | 16 | - debug: var=hc_output 17 | 18 | -------------------------------------------------------------------------------- /playbook_aix_nim_vios_update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "VIOS update on AIX" 3 | hosts: all 4 | gather_facts: no 5 | vars: 6 | log_file: "/tmp/ansible_vios_update_debug.log" 7 | 8 | tasks: 9 | 10 | #- name: "AIX VIOS HEALTH CHECK" 11 | # aix_nim_vios_hc: 12 | # description: 'Check the VIOS(es) can be updated' 13 | # targets: "(gdrh10v1) (gdrh10v2) (gdrh9v1,gdrh9v2)" 14 | # action: "health_check" 15 | # vars: "{{ vars }}" 16 | 17 | # register: hc_result 18 | 19 | 20 | - name: "AIX VIOS ALT DISK COPY" 21 | aix_nim_vios_alt_disk: 22 | description: 'Perform the rootvg copy to an alternate disk' 23 | targets: "(gdrh10v1,hdisk1) (gdrh10v2,hdisk2) (gdrh9v1,hdisk1,gdrh9v2,hdisk1)" 24 | action: "alt_disk_copy" 25 | #vios_status: "{{ hc_result.status }}" 26 | #nim_node: "{{ hc_result.nim_node }}" 27 | vars: "{{ vars }}" 28 | 29 | register: altd_result 30 | 31 | 32 | - name: "AIX NIM update ios" 33 | aix_nim_updateios: 34 | targets: "(gdrh10v1) (gdrh10v2) (gdrh9v1,gdrh9v2)" 35 | #filesets: "none" 36 | installp_bundle: "__smit_bundle_8323538" 37 | lpp_source: "VIOS_225_30-lpp_source" 38 | accept_licenses: "yes" 39 | updateios_flags: "install" 40 | preview: "no" 41 | #time_limit: "mm/dd/yyyy hh:mm" 42 | vios_status: "{{ altd_result.status }}" 43 | nim_node: "{{ altd_result.nim_node }}" 44 | vars: "{{ vars }}" 45 | 46 | register: updt_result 47 | 48 | 49 | ## Uncommented this section to perfom a cleanup, but be careful 50 | ## because this would remove the alternate rootvg which is your 51 | ## backup. 52 | #- name: "AIX VIOS ALT DISK CLEANUP" 53 | #- aix_nim_vios_alt_disk: 54 | # description: 'Remove the altinst_rootvg from the alternate disk' 55 | # targets: "(gdrh10v1,hdisk1) (gdrh10v2,hdisk2) (gdrh9v1,hdisk1) (gdrh9v2,hdisk1)" 56 | # action: alt_disk_clean 57 | # vios_status: "{{ updt_result.status }}" 58 | # vars: "{{ vars }}" 59 | 60 | #- register: altdisk_result 61 | 62 | #- debug: var=hc_result.output 63 | - debug: var=altd_result.output 64 | - debug: var=updt_result.output 65 | #- debug: var=altdisk_result.output 66 | 67 | 68 | -------------------------------------------------------------------------------- /playbook_aix_nim_vios_upgrade.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "VIOS upgrade on AIX" 3 | hosts: all 4 | gather_facts: no 5 | vars: 6 | log_file: "/tmp/ansible_upgradeios_debug.log" 7 | backup_prefix: "vro_ios_bckp" 8 | backup_location: "/export/nim/ios_backup" 9 | 10 | tasks: 11 | 12 | #- name: "AIX VIOS HEALTH CHECK" 13 | # aix_nim_vios_hc: 14 | # description: 'Check the VIOS(es) can be updated' 15 | # targets: "(gdrh10v1) (gdrh10v2) (gdrh9v1,gdrh9v2)" 16 | # action: "health_check" 17 | # vars: "{{ vars }}" 18 | # 19 | # register: hc_result 20 | 21 | - name: "AIX NIM VIOS Backup" 22 | aix_nim_upgradeios: 23 | description: 'Create a vios backup' 24 | action: "backup" 25 | #targets: "(p7juav1,p7juav2) (p7jufv1,p7jufv2)" 26 | targets: "(p7juav1)" 27 | #vios_status: "{{ hc_result.status }}" 28 | vars: "{{ vars }}" 29 | backup_prefix: "{{ vars.backup_prefix }}" 30 | location: "{{ vars.backup_location }}" 31 | #force: "yes" 32 | #time_limit: "mm/dd/yyyy hh:mm" 33 | 34 | register: backup_result 35 | 36 | 37 | - name: "AIX NIM VIOS View Backup" 38 | aix_nim_upgradeios: 39 | description: 'Display vios backup information' 40 | action: "view_backup" 41 | #targets: "(p7juav1,p7juav2) (p7jufv1,p7jufv2)" 42 | targets: "(p7juav1)" 43 | nim_node: "{{ backup_result.nim_node }}" 44 | vios_status: "{{ backup_result.status }}" 45 | vars: "{{ vars }}" 46 | backup_prefix: "{{ vars.backup_prefix }}" 47 | 48 | 49 | - name: "AIX NIM VIOS upgrade and restore backup" 50 | aix_nim_upgradeios: 51 | description: 'Upgrade vios and restore vios backup' 52 | action: "upgrade_restore" 53 | #targets: "(p7juav1,p7juav2) (p7jufv1,p7jufv2)" 54 | targets: "(p7juav1)" 55 | nim_node: "{{ backup_result.nim_node }}" 56 | vios_status: "{{ backup_result.status }}" 57 | vars: "{{ vars }}" 58 | #backup_prefix: "{{ vars.backup_prefix }}" 59 | boot_client: "yes" 60 | resolv_conf: "" 61 | spot_prefix: "" 62 | mksysb_prefix: "" 63 | bosinst_data_prefix: "" 64 | #time_limit: "mm/dd/yyyy hh:mm" 65 | 66 | register: upgrade_result 67 | 68 | ## Use this section to restore a backup previously created without upgrade 69 | #- name: "AIX NIM VIOS Restore Backup" 70 | # aix_nim_upgradeios: 71 | # description: 'Restore a vios backup' 72 | # action: "restore_backup" 73 | # #targets: "(p7juav1,p7juav2) (p7jufv1,p7jufv2)" 74 | # targets: "(p7juav1)" 75 | # nim_node: "{{ upgrade_result.nim_node }}" 76 | # #vios_status: "{{ backup_result.status }}" 77 | # vars: "{{ vars }}" 78 | # backup_prefix: "{{ vars.backup_prefix }}" 79 | # 80 | # register: restore_result 81 | 82 | #- debug: var=hc_result.output 83 | #- debug: var=backup_result.output 84 | #- debug: var=upgrade_result.output 85 | #- debug: var=restore_result.output 86 | 87 | 88 | -------------------------------------------------------------------------------- /playbook_aix_nim_viosupgrade.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "VIOS upgrade on AIX" 3 | hosts: all 4 | gather_facts: no 5 | vars: 6 | log_file: "/tmp/ansible_viosupgrade_debug.log" 7 | targets_info: "/tmp/ansible_target_info" 8 | 9 | tasks: 10 | 11 | #- name: "AIX VIOS HEALTH CHECK" 12 | # aix_nim_vios_hc: 13 | # description: 'Check the VIOS(es) can be updated' 14 | # targets: "(gdrh10v1) (gdrh10v2) (gdrh9v1,gdrh9v2)" 15 | # action: "health_check" 16 | # vars: "{{ vars }}" 17 | # 18 | # register: hc_result 19 | 20 | #- name: "AIX NIM viosupgrade from file" 21 | # aix_nim_viosupgrade: 22 | # description: 'Upgrade vios' 23 | # action: "altdisk_install" # can be bos_install 24 | # #vios_status: "{{ hc_result.status }}" 25 | # vars: "{{ vars }}" 26 | # target_file_name: "{{ vars.targets_info }}" 27 | 28 | # register: file_upgrade_result 29 | 30 | 31 | #- name: "AIX NIM get viosupgrade status from file" 32 | # aix_nim_viosupgrade: 33 | # description: 'Get status of the vios upgrade' 34 | # action: "get_status" 35 | # #nim_node: "{{ backup_result.nim_node }}" 36 | # #vios_status: "{{ file_altdisk_upgrade_result.status }}" 37 | # vars: "{{ vars }}" 38 | # target_file_name: "{{ vars.targets_info }}" 39 | 40 | 41 | - name: "AIX NIM viosupgrade from list" 42 | aix_nim_viosupgrade: 43 | description: 'Upgrade vios' 44 | action: "altdisk_install" # can be bos_install 45 | #vios_status: "{{ hc_result.status }}" 46 | vars: "{{ vars }}" 47 | targets: p7juav1,p7juav2 48 | mksysb_name: {'p7juav1': 'mksysb_p7juav1', 'p7juav2': 'mksysb_p7juav2'} 49 | spot_name: {'p7juav1': 'spot_p7juav1', 'p7juav2': 'spot_p7juav2'} 50 | backup_file: {'p7juav1': '/export/nim/ios_backup/ios_backup_p7juav1'} # if p7juav1 has a ios_backup already taken 51 | rootvg_clone_disk: {'p7juav1': 'hdisk1', 'p7juav2': 'hdisk1'} # could use {'all': 'hdisk1'} 52 | #rootvg_install_disk: {'p7juav1': 'hdisk1', 'p7juav2': 'hdisk1'} # only for action: bos_install 53 | res_resolv_conf: {'all': 'resolv_conf'} 54 | res_script: {'all': 'post_inst_script'} 55 | res_fb_script: {'all': 'fb_script'} 56 | #res_file_res: {'all': 'file_res'} 57 | res_image_data: {'all': 'image_data'} 58 | res_log: {'p7juav1': '/tmp/viosupgrade_p7juav1.log', 'p7juav2': '/tmp/viosupgrade_p7juav2.log'} 59 | cluster_exists: {'all': true} 60 | validate_input_data: {'all': false} 61 | #skip_rootvg_cloning: {'all': true} # only for action: bos_install 62 | 63 | register: upgrade_result 64 | 65 | 66 | - name: "AIX NIM get viosupgrade status from list" 67 | aix_nim_viosupgrade: 68 | description: 'Get status of the vios upgrade' 69 | action: "get_status" 70 | #nim_node: "{{ backup_result.nim_node }}" 71 | #vios_status: "{{ file_altdisk_upgrade_result.status }}" 72 | vars: "{{ vars }}" 73 | targets: p7juav1,p7juav2 74 | 75 | 76 | #- debug: var=hc_result.output 77 | #- debug: var=file_upgrade_result.output 78 | #- debug: var=upgrade_result.output 79 | 80 | 81 | -------------------------------------------------------------------------------- /playbook_aix_suma.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | gather_facts: True 3 | 4 | vars: 5 | # File that ansible will use for logs 6 | logfile: /var/log/ansible_aix_suma.log 7 | 8 | tasks: 9 | 10 | 11 | # ####################################################################### 12 | # ### AIX SUMA: All targets 13 | # ####################################################################### 14 | 15 | - name: "AIX SUMA: All targets" 16 | aix_suma: 17 | targets: all 18 | ignore_errors: True 19 | 20 | register: targets 21 | - debug: var=targets.debug_targets 22 | 23 | -------------------------------------------------------------------------------- /playbook_aix_suma_nim.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA/NIM on AIX playbook" 3 | hosts: all 4 | gather_facts: no 5 | 6 | vars: 7 | target_list: quimby0[7:9] 8 | 9 | tasks: 10 | 11 | ####################################################################### 12 | ### SUMA on AIX 13 | ####################################################################### 14 | - name: "AIX SUMA" 15 | aix_suma: 16 | oslevel: 'latest' 17 | targets: "{{ target_list }}" 18 | location: '/export/extra/lpp_source' 19 | action: download 20 | description: 'Download latest' 21 | 22 | register: suma_res 23 | 24 | - debug: var=suma_res 25 | 26 | ####################################################################### 27 | ### NIM install on AIX 28 | ####################################################################### 29 | - name: "AIX NIM" 30 | aix_nim: 31 | action: 'update' 32 | targets: "{{ suma_res.target_list }}" 33 | lpp_source: "{{ suma_res.lpp_source_name }}" 34 | description: 'NIM update latest' 35 | 36 | register: nim_res 37 | 38 | - debug: var=nim_res.nim_output 39 | 40 | -------------------------------------------------------------------------------- /playbook_aix_suma_targets_all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA on AIX" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | ####################################################################### 9 | ### SUMA on AIX: All targets 10 | ####################################################################### 11 | 12 | - name: "AIX SUMA: All targets" 13 | aix_suma: 14 | oslevel: '7100-04-02-1614' 15 | location: '' 16 | targets: all 17 | action: download 18 | ignore_errors: True 19 | 20 | register: targets 21 | - debug: var=targets.debug_targets 22 | -------------------------------------------------------------------------------- /playbook_aix_suma_targets_list.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA on AIX" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | ####################################################################### 9 | ### SUMA on AIX: List of targets 10 | ####################################################################### 11 | 12 | - name: "AIX SUMA: List of targets" 13 | aix_suma: 14 | oslevel: '7100-04-02-1614' 15 | location: '' 16 | targets: quimby07 quimby09 quimby11 17 | action: download 18 | ignore_errors: True 19 | 20 | register: targets 21 | - debug: var=targets.debug_targets 22 | -------------------------------------------------------------------------------- /playbook_aix_suma_targets_range.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA on AIX" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | ####################################################################### 9 | ### SUMA on AIX: Range of targets 10 | ####################################################################### 11 | 12 | - name: "AIX SUMA: Range of targets" 13 | aix_suma: 14 | oslevel: '7100-04-02-1614' 15 | location: '' 16 | targets: quimby[7:12] 17 | action: download 18 | ignore_errors: True 19 | 20 | register: targets 21 | - debug: var=targets.debug_targets 22 | -------------------------------------------------------------------------------- /playbook_aix_suma_targets_star.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: "SUMA on AIX" 3 | hosts: all 4 | gather_facts: yes 5 | 6 | tasks: 7 | 8 | ####################################################################### 9 | ### AIX SUMA: Star with targets 10 | ####################################################################### 11 | 12 | - name: "AIX SUMA: Wildcard4 with targets" 13 | aix_suma: 14 | targets: '*' 15 | ignore_errors: True 16 | 17 | register: targets 18 | - debug: var=targets.debug_targets 19 | --------------------------------------------------------------------------------