From a5a594a9805352999338aa648e862feee20669b7 Mon Sep 17 00:00:00 2001 From: deflax Date: Tue, 1 Aug 2017 14:21:22 +0300 Subject: [PATCH] craete seed function phase 3 --- app/admin/routes.py | 23 +++++-- app/main/routes.py | 6 +- app/models.py | 46 +++++++------ app/smanager/routes.py | 2 +- app/templates/admin/list_cancelled.html | 65 +++++++++++++++++++ app/templates/main/dashboard.html | 44 +++++++------ .../vmanager/{deploy.html => create.html} | 56 ++-------------- app/vmanager/forms.py | 22 +++---- app/vmanager/routes.py | 39 ++++++----- cronexec.sh | 2 +- manage.py | 29 +++++++-- 11 files changed, 194 insertions(+), 140 deletions(-) create mode 100644 app/templates/admin/list_cancelled.html rename app/templates/vmanager/{deploy.html => create.html} (53%) diff --git a/app/admin/routes.py b/app/admin/routes.py index a0430c9..a734bb7 100644 --- a/app/admin/routes.py +++ b/app/admin/routes.py @@ -39,28 +39,37 @@ def index(): @login_required @admin_required def list_deployments(): - alldeployments = Deployment.query.order_by(Deployment.daysleft.asc()).all() + alldeployments = Deployment.query.filter_by(cancelled=False).order_by(Deployment.daysleft.asc()).all() return render_template('admin/list_deployments.html', deployments=alldeployments) @admin.route("/listservices", methods=['GET']) @login_required @admin_required def list_services(): - allservices = Service.query.order_by(Service.daysleft.asc()).all() + allservices = Service.query.filter_by(cancelled=False).order_by(Service.daysleft.asc()).all() return render_template('admin/list_services.html', services=allservices) @admin.route("/listdomains", methods=['GET']) @login_required @admin_required def list_domains(): - alldomains = Domain.query.order_by(Domain.daysleft.asc()).all() + alldomains = Domain.query.filter_by(cancelled=False).order_by(Domain.daysleft.asc()).all() return render_template('admin/list_domains.html', domains=alldomains) +@admin.route("/listcancelled", methods=['GET']) +@login_required +@admin_required +def list_cancelled(): + expdeployments = Deployment.query.filter_by(cancelled=True).all() + expservices = Service.query.filter_by(cancelled=True).all() + expdomains = Domain.query.filter_by(cancelled=True).all() + return render_template('admin/list_cancelled.html', deployments=expdeployments, services=expservices, domains=expdomains) + @admin.route("/listaddresses", methods=['GET']) @login_required @admin_required def list_addresses(): - alladdresses = Address.query.order_by(Address.ip.asc()).all() + alladdresses = Address.query.filter_by(cancelled=False).order_by(Address.ip.asc()).all() return render_template('admin/list_addresses.html', addresses=alladdresses) @admin.route("/addr2pool", methods=['GET', 'POST']) @@ -143,7 +152,7 @@ def transaction(user_pid=0): def dashboard(user_pid=0): cuser = User.query.filter_by(pid=user_pid).first() - inv_deployments = cuser.inv_deployments.order_by(Deployment.date_created.desc()).all() + inv_deployments = cuser.inv_deployments.filter_by(cancelled=False).order_by(Deployment.date_created.desc()).all() inv_deploycubeids = [] inv_deployments_list = [] for invcls in inv_deployments: @@ -151,13 +160,13 @@ def dashboard(user_pid=0): inv_deploycubeids.extend([invcls.machine_id]) inv_deployments_list.extend([invcls.machine_alias]) - inv_services = cuser.inv_services.order_by(Service.date_last_charge.asc()).all() + inv_services = cuser.inv_services.filter_by(cancelled=False).order_by(Service.date_last_charge.asc()).all() inv_services_list = [] for invcls in inv_services: if invcls.user_id == cuser.pid and invcls.enabled == True: inv_services_list.extend([invcls.description]) - inv_domains = cuser.inv_domains.order_by(Domain.date_created.desc()).all() + inv_domains = cuser.inv_domains.filter_by(cancelled=False).order_by(Domain.date_created.desc()).all() inv_domains_list = [] for invcls in inv_domains: if invcls.user_id == cuser.pid and invcls.enabled == True: diff --git a/app/main/routes.py b/app/main/routes.py index 4d1d237..d2ef055 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -39,7 +39,7 @@ def terms(): def dashboard(): cuser = current_user - inv_deployments = cuser.inv_deployments.order_by(Deployment.date_created.desc()).all() + inv_deployments = cuser.inv_deployments.filter_by(cancelled=False).order_by(Deployment.date_created.desc()).all() inv_deploycubeids = [] inv_deployments_list = [] for invcls in inv_deployments: @@ -47,13 +47,13 @@ def dashboard(): inv_deploycubeids.extend([invcls.machine_id]) inv_deployments_list.extend([invcls.machine_alias]) - inv_services = cuser.inv_services.order_by(Service.date_last_charge.asc()).all() + inv_services = cuser.inv_services.filter_by(cancelled=False).order_by(Service.date_last_charge.asc()).all() inv_services_list = [] for invcls in inv_services: if invcls.user_id == cuser.pid and invcls.enabled == True: inv_services_list.extend([invcls.description]) - inv_domains = cuser.inv_domains.order_by(Domain.date_created.desc()).all() + inv_domains = cuser.inv_domains.filter_by(cancelled=False).order_by(Domain.date_created.desc()).all() inv_domains_list = [] for invcls in inv_domains: if invcls.user_id == cuser.pid and invcls.enabled == True: diff --git a/app/models.py b/app/models.py index bca1e9e..ab0053d 100644 --- a/app/models.py +++ b/app/models.py @@ -208,19 +208,38 @@ def contact_proxmaster(data, method, cubeid=0): url = current_app.config['PROXMASTER_URL'] data['apikey'] = current_app.config['APIKEY'] data_json = json.dumps(data) - if method == 'vmcreate': url = '{}/{}'.format(url, method) else: url = '{}/{}/{}'.format(url, method, cubeid) - try: + db_result = requests.post( url, data=data_json, headers={"content-type": "application/json"}, timeout=30 ) proxjson = db_result.json() - #print('proxmaster query {}'.format(str(proxjson))) return proxjson except: return None +class Deployment(db.Model): + __tablename__ = 'deployments' + pid = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK + date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) + date_last_charge = db.Column(db.DateTime) + period = db.Column(db.Integer) + daysleft = db.Column(db.Integer) + warning = db.Column(db.Boolean, default=False) + enabled = db.Column(db.Boolean, default=True) + cancelled = db.Column(db.Boolean, default=False) + + protected = db.Column(db.Boolean, default=False) #machines with this False will be autodeleted after the warning period is over. + connected = db.Column(db.Boolean, default=False) #requires: protected=True, indicate that a connection is enabled + machine_id = db.Column(db.BigInteger) #cubeid + machine_alias = db.Column(db.String) #dns name + machine_cpu = db.Column(db.Integer) + machine_mem = db.Column(db.Integer) + machine_hdd = db.Column(db.Integer) + machine_addresses = db.relationship('Address', backref='deployments', lazy='dynamic') + class Service(db.Model): __tablename__ = 'services' pid = db.Column(db.Integer, primary_key=True) #PK @@ -237,27 +256,6 @@ class Service(db.Model): description = db.Column(db.Unicode) price = db.Column(db.Float) -class Deployment(db.Model): - __tablename__ = 'deployments' - pid = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, db.ForeignKey('users.pid')) #FK - date_created = db.Column(db.DateTime, index=True, default=datetime.utcnow) - date_last_charge = db.Column(db.DateTime) - period = db.Column(db.Integer) - daysleft = db.Column(db.Integer) - warning = db.Column(db.Boolean, default=False) - enabled = db.Column(db.Boolean, default=True) - protected = db.Column(db.Boolean, default=False) #machines with this False will be autodeleted after the warning period is over. - cancelled = db.Column(db.Boolean, default=False) - - machine_id = db.Column(db.BigInteger) #cubeid - machine_alias = db.Column(db.String) #dns name - machine_cpu = db.Column(db.Integer) - machine_mem = db.Column(db.Integer) - machine_hdd = db.Column(db.Integer) - machine_addresses = db.relationship('Address', backref='deployments', lazy='dynamic') - -#NAMESPACE class Region(db.Model): __tablename__ = 'regions' pid = db.Column(db.Integer, primary_key=True) diff --git a/app/smanager/routes.py b/app/smanager/routes.py index 1454b13..27d1741 100644 --- a/app/smanager/routes.py +++ b/app/smanager/routes.py @@ -61,7 +61,7 @@ def activate(itemid=0): service.enabled = True db.session.commit() - transaction = Transaction(user_id=int(owner.pid), description='Deployment {} activated for {} month(s)'.format(str(service.description), form.period.data), value=-price) + transaction = Transaction(user_id=int(owner.pid), description='Deployment {} activated for {} month(s)'.format(str(service.description), form.period.data), value=-total) db.session.add(transaction) db.session.commit() diff --git a/app/templates/admin/list_cancelled.html b/app/templates/admin/list_cancelled.html new file mode 100644 index 0000000..74b7869 --- /dev/null +++ b/app/templates/admin/list_cancelled.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} + +{% block styles %} +{{ super() }} + +{% endblock %} + +{% block page_content %} +
+ {% include "admin/admin_tasks.html" %} + +
+
+
Deployments
+

+ + + + + + + + + + + + + + + + {% for deploy in deployments %} + {% if deploy.enabled == False %} + + {% else %} + {% if deploy.warning == True %} + + {% else %} + + {% endif %} + {% endif %} + + + + + + + + + + {% endfor %} + +
OwnerCube IDAliasCPUMemHDDPriceLast ChargedDays Left
{{ deploy.owner.email }}{{ deploy.machine_id }}{{ deploy.machine_alias }}{{ deploy.machine_cpu }}{{ deploy.machine_mem }} MB{{ deploy.machine_hdd }} GB{{ deploy.price }}{{ moment(deploy.date_last_charge).format('lll') }} ({{ moment(deploy.date_last_charge).fromNow() }}){{ deploy.daysleft }}
+

+
+
+ +
+ + +
+
+ + +{% endblock %} + diff --git a/app/templates/main/dashboard.html b/app/templates/main/dashboard.html index f72ceb2..6736f8b 100644 --- a/app/templates/main/dashboard.html +++ b/app/templates/main/dashboard.html @@ -133,27 +133,33 @@ addEventListener("DOMContentLoaded", function() { - - - - - - - - + + + + + + + {% for deploy in inv_deployments %} {% if deploy.enabled == False %} - - + + - - {% else %} {% if deploy.warning == True %} @@ -161,25 +167,23 @@ addEventListener("DOMContentLoaded", function() { {% else %} {% endif %} - - + + + - - {% if deploy.warning == True %} - + {% else %} {% endif %} - {% endif %} {% endfor %} diff --git a/app/templates/vmanager/deploy.html b/app/templates/vmanager/create.html similarity index 53% rename from app/templates/vmanager/deploy.html rename to app/templates/vmanager/create.html index 1b9021b..0b069fe 100644 --- a/app/templates/vmanager/deploy.html +++ b/app/templates/vmanager/create.html @@ -1,6 +1,6 @@ {% extends "base.html" %} -{% block title %}Deploy{% endblock %} +{% block title %}Create{% endblock %} {% block page_content %} @@ -33,79 +33,37 @@
-
-
-
{{ product_name }}
-
-
- {{ product_description }} -
-
-
- -
+
Настройки
-
+

{{ form.servername.label }}
{{ form.servername(size=42) }}
{% for error in form.servername.errors %} {{ error }}
{% endfor %}

- -
-

{{ form.region.label }}
{{ form.region }}
{% for error in form.region.errors %} {{ error }}
{% endfor %}

- -
- -

- {{ form.vmpassword.label }}
{{ form.vmpassword }}
- {% for error in form.vmpassword.errors %} - {{ error }}
- {% endfor %} -

- -
-

-

-
- {{ form.cpu.label }} {{ form.cpu(class_="noUiSluder") }}
-
- {{ form.mem.label }} {{ form.mem }}
- {{ form.hdd.label }} {{ form.hdd }}
- {{ form.recipe.label }} {{ form.recipe }}
- {{ form.ipv4.label }} {{ form.ipv4 }}
-

- -
- - {{ form.invite_key.label }} {{ form.invite_key }}
+ {{ form.invite_key.label }}
{{ form.invite_key }}
{% for error in form.invite_key.errors %} {{ error }}
{% endfor %} - - -

-
- +

{{ form.csrf_token() }} {{ form.submit }}

-
- - +
+
diff --git a/app/vmanager/forms.py b/app/vmanager/forms.py index df22924..feea6c4 100644 --- a/app/vmanager/forms.py +++ b/app/vmanager/forms.py @@ -3,24 +3,20 @@ from wtforms import StringField, PasswordField, BooleanField, SubmitField, Selec from wtforms import validators, ValidationError from wtforms.fields.html5 import EmailField +from .. import db -class DeployForm(FlaskForm): - servername = StringField('Име/Домейн:', [validators.Regexp(message='пример: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$')]) - region = SelectField('Регион:') - vmpassword = StringField('Парола:', [validators.DataRequired()]) - cpu = StringField('Процесорни ядра:') - mem = StringField('Памет:') - hdd = StringField('Дисково пространство:') - recipe = SelectField('Рецепта') - ipv4 = SelectField('Брой публични IP адреса', choices=[('1', '1'),('2', '2' ), ('3', '3')]) - invite_key = StringField('Покана', [validators.DataRequired(), validators.Length(6,35)]) - + +class CreateForm(FlaskForm): + region_choices = [(1, 'Plovdiv, Bulgaria')] + servername = StringField('Domain Name:', [validators.Regexp(message='ex.: myservice1.com, myservice2.local', regex='^[a-zA-Z0-9][a-zA-Z0-9-_]{0,61}[a-zA-Z0-9]{0,1}\.([a-zA-Z]{1,6}|[a-zA-Z0-9-]{1,30}\.[a-zA-Z]{2,3})$'), validators.Length(6,64)]) + region = SelectField('Region:', choices=region_choices, coerce=int) + invite_key = StringField('Invite Code:', [validators.DataRequired(), validators.Length(6,35)]) def validate_invite_key(self, field): if field.data != 'inv1919': raise ValidationError('Denied') - - submit = SubmitField('Deploy') + submit = SubmitField('Create') class ActivateForm(FlaskForm): period = SelectField('Deploy Period', choices=[(1, '1 Month'), (3, '3 Months'), (6, '6 Months'), (12, '1 Year'), (24, '2 Years')], coerce=int) submit = SubmitField('Activate') + diff --git a/app/vmanager/routes.py b/app/vmanager/routes.py index 9249ae9..40ff50b 100644 --- a/app/vmanager/routes.py +++ b/app/vmanager/routes.py @@ -3,7 +3,7 @@ from flask_login import login_required, login_user, logout_user, current_user from flask_sqlalchemy import get_debug_queries from . import vmanager -from .forms import DeployForm, ActivateForm +from .forms import CreateForm, ActivateForm from .. import db from ..email import send_email from ..models import User, Permission, Transaction, Deployment, Service, Region, Address, Domain, contact_proxmaster @@ -34,17 +34,22 @@ def createvm(): flash('Please update profile info for this operation.') return redirect(url_for('settings.profile')) - deployment_seeds = Deployment.query.filter_by(protected=False).all() + deployment_seeds = current_user.inv_deployments.filter_by(cancelled=False).filter_by(protected=False).all() if deployment_seeds != []: - flash('Offline deployments exist.') + flash('Offline deployments still exist.') return redirect(url_for('main.dashboard')) + form = CreateForm() if current_user.confirmed and form.validate_on_submit(): + selected_region = Region.query.filter_by(pid=int(form.region.data)).first() + ### + #flash('Region: {}'.format(str(selected_region.description))) + #return redirect(url_for('main.dashboard')) + data = { 'clientid': str(current_user.pid), - 'clientname': str(current_user.name), 'clientemail': str(current_user.email), - 'hostname': str(form.deployname.data), - 'region': form.region.data, + 'hostname': str(form.servername.data), + 'region': str(selected_region.name), 'type': 'kvm', 'cpu': '1', 'mem': '512', @@ -57,21 +62,20 @@ def createvm(): return redirect(url_for('main.dashboard')) if query is not None: - deployment = Deployment(user_id=client_id, product_id=product_id, machine_alias=form.servername.data, machine_id=query['cube'], machine_cpu=form.cpu.data, machine_mem=form.mem.data, machine_hdd=form.hdd.data, enabled=True) + deployment = Deployment(user_id=int(current_user.pid), machine_alias=query['hostname'], machine_id=query['cube'], machine_cpu=data['cpu'], machine_mem=data['mem'], machine_hdd=data['hdd'], enabled=True, protected=False, daysleft=15, warning=True) db.session.add(deployment) db.session.commit() - - flash('Deploy requested.') + flash('Deploy created successfully in region "{}".'.format(str(selected_region.description))) else: flash('Deploy cancelled! Please try again later...') return redirect(url_for('main.dashboard')) - + return render_template('vmanager/create.html', form=form) @vmanager.route('/activate/', methods=['GET', 'POST']) @login_required def activate(itemid=0): - result = current_user.inv_deployments.all() + result = current_user.inv_deployments.filter_by(cancelled=False).all() inventory = [] for invcls in result: inventory.extend([invcls.machine_id]) @@ -88,7 +92,13 @@ def activate(itemid=0): mem_cost = ( deploy.machine_mem / 1024 ) * current_app.config['MEM_RATIO'] hdd_cost = deploy.machine_hdd * current_app.config['HDD_RATIO'] ppm = round(cpu_cost + mem_cost + hdd_cost) - form = ActivateForm(period=int(deploy.period)) + + if deploy.period is None: + form = ActivateForm(period=1) + total = ppm * 1 + else: + form = ActivateForm(period=int(deploy.period)) + total = ppm * deploy.period owner = deploy.owner if owner.confirmed and form.validate_on_submit(): @@ -106,21 +116,20 @@ def activate(itemid=0): deploy.daysleft = daysleft.days + extradays.days deploy.warning = False deploy.enabled = True + deploy.protected = True db.session.commit() transaction = Transaction(user_id=int(owner.pid), description='Deployment {} activated for {} month(s)'.format(str(deploy.machine_alias), form.period.data), value=-total) db.session.add(transaction) db.session.commit() - owner.wallet = owner.wallet - total db.session.commit() - flash('Deployment {} activated for {} month(s)'.format(str(deploy.machine_alias), form.period.data)) if owner.is_administrator: return redirect(url_for('admin.list_deployments')) else: return redirect(url_for('main.dashboard')) - return render_template('vmanager/activate.html', form=form, deploy=deploy, cpu_cost=cpu_cost, mem_cost=mem_cost, hdd_cost=hdd_cost, ppm=ppm, total=(ppm * deploy.period), currency=owner.currency) + return render_template('vmanager/activate.html', form=form, deploy=deploy, cpu_cost=cpu_cost, mem_cost=mem_cost, hdd_cost=hdd_cost, ppm=ppm, total=total, currency=owner.currency) @vmanager.route('//') diff --git a/cronexec.sh b/cronexec.sh index 1fed76b..be99172 100755 --- a/cronexec.sh +++ b/cronexec.sh @@ -1,4 +1,4 @@ #!/bin/bash scriptdir=`dirname $0` cd $scriptdir -/bin/bash -c "source ../bin/activate; python3 manage.py autodisable ; python3 manage.py autowarn" +/bin/bash -c "source ../bin/activate; python3 manage.py autodisable ; python3 manage.py autowarn ; python3 manage.py autoremove" diff --git a/manage.py b/manage.py index 6957927..fb99b28 100644 --- a/manage.py +++ b/manage.py @@ -36,16 +36,31 @@ def deploy(): # create user roles Role.insert_roles() +@manager.command +def autoremove(): + from app.models import User, Deployment + today = datetime.utcnow() + print('\nScan for unprotected deployments, lower their days left by 1 and autodelete them if expired. Script Started at {}'.format(today)) + drafts = Deployment.query.filter_by(cancelled=False).filter_by(protected=False).all() + for draft in drafts: + daysleft = draft.daysleft + daysleft -= 1 + draft.daysleft = daysleft + print('Draft {} will be autoremoved after {} days.'.format(draft.machine_alias, daysleft)) + db.session.commit() + @manager.command def autodisable(): from app.models import User, Deployment, Service, Domain + from sqlalchemy import and_, or_, not_ today = datetime.utcnow() print('\nScan for active expired items and set them as inactive. Script started at {}'.format(today)) dep_c = 0 srv_c = 0 dom_c = 0 - deployments_ena = Deployment.query.filter_by(enabled=True).all() - for deploy in deployments_ena: + deployments = Deployment.query.filter_by(cancelled=False).filter_by(enabled=True).filter_by(protected=True).all() + for deploy in deployments: + #print('Found {}.'.format(str(deploy.machine_alias))) lastcharge = deploy.date_last_charge expiry = lastcharge + relativedelta(lastcharge, months=+(deploy.period)) if today > expiry: @@ -56,7 +71,7 @@ def autodisable(): db.session.commit() else: dep_c += 1 - services_ena = Service.query.filter_by(enabled=True).all() + services_ena = Service.query.filter_by(cancelled=False).filter_by(enabled=True).all() for service in services_ena: lastcharge = service.date_last_charge expiry = lastcharge + relativedelta(lastcharge, months=+(service.period)) @@ -68,7 +83,7 @@ def autodisable(): db.session.commit() else: srv_c += 1 - domains_ena = Domain.query.filter_by(enabled=True).all() + domains_ena = Domain.query.filter_by(cancelled=False).filter_by(enabled=True).all() for domain in domains_ena: expiry = domain.date_expire if today > expiry: @@ -86,7 +101,7 @@ def autowarn(): from app.models import User, Deployment, Service, Domain, Transaction today = datetime.utcnow() print('\nScan for items that will expire soon and enable the warning flag. Script started at {}'.format(today)) - deployments_ena = Deployment.query.filter_by(enabled=True).all() + deployments_ena = Deployment.query.filter_by(cancelled=False).filter_by(enabled=True).filter_by(protected=True).all() for deploy in deployments_ena: lastcharge = deploy.date_last_charge expiry = lastcharge + relativedelta(lastcharge, months=+(deploy.period)) @@ -102,7 +117,7 @@ def autowarn(): deploy.warning = False db.session.commit() - services_ena = Service.query.filter_by(enabled=True).all() + services_ena = Service.query.filter_by(cancelled=False).filter_by(enabled=True).all() for service in services_ena: lastcharge = service.date_last_charge expiry = lastcharge + relativedelta(lastcharge, months=+(service.period)) @@ -118,7 +133,7 @@ def autowarn(): service.warning = False db.session.commit() - domains_ena = Domain.query.filter_by(enabled=True).all() + domains_ena = Domain.query.filter_by(cancelled=False).filter_by(enabled=True).all() for domain in domains_ena: expiry = domain.date_expire daysleft = expiry - today
NameCPUMemHDDNetworkControlRemoteTime Left + + + + + + + + +
{% if status[deploy.machine_id] == 'running' %}{% else %}{% endif %}{{ deploy.machine_alias }}{{ deploy.machine_cpu }} Cores{% if deploy.enabled == True %}{% else %}{% endif %} {{ deploy.machine_alias }}{{ deploy.machine_cpu }} {% if deploy.machine_cpu > 1 %}cores{% else %}core{% endif %} {{ deploy.machine_mem }} MB {{ deploy.machine_hdd }} GB{% for addr in deploy.machine_addresses %} {{ addr.ip }}
{% endfor %}
{% if status[deploy.machine_id] == 'running' %}{% else %}{% endif %}{{ deploy.machine_alias }}{{ deploy.machine_cpu }} Cores{% if deploy.protected == True %}{% else %}{% endif %}{{ deploy.machine_alias }}{% for addr in deploy.machine_addresses %} {% endfor %}{{ deploy.machine_cpu }} {% if deploy.machine_cpu > 1 %}cores{% else %}core{% endif %} {{ deploy.machine_mem }} MB {{ deploy.machine_hdd }} GB{% for addr in deploy.machine_addresses %}{{ addr.ip }}
{% endfor %}
{% if status[deploy.machine_id] == 'running' %} {% else %} - {% endif %}{% if status[deploy.machine_id] == 'running' %} - {% endif %} + {% endif %} + {% if status[deploy.machine_id] == 'running' %}{% endif %}{{ deploy.daysleft }} day(s)