Compare commits

..

1 commit

387 changed files with 4023 additions and 11123 deletions

3
.gitignore vendored
View file

@ -22,6 +22,5 @@ uncloud/version.py
build/
venv/
dist/
.history/
*.iso
*.sqlite3

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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 ..."

View file

@ -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;
# }
# }

View file

@ -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

View file

@ -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"

View file

@ -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
View file

@ -1,2 +0,0 @@
*.pdf
*.tex

View file

@ -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

View file

@ -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

View file

@ -1,4 +0,0 @@
from django.contrib import admin
from .models import VMInstance
admin.site.register(VMInstance)

View file

@ -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

View file

@ -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']

View file

@ -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)),
],
),
]

View file

@ -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',
),
]

View file

@ -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),
),
]

View file

@ -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')),
],
),
]

View file

@ -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',
),
]

View file

@ -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',
),
]

View file

@ -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)),
],
),
]

View file

@ -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),
),
]

View file

@ -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

View file

@ -1,8 +0,0 @@
from rest_framework import serializers
from .models import *
class VMInstanceSerializer(serializers.ModelSerializer):
class Meta:
model = VMInstance
fields = '__all__'

View file

@ -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

View file

@ -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;
}

View file

@ -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);

View file

@ -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");
}
}
});
}
});

View file

@ -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);
});
});

View file

@ -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)

View file

@ -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>

View file

@ -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 %}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 &copy; ungleich glarus ag {% now "Y" %}. {% trans "All Rights Reserved" %}</p>
</div>
</footer>

View file

@ -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" %}&nbsp;&nbsp;<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>

View file

@ -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 %}

View file

@ -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>&nbsp;
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>

View file

@ -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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;{% 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>

View file

@ -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)

View file

@ -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'),
]

View file

@ -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()

View file

@ -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)

View file

@ -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 }}

View file

@ -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()),
],
),
]

View file

@ -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

View file

View file

@ -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
View file

@ -1,2 +0,0 @@
local_settings.py
ldap_max_uid_file

View 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__ = ()

View file

@ -1,6 +0,0 @@
from django.contrib import admin
from .models import *
for m in [ UncloudProvider, UncloudNetwork ]:
admin.site.register(m)

View file

@ -1,8 +0,0 @@
from django import forms
from django.contrib.auth.models import User
class UserDeleteForm(forms.ModelForm):
class Meta:
model = User
fields = []

View file

@ -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

View file

@ -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)),
],
),
]

View file

@ -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

View file

@ -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',
),
]

View file

@ -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}"

View file

@ -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))

View file

@ -1,4 +0,0 @@
#content {
width: 400px;
margin: auto;
}

View file

@ -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 %}

View file

@ -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 %}

View file

@ -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"),
]

View file

@ -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')

View file

@ -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})")

View file

@ -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()

View file

@ -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 %}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View 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

View 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

View 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.

View 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