piku/piku.py

593 wiersze
18 KiB
Python
Czysty Zwykły widok Historia

2016-03-26 12:52:54 +00:00
#!/usr/bin/env python
2016-03-27 12:19:09 +00:00
import os, sys, stat, re, shutil, socket
2016-03-26 23:01:28 +00:00
from click import argument, command, group, option, secho as echo
2016-04-01 23:16:10 +00:00
from collections import defaultdict, deque
from glob import glob
from os.path import abspath, basename, dirname, exists, getmtime, join, splitext
2016-03-27 12:19:09 +00:00
from subprocess import call
2016-03-29 20:57:51 +00:00
from time import sleep
2016-03-26 12:52:54 +00:00
2016-03-29 20:18:38 +00:00
# === Globals - all tweakable settings are here ===
2016-03-27 12:12:13 +00:00
2016-03-27 12:08:30 +00:00
PIKU_ROOT = os.environ.get('PIKU_ROOT', join(os.environ['HOME'],'.piku'))
2016-03-28 22:37:36 +00:00
2016-03-27 12:08:30 +00:00
APP_ROOT = abspath(join(PIKU_ROOT, "apps"))
2016-03-27 12:20:44 +00:00
ENV_ROOT = abspath(join(PIKU_ROOT, "envs"))
2016-03-27 12:08:30 +00:00
GIT_ROOT = abspath(join(PIKU_ROOT, "repos"))
LOG_ROOT = abspath(join(PIKU_ROOT, "logs"))
2016-03-28 22:37:36 +00:00
UWSGI_AVAILABLE = abspath(join(PIKU_ROOT, "uwsgi-available"))
UWSGI_ENABLED = abspath(join(PIKU_ROOT, "uwsgi-enabled"))
UWSGI_ROOT = abspath(join(PIKU_ROOT, "uwsgi"))
2016-03-26 12:52:54 +00:00
2016-03-27 12:12:13 +00:00
2016-03-29 20:18:38 +00:00
# === Utility functions ===
2016-03-26 12:52:54 +00:00
2016-03-26 21:33:02 +00:00
def sanitize_app_name(app):
2016-03-26 17:14:13 +00:00
"""Sanitize the app name and build matching path"""
2016-04-01 21:53:08 +00:00
2016-03-26 17:14:13 +00:00
app = "".join(c for c in app if c.isalnum() or c in ('.','_')).rstrip()
2016-03-26 21:33:02 +00:00
return app
2016-03-26 17:14:13 +00:00
2016-03-26 12:52:54 +00:00
def get_free_port(address=""):
2016-03-28 18:28:24 +00:00
"""Find a free TCP port (entirely at random)"""
2016-04-01 21:53:08 +00:00
2016-03-26 12:52:54 +00:00
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((address,0))
port = s.getsockname()[1]
s.close()
return port
2016-03-26 17:28:01 +00:00
2016-04-02 16:38:53 +00:00
def write_config(filename, bag, separator='='):
"""Helper for writing out config files"""
with open(filename, 'w') as h:
for k, v in bag.iteritems():
h.write('%s%s%s\n' % (k,separator,str(v)))
2016-04-02 16:38:53 +00:00
2016-03-26 12:52:54 +00:00
def setup_authorized_keys(ssh_fingerprint, script_path, pubkey):
2016-03-26 17:39:23 +00:00
"""Sets up an authorized_keys file to redirect SSH commands"""
2016-04-01 21:53:08 +00:00
2016-03-27 12:08:30 +00:00
authorized_keys = join(os.environ['HOME'],'.ssh','authorized_keys')
2016-03-27 12:18:04 +00:00
if not exists(dirname(authorized_keys)):
os.makedirs(dirname(authorized_keys))
2016-03-26 17:39:23 +00:00
# Restrict features and force all SSH commands to go through our script
2016-03-28 17:40:52 +00:00
with open(authorized_keys, 'a') as h:
h.write("""command="FINGERPRINT=%(ssh_fingerprint)s NAME=default %(script_path)s $SSH_ORIGINAL_COMMAND",no-agent-forwarding,no-user-rc,no-X11-forwarding,no-port-forwarding %(pubkey)s\n""" % locals())
2016-03-29 19:24:17 +00:00
2016-03-27 12:12:13 +00:00
2016-03-29 19:24:17 +00:00
def parse_procfile(filename):
"""Parses a Procfile and returns the worker types. Only one worker of each type is allowed."""
2016-04-01 21:53:08 +00:00
2016-03-29 19:24:17 +00:00
workers = {}
if not exists(filename):
return None
with open(filename, 'r') as procfile:
for line in procfile:
2016-03-29 19:38:48 +00:00
try:
kind, command = map(lambda x: x.strip(), line.split(":", 1))
if kind in ['web', 'worker', 'wsgi']:
workers[kind] = command
except:
echo("Warning: unrecognized Procfile declaration '%s'" % line, fg='yellow')
if not len(workers):
2016-04-02 16:38:53 +00:00
return {}
2016-03-29 19:24:17 +00:00
# WSGI trumps regular web workers
if 'wsgi' in workers:
if 'web' in workers:
del(workers['web'])
return workers
2016-03-31 22:39:29 +00:00
def parse_settings(filename, env={}):
2016-03-31 22:39:29 +00:00
"""Parses a settings file and returns a dict with environment variables"""
def expandvars(buffer, env, default=None, skip_escaped=False):
def replace_var(match):
return env.get(match.group(2) or match.group(1), match.group(0) if default is None else default)
pattern = (r'(?<!\\)' if skip_escaped else '') + r'\$(\w+|\{([^}]*)\})'
return re.sub(pattern, replace_var, buffer)
2016-04-01 21:53:08 +00:00
2016-03-31 22:39:29 +00:00
if not exists(filename):
2016-04-02 15:46:03 +00:00
return {}
2016-03-31 22:39:29 +00:00
with open(filename, 'r') as settings:
for line in settings:
if '#' == line[0]: # allow for comments
2016-04-02 19:13:57 +00:00
continue
2016-03-31 22:39:29 +00:00
try:
k, v = map(lambda x: x.strip(), line.split("=", 1))
env[k] = expandvars(v, env)
2016-03-31 22:39:29 +00:00
except:
2016-04-02 16:38:53 +00:00
echo("Error: malformed setting '%s', ignoring file." % line, fg='red')
return {}
2016-03-31 22:39:29 +00:00
return env
2016-03-29 19:24:17 +00:00
2016-03-27 12:12:13 +00:00
def do_deploy(app):
"""Deploy an app by resetting the work directory"""
2016-04-01 21:53:08 +00:00
2016-03-27 12:12:13 +00:00
app_path = join(APP_ROOT, app)
2016-03-29 19:24:17 +00:00
procfile = join(app_path, 'Procfile')
2016-04-01 23:16:10 +00:00
log_path = join(LOG_ROOT, app)
2016-03-27 12:12:13 +00:00
env = {'GIT_WORK_DIR': app_path}
if exists(app_path):
echo("-----> Deploying app '%s'" % app, fg='green')
2016-03-27 12:19:09 +00:00
call('git pull --quiet', cwd=app_path, env=env, shell=True)
call('git checkout -f', cwd=app_path, env=env, shell=True)
2016-04-01 23:16:10 +00:00
if not exists(log_path):
os.makedirs(log_path)
2016-03-29 19:24:17 +00:00
workers = parse_procfile(procfile)
2016-04-02 16:38:53 +00:00
if len(workers):
2016-03-29 19:24:17 +00:00
if exists(join(app_path, 'requirements.txt')):
echo("-----> Python app detected.", fg='green')
deploy_python(app)
2016-03-29 19:24:17 +00:00
else:
echo("-----> Could not detect runtime!", fg='red')
# TODO: detect other runtimes
2016-03-28 18:28:24 +00:00
else:
echo("Error: Invalid Procfile for app '%s'." % app, fg='red')
2016-03-27 12:12:13 +00:00
else:
echo("Error: app '%s' not found." % app, fg='red')
2016-03-28 17:40:52 +00:00
def deploy_python(app):
2016-03-28 18:28:24 +00:00
"""Deploy a Python application"""
2016-04-01 21:53:08 +00:00
2016-04-01 23:16:10 +00:00
virtualenv_path = join(ENV_ROOT, app)
2016-04-01 21:53:08 +00:00
requirements = join(APP_ROOT, app, 'requirements.txt')
2016-04-01 21:50:18 +00:00
2016-04-01 23:22:40 +00:00
first_time = False
2016-04-01 21:50:18 +00:00
if not exists(virtualenv_path):
2016-03-28 17:40:52 +00:00
echo("-----> Creating virtualenv for '%s'" % app, fg='green')
2016-04-01 21:50:18 +00:00
os.makedirs(virtualenv_path)
2016-03-28 17:40:52 +00:00
call('virtualenv %s' % app, cwd=ENV_ROOT, shell=True)
2016-04-01 23:22:40 +00:00
first_time = True
2016-03-28 18:28:24 +00:00
2016-04-01 23:22:40 +00:00
if first_time or getmtime(requirements) > getmtime(virtualenv_path):
2016-04-01 21:50:18 +00:00
echo("-----> Running pip for '%s'" % app, fg='green')
activation_script = join(virtualenv_path,'bin','activate_this.py')
execfile(activation_script, dict(__file__=activation_script))
call('pip install -r %s' % requirements, cwd=virtualenv_path, shell=True)
spawn_app(app)
def spawn_app(app, deltas={}):
2016-03-31 22:42:13 +00:00
"""Create all workers for an app"""
2016-04-01 21:53:08 +00:00
app_path = join(APP_ROOT, app)
procfile = join(app_path, 'Procfile')
workers = parse_procfile(procfile)
2016-04-02 16:38:53 +00:00
ordinals = defaultdict(lambda:1)
worker_count = {k:1 for k in workers.keys()}
2016-04-01 21:50:18 +00:00
# the Python virtualenv
2016-04-01 23:16:10 +00:00
virtualenv_path = join(ENV_ROOT, app)
2016-04-01 21:50:18 +00:00
# Settings shipped with the app
env_file = join(APP_ROOT, app, 'ENV')
# Custom overrides
2016-04-01 23:16:10 +00:00
settings = join(ENV_ROOT, app, 'ENV')
2016-04-02 15:46:03 +00:00
# Live settings
live = join(ENV_ROOT, app, 'LIVE_ENV')
2016-04-02 16:38:53 +00:00
# Scaling
scaling = join(ENV_ROOT, app, 'SCALING')
2016-03-31 22:39:29 +00:00
env = {
'PATH': os.environ['PATH'],
2016-04-01 21:50:18 +00:00
'VIRTUAL_ENV': virtualenv_path,
2016-04-01 23:29:59 +00:00
'PORT': str(get_free_port()),
'PWD': dirname(env_file),
2016-03-31 22:39:29 +00:00
}
2016-04-01 21:53:08 +00:00
2016-03-31 22:39:29 +00:00
# Load environment variables shipped with repo (if any)
if exists(env_file):
env.update(parse_settings(env_file, env))
2016-03-31 22:39:29 +00:00
# Override with custom settings (if any)
if exists(settings):
env.update(parse_settings(settings, env))
# Configured worker count
2016-04-02 16:38:53 +00:00
if exists(scaling):
worker_count.update({k: int(v) for k,v in parse_procfile(scaling).iteritems()})
to_create = {}
to_destroy = {}
for k, v in worker_count.iteritems():
to_create[k] = range(1,worker_count[k] + 1)
2016-04-02 22:37:42 +00:00
if k in deltas and deltas[k]:
to_create[k] = range(1, worker_count[k] + deltas[k] + 1)
2016-04-02 22:37:42 +00:00
if deltas[k] < 0:
to_destroy[k] = range(worker_count[k], worker_count[k] + deltas[k], -1)
worker_count[k] = worker_count[k]+deltas[k]
2016-04-02 22:37:42 +00:00
# Save current settings
write_config(live, env)
write_config(scaling, worker_count, ':')
# Create new workers
for k, v in to_create.iteritems():
for w in v:
enabled = join(UWSGI_ENABLED, '%s_%s.%d.ini' % (app, k, w))
if not exists(enabled):
spawn_worker(app, k, workers[k], env, w)
# Remove unnecessary workers (leave logfiles)
for k, v in to_destroy.iteritems():
for w in v:
enabled = join(UWSGI_ENABLED, '%s_%s.%d.ini' % (app, k, w))
if exists(enabled):
echo("-----> Terminating '%s:%s.%d'" % (app, k, w), fg='yellow')
os.unlink(enabled)
def spawn_worker(app, kind, command, env, ordinal=1):
2016-03-31 22:42:13 +00:00
"""Set up and deploy a single worker of a given kind"""
2016-04-01 21:50:18 +00:00
env_path = join(ENV_ROOT, app)
2016-04-02 18:31:28 +00:00
available = join(UWSGI_AVAILABLE, '%s_%s.%d.ini' % (app, kind, ordinal))
enabled = join(UWSGI_ENABLED, '%s_%s.%d.ini' % (app, kind, ordinal))
2016-03-28 18:28:24 +00:00
2016-03-29 19:24:17 +00:00
settings = [
('virtualenv', join(ENV_ROOT, app)),
('chdir', join(APP_ROOT, app)),
('master', 'true'),
('project', app),
('max-requests', '1000'),
('processes', '1'),
2016-04-02 18:47:21 +00:00
('procname-prefix', '%s:%s:' % (app, kind)),
('enable-threads', 'true'),
('log-maxsize', '1048576'),
2016-04-02 18:31:28 +00:00
('logto', '%s.%d.log' % (join(LOG_ROOT, app, kind), ordinal)),
('log-backupname', '%s.%d.log.old' % (join(LOG_ROOT, app, kind), ordinal)),
2016-03-29 19:24:17 +00:00
]
for k, v in env.iteritems():
2016-04-01 23:16:10 +00:00
settings.append(('env', '%s=%s' % (k,v)))
2016-04-01 21:50:18 +00:00
if kind == 'wsgi':
2016-04-02 00:11:25 +00:00
echo("-----> Setting HTTP port to %s" % env['PORT'], fg='yellow')
settings.extend([
('module', command),
2016-04-02 18:47:21 +00:00
('threads', '4'),
('http', ':%s' % env['PORT'])
2016-04-01 22:26:37 +00:00
])
2016-03-29 19:24:17 +00:00
else:
settings.append(('attach-daemon', command))
2016-04-01 21:50:18 +00:00
2016-03-28 18:11:19 +00:00
with open(available, 'w') as h:
2016-03-29 19:24:17 +00:00
h.write('[uwsgi]\n')
for k, v in settings:
h.write("%s = %s\n" % (k, v))
2016-04-01 23:16:10 +00:00
if exists(enabled):
os.unlink(enabled)
echo("-----> Spawning '%s:%s.%d'" % (app, kind, ordinal), fg='green')
2016-03-28 17:40:52 +00:00
shutil.copyfile(available, enabled)
2016-04-01 22:26:37 +00:00
def multi_tail(app, filenames):
"""Tails multiple log files"""
def peek(handle):
where = handle.tell()
line = handle.readline()
if not line:
handle.seek(where)
return None
return line
inodes = {}
files = {}
prefixes = {}
for f in filenames:
2016-04-01 23:16:10 +00:00
prefixes[f] = splitext(basename(f))[0]
2016-04-01 22:26:37 +00:00
files[f] = open(f)
inodes[f] = os.stat(f).st_ino
2016-04-01 23:16:10 +00:00
files[f].seek(0, 2)
2016-04-01 22:26:37 +00:00
longest = max(map(len, prefixes.values()))
2016-04-01 23:16:10 +00:00
for f in filenames:
for line in deque(open(f), 20):
yield "%s | %s" % (prefixes[f].ljust(longest), line)
2016-04-01 22:26:37 +00:00
while True:
updated = False
for f in filenames:
line = peek(files[f])
if not line:
continue
else:
updated = True
yield "%s | %s" % (prefixes[f].ljust(longest), line)
if not updated:
2016-04-01 23:16:10 +00:00
sleep(1)
2016-04-01 22:26:37 +00:00
for f in filenames:
2016-04-01 23:16:10 +00:00
if exists(f):
if os.stat(f).st_ino != inodes[f]:
files[f] = open(f)
inodes[f] = os.stat(f).st_ino
else:
filenames.remove(f)
2016-04-01 22:26:37 +00:00
2016-03-27 12:12:13 +00:00
2016-03-29 20:18:38 +00:00
# === CLI commands ===
2016-03-26 12:52:54 +00:00
2016-03-27 12:08:30 +00:00
@group()
2016-03-26 17:28:01 +00:00
def piku():
2016-03-27 12:16:00 +00:00
"""Initialize paths"""
2016-03-28 10:26:10 +00:00
for p in [APP_ROOT, GIT_ROOT, ENV_ROOT, UWSGI_ROOT, UWSGI_AVAILABLE, UWSGI_ENABLED, LOG_ROOT]:
2016-03-27 12:16:00 +00:00
if not exists(p):
os.makedirs(p)
2016-03-26 22:47:39 +00:00
2016-03-26 12:52:54 +00:00
2016-03-26 17:28:01 +00:00
@piku.resultcallback()
2016-03-26 12:52:54 +00:00
def cleanup(ctx):
2016-03-26 17:33:14 +00:00
"""Callback from command execution -- currently used for debugging"""
2016-03-28 18:28:24 +00:00
pass
#print sys.argv[1:]
2016-03-26 21:33:02 +00:00
#print os.environ
2016-03-26 12:52:54 +00:00
2016-03-26 22:47:39 +00:00
2016-03-29 20:18:38 +00:00
# --- User commands ---
2016-03-26 12:52:54 +00:00
2016-04-02 15:46:03 +00:00
@piku.command("config")
@argument('app')
def deploy_app(app):
"""Show application configuration"""
app = sanitize_app_name(app)
config_file = join(ENV_ROOT, app, 'ENV')
if exists(config_file):
2016-04-02 16:38:53 +00:00
echo(open(config_file).read().strip(), fg='white')
2016-04-02 15:46:03 +00:00
# no output if file is missing, for scripting purposes
@piku.command("config:get")
@argument('app')
@argument('setting')
def deploy_app(app, setting):
"""Retrieve a configuration setting"""
app = sanitize_app_name(app)
config_file = join(ENV_ROOT, app, 'ENV')
if exists(config_file):
env = parse_settings(config_file)
if setting in env:
echo("%s" % env[setting], fg='white')
# no output if file or setting is missing, for scripting purposes
@piku.command("config:set")
@argument('app')
@argument('settings', nargs=-1)
def deploy_app(app, settings):
"""Show application configuration"""
app = sanitize_app_name(app)
config_file = join(ENV_ROOT, app, 'ENV')
env = parse_settings(config_file)
items = {}
for s in settings:
try:
k, v = map(lambda x: x.strip(), s.split("=", 1))
env[k] = v
echo("Setting %s=%s for '%s'" % (k, v, app), fg='white')
except:
2016-04-02 16:38:53 +00:00
echo("Error: malformed setting '%s'" % s, fg='red')
return
write_config(config_file, env)
2016-04-02 15:46:03 +00:00
do_deploy(app)
@piku.command("config:live")
@argument('app')
def deploy_app(app):
"""Show current application settings"""
app = sanitize_app_name(app)
live_config = join(ENV_ROOT, app, 'LIVE_ENV')
if exists(live_config):
2016-04-02 16:38:53 +00:00
echo(open(live_config).read().strip(), fg='white')
2016-04-02 15:46:03 +00:00
# no output if file or app is missing, for scripting purposes
2016-03-26 22:08:10 +00:00
@piku.command("deploy")
@argument('app')
def deploy_app(app):
2016-03-26 23:01:28 +00:00
"""Deploy an application"""
2016-04-01 21:53:08 +00:00
2016-03-26 22:08:10 +00:00
app = sanitize_app_name(app)
2016-03-26 22:19:30 +00:00
do_deploy(app)
2016-03-26 22:47:39 +00:00
2016-03-29 20:18:38 +00:00
@piku.command("destroy")
@argument('app')
def destroy_app(app):
"""Destroy an application"""
2016-04-01 21:53:08 +00:00
2016-03-29 20:18:38 +00:00
app = sanitize_app_name(app)
2016-04-01 21:53:08 +00:00
2016-03-29 20:18:38 +00:00
for p in [join(x, app) for x in [APP_ROOT, GIT_ROOT, ENV_ROOT, LOG_ROOT]]:
if exists(p):
echo("Removing folder '%s'" % p, fg='yellow')
shutil.rmtree(p)
2016-04-01 21:53:08 +00:00
2016-03-31 22:53:35 +00:00
for p in [join(x, '%s*.ini' % app) for x in [UWSGI_AVAILABLE, UWSGI_ENABLED]]:
g = glob(p)
if len(g):
for f in g:
echo("Removing file '%s'" % f, fg='yellow')
os.remove(f)
2016-03-26 22:47:39 +00:00
2016-03-27 12:08:30 +00:00
@piku.command("disable")
@argument('app')
def disable_app(app):
"""Disable an application"""
2016-04-01 21:53:08 +00:00
2016-03-27 12:08:30 +00:00
app = sanitize_app_name(app)
config = glob(join(UWSGI_ENABLED, '%s*.ini' % app))
2016-04-01 21:53:08 +00:00
if len(config):
2016-03-27 12:08:30 +00:00
echo("Disabling app '%s'..." % app, fg='yellow')
for c in config:
os.remove(c)
2016-03-28 22:35:31 +00:00
else:
echo("Error: app '%s' not deployed!" % app, fg='red')
2016-03-28 22:35:31 +00:00
2016-03-27 12:08:30 +00:00
@piku.command("enable")
@argument('app')
def enable_app(app):
"""Enable an application"""
app = sanitize_app_name(app)
enabled = glob(join(UWSGI_ENABLED, '%s*.ini' % app))
available = glob(join(UWSGI_AVAILABLE, '%s*.ini' % app))
2016-04-01 21:53:08 +00:00
2016-03-27 12:08:30 +00:00
if exists(join(APP_ROOT, app)):
if len(enabled):
if len(available):
2016-03-27 12:08:30 +00:00
echo("Enabling app '%s'..." % app, fg='yellow')
for a in available:
shutil.copy(a, join(UWSGI_ENABLED, app))
2016-03-27 12:08:30 +00:00
else:
echo("Error: app '%s' is not configured.", fg='red')
2016-03-27 12:12:43 +00:00
else:
2016-03-27 12:08:30 +00:00
echo("Warning: app '%s' is already enabled, skipping.", fg='yellow')
else:
echo("Error: app '%s' does not exist.", fg='red')
2016-03-28 18:28:24 +00:00
2016-04-02 18:26:05 +00:00
@piku.command("apps")
2016-03-29 20:18:38 +00:00
def list_apps():
"""List applications"""
2016-04-01 21:53:08 +00:00
2016-03-29 20:18:38 +00:00
for a in os.listdir(APP_ROOT):
echo(a, fg='green')
@piku.command("restart")
@argument('app')
def restart_app(app):
"""Restart an application"""
2016-04-01 21:53:08 +00:00
2016-04-02 17:09:39 +00:00
app = sanitize_app_name(app)
config = glob(join(UWSGI_ENABLED, '%s*.ini' % app))
if len(config):
2016-03-29 20:18:38 +00:00
echo("Restarting app '%s'..." % app, fg='yellow')
2016-04-02 17:09:39 +00:00
for c in config:
os.remove(c)
do_deploy(app)
2016-03-29 20:18:38 +00:00
else:
2016-04-02 17:09:39 +00:00
echo("Error: app '%s' not deployed!" % app, fg='red')
2016-03-29 20:18:38 +00:00
2016-04-02 18:22:51 +00:00
@piku.command("ps")
2016-04-02 16:38:53 +00:00
@argument('app')
def deploy_app(app):
"""Show application worker count"""
app = sanitize_app_name(app)
config_file = join(ENV_ROOT, app, 'SCALING')
if exists(config_file):
echo(open(config_file).read().strip(), fg='white')
# no output if file is missing, for scripting purposes
2016-04-02 18:22:51 +00:00
@piku.command("ps:scale")
2016-04-02 16:38:53 +00:00
@argument('app')
@argument('settings', nargs=-1)
def deploy_app(app, settings):
"""Show application configuration"""
app = sanitize_app_name(app)
if not exists(join(APP_ROOT, app)):
return
config_file = join(ENV_ROOT, app, 'SCALING')
2016-04-02 22:37:42 +00:00
worker_count = {k:int(v) for k, v in parse_procfile(config_file).iteritems()}
2016-04-02 16:38:53 +00:00
items = {}
deltas = {}
2016-04-02 16:38:53 +00:00
for s in settings:
try:
2016-04-02 18:22:51 +00:00
k, v = map(lambda x: x.strip(), s.split("=", 1))
c = int(v) # check for integer value
2016-04-02 16:49:19 +00:00
if c < 0:
echo("Error: cannot scale type '%s' below 0" % k, fg='red')
return
2016-04-02 16:38:53 +00:00
if k not in worker_count:
echo("Error: worker type '%s' not present in '%s'" % (k, app), fg='red')
return
2016-04-02 22:37:42 +00:00
deltas[k] = c - worker_count[k]
2016-04-02 16:38:53 +00:00
except:
echo("Error: malformed setting '%s'" % s, fg='red')
return
spawn_app(app, deltas)
2016-04-02 16:38:53 +00:00
2016-04-02 18:27:45 +00:00
@piku.command("logs")
2016-04-02 15:46:03 +00:00
@argument('app')
def tail_logs(app):
"""Tail an application log"""
app = sanitize_app_name(app)
logfiles = glob(join(LOG_ROOT, app, '*.log'))
if len(logfiles):
for line in multi_tail(app, logfiles):
echo(line.strip(), fg='white')
else:
echo("No logs found for app '%s'." % app, fg='yellow')
2016-03-29 20:18:38 +00:00
# --- Internal commands ---
2016-03-26 22:47:39 +00:00
2016-03-26 17:28:01 +00:00
@piku.command("git-hook")
2016-03-26 12:52:54 +00:00
@argument('app')
def git_hook(app):
2016-03-28 22:35:31 +00:00
"""INTERNAL: Post-receive git hook"""
2016-04-01 21:53:08 +00:00
2016-03-26 21:33:02 +00:00
app = sanitize_app_name(app)
2016-03-27 12:08:30 +00:00
repo_path = join(GIT_ROOT, app)
app_path = join(APP_ROOT, app)
2016-04-01 21:53:08 +00:00
2016-03-26 17:14:13 +00:00
for line in sys.stdin:
2016-03-26 17:15:19 +00:00
oldrev, newrev, refname = line.strip().split(" ")
2016-03-26 22:08:10 +00:00
#print "refs:", oldrev, newrev, refname
2016-03-26 17:14:13 +00:00
if refname == "refs/heads/master":
2016-03-26 17:23:29 +00:00
# Handle pushes to master branch
2016-03-27 12:08:30 +00:00
if not exists(app_path):
echo("-----> Creating app '%s'" % app, fg='green')
2016-03-26 21:33:02 +00:00
os.makedirs(app_path)
2016-03-27 12:19:09 +00:00
call('git clone --quiet %s %s' % (repo_path, app), cwd=APP_ROOT, shell=True)
2016-03-26 22:19:30 +00:00
do_deploy(app)
2016-03-26 17:14:13 +00:00
else:
2016-03-28 22:45:35 +00:00
# TODO: Handle pushes to another branch
echo("receive-branch '%s': %s, %s" % (app, newrev, refname))
2016-03-29 20:18:38 +00:00
@piku.command("git-receive-pack")
@argument('app')
def receive(app):
"""INTERNAL: Handle git pushes for an app"""
2016-04-01 21:53:08 +00:00
2016-03-29 20:18:38 +00:00
app = sanitize_app_name(app)
hook_path = join(GIT_ROOT, app, 'hooks', 'post-receive')
2016-04-01 21:53:08 +00:00
2016-03-29 20:18:38 +00:00
if not exists(hook_path):
os.makedirs(dirname(hook_path))
# Initialize the repository with a hook to this script
call("git init --quiet --bare " + app, cwd=GIT_ROOT, shell=True)
with open(hook_path,'w') as h:
h.write("""#!/usr/bin/env bash
set -e; set -o pipefail;
cat | PIKU_ROOT="%s" $HOME/piku.py git-hook %s""" % (PIKU_ROOT, app)) # TODO: remove hardcoded script name
# Make the hook executable by our user
os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IXUSR)
# Handle the actual receive. We'll be called with 'git-hook' after it happens
call('git-shell -c "%s"' % " ".join(sys.argv[1:]), cwd=GIT_ROOT, shell=True)
2016-03-28 18:28:24 +00:00
2016-03-26 21:33:02 +00:00
2016-03-26 12:52:54 +00:00
if __name__ == '__main__':
2016-03-26 17:28:01 +00:00
piku()