def migrate(self, *apps): """ Applies all outstanding deltas to the database. Here the south API is a little friendlier and you can apply migrations to the entire django project. Or, if you so desire, you can apply only to specific apps. Just pass the list as an argument. Args: apps: list of apps to apply migrations, if none given, will run migrations across the entire project Usage: blt south.migrate [apps] Examples: blt south.migrate - default, runs all migrations blt south.migrate invest payment - runs migrations for invest and payment apps """ with cd(self.cfg['django']['DJANGO_ROOT']): if not apps: local('python manage.py migrate') else: for app in apps: local('python manage.py migrate %s' % app)
def config(self, action='', *configs): """ Executes a set/get/unset action to the remote heroku config. Args: action: string config action. either set, get, or unset configs: list of configurations Usage: blt e:[env] heroku.config [set|get|unset] ["Key=Value"] Examples: blt e:s heroku.config - default lists the current config on staging blt e:s heroku.config set - sets *ALL* config defined in beltenv configuration file on heroku blt e:p heroku.config set "SSL_ENABLED=True" - sets the SSL_ENABLED config setting to True in production blt e:p heroku.config unset SSL_ENABLED - unsets the SSL_ENABLED config setting """ if not action: local('heroku config --app %s' % self.cfg['heroku']['app']) else: if not configs: # if we don't have any runtime configs from the commandline, # we want to run a list comprehension to convert all of the # items in the beltenv configuration dict into a list of # "key=value" strings: configs = [''.join([k,'=',v]) for k,v in self.cfg['heroku']['config'].iteritems()] local('heroku config:%s %s --app %s' % (action , ' '.join(configs) , self.cfg['heroku']['app']))
def push(self, git_arg=''): """ Pushes your local git branch to heroku. We handle pushing from a non-master branch for you, just set ``git_branch`` in your beltenv configuration. Args: git_arg: any valid argument to git push (optional) Usage: blt e:[env] heroku.push [arg] Examples: blt e:s heroku.push - pushes branch to heroku staging environment blt e:s heroku.push force - forces a push to heroku staging blt e:p heroku.push verbose - pushes to production in verbose mode """ if git_arg: local('git push %s %s:master --%s' % (self.cfg['heroku']['git_remote'] , self.cfg['heroku']['git_branch'] , git_arg)) else: local('git push %s %s:master' % (self.cfg['heroku']['git_remote'] , self.cfg['heroku']['git_branch']))
def dump(self, deal_slug, database): """ Dumps a given deal out to a fixture file. This makes use of the custom django management command which encapsulates the logic to pull out all of the important model data for a deal. It uses the passed "database" arg to pull the appropriate DATABASE_URL from the bltenv settings. It outputs the fixture to the configured BUNDLE_ROOT. Args: deal_slug: the unique slug for the deal database: the database to dump from Usage: blt bundle.dump (deal_slug) (database) Examples: blt bundle.dump blumhouse-productions STAGING """ environ["DATABASE_URL"] = self.cfg["bundle"][database] fixture_file = self._fixture_file(deal_slug) self._prep_bundle_dir(path.split(fixture_file)[0]) with cd(self.cfg["django"]["DJANGO_ROOT"]): local("python manage.py bundle dump %s %s" % (deal_slug, fixture_file))
def addon(self, action='', *addons): """ Executes an add/remove/upgrade action to the remote heroku addons. Args: action: string addon action. either add, remove, or upgrade addons: list of addons Usage: blt e:[env] heroku.addon [add|remove|upgrade] ["addon:level"] Examples: blt e:s heroku.addon - default lists the current addons on staging blt e:s heroku.addon add - adds *ALL* addons defined in beltenv configuration file blt e:p heroku.addon add "newrelic:standard" - adds the standard version of newrelic in production blt e:p heroku.addon remove newrelic - removes newrelic from prod """ if not action: local('heroku addons --app %s' % self.cfg['heroku']['app']) else: if not addons: # much like the "config" command above, we want to convert the # configured addons to a list of "addon:level" pairs addons = [''.join([k,':',v]) for k,v in self.cfg['heroku']['addons'].iteritems()] for addon in addons: local('heroku addons:%s %s --app %s' % (action, addon, self.cfg['heroku']['app']))
def domain(self, action=None, *domains): """ Executes an add/clear/remove action to the remote heroku domains. Args: action: domain action. either add, clear, or remove domains: list of domains Usage: blt e:[env] heroku.domain [add|clear|remove] [domain] Examples: blt e:s heroku.domain - default, lists the current domains on staging blt e:s heroku.domain add - adds *ALL* domains defined in beltenv configuration file blt e:p heroku.domain add "matter.com" - adds the "matter.com" domain to the production heroku app blt e:p heroku.domain clear - clears all domains in production """ if not action: local('heroku domains --app %s' % self.cfg['heroku']['app']) else: if not domains: domains = self.cfg['heroku']['domains'] for domain in domains: local('heroku domains:%s %s --app %s' % (action, domain, self.cfg['heroku']['app']))
def load(self, deal_slug, database): """ Loads a deal from a fixture file. This will load the fixture located in the BUNDLE_ROOT directory to the passed in database target. Special care has been taken to allow this command to detect previously loaded data and can be run multiple times without affecting existing database entries. If you need to push a fresh update, use the ``delete`` command to start with a fresh slate. Args: deal_slug: the unique slug for the deal database: the database to load into Usage: blt bundle.load (deal_slug) (database) Examples: blt bundle.load blumhouse-productions PRODUCTION """ environ["DATABASE_URL"] = self.cfg["bundle"][database] fixture_file = self._fixture_file(deal_slug) with cd(self.cfg["django"]["DJANGO_ROOT"]): local("python manage.py bundle load %s" % fixture_file)
def gunicorn_server(self, *args): "Runs Gunicorn server for pseudo-production testing." project = self.cfg['django']['PROJECT_DIR'] args_str = ''.join(args) with cd(self.cfg['django']['DJANGO_ROOT']): flag_str = '%s --access-logfile=- --error-logfile=-' % args_str local('gunicorn %s %s.wsgi:application' % (flag_str, project))
def info(self): """ Shows the info for your current heroku environment Usage: blt e:[env] heroku.info """ local('heroku apps:info --app %s' % self.cfg['heroku']['app'])
def test(self, *apps): """ Runs py.test for the django project. If <apps> is empty, all tests will be run. Otherwise, tests may be specified in the following way: <appname>[.<search_phrase_1>[.<search_phrase_2>...<search_phrase_n>]]. For example, given an app named "my_app" and a test defined by unittest as "MyTestCase.test_something" the following command would run just that test: "blt django.test my_app.MyTestCase.test_something". To run every test in the test case: "blt django.test my_app.MyTestCase". Or, to run every test for the app: "blt django.test my_app". Specifying multiple apps is also possible: "blt django.test my_app,my_other_app,your_app". However, due to the way test searching is performed by py.test, it is not recommended to use search phrases when testing multiple apps. Usage: blt django.test [apps] Examples: blt django.test - default, runs all tests in the project blt django.test my_app1 my_app2 - runs all tests for my_app1 and my_app2 blt django.test my_app.test_case - runs the test_case in my_app """ project = self.cfg['django']['PROJECT_DIR'] # first clear out any .pyc files. these are cached and could provide # bad test results. example: you import a module in your test case, # but have deleted it on the filesystem. if the .pyc file still # exists, the test will still pass. local('find . -name "*.pyc" -exec rm -f {} \;') test_names = [] app_names = [] flags = [] args = [] for n in apps: if n.startswith('-'): flags.append(n) elif '.' in n: a, t = n.split('.', 1) app_names.append(a) test_names += t.split('.') else: app_names.append(n) args.append(' '.join(flags)) args.append(' '.join(app_names)) args.append('-k "%s"' % ' and '.join(test_names) if len(test_names) else '') command = 'py.test --ds {0}.settings {1}'.format(project, ' '.join(args)).strip() with cd(self.cfg['django']['DJANGO_ROOT']): local(command)
def destroy(self): """ Destroys a heroku app. The heroku toolbelt will verify this operation before executing. Usage: blt e:[env] heroku.destroy """ local('heroku apps:destroy %s' % self.cfg['heroku']['app'])
def covrpt(self): """ This will open the html version of the coverage report in your browser. Usage: blt django.covrpt """ project = self.cfg['django']['PROJECT_DIR'] with cd(self.cfg['django']['DJANGO_ROOT']): local('open coverage_html/index.html')
def status(self, app=None): """ Check the status of outstanding database migrations. Sadly, south gives us no easy api access to get status of what models have changed on an app. It only spits out the summary of changes to stderr and the actual Migration classes to stdout. The status command tries to encapsulate and parse this out to be helpful, but it is obviously brittle and should be tested whenever we upgrade the south library. Args: app: django app to check status for Usage: blt south.status (app) Examples: blt south.status invest - displays status for the invest app """ if not app: msg = ("\nsouth status requires an *app* to check. for example:\n" , " blt south.status my_app\n" , "To check which apps have had model changes run:\n" , " git status | grep models.py") abort("\n".join(msg)) with cd(self.cfg['django']['DJANGO_ROOT']): puts("-- Model Check -----------------------------------------------------------") out = local('python manage.py schemamigration %s --auto --stdout 2>&1' % app, collect_output=True, abort_on_stderr=False) if out.strip() == 'Nothing seems to have changed.': puts("Model is in sync with migrations") else: puts("Model changes found:\n\n%s" % out) puts("\n==> Run `blt south.delta %s` to create a migration set." % app) puts("\n-- Unapplied Migrations --------------------------------------------------") out = local('python manage.py migrate %s --list | grep -v "*" 2>&1' % app, collect_output=True) if out.strip() == app: puts("All migrations have been applied to db") else: puts("Migrations need to be applied to db:\n%s" % out.strip()) puts("\n==> Run `blt south.apply` to push to the database\n")
def create(self): """ Provisions a fully configured heroku app from scratch. Behind the scenes, you're getting the following: - heroku apps:create - git push heroku - heroku config - heroku addons - heroku domains - all post deploy hooks The command will handle creating a special git remote for the environment and can push from a non-master branch, things which are not easily accomplished with the heroku toolbelt. This is driven from the bltenv configuration. Usage: blt e:[env] heroku.create """ print "******** Creating New Heroku App ********" print "App Name: " + green(self.cfg['heroku']['app']) print "Environment: " + green(self.cfg['blt_envtype']) print "Branch: " + green(self.cfg['heroku']['git_branch']) proceed = prompt("\nLook good? ==>", default='no') if proceed.lower() != 'yes' and proceed.lower() != 'y': abort('Aborting heroku creation.') local('heroku apps:create %s --remote %s' % (self.cfg['heroku']['app'] , self.cfg['heroku']['git_remote'])) self.config('set') self.push() self.addon('add') # if we have domains configured, add them if 'domains' in self.cfg['heroku']: self.domain('add') # handle post deploy steps self.run(*self.cfg['heroku']['post_deploy']) print '\nHeroku Deploy Complete!' url = '==> http://%s.herokuapp.com/' % self.cfg['heroku']['app'] print url
def run(self, *commands): """ Runs a given command on heroku. Args: commands: list of shell commands to be run on the heroku instance Usage: blt e:[env] heroku.run "command" Examples: blt e:s heroku.run bash - opens a bash session on heroku staging blt e:p heroku.run "ls -altr" - runs ls -altr on heroku prod """ for command in commands: local('heroku run "%s" --app %s' % (command , self.cfg['heroku']['app']))
def cov(self, *apps): """ Runs coverage for the django project. Note that this command will run the py.test suite first as it is required for generating the coverage report. If you'd like to filter to a specific set of apps, you can pass it into this command. Usage: blt django.cov [apps] [flags] Examples: blt django.cov - default, runs coverage for the entire project blt django.cov my_app1 my_app2 - runs coverage for my_app1 and my_app2 """ project = self.cfg['django']['PROJECT_DIR'] # first clear out any .pyc files. these are cached and could provide # bad test results. example: you import a module in your test case, # but have deleted it on the filesystem. if the .pyc file still # exists, the test will still pass. local('find . -name "*.pyc" -exec rm -f {} \;') app_names = [] flags = [] pytest_args = [] cov_args = [] for n in apps: if n.startswith('-'): flags.append(n) else: app_names.append(n) # setup py.test args pytest_args = ' '.join(app_names) cov_args = ','.join(app_names) if app_names: cmd = 'coverage run --source {0} {1} -m py.test --ds {2}.settings {3}'.format( cov_args, ' '.join(flags), project, pytest_args).strip() else: cmd = 'coverage run {0} -m py.test --ds {1}.settings'.format( ' '.join(flags), project).strip() with cd(self.cfg['django']['DJANGO_ROOT']): local(cmd) local('coverage report') local('coverage html -d coverage_html')
def runserver(self, ip='127.0.0.1', port='8000'): """ Runs django's development http server Args: ip: the ip address to host on, default is 127.0.0.1 port: the port on host server, default is 8000 Usage: blt django.runserver [ip] [port] Examples: blt django.runserver - default, runs on 127.0.0.1:8000 blt django.runserver 10.1.156.3 - runs on 10.1.156.3:8000 blt django.runserver 10.1.156.3 8888 - runs on 10.1.156.3:8888 """ with cd(self.cfg['django']['DJANGO_ROOT']): print "Setting ASSETS_DEBUG=True" environ['ASSETS_DEBUG'] = "True" local('python manage.py runserver %s:%s' % (ip, port))
def delta(self, *apps): """ Creates a new schema changeset for an app. Unfortunately, south doesn't allow project-wide schemamigrations, you have to provide a specific app name. Args: apps: a list of apps to create changesets for Usage: blt south.delta (apps) Examples: blt south.delta invest payment - creates deltas for invest and payment apps """ if not apps: abort('\n\nPlease provide an app name, e.g. \n blt south.delta my_app') with cd(self.cfg['django']['DJANGO_ROOT']): for app in apps: local('python manage.py schemamigration %s --auto' % app)
def delete(self, deal_slug, database): """ Deletes a deal on the platform. This will completely delete the deal and all related objects on the passed in database. Because we view bundles as atomic units and never overwrite any existing entries on the database, you are required to run this command if you want update anything in the db. Args: deal_slug: the unique slug for the deal database: the database to delete from Usage: blt bundle.delete (deal_slug) (database) Examples: blt bundle.delete blumhouse-productions STAGING """ environ["DATABASE_URL"] = self.cfg["bundle"][database] with cd(self.cfg["django"]["DJANGO_ROOT"]): local("python manage.py bundle delete %s" % (deal_slug))
def up(self): """ Update a django environment with latest settings. This command does everything required to get a django project running. These are the steps that will be executed when the command is issued: 1. pip install requirements 2. syncdb 3. migrate 4. loaddata initial 5. collectstatic 6. assets build It has been designed with rerunning in mind and can be safely executed without any unwelcome side affects. For example, any pip packages that have already been installed will be skipped on rerun. Any database tables will be preserved (no dropping data), etc. Note that this must be run in a virtualenv. blt will detect if you are not using one and will barf with a helpful error. Usage: blt django.up """ django_root = self.cfg['django']['DJANGO_ROOT'] # install packages from pip's requirements.txt local('pip install -r requirements.txt') with cd(django_root): try: local('python manage.py syncdb') local('python manage.py migrate') except: msg = '\n'.join(["ERROR: Python couldn't find django. Are you in a virtualenv?" , "Try workon MY_SWEET_VIRTENV_HERE"]) abort(msg) with cd(django_root): # Load dev data local('python manage.py loaddata initial') # collect static files local('python manage.py collectstatic --noinput') # Compile static asset bundles local('python manage.py assets build')
def shell(self): """Opens a session to django's shell""" with cd(self.cfg['django']['DJANGO_ROOT']): local('python manage.py shell')
def collectstatic(self): """Runs django's collectstatic and the webassets build in one command""" with cd(self.cfg['django']['DJANGO_ROOT']): local('python manage.py collectstatic --noinput') local('python manage.py assets build')