source: rattail-fabric/rattail_fabric/core.py @ 2393374

Last change on this file since 2393374 was 2393374, checked in by Lance Edgar <ledgar@…>, 12 months ago

Tweak how 'env' is added to context for mako template upload

  • Property mode set to 100644
File size: 10.4 KB
Line 
1# -*- coding: utf-8; -*-
2################################################################################
3#
4#  Rattail -- Retail Software Framework
5#  Copyright © 2010-2018 Lance Edgar
6#
7#  This file is part of Rattail.
8#
9#  Rattail is free software: you can redistribute it and/or modify it under the
10#  terms of the GNU General Public License as published by the Free Software
11#  Foundation, either version 3 of the License, or (at your option) any later
12#  version.
13#
14#  Rattail is distributed in the hope that it will be useful, but WITHOUT ANY
15#  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16#  FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
17#  details.
18#
19#  You should have received a copy of the GNU General Public License along with
20#  Rattail.  If not, see <http://www.gnu.org/licenses/>.
21#
22################################################################################
23"""
24Core Fabric Library
25"""
26
27from __future__ import unicode_literals, absolute_import
28
29import os
30import re
31import tempfile
32
33import six
34
35from fabric.api import sudo, run, cd, local, env, settings, put as fab_put
36from fabric.contrib.files import exists, append, upload_template as fab_upload_template, is_link
37
38from mako.template import Template
39
40
41UNSPECIFIED = object()
42
43
44def get_debian_version():
45    """
46    Fetch the version of Debian running on the target system.
47    """
48    version = run('cat /etc/debian_version')
49    match = re.match(r'^(\d+\.\d+)$', version)
50    if match:
51        return float(match.group(1))
52
53
54def get_ubuntu_version():
55    """
56    Fetch the version of Ubuntu running on the target system
57    """
58    info = run('cat /etc/lsb-release')
59    match = re.search(r'DISTRIB_RELEASE=(\d+\.\d+)', info)
60    if match:
61        return float(match.group(1))
62
63
64def mkdir(paths, owner='root:root', mode=None):
65    """
66    Recursively make one or more directories.
67    """
68    if isinstance(paths, six.string_types):
69        paths = [paths]
70    sudo('mkdir --parents {0}'.format(' '.join(paths)))
71    if owner != 'root:root':
72        if ':' not in owner:
73            owner = '{0}:{0}'.format(owner)
74        sudo('chown {} {}'.format(owner, ' '.join(paths)))
75    if mode is not None:
76        sudo('chmod {0} {1}'.format(mode, ' '.join(paths)))
77
78
79def make_system_user(name='rattail', home='/srv/rattail', uid=None, shell=None):
80    """
81    Make a new system user account, with the given home folder and shell path.
82    """
83    with settings(warn_only=True):
84        result = sudo('getent passwd {0}'.format(name))
85    if result.failed:
86        uid = '--uid {0}'.format(uid) if uid else ''
87        shell = '--shell {0}'.format(shell) if shell else ''
88        sudo('adduser --system --home {0} --group {1} {2} {3}'.format(home, name, uid, shell))
89
90
91def set_timezone(timezone):
92    """
93    Set the system timezone to the given value, e.g. 'America/Chicago'.
94    """
95    sudo('echo {} >/etc/timezone'.format(timezone))
96    if is_link('/etc/localtime'):
97        sudo('ln --symbolic --force /usr/share/zoneinfo/{} /etc/localtime'.format(timezone))
98    else:
99        sudo('cp /usr/share/zoneinfo/{} /etc/localtime'.format(timezone))
100
101
102def agent_sudo(cmd, user=None):
103    """
104    Run a 'sudo' command on the target server, with full agent forwarding.
105    """
106    with settings(forward_agent=True):
107        sudo('SSH_AUTH_SOCK=$SSH_AUTH_SOCK {}'.format(cmd), shell=False, user=user)
108
109
110def put(local_path, remote_path, owner='root:root', **kwargs):
111    """
112    Put a file on the server, and set its ownership.
113    """
114    if 'mode' not in kwargs:
115        kwargs.setdefault('mirror_local_mode', True)
116    kwargs['use_sudo'] = True
117    fab_put(local_path, remote_path, **kwargs)
118    if ':' not in owner:
119        owner = '{}:'.format(owner)
120    sudo("chown {} '{}'".format(owner, remote_path))
121
122
123def upload_template(local_path, remote_path, owner='root:root', **kwargs):
124    """
125    Upload a template to the server, and set its ownership.
126    """
127    if 'mode' not in kwargs:
128        kwargs.setdefault('mirror_local_mode', True)
129    kwargs['use_sudo'] = True
130    fab_upload_template(local_path, remote_path, **kwargs)
131    sudo('chown {0} {1}'.format(owner, remote_path))
132
133
134def upload_mako_template(local_path, remote_path, context={}, encoding='utf_8', **kwargs):
135    """
136    Render a local file as a Mako template, and upload the result to the server.
137    """
138    template = Template(filename=local_path)
139
140    # make copy of context; add env to it
141    context = dict(context)
142    context['env'] = env
143
144    temp_dir = tempfile.mkdtemp(prefix='rattail-fabric.')
145    temp_path = os.path.join(temp_dir, os.path.basename(local_path))
146    with open(temp_path, 'wb') as f:
147        text = template.render(**context)
148        f.write(text.encode(encoding))
149    os.chmod(temp_path, os.stat(local_path).st_mode)
150
151    put(temp_path, remote_path, **kwargs)
152    os.remove(temp_path)
153    os.rmdir(temp_dir)
154
155
156class Deployer(object):
157
158    def __init__(self, deploy_path, last_segment='deploy'):
159        if not os.path.isdir(deploy_path):
160            deploy_path = os.path.abspath(os.path.join(os.path.dirname(deploy_path), last_segment))
161        self.deploy_path = deploy_path
162
163    def __call__(self, local_path, remote_path, **kwargs):
164        self.deploy(local_path, remote_path, **kwargs)
165
166    def full_path(self, local_path):
167        return '{}/{}'.format(self.deploy_path, local_path)
168
169    def local_exists(self, local_path):
170        return os.path.exists(self.full_path(local_path))
171
172    def deploy(self, local_path, remote_path, **kwargs):
173        local_path = self.full_path(local_path)
174        context = kwargs.pop('context', {})
175        if local_path.endswith('.template'):
176            upload_template(local_path, remote_path, context=env, **kwargs)
177        elif local_path.endswith('.mako'):
178            upload_mako_template(local_path, remote_path, context=context, **kwargs)
179        else:
180            put(local_path, remote_path, **kwargs)
181
182    def sudoers(self, local_path, remote_path, mode='0440', **kwargs):
183        self.deploy(local_path, '/tmp/sudoers', mode=mode)
184        sudo('mv /tmp/sudoers {0}'.format(remote_path))
185
186    def apache_site(self, local_path, name, **kwargs):
187        from rattail_fabric.apache import deploy_site
188        deploy_site(self, local_path, name, **kwargs)
189
190    def apache_conf(self, local_path, name, **kwargs):
191        from rattail_fabric.apache import deploy_conf
192        deploy_conf(self, local_path, name, **kwargs)
193
194    def backup_app(self, envname='backup', *args, **kwargs):
195        from rattail_fabric.backup import deploy_backup_app
196        deploy_backup_app(self, envname, *args, **kwargs)
197
198    def certbot_account(self, uuid, localdir='certbot/account'):
199        """
200        Deploy files to establish a certbot account on target server
201        """
202        localdir = localdir.rstrip('/')
203        paths = [
204            '/etc/letsencrypt/accounts',
205            '/etc/letsencrypt/accounts/acme-v01.api.letsencrypt.org',
206            '/etc/letsencrypt/accounts/acme-v01.api.letsencrypt.org/directory',
207        ]
208        final_path = '{}/{}'.format(paths[-1], uuid)
209        paths.append(final_path)
210        if not exists(final_path):
211            mkdir(paths, mode='0700')
212            with cd(final_path):
213                self.deploy('{}/private_key.json'.format(localdir), 'private_key.json', mode='0600')
214                self.deploy('{}/meta.json'.format(localdir), 'meta.json')
215                self.deploy('{}/regr.json'.format(localdir), 'regr.json')
216
217    def luigi_daemon(self, local_path, name=None, register=True, start=True, **kwargs):
218        if name is None:
219            name = local_path.split('/')[-1]
220        self.deploy(local_path, '/etc/init.d/{}'.format(name), **kwargs)
221        if register:
222            sudo('update-rc.d {} defaults'.format(name))
223            if start:
224                sudo('service {} restart'.format(name))
225
226    def soffice_daemon(self, local_path, name=None, register=True, start=True, **kwargs):
227        if name is None:
228            name = local_path.split('/')[-1]
229        self.deploy(local_path, '/etc/init.d/{}'.format(name), **kwargs)
230        if register:
231            sudo('update-rc.d {} defaults'.format(name))
232            if start:
233                sudo('service {} restart'.format(name))
234
235
236def make_deploy(deploy_path, last_segment='deploy'):
237    """
238    Make a ``deploy()`` function, for uploading files to the server.
239
240    During a deployment, one usually needs to upload certain additional files
241    to the server.  It's also often necessary to dynamically define certain
242    settings etc. within these files.  The :func:`upload_template()` and
243    :func:`put()` functions, respectively, handle uploading files which do and
244    do not require dynamic variable substitution.
245
246    The return value from ``make_deploy()`` is a function which will call
247    ``put()`` or ``upload_template()`` based on whether or not the file path
248    ends with ``'.template'``.
249
250    To make the ``deploy()`` function even simpler for the caller, it will
251    assume a certain context for local file paths.  This means one only need
252    provide a base file name when calling ``deploy()``, and it will be
253    interpreted as relative to the function's context path.
254
255    The ``deploy_path`` argument is used to establish the context path for the
256    function.  If it is a folder path, it will be used as-is; otherwise it will
257    be constructed by joining the parent folder of ``deploy_path`` with the
258    value of ``last_segment``.
259
260    Typical usage then is something like::
261
262       from rattail_fabric import make_deploy
263
264       deploy = make_deploy(__file__)
265
266       deploy('rattail/init-filemon', '/etc/init.d/rattail-filemon',
267              mode='0755')
268
269       deploy('rattail/rattail.conf.template', '/etc/rattail.conf')
270
271    This shows what is intended to be typical, i.e. where ``__file__`` is the
272    only argument required for ``make_deploy()``.  For the above to work will
273    require you to have something like this file structure, where
274    ``fabfile.py`` is the script which contains the above code::
275
276       myproject/
277       |-- fabfile.py
278       |-- deploy/
279           `-- rattail/
280               |-- init-filemon
281               |-- rattail.conf.template
282    """
283    return Deployer(deploy_path, last_segment)
284
285
286def rsync(host, *paths):
287    """
288    Runs rsync as root, for the given host and file paths.
289    """
290    for path in paths:
291        assert path.startswith('/')
292        path = path.rstrip('/')
293        # escape path for rsync
294        path = path.replace(' ', r'\\\ ').replace("'", r"\\\'")
295        agent_sudo('rsync -aP --del root@{0}:{1}/ {1}'.format(host, path))
Note: See TracBrowser for help on using the repository browser.