Compare commits
45 Commits
fix-email-
...
master
Author | SHA1 | Date |
---|---|---|
Loïc Dachary | eaff09efd4 | |
Loïc Dachary | d07a644552 | |
Loïc Dachary | 93c38fc1f9 | |
Loïc Dachary | 788e025b98 | |
Loïc Dachary | 1c165fcea3 | |
Aravinth Manivannan | 2e36a186ee | |
Loïc Dachary | ad925cddfc | |
Loïc Dachary | 2725b9b1f6 | |
Loïc Dachary | 0f1003dbe8 | |
Aravinth Manivannan | 187d22118f | |
Hostea dashboard | 9b9fb8362d | |
Loïc Dachary | 498b95848e | |
Loïc Dachary | b5afc49f5d | |
Hostea dashboard | 86fe9a2c59 | |
Hostea dashboard | 9d2a53f2e2 | |
Loïc Dachary | c7def47215 | |
Hostea dashboard | 9f5032bd28 | |
Hostea dashboard | 669a22a004 | |
Loïc Dachary | 0ab82fe0b4 | |
Hostea dashboard | 0e0b8db940 | |
Hostea dashboard | ac0775e075 | |
Hostea dashboard | 51e7ea4d5e | |
Loïc Dachary | 98973301c3 | |
Hostea dashboard | 3ebc518e1f | |
Aravinth Manivannan | 25db390e4d | |
Hostea dashboard | 6365b84d45 | |
Hostea dashboard | 41ca63244e | |
Hostea dashboard | 0e2112e30f | |
Loïc Dachary | 5b7cf06c7a | |
Hostea dashboard | 1792713952 | |
Loïc Dachary | 3019d9d739 | |
Hostea dashboard | bcb2e26f61 | |
Hostea dashboard | 011fb4816f | |
Hostea dashboard | 6c31555a52 | |
Hostea dashboard | 060e9b84d4 | |
Hostea dashboard | 418bb7dec0 | |
Loïc Dachary | c4375a43b2 | |
Loïc Dachary | 705c3a282b | |
Loïc Dachary | cfd5518518 | |
Aravinth Manivannan | a95158f3df | |
Aravinth Manivannan | b12cc044da | |
Aravinth Manivannan | cc12d1a77d | |
Aravinth Manivannan | cb6bce0c44 | |
Aravinth Manivannan | bebf18946a | |
Loïc Dachary | 6e84746a2c |
|
@ -3,5 +3,5 @@ export db=""
|
||||||
export STRIPE_SECRET_KEY=""
|
export STRIPE_SECRET_KEY=""
|
||||||
export STRIPE_PUBLIC_KEY=""
|
export STRIPE_PUBLIC_KEY=""
|
||||||
export HOSTEA_INFRA_HOSTEA_REPO_REMOTE="ssh://git@localhost:22/hostea/fleet.git"
|
export HOSTEA_INFRA_HOSTEA_REPO_REMOTE="ssh://git@localhost:22/hostea/fleet.git"
|
||||||
export HOSTEA_META_GITEA_INSTANCE="http://localhost:3000"
|
export HOSTEA_META_FORGEJO_INSTANCE="http://localhost:3000"
|
||||||
export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||||
|
|
|
@ -4,8 +4,8 @@ pipeline:
|
||||||
environment:
|
environment:
|
||||||
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
|
- DATABSE_URL=postgres://postgres:password@database:5432/postgres
|
||||||
- EMAIL_URL=smtp://admin:password@smtp:10025
|
- EMAIL_URL=smtp://admin:password@smtp:10025
|
||||||
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@gitea:22/hostea/
|
- HOSTEA_INFRA_HOSTEA_REPO_REMOTE=ssh://git@forgejo:22/hostea/
|
||||||
- HOSTEA_META_GITEA_INSTANCE=http://gitea:3000
|
- HOSTEA_META_FORGEJO_INSTANCE=http://forgejo:3000
|
||||||
commands:
|
commands:
|
||||||
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
- export HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY="$(realpath ./tests/fleet-deploy-key)"
|
||||||
- pip install virtualenv
|
- pip install virtualenv
|
||||||
|
@ -21,9 +21,9 @@ services:
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_PASSWORD=password
|
- POSTGRES_PASSWORD=password
|
||||||
|
|
||||||
gitea:
|
forgejo:
|
||||||
image: gitea/gitea:1.16.5
|
image: codeberg.org/forgejo/forgejo:1.18.0-1
|
||||||
container_name: hostea-dash-gitea
|
container_name: hostea-dash-forgejo
|
||||||
|
|
||||||
smtp:
|
smtp:
|
||||||
image: maildev/maildev:latest
|
image: maildev/maildev:latest
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
FROM python
|
FROM python
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source https://gitea.hostea.org/Hostea/dashboard
|
LABEL org.opencontainers.image.source https://forgejo.hostea.org/Hostea/dashboard
|
||||||
|
|
||||||
RUN useradd -ms /bin/bash -u 1001 hostea
|
RUN useradd -ms /bin/bash -u 1001 hostea
|
||||||
RUN apt-get update && apt-get install -y ca-certificates git
|
RUN apt-get update && apt-get install -y ca-certificates git
|
||||||
|
|
9
Makefile
|
@ -9,6 +9,7 @@ endef
|
||||||
|
|
||||||
default: ## Run app
|
default: ## Run app
|
||||||
$(call run_migrations)
|
$(call run_migrations)
|
||||||
|
. ./venv/bin/activate && yes yes | python manage.py collectstatic
|
||||||
. ./venv/bin/activate && python manage.py runserver
|
. ./venv/bin/activate && python manage.py runserver
|
||||||
|
|
||||||
coverage: ## Generate test coverage report
|
coverage: ## Generate test coverage report
|
||||||
|
@ -38,13 +39,7 @@ integration-test: ## run integration tests
|
||||||
. ./venv/bin/activate && integration/tests.sh
|
. ./venv/bin/activate && integration/tests.sh
|
||||||
|
|
||||||
lint: ## Run linter
|
lint: ## Run linter
|
||||||
@./venv/bin/black ./dashboard/
|
@./venv/bin/black dashboard accounts dash support billing infrastructure integration
|
||||||
@./venv/bin/black ./accounts/
|
|
||||||
@./venv/bin/black ./dash/
|
|
||||||
@./venv/bin/black ./support/
|
|
||||||
@./venv/bin/black ./billing/
|
|
||||||
@./venv/bin/black ./infrastructure/
|
|
||||||
@./venv/bin/black ./integration/
|
|
||||||
|
|
||||||
migrate: ## Run migrations
|
migrate: ## Run migrations
|
||||||
$(call run_migrations)
|
$(call run_migrations)
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
# Hostea dashboard
|
# Hostea dashboard
|
||||||
|
|
||||||
[![status-badge](https://woodpecker.hostea.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.hostea.org/Hostea/dashboard)
|
[![status-badge](https://woodpecker.gna.org/api/badges/Hostea/dashboard/status.svg)](https://woodpecker.gna.org/Hostea/dashboard)
|
||||||
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0)
|
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0)
|
||||||
[![Chat](https://img.shields.io/badge/matrix-hostea:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/#hostea:matrix.batsense.net)
|
[![Chat](https://img.shields.io/badge/matrix-hostea:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/#hostea:matrix.batsense.net)
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 4.0.3 on 2022-07-10 06:14
|
||||||
|
|
||||||
|
import accounts.utils
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
("accounts", "0005_accountconfirmchallenge_created_at"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="PasswordResetChallenge",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"public_ref",
|
||||||
|
models.CharField(
|
||||||
|
default=accounts.utils.gen_secret,
|
||||||
|
editable=False,
|
||||||
|
max_length=32,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Public referece to challenge text",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created_at", models.DateTimeField(auto_now_add=True)),
|
||||||
|
(
|
||||||
|
"challenge_text",
|
||||||
|
models.CharField(
|
||||||
|
default=accounts.utils.gen_secret,
|
||||||
|
editable=False,
|
||||||
|
max_length=32,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
unique=True,
|
||||||
|
verbose_name="Challenge text",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"owned_by",
|
||||||
|
models.OneToOneField(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
|
@ -52,3 +52,36 @@ class AccountConfirmChallenge(models.Model):
|
||||||
|
|
||||||
def pending_url(self):
|
def pending_url(self):
|
||||||
return reverse("accounts.verify.pending", args=(self.public_ref,))
|
return reverse("accounts.verify.pending", args=(self.public_ref,))
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordResetChallenge(models.Model):
|
||||||
|
owned_by = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||||
|
public_ref = models.CharField(
|
||||||
|
"Public referece to challenge text",
|
||||||
|
unique=True,
|
||||||
|
max_length=32,
|
||||||
|
default=gen_secret,
|
||||||
|
editable=False,
|
||||||
|
)
|
||||||
|
created_at = models.DateTimeField(auto_now_add=True, blank=True)
|
||||||
|
|
||||||
|
challenge_text = models.CharField(
|
||||||
|
"Challenge text",
|
||||||
|
unique=True,
|
||||||
|
max_length=32,
|
||||||
|
default=gen_secret,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{self.challenge_text}"
|
||||||
|
|
||||||
|
def verification_link(self):
|
||||||
|
"""
|
||||||
|
Get verification link
|
||||||
|
"""
|
||||||
|
return reverse("accounts.password.reset", args=(self.challenge_text,))
|
||||||
|
|
||||||
|
def pending_url(self):
|
||||||
|
return reverse("accounts.password.reset.resend", args=(self.public_ref,))
|
||||||
|
|
|
@ -1,24 +1,23 @@
|
||||||
{% extends "common/components/base.html" %}
|
{% extends "common/components/base.html" %}
|
||||||
{% block title %}{% block title_name %} {% endblock %} | Hostea Dashbaord{% endblock %}
|
{% block title %}{% block title_name %} {% endblock %} | Gna! Dashboard{% endblock %}
|
||||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<main class="auth__main">
|
<main class="auth__main">
|
||||||
<section class="main">
|
<section class="main">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<h1>Free Forge Ecosystem for Free Developers</h1>
|
<h1><a href="https://forgejo.org">Forgejo</a> hosting and <a href="/forgejo-clinic/">service</a></h1>
|
||||||
<p class="welcome">
|
<p class="welcome">
|
||||||
Hostea is a self-hostable libre software development suite comprising Gitea and Woodpecker CI with payments integration.
|
A free forge ecosystem for free developers.
|
||||||
</p>
|
</p>
|
||||||
<ul class="index-banner__features-list">
|
<ul class="index-banner__features-list">
|
||||||
<li class="index-banner__features">Fully managed</li>
|
<li class="index-banner__features">Dedicated <a href="https://hosteadashboard.gna.org/register/">Forgejo hosting</a> and <a href="https://woodpecker-ci.org/">Woodpecker CI</a> from 10€/month</li>
|
||||||
<li class="index-banner__features">100% Free Software</li>
|
<li class="index-banner__features">Clinic to <a href="https://gna.org/forgejo-clinic/">heal sick Forgejo</a> instances</li>
|
||||||
<li class="index-banner__features">Fully Self-Hostable</li>
|
<li class="index-banner__features">100% <a href="https://www.gnu.org/philosophy/free-sw.html">Free Software</a></li>
|
||||||
<li class="index-banner__features">Observable and reliable</li>
|
<li class="index-banner__features">Radically <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">Transparent</a></li>
|
||||||
<li class="index-banner__features">Federation when available</li>
|
<li class="index-banner__features">Run by a <a href="https://forum.gna.org/t/about-governance-and-decisions-in-hostea/55">horizontal collective</a></li>
|
||||||
<li class="index-banner__features">Radically transparent</li>
|
<li class="index-banner__features">25% of the income <a href="https://forum.gna.org/t/decision-revenue-sharing-model/92">dedicated to sustain Free Software dependencies</a></li>
|
||||||
<li class="index-banner__features">Horizontal community</li>
|
<li class="index-banner__features">Committed to <a href="https://forgefriends.org/blog/2022/06/30/2022-06-state-forge-federation/">further forge federation</a></li>
|
||||||
<li class="index-banner__features">Run Hostea and become a service provider!</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -34,12 +34,12 @@
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div class="form__action-container">
|
<div class="form__action-container">
|
||||||
<a href="/forgot-password">Forgot password?</a>
|
<a href="{% url 'accounts.password.reset.new' %}">Forgot password?</a>
|
||||||
<button class="form__submit" type="submit">Login</button>
|
<button class="form__submit" type="submit">Login</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<p class="form__alt-action">
|
<p class="form__alt-action">
|
||||||
New to Hostea?
|
New to Gna!?
|
||||||
<a href="{% url 'accounts.register' %}">Create an account</a>
|
<a href="{% url 'accounts.register' %}">Create an account</a>
|
||||||
</p>
|
</p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
{% extends "common/components/base.html" %}
|
||||||
|
{% block title %} Reset Password| Gna! Dashboard{% endblock %}
|
||||||
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="dialogue-box__container">
|
||||||
|
<h2>Reset password</h2>
|
||||||
|
<form
|
||||||
|
action="{% url 'accounts.password.reset.new' %}"
|
||||||
|
method="POST"
|
||||||
|
class="form"
|
||||||
|
accept-charset="utf-8"
|
||||||
|
>
|
||||||
|
{% include "common/components/error.html" %} {% csrf_token %}
|
||||||
|
<label class="form__label" for="email">
|
||||||
|
Email
|
||||||
|
<input
|
||||||
|
class="form__input"
|
||||||
|
name="email"
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="form__action-container">
|
||||||
|
<button class="form__submit" type="submit">Send Password Reset Link</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{% extends "common/components/base.html" %}
|
||||||
|
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
|
||||||
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="dialogue-box__container">
|
||||||
|
<h2>Reset password</h2>
|
||||||
|
<p>Verification link is sent to email address: {{email}}</p>
|
||||||
|
<form
|
||||||
|
action="{% url 'accounts.password.reset.resend' public_ref=public_ref %}"
|
||||||
|
method="POST"
|
||||||
|
class="form"
|
||||||
|
accept-charset="utf-8"
|
||||||
|
>
|
||||||
|
{% include "common/components/error.html" %} {% csrf_token %}
|
||||||
|
<div class="form__action-container">
|
||||||
|
<button class="form__submit" type="submit">Click here to resend email</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -0,0 +1,40 @@
|
||||||
|
{% extends "common/components/base.html" %}
|
||||||
|
{% block title %} Reset Password | Gna! Dashboard{% endblock %}
|
||||||
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="dialogue-box__container">
|
||||||
|
<h2>Reset Password</h2>
|
||||||
|
<form
|
||||||
|
action="{% url 'accounts.password.reset' challenge=challenge %}"
|
||||||
|
method="POST"
|
||||||
|
class="form"
|
||||||
|
accept-charset="utf-8"
|
||||||
|
>
|
||||||
|
{% include "common/components/error.html" %} {% csrf_token %}
|
||||||
|
<label class="form__label" for="password">
|
||||||
|
password
|
||||||
|
<input
|
||||||
|
class="form__input"
|
||||||
|
name="password"
|
||||||
|
required
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form__label" for="confirm_password">
|
||||||
|
Re-enter Password
|
||||||
|
<input
|
||||||
|
class="form__input"
|
||||||
|
name="confirm_password"
|
||||||
|
required
|
||||||
|
id="confirm_password"
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<div class="form__action-container">
|
||||||
|
<button class="form__submit" type="submit">Reset Password</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "common/components/base.html" %}
|
{% extends "common/components/base.html" %}
|
||||||
{% block title %} Confirm Access | Hostea Dashbaord{% endblock %}
|
{% block title %} Confirm Access | Gna! Dashboard{% endblock %}
|
||||||
{% block nav %} {% include "dash/common/components/primary-nav.html" %} {% endblock %}
|
{% block nav %} {% include "dash/common/components/primary-nav.html" %} {% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "common/components/base.html" %}
|
{% extends "common/components/base.html" %}
|
||||||
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
|
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
|
||||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="dialogue-box__container">
|
<div class="dialogue-box__container">
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% extends "common/components/base.html" %}
|
{% extends "common/components/base.html" %}
|
||||||
{% block title %} Confirm Account | Hostea Dashbaord{% endblock %}
|
{% block title %} Confirm Account | Gna! Dashboard{% endblock %}
|
||||||
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
{% block nav %} {% include "common/components/nav/pub.html" %} {% endblock %}
|
||||||
{% block main %}
|
{% block main %}
|
||||||
<div class="dialogue-box__container">
|
<div class="dialogue-box__container">
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
Hello {{ username }},
|
||||||
|
|
||||||
|
You have a new password!
|
||||||
|
|
||||||
|
Your password for signing in to Gna! was recently changed. If you made this change, then we're all set.
|
||||||
|
|
||||||
|
If you did not make this change, please reset your password to secure your account.
|
||||||
|
|
||||||
|
{% url 'accounts.password.reset.new' %}
|
||||||
|
|
||||||
|
Either way, feel free to reach out with any questions you might have. We're here to help.
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
Gna! team
|
|
@ -0,0 +1,9 @@
|
||||||
|
Hello {{ email }},
|
||||||
|
|
||||||
|
Please click on the link below to reset your password:
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
If you don't recognise this activity, please delete this mail.
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
Gna! team
|
|
@ -6,4 +6,4 @@ Please click on the link below to verify your email.
|
||||||
If you don't recognise this activity, please delete this mail.
|
If you don't recognise this activity, please delete this mail.
|
||||||
|
|
||||||
Cheers,
|
Cheers,
|
||||||
Hostea team
|
Gna! team
|
||||||
|
|
|
@ -10,21 +10,11 @@
|
||||||
title="RSS"
|
title="RSS"
|
||||||
>Home</a>
|
>Home</a>
|
||||||
<span class="footer__column-divider--mobile-visible">|</span>
|
<span class="footer__column-divider--mobile-visible">|</span>
|
||||||
<a class="license__link" rel="noreferrer" href="https://hostea.org/about" target="_blank"
|
<a class="license__link" rel="noreferrer" href="https://gna.org/about" target="_blank"
|
||||||
> About</a
|
> About</a
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer__column">
|
|
||||||
<a
|
|
||||||
class="license__link"
|
|
||||||
rel="noreferrer"
|
|
||||||
href="https://www.eff.org/issues/do-not-track/amp/"
|
|
||||||
target="_blank"
|
|
||||||
>No AMP</a
|
|
||||||
>
|
|
||||||
|
|
||||||
<div class="footer__column-divider">|</div>
|
|
||||||
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
|
<a href="mailto:{{ footer.admin_email }}" class="footer__link"
|
||||||
>Contact Instance Maintainer</a
|
>Contact Instance Maintainer</a
|
||||||
>
|
>
|
||||||
|
|
|
@ -3,11 +3,8 @@
|
||||||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||||
<div class="nav__header">
|
<div class="nav__header">
|
||||||
<a class="nav__logo-container" href="/">
|
<a class="nav__logo-container" href="/">
|
||||||
<img src="{% static 'img/android-icon-48x48.png' %}"
|
<img class="nav__logo-img" src="{% static 'img/logo.png' %}"
|
||||||
alt="Hostea temporary logo"/>
|
alt="Gna! logo"/>
|
||||||
<p class="nav__home-btn">
|
|
||||||
ostea
|
|
||||||
</p>
|
|
||||||
</a>
|
</a>
|
||||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||||
<span class="nav__hamburger-inner"></span>
|
<span class="nav__hamburger-inner"></span>
|
||||||
|
|
|
@ -33,7 +33,7 @@ from django.conf import settings
|
||||||
|
|
||||||
from oauth2_provider.models import get_application_model
|
from oauth2_provider.models import get_application_model
|
||||||
|
|
||||||
from .models import AccountConfirmChallenge
|
from .models import AccountConfirmChallenge, PasswordResetChallenge
|
||||||
from .management.commands.rm_unverified_users import (
|
from .management.commands.rm_unverified_users import (
|
||||||
Command as CleanUnverifiedUsersCommand,
|
Command as CleanUnverifiedUsersCommand,
|
||||||
)
|
)
|
||||||
|
@ -77,7 +77,9 @@ class LoginTest(TestCase):
|
||||||
Tests if login template renders
|
Tests if login template renders
|
||||||
"""
|
"""
|
||||||
resp = self.client.get(reverse("accounts.login"))
|
resp = self.client.get(reverse("accounts.login"))
|
||||||
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
|
self.assertEqual(
|
||||||
|
b"A free forge ecosystem for free developers" in resp.content, True
|
||||||
|
)
|
||||||
|
|
||||||
def test_login_works(self):
|
def test_login_works(self):
|
||||||
"""
|
"""
|
||||||
|
@ -158,6 +160,71 @@ class LoginTest(TestCase):
|
||||||
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
self.assertEqual(resp.headers["location"], reverse("dash.instances.list"))
|
||||||
|
|
||||||
|
|
||||||
|
class ResetPasswordTest(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.username = "reset_password_user"
|
||||||
|
register_util(t=self, username=self.username)
|
||||||
|
|
||||||
|
def reset_password(self):
|
||||||
|
c = Client()
|
||||||
|
payload = {
|
||||||
|
"email": self.email,
|
||||||
|
}
|
||||||
|
resp = c.get(reverse("accounts.password.reset.new"))
|
||||||
|
self.assertEqual(resp.status_code == 200)
|
||||||
|
|
||||||
|
resp = c.post(reverse("accounts.password.reset.new"), payload)
|
||||||
|
self.assertEqual(resp.status_code == 302)
|
||||||
|
challenge = PasswordResetChallenge.objects.filter(owned_by=self.user)
|
||||||
|
self.assertEqual(resp.headers["location"] == challenge.pending_url(), True)
|
||||||
|
|
||||||
|
password_reset_mail = mail.outbox.pop()
|
||||||
|
self.assertEqual("reset your password" in password_reset_mail, True)
|
||||||
|
self.assertEqual(challenge.verification_link() in password_reset_mail, True)
|
||||||
|
|
||||||
|
resp = c.get(self.challenge.verification_link())
|
||||||
|
self.assertEqual(resp.status_code == 200)
|
||||||
|
|
||||||
|
new_password = "newpasdasdf234234"
|
||||||
|
|
||||||
|
# passwords don't match
|
||||||
|
payload = {
|
||||||
|
"password": new_password,
|
||||||
|
"confirm_password": self.password,
|
||||||
|
}
|
||||||
|
resp = c.post(self.challenge.verification_link(), payload)
|
||||||
|
self.assertEqual(resp.status_code == 400)
|
||||||
|
|
||||||
|
# change password
|
||||||
|
payload["confirm_password"] = new_password
|
||||||
|
resp = c.post(self.challenge.verification_link(), payload)
|
||||||
|
self.assertEqual(resp.status_code == 302)
|
||||||
|
self.assertEqual(resp.headers["location"], reverse("accounts.login"))
|
||||||
|
|
||||||
|
# verify password changed notification email was sent
|
||||||
|
password_updated_email = mail.outbox.pop()
|
||||||
|
self.assertEqual(
|
||||||
|
"Your password for signing in to Hostea was recently changed. If you made this change, then we're all set."
|
||||||
|
in password_updated_email,
|
||||||
|
True,
|
||||||
|
)
|
||||||
|
self.assertEqual(reverse("accounts.reset.new") in password_updated_email, True)
|
||||||
|
|
||||||
|
# trying to login with old password
|
||||||
|
payload = {
|
||||||
|
"login": self.username,
|
||||||
|
"password": self.password,
|
||||||
|
}
|
||||||
|
resp = self.client.post(reverse("accounts.login"), payload)
|
||||||
|
self.assertEqual(resp.status_code, 401)
|
||||||
|
self.assertEqual(b"Login Failed" in resp.content, True)
|
||||||
|
|
||||||
|
payload["password"] = new_password
|
||||||
|
resp = c.post(reverse("accounts.login"), payload)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(resp.headers["location"], reverse("accounts.home"))
|
||||||
|
|
||||||
|
|
||||||
class RegistrationTest(TestCase):
|
class RegistrationTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.username = "register_user"
|
self.username = "register_user"
|
||||||
|
@ -169,7 +236,9 @@ class RegistrationTest(TestCase):
|
||||||
Tests if register template renders
|
Tests if register template renders
|
||||||
"""
|
"""
|
||||||
resp = self.client.get(reverse("accounts.register"))
|
resp = self.client.get(reverse("accounts.register"))
|
||||||
self.assertEqual(b"Free Forge Ecosystem" in resp.content, True)
|
self.assertEqual(
|
||||||
|
b"A free forge ecosystem for free developers." in resp.content, True
|
||||||
|
)
|
||||||
|
|
||||||
def test_register_works(self):
|
def test_register_works(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -25,6 +25,9 @@ from .views import (
|
||||||
resend_verification_email_view,
|
resend_verification_email_view,
|
||||||
verification_pending_view,
|
verification_pending_view,
|
||||||
sudo,
|
sudo,
|
||||||
|
password_reset_send_verificaiton_link,
|
||||||
|
password_resend_verification_link_pending,
|
||||||
|
reset_password,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -44,5 +47,20 @@ urlpatterns = [
|
||||||
),
|
),
|
||||||
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
|
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
|
||||||
path("accounts/sudo/", sudo, name="accounts.sudo"),
|
path("accounts/sudo/", sudo, name="accounts.sudo"),
|
||||||
|
path(
|
||||||
|
"accounts/password/reset/challenge/",
|
||||||
|
password_reset_send_verificaiton_link,
|
||||||
|
name="accounts.password.reset.new",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"accounts/password/reset/<str:challenge>/",
|
||||||
|
reset_password,
|
||||||
|
name="accounts.password.reset",
|
||||||
|
),
|
||||||
|
path(
|
||||||
|
"accounts/password/reset/challenge/<str:public_ref>/",
|
||||||
|
password_resend_verification_link_pending,
|
||||||
|
name="accounts.password.reset.resend",
|
||||||
|
),
|
||||||
path("", protected_view, name="accounts.home"),
|
path("", protected_view, name="accounts.home"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -30,6 +30,55 @@ def gen_secret() -> str:
|
||||||
return get_random_string(32)
|
return get_random_string(32)
|
||||||
|
|
||||||
|
|
||||||
|
def send_password_changed_email(request):
|
||||||
|
ctx = {
|
||||||
|
"username": request.user.username,
|
||||||
|
}
|
||||||
|
|
||||||
|
body = render_to_string(
|
||||||
|
"accounts/emails/password-changed.txt",
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = request.user.email
|
||||||
|
|
||||||
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject="[Gna!] Password changed",
|
||||||
|
message=body,
|
||||||
|
from_email=f"No reply Gna!<{sender}>",
|
||||||
|
recipient_list=[email],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_password_reset_email(request, challenge):
|
||||||
|
verification_link = (
|
||||||
|
f"{request.scheme}://{request.get_host()}{challenge.verification_link()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = {
|
||||||
|
"link": verification_link,
|
||||||
|
"email": challenge.owned_by.email,
|
||||||
|
}
|
||||||
|
|
||||||
|
body = render_to_string(
|
||||||
|
"accounts/emails/password-reset-link.txt",
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = challenge.owned_by.email
|
||||||
|
|
||||||
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject="[Gna!] Password reset link",
|
||||||
|
message=body,
|
||||||
|
from_email=f"No reply Gna!<{sender}>",
|
||||||
|
recipient_list=[email],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def send_verification_email(request, challenge):
|
def send_verification_email(request, challenge):
|
||||||
verification_link = (
|
verification_link = (
|
||||||
f"{request.scheme}://{request.get_host()}{challenge.verification_link()}"
|
f"{request.scheme}://{request.get_host()}{challenge.verification_link()}"
|
||||||
|
@ -50,9 +99,9 @@ def send_verification_email(request, challenge):
|
||||||
sender = settings.DEFAULT_FROM_EMAIL
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
send_mail(
|
send_mail(
|
||||||
subject="[Hostea] Please confirm your email address",
|
subject="[Gna!] Please confirm your email address",
|
||||||
message=body,
|
message=body,
|
||||||
from_email=f"No reply Hostea<{sender}>", # TODO read from settings.py
|
from_email=f"No reply Gna!<{sender}>",
|
||||||
recipient_list=[email],
|
recipient_list=[email],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -25,8 +25,8 @@ from django.urls import reverse
|
||||||
|
|
||||||
from dash.utils import footer_ctx
|
from dash.utils import footer_ctx
|
||||||
|
|
||||||
from .models import AccountConfirmChallenge
|
from .models import AccountConfirmChallenge, PasswordResetChallenge
|
||||||
from .utils import send_verification_email, ConfirmAccess
|
from .utils import send_verification_email, ConfirmAccess, send_password_reset_email
|
||||||
from .decorators import redirect_if_authenticated
|
from .decorators import redirect_if_authenticated
|
||||||
|
|
||||||
|
|
||||||
|
@ -242,3 +242,107 @@ def sudo(request):
|
||||||
|
|
||||||
ConfirmAccess.set(request=request)
|
ConfirmAccess.set(request=request)
|
||||||
return redirect(request.POST["next"])
|
return redirect(request.POST["next"])
|
||||||
|
|
||||||
|
|
||||||
|
@redirect_if_authenticated
|
||||||
|
@csrf_protect
|
||||||
|
def password_reset_send_verificaiton_link(request):
|
||||||
|
def default_ctx():
|
||||||
|
return {
|
||||||
|
"title": "Reset Password",
|
||||||
|
"footer": footer_ctx(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
ctx = default_ctx()
|
||||||
|
return render(request, "accounts/auth/password-reset-form.html", ctx)
|
||||||
|
|
||||||
|
challenge = None
|
||||||
|
|
||||||
|
email = request.POST["email"]
|
||||||
|
User = get_user_model()
|
||||||
|
user = get_object_or_404(User, email=email)
|
||||||
|
if not PasswordResetChallenge.objects.filter(owned_by=user).exists():
|
||||||
|
challenge = PasswordResetChallenge(owned_by=user)
|
||||||
|
challenge.save()
|
||||||
|
send_password_reset_email(request, challenge=challenge)
|
||||||
|
else:
|
||||||
|
challenge = PasswordResetChallenge.objects.get(owned_by=user)
|
||||||
|
return redirect(challenge.pending_url())
|
||||||
|
|
||||||
|
|
||||||
|
@redirect_if_authenticated
|
||||||
|
@csrf_protect
|
||||||
|
def password_resend_verification_link_pending(request, public_ref):
|
||||||
|
challenge = get_object_or_404(PasswordResetChallenge, public_ref=public_ref)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
ctx = {
|
||||||
|
"email": challenge.owned_by.email,
|
||||||
|
"public_ref": challenge.public_ref,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request,
|
||||||
|
"accounts/auth/password-reset-resend-verification.html",
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
send_password_reset_email(request, challenge=challenge)
|
||||||
|
ctx = {
|
||||||
|
"email": challenge.owned_by.email,
|
||||||
|
"public_ref": challenge.public_ref,
|
||||||
|
}
|
||||||
|
|
||||||
|
return render(
|
||||||
|
request, "accounts/auth/password-reset-resend-verification.html", context=ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@csrf_protect
|
||||||
|
def reset_password(request, challenge):
|
||||||
|
def default_ctx(challenge):
|
||||||
|
return {
|
||||||
|
"title": "Reset Password",
|
||||||
|
"footer": footer_ctx(),
|
||||||
|
"challenge": challenge,
|
||||||
|
}
|
||||||
|
|
||||||
|
challenge = get_object_or_404(PasswordResetChallenge, challenge_text=challenge)
|
||||||
|
|
||||||
|
if request.method == "GET":
|
||||||
|
ctx = default_ctx(challenge=challenge)
|
||||||
|
return render(request, "accounts/auth/password-reset.html", ctx)
|
||||||
|
|
||||||
|
confirm_password = request.POST["confirm_password"]
|
||||||
|
password = request.POST["password"]
|
||||||
|
|
||||||
|
if password != confirm_password:
|
||||||
|
ctx = default_ctx(challenge=challenge)
|
||||||
|
ctx["error"] = {
|
||||||
|
"title": "Reset Password Failed",
|
||||||
|
"reason": "Passwords don't match, please verify input",
|
||||||
|
}
|
||||||
|
return render(
|
||||||
|
request, "accounts/auth/password-reset.html", status=400, context=ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
user = challenge.owned_by
|
||||||
|
try:
|
||||||
|
validate_password(password, user=user)
|
||||||
|
except ValidationError as err:
|
||||||
|
ctx = default_ctx(challenge=challenge)
|
||||||
|
reason = ""
|
||||||
|
for r in err:
|
||||||
|
reason += r + " "
|
||||||
|
|
||||||
|
ctx["error"] = {"title": "Reset Password Failed", "reason": reason}
|
||||||
|
return render(
|
||||||
|
request, "accounts/auth/password-reset.html", status=400, context=ctx
|
||||||
|
)
|
||||||
|
|
||||||
|
user.set_password(password)
|
||||||
|
user.save()
|
||||||
|
challenge.delete()
|
||||||
|
send_password_reset_email(request)
|
||||||
|
return redirect("accounts.login")
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.core.mail import send_mail
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
|
|
||||||
from oauth2_provider.models import get_application_model
|
from oauth2_provider.models import get_application_model
|
||||||
|
@ -22,7 +24,8 @@ from oauth2_provider.generators import generate_client_id, generate_client_secre
|
||||||
|
|
||||||
from accounts.utils import gen_secret
|
from accounts.utils import gen_secret
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
from billing.utils import generate_invoice, payment_fullfilled
|
from infrastructure.models import InstanceCreated
|
||||||
|
from billing.utils import generate_invoice, payment_fullfilled, get_invoice_link
|
||||||
|
|
||||||
Application = get_application_model()
|
Application = get_application_model()
|
||||||
|
|
||||||
|
@ -33,10 +36,34 @@ class Command(BaseCommand):
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
instances = Instance.objects.all()
|
instances = Instance.objects.all()
|
||||||
if instances:
|
if instances:
|
||||||
for instance in Instance.objects.all():
|
for paid_instance in InstanceCreated.objects.all():
|
||||||
self.stdout.write(f"Found instance: {instance}")
|
self.stdout.write(f"Found instance: {paid_instance.instance}")
|
||||||
if not payment_fullfilled(instance=instance):
|
if not payment_fullfilled(instance=paid_instance.instance):
|
||||||
self.stdout.write(f"Payment not fulfilled for instance: {instance}")
|
self.stdout.write(
|
||||||
payment = generate_invoice(instance=instance)
|
f"Payment not fulfilled for instance: {paid_instance.instance}"
|
||||||
|
)
|
||||||
|
payment = generate_invoice(instance=paid_instance.instance)
|
||||||
|
owner = paid_instance.instance.owned_by
|
||||||
|
ctx = {
|
||||||
|
"username": owner.username,
|
||||||
|
"payment": payment,
|
||||||
|
"link": get_invoice_link(payment=payment),
|
||||||
|
}
|
||||||
|
|
||||||
|
body = render_to_string(
|
||||||
|
"billing/emails/renew-subscription.txt",
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
email = owner.email
|
||||||
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject="[Gna!] Payment receipt for your Gna! VM",
|
||||||
|
message=body,
|
||||||
|
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
|
||||||
|
recipient_list=[email],
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.stdout.write("No instances available")
|
self.stdout.write("No instances available")
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 4.0.3 on 2022-07-08 13:52
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("billing", "0004_payment_billing_pay_paid_by_77f57c_idx"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="payment",
|
||||||
|
name="vm_deleted",
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
|
@ -67,6 +67,8 @@ class Payment(BasePayment):
|
||||||
date = models.DateTimeField(auto_now_add=True, blank=True)
|
date = models.DateTimeField(auto_now_add=True, blank=True)
|
||||||
objects = PaymentModelManager()
|
objects = PaymentModelManager()
|
||||||
|
|
||||||
|
vm_deleted = models.BooleanField(default=False, null=False)
|
||||||
|
|
||||||
def get_failure_url(self) -> str:
|
def get_failure_url(self) -> str:
|
||||||
url = urlparse(settings.PAYMENT_HOST)
|
url = urlparse(settings.PAYMENT_HOST)
|
||||||
return urlunparse(
|
return urlunparse(
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
Hello {{ username }}!
|
Hello {{ username }}!
|
||||||
|
|
||||||
An invoice is generated for your Hostea VM {{ payment.instance_name }}.
|
An invoice is generated for your Gna! VM {{ payment.instance_name }}.
|
||||||
|
|
||||||
- Configuration: {{payment.instance_configuration_id.name}}
|
- Configuration: {{payment.instance_configuration_id.name}}
|
||||||
- Invoice generated on: {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
|
- Invoice generated on: {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
|
||||||
|
@ -11,4 +11,4 @@ To pay, please click the link below:
|
||||||
{{ link }}
|
{{ link }}
|
||||||
|
|
||||||
Cheers,
|
Cheers,
|
||||||
Hostea team
|
Gna! team
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
Hello {{ username }}!
|
Hello {{ username }}!
|
||||||
|
|
||||||
|
|
||||||
This is a receipt for your latest Hostea payment.
|
This is a receipt for your latest Gna! payment.
|
||||||
|
|
||||||
-----------------------------------------------------
|
-----------------------------------------------------
|
||||||
|
|
||||||
Hostea Receipt - {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
|
Gna! Receipt - {{payment.date.month}}/{{payment.date.day}}/{{payment.date.year}}
|
||||||
|
|
||||||
- Instance Name: {{ payment.instance_name }}
|
- Instance Name: {{ payment.instance_name }}
|
||||||
- Configuration: {{payment.instance_configuration_id.name}}
|
- Configuration: {{payment.instance_configuration_id.name}}
|
||||||
|
@ -19,4 +19,4 @@ To view the receipt online, please see the following link:
|
||||||
We appreciate your business!
|
We appreciate your business!
|
||||||
|
|
||||||
Cheers,
|
Cheers,
|
||||||
Hostea team
|
Gna! team
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
Hello {{ username }}!
|
||||||
|
|
||||||
|
Your Gna! VM subscription is due for renewal. Please click the link
|
||||||
|
below to renew your subscription:
|
||||||
|
|
||||||
|
{{link}}
|
||||||
|
|
||||||
|
-----------------------------------------------------
|
||||||
|
|
||||||
|
- Instance Name: {{ payment.instance_name }}
|
||||||
|
- Configuration: {{payment.instance_configuration_id.name}}
|
||||||
|
- Total Amount: {{payment.total}} {{payment.currency|upper}}
|
||||||
|
|
||||||
|
We appreciate your business!
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
Gna! team
|
232
billing/tests.py
|
@ -35,107 +35,97 @@ from .models import Payment
|
||||||
from .utils import payment_fullfilled
|
from .utils import payment_fullfilled
|
||||||
|
|
||||||
|
|
||||||
# class BillingTest(TestCase):
|
class BillingTest(TestCase):
|
||||||
# """
|
"""
|
||||||
# Tests billing system
|
Tests billing system
|
||||||
# """
|
"""
|
||||||
#
|
|
||||||
# def setUp(self):
|
def setUp(self):
|
||||||
# self.username = "billing_user"
|
self.username = "billing_user"
|
||||||
# register_util(t=self, username=self.username)
|
register_util(t=self, username=self.username)
|
||||||
# create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
#
|
|
||||||
# @override_settings(HOSTEA=infra_custom_config(test_name="test_payments"))
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_payments"))
|
||||||
# def test_payments(self):
|
def test_payments(self):
|
||||||
# c = Client()
|
c = Client()
|
||||||
# login_util(self, c, "accounts.home")
|
login_util(self, c, "accounts.home")
|
||||||
# instance_name = "test_payments"
|
instance_name = "tpayments"
|
||||||
# create_instance_util(
|
create_instance_util(
|
||||||
# t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# instance = Instance.objects.get(name=instance_name)
|
instance = Instance.objects.get(name=instance_name)
|
||||||
#
|
|
||||||
# self.assertEqual(payment_fullfilled(instance=instance), True)
|
self.assertEqual(payment_fullfilled(instance=instance), True)
|
||||||
#
|
|
||||||
# payment = get_payment_model().objects.get(paid_by=self.user)
|
payment = get_payment_model().objects.get(paid_by=self.user)
|
||||||
# invoice_uri = reverse("billing.invoice.details", args=(payment.public_ref,))
|
invoice_uri = reverse("billing.invoice.details", args=(payment.public_ref,))
|
||||||
#
|
|
||||||
# # check if paid invoice is listed in paid invoice list view
|
# check if paid invoice is listed in paid invoice list view
|
||||||
# resp = c.get(reverse("billing.invoice.paid"))
|
resp = c.get(reverse("billing.invoice.paid"))
|
||||||
# self.assertEqual(str.encode(invoice_uri) in resp.content, True)
|
self.assertEqual(str.encode(invoice_uri) in resp.content, True)
|
||||||
#
|
|
||||||
# # check if the paid invoice is displayed in the pending invoice view, should not be displayed
|
# check if the paid invoice is displayed in the pending invoice view, should not be displayed
|
||||||
# resp = c.get(reverse("billing.invoice.pending"))
|
resp = c.get(reverse("billing.invoice.pending"))
|
||||||
# self.assertEqual(str.encode(invoice_uri) in resp.content, False)
|
self.assertEqual(str.encode(invoice_uri) in resp.content, False)
|
||||||
#
|
|
||||||
# # check if the invoice details view is rendering paid invoice version
|
# check if the invoice details view is rendering paid invoice version
|
||||||
# resp = c.get(invoice_uri)
|
resp = c.get(invoice_uri)
|
||||||
# self.assertEqual(str.encode(instance_name) in resp.content, True)
|
self.assertEqual(str.encode(instance_name) in resp.content, True)
|
||||||
# self.assertEqual(
|
self.assertEqual(
|
||||||
# str.encode(str(self.instance_config[0].rent)) in resp.content, True
|
str.encode(str(self.instance_config[0].rent)) in resp.content, True
|
||||||
# )
|
)
|
||||||
# self.assertEqual(str.encode("Paid on") in resp.content, True)
|
self.assertEqual(str.encode("Paid on") in resp.content, True)
|
||||||
#
|
|
||||||
# # try to generate an invoice for the second time on the same VM
|
# try to generate an invoice for the second time on the same VM
|
||||||
# # shouldn't be possible since payment is already made for the duration
|
# shouldn't be possible since payment is already made for the duration
|
||||||
# payment_uri = reverse("billing.invoice.generate", args=(instance.name,))
|
payment_uri = reverse("billing.invoice.generate", args=(instance.name,))
|
||||||
# resp = c.get(payment_uri)
|
resp = c.get(payment_uri)
|
||||||
# self.assertEqual(resp.status_code, 400)
|
self.assertEqual(resp.status_code, 400)
|
||||||
#
|
|
||||||
# ## payment success page; no real functionality but user is redirected here
|
## payment success page; no real functionality but user is redirected here
|
||||||
# # by stripe if payment is successful
|
# by stripe if payment is successful
|
||||||
# resp = c.get(reverse("billing.invoice.success", args=(payment.public_ref,)))
|
resp = c.get(reverse("billing.invoice.success", args=(payment.public_ref,)))
|
||||||
# self.assertEqual(
|
self.assertEqual(
|
||||||
# resp.headers["Location"],
|
resp.headers["Location"],
|
||||||
# reverse("infra.create", args=(payment.instance_name,)),
|
reverse("infra.create", args=(payment.instance_name,)),
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# # create_instance_util creates an instance and pays for it. An email is
|
# create_instance_util creates an instance and pays for it. An email is
|
||||||
# # sent when the invoice is generated and one after payment is made
|
# sent when the invoice is generated and one after payment is made
|
||||||
# #
|
#
|
||||||
# # So we are first checking for the last email that was sent(receipt)
|
# So we are first checking for the last email that was sent(receipt)
|
||||||
# # and then the Gitea instance credentials notification followed by the
|
# and then the Forgejo instance credentials notification followed by the
|
||||||
# # invoice generation email.
|
# invoice generation email.
|
||||||
# receipt_mail = mail.outbox.pop()
|
receipt_mail = mail.outbox.pop()
|
||||||
# self.assertEqual(
|
print(receipt_mail.body)
|
||||||
# all(
|
self.assertEqual(
|
||||||
# [
|
all(
|
||||||
# receipt_mail.to[0] == self.email,
|
[
|
||||||
# "This is a receipt for your latest Hostea payment"
|
receipt_mail.to[0] == self.email,
|
||||||
# in receipt_mail.body,
|
"This is a receipt for your latest Gna! payment"
|
||||||
# ]
|
in receipt_mail.body,
|
||||||
# ),
|
]
|
||||||
# True,
|
),
|
||||||
# )
|
True,
|
||||||
#
|
)
|
||||||
# instance_notificaiton = mail.outbox.pop()
|
|
||||||
# self.assertEqual(
|
instance_notificaiton = mail.outbox.pop()
|
||||||
# all(
|
self.assertEqual(
|
||||||
# [
|
all(
|
||||||
# instance_notificaiton.to[0] == self.email,
|
[
|
||||||
# "Congratulations on your new Gitea instance!"
|
instance_notificaiton.to[0] == self.email,
|
||||||
# in instance_notificaiton.body,
|
"Congratulations on your new Gna! instance!"
|
||||||
# ]
|
in instance_notificaiton.body,
|
||||||
# ),
|
]
|
||||||
# True,
|
),
|
||||||
# )
|
True,
|
||||||
#
|
)
|
||||||
# invoice_generated_mail = mail.outbox.pop()
|
|
||||||
# self.assertEqual(
|
## payment failure page; no real functionality but user is redirected here
|
||||||
# all(
|
# by stripe if payment is successful
|
||||||
# [
|
resp = c.get(reverse("billing.invoice.fail", args=(payment.public_ref,)))
|
||||||
# invoice_generated_mail.to[0] == self.email,
|
self.assertEqual(b"failed" in resp.content, True)
|
||||||
# "An invoice is generated" in invoice_generated_mail.body,
|
|
||||||
# ]
|
|
||||||
# ),
|
|
||||||
# True,
|
|
||||||
# )
|
|
||||||
#
|
|
||||||
# ## payment failure page; no real functionality but user is redirected here
|
|
||||||
# # by stripe if payment is successful
|
|
||||||
# resp = c.get(reverse("billing.invoice.fail", args=(payment.public_ref,)))
|
|
||||||
# self.assertEqual(b"failed" in resp.content, True)
|
|
||||||
|
|
||||||
|
|
||||||
class GenerateInvoiceCommand(TestCase):
|
class GenerateInvoiceCommand(TestCase):
|
||||||
|
@ -148,13 +138,51 @@ class GenerateInvoiceCommand(TestCase):
|
||||||
register_util(t=self, username=self.username)
|
register_util(t=self, username=self.username)
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
HOSTEA=infra_custom_config(
|
||||||
|
test_name="test_dont_send_invoices_to_not_created_vms"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
def test_dont_send_invoices_to_not_created_vms(self):
|
||||||
|
|
||||||
|
c = Client()
|
||||||
|
login_util(self, c, "accounts.home")
|
||||||
|
|
||||||
|
instance_name = "tnoinvonocrevm"
|
||||||
|
|
||||||
|
payload = {"name": instance_name, "configuration": self.instance_config[0].name}
|
||||||
|
|
||||||
|
resp = c.post(reverse("dash.instances.new"), payload)
|
||||||
|
self.assertEqual(resp.status_code, 302)
|
||||||
|
self.assertEqual(
|
||||||
|
resp.headers["location"],
|
||||||
|
reverse("billing.invoice.generate", args=(instance_name,)),
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = StringIO()
|
||||||
|
stderr = StringIO()
|
||||||
|
|
||||||
|
instance = Instance.objects.get(name=instance_name)
|
||||||
|
self.assertEqual(payment_fullfilled(instance=instance), False)
|
||||||
|
prev_len = len(mail.outbox)
|
||||||
|
|
||||||
|
# username exists
|
||||||
|
call_command(
|
||||||
|
"generate_invoice",
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
|
)
|
||||||
|
out = stdout.getvalue()
|
||||||
|
print(out)
|
||||||
|
self.assertEqual(instance_name not in out, True)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
HOSTEA=infra_custom_config(test_name="test_generate_invoice_cmd")
|
HOSTEA=infra_custom_config(test_name="test_generate_invoice_cmd")
|
||||||
)
|
)
|
||||||
def test_cmd(self):
|
def test_cmd(self):
|
||||||
c = Client()
|
c = Client()
|
||||||
login_util(self, c, "accounts.home")
|
login_util(self, c, "accounts.home")
|
||||||
instance_name = "test_generate_invoice_cmd"
|
instance_name = "tgeninvmd"
|
||||||
create_instance_util(
|
create_instance_util(
|
||||||
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||||
)
|
)
|
||||||
|
|
|
@ -34,16 +34,20 @@ def __get_delta():
|
||||||
delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month
|
delta = now - timedelta(seconds=(60 * 60 * 24 * 30)) # one month
|
||||||
return delta
|
return delta
|
||||||
|
|
||||||
|
|
||||||
def get_invoice_link(payment: Payment):
|
def get_invoice_link(payment: Payment):
|
||||||
invoice_link = reverse("billing.invoice.details", args=(payment.public_ref,))
|
invoice_link = reverse("billing.invoice.details", args=(payment.public_ref,))
|
||||||
parsed = urlparse(settings.PAYMENT_HOST)
|
parsed = urlparse(settings.PAYMENT_HOST)
|
||||||
return urlunparse((parsed.scheme, parsed.netloc, invoice_link, "", "", ""))
|
return urlunparse((parsed.scheme, parsed.netloc, invoice_link, "", "", ""))
|
||||||
|
|
||||||
|
|
||||||
def payment_fullfilled(instance: Instance) -> bool:
|
def payment_fullfilled(instance: Instance) -> bool:
|
||||||
delta = __get_delta()
|
delta = __get_delta()
|
||||||
|
|
||||||
payment = None
|
payment = None
|
||||||
for p in Payment.objects.filter(date__gt=(delta), instance_name=instance.name):
|
for p in Payment.objects.filter(
|
||||||
|
date__gt=(delta), instance_name=instance.name, vm_deleted=False
|
||||||
|
):
|
||||||
if p.status == PaymentStatus.CONFIRMED:
|
if p.status == PaymentStatus.CONFIRMED:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -75,7 +79,9 @@ def generate_invoice(instance: Instance) -> Payment:
|
||||||
delta = __get_delta()
|
delta = __get_delta()
|
||||||
|
|
||||||
payment = None
|
payment = None
|
||||||
for p in Payment.objects.filter(date__gt=(delta), instance_name=instance.name):
|
for p in Payment.objects.filter(
|
||||||
|
date__gt=(delta), instance_name=instance.name, vm_deleted=False
|
||||||
|
):
|
||||||
if p.status == PaymentStatus.CONFIRMED:
|
if p.status == PaymentStatus.CONFIRMED:
|
||||||
raise GenerateInvoiceException(code=GenerateInvoiceErrorCode.ALREADY_PAID)
|
raise GenerateInvoiceException(code=GenerateInvoiceErrorCode.ALREADY_PAID)
|
||||||
if any([p.status == PaymentStatus.INPUT, p.status == PaymentStatus.WAITING]):
|
if any([p.status == PaymentStatus.INPUT, p.status == PaymentStatus.WAITING]):
|
||||||
|
@ -92,28 +98,4 @@ def generate_invoice(instance: Instance) -> Payment:
|
||||||
instance=instance,
|
instance=instance,
|
||||||
)
|
)
|
||||||
|
|
||||||
invoice_link = get_invoice_link(payment=payment)
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
"username": instance.owned_by.username,
|
|
||||||
"link": invoice_link,
|
|
||||||
"payment": payment,
|
|
||||||
}
|
|
||||||
|
|
||||||
body = render_to_string(
|
|
||||||
"billing/emails/payment-notification.txt",
|
|
||||||
context=ctx,
|
|
||||||
)
|
|
||||||
|
|
||||||
email = instance.owned_by.email
|
|
||||||
sender = settings.DEFAULT_FROM_EMAIL
|
|
||||||
|
|
||||||
|
|
||||||
send_mail(
|
|
||||||
subject="[Hostea] An invoice is generated for your Hostea VM",
|
|
||||||
message=body,
|
|
||||||
from_email=f"No reply Hostea<{sender}>", # TODO read from settings.py
|
|
||||||
recipient_list=[email],
|
|
||||||
)
|
|
||||||
|
|
||||||
return payment
|
return payment
|
||||||
|
|
|
@ -33,7 +33,7 @@ from .utils import (
|
||||||
generate_invoice as generate_invoice_util,
|
generate_invoice as generate_invoice_util,
|
||||||
GenerateInvoiceErrorCode,
|
GenerateInvoiceErrorCode,
|
||||||
GenerateInvoiceException,
|
GenerateInvoiceException,
|
||||||
get_invoice_link
|
get_invoice_link,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -128,9 +128,9 @@ def payment_success(request, payment_public_id):
|
||||||
sender = settings.DEFAULT_FROM_EMAIL
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
send_mail(
|
send_mail(
|
||||||
subject="[Hostea] Payment receipt your Hostea VM",
|
subject="[Gna!] Payment receipt for your Gna! VM",
|
||||||
message=body,
|
message=body,
|
||||||
from_email=f"No reply Hostea<{sender}>", # TODO read from settings.py
|
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
|
||||||
recipient_list=[email],
|
recipient_list=[email],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,10 @@ header {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav__logo-img {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
.nav__toggle {
|
.nav__toggle {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 838 B After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 912 B After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 7.1 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 941 B After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 961 B After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 1003 B After Width: | Height: | Size: 4.0 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 4.3 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 711 B After Width: | Height: | Size: 955 B |
Before Width: | Height: | Size: 829 B After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 3.7 KiB |
|
@ -4,7 +4,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
|
<link rel="stylesheet" href="{% static 'css/main.css' %}" />
|
||||||
<title>{{ title }}| Hostea Dashbaord</title>
|
<title>{{ title }}| Gna! Dashboard</title>
|
||||||
{% include "common/components/meta.html" %}
|
{% include "common/components/meta.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,36 +1,41 @@
|
||||||
{% load static %}
|
{% load static %}
|
||||||
<nav class="nav__container">
|
<nav class="nav__container">
|
||||||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||||
<div class="nav__header">
|
<div class="nav__header">
|
||||||
<a class="nav__logo-container" href="/">
|
<a class="nav__logo-container" href="https://gna.org">
|
||||||
<img
|
<img alt="Gna! logo" class="nav__logo-img" src="{% static 'img/logo.png' %}" />
|
||||||
src="{% static 'img/android-icon-48x48.png' %}"
|
</a>
|
||||||
alt="Hostea temporary logo"
|
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||||
/>
|
<span class="nav__hamburger-inner"></span>
|
||||||
<p class="nav__home-btn">ostea</p>
|
</label>
|
||||||
</a>
|
</div>
|
||||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
|
||||||
<span class="nav__hamburger-inner"></span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="nav__spacer"></div>
|
<div class="nav__spacer"></div>
|
||||||
|
|
||||||
<div class="nav__link-group">
|
<div class="nav__link-group">
|
||||||
<div class="nav__link-container">
|
<div class="nav__link-container">
|
||||||
<a class="nav__link" rel="noreferrer" href="{% url 'dash.instances.new' %}"
|
<a
|
||||||
>Add Instance</a
|
class="nav__link"
|
||||||
>
|
rel="noreferrer"
|
||||||
</div>
|
href="{% url 'dash.instances.new' %}"
|
||||||
<div class="nav__link-container">
|
>Add Instance</a
|
||||||
<a class="nav__link" rel="noreferrer" href="{% url 'support.home' %}"
|
>
|
||||||
>Support</a
|
</div>
|
||||||
>
|
<div class="nav__link-container">
|
||||||
</div>
|
<a
|
||||||
<div class="nav__link-container">
|
class="nav__link"
|
||||||
<a class="nav__link" rel="noreferrer" href="{% url 'accounts.logout' %}"
|
rel="noreferrer"
|
||||||
>Logout</a
|
href="{% url 'support.home' %}"
|
||||||
>
|
>Support</a
|
||||||
</div>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="nav__link-container">
|
||||||
|
<a
|
||||||
|
class="nav__link"
|
||||||
|
rel="noreferrer"
|
||||||
|
href="{% url 'accounts.logout' %}"
|
||||||
|
>Logout</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<p>Created On: {{ instance.created_at }}</p>
|
<p>Created On: {{ instance.created_at }}</p>
|
||||||
<p><a href="{{gitea_uri}}">Gitea Instance</a>|<a href="{{woodpecker}}">Woodpecker CI</a></p>
|
<p><a href="{{forgejo_uri}}">Forgejo Instance</a>|<a href="{{woodpecker}}">Woodpecker CI</a></p>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
action="{% url 'dash.instances.delete' name=instance.name %}"
|
action="{% url 'dash.instances.delete' name=instance.name %}"
|
||||||
|
|
|
@ -30,6 +30,7 @@ from payments import get_payment_model, RedirectNeeded, PaymentStatus
|
||||||
from accounts.tests import login_util, register_util
|
from accounts.tests import login_util, register_util
|
||||||
|
|
||||||
from .models import InstanceConfiguration, Instance
|
from .models import InstanceConfiguration, Instance
|
||||||
|
from .utils import create_instance, sanitize_vm_name, VmErrors, VmException
|
||||||
|
|
||||||
|
|
||||||
def create_configurations(t: TestCase):
|
def create_configurations(t: TestCase):
|
||||||
|
@ -170,7 +171,7 @@ class InstancesConfig(TestCase):
|
||||||
"""
|
"""
|
||||||
Expects InstancesConfig titled "s1-2", "s1-4" and "s1-8"
|
Expects InstancesConfig titled "s1-2", "s1-4" and "s1-8"
|
||||||
|
|
||||||
ref: https://gitea.hostea.org/Hostea/july-mvp/issues/10#issuecomment-639
|
ref: https://forgejo.gna.org/Hostea/july-mvp/issues/10#issuecomment-639
|
||||||
"""
|
"""
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
InstanceConfiguration.objects.filter(
|
InstanceConfiguration.objects.filter(
|
||||||
|
@ -197,6 +198,30 @@ class CreateInstance(TestCase):
|
||||||
register_util(t=self, username="createinstance_user")
|
register_util(t=self, username="createinstance_user")
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
|
|
||||||
|
def test_sanitize_vm_name(self):
|
||||||
|
self.assertEqual(sanitize_vm_name(vm_name="LOWERname"), "lowername")
|
||||||
|
|
||||||
|
with self.assertRaises(VmException):
|
||||||
|
sanitize_vm_name(vm_name="12345452131324234234234234")
|
||||||
|
|
||||||
|
with self.assertRaises(VmException):
|
||||||
|
sanitize_vm_name(vm_name="122342$#34234")
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(
|
||||||
|
HOSTEA=infra_custom_config(test_name="test_create_instance_util")
|
||||||
|
)
|
||||||
|
def test_create_instance_util(self):
|
||||||
|
configuration = self.instance_config[0].name
|
||||||
|
|
||||||
|
with self.assertRaises(VmException):
|
||||||
|
create_instance(
|
||||||
|
vm_name="12345452131324234234234234",
|
||||||
|
configuration_name=configuration,
|
||||||
|
user=self.user,
|
||||||
|
)
|
||||||
|
|
||||||
@override_settings(
|
@override_settings(
|
||||||
HOSTEA=infra_custom_config(test_name="test_create_instance_renders")
|
HOSTEA=infra_custom_config(test_name="test_create_instance_renders")
|
||||||
)
|
)
|
||||||
|
@ -214,14 +239,14 @@ class CreateInstance(TestCase):
|
||||||
self.assertEqual(str.encode(test) in resp.content, True)
|
self.assertEqual(str.encode(test) in resp.content, True)
|
||||||
|
|
||||||
# create instance
|
# create instance
|
||||||
|
instance_name = "testirenrs"
|
||||||
payload = {
|
payload = {
|
||||||
"name": "test_create_instance_renders",
|
"name": instance_name,
|
||||||
"configuration": self.instance_config[0].name,
|
"configuration": self.instance_config[0].name,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.assertEqual(Instance.objects.filter(name=payload["name"]).exists(), False)
|
self.assertEqual(Instance.objects.filter(name=payload["name"]).exists(), False)
|
||||||
|
|
||||||
instance_name = "test_create_instance_renders"
|
|
||||||
create_instance_util(
|
create_instance_util(
|
||||||
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||||
)
|
)
|
||||||
|
|
|
@ -14,7 +14,6 @@
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
|
|
||||||
from git import Repo
|
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -24,6 +23,8 @@ from .models import Instance, InstanceConfiguration
|
||||||
@unique
|
@unique
|
||||||
class VmErrors(Enum):
|
class VmErrors(Enum):
|
||||||
NAME_EXISTS = "Instance name exists, please try again with a different name"
|
NAME_EXISTS = "Instance name exists, please try again with a different name"
|
||||||
|
ILLEGAL_NAME = "Only alphanumeric characters are allowed in instance name"
|
||||||
|
NAME_TOO_LONG = "Instance name must be less than 20 characters"
|
||||||
NO_CONFIG = "Configuration doesn't exist, please try again."
|
NO_CONFIG = "Configuration doesn't exist, please try again."
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
|
@ -42,13 +43,30 @@ class VmException(Exception):
|
||||||
return self.error
|
return self.error
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_vm_name(vm_name: str) -> str:
|
||||||
|
"""
|
||||||
|
Sanity checks and normalization of the vm name
|
||||||
|
"""
|
||||||
|
|
||||||
|
vm_name = vm_name.lower()
|
||||||
|
|
||||||
|
if len(vm_name) > 20:
|
||||||
|
raise VmException(code=VmErrors.NAME_TOO_LONG)
|
||||||
|
if not str.isalnum(vm_name):
|
||||||
|
raise VmException(code=VmErrors.ILLEGAL_NAME)
|
||||||
|
|
||||||
|
if Instance.objects.filter(name=vm_name).exists():
|
||||||
|
raise VmException(code=VmErrors.NAME_EXISTS)
|
||||||
|
|
||||||
|
return vm_name
|
||||||
|
|
||||||
|
|
||||||
def create_instance(vm_name: str, configuration_name: str, user: User) -> Instance:
|
def create_instance(vm_name: str, configuration_name: str, user: User) -> Instance:
|
||||||
"""
|
"""
|
||||||
Create instance view
|
Create instance view
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if Instance.objects.filter(name=vm_name).exists():
|
vm_name = sanitize_vm_name(vm_name)
|
||||||
raise VmException(code=VmErrors.NAME_EXISTS)
|
|
||||||
|
|
||||||
if not InstanceConfiguration.objects.filter(name=configuration_name).exists():
|
if not InstanceConfiguration.objects.filter(name=configuration_name).exists():
|
||||||
raise VmException(code=VmErrors.NO_CONFIG)
|
raise VmException(code=VmErrors.NO_CONFIG)
|
||||||
|
@ -71,7 +89,7 @@ def footer_ctx():
|
||||||
"link": settings.HOSTEA["SOURCE_CODE"],
|
"link": settings.HOSTEA["SOURCE_CODE"],
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
link = "https://gitea.hostea.org/Hostea/dashboard"
|
link = "https://forgejo.gna.org/Hostea/dashboard"
|
||||||
source_code = {"text": "Source Code", "link": link}
|
source_code = {"text": "Source Code", "link": link}
|
||||||
try:
|
try:
|
||||||
r = Repo(".")
|
r = Repo(".")
|
||||||
|
|
|
@ -89,10 +89,7 @@ def create_instance(request):
|
||||||
return redirect(reverse("billing.invoice.generate", args=(instance.name,)))
|
return redirect(reverse("billing.invoice.generate", args=(instance.name,)))
|
||||||
except VmException as e:
|
except VmException as e:
|
||||||
ctx = get_ctx()
|
ctx = get_ctx()
|
||||||
if e.code == VmErrors.NAME_EXISTS:
|
reason = e.code.value
|
||||||
reason = ("Instance name exists, please try again with a different name",)
|
|
||||||
elif e.code == VmErrors.NO_CONFIG:
|
|
||||||
reason = "Configuration doesn't exist, please try again."
|
|
||||||
|
|
||||||
ctx["error"] = {
|
ctx["error"] = {
|
||||||
"title": "Can't create instance",
|
"title": "Can't create instance",
|
||||||
|
@ -120,12 +117,12 @@ def view_instance(request, name: str):
|
||||||
instance = get_object_or_404(Instance, owned_by=user, name=name)
|
instance = get_object_or_404(Instance, owned_by=user, name=name)
|
||||||
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
ctx = default_ctx(title=PAGE_TITLE, username=user.username)
|
||||||
instance.configuration = instance.configuration_id
|
instance.configuration = instance.configuration_id
|
||||||
gitea_uri = Infra.get_gitea_uri(instance=instance)
|
forgejo_uri = Infra.get_forgejo_uri(instance=instance)
|
||||||
woodpecker = Infra.get_woodpecker_uri(instance=instance)
|
woodpecker = Infra.get_woodpecker_uri(instance=instance)
|
||||||
|
|
||||||
ctx["instance"] = instance
|
ctx["instance"] = instance
|
||||||
ctx["woodpecker"] = woodpecker
|
ctx["woodpecker"] = woodpecker
|
||||||
ctx["gitea_uri"] = gitea_uri
|
ctx["forgejo_uri"] = forgejo_uri
|
||||||
return render(request, "dash/instances/view/index.html", context=ctx)
|
return render(request, "dash/instances/view/index.html", context=ctx)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -44,8 +44,8 @@ PAYMENT_VARIANTS = {
|
||||||
"stripe": (
|
"stripe": (
|
||||||
"payments.stripe.StripeProvider", # please don't change this
|
"payments.stripe.StripeProvider", # please don't change this
|
||||||
{
|
{
|
||||||
"secret_key": env.get_value("STRIPE_SECRET_KEY"),
|
"secret_key": env.get_value("STRIPE_SECRET_KEY", default="UNSET"),
|
||||||
"public_key": env.get_value("STRIPE_PUBLIC_KEY"),
|
"public_key": env.get_value("STRIPE_PUBLIC_KEY", default="UNSET"),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -54,19 +54,19 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||||
"SUDO_TTL": 60 * 5,
|
"SUDO_TTL": 60 * 5,
|
||||||
},
|
},
|
||||||
"META": {
|
"META": {
|
||||||
"GITEA_INSTANCE": env.get_value(
|
"FORGEJO_INSTANCE": env.get_value(
|
||||||
"HOSTEA_META_GITEA_INSTANCE"
|
"HOSTEA_META_FORGEJO_INSTANCE"
|
||||||
), # meta Gitea insatnce
|
), # meta Forgejo insatnce
|
||||||
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
||||||
# Repository dedicated for handling support
|
# Repository dedicated for handling support
|
||||||
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
|
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
|
||||||
"SUPPORT_REPOSITORY": "support",
|
"SUPPORT_REPOSITORY": "support",
|
||||||
},
|
},
|
||||||
"INFRA": {
|
"INFRA": {
|
||||||
|
@ -78,14 +78,14 @@ HOSTEA = {
|
||||||
# SSH key that can push to the Git repository remote mentioned above
|
# SSH key that can push to the Git repository remote mentioned above
|
||||||
"SSH_KEY": env.get_value("HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY"),
|
"SSH_KEY": env.get_value("HOSTEA_INFRA_HOSTEA_REPO_SSH_KEY"),
|
||||||
},
|
},
|
||||||
"HOSTEA_DOMAIN": "hostea.org", # domain at which Hostea VMs will be spun up
|
"HOSTEA_DOMAIN": "gna.org", # domain at which Hostea VMs will be spun up
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Please see EMAIL_* configuration options:
|
# Please see EMAIL_* configuration options:
|
||||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#email-host
|
# https://docs.djangoproject.com/en/4.1/ref/settings/#email-host
|
||||||
EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025")
|
EMAIL_CONFIG = env.email("EMAIL_URL", default="smtp://admin:password@localhost:10025")
|
||||||
DEFAULT_FROM_EMAIL = "no-reply@hostea.org"
|
DEFAULT_FROM_EMAIL = "no-reply@gna.org"
|
||||||
|
|
||||||
vars().update(EMAIL_CONFIG)
|
vars().update(EMAIL_CONFIG)
|
||||||
|
|
||||||
|
|
|
@ -53,17 +53,17 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||||
"SUDO_TTL": 60 * 5,
|
"SUDO_TTL": 60 * 5,
|
||||||
},
|
},
|
||||||
"META": {
|
"META": {
|
||||||
"GITEA_INSTANCE": "https://gitea.hostea.org", # meta Gitea insatnce
|
"FORGEJO_INSTANCE": "https://forgejo.gna.org", # meta Forgejo insatnce
|
||||||
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
||||||
# Repository dedicated for handling support
|
# Repository dedicated for handling support
|
||||||
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
|
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
|
||||||
"SUPPORT_REPOSITORY": "support",
|
"SUPPORT_REPOSITORY": "support",
|
||||||
},
|
},
|
||||||
"INFRA": {
|
"INFRA": {
|
||||||
|
@ -87,4 +87,4 @@ EMAIL_USE_SSL = False
|
||||||
EMAIL_PORT = 10025
|
EMAIL_PORT = 10025
|
||||||
EMAIL_HOST_USER = "admin"
|
EMAIL_HOST_USER = "admin"
|
||||||
EMAIL_HOST_PASSWORD = "password"
|
EMAIL_HOST_PASSWORD = "password"
|
||||||
DEFAULT_FROM_EMAIL = "no-reply@hostea.org"
|
DEFAULT_FROM_EMAIL = "no-reply@gna.org"
|
||||||
|
|
|
@ -171,18 +171,18 @@ PAYMENT_VARIANTS = {
|
||||||
### Dashbaord specific configuration options
|
### Dashbaord specific configuration options
|
||||||
|
|
||||||
HOSTEA = {
|
HOSTEA = {
|
||||||
"SOURCE_CODE": "https://gitea.hostea.org/Hostea/dashboard",
|
"SOURCE_CODE": "https://forgejo.gna.org/Hostea/dashboard",
|
||||||
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
|
"RESTRICT_NEW_INTEGRATION_INSTALLATION": True,
|
||||||
"INSTANCE_MAINTAINER_CONTACT": "contact@hostea.example.org",
|
"INSTANCE_MAINTAINER_CONTACT": "contact@gna.example.org",
|
||||||
"ACCOUNTS": {
|
"ACCOUNTS": {
|
||||||
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
"MAX_VERIFICATION_TOLERANCE_PERIOD": 60 * 60 * 24, # in seconds
|
||||||
"SUDO_TTL": 60 * 5,
|
"SUDO_TTL": 60 * 5,
|
||||||
},
|
},
|
||||||
"META": {
|
"META": {
|
||||||
"GITEA_INSTANCE": "http://localhost:3000", # meta Gitea insatnce
|
"FORGEJO_INSTANCE": "http://localhost:3000", # meta Forgejo insatnce
|
||||||
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
||||||
# Repository dedicated for handling support
|
# Repository dedicated for handling support
|
||||||
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
|
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
|
||||||
"SUPPORT_REPOSITORY": "support",
|
"SUPPORT_REPOSITORY": "support",
|
||||||
},
|
},
|
||||||
"INFRA": {
|
"INFRA": {
|
||||||
|
@ -194,7 +194,7 @@ HOSTEA = {
|
||||||
# SSH key that can push to the Git repository remote mentioned above
|
# SSH key that can push to the Git repository remote mentioned above
|
||||||
"SSH_KEY": "/srv/hostea/deploy",
|
"SSH_KEY": "/srv/hostea/deploy",
|
||||||
},
|
},
|
||||||
"HOSTEA_DOMAIN": "vm.hostea.org", # domain at which Hostea VMs will be spun up
|
"HOSTEA_DOMAIN": "vm.gna.org", # domain at which Hostea VMs will be spun up
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -206,7 +206,7 @@ EMAIL_USE_SSL = False
|
||||||
EMAIL_PORT = 10025
|
EMAIL_PORT = 10025
|
||||||
EMAIL_HOST_USER = "admin"
|
EMAIL_HOST_USER = "admin"
|
||||||
EMAIL_HOST_PASSWORD = "password"
|
EMAIL_HOST_PASSWORD = "password"
|
||||||
DEFAULT_FROM_EMAIL: "no-reply@hostea.org"
|
DEFAULT_FROM_EMAIL: "no-reply@gna.org"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from dashboard.local_settings import *
|
from dashboard.local_settings import *
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
version: "3"
|
version: "3"
|
||||||
|
|
||||||
#networks:
|
#networks:
|
||||||
# hostea-dash-gitea:
|
# hostea-dash-forgejo:
|
||||||
# external: false
|
# external: false
|
||||||
# hostea-dash-smtp:
|
# hostea-dash-smtp:
|
||||||
# external: false
|
# external: false
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gitea:
|
forgejo:
|
||||||
image: gitea/gitea:1.16.5
|
image: codeberg.org/forgejo/forgejo:1.18.0-1
|
||||||
container_name: hostea-dash-gitea
|
container_name: hostea-dash-forgejo
|
||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
- USER_UID=1000
|
- USER_UID=1000
|
||||||
- USER_GID=1000
|
- USER_GID=1000
|
||||||
restart: always
|
restart: always
|
||||||
#networks:
|
#networks:
|
||||||
# - hostea-dash-gitea
|
# - hostea-dash-forgejo
|
||||||
volumes:
|
volumes:
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
1. Clone the project
|
1. Clone the project
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://gitea.hostea.org/Hostea/dashboard.git && cd dashboard
|
git clone https://forgejo.gna.org/Hostea/dashboard.git && cd dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create `virtualenv` and activate environment
|
2. Create `virtualenv` and activate environment
|
||||||
|
|
|
@ -44,8 +44,8 @@ hence the current redundancy in configuration and cronjob duration.
|
||||||
|
|
||||||
## Support Platform Integration
|
## Support Platform Integration
|
||||||
|
|
||||||
Hostea Dashbaord delegates support to Hostea's meta Gitea instance, as
|
Hostea Dashbaord delegates support to Hostea's meta Forgejo instance, as
|
||||||
discussed [here](https://gitea.hostea.org/Hostea/july-mvp/issues/17).
|
discussed [here](https://forgejo.gna.org/Hostea/july-mvp/issues/17).
|
||||||
|
|
||||||
To configure support platform integration , please set the following
|
To configure support platform integration , please set the following
|
||||||
attributes in `settings.py`:
|
attributes in `settings.py`:
|
||||||
|
@ -55,10 +55,10 @@ HOSTEA = {
|
||||||
# <--snip--->
|
# <--snip--->
|
||||||
"META": {
|
"META": {
|
||||||
# <--snip--->
|
# <--snip--->
|
||||||
"GITEA_INSTANCE": "https://gitea.hostea.org", # meta Gitea insatnce
|
"FORGEJO_INSTANCE": "https://forgejo.gna.org", # meta Forgejo insatnce
|
||||||
"GITEA_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
"FORGEJO_ORG_NAME": "Hostea", # Organisation name on Hostea meta instance
|
||||||
# Repository dedicated for handling support
|
# Repository dedicated for handling support
|
||||||
# ref: https://gitea.hostea.org/Hostea/july-mvp/issues/17
|
# ref: https://forgejo.gna.org/Hostea/july-mvp/issues/17
|
||||||
"SUPPORT_REPOSITORY": "support",
|
"SUPPORT_REPOSITORY": "support",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,9 +93,9 @@ class Command(BaseCommand):
|
||||||
instance = create_instance(
|
instance = create_instance(
|
||||||
vm_name=vm_name, configuration_name=size, user=user
|
vm_name=vm_name, configuration_name=size, user=user
|
||||||
)
|
)
|
||||||
(gitea_password, _commit) = create_vm_if_not_exists(instance)
|
(forgejo_password, _commit) = create_vm_if_not_exists(instance)
|
||||||
print("Instance created")
|
print("Instance created")
|
||||||
print(f"Gitea admin password: {gitea_password}")
|
print(f"Forgejo admin password: {forgejo_password}")
|
||||||
except VmException as e:
|
except VmException as e:
|
||||||
if e.code == VmErrors.NAME_EXISTS:
|
if e.code == VmErrors.NAME_EXISTS:
|
||||||
instance = Instance.objects.get(name=vm_name)
|
instance = Instance.objects.get(name=vm_name)
|
||||||
|
@ -104,9 +104,9 @@ class Command(BaseCommand):
|
||||||
name=size
|
name=size
|
||||||
)
|
)
|
||||||
instance.save()
|
instance.save()
|
||||||
(gitea_password, _commit) = create_vm_if_not_exists(instance)
|
(forgejo_password, _commit) = create_vm_if_not_exists(instance)
|
||||||
print("Instance created")
|
print("Instance created")
|
||||||
print(f"Gitea admin password: {gitea_password}")
|
print(f"Forgejo admin password: {forgejo_password}")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.stderr.write(self.style.ERROR(f"error: {str(e)}"))
|
self.stderr.write(self.style.ERROR(f"error: {str(e)}"))
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
Hello {{ username }},
|
||||||
|
|
||||||
|
Congratulations on your new Gna! instance!
|
||||||
|
|
||||||
|
Your Gna! instance is being prepared, you will receive an email
|
||||||
|
notification when it is ready.
|
||||||
|
|
||||||
|
You can use the following credentials to log into an admin account on
|
||||||
|
your new Gna! Forgejo instance. Great powers come with great
|
||||||
|
responsibilities, so use the admin credentials wisely. When in doubt,
|
||||||
|
consult the Forgejo docs or contact support!
|
||||||
|
|
||||||
|
- username : root
|
||||||
|
- password: {{ forgejo_password }}
|
||||||
|
- Forgejo {{ forgejo_uri }}
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
Gna! team
|
|
@ -1,15 +0,0 @@
|
||||||
Hello {{ username }},
|
|
||||||
|
|
||||||
Congratulations on your new Gitea instance!
|
|
||||||
|
|
||||||
You can use the following credentials to log into an admin account on
|
|
||||||
your new Gitea instance. Great powers come with great responsibilities,
|
|
||||||
so use the admin credentials wisely. When in doubt, consult the Gitea
|
|
||||||
docs or contact support!
|
|
||||||
|
|
||||||
- username : root
|
|
||||||
- password: {{ gitea_password }}
|
|
||||||
- Gitea {{ gitea_uri }}
|
|
||||||
|
|
||||||
Cheers,
|
|
||||||
Hostea team
|
|
|
@ -1,11 +1,11 @@
|
||||||
Hello {{ username }}!,
|
Hello {{ username }}!,
|
||||||
|
|
||||||
The deployment job has run to completion and your Hostea instance is now online!
|
The deployment job has run to completion and your Gna! instance is now online!
|
||||||
Credentials to admin account was sent in an earlier email, please contact
|
Credentials to admin account was sent in an earlier email, please contact
|
||||||
support if didn't receive it.
|
support if didn't receive it.
|
||||||
|
|
||||||
Gitea: {{ gitea_uri }}
|
Forgejo: {{ forgejo_uri }}
|
||||||
Woodpecker CI: {{ woodpecker_uri }}
|
Woodpecker CI: {{ woodpecker_uri }}
|
||||||
|
|
||||||
Cheers,
|
Cheers,
|
||||||
Hostea team
|
Gna! team
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
Hello {{ username }}!,
|
||||||
|
|
||||||
|
|
||||||
|
A customer has purchased a new instance. Please find the details below:
|
||||||
|
|
||||||
|
Forgejo: {{ forgejo_uri }}
|
||||||
|
Woodpecker CI: {{ woodpecker_uri }}
|
||||||
|
|
||||||
|
Cheers,
|
||||||
|
Gna! team
|
|
@ -1,11 +1,11 @@
|
||||||
{% extends 'dash/common/base.html' %} {% block dash %}
|
{% extends 'dash/common/base.html' %} {% block dash %}
|
||||||
<h1>{{ title }}</h1>
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
<h2>Gitea Admin Credentials</h2>
|
<h2>Forgejo Admin Credentials</h2>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
<li><b>Username:</b> root</li>
|
<li><b>Username:</b> root</li>
|
||||||
<li><b>Password:</b> {{ gitea_password }}</li>
|
<li><b>Password:</b> {{ forgejo_password }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
enough --domain $domain host create {{subdomain}}-host
|
enough --domain $domain host create {{subdomain}}-host
|
||||||
enough --domain $domain service create --host {{subdomain}}-host gitea
|
enough --domain $domain service create --host {{subdomain}}-host forgejo
|
||||||
|
|
|
@ -2,49 +2,49 @@
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Public hostname of the Gitea instance
|
# Public hostname of the Forgejo instance
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
gitea_host: "{{ subdomain }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
|
forgejo_host: "{{ subdomain }}.{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Mailer from
|
# Mailer from
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
gitea_mailer_from: "noreply@{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
|
forgejo_mailer_from: "noreply@{{ '{' }}{{ '{' }} domain {{ '}' }}{{ '}' }}"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# SSH port of the Gitea instance
|
# SSH port of the Forgejo instance
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
gitea_ssh_port: "22"
|
forgejo_ssh_port: "22"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Gitea version
|
# Forgejo version
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
gitea_version: "1.16.8"
|
#forgejo_version: "1.18.0-1"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Admin user name
|
# Admin user name
|
||||||
#
|
#
|
||||||
gitea_user: root
|
forgejo_user: root
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Admin user password
|
# Admin user password
|
||||||
#
|
#
|
||||||
gitea_password: "{{ gitea_password }}"
|
forgejo_password: "{{ forgejo_password }}"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Admin user email
|
# Admin user email
|
||||||
#
|
#
|
||||||
gitea_email: "{{ gitea_email }}"
|
forgejo_email: "{{ forgejo_email }}"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
|
@ -60,9 +60,9 @@ woodpecker_host: "{{ '{' }}{{ '{' }} woodpecker_hostname {{ '}' }}{{ '}' }}.{{ '
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
||||||
# Gitea users with admin rights on woodpecker
|
# Forgejo users with admin rights on woodpecker
|
||||||
#
|
#
|
||||||
woodpecker_admins: "{{ '{' }}{{ '{' }} gitea_user {{ '}' }}{{ '}' }}"
|
woodpecker_admins: "{{ '{' }}{{ '{' }} forgejo_user {{ '}' }}{{ '}' }}"
|
||||||
#
|
#
|
||||||
#######################################
|
#######################################
|
||||||
#
|
#
|
|
@ -1,4 +1,4 @@
|
||||||
gitea-service-group:
|
forgejo-service-group:
|
||||||
hosts:
|
hosts:
|
||||||
{{ subdomain }}-host:
|
{{ subdomain }}-host:
|
||||||
ansible_port: 2222
|
ansible_port: 2222
|
||||||
|
|
|
@ -12,25 +12,21 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
import shutil
|
|
||||||
import time
|
import time
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
from urllib.parse import urlunparse
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import requests
|
|
||||||
from django.test import TestCase, Client, override_settings
|
from django.test import TestCase, Client, override_settings
|
||||||
from django.conf import settings
|
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
|
from django.core import mail
|
||||||
from git import Repo
|
|
||||||
|
|
||||||
from dash.models import Instance, InstanceConfiguration
|
from dash.models import Instance, InstanceConfiguration
|
||||||
from accounts.tests import register_util, login_util
|
from accounts.tests import register_util, login_util
|
||||||
from dash.tests import create_configurations, create_instance_util, infra_custom_config
|
from dash.tests import create_configurations, create_instance_util, infra_custom_config
|
||||||
from infrastructure.management.commands.vm import translate_sizes
|
from infrastructure.management.commands.vm import translate_sizes
|
||||||
|
|
||||||
from .utils import Infra, Worker
|
from billing.utils import payment_fullfilled
|
||||||
|
|
||||||
|
from .utils import Infra, Worker, create_vm_if_not_exists, delete_vm
|
||||||
from .models import InstanceCreated, Job, JobType
|
from .models import InstanceCreated, Job, JobType
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,164 +38,209 @@ class InfraUtilTest(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.username = "infrautil_user"
|
self.username = "infrautil_user"
|
||||||
register_util(t=self, username=self.username)
|
register_util(t=self, username=self.username)
|
||||||
|
self.user.is_staff = True
|
||||||
|
self.user.save()
|
||||||
create_configurations(t=self)
|
create_configurations(t=self)
|
||||||
|
|
||||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
|
# @override_settings(HOSTEA=infra_custom_config(test_name="test_path_util"))
|
||||||
def test_path_utils(self):
|
# def test_path_utils(self):
|
||||||
infra = Infra()
|
# infra = Infra()
|
||||||
subdomain = "foo"
|
# subdomain = "foo"
|
||||||
base = infra.repo_path
|
# base = infra.repo_path
|
||||||
|
#
|
||||||
|
# self.assertEqual(
|
||||||
|
# base.joinpath(f"inventory/host_vars/{subdomain}-host/"),
|
||||||
|
# infra._host_vars_dir(subdomain=subdomain),
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# self.assertEqual(
|
||||||
|
# base.joinpath(f"inventory/host_vars/{subdomain}-host/forgejo.yml"),
|
||||||
|
# infra._forgejo_path(subdomain=subdomain),
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# self.assertEqual(
|
||||||
|
# base.joinpath(f"inventory/host_vars/{subdomain}-host/provision.yml"),
|
||||||
|
# infra._provision_path(subdomain=subdomain),
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# self.assertEqual(
|
||||||
|
# base.joinpath(f"inventory/{subdomain}-backup.yml"),
|
||||||
|
# infra._backup_path(subdomain=subdomain),
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# self.assertEqual(
|
||||||
|
# base.joinpath(f"hosts-scripts/{subdomain}-host.sh"),
|
||||||
|
# infra._hostscript_path(subdomain=subdomain),
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# @override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
|
||||||
|
# def test_add_vm(self):
|
||||||
|
# c = Client()
|
||||||
|
# login_util(self, c, "accounts.home")
|
||||||
|
# subdomain = "add_vm"
|
||||||
|
#
|
||||||
|
# create_instance_util(
|
||||||
|
# t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# instance = Instance.objects.get(name=subdomain)
|
||||||
|
#
|
||||||
|
# infra = Infra()
|
||||||
|
# before_add = infra._sha()
|
||||||
|
# (password, after_add) = infra.add_vm(instance=instance)
|
||||||
|
# self.assertNotEqual(before_add, after_add)
|
||||||
|
#
|
||||||
|
# before_rm = after_add
|
||||||
|
# after_rm = infra.remove_vm(instance=instance)
|
||||||
|
# self.assertNotEqual(before_rm, after_rm)
|
||||||
|
#
|
||||||
|
# @override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
|
||||||
|
# def test_cmd(self):
|
||||||
|
# subdomain = "cmd_vm"
|
||||||
|
# infra = Infra()
|
||||||
|
# c = Client()
|
||||||
|
# login_util(self, c, "accounts.home")
|
||||||
|
#
|
||||||
|
# self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
|
||||||
|
# # username exists
|
||||||
|
# call_command(
|
||||||
|
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# instance = Instance.objects.get(name=subdomain)
|
||||||
|
#
|
||||||
|
# self.assertEqual(infra.get_flavor(instance=instance), "openstack_flavor_medium")
|
||||||
|
#
|
||||||
|
# self.assertEqual(instance.owned_by, self.user)
|
||||||
|
# self.assertEqual(
|
||||||
|
# instance.configuration_id, InstanceConfiguration.objects.get(name="s1-4")
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# instance_created = InstanceCreated.objects.get(instance=instance)
|
||||||
|
# self.assertEqual(instance_created.instance, instance)
|
||||||
|
#
|
||||||
|
# self.assertEqual(instance_created.created, True)
|
||||||
|
#
|
||||||
|
# # run create vm command again with same configuration to crudely check idempotency
|
||||||
|
# call_command(
|
||||||
|
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # run create vm command again with different configuration but same name
|
||||||
|
# # to crudely check idempotency
|
||||||
|
# call_command(
|
||||||
|
# "vm", "create", subdomain, f"--owner={self.username}", "--flavor=large"
|
||||||
|
# )
|
||||||
|
# instance.refresh_from_db()
|
||||||
|
# # verify new size is updated in DB
|
||||||
|
# self.assertEqual(
|
||||||
|
# str.strip(instance.configuration_id.name)
|
||||||
|
# == str.strip(translate_sizes("large")),
|
||||||
|
# True,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# # verify new size is updated in repository
|
||||||
|
# self.assertEqual(
|
||||||
|
# str.strip(infra.translate_size(instance=instance))
|
||||||
|
# == str.strip(infra.get_flavor(instance=instance)),
|
||||||
|
# True,
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# call_command("vm", "delete", subdomain)
|
||||||
|
#
|
||||||
|
# self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
|
||||||
|
# host_vars_dir = infra._host_vars_dir(subdomain)
|
||||||
|
# self.assertEqual(host_vars_dir.exists(), False)
|
||||||
|
#
|
||||||
|
# # run delete VM command to crudely check idempotency
|
||||||
|
# call_command("vm", "delete", subdomain)
|
||||||
|
#
|
||||||
|
# def test_worker(self):
|
||||||
|
# subdomain = "forgejo" # yes, forgejo.hostea.org exists. will use it till I
|
||||||
|
# # figure out how to use requests_mock within django
|
||||||
|
# c = Client()
|
||||||
|
# login_util(self, c, "accounts.home")
|
||||||
|
# create_instance_util(
|
||||||
|
# t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# instance = Instance.objects.get(name=subdomain)
|
||||||
|
# job = Job.objects.create(instance=instance, job_type=JobType.PING)
|
||||||
|
# forgejo_uri = Infra.get_forgejo_uri(instance=instance)
|
||||||
|
# print(f"mocking {forgejo_uri}")
|
||||||
|
#
|
||||||
|
# w = Worker(job=job)
|
||||||
|
# w.start()
|
||||||
|
# time.sleep(15)
|
||||||
|
# self.assertEqual(w.is_alive(), False)
|
||||||
|
# w.join()
|
||||||
|
# self.assertEqual(
|
||||||
|
# Job.objects.filter(instance=instance, job_type=JobType.PING).exists(), True
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
@override_settings(HOSTEA=infra_custom_config(test_name="test_vm_delete_payments"))
|
||||||
|
def test_vm_delete_payments(self):
|
||||||
|
"""
|
||||||
|
Test if the dashboard generates invoices for a VM crated with a name
|
||||||
|
matching a VM name that was deleted that existed.
|
||||||
|
|
||||||
self.assertEqual(
|
ref: https://forgejo.hostea.org/Hostea/dashboard/issues/38#issuecomment-1162
|
||||||
base.joinpath(f"inventory/host_vars/{subdomain}-host/"),
|
"""
|
||||||
infra._host_vars_dir(subdomain=subdomain),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
base.joinpath(f"inventory/host_vars/{subdomain}-host/gitea.yml"),
|
|
||||||
infra._gitea_path(subdomain=subdomain),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
base.joinpath(f"inventory/host_vars/{subdomain}-host/provision.yml"),
|
|
||||||
infra._provision_path(subdomain=subdomain),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
base.joinpath(f"inventory/{subdomain}-backup.yml"),
|
|
||||||
infra._backup_path(subdomain=subdomain),
|
|
||||||
)
|
|
||||||
|
|
||||||
self.assertEqual(
|
|
||||||
base.joinpath(f"hosts-scripts/{subdomain}-host.sh"),
|
|
||||||
infra._hostscript_path(subdomain=subdomain),
|
|
||||||
)
|
|
||||||
|
|
||||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_add_vm"))
|
|
||||||
def test_add_vm(self):
|
|
||||||
infra = Infra()
|
|
||||||
c = Client()
|
c = Client()
|
||||||
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
|
||||||
login_util(self, c, "accounts.home")
|
login_util(self, c, "accounts.home")
|
||||||
subdomain = "add_vm"
|
instance_name = "trmpayments"
|
||||||
|
|
||||||
base = infra.repo_path
|
infra = Infra()
|
||||||
|
|
||||||
create_instance_util(
|
create_instance_util(
|
||||||
t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
|
t=self, c=c, instance_name=instance_name, config=self.instance_config[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
before_add = infra.repo.head.commit.hexsha
|
instance = Instance.objects.get(name=instance_name)
|
||||||
instance = Instance.objects.get(name=subdomain)
|
self.assertEqual(payment_fullfilled(instance=instance), True)
|
||||||
woodpecker_agent_secret = infra.add_vm(instance=instance)
|
create_vm_if_not_exists(instance=instance)
|
||||||
after_add = infra.repo.head.commit.hexsha
|
|
||||||
self.assertEqual(before_add is not after_add, True)
|
|
||||||
|
|
||||||
# c = infra_custom_config(test_name="test_add_vm--get-head")
|
# delete VM
|
||||||
path = Path("/tmp/hostea/dashboard/check-test_add_vm")
|
delete_vm(instance=instance)
|
||||||
if path.exists():
|
self.assertEqual(Instance.objects.filter(name=instance_name).exists(), False)
|
||||||
shutil.rmtree(path)
|
|
||||||
repo = Repo.clone_from(conf["REMOTE"], path, env=infra.env)
|
|
||||||
repo.git.pull(env=infra.env)
|
|
||||||
self.assertEqual(repo.head.commit.hexsha == after_add, True)
|
|
||||||
|
|
||||||
before_rm = infra.repo.head.commit.hexsha
|
# re-create VM with management command as it bypasses payments. We
|
||||||
infra.remove_vm(instance=instance)
|
# usually use create_instance_util but it will pay for the instance too
|
||||||
after_rm = infra.repo.head.commit.hexsha
|
call_command(
|
||||||
self.assertEqual(before_add is not after_add, True)
|
"vm", "create", instance_name, f"--owner={self.username}", "--flavor=medium"
|
||||||
|
)
|
||||||
repo.git.pull(env=infra.env)
|
# verify VM is created
|
||||||
self.assertEqual(repo.head.commit.hexsha == after_rm, True)
|
self.assertEqual(Instance.objects.filter(name=instance_name).exists(), True)
|
||||||
|
# verify payment is unfulfilled
|
||||||
@override_settings(HOSTEA=infra_custom_config(test_name="test_cmd"))
|
instance = Instance.objects.get(name=instance_name)
|
||||||
def test_cmd(self):
|
self.assertEqual(payment_fullfilled(instance=instance), False)
|
||||||
subdomain = "cmd_vm"
|
|
||||||
infra = Infra()
|
|
||||||
c = Client()
|
|
||||||
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
|
||||||
login_util(self, c, "accounts.home")
|
|
||||||
|
|
||||||
base = infra.repo_path
|
|
||||||
|
|
||||||
|
# generate invoice
|
||||||
stdout = StringIO()
|
stdout = StringIO()
|
||||||
stderr = StringIO()
|
stderr = StringIO()
|
||||||
|
|
||||||
self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
|
|
||||||
# username exists
|
|
||||||
call_command(
|
call_command(
|
||||||
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
|
"generate_invoice",
|
||||||
|
stdout=stdout,
|
||||||
|
stderr=stderr,
|
||||||
)
|
)
|
||||||
out = stdout.getvalue()
|
out = stdout.getvalue()
|
||||||
|
print("out")
|
||||||
|
print(out)
|
||||||
|
self.assertEqual(instance_name in out, True)
|
||||||
|
self.assertEqual(f"Payment not fulfilled for instance: {instance}" in out, True)
|
||||||
|
|
||||||
instance = Instance.objects.get(name=subdomain)
|
staff_notification = None
|
||||||
|
for m in mail.outbox:
|
||||||
self.assertEqual(infra.get_flavor(instance=instance), "openstack_flavor_medium")
|
if "New instance alert" in m.subject:
|
||||||
|
staff_notification = m
|
||||||
self.assertEqual(instance.owned_by, self.user)
|
break
|
||||||
|
self.assertEqual(staff_notification.to[0], self.email)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
instance.configuration_id, InstanceConfiguration.objects.get(name="s1-4")
|
"[Gna!] New instance alert" in staff_notification.subject, True
|
||||||
)
|
)
|
||||||
|
|
||||||
instance_created = InstanceCreated.objects.get(instance=instance)
|
|
||||||
self.assertEqual(instance_created.instance, instance)
|
|
||||||
|
|
||||||
self.assertEqual(instance_created.created, True)
|
|
||||||
|
|
||||||
# run create vm command again with same configuration to crudely check idempotency
|
|
||||||
call_command(
|
|
||||||
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=medium"
|
|
||||||
)
|
|
||||||
|
|
||||||
# run create vm command again with different configuration but same name
|
|
||||||
# to crudely check idempotency
|
|
||||||
old_size = instance.configuration_id
|
|
||||||
call_command(
|
|
||||||
"vm", "create", subdomain, f"--owner={self.username}", "--flavor=large"
|
|
||||||
)
|
|
||||||
instance.refresh_from_db()
|
|
||||||
# verify new size is updated in DB
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
str.strip(instance.configuration_id.name)
|
"A customer has purchased a new instance. Please find the details below:"
|
||||||
== str.strip(translate_sizes("large")),
|
in staff_notification.body,
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
|
||||||
# verify new size is updated in repository
|
|
||||||
self.assertEqual(
|
|
||||||
str.strip(infra.translate_size(instance=instance))
|
|
||||||
== str.strip(infra.get_flavor(instance=instance)),
|
|
||||||
True,
|
|
||||||
)
|
|
||||||
|
|
||||||
call_command("vm", "delete", subdomain)
|
|
||||||
out = stdout.getvalue()
|
|
||||||
|
|
||||||
self.assertEqual(Instance.objects.filter(name=subdomain).exists(), False)
|
|
||||||
host_vars_dir = infra._host_vars_dir(subdomain)
|
|
||||||
self.assertEqual(host_vars_dir.exists(), False)
|
|
||||||
|
|
||||||
# run delete VM command to crudely check idempotency
|
|
||||||
call_command("vm", "delete", subdomain)
|
|
||||||
|
|
||||||
def test_worker(self):
|
|
||||||
subdomain = "gitea" # yes, gitea.hostea.org exists. will use it till I
|
|
||||||
# figure out how to use requests_mock within django
|
|
||||||
c = Client()
|
|
||||||
login_util(self, c, "accounts.home")
|
|
||||||
create_instance_util(
|
|
||||||
t=self, c=c, instance_name=subdomain, config=self.instance_config[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
instance = Instance.objects.get(name=subdomain)
|
|
||||||
job = Job.objects.create(instance=instance, job_type=JobType.PING)
|
|
||||||
gitea_uri = Infra.get_gitea_uri(instance=instance)
|
|
||||||
print(f"mocking {gitea_uri}")
|
|
||||||
|
|
||||||
w = Worker(job=job)
|
|
||||||
w.start()
|
|
||||||
time.sleep(15)
|
|
||||||
self.assertEqual(w.is_alive(), False)
|
|
||||||
w.join()
|
|
||||||
self.assertEqual(
|
|
||||||
Job.objects.filter(instance=instance, job_type=JobType.PING).exists(), True
|
|
||||||
)
|
|
||||||
|
|
|
@ -12,25 +12,30 @@
|
||||||
#
|
#
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import sh
|
||||||
import shutil
|
import shutil
|
||||||
import yaml
|
import yaml
|
||||||
import requests
|
import requests
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from threading import Thread, Event
|
from threading import Thread
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.mail import send_mail
|
from django.core.mail import send_mail
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from git import Repo, Commit
|
from payments import get_payment_model
|
||||||
from git.exc import InvalidGitRepositoryError
|
|
||||||
|
|
||||||
from dash.models import Instance
|
from dash.models import Instance
|
||||||
|
|
||||||
from infrastructure.models import InstanceCreated, JobType, Job
|
from infrastructure.models import InstanceCreated, JobType, Job
|
||||||
|
|
||||||
|
logging.basicConfig()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Worker(Thread):
|
class Worker(Thread):
|
||||||
def __init__(self, job: Job):
|
def __init__(self, job: Job):
|
||||||
|
@ -38,12 +43,12 @@ class Worker(Thread):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
gitea_uri = Infra.get_gitea_uri(instance=self.job.instance)
|
forgejo_uri = Infra.get_forgejo_uri(instance=self.job.instance)
|
||||||
woodpecker = Infra.get_woodpecker_uri(instance=self.job.instance)
|
woodpecker = Infra.get_woodpecker_uri(instance=self.job.instance)
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
print(f"[ping] Trying to reach {gitea_uri}")
|
print(f"[ping] Trying to reach {forgejo_uri}")
|
||||||
resp = requests.get(gitea_uri)
|
resp = requests.get(forgejo_uri)
|
||||||
if resp.status_code == 200:
|
if resp.status_code == 200:
|
||||||
break
|
break
|
||||||
except Exception:
|
except Exception:
|
||||||
|
@ -55,7 +60,7 @@ class Worker(Thread):
|
||||||
email = job.instance.owned_by.email
|
email = job.instance.owned_by.email
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"gitea_uri": gitea_uri,
|
"forgejo_uri": forgejo_uri,
|
||||||
"woodpecker_uri": woodpecker,
|
"woodpecker_uri": woodpecker,
|
||||||
"username": job.instance.owned_by.username,
|
"username": job.instance.owned_by.username,
|
||||||
}
|
}
|
||||||
|
@ -67,36 +72,72 @@ class Worker(Thread):
|
||||||
sender = settings.DEFAULT_FROM_EMAIL
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
send_mail(
|
send_mail(
|
||||||
subject="[Hostea] Your Hostea instance is now online!",
|
subject="[Gna!] Your Gna! instance is now online!",
|
||||||
message=body,
|
message=body,
|
||||||
from_email=f"No reply Hostea<{sender}>", # TODO read from settings.py
|
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
|
||||||
recipient_list=[email],
|
recipient_list=[email],
|
||||||
)
|
)
|
||||||
job.delete()
|
job.delete()
|
||||||
|
|
||||||
|
|
||||||
def create_vm_if_not_exists(instance: Instance) -> (str, Commit):
|
def create_vm_if_not_exists(instance: Instance) -> (str, str):
|
||||||
"""
|
"""
|
||||||
Create VM utility. Gitea password is returned
|
Create VM utility. Forgejo password is returned
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def notify_staff(instance: Instance):
|
||||||
|
infra = Infra()
|
||||||
|
User = get_user_model()
|
||||||
|
forgejo_uri = Infra.get_forgejo_uri(instance=instance)
|
||||||
|
woodpecker = Infra.get_woodpecker_uri(instance=instance)
|
||||||
|
|
||||||
|
for staff in User.objects.filter(is_staff=True):
|
||||||
|
ctx = {
|
||||||
|
"forgejo_uri": forgejo_uri,
|
||||||
|
"woodpecker_uri": woodpecker,
|
||||||
|
"username": staff.username,
|
||||||
|
}
|
||||||
|
body = render_to_string(
|
||||||
|
"infrastructure/emails/staff-new-instance-alert.txt",
|
||||||
|
context=ctx,
|
||||||
|
)
|
||||||
|
|
||||||
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
|
send_mail(
|
||||||
|
subject="[Gna!] New instance alert",
|
||||||
|
message=body,
|
||||||
|
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
|
||||||
|
recipient_list=[staff.email],
|
||||||
|
)
|
||||||
|
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
if not InstanceCreated.objects.filter(instance=instance).exists():
|
if not InstanceCreated.objects.filter(instance=instance).exists():
|
||||||
(gitea_password, commit) = infra.add_vm(instance=instance)
|
(forgejo_password, commit) = infra.add_vm(instance=instance)
|
||||||
InstanceCreated.objects.create(instance=instance, created=True)
|
InstanceCreated.objects.create(instance=instance, created=True)
|
||||||
|
notify_staff(instance=instance)
|
||||||
job = Job.objects.create(instance=instance, job_type=str(JobType.PING))
|
job = Job.objects.create(instance=instance, job_type=str(JobType.PING))
|
||||||
Worker(job=job).start()
|
Worker(job=job).start()
|
||||||
return (gitea_password, commit)
|
return (forgejo_password, commit)
|
||||||
else:
|
else:
|
||||||
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
|
if str.strip(infra.get_flavor(instance=instance)) != str.strip(
|
||||||
infra.translate_size(instance=instance)
|
infra.translate_size(instance=instance)
|
||||||
):
|
):
|
||||||
# Worker.init_global()
|
# Worker.init_global()
|
||||||
|
notify_staff(instance=instance)
|
||||||
return infra.add_vm(instance=instance)
|
return infra.add_vm(instance=instance)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def delete_vm(instance: Instance):
|
def delete_vm(instance: Instance):
|
||||||
infra = Infra()
|
infra = Infra()
|
||||||
|
Payment = get_payment_model()
|
||||||
|
for payment in Payment.objects.filter(
|
||||||
|
paid_by=instance.owned_by, instance_name=instance.name
|
||||||
|
):
|
||||||
|
payment.vm_deleted = True
|
||||||
|
payment.save()
|
||||||
|
|
||||||
infra.remove_vm(instance=instance)
|
infra.remove_vm(instance=instance)
|
||||||
if InstanceCreated.objects.filter(instance=instance).exists():
|
if InstanceCreated.objects.filter(instance=instance).exists():
|
||||||
InstanceCreated.objects.get(instance=instance).delete()
|
InstanceCreated.objects.get(instance=instance).delete()
|
||||||
|
@ -111,18 +152,17 @@ class Infra:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
||||||
self.repo_path = Path(conf["PATH"])
|
self.repo_path = Path(conf["PATH"])
|
||||||
if not self.repo_path.exists():
|
|
||||||
os.makedirs(self.repo_path)
|
|
||||||
|
|
||||||
ssh_cmd = f"/usr/bin/ssh -oStrictHostKeyChecking=no -i {conf['SSH_KEY']}"
|
|
||||||
self.env = {"GIT_SSH_COMMAND": ssh_cmd}
|
|
||||||
self._clone()
|
self._clone()
|
||||||
|
|
||||||
def _clone(self):
|
def _clone(self):
|
||||||
|
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
||||||
|
ssh_cmd = f"/usr/bin/ssh -oStrictHostKeyChecking=no -i {conf['SSH_KEY']}"
|
||||||
|
self.git = sh.git.bake(_env={"GIT_SSH_COMMAND": ssh_cmd})
|
||||||
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
conf = settings.HOSTEA["INFRA"]["HOSTEA_REPO"]
|
||||||
if os.path.exists(self.repo_path):
|
if os.path.exists(self.repo_path):
|
||||||
shutil.rmtree(self.repo_path)
|
shutil.rmtree(self.repo_path)
|
||||||
self.repo = Repo.clone_from(conf["REMOTE"], self.repo_path, env=self.env)
|
self.git.clone(conf["REMOTE"], self.repo_path)
|
||||||
|
self.git = self.git.bake("-C", self.repo_path)
|
||||||
|
|
||||||
def _host_vars_dir(self, subdomain: str) -> Path:
|
def _host_vars_dir(self, subdomain: str) -> Path:
|
||||||
"""
|
"""
|
||||||
|
@ -137,9 +177,9 @@ class Infra:
|
||||||
return self._host_vars_dir(subdomain=subdomain).joinpath("provision.yml")
|
return self._host_vars_dir(subdomain=subdomain).joinpath("provision.yml")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_gitea_uri(cls, instance: Instance) -> str:
|
def get_forgejo_uri(cls, instance: Instance) -> str:
|
||||||
"""
|
"""
|
||||||
Get an instance's Gitea URI
|
Get an instance's Forgejo URI
|
||||||
"""
|
"""
|
||||||
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
|
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
|
||||||
return f"https://{instance.name}.{base}"
|
return f"https://{instance.name}.{base}"
|
||||||
|
@ -154,7 +194,7 @@ class Infra:
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_woodpecker_uri(cls, instance: Instance) -> str:
|
def get_woodpecker_uri(cls, instance: Instance) -> str:
|
||||||
"""
|
"""
|
||||||
Get an instance's Gitea URI
|
Get an instance's Forgejo URI
|
||||||
"""
|
"""
|
||||||
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
|
base = settings.HOSTEA["INFRA"]["HOSTEA_DOMAIN"]
|
||||||
return f"https://{cls._gen_woodpecker_hostname(instance=instance)}.{base}"
|
return f"https://{cls._gen_woodpecker_hostname(instance=instance)}.{base}"
|
||||||
|
@ -171,12 +211,12 @@ class Infra:
|
||||||
return config["openstack_flavor"].split("{{ ")[1].split(" }}")[0]
|
return config["openstack_flavor"].split("{{ ")[1].split(" }}")[0]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _gitea_path(self, subdomain: str) -> Path:
|
def _forgejo_path(self, subdomain: str) -> Path:
|
||||||
"""
|
"""
|
||||||
utility method: get gitea file for a subdomain
|
utility method: get forgejo file for a subdomain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self._host_vars_dir(subdomain=subdomain).joinpath("gitea.yml")
|
return self._host_vars_dir(subdomain=subdomain).joinpath("forgejo.yml")
|
||||||
|
|
||||||
def _backup_path(self, subdomain: str) -> Path:
|
def _backup_path(self, subdomain: str) -> Path:
|
||||||
"""
|
"""
|
||||||
|
@ -210,25 +250,21 @@ class Infra:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
f.write("\n")
|
f.write("\n")
|
||||||
|
|
||||||
def _add_files(self, subdomain: str):
|
def _push(self, message):
|
||||||
"""
|
self.git.add(".")
|
||||||
Add all relevant files of a VM
|
self.git.config("user.email", settings.HOSTEA["INSTANCE_MAINTAINER_CONTACT"])
|
||||||
"""
|
self.git.config("user.name", "Hostea dashboard")
|
||||||
self.repo.git.add(str(self._host_vars_dir(subdomain=subdomain)))
|
try:
|
||||||
self.repo.git.add(str(self._backup_path(subdomain=subdomain)))
|
self.git.commit("-m", f"dashboard: {message}")
|
||||||
self.repo.git.add(str(self._service_path(subdomain=subdomain)))
|
except sh.ErrorReturnCode_1:
|
||||||
self.repo.git.add(str(self._hostscript_path(subdomain=subdomain)))
|
logger.debug("no change")
|
||||||
|
else:
|
||||||
|
self.git.push("origin", "master")
|
||||||
|
return self._sha()
|
||||||
|
|
||||||
def _commit(self, action: str, subdomain: str) -> Commit:
|
def _sha(self):
|
||||||
"""
|
sha = self.git("rev-parse", "origin/master")
|
||||||
Commit changes to a VM configuration
|
return str(sha).strip()
|
||||||
"""
|
|
||||||
|
|
||||||
self._add_files(subdomain=subdomain)
|
|
||||||
return self.repo.git.commit(
|
|
||||||
message=f"{action} VM {subdomain}",
|
|
||||||
author="Dashboard Bot <bot@dashboard.hostea.org>",
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def translate_size(instance: Instance) -> str:
|
def translate_size(instance: Instance) -> str:
|
||||||
|
@ -244,13 +280,12 @@ class Infra:
|
||||||
return "openstack_flavor_large"
|
return "openstack_flavor_large"
|
||||||
return instance.configuration_id.name
|
return instance.configuration_id.name
|
||||||
|
|
||||||
def add_vm(self, instance: Instance) -> (str, Commit):
|
def add_vm(self, instance: Instance) -> (str, str):
|
||||||
"""
|
"""
|
||||||
Add new VM to infrastructure repository
|
Add new VM to infrastructure repository
|
||||||
|
|
||||||
The gitea user password is returned
|
The forgejo user password is returned
|
||||||
"""
|
"""
|
||||||
self._clone()
|
|
||||||
|
|
||||||
subdomain = instance.name
|
subdomain = instance.name
|
||||||
host_vars_dir = self._host_vars_dir(subdomain)
|
host_vars_dir = self._host_vars_dir(subdomain)
|
||||||
|
@ -263,22 +298,22 @@ class Infra:
|
||||||
os.makedirs(hostscript_path)
|
os.makedirs(hostscript_path)
|
||||||
|
|
||||||
woodpecker_agent_secret = get_random_string(64)
|
woodpecker_agent_secret = get_random_string(64)
|
||||||
gitea_password = get_random_string(20)
|
forgejo_password = get_random_string(20)
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"woodpecker_agent_secret": woodpecker_agent_secret,
|
"woodpecker_agent_secret": woodpecker_agent_secret,
|
||||||
"woodpecker_hostname": self._gen_woodpecker_hostname(instance=instance),
|
"woodpecker_hostname": self._gen_woodpecker_hostname(instance=instance),
|
||||||
"woodpecker_admins": f"{instance.owned_by.username}",
|
"woodpecker_admins": f"{instance.owned_by.username}",
|
||||||
"gitea_email": instance.owned_by.email,
|
"forgejo_email": instance.owned_by.email,
|
||||||
"gitea_password": gitea_password,
|
"forgejo_password": forgejo_password,
|
||||||
"subdomain": subdomain,
|
"subdomain": subdomain,
|
||||||
}
|
}
|
||||||
|
|
||||||
gitea = self._gitea_path(subdomain)
|
forgejo = self._forgejo_path(subdomain)
|
||||||
with open(gitea, "w+", encoding="utf-8") as f:
|
with open(forgejo, "w+", encoding="utf-8") as f:
|
||||||
f.write(
|
f.write(
|
||||||
render_to_string(
|
render_to_string(
|
||||||
"infrastructure/yml/gitea.yml",
|
"infrastructure/yml/forgejo.yml",
|
||||||
context=ctx,
|
context=ctx,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -321,29 +356,26 @@ class Infra:
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
commit = self._commit(action="add", subdomain=subdomain)
|
commit = self._push(f"add vm {subdomain}")
|
||||||
self.repo.git.push(env=self.env)
|
return (forgejo_password, commit)
|
||||||
return (gitea_password, commit)
|
|
||||||
|
|
||||||
def remove_vm(self, instance: Instance):
|
def remove_vm(self, instance: Instance):
|
||||||
"""
|
"""
|
||||||
Remove a VM from infrastructure repository
|
Remove a VM from infrastructure repository
|
||||||
"""
|
"""
|
||||||
self._clone()
|
|
||||||
subdomain = instance.name
|
subdomain = instance.name
|
||||||
|
|
||||||
try:
|
host_vars_dir = self._host_vars_dir(subdomain)
|
||||||
|
if os.path.exists(host_vars_dir):
|
||||||
host_vars_dir = self._host_vars_dir(subdomain)
|
|
||||||
shutil.rmtree(host_vars_dir)
|
shutil.rmtree(host_vars_dir)
|
||||||
|
|
||||||
backup = self._backup_path(subdomain)
|
backup = self._backup_path(subdomain)
|
||||||
|
if os.path.exists(backup):
|
||||||
os.remove(backup)
|
os.remove(backup)
|
||||||
|
|
||||||
service = self._service_path(subdomain)
|
service = self._service_path(subdomain)
|
||||||
|
if os.path.exists(service):
|
||||||
os.remove(service)
|
os.remove(service)
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
hostscript = self._hostscript_path(subdomain)
|
hostscript = self._hostscript_path(subdomain)
|
||||||
with open(hostscript, "w+", encoding="utf-8") as f:
|
with open(hostscript, "w+", encoding="utf-8") as f:
|
||||||
|
@ -356,5 +388,4 @@ class Infra:
|
||||||
context={"subdomain": subdomain},
|
context={"subdomain": subdomain},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
self._commit(action="rm", subdomain=subdomain)
|
return self._push(f"rm vm {subdomain}")
|
||||||
self.repo.git.push(env=self.env)
|
|
||||||
|
|
|
@ -53,25 +53,25 @@ def create_instance(request, instance_name: str):
|
||||||
|
|
||||||
res = create_vm_if_not_exists(instance=instance)
|
res = create_vm_if_not_exists(instance=instance)
|
||||||
if res is not None:
|
if res is not None:
|
||||||
(gitea_password, commit) = res
|
(forgejo_password, commit) = res
|
||||||
|
|
||||||
ctx = {
|
ctx = {
|
||||||
"username": request.user.username,
|
"username": request.user.username,
|
||||||
"gitea_password": gitea_password,
|
"forgejo_password": forgejo_password,
|
||||||
"gitea_uri": Infra.get_gitea_uri(instance=instance),
|
"forgejo_uri": Infra.get_forgejo_uri(instance=instance),
|
||||||
}
|
}
|
||||||
|
|
||||||
body = render_to_string(
|
body = render_to_string(
|
||||||
"infrastructure/emails/gitea-creds.txt",
|
"infrastructure/emails/forgejo-creds.txt",
|
||||||
context=ctx,
|
context=ctx,
|
||||||
)
|
)
|
||||||
|
|
||||||
sender = settings.DEFAULT_FROM_EMAIL
|
sender = settings.DEFAULT_FROM_EMAIL
|
||||||
|
|
||||||
send_mail(
|
send_mail(
|
||||||
subject="[Hostea] Your Hostea instance is now online!",
|
subject="[Gna!] Forgejo admin credentials",
|
||||||
message=body,
|
message=body,
|
||||||
from_email=f"No reply Hostea<{sender}>", # TODO read from settings.py
|
from_email=f"No reply Gna!<{sender}>", # TODO read from settings.py
|
||||||
recipient_list=[request.user.email],
|
recipient_list=[request.user.email],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ init() {
|
||||||
sleep 5
|
sleep 5
|
||||||
# wait_for_env
|
# wait_for_env
|
||||||
fi
|
fi
|
||||||
gitea_root
|
forgejo_root
|
||||||
support_repo_init
|
support_repo_init
|
||||||
fleet_repo_init
|
fleet_repo_init
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@ import argparse
|
||||||
from requests import Session
|
from requests import Session
|
||||||
|
|
||||||
|
|
||||||
def gitea_from_args(args, c: Session):
|
def forgejo_from_args(args, c: Session):
|
||||||
from .gitea import Gitea
|
from .forgejo import Forgejo
|
||||||
|
|
||||||
return Gitea(
|
return Forgejo(
|
||||||
host=args.host,
|
host=args.host,
|
||||||
username=args.username,
|
username=args.username,
|
||||||
password=args.password,
|
password=args.password,
|
||||||
|
@ -15,7 +15,7 @@ def gitea_from_args(args, c: Session):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Gitea:
|
class Forgejo:
|
||||||
def __init__(self, parser, c: Session):
|
def __init__(self, parser, c: Session):
|
||||||
self.c = c
|
self.c = c
|
||||||
self.parser = parser
|
self.parser = parser
|
||||||
|
@ -29,56 +29,56 @@ class Gitea:
|
||||||
|
|
||||||
def __add_credentials_parser(self, parser):
|
def __add_credentials_parser(self, parser):
|
||||||
group = parser.add_argument_group("credentials", "User credentials")
|
group = parser.add_argument_group("credentials", "User credentials")
|
||||||
group.add_argument("username", type=str, help="Gitea user's username")
|
group.add_argument("username", type=str, help="Forgejo user's username")
|
||||||
group.add_argument("password", type=str, help="Gitea user's password")
|
group.add_argument("password", type=str, help="Forgejo user's password")
|
||||||
group.add_argument("email", type=str, help="Gitea user's email")
|
group.add_argument("email", type=str, help="Forgejo user's email")
|
||||||
group.add_argument("host", type=str, help="URI at which Gitea is running")
|
group.add_argument("host", type=str, help="URI at which Forgejo is running")
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.install()
|
forgejo.install()
|
||||||
|
|
||||||
self.install_parser = self.subparser.add_parser(
|
self.install_parser = self.subparser.add_parser(
|
||||||
name="install", description="Install Gitea", help="Install Gitea"
|
name="install", description="Install Forgejo", help="Install Forgejo"
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.install_parser)
|
self.__add_credentials_parser(self.install_parser)
|
||||||
self.install_parser.set_defaults(func=run)
|
self.install_parser.set_defaults(func=run)
|
||||||
|
|
||||||
def register(self):
|
def register(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.register()
|
forgejo.register()
|
||||||
|
|
||||||
self.register_parser = self.subparser.add_parser(
|
self.register_parser = self.subparser.add_parser(
|
||||||
name="register",
|
name="register",
|
||||||
description="Gitea user registration",
|
description="Forgejo user registration",
|
||||||
help="Register a user on Gitea",
|
help="Register a user on Forgejo",
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.register_parser)
|
self.__add_credentials_parser(self.register_parser)
|
||||||
self.register_parser.set_defaults(func=run)
|
self.register_parser.set_defaults(func=run)
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.login()
|
forgejo.login()
|
||||||
|
|
||||||
self.login_parser = self.subparser.add_parser(
|
self.login_parser = self.subparser.add_parser(
|
||||||
name="login", description="Gitea user login", help="Login on Gitea"
|
name="login", description="Forgejo user login", help="Login on Forgejo"
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.login_parser)
|
self.__add_credentials_parser(self.login_parser)
|
||||||
self.login_parser.set_defaults(func=run)
|
self.login_parser.set_defaults(func=run)
|
||||||
|
|
||||||
def create_repository(self):
|
def create_repository(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.login()
|
forgejo.login()
|
||||||
gitea.create_repository(name=args.repo_name)
|
forgejo.create_repository(name=args.repo_name)
|
||||||
|
|
||||||
self.create_repository_parser = self.subparser.add_parser(
|
self.create_repository_parser = self.subparser.add_parser(
|
||||||
name="create_repo",
|
name="create_repo",
|
||||||
description="Create repository on Gitea",
|
description="Create repository on Forgejo",
|
||||||
help="Create repository on Gitea",
|
help="Create repository on Forgejo",
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.create_repository_parser)
|
self.__add_credentials_parser(self.create_repository_parser)
|
||||||
self.create_repository_parser.set_defaults(func=run)
|
self.create_repository_parser.set_defaults(func=run)
|
||||||
|
@ -88,10 +88,10 @@ class Gitea:
|
||||||
|
|
||||||
def install_sso(self):
|
def install_sso(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.login()
|
forgejo.login()
|
||||||
print(f"CLIENT ID: {args.client_id}")
|
print(f"CLIENT ID: {args.client_id}")
|
||||||
gitea.install_sso(
|
forgejo.install_sso(
|
||||||
sso_name=args.sso_name,
|
sso_name=args.sso_name,
|
||||||
client_id=args.client_id,
|
client_id=args.client_id,
|
||||||
client_secret=args.client_secret,
|
client_secret=args.client_secret,
|
||||||
|
@ -100,8 +100,8 @@ class Gitea:
|
||||||
|
|
||||||
self.install_sso_parser = self.subparser.add_parser(
|
self.install_sso_parser = self.subparser.add_parser(
|
||||||
name="install_sso",
|
name="install_sso",
|
||||||
description="Install SSO on Gitea",
|
description="Install SSO on Forgejo",
|
||||||
help="Install SSO on Gitea",
|
help="Install SSO on Forgejo",
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.install_sso_parser)
|
self.__add_credentials_parser(self.install_sso_parser)
|
||||||
self.install_sso_parser.add_argument(
|
self.install_sso_parser.add_argument(
|
||||||
|
@ -123,14 +123,14 @@ class Gitea:
|
||||||
|
|
||||||
def add_deploy_key(self):
|
def add_deploy_key(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
gitea = gitea_from_args(args, c=c)
|
forgejo = forgejo_from_args(args, c=c)
|
||||||
gitea.login()
|
forgejo.login()
|
||||||
gitea.add_deploy_key(repo=args.repo_name, key=args.key_file)
|
forgejo.add_deploy_key(repo=args.repo_name, key=args.key_file)
|
||||||
|
|
||||||
self.add_deploy_key_parser = self.subparser.add_parser(
|
self.add_deploy_key_parser = self.subparser.add_parser(
|
||||||
name="add_deploy_key",
|
name="add_deploy_key",
|
||||||
description="Create repository on Gitea",
|
description="Create repository on Forgejo",
|
||||||
help="Add deploy key to a repository on Gitea",
|
help="Add deploy key to a repository on Forgejo",
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.add_deploy_key_parser)
|
self.__add_credentials_parser(self.add_deploy_key_parser)
|
||||||
self.add_deploy_key_parser.add_argument(
|
self.add_deploy_key_parser.add_argument(
|
||||||
|
@ -203,21 +203,21 @@ class Hostea:
|
||||||
|
|
||||||
def support(self):
|
def support(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
from .gitea import GiteaSSO
|
from .forgejo import ForgejoSSO
|
||||||
|
|
||||||
dash = dash_from_args(args, c=c)
|
dash = dash_from_args(args, c=c)
|
||||||
dash.login()
|
dash.login()
|
||||||
|
|
||||||
gitea = GiteaSSO(
|
forgejo = ForgejoSSO(
|
||||||
username=dash.username,
|
username=dash.username,
|
||||||
email=dash.email,
|
email=dash.email,
|
||||||
gitea_host=args.gitea_host,
|
forgejo_host=args.forgejo_host,
|
||||||
hostea_org=args.gitea_hostea_org,
|
hostea_org=args.forgejo_hostea_org,
|
||||||
support_repo=args.support_repo,
|
support_repo=args.support_repo,
|
||||||
c=c,
|
c=c,
|
||||||
)
|
)
|
||||||
dash.new_ticket(gitea.new_issues_uri)
|
dash.new_ticket(forgejo.new_issues_uri)
|
||||||
gitea.new_issue()
|
forgejo.new_issue()
|
||||||
|
|
||||||
self.support_parser = self.subparser.add_parser(
|
self.support_parser = self.subparser.add_parser(
|
||||||
name="support",
|
name="support",
|
||||||
|
@ -226,12 +226,12 @@ class Hostea:
|
||||||
)
|
)
|
||||||
self.__add_credentials_parser(self.support_parser)
|
self.__add_credentials_parser(self.support_parser)
|
||||||
self.support_parser.add_argument(
|
self.support_parser.add_argument(
|
||||||
"gitea_host", type=str, help="URI at which Gitea is running"
|
"forgejo_host", type=str, help="URI at which Forgejo is running"
|
||||||
)
|
)
|
||||||
self.support_parser.add_argument(
|
self.support_parser.add_argument(
|
||||||
"gitea_hostea_org",
|
"forgejo_hostea_org",
|
||||||
type=str,
|
type=str,
|
||||||
help="Hostea namespace(username/org) on Gitea, where support repository is hosted",
|
help="Hostea namespace(username/org) on Forgejo, where support repository is hosted",
|
||||||
)
|
)
|
||||||
self.support_parser.add_argument(
|
self.support_parser.add_argument(
|
||||||
"support_repo", type=str, help="support repository name"
|
"support_repo", type=str, help="support repository name"
|
||||||
|
@ -245,28 +245,28 @@ class Cli:
|
||||||
c = Session()
|
c = Session()
|
||||||
self.c = c
|
self.c = c
|
||||||
self.parser = argparse.ArgumentParser(
|
self.parser = argparse.ArgumentParser(
|
||||||
description="Install and Bootstrap Gitea and Hostea Dashboard"
|
description="Install and Bootstrap Forgejo and Hostea Dashboard"
|
||||||
)
|
)
|
||||||
self.subparser = self.parser.add_subparsers()
|
self.subparser = self.parser.add_subparsers()
|
||||||
self.check_env()
|
self.check_env()
|
||||||
self.gitea()
|
self.forgejo()
|
||||||
self.hostea()
|
self.hostea()
|
||||||
|
|
||||||
def __add_credentials_parser(self, parser):
|
def __add_credentials_parser(self, parser):
|
||||||
group = parser.add_argument_group("credentials", "User credentials")
|
group = parser.add_argument_group("credentials", "User credentials")
|
||||||
group.add_argument("username", type=str, help="Gitea user's username")
|
group.add_argument("username", type=str, help="Forgejo user's username")
|
||||||
group.add_argument("password", type=str, help="Gitea user's password")
|
group.add_argument("password", type=str, help="Forgejo user's password")
|
||||||
group.add_argument("email", type=str, help="Gitea user's email")
|
group.add_argument("email", type=str, help="Forgejo user's email")
|
||||||
|
|
||||||
def check_env(self):
|
def check_env(self):
|
||||||
def run(args, c: Session):
|
def run(args, c: Session):
|
||||||
from .gitea import Gitea
|
from .forgejo import Forgejo
|
||||||
from .hostea import Hostea
|
from .hostea import Hostea
|
||||||
|
|
||||||
Hostea.check_online(
|
Hostea.check_online(
|
||||||
dashboard_host=args.hostea_host, maildev_host=args.maildev_host
|
dashboard_host=args.hostea_host, maildev_host=args.maildev_host
|
||||||
)
|
)
|
||||||
Gitea.check_online(host=args.gitea_host)
|
Forgejo.check_online(host=args.forgejo_host)
|
||||||
|
|
||||||
self.check_env_parser = self.subparser.add_parser(
|
self.check_env_parser = self.subparser.add_parser(
|
||||||
name="check_env",
|
name="check_env",
|
||||||
|
@ -275,7 +275,7 @@ class Cli:
|
||||||
)
|
)
|
||||||
|
|
||||||
self.check_env_parser.add_argument(
|
self.check_env_parser.add_argument(
|
||||||
"gitea_host", type=str, help="URI at which Gitea is running"
|
"forgejo_host", type=str, help="URI at which Forgejo is running"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.check_env_parser.add_argument(
|
self.check_env_parser.add_argument(
|
||||||
|
@ -295,13 +295,13 @@ class Cli:
|
||||||
)
|
)
|
||||||
Hostea(parser=self.hostea, c=self.c)
|
Hostea(parser=self.hostea, c=self.c)
|
||||||
|
|
||||||
def gitea(self):
|
def forgejo(self):
|
||||||
self.gitea = self.subparser.add_parser(
|
self.forgejo = self.subparser.add_parser(
|
||||||
name="gitea",
|
name="forgejo",
|
||||||
description="Gitea",
|
description="Forgejo",
|
||||||
help="Gitea-related functionality",
|
help="Forgejo-related functionality",
|
||||||
)
|
)
|
||||||
Gitea(parser=self.gitea, c=self.c)
|
Forgejo(parser=self.forgejo, c=self.c)
|
||||||
|
|
||||||
def parse(self):
|
def parse(self):
|
||||||
return self.parser.parse_args()
|
return self.parser.parse_args()
|
||||||
|
|
|
@ -13,7 +13,7 @@ class ParseCSRF(HTMLParser):
|
||||||
# return cls(name="csrfmiddlewaretoken")
|
# return cls(name="csrfmiddlewaretoken")
|
||||||
#
|
#
|
||||||
# @classmethod
|
# @classmethod
|
||||||
# def gitea_parser(cls) -> "ParseCSRF":
|
# def forgejo_parser(cls) -> "ParseCSRF":
|
||||||
# return cls(name="_csrf")
|
# return cls(name="_csrf")
|
||||||
#
|
#
|
||||||
def handle_starttag(self, tag: str, attrs: (str, str)):
|
def handle_starttag(self, tag: str, attrs: (str, str)):
|
||||||
|
|
|
@ -10,15 +10,15 @@ import requests
|
||||||
|
|
||||||
from .csrf import ParseCSRF
|
from .csrf import ParseCSRF
|
||||||
|
|
||||||
# GITEA_USER = "root"
|
# FORGEJO_USER = "root"
|
||||||
# GITEA_EMAIL = "root@example.com"
|
# FORGEJO_EMAIL = "root@example.com"
|
||||||
# GITEA_PASSWORD = "foobarpassword"
|
# FORGEJO_PASSWORD = "foobarpassword"
|
||||||
# HOST = "http://localhost:8080"
|
# HOST = "http://localhost:8080"
|
||||||
#
|
#
|
||||||
# REPOS = []
|
# REPOS = []
|
||||||
|
|
||||||
|
|
||||||
class Gitea:
|
class Forgejo:
|
||||||
def __init__(self, host: str, username: str, password: str, email: str, c: Session):
|
def __init__(self, host: str, username: str, password: str, email: str, c: Session):
|
||||||
self.host = host
|
self.host = host
|
||||||
self.username = username
|
self.username = username
|
||||||
|
@ -48,7 +48,7 @@ class Gitea:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def check_online(host: str):
|
def check_online(host: str):
|
||||||
"""
|
"""
|
||||||
Check if Gitea instance is online
|
Check if Forgejo instance is online
|
||||||
"""
|
"""
|
||||||
count = 0
|
count = 0
|
||||||
parsed = urlparse(host)
|
parsed = urlparse(host)
|
||||||
|
@ -67,7 +67,7 @@ class Gitea:
|
||||||
|
|
||||||
def install(self):
|
def install(self):
|
||||||
"""
|
"""
|
||||||
Install Gitea, first form that a user sees when a new instance is
|
Install Forgejo, first form that a user sees when a new instance is
|
||||||
deployed
|
deployed
|
||||||
"""
|
"""
|
||||||
# cwd = os.environ.get("PWD")
|
# cwd = os.environ.get("PWD")
|
||||||
|
@ -77,12 +77,12 @@ class Gitea:
|
||||||
"db_host": "localhost:3306",
|
"db_host": "localhost:3306",
|
||||||
"db_user": "root",
|
"db_user": "root",
|
||||||
"db_passwd": "",
|
"db_passwd": "",
|
||||||
"db_name": "gitea",
|
"db_name": "forgejo",
|
||||||
"ssl_mode": "disable",
|
"ssl_mode": "disable",
|
||||||
"db_schema": "",
|
"db_schema": "",
|
||||||
"charset": "utf8",
|
"charset": "utf8",
|
||||||
"db_path": "/data/gitea/gitea.db",
|
"db_path": "/data/gitea/gitea.db",
|
||||||
"app_name": "Gitea:+Git+with+a+cup+of+tea",
|
"app_name": "Forgejo:+Beyond+Coding+We+Forge",
|
||||||
"repo_root_path": "/data/git/repositories",
|
"repo_root_path": "/data/git/repositories",
|
||||||
"lfs_root_path": "/data/git/lfs",
|
"lfs_root_path": "/data/git/lfs",
|
||||||
"run_user": "git",
|
"run_user": "git",
|
||||||
|
@ -316,26 +316,26 @@ class ParseSSOLogin(HTMLParser):
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class GiteaSSO:
|
class ForgejoSSO:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
username: str,
|
username: str,
|
||||||
email: str,
|
email: str,
|
||||||
gitea_host: str,
|
forgejo_host: str,
|
||||||
hostea_org: str,
|
hostea_org: str,
|
||||||
support_repo: str,
|
support_repo: str,
|
||||||
c: Session,
|
c: Session,
|
||||||
):
|
):
|
||||||
self.c = c
|
self.c = c
|
||||||
self.username = username
|
self.username = username
|
||||||
self.gitea_host = gitea_host
|
self.forgejo_host = forgejo_host
|
||||||
self.hostea_org = hostea_org
|
self.hostea_org = hostea_org
|
||||||
self.support_repo = support_repo
|
self.support_repo = support_repo
|
||||||
self.email = email
|
self.email = email
|
||||||
|
|
||||||
self.__csrf_key = "_csrf"
|
self.__csrf_key = "_csrf"
|
||||||
|
|
||||||
url = urlparse(self.gitea_host)
|
url = urlparse(self.forgejo_host)
|
||||||
repo = f"{self.hostea_org}/{self.support_repo}"
|
repo = f"{self.hostea_org}/{self.support_repo}"
|
||||||
issues = f"{repo}/issues"
|
issues = f"{repo}/issues"
|
||||||
new_issues = f"{issues}/new"
|
new_issues = f"{issues}/new"
|
||||||
|
@ -371,8 +371,8 @@ class GiteaSSO:
|
||||||
parser = ParseSSOLogin()
|
parser = ParseSSOLogin()
|
||||||
parser.feed(resp.text)
|
parser.feed(resp.text)
|
||||||
|
|
||||||
url = urlparse(self.gitea_host)
|
url = urlparse(self.forgejo_host)
|
||||||
## SSO URL in Gitea login page
|
## SSO URL in Forgejo login page
|
||||||
sso = urlunparse((url.scheme, url.netloc, parser.url, "", "", ""))
|
sso = urlunparse((url.scheme, url.netloc, parser.url, "", "", ""))
|
||||||
|
|
||||||
# redirects are enabled to for a cleaner implementation. Commented out
|
# redirects are enabled to for a cleaner implementation. Commented out
|
|
@ -11,12 +11,12 @@ is_ci(){
|
||||||
if is_ci
|
if is_ci
|
||||||
then
|
then
|
||||||
MAILDEV_URL="http://smtp:1080"
|
MAILDEV_URL="http://smtp:1080"
|
||||||
GITEA_URL="http://gitea:3000"
|
FORGEJO_URL="http://forgejo:3000"
|
||||||
GITEA_SSH_URL="ssh://git@gitea:22"
|
FORGEJO_SSH_URL="ssh://git@forgejo:22"
|
||||||
else
|
else
|
||||||
MAILDEV_URL="http://localhost:1080"
|
MAILDEV_URL="http://localhost:1080"
|
||||||
GITEA_URL="http://localhost:3000"
|
FORGEJO_URL="http://localhost:3000"
|
||||||
GITEA_SSH_URL="ssh://git@localhost:22"
|
FORGEJO_SSH_URL="ssh://git@localhost:22"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readonly DASHBOARD_URL="http://localhost:8000"
|
readonly DASHBOARD_URL="http://localhost:8000"
|
||||||
|
@ -27,22 +27,22 @@ readonly DASHBOARD_OIDC_DISCOVERY_URL="$DASHBOARD_URL/o/.well-known/openid-confi
|
||||||
readonly DASHBOARD_ADMIN_USERNAME=root
|
readonly DASHBOARD_ADMIN_USERNAME=root
|
||||||
readonly DASHBOARD_ADMIN_PASSWORD=supercomplicatedpassword
|
readonly DASHBOARD_ADMIN_PASSWORD=supercomplicatedpassword
|
||||||
readonly DASHBOARD_ADMIN_EMAIL="$DASHBOARD_ADMIN_USERNAME@dash.example.org"
|
readonly DASHBOARD_ADMIN_EMAIL="$DASHBOARD_ADMIN_USERNAME@dash.example.org"
|
||||||
readonly DASHBOARD_OIDC_APP_NAME=hostea-gitea
|
readonly DASHBOARD_OIDC_APP_NAME=hostea-forgejo
|
||||||
|
|
||||||
readonly GITEA_ROOT_USERNAME=root
|
readonly FORGEJO_ROOT_USERNAME=root
|
||||||
readonly GITEA_ROOT_EMAIL="$GITEA_ROOT_USERNAME@example.org"
|
readonly FORGEJO_ROOT_EMAIL="$FORGEJO_ROOT_USERNAME@example.org"
|
||||||
readonly GITEA_ROOT_PASSOWRD=supercomplicatedpassword
|
readonly FORGEJO_ROOT_PASSOWRD=supercomplicatedpassword
|
||||||
readonly GITEA_HOSTEA_SSO_NAME=hostea-sso
|
readonly FORGEJO_HOSTEA_SSO_NAME=hostea-sso
|
||||||
readonly GITEA_OIDC_CALLBACK="$GITEA_URL/user/oauth2/$GITEA_HOSTEA_SSO_NAME/callback"
|
readonly FORGEJO_OIDC_CALLBACK="$FORGEJO_URL/user/oauth2/$FORGEJO_HOSTEA_SSO_NAME/callback"
|
||||||
readonly GITEA_HOSTEA_FLEET_DEPLOY_KEY="$(realpath tests/fleet-deploy-key.pub)"
|
readonly FORGEJO_HOSTEA_FLEET_DEPLOY_KEY="$(realpath tests/fleet-deploy-key.pub)"
|
||||||
readonly GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE="$(realpath tests/fleet-deploy-key)"
|
readonly FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE="$(realpath tests/fleet-deploy-key)"
|
||||||
|
|
||||||
readonly GITEA_HOSTEA_USERNAME=hostea
|
readonly FORGEJO_HOSTEA_USERNAME=hostea
|
||||||
readonly GITEA_HOSTEA_PASSWORD=supercomplicatedpassword
|
readonly FORGEJO_HOSTEA_PASSWORD=supercomplicatedpassword
|
||||||
readonly GITEA_HOSTEA_EMAIL="$GITEA_HOSTEA_USERNAME@example.org"
|
readonly FORGEJO_HOSTEA_EMAIL="$FORGEJO_HOSTEA_USERNAME@example.org"
|
||||||
readonly GITEA_HOSTEA_SUPPORT_REPO="support"
|
readonly FORGEJO_HOSTEA_SUPPORT_REPO="support"
|
||||||
readonly GITEA_HOSTEA_FLEET_REPO="fleet"
|
readonly FORGEJO_HOSTEA_FLEET_REPO="fleet"
|
||||||
readonly GITEA_HOSTEA_FLEET_REPO_REMOTE="$GITEA_SSH_URL/$GITEA_HOSTEA_USERNAME/$GITEA_HOSTEA_FLEET_REPO.git"
|
readonly FORGEJO_HOSTEA_FLEET_REPO_REMOTE="$FORGEJO_SSH_URL/$FORGEJO_HOSTEA_USERNAME/$FORGEJO_HOSTEA_FLEET_REPO.git"
|
||||||
|
|
||||||
readonly HOSTEA_CUSTOMER_USERNAME=batman
|
readonly HOSTEA_CUSTOMER_USERNAME=batman
|
||||||
readonly HOSTEA_CUSTOMER_PASSWORD=supercomplicatedpassword
|
readonly HOSTEA_CUSTOMER_PASSWORD=supercomplicatedpassword
|
||||||
|
@ -53,7 +53,7 @@ OIDC_CLIENT_SECRET=""
|
||||||
|
|
||||||
wait_for_env() {
|
wait_for_env() {
|
||||||
python -m integration \
|
python -m integration \
|
||||||
check_env $GITEA_URL $DASHBOARD_URL $MAILDEV_URL
|
check_env $FORGEJO_URL $DASHBOARD_URL $MAILDEV_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
# create OIDC app on Hostea Dashboard
|
# create OIDC app on Hostea Dashboard
|
||||||
|
@ -67,90 +67,90 @@ oidc_dashboard_init() {
|
||||||
|
|
||||||
resp=$(python manage.py create_oidc \
|
resp=$(python manage.py create_oidc \
|
||||||
$DASHBOARD_OIDC_APP_NAME $DASHBOARD_ADMIN_USERNAME \
|
$DASHBOARD_OIDC_APP_NAME $DASHBOARD_ADMIN_USERNAME \
|
||||||
$GITEA_OIDC_CALLBACK)
|
$FORGEJO_OIDC_CALLBACK)
|
||||||
OIDC_CLIENT_ID=$(echo $resp | cut -d ":" -f 2 | cut -d " " -f 2)
|
OIDC_CLIENT_ID=$(echo $resp | cut -d ":" -f 2 | cut -d " " -f 2)
|
||||||
OIDC_CLIENT_SECRET=$(echo $resp | cut -d ":" -f 3 | cut -d " " -f 2)
|
OIDC_CLIENT_SECRET=$(echo $resp | cut -d ":" -f 3 | cut -d " " -f 2)
|
||||||
}
|
}
|
||||||
|
|
||||||
# register root user on Gitea to simulate Hoste admin and integrate SSO
|
# register root user on Forgejo to simulate Hoste admin and integrate SSO
|
||||||
gitea_root(){
|
forgejo_root(){
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea install \
|
forgejo install \
|
||||||
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
|
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
|
||||||
$GITEA_ROOT_EMAIL \
|
$FORGEJO_ROOT_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea register \
|
forgejo register \
|
||||||
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
|
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
|
||||||
$GITEA_ROOT_EMAIL \
|
$FORGEJO_ROOT_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea login \
|
forgejo login \
|
||||||
$GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
|
$FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
|
||||||
$GITEA_ROOT_EMAIL \
|
$FORGEJO_ROOT_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
# python -m integration \
|
# python -m integration \
|
||||||
# gitea install_sso \
|
# forgejo install_sso \
|
||||||
# $GITEA_ROOT_USERNAME $GITEA_ROOT_PASSOWRD \
|
# $FORGEJO_ROOT_USERNAME $FORGEJO_ROOT_PASSOWRD \
|
||||||
# $GITEA_ROOT_EMAIL \
|
# $FORGEJO_ROOT_EMAIL \
|
||||||
# $GITEA_URL \
|
# $FORGEJO_URL \
|
||||||
# $GITEA_HOSTEA_SSO_NAME \
|
# $FORGEJO_HOSTEA_SSO_NAME \
|
||||||
# $OIDC_CLIENT_ID $OIDC_CLIENT_SECRET \
|
# $OIDC_CLIENT_ID $OIDC_CLIENT_SECRET \
|
||||||
# $DASHBOARD_OIDC_DISCOVERY_URL
|
# $DASHBOARD_OIDC_DISCOVERY_URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# register user "Hostea" on Gitea and create support repository
|
# register user "Hostea" on Forgejo and create support repository
|
||||||
support_repo_init() {
|
support_repo_init() {
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea register \
|
forgejo register \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea login \
|
forgejo login \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea create_repo \
|
forgejo create_repo \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL \
|
$FORGEJO_URL \
|
||||||
$GITEA_HOSTEA_SUPPORT_REPO
|
$FORGEJO_HOSTEA_SUPPORT_REPO
|
||||||
}
|
}
|
||||||
|
|
||||||
new_fleet_repo_init() {
|
new_fleet_repo_init() {
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea create_repo \
|
forgejo create_repo \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL \
|
$FORGEJO_URL \
|
||||||
$1
|
$1
|
||||||
|
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea add_deploy_key \
|
forgejo add_deploy_key \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL \
|
$FORGEJO_URL \
|
||||||
$1 \
|
$1 \
|
||||||
$GITEA_HOSTEA_FLEET_DEPLOY_KEY
|
$FORGEJO_HOSTEA_FLEET_DEPLOY_KEY
|
||||||
|
|
||||||
tmp_dir=$(mktemp -d)
|
tmp_dir=$(mktemp -d)
|
||||||
pushd $tmp_dir
|
pushd $tmp_dir
|
||||||
echo "init" >> README
|
echo "init" >> README
|
||||||
|
git init
|
||||||
if is_ci
|
if is_ci
|
||||||
then
|
then
|
||||||
git config --global user.email "${CI_COMMIT_AUTHOR_EMAIL}"
|
git config user.email "hostea-dashobard-test@example.org"
|
||||||
git config --global user.name "${CI_COMMIT_AUTHOR}"
|
git config user.name "hoste-dashobard-test"
|
||||||
chmod 600 $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE
|
chmod 600 $FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE
|
||||||
fi
|
fi
|
||||||
git init
|
|
||||||
git add README
|
git add README
|
||||||
git commit -m "init"
|
git commit -m "init"
|
||||||
REMOTE="$GITEA_SSH_URL/$GITEA_HOSTEA_USERNAME/$1.git"
|
REMOTE="$FORGEJO_SSH_URL/$FORGEJO_HOSTEA_USERNAME/$1.git"
|
||||||
git remote add origin $REMOTE
|
git remote add origin $REMOTE
|
||||||
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $GITEA_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
|
GIT_SSH_COMMAND="/usr/bin/ssh -oStrictHostKeyChecking=no -i $FORGEJO_HOSTEA_FLEET_DEPLOY_KEY_PRIVATE" \
|
||||||
git push --set-upstream origin master
|
git push --set-upstream origin master
|
||||||
popd
|
popd
|
||||||
rm -rf $tmp_dir
|
rm -rf $tmp_dir
|
||||||
|
@ -158,20 +158,20 @@ new_fleet_repo_init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
# register user "Hostea" on Gitea and create support repository
|
# register user "Hostea" on Forgejo and create support repository
|
||||||
fleet_repo_init() {
|
fleet_repo_init() {
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea register \
|
forgejo register \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL || true
|
$FORGEJO_URL || true
|
||||||
python -m integration \
|
python -m integration \
|
||||||
gitea login \
|
forgejo login \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_PASSWORD \
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_PASSWORD \
|
||||||
$GITEA_HOSTEA_EMAIL \
|
$FORGEJO_HOSTEA_EMAIL \
|
||||||
$GITEA_URL
|
$FORGEJO_URL
|
||||||
|
|
||||||
new_fleet_repo_init $GITEA_HOSTEA_FLEET_REPO
|
new_fleet_repo_init $FORGEJO_HOSTEA_FLEET_REPO
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,8 +192,8 @@ hostea_customer_simulation() {
|
||||||
$HOSTEA_CUSTOMER_USERNAME $HOSTEA_CUSTOMER_PASSWORD \
|
$HOSTEA_CUSTOMER_USERNAME $HOSTEA_CUSTOMER_PASSWORD \
|
||||||
$HOSTEA_CUSTOMER_EMAIL \
|
$HOSTEA_CUSTOMER_EMAIL \
|
||||||
$DASHBOARD_URL \
|
$DASHBOARD_URL \
|
||||||
$GITEA_URL \
|
$FORGEJO_URL \
|
||||||
$GITEA_HOSTEA_USERNAME $GITEA_HOSTEA_SUPPORT_REPO
|
$FORGEJO_HOSTEA_USERNAME $FORGEJO_HOSTEA_SUPPORT_REPO
|
||||||
}
|
}
|
||||||
|
|
||||||
setup_env() {
|
setup_env() {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import sys
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""Run administrative tasks."""
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'dashboard.settings')
|
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "dashboard.settings")
|
||||||
try:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from django.core.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
|
@ -18,5 +18,5 @@ def main():
|
||||||
execute_from_command_line(sys.argv)
|
execute_from_command_line(sys.argv)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -15,9 +15,7 @@ django-oauth-toolkit==2.0.0
|
||||||
django-payments==1.0.0
|
django-payments==1.0.0
|
||||||
django-phonenumber-field==6.3.0
|
django-phonenumber-field==6.3.0
|
||||||
djangorestframework==3.13.1
|
djangorestframework==3.13.1
|
||||||
gitdb==4.0.9
|
greenlet==1.1.3.post0
|
||||||
GitPython==3.1.27
|
|
||||||
greenlet==1.1.2
|
|
||||||
idna==3.3
|
idna==3.3
|
||||||
install==1.3.5
|
install==1.3.5
|
||||||
isort==5.10.1
|
isort==5.10.1
|
||||||
|
@ -40,6 +38,7 @@ pytz==2022.1
|
||||||
PyYAML==6.0
|
PyYAML==6.0
|
||||||
requests==2.27.1
|
requests==2.27.1
|
||||||
six==1.16.0
|
six==1.16.0
|
||||||
|
sh==1.14.2
|
||||||
smmap==5.0.0
|
smmap==5.0.0
|
||||||
sqlparse==0.4.2
|
sqlparse==0.4.2
|
||||||
stripe==3.4.0
|
stripe==3.4.0
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You will be redirected to Hostea's issue tracker
|
You will be redirected to Gna!'s issue tracker
|
||||||
<span id="timer">momentarily</span>. If not, please click
|
<span id="timer">momentarily</span>. If not, please click
|
||||||
<a id="redirect-url" href="{{ support.list }}">here.</a>
|
<a id="redirect-url" href="{{ support.list }}">here.</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<h2>{{ title }}</h2>
|
<h2>{{ title }}</h2>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
You will be redirected to Hostea's issue tracker
|
You will be redirected to Gna!'s issue tracker
|
||||||
<span id="timer">momentarily</span>. If not, please click
|
<span id="timer">momentarily</span>. If not, please click
|
||||||
<a id="redirect-url" href="{{ support.new }}">here.</a>
|
<a id="redirect-url" href="{{ support.new }}">here.</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
|
@ -24,8 +24,8 @@ from .utils import IssueTracker
|
||||||
|
|
||||||
hostea_issue_tracker_settings = settings.HOSTEA
|
hostea_issue_tracker_settings = settings.HOSTEA
|
||||||
hostea_issue_tracker_settings["META"] = {
|
hostea_issue_tracker_settings["META"] = {
|
||||||
"GITEA_INSTANCE": "https://gitea.hostea.org",
|
"FORGEJO_INSTANCE": "https://forgejo.gna.org",
|
||||||
"GITEA_ORG_NAME": "Hostea",
|
"FORGEJO_ORG_NAME": "Hostea",
|
||||||
"SUPPORT_REPOSITORY": "support",
|
"SUPPORT_REPOSITORY": "support",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -41,8 +41,8 @@ class IssueTrackerTests(TestCase):
|
||||||
Verify default credentials; all further tests are based on defaults set
|
Verify default credentials; all further tests are based on defaults set
|
||||||
"""
|
"""
|
||||||
it = IssueTracker()
|
it = IssueTracker()
|
||||||
self.assertEqual(it.config["GITEA_INSTANCE"], "https://gitea.hostea.org")
|
self.assertEqual(it.config["FORGEJO_INSTANCE"], "https://forgejo.gna.org")
|
||||||
self.assertEqual(it.config["GITEA_ORG_NAME"], "Hostea")
|
self.assertEqual(it.config["FORGEJO_ORG_NAME"], "Hostea")
|
||||||
self.assertEqual(it.config["SUPPORT_REPOSITORY"], "support")
|
self.assertEqual(it.config["SUPPORT_REPOSITORY"], "support")
|
||||||
|
|
||||||
def test_uri_builders(self):
|
def test_uri_builders(self):
|
||||||
|
@ -51,10 +51,10 @@ class IssueTrackerTests(TestCase):
|
||||||
"""
|
"""
|
||||||
it = IssueTracker()
|
it = IssueTracker()
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
it.get_issue_tracker(), "https://gitea.hostea.org/Hostea/support/issues"
|
it.get_issue_tracker(), "https://forgejo.gna.org/Hostea/support/issues"
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
it.open_issue(), "https://gitea.hostea.org/Hostea/support/issues/new"
|
it.open_issue(), "https://forgejo.gna.org/Hostea/support/issues/new"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@ class IssueTracker:
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = settings.HOSTEA["META"]
|
self.config = settings.HOSTEA["META"]
|
||||||
self.instance = urlparse(self.config["GITEA_INSTANCE"])
|
self.instance = urlparse(self.config["FORGEJO_INSTANCE"])
|
||||||
self.repo = (
|
self.repo = (
|
||||||
f"{self.config['GITEA_ORG_NAME']}/{self.config['SUPPORT_REPOSITORY']}"
|
f"{self.config['FORGEJO_ORG_NAME']}/{self.config['SUPPORT_REPOSITORY']}"
|
||||||
)
|
)
|
||||||
self.issues = f"{self.repo}/issues"
|
self.issues = f"{self.repo}/issues"
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block title %}Forgot Your Password?{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1>Forgot your password?</h1>
|
||||||
|
<p>Enter your email address below, and we'll email instructions for setting a new one.</p>
|
||||||
|
|
||||||
|
<form method="POST">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{ form.as_p }}
|
||||||
|
<input type="submit" value="Send me instructions!">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|