hg 8.88 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
#!/usr/bin/python
#-*- coding: utf-8 -*-

# (c) 2013, Yeukhon Wong <yeukhon@acm.org>
#
# This module was originally inspired by Brad Olson's ansible-module-mercurial
# <https://github.com/bradobro/ansible-module-mercurial>.
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.

import os
import shutil
import ConfigParser
from subprocess import Popen, PIPE

DOCUMENTATION = '''
---
module: hg
short_description: Manages Mercurial (hg) repositories.
description:
    - Manages Mercurial (hg) repositories. Supports SSH, HTTP/S and local address.
version_added: "1.0"
author: Yeukhon Wong
options:
    repo:
        description:
            - The repository location.
        required: true
        default: null
    dest:
        description:
            - Absolute path of where the repository should be cloned to.
        required: true
        default: null
    state:
        description:
            - C(hg clone) is performed when state is set to C(present). C(hg pull) and C(hg update) is
              performed when state is set to C(latest). If you want the latest copy of the repository,
              just rely on C(present). C(latest) assumes the repository is already on disk.
        required: false
        default: present
        choices: [ "present", "absent", "latest" ]
    revision:
        description:
            - Equivalent C(-r) option in hg command, which can either be a changeset number or a branch
              name.
        required: false
        default: "default"
    force:
        description:
            - Whether to discard uncommitted changes and remove untracked files or not. Basically, it
              combines C(hg up -C) and C(hg purge).
        required: false
        default: "yes"
        choices: [ "yes", "no" ]

examples:
    - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name"
      description: Clone the default branch of repo_name.

    - code: "hg: repo=https://bitbucket.org/user/repo_name dest=/home/user/repo_name force=yes state=latest"
      description: Ensure the repository at dest is latest and discard any uncommitted and/or untracked files.

notes:
    - If the task seems to be hanging, first verify remote host is in C(known_hosts).
      SSH will prompt user to authorize the first contact with a remote host. One solution is to add
      C(StrictHostKeyChecking no) in C(.ssh/config) which will accept and authorize the connection
      on behalf of the user. However, if you run as a different user such as setting sudo to True),
      for example, root will not look at the user .ssh/config setting.

requirements: [ ]
'''

class HgError(Exception):
    """  Custom exception class to report hg command error. """

    def __init__(self, msg, stderr=''):
        self.msg = msg + \
                  "\n\nExtra information on this error: \n" + \
                   stderr
    def __str__(self):
        return self.msg

def _set_hgrc(hgrc, vals):
    # val is a list of triple-tuple of the form [(section, option, value),...]
    parser = ConfigParser.SafeConfigParser()
    parser.read(hgrc)

    for each in vals:
        section,option, value = each
        if not parser.has_section(section):
            parser.add_section(section)
        parser.set(section, option, value)
    
    f = open(hgrc, 'w')
    parser.write(f)
    f.close()

def _undo_hgrc(hgrc, vals):
    parser = ConfigParser.SafeConfigParser()
    parser.read(hgrc)
    
    for each in vals:
        section, option, value = each
        if parser.has_section(section):
            parser.remove_option(section, option)

    f = open(hgrc, 'w')
    parser.write(f)
    f.close()

125 126 127 128 129 130
def _hg_command(module, args_list):
    (rc, out, err) = module.run_command(['hg'] + args_list)
    return (out, err, rc)

def _hg_discard(module, dest):
    out, err, code = _hg_command(module, ['up', '-C', '-R', dest])
131 132 133
    if code != 0:
        raise HgError(err)

134
def _hg_purge(module, dest):
135 136 137
    hgrc = os.path.join(dest, '.hg/hgrc')
    purge_option = [('extensions', 'purge', '')]
    _set_hgrc(hgrc, purge_option)
138
    out, err, code = _hg_command(module, ['purge', '-R', dest])
139 140 141 142 143
    if code == 0:
        _undo_hgrc(hgrc, purge_option)
    else:
        raise HgError(err)
 
144
def _hg_verify(module, dest):
145 146 147
    error1 = "hg verify failed."
    error2 = "{dest} is not a repository.".format(dest=dest)

148
    out, err, code = _hg_command(module, ['verify', '-R', dest])
149 150 151 152 153 154 155
    if code == 1:
        raise HgError(error1, stderr=err)
    elif code == 255:
        raise HgError(error2, stderr=err)
    elif code == 0:
        return True

156
def _post_op_hg_revision_check(module, dest, revision):
157 158 159 160 161 162 163 164 165 166 167 168 169 170
    """
    Verify the tip is the same as `revision`.

    This function is usually called after some hg operations
    such as `clone`. However, this check is skipped if `revision`
    is the string `default` since it will result an error. 
    Instead, pull is performed.

    """

    err1 = "Unable to perform hg tip."
    err2 = "tip is different from %s. See below for extended summary." % revision

    if revision == 'default':
171
        out, err, code = _hg_command(module, ['pull', '-R', dest])
172 173 174 175 176
        if "no changes found" in out:
            return True
        else:
            raise HgError(err2, stderr=out)
    else:
177
        out, err, code = _hg_command(module, ['tip', '-R', dest])
178 179 180 181 182 183 184 185
        if revision in out:   # revision should be part of the output (changeset: $revision ...)
            return True
        else:
            if code != 0: # something went wrong with hg tip
                raise HgError(err1, stderr=err)
            else: # hg tip is fine, but tip != revision
                raise HgError(err2, stderr=out)

186 187 188
def force_and_clean(module, dest):
    _hg_discard(module, dest)
    _hg_purge(module, dest)
189

190
def pull_and_update(module, repo, dest, revision, force):
191
    if force == 'yes':
192
        force_and_clean(module, dest)
193

194
    if _hg_verify(module, dest):
195
        cmd1 = ['pull', '-R', dest, '-r', revision]
196
        out, err, code = _hg_command(module, cmd1)
197 198 199 200 201

        if code == 1:
            raise HgError("Unable to perform pull on %s" % dest, stderr=err)
        elif code == 0:        
            cmd2 = ['update', '-R', dest, '-r', revision]
202
            out, err, code = _hg_command(module, cmd2)
203 204 205 206
            if code == 1:
                raise HgError("There are unresolved files in %s" % dest, stderr=err)
            elif code == 0:
                # so far pull and update seems to be working, check revision and $revision are equal
207
                _post_op_hg_revision_check(module, dest, revision)
208 209 210 211
                return True
        # when code aren't 1 or 0 in either command
        raise HgError("", stderr=err)

212
def clone(module, repo, dest, revision, force):
213
    if os.path.exists(dest):
214 215
        if _hg_verify(module, dest):  # make sure it's a real repo
            if _post_op_hg_revision_check(module, dest, revision): # make sure revision and $revision are equal
216
                if force == 'yes':
217
                    force_and_clean(module, dest)
218 219 220
                return False

    cmd = ['clone', repo, dest, '-r', revision]
221
    out, err, code = _hg_command(module, cmd)
222
    if code == 0:
223 224
        _hg_verify(module, dest)
        _post_op_hg_revision_check(module, dest, revision)
225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250
        return True
    else:
        raise HgError(err, stderr='')

def main():
    module = AnsibleModule(
        argument_spec = dict(
            repo = dict(required=True),	    
            dest = dict(required=True),
            state = dict(default='present', choices=['present', 'absent', 'latest']),
            revision = dict(default="default"),
            force = dict(default='yes', choices=['yes', 'no']),
        ),
    )
    repo = module.params['repo']
    state = module.params['state']
    dest = module.params['dest']
    revision = module.params['revision']
    force = module.params['force']

    try:
        if state == 'absent':
            if not os.path.exists(dest):
                shutil.rmtree(dest)
            changed = True
        elif state == 'present':
251
            changed = clone(module, repo, dest, revision, force)
252
        elif state == 'latest':
253
            changed = pull_and_update(module, repo, dest, revision, force)
254 255 256 257 258 259 260 261

        module.exit_json(dest=dest, changed=changed)
    except Exception as e:
        module.fail_json(msg=str(e), params=module.params)

# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()