1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# Copyright: (c) 2013, André Paramés <git@andreparames.com>
5# Based on the Git module by Michael DeHaan <michael.dehaan@gmail.com>
6# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
7
8from __future__ import absolute_import, division, print_function
9__metaclass__ = type
10
11ANSIBLE_METADATA = {'metadata_version': '1.1',
12                    'status': ['preview'],
13                    'supported_by': 'community'}
14
15DOCUMENTATION = u'''
16---
17module: bzr
18author:
19- André Paramés (@andreparames)
20version_added: "1.1"
21short_description: Deploy software (or files) from bzr branches
22description:
23    - Manage I(bzr) branches to deploy files or software.
24options:
25    name:
26        description:
27            - SSH or HTTP protocol address of the parent branch.
28        aliases: [ parent ]
29        required: yes
30    dest:
31        description:
32            - Absolute path of where the branch should be cloned to.
33        required: yes
34    version:
35        description:
36            - What version of the branch to clone.  This can be the
37              bzr revno or revid.
38        default: head
39    force:
40        description:
41            - If C(yes), any modified files in the working
42              tree will be discarded.  Before 1.9 the default
43              value was C(yes).
44        type: bool
45        default: 'no'
46    executable:
47        description:
48            - Path to bzr executable to use. If not supplied,
49              the normal mechanism for resolving binary paths will be used.
50        version_added: '1.4'
51'''
52
53EXAMPLES = '''
54# Example bzr checkout from Ansible Playbooks
55- bzr:
56    name: bzr+ssh://foosball.example.org/path/to/branch
57    dest: /srv/checkout
58    version: 22
59'''
60
61import os
62import re
63
64from ansible.module_utils.basic import AnsibleModule
65
66
67class Bzr(object):
68    def __init__(self, module, parent, dest, version, bzr_path):
69        self.module = module
70        self.parent = parent
71        self.dest = dest
72        self.version = version
73        self.bzr_path = bzr_path
74
75    def _command(self, args_list, cwd=None, **kwargs):
76        (rc, out, err) = self.module.run_command([self.bzr_path] + args_list, cwd=cwd, **kwargs)
77        return (rc, out, err)
78
79    def get_version(self):
80        '''samples the version of the bzr branch'''
81
82        cmd = "%s revno" % self.bzr_path
83        rc, stdout, stderr = self.module.run_command(cmd, cwd=self.dest)
84        revno = stdout.strip()
85        return revno
86
87    def clone(self):
88        '''makes a new bzr branch if it does not already exist'''
89        dest_dirname = os.path.dirname(self.dest)
90        try:
91            os.makedirs(dest_dirname)
92        except Exception:
93            pass
94        if self.version.lower() != 'head':
95            args_list = ["branch", "-r", self.version, self.parent, self.dest]
96        else:
97            args_list = ["branch", self.parent, self.dest]
98        return self._command(args_list, check_rc=True, cwd=dest_dirname)
99
100    def has_local_mods(self):
101
102        cmd = "%s status -S" % self.bzr_path
103        rc, stdout, stderr = self.module.run_command(cmd, cwd=self.dest)
104        lines = stdout.splitlines()
105
106        lines = filter(lambda c: not re.search('^\\?\\?.*$', c), lines)
107        return len(lines) > 0
108
109    def reset(self, force):
110        '''
111        Resets the index and working tree to head.
112        Discards any changes to tracked files in the working
113        tree since that commit.
114        '''
115        if not force and self.has_local_mods():
116            self.module.fail_json(msg="Local modifications exist in branch (force=no).")
117        return self._command(["revert"], check_rc=True, cwd=self.dest)
118
119    def fetch(self):
120        '''updates branch from remote sources'''
121        if self.version.lower() != 'head':
122            (rc, out, err) = self._command(["pull", "-r", self.version], cwd=self.dest)
123        else:
124            (rc, out, err) = self._command(["pull"], cwd=self.dest)
125        if rc != 0:
126            self.module.fail_json(msg="Failed to pull")
127        return (rc, out, err)
128
129    def switch_version(self):
130        '''once pulled, switch to a particular revno or revid'''
131        if self.version.lower() != 'head':
132            args_list = ["revert", "-r", self.version]
133        else:
134            args_list = ["revert"]
135        return self._command(args_list, check_rc=True, cwd=self.dest)
136
137
138# ===========================================
139
140def main():
141    module = AnsibleModule(
142        argument_spec=dict(
143            dest=dict(type='path', required=True),
144            name=dict(type='str', required=True, aliases=['parent']),
145            version=dict(type='str', default='head'),
146            force=dict(type='bool', default='no'),
147            executable=dict(type='str'),
148        )
149    )
150
151    dest = module.params['dest']
152    parent = module.params['name']
153    version = module.params['version']
154    force = module.params['force']
155    bzr_path = module.params['executable'] or module.get_bin_path('bzr', True)
156
157    bzrconfig = os.path.join(dest, '.bzr', 'branch', 'branch.conf')
158
159    rc, out, err = (0, None, None)
160
161    bzr = Bzr(module, parent, dest, version, bzr_path)
162
163    # if there is no bzr configuration, do a branch operation
164    # else pull and switch the version
165    before = None
166    local_mods = False
167    if not os.path.exists(bzrconfig):
168        (rc, out, err) = bzr.clone()
169
170    else:
171        # else do a pull
172        local_mods = bzr.has_local_mods()
173        before = bzr.get_version()
174        (rc, out, err) = bzr.reset(force)
175        if rc != 0:
176            module.fail_json(msg=err)
177        (rc, out, err) = bzr.fetch()
178        if rc != 0:
179            module.fail_json(msg=err)
180
181    # switch to version specified regardless of whether
182    # we cloned or pulled
183    (rc, out, err) = bzr.switch_version()
184
185    # determine if we changed anything
186    after = bzr.get_version()
187    changed = False
188
189    if before != after or local_mods:
190        changed = True
191
192    module.exit_json(changed=changed, before=before, after=after)
193
194
195if __name__ == '__main__':
196    main()
197