1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Copyright: (c) 2017 John Kwiatkoski (@JayKayy) <jkwiat40@gmail.com> 5# Copyright: (c) 2018 Alexander Bethke (@oolongbrothers) <oolongbrothers@gmx.net> 6# Copyright: (c) 2017 Ansible Project 7# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 8 9from __future__ import (absolute_import, division, print_function) 10__metaclass__ = type 11 12DOCUMENTATION = r''' 13--- 14module: flatpak 15short_description: Manage flatpaks 16description: 17- Allows users to add or remove flatpaks. 18- See the M(community.general.flatpak_remote) module for managing flatpak remotes. 19author: 20- John Kwiatkoski (@JayKayy) 21- Alexander Bethke (@oolongbrothers) 22requirements: 23- flatpak 24options: 25 executable: 26 description: 27 - The path to the C(flatpak) executable to use. 28 - By default, this module looks for the C(flatpak) executable on the path. 29 type: path 30 default: flatpak 31 method: 32 description: 33 - The installation method to use. 34 - Defines if the I(flatpak) is supposed to be installed globally for the whole C(system) 35 or only for the current C(user). 36 type: str 37 choices: [ system, user ] 38 default: system 39 name: 40 description: 41 - The name of the flatpak to manage. To operate on several packages this 42 can accept a list of packages. 43 - When used with I(state=present), I(name) can be specified as a URL to a 44 C(flatpakref) file or the unique reverse DNS name that identifies a flatpak. 45 - Both C(https://) and C(http://) URLs are supported. 46 - When supplying a reverse DNS name, you can use the I(remote) option to specify on what remote 47 to look for the flatpak. An example for a reverse DNS name is C(org.gnome.gedit). 48 - When used with I(state=absent), it is recommended to specify the name in the reverse DNS 49 format. 50 - When supplying a URL with I(state=absent), the module will try to match the 51 installed flatpak based on the name of the flatpakref to remove it. However, there is no 52 guarantee that the names of the flatpakref file and the reverse DNS name of the installed 53 flatpak do match. 54 type: list 55 elements: str 56 required: true 57 no_dependencies: 58 description: 59 - If installing runtime dependencies should be omitted or not 60 - This parameter is primarily implemented for integration testing this module. 61 There might however be some use cases where you would want to have this, like when you are 62 packaging your own flatpaks. 63 type: bool 64 default: false 65 version_added: 3.2.0 66 remote: 67 description: 68 - The flatpak remote (repository) to install the flatpak from. 69 - By default, C(flathub) is assumed, but you do need to add the flathub flatpak_remote before 70 you can use this. 71 - See the M(community.general.flatpak_remote) module for managing flatpak remotes. 72 type: str 73 default: flathub 74 state: 75 description: 76 - Indicates the desired package state. 77 choices: [ absent, present ] 78 type: str 79 default: present 80''' 81 82EXAMPLES = r''' 83- name: Install the spotify flatpak 84 community.general.flatpak: 85 name: https://s3.amazonaws.com/alexlarsson/spotify-repo/spotify.flatpakref 86 state: present 87 88- name: Install the gedit flatpak package without dependencies (not recommended) 89 community.general.flatpak: 90 name: https://git.gnome.org/browse/gnome-apps-nightly/plain/gedit.flatpakref 91 state: present 92 no_dependencies: true 93 94- name: Install the gedit package from flathub for current user 95 community.general.flatpak: 96 name: org.gnome.gedit 97 state: present 98 method: user 99 100- name: Install the Gnome Calendar flatpak from the gnome remote system-wide 101 community.general.flatpak: 102 name: org.gnome.Calendar 103 state: present 104 remote: gnome 105 106- name: Install multiple packages 107 community.general.flatpak: 108 name: 109 - org.gimp.GIMP 110 - org.inkscape.Inkscape 111 - org.mozilla.firefox 112 113- name: Remove the gedit flatpak 114 community.general.flatpak: 115 name: org.gnome.gedit 116 state: absent 117 118- name: Remove multiple packages 119 community.general.flatpak: 120 name: 121 - org.gimp.GIMP 122 - org.inkscape.Inkscape 123 - org.mozilla.firefox 124 state: absent 125''' 126 127RETURN = r''' 128command: 129 description: The exact flatpak command that was executed 130 returned: When a flatpak command has been executed 131 type: str 132 sample: "/usr/bin/flatpak install --user --nontinteractive flathub org.gnome.Calculator" 133msg: 134 description: Module error message 135 returned: failure 136 type: str 137 sample: "Executable '/usr/local/bin/flatpak' was not found on the system." 138rc: 139 description: Return code from flatpak binary 140 returned: When a flatpak command has been executed 141 type: int 142 sample: 0 143stderr: 144 description: Error output from flatpak binary 145 returned: When a flatpak command has been executed 146 type: str 147 sample: "error: Error searching remote flathub: Can't find ref org.gnome.KDE" 148stdout: 149 description: Output from flatpak binary 150 returned: When a flatpak command has been executed 151 type: str 152 sample: "org.gnome.Calendar/x86_64/stable\tcurrent\norg.gnome.gitg/x86_64/stable\tcurrent\n" 153''' 154 155from distutils.version import StrictVersion 156 157from ansible.module_utils.six.moves.urllib.parse import urlparse 158from ansible.module_utils.basic import AnsibleModule 159 160OUTDATED_FLATPAK_VERSION_ERROR_MESSAGE = "Unknown option --columns=application" 161 162 163def install_flat(module, binary, remote, names, method, no_dependencies): 164 """Add new flatpaks.""" 165 global result 166 uri_names = [] 167 id_names = [] 168 for name in names: 169 if name.startswith('http://') or name.startswith('https://'): 170 uri_names.append(name) 171 else: 172 id_names.append(name) 173 base_command = [binary, "install", "--{0}".format(method)] 174 flatpak_version = _flatpak_version(module, binary) 175 if StrictVersion(flatpak_version) < StrictVersion('1.1.3'): 176 base_command += ["-y"] 177 else: 178 base_command += ["--noninteractive"] 179 if no_dependencies: 180 base_command += ["--no-deps"] 181 if uri_names: 182 command = base_command + uri_names 183 _flatpak_command(module, module.check_mode, command) 184 if id_names: 185 command = base_command + [remote] + id_names 186 _flatpak_command(module, module.check_mode, command) 187 result['changed'] = True 188 189 190def uninstall_flat(module, binary, names, method): 191 """Remove existing flatpaks.""" 192 global result 193 installed_flat_names = [ 194 _match_installed_flat_name(module, binary, name, method) 195 for name in names 196 ] 197 command = [binary, "uninstall"] 198 flatpak_version = _flatpak_version(module, binary) 199 if StrictVersion(flatpak_version) < StrictVersion('1.1.3'): 200 command += ["-y"] 201 else: 202 command += ["--noninteractive"] 203 command += ["--{0}".format(method)] + installed_flat_names 204 _flatpak_command(module, module.check_mode, command) 205 result['changed'] = True 206 207 208def flatpak_exists(module, binary, names, method): 209 """Check if the flatpaks are installed.""" 210 command = [binary, "list", "--{0}".format(method), "--app"] 211 output = _flatpak_command(module, False, command) 212 installed = [] 213 not_installed = [] 214 for name in names: 215 parsed_name = _parse_flatpak_name(name).lower() 216 if parsed_name in output.lower(): 217 installed.append(name) 218 else: 219 not_installed.append(name) 220 return installed, not_installed 221 222 223def _match_installed_flat_name(module, binary, name, method): 224 # This is a difficult function, since if the user supplies a flatpakref url, 225 # we have to rely on a naming convention: 226 # The flatpakref file name needs to match the flatpak name 227 global result 228 parsed_name = _parse_flatpak_name(name) 229 # Try running flatpak list with columns feature 230 command = [binary, "list", "--{0}".format(method), "--app", "--columns=application"] 231 _flatpak_command(module, False, command, ignore_failure=True) 232 if result['rc'] != 0 and OUTDATED_FLATPAK_VERSION_ERROR_MESSAGE in result['stderr']: 233 # Probably flatpak before 1.2 234 matched_flatpak_name = \ 235 _match_flat_using_flatpak_column_feature(module, binary, parsed_name, method) 236 else: 237 # Probably flatpak >= 1.2 238 matched_flatpak_name = \ 239 _match_flat_using_outdated_flatpak_format(module, binary, parsed_name, method) 240 241 if matched_flatpak_name: 242 return matched_flatpak_name 243 else: 244 result['msg'] = "Flatpak removal failed: Could not match any installed flatpaks to " +\ 245 "the name `{0}`. ".format(_parse_flatpak_name(name)) +\ 246 "If you used a URL, try using the reverse DNS name of the flatpak" 247 module.fail_json(**result) 248 249 250def _match_flat_using_outdated_flatpak_format(module, binary, parsed_name, method): 251 global result 252 command = [binary, "list", "--{0}".format(method), "--app", "--columns=application"] 253 output = _flatpak_command(module, False, command) 254 for row in output.split('\n'): 255 if parsed_name.lower() == row.lower(): 256 return row 257 258 259def _match_flat_using_flatpak_column_feature(module, binary, parsed_name, method): 260 global result 261 command = [binary, "list", "--{0}".format(method), "--app"] 262 output = _flatpak_command(module, False, command) 263 for row in output.split('\n'): 264 if parsed_name.lower() in row.lower(): 265 return row.split()[0] 266 267 268def _parse_flatpak_name(name): 269 if name.startswith('http://') or name.startswith('https://'): 270 file_name = urlparse(name).path.split('/')[-1] 271 file_name_without_extension = file_name.split('.')[0:-1] 272 common_name = ".".join(file_name_without_extension) 273 else: 274 common_name = name 275 return common_name 276 277 278def _flatpak_version(module, binary): 279 global result 280 command = [binary, "--version"] 281 output = _flatpak_command(module, False, command) 282 version_number = output.split()[1] 283 return version_number 284 285 286def _flatpak_command(module, noop, command, ignore_failure=False): 287 global result 288 result['command'] = ' '.join(command) 289 if noop: 290 result['rc'] = 0 291 return "" 292 293 result['rc'], result['stdout'], result['stderr'] = module.run_command( 294 command, check_rc=not ignore_failure 295 ) 296 return result['stdout'] 297 298 299def main(): 300 # This module supports check mode 301 module = AnsibleModule( 302 argument_spec=dict( 303 name=dict(type='list', elements='str', required=True), 304 remote=dict(type='str', default='flathub'), 305 method=dict(type='str', default='system', 306 choices=['user', 'system']), 307 state=dict(type='str', default='present', 308 choices=['absent', 'present']), 309 no_dependencies=dict(type='bool', default=False), 310 executable=dict(type='path', default='flatpak') 311 ), 312 supports_check_mode=True, 313 ) 314 315 name = module.params['name'] 316 state = module.params['state'] 317 remote = module.params['remote'] 318 no_dependencies = module.params['no_dependencies'] 319 method = module.params['method'] 320 executable = module.params['executable'] 321 binary = module.get_bin_path(executable, None) 322 323 global result 324 result = dict( 325 changed=False 326 ) 327 328 # If the binary was not found, fail the operation 329 if not binary: 330 module.fail_json(msg="Executable '%s' was not found on the system." % executable, **result) 331 332 installed, not_installed = flatpak_exists(module, binary, name, method) 333 if state == 'present' and not_installed: 334 install_flat(module, binary, remote, not_installed, method, no_dependencies) 335 elif state == 'absent' and installed: 336 uninstall_flat(module, binary, installed, method) 337 338 module.exit_json(**result) 339 340 341if __name__ == '__main__': 342 main() 343