From 08eb38dc5713419330b21fc3b53c01d2d4179941 Mon Sep 17 00:00:00 2001 From: forest Date: Wed, 21 Jul 2021 16:45:53 -0500 Subject: [PATCH 1/3] correctly enforce affordable_vm_sizes post form submission --- .gitignore | 1 + capsulflask/console.py | 17 ++++++++++------- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index bb022e9..c9bb7c2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ notes.txt .env +.env.bak .vscode *.pyc diff --git a/capsulflask/console.py b/capsulflask/console.py index 5da18e9..d15155d 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -199,6 +199,14 @@ def create(): capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024) errors = list() + affordable_vm_sizes = dict() + for key, vm_size in vm_sizes.items(): + # if a user deposits $7.50 and then creates an f1-s vm which costs 7.50 a month, + # then they have to delete the vm and re-create it, they will not be able to, they will have to pay again. + # so for UX it makes a lot of sense to give a small margin of 25 cents for usability sake + if vm_size["dollars_per_month"] <= account_balance+0.25: + affordable_vm_sizes[key] = vm_size + if request.method == "POST": if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: return abort(418, f"u want tea") @@ -209,6 +217,8 @@ def create(): errors.append("Size is required") elif size not in vm_sizes: errors.append(f"Invalid size {size}") + elif size not in affordable_vm_sizes: + errors.append(f"Your account must have enough credit to run an {size} for 1 month before you will be allowed to create it") if not os: errors.append("OS is required") @@ -260,13 +270,6 @@ def create(): return redirect(f"{url_for('console.index')}?created={id}") - affordable_vm_sizes = dict() - for key, vm_size in vm_sizes.items(): - # if a user deposits $7.50 and then creates an f1-s vm which costs 7.50 a month, - # then they have to delete the vm and re-create it, they will not be able to, they will have to pay again. - # so for UX it makes a lot of sense to give a small margin of 25 cents for usability sake - if vm_size["dollars_per_month"] <= account_balance+0.25: - affordable_vm_sizes[key] = vm_size for error in errors: flash(error) From 5bb76173dd5599b9e3ea3475f1f69eb2293eb597 Mon Sep 17 00:00:00 2001 From: 3wordchant <3wordchant@noreply.git.autonomic.zone> Date: Thu, 22 Jul 2021 01:15:39 +0200 Subject: [PATCH 2/3] Add custom themes, THEME setting, basic "yolocolo" theme (#7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Specify `THEME=yourtheme`, add some HTML files in `capsulflask/theme/yourtheme` 👌 We probably want to reduce copypasta in the current `yolocolo` theme by using template inheritance, at some point. Reviewed-on: https://git.autonomic.zone/3wordchant/capsul-flask/pulls/7 Co-authored-by: 3wordchant <3wordchant@noreply.git.autonomic.zone> Co-committed-by: 3wordchant <3wordchant@noreply.git.autonomic.zone> --- capsulflask/__init__.py | 13 ++- capsulflask/static/favicon.yolocolo.ico | Bin 0 -> 1758 bytes capsulflask/static/icon.yolocolo.png | Bin 0 -> 1209 bytes capsulflask/static/style.yolocolo.css | 35 +++++++ .../theme/yolocolo/account-balance.html | 94 ++++++++++++++++++ capsulflask/theme/yolocolo/base.html | 60 +++++++++++ capsulflask/theme/yolocolo/capsuls.html | 68 +++++++++++++ capsulflask/theme/yolocolo/faq.html | 46 +++++++++ capsulflask/theme/yolocolo/index.html | 28 ++++++ capsulflask/theme/yolocolo/pricing.html | 23 +++++ capsulflask/theme/yolocolo/support.html | 21 ++++ 11 files changed, 387 insertions(+), 1 deletion(-) create mode 100644 capsulflask/static/favicon.yolocolo.ico create mode 100644 capsulflask/static/icon.yolocolo.png create mode 100644 capsulflask/static/style.yolocolo.css create mode 100644 capsulflask/theme/yolocolo/account-balance.html create mode 100644 capsulflask/theme/yolocolo/base.html create mode 100644 capsulflask/theme/yolocolo/capsuls.html create mode 100644 capsulflask/theme/yolocolo/faq.html create mode 100644 capsulflask/theme/yolocolo/index.html create mode 100644 capsulflask/theme/yolocolo/pricing.html create mode 100644 capsulflask/theme/yolocolo/support.html diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index 55643bb..7aabe3f 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -2,6 +2,7 @@ import logging from logging.config import dictConfig as logging_dict_config import atexit +import jinja2 import os import hashlib import requests @@ -71,7 +72,9 @@ app.config.from_mapping( #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"), - BTCPAY_URL=os.environ.get("BTCPAY_URL", default="") + BTCPAY_URL=os.environ.get("BTCPAY_URL", default=""), + + THEME=os.environ.get("THEME", default="") ) app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL']) @@ -154,6 +157,14 @@ is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line app.logger.info(f"is_running_server: {is_running_server}") +if app.config['THEME'] != "": + my_loader = jinja2.ChoiceLoader([ + jinja2.FileSystemLoader( + [os.path.join('capsulflask', 'theme', app.config['THEME']), + 'capsulflask/templates']), + ]) + app.jinja_loader = my_loader + if app.config['HUB_MODE_ENABLED']: if app.config['HUB_MODEL'] == "capsul-flask": diff --git a/capsulflask/static/favicon.yolocolo.ico b/capsulflask/static/favicon.yolocolo.ico new file mode 100644 index 0000000000000000000000000000000000000000..bb2e8850fa0c99ebdfa1e36bb22eab929911bfd5 GIT binary patch literal 1758 zcmeIyOGs2v7{KxWJgO-tn_gx$HPiHR%*vUXM^;l!Wjdvk&CF>dxNsu~f-6DY=)#TY z&b5Mqf*VmmS1!cG-~)Vt2#O%$V=FW4Vh7=%8848i94AqYdjYfl3t3{{Nq1Wp%8jYAtCd_6t7K;U|)r!q#!)~|Za5!)} zow!^s+-^4>j|Z>Ui_ho7@Anf5g$Rek6c!dzR8&N9aWN$&C6tzyQdU+*d3iY%6%|xg zR#H_}MRj#GH8nNV*49#2S4Vw)Jq-;FG&VNU)YL?Cb2BY1Ewr|_($>~SdwV+_9UXLb zcGA_=MR#{MJv}}2_V&`(*GGSUKLY~;3=R%5G&Dps8fAERn30hYMn^{(8yjPMe4L4i z2_`2enVOnndU~3fnHgqhXPKLuV}5>~g@pwc7Z+JtT4H&5nU$3lR##V9TU%p&eVvVs z4K_D7iN#`UZEdl=z0J;SRCqa z|KfR?o}TU&vIib09f(u6KFa>)qWY=kPr=WUe@IgKdj9hZ_Y%F}n##O$YmnR%=koSOJDMYT)cbE{i?AzM&L1?KbAA&cAp%zExxuq9 zl-k|T5~AR?ls_es{V@Awj?4b_mE;y-CryNs@xPbcl$V>4D@2wfXX=w!d-%kgGD%6RrRkdc@tqHNO72nQ1P<(D3=>i_Vk!2jm# E2RYs=b^rhX literal 0 HcmV?d00001 diff --git a/capsulflask/static/icon.yolocolo.png b/capsulflask/static/icon.yolocolo.png new file mode 100644 index 0000000000000000000000000000000000000000..987ea74fa0fc2c3429eaca6c6b21a2ba3460c2bb GIT binary patch literal 1209 zcmV;q1V;ObP)EX>4Tx04R}tkv&MmKpe$iQ>7vm1q(`X$WWauh>AFB6^c+H)C#RSm|XfHG-*gu zTpR`0f`cE6RRL;k>@Q#C4iONMZqtkRU=q6&onSMx0ii6botEPx$zwu3sXTLaq%k za?GOw4YKP8|AXJ%T7`*mFDaY=I$s>;V;BhS0*#vEd>=bb;{*sk16O*>U#SDrpQP7X zTI>ku-3BhMTbi;5TeSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00NOoL_t(Y$HkV5E4k{`tK79MU7KsSCT<%93zP`S4cX#(K@Apb7rReMHV{L7X zs;Vk%+veuxhKGj-bX_M92oMYgsjjZ(;NSq;wwa!u_EwNjP)dp8IAWTn0HW)<0G{!W zTUl8VrIaY8K5r+!Vis7I#mLAA;cyrLUDxU9>7lo`7fsWOTIerCE-o%IK0c0N7`U!W zB9XwdEE*ab060B8Wq5eFu)=p1wYIiOE|-%`CL^&}Ofs2F-iD*2BZkV zO-&6;OG~8FX=-b0IXgT1oxtJYAtxs%ghC-kM@IqJ-rlCYy`8qUwtP`;ZEaCmSy}oS zn5HRpb#=0{vm=j>kD`ooF=b z{e#cPohervAerVOG``X&F+09lSzq0B41{~%*>3W(`j)WN3O50ORIy=k +

Account Balance: ${{ account_balance }}

+ +
+ + {% if has_vms and has_payments and warning_text != "" %} +
+
{{ warning_text }}
+
+ {% endif %} +
+ {% if has_payments %} +
+
+

Payments

+
+ + + + + + + + + + {% for payment in payments %} + + + + + {% endfor %} + +
amountdate
${{ payment["dollars"] }}{{ payment["created"] }}
+
+ {% endif %} + + +
+ + + {% if has_vms %} +
+

Capsuls Billed

+
+
+ + + + + + + + + + + + + {% for vm in vms_billed %} + + + + + + + + + {% endfor %} + +
idcreateddeleted$/monthmonths$ billed
{{ vm["id"] }}{{ vm["created"] }}{{ vm["deleted"] }}${{ vm["dollars_per_month"] }}{{ vm["months"] }}${{ vm["dollars"] }}
+
+ + {% endif %} +
+{% endblock %} + +{% block pagesource %}/templates/create-capsul.html{% endblock %} diff --git a/capsulflask/theme/yolocolo/base.html b/capsulflask/theme/yolocolo/base.html new file mode 100644 index 0000000..eae2af4 --- /dev/null +++ b/capsulflask/theme/yolocolo/base.html @@ -0,0 +1,60 @@ + + + + + {% block title %}{% endblock %}{% if self.title() %} - {% endif %}Capsul + + + + {% block head %}{% endblock %} + + + + + +{% for message in get_flashed_messages() %} +
{{ message }}
+{% endfor %} +{% block custom_flash %}{% endblock %} +
+ {% block content %}{% endblock %} +
+{% block subcontent %}{% endblock %} + + + + + diff --git a/capsulflask/theme/yolocolo/capsuls.html b/capsulflask/theme/yolocolo/capsuls.html new file mode 100644 index 0000000..f64a674 --- /dev/null +++ b/capsulflask/theme/yolocolo/capsuls.html @@ -0,0 +1,68 @@ +{% extends 'base.html' %} + +{% block title %}Capsuls{% endblock %} + +{% block custom_flash %} +{% if created %} +
{{ created }} successfully created!
+{% endif %} +{% endblock %} + +{% block content %} +
+

Capsuls

+
+
+ {% if has_vms %} + +
+ + + + + + + + + + + + + + + {% for vm in vms %} + + {% if vm['state'] == 'starting' or vm['state'] == 'stopping' %} + + {% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' or vm['state'] == 'stopped' %} + + {% elif vm['state'] == 'unknown' %} + + {% else %} + + {% endif %} + + + + + + + + + + {% endfor %} + +
idsizecpumemipv4oscreated
?{{ vm["id"] }}{{ vm["size"] }}{{ vm["ipv4"] }}{{ vm["os"] }}{{ vm["created"] }}
+
+ + {% else %} +
+

You don't have any Capsuls running. Create one today!

+
+ {% endif %} +
+{% endblock %} + +{% block pagesource %}/templates/capsuls.html{% endblock %} diff --git a/capsulflask/theme/yolocolo/faq.html b/capsulflask/theme/yolocolo/faq.html new file mode 100644 index 0000000..54dbc95 --- /dev/null +++ b/capsulflask/theme/yolocolo/faq.html @@ -0,0 +1,46 @@ +{% extends 'base.html' %} + +{% block title %}FAQ{% endblock %} + +{% block content %} +

Frequently Asked Questions

+{% endblock %} + +{% block subcontent %} +

+

    +
  • + What is this? +

    + This is a technical demo of Capsul, for the + as-yet-untitled Cotech server hosting + initiative, which you can read + about on the Cotech forum. +

    +
  • +
  • + What do you mean, "technical demo"? +

    No backups

    +

    No service level agreement

    +

    "Best effort" support

    +
  • +
  • + Where can I get this, but, more reliable? +

    Cyberia, the authors of this platform, run the canonical instance, Capsul.org, on hardware they own. Please + send them your money! (cash, crypto, or card accepted).

    +
  • +
  • + How do I use this system? +

    Please see the official Capsul FAQ + page.

    +
  • +
+

+ +{% endblock %} + +{% block pagesource %}/templates/faq.html{% endblock %} + diff --git a/capsulflask/theme/yolocolo/index.html b/capsulflask/theme/yolocolo/index.html new file mode 100644 index 0000000..f785803 --- /dev/null +++ b/capsulflask/theme/yolocolo/index.html @@ -0,0 +1,28 @@ +{% extends 'base.html' %} + +{% block content %} +

+
+             _                 _
+ _   _  ___ | | ___   ___ ___ | | ___
+| | | |/ _ \| |/ _ \ / __/ _ \| |/ _ \
+| |_| | (_) | | (_) | (_| (_) | | (_) |
+ \__, |\___/|_|\___/ \___\___/|_|\___/
+ |___/
+
+  
+ Co-operative hosting using Cyberia's Capsul +{% endblock %} + +{% block subcontent %} +

+

    +
  • Sign up for an account!
  • +
  • Add some funds!
  • +
  • Create a VPS!
  • +
  • Give your feedback!
  • +
+

+{% endblock %} + +{% block pagesource %}/templates/index.html{% endblock %} diff --git a/capsulflask/theme/yolocolo/pricing.html b/capsulflask/theme/yolocolo/pricing.html new file mode 100644 index 0000000..c2156cc --- /dev/null +++ b/capsulflask/theme/yolocolo/pricing.html @@ -0,0 +1,23 @@ +{% extends 'base.html' %} + +{% block title %}Pricing{% endblock %} + +{% block content %} +
+

CAPSUL TYPES & PRICING

+
+
+

+ Rates for this service aren't set yet. You can see Cyberia's Capsul pricing + on their website. +

+
+
+
+      SUPPORTED OPERATING SYSTEMS:
+
+      {% for os_id, os in operating_systems.items() %}   - {{ os.description }} 
+      {% endfor %}
+    
+
+{% endblock %} diff --git a/capsulflask/theme/yolocolo/support.html b/capsulflask/theme/yolocolo/support.html new file mode 100644 index 0000000..ded9b82 --- /dev/null +++ b/capsulflask/theme/yolocolo/support.html @@ -0,0 +1,21 @@ +{% extends 'base.html' %} + +{% block title %}Support{% endblock %} + +{% block content %} +
+

SUPPORT

+
+ +{% endblock %} + +{% block subcontent %} +

+ You can also find us on Matrix: #untitled-hosting.public:autonomic.zone. +

+{% endblock %} + +{% block pagesource %}/templates/support.html{% endblock %} From 72c04d849522fe61cd625088275bf329354b737e Mon Sep 17 00:00:00 2001 From: 3wordchant <3wordchant@noreply.git.autonomic.zone> Date: Thu, 22 Jul 2021 01:18:10 +0200 Subject: [PATCH 3/3] Docker image, & local development docker-compose.yml (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds: - a Docker image, which can be used in both development and production - a `docker-compose.yml` file for local development (could probably be adapted for production deployments) Testing: - `git checkout docker` - `docker-compose up` - pray 🙏 - go to http://localhost:5000 ## App architecture I added the ability to load secret config variables (`HUB_TOKEN`, `STRIPE_SECRET_KEY` etc) from files, to support [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) natively. The code should probably log an error if a specified `HUB_TOKEN_FILE` doesn't exist instead of failing silently.. ## Docker architecture This uses a multi-stage build to reduce the size of the final image -- having pipenv install to a predefined virtualenv, and then copying that over. The compose file doesn't include a definition for a cron runner service, and I haven't tested running one yet. Here be dragons! You can rebuild the image locally using `docker-compose build`, but this isn't required for changes to the app code, only if you edit the `Dockerfile`, or want to publish your image for use on a swarm server (in which case you will need to edit the image name to put in your own Docker hub credentials). Currently, the image is rebuilt (should set up auto-tagging..) and published with every push to this 3wordchant/capsul-flask fork. Reviewed-on: https://git.autonomic.zone/3wordchant/capsul-flask/pulls/2 Co-authored-by: 3wordchant <3wordchant@noreply.git.autonomic.zone> Co-committed-by: 3wordchant <3wordchant@noreply.git.autonomic.zone> --- .drone.yml | 13 +++++++++++ Dockerfile | 48 +++++++++++++++++++++++++++++++++++++++++ capsulflask/__init__.py | 16 ++++++++++++++ docker-compose.yml | 36 +++++++++++++++++++++++++++++++ 4 files changed, 113 insertions(+) create mode 100644 .drone.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.drone.yml b/.drone.yml new file mode 100644 index 0000000..88fa8ac --- /dev/null +++ b/.drone.yml @@ -0,0 +1,13 @@ +--- +kind: pipeline +name: publish docker image +steps: + - name: build and publish + image: plugins/docker + settings: + username: + from_secret: docker_reg_username_3wc + password: + from_secret: docker_reg_passwd_3wc + repo: 3wordchant/capsul-flask + tags: ${DRONE_COMMIT_BRANCH} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e148126 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,48 @@ +FROM python:3.8-alpine as build + +RUN apk add --no-cache \ + build-base \ + gcc \ + gettext \ + git \ + jpeg-dev \ + libffi-dev \ + libjpeg \ + musl-dev \ + postgresql-dev \ + python3-dev \ + zlib-dev + +RUN mkdir -p /app/{code,venv} +WORKDIR /app/code +COPY Pipfile Pipfile.lock /app/code/ + +RUN python3 -m venv /app/venv +RUN pip install pipenv setuptools +ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv" +RUN pip install wheel cppy +# Install dependencies into the virtual environment with Pipenv +RUN pipenv install --deploy --verbose + +FROM python:3.8-alpine + +RUN apk add --no-cache \ + cloud-utils \ + libjpeg \ + libpq \ + libstdc++ \ + libvirt-client \ + openssh-client \ + virt-install + +COPY . /app/code/ +WORKDIR /app/code + +COPY --from=build /app/venv /app/venv +ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv" + +CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gevent", "--worker-connections", "1000", "app:app"] + +VOLUME /app/code + +EXPOSE 5000 diff --git a/capsulflask/__init__.py b/capsulflask/__init__.py index 7aabe3f..ac258a2 100644 --- a/capsulflask/__init__.py +++ b/capsulflask/__init__.py @@ -27,8 +27,24 @@ class StdoutMockFlaskMail: def send(self, message: Message): current_app.logger.info(f"Email would have been sent if configured:\n\nto: {','.join(message.recipients)}\nsubject: {message.subject}\nbody:\n\n{message.body}\n\n") + load_dotenv(find_dotenv()) +for var_name in [ + "SPOKE_HOST_TOKEN", "HUB_TOKEN", "STRIPE_SECRET_KEY", + "BTCPAY_PRIVATE_KEY", "MAIL_PASSWORD" +]: + var = os.environ.get(f"{var_name}_FILE") + if not var: + continue + + if not os.path.isfile(var): + continue + + with open(var) as secret_file: + os.environ[var_name] = secret_file.read().rstrip('\n') + del os.environ[f"{var_name}_FILE"] + app = Flask(__name__) app.config.from_mapping( diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8a62470 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +--- +version: "3.8" + +services: + app: + image: 3wordchant/capsul-flask:latest + build: . + volumes: + - "./:/app/code" + - "../tank:/tank" + # - "/var/run/libvirt/libvirt-sock:/var/run/libvirt/libvirt-sock" + depends_on: + - db + ports: + - "5000:5000" + environment: + - "POSTGRES_CONNECTION_PARAMETERS=host=db port=5432 user=capsul password=capsul dbname=capsul" + - SPOKE_MODEL=shell-scripts + #- FLASK_DEBUG=1 + - BASE_URL=http://localhost:5000 + - ADMIN_PANEL_ALLOW_EMAIL_ADDRESSES=3wc.capsul@doesthisthing.work + - VIRSH_DEFAULT_CONNECT_URI=qemu:///system + # The image uses gunicorn by default, let's override it with Flask's + # built-in development server + command: ["flask", "run", "-h", "0.0.0.0", "-p", "5000"] + db: + image: "postgres:9.6.5-alpine" + volumes: + - "postgres:/var/lib/postgresql/data" + environment: + POSTGRES_USER: capsul + POSTGRES_PASSWORD: capsul + POSTGRES_DB: capsul + +volumes: + postgres: