baste / baste / __init__.py

  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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
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
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# coding=utf-8
# Copyright (c) 2012 Structured Abstraction Inc.
# 
# See LICENSE for licensing details.
"""
Baste provides extensions for fabric [1].

Baste allows you to tack together a development stack with simple components in
preparation for development.

[1] - http://docs.fabfile.org/en/1.3.4/index.html

"""
from __future__ import with_statement

import datetime
import os.path
import string
import random

from fabric.api import (
        env,
        execute,
        hide,
        lcd,
        local as local_run,
        run as remote_run,
        settings,
        sudo as remote_sudo,
        prompt
    )
from fabric import colors
from fabric.contrib.project import rsync_project

try:
    from collections import OrderedDict
except ImportError:
    from baste.py24 import OrderedDict

__all__ = [
    'DiffCommand',
    'Git',
    'Mercurial',
    'OrderedDict',
    'PgRestore',
    'PgShell',
    'PgLoadPlain',
    'MysqlLoadPlain',
    'project_relative',
    'python_dependency',
    'Repository',
    'RsyncMedia',
    'RsyncDeployment',
    'StatusCommand',
    'Subversion',
]

#-------------------------------------------------------------------------------
class ExecuteOnHosts(object):
    """
    Represents a command to be run on a set of hosts.

    Using this allows us to temporarily override the fabric env.hosts for a
    specific command.
    """
    #---------------------------------------------------------------------------
    def __init__(self, command, hosts):
        self.command = command
        self.hosts = list(hosts)

    #---------------------------------------------------------------------------
    def __call__(self, *args, **kwargs):
        kwargs['hosts'] = self.hosts
        return execute(self.command, *args, **kwargs)

#-------------------------------------------------------------------------------
def local_sudo(command, user):
    """
    Run a shell command on a local host, with superuser privileges.

    `sudo` is identical in every way to fabric's `fabric.operations.sudo`, except
    that it will run the command locally, instead of remotely.
    """
    cmd = """sudo -u %s sh -c "%s" """ % (user, command)
    local_run(cmd)

#-------------------------------------------------------------------------------
class BasteEnvironment(object):
    """
    A simple class used to store baste environment settings.
    """
    #---------------------------------------------------------------------------
    def get_baste_dev_hosts(self):
        """
        A helper which retrieves the environments `baste_dev_hosts` setting.

        Defaults to "localhost".
        """
        return getattr(env, 'baste_dev_hosts', 'localhost')
    baste_dev_hosts = property(get_baste_dev_hosts)

    #---------------------------------------------------------------------------
    def get_baste_env_hosts(self):
        """
        Parses the `baste_dev_hosts` as a set of semi-colon separated host values.
        """
        return self.baste_dev_hosts.split(";")
    baste_env_hosts = property(get_baste_env_hosts)

    #---------------------------------------------------------------------------
    def get_run_command(self):
        """
        Returns a run command that will run against either a local or remote env.

        Uses the fabric env setting `baste_dev_hosts` to determine which it is.
        `baste_dev_hosts` may be set to "localhost" or "host1;host2". The default
        is "localhost".  When set to "localhost", the fabric "local" command
        will be used and commands are run directly, rather than over ssh. If
        `baste_dev_hosts` is anything else, it is interpreted as the env.hosts
        necessary to run the commands over an ssh connection.  Multiple hosts
        can be specified by separating them with semi-colons.

        """
        if self.baste_dev_hosts == "localhost":
            return local_run
        else:
            env.hosts = hosts = self.baste_env_hosts
            return ExecuteOnHosts(remote_run, hosts)
    run = property(get_run_command)

    #---------------------------------------------------------------------------
    def get_sudo_command(self):
        """
        Returns a sudo command that will run against either a local or remote env.

        Uses the fabric env setting `baste_dev_hosts` to determine which it is.
        `baste_dev_hosts` may be set to "localhost" or "host1;host2". The default
        is "localhost".  When set to "localhost", the fabric "local" command
        will be used and commands are run directly, rather than over ssh. If
        `baste_dev_hosts` is anything else, it is interpreted as the env.hosts
        necessary to run the commands over an ssh connection.  Multiple hosts
        can be specified by separating them with semi-colons.
        """
        if self.baste_dev_hosts == "localhost":
            return local_sudo
        else:
            env.hosts = hosts = self.baste_env_hosts
            return ExecuteOnHosts(remote_sudo, hosts)
    sudo = property(get_sudo_command)
baste_env = BasteEnvironment()

#-------------------------------------------------------------------------------
def project_relative(path):
    return os.path.join(os.path.dirname(env.real_fabfile), path)

#-------------------------------------------------------------------------------
class Repository(object):
    """
    Generic base repository which knows how to construct a safe update command.
    """
    #---------------------------------------------------------------------------
    def __init__(self, name, url):
        """Instantiate the repository with the given name and url"""
        self.name = name
        self.directory = project_relative(self.name)
        self.url = url

    #---------------------------------------------------------------------------
    def status(self, capture=False):
        return baste_env.run(self.status_command(), capture)

    #---------------------------------------------------------------------------
    def update(self):
        print(colors.green("[update] ") + self.name)
        with hide('running'):
            cmd = "test -d %s && %s || %s" % (
                        self.directory, self.update_command(), self.create_command()
                        )
            baste_env.run(cmd)

#-------------------------------------------------------------------------------
class Subversion(Repository):
    """Represents a subversion repository."""
    #---------------------------------------------------------------------------
    def create_command(self):
        return "svn co %s %s" % (self.url, self.directory)

    #---------------------------------------------------------------------------
    def update_command(self):
        return "svn up %s" % (self.directory)

    #---------------------------------------------------------------------------
    def status_command(self):
        return "svn st %s" % (self.directory)

    #---------------------------------------------------------------------------
    def diff_command(self):
        return "svn diff %s" % (self.directory)

#-------------------------------------------------------------------------------
class Mercurial(Repository):
    """Represents a mercurial repository."""
    #---------------------------------------------------------------------------
    def create_command(self):
        return "hg clone %s %s" % (self.url, self.directory)

    #---------------------------------------------------------------------------
    def update_command(self):
        return "hg -R %s pull -u" % (self.directory)

    #---------------------------------------------------------------------------
    def status_command(self):
        return "hg -R %s status" % (self.directory)

    #---------------------------------------------------------------------------
    def diff_command(self):
        return "hg -R %s diff" % (self.directory)

#-------------------------------------------------------------------------------
class Git(Repository):
    """Represents a mercurial repository."""
    #---------------------------------------------------------------------------
    def create_command(self):
        return "git clone %s %s" % (self.url, self.directory)

    #---------------------------------------------------------------------------
    def update_command(self):
        return "git --git-dir=%s/.git pull origin master" % (self.directory)

    #---------------------------------------------------------------------------
    def status_command(self):
        return "cd %s && git status" % (self.directory)

    #---------------------------------------------------------------------------
    def diff_command(self):
        return "cd %s && git diff" % (self.directory)

#-------------------------------------------------------------------------------
def python_dependency(package, python_version, dir=None):
    """Adds the given package as a dependency for the given python version."""
    if dir is None:
        dir = package
    symlink = "env/lib/%s/%s" % (python_version, package)
    symlink = project_relative(symlink)
    python_path = os.path.abspath(dir)
    create_symbolic_link = "ln -s %s %s" % (python_path, symlink)
    print(colors.green("[install] ") + package)
    with hide('running'):
        baste_env.run("rm %s; %s" % (symlink, create_symbolic_link))

#-------------------------------------------------------------------------------
class StatusCommand(object):
    """Helper which prints the status for all of the repos given to it."""

    #---------------------------------------------------------------------------
    def __init__(self, repos):
        self.repos = repos

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Prints the status for each of the repositories.
        """
        with hide('running'):
            for repo in self.repos.values():
                print(colors.green("[status] ") + repo.name)
                repo.status()

#-------------------------------------------------------------------------------
class DiffCommand(object):
    """Helper which prints the diff for all of the repos given to it."""

    #---------------------------------------------------------------------------
    def __init__(self, repos):
        self.repos = repos

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Prints the status for each of the repositories.
        """
        commands = []
        for repo in self.repos.values():
            commands.append(repo.diff_command())
        baste_env.run("{ %s; } | less" % (" && ".join(commands)))

#-------------------------------------------------------------------------------
class PgRestore(object):
    #---------------------------------------------------------------------------
    def __init__(self, file, db, user, format="custom"):
        self.file = file
        self.db = db
        self.user = user
        self.format = format

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Uses the pg_restore command to restore the database from the given file.
        """
        baste_env.run(
            "pg_restore --clean --no-owner --no-privileges --format=%s --host=localhost --username=%s --dbname=%s %s" % (
                self.format, self.user, self.db, self.file
            )
        )

#-------------------------------------------------------------------------------
class PgLoadPlain(object):
    #---------------------------------------------------------------------------
    def __init__(self, file, db, user):
        self.file = file
        self.db = db
        self.user = user

    #---------------------------------------------------------------------------
    def __call__(self):
        """Uses psql to load a plain dump format"""
        baste_env.run(
            "bzcat %s | psql --host=localhost --username=%s --dbname=%s" % (
                self.file, self.user, self.db
            )
        )

#-------------------------------------------------------------------------------
class PgShell(object):
    #---------------------------------------------------------------------------
    def __init__(self, db, user):
        self.db = db
        self.user = user

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Uses the psql command to give someone a shell within the db.
        """
        baste_env.run("psql --host=localhost --username=%s %s" % (self.user, self.db))

#-------------------------------------------------------------------------------
class MysqlLoadPlain(object):
    #--------------------------------------------------------------------------- 
    def __init__(self, file, db, user):
        self.file = file
        self.db = db
        self.user = user

    #---------------------------------------------------------------------------
    def __call__(self):
        """Uses mysql command line client to load a plain dump format."""
        baste_env.run(
            "mysql -h localhost -u %s -p %s < %s" % (self.user, self.db, self.file)
        )

#-------------------------------------------------------------------------------
class RsyncMedia(object):
    #---------------------------------------------------------------------------
    def __init__(self, host, remote_directory, local_directory):
        self.host = host
        self.local_directory = local_directory
        self.remote_directory = remote_directory

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Uses the psql command to give someone a shell within the db.
        """
        local_run("rsync --progress -avz --exclude=\".svn/*\" --exclude=\".svn\" -e ssh %s:%s %s" % (self.host, self.remote_directory, self.local_directory))

REPO_EXCLUDES = [
    '.svn', '.git', '.hg', '.hgignore', 'requirements.txt', 'start.sh',
    'fabfile.py', '*.pyc', 'env', '.swp'
]

#-------------------------------------------------------------------------------
class RsyncDeployment(object):
    """
    An rsync deployment consists of the following steps:
        1. the local_directory is rsync'ed to the remote_directory/source
        2. an cp command will be run that cps remote_directory/source to
           remote_directory/<current_date_time>
        3. a remote command is run to symlink remote_directory/current to
           remote_directory/<current_date_time>
        4. It is then up to the caller to run the command to reload the server

    Please note that this command WILL delete files on the remote_director/source
    """
    #---------------------------------------------------------------------------
    def __init__(self, remote_directory, local_directory):
        self.remote_directory = remote_directory
        self.local_directory = local_directory

    #---------------------------------------------------------------------------
    def __call__(self):
        """
        Actually perform the deployment.
        """
        source_directory = os.path.join(self.remote_directory, "source")
        current_symlink = os.path.join(self.remote_directory, "current")
        tmp_symlink = os.path.join(self.remote_directory, "current_tmp")
        date_directory = os.path.join(
                self.remote_directory,
                datetime.datetime.now().strftime("%Y-%m-%dT%H%M%S")
            )
        rsync_project(
                remote_dir=source_directory,
                local_dir=self.local_directory,
                exclude=REPO_EXCLUDES,
                delete=True,
            )
        remote_run("cp -r %s %s" % (source_directory, date_directory))
        remote_run("ln -sf %s %s; mv -Tf %s %s" % (date_directory, tmp_symlink, tmp_symlink, current_symlink))


#-------------------------------------------------------------------------------
class UbuntuPgCreateDbAndUser(object):
    #---------------------------------------------------------------------------
    def __init__(self, db_name, db_user):
        self.db_name = db_name
        self.db_user = db_user

    #---------------------------------------------------------------------------
    def __call__(self):
        with settings(warn_only=True, hide=('warnings',)):
            baste_env.sudo("dropdb %s" % self.db_name, user="postgres")
            baste_env.sudo("dropuser %s" % self.db_user, user="postgres")
            baste_env.sudo("createuser --createdb --pwprompt --no-superuser --no-createrole %s" %
                    self.db_user, user="postgres")
            baste_env.sudo("createdb -O %s %s" % (self.db_user, self.db_name), user="postgres")


#-------------------------------------------------------------------------------
def pay_attention_confirm(question, yes=None):
    """Ask a user a question, require a unique answer for confirmation."""

    if yes is None:
        yes = ''.join([random.choice(string.letters) for x in range(3)])
        yes = yes.lower()

    response = prompt('%s [(%s) y/n]' % (question, yes)) 
    if response == yes:
        return True
    return False
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.