Compare commits
1 commit
master
...
user-billi
Author | SHA1 | Date | |
---|---|---|---|
caf7f7a2c2 |
387 changed files with 4023 additions and 11123 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -22,6 +22,5 @@ uncloud/version.py
|
|||
build/
|
||||
venv/
|
||||
dist/
|
||||
.history/
|
||||
|
||||
*.iso
|
||||
*.sqlite3
|
||||
|
|
|
@ -4,7 +4,7 @@ stages:
|
|||
|
||||
run-tests:
|
||||
stage: test
|
||||
image: code.ungleich.ch:5050/uncloud/uncloud/uncloud-ci:latest
|
||||
image: fedora:latest
|
||||
services:
|
||||
- postgres:latest
|
||||
variables:
|
||||
|
@ -12,7 +12,11 @@ run-tests:
|
|||
DATABASE_USER: postgres
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
coverage: /^TOTAL.+?(\d+\%)$/
|
||||
before_script:
|
||||
- dnf install -y python3-devel python3-pip python3-coverage libpq-devel openldap-devel gcc chromium
|
||||
script:
|
||||
- cd uncloud_django_based/uncloud
|
||||
- pip install -r requirements.txt
|
||||
- cp uncloud/secrets_sample.py uncloud/secrets.py
|
||||
- coverage run --source='.' ./manage.py test
|
||||
- coverage report
|
||||
|
|
71
README.md
71
README.md
|
@ -1,70 +1,3 @@
|
|||
# Uncloud
|
||||
# ucloud
|
||||
|
||||
Cloud management platform, the ungleich way.
|
||||
|
||||
|
||||
[![pipeline status](https://code.ungleich.ch/uncloud/uncloud/badges/master/pipeline.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master)
|
||||
[![coverage report](https://code.ungleich.ch/uncloud/uncloud/badges/master/coverage.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master)
|
||||
|
||||
## Useful commands
|
||||
|
||||
* `./manage.py import-vat-rates path/to/csv`
|
||||
* `./manage.py createsuperuser`
|
||||
|
||||
## Development setup
|
||||
|
||||
Install system dependencies:
|
||||
|
||||
* On Fedora, you will need the following packages: `python3-virtualenv python3-devel openldap-devel gcc chromium`
|
||||
* sudo apt-get install libpq-dev python-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libffi-dev
|
||||
|
||||
|
||||
NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`.
|
||||
|
||||
```
|
||||
# Initialize virtualenv.
|
||||
» virtualenv .venv
|
||||
Using base prefix '/usr'
|
||||
New python executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python3
|
||||
Also creating executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python
|
||||
Installing setuptools, pip, wheel...
|
||||
done.
|
||||
|
||||
# Enter virtualenv.
|
||||
» source .venv/bin/activate
|
||||
|
||||
# Install dependencies.
|
||||
» pip install -r requirements.txt
|
||||
[...]
|
||||
|
||||
# Run migrations.
|
||||
» ./manage.py migrate
|
||||
Operations to perform:
|
||||
Apply all migrations: admin, auth, contenttypes, opennebula, sessions, uncloud_auth, uncloud_net, uncloud_pay, uncloud_service, uncloud_vm
|
||||
Running migrations:
|
||||
[...]
|
||||
|
||||
# Run webserver.
|
||||
» ./manage.py runserver
|
||||
Watching for file changes with StatReloader
|
||||
Performing system checks...
|
||||
|
||||
System check identified no issues (0 silenced).
|
||||
May 07, 2020 - 10:17:08
|
||||
Django version 3.0.6, using settings 'uncloud.settings'
|
||||
Starting development server at http://127.0.0.1:8000/
|
||||
Quit the server with CONTROL-C.
|
||||
```
|
||||
### Run Background Job Queue
|
||||
We use Django Q to handle the asynchronous code and Background Cron jobs
|
||||
To start the workers make sure first that Redis or the Django Q broker is working and you can edit it's settings in the settings file.
|
||||
```
|
||||
./manage.py qcluster
|
||||
```
|
||||
|
||||
### Note on PGSQL
|
||||
|
||||
If you want to use Postgres:
|
||||
|
||||
* Install on configure PGSQL on your base system.
|
||||
* OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest`
|
||||
Checkout https://ungleich.ch/ucloud/ for the documentation of ucloud.
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
* Intro
|
||||
This file lists issues that should be handled, are small and likely
|
||||
not yet high prio.
|
||||
* Issues
|
||||
** TODO Register prefered address in User model
|
||||
** TODO Allow to specify different recurring periods
|
|
@ -1,18 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
dbhost=$1; shift
|
||||
|
||||
ssh -L5432:localhost:5432 "$dbhost" &
|
||||
|
||||
python manage.py "$@"
|
||||
|
||||
|
||||
|
||||
# command only needs to be active while manage command is running
|
||||
|
||||
# -T no pseudo terminal
|
||||
|
||||
|
||||
# alternatively: commands output shell code
|
||||
|
||||
# ssh uncloud@dbhost "python manage.py --hostname xxx ..."
|
|
@ -1,64 +0,0 @@
|
|||
flush ruleset
|
||||
|
||||
table bridge filter {
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority 0;
|
||||
policy accept;
|
||||
|
||||
ibrname br100 jump netpublic
|
||||
}
|
||||
|
||||
chain netpublic {
|
||||
iifname vxlan100 jump from_uncloud
|
||||
|
||||
# Default blocks: router advertisements, dhcpv6, dhcpv4
|
||||
icmpv6 type nd-router-advert drop
|
||||
ip6 version 6 udp sport 547 drop
|
||||
ip version 4 udp sport 67 drop
|
||||
|
||||
# Individual blocks
|
||||
# iifname tap1 jump vm1
|
||||
}
|
||||
|
||||
chain vm1 {
|
||||
ether saddr != 02:00:f0:a9:c4:4e drop
|
||||
ip6 saddr != 2a0a:e5c1:111:888:0:f0ff:fea9:c44e drop
|
||||
}
|
||||
|
||||
chain from_uncloud {
|
||||
accept
|
||||
}
|
||||
}
|
||||
|
||||
# table ip6 filter {
|
||||
# chain forward {
|
||||
# type filter hook forward priority 0;
|
||||
|
||||
# # policy drop;
|
||||
|
||||
# ct state established,related accept;
|
||||
|
||||
# }
|
||||
|
||||
# }
|
||||
|
||||
# table ip filter {
|
||||
# chain input {
|
||||
# type filter hook input priority filter; policy drop;
|
||||
# iif "lo" accept
|
||||
# icmp type { echo-reply, destination-unreachable, source-quench, redirect, echo-request, router-advertisement, router-solicitation, time-exceeded, parameter-problem, timestamp-request, timestamp-reply, info-request, info-reply, address-mask-request, address-mask-reply } accept
|
||||
# ct state established,related accept
|
||||
# tcp dport { 22 } accept
|
||||
# log prefix "firewall-ipv4: "
|
||||
# udp sport 67 drop
|
||||
# }
|
||||
|
||||
# chain forward {
|
||||
# type filter hook forward priority filter; policy drop;
|
||||
# log prefix "firewall-ipv4: "
|
||||
# }
|
||||
|
||||
# chain output {
|
||||
# type filter hook output priority filter; policy accept;
|
||||
# }
|
||||
# }
|
|
@ -1,24 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
vmid=$1; shift
|
||||
|
||||
qemu=/usr/bin/qemu-system-x86_64
|
||||
|
||||
accel=kvm
|
||||
#accel=tcg
|
||||
|
||||
memory=1024
|
||||
cores=2
|
||||
uuid=732e08c7-84f8-4d43-9571-263db4f80080
|
||||
|
||||
export bridge=br100
|
||||
|
||||
$qemu -name uc${vmid} \
|
||||
-machine pc,accel=${accel} \
|
||||
-m ${memory} \
|
||||
-smp ${cores} \
|
||||
-uuid ${uuid} \
|
||||
-drive file=alpine-virt-3.11.2-x86_64.iso,media=cdrom \
|
||||
-drive file=alpine-virt-3.11.2-x86_64.iso,media=cdrom \
|
||||
-netdev tap,id=netmain,script=./ifup.sh \
|
||||
-device virtio-net-pci,netdev=netmain,id=net0,mac=02:00:f0:a9:c4:4e
|
|
@ -1,39 +0,0 @@
|
|||
#!/bin/sh
|
||||
# Nico Schottelius, 2021-01-17
|
||||
|
||||
set -e
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "$0 target-host"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
target_host=$1; shift
|
||||
user=app
|
||||
|
||||
dir=${0%/*}
|
||||
uncloud_base=$(cd ${dir}/.. && pwd -P)
|
||||
conf_name=local_settings-${target_host}.py
|
||||
conf_file=${uncloud_base}/uncloud/${conf_name}
|
||||
|
||||
if [ ! -e ${conf_file} ]; then
|
||||
echo "No settings for ${target_host}."
|
||||
echo "Create ${conf_file} before using this script."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Deploy
|
||||
rsync -av \
|
||||
--exclude venv/ \
|
||||
--exclude '*.pyc' \
|
||||
--exclude uncloud/local_settings.py \
|
||||
--delete \
|
||||
${uncloud_base}/ ${user}@${target_host}:app/
|
||||
|
||||
ssh "${user}@${target_host}" ". ~/pyvenv/bin/activate; cd ~/app; pip install -r requirements.txt"
|
||||
|
||||
# Config
|
||||
ssh "${user}@${target_host}" "cd ~/app/uncloud; ln -sf ${conf_name} local_settings.py"
|
||||
|
||||
# Restart / Apply
|
||||
ssh "${user}@${target_host}" "sudo /etc/init.d/uwsgi restart"
|
|
@ -1,7 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
# For undoing/redoing everything
|
||||
# Needed in special cases and needs to be avoided as soon as
|
||||
# uncloud.version >= 1
|
||||
for a in */migrations; do rm ${a}/*.py; done
|
||||
for a in */migrations; do python manage.py makemigrations ${a%%/migrations}; done
|
2
doc/.gitignore
vendored
2
doc/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
*.pdf
|
||||
*.tex
|
|
@ -1,85 +0,0 @@
|
|||
* How to handle billing in general
|
||||
** Manual test flow / setting up bills
|
||||
- Needs orders
|
||||
-
|
||||
** Orders
|
||||
- Orders are the heart of uncloud billing
|
||||
- Have a starting date
|
||||
- Have an ending date
|
||||
- Orders are immutable
|
||||
- Can usually not be cancelled / cancellation is not a refund
|
||||
- Customer/user commits on a certain period -> gets discount
|
||||
based on it
|
||||
- Can be upgraded
|
||||
- Create a new order
|
||||
- We link the new order to the old order and say this one
|
||||
replaces it
|
||||
- If the price of the new order is HIGHER than the OLD order,
|
||||
then we charge the difference until the end of the order period
|
||||
- In the next billing run we set the OLD order to not to bill anymore
|
||||
- And only the NEW order will be billed afterwards
|
||||
- Can be downgraded in the next period (but not for this period)
|
||||
- We create a new order, same as for upgrade
|
||||
- The new order starts directly after the OLD order
|
||||
- As the amount is LOWER than the OLD order, no additional charge is done
|
||||
during this order period
|
||||
- We might need to have an activate datetime
|
||||
- When to implement this
|
||||
- Order periods can be
|
||||
*** Statuses
|
||||
- CREATING/PREPARING
|
||||
- INACTIVE (?)
|
||||
- TO_BILL
|
||||
- NOT_TO_BILL: we use this to accelerate queries to the DB
|
||||
*** Updating status of orders
|
||||
- If has succeeding order and billing date is last month -> set inactive
|
||||
** Bills
|
||||
- Are always for a month
|
||||
- Can be preliminary
|
||||
*** Which orders to include
|
||||
- Not the cancelled ones / not active ones
|
||||
** Flows / Approach
|
||||
*** Finding all orders for a bill
|
||||
- Get all orders, state != NOT_TO_BILL; for each order do:
|
||||
- is it a one time order?
|
||||
- has it a bill assigned?
|
||||
- yes: set to NOT_TO_BILL
|
||||
- no:
|
||||
- get_or_create_bill_for_this_month
|
||||
- assign bill to this order
|
||||
- set to NOT_TO_BILL
|
||||
- is it a recurring order?
|
||||
- if it has a REPLACING order:
|
||||
-
|
||||
- First of month
|
||||
- Last of month
|
||||
*** Handling replacement of orders
|
||||
- The OLD order will appear in the month that it was cancelled on
|
||||
the bill
|
||||
- The OLD order needs to be set to NOT_TO_BILL after it was billed
|
||||
the last time
|
||||
- The NEW order will be added pro rata if the amount is higher in
|
||||
the same month
|
||||
- The NEW order will be used next month
|
||||
**** Disabling the old order
|
||||
- On billing run
|
||||
- If order.replacement_order (naming!) is set
|
||||
- if the order.replacement_order starts during THIS_MONTH
|
||||
- add order to bill
|
||||
- if NOT:
|
||||
- the order was already replaced in a previous billing period
|
||||
- set the order to NOT_TO_BILL
|
||||
**** Billing the new order
|
||||
- If order.previous_order
|
||||
*** Handling multiple times a recurring order
|
||||
- For each recurring order check the order.period
|
||||
- Find out when it was billed last
|
||||
- lookup latest bill
|
||||
- Calculate how many times it has been used until 2359, last day
|
||||
of month
|
||||
- For preliminary bill: until datetime.now()
|
||||
- Call the bill_end_datetime
|
||||
- Getting duration: bill_end_datetime - order.last_billed
|
||||
- Amount in seconds; duration_in_seconds
|
||||
- Divide duration_in_seconds by order.period; amount_used:
|
||||
- If >= 1: add amount_used * order.recurring_amount to bill
|
|
@ -1,485 +0,0 @@
|
|||
* Bootstrap / Installation / Deployment
|
||||
** Pre-requisites by operating system
|
||||
*** General
|
||||
To run uncloud you need:
|
||||
- ldap development libraries
|
||||
- libxml2-dev libxslt-dev
|
||||
- gcc / libc headers: for compiling things
|
||||
- python3-dev
|
||||
- wireguard: wg (for checking keys)
|
||||
*** Alpine
|
||||
#+BEGIN_SRC sh
|
||||
apk add openldap-dev postgresql-dev libxml2-dev libxslt-dev gcc python3-dev musl-dev wireguard-tools-wg
|
||||
#+END_SRC
|
||||
*** Debian/Devuan:
|
||||
#+BEGIN_SRC sh
|
||||
apt install postgresql-server-dev-all
|
||||
#+END_SRC
|
||||
** Creating a virtual environment / installing python requirements
|
||||
*** Virtual env
|
||||
To separate uncloud requirements, you can use a python virtual
|
||||
env as follows:
|
||||
#+BEGIN_SRC sh
|
||||
python3 -m venv venv
|
||||
. ./venv/bin/activate
|
||||
#+END_SRC
|
||||
Then install the requirements
|
||||
#+BEGIN_SRC sh
|
||||
pip install -r requirements.txt
|
||||
#+END_SRC
|
||||
** Setting up the the database
|
||||
*** Install the database service
|
||||
The database can run on the same host as uncloud, but can also run
|
||||
a different server. Consult the usual postgresql documentation for
|
||||
a secure configuration.
|
||||
|
||||
The database needs to be accessible from all worker nodes.
|
||||
**** Alpine
|
||||
#+BEGIN_SRC sh
|
||||
apk add postgresql-server
|
||||
rc-update add postgresql
|
||||
rc-service postgresql start`
|
||||
#+END_SRC
|
||||
|
||||
**** Debian/Devuan:
|
||||
#+BEGIN_SRC sh
|
||||
apt install postgresql
|
||||
#+END_SRC
|
||||
*** Create the database
|
||||
Due to the use of the JSONField, postgresql is required.
|
||||
To get started,
|
||||
create a database and have it owned by the user that runs uncloud
|
||||
(usually "uncloud"):
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
bridge:~# su - postgres
|
||||
bridge:~$ psql
|
||||
postgres=# create role uncloud login;
|
||||
postgres=# create database uncloud owner nico;
|
||||
#+END_SRC
|
||||
*** Creating the schema
|
||||
#+BEGIN_SRC sh
|
||||
python manage.py migrate
|
||||
#+END_SRC
|
||||
|
||||
*** Configuring remote access
|
||||
- Get a letsencrypt certificate
|
||||
- Expose SSL ports
|
||||
- Create a user
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
certbot certonly --standalone \
|
||||
-d <yourdbhostname> -m your@email.come \
|
||||
--agree-tos --no-eff-email
|
||||
#+END_SRC
|
||||
|
||||
- Configuring postgresql.conf:
|
||||
#+BEGIN_SRC sh
|
||||
listen_addresses = '*' # what IP address(es) to listen on;
|
||||
ssl = on
|
||||
ssl_cert_file = '/etc/postgresql/server.crt'
|
||||
ssl_key_file = '/etc/postgresql/server.key'
|
||||
|
||||
#+END_SRC
|
||||
|
||||
- Cannot load directly due to permission error:
|
||||
2020-12-26 13:01:55.235 CET [27805] FATAL: could not load server
|
||||
certificate file
|
||||
"/etc/letsencrypt/live/2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/fullchain.pem":
|
||||
Permission denied
|
||||
|
||||
- hook
|
||||
#+BEGIN_SRC sh
|
||||
bridge:/etc/letsencrypt/renewal-hooks/deploy# cat /etc/letsencrypt/renewal-hooks/deploy/postgresql
|
||||
#!/bin/sh
|
||||
|
||||
umask 0177
|
||||
export DOMAIN=2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name
|
||||
export DATA_DIR=/etc/postgresql
|
||||
|
||||
cp /etc/letsencrypt/live/$DOMAIN/fullchain.pem $DATA_DIR/server.crt
|
||||
cp /etc/letsencrypt/live/$DOMAIN/privkey.pem $DATA_DIR/server.key
|
||||
chown postgres:postgres $DATA_DIR/server.crt $DATA_DIR/server.key
|
||||
#+END_SRC
|
||||
|
||||
- Allowing access with md5 encrypted password encrypted via TLS
|
||||
#+BEGIN_SRC sh
|
||||
hostssl all all ::/0 md5
|
||||
#+END_SRC
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
|
||||
postgres=# create role uncloud password '...';
|
||||
CREATE ROLE
|
||||
postgres=# alter role uncloud login ;
|
||||
ALTER ROLE
|
||||
#+END_SRC
|
||||
|
||||
Testing the connection:
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
psql postgresql://uncloud@2a0a-e5c0-0013-0000-9f4b-e619-efe5-a4ac.has-a.name/uncloud?sslmode
|
||||
=require
|
||||
g #+END_SRC
|
||||
|
||||
** Bootstrap
|
||||
- Login via a user so that the user object gets created
|
||||
- Run the following (replace nicocustomer with the username)
|
||||
#+BEGIN_SRC sh
|
||||
python manage.py bootstrap-user --username nicocustomer
|
||||
#+END_SRC
|
||||
|
||||
** Initialise the database
|
||||
While it is not strictly required to add default values to the
|
||||
database, it might significantly reduce the starting time with
|
||||
uncloud.
|
||||
|
||||
To add the default database values run:
|
||||
|
||||
#+BEGIN_SRC shell
|
||||
# Add local objects
|
||||
python manage.py db-add-defaults
|
||||
|
||||
# Import VAT rates
|
||||
python manage.py import-vat-rates
|
||||
#+END_SRC
|
||||
|
||||
** Worker nodes
|
||||
Nodes that realise services (VMHosts, VPNHosts, etc.) need to be
|
||||
accessible from the main node and also need access to the database.
|
||||
|
||||
Workers usually should have an "uncloud" user account, even though
|
||||
strictly speaking the username can be any.
|
||||
*** WireGuardVPN Server
|
||||
- Allow write access to /etc/wireguard for uncloud user
|
||||
- Allow sudo access to "ip" and "wg"
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
chown uncloud /etc/wireguard/
|
||||
[14:30] vpn-2a0ae5c1200:/etc/sudoers.d# cat uncloud
|
||||
app ALL=(ALL) NOPASSWD:/sbin/ip
|
||||
app ALL=(ALL) NOPASSWD:/usr/bin/wg
|
||||
#+END_SRC
|
||||
** Typical source code based deployment
|
||||
- Deploy using bin/deploy.sh on a remote server
|
||||
- Remote server should have
|
||||
- postgresql running, accessible via TLS from outside
|
||||
- rabbitmq-configured [in progress]
|
||||
|
||||
* Testing / CLI Access
|
||||
Access via the commandline (CLI) can be done using curl or
|
||||
httpie. In our examples we will use httpie.
|
||||
** Checkout out the API
|
||||
#+BEGIN_SRC sh
|
||||
http localhost:8000/api/
|
||||
#+END_SRC
|
||||
** Authenticate via ldap user in password store
|
||||
#+BEGIN_SRC sh
|
||||
http --auth nicocustomer:$(pass ldap/nicocustomer) localhost:8000/api/
|
||||
#+END_SRC
|
||||
* Database
|
||||
** uncloud clients access the data base from a variety of outside hosts
|
||||
** So the postgresql data base needs to be remotely accessible
|
||||
** Instead of exposing the tcp socket, we make postgresql bind to localhost via IPv6
|
||||
*** ::1, port 5432
|
||||
** Then we remotely connect to the database server with ssh tunneling
|
||||
*** ssh -L5432:localhost:5432 uncloud-database-host
|
||||
** Configuring your database for SSH based remote access
|
||||
*** host all all ::1/128 trust
|
||||
|
||||
* URLs
|
||||
- api/ - the rest API
|
||||
* uncloud Products
|
||||
** Product features
|
||||
- Dependencies on other products
|
||||
- Minimum parameters (min cpu, min ram, etc).
|
||||
- Can also realise the dcl vm
|
||||
- dualstack vm = VM + IPv4 + SSD
|
||||
- Need to have a non-misguiding name for the "bare VM"
|
||||
- Should support network boot (?)
|
||||
|
||||
** VPN
|
||||
*** How to add a new VPN Host
|
||||
**** Install wireguard to the host
|
||||
**** Install uncloud to the host
|
||||
**** Add `python manage.py vpn --hostname fqdn-of-this-host` to the crontab
|
||||
**** Use the CLI to configure one or more VPN Networks for this host
|
||||
*** Example of adding a VPN host at ungleich
|
||||
**** Create a new dual stack alpine VM
|
||||
**** Add it to DNS as vpn-XXX.ungleich.ch
|
||||
**** Route a /40 network to its IPv6 address
|
||||
**** Install wireguard on it
|
||||
**** TODO [#C] Enable wireguard on boot
|
||||
**** TODO [#C] Create a new VPNPool on uncloud with
|
||||
***** the network address (selecting from our existing pool)
|
||||
***** the network size (/...)
|
||||
***** the vpn host that provides the network (selecting the created VM)
|
||||
***** the wireguard private key of the vpn host (using wg genkey)
|
||||
***** http command
|
||||
```
|
||||
http -a nicoschottelius:$(pass
|
||||
ungleich.ch/nico.schottelius@ungleich.ch)
|
||||
http://localhost:8000/admin/vpnpool/ network=2a0a:e5c1:200:: \
|
||||
network_size=40 subnetwork_size=48
|
||||
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch
|
||||
wireguard_private_key=...
|
||||
```
|
||||
*** Example http commands / REST calls
|
||||
**** creating a new vpn pool
|
||||
http -a nicoschottelius:$(pass
|
||||
ungleich.ch/nico.schottelius@ungleich.ch)
|
||||
http://localhost:8000/admin/vpnpool/ network_size=40
|
||||
subnetwork_size=48 network=2a0a:e5c1:200::
|
||||
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch wireguard_private_key=$(wg
|
||||
genkey)
|
||||
**** Creating a new vpn network
|
||||
*** Creating a VPN pool
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
http -a uncloudadmin:$(pass uncloudadmin) https://localhost:8000/v1/admin/vpnpool/ \
|
||||
network=2a0a:e5c1:200:: network_size=40 subnetwork_size=48 \
|
||||
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch wireguard_private_key=$(wg genkey)
|
||||
#+END_SRC
|
||||
|
||||
This will create the VPNPool 2a0a:e5c1:200::/40 from which /48
|
||||
networks will be used for clients.
|
||||
|
||||
VPNPools can only be managed by staff.
|
||||
|
||||
*** Managing VPNNetworks
|
||||
|
||||
To request a network as a client, use the following call:
|
||||
|
||||
#+BEGIN_SRC sh
|
||||
http -a user:$(pass user) https://localhost:8000/v1/net/vpn/ \
|
||||
network_size=48 \
|
||||
wireguard_public_key=$(wg genkey | tee privatekey | wg pubkey)
|
||||
```
|
||||
|
||||
VPNNetworks can be managed by all authenticated users.
|
||||
|
||||
* Developer Handbook
|
||||
The following section describe decisions / architecture of
|
||||
uncloud. These chapters are intended to be read by developers.
|
||||
** This Documentation
|
||||
This documentation is written in org-mode. To compile it to
|
||||
html/pdf, just open emacs and press *C-c C-e l p*.
|
||||
** Models
|
||||
*** Bill
|
||||
Bills are summarising usage in a specific timeframe. Bills usually
|
||||
spawn one month.
|
||||
*** BillRecord
|
||||
Bill records are used to model the usage of one order during the
|
||||
timeframe.
|
||||
*** Order
|
||||
Orders register the intent of a user to buy something. They might
|
||||
refer to a product. (???)
|
||||
Order register the one time price and the recurring price. These
|
||||
fields should be treated as immutable. If they need to be modified,
|
||||
a new order that replaces the current order should be created.
|
||||
**** Replacing orders
|
||||
If an order is updated, a new order is created and points to the
|
||||
old order. The old order stops one second before the new order
|
||||
starts.
|
||||
|
||||
If a order has been replaced can be seen by its replaced_by count:
|
||||
#+BEGIN_SRC sh
|
||||
>>> Order.objects.get(id=1).replaced_by.count()
|
||||
1
|
||||
#+END_SRC
|
||||
|
||||
*** Product and Product Children
|
||||
- A product describes something a user can buy
|
||||
- A product inherits from the uncloud_pay.models.Product model to
|
||||
get basic attributes
|
||||
** Identifiers
|
||||
*** Problem description
|
||||
Identifiers can be integers, strings or other objects. They should
|
||||
be unique.
|
||||
*** Approach 1: integers
|
||||
Integers are somewhat easy to remember, but also include
|
||||
predictable growth, which might allow access to guessed hacking
|
||||
(obivously proper permissions should prevent this).
|
||||
*** Approach 2: random uuids
|
||||
UUIDs are 128 bit integers. Python supports uuid.uuid4() for random
|
||||
uuids.
|
||||
*** Approach 3: IPv6 addresses
|
||||
uncloud heavily depends on IPv6 in the first place. uncloud could
|
||||
use a /48 to identify all objects. Objects that have IPv6 addresses
|
||||
on their own, don't need to draw from the system /48.
|
||||
**** Possible Subnetworks
|
||||
Assuming uncloud uses a /48 to represent all resources.
|
||||
|
||||
| Network | Name | Description |
|
||||
|-----------------+-----------------+----------------------------------------------|
|
||||
| 2001:db8::/48 | uncloud network | All identifiers drawn from here |
|
||||
| 2001:db8:1::/64 | VM network | Every VM has an IPv6 address in this network |
|
||||
| 2001:db8:2::/64 | Bill network | Every bill has an IPv6 address |
|
||||
| 2001:db8:3::/64 | Order network | Every order has an IPv6 address |
|
||||
| 2001:db8:5::/64 | Product network | Every product (?) has an IPv6 address |
|
||||
| 2001:db8:4::/64 | Disk network | Every disk is identified |
|
||||
|
||||
**** Tests
|
||||
[15:47:37] black3.place6:~# rbd create -s 10G ssd/2a0a:e5c0:1::8
|
||||
|
||||
*** Decision
|
||||
We use integers, because they are easy.
|
||||
|
||||
** Distributing/Dispatching/Orchestrating
|
||||
*** Variant 1: using cdist
|
||||
- The uncloud server can git commit things
|
||||
- The uncloud server loads cdist and configures the server
|
||||
- Advantages
|
||||
- Fully integrated into normal flow
|
||||
- Disadvantage
|
||||
- web frontend has access to more data than it needs
|
||||
- On compromise of the machine, more data leaks
|
||||
- Some cdist usual delay
|
||||
*** Variant 2: via celery
|
||||
- The uncloud server dispatches via celery
|
||||
- Every decentral node also runs celery/connects to the broker
|
||||
- Summary brokers:
|
||||
- If local only celery -> good to use redis - Broker
|
||||
- If remote: probably better to use rabbitmq
|
||||
- redis
|
||||
- simpler
|
||||
- rabbitmq
|
||||
- more versatile
|
||||
- made for remote connections
|
||||
- quorom queues would be nice, but not clear if supported
|
||||
- https://github.com/celery/py-amqp/issues/302
|
||||
- https://github.com/celery/celery/issues/6067
|
||||
- Cannot be installed on alpine Linux at the moment
|
||||
- Advantage
|
||||
- Very python / django integrated
|
||||
- Rather instant
|
||||
- Disadvantages
|
||||
- Every decentral node needs to have the uncloud code available
|
||||
- Decentral nodes *might* need to access the database
|
||||
- Tasks can probably be written to work without that
|
||||
(i.e. only strings/bytes)
|
||||
|
||||
**** log/tests
|
||||
(venv) [19:54] vpn-2a0ae5c1200:~/uncloud$ celery -A uncloud -b redis://bridge.place7.ungleich.ch worker -n worker1@%h --logfile ~/celery.log -
|
||||
Q vpn-2a0ae5c1200.ungleich.ch
|
||||
|
||||
|
||||
*** Variant 3: dedicated cdist instance via message broker
|
||||
- A separate VM/machine
|
||||
- Has Checkout of ~/.cdist
|
||||
- Has cdist checkout
|
||||
- Tiny API for management
|
||||
- Not directly web accessible
|
||||
- "cdist" queue
|
||||
|
||||
** Milestones :uncloud:
|
||||
*** 1.1 (cleanup 1)
|
||||
**** TODO [#C] Unify ValidationError, FieldError - define proper Exception
|
||||
- What do we use for model errors
|
||||
**** TODO [#C] Cleanup the results handling in celery
|
||||
- Remove the results broker?
|
||||
- Setup app to ignore results?
|
||||
- Actually use results?
|
||||
*** 1.0 (initial release)
|
||||
**** TODO [#C] Initial Generic product support
|
||||
- Product
|
||||
***** TODO [#C] Recurring product support
|
||||
****** TODO [#C] Support replacing orders for updates
|
||||
****** DONE [#A] Finish split of bill creation
|
||||
CLOSED: [2020-09-11 Fri 23:19]
|
||||
****** TODO [#C] Test the new functions in the Order class
|
||||
****** Define the correct order replacement logic
|
||||
Assumption:
|
||||
- recurringperiods are 30days
|
||||
******* Case 1: downgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to downgrade after 15 days to 5 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days
|
||||
- order 2: 5 CHF starting 30days + 1s
|
||||
- Sum of the two orders is 15 CHF
|
||||
- Question is
|
||||
- when is the VM shutdown?
|
||||
- a) instantly
|
||||
- b) at the end of the cycle
|
||||
- best solution
|
||||
- user can choose between a ... b any time
|
||||
******* Duration
|
||||
- You cannot cancel the duration
|
||||
- You can upgrade and with that cancel the duration
|
||||
- The idea of a duration is that you commit for it
|
||||
- If you want to commit lower (daily basis for instance) you
|
||||
have higher per period prices
|
||||
******* Case X
|
||||
- User has VM with 2 Core / 2 GB RAM
|
||||
- User modifies with to 1 core / 3 GB RAM
|
||||
- We treat it as down/upgrade independent of the modifications
|
||||
|
||||
******* Case 2: upgrading after 1 day
|
||||
- committed for 30 days
|
||||
- upgrade after 1 day
|
||||
- so first order will be charged for 1/30ths
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Order 1 : 1 VM with 2 Core / 2 GB / 10 SSD -- 10 CHF
|
||||
- 30days period, stopped after 15, so quantity is 0.5 = 5 CHF
|
||||
- Order 2 : 1 VM with 2 Core / 6 GB / 10 SSD -- 20 CHF
|
||||
- after 15 days
|
||||
- VM is upgraded instantly
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +15days = 0.5 units = 5 CHF
|
||||
- order 2: 20 CHF starting 15days + 1s ... +30 days after
|
||||
the 15 days -> 45 days = 1 unit = 20 CHF
|
||||
- Total on bill: 25 CHF
|
||||
|
||||
******* Case 2: upgrading
|
||||
- User commits to 10 CHF for 30 days
|
||||
- Wants to upgrade after 15 days to 20 CHF product
|
||||
- Expected result:
|
||||
- order 1: 10 CHF until +30days = 1 units = 10 CHF
|
||||
|
||||
- order 2: 20 CHF starting 15days + 1s = 1 unit = 20 CHF
|
||||
- Total on bill: 30 CHF
|
||||
|
||||
|
||||
****** TODO [#C] Note: ending date not set if replaced by default (implicit!)
|
||||
- Should the new order modify the old order on save()?
|
||||
****** DONE Fix totally wrong bill dates in our test case
|
||||
CLOSED: [2020-09-09 Wed 01:00]
|
||||
- 2020 used instead of 2019
|
||||
- Was due to existing test data ...
|
||||
***** DONE Bill logic is still wrong
|
||||
CLOSED: [2020-11-05 Thu 18:58]
|
||||
- Bill starting_date is the date of the first order
|
||||
- However first encountered order does not have to be the
|
||||
earliest in the bill!
|
||||
- Bills should not have a duration
|
||||
- Bills should only have a (unique) issue date
|
||||
- We charge based on bill_records
|
||||
- Last time charged issue date of the bill OR earliest date
|
||||
after that
|
||||
- Every bill generation checks all (relevant) orders
|
||||
- add a flag "not_for_billing" or "closed"
|
||||
- query on that flag
|
||||
- verify it every time
|
||||
|
||||
***** TODO Generating bill for admins/staff
|
||||
-
|
||||
|
||||
|
||||
|
||||
|
||||
**** Bill fixes needed
|
||||
***** TODO Double bill in bill id
|
||||
***** TODO Name the currency
|
||||
***** TODO Maybe remove the chromium pdf rendering artefacts
|
||||
- date on the top
|
||||
- title on the top
|
||||
- filename bottom left
|
||||
- page number could even stay
|
||||
***** TODO Try to shorten the timestamp (remove time zone?)
|
||||
***** TODO Bill date might be required
|
||||
***** TODO Total and VAT are empty
|
||||
***** TODO Line below detail/ heading
|
|
@ -1,4 +0,0 @@
|
|||
from django.contrib import admin
|
||||
from .models import VMInstance
|
||||
|
||||
admin.site.register(VMInstance)
|
|
@ -1,9 +0,0 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class MatrixhostingConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'matrixhosting'
|
||||
|
||||
def ready(self):
|
||||
from . import signals
|
|
@ -1,48 +0,0 @@
|
|||
import tldextract
|
||||
|
||||
from django import forms
|
||||
from django.forms import ModelForm
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from .validators import domain_name_validator
|
||||
from uncloud_pay.models import BillingAddress
|
||||
|
||||
|
||||
class DomainNameField(forms.CharField):
|
||||
description = 'Domain name form field'
|
||||
default_validators = [domain_name_validator, ]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DomainNameField, self).__init__(*args, **kwargs)
|
||||
|
||||
class RequestHostedVMForm(forms.Form):
|
||||
cores = forms.IntegerField(label='CPU', min_value=1, max_value=48, initial=1)
|
||||
memory = forms.IntegerField(label='RAM', min_value=2, max_value=200, initial=2)
|
||||
storage = forms.IntegerField(label='Storage', min_value=100, max_value=10000, initial=100)
|
||||
matrix_domain = DomainNameField(required=True)
|
||||
homeserver_domain = DomainNameField(required=True)
|
||||
webclient_domain = DomainNameField(required=True)
|
||||
is_open_registration = forms.BooleanField(required=False, initial=False)
|
||||
pricing_name = forms.CharField(required=True)
|
||||
|
||||
def clean(self):
|
||||
homeserver_domain = self.cleaned_data.get('homeserver_domain', False)
|
||||
webclient_domain = self.cleaned_data.get('webclient_domain', False)
|
||||
if homeserver_domain and webclient_domain:
|
||||
# Homserver-Domain and Webclient-Domain cannot be below the same second level domain (i.e. homeserver.abc.ch and webclient.def.cloud are ok,
|
||||
# homeserver.abc.ch and webclient.abc.ch are not ok
|
||||
homeserver_base = tldextract.extract(homeserver_domain).domain
|
||||
webclient_base = tldextract.extract(webclient_domain).domain
|
||||
if homeserver_base == webclient_base:
|
||||
self._errors['webclient_domain'] = self.error_class([
|
||||
'Homserver-Domain and Webclient-Domain cannot be below the same second level domain'])
|
||||
return self.cleaned_data
|
||||
|
||||
|
||||
class BillingAddressForm(ModelForm):
|
||||
class Meta:
|
||||
model = BillingAddress
|
||||
fields = ['full_name', 'street',
|
||||
'city', 'postal_code', 'country', 'vat_number', 'active', 'owner']
|
||||
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-06-30 07:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VMPricing',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True)),
|
||||
('vat_inclusive', models.BooleanField(default=True)),
|
||||
('vat_percentage', models.DecimalField(blank=True, decimal_places=5, default=0, max_digits=7)),
|
||||
('set_up_fees', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
|
||||
('cores_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
|
||||
('ram_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
|
||||
('storage_unit_price', models.DecimalField(decimal_places=5, default=0, max_digits=7)),
|
||||
('discount_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('discount_amount', models.DecimalField(decimal_places=2, default=0, max_digits=6)),
|
||||
('stripe_coupon_id', models.CharField(blank=True, max_length=255, null=True)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-01 08:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('matrixhosting', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='VMPricing',
|
||||
new_name='MatrixVMPricing',
|
||||
),
|
||||
]
|
|
@ -1,33 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-03 15:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('matrixhosting', '0002_rename_vmpricing_matrixvmpricing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='matrixvmpricing',
|
||||
name='cores_unit_price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='matrixvmpricing',
|
||||
name='ram_unit_price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='matrixvmpricing',
|
||||
name='set_up_fees',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='matrixvmpricing',
|
||||
name='storage_unit_price',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=7),
|
||||
),
|
||||
]
|
|
@ -1,43 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-05 06:52
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0014_auto_20210703_1747'),
|
||||
('matrixhosting', '0003_auto_20210703_1523'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VMSpecs',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('cores', models.IntegerField(default=1)),
|
||||
('memory', models.IntegerField(default=2)),
|
||||
('storage', models.IntegerField(default=100)),
|
||||
('matrix_domain', models.CharField(max_length=255)),
|
||||
('homeserver_domain', models.CharField(max_length=255)),
|
||||
('webclient_domain', models.CharField(max_length=255)),
|
||||
('is_open_registration', models.BooleanField(default=False, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MatrixHostingOrder',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('vm_id', models.IntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('status', models.CharField(choices=[('draft', 'Draft'), ('declined', 'Declined'), ('approved', 'Approved')], default='draft', max_length=100)),
|
||||
('stripe_charge_id', models.CharField(max_length=100, null=True)),
|
||||
('price', models.FloatField()),
|
||||
('billing_address', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='uncloud_pay.billingaddress')),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.stripecustomer')),
|
||||
('specs', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='matrixhosting.vmspecs')),
|
||||
('vm_pricing', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='matrixhosting.matrixvmpricing')),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-05 08:49
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('matrixhosting', '0004_matrixhostingorder_vmspecs'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='MatrixHostingOrder',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='VMSpecs',
|
||||
),
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-06 13:21
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('matrixhosting', '0005_auto_20210705_0849'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='MatrixVMPricing',
|
||||
),
|
||||
]
|
|
@ -1,31 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-09 09:14
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('uncloud_pay', '0021_auto_20210709_0914'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('matrixhosting', '0006_delete_matrixvmpricing'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VMInstance',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip', models.TextField(default='')),
|
||||
('config', models.JSONField()),
|
||||
('creation_date', models.DateTimeField(auto_now_add=True)),
|
||||
('termination_date', models.DateTimeField(blank=True, null=True)),
|
||||
('order', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='instance_id', to='uncloud_pay.order')),
|
||||
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-13 10:20
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('matrixhosting', '0008_remove_vminstance_ip'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='vminstance',
|
||||
name='vm_id',
|
||||
field=models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
|
||||
),
|
||||
]
|
|
@ -1,77 +0,0 @@
|
|||
import logging
|
||||
import uuid
|
||||
import os
|
||||
import sys
|
||||
import gitlab
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from uncloud_pay.models import Order
|
||||
|
||||
|
||||
# Initialize logger.
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class VMInstance(models.Model):
|
||||
owner = models.ForeignKey(get_user_model(),
|
||||
on_delete=models.CASCADE,
|
||||
editable=True)
|
||||
|
||||
vm_id = models.UUIDField(default=uuid.uuid4, editable=False, unique=True)
|
||||
|
||||
config = models.JSONField(null=False, blank=False)
|
||||
|
||||
order = models.OneToOneField(Order, on_delete=models.CASCADE, related_name='instance_id')
|
||||
|
||||
creation_date = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
termination_date = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Read the deployment yaml file and render the template
|
||||
# Then save it as new yaml file and push it to github repo
|
||||
if 'test' in sys.argv:
|
||||
return super().save(*args, **kwargs)
|
||||
template_dir = os.path.join(os.path.dirname(__file__), 'yaml')
|
||||
env = Environment(loader = FileSystemLoader(template_dir),autoescape = True)
|
||||
tmpl = env.get_template('deployment.yaml.tmpl')
|
||||
result = tmpl.render(
|
||||
name=self.vm_id
|
||||
)
|
||||
gl = gitlab.Gitlab(settings.GITLAB_SERVER, oauth_token=settings.GITLAB_OAUTH_TOKEN)
|
||||
project = gl.projects.get(settings.GITLAB_PROJECT_ID)
|
||||
project.files.create({'file_path': settings.GITLAB_YAML_DIR + f'{self.vm_id}.yaml',
|
||||
'branch': 'master',
|
||||
'content': result,
|
||||
'author_email': settings.GITLAB_AUTHOR_EMAIL,
|
||||
'author_name': settings.GITLAB_AUTHOR_NAME,
|
||||
'commit_message': f'Add New Deployment for {self.vm_id}'})
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# Delete the deployment yaml file first then
|
||||
# Then delete it
|
||||
if 'test' in sys.argv:
|
||||
return super().delete(*args, **kwargs)
|
||||
gl = gitlab.Gitlab(settings.GITLAB_SERVER, oauth_token=settings.GITLAB_OAUTH_TOKEN)
|
||||
project = gl.projects.get(settings.GITLAB_PROJECT_ID)
|
||||
f_path = settings.GITLAB_YAML_DIR + f'{self.vm_id}.yaml'
|
||||
file = project.files.get(file_path=f_path, ref='master')
|
||||
if file:
|
||||
project.files.delete(file_path=f_path,
|
||||
commit_message=f'Delete {self.vm_id}', branch='master',
|
||||
author_email=settings.GITLAB_AUTHOR_EMAIL,
|
||||
author_name=settings.GITLAB_AUTHOR_NAME)
|
||||
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.id}-{self.order}"
|
||||
|
||||
def delete_for_bill(self, bill):
|
||||
#TODO delete related instances
|
||||
return True
|
|
@ -1,8 +0,0 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from .models import *
|
||||
|
||||
class VMInstanceSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = VMInstance
|
||||
fields = '__all__'
|
|
@ -1,10 +0,0 @@
|
|||
from matrixhosting.models import VMInstance
|
||||
from uncloud_pay.models import Order
|
||||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
@receiver(post_save, sender=Order)
|
||||
def create_instance(sender, instance, created, **kwargs):
|
||||
machine = VMInstance.objects.filter(order=instance).first()
|
||||
if not machine:
|
||||
VMInstance.objects.create(owner=instance.owner, order=instance, config=instance.config)
|
File diff suppressed because it is too large
Load diff
|
@ -1,618 +0,0 @@
|
|||
.navbar-transparent #logoWhite {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-transparent #logoBlack {
|
||||
display: block;
|
||||
width: 220px;
|
||||
}
|
||||
|
||||
.topnav .navbar-fixed-top .navbar-collapse {
|
||||
max-height: 740px;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-header {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.navbar-right .highlights-dropdown .dropdown-menu {
|
||||
left: 0 !important;
|
||||
min-width: 155px;
|
||||
margin-left: 15px;
|
||||
padding: 0 5px 8px !important;
|
||||
}
|
||||
|
||||
@media(min-width: 768px) {
|
||||
.navbar-default .navbar-nav>li a,
|
||||
.navbar-right .highlights-dropdown .dropdown-menu>li a {
|
||||
font-weight: 300;
|
||||
}
|
||||
.navbar-right .highlights-dropdown .dropdown-menu {
|
||||
border-width: 0 0 1px 0;
|
||||
border-color: #e7e7e7;
|
||||
box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-right .highlights-dropdown .dropdown-menu>li a {
|
||||
font-size: 13px;
|
||||
font-family: 'Lato', sans-serif;
|
||||
padding: 1px 10px 1px 18px !important;
|
||||
background: transparent;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.navbar-right .highlights-dropdown .dropdown-menu>li a:hover,
|
||||
.navbar-right .highlights-dropdown .dropdown-menu>li a:focus,
|
||||
.navbar-right .highlights-dropdown .dropdown-menu>li a:active {
|
||||
background: transparent;
|
||||
text-decoration: underline !important;
|
||||
}
|
||||
|
||||
.un-icon {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
opacity: 0.5;
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
|
||||
/***** DCL payment page **********/
|
||||
|
||||
.dcl-order-container {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-place-order-text {
|
||||
color: #808080;
|
||||
}
|
||||
|
||||
.card-warning-content {
|
||||
font-weight: 300;
|
||||
border: 1px solid #a1a1a1;
|
||||
border-radius: 3px;
|
||||
padding: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.card-warning-error {
|
||||
border: 1px solid #EB4D5C;
|
||||
color: #EB4D5C;
|
||||
}
|
||||
|
||||
.card-warning-addtional-margin {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.card-cvc-element label {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.card-element {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-element label {
|
||||
width: 100%;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.my-input {
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.card-cvc-element .my-input {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#card-errors {
|
||||
clear: both;
|
||||
padding: 0 0 10px;
|
||||
color: #eb4d5c;
|
||||
}
|
||||
|
||||
.credit-card-goup {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.card-expiry-element {
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
.card-cvc-element {
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
#billing-form .form-control {
|
||||
box-shadow: none !important;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
.dcl-order-container {
|
||||
width: 990px;
|
||||
padding: 0 15px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-vm p.copyright {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav>.open>a,
|
||||
.navbar-default .navbar-nav>.open>a:focus,
|
||||
.navbar-default .navbar-nav>.open>a:hover,
|
||||
.navbar-default .navbar-nav>.active>a,
|
||||
.navbar-default .navbar-nav>.active>a:focus,
|
||||
.navbar-default .navbar-nav>.active>a:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.navbar-default .navbar-nav .open .dropdown-menu>.active a,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu>.active a:focus,
|
||||
.navbar-default .navbar-nav .open .dropdown-menu>.active a:hover {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* bootstrap input box-shadow disable */
|
||||
|
||||
.has-error .form-control:focus,
|
||||
.has-error .form-control:active,
|
||||
.has-success .form-control:focus,
|
||||
.has-success .form-control:active {
|
||||
box-shadow: inset 0 0 1px rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
|
||||
.content-dashboard {
|
||||
min-height: calc(100vh - 96px);
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
max-width: 1120px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.content-dashboard {
|
||||
padding: 0 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575px) {
|
||||
select {
|
||||
width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn:focus,
|
||||
.btn:active:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/***********Styles for Model********************/
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0px;
|
||||
font-family: Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
width: 100%;
|
||||
float: left;
|
||||
border-radius: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
min-height: 30px;
|
||||
border-bottom: 0px solid #e5e5e5;
|
||||
padding: 0px 15px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.modal-header .close {
|
||||
font-size: 75px;
|
||||
font-weight: 300;
|
||||
margin-top: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 11px;
|
||||
z-index: 10;
|
||||
line-height: 60px;
|
||||
}
|
||||
|
||||
.modal-header .close span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.modal-header .close:focus {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
float: left;
|
||||
padding: 0px 30px 15px 30px;
|
||||
}
|
||||
|
||||
.modal-body .modal-icon i {
|
||||
font-size: 80px;
|
||||
font-weight: 100;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-body .modal-icon {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
margin: 0;
|
||||
line-height: 1.42857143;
|
||||
font-size: 25px;
|
||||
padding: 0;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.modal-text {
|
||||
padding-top: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.modal-text p:not(:last-of-type) {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.modal-title+.modal-footer {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 0px solid #e5e5e5;
|
||||
width: 100%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
padding: 15px 15px;
|
||||
}
|
||||
|
||||
.modal {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
vertical-align: middle;
|
||||
width: 40%;
|
||||
margin: 15px auto;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.modal-dialog {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.modal-dialog {
|
||||
width: 95%;
|
||||
}
|
||||
}
|
||||
|
||||
@media(min-width: 576px) {
|
||||
.modal:before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
margin-right: -4px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* ========= */
|
||||
|
||||
.btn-wide {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.choice-btn {
|
||||
min-width: 110px;
|
||||
background-color: #3C5480;
|
||||
color: #fff;
|
||||
border: 2px solid #3C5480;
|
||||
padding: 4px 10px;
|
||||
transition: 0.3s all ease-out;
|
||||
}
|
||||
|
||||
.choice-btn:focus,
|
||||
.choice-btn:hover,
|
||||
.choice-btn:active {
|
||||
color: #3C5480;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.choice-btn {
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.payment-container {
|
||||
padding-top: 70px;
|
||||
padding-bottom: 11%;
|
||||
}
|
||||
|
||||
.last-p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dcl-payment-section {
|
||||
max-width: 391px;
|
||||
margin: 0 auto 30px;
|
||||
padding: 0 10px 30px;
|
||||
border-bottom: 1px solid #edebeb;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.dcl-payment-section hr {
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.dcl-payment-section .top-hr {
|
||||
margin-left: -10px;
|
||||
}
|
||||
|
||||
.dcl-payment-section h3 {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dcl-payment-section p {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.dcl-payment-section .card-warning-content {
|
||||
padding: 8px 10px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-payment-order strong {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.dcl-payment-order p {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.dcl-payment-section .form-group {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.dcl-payment-section .form-control {
|
||||
box-shadow: none;
|
||||
padding: 6px 12px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.dcl-payment-user {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dcl-payment-user h4 {
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.dcl-payment-grid {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.dcl-payment-box {
|
||||
width: 50%;
|
||||
position: relative;
|
||||
padding: 0 30px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2) {
|
||||
order: 1;
|
||||
}
|
||||
.dcl-payment-box:nth-child(4) {
|
||||
order: 2;
|
||||
}
|
||||
.dcl-payment-section {
|
||||
padding-top: 15px;
|
||||
padding-bottom: 15px;
|
||||
margin-bottom: 0;
|
||||
border-bottom-width: 5px;
|
||||
}
|
||||
.dcl-payment-box:nth-child(2n) .dcl-payment-section {
|
||||
border-bottom: none;
|
||||
}
|
||||
.dcl-payment-box:nth-child(1):after,
|
||||
.dcl-payment-box:nth-child(2):after {
|
||||
content: ' ';
|
||||
display: block;
|
||||
background: #eee;
|
||||
width: 1px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
top: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#virtual_machine_create_form {
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.btn-vm-contact {
|
||||
color: #fff;
|
||||
background: #A3C0E2;
|
||||
border: 2px solid #A3C0E2;
|
||||
padding: 5px 25px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1.3px;
|
||||
}
|
||||
|
||||
.btn-vm-contact:hover,
|
||||
.btn-vm-contact:focus {
|
||||
background: #fff;
|
||||
color: #a3c0e2;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* hosting-order */
|
||||
|
||||
.order-detail-container {
|
||||
max-width: 600px;
|
||||
margin: 100px auto 40px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 30px 30px 20px;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.order-detail-container .dashboard-title-thin {
|
||||
margin-top: 0;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.order-detail-container .dashboard-title-thin .un-icon {
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
.order-detail-container .dashboard-container-head {
|
||||
position: relative;
|
||||
padding: 0;
|
||||
margin-bottom: 38px;
|
||||
}
|
||||
|
||||
.order-detail-container .order-details {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.order-detail-container h4 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.order-detail-container p {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.order-detail-container hr {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .thin-hr {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.order-detail-container .subtotal-price .text-primary {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.order-detail-container .total-price {
|
||||
font-size: 18px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.order-detail-container {
|
||||
padding: 15px;
|
||||
}
|
||||
.order-confirm-btn {
|
||||
text-align: center;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.order-detail-container .dashboard-container-options {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: -4px;
|
||||
}
|
||||
.order-detail-container .dashboard-container-options .svg-img {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.order_detail_footer {
|
||||
font-size: 9px;
|
||||
letter-spacing: 1px;
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.order_detail_footer strong {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.order_detail_footer small {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.dashboard-title-thin {
|
||||
font-weight: 300;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.dashboard-title-thin .un-icon {
|
||||
height: 34px;
|
||||
margin-right: 5px;
|
||||
margin-top: -2px;
|
||||
width: 34px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@media (max-width:767px) {
|
||||
.dashboard-title-thin {
|
||||
font-size: 22px;
|
||||
}
|
||||
.dashboard-title-thin .un-icon {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-top: -3px;
|
||||
}
|
||||
}
|
||||
|
||||
.locale_date {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.locale_date.done {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.btn-vm-back {
|
||||
color: #fff;
|
||||
background: #C4CEDA;
|
||||
border: 2px solid #C4CEDA;
|
||||
padding: 5px 25px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 1.3px;
|
||||
}
|
||||
|
||||
.btn-vm-back:hover,
|
||||
.btn-vm-back:focus {
|
||||
color: #fff;
|
||||
background: #8da4c0;
|
||||
border-color: #8da4c0;
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
(function($) {
|
||||
"use strict"; // Start of use strict
|
||||
|
||||
$(document).ready(function() {
|
||||
function fetch_pricing() {
|
||||
var url = '/matrix/pricing/' + $('#pricing_name').val() + '/calculate/';
|
||||
var cores = $('#cores').val();
|
||||
var memory = $('#memory').val();
|
||||
var storage = $('#storage').val();
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: url,
|
||||
data: { cores: cores, memory: memory, storage: storage},
|
||||
dataType: 'json',
|
||||
success: function (data) {
|
||||
if (data && data['price']) {
|
||||
$('#total').text(data['price']);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function incrementValue(e) {
|
||||
var valueElement = $(e.target).parent().parent().find('input');
|
||||
var step = $(valueElement).attr('step');
|
||||
var min = parseInt($(valueElement).attr('min'));
|
||||
var max = parseInt($(valueElement).attr('max'));
|
||||
var new_value = 0;
|
||||
if (e.data.inc == 1) {
|
||||
new_value = Math.min(parseInt($(valueElement).val()) + parseInt(step) * e.data.inc, max);
|
||||
} else {
|
||||
new_value = Math.max(parseInt($(valueElement).val()) + parseInt(step) * e.data.inc, min);
|
||||
}
|
||||
$(valueElement).val(new_value);
|
||||
fetch_pricing();
|
||||
return false;
|
||||
};
|
||||
if ($('#pricing_name') != undefined) {
|
||||
fetch_pricing();
|
||||
}
|
||||
|
||||
$('.fa-plus-circle.right').bind('click', {inc: 1}, incrementValue);
|
||||
|
||||
$('.fa-minus-circle.left').bind('click', {inc: -1}, incrementValue);
|
||||
});
|
||||
})(jQuery);
|
|
@ -1,36 +0,0 @@
|
|||
$( document ).ready(function() {
|
||||
var create_vm_form = $('#virtual_machine_create_form');
|
||||
create_vm_form.submit(placeOrderPayment);
|
||||
function placeOrderPayment(e) {
|
||||
e.preventDefault();
|
||||
$.ajax({
|
||||
url: create_vm_form.attr('action'),
|
||||
type: 'POST',
|
||||
data: create_vm_form.serialize(),
|
||||
init: function () {
|
||||
ok_btn = $('#createvm-modal-done-btn');
|
||||
close_btn = $('#createvm-modal-close-btn');
|
||||
ok_btn.addClass('btn btn-success btn-ok btn-wide hide');
|
||||
close_btn.addClass('btn btn-danger btn-ok btn-wide hide');
|
||||
},
|
||||
success: function (data) {
|
||||
fa_icon = $('.modal-icon').find('.fa-cog');
|
||||
modal_btn = $('#createvm-modal-done-btn');
|
||||
if (data.error) {
|
||||
// Display error.message in your UI.
|
||||
modal_btn.attr('href', error_url).removeClass('visually-hidden');
|
||||
fa_icon.attr('class', 'fa fa-close');
|
||||
modal_btn.attr('class', '').addClass('btn btn-danger btn-ok btn-wide');
|
||||
$('#createvm-modal-title').text("Error Occurred");
|
||||
$('#createvm-modal-body').html(data.error.message);
|
||||
} else {
|
||||
// The payment has succeeded
|
||||
// Display a success message
|
||||
modal_btn.attr('href', data.redirect).removeClass('visually-hidden');
|
||||
$('#createvm-modal-title').text("Order Succeeded");
|
||||
$('#createvm-modal-body').html("Order has been added and the instance will be ready soon");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,204 +0,0 @@
|
|||
var cardBrandToPfClass = {
|
||||
'visa': 'pf-visa',
|
||||
'mastercard': 'pf-mastercard',
|
||||
'amex': 'pf-american-express',
|
||||
'discover': 'pf-discover',
|
||||
'diners': 'pf-diners',
|
||||
'jcb': 'pf-jcb',
|
||||
'unknown': 'pf-credit-card'
|
||||
};
|
||||
function setBrandIcon(brand) {
|
||||
var brandIconElement = document.getElementById('brand-icon');
|
||||
var pfClass = 'pf-credit-card';
|
||||
if (brand in cardBrandToPfClass) {
|
||||
pfClass = cardBrandToPfClass[brand];
|
||||
}
|
||||
for (var i = brandIconElement.classList.length - 1; i >= 0; i--) {
|
||||
brandIconElement.classList.remove(brandIconElement.classList[i]);
|
||||
}
|
||||
brandIconElement.classList.add('pf');
|
||||
brandIconElement.classList.add(pfClass);
|
||||
}
|
||||
|
||||
|
||||
$(document).ready(function () {
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr, settings) {
|
||||
function getCookie(name) {
|
||||
var cookieValue = null;
|
||||
if (document.cookie && document.cookie != '') {
|
||||
var cookies = document.cookie.split(';');
|
||||
for (var i = 0; i < cookies.length; i++) {
|
||||
var cookie = jQuery.trim(cookies[i]);
|
||||
// Does this cookie string begin with the name we want?
|
||||
if (cookie.substring(0, name.length + 1) == (name + '=')) {
|
||||
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieValue;
|
||||
}
|
||||
|
||||
if (!(/^http:.*/.test(settings.url) || /^https:.*/.test(settings.url))) {
|
||||
// Only send the token to relative URLs i.e. locally.
|
||||
xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
var hasCreditcard = window.hasCreditcard || false;
|
||||
if (!hasCreditcard && window.stripeKey) {
|
||||
var stripe = Stripe(window.stripeKey);
|
||||
if (window.pm_id) {
|
||||
|
||||
} else {
|
||||
var element_style = {
|
||||
fonts: [{
|
||||
family: 'lato-light',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Light.woff) format("woff2")'
|
||||
}, {
|
||||
family: 'lato-regular',
|
||||
src: 'url(https://cdn.jsdelivr.net/font-lato/2.0/Lato/Lato-Regular.woff) format("woff2")'
|
||||
}
|
||||
],
|
||||
locale: window.current_lan
|
||||
};
|
||||
var elements = stripe.elements(element_style);
|
||||
var credit_card_text_style = {
|
||||
base: {
|
||||
iconColor: '#666EE8',
|
||||
color: '#31325F',
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-light', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
color: '#777'
|
||||
}
|
||||
},
|
||||
invalid: {
|
||||
iconColor: '#eb4d5c',
|
||||
color: '#eb4d5c',
|
||||
lineHeight: '25px',
|
||||
fontWeight: 300,
|
||||
fontFamily: "'lato-regular', sans-serif",
|
||||
fontSize: '14px',
|
||||
'::placeholder': {
|
||||
color: '#eb4d5c',
|
||||
fontWeight: 400
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var enter_ccard_text = "Enter your credit card number";
|
||||
if (typeof window.enter_your_card_text !== 'undefined') {
|
||||
enter_ccard_text = window.enter_your_card_text;
|
||||
}
|
||||
var cardNumberElement = elements.create('cardNumber', {
|
||||
style: credit_card_text_style,
|
||||
placeholder: enter_ccard_text
|
||||
});
|
||||
cardNumberElement.mount('#card-number-element');
|
||||
|
||||
var cardExpiryElement = elements.create('cardExpiry', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardExpiryElement.mount('#card-expiry-element');
|
||||
|
||||
var cardCvcElement = elements.create('cardCvc', {
|
||||
style: credit_card_text_style
|
||||
});
|
||||
cardCvcElement.mount('#card-cvc-element');
|
||||
cardNumberElement.on('change', function (event) {
|
||||
if (event.brand) {
|
||||
setBrandIcon(event.brand);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function submitBillingForm(pmId) {
|
||||
var billing_form = $('#billing-form');
|
||||
billing_form.append('<input type="hidden" name="id_payment_method" value="' + pmId + '" />');
|
||||
billing_form.submit();
|
||||
}
|
||||
|
||||
var $form_new = $('#payment-form-new');
|
||||
$form_new.submit(payWithPaymentIntent);
|
||||
window.result = "";
|
||||
window.card = "";
|
||||
function payWithPaymentIntent(e) {
|
||||
e.preventDefault();
|
||||
function stripePMHandler(paymentMethod) {
|
||||
// Insert the token ID into the form so it gets submitted to the server
|
||||
console.log(paymentMethod);
|
||||
$('#id_payment_method').val(paymentMethod.id);
|
||||
submitBillingForm(paymentMethod.id);
|
||||
}
|
||||
stripe.createPaymentMethod({
|
||||
type: 'card',
|
||||
card: cardNumberElement,
|
||||
})
|
||||
.then(function(result) {
|
||||
// Handle result.error or result.paymentMethod
|
||||
window.result = result;
|
||||
if(result.error) {
|
||||
var errorElement = document.getElementById('card-errors');
|
||||
errorElement.textContent = result.error.message;
|
||||
} else {
|
||||
console.log("created paymentMethod " + result.paymentMethod.id);
|
||||
stripePMHandler(result.paymentMethod);
|
||||
}
|
||||
});
|
||||
window.card = cardNumberElement;
|
||||
}
|
||||
|
||||
/* Form validation */
|
||||
$.validator.addMethod("month", function (value, element) {
|
||||
return this.optional(element) || /^(01|02|03|04|05|06|07|08|09|10|11|12)$/.test(value);
|
||||
}, "Please specify a valid 2-digit month.");
|
||||
|
||||
$.validator.addMethod("year", function (value, element) {
|
||||
return this.optional(element) || /^[0-9]{2}$/.test(value);
|
||||
}, "Please specify a valid 2-digit year.");
|
||||
|
||||
validator = $form_new.validate({
|
||||
rules: {
|
||||
cardNumber: {
|
||||
required: true,
|
||||
creditcard: true,
|
||||
digits: true
|
||||
},
|
||||
expMonth: {
|
||||
required: true,
|
||||
month: true
|
||||
},
|
||||
expYear: {
|
||||
required: true,
|
||||
year: true
|
||||
},
|
||||
cvCode: {
|
||||
required: true,
|
||||
digits: true
|
||||
}
|
||||
},
|
||||
highlight: function (element) {
|
||||
$(element).closest('.form-control').removeClass('success').addClass('error');
|
||||
},
|
||||
unhighlight: function (element) {
|
||||
$(element).closest('.form-control').removeClass('error').addClass('success');
|
||||
},
|
||||
errorPlacement: function (error, element) {
|
||||
$(element).closest('.form-group').append(error);
|
||||
}
|
||||
});
|
||||
|
||||
$('.credit-card-info .btn.choice-btn').click(function () {
|
||||
var id = this.dataset['id_card'];
|
||||
$('#id_card').val(id);
|
||||
submitBillingForm(id);
|
||||
});
|
||||
|
||||
});
|
|
@ -1,64 +0,0 @@
|
|||
import logging
|
||||
from datetime import date, timedelta, timezone
|
||||
from django.conf import settings
|
||||
from django.template.loader import render_to_string
|
||||
from django_q.tasks import async_task, schedule
|
||||
from django_q.models import Schedule
|
||||
from django.db.models import Q
|
||||
from uncloud_pay.models import Bill, Payment
|
||||
from uncloud_pay.selectors import has_enough_balance, get_balance_for_user
|
||||
from .models import VMInstance
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def send_warning_email(bill, html_message):
|
||||
schedule('django.core.mail.send_mail',
|
||||
'Renewal Warning',
|
||||
None,
|
||||
settings.RENEWAL_FROM_EMAIL,
|
||||
[bill.owner.email],
|
||||
html_message,
|
||||
schedule_type=Schedule.ONCE,
|
||||
next_run=timezone.now() + timedelta(hours=1))
|
||||
|
||||
def charge_open_bills():
|
||||
un_paid_bills = Bill.objects.filter(is_closed=False)
|
||||
for bill in un_paid_bills:
|
||||
date_diff = (date.today() - bill.due_date.date()).days
|
||||
# If there is not enough money in the account 7 days before renewal, the system sends a warning
|
||||
# If there is not enough money in the account 3 days before renewal, the system sends a 2nd warning
|
||||
# If on renewal date there is not enough money in the account, delete the instance
|
||||
if date_diff == 7:
|
||||
if not has_enough_balance(bill.owner):
|
||||
context = {'name': bill.owner.name, 'message': "You don't have enough balance for renewal... upload to your account _here"}
|
||||
html_message = render_to_string('matrixhosting/emails/renewal_warning.html', context)
|
||||
send_warning_email(bill, html_message)
|
||||
elif date_diff == 3:
|
||||
if not has_enough_balance(bill.owner):
|
||||
context = {'name': bill.owner.name, 'message': "You don't have enough balance for renewal... Your instance will be deleted in 3 days"}
|
||||
html_message = render_to_string('matrixhosting/emails/renewal_warning.html', context)
|
||||
send_warning_email(bill, html_message)
|
||||
elif date_diff <= 0:
|
||||
if not has_enough_balance(bill.owner):
|
||||
VMInstance.delete_for_bill(bill)
|
||||
else:
|
||||
try:
|
||||
balance = get_balance_for_user(bill.owner)
|
||||
if balance < 0:
|
||||
payment = Payment.objects.create(owner=bill.owner, amount=balance, source='stripe')
|
||||
if payment:
|
||||
bill.close()
|
||||
bill.close()
|
||||
except Exception as e:
|
||||
log.error(f"It seems that there is issue in payment for {bill.owner.name}", e)
|
||||
# do nothing
|
||||
|
||||
|
||||
def process_recurring_orders():
|
||||
"""
|
||||
Check for pending recurring and charge it and generate bills or send the customer warning
|
||||
"""
|
||||
Bill.create_bills_for_all_users()
|
||||
|
||||
def delete_instance(instance_id):
|
||||
VMInstance.objects.delete(instance_id)
|
|
@ -1,60 +0,0 @@
|
|||
{% load static i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
{% load bootstrap5 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{LANGUAGE_CODE}}">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Matrix Hosting by ungleich">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<title>Matrix Hosting - {% block title %} made in Switzerland{% endblock %}</title>
|
||||
|
||||
<!-- Vendor CSS -->
|
||||
<!-- Bootstrap Core CSS -->
|
||||
{% bootstrap_css %}
|
||||
<!-- Icon Fonts -->
|
||||
<link href="{% static 'fontawesome_free/css/all.min.css' %}" rel="stylesheet" type="text/css">
|
||||
<!-- Custom CSS -->
|
||||
<link href="{% static 'matrixhosting/css/common.css' %}" rel="stylesheet">
|
||||
{% block css_extra %}
|
||||
{% endblock css_extra %}
|
||||
|
||||
<!-- External Fonts -->
|
||||
<link href="//fonts.googleapis.com/css?family=Lato:300,400,600,700" rel="stylesheet" type="text/css">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
|
||||
<!-- Google analytics -->
|
||||
<!-- End Google Analytics -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
{% block navbar %}
|
||||
{% include "matrixhosting/includes/_navbar.html" %}
|
||||
{% endblock navbar %}
|
||||
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
|
||||
{% include "matrixhosting/includes/_footer.html" %}
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="{% static 'fontawesome_free/js/all.min.js' %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
{% bootstrap_javascript %}
|
||||
<!-- Custom JS -->
|
||||
<script src="{% static 'matrixhosting/js/main.js' %}"></script>
|
||||
{% block js_extra %}
|
||||
{% endblock js_extra %}
|
||||
</body>
|
||||
</html>
|
|
@ -1,127 +0,0 @@
|
|||
{% extends "matrixhosting/base.html" %} {% load static i18n %}
|
||||
{% block content%}
|
||||
<!-- Page Content -->
|
||||
{% csrf_token %}
|
||||
<div>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">#</th>
|
||||
<th scope="col">Description</th>
|
||||
<th scope="col">Starting At</th>
|
||||
<th scope="col">Config</th>
|
||||
<th scope="col">Pricing Plan</th>
|
||||
<th scope="col">OneTime Price</th>
|
||||
<th scope="col">Recurring Price</th>
|
||||
<th scope="col">Ending At</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for object in object_list %}
|
||||
<tr data-id="{{object.id}}">
|
||||
<th scope="row">{{ object.id }}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
<td>{{ object.starting_date }}</td>
|
||||
<td>{{ object.config }}</td>
|
||||
<td>{{ object.pricing_plan}}</td>
|
||||
<td>{{ object.one_time_price }}</td>
|
||||
<td>{{ object.recurring_price }}</td>
|
||||
<td>{{ object.ending_date }}</td>
|
||||
{% if object.ending_date %}
|
||||
<td></td>
|
||||
{% else %}
|
||||
<td>
|
||||
<button
|
||||
class="btn btn-danger btn-sm cancel-subscription"
|
||||
type="submit"
|
||||
name="action"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="modal fade"
|
||||
tabindex="-1"
|
||||
role="dialog"
|
||||
aria-labelledby="mySmallModalLabel"
|
||||
aria-hidden="true"
|
||||
id="mi-modal"
|
||||
>
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" id="myModalLabel">Cancel Subscription</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Are you sure that you want to cancel this subscription?. </p>
|
||||
<p>
|
||||
The instance will be active till the end date of the last bill and will be deleted
|
||||
after that.
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" id="modal-btn-yes">
|
||||
Yes
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="modal-btn-no">
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="alert" role="alert" id="result"></div>
|
||||
<!-- /.banner -->
|
||||
{% endblock %}
|
||||
|
||||
{% block js_extra %}
|
||||
<script type="text/javascript">
|
||||
var modalConfirm = function (callback) {
|
||||
$(".cancel-subscription").on("click", function (event) {
|
||||
$('.selected').removeClass('selected');
|
||||
$(event.target).parent().parent().addClass('selected');
|
||||
$("#mi-modal").modal("show");
|
||||
});
|
||||
|
||||
$("#modal-btn-yes").on("click", function () {
|
||||
callback(true);
|
||||
});
|
||||
|
||||
$("#modal-btn-no").on("click", function () {
|
||||
callback(false);
|
||||
$("#mi-modal").modal("hide");
|
||||
});
|
||||
};
|
||||
|
||||
modalConfirm(function (confirm) {
|
||||
if (confirm) {
|
||||
var selected_order = $('.selected').data('id');
|
||||
$.ajax({
|
||||
url: '{% url "matrix:dashboard" %}',
|
||||
type: 'POST',
|
||||
data: {'order_id': selected_order, 'csrfmiddlewaretoken': '{{ csrf_token }}',},
|
||||
success: function (data) {
|
||||
$("#mi-modal").modal("hide");
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -1,13 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Renewal Warning</title>
|
||||
</head>
|
||||
<body>
|
||||
hello <strong>{{name}},</strong>
|
||||
{{message}}
|
||||
</body>
|
||||
</html>
|
|
@ -1,101 +0,0 @@
|
|||
{% load static i18n %}
|
||||
|
||||
<form id="order_form" method="POST" action="{% url 'matrix:index' %}" data-toggle="validator" role="form">
|
||||
{% csrf_token %}
|
||||
<div class="title">
|
||||
<h3>{% trans "Matrix Chat hosting" %} </h3>
|
||||
</div>
|
||||
<div class="price">
|
||||
<span id="total"> {{ matrix_vm_pricing.name }}</span>
|
||||
<span>CHF/{% trans "month" %}</span>
|
||||
<div class="price-text">
|
||||
<p>
|
||||
{% if matrix_vm_pricing.set_up_fees %}{{ matrix_vm_pricing.set_up_fees }} CHF Setup<br>{% endif %}
|
||||
{% if matrix_vm_pricing.vat_inclusive %}{% trans "VAT included" %} <br>{% endif %}
|
||||
{% if matrix_vm_pricing.discount_amount %}
|
||||
{% trans "You save" %} {{ matrix_vm_pricing.discount_amount }} CHF
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="descriptions">
|
||||
<div class="description form-group">
|
||||
<p>{% trans "Hosted in Switzerland" %}</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="description input">
|
||||
<i class="fa fa-minus-circle left" data-minus="cores" aria-hidden="true"></i>
|
||||
<input class="input-price select-number" type="number" min="1" max="48" id="cores" step="1" name="cores"
|
||||
{% if form.cores.value != None %}value="{{ form.cores.value }}"{% endif %} data-error="{% trans 'Please enter a value in range 1 - 48.' %}" required>
|
||||
<span> Core</span>
|
||||
<i class="fa fa-plus-circle right" data-plus="cores" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="help-block with-errors">
|
||||
{% for message in messages %}
|
||||
{% if 'cores' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li>{{ message|safe }}</li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="description input">
|
||||
<i class="fa fa-minus-circle left" data-minus="memory" aria-hidden="true"></i>
|
||||
<input id="memory" class="input-price select-number" type="number" min="2" max="200" name="memory"
|
||||
{% if form.memory.value != None %}value="{{ form.memory.value }}"{% endif %} data-error="{% blocktrans with min_ram=min_ram %}Please enter a value in range {{min_ram}} - 200.{% endblocktrans %}" required step="1">
|
||||
<span> GB RAM</span>
|
||||
<i class="fa fa-plus-circle right" data-plus="memory" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="help-block with-errors">
|
||||
{% for message in messages %}
|
||||
{% if 'memory' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
{{ message|safe }}
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="description input">
|
||||
<i class="fa fa-minus-circle left" data-minus="storage" aria-hidden="true"></i>
|
||||
<input id="storage" class="input-price select-number" type="number" min="100" max="10000" step="100"
|
||||
name="storage" {% if form.storage.value != None %}value="{{ form.storage.value }}"{% endif %} data-error="{% trans 'Please enter a value in range 100 - 10000.' %}" required>
|
||||
<span>{% trans "GB Storage (SSD)" %}</span>
|
||||
<i class="fa fa-plus-circle right" data-plus="storage" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="help-block with-errors">
|
||||
{% for message in messages %}
|
||||
{% if 'storage' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
{{ message|safe }}
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="description domain select-configuration input form-group justify-center">
|
||||
<input type="text" id="matrix_domain" name="matrix_domain" placeholder="Matrix Domain" {% if form.matrix_domain.value != None %}value="{{ form.matrix_domain.value }}"{% endif %}></input>
|
||||
<p class="text-danger">{{ form.matrix_domain.errors }}</p>
|
||||
</div>
|
||||
<div class="description domain select-configuration input form-group justify-center">
|
||||
<input type="text" id="homeserver_domain" name="homeserver_domain" placeholder="Homeserver Domain" {% if form.homeserver_domain.value != None %}value="{{ form.homeserver_domain.value }}"{% endif %} ></input>
|
||||
<p class="text-danger">{{ form.homeserver_domain.errors }}</p>
|
||||
</div>
|
||||
<div class="description domain select-configuration input form-group justify-center">
|
||||
<input type="text" id="webclient_domain" name="webclient_domain" placeholder="Webclient Domain" {% if form.webclient_domain.value != None %}value="{{ form.webclient_domain.value }}"{% endif %}></input>
|
||||
<p class="text-danger">{{ form.webclient_domain.errors }}</p>
|
||||
</div>
|
||||
<div class="description input form-group">
|
||||
<div class="fieldWrapper">
|
||||
<span>Is open registration possible:</span>
|
||||
{{ form.is_open_registration }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<input type="hidden" name="pricing_name" id="pricing_name" value="{% if matrix_vm_pricing.name %}{{matrix_vm_pricing.name}}{% else %}unknown{% endif%}"></input>
|
||||
<input type="submit" class="btn btn-primary" value="{% trans 'Continue' %}"></input>
|
||||
</form>
|
|
@ -1,43 +0,0 @@
|
|||
{% load i18n %}
|
||||
<form action="" id="payment-form-new" method="POST">
|
||||
<input type="hidden" name="token"/>
|
||||
<input type="hidden" name="id_card" id="id_card" value=""/>
|
||||
<div class="group">
|
||||
<div class="credit-card-goup">
|
||||
<div class="card-element card-number-element">
|
||||
<label>{%trans "Card Number" %}</label>
|
||||
<div id="card-number-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-5 card-element card-expiry-element">
|
||||
<label>{%trans "Expiry Date" %}</label>
|
||||
<div id="card-expiry-element" class="field my-input"></div>
|
||||
</div>
|
||||
<div class="col-xs-3 col-xs-offset-4 card-element card-cvc-element">
|
||||
<label>{%trans "CVC" %}</label>
|
||||
<div id="card-cvc-element" class="field my-input"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-element brand">
|
||||
<label>{%trans "Card Type" %}</label>
|
||||
<i class="pf pf-credit-card" id="brand-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="card-errors"></div>
|
||||
<div id='payment_error'>
|
||||
{% for message in messages %}
|
||||
{% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %}
|
||||
<ul class="list-unstyled">
|
||||
<li><p class="card-warning-content card-warning-error">{{ message|safe }}</p></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<button class="btn btn-vm-contact btn-wide" type="submit" name="payment-form">{%trans "SUBMIT" %}</button>
|
||||
</div>
|
||||
<div style="display:none;">
|
||||
<p class="payment-errors"></p>
|
||||
</div>
|
||||
</form>
|
|
@ -1,18 +0,0 @@
|
|||
{% load i18n %}
|
||||
<footer>
|
||||
<div class="container">
|
||||
<ul class="list-inline">
|
||||
<li>
|
||||
<a class="url-init" href="">{% trans "Home" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="url-init" href="">{% trans "Contact" %}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="url-init" href="">{% trans "Terms of Service" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p class="copyright text-muted small">Copyright © ungleich glarus ag {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
|
||||
</div>
|
||||
</footer>
|
|
@ -1,33 +0,0 @@
|
|||
{% load static i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{% url 'uncloudindex' %}">Matrix Hosting</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavDropdown" aria-controls="navbarNavDropdown" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavDropdown">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" aria-current="page" href="{% url 'matrix:index' %}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Features</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Pricing</a>
|
||||
</li>
|
||||
{% if not request.user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_login' %}">{% trans "Login" %} <i class="fa fa-sign-in-alt"></i></a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'matrix:dashboard' %}">{% trans "Dashboard" %}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
|
@ -1,21 +0,0 @@
|
|||
{% extends "matrixhosting/base.html" %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Page Content -->
|
||||
|
||||
<div class="split-section pricing-section section-gradient" id="price">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="price-calc-section">
|
||||
<div class="card">
|
||||
{% include "matrixhosting/includes/_calculator_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.banner -->
|
||||
{% endblock %}
|
|
@ -1,268 +0,0 @@
|
|||
{% load static i18n %}
|
||||
{% load bootstrap5 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Matrix Hosting by ungleich">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<title>Matrix Hosting - {% block title %} made in Switzerland{% endblock %}</title>
|
||||
|
||||
<!-- Vendor CSS -->
|
||||
<!-- Bootstrap Core CSS -->
|
||||
{% bootstrap_css %}
|
||||
|
||||
<!-- External Fonts -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paymentfont/1.2.5/css/paymentfont.min.css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=Lato:300,400,600,700" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="{% static 'matrixhosting/css/hosting.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<script>
|
||||
window.paymentIntentSecret = "{{payment_intent_secret}}";
|
||||
</script>
|
||||
<div id="order-detail{{order.pk}}" class="order-detail-container">
|
||||
{% if messages %}
|
||||
<div class="alert alert-warning">
|
||||
{% for message in messages %}
|
||||
<span>{{ message }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if not error %}
|
||||
<div class="dashboard-container-head">
|
||||
<h1 class="dashboard-title-thin">
|
||||
{% blocktrans with page_header_text=page_header_text|default:"Order" %}{{page_header_text}}{% endblocktrans %}
|
||||
</h1>
|
||||
</div>
|
||||
<div class="order-details">
|
||||
<hr>
|
||||
<div>
|
||||
<address>
|
||||
<h4>{% trans "Billed to" %}:</h4>
|
||||
<p>
|
||||
{% with request.session.billing_address_data as billing_address %}
|
||||
{{billing_address.full_name}}<br>
|
||||
{{billing_address.street}}, {{billing_address.postal_code}}<br>
|
||||
{{billing_address.city}}, {{billing_address.country}}
|
||||
{% if billing_address.vat_number %}
|
||||
<br/>{% trans "VAT Number" %} {{billing_address.vat_number}}
|
||||
{% if pricing.vat_country != "ch" and pricing.vat_validation_status != "not_needed" %}
|
||||
{% if pricing.vat_validation_status == "verified" %}
|
||||
<span class="fa fa-fw fa-check-circle" aria-hidden="true" title='{% trans "Your VAT number has been verified" %}'></span>
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-info-circle" aria-hidden="true" title='{% trans "Your VAT number is under validation. VAT will be adjusted, once the validation is complete." %}'></span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</p>
|
||||
</address>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h4>{% trans "Payment method" %}:</h4>
|
||||
<p>
|
||||
{{card.brand|default:_('Credit Card')}} {% trans "ending in" %} ****{{card.last4}}<br>
|
||||
{% trans "Expiry" %} {{card.exp_year}}/{{card.exp_month}}<br/>
|
||||
{{request.user.email}}
|
||||
</p>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
<h4>{% trans "Order summary" %}</h4>
|
||||
<style>
|
||||
@media screen and (max-width:400px){
|
||||
.header-no-left-padding {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
}
|
||||
@media screen and (max-width:767px){
|
||||
.cmf-ord-heading {
|
||||
font-size: 11px;
|
||||
}
|
||||
.order-detail-container .order-details {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width:367px){
|
||||
.cmf-ord-heading {
|
||||
font-size: 11px;
|
||||
}
|
||||
.order-detail-container .order-details {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<p>
|
||||
<strong>{% trans "Product" %}:</strong>
|
||||
Matrix Chat Hosting
|
||||
</p>
|
||||
<div class="row">
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<span>{% trans "Cores" %}: </span>
|
||||
<strong class="pull-right">{{order.cores}}</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Memory" %}: </span>
|
||||
<strong class="pull-right">{{order.memory}} GB</strong>
|
||||
</p>
|
||||
<p>
|
||||
<span>{% trans "Disk space" %}: </span>
|
||||
<strong class="pull-right">{{order.storage}} GB</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<p>
|
||||
<strong class="text-uppercase">{% trans "Price Before VAT" %}</strong>
|
||||
<strong class="pull-right">{{pricing.subtotal|floatformat:2}} CHF</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span></span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "Pre VAT" %}</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4 header-no-left-padding">
|
||||
<p class="text-right"><strong class="cmf-ord-heading">{% trans "With VAT for" %} {{pricing.vat_country}} ({{pricing.vat_percent}}%)</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span>Subtotal</span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><span class="pull-right" >{{pricing.subtotal|floatformat:2}} CHF</span></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><span class="pull-right">{{pricing.price_with_vat|floatformat:2}} CHF</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% if pricing.discount.amount > 0 %}
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><span>{{pricing.discount.name}}</span></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><span class="pull-right">-{{pricing.discount.amount|floatformat:2}} CHF</span></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><span class="pull-right">-{{pricing.discount.amount_with_vat|floatformat:2}} CHF</span></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<div class="row">
|
||||
<div class="col-md-4 col-sm-4 col-xs-4">
|
||||
<p><strong>Total</strong></p>
|
||||
</div>
|
||||
<div class="col-md-3 col-sm-3 col-xs-4">
|
||||
<p><strong class="pull-right">{{pricing.subtotal_after_discount|floatformat:2}} CHF</strong></p>
|
||||
</div>
|
||||
<div class="col-md-5 col-sm-5 col-xs-4">
|
||||
<p><strong class="pull-right">{{pricing.price_after_discount_with_vat|floatformat:2}} CHF</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-12">
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<div class="col-sm-9">
|
||||
<strong class="text-uppercase align-center">{% trans "Your Price in Total" %}</strong>
|
||||
<strong class="total-price pull-right">{{pricing.total_price|floatformat:2}} CHF</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr class="thin-hr">
|
||||
</div>
|
||||
<form id="virtual_machine_create_form" action="{% url 'matrix:order_details' %}" method="POST">
|
||||
{% csrf_token %}
|
||||
<div class="row">
|
||||
<div class="col-sm-8">
|
||||
<div class="dcl-place-order-text">{% blocktrans with vm_total_price=vm.total_price|floatformat:2 %}By clicking "Place order" you agree to our <a href="">Terms of Service</a> and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.</div>
|
||||
</div>
|
||||
<div class="col-sm-4 order-confirm-btn text-right">
|
||||
<button class="btn choice-btn" id="btn-create-vm" data-bs-toggle="modal" data-bs-target="#createvm-modal">
|
||||
{% trans "Place order" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Create VM Modal -->
|
||||
<div class="modal fade" id="createvm-modal" tabindex="-1" role="dialog"
|
||||
aria-hidden="true" data-backdrop="static" data-keyboard="false">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-icon">
|
||||
<i class="fa fa-cog fa-spin fa-3x fa-fw"></i>
|
||||
<span class="sr-only">{% trans "Processing..." %}</span>
|
||||
</div>
|
||||
<h4 class="modal-title" id="createvm-modal-title"></h4>
|
||||
<div class="modal-text" id="createvm-modal-body">
|
||||
{% trans "Hold tight, we are processing your request" %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a id="createvm-modal-done-btn" class="btn btn-success btn-ok btn-wide visually-hidden" href="">{% trans "OK" %}</a>
|
||||
<button id="createvm-modal-close-btn" type="button" class="btn btn-danger btn-ok btn-wide visually-hidden" data-dismiss="modal" aria-label="create-vm-close">{% trans "Close" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- / Create VM Modal -->
|
||||
|
||||
<script type="text/javascript">
|
||||
var create_vm_error_message = 'Some problem encountered. Please try again later';
|
||||
var pm_id = '{{id_payment_method}}';
|
||||
var error_url = '{{ error_msg.redirect }}';
|
||||
var success_url = '{{ success_msg.redirect }}';
|
||||
window.stripeKey = "{{stripe_key}}";
|
||||
window.isSubscription = ("{{is_subscription}}" === 'true');
|
||||
</script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
|
||||
|
||||
<script src="{% static 'fontawesome_free/js/all.min.js' %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
{% bootstrap_javascript %}
|
||||
<!-- Custom JS -->
|
||||
<script type="text/javascript" src="{% static 'matrixhosting/js/order.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,169 +0,0 @@
|
|||
{% load static i18n %}
|
||||
{% load bootstrap5 %}
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="description" content="Matrix Hosting by ungleich">
|
||||
<meta name="author" content="ungleich glarus ag">
|
||||
<title>Matrix Hosting - {% block title %} made in Switzerland{% endblock %}</title>
|
||||
|
||||
<!-- Vendor CSS -->
|
||||
<!-- Bootstrap Core CSS -->
|
||||
{% bootstrap_css %}
|
||||
|
||||
<!-- External Fonts -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/paymentfont/1.2.5/css/paymentfont.min.css"/>
|
||||
<link href="//fonts.googleapis.com/css?family=Lato:300,400,600,700" rel="stylesheet" type="text/css">
|
||||
|
||||
<link href="{% static 'matrixhosting/css/hosting.css' %}" rel="stylesheet">
|
||||
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<div class="row">
|
||||
<div class="dcl-payment-section">
|
||||
<h3>{%trans "Your Order" %}</h3>
|
||||
<hr class="top-hr">
|
||||
<div class="dcl-payment-order">
|
||||
<p>{% trans "Cores"%} <strong class="float-end">{{request.session.order.cores|floatformat}}</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Memory"%} <strong class="float-end">{{request.session.order.memory|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>{% trans "Disk space"%} <strong class="float-end">{{request.session.order.storage|floatformat}} GB</strong></p>
|
||||
<hr>
|
||||
<p>
|
||||
<strong>{%trans "Total" %}</strong>
|
||||
<small>
|
||||
({% if matrix_vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %})
|
||||
</small>
|
||||
<strong class="float-end">{{request.session.order.subtotal|floatformat}} CHF / {% trans "Month" %}</strong>
|
||||
</p>
|
||||
<hr>
|
||||
{% if matrix_vm_pricing.discount_amount %}
|
||||
<p class="mb-0">
|
||||
<strong>{{ request.session.order.discount.name }}</strong>
|
||||
<strong class="float-end text-success">- {{ request.session.order.discount.amount }} CHF / {% trans "Month" %}</strong>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="dcl-payment-section">
|
||||
<h2><b>{%trans "Billing Address"%}</b></h2>
|
||||
<hr class="top-hr">
|
||||
{% for message in messages %}
|
||||
{% if 'vat_error' in message.tags %}
|
||||
<ul class="list-unstyled"><li>
|
||||
{{ message|safe }}
|
||||
</li></ul>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<form role="form" id="billing-form" method="post" action="" novalidate>
|
||||
{% csrf_token %}
|
||||
{% for field in billing_address_form %}
|
||||
{% if field.html_name in 'active,owner' %}
|
||||
{{ field.as_hidden }}
|
||||
{%else %}
|
||||
{% bootstrap_field field show_label=False type='fields'%}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col">
|
||||
<div class="dcl-payment-section">
|
||||
{% with cards_len=cards|length %}
|
||||
<h3><b>{%trans "Credit Card"%}</b></h3>
|
||||
<hr class="top-hr">
|
||||
<p>
|
||||
{% if cards_len > 0 %}
|
||||
{% blocktrans %}Please select one of the cards that you used before or fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% else %}
|
||||
{% blocktrans %}Please fill in your credit card information below. We are using <a href="https://stripe.com" target="_blank">Stripe</a> for payment and do not store your information in our database.{% endblocktrans %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<div>
|
||||
{% for card in cards %}
|
||||
<div class="credit-card-info">
|
||||
<div class="col-xs-6 no-padding">
|
||||
<h5 class="billing-head">{% trans "Credit Card" %}</h5>
|
||||
<h5 class="membership-lead">{% trans "Last" %} 4: ***** {{card.last4}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Type" %}: {{card.brand}}</h5>
|
||||
<h5 class="membership-lead">{% trans "Expiry" %}: {{card.month}}/{{card.year}}</h5>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right align-bottom">
|
||||
<a class="btn choice-btn choice-btn-faded" href="#" data-id_card="{{card.id}}">{% trans "SELECT" %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if cards_len > 0 %}
|
||||
<div class="new-card-head">
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<h4>{% trans "Add a new credit card" %}</h4>
|
||||
</div>
|
||||
<div class="col-xs-6 text-right new-card-button-margin">
|
||||
<button data-bs-toggle="collapse" data-bs-target="#newcard" class="btn choice-btn">
|
||||
<span class="fa fa-plus"></span> {% trans "NEW CARD" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="newcard" class="collapse">
|
||||
<hr class="thick-hr">
|
||||
<div class="card-details-box">
|
||||
<h3>{%trans "New Credit Card" %}</h3>
|
||||
<hr>
|
||||
{% include "matrixhosting/includes/_card.html" %}
|
||||
</div>
|
||||
</div>
|
||||
{% else%}
|
||||
{% include "matrixhosting/includes/_card.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if stripe_key %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
<script type="text/javascript">
|
||||
window.processing_text = '{%trans "Processing" %}';
|
||||
window.enter_your_card_text = '{%trans "Enter your credit card number" %}';
|
||||
|
||||
(function () {
|
||||
window.stripeKey = "{{stripe_key}}";
|
||||
window.current_lan = "{{LANGUAGE_CODE}}";
|
||||
})();
|
||||
</script>
|
||||
{%endif%}
|
||||
|
||||
<!-- jQuery -->
|
||||
<script src="https://js.stripe.com/v3/"></script>
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js" integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
|
||||
|
||||
<script src="{% static 'fontawesome_free/js/all.min.js' %}"></script>
|
||||
<!-- Bootstrap Core JavaScript -->
|
||||
{% bootstrap_javascript %}
|
||||
<!-- Custom JS -->
|
||||
<script type="text/javascript" src="{% static 'matrixhosting/js/payment.js' %}"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -1,67 +0,0 @@
|
|||
import datetime
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.utils import timezone
|
||||
|
||||
from .models import VMInstance
|
||||
from uncloud_pay.models import Order, PricingPlan, BillingAddress, Product, RecurringPeriod
|
||||
|
||||
|
||||
vm_product_config = {
|
||||
'features': {
|
||||
'cores':
|
||||
{ 'min': 1,
|
||||
'max': 48
|
||||
},
|
||||
'ram_gb':
|
||||
{ 'min': 2,
|
||||
'max': 200
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
class VMInstanceTestCase(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
RecurringPeriod.populate_db_defaults()
|
||||
self.user = get_user_model().objects.create(
|
||||
username='random_user',
|
||||
email='jane.random@domain.tld')
|
||||
self.config = json.dumps({
|
||||
'cores': 1,
|
||||
'memory': 2,
|
||||
'storage': 100,
|
||||
'homeserver_domain': '',
|
||||
'webclient_domain': '',
|
||||
'matrix_domain': '',
|
||||
})
|
||||
self.pricing_plan = PricingPlan.objects.create(name="PricingSample", set_up_fees=35, cores_unit_price=3,
|
||||
ram_unit_price=4, storage_unit_price=0.02)
|
||||
self.ba = BillingAddress.objects.create(
|
||||
owner=self.user,
|
||||
organization = 'Test org',
|
||||
street="unknown",
|
||||
city="unknown",
|
||||
postal_code="somewhere else",
|
||||
active=True)
|
||||
|
||||
self.product = Product.objects.create(name="Testproduct",
|
||||
description="Only for testing",
|
||||
config=vm_product_config)
|
||||
self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days")
|
||||
self.product.recurring_periods.add(self.default_recurring_period,
|
||||
through_defaults= { 'is_default': True })
|
||||
|
||||
def test_create_matrix_vm(self):
|
||||
order = Order.objects.create(owner=self.user,
|
||||
recurring_period=self.default_recurring_period,
|
||||
billing_address=self.ba,
|
||||
pricing_plan = self.pricing_plan,
|
||||
product=self.product,
|
||||
config=self.config)
|
||||
instances = VMInstance.objects.filter(order=order)
|
||||
self.assertEqual(len(instances), 1)
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
from .views import IndexView, PricingView, OrderPaymentView, OrderDetailsView, Dashboard
|
||||
|
||||
app_name = 'matrixhosting'
|
||||
|
||||
urlpatterns = [
|
||||
path('pricing/<slug:name>/calculate/', PricingView.as_view(), name='pricing_calculator'),
|
||||
path('payment/', OrderPaymentView.as_view(), name='payment'),
|
||||
path('order/details/', OrderDetailsView.as_view(), name='order_details'),
|
||||
path('dashboard/', Dashboard.as_view(), name='dashboard'),
|
||||
path('', IndexView.as_view(), name='index'),
|
||||
]
|
|
@ -1,34 +0,0 @@
|
|||
from django.core.validators import RegexValidator
|
||||
|
||||
|
||||
def _validator():
|
||||
|
||||
ul = '\u00a1-\uffff' # unicode letters range (must not be a raw string)
|
||||
|
||||
# IP patterns
|
||||
ipv4_re = r'(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)(?:\.(?:25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'
|
||||
ipv6_re = r'\[[0-9a-f:\.]+\]' # (simple regex, validated later)
|
||||
|
||||
# Host patterns
|
||||
hostname_re = r'[a-z' + ul + \
|
||||
r'0-9](?:[a-z' + ul + r'0-9-]{0,61}[a-z' + ul + r'0-9])?'
|
||||
# Max length for domain name labels is 63 characters per RFC 1034 sec. 3.1
|
||||
domain_re = r'(?:\.(?!-)[a-z' + ul + r'0-9-]{1,63}(?<!-))*'
|
||||
tld_re = (
|
||||
r'\.' # dot
|
||||
r'(?!-)' # can't start with a dash
|
||||
r'(?:[a-z' + ul + '-]{2,63}' # domain label
|
||||
r'|xn--[a-z0-9]{1,59})' # or punycode label
|
||||
r'(?<!-)' # can't end with a dash
|
||||
r'\.?' # may have a trailing dot
|
||||
r'/?'
|
||||
)
|
||||
host_re = '(' + hostname_re + domain_re + tld_re + ')'
|
||||
regex = (
|
||||
r'(?:' + ipv4_re + '|' + ipv6_re + '|' + host_re + ')'
|
||||
r'(?::\d{2,5})?' # port
|
||||
r'\Z')
|
||||
return RegexValidator(regex, message='Enter a valid Domain (Not a URL)', code='invalid_domain')
|
||||
|
||||
|
||||
domain_name_validator = _validator()
|
|
@ -1,301 +0,0 @@
|
|||
import logging
|
||||
import json
|
||||
|
||||
from django.shortcuts import redirect, render
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import get_language, ugettext_lazy as _
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.views.decorators.cache import cache_control
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.views.generic import FormView, DetailView
|
||||
from django.views.generic.list import ListView
|
||||
from matrixhosting.forms import RequestHostedVMForm, BillingAddressForm
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.http import (
|
||||
HttpResponseRedirect, JsonResponse
|
||||
)
|
||||
from rest_framework import viewsets, permissions
|
||||
|
||||
from uncloud_pay.models import PricingPlan
|
||||
from uncloud_pay.utils import get_order_total_with_vat
|
||||
from uncloud_pay.models import *
|
||||
from uncloud_pay.utils import validate_vat_number
|
||||
from uncloud_pay.selectors import get_billing_address_for_user
|
||||
import uncloud_pay.stripe as uncloud_stripe
|
||||
from .models import VMInstance
|
||||
from .serializers import *
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PricingView(View):
|
||||
def get(self, request, **args):
|
||||
subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = get_order_total_with_vat(
|
||||
request.GET.get('cores'),
|
||||
request.GET.get('memory'),
|
||||
request.GET.get('storage'),
|
||||
pricing_name = args['name']
|
||||
)
|
||||
return JsonResponse({'subtotal': subtotal})
|
||||
|
||||
class IndexView(FormView):
|
||||
template_name = "matrixhosting/index.html"
|
||||
form_class = RequestHostedVMForm
|
||||
success_url = "/matrixhosting#requestform"
|
||||
success_message = "Thank you, we will contact you as soon as possible"
|
||||
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['matrix_vm_pricing'] = PricingPlan.get_default_pricing()
|
||||
return context
|
||||
|
||||
def form_valid(self, form):
|
||||
self.request.session['order'] = form.cleaned_data
|
||||
subtotal, subtotal_with_discount, total, vat, vat_percent, discount = get_order_total_with_vat(
|
||||
form.cleaned_data['cores'],
|
||||
form.cleaned_data['memory'],
|
||||
form.cleaned_data['storage'],
|
||||
form.cleaned_data['pricing_name'],
|
||||
False
|
||||
)
|
||||
self.request.session['pricing'] = {'name': form.cleaned_data['pricing_name'],
|
||||
'subtotal': subtotal, 'vat': vat,
|
||||
'vat_percent': vat_percent, 'discount': discount}
|
||||
return HttpResponseRedirect(reverse('matrix:payment'))
|
||||
|
||||
|
||||
class OrderPaymentView(FormView):
|
||||
template_name = 'matrixhosting/payment.html'
|
||||
success_url = 'matrix:order_confirmation'
|
||||
form_class = BillingAddressForm
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(OrderPaymentView, self).get_context_data(**kwargs)
|
||||
if 'billing_address_data' in self.request.session:
|
||||
billing_address_form = BillingAddressForm(
|
||||
initial=self.request.session['billing_address_data']
|
||||
)
|
||||
else:
|
||||
old_active = get_billing_address_for_user(self.request.user)
|
||||
billing_address_form = BillingAddressForm(
|
||||
instance=old_active
|
||||
) if old_active else BillingAddressForm(
|
||||
initial={'active': True, 'owner': self.request.user.id}
|
||||
)
|
||||
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
|
||||
cards = uncloud_stripe.get_customer_cards(customer_id)
|
||||
context.update({
|
||||
'matrix_vm_pricing': PricingPlan.get_by_name(self.request.session.get('pricing', {'name': 'unknown'})['name']),
|
||||
'billing_address_form': billing_address_form,
|
||||
'cards': cards,
|
||||
'stripe_key': settings.STRIPE_PUBLIC_KEY
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
for k in ['vat_validation_status', 'token', 'id_payment_method']:
|
||||
if request.session.get(k):
|
||||
request.session.pop(k)
|
||||
if 'order' not in request.session:
|
||||
return HttpResponseRedirect(reverse('matrix:index'))
|
||||
return self.render_to_response(self.get_context_data())
|
||||
|
||||
def form_valid(self, address_form):
|
||||
id_payment_method = self.request.POST.get('id_payment_method', None)
|
||||
self.request.session["id_payment_method"] = id_payment_method
|
||||
this_user = {
|
||||
'email': self.request.user.email,
|
||||
'username': self.request.user.username
|
||||
}
|
||||
customer_id = uncloud_stripe.get_customer_id_for(self.request.user)
|
||||
uncloud_stripe.attach_payment_method(id_payment_method, customer_id)
|
||||
address = get_billing_address_for_user(self.request.user)
|
||||
if address:
|
||||
form = BillingAddressForm(self.request.POST, instance=address)
|
||||
else:
|
||||
form = BillingAddressForm(self.request.POST)
|
||||
if form.is_valid:
|
||||
billing_address_ins = form.save()
|
||||
self.request.session["billing_address_id"] = billing_address_ins.id
|
||||
self.request.session['billing_address_data'] = address_form.cleaned_data
|
||||
self.request.session['billing_address_data']['owner'] = self.request.user.id
|
||||
self.request.session['user'] = this_user
|
||||
self.request.session['customer'] = customer_id
|
||||
|
||||
vat_number = address_form.cleaned_data.get('vat_number').strip()
|
||||
if vat_number:
|
||||
validate_result = validate_vat_number(
|
||||
stripe_customer_id=customer_id,
|
||||
billing_address_id=billing_address_ins.id
|
||||
)
|
||||
|
||||
if 'error' in validate_result and validate_result['error']:
|
||||
messages.add_message(
|
||||
self.request, messages.ERROR, validate_result["error"],
|
||||
extra_tags='vat_error'
|
||||
)
|
||||
return HttpResponseRedirect(
|
||||
reverse('matrix:payment') + '#vat_error'
|
||||
)
|
||||
self.request.session["vat_validation_status"] = validate_result["status"]
|
||||
|
||||
return HttpResponseRedirect(reverse('matrix:order_details'))
|
||||
|
||||
|
||||
class OrderDetailsView(DetailView):
|
||||
template_name = "matrixhosting/order_detail.html"
|
||||
context_object_name = "order"
|
||||
model = Order
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
@cache_control(no_cache=True, must_revalidate=True, no_store=True)
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {}
|
||||
if ('order' not in request.session or 'user' not in request.session):
|
||||
return HttpResponseRedirect(reverse('matrix:index'))
|
||||
if 'id_payment_method' in self.request.session:
|
||||
card = uncloud_stripe.get_card_from_payment(self.request.user, self.request.session['id_payment_method'])
|
||||
if not card:
|
||||
return HttpResponseRedirect(reverse('matrix:payment'))
|
||||
context['card'] = card
|
||||
elif 'id_payment_method' not in self.request.session or 'vat_validation_status' not in self.request.session:
|
||||
return HttpResponseRedirect(reverse('matrix:payment'))
|
||||
specs = request.session.get('order')
|
||||
pricing = request.session.get('pricing')
|
||||
billing_address = BillingAddress.objects.get(id=request.session.get('billing_address_id'))
|
||||
vat_rate = VATRate.get_vat_rate(billing_address)
|
||||
vat_validation_status = "verified" if billing_address.vat_number_validated_on and billing_address.vat_number_verified else False
|
||||
subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = get_order_total_with_vat(
|
||||
specs['cores'], specs['memory'], specs['storage'], request.session['pricing']['name'],
|
||||
vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status
|
||||
)
|
||||
pricing = {
|
||||
"subtotal": subtotal, "discount": discount, "vat": vat, "vat_percent": vat_percent,
|
||||
"vat_country": billing_address.country.lower(),
|
||||
"subtotal_after_discount": subtotal_after_discount,
|
||||
"price_after_discount_with_vat": price_after_discount_with_vat
|
||||
|
||||
}
|
||||
pricing["price_with_vat"] = round(subtotal * (1 + pricing["vat_percent"] * 0.01), 2)
|
||||
discount["amount_with_vat"] = round(pricing["price_with_vat"] - pricing["price_after_discount_with_vat"], 2)
|
||||
pricing["total_price"] = pricing["price_after_discount_with_vat"]
|
||||
self.request.session['total_price'] = pricing["price_after_discount_with_vat"]
|
||||
payment_intent_response = uncloud_stripe.get_payment_intent(request.user, pricing["price_after_discount_with_vat"])
|
||||
context.update({
|
||||
'payment_intent_secret': payment_intent_response.client_secret,
|
||||
'order': specs,
|
||||
'pricing': pricing,
|
||||
'stripe_key': settings.STRIPE_PUBLIC_KEY,
|
||||
})
|
||||
return render(request, self.template_name, context)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
customer = StripeCustomer.objects.get(owner=self.request.user)
|
||||
billing_address = BillingAddress.objects.get(id=request.session.get('billing_address_id'))
|
||||
if 'id_payment_method' in request.session:
|
||||
card = uncloud_stripe.get_card_from_payment(self.request.user, self.request.session['id_payment_method'])
|
||||
if not card:
|
||||
return show_error("There was a payment related error.", self.request)
|
||||
else:
|
||||
return show_error("There was a payment related error.", self.request)
|
||||
|
||||
order = finalize_order(request, customer,
|
||||
billing_address,
|
||||
self.request.session['total_price'],
|
||||
PricingPlan.get_by_name(self.request.session['pricing']['name']),
|
||||
request.session.get('order'))
|
||||
if order:
|
||||
bill = Bill.create_next_bill_for_user_address(billing_address)
|
||||
payment= Payment.objects.create(owner=request.user, amount=self.request.session['total_price'], source='stripe')
|
||||
if payment:
|
||||
#Close the bill as the payment has been added
|
||||
bill.close()
|
||||
response = {
|
||||
'status': True,
|
||||
'redirect': (reverse('matrix:dashboard')),
|
||||
'msg_title': str(_('Thank you for the order.')),
|
||||
'msg_body': str(
|
||||
_('Your VM will be up and running in a few moments.'
|
||||
' We will send you a confirmation email as soon as'
|
||||
' it is ready.'))
|
||||
}
|
||||
|
||||
return JsonResponse(response)
|
||||
|
||||
|
||||
def finalize_order(request, customer, billing_address,
|
||||
one_time_price, pricing_plan,
|
||||
specs):
|
||||
product = Product.objects.first()
|
||||
recurring_period_product = ProductToRecurringPeriod.objects.filter(product=product, is_default=True).first()
|
||||
order = Order.objects.create(
|
||||
owner=request.user,
|
||||
customer=customer,
|
||||
billing_address=billing_address,
|
||||
one_time_price=one_time_price,
|
||||
pricing_plan=pricing_plan,
|
||||
recurring_period= recurring_period_product.recurring_period,
|
||||
product = product,
|
||||
config=json.dumps(specs)
|
||||
)
|
||||
return order
|
||||
|
||||
|
||||
class Dashboard(ListView):
|
||||
template_name = "matrixhosting/dashboard.html"
|
||||
model = Order
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, *args, **kwargs):
|
||||
return super().dispatch(*args, **kwargs)
|
||||
|
||||
def get_queryset(self):
|
||||
return Order.objects.filter(owner=self.request.user)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
order = Order.objects.get(id=request.POST.get('order_id', 0))
|
||||
order.cancel()
|
||||
return JsonResponse({'message': 'Successfully Cancelled'})
|
||||
|
||||
|
||||
def get_error_response_dict(request):
|
||||
response = {
|
||||
'status': False,
|
||||
'redirect': "{url}#{section}".format(
|
||||
url=(reverse('matrix:payment')),
|
||||
section='payment_error'
|
||||
),
|
||||
'msg_title': str(_('Error.')),
|
||||
'msg_body': str(
|
||||
_('There was a payment related error.'
|
||||
' On close of this popup, you will be redirected back to'
|
||||
' the payment page.'))
|
||||
}
|
||||
return response
|
||||
|
||||
|
||||
def show_error(msg, request):
|
||||
messages.add_message(request, messages.ERROR, msg,
|
||||
extra_tags='failed_payment')
|
||||
return JsonResponse(get_error_response_dict(request))
|
||||
|
||||
|
||||
class MachineViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = VMInstanceSerializer
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return VMInstance.objects.filter(owner=self.request.user)
|
||||
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: {{ name }}-matrix
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ name }}-matrix
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ name }}-matrix
|
||||
use-as-service: {{ name }}
|
|
@ -1,21 +0,0 @@
|
|||
# Generated by Django 3.1 on 2020-12-13 10:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='VM',
|
||||
fields=[
|
||||
('vmid', models.IntegerField(primary_key=True, serialize=False)),
|
||||
('data', models.JSONField()),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,16 +0,0 @@
|
|||
from rest_framework import viewsets, permissions
|
||||
|
||||
#from .models import VM
|
||||
# from .serializers import OpenNebulaVMSerializer
|
||||
|
||||
# class VMViewSet(viewsets.ModelViewSet):
|
||||
# permission_classes = [permissions.IsAuthenticated]
|
||||
# serializer_class = OpenNebulaVMSerializer
|
||||
|
||||
# def get_queryset(self):
|
||||
# if self.request.user.is_superuser:
|
||||
# obj = VM.objects.all()
|
||||
# else:
|
||||
# obj = VM.objects.filter(owner=self.request.user)
|
||||
|
||||
# return obj
|
|
@ -1,3 +0,0 @@
|
|||
FROM fedora:latest
|
||||
|
||||
RUN dnf install -y python3-devel python3-pip python3-coverage libpq-devel openldap-devel gcc chromium
|
2
uncloud/.gitignore
vendored
2
uncloud/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
|||
local_settings.py
|
||||
ldap_max_uid_file
|
|
@ -1,253 +0,0 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
import decimal
|
||||
|
||||
# Define DecimalField properties, used to represent amounts of money.
|
||||
AMOUNT_MAX_DIGITS=10
|
||||
AMOUNT_DECIMALS=2
|
||||
|
||||
decimal.getcontext().prec = AMOUNT_DECIMALS
|
||||
|
||||
# http://xml.coverpages.org/country3166.html
|
||||
COUNTRIES = (
|
||||
('AD', _('Andorra')),
|
||||
('AE', _('United Arab Emirates')),
|
||||
('AF', _('Afghanistan')),
|
||||
('AG', _('Antigua & Barbuda')),
|
||||
('AI', _('Anguilla')),
|
||||
('AL', _('Albania')),
|
||||
('AM', _('Armenia')),
|
||||
('AN', _('Netherlands Antilles')),
|
||||
('AO', _('Angola')),
|
||||
('AQ', _('Antarctica')),
|
||||
('AR', _('Argentina')),
|
||||
('AS', _('American Samoa')),
|
||||
('AT', _('Austria')),
|
||||
('AU', _('Australia')),
|
||||
('AW', _('Aruba')),
|
||||
('AZ', _('Azerbaijan')),
|
||||
('BA', _('Bosnia and Herzegovina')),
|
||||
('BB', _('Barbados')),
|
||||
('BD', _('Bangladesh')),
|
||||
('BE', _('Belgium')),
|
||||
('BF', _('Burkina Faso')),
|
||||
('BG', _('Bulgaria')),
|
||||
('BH', _('Bahrain')),
|
||||
('BI', _('Burundi')),
|
||||
('BJ', _('Benin')),
|
||||
('BM', _('Bermuda')),
|
||||
('BN', _('Brunei Darussalam')),
|
||||
('BO', _('Bolivia')),
|
||||
('BR', _('Brazil')),
|
||||
('BS', _('Bahama')),
|
||||
('BT', _('Bhutan')),
|
||||
('BV', _('Bouvet Island')),
|
||||
('BW', _('Botswana')),
|
||||
('BY', _('Belarus')),
|
||||
('BZ', _('Belize')),
|
||||
('CA', _('Canada')),
|
||||
('CC', _('Cocos (Keeling) Islands')),
|
||||
('CF', _('Central African Republic')),
|
||||
('CG', _('Congo')),
|
||||
('CH', _('Switzerland')),
|
||||
('CI', _('Ivory Coast')),
|
||||
('CK', _('Cook Iislands')),
|
||||
('CL', _('Chile')),
|
||||
('CM', _('Cameroon')),
|
||||
('CN', _('China')),
|
||||
('CO', _('Colombia')),
|
||||
('CR', _('Costa Rica')),
|
||||
('CU', _('Cuba')),
|
||||
('CV', _('Cape Verde')),
|
||||
('CX', _('Christmas Island')),
|
||||
('CY', _('Cyprus')),
|
||||
('CZ', _('Czech Republic')),
|
||||
('DE', _('Germany')),
|
||||
('DJ', _('Djibouti')),
|
||||
('DK', _('Denmark')),
|
||||
('DM', _('Dominica')),
|
||||
('DO', _('Dominican Republic')),
|
||||
('DZ', _('Algeria')),
|
||||
('EC', _('Ecuador')),
|
||||
('EE', _('Estonia')),
|
||||
('EG', _('Egypt')),
|
||||
('EH', _('Western Sahara')),
|
||||
('ER', _('Eritrea')),
|
||||
('ES', _('Spain')),
|
||||
('ET', _('Ethiopia')),
|
||||
('FI', _('Finland')),
|
||||
('FJ', _('Fiji')),
|
||||
('FK', _('Falkland Islands (Malvinas)')),
|
||||
('FM', _('Micronesia')),
|
||||
('FO', _('Faroe Islands')),
|
||||
('FR', _('France')),
|
||||
('FX', _('France, Metropolitan')),
|
||||
('GA', _('Gabon')),
|
||||
('GB', _('United Kingdom (Great Britain)')),
|
||||
('GD', _('Grenada')),
|
||||
('GE', _('Georgia')),
|
||||
('GF', _('French Guiana')),
|
||||
('GH', _('Ghana')),
|
||||
('GI', _('Gibraltar')),
|
||||
('GL', _('Greenland')),
|
||||
('GM', _('Gambia')),
|
||||
('GN', _('Guinea')),
|
||||
('GP', _('Guadeloupe')),
|
||||
('GQ', _('Equatorial Guinea')),
|
||||
('GR', _('Greece')),
|
||||
('GS', _('South Georgia and the South Sandwich Islands')),
|
||||
('GT', _('Guatemala')),
|
||||
('GU', _('Guam')),
|
||||
('GW', _('Guinea-Bissau')),
|
||||
('GY', _('Guyana')),
|
||||
('HK', _('Hong Kong')),
|
||||
('HM', _('Heard & McDonald Islands')),
|
||||
('HN', _('Honduras')),
|
||||
('HR', _('Croatia')),
|
||||
('HT', _('Haiti')),
|
||||
('HU', _('Hungary')),
|
||||
('ID', _('Indonesia')),
|
||||
('IE', _('Ireland')),
|
||||
('IL', _('Israel')),
|
||||
('IN', _('India')),
|
||||
('IO', _('British Indian Ocean Territory')),
|
||||
('IQ', _('Iraq')),
|
||||
('IR', _('Islamic Republic of Iran')),
|
||||
('IS', _('Iceland')),
|
||||
('IT', _('Italy')),
|
||||
('JM', _('Jamaica')),
|
||||
('JO', _('Jordan')),
|
||||
('JP', _('Japan')),
|
||||
('KE', _('Kenya')),
|
||||
('KG', _('Kyrgyzstan')),
|
||||
('KH', _('Cambodia')),
|
||||
('KI', _('Kiribati')),
|
||||
('KM', _('Comoros')),
|
||||
('KN', _('St. Kitts and Nevis')),
|
||||
('KP', _('Korea, Democratic People\'s Republic of')),
|
||||
('KR', _('Korea, Republic of')),
|
||||
('KW', _('Kuwait')),
|
||||
('KY', _('Cayman Islands')),
|
||||
('KZ', _('Kazakhstan')),
|
||||
('LA', _('Lao People\'s Democratic Republic')),
|
||||
('LB', _('Lebanon')),
|
||||
('LC', _('Saint Lucia')),
|
||||
('LI', _('Liechtenstein')),
|
||||
('LK', _('Sri Lanka')),
|
||||
('LR', _('Liberia')),
|
||||
('LS', _('Lesotho')),
|
||||
('LT', _('Lithuania')),
|
||||
('LU', _('Luxembourg')),
|
||||
('LV', _('Latvia')),
|
||||
('LY', _('Libyan Arab Jamahiriya')),
|
||||
('MA', _('Morocco')),
|
||||
('MC', _('Monaco')),
|
||||
('MD', _('Moldova, Republic of')),
|
||||
('MG', _('Madagascar')),
|
||||
('MH', _('Marshall Islands')),
|
||||
('ML', _('Mali')),
|
||||
('MN', _('Mongolia')),
|
||||
('MM', _('Myanmar')),
|
||||
('MO', _('Macau')),
|
||||
('MP', _('Northern Mariana Islands')),
|
||||
('MQ', _('Martinique')),
|
||||
('MR', _('Mauritania')),
|
||||
('MS', _('Monserrat')),
|
||||
('MT', _('Malta')),
|
||||
('MU', _('Mauritius')),
|
||||
('MV', _('Maldives')),
|
||||
('MW', _('Malawi')),
|
||||
('MX', _('Mexico')),
|
||||
('MY', _('Malaysia')),
|
||||
('MZ', _('Mozambique')),
|
||||
('NA', _('Namibia')),
|
||||
('NC', _('New Caledonia')),
|
||||
('NE', _('Niger')),
|
||||
('NF', _('Norfolk Island')),
|
||||
('NG', _('Nigeria')),
|
||||
('NI', _('Nicaragua')),
|
||||
('NL', _('Netherlands')),
|
||||
('NO', _('Norway')),
|
||||
('NP', _('Nepal')),
|
||||
('NR', _('Nauru')),
|
||||
('NU', _('Niue')),
|
||||
('NZ', _('New Zealand')),
|
||||
('OM', _('Oman')),
|
||||
('PA', _('Panama')),
|
||||
('PE', _('Peru')),
|
||||
('PF', _('French Polynesia')),
|
||||
('PG', _('Papua New Guinea')),
|
||||
('PH', _('Philippines')),
|
||||
('PK', _('Pakistan')),
|
||||
('PL', _('Poland')),
|
||||
('PM', _('St. Pierre & Miquelon')),
|
||||
('PN', _('Pitcairn')),
|
||||
('PR', _('Puerto Rico')),
|
||||
('PT', _('Portugal')),
|
||||
('PW', _('Palau')),
|
||||
('PY', _('Paraguay')),
|
||||
('QA', _('Qatar')),
|
||||
('RE', _('Reunion')),
|
||||
('RO', _('Romania')),
|
||||
('RU', _('Russian Federation')),
|
||||
('RW', _('Rwanda')),
|
||||
('SA', _('Saudi Arabia')),
|
||||
('SB', _('Solomon Islands')),
|
||||
('SC', _('Seychelles')),
|
||||
('SD', _('Sudan')),
|
||||
('SE', _('Sweden')),
|
||||
('SG', _('Singapore')),
|
||||
('SH', _('St. Helena')),
|
||||
('SI', _('Slovenia')),
|
||||
('SJ', _('Svalbard & Jan Mayen Islands')),
|
||||
('SK', _('Slovakia')),
|
||||
('SL', _('Sierra Leone')),
|
||||
('SM', _('San Marino')),
|
||||
('SN', _('Senegal')),
|
||||
('SO', _('Somalia')),
|
||||
('SR', _('Suriname')),
|
||||
('ST', _('Sao Tome & Principe')),
|
||||
('SV', _('El Salvador')),
|
||||
('SY', _('Syrian Arab Republic')),
|
||||
('SZ', _('Swaziland')),
|
||||
('TC', _('Turks & Caicos Islands')),
|
||||
('TD', _('Chad')),
|
||||
('TF', _('French Southern Territories')),
|
||||
('TG', _('Togo')),
|
||||
('TH', _('Thailand')),
|
||||
('TJ', _('Tajikistan')),
|
||||
('TK', _('Tokelau')),
|
||||
('TM', _('Turkmenistan')),
|
||||
('TN', _('Tunisia')),
|
||||
('TO', _('Tonga')),
|
||||
('TP', _('East Timor')),
|
||||
('TR', _('Turkey')),
|
||||
('TT', _('Trinidad & Tobago')),
|
||||
('TV', _('Tuvalu')),
|
||||
('TW', _('Taiwan, Province of China')),
|
||||
('TZ', _('Tanzania, United Republic of')),
|
||||
('UA', _('Ukraine')),
|
||||
('UG', _('Uganda')),
|
||||
('UM', _('United States Minor Outlying Islands')),
|
||||
('US', _('United States of America')),
|
||||
('UY', _('Uruguay')),
|
||||
('UZ', _('Uzbekistan')),
|
||||
('VA', _('Vatican City State (Holy See)')),
|
||||
('VC', _('St. Vincent & the Grenadines')),
|
||||
('VE', _('Venezuela')),
|
||||
('VG', _('British Virgin Islands')),
|
||||
('VI', _('United States Virgin Islands')),
|
||||
('VN', _('Viet Nam')),
|
||||
('VU', _('Vanuatu')),
|
||||
('WF', _('Wallis & Futuna Islands')),
|
||||
('WS', _('Samoa')),
|
||||
('YE', _('Yemen')),
|
||||
('YT', _('Mayotte')),
|
||||
('YU', _('Yugoslavia')),
|
||||
('ZA', _('South Africa')),
|
||||
('ZM', _('Zambia')),
|
||||
('ZR', _('Zaire')),
|
||||
('ZW', _('Zimbabwe')),
|
||||
)
|
||||
|
||||
|
||||
__all__ = ()
|
|
@ -1,6 +0,0 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from .models import *
|
||||
|
||||
for m in [ UncloudProvider, UncloudNetwork ]:
|
||||
admin.site.register(m)
|
|
@ -1,8 +0,0 @@
|
|||
from django import forms
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class UserDeleteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = []
|
|
@ -1,43 +0,0 @@
|
|||
import random
|
||||
import string
|
||||
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
from uncloud_pay.models import BillingAddress, RecurringPeriod, Product
|
||||
from uncloud.models import UncloudProvider, UncloudNetwork
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Add standard uncloud values'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
pass
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Order matters, objects can be dependent on each other
|
||||
|
||||
admin_username="uncloud-admin"
|
||||
pw_length = 32
|
||||
|
||||
# Only set password if the user did not exist before
|
||||
try:
|
||||
admin_user = get_user_model().objects.get(username=settings.UNCLOUD_ADMIN_NAME)
|
||||
except ObjectDoesNotExist:
|
||||
random_password = ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(pw_length))
|
||||
|
||||
admin_user = get_user_model().objects.create_user(username=settings.UNCLOUD_ADMIN_NAME, password=random_password)
|
||||
admin_user.is_superuser=True
|
||||
admin_user.is_staff=True
|
||||
admin_user.save()
|
||||
|
||||
print(f"Created admin user '{admin_username}' with password '{random_password}'")
|
||||
|
||||
BillingAddress.populate_db_defaults()
|
||||
RecurringPeriod.populate_db_defaults()
|
||||
Product.populate_db_defaults()
|
||||
|
||||
UncloudNetwork.populate_db_defaults()
|
||||
UncloudProvider.populate_db_defaults()
|
File diff suppressed because one or more lines are too long
|
@ -1,19 +0,0 @@
|
|||
# Generated by Django 3.1 on 2020-12-20 17:16
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='UncloudTasks',
|
||||
fields=[
|
||||
('task_id', models.UUIDField(primary_key=True, serialize=False)),
|
||||
],
|
||||
),
|
||||
]
|
|
@ -1,17 +0,0 @@
|
|||
# Generated by Django 3.1 on 2020-12-20 17:28
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud', '0002_uncloudtasks'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameModel(
|
||||
old_name='UncloudTasks',
|
||||
new_name='UncloudTask',
|
||||
),
|
||||
]
|
File diff suppressed because one or more lines are too long
|
@ -1,16 +0,0 @@
|
|||
# Generated by Django 3.2.4 on 2021-07-07 15:11
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('uncloud', '0004_auto_20210101_1308'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='UncloudTask',
|
||||
),
|
||||
]
|
|
@ -1,209 +0,0 @@
|
|||
from django.db import models
|
||||
from django.db.models import JSONField, Q
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
from django.core.exceptions import FieldError
|
||||
|
||||
from uncloud import COUNTRIES
|
||||
from .selectors import filter_for_when
|
||||
|
||||
class UncloudModel(models.Model):
|
||||
"""
|
||||
This class extends the standard model with an
|
||||
extra_data field that can be used to include public,
|
||||
but internal information.
|
||||
|
||||
For instance if you migrate from an existing virtualisation
|
||||
framework to uncloud.
|
||||
|
||||
The extra_data attribute should be considered a hack and whenever
|
||||
data is necessary for running uncloud, it should **not** be stored
|
||||
in there.
|
||||
|
||||
"""
|
||||
|
||||
extra_data = JSONField(editable=False, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types
|
||||
class UncloudStatus(models.TextChoices):
|
||||
PENDING = 'PENDING', _('Pending')
|
||||
AWAITING_PAYMENT = 'AWAITING_PAYMENT', _('Awaiting payment')
|
||||
BEING_CREATED = 'BEING_CREATED', _('Being created')
|
||||
SCHEDULED = 'SCHEDULED', _('Scheduled') # resource selected, waiting for dispatching
|
||||
ACTIVE = 'ACTIVE', _('Active')
|
||||
MODIFYING = 'MODIFYING', _('Modifying') # Resource is being changed
|
||||
DELETED = 'DELETED', _('Deleted') # Resource has been deleted
|
||||
DISABLED = 'DISABLED', _('Disabled') # Is usable, but cannot be used for new things
|
||||
UNUSABLE = 'UNUSABLE', _('Unusable'), # Has some kind of error
|
||||
|
||||
|
||||
|
||||
###
|
||||
# General address handling
|
||||
class CountryField(models.CharField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
kwargs.setdefault('choices', COUNTRIES)
|
||||
kwargs.setdefault('default', 'CH')
|
||||
kwargs.setdefault('max_length', 2)
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def get_internal_type(self):
|
||||
return "CharField"
|
||||
|
||||
|
||||
class UncloudAddress(models.Model):
|
||||
full_name = models.CharField(max_length=256, null=False)
|
||||
organization = models.CharField(max_length=256, blank=True, null=True)
|
||||
street = models.CharField(max_length=256, null=False)
|
||||
city = models.CharField(max_length=256, null=False)
|
||||
postal_code = models.CharField(max_length=64)
|
||||
country = CountryField(blank=False, null=False)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class UncloudValidTimeFrame(models.Model):
|
||||
"""
|
||||
A model that allows to limit validity of something to a certain
|
||||
time frame. Used for versioning basically.
|
||||
|
||||
Logic:
|
||||
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(fields=['owner'],
|
||||
condition=models.Q(active=True),
|
||||
name='one_active_card_per_user')
|
||||
]
|
||||
|
||||
|
||||
valid_from = models.DateTimeField(default=timezone.now, null=True, blank=True)
|
||||
valid_to = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
@classmethod
|
||||
def get_current(cls, *args, **kwargs):
|
||||
now = timezone.now()
|
||||
|
||||
# With both given
|
||||
cls.objects.filter(valid_from__lte=now,
|
||||
valid_to__gte=now)
|
||||
|
||||
# With to missing
|
||||
cls.objects.filter(valid_from__lte=now,
|
||||
valid_to__isnull=true)
|
||||
|
||||
# With from missing
|
||||
cls.objects.filter(valid_from__isnull=true,
|
||||
valid_to__gte=now)
|
||||
|
||||
# Both missing
|
||||
cls.objects.filter(valid_from__isnull=true,
|
||||
valid_to__gte=now)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
###
|
||||
# UncloudNetworks are used as identifiers - such they are a base of uncloud
|
||||
|
||||
class UncloudNetwork(models.Model):
|
||||
"""
|
||||
Storing IP networks
|
||||
"""
|
||||
|
||||
network_address = models.GenericIPAddressField(null=False, unique=True)
|
||||
network_mask = models.IntegerField(null=False,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(128)]
|
||||
)
|
||||
|
||||
description = models.CharField(max_length=256)
|
||||
|
||||
@classmethod
|
||||
def populate_db_defaults(cls):
|
||||
for net, desc in [
|
||||
( "2a0a:e5c0:11::", "uncloud Billing" ),
|
||||
( "2a0a:e5c0:11:1::", "uncloud Referral" ),
|
||||
( "2a0a:e5c0:11:2::", "uncloud Coupon" )
|
||||
]:
|
||||
obj, created = cls.objects.get_or_create(network_address=net,
|
||||
defaults= {
|
||||
'network_mask': 64,
|
||||
'description': desc
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not ':' in self.network_address and self.network_mask > 32:
|
||||
raise FieldError("Mask cannot exceed 32 for IPv4")
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.network_address}/{self.network_mask} {self.description}"
|
||||
|
||||
###
|
||||
# Who is running / providing this instance of uncloud?
|
||||
|
||||
class UncloudProvider(UncloudAddress):
|
||||
"""
|
||||
A class resembling who is running this uncloud instance.
|
||||
This might change over time so we allow starting/ending dates
|
||||
|
||||
This also defines the taxation rules.
|
||||
|
||||
starting/ending date define from when to when this is valid. This way
|
||||
we can model address changes and have it correct in the bills.
|
||||
"""
|
||||
|
||||
# Meta:
|
||||
# FIXMe: only allow non overlapping time frames -- how to define this as a constraint?
|
||||
starting_date = models.DateField()
|
||||
ending_date = models.DateField(blank=True, null=True)
|
||||
|
||||
billing_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderbill", on_delete=models.CASCADE)
|
||||
referral_network = models.ForeignKey(UncloudNetwork, related_name="uncloudproviderreferral", on_delete=models.CASCADE)
|
||||
coupon_network = models.ForeignKey(UncloudNetwork, related_name="uncloudprovidercoupon", on_delete=models.CASCADE)
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_provider(cls, when=None):
|
||||
"""
|
||||
Find active provide at a certain time - if there was any
|
||||
"""
|
||||
|
||||
|
||||
return cls.objects.get(Q(starting_date__gte=when, ending_date__lte=when) |
|
||||
Q(starting_date__gte=when, ending_date__isnull=True))
|
||||
|
||||
|
||||
@classmethod
|
||||
def populate_db_defaults(cls):
|
||||
obj, created = cls.objects.get_or_create(full_name="ungleich glarus ag",
|
||||
street="Bahnhofstrasse 1",
|
||||
postal_code="8783",
|
||||
city="Linthal",
|
||||
country="CH",
|
||||
starting_date=timezone.now(),
|
||||
billing_network=UncloudNetwork.objects.get(description="uncloud Billing"),
|
||||
referral_network=UncloudNetwork.objects.get(description="uncloud Referral"),
|
||||
coupon_network=UncloudNetwork.objects.get(description="uncloud Coupon")
|
||||
)
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.full_name} {self.country}"
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
def filter_for_when(queryset, when=None):
|
||||
"""
|
||||
Return a filtered queryset which is valid for the given date
|
||||
|
||||
Logic:
|
||||
|
||||
Look for entries that have a starting date before when
|
||||
and either
|
||||
- No ending date
|
||||
- Ending date after "when"
|
||||
|
||||
Returns a queryset, you'll neet to apply .first() or similar on it
|
||||
|
||||
"""
|
||||
|
||||
if not when:
|
||||
when = timezone.now()
|
||||
|
||||
return queryset.filter(starting_date__lte=when).filter(Q(ending_date__gte=when) |
|
||||
Q(ending_date__isnull=True))
|
|
@ -1,4 +0,0 @@
|
|||
#content {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
{% extends 'bootstrap5/bootstrap5.html' %}
|
||||
{% block bootstrap5_before_content %}
|
||||
|
||||
<nav class="navbar sticky-top navbar-expand-lg navbar-light bg-light">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="{% url 'uncloudindex' %}">uncloud</a>
|
||||
<a class="navbar-brand" href="{% url 'matrix:index' %}">Matrix Hosting</a>
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
{% if user.is_authenticated %}
|
||||
<span class="navbar-text">Logged in as {{ user }}. Your balance:
|
||||
{{ balance }} CHF. </span>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_logout' %}">Logout</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'account_login' %}">Login</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{% endblock %}
|
|
@ -1,170 +0,0 @@
|
|||
{% extends 'uncloud/base.html' %}
|
||||
{% block title %}Welcome to uncloud [beta]{% endblock %}
|
||||
|
||||
{% block bootstrap5_content %}
|
||||
<div class="container">
|
||||
<div id="content">
|
||||
<div id="intro" class="row">
|
||||
<div class=col>
|
||||
<h1>Welcome to uncloud [beta]</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="about" class="row">
|
||||
<div class="col"><h3>About uncloud</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
Welcome to uncloud, the Open Source cloud management
|
||||
system by <a href="https://ungleich.ch">ungleich</a>.
|
||||
It is an <a href="{% url 'api-root' %}">API</a> driven system with
|
||||
some convience views provided by
|
||||
the <a href="https://www.django-rest-framework.org/">Django Rest
|
||||
Framework</a>. You can
|
||||
freely <a href="https://code.ungleich.ch/uncloud/uncloud/">access
|
||||
the source code of uncloud</a>.
|
||||
<strong>This is a BETA service.</strong> As such, some
|
||||
functionality might not be very sophisticated.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="howto" class="row">
|
||||
<div class="col"><h3>Getting started</h3></div>
|
||||
<div class="col-8">
|
||||
<p>uncloud is designed to be as easy as possible to use. However,
|
||||
there are some "real world" requirements that need to be met to
|
||||
start using uncloud:
|
||||
|
||||
<ul>
|
||||
<li>First you need
|
||||
to <a href="https://account.ungleich.ch">register an
|
||||
account</a>. If you already have one, you can
|
||||
<a href="{% url 'account_login' %}">login</a>.
|
||||
<li>If you have forgotten your password or other issues with
|
||||
logging in, you can contact the ungleich support
|
||||
via <strong>support at ungleich.ch</strong>.
|
||||
|
||||
<li>Secondy you will need to
|
||||
<a href="{% url 'billingaddress-list' %}">create a billing
|
||||
address</a>. This is required for determining the correct
|
||||
tax.
|
||||
<li>Next you will need to
|
||||
<a href="{% url 'cc_register' %}">register a credit card</a>
|
||||
from which payments can be made. Your credit card will not
|
||||
be charged without your consent.
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div id="howto" class="row">
|
||||
<div class="col"><h3>Introduction to uncloud concepts</h3></div>
|
||||
<div class="col-8">
|
||||
<p>We plan to offer many services on uncloud ranging from
|
||||
for free, for a small amount or regular charges. As transfer
|
||||
fees are a major challenge for our business, we based uncloud
|
||||
on the <strong>pre-paid account model</strong>. Which means
|
||||
that you can charge your account and then use your balance to
|
||||
pay for product usage.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="creditcards" class="row">
|
||||
<div class="col"><h3>Credit cards</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
Credit cards are registered with stripe. We only save a the
|
||||
last 4 digits and the expiry date of the card to make
|
||||
identification for you easier.
|
||||
</p>
|
||||
<ul>
|
||||
<li><a href="{% url 'cc_register' %}">Register a credit card</a>
|
||||
(this is required to be done via Javascript so that we never see
|
||||
your credit card, but it is sent directly to stripe)
|
||||
<li><a href="{% url 'stripecreditcard-list' %}">You can list your
|
||||
credit cards</a>
|
||||
By default the first credit card is used for charging
|
||||
("active: true") and later added cards will not be
|
||||
used. To change this, first disable the active flag and
|
||||
then set it on another credit card.
|
||||
</div>
|
||||
</div>
|
||||
<div id="pay" class="row">
|
||||
<div class="col"><h3>Billing Address, Payments and Balance</h3></div>
|
||||
<div class="col-8">
|
||||
<p>Billing addresses behave similar to credit cards: you can
|
||||
have many of them, but only one can be active. The active
|
||||
billing address is taken for creating new orders.</p>
|
||||
|
||||
<p>In uncloud we use the pre-paid model: you can add money to
|
||||
your account via payments. You can always check your
|
||||
balance. The products you use will automatically be charged from
|
||||
your existing balance.
|
||||
</p>
|
||||
|
||||
<p>In the future you will be able opt-in to automatically
|
||||
recharging your account at a certain time frame or whenever it
|
||||
is below a certain amount</p>
|
||||
|
||||
|
||||
<ul>
|
||||
<li><a href="{% url 'billingaddress-list' %}">Create or list
|
||||
your billing addresses</a>
|
||||
<li><a href="{% url 'orders-list' %}">List your Orders</a>
|
||||
<li><a href="{% url 'bills-list' %}">List your Bills</a>
|
||||
<li><a href="{% url 'payment-list' %}">Make a payment or list your payments</a>
|
||||
<li><a href="{% url 'payment-balance-list' %}">Show your balance</a>
|
||||
<li><a href="{% url 'machines-list' %}">Show your VM Instances</a>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="net" class="row">
|
||||
<div class="col"><h3>Networking</h3></div>
|
||||
<div class="col-8">
|
||||
<p>
|
||||
With uncloud you can use a variety of network related
|
||||
services.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li>You can <a href="{% url 'wireguardvpnnetwork-list' %}">list or
|
||||
create VPNs</a> based on wireguard
|
||||
<ul>
|
||||
<li>Checkout
|
||||
<a href="{% url 'wireguardvpnnetworksizes-list' %}">which
|
||||
network sizes are available</a> at the moment.
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="net" class="row">
|
||||
<div class="col"><h3>Current limitations</h3></div>
|
||||
<div class="col-8">
|
||||
<ul>
|
||||
<li>Payments are only possible in CHF.
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.is_authenticated %}
|
||||
<div id="account-settings" class="row">
|
||||
<div class="col"><h3>Account Settings</h3></div>
|
||||
<div class="col-8">
|
||||
<ul>
|
||||
<form method="POST">
|
||||
{% csrf_token %}
|
||||
<fieldset class="form-group">
|
||||
<legend class="border-bottom mb-4">Delete User Account</legend>
|
||||
<p>Are you sure you want to delete your account? This will permanently delete your
|
||||
profile and any orders you have generated.</p>
|
||||
{{ delete_form }}
|
||||
</fieldset>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger btn-lg" type="submit" name="action">Delete Account</button>
|
||||
</div>
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -1,65 +0,0 @@
|
|||
"""uncloud URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/3.0/topics/http/urls/
|
||||
"""
|
||||
|
||||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.conf import settings
|
||||
from django.conf.urls.static import static
|
||||
|
||||
from rest_framework import routers
|
||||
from rest_framework.schemas import get_schema_view
|
||||
|
||||
#from opennebula import views as oneviews
|
||||
from uncloud import views as uncloudviews
|
||||
from uncloud_auth import views as authviews
|
||||
from uncloud_net import views as netviews
|
||||
from uncloud_pay import views as payviews
|
||||
from uncloud_vm import views as vmviews
|
||||
from uncloud_service import views as serviceviews
|
||||
from matrixhosting import views as matrixviews
|
||||
|
||||
router = routers.DefaultRouter()
|
||||
|
||||
# Beta endpoints
|
||||
router.register(r'beta/vm', vmviews.NicoVMProductViewSet, basename='nicovmproduct')
|
||||
|
||||
################################################################################
|
||||
# v2
|
||||
|
||||
# Net
|
||||
router.register(r'v2/net/wireguardvpn', netviews.WireGuardVPNViewSet, basename='wireguardvpnnetwork')
|
||||
router.register(r'v2/net/wireguardvpnsizes', netviews.WireGuardVPNSizes, basename='wireguardvpnnetworksizes')
|
||||
|
||||
# Payment related for a user
|
||||
router.register(r'v2/payment/credit-card', payviews.CreditCardViewSet, basename='stripecreditcard')
|
||||
router.register(r'v2/payment/payment', payviews.PaymentViewSet, basename='payment')
|
||||
router.register(r'v2/payment/balance', payviews.BalanceViewSet, basename='payment-balance')
|
||||
router.register(r'v2/payment/address', payviews.BillingAddressViewSet, basename='billingaddress')
|
||||
router.register(r'v2/orders', payviews.OrderViewSet, basename='orders')
|
||||
router.register(r'v2/bill', payviews.BillViewSet, basename='bills')
|
||||
router.register(r'v2/machines', matrixviews.MachineViewSet, basename='machines')
|
||||
|
||||
# Generic helper views that are usually not needed
|
||||
router.register(r'v2/generic/vat-rate', payviews.VATRateViewSet, basename='vatrate')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
path(r'api/', include(router.urls), name='api'),
|
||||
|
||||
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), # for login to REST API
|
||||
path('openapi', get_schema_view(
|
||||
title="uncloud",
|
||||
description="uncloud API",
|
||||
version="2.0.0"
|
||||
), name='openapi-schema'),
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
path('accounts/', include('allauth.urls')),
|
||||
path('cc/reg/', payviews.RegisterCard.as_view(), name="cc_register"),
|
||||
path('matrix/', include('matrixhosting.urls', namespace='matrix')),
|
||||
path('', uncloudviews.UncloudIndex.as_view(), name="uncloudindex"),
|
||||
]
|
|
@ -1,23 +0,0 @@
|
|||
from django.views.generic.base import TemplateView
|
||||
from django.contrib import messages
|
||||
from django.shortcuts import redirect
|
||||
|
||||
from uncloud_pay.selectors import get_balance_for_user
|
||||
from .forms import UserDeleteForm
|
||||
|
||||
class UncloudIndex(TemplateView):
|
||||
template_name = "uncloud/index.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
if self.request.user.is_authenticated:
|
||||
context['balance'] = get_balance_for_user(self.request.user)
|
||||
context['delete_form'] = UserDeleteForm(instance=self.request.user)
|
||||
return context
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
UserDeleteForm(request.POST, instance=request.user)
|
||||
user = request.user
|
||||
user.delete()
|
||||
messages.info(request, 'Your account has been deleted.')
|
||||
return redirect('uncloudindex')
|
|
@ -1,21 +0,0 @@
|
|||
from django.core.management.base import BaseCommand
|
||||
from django.contrib.auth import get_user_model
|
||||
import sys
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = 'Give Admin rights to existing user'
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('username', type=str)
|
||||
parser.add_argument('--superuser', action='store_true')
|
||||
|
||||
def handle(self, *args, **options):
|
||||
user = get_user_model().objects.get(username=options['username'])
|
||||
user.is_staff = True
|
||||
|
||||
if options['superuser']:
|
||||
user.is_superuser = True
|
||||
|
||||
user.save()
|
||||
|
||||
print(f"{user.username} is now admin (superuser={user.is_superuser})")
|
|
@ -1,72 +0,0 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
from ldap3.core.exceptions import LDAPEntryAlreadyExistsResult
|
||||
from rest_framework import serializers
|
||||
|
||||
from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS
|
||||
from uncloud_pay.models import BillingAddress
|
||||
|
||||
from .ungleich_ldap import LdapManager
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
read_only_fields = [ 'username', 'balance', 'maximum_credit' ]
|
||||
fields = read_only_fields + [ 'email' ] # , 'primary_billing_address' ]
|
||||
|
||||
def validate(self, data):
|
||||
"""
|
||||
Ensure that the primary billing address belongs to the user
|
||||
"""
|
||||
# The following is raising exceptions probably, it is WIP somewhere
|
||||
# if 'primary_billing_address' in data:
|
||||
# if not data['primary_billing_address'].owner == self.instance:
|
||||
# raise serializers.ValidationError('Invalid data')
|
||||
|
||||
return data
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
ldap_manager = LdapManager()
|
||||
return_val, _ = ldap_manager.change_user_details(
|
||||
instance.username, {'mail': validated_data.get('email')}
|
||||
)
|
||||
if not return_val:
|
||||
raise serializers.ValidationError('Couldn\'t update email')
|
||||
instance.email = validated_data.get('email')
|
||||
instance.save()
|
||||
return instance
|
||||
|
||||
|
||||
class UserRegistrationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = get_user_model()
|
||||
fields = ['username', 'first_name', 'last_name', 'email', 'password']
|
||||
extra_kwargs = {
|
||||
'password': {'style': {'input_type': 'password'}},
|
||||
'first_name': {'allow_blank': False, 'required': True},
|
||||
'last_name': {'allow_blank': False, 'required': True},
|
||||
'email': {'allow_blank': False, 'required': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
ldap_manager = LdapManager()
|
||||
try:
|
||||
data = {
|
||||
'user': validated_data['username'],
|
||||
'password': validated_data['password'],
|
||||
'email': validated_data['email'],
|
||||
'firstname': validated_data['first_name'],
|
||||
'lastname': validated_data['last_name'],
|
||||
}
|
||||
ldap_manager.create_user(**data)
|
||||
except LDAPEntryAlreadyExistsResult:
|
||||
raise serializers.ValidationError(
|
||||
{'username': ['A user with that username already exists.']}
|
||||
)
|
||||
else:
|
||||
return get_user_model().objects.create_user(**validated_data)
|
||||
|
||||
|
||||
class ImportUserSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
|
@ -1,21 +0,0 @@
|
|||
{% extends 'uncloud/base.html' %}
|
||||
{% load bootstrap5 %}
|
||||
|
||||
{% block bootstrap5_content %}
|
||||
<div class="container">
|
||||
<div id="content">
|
||||
<div id="intro" class="row">
|
||||
|
||||
<h1>Login to uncloud</h1>
|
||||
<form method="post" class="form">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
{% buttons %}
|
||||
<button type="submit" class="btn btn-primary">Submit</button>
|
||||
{% endbuttons %}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -1,42 +0,0 @@
|
|||
import ldap
|
||||
# from django.conf import settings
|
||||
|
||||
AUTH_LDAP_SERVER_URI = "ldaps://ldap1.ungleich.ch,ldaps://ldap2.ungleich.ch"
|
||||
AUTH_LDAP_BIND_DN="uid=django-create,ou=system,dc=ungleich,dc=ch"
|
||||
AUTH_LDAP_BIND_PASSWORD="kS#e+v\zjKn]L!,RIu2}V+DUS"
|
||||
# AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch",
|
||||
# ldap.SCOPE_SUBTREE,
|
||||
# "(uid=%(user)s)")
|
||||
|
||||
|
||||
|
||||
ldap_object = ldap.initialize(AUTH_LDAP_SERVER_URI)
|
||||
cancelid = ldap_object.bind(AUTH_LDAP_BIND_DN, AUTH_LDAP_BIND_PASSWORD)
|
||||
|
||||
res = ldap_object.search_s("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=nico)")
|
||||
print(res)
|
||||
|
||||
# class LDAP(object):
|
||||
# """
|
||||
# Managing users in LDAP
|
||||
|
||||
# Requires the following settings?
|
||||
|
||||
# LDAP_USER_DN: where to create users in the tree
|
||||
|
||||
# LDAP_ADMIN_DN: which DN to use for managing users
|
||||
# LDAP_ADMIN_PASSWORD: which password to used
|
||||
|
||||
# This module will reuse information from djagno_auth_ldap, including:
|
||||
|
||||
# AUTH_LDAP_SERVER_URI
|
||||
|
||||
# """
|
||||
# def __init__(self):
|
||||
# pass
|
||||
|
||||
# def create_user(self):
|
||||
# pass
|
||||
|
||||
# def change_password(self):
|
||||
# pass
|
|
@ -1,284 +0,0 @@
|
|||
import base64
|
||||
import hashlib
|
||||
import logging
|
||||
import random
|
||||
|
||||
import ldap3
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LdapManager:
|
||||
__instance = None
|
||||
def __new__(cls):
|
||||
if LdapManager.__instance is None:
|
||||
LdapManager.__instance = object.__new__(cls)
|
||||
return LdapManager.__instance
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the LDAP subsystem.
|
||||
"""
|
||||
self.rng = random.SystemRandom()
|
||||
self.server = ldap3.Server(settings.AUTH_LDAP_SERVER)
|
||||
|
||||
|
||||
def get_admin_conn(self):
|
||||
"""
|
||||
Return a bound :class:`ldap3.Connection` instance which has write
|
||||
permissions on the dn in which the user accounts reside.
|
||||
"""
|
||||
conn = self.get_conn(user=settings.LDAP_ADMIN_DN,
|
||||
password=settings.LDAP_ADMIN_PASSWORD,
|
||||
raise_exceptions=True)
|
||||
conn.bind()
|
||||
return conn
|
||||
|
||||
|
||||
def get_conn(self, **kwargs):
|
||||
"""
|
||||
Return an unbound :class:`ldap3.Connection` which talks to the configured
|
||||
LDAP server.
|
||||
|
||||
The *kwargs* are passed to the constructor of :class:`ldap3.Connection` and
|
||||
can be used to set *user*, *password* and other useful arguments.
|
||||
"""
|
||||
return ldap3.Connection(self.server, **kwargs)
|
||||
|
||||
|
||||
def _ssha_password(self, password):
|
||||
"""
|
||||
Apply the SSHA password hashing scheme to the given *password*.
|
||||
*password* must be a :class:`bytes` object, containing the utf-8
|
||||
encoded password.
|
||||
|
||||
Return a :class:`bytes` object containing ``ascii``-compatible data
|
||||
which can be used as LDAP value, e.g. after armoring it once more using
|
||||
base64 or decoding it to unicode from ``ascii``.
|
||||
"""
|
||||
SALT_BYTES = 15
|
||||
|
||||
sha1 = hashlib.sha1()
|
||||
salt = self.rng.getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES,
|
||||
"little")
|
||||
sha1.update(password)
|
||||
sha1.update(salt)
|
||||
|
||||
digest = sha1.digest()
|
||||
passwd = b"{SSHA}" + base64.b64encode(digest + salt)
|
||||
return passwd
|
||||
|
||||
|
||||
def create_user(self, user, password, firstname, lastname, email):
|
||||
conn = self.get_admin_conn()
|
||||
uidNumber = self._get_max_uid() + 1
|
||||
logger.debug("uidNumber={uidNumber}".format(uidNumber=uidNumber))
|
||||
user_exists = True
|
||||
while user_exists:
|
||||
user_exists, _ = self.check_user_exists(
|
||||
"",
|
||||
'(&(objectClass=inetOrgPerson)(objectClass=posixAccount)'
|
||||
'(objectClass=top)(uidNumber={uidNumber}))'.format(
|
||||
uidNumber=uidNumber
|
||||
)
|
||||
)
|
||||
if user_exists:
|
||||
logger.debug(
|
||||
"{uid} exists. Trying next.".format(uid=uidNumber)
|
||||
)
|
||||
uidNumber += 1
|
||||
logger.debug("{uid} does not exist. Using it".format(uid=uidNumber))
|
||||
self._set_max_uid(uidNumber)
|
||||
try:
|
||||
uid = user # user.encode("utf-8")
|
||||
conn.add("uid={uid},{customer_dn}".format(
|
||||
uid=uid, customer_dn=settings.LDAP_CUSTOMER_DN
|
||||
),
|
||||
["inetOrgPerson", "posixAccount", "ldapPublickey"],
|
||||
{
|
||||
"uid": [uid],
|
||||
"sn": [lastname.encode("utf-8")],
|
||||
"givenName": [firstname.encode("utf-8")],
|
||||
"cn": [uid],
|
||||
"displayName": ["{} {}".format(firstname, lastname).encode("utf-8")],
|
||||
"uidNumber": [str(uidNumber)],
|
||||
"gidNumber": [str(settings.LDAP_CUSTOMER_GROUP_ID)],
|
||||
"loginShell": ["/bin/bash"],
|
||||
"homeDirectory": ["/home/{}".format(user).encode("utf-8")],
|
||||
"mail": email.encode("utf-8"),
|
||||
"userPassword": [self._ssha_password(
|
||||
password.encode("utf-8")
|
||||
)]
|
||||
}
|
||||
)
|
||||
logger.debug('Created user %s %s' % (user.encode('utf-8'),
|
||||
uidNumber))
|
||||
except Exception as ex:
|
||||
logger.debug('Could not create user %s' % user.encode('utf-8'))
|
||||
logger.error("Exception: " + str(ex))
|
||||
raise
|
||||
finally:
|
||||
conn.unbind()
|
||||
|
||||
|
||||
def change_password(self, uid, new_password):
|
||||
"""
|
||||
Changes the password of the user identified by user_dn
|
||||
|
||||
:param uid: str The uid that identifies the user
|
||||
:param new_password: str The new password string
|
||||
:return: True if password was changed successfully False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
|
||||
# Make sure the user exists first to change his/her details
|
||||
user_exists, entries = self.check_user_exists(
|
||||
uid=uid,
|
||||
search_base=settings.ENTIRE_SEARCH_BASE
|
||||
)
|
||||
return_val = False
|
||||
if user_exists:
|
||||
try:
|
||||
return_val = conn.modify(
|
||||
entries[0].entry_dn,
|
||||
{
|
||||
"userpassword": (
|
||||
ldap3.MODIFY_REPLACE,
|
||||
[self._ssha_password(new_password.encode("utf-8"))]
|
||||
)
|
||||
}
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.error("Exception: " + str(ex))
|
||||
else:
|
||||
logger.error("User {} not found".format(uid))
|
||||
|
||||
conn.unbind()
|
||||
return return_val
|
||||
|
||||
def change_user_details(self, uid, details):
|
||||
"""
|
||||
Updates the user details as per given values in kwargs of the user
|
||||
identified by user_dn.
|
||||
|
||||
Assumes that all attributes passed in kwargs are valid.
|
||||
|
||||
:param uid: str The uid that identifies the user
|
||||
:param details: dict A dictionary containing the new values
|
||||
:return: True if user details were updated successfully False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
|
||||
# Make sure the user exists first to change his/her details
|
||||
user_exists, entries = self.check_user_exists(
|
||||
uid=uid,
|
||||
search_base=settings.ENTIRE_SEARCH_BASE
|
||||
)
|
||||
|
||||
return_val = False
|
||||
if user_exists:
|
||||
details_dict = {k: (ldap3.MODIFY_REPLACE, [v.encode("utf-8")]) for
|
||||
k, v in details.items()}
|
||||
try:
|
||||
return_val = conn.modify(entries[0].entry_dn, details_dict)
|
||||
msg = "success"
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
logger.error("Exception: " + msg)
|
||||
finally:
|
||||
conn.unbind()
|
||||
else:
|
||||
msg = "User {} not found".format(uid)
|
||||
logger.error(msg)
|
||||
conn.unbind()
|
||||
return return_val, msg
|
||||
|
||||
def check_user_exists(self, uid, search_filter="", attributes=None,
|
||||
search_base=settings.LDAP_CUSTOMER_DN):
|
||||
"""
|
||||
Check if the user with the given uid exists in the customer group.
|
||||
|
||||
:param uid: str representing the user
|
||||
:param search_filter: str representing the filter condition to find
|
||||
users. If its empty, the search finds the user with
|
||||
the given uid.
|
||||
:param attributes: list A list of str representing all the attributes
|
||||
to be obtained in the result entries
|
||||
:param search_base: str
|
||||
:return: tuple (bool, [ldap3.abstract.entry.Entry ..])
|
||||
A bool indicating if the user exists
|
||||
A list of all entries obtained in the search
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
entries = []
|
||||
try:
|
||||
result = conn.search(
|
||||
search_base=search_base,
|
||||
search_filter=search_filter if len(search_filter)> 0 else
|
||||
'(uid={uid})'.format(uid=uid),
|
||||
attributes=attributes
|
||||
)
|
||||
entries = conn.entries
|
||||
finally:
|
||||
conn.unbind()
|
||||
return result, entries
|
||||
|
||||
def delete_user(self, uid):
|
||||
"""
|
||||
Deletes the user with the given uid from ldap
|
||||
|
||||
:param uid: str representing the user
|
||||
:return: True if the delete was successful False otherwise
|
||||
"""
|
||||
conn = self.get_admin_conn()
|
||||
try:
|
||||
return_val = conn.delete(
|
||||
("uid={uid}," + settings.LDAP_CUSTOMER_DN).format(uid=uid),
|
||||
)
|
||||
msg = "success"
|
||||
except Exception as ex:
|
||||
msg = str(ex)
|
||||
logger.error("Exception: " + msg)
|
||||
return_val = False
|
||||
finally:
|
||||
conn.unbind()
|
||||
return return_val, msg
|
||||
|
||||
def _set_max_uid(self, max_uid):
|
||||
"""
|
||||
a utility function to save max_uid value to a file
|
||||
|
||||
:param max_uid: an integer representing the max uid
|
||||
:return:
|
||||
"""
|
||||
with open(settings.LDAP_MAX_UID_FILE_PATH, 'w+') as handler:
|
||||
handler.write(str(max_uid))
|
||||
|
||||
def _get_max_uid(self):
|
||||
"""
|
||||
A utility function to read the max uid value that was previously set
|
||||
|
||||
:return: An integer representing the max uid value that was previously
|
||||
set
|
||||
"""
|
||||
try:
|
||||
with open(settings.LDAP_MAX_UID_FILE_PATH, 'r+') as handler:
|
||||
try:
|
||||
return_value = int(handler.read())
|
||||
except ValueError as ve:
|
||||
logger.error(
|
||||
"Error reading int value from {}. {}"
|
||||
"Returning default value {} instead".format(
|
||||
settings.LDAP_MAX_UID_PATH,
|
||||
str(ve),
|
||||
settings.LDAP_DEFAULT_START_UID
|
||||
)
|
||||
)
|
||||
return_value = settings.LDAP_DEFAULT_START_UID
|
||||
return return_value
|
||||
except FileNotFoundError as fnfe:
|
||||
logger.error("File not found : " + str(fnfe))
|
||||
return_value = settings.LDAP_DEFAULT_START_UID
|
||||
logger.error("So, returning UID={}".format(return_value))
|
||||
return return_value
|
|
@ -1,77 +0,0 @@
|
|||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth import logout
|
||||
|
||||
from django_auth_ldap.backend import LDAPBackend
|
||||
from rest_framework import mixins, permissions, status, viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
|
||||
from .serializers import *
|
||||
|
||||
|
||||
class LoginView(auth_views.LoginView):
|
||||
template_name = 'uncloud_auth/login.html'
|
||||
|
||||
class LogoutView(auth_views.LogoutView):
|
||||
pass
|
||||
# template_name = 'uncloud_auth/logo.html'
|
||||
|
||||
|
||||
class UserViewSet(viewsets.GenericViewSet):
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
serializer_class = UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.user
|
||||
|
||||
def list(self, request, format=None):
|
||||
# This is a bit stupid: we have a user, we create a queryset by
|
||||
# matching on the username. But I don't know a "nicer" way.
|
||||
# Nico, 2020-03-18
|
||||
user = request.user
|
||||
serializer = self.get_serializer(user, context = {'request': request})
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=False, methods=['post'])
|
||||
def change_email(self, request):
|
||||
serializer = self.get_serializer(
|
||||
request.user, data=request.data, context={'request': request}
|
||||
)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
serializer.save()
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class AccountManagementViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
|
||||
serializer_class = UserRegistrationSerializer
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
self.perform_create(serializer)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(
|
||||
serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||||
)
|
||||
|
||||
|
||||
class AdminUserViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
permission_classes = [permissions.IsAdminUser]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'import_from_ldap':
|
||||
return ImportUserSerializer
|
||||
else:
|
||||
return UserSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return get_user_model().objects.all()
|
||||
|
||||
@action(detail=False, methods=['post'], url_path='import_from_ldap')
|
||||
def import_from_ldap(self, request, pk=None):
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
ldap_username = serializer.validated_data.pop("username")
|
||||
user = LDAPBackend().populate_user(ldap_username)
|
||||
|
||||
return Response(UserSerializer(user, context = {'request': request}).data)
|
|
@ -10,7 +10,6 @@
|
|||
| SSH -L tunnel | All nodes can use [::1]:5432 | SSH setup can be fragile |
|
||||
| ssh djangohost manage.py | All DB ops locally | Code is only executed on django host |
|
||||
| https + token | Rest alike / consistent access | Code is only executed on django host |
|
||||
| from_django | Everything is on the django host | main host can become bottleneck |
|
||||
** remote vs. local Django code execution
|
||||
- If manage.py is executed locally (= on the client), it can
|
||||
check/modify local configs
|
||||
|
@ -20,9 +19,3 @@
|
|||
- Remote execution (= on the primary django host) can acess the db
|
||||
via unix socket
|
||||
- However remote execution cannot check local state
|
||||
** from_django
|
||||
- might reuse existing methods like celery
|
||||
- reduces the amount of things to be installed on the client to
|
||||
almost zero
|
||||
- follows the opennebula model
|
||||
- has a single point of failurebin
|
82
uncloud_django_based/uncloud/doc/README-object-relations.md
Normal file
82
uncloud_django_based/uncloud/doc/README-object-relations.md
Normal file
|
@ -0,0 +1,82 @@
|
|||
## Introduction
|
||||
|
||||
This article describes how models relate to each other and what the
|
||||
design ideas are. It is meant to prevent us from double implementing
|
||||
something or changing something that is already solved.
|
||||
|
||||
|
||||
## Products
|
||||
|
||||
A product is something someone can order. We might have "low level"
|
||||
products that need to be composed (= higher degree of flexibility, but
|
||||
more amount of details necessary) and "composed products" that present
|
||||
some defaults or select other products automatically (f.i. a "dual
|
||||
stack VM" can be a VM + a disk + an IPv4 address).
|
||||
|
||||
|
||||
## Bills
|
||||
|
||||
Bills represent active orders of a month. Bills can be shown during a
|
||||
month but only become definitive at the end of the month.
|
||||
|
||||
## Orders
|
||||
|
||||
When customer X order a (set) of product, it generates an order for billing
|
||||
purposes. The ordered products point to that order and register an Order Record
|
||||
at creation.
|
||||
|
||||
Orders and Order Records are assumed immutable => they are used to generate
|
||||
bills and should not be mutated. If a product is updated (e.g. adding RAM to
|
||||
VM), a new order should be generated.
|
||||
|
||||
The order MUST NOT be deleted when a product is deleted, as it is used for
|
||||
billing (including past bills).
|
||||
|
||||
### Order record
|
||||
|
||||
Used to store billing details of a product at creation: will stay there even if
|
||||
the product change (e.g. new pricing, updated) and act as some kind of archive.
|
||||
Used to generate bills.
|
||||
|
||||
## Payment Methods
|
||||
|
||||
Users/customers can register payment methods.
|
||||
|
||||
## Sample flows / products
|
||||
|
||||
### A VM snapshot
|
||||
|
||||
A VM snapshot creates a snapshot of all disks attached to a VM to be
|
||||
able to rollback the VM to a previous state.
|
||||
|
||||
Creating a VM snapshot (-product) creates a related order. Deleting a
|
||||
VMSnapshotproduct sets the order to deleted.
|
||||
|
||||
### Object Storage
|
||||
|
||||
(tbd by Balazs)
|
||||
|
||||
### A "raw" VM
|
||||
|
||||
(tbd by Ahmed)
|
||||
|
||||
### An IPv6 only VM
|
||||
|
||||
(tbd by Ahmed)
|
||||
|
||||
### A dual stack VM
|
||||
|
||||
(tbd by Ahmed)
|
||||
|
||||
### A managed service (e.g. Matrix-as-a-Service)
|
||||
|
||||
Customer orders service with:
|
||||
* Service-specific configuration: e.g. domain name for matrix
|
||||
* VM configuration:
|
||||
- CPU
|
||||
- Memory
|
||||
- Disk (soon)
|
||||
|
||||
It creates a new Order with two products/records:
|
||||
* Service itself (= management)
|
||||
* Underlying VM
|
8
uncloud_django_based/uncloud/doc/README-postgresql.org
Normal file
8
uncloud_django_based/uncloud/doc/README-postgresql.org
Normal file
|
@ -0,0 +1,8 @@
|
|||
* uncloud clients access the data base from a variety of outside hosts
|
||||
* So the postgresql data base needs to be remotely accessible
|
||||
* Instead of exposing the tcp socket, we make postgresql bind to localhost via IPv6
|
||||
** ::1, port 5432
|
||||
* Then we remotely connect to the database server with ssh tunneling
|
||||
** ssh -L5432:localhost:5432 uncloud-database-host
|
||||
* Configuring your database for SSH based remote access
|
||||
** host all all ::1/128 trust
|
34
uncloud_django_based/uncloud/doc/README-products.md
Normal file
34
uncloud_django_based/uncloud/doc/README-products.md
Normal file
|
@ -0,0 +1,34 @@
|
|||
## Introduction
|
||||
|
||||
This document describes how to create, modify or
|
||||
delete a product and use it.
|
||||
|
||||
A product (like a VMSnapshotproduct) creates an order when ordered.
|
||||
The "order" is used to combine products together.
|
||||
|
||||
Sub-products or related products link to the same order.
|
||||
Each product has one (?) orderrecord
|
||||
|
||||
|
||||
## How to delete a product (logic 1)
|
||||
|
||||
If a user want so delete (=cancel) a product, the following steps
|
||||
should be taken:
|
||||
|
||||
* the associated order is set to cancelled
|
||||
* the product itself is deleted
|
||||
|
||||
[above steps to be reviewed]
|
||||
|
||||
## How to delete a product (rest api)
|
||||
|
||||
http -a nicoschottelius:$(pass
|
||||
ungleich.ch/nico.schottelius@ungleich.ch)
|
||||
http://localhost:8000/net/vpn/43c83088-f4d6-49b9-86c7-40251ac07ada/
|
||||
|
||||
-> does not delete the reservation.
|
||||
|
||||
|
||||
### Deleting a VPN
|
||||
|
||||
When the product is deleted, the network must be marked as free.
|
34
uncloud_django_based/uncloud/doc/README-vpn.org
Normal file
34
uncloud_django_based/uncloud/doc/README-vpn.org
Normal file
|
@ -0,0 +1,34 @@
|
|||
* How to add a new VPN Host
|
||||
** Install wireguard to the host
|
||||
** Install uncloud to the host
|
||||
** Add `python manage.py vpn --hostname fqdn-of-this-host` to the crontab
|
||||
** Use the CLI to configure one or more VPN Networks for this host
|
||||
* Example of adding a VPN host at ungleich
|
||||
** Create a new dual stack alpine VM
|
||||
** Add it to DNS as vpn-XXX.ungleich.ch
|
||||
** Route a /40 network to its IPv6 address
|
||||
** Install wireguard on it
|
||||
** TODO Enable wireguard on boot
|
||||
** TODO Create a new VPNPool on uncloud with
|
||||
*** the network address (selecting from our existing pool)
|
||||
*** the network size (/...)
|
||||
*** the vpn host that provides the network (selecting the created VM)
|
||||
*** the wireguard private key of the vpn host (using wg genkey)
|
||||
*** http command
|
||||
```
|
||||
http -a nicoschottelius:$(pass
|
||||
ungleich.ch/nico.schottelius@ungleich.ch)
|
||||
http://localhost:8000/admin/vpnpool/ network=2a0a:e5c1:200:: \
|
||||
network_size=40 subnetwork_size=48
|
||||
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch
|
||||
wireguard_private_key=...
|
||||
```
|
||||
* Example http commands / REST calls
|
||||
** creating a new vpn pool
|
||||
http -a nicoschottelius:$(pass
|
||||
ungleich.ch/nico.schottelius@ungleich.ch)
|
||||
http://localhost:8000/admin/vpnpool/ network_size=40
|
||||
subnetwork_size=48 network=2a0a:e5c1:200::
|
||||
vpn_hostname=vpn-2a0ae5c1200.ungleich.ch wireguard_private_key=$(wg
|
||||
genkey)
|
||||
** Creating a new vpn network
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue