diff --git a/action_plugins/moinmoin_page.py b/action_plugins/moinmoin_page.py new file mode 100755 index 0000000000000000000000000000000000000000..dff9b56c65029ea4f2107f886910da1c3733d221 --- /dev/null +++ b/action_plugins/moinmoin_page.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python3 + +# Copyright: (c) 2019, Alexandre Iooss <erdnaxe@crans.org> +# +# GNU General Public License v3.0+ + +import re +import urllib.error +import urllib.parse +import urllib.request +import difflib + + +from ansible.errors import AnsibleError +from ansible.plugins.action import ActionBase +from ansible.utils.display import Display +from ansible.module_utils._text import to_native + +display = Display() + + +class ActionModule(ActionBase): + + TRANSFERS_FILES = False + _VALID_ARGS = frozenset(('url', 'user', 'password', 'content', 'revision_comment')) + + def login(self, url, user, password): + """ + Log in and return session cookie or None if failed + + :param url: random wiki url (not root page) + :param user: wiki user + :param password: user's password + :return: session cookie + """ + # Send a HTTP POST request + data = urllib.parse.urlencode({ + 'action': 'login', + 'login': 'Connexion', + 'name': user, + 'password': password + }).encode() + req = urllib.request.Request(url, data) + try: + response = urllib.request.urlopen(req) + cookie = response.getheader('set-cookie') + except urllib.error.HTTPError as e: + # If 404, then also return header + cookie = e.getheader('set-cookie') + + # Check that authentication worked + if not cookie: + raise AnsibleError(to_native('server did not return a session cookie')) + return cookie + + def craft_request(self, suffix): + """ + Crafts a function that takes an url and a cookie, + and returns the content of the requested page with given action suffix. + """ + def f(url, cookie): + req = urllib.request.Request(url + suffix) + req.add_header("Cookie", cookie) + content = urllib.request.urlopen(req).read().decode('utf-8') + return content + return f + + + def edit_ticket(self, url, cookie): + """ + Return edition ticket of url + + :param url: page to edit + :param cookie: session cookie + :return: edit ticket + """ + # Send request with session cookie + content = self.craft_request("?action=edit&editor=text")(url, cookie) + + # Search for ticket + search = re.search('name=\"ticket\" value=\"([^\"]*)\"', content) + if not search: + raise AnsibleError(to_native('no edit ticket was found')) + + return search.group(1) + + + def edit(self, url, user, password, content, revision_comment, cookie): + """ + Edit a MoinMoin wiki page + + :param url: page to edit + :param user: wiki user + :param password: user's password + :param content: content to place on this page + :param revision_comment: revision comment + """ + # Connect and get edit ticket + ticket = self.edit_ticket(url, cookie) + + # Create request and send + data = { + 'button_save': 'Enregistrer les modifications', + 'category': '', + 'comment': revision_comment.encode("utf-8"), + 'savetext': content.encode("utf-8"), + 'action': 'edit', + 'ticket': ticket + } + req = urllib.request.Request(url, urllib.parse.urlencode(data).encode()) + req.add_header("Cookie", cookie) + urllib.request.urlopen(req) + + + def run(self, tmp=None, task_vars=None): + """ + The run method is the main Action Plugin driver. All work is done from within this method. + + tmp: Temporary directory. Sometimes an action plugin sets up + a temporary directory and then calls another module. This parameter + allows us to reuse the same directory for both. + + task_vars: The variables (host vars, group vars, config vars, etc) associated with this task. + Note that while this will contain Ansible facts from the host, they should be used + with caution as a user running Ansible can disable their collection. If you want + make sure that your Action Plugin always has access to the ones it needs, you may + want to consider running the setup module directly in the run the method and getting + the Ansible facts that way. + The strategy plugin which manages running tasks on instances uses an ansible.vars.manager + VariableManager instance to retrieve this context specific dict of variables. + """ + if task_vars is None: + task_vars = dict() + + + result = super(ActionModule, self).run(tmp, task_vars) + del tmp + + result['changed'] = False + + + url = self._task.args.get("url") + user = self._task.args.get("user") + password = self._task.args.get("password") + content = self._task.args.get("content") + revision_comment = self._task.args.get("revision_comment") + + cookie = self.login(url, user, password) + + changed = False + + try: + raw = self.craft_request("?action=raw")(url, cookie) + if raw != content: + changed = True + except urllib.error.HTTPError: # We will create the page. + changed = True + raw = "" + + # Display any change + if changed: + diff = difflib.unified_diff(raw.splitlines(), content.splitlines(), fromfile="old", tofile="new", lineterm="") + for line in diff: + if line.startswith("-"): + display.display(line, "red") + elif line.startswith("+"): + display.display(line, "green") + elif line.startswith("@"): + display.display(line, "yellow") + else: + display.display(line) + + # Do apply the change if not in check mode + if not self._play_context.check_mode: + self.edit(url, user, password, content, revision_comment, cookie) + + + self._supports_check_mode = True + self._supports_async = False + + return result