diff --git a/.gitignore b/.gitignore index 426f11b..cbb171f 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,5 @@ uncloud/version.py build/ venv/ dist/ -.history/ + *.iso -*.sqlite3 diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e468591..afdc4a1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/README.md b/README.md index 07f5c91..0e32f57 100644 --- a/README.md +++ b/README.md @@ -1,70 +1,3 @@ -# Uncloud +# ucloud -Cloud management platform, the ungleich way. - - -[![pipeline status](https://code.ungleich.ch/uncloud/uncloud/badges/master/pipeline.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) -[![coverage report](https://code.ungleich.ch/uncloud/uncloud/badges/master/coverage.svg)](https://code.ungleich.ch/uncloud/uncloud/commits/master) - -## Useful commands - -* `./manage.py import-vat-rates path/to/csv` -* `./manage.py createsuperuser` - -## Development setup - -Install system dependencies: - -* On Fedora, you will need the following packages: `python3-virtualenv python3-devel openldap-devel gcc chromium` -* sudo apt-get install libpq-dev python-dev libxml2-dev libxslt1-dev libldap2-dev libsasl2-dev libffi-dev - - -NOTE: you will need to configure a LDAP server and credentials for authentication. See `uncloud/settings.py`. - -``` -# Initialize virtualenv. -» virtualenv .venv -Using base prefix '/usr' -New python executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python3 -Also creating executable in /home/fnux/Workspace/ungleich/uncloud/uncloud/.venv/bin/python -Installing setuptools, pip, wheel... -done. - -# Enter virtualenv. -» source .venv/bin/activate - -# Install dependencies. -» pip install -r requirements.txt -[...] - -# Run migrations. -» ./manage.py migrate -Operations to perform: - Apply all migrations: admin, auth, contenttypes, opennebula, sessions, uncloud_auth, uncloud_net, uncloud_pay, uncloud_service, uncloud_vm -Running migrations: - [...] - -# Run webserver. -» ./manage.py runserver -Watching for file changes with StatReloader -Performing system checks... - -System check identified no issues (0 silenced). -May 07, 2020 - 10:17:08 -Django version 3.0.6, using settings 'uncloud.settings' -Starting development server at http://127.0.0.1:8000/ -Quit the server with CONTROL-C. -``` -### Run Background Job Queue -We use Django Q to handle the asynchronous code and Background Cron jobs -To start the workers make sure first that Redis or the Django Q broker is working and you can edit it's settings in the settings file. -``` -./manage.py qcluster -``` - -### Note on PGSQL - -If you want to use Postgres: - -* Install on configure PGSQL on your base system. -* OR use a container! `podman run --rm -p 5432:5432 -e POSTGRES_HOST_AUTH_METHOD=trust -it postgres:latest` +Checkout https://ungleich.ch/ucloud/ for the documentation of ucloud. diff --git a/archive/issues.org b/archive/issues.org deleted file mode 100644 index 840ec3c..0000000 --- a/archive/issues.org +++ /dev/null @@ -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 diff --git a/archive/uncloud_django_based/hacks/command-wrapper.sh b/archive/uncloud_django_based/hacks/command-wrapper.sh deleted file mode 100644 index d6ddd13..0000000 --- a/archive/uncloud_django_based/hacks/command-wrapper.sh +++ /dev/null @@ -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 ..." diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules-v2 b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules-v2 deleted file mode 100644 index b6d4cf3..0000000 --- a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules-v2 +++ /dev/null @@ -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; -# } -# } diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm-2.sh b/archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm-2.sh deleted file mode 100755 index af9dec7..0000000 --- a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm-2.sh +++ /dev/null @@ -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 diff --git a/bin/deploy.sh b/bin/deploy.sh deleted file mode 100755 index 99f7ba0..0000000 --- a/bin/deploy.sh +++ /dev/null @@ -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" diff --git a/bin/make-migrations-from-scratch.sh b/bin/make-migrations-from-scratch.sh deleted file mode 100755 index 8baccfa..0000000 --- a/bin/make-migrations-from-scratch.sh +++ /dev/null @@ -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 diff --git a/doc/.gitignore b/doc/.gitignore deleted file mode 100644 index b51a70d..0000000 --- a/doc/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.pdf -*.tex diff --git a/doc/README-billing.org b/doc/README-billing.org deleted file mode 100644 index 50b26fa..0000000 --- a/doc/README-billing.org +++ /dev/null @@ -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 diff --git a/doc/uncloud-manual-2020-08-01.org b/doc/uncloud-manual-2020-08-01.org deleted file mode 100644 index b997600..0000000 --- a/doc/uncloud-manual-2020-08-01.org +++ /dev/null @@ -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 -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 diff --git a/matrixhosting/admin.py b/matrixhosting/admin.py deleted file mode 100644 index c33589b..0000000 --- a/matrixhosting/admin.py +++ /dev/null @@ -1,4 +0,0 @@ -from django.contrib import admin -from .models import VMInstance - -admin.site.register(VMInstance) diff --git a/matrixhosting/apps.py b/matrixhosting/apps.py deleted file mode 100644 index ad02796..0000000 --- a/matrixhosting/apps.py +++ /dev/null @@ -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 diff --git a/matrixhosting/forms.py b/matrixhosting/forms.py deleted file mode 100644 index 793aeeb..0000000 --- a/matrixhosting/forms.py +++ /dev/null @@ -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'] - - diff --git a/matrixhosting/migrations/0001_initial.py b/matrixhosting/migrations/0001_initial.py deleted file mode 100644 index dce49c3..0000000 --- a/matrixhosting/migrations/0001_initial.py +++ /dev/null @@ -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)), - ], - ), - ] diff --git a/matrixhosting/migrations/0002_rename_vmpricing_matrixvmpricing.py b/matrixhosting/migrations/0002_rename_vmpricing_matrixvmpricing.py deleted file mode 100644 index f21241d..0000000 --- a/matrixhosting/migrations/0002_rename_vmpricing_matrixvmpricing.py +++ /dev/null @@ -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', - ), - ] diff --git a/matrixhosting/migrations/0003_auto_20210703_1523.py b/matrixhosting/migrations/0003_auto_20210703_1523.py deleted file mode 100644 index fe45ab0..0000000 --- a/matrixhosting/migrations/0003_auto_20210703_1523.py +++ /dev/null @@ -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), - ), - ] diff --git a/matrixhosting/migrations/0004_matrixhostingorder_vmspecs.py b/matrixhosting/migrations/0004_matrixhostingorder_vmspecs.py deleted file mode 100644 index 0259c51..0000000 --- a/matrixhosting/migrations/0004_matrixhostingorder_vmspecs.py +++ /dev/null @@ -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')), - ], - ), - ] diff --git a/matrixhosting/migrations/0005_auto_20210705_0849.py b/matrixhosting/migrations/0005_auto_20210705_0849.py deleted file mode 100644 index 742a63f..0000000 --- a/matrixhosting/migrations/0005_auto_20210705_0849.py +++ /dev/null @@ -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', - ), - ] diff --git a/matrixhosting/migrations/0006_delete_matrixvmpricing.py b/matrixhosting/migrations/0006_delete_matrixvmpricing.py deleted file mode 100644 index f6b0f01..0000000 --- a/matrixhosting/migrations/0006_delete_matrixvmpricing.py +++ /dev/null @@ -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', - ), - ] diff --git a/matrixhosting/migrations/0007_vminstance.py b/matrixhosting/migrations/0007_vminstance.py deleted file mode 100644 index 2990e10..0000000 --- a/matrixhosting/migrations/0007_vminstance.py +++ /dev/null @@ -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)), - ], - ), - ] diff --git a/matrixhosting/migrations/0009_vminstance_vm_id.py b/matrixhosting/migrations/0009_vminstance_vm_id.py deleted file mode 100644 index 2771f58..0000000 --- a/matrixhosting/migrations/0009_vminstance_vm_id.py +++ /dev/null @@ -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), - ), - ] diff --git a/matrixhosting/models.py b/matrixhosting/models.py deleted file mode 100644 index 8945f69..0000000 --- a/matrixhosting/models.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/matrixhosting/serializers.py b/matrixhosting/serializers.py deleted file mode 100644 index 7711612..0000000 --- a/matrixhosting/serializers.py +++ /dev/null @@ -1,8 +0,0 @@ -from rest_framework import serializers - -from .models import * - -class VMInstanceSerializer(serializers.ModelSerializer): - class Meta: - model = VMInstance - fields = '__all__' \ No newline at end of file diff --git a/matrixhosting/signals.py b/matrixhosting/signals.py deleted file mode 100644 index 494a1fc..0000000 --- a/matrixhosting/signals.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/matrixhosting/static/matrixhosting/css/common.css b/matrixhosting/static/matrixhosting/css/common.css deleted file mode 100644 index 6ef2b64..0000000 --- a/matrixhosting/static/matrixhosting/css/common.css +++ /dev/null @@ -1,1346 +0,0 @@ -body, -html { - width: 100%; - height: 100%; -} - -body, -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: 'Lato', sans-serif; -} - - -/* bootstrap danger color override from #a94442 */ - -.text-danger, -.has-error .help-block, -.has-error .control-label, -.has-error .radio, -.has-error .checkbox, -.has-error .radio-inline, -.has-error .checkbox-inline, -.has-error.radio label, -.has-error.checkbox label, -.has-error.radio-inline label, -.has-error.checkbox-inline label, -.has-error .form-control-feedback, -.alert-danger, -.list-group-item-danger, -a.list-group-item-danger, -a.list-group-item-danger:hover, -a.list-group-item-danger:focus, -.panel-danger>.panel-heading { - color: #eb4d5c; -} - -.alert-danger { - background: rgba(235, 204, 209, 0.2); -} - -.has-error .form-control, -.has-error .form-control:focus, -.has-error .form-control:active, -.has-error .input-group-addon { - color: #eb4d5c; - border-color: #eb4d5c; -} - -a.list-group-item-danger.active, -a.list-group-item-danger.active:hover, -a.list-group-item-danger.active:focus { - background-color: #eb4d5c; - border-color: #eb4d5c; -} - -.panel-danger>.panel-heading .badge { - background-color: #eb4d5c; -} - -.topnav { - font-size: 14px; -} - -.navbar-default { - background: #fff; - padding: 5px; -} - -.navbar-brand { - padding: 10px; -} - -.navbar-brand > img { - height: 100%; -} - -#logoWhite, -.navbar-transparent #logoBlack { - display: none; -} - -#logoBlack, -.navbar-transparent #logoWhite { - display: block; -} - -@media (min-width: 768px) { - .navbar-right { - margin-right: 10px; - } - .navbar-brand { - padding-right: 15px; - padding-left: 15px; - } -} - -.navbar .dcl-link { - display: block; - padding: 15px; - color: #777; -} - -.navbar .dcl-link:focus, -.navbar .dcl-link:active, -.navbar .dcl-link:hover { - text-decoration: none; -} - -.navbar .dropdown-menu .dcl-link { - padding: 1px 10px; -} - -p.copyright { - margin: 0; -} - -footer { - font-weight: 300; - padding: 25px 0; - background-color: #f8f8f8; -} - -footer .list-inline { - margin-bottom: 15px; -} - -footer a { - color: #777; -} - -footer .dcl-link-separator { - position: relative; - padding-left: 10px; -} - -footer .dcl-link-separator::before { - content: ""; - position: absolute; - display: inline-block; - top: 9px; - bottom: 0; - left: -2px; - right: 0; - width: 2px; - height: 2px; - border-radius: 100%; - background: #777; -} - -.mb-0 { - margin-bottom: 0; -} - -.thin-hr { - margin-top: 10px; - margin-bottom: 10px; -} - -.payment-container .credit-card-info { - padding-bottom: 15px; - border-bottom: 1px solid #eee; -} -.credit-card-info { - display: flex; -} - -.credit-card-info .align-bottom { - align-self: flex-end; - padding-right: 0 !important; -} - -.new-card-head { - margin-top: 10px; -} -.new-card-button-margin button{ - margin-top: 5px; - margin-bottom: 5px; -} - -.input-no-border { - border: none !important; - background: transparent !important; - resize: none; -} - -.existing-keys-title { - font-weight: bold; - font-size: 14px; -} - -@media(max-width:767px) { - .vspace-top { - margin-top: 35px; - } -} - -/* index */ -.btn { - box-shadow: 0 1px 4px rgba(0, 0, 0, .6); -} - -.fa-li.fa-lg { - color: #29427A; - margin-top: 6px; -} - -.btn-transparent { - background: transparent; - border: 2px solid #fff; - color: #fff; - transition: all .2s ease-in; -} - -.btn-primary { - background: #29427A; - border-color: #29427A; - color: #fff; - width: auto; -} - -.btn-primary:hover { - background: rgba(41, 66, 122, 0.8); - border-color: #29427A; -} - -.btn-transparent:hover { - background: #fff; - border: 2px solid #fff; - color: #000; - transition: all .2s ease-in; -} - -.btn-lg { - min-width: 180px; -} - -.lead { - font-size: 18px; -} - -@media (min-width: 768px) { - .lead { - font-size: 21px; - } -} - - -/* Top navbar */ - -.navbar { - transition: all .3s ease-in; - font-weight: 400; -} - -.navbar-default .navbar-nav>.open>a, -.navbar-default .navbar-nav>.open>a:focus, -.navbar-default .navbar-nav>.open>a:hover { - background: transparent; -} - -.navbar-default .navbar-nav>.active>a, -.navbar-default .navbar-nav>.active>a:focus, -.navbar-default .navbar-nav>.active>a:hover { - background: #2D457A; - color: #fff; - border-radius: 6px; -} - -@media (max-width: 767px) { - .navbar-default .navbar-nav>li>a{ - font-weight: 400; - } -} - -.navbar-transparent .navbar-nav>li a, -.navbar-transparent .navbar-nav>.open>a, -.navbar-transparent .navbar-nav>.open>a:focus, -.navbar-transparent .navbar-nav>.open>a:hover { - color: #fff; -} - - -.navbar-transparent .navbar-nav>li a:focus, -.navbar-transparent .navbar-nav>li a:active, -.navbar-transparent .navbar-nav>li a:hover { - color: #fff; - background-color: transparent; - text-decoration: none; -} - -.topnav .nav .open>a, -.topnav .nav .open>a:focus, -.topnav .nav .open>a:hover { - background: transparent; -} - -.navbar-transparent .navbar-nav>li>.on-hover-border { - transition: all 0.3s linear; - box-shadow: none; -} - -.navbar-transparent .navbar-nav>li>.on-hover-border:hover { - box-shadow: 0 0 0 1px #eee; - border-radius: 5px; -} - -.navbar-transparent { - background: transparent; - border: none; - padding: 20px; -} - -.navbar-transparent .nav-language .select-language { - color: #fff; -} - -.nav-language { - position: relative; -} - -.nav-language .select-language { - padding: 15px 10px; - color: #777; -} - -.nav-language .select-language span { - margin-left: 5px; - margin-right: 5px; - font-weight: normal; -} - -.nav-language .drop-language { - top: 45px; - left: auto !important; - width: 100px; - min-width: 100px; - height: 40px; - padding: 9px 10px; - -webkit-box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); - -moz-box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); - box-shadow: -8px 13px 31px -8px rgba(77, 77, 77, 1); - z-index: 100; - text-align: center; - border-radius: 4px; -} - -.nav-language .drop-language a { - cursor: pointer; - padding: 5px 10px !important; -} - -.nav-language .open .drop-language { - width: 100px; - min-width: 100px; -} - -.dropdown-menu { - border: 1px solid #fff; - -webkit-box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5); - -moz-box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5); - box-shadow: -8px 14px 20px -5px rgba(77, 77, 77, 0.5); - border-radius: 4px !important; - left: 0 !important; - min-width: 155px; - padding: 5px; - margin-left: 15px; -} - -.dropdown-menu>li a:focus, -.dropdown-menu>li a:hover { - background: transparent; - text-decoration: underline !important; -} - -@media (min-width: 768px) { - .dropdown-menu>li>a { - font-weight: 300; - } -} - -.highlights-dropdown .dropdown-menu>li>a { - font-size: 13px; - padding: 1px 10px; -} - - -/* Show the dropdown menu on hover */ - -@media (min-width: 768px) { - .nav-language .dropdown:hover .dropdown-menu { - display: block; - } -} - -@media (max-width: 767px) { - .nav-language .open .dropdown-menu>li>a { - line-height: 1.42857143; - } -} - -.navbar-transparent .nav-language .drop-language { - background: transparent; - border: 1px solid #fff; -} - -.navbar-transparent .nav-language .drop-language a { - color: #fff; - padding: 5px 10px !important; -} - - -/* dcl header */ -.dcl-header { - padding: 150px 0 150px 0; - text-align: center; - color: #f8f8f8; - background: url(../img/pattern.jpg) no-repeat center center; - background-size: cover; - position: relative; - background-attachment: fixed; -} - -.dcl-header::before { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(90, 116, 175, 0.85); -} - -.dcl-header .container { - position: relative; -} - -.dcl-header h1 { - font-size: 65px; - margin: 0; - padding: 0; -} - -@media(max-width:767px) { - .dcl-header h1 { - font-size: 50px; - } -} - -.intro-header { - min-height: 100vh; - text-align: center; - color: #fff; - background: url(../img/configure.jpg) no-repeat center center; - background-size: cover; - position: relative; - display: flex; - justify-content: center; - align-items: center; -} - -.intro-header::before { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(38, 59, 107, 0.7); -} - -.intro-header-2 { - padding-top: 50px; - /* If you're making other pages, make sure there is 50px of padding to make sure the navbar doesn't overlap content! */ - padding-bottom: 50px; - color: #f8f8f8; - background: url(../img/pattern.jpg) no-repeat center center; - background-size: cover; - position: relative; -} - -.intro-header-2::before { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(41, 66, 122, 0.59); -} - -.intro-message { - position: relative; - width: 80%; - margin: 0 auto; -} - -.intro-message>h1 { - margin: 0; - font-size: 6em; -} - -.intro-divider { - width: 400px; - border-top: 1px solid #f8f8f8; - border-bottom: 1px solid rgba(0, 0, 0, 0.2); -} - -.intro-pricing { - text-align: center; - color: #fff; - background: url(../img/pattern.jpg) no-repeat center center; - background-size: cover; - height: 70vh; - max-height: 400px; - display: flex; - justify-content: center; - align-items: center; - position: relative; -} - -.intro-pricing.success-pricing { - height: 100vh; - max-height: 100vh; -} - -.intro-pricing::before { - content: ""; - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - background: rgba(90, 116, 175, 0.7); -} - -.intro-pricing .intro-message .section-heading { - font-size: 45px; - width: 80%; - margin: 0 auto; -} - -.split-section { - padding: 70px 0; - border-top: 1px solid #f6f7f8; -} - -.split-section .icon-section { - position: relative; - min-height: 330px; -} - -.split-section .icon-section i { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); - font-size: 216px; - color: #5A74AF; -} - -.split-section h2 { - font-size: 36px; - font-weight: 400; -} - -.split-section .split-title-plain h2 { - font-size: 40px; - font-weight: 300; - line-height: 50px; - color: #3a3a3a; -} - -.split-section .split-title { - position: relative; - margin-bottom: 25px; -} - -.split-section .split-title h2 { - font-size: 50px; - font-weight: 300; - padding-bottom: 25px; - letter-spacing: 2px; -} - -.section-gradient { - background: -webkit-linear-gradient(#f0f4f7, #fff) no-repeat; - background: -o-linear-gradient(#f0f4f7, #fff) no-repeat; - background: linear-gradient(#f0f4f7, #fff) no-repeat; -} - -.split-section.left .split-description { - margin-right: auto; -} - -.split-section .split-description .lead { - color: #3a3a3a; -} - -@media (min-width: 768px) { - .split-section .split-description .lead { - font-size: 21px; - } - .split-section .space .split-description .lead { - font-size: 20px; - } -} - -.split-section.right .split-description { - width: 90%; - margin-left: auto; -} - -.split-section.right .split-description.title p { - font-size: 27px; - margin-bottom: 10px; - text-align: left; -} - -.split-section.right .split-text ul, -.split-section.left .split-text, -.split-section.left .space { - text-align: left; -} - -.split-section.right .split-text, -.split-section.right .space { - text-align: right; -} - -.split-section .split-title::before { - content: ""; - position: absolute; - bottom: 0; - background: #29427A; - height: 7px; - width: 70px; - left: auto; -} - -.split-section.right .split-title::before { - right: 0; -} - -.split-section.left .split-title::before { - left: 0; -} - -.section-figure { - display: flex; - flex-wrap: wrap; - justify-content: center; - text-align: center; -} - -.section-figure .section-image { - padding: 20px 40px 30px; - flex-basis: 50%; - flex-grow: 1; - display: flex; - flex-direction: column; - justify-content: space-between; -} - -@media (max-width: 767px) { - .section-figure .section-image { - flex-basis: 100%; - } -} - -.split-section-plain .section-figure .section-image { - flex-grow: 0; - padding: 50px 15px 0; -} - -.split-section-plain .section-figure { - justify-content: flex-start; -} - -@media (min-width: 768px) { - .split-section-plain .split-figure { - width: 41.66666667%; - } - .split-section-plain .split-figure.col-sm-pull-6 { - right: 58.33333333%; - } - .split-section-plain .split-text { - width: 58.33333333%; - } - .split-section-plain .split-text.col-sm-push-6 { - left: 41.66666667%; - } -} - -.section-image img { - margin: auto; -} - -.section-image-caption { - padding-top: 20px; - display: inline-block; - color: #999 !important; - word-break: break-all; -} - -.price-calc-section .card { - width: 350px; - margin: 0 auto; - background: #fff; - box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); - padding-bottom: 40px; - border-radius: 7px; - position: relative; -} - -.price-calc-section .card .title { - padding: 15px 40px; -} - -.price-calc-section .card .price { - background: #5A74AF; - padding: 22px; - color: #fff; - font-size: 32px; -} - -.price-calc-section .card .description { - padding: 12px; -} - -.price-calc-section .card .descriptions { - padding: 10px 30px; -} - -.price-calc-section .card .description p { - margin: 0; -} - -.price-calc-section .card .btn { - margin-top: 20px; -} - -@keyframes sending { - 0% { - content: '.'; - } - 50% { - content: '..'; - } - 100% { - content: '...'; - } -} -/*Why DCL*/ - -#tech_stack { - background: #fff; -} - -#tech_stack h3 { - font-size: 42px; - width: 70%; -} - -.space { - max-width: 660px; - margin: auto; -} - -.percent-text { - font-size: 50px; - color: #999; -} - -.space-middle { - /* padding: 45px 0; */ - display: inline-block; -} - -.ssdimg { - margin: 0 15px; -} - -@media (max-width: 767px) { - .ssdimg img { - max-height: 120px; - } -} - -.padding-vertical { - padding: 30px 2px 20px; -} - - -/*Pricing page*/ - -.price-calc-section { - display: flex; - margin-top: 25px; - margin-bottom: 25px; -} - -.price-calc-section .card { - width: 100%; - margin: 0 auto; - background: #fff; - box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1), 0 0 6px rgba(0, 0, 0, 0.15); - padding-bottom: 40px; - border-radius: 7px; - text-align: center; - max-width: 400px; - position: relative; -} - -.price-calc-section .card .title { - padding: 15px 40px; -} - -.price-calc-section .card .title h3 { - font-weight: normal; -} - -.price-calc-section .card .price { - background: #5A74AF; - padding: 22px; - color: #fff; - font-size: 32px; -} - -.price-calc-section .card .price .price-text { - font-size: 14px; -} - -.price-calc-section .card .description { - padding: 12px; - position: relative; - display: flex; - justify-content: space-around !important; - align-items: center !important; -} - -.price-calc-section .card .description span { - font-size: 16px; - margin-left: 0px; - width: 30%; - text-align: left; -} - -.price-calc-section .card .description .select-number { - font-size: 20px; - text-align: center; - width: 85px; -} - -.price-calc-section .card .description i { - color: #29427A; - cursor: pointer; - font-size: 24px; -} - -.price-calc-section .card .description .left { - margin-right: 7px; -} - -.price-calc-section .card .description .right { - margin-left: 7px; -} - -.price-calc-section .card .descriptions { - padding: 10px 30px; -} - -.price-calc-section .card .description p { - margin: 0; -} - -.price-calc-section .card .btn { - margin-top: 20px; - font-size: 20px; - width: 200px; - border: none; -} - -.price-calc-section .card .select-configuration select { - outline: none; - background: #fff; - border-color: #d0d0d0; - height: 40px; - width: 200px; - text-align: center; - font-size: 16px; - margin-left: 10px; -} - -.price-calc-section .card .check-ip { - font-size: 18px; -} - -.price-calc-section .card .justify-center { - justify-content: center !important; -} - -.price-calc-section .card .description.input label { - font-size: 15px; - font-weight: 700; - margin-bottom: 0; - width: 40px; -} - - -/*Changed class****.price-calc-section .card .description.input input*/ - -.price-calc-section .card .description input { - width: 200px; - font-size: 14px; - text-align: left; - padding: 5px 10px; - border-radius: 4px; - border: 1px solid #d0d0d0; - background: #fff; - margin-left: 10px; -} - -.price-calc-section .card .check-ip input[type=checkbox] { - font-size: 17px; - margin: 0 8px; -} - -.help-block.with-errors { - text-align: center; - margin: 0; - padding: 0; -} - -.form-group { - margin: 0; - border-bottom: 1px solid rgba(128, 128, 128, 0.3); -} - -@media(max-width:767px) { - #tech_stack h3 { - font-size: 30px; - line-height: 40px; - width: 100%; - } - .navbar-nav .open .dropdown-menu { - text-align: left; - font-size: 12px; - } - - .navbar-default .navbar-nav>.open>a, - .navbar-default .navbar-nav>.open>a:focus, - .navbar-default .navbar-nav>.open>a:hover { - background: transparent; - color: #777 !important; - } -} - - -@media(max-width:767px) { - .section-sm-center .split-text, - .section-sm-center .space { - text-align: center !important; - margin-bottom: 40px; - } - .section-sm-center .split-title::before { - left: 50% !important; - transform: translate(-50%, 0); - } - .section-sm-center .split-description { - width: 100% !important; - } -} - -@media(max-width:767px) { - .navbar-transparent li a { - color: #777 !important; - } - .intro-message { - padding-bottom: 15%; - } - .intro-message>h1 { - font-size: 3em; - } - ul.intro-social-buttons>li { - display: block; - margin-bottom: 20px; - padding: 0; - } - .intro-pricing .intro-message .section-heading { - font-size: 35px; - width: 80%; - margin: 0 auto; - } - .intro-pricing .intro-message { - padding-bottom: 0; - } - ul.intro-social-buttons>li:last-child { - margin-bottom: 0; - } - .intro-divider { - width: 100%; - } - .navbar-transparent { - background: #fff; - border: none; - padding: 5px; - } - .navbar-transparent #logoBlack { - display: block; - } - .navbar-transparent #logoWhite { - display: none; - } - .navbar-transparent .nav-language .select-language { - color: #777; - } - .navbar-transparent .nav-language .drop-language a { - color: #777; - } - .navbar-transparent .nav-language .drop-language { - background: #fff; - z-index: 100000; - left: 9px; - border: 1px solid rgba(119, 119, 119, 0.4); - box-shadow: none; - } - .navbar-default .nav-language .drop-language { - background: #fff; - z-index: 100000; - left: 9px; - border: 1px solid rgba(119, 119, 119, 0.4); - box-shadow: none; - } - .navbar-default .nav-language .select-language { - color: #777; - } - .navbar-default .nav-language .drop-language a { - color: #777; - } - .navbar-transparent .navbar-nav>li>a:focus, - .navbar-transparent .navbar-nav>li>a:hover { - color: #333; - background-color: transparent; - } - .navbar-default .navbar-nav>li>a:focus, - .navbar-default .navbar-nav>li>a:hover { - color: #333; - background-color: transparent; - } - .split-section { - padding: 20px 0; - } - .split-section .icon-section { - min-height: 160px; - } - .split-section .icon-section i { - font-size: 120px; - } - .split-section h2 { - font-size: 28px; - } - .split-section .split-title-plain h2 { - font-size: 30px; - line-height: 35px; - } - .split-section .split-title h2 { - font-size: 32px; - line-height: 34px; - } - .contact-section .title { - margin: 0 auto; - } - .contact-section .title h2 { - font-size: 45px; - line-height: 40px; - margin-top: 35px; - } - .contact-section .title h2::before { - left: 50%; - transform: translate(-50%, 0); - } - .contact-section .card .social a { - color: #29427A; - font-size: 30px; - } - .intro-pricing .intro-message .section-heading { - font-size: 30px; - } - .price-calc-section { - flex-direction: column; - /* padding: 60px 10px !important; */ - } - .price-calc-section .card { - width: 90%; - } - .price-calc-section .text { - width: 80%; - text-align: center; - margin: 0 auto; - margin-top: 20px; - } - .price-calc-section .text .section-heading { - font-size: 35px; - line-height: 35px; - padding-bottom: 15px; - text-align: center; - } - .price-calc-section .text .section-heading::before { - left: 50%; - transform: translate(-50%, 0); - } - .price-calc-section .text .description { - font-size: 18px; - text-align: center; - } - .price-calc-section .card .description .select-number { - font-size: 17px; - text-align: center; - width: 60px; - } -} - -@media(max-width:575px) { - .percent-text { - font-weight: normal; - font-size: 37px; - } - .contact-section .card { - width: 90%; - } - .form-beta { - width: 90%; - padding: 25px 10px; - } - .intro-message>h1 { - font-size: 2em; - } - .price-calc-section .text .section-heading { - font-size: 24px; - line-height: 25px; - } - .price-calc-section .card .description span { - font-size: 15px; - } -} - -.network-name { - text-transform: uppercase; - font-size: 14px; - font-weight: 300; - letter-spacing: 2px; - line-height: 24px; - display: block; -} - -.section-heading { - margin-bottom: 30px; -} - -footer { - padding: 50px 20px; -} - -.topnav a:focus { - outline: none; - outline-offset: 0; -} - -.topnav .btn:focus { - outline: none !important; - outline-offset: 0; -} - -.flex-row-rev { - margin-top: 25px; -} - -.flex-row .percent-text { - display: flex; - align-items: center; -} - -@media (min-width: 768px) { - .flex-row { - display: flex; - align-items: center; - justify-content: space-between; - } - .flex-row .percent-text { - flex-shrink: 0; - padding: 0 15px; - } - .flex-row .desc-text { - text-align: right; - } - .flex-row .desc-text, - .flex-row .percent-text { - max-width: 430px; - } - .flex-row-rev .desc-text { - max-width: 600px; - text-align: left; - } - .flex-row-rev .percent-text { - order: 2; - } - .flex-row-rev { - margin-bottom: 25px; - } -} - -.checkmark { - display: inline-block; -} - -.checkmark:after { - /*Add another block-level blank space*/ - content: ''; - display: block; - /*Make it a small rectangle so the border will create an L-shape*/ - width: 25px; - height: 60px; - /*Add a white border on the bottom and left, creating that 'L' */ - border: solid #777; - border-width: 0 3px 3px 0; - /*Rotate the L 45 degrees to turn it into a checkmark*/ - transform: rotate(45deg); -} - - -/* new styles for whydcl section cms plugin (to replace older style) */ - -.banner-list { - border-top: 2px solid #eee; - padding: 50px 0; -} - -.banner-list-heading h2 { - font-size: 42px; -} - -@media (max-width: 767px) { - .banner-list-heading h2 { - font-size: 30px; - } -} - - -/* cms section promo */ - -.promo-section { - padding: 75px 15px; -} - -.promo-section.promo-with-bg { - color: #fff; - background-size: cover; - background-position: center; -} - -.promo-section.promo-with-bg a { - color: #87B6EA; -} - -.promo-section.promo-with-bg a:hover, -.promo-section.promo-with-bg a:focus { - color: #77a6da; -} - -.promo-section h3 { - font-weight: 700; - font-size: 36px; - text-transform: uppercase; - letter-spacing: 0.5px; - margin-top: 10px; - margin-bottom: 25px; -} - -.promo-section h4 { - font-size: 24px; - margin-bottom: 20px; -} - -.promo-section p { - font-size: 18px; - line-height: 1.5; -} - -.promo-section.text-center p { - max-width: 720px; - margin: auto; -} - -.promo-section.text-center h3, -.promo-section.text-center h4 { - margin-bottom: 35px; -} - -.split-text .split-subsection { - margin-top: 25px; - margin-bottom: 25px; -} - -.split-text .promo-section { - padding: 20px 15px; - margin-top: 30px; - margin-bottom: 30px; -} - -.split-text .promo-section .container { - width: auto; -} - -.split-text .promo-section h3, -.split-text .promo-section h4 { - margin-bottom: 15px; -} - -@media (max-width: 767px) { - .split-text .split-subsection { - margin-left: -15px; - margin-right: -15px; - } - .promo-section h3 { - font-size: 29px; - } - .split-text .promo-section { - padding-left: 0; - padding-right: 0; - } -} - -ul.errorlist { - padding-left: 0px; -} -ul.errorlist > li { - color: red; - list-style-type: none; -} -div.domain { - flex-direction: column; -} \ No newline at end of file diff --git a/matrixhosting/static/matrixhosting/css/hosting.css b/matrixhosting/static/matrixhosting/css/hosting.css deleted file mode 100644 index b3e0bbb..0000000 --- a/matrixhosting/static/matrixhosting/css/hosting.css +++ /dev/null @@ -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; - } - \ No newline at end of file diff --git a/matrixhosting/static/matrixhosting/js/main.js b/matrixhosting/static/matrixhosting/js/main.js deleted file mode 100644 index 52ae598..0000000 --- a/matrixhosting/static/matrixhosting/js/main.js +++ /dev/null @@ -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); diff --git a/matrixhosting/static/matrixhosting/js/order.js b/matrixhosting/static/matrixhosting/js/order.js deleted file mode 100644 index af733bf..0000000 --- a/matrixhosting/static/matrixhosting/js/order.js +++ /dev/null @@ -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"); - } - } - }); - } -}); \ No newline at end of file diff --git a/matrixhosting/static/matrixhosting/js/payment.js b/matrixhosting/static/matrixhosting/js/payment.js deleted file mode 100644 index d009b78..0000000 --- a/matrixhosting/static/matrixhosting/js/payment.js +++ /dev/null @@ -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(''); - 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); - }); - -}); diff --git a/matrixhosting/tasks.py b/matrixhosting/tasks.py deleted file mode 100644 index c681e8c..0000000 --- a/matrixhosting/tasks.py +++ /dev/null @@ -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) \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/base.html b/matrixhosting/templates/matrixhosting/base.html deleted file mode 100644 index 3abc7a2..0000000 --- a/matrixhosting/templates/matrixhosting/base.html +++ /dev/null @@ -1,60 +0,0 @@ -{% load static i18n %} -{% get_current_language as LANGUAGE_CODE %} -{% load bootstrap5 %} - - - - - - - - - - - Matrix Hosting - {% block title %} made in Switzerland{% endblock %} - - - - {% bootstrap_css %} - - - - - {% block css_extra %} - {% endblock css_extra %} - - - - - - - - - - - - - - {% block navbar %} - {% include "matrixhosting/includes/_navbar.html" %} - {% endblock navbar %} - - {% block content %} - {% endblock %} - - {% include "matrixhosting/includes/_footer.html" %} - - - - - - {% bootstrap_javascript %} - - - {% block js_extra %} - {% endblock js_extra %} - - diff --git a/matrixhosting/templates/matrixhosting/dashboard.html b/matrixhosting/templates/matrixhosting/dashboard.html deleted file mode 100644 index d183665..0000000 --- a/matrixhosting/templates/matrixhosting/dashboard.html +++ /dev/null @@ -1,127 +0,0 @@ -{% extends "matrixhosting/base.html" %} {% load static i18n %} -{% block content%} - -{% csrf_token %} -
-
-
-
- - - - - - - - - - - - - - - - {% for object in object_list %} - - - - - - - - - - {% if object.ending_date %} - - {% else %} - - {% endif %} - - {% endfor %} - -
#DescriptionStarting AtConfigPricing PlanOneTime PriceRecurring PriceEnding At
{{ object.id }}{{ object.description }}{{ object.starting_date }}{{ object.config }}{{ object.pricing_plan}}{{ object.one_time_price }}{{ object.recurring_price }}{{ object.ending_date }} - -
-
-
-
-
- - - - - -{% endblock %} - -{% block js_extra %} - -{% endblock %} diff --git a/matrixhosting/templates/matrixhosting/emails/renewal_warning.html b/matrixhosting/templates/matrixhosting/emails/renewal_warning.html deleted file mode 100644 index 75a4782..0000000 --- a/matrixhosting/templates/matrixhosting/emails/renewal_warning.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Renewal Warning - - - hello {{name}}, - {{message}} - - \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/includes/_calculator_form.html b/matrixhosting/templates/matrixhosting/includes/_calculator_form.html deleted file mode 100644 index fb4ce57..0000000 --- a/matrixhosting/templates/matrixhosting/includes/_calculator_form.html +++ /dev/null @@ -1,101 +0,0 @@ -{% load static i18n %} - -
- {% csrf_token %} -
-

{% trans "Matrix Chat hosting" %}

-
-
- {{ matrix_vm_pricing.name }} - CHF/{% trans "month" %} -
-

- {% if matrix_vm_pricing.set_up_fees %}{{ matrix_vm_pricing.set_up_fees }} CHF Setup
{% endif %} - {% if matrix_vm_pricing.vat_inclusive %}{% trans "VAT included" %}
{% endif %} - {% if matrix_vm_pricing.discount_amount %} - {% trans "You save" %} {{ matrix_vm_pricing.discount_amount }} CHF - {% endif %} -

-
-
-
-
-

{% trans "Hosted in Switzerland" %}

-
-
-
- - - Core - -
-
- {% for message in messages %} - {% if 'cores' in message.tags %} -
    -
  • {{ message|safe }}
  • -
- {% endif %} - {% endfor %} -
-
-
-
- - - GB RAM - -
-
- {% for message in messages %} - {% if 'memory' in message.tags %} -
  • - {{ message|safe }} -
- {% endif %} - {% endfor %} -
-
-
-
- - - {% trans "GB Storage (SSD)" %} - -
-
- {% for message in messages %} - {% if 'storage' in message.tags %} -
  • - {{ message|safe }} -
- {% endif %} - {% endfor %} -
-
-
- -

{{ form.matrix_domain.errors }}

-
-
- -

{{ form.homeserver_domain.errors }}

-
-
- -

{{ form.webclient_domain.errors }}

-
-
-
- Is open registration possible: - {{ form.is_open_registration }} -
-
- -
- - -
\ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/includes/_card.html b/matrixhosting/templates/matrixhosting/includes/_card.html deleted file mode 100644 index ec30a7d..0000000 --- a/matrixhosting/templates/matrixhosting/includes/_card.html +++ /dev/null @@ -1,43 +0,0 @@ -{% load i18n %} -
- - -
-
-
- -
-
-
-
- -
-
-
- -
-
-
-
- - -
-
-
-
-
- {% for message in messages %} - {% if 'failed_payment' in message.tags or 'make_charge_error' in message.tags or 'error' in message.tags %} -
    -
  • {{ message|safe }}

  • -
- {% endif %} - {% endfor %} -
-
- -
-
-

-
-
\ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/includes/_footer.html b/matrixhosting/templates/matrixhosting/includes/_footer.html deleted file mode 100644 index 07d9fb6..0000000 --- a/matrixhosting/templates/matrixhosting/includes/_footer.html +++ /dev/null @@ -1,18 +0,0 @@ -{% load i18n %} - diff --git a/matrixhosting/templates/matrixhosting/includes/_navbar.html b/matrixhosting/templates/matrixhosting/includes/_navbar.html deleted file mode 100644 index 599c6b9..0000000 --- a/matrixhosting/templates/matrixhosting/includes/_navbar.html +++ /dev/null @@ -1,33 +0,0 @@ -{% load static i18n %} -{% get_current_language as LANGUAGE_CODE %} - \ No newline at end of file diff --git a/matrixhosting/templates/matrixhosting/index.html b/matrixhosting/templates/matrixhosting/index.html deleted file mode 100644 index ab9085c..0000000 --- a/matrixhosting/templates/matrixhosting/index.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends "matrixhosting/base.html" %} -{% load static i18n %} - -{% block content %} - - -
-
-
-
-
-
- {% include "matrixhosting/includes/_calculator_form.html" %} -
-
-
-
-
-
- -{% endblock %} diff --git a/matrixhosting/templates/matrixhosting/order_detail.html b/matrixhosting/templates/matrixhosting/order_detail.html deleted file mode 100644 index b6942e3..0000000 --- a/matrixhosting/templates/matrixhosting/order_detail.html +++ /dev/null @@ -1,268 +0,0 @@ -{% load static i18n %} -{% load bootstrap5 %} - - - - - - - - - - Matrix Hosting - {% block title %} made in Switzerland{% endblock %} - - - - {% bootstrap_css %} - - - - - - - - - - - - - - -
- {% if messages %} -
- {% for message in messages %} - {{ message }} - {% endfor %} -
- {% endif %} - {% if not error %} -
-

- {% blocktrans with page_header_text=page_header_text|default:"Order" %}{{page_header_text}}{% endblocktrans %} -

-
-
-
-
-
-

{% trans "Billed to" %}:

-

- {% with request.session.billing_address_data as billing_address %} - {{billing_address.full_name}}
- {{billing_address.street}}, {{billing_address.postal_code}}
- {{billing_address.city}}, {{billing_address.country}} - {% if billing_address.vat_number %} -
{% 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" %} - - {% else %} - - {% endif %} - {% endif %} - {% endif %} - {% endwith %} -

-
-
-
-
-

{% trans "Payment method" %}:

-

- {{card.brand|default:_('Credit Card')}} {% trans "ending in" %} ****{{card.last4}}
- {% trans "Expiry" %} {{card.exp_year}}/{{card.exp_month}}
- {{request.user.email}} -

-
-
-
-

{% trans "Order summary" %}

- -

- {% trans "Product" %}:  - Matrix Chat Hosting -

-
-
-

- {% trans "Cores" %}: - {{order.cores}} -

-

- {% trans "Memory" %}: - {{order.memory}} GB -

-

- {% trans "Disk space" %}: - {{order.storage}} GB -

-
-
-
-
-
-

- {% trans "Price Before VAT" %} - {{pricing.subtotal|floatformat:2}} CHF -

-
-
-
-
-
-
-
-

-
-
-

{% trans "Pre VAT" %}

-
-
-

{% trans "With VAT for" %} {{pricing.vat_country}} ({{pricing.vat_percent}}%)

-
-
-
-
-

Subtotal

-
-
-

{{pricing.subtotal|floatformat:2}} CHF

-
-
-

{{pricing.price_with_vat|floatformat:2}} CHF

-
-
- {% if pricing.discount.amount > 0 %} -
-
-

{{pricing.discount.name}}

-
-
-

-{{pricing.discount.amount|floatformat:2}} CHF

-
-
-

-{{pricing.discount.amount_with_vat|floatformat:2}} CHF

-
-
- {% endif %} -
-
-
-
-
-
-
-

Total

-
-
-

{{pricing.subtotal_after_discount|floatformat:2}} CHF

-
-
-

{{pricing.price_after_discount_with_vat|floatformat:2}} CHF

-
-
-
-
-
-
-
- {% trans "Your Price in Total" %} - {{pricing.total_price|floatformat:2}} CHF -
-
-
-
-
-
- {% csrf_token %} -
-
-
{% blocktrans with vm_total_price=vm.total_price|floatformat:2 %}By clicking "Place order" you agree to our Terms of Service and this plan will charge your credit card account with {{ vm_total_price }} CHF/month{% endblocktrans %}.
-
-
- -
-
-
- {% endif %} -
- - - - - - - - - - - - - - - {% bootstrap_javascript %} - - - - diff --git a/matrixhosting/templates/matrixhosting/payment.html b/matrixhosting/templates/matrixhosting/payment.html deleted file mode 100644 index e296d0c..0000000 --- a/matrixhosting/templates/matrixhosting/payment.html +++ /dev/null @@ -1,169 +0,0 @@ -{% load static i18n %} -{% load bootstrap5 %} - - - - - - - - - - - Matrix Hosting - {% block title %} made in Switzerland{% endblock %} - - - - {% bootstrap_css %} - - - - - - - - - - - - - -
-
-
-
-
-

{%trans "Your Order" %}

-
-
-

{% trans "Cores"%} {{request.session.order.cores|floatformat}}

-
-

{% trans "Memory"%} {{request.session.order.memory|floatformat}} GB

-
-

{% trans "Disk space"%} {{request.session.order.storage|floatformat}} GB

-
-

- {%trans "Total" %}   - - ({% if matrix_vm_pricing.vat_inclusive %}{%trans "including VAT" %}{% else %}{%trans "excluding VAT" %}{% endif %}) - - {{request.session.order.subtotal|floatformat}} CHF / {% trans "Month" %} -

-
- {% if matrix_vm_pricing.discount_amount %} -

- {{ request.session.order.discount.name }}   - - {{ request.session.order.discount.amount }} CHF / {% trans "Month" %} -

- {% endif %} -
-
-
-
-

{%trans "Billing Address"%}

-
- {% for message in messages %} - {% if 'vat_error' in message.tags %} -
  • - {{ message|safe }} -
- {% endif %} - {% endfor %} -
- {% 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 %} -
-
-
-
-
-
-
- {% with cards_len=cards|length %} -

{%trans "Credit Card"%}

-
-

- {% 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 Stripe 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 Stripe for payment and do not store your information in our database.{% endblocktrans %} - {% endif %} -

-
- {% for card in cards %} -
-
-
{% trans "Credit Card" %}
-
{% trans "Last" %} 4: ***** {{card.last4}}
-
{% trans "Type" %}: {{card.brand}}
-
{% trans "Expiry" %}: {{card.month}}/{{card.year}}
-
- -
- {% endfor %} - {% if cards_len > 0 %} -
-
-
-

{% trans "Add a new credit card" %}

-
-
- -
-
-
-
-
-
-

{%trans "New Credit Card" %}

-
- {% include "matrixhosting/includes/_card.html" %} -
-
- {% else%} - {% include "matrixhosting/includes/_card.html" %} - {% endif %} -
- {% endwith %} -
-
-
-
- {% if stripe_key %} - {% get_current_language as LANGUAGE_CODE %} - - {%endif%} - - - - - - - - - {% bootstrap_javascript %} - - - - diff --git a/matrixhosting/tests.py b/matrixhosting/tests.py deleted file mode 100644 index 27468b3..0000000 --- a/matrixhosting/tests.py +++ /dev/null @@ -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) - - diff --git a/matrixhosting/urls.py b/matrixhosting/urls.py deleted file mode 100644 index 318ef8b..0000000 --- a/matrixhosting/urls.py +++ /dev/null @@ -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//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'), -] diff --git a/matrixhosting/validators.py b/matrixhosting/validators.py deleted file mode 100644 index 08cc818..0000000 --- a/matrixhosting/validators.py +++ /dev/null @@ -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}(? 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}" - diff --git a/uncloud/selectors.py b/uncloud/selectors.py deleted file mode 100644 index 52b8548..0000000 --- a/uncloud/selectors.py +++ /dev/null @@ -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)) diff --git a/uncloud/static/uncloud/uncloud.css b/uncloud/static/uncloud/uncloud.css deleted file mode 100644 index 51d93ef..0000000 --- a/uncloud/static/uncloud/uncloud.css +++ /dev/null @@ -1,4 +0,0 @@ -#content { - width: 400px; - margin: auto; -} diff --git a/uncloud/templates/uncloud/base.html b/uncloud/templates/uncloud/base.html deleted file mode 100644 index cbf0686..0000000 --- a/uncloud/templates/uncloud/base.html +++ /dev/null @@ -1,29 +0,0 @@ -{% extends 'bootstrap5/bootstrap5.html' %} -{% block bootstrap5_before_content %} - - -{% endblock %} diff --git a/uncloud/templates/uncloud/index.html b/uncloud/templates/uncloud/index.html deleted file mode 100644 index b8b5828..0000000 --- a/uncloud/templates/uncloud/index.html +++ /dev/null @@ -1,170 +0,0 @@ -{% extends 'uncloud/base.html' %} -{% block title %}Welcome to uncloud [beta]{% endblock %} - -{% block bootstrap5_content %} -
-
-
-
-

Welcome to uncloud [beta]

-
-
- -
-

About uncloud

-
-

- Welcome to uncloud, the Open Source cloud management - system by ungleich. - It is an API driven system with - some convience views provided by - the Django Rest - Framework. You can - freely access - the source code of uncloud. - This is a BETA service. As such, some - functionality might not be very sophisticated. -

-
-
-
-

Getting started

-
-

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

    -
  • First you need - to register an - account. If you already have one, you can - login. -
  • If you have forgotten your password or other issues with - logging in, you can contact the ungleich support - via support at ungleich.ch. - -
  • Secondy you will need to - create a billing - address. This is required for determining the correct - tax. -
  • Next you will need to - register a credit card - from which payments can be made. Your credit card will not - be charged without your consent. -
-
-
-
-

Introduction to uncloud concepts

-
-

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 pre-paid account model. Which means - that you can charge your account and then use your balance to - pay for product usage.

-
-
- -
-

Credit cards

-
-

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

-
    -
  • Register a credit card - (this is required to be done via Javascript so that we never see - your credit card, but it is sent directly to stripe) -
  • You can list your - credit cards - 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. -
-
-
-

Billing Address, Payments and Balance

-
-

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.

- -

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

- -

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

- - - -
-
- -
-

Networking

-
-

- With uncloud you can use a variety of network related - services. -

- - -
-
- -
-

Current limitations

-
-
    -
  • Payments are only possible in CHF. -
-
-
- {% if user.is_authenticated %} -
-

Account Settings

-
-
    -
    - {% csrf_token %} -
    - Delete User Account -

    Are you sure you want to delete your account? This will permanently delete your - profile and any orders you have generated.

    - {{ delete_form }} -
    -
    - -
    -
    -
-
-
- {% endif %} -
-
-{% endblock %} diff --git a/uncloud/urls.py b/uncloud/urls.py deleted file mode 100644 index 14e45fd..0000000 --- a/uncloud/urls.py +++ /dev/null @@ -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"), -] diff --git a/uncloud/views.py b/uncloud/views.py deleted file mode 100644 index a4bf683..0000000 --- a/uncloud/views.py +++ /dev/null @@ -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') \ No newline at end of file diff --git a/uncloud_auth/management/commands/make-admin.py b/uncloud_auth/management/commands/make-admin.py deleted file mode 100644 index 9157439..0000000 --- a/uncloud_auth/management/commands/make-admin.py +++ /dev/null @@ -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})") diff --git a/uncloud_auth/serializers.py b/uncloud_auth/serializers.py deleted file mode 100644 index c3f6694..0000000 --- a/uncloud_auth/serializers.py +++ /dev/null @@ -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() diff --git a/uncloud_auth/templates/uncloud_auth/login.html b/uncloud_auth/templates/uncloud_auth/login.html deleted file mode 100644 index 2a3bfd6..0000000 --- a/uncloud_auth/templates/uncloud_auth/login.html +++ /dev/null @@ -1,21 +0,0 @@ -{% extends 'uncloud/base.html' %} -{% load bootstrap5 %} - -{% block bootstrap5_content %} -
-
-
- -

Login to uncloud

-
- {% csrf_token %} - {% bootstrap_form form %} - {% buttons %} - - {% endbuttons %} -
-
-
-
- -{% endblock %} diff --git a/uncloud_auth/uldap.py b/uncloud_auth/uldap.py deleted file mode 100644 index aa90c77..0000000 --- a/uncloud_auth/uldap.py +++ /dev/null @@ -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 diff --git a/uncloud_auth/ungleich_ldap.py b/uncloud_auth/ungleich_ldap.py deleted file mode 100644 index f22b423..0000000 --- a/uncloud_auth/ungleich_ldap.py +++ /dev/null @@ -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 diff --git a/uncloud_auth/views.py b/uncloud_auth/views.py deleted file mode 100644 index 9310a4c..0000000 --- a/uncloud_auth/views.py +++ /dev/null @@ -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) diff --git a/archive/uncloud_django_based/hacks/abk-hacks.py b/uncloud_django_based/abk-hacks.py similarity index 100% rename from archive/uncloud_django_based/hacks/abk-hacks.py rename to uncloud_django_based/abk-hacks.py diff --git a/archive/uncloud_django_based/hacks/abkhack/opennebula_hacks.py b/uncloud_django_based/abkhack/opennebula_hacks.py similarity index 100% rename from archive/uncloud_django_based/hacks/abkhack/opennebula_hacks.py rename to uncloud_django_based/abkhack/opennebula_hacks.py diff --git a/archive/uncloud_django_based/meow-payv1/README.md b/uncloud_django_based/meow-payv1/README.md similarity index 100% rename from archive/uncloud_django_based/meow-payv1/README.md rename to uncloud_django_based/meow-payv1/README.md diff --git a/archive/uncloud_django_based/meow-payv1/config.py b/uncloud_django_based/meow-payv1/config.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/config.py rename to uncloud_django_based/meow-payv1/config.py diff --git a/archive/uncloud_django_based/meow-payv1/hack-a-vpn.py b/uncloud_django_based/meow-payv1/hack-a-vpn.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/hack-a-vpn.py rename to uncloud_django_based/meow-payv1/hack-a-vpn.py diff --git a/archive/uncloud_django_based/meow-payv1/helper.py b/uncloud_django_based/meow-payv1/helper.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/helper.py rename to uncloud_django_based/meow-payv1/helper.py diff --git a/archive/uncloud_django_based/meow-payv1/products/ipv6-only-django.json b/uncloud_django_based/meow-payv1/products/ipv6-only-django.json similarity index 100% rename from archive/uncloud_django_based/meow-payv1/products/ipv6-only-django.json rename to uncloud_django_based/meow-payv1/products/ipv6-only-django.json diff --git a/archive/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json b/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json similarity index 100% rename from archive/uncloud_django_based/meow-payv1/products/ipv6-only-vm.json rename to uncloud_django_based/meow-payv1/products/ipv6-only-vm.json diff --git a/archive/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json b/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json similarity index 100% rename from archive/uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json rename to uncloud_django_based/meow-payv1/products/ipv6-only-vpn.json diff --git a/archive/uncloud_django_based/meow-payv1/products/ipv6box.json b/uncloud_django_based/meow-payv1/products/ipv6box.json similarity index 100% rename from archive/uncloud_django_based/meow-payv1/products/ipv6box.json rename to uncloud_django_based/meow-payv1/products/ipv6box.json diff --git a/archive/uncloud_django_based/meow-payv1/products/membership.json b/uncloud_django_based/meow-payv1/products/membership.json similarity index 100% rename from archive/uncloud_django_based/meow-payv1/products/membership.json rename to uncloud_django_based/meow-payv1/products/membership.json diff --git a/archive/uncloud_django_based/meow-payv1/requirements.txt b/uncloud_django_based/meow-payv1/requirements.txt similarity index 100% rename from archive/uncloud_django_based/meow-payv1/requirements.txt rename to uncloud_django_based/meow-payv1/requirements.txt diff --git a/archive/uncloud_django_based/meow-payv1/sample-pay.conf b/uncloud_django_based/meow-payv1/sample-pay.conf similarity index 100% rename from archive/uncloud_django_based/meow-payv1/sample-pay.conf rename to uncloud_django_based/meow-payv1/sample-pay.conf diff --git a/archive/uncloud_django_based/meow-payv1/schemas.py b/uncloud_django_based/meow-payv1/schemas.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/schemas.py rename to uncloud_django_based/meow-payv1/schemas.py diff --git a/archive/uncloud_django_based/meow-payv1/stripe_hack.py b/uncloud_django_based/meow-payv1/stripe_hack.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/stripe_hack.py rename to uncloud_django_based/meow-payv1/stripe_hack.py diff --git a/archive/uncloud_django_based/meow-payv1/stripe_utils.py b/uncloud_django_based/meow-payv1/stripe_utils.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/stripe_utils.py rename to uncloud_django_based/meow-payv1/stripe_utils.py diff --git a/archive/uncloud_django_based/meow-payv1/ucloud_pay.py b/uncloud_django_based/meow-payv1/ucloud_pay.py similarity index 100% rename from archive/uncloud_django_based/meow-payv1/ucloud_pay.py rename to uncloud_django_based/meow-payv1/ucloud_pay.py diff --git a/archive/uncloud_django_based/notes-abk.md b/uncloud_django_based/notes-abk.md similarity index 100% rename from archive/uncloud_django_based/notes-abk.md rename to uncloud_django_based/notes-abk.md diff --git a/archive/uncloud_django_based/notes-nico.org b/uncloud_django_based/notes-nico.org similarity index 100% rename from archive/uncloud_django_based/notes-nico.org rename to uncloud_django_based/notes-nico.org diff --git a/archive/uncloud_django_based/plan.org b/uncloud_django_based/plan.org similarity index 100% rename from archive/uncloud_django_based/plan.org rename to uncloud_django_based/plan.org diff --git a/archive/uncloud_django_based/uncloud/.gitignore b/uncloud_django_based/uncloud/.gitignore similarity index 100% rename from archive/uncloud_django_based/uncloud/.gitignore rename to uncloud_django_based/uncloud/.gitignore diff --git a/doc/README-how-to-configure-remote-uncloud-clients.org b/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org similarity index 80% rename from doc/README-how-to-configure-remote-uncloud-clients.org rename to uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org index b48886b..7217e1f 100644 --- a/doc/README-how-to-configure-remote-uncloud-clients.org +++ b/uncloud_django_based/uncloud/doc/README-how-to-configure-remote-uncloud-clients.org @@ -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 diff --git a/uncloud_django_based/uncloud/doc/README-object-relations.md b/uncloud_django_based/uncloud/doc/README-object-relations.md new file mode 100644 index 0000000..58f2413 --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README-object-relations.md @@ -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 diff --git a/uncloud_django_based/uncloud/doc/README-postgresql.org b/uncloud_django_based/uncloud/doc/README-postgresql.org new file mode 100644 index 0000000..9e5cc10 --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README-postgresql.org @@ -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 diff --git a/uncloud_django_based/uncloud/doc/README-products.md b/uncloud_django_based/uncloud/doc/README-products.md new file mode 100644 index 0000000..1b1190d --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README-products.md @@ -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. diff --git a/uncloud_django_based/uncloud/doc/README-vpn.org b/uncloud_django_based/uncloud/doc/README-vpn.org new file mode 100644 index 0000000..7d041cb --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README-vpn.org @@ -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 diff --git a/uncloud_django_based/uncloud/doc/README.md b/uncloud_django_based/uncloud/doc/README.md new file mode 100644 index 0000000..390a3af --- /dev/null +++ b/uncloud_django_based/uncloud/doc/README.md @@ -0,0 +1,95 @@ +## Install + +### OS package requirements + +Alpine: + +``` +apk add openldap-dev postgresql-dev +``` + +Debian/Devuan: + +``` +apt install postgresql-server-dev-all +``` + + +### Python requirements + +If you prefer using a venv, use: + +``` +python -m venv venv +. ./venv/bin/activate +``` + +Then install the requirements + +``` +pip install -r requirements.txt +``` + +### Database requirements + +Due to the use of the JSONField, postgresql is required. + +First create a role to be used: + +``` +postgres=# create role nico login; +``` + +Then create the database owner by the new role: + +``` +postgres=# create database uncloud owner nico; +``` + +Installing the postgresql service is os dependent, but some hints: + +* Alpine: `apk add postgresql-server && rc-update add postgresql && rc-service postgresql start` +* Debian/Devuan: `apt install postgresql` + +After postresql is started, apply the migrations: + +``` +python manage.py migrate +``` + +### Secrets + +cp `uncloud/secrets_sample.py` to `uncloud/secrets.py` and replace the +sample values with real values. + + +## Flows / Orders + +### Creating a VMHost + + + +### Creating a VM + +* Create a VMHost +* Create a VM on a VMHost + + +### Creating a VM Snapshot + + +## Working Beta APIs + +These APIs can be used for internal testing. + +### URL Overview + +``` +http -a nicoschottelius:$(pass ungleich.ch/nico.schottelius@ungleich.ch) http://localhost:8000 +``` + +### Snapshotting + +``` +http -a nicoschottelius:$(pass ungleich.ch/nico.schottelius@ungleich.ch) http://localhost:8000/vm/snapshot/ vm_uuid=$(uuidgen) +``` diff --git a/manage.py b/uncloud_django_based/uncloud/manage.py similarity index 100% rename from manage.py rename to uncloud_django_based/uncloud/manage.py diff --git a/models.dot b/uncloud_django_based/uncloud/models.dot similarity index 100% rename from models.dot rename to uncloud_django_based/uncloud/models.dot diff --git a/models.png b/uncloud_django_based/uncloud/models.png similarity index 100% rename from models.png rename to uncloud_django_based/uncloud/models.png diff --git a/archive/uncloud_etcd_based/docs/__init__.py b/uncloud_django_based/uncloud/opennebula/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/docs/__init__.py rename to uncloud_django_based/uncloud/opennebula/__init__.py diff --git a/opennebula/admin.py b/uncloud_django_based/uncloud/opennebula/admin.py similarity index 100% rename from opennebula/admin.py rename to uncloud_django_based/uncloud/opennebula/admin.py diff --git a/opennebula/apps.py b/uncloud_django_based/uncloud/opennebula/apps.py similarity index 100% rename from opennebula/apps.py rename to uncloud_django_based/uncloud/opennebula/apps.py diff --git a/opennebula/management/commands/opennebula-synchosts.py b/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-synchosts.py similarity index 100% rename from opennebula/management/commands/opennebula-synchosts.py rename to uncloud_django_based/uncloud/opennebula/management/commands/opennebula-synchosts.py diff --git a/opennebula/management/commands/opennebula-syncvms.py b/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-syncvms.py similarity index 88% rename from opennebula/management/commands/opennebula-syncvms.py rename to uncloud_django_based/uncloud/opennebula/management/commands/opennebula-syncvms.py index 3c12fa9..458528b 100644 --- a/opennebula/management/commands/opennebula-syncvms.py +++ b/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-syncvms.py @@ -1,9 +1,12 @@ import json +import uncloud.secrets as secrets + + from xmlrpc.client import ServerProxy as RPCClient + from django_auth_ldap.backend import LDAPBackend from django.core.management.base import BaseCommand -from django.conf import settings from xmltodict import parse from opennebula.models import VM as VMModel @@ -16,9 +19,9 @@ class Command(BaseCommand): pass def handle(self, *args, **options): - with RPCClient(settings.OPENNEBULA_URL) as rpc_client: + with RPCClient(secrets.OPENNEBULA_URL) as rpc_client: success, response, *_ = rpc_client.one.vmpool.infoextended( - settings.OPENNEBULA_USER_PASS, -2, -1, -1, -1 + secrets.OPENNEBULA_USER_PASS, -2, -1, -1, -1 ) if success: vms = json.loads(json.dumps(parse(response)))['VM_POOL']['VM'] diff --git a/opennebula/management/commands/opennebula-to-uncloud.py b/uncloud_django_based/uncloud/opennebula/management/commands/opennebula-to-uncloud.py similarity index 100% rename from opennebula/management/commands/opennebula-to-uncloud.py rename to uncloud_django_based/uncloud/opennebula/management/commands/opennebula-to-uncloud.py diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0001_initial.py b/uncloud_django_based/uncloud/opennebula/migrations/0001_initial.py new file mode 100644 index 0000000..4c0527a --- /dev/null +++ b/uncloud_django_based/uncloud/opennebula/migrations/0001_initial.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.3 on 2020-02-23 17:12 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='VM', + fields=[ + ('vmid', models.IntegerField(primary_key=True, serialize=False)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('data', django.contrib.postgres.fields.jsonb.JSONField()), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0002_auto_20200225_1335.py b/uncloud_django_based/uncloud/opennebula/migrations/0002_auto_20200225_1335.py new file mode 100644 index 0000000..1554aa6 --- /dev/null +++ b/uncloud_django_based/uncloud/opennebula/migrations/0002_auto_20200225_1335.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.3 on 2020-02-25 13:35 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('opennebula', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='vm', + name='uuid', + ), + migrations.RemoveField( + model_name='vm', + name='vmid', + ), + migrations.AddField( + model_name='vm', + name='id', + field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0003_auto_20200225_1428.py b/uncloud_django_based/uncloud/opennebula/migrations/0003_auto_20200225_1428.py new file mode 100644 index 0000000..8bb3d8d --- /dev/null +++ b/uncloud_django_based/uncloud/opennebula/migrations/0003_auto_20200225_1428.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.3 on 2020-02-25 14:28 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('opennebula', '0002_auto_20200225_1335'), + ] + + operations = [ + migrations.AlterField( + model_name='vm', + name='id', + field=models.CharField(default=uuid.uuid4, max_length=64, primary_key=True, serialize=False, unique=True), + ), + ] diff --git a/uncloud_django_based/uncloud/opennebula/migrations/0004_auto_20200225_1816.py b/uncloud_django_based/uncloud/opennebula/migrations/0004_auto_20200225_1816.py new file mode 100644 index 0000000..5b39f26 --- /dev/null +++ b/uncloud_django_based/uncloud/opennebula/migrations/0004_auto_20200225_1816.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-02-25 18:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('opennebula', '0003_auto_20200225_1428'), + ] + + operations = [ + migrations.RemoveField( + model_name='vm', + name='id', + ), + migrations.AddField( + model_name='vm', + name='vmid', + field=models.IntegerField(default=42, primary_key=True, serialize=False), + preserve_default=False, + ), + ] diff --git a/archive/uncloud_etcd_based/docs/source/__init__.py b/uncloud_django_based/uncloud/opennebula/migrations/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/docs/source/__init__.py rename to uncloud_django_based/uncloud/opennebula/migrations/__init__.py diff --git a/opennebula/models.py b/uncloud_django_based/uncloud/opennebula/models.py similarity index 94% rename from opennebula/models.py rename to uncloud_django_based/uncloud/opennebula/models.py index f15b845..826b615 100644 --- a/opennebula/models.py +++ b/uncloud_django_based/uncloud/opennebula/models.py @@ -1,7 +1,7 @@ import uuid from django.db import models from django.contrib.auth import get_user_model -from uncloud_pay.models import Product +from django.contrib.postgres.fields import JSONField # ungleich specific storage_class_mapping = { @@ -12,7 +12,8 @@ storage_class_mapping = { class VM(models.Model): vmid = models.IntegerField(primary_key=True) - data = models.JSONField() + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + data = JSONField() @property def uncloud_name(self): diff --git a/opennebula/serializers.py b/uncloud_django_based/uncloud/opennebula/serializers.py similarity index 100% rename from opennebula/serializers.py rename to uncloud_django_based/uncloud/opennebula/serializers.py diff --git a/opennebula/tests.py b/uncloud_django_based/uncloud/opennebula/tests.py similarity index 100% rename from opennebula/tests.py rename to uncloud_django_based/uncloud/opennebula/tests.py diff --git a/uncloud_django_based/uncloud/opennebula/views.py b/uncloud_django_based/uncloud/opennebula/views.py new file mode 100644 index 0000000..89b1a52 --- /dev/null +++ b/uncloud_django_based/uncloud/opennebula/views.py @@ -0,0 +1,16 @@ +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 diff --git a/requirements.txt b/uncloud_django_based/uncloud/requirements.txt similarity index 60% rename from requirements.txt rename to uncloud_django_based/uncloud/requirements.txt index 8231fd0..a7fc9f2 100644 --- a/requirements.txt +++ b/uncloud_django_based/uncloud/requirements.txt @@ -1,15 +1,12 @@ -# Django basics -Django==3.2.4 +django djangorestframework django-auth-ldap -django-bootstrap-v5 -fontawesome-free - -psycopg2 -ldap3 -django-allauth +stripe xmltodict +psycopg2 + parsedatetime + # Follow are for creating graph models pyparsing pydot @@ -21,14 +18,7 @@ django-hardcopy # schema support pyyaml uritemplate -tldextract -# Payment & VAT + +# Comprehensive interface to validate VAT numbers, making use of the VIES +# service for European countries. vat-validator -stripe - -#Jobs -django-q -redis - -jinja2 -python-gitlab diff --git a/uncloud_django_based/uncloud/uncloud/.gitignore b/uncloud_django_based/uncloud/uncloud/.gitignore new file mode 100644 index 0000000..ef418f5 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud/.gitignore @@ -0,0 +1 @@ +secrets.py diff --git a/uncloud_django_based/uncloud/uncloud/__init__.py b/uncloud_django_based/uncloud/uncloud/__init__.py new file mode 100644 index 0000000..9e2545a --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud/__init__.py @@ -0,0 +1,4 @@ +# Define DecimalField properties, used to represent amounts of money. +# Used in pay and auth +AMOUNT_MAX_DIGITS=10 +AMOUNT_DECIMALS=2 diff --git a/uncloud/asgi.py b/uncloud_django_based/uncloud/uncloud/asgi.py similarity index 100% rename from uncloud/asgi.py rename to uncloud_django_based/uncloud/uncloud/asgi.py diff --git a/uncloud/management/commands/uncloud.py b/uncloud_django_based/uncloud/uncloud/management/commands/uncloud.py similarity index 100% rename from uncloud/management/commands/uncloud.py rename to uncloud_django_based/uncloud/uncloud/management/commands/uncloud.py diff --git a/uncloud_django_based/uncloud/uncloud/models.py b/uncloud_django_based/uncloud/uncloud/models.py new file mode 100644 index 0000000..bd7a931 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud/models.py @@ -0,0 +1,35 @@ +from django.db import models +from django.contrib.postgres.fields import JSONField +from django.utils.translation import gettext_lazy as _ + +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 diff --git a/uncloud_django_based/uncloud/uncloud/secrets_sample.py b/uncloud_django_based/uncloud/uncloud/secrets_sample.py new file mode 100644 index 0000000..150fefb --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud/secrets_sample.py @@ -0,0 +1,21 @@ +from django.core.management.utils import get_random_secret_key + +# XML-RPC interface of opennebula +OPENNEBULA_URL = 'https://opennebula.ungleich.ch:2634/RPC2' + +# user:pass for accessing opennebula +OPENNEBULA_USER_PASS = 'user:password' + +POSTGRESQL_DB_NAME="uncloud" + +# See https://django-auth-ldap.readthedocs.io/en/latest/authentication.html +LDAP_ADMIN_DN="" +LDAP_ADMIN_PASSWORD="" +LDAP_SERVER_URI = "" + +# Stripe (Credit Card payments) +STRIPE_KEY="" +STRIPE_PUBLIC_KEY="" + +# The django secret key +SECRET_KEY=get_random_secret_key() diff --git a/uncloud/settings.py b/uncloud_django_based/uncloud/uncloud/settings.py similarity index 52% rename from uncloud/settings.py rename to uncloud_django_based/uncloud/uncloud/settings.py index be6cc11..b525073 100644 --- a/uncloud/settings.py +++ b/uncloud_django_based/uncloud/uncloud/settings.py @@ -11,37 +11,44 @@ https://docs.djangoproject.com/en/3.0/ref/settings/ """ import os -import re import ldap -import sys -from django.core.management.utils import get_random_secret_key +# Uncommitted file with secrets +import uncloud.secrets + from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion +# Uncommitted file with local settings i.e logging +try: + from uncloud.local_settings import LOGGING, DATABASES +except ModuleNotFoundError: + LOGGING = {} + # https://docs.djangoproject.com/en/3.0/ref/settings/#databases + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.postgresql', + 'NAME': uncloud.secrets.POSTGRESQL_DB_NAME, + 'HOST': os.environ.get('DATABASE_HOST', '::1'), + 'USER': os.environ.get('DATABASE_USER', 'postgres'), + } + } -LOGGING = {} # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -sys.modules['fontawesome_free'] = __import__('fontawesome-free') - -# https://docs.djangoproject.com/en/3.0/ref/settings/#databases -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.0/howto/deployment/checklist/ +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = uncloud.secrets.SECRET_KEY + # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -SITE_ID = 1 +ALLOWED_HOSTS = [] + # Application definition @@ -51,16 +58,9 @@ INSTALLED_APPS = [ 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', - 'django.contrib.sites', - 'allauth', - 'allauth.account', - 'allauth.socialaccount', 'django.contrib.staticfiles', 'django_extensions', 'rest_framework', - 'bootstrap5', - 'django_q', - 'fontawesome_free', 'uncloud', 'uncloud_pay', 'uncloud_auth', @@ -68,8 +68,7 @@ INSTALLED_APPS = [ 'uncloud_storage', 'uncloud_vm', 'uncloud_service', - 'opennebula', - 'matrixhosting', + 'opennebula' ] MIDDLEWARE = [ @@ -120,22 +119,11 @@ AUTH_PASSWORD_VALIDATORS = [ 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] -############################################################################### -# Authall Settings -ACCOUNT_AUTHENTICATION_METHOD = "username" -ACCOUNT_EMAIL_CONFIRMATION_EXPIRE_DAYS = 1 -ACCOUNT_EMAIL_REQUIRED = False -ACCOUNT_EMAIL_VERIFICATION = "optional" -ACCOUNT_UNIQUE_EMAIL = False + ################################################################################ # AUTH/LDAP -AUTH_LDAP_SERVER_URI = "" -AUTH_LDAP_BIND_DN = "" -AUTH_LDAP_BIND_PASSWORD = "" -AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", - ldap.SCOPE_SUBTREE, - "(uid=%(user)s)") +AUTH_LDAP_SERVER_URI = uncloud.secrets.LDAP_SERVER_URI AUTH_LDAP_USER_ATTR_MAP = { "first_name": "givenName", @@ -143,12 +131,18 @@ AUTH_LDAP_USER_ATTR_MAP = { "email": "mail" } + +AUTH_LDAP_BIND_DN = uncloud.secrets.LDAP_ADMIN_DN +AUTH_LDAP_BIND_PASSWORD = uncloud.secrets.LDAP_ADMIN_PASSWORD + +AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=ungleich,dc=ch", ldap.SCOPE_SUBTREE, "(uid=%(user)s)") + + ################################################################################ # AUTH/Django AUTHENTICATION_BACKENDS = [ "django_auth_ldap.backend.LDAPBackend", - "django.contrib.auth.backends.ModelBackend", - 'allauth.account.auth_backends.AuthenticationBackend', + "django.contrib.auth.backends.ModelBackend" ] AUTH_USER_MODEL = 'uncloud_auth.User' @@ -164,6 +158,7 @@ REST_FRAMEWORK = { } + # Internationalization # https://docs.djangoproject.com/en/3.0/topics/i18n/ @@ -182,84 +177,3 @@ USE_TZ = True # https://docs.djangoproject.com/en/3.0/howto/static-files/ STATIC_URL = '/static/' STATICFILES_DIRS = [ os.path.join(BASE_DIR, "static") ] -STATICFILES_FINDERS = [ - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -] - -#VM Deployment TEMPLATE -GITLAB_SERVER = 'https://code.ungleich.ch' -GITLAB_OAUTH_TOKEN = '' -GITLAB_PROJECT_ID = 388 -GITLAB_AUTHOR_EMAIL = '' -GITLAB_AUTHOR_NAME = '' -GITLAB_YAML_DIR = '' - -# XML-RPC interface of opennebula -OPENNEBULA_URL = 'https://opennebula.example.com:2634/RPC2' - -# user:pass for accessing opennebula -OPENNEBULA_USER_PASS = 'user:password' - -# Stripe (Credit Card payments) -STRIPE_KEY="" -STRIPE_PUBLIC_KEY="" -BILL_PAYMENT_DELAY = 0 -# The django secret key -SECRET_KEY=get_random_secret_key() - -ALLOWED_HOSTS = [] - -# required for hardcopy / pdf rendering: https://github.com/loftylabs/django-hardcopy -CHROME_PATH = '/usr/bin/chromium-browser' - -# Username that is created by default and owns the configuration objects -UNCLOUD_ADMIN_NAME = "uncloud-admin" - -LOGIN_REDIRECT_URL = '/' -LOGOUT_REDIRECT_URL = '/' - -# replace these in local_settings.py -AUTH_LDAP_SERVER_URI = "ldaps://ldap1.example.com,ldaps://ldap2.example.com" -AUTH_LDAP_BIND_DN="uid=django,ou=system,dc=example,dc=com" -AUTH_LDAP_BIND_PASSWORD="a very secure ldap password" -AUTH_LDAP_USER_SEARCH = LDAPSearch("dc=example,dc=com", - ldap.SCOPE_SUBTREE, - "(uid=%(user)s)") - -# where to create customers -LDAP_CUSTOMER_DN="ou=customer,dc=example,dc=com" - -EMAIL_USE_TLS = True -EMAIL_HOST = '' -EMAIL_PORT = 465 -EMAIL_HOST_USER = DEFAULT_FROM_EMAIL = '' -EMAIL_HOST_PASSWORD = '' -DEFAULT_FROM_EMAIL = '' -RENEWAL_FROM_EMAIL = 'test@example.com' -# Should be removed in production -EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' - -############## -# Jobs -Q_CLUSTER = { - 'name': 'matrixhosting', - 'workers': 1, - 'recycle': 500, - 'timeout': 60, - 'compress': True, - 'cpu_affinity': 1, - 'save_limit': 250, - 'queue_limit': 500, - 'label': 'Django Q', - 'redis': { - 'host': '127.0.0.1', - 'port': 6379, - 'db': 0, } -} - -# Overwrite settings with local settings, if existing -try: - from uncloud.local_settings import * -except (ModuleNotFoundError, ImportError): - pass diff --git a/uncloud_django_based/uncloud/uncloud/urls.py b/uncloud_django_based/uncloud/uncloud/urls.py new file mode 100644 index 0000000..14a87e8 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud/urls.py @@ -0,0 +1,88 @@ +"""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/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.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_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 + +router = routers.DefaultRouter() + +# VM +router.register(r'vm/snapshot', vmviews.VMSnapshotProductViewSet, basename='vmsnapshotproduct') +router.register(r'vm/diskimage', vmviews.VMDiskImageProductViewSet, basename='vmdiskimageproduct') +router.register(r'vm/disk', vmviews.VMDiskProductViewSet, basename='vmdiskproduct') +router.register(r'vm/vm', vmviews.VMProductViewSet, basename='vmproduct') + +# creates VM from os image +#router.register(r'vm/ipv6onlyvm', vmviews.VMProductViewSet, basename='vmproduct') +# ... AND adds IPv4 mapping +#router.register(r'vm/dualstackvm', vmviews.VMProductViewSet, basename='vmproduct') + +# Services +router.register(r'service/matrix', serviceviews.MatrixServiceProductViewSet, basename='matrixserviceproduct') +router.register(r'service/generic', serviceviews.GenericServiceProductViewSet, basename='genericserviceproduct') + + +# Net +router.register(r'net/vpn', netviews.VPNNetworkViewSet, basename='vpnnet') +router.register(r'net/vpnreservation', netviews.VPNNetworkReservationViewSet, basename='vpnnetreservation') + + +# Pay +router.register(r'address', payviews.BillingAddressViewSet, basename='address') +router.register(r'bill', payviews.BillViewSet, basename='bill') +router.register(r'order', payviews.OrderViewSet, basename='order') +router.register(r'payment', payviews.PaymentViewSet, basename='payment') +router.register(r'payment-method', payviews.PaymentMethodViewSet, basename='payment-method') + + +# admin/staff urls +router.register(r'admin/bill', payviews.AdminBillViewSet, basename='admin/bill') +router.register(r'admin/payment', payviews.AdminPaymentViewSet, basename='admin/payment') +router.register(r'admin/order', payviews.AdminOrderViewSet, basename='admin/order') +router.register(r'admin/vmhost', vmviews.VMHostViewSet) +router.register(r'admin/vmcluster', vmviews.VMClusterViewSet) +router.register(r'admin/vpnpool', netviews.VPNPoolViewSet) + +router.register(r'admin/opennebula', oneviews.VMViewSet, basename='opennebula') + +# User/Account +router.register(r'user', authviews.UserViewSet, basename='user') + + +urlpatterns = [ + path('', include(router.urls)), + # web/ = stuff to view in the browser + + path('web/pdf/', payviews.MyPDFView.as_view(), name='pdf'), + 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="1.0.0" + ), name='openapi-schema'), +] diff --git a/uncloud/wsgi.py b/uncloud_django_based/uncloud/uncloud/wsgi.py similarity index 100% rename from uncloud/wsgi.py rename to uncloud_django_based/uncloud/uncloud/wsgi.py diff --git a/archive/uncloud_etcd_based/test/__init__.py b/uncloud_django_based/uncloud/uncloud_auth/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/test/__init__.py rename to uncloud_django_based/uncloud/uncloud_auth/__init__.py diff --git a/uncloud_auth/admin.py b/uncloud_django_based/uncloud/uncloud_auth/admin.py similarity index 100% rename from uncloud_auth/admin.py rename to uncloud_django_based/uncloud/uncloud_auth/admin.py diff --git a/uncloud_auth/apps.py b/uncloud_django_based/uncloud/uncloud_auth/apps.py similarity index 100% rename from uncloud_auth/apps.py rename to uncloud_django_based/uncloud/uncloud_auth/apps.py diff --git a/uncloud_auth/migrations/0001_initial.py b/uncloud_django_based/uncloud/uncloud_auth/migrations/0001_initial.py similarity index 88% rename from uncloud_auth/migrations/0001_initial.py rename to uncloud_django_based/uncloud/uncloud_auth/migrations/0001_initial.py index b263dc6..a1f8d00 100644 --- a/uncloud_auth/migrations/0001_initial.py +++ b/uncloud_django_based/uncloud/uncloud_auth/migrations/0001_initial.py @@ -1,8 +1,7 @@ -# Generated by Django 3.1 on 2020-12-13 10:38 +# Generated by Django 3.0.3 on 2020-03-03 16:49 import django.contrib.auth.models import django.contrib.auth.validators -import django.core.validators from django.db import migrations, models import django.utils.timezone @@ -12,7 +11,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), + ('auth', '0011_update_proxy_permissions'), ] operations = [ @@ -24,13 +23,12 @@ class Migration(migrations.Migration): ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('maximum_credit', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/0002_auto_20200318_1343.py b/uncloud_django_based/uncloud/uncloud_auth/migrations/0002_auto_20200318_1343.py new file mode 100644 index 0000000..ad2654f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_auth/migrations/0002_auto_20200318_1343.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.3 on 2020-03-18 13:43 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_auth', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='amount', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + migrations.AddField( + model_name='user', + name='maximum_credit', + field=models.FloatField(default=0), + preserve_default=False, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_auth/migrations/0003_auto_20200318_1345.py b/uncloud_django_based/uncloud/uncloud_auth/migrations/0003_auto_20200318_1345.py new file mode 100644 index 0000000..31b1717 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_auth/migrations/0003_auto_20200318_1345.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-03-18 13:45 + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_auth', '0002_auto_20200318_1343'), + ] + + operations = [ + migrations.RemoveField( + model_name='user', + name='amount', + ), + migrations.AlterField( + model_name='user', + name='maximum_credit', + field=models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/archive/uncloud_etcd_based/uncloud/cli/__init__.py b/uncloud_django_based/uncloud/uncloud_auth/migrations/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/__init__.py rename to uncloud_django_based/uncloud/uncloud_auth/migrations/__init__.py diff --git a/uncloud_auth/models.py b/uncloud_django_based/uncloud/uncloud_auth/models.py similarity index 81% rename from uncloud_auth/models.py rename to uncloud_django_based/uncloud/uncloud_auth/models.py index 90463e1..c3a0912 100644 --- a/uncloud_auth/models.py +++ b/uncloud_django_based/uncloud/uncloud_auth/models.py @@ -4,6 +4,8 @@ from django.core.validators import MinValueValidator from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS +from uncloud_pay.models import get_balance_for_user + class User(AbstractUser): """ We use the standard user and add a maximum credit that is allowed @@ -15,3 +17,7 @@ class User(AbstractUser): max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS, validators=[MinValueValidator(0)]) + + @property + def balance(self): + return get_balance_for_user(self) diff --git a/uncloud_django_based/uncloud/uncloud_auth/serializers.py b/uncloud_django_based/uncloud/uncloud_auth/serializers.py new file mode 100644 index 0000000..de369c3 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_auth/serializers.py @@ -0,0 +1,15 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers + +from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS + +class UserSerializer(serializers.ModelSerializer): + + class Meta: + model = get_user_model() + fields = ['username', 'email', 'balance', 'maximum_credit' ] + + + + balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS) diff --git a/uncloud_django_based/uncloud/uncloud_auth/views.py b/uncloud_django_based/uncloud/uncloud_auth/views.py new file mode 100644 index 0000000..2f78e1f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_auth/views.py @@ -0,0 +1,17 @@ +from rest_framework import viewsets, permissions, status +from .serializers import * + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + if self.request.user.is_superuser: + obj = get_user_model().objects.all() + else: + # 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 + obj = get_user_model().objects.filter(username=self.request.user.username) + + return obj diff --git a/archive/uncloud_etcd_based/uncloud/client/__init__.py b/uncloud_django_based/uncloud/uncloud_net/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/client/__init__.py rename to uncloud_django_based/uncloud/uncloud_net/__init__.py diff --git a/uncloud_service/admin.py b/uncloud_django_based/uncloud/uncloud_net/admin.py similarity index 100% rename from uncloud_service/admin.py rename to uncloud_django_based/uncloud/uncloud_net/admin.py diff --git a/uncloud_net/apps.py b/uncloud_django_based/uncloud/uncloud_net/apps.py similarity index 100% rename from uncloud_net/apps.py rename to uncloud_django_based/uncloud/uncloud_net/apps.py diff --git a/uncloud_net/management/commands/vpn.py b/uncloud_django_based/uncloud/uncloud_net/management/commands/vpn.py similarity index 100% rename from uncloud_net/management/commands/vpn.py rename to uncloud_django_based/uncloud/uncloud_net/management/commands/vpn.py diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0001_initial.py b/uncloud_django_based/uncloud/uncloud_net/migrations/0001_initial.py new file mode 100644 index 0000000..940d63f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/migrations/0001_initial.py @@ -0,0 +1,68 @@ +# Generated by Django 3.0.5 on 2020-04-06 21:38 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='MACAdress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='VPNPool', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('network', models.GenericIPAddressField(unique=True)), + ('network_size', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])), + ('subnetwork_size', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])), + ('vpn_hostname', models.CharField(max_length=256)), + ('wireguard_private_key', models.CharField(max_length=48)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='VPNNetworkReservation', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('address', models.GenericIPAddressField(primary_key=True, serialize=False)), + ('vpnpool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.VPNPool')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='VPNNetwork', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32)), + ('wireguard_public_key', models.CharField(max_length=48)), + ('network', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.VPNNetworkReservation')), + ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0002_auto_20200409_1225.py b/uncloud_django_based/uncloud/uncloud_net/migrations/0002_auto_20200409_1225.py new file mode 100644 index 0000000..fcc2374 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/migrations/0002_auto_20200409_1225.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.5 on 2020-04-09 12:25 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_net', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='vpnnetworkreservation', + name='status', + field=models.CharField(choices=[('used', 'used'), ('free', 'free')], default='used', max_length=256), + ), + migrations.AlterField( + model_name='vpnnetwork', + name='network', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.VPNNetworkReservation'), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py new file mode 100644 index 0000000..24f4a7f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/migrations/0003_auto_20200417_0551.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_net', '0002_auto_20200409_1225'), + ] + + operations = [ + migrations.AlterField( + model_name='vpnnetwork', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32), + ), + ] diff --git a/archive/uncloud_etcd_based/uncloud/configure/__init__.py b/uncloud_django_based/uncloud/uncloud_net/migrations/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/configure/__init__.py rename to uncloud_django_based/uncloud/uncloud_net/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_net/models.py b/uncloud_django_based/uncloud/uncloud_net/models.py new file mode 100644 index 0000000..26a6eb8 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/models.py @@ -0,0 +1,183 @@ +import uuid +import ipaddress + +from django.db import models +from django.contrib.auth import get_user_model +from django.core.validators import MinValueValidator, MaxValueValidator + + +from uncloud_pay.models import Product, RecurringPeriod +from uncloud.models import UncloudModel, UncloudStatus + + +class MACAdress(models.Model): + default_prefix = 0x420000000000 + +class VPNPool(UncloudModel): + """ + Network address pools from which VPNs can be created + """ + + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + network = models.GenericIPAddressField(unique=True) + network_size = models.IntegerField(validators=[MinValueValidator(0), + MaxValueValidator(128)]) + + subnetwork_size = models.IntegerField(validators=[ + MinValueValidator(0), + MaxValueValidator(128) + ]) + + vpn_hostname = models.CharField(max_length=256) + + wireguard_private_key = models.CharField(max_length=48) + + @property + def num_maximum_networks(self): + """ + sample: + network_size = 40 + subnetwork_size = 48 + maximum_networks = 2^(48-40) + + 2nd sample: + network_size = 8 + subnetwork_size = 24 + maximum_networks = 2^(24-8) + """ + + return 2**(self.subnetwork_size - self.network_size) + + @property + def used_networks(self): + return self.vpnnetworkreservation_set.filter(vpnpool=self, status='used') + + @property + def free_networks(self): + return self.vpnnetworkreservation_set.filter(vpnpool=self, status='free') + + @property + def num_used_networks(self): + return len(self.used_networks) + + @property + def num_free_networks(self): + return self.num_maximum_networks - self.num_used_networks + len(self.free_networks) + + @property + def next_free_network(self): + if self.num_free_networks == 0: + # FIXME: use right exception + raise Exception("No free networks") + + if len(self.free_networks) > 0: + return self.free_networks[0].address + + if len(self.used_networks) > 0: + """ + sample: + + pool = 2a0a:e5c1:200::/40 + last_used = 2a0a:e5c1:204::/48 + + next: + """ + + last_net = ipaddress.ip_network(self.used_networks.last().address) + last_net_ip = last_net[0] + + if last_net_ip.version == 6: + offset_to_next = 2**(128 - self.subnetwork_size) + elif last_net_ip.version == 4: + offset_to_next = 2**(32 - self.subnetwork_size) + + next_net_ip = last_net_ip + offset_to_next + + return str(next_net_ip) + else: + # first network to be created + return self.network + + @property + def wireguard_config_filename(self): + return '/etc/wireguard/{}.conf'.format(self.network) + + @property + def wireguard_config(self): + wireguard_config = [ + """ +[Interface] +ListenPort = 51820 +PrivateKey = {privatekey} +""".format(privatekey=self.wireguard_private_key) ] + + peers = [] + + for reservation in self.vpnnetworkreservation_set.filter(status='used'): + public_key = reservation.vpnnetwork_set.first().wireguard_public_key + peer_network = "{}/{}".format(reservation.address, self.subnetwork_size) + owner = reservation.vpnnetwork_set.first().owner + + peers.append(""" +# Owner: {owner} +[Peer] +PublicKey = {public_key} +AllowedIPs = {peer_network} +""".format( + owner=owner, + public_key=public_key, + peer_network=peer_network)) + + wireguard_config.extend(peers) + + return "\n".join(wireguard_config) + + + def configure_wireguard_vpnserver(self): + """ + This method is designed to run as a celery task and should + not be called directly from the web + """ + + # subprocess, ssh + + pass + + +class VPNNetworkReservation(UncloudModel): + """ + This class tracks the used VPN networks. It will be deleted, when the product is cancelled. + """ + vpnpool = models.ForeignKey(VPNPool, + on_delete=models.CASCADE) + + address = models.GenericIPAddressField(primary_key=True) + + status = models.CharField(max_length=256, + default='used', + choices = ( + ('used', 'used'), + ('free', 'free') + ) + ) + + +class VPNNetwork(Product): + """ + A selected network. Used for tracking reservations / used networks + """ + network = models.ForeignKey(VPNNetworkReservation, + on_delete=models.CASCADE, + editable=False) + + wireguard_public_key = models.CharField(max_length=48) + + def delete(self, *args, **kwargs): + self.network.status = 'free' + self.network.save() + super().save(*args, **kwargs) + print("deleted {}".format(self)) + +# managing deletion +# - record free network (?) diff --git a/uncloud_django_based/uncloud/uncloud_net/serializers.py b/uncloud_django_based/uncloud/uncloud_net/serializers.py new file mode 100644 index 0000000..dc4866e --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/serializers.py @@ -0,0 +1,100 @@ +import base64 + +from django.contrib.auth import get_user_model +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers + +from .models import * + +class VPNPoolSerializer(serializers.ModelSerializer): + class Meta: + model = VPNPool + fields = '__all__' + +class VPNNetworkReservationSerializer(serializers.ModelSerializer): + class Meta: + model = VPNNetworkReservation + fields = '__all__' + + +class VPNNetworkSerializer(serializers.ModelSerializer): + class Meta: + model = VPNNetwork + fields = '__all__' + + # This is required for finding the VPN pool, but does not + # exist in the model + network_size = serializers.IntegerField(min_value=0, + max_value=128, + write_only=True) + + def validate_wireguard_public_key(self, value): + msg = _("Supplied key is not a valid wireguard public key") + + """ FIXME: verify that this does not create broken wireguard config files, + i.e. contains \n or similar! + We might even need to be more strict to not break wireguard... + """ + + try: + base64.standard_b64decode(value) + except Exception as e: + raise serializers.ValidationError(msg) + + if '\n' in value: + raise serializers.ValidationError(msg) + + return value + + def validate(self, data): + + # FIXME: filter for status = active or similar + all_pools = VPNPool.objects.all() + sizes = [ p.subnetwork_size for p in all_pools ] + + pools = VPNPool.objects.filter(subnetwork_size=data['network_size']) + + if len(pools) == 0: + msg = _("No pool available for networks with size = {}. Available are: {}".format(data['network_size'], sizes)) + raise serializers.ValidationError(msg) + + return data + + def create(self, validated_data): + """ + Creating a new vpnnetwork - there are a couple of race conditions, + especially when run in parallel. + + What we should be doing: + + - create a reservation race free + - map the reservation to a network (?) + """ + + pools = VPNPool.objects.filter(subnetwork_size=validated_data['network_size']) + + vpn_network = None + + for pool in pools: + if pool.num_free_networks > 0: + next_address = pool.next_free_network + + reservation, created = VPNNetworkReservation.objects.update_or_create( + vpnpool=pool, address=next_address, + defaults = { + 'status': 'used' + }) + + vpn_network = VPNNetwork.objects.create( + owner=self.context['request'].user, + network=reservation, + wireguard_public_key=validated_data['wireguard_public_key'] + ) + + break + if not vpn_network: + # FIXME: use correct exception + raise Exception("Did not find any free pool") + + + return vpn_network diff --git a/uncloud_service/tests.py b/uncloud_django_based/uncloud/uncloud_net/tests.py similarity index 100% rename from uncloud_service/tests.py rename to uncloud_django_based/uncloud/uncloud_net/tests.py diff --git a/uncloud_django_based/uncloud/uncloud_net/views.py b/uncloud_django_based/uncloud/uncloud_net/views.py new file mode 100644 index 0000000..1f7cf4a --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_net/views.py @@ -0,0 +1,32 @@ + +from django.shortcuts import render + +from rest_framework import viewsets, permissions + + +from .models import * +from .serializers import * + + +class VPNPoolViewSet(viewsets.ModelViewSet): + serializer_class = VPNPoolSerializer + permission_classes = [permissions.IsAdminUser] + queryset = VPNPool.objects.all() + +class VPNNetworkReservationViewSet(viewsets.ModelViewSet): + serializer_class = VPNNetworkReservationSerializer + permission_classes = [permissions.IsAdminUser] + queryset = VPNNetworkReservation.objects.all() + + +class VPNNetworkViewSet(viewsets.ModelViewSet): + serializer_class = VPNNetworkSerializer + permission_classes = [permissions.IsAdminUser] + + def get_queryset(self): + if self.request.user.is_superuser: + obj = VPNNetwork.objects.all() + else: + obj = VPNNetwork.objects.filter(owner=self.request.user) + + return obj diff --git a/archive/uncloud_etcd_based/uncloud/network/__init__.py b/uncloud_django_based/uncloud/uncloud_pay/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/__init__.py rename to uncloud_django_based/uncloud/uncloud_pay/__init__.py diff --git a/uncloud_storage/admin.py b/uncloud_django_based/uncloud/uncloud_pay/admin.py similarity index 100% rename from uncloud_storage/admin.py rename to uncloud_django_based/uncloud/uncloud_pay/admin.py diff --git a/uncloud_pay/apps.py b/uncloud_django_based/uncloud/uncloud_pay/apps.py similarity index 100% rename from uncloud_pay/apps.py rename to uncloud_django_based/uncloud/uncloud_pay/apps.py diff --git a/uncloud_pay/helpers.py b/uncloud_django_based/uncloud/uncloud_pay/helpers.py similarity index 100% rename from uncloud_pay/helpers.py rename to uncloud_django_based/uncloud/uncloud_pay/helpers.py diff --git a/uncloud_pay/management/commands/charge-negative-balance.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py similarity index 50% rename from uncloud_pay/management/commands/charge-negative-balance.py rename to uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py index 8405bd3..8ee8736 100644 --- a/uncloud_pay/management/commands/charge-negative-balance.py +++ b/uncloud_django_based/uncloud/uncloud_pay/management/commands/charge-negative-balance.py @@ -1,7 +1,6 @@ from django.core.management.base import BaseCommand from uncloud_auth.models import User -from uncloud_pay.models import Order, Bill, get_balance_for_user -import uncloud_pay.stripe as uncloud_stripe +from uncloud_pay.models import Order, Bill, PaymentMethod, get_balance_for_user from datetime import timedelta from django.utils import timezone @@ -19,10 +18,14 @@ class Command(BaseCommand): balance = get_balance_for_user(user) if balance < 0: print("User {} has negative balance ({}), charging.".format(user.username, balance)) - amount_to_be_charged = abs(balance) - result = uncloud_stripe.charge_customer(user, amount_to_be_charged) - if result.status != 'succeeded': - print("ERR: charging {} with method {} failed" - .format(user.username, result) - ) + payment_method = PaymentMethod.get_primary_for(user) + if payment_method != None: + amount_to_be_charged = abs(balance) + charge_ok = payment_method.charge(amount_to_be_charged) + if not charge_ok: + print("ERR: charging {} with method {} failed" + .format(user.username, payment_method.uuid) + ) + else: + print("ERR: no payment method registered for {}".format(user.username)) print("=> Done.") diff --git a/uncloud_pay/management/commands/generate-bills.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/generate-bills.py similarity index 100% rename from uncloud_pay/management/commands/generate-bills.py rename to uncloud_django_based/uncloud/uncloud_pay/management/commands/generate-bills.py diff --git a/uncloud_pay/management/commands/handle-overdue-bills.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/handle-overdue-bills.py similarity index 100% rename from uncloud_pay/management/commands/handle-overdue-bills.py rename to uncloud_django_based/uncloud/uncloud_pay/management/commands/handle-overdue-bills.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py new file mode 100644 index 0000000..32938e4 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/management/commands/import-vat-rates.py @@ -0,0 +1,44 @@ +from django.core.management.base import BaseCommand +from uncloud_pay.models import VATRate +import csv + + +class Command(BaseCommand): + help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv''' + + def add_arguments(self, parser): + parser.add_argument('csv_file', nargs='+', type=str) + + def handle(self, *args, **options): + try: + for c_file in options['csv_file']: + print("c_file = %s" % c_file) + with open(c_file, mode='r') as csv_file: + csv_reader = csv.DictReader(csv_file) + line_count = 0 + for row in csv_reader: + if line_count == 0: + line_count += 1 + obj, created = VATRate.objects.get_or_create( + start_date=row["start_date"], + stop_date=row["stop_date"] if row["stop_date"] is not "" else None, + territory_codes=row["territory_codes"], + currency_code=row["currency_code"], + rate=row["rate"], + rate_type=row["rate_type"], + description=row["description"] + ) + if created: + self.stdout.write(self.style.SUCCESS( + '%s. %s - %s - %s - %s' % ( + line_count, + obj.start_date, + obj.stop_date, + obj.territory_codes, + obj.rate + ) + )) + line_count+=1 + + except Exception as e: + print(" *** Error occurred. Details {}".format(str(e))) diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0001_initial.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0001_initial.py new file mode 100644 index 0000000..89fa586 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0001_initial.py @@ -0,0 +1,85 @@ +# Generated by Django 3.0.3 on 2020-03-05 10:17 + +from django.conf import settings +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_auth', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Bill', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('starting_date', models.DateTimeField()), + ('ending_date', models.DateTimeField()), + ('due_date', models.DateField()), + ('valid', models.BooleanField(default=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('creation_date', models.DateTimeField(auto_now_add=True)), + ('starting_date', models.DateTimeField(auto_now_add=True)), + ('ending_date', models.DateTimeField(blank=True, null=True)), + ('recurring_period', models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('MINUTE', 'Per Minute'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('SECOND', 'Per Second')], default='MONTH', max_length=32)), + ('bill', models.ManyToManyField(blank=True, editable=False, to='uncloud_pay.Bill')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='StripeCustomer', + fields=[ + ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)), + ('stripe_id', models.CharField(max_length=32)), + ], + ), + migrations.CreateModel( + name='Payment', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('source', models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral'), ('unknown', 'Unknown')], default='unknown', max_length=256)), + ('timestamp', models.DateTimeField(auto_now_add=True)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='OrderRecord', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('description', models.TextField()), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ], + ), + migrations.CreateModel( + name='PaymentMethod', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('source', models.CharField(choices=[('stripe', 'Stripe'), ('unknown', 'Unknown')], default='stripe', max_length=256)), + ('description', models.TextField()), + ('primary', models.BooleanField(default=True)), + ('stripe_card_id', models.CharField(blank=True, max_length=32, null=True)), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'unique_together': {('owner', 'primary')}, + }, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py new file mode 100644 index 0000000..0768dd0 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0002_auto_20200305_1524.py @@ -0,0 +1,27 @@ +# Generated by Django 3.0.3 on 2020-03-05 15:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0001_initial'), + ] + + operations = [ + migrations.RenameField( + model_name='paymentmethod', + old_name='stripe_card_id', + new_name='stripe_payment_method_id', + ), + migrations.AddField( + model_name='paymentmethod', + name='stripe_setup_intent_id', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AlterUniqueTogether( + name='paymentmethod', + unique_together=set(), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py new file mode 100644 index 0000000..4157732 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0003_auto_20200305_1354.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.3 on 2020-03-05 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0002_auto_20200305_1524'), + ] + + operations = [ + migrations.AlterField( + model_name='paymentmethod', + name='primary', + field=models.BooleanField(default=False, editable=False), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0004_auto_20200409_1225.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0004_auto_20200409_1225.py new file mode 100644 index 0000000..32aac87 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0004_auto_20200409_1225.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-04-09 12:25 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0003_auto_20200305_1354'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('MINUTE', 'Per Minute'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), + ), + migrations.AlterField( + model_name='order', + name='starting_date', + field=models.DateTimeField(), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200413_0924.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200413_0924.py new file mode 100644 index 0000000..3f6a646 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0005_auto_20200413_0924.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-13 09:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0004_auto_20200409_1225'), + ] + + operations = [ + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py new file mode 100644 index 0000000..1f37eae --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_auto_20200415_1003.py @@ -0,0 +1,31 @@ +# Generated by Django 3.0.5 on 2020-04-15 10:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0005_auto_20200413_0924'), + ] + + operations = [ + migrations.CreateModel( + name='VATRate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('start_date', models.DateField(blank=True, null=True)), + ('stop_date', models.DateField(blank=True, null=True)), + ('territory_codes', models.TextField(blank=True, default='')), + ('currency_code', models.CharField(max_length=10)), + ('rate', models.FloatField()), + ('rate_type', models.TextField(blank=True, default='')), + ('description', models.TextField(blank=True, default='')), + ], + ), + migrations.AlterField( + model_name='order', + name='recurring_period', + field=models.CharField(choices=[('ONCE', 'Onetime'), ('YEAR', 'Per Year'), ('MONTH', 'Per Month'), ('WEEK', 'Per Week'), ('DAY', 'Per Day'), ('HOUR', 'Per Hour'), ('MINUTE', 'Per Minute'), ('SECOND', 'Per Second')], default='MONTH', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py new file mode 100644 index 0000000..79b25ab --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0006_billingaddress.py @@ -0,0 +1,29 @@ +# Generated by Django 3.0.5 on 2020-04-15 12:29 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uncloud_pay.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0006_auto_20200415_1003'), + ] + + operations = [ + migrations.CreateModel( + name='BillingAddress', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('street', models.CharField(max_length=100)), + ('city', models.CharField(max_length=50)), + ('postal_code', models.CharField(max_length=50)), + ('country', uncloud_pay.models.CountryField(choices=[('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')], default='CH', max_length=2)), + ('vat_number', models.CharField(blank=True, default='', max_length=100)), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py new file mode 100644 index 0000000..c9c2342 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/migrations/0007_auto_20200418_0737.py @@ -0,0 +1,42 @@ +# Generated by Django 3.0.5 on 2020-04-18 07:37 + +from django.db import migrations, models +import django.db.models.deletion +import uncloud_pay.models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0006_billingaddress'), + ] + + operations = [ + migrations.RemoveField( + model_name='billingaddress', + name='id', + ), + migrations.AddField( + model_name='billingaddress', + name='name', + field=models.CharField(default='unknown', max_length=100), + preserve_default=False, + ), + migrations.AddField( + model_name='billingaddress', + name='uuid', + field=models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False), + ), + migrations.AddField( + model_name='order', + name='billing_address', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.BillingAddress'), + preserve_default=False, + ), + migrations.AlterField( + model_name='billingaddress', + name='country', + field=uncloud_pay.models.CountryField(blank=True, choices=[('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')], default='CH', max_length=2), + ), + ] diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py b/uncloud_django_based/uncloud/uncloud_pay/migrations/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py rename to uncloud_django_based/uncloud/uncloud_pay/migrations/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_pay/models.py b/uncloud_django_based/uncloud/uncloud_pay/models.py new file mode 100644 index 0000000..bcce598 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/models.py @@ -0,0 +1,1021 @@ +from django.db import models +from django.db.models import Q +from django.contrib.auth import get_user_model +from django.core.validators import MinValueValidator +from django.utils.translation import gettext_lazy as _ +from django.utils import timezone +from django.core.exceptions import ObjectDoesNotExist + +import uuid +import logging +from functools import reduce +import itertools +from math import ceil +from datetime import timedelta +from calendar import monthrange +from decimal import Decimal + +import uncloud_pay.stripe +from uncloud_pay.helpers import beginning_of_month, end_of_month +from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS +from uncloud.models import UncloudModel, UncloudStatus + +from decimal import Decimal +import decimal + +# Define DecimalField properties, used to represent amounts of money. +AMOUNT_MAX_DIGITS=10 +AMOUNT_DECIMALS=2 + +# FIXME: check why we need +1 here. +decimal.getcontext().prec = AMOUNT_DECIMALS + 1 + +# Used to generate bill due dates. +BILL_PAYMENT_DELAY=timedelta(days=10) + +# 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')), +) + +# Initialize logger. +logger = logging.getLogger(__name__) + +# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types +class RecurringPeriod(models.TextChoices): + ONE_TIME = 'ONCE', _('Onetime') + PER_YEAR = 'YEAR', _('Per Year') + PER_MONTH = 'MONTH', _('Per Month') + PER_WEEK = 'WEEK', _('Per Week') + PER_DAY = 'DAY', _('Per Day') + PER_HOUR = 'HOUR', _('Per Hour') + PER_MINUTE = 'MINUTE', _('Per Minute') + PER_SECOND = 'SECOND', _('Per Second') + +class CountryField(models.CharField): + def __init__(self, *args, **kwargs): + kwargs.setdefault('choices', COUNTRIES) + kwargs.setdefault('default', 'CH') + kwargs.setdefault('max_length', 2) + + super(CountryField, self).__init__(*args, **kwargs) + + def get_internal_type(self): + return "CharField" + +def get_balance_for_user(user): + bills = reduce( + lambda acc, entry: acc + entry.total, + Bill.objects.filter(owner=user), + 0) + payments = reduce( + lambda acc, entry: acc + entry.amount, + Payment.objects.filter(owner=user), + 0) + return payments - bills + +class StripeCustomer(models.Model): + owner = models.OneToOneField( get_user_model(), + primary_key=True, + on_delete=models.CASCADE) + stripe_id = models.CharField(max_length=32) + +### +# Payments and Payment Methods. + +class Payment(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE) + + amount = models.DecimalField( + default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + source = models.CharField(max_length=256, + choices = ( + ('wire', 'Wire Transfer'), + ('stripe', 'Stripe'), + ('voucher', 'Voucher'), + ('referral', 'Referral'), + ('unknown', 'Unknown') + ), + default='unknown') + timestamp = models.DateTimeField(editable=False, auto_now_add=True) + + # We override save() in order to active products awaiting payment. + def save(self, *args, **kwargs): + # _state.adding is switched to false after super(...) call. + being_created = self._state.adding + + unpaid_bills_before_payment = Bill.get_unpaid_for(self.owner) + super(Payment, self).save(*args, **kwargs) # Save payment in DB. + unpaid_bills_after_payment = Bill.get_unpaid_for(self.owner) + + newly_paid_bills = list( + set(unpaid_bills_before_payment) - set(unpaid_bills_after_payment)) + for bill in newly_paid_bills: + bill.activate_products() + +class PaymentMethod(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE, + editable=False) + source = models.CharField(max_length=256, + choices = ( + ('stripe', 'Stripe'), + ('unknown', 'Unknown'), + ), + default='stripe') + description = models.TextField() + primary = models.BooleanField(default=False, editable=False) + + # Only used for "Stripe" source + stripe_payment_method_id = models.CharField(max_length=32, blank=True, null=True) + stripe_setup_intent_id = models.CharField(max_length=32, blank=True, null=True) + + @property + def stripe_card_last4(self): + if self.source == 'stripe' and self.active: + payment_method = uncloud_pay.stripe.get_payment_method( + self.stripe_payment_method_id) + return payment_method.card.last4 + else: + return None + + @property + def active(self): + if self.source == 'stripe' and self.stripe_payment_method_id != None: + return True + else: + return False + + def charge(self, amount): + if not self.active: + raise Exception('This payment method is inactive.') + + if amount < 0: # Make sure we don't charge negative amount by errors... + raise Exception('Cannot charge negative amount.') + + if self.source == 'stripe': + stripe_customer = StripeCustomer.objects.get(owner=self.owner).stripe_id + stripe_payment = uncloud_pay.stripe.charge_customer( + amount, stripe_customer, self.stripe_payment_method_id) + if 'paid' in stripe_payment and stripe_payment['paid'] == False: + raise Exception(stripe_payment['error']) + else: + payment = Payment.objects.create( + owner=self.owner, source=self.source, amount=amount) + + return payment + else: + raise Exception('This payment method is unsupported/cannot be charged.') + + def set_as_primary_for(self, user): + methods = PaymentMethod.objects.filter(owner=user, primary=True) + for method in methods: + print(method) + method.primary = False + method.save() + + self.primary = True + self.save() + + def get_primary_for(user): + methods = PaymentMethod.objects.filter(owner=user) + for method in methods: + # Do we want to do something with non-primary method? + if method.active and method.primary: + return method + + return None + + class Meta: + # TODO: limit to one primary method per user. + # unique_together is no good since it won't allow more than one + # non-primary method. + pass + +### +# Bills. + +class BillingAddress(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + + name = models.CharField(max_length=100) + street = models.CharField(max_length=100) + city = models.CharField(max_length=50) + postal_code = models.CharField(max_length=50) + country = CountryField(blank=True) + vat_number = models.CharField(max_length=100, default="", blank=True) + + @staticmethod + def get_addresses_for(user): + return BillingAddress.objects.filter(owner=user) + + @staticmethod + def get_preferred_address_for(user): + addresses = get_addresses_for(user) + if len(addresses) == 0: + return None + else: + # TODO: allow user to set primary/preferred address + return addresses[0] + + def __str__(self): + return "{}, {}, {} {}, {}".format( + self.name, self.street, self.postal_code, self.city, + self.country) + +# Populated with the import-vat-numbers django command. +class VATRate(models.Model): + start_date = models.DateField(blank=True, null=True) + stop_date = models.DateField(blank=True, null=True) + territory_codes = models.TextField(blank=True, default='') + currency_code = models.CharField(max_length=10) + rate = models.FloatField() + rate_type = models.TextField(blank=True, default='') + description = models.TextField(blank=True, default='') + + @staticmethod + def get_for_country(country_code): + vat_rate = None + try: + vat_rate = VATRate.objects.get( + territory_codes=country_code, start_date__isnull=False, stop_date=None + ) + return vat_rate.rate + except VATRate.DoesNotExist as dne: + logger.debug(str(dne)) + logger.debug("Did not find VAT rate for %s, returning 0" % country_code) + return 0 + +class Bill(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE) + + creation_date = models.DateTimeField(auto_now_add=True) + starting_date = models.DateTimeField() + ending_date = models.DateTimeField() + due_date = models.DateField() + + valid = models.BooleanField(default=True) + + # Trigger product activation if bill paid at creation (from balance). + def save(self, *args, **kwargs): + super(Bill, self).save(*args, **kwargs) + if not self in Bill.get_unpaid_for(self.owner): + self.activate_products() + + @property + def reference(self): + return "{}-{}".format( + self.owner.username, + self.creation_date.strftime("%Y-%m-%d-%H%M")) + + @property + def records(self): + bill_records = [] + orders = Order.objects.filter(bill=self) + for order in orders: + for order_record in order.records: + bill_record = BillRecord(self, order_record) + bill_records.append(bill_record) + + return bill_records + + @property + def amount(self): + return reduce(lambda acc, record: acc + record.amount, self.records, 0) + + @property + def vat_amount(self): + return reduce(lambda acc, record: acc + record.vat_amount, self.records, 0) + + @property + def total(self): + return self.amount + self.vat_amount + + @property + def final(self): + # A bill is final when its ending date is passed. + return self.ending_date < timezone.now() + + def activate_products(self): + for order in self.order_set.all(): + # FIXME: using __something might not be a good idea. + for product_class in Product.__subclasses__(): + for product in product_class.objects.filter(order=order): + if product.status == UncloudStatus.AWAITING_PAYMENT: + product.status = UncloudStatus.PENDING + product.save() + + @property + def billing_address(self): + orders = Order.objects.filter(bill=self) + # The genrate_for method makes sure all the orders of a bill share the + # same billing address. TODO: It would be nice to enforce that somehow... + return orders[0].billing_address + + # TODO: split this huuuge method! + @staticmethod + def generate_for(year, month, user): + # /!\ We exclusively work on the specified year and month. + generated_bills = [] + + # Default values for next bill (if any). + starting_date=beginning_of_month(year, month) + ending_date=end_of_month(year, month) + creation_date=timezone.now() + + # Select all orders active on the request period (i.e. starting on or after starting_date). + orders = Order.objects.filter( + Q(ending_date__gte=starting_date) | Q(ending_date__isnull=True), + owner=user) + + # Check if there is already a bill covering the order and period pair: + # * Get latest bill by ending_date: previous_bill.ending_date + # * For monthly bills: if previous_bill.ending_date is before + # (next_bill) ending_date, a new bill has to be generated. + # * For yearly bill: if previous_bill.ending_date is on working + # month, generate new bill. + unpaid_orders = { 'monthly_or_less': [], 'yearly': {} } + for order in orders: + try: + previous_bill = order.bill.latest('ending_date') + except ObjectDoesNotExist: + previous_bill = None + + # FIXME: control flow is confusing in this block. + if order.recurring_period == RecurringPeriod.PER_YEAR: + # We ignore anything smaller than a day in here. + next_yearly_bill_start_on = None + if previous_bill == None: + next_yearly_bill_start_on = order.starting_date + elif previous_bill.ending_date <= ending_date: + next_yearly_bill_start_on = (previous_bill.ending_date + timedelta(days=1)) + + # Store for bill generation. One bucket per day of month with a starting bill. + # bucket is a reference here, no need to reassign. + if next_yearly_bill_start_on: + # We want to group orders by date but keep using datetimes. + next_yearly_bill_start_on = next_yearly_bill_start_on.replace( + minute=0, hour=0, second=0, microsecond=0) + bucket = unpaid_orders['yearly'].get(next_yearly_bill_start_on) + if bucket == None: + unpaid_orders['yearly'][next_yearly_bill_start_on] = [order] + else: + unpaid_orders['yearly'][next_yearly_bill_start_on] = bucket + [order] + else: + if previous_bill == None or previous_bill.ending_date < ending_date: + unpaid_orders['monthly_or_less'].append(order) + + # Handle working month's billing. + if len(unpaid_orders['monthly_or_less']) > 0: + # TODO: PREPAID billing is not supported yet. + prepaid_due_date = min(creation_date, starting_date) + BILL_PAYMENT_DELAY + postpaid_due_date = max(creation_date, ending_date) + BILL_PAYMENT_DELAY + + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['monthly_or_less'], + lambda o: o.billing_address) + + for addr, bill_orders in per_address_orders: + next_monthly_bill = Bill.objects.create(owner=user, + creation_date=creation_date, + starting_date=starting_date, # FIXME: this is a hack! + ending_date=ending_date, + due_date=postpaid_due_date) + + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_monthly_bill) + + logger.info("Generated monthly bill {} (amount: {}) for user {}." + .format(next_monthly_bill.uuid, next_monthly_bill.total, user)) + + # Add to output. + generated_bills.append(next_monthly_bill) + + # Handle yearly bills starting on working month. + if len(unpaid_orders['yearly']) > 0: + # For every starting date, generate new bill. + for next_yearly_bill_start_on in unpaid_orders['yearly']: + # No postpaid for yearly payments. + prepaid_due_date = min(creation_date, next_yearly_bill_start_on) + BILL_PAYMENT_DELAY + # Bump by one year, remove one day. + ending_date = next_yearly_bill_start_on.replace( + year=next_yearly_bill_start_on.year+1) - timedelta(days=1) + + # There should not be any bill linked to orders with different + # billing addresses. + per_address_orders = itertools.groupby( + unpaid_orders['yearly'][next_yearly_bill_start_on], + lambda o: o.billing_address) + + for addr, bill_orders in per_address_orders: + next_yearly_bill = Bill.objects.create(owner=user, + creation_date=creation_date, + starting_date=next_yearly_bill_start_on, + ending_date=ending_date, + due_date=prepaid_due_date) + + # It is not possible to register many-to-many relationship before + # the two end-objects are saved in database. + for order in bill_orders: + order.bill.add(next_yearly_bill) + + logger.info("Generated yearly bill {} (amount: {}) for user {}." + .format(next_yearly_bill.uuid, next_yearly_bill.total, user)) + + # Add to output. + generated_bills.append(next_yearly_bill) + + # Return generated (monthly + yearly) bills. + return generated_bills + + @staticmethod + def get_unpaid_for(user): + balance = get_balance_for_user(user) + unpaid_bills = [] + # No unpaid bill if balance is positive. + if balance >= 0: + return unpaid_bills + else: + bills = Bill.objects.filter( + owner=user, + ).order_by('-creation_date') + + # Amount to be paid by the customer. + unpaid_balance = abs(balance) + for bill in bills: + if unpaid_balance <= 0: + break + + unpaid_balance -= bill.total + unpaid_bills.append(bill) + + return unpaid_bills + + @staticmethod + def get_overdue_for(user): + unpaid_bills = Bill.get_unpaid_for(user) + return list(filter(lambda bill: bill.due_date > timezone.now(), unpaid_bills)) + +class BillRecord(): + """ + Entry of a bill, dynamically generated from order records. + """ + + def __init__(self, bill, order_record): + self.bill = bill + self.order = order_record.order + self.recurring_price = order_record.recurring_price + self.recurring_period = order_record.recurring_period + self.description = order_record.description + + if self.order.starting_date >= self.bill.starting_date: + self.one_time_price = order_record.one_time_price + else: + self.one_time_price = 0 + + @property + def recurring_count(self): + # Compute billing delta. + billed_until = self.bill.ending_date + if self.order.ending_date != None and self.order.ending_date <= self.bill.ending_date: + billed_until = self.order.ending_date + + billed_from = self.bill.starting_date + if self.order.starting_date > self.bill.starting_date: + billed_from = self.order.starting_date + + if billed_from > billed_until: + # TODO: think about and check edge cases. This should not be + # possible. + raise Exception('Impossible billing delta!') + + billed_delta = billed_until - billed_from + + # TODO: refactor this thing? + # TODO: weekly + if self.recurring_period == RecurringPeriod.PER_YEAR: + # XXX: Should always be one => we do not bill for more than one year. + # TODO: check billed_delta is ~365 days. + return 1 + elif self.recurring_period == RecurringPeriod.PER_MONTH: + days = ceil(billed_delta / timedelta(days=1)) + + # Monthly bills always cover one single month. + if (self.bill.starting_date.year != self.bill.starting_date.year or + self.bill.starting_date.month != self.bill.ending_date.month): + raise Exception('Bill {} covers more than one month. Cannot bill PER_MONTH.'. + format(self.bill.uuid)) + + # XXX: minumal length of monthly order is to be enforced somewhere else. + (_, days_in_month) = monthrange( + self.bill.starting_date.year, + self.bill.starting_date.month) + return days / days_in_month + elif self.recurring_period == RecurringPeriod.PER_WEEK: + weeks = ceil(billed_delta / timedelta(week=1)) + return weeks + elif self.recurring_period == RecurringPeriod.PER_DAY: + days = ceil(billed_delta / timedelta(days=1)) + return days + elif self.recurring_period == RecurringPeriod.PER_HOUR: + hours = ceil(billed_delta / timedelta(hours=1)) + return hours + elif self.recurring_period == RecurringPeriod.PER_SECOND: + seconds = ceil(billed_delta / timedelta(seconds=1)) + return seconds + elif self.recurring_period == RecurringPeriod.ONE_TIME: + return 0 + else: + raise Exception('Unsupported recurring period: {}.'. + format(record.recurring_period)) + + @property + def vat_rate(self): + return Decimal(VATRate.get_for_country(self.bill.billing_address.country)) + + @property + def vat_amount(self): + return self.amount * self.vat_rate + + @property + def amount(self): + return Decimal(float(self.recurring_price) * self.recurring_count) + self.one_time_price + + @property + def total(self): + return self.amount + self.vat_amount + +### +# Orders. + +# Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating +# bills. Do **NOT** mutate then! +class Order(models.Model): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE, + editable=False) + billing_address = models.ForeignKey(BillingAddress, on_delete=models.CASCADE) + + # TODO: enforce ending_date - starting_date to be larger than recurring_period. + creation_date = models.DateTimeField(auto_now_add=True) + starting_date = models.DateTimeField() + ending_date = models.DateTimeField(blank=True, + null=True) + + bill = models.ManyToManyField(Bill, + editable=False, + blank=True) + + recurring_period = models.CharField(max_length=32, + choices = RecurringPeriod.choices, + default = RecurringPeriod.PER_MONTH) + + # Trigger initial bill generation at order creation. + def save(self, *args, **kwargs): + super(Order, self).save(*args, **kwargs) + Bill.generate_for(self.starting_date.year, self.starting_date.month, self.owner) + + @property + def records(self): + return OrderRecord.objects.filter(order=self) + + @property + def one_time_price(self): + return reduce(lambda acc, record: acc + record.one_time_price, self.records, 0) + + @property + def recurring_price(self): + return reduce(lambda acc, record: acc + record.recurring_price, self.records, 0) + + # Used by uncloud_pay tests. + @property + def bills(self): + return Bill.objects.filter(order=self) + + def add_record(self, one_time_price, recurring_price, description): + OrderRecord.objects.create(order=self, + one_time_price=one_time_price, + recurring_price=recurring_price, + description=description) + + + +class OrderRecord(models.Model): + """ + Order records store billing informations for products: the actual product + might be mutated and/or moved to another order but we do not want to loose + the details of old orders. + + Used as source of trust to dynamically generate bill entries. + """ + + order = models.ForeignKey(Order, on_delete=models.CASCADE) + one_time_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + recurring_price = models.DecimalField(default=0.0, + max_digits=AMOUNT_MAX_DIGITS, + decimal_places=AMOUNT_DECIMALS, + validators=[MinValueValidator(0)]) + + description = models.TextField() + + + @property + def recurring_period(self): + return self.order.recurring_period + + @property + def starting_date(self): + return self.order.starting_date + + @property + def ending_date(self): + return self.order.ending_date + + +### +# Products + +# Abstract (= no database representation) class used as parent for products +# (e.g. uncloud_vm.models.VMProduct). +class Product(UncloudModel): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + owner = models.ForeignKey(get_user_model(), + on_delete=models.CASCADE, + editable=False) + + description = "Generic Product" + + status = models.CharField(max_length=32, + choices=UncloudStatus.choices, + default=UncloudStatus.AWAITING_PAYMENT) + + order = models.ForeignKey(Order, + on_delete=models.CASCADE, + editable=False, + null=True) + + # Default period for all products + default_recurring_period = RecurringPeriod.PER_MONTH + + # Used to save records. + def save(self, *args, **kwargs): + # _state.adding is switched to false after super(...) call. + being_created = self._state.adding + + super(Product, self).save(*args, **kwargs) + + # Make sure we only create records on creation. + if being_created: + record = OrderRecord( + one_time_price=self.one_time_price, + recurring_price=self.recurring_price, + description=self.description) + self.order.orderrecord_set.add(record, bulk=False) + + @property + def recurring_price(self): + pass # To be implemented in child. + + @property + def one_time_price(self): + return 0 + + @property + def recurring_period(self): + return self.order.recurring_period + + @property + def billing_address(self): + return self.order.billing_address + + @staticmethod + def allowed_recurring_periods(): + return RecurringPeriod.choices + + class Meta: + abstract = True + + def discounted_price_by_period(self, requested_period): + """ + Each product has a standard recurring period for which + we define a pricing. I.e. VPN is usually year, VM is usually monthly. + + The user can opt-in to use a different period, which influences the price: + The longer a user commits, the higher the discount. + + Products can also be limited in the available periods. For instance + a VPN only makes sense to be bought for at least one day. + + Rules are as follows: + + given a standard recurring period of ..., changing to ... modifies price ... + + + # One month for free if buying / year, compared to a month: about 8.33% discount + per_year -> per_month -> /11 + per_month -> per_year -> *11 + + # Month has 30.42 days on average. About 7.9% discount to go monthly + per_month -> per_day -> /28 + per_day -> per_month -> *28 + + # Day has 24h, give one for free + per_day -> per_hour -> /23 + per_hour -> per_day -> /23 + + + Examples + + VPN @ 120CHF/y becomes + - 10.91 CHF/month (130.91 CHF/year) + - 0.39 CHF/day (142.21 CHF/year) + + VM @ 15 CHF/month becomes + - 165 CHF/month (13.75 CHF/month) + - 0.54 CHF/day (16.30 CHF/month) + + """ + + + if self.default_recurring_period == RecurringPeriod.PER_YEAR: + if requested_period == RecurringPeriod.PER_YEAR: + return self.recurring_price + if requested_period == RecurringPeriod.PER_MONTH: + return self.recurring_price/11. + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price/11./28. + + elif self.default_recurring_period == RecurringPeriod.PER_MONTH: + if requested_period == RecurringPeriod.PER_YEAR: + return self.recurring_price*11 + if requested_period == RecurringPeriod.PER_MONTH: + return self.recurring_price + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price/28. + + elif self.default_recurring_period == RecurringPeriod.PER_DAY: + if requested_period == RecurringPeriod.PER_YEAR: + return self.recurring_price*11*28 + if requested_period == RecurringPeriod.PER_MONTH: + return self.recurring_price*28 + if requested_period == RecurringPeriod.PER_DAY: + return self.recurring_price + else: + # FIXME: use the right type of exception here! + raise Exception("Did not implement the discounter for this case") diff --git a/uncloud_django_based/uncloud/uncloud_pay/serializers.py b/uncloud_django_based/uncloud/uncloud_pay/serializers.py new file mode 100644 index 0000000..1b5db24 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/serializers.py @@ -0,0 +1,89 @@ +from django.contrib.auth import get_user_model +from rest_framework import serializers +from .models import * + +### +# Payments and Payment Methods. + +class PaymentSerializer(serializers.ModelSerializer): + class Meta: + model = Payment + fields = '__all__' + +class PaymentMethodSerializer(serializers.ModelSerializer): + stripe_card_last4 = serializers.IntegerField() + + class Meta: + model = PaymentMethod + fields = ['uuid', 'source', 'description', 'primary', 'stripe_card_last4', 'active'] + +class UpdatePaymentMethodSerializer(serializers.ModelSerializer): + class Meta: + model = PaymentMethod + fields = ['description', 'primary'] + +class ChargePaymentMethodSerializer(serializers.Serializer): + amount = serializers.DecimalField(max_digits=10, decimal_places=2) + +class CreatePaymentMethodSerializer(serializers.ModelSerializer): + please_visit = serializers.CharField(read_only=True) + class Meta: + model = PaymentMethod + fields = ['source', 'description', 'primary', 'please_visit'] + +### +# Orders & Products. + +class OrderRecordSerializer(serializers.ModelSerializer): + class Meta: + model = OrderRecord + fields = ['one_time_price', 'recurring_price', 'description'] + + +class OrderSerializer(serializers.ModelSerializer): + records = OrderRecordSerializer(many=True, read_only=True) + class Meta: + model = Order + fields = ['uuid', 'creation_date', 'starting_date', 'ending_date', + 'bill', 'recurring_period', 'records', 'recurring_price', 'one_time_price'] + + +### +# Bills + +# TODO: remove magic numbers for decimal fields +class BillRecordSerializer(serializers.Serializer): + order = serializers.HyperlinkedRelatedField( + view_name='order-detail', + read_only=True) + description = serializers.CharField() + one_time_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + recurring_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + recurring_period = serializers.ChoiceField(choices=RecurringPeriod.choices) + recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + vat_rate = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + vat_amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) + +class BillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + fields = ['uuid', 'name', 'street', 'city', 'postal_code', 'country', 'vat_number'] + +class BillSerializer(serializers.ModelSerializer): + billing_address = BillingAddressSerializer(read_only=True) + records = BillRecordSerializer(many=True, read_only=True) + + class Meta: + model = Bill + fields = ['reference', 'owner', 'amount', 'vat_amount', 'total', + 'due_date', 'creation_date', 'starting_date', 'ending_date', + 'records', 'final', 'billing_address'] + +# We do not want users to mutate the country / VAT number of an address, as it +# will change VAT on existing bills. +class UpdateBillingAddressSerializer(serializers.ModelSerializer): + class Meta: + model = BillingAddress + fields = ['uuid', 'street', 'city', 'postal_code'] diff --git a/uncloud_django_based/uncloud/uncloud_pay/stripe.py b/uncloud_django_based/uncloud/uncloud_pay/stripe.py new file mode 100644 index 0000000..f23002b --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/stripe.py @@ -0,0 +1,114 @@ +import stripe +import stripe.error +import logging + +from django.core.exceptions import ObjectDoesNotExist +import uncloud_pay.models + +import uncloud.secrets + +# Static stripe configuration used below. +CURRENCY = 'chf' + +# README: We use the Payment Intent API as described on +# https://stripe.com/docs/payments/save-and-reuse + +# For internal use only. +stripe.api_key = uncloud.secrets.STRIPE_KEY + +# Helper (decorator) used to catch errors raised by stripe logic. +# Catch errors that should not be displayed to the end user, raise again. +def handle_stripe_error(f): + def handle_problems(*args, **kwargs): + response = { + 'paid': False, + 'response_object': None, + 'error': None + } + + common_message = "Currently it is not possible to make payments. Please try agin later." + try: + response_object = f(*args, **kwargs) + return response_object + except stripe.error.CardError as e: + # Since it's a decline, stripe.error.CardError will be caught + body = e.json_body + logging.error(str(e)) + + raise e # For error handling. + except stripe.error.RateLimitError: + logging.error("Too many requests made to the API too quickly.") + raise Exception(common_message) + except stripe.error.InvalidRequestError as e: + logging.error(str(e)) + raise Exception('Invalid parameters.') + except stripe.error.AuthenticationError as e: + # Authentication with Stripe's API failed + # (maybe you changed API keys recently) + logging.error(str(e)) + raise Exception(common_message) + except stripe.error.APIConnectionError as e: + logging.error(str(e)) + raise Exception(common_message) + except stripe.error.StripeError as e: + # XXX: maybe send email + logging.error(str(e)) + raise Exception(common_message) + except Exception as e: + # maybe send email + logging.error(str(e)) + raise Exception(common_message) + + return handle_problems + +# Actual Stripe logic. + +def public_api_key(): + return uncloud.secrets.STRIPE_PUBLIC_KEY + +def get_customer_id_for(user): + try: + # .get() raise if there is no matching entry. + return uncloud_pay.models.StripeCustomer.objects.get(owner=user).stripe_id + except ObjectDoesNotExist: + # No entry yet - making a new one. + try: + customer = create_customer(user.username, user.email) + uncloud_stripe_mapping = uncloud_pay.models.StripeCustomer.objects.create( + owner=user, stripe_id=customer.id) + return uncloud_stripe_mapping.stripe_id + except Exception as e: + return None + +@handle_stripe_error +def create_setup_intent(customer_id): + return stripe.SetupIntent.create(customer=customer_id) + +@handle_stripe_error +def get_setup_intent(setup_intent_id): + return stripe.SetupIntent.retrieve(setup_intent_id) + +def get_payment_method(payment_method_id): + return stripe.PaymentMethod.retrieve(payment_method_id) + +@handle_stripe_error +def charge_customer(amount, customer_id, card_id): + # Amount is in CHF but stripes requires smallest possible unit. + # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount + adjusted_amount = int(amount * 100) + return stripe.PaymentIntent.create( + amount=adjusted_amount, + currency=CURRENCY, + customer=customer_id, + payment_method=card_id, + off_session=True, + confirm=True, + ) + +@handle_stripe_error +def create_customer(name, email): + return stripe.Customer.create(name=name, email=email) + +@handle_stripe_error +def get_customer(customer_id): + return stripe.Customer.retrieve(customer_id) diff --git a/uncloud_pay/templates/uncloud_pay/bill.html.j2 b/uncloud_django_based/uncloud/uncloud_pay/templates/bill.html similarity index 96% rename from uncloud_pay/templates/uncloud_pay/bill.html.j2 rename to uncloud_django_based/uncloud/uncloud_pay/templates/bill.html index 7cf10f8..8f6c217 100644 --- a/uncloud_pay/templates/uncloud_pay/bill.html.j2 +++ b/uncloud_django_based/uncloud/uncloud_pay/templates/bill.html @@ -6,7 +6,7 @@ Icons, fonts, etc. are INLINED. This is rather ugly, but as the PDF generation is based on a local snapshot of the HTML file, URLs are - screwed if they are not absolute to the *local* filesystem. + screwed if they are not absolute. As this document is used ONLY for bills and ONLY for downloading, I decided that this is an acceptable uglyness. @@ -26,7 +26,7 @@ - {{ bill }} + Bill name + + +
+

Error

+

{{ error }}

+
+ + diff --git a/uncloud_django_based/uncloud/uncloud_pay/templates/stripe-payment.html.j2 b/uncloud_django_based/uncloud/uncloud_pay/templates/stripe-payment.html.j2 new file mode 100644 index 0000000..6c59740 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/templates/stripe-payment.html.j2 @@ -0,0 +1,76 @@ + + + + Stripe Card Registration + + + + + + + + +
+

Registering Stripe Credit Card

+ + + +
+
+ +
+ + +
+
+ + + + + + + + diff --git a/uncloud_django_based/uncloud/uncloud_pay/tests.py b/uncloud_django_based/uncloud/uncloud_pay/tests.py new file mode 100644 index 0000000..64f0442 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/tests.py @@ -0,0 +1,228 @@ +from django.test import TestCase +from django.contrib.auth import get_user_model +from datetime import datetime, date, timedelta + +from .models import * +from uncloud_service.models import GenericServiceProduct + +class BillingTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + self.billing_address = BillingAddress.objects.create( + owner=self.user, + street="unknown", + city="unknown", + postal_code="unknown") + + def test_basic_monthly_billing(self): + one_time_price = 10 + recurring_price = 20 + description = "Test Product 1" + + # Three months: full, full, partial. + starting_date = datetime.fromisoformat('2020-03-01') + ending_date = datetime.fromisoformat('2020-05-08') + + # Create order to be billed. + order = Order.objects.create( + owner=self.user, + starting_date=starting_date, + ending_date=ending_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address) + order.add_record(one_time_price, recurring_price, description) + + # Generate & check bill for first month: full recurring_price + setup. + first_month_bills = order.bills # Initial bill generated at order creation. + self.assertEqual(len(first_month_bills), 1) + self.assertEqual(first_month_bills[0].amount, one_time_price + recurring_price) + + # Generate & check bill for second month: full recurring_price. + second_month_bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(second_month_bills), 1) + self.assertEqual(second_month_bills[0].amount, recurring_price) + + # Generate & check bill for third and last month: partial recurring_price. + third_month_bills = Bill.generate_for(2020, 5, self.user) + self.assertEqual(len(third_month_bills), 1) + # 31 days in May. + self.assertEqual(float(third_month_bills[0].amount), + round((7/31) * recurring_price, AMOUNT_DECIMALS)) + + # Check that running Bill.generate_for() twice does not create duplicates. + self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) + + def test_basic_yearly_billing(self): + one_time_price = 10 + recurring_price = 150 + description = "Test Product 1" + + starting_date = datetime.fromisoformat('2020-03-31T08:05:23') + + # Create order to be billed. + order = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_YEAR, + billing_address=self.billing_address) + order.add_record(one_time_price, recurring_price, description) + + # Generate & check bill for first year: recurring_price + setup. + first_year_bills = order.bills # Initial bill generated at order creation. + self.assertEqual(len(first_year_bills), 1) + self.assertEqual(first_year_bills[0].starting_date.date(), + date.fromisoformat('2020-03-31')) + self.assertEqual(first_year_bills[0].ending_date.date(), + date.fromisoformat('2021-03-30')) + self.assertEqual(first_year_bills[0].amount, + recurring_price + one_time_price) + + # Generate & check bill for second year: recurring_price. + second_year_bills = Bill.generate_for(2021, 3, self.user) + self.assertEqual(len(second_year_bills), 1) + self.assertEqual(second_year_bills[0].starting_date.date(), + date.fromisoformat('2021-03-31')) + self.assertEqual(second_year_bills[0].ending_date.date(), + date.fromisoformat('2022-03-30')) + self.assertEqual(second_year_bills[0].amount, recurring_price) + + # Check that running Bill.generate_for() twice does not create duplicates. + self.assertEqual(len(Bill.generate_for(2020, 3, self.user)), 0) + self.assertEqual(len(Bill.generate_for(2020, 4, self.user)), 0) + self.assertEqual(len(Bill.generate_for(2020, 2, self.user)), 0) + self.assertEqual(len(Bill.generate_for(2021, 3, self.user)), 0) + + def test_basic_hourly_billing(self): + one_time_price = 10 + recurring_price = 1.4 + description = "Test Product 1" + + starting_date = datetime.fromisoformat('2020-03-31T08:05:23') + ending_date = datetime.fromisoformat('2020-04-01T11:13:32') + + # Create order to be billed. + order = Order.objects.create( + owner=self.user, + starting_date=starting_date, + ending_date=ending_date, + recurring_period=RecurringPeriod.PER_HOUR, + billing_address=self.billing_address) + order.add_record(one_time_price, recurring_price, description) + + # Generate & check bill for first month: recurring_price + setup. + first_month_bills = order.bills + self.assertEqual(len(first_month_bills), 1) + self.assertEqual(float(first_month_bills[0].amount), + round(16 * recurring_price, AMOUNT_DECIMALS) + one_time_price) + + # Generate & check bill for first month: recurring_price. + second_month_bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(second_month_bills), 1) + self.assertEqual(float(second_month_bills[0].amount), + round(12 * recurring_price, AMOUNT_DECIMALS)) + +class ProductActivationTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + self.billing_address = BillingAddress.objects.create( + owner=self.user, + street="unknown", + city="unknown", + postal_code="unknown") + + def test_product_activation(self): + starting_date = datetime.fromisoformat('2020-03-01') + + order = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address) + + product = GenericServiceProduct( + custom_description="Test product", + custom_one_time_price=0, + custom_recurring_price=20, + owner=self.user, + order=order) + product.save() + + # XXX: to be automated. + order.add_record(product.one_time_price, product.recurring_price, product.description) + + # Validate initial state: must be awaiting payment. + self.assertEqual(product.status, UncloudStatus.AWAITING_PAYMENT) + + # Pay initial bill, check that product is activated. + amount = product.order.bills[0].amount + payment = Payment(owner=self.user, amount=amount) + payment.save() + self.assertEqual( + GenericServiceProduct.objects.get(uuid=product.uuid).status, + UncloudStatus.PENDING + ) + +class BillingAddressTestCase(TestCase): + def setUp(self): + self.user = get_user_model().objects.create( + username='jdoe', + email='john.doe@domain.tld') + + self.billing_address_01 = BillingAddress.objects.create( + owner=self.user, + street="unknown1", + city="unknown1", + postal_code="unknown1", + country="CH") + + self.billing_address_02 = BillingAddress.objects.create( + owner=self.user, + street="unknown2", + city="unknown2", + postal_code="unknown2", + country="CH") + + def test_billing_with_single_address(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + + # We need a single bill since we work with a single address. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 1) + + def test_billing_with_multiple_addresses(self): + # Create new orders somewhere in the past so that we do not encounter + # auto-created initial bills. + starting_date = datetime.fromisoformat('2020-03-01') + + order_01 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_01) + order_02 = Order.objects.create( + owner=self.user, + starting_date=starting_date, + recurring_period=RecurringPeriod.PER_MONTH, + billing_address=self.billing_address_02) + + # We need different bills since we work with different addresses. + bills = Bill.generate_for(2020, 4, self.user) + self.assertEqual(len(bills), 2) diff --git a/uncloud_django_based/uncloud/uncloud_pay/views.py b/uncloud_django_based/uncloud/uncloud_pay/views.py new file mode 100644 index 0000000..aaf90e2 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_pay/views.py @@ -0,0 +1,294 @@ +from django.shortcuts import render +from django.db import transaction +from django.contrib.auth import get_user_model +from rest_framework import viewsets, mixins, permissions, status, views +from rest_framework.renderers import TemplateHTMLRenderer +from rest_framework.response import Response +from rest_framework.decorators import action +from rest_framework.reverse import reverse +from rest_framework.decorators import renderer_classes +from vat_validator import validate_vat, vies +from vat_validator.countries import EU_COUNTRY_CODES + +import json +import logging + +from .models import * +from .serializers import * +from datetime import datetime +from vat_validator import sanitize_vat +import uncloud_pay.stripe as uncloud_stripe + +logger = logging.getLogger(__name__) + +### +# Payments and Payment Methods. + +class PaymentViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = PaymentSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Payment.objects.filter(owner=self.request.user) + +class OrderViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = OrderSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Order.objects.filter(owner=self.request.user) + +class PaymentMethodViewSet(viewsets.ModelViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_serializer_class(self): + if self.action == 'create': + return CreatePaymentMethodSerializer + elif self.action == 'update': + return UpdatePaymentMethodSerializer + elif self.action == 'charge': + return ChargePaymentMethodSerializer + else: + return PaymentMethodSerializer + + def get_queryset(self): + return PaymentMethod.objects.filter(owner=self.request.user) + + # XXX: Handling of errors is far from great down there. + @transaction.atomic + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + # Set newly created method as primary if no other method is. + if PaymentMethod.get_primary_for(request.user) == None: + serializer.validated_data['primary'] = True + + if serializer.validated_data['source'] == "stripe": + # Retrieve Stripe customer ID for user. + customer_id = uncloud_stripe.get_customer_id_for(request.user) + if customer_id == None: + return Response( + {'error': 'Could not resolve customer stripe ID.'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + try: + setup_intent = uncloud_stripe.create_setup_intent(customer_id) + except Exception as e: + return Response({'error': str(e)}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + payment_method = PaymentMethod.objects.create( + owner=request.user, + stripe_setup_intent_id=setup_intent.id, + **serializer.validated_data) + + # TODO: find a way to use reverse properly: + # https://www.django-rest-framework.org/api-guide/reverse/ + path = "payment-method/{}/register-stripe-cc".format( + payment_method.uuid) + stripe_registration_url = reverse('api-root', request=request) + path + return Response({'please_visit': stripe_registration_url}) + else: + serializer.save(owner=request.user, **serializer.validated_data) + return Response(serializer.data) + + @action(detail=True, methods=['post']) + def charge(self, request, pk=None): + payment_method = self.get_object() + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + amount = serializer.validated_data['amount'] + try: + payment = payment_method.charge(amount) + output_serializer = PaymentSerializer(payment) + return Response(output_serializer.data) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + @action(detail=True, methods=['get'], url_path='register-stripe-cc', renderer_classes=[TemplateHTMLRenderer]) + def register_stripe_cc(self, request, pk=None): + payment_method = self.get_object() + + if payment_method.source != 'stripe': + return Response( + {'error': 'This is not a Stripe-based payment method.'}, + template_name='error.html.j2') + + if payment_method.active: + return Response( + {'error': 'This payment method is already active'}, + template_name='error.html.j2') + + try: + setup_intent = uncloud_stripe.get_setup_intent( + payment_method.stripe_setup_intent_id) + except Exception as e: + return Response( + {'error': str(e)}, + template_name='error.html.j2') + + # TODO: find a way to use reverse properly: + # https://www.django-rest-framework.org/api-guide/reverse/ + callback_path= "payment-method/{}/activate-stripe-cc/".format( + payment_method.uuid) + callback = reverse('api-root', request=request) + callback_path + + # Render stripe card registration form. + template_args = { + 'client_secret': setup_intent.client_secret, + 'stripe_pk': uncloud_stripe.public_api_key, + 'callback': callback + } + return Response(template_args, template_name='stripe-payment.html.j2') + + @action(detail=True, methods=['post'], url_path='activate-stripe-cc') + def activate_stripe_cc(self, request, pk=None): + payment_method = self.get_object() + try: + setup_intent = uncloud_stripe.get_setup_intent( + payment_method.stripe_setup_intent_id) + except Exception as e: + return Response({'error': str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + # Card had been registered, fetching payment method. + print(setup_intent) + if setup_intent.payment_method: + payment_method.stripe_payment_method_id = setup_intent.payment_method + payment_method.save() + + return Response({ + 'uuid': payment_method.uuid, + 'activated': payment_method.active}) + else: + error = 'Could not fetch payment method from stripe. Please try again.' + return Response({'error': error}) + + @action(detail=True, methods=['post'], url_path='set-as-primary') + def set_as_primary(self, request, pk=None): + payment_method = self.get_object() + payment_method.set_as_primary_for(request.user) + + serializer = self.get_serializer(payment_method) + return Response(serializer.data) + +### +# Bills and Orders. + +class BillViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = BillSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Bill.objects.filter(owner=self.request.user) + + def unpaid(self, request): + return Bill.objects.filter(owner=self.request.user, paid=False) + + +class OrderViewSet(viewsets.ReadOnlyModelViewSet): + serializer_class = OrderSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Order.objects.filter(owner=self.request.user) + +class BillingAddressViewSet(mixins.CreateModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + mixins.ListModelMixin, + viewsets.GenericViewSet): + permission_classes = [permissions.IsAuthenticated] + + def get_serializer_class(self): + if self.action == 'update': + return UpdateBillingAddressSerializer + else: + return BillingAddressSerializer + + def get_queryset(self): + return self.request.user.billingaddress_set.all() + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + + # Validate VAT numbers. + country = serializer.validated_data["country"] + vat_number = serializer.validated_data["vat_number"] + + # We ignore empty VAT numbers. + if vat_number != "": + if not validate_vat(country, vat_number): + return Response( + {'error': 'Malformed VAT number.'}, + status=status.HTTP_400_BAD_REQUEST) + elif country in EU_COUNTRY_CODES: + # XXX: make a synchroneous call to a third patry API here might not be a good idea.. + try: + vies_state = vies.check_vat(country, vat_number) + if not vies_state.valid: + return Response( + {'error': 'European VAT number does not exist in VIES.'}, + status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + logger.warning(e) + return Response( + {'error': 'Could not validate EU VAT number against VIES. Try again later..'}, + status=status.HTTP_500_INTERNAL_SERVER_ERROR) + + + serializer.save(owner=request.user) + return Response(serializer.data) + +### +# Old admin stuff. + +class AdminPaymentViewSet(viewsets.ModelViewSet): + serializer_class = PaymentSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Payment.objects.all() + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(timestamp=datetime.now()) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + +class AdminBillViewSet(viewsets.ModelViewSet): + serializer_class = BillSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Bill.objects.all() + + def unpaid(self, request): + return Bill.objects.filter(owner=self.request.user, paid=False) + + def create(self, request): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save(creation_date=datetime.now()) + + headers = self.get_success_headers(serializer.data) + return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) + +class AdminOrderViewSet(viewsets.ModelViewSet): + serializer_class = OrderSerializer + permission_classes = [permissions.IsAuthenticated] + + def get_queryset(self): + return Order.objects.all() + +# PDF tests +from django.views.generic import TemplateView +from hardcopy.views import PDFViewMixin, PNGViewMixin + +class MyPDFView(PDFViewMixin, TemplateView): + template_name = "bill.html" + # def get_filename(self): + # return "my_file_{}.pdf".format(now().strftime('Y-m-d')) diff --git a/matrixhosting/__init__.py b/uncloud_django_based/uncloud/uncloud_service/__init__.py similarity index 100% rename from matrixhosting/__init__.py rename to uncloud_django_based/uncloud/uncloud_service/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_service/admin.py b/uncloud_django_based/uncloud/uncloud_service/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_service/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/uncloud_service/apps.py b/uncloud_django_based/uncloud/uncloud_service/apps.py similarity index 71% rename from uncloud_service/apps.py rename to uncloud_django_based/uncloud/uncloud_service/apps.py index 190bd35..184e181 100644 --- a/uncloud_service/apps.py +++ b/uncloud_django_based/uncloud/uncloud_service/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class UngleichServiceConfig(AppConfig): - name = 'uncloud_service' + name = 'ungleich_service' diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/0001_initial.py b/uncloud_django_based/uncloud/uncloud_service/migrations/0001_initial.py new file mode 100644 index 0000000..f0f5535 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_service/migrations/0001_initial.py @@ -0,0 +1,36 @@ +# Generated by Django 3.0.5 on 2020-04-13 09:38 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('uncloud_pay', '0005_auto_20200413_0924'), + ('uncloud_vm', '0010_auto_20200413_0924'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='MatrixServiceProduct', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32)), + ('domain', models.CharField(default='domain.tld', max_length=255)), + ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py new file mode 100644 index 0000000..717f163 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_service/migrations/0002_auto_20200418_0641.py @@ -0,0 +1,41 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.conf import settings +import django.contrib.postgres.fields.jsonb +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('uncloud_pay', '0005_auto_20200413_0924'), + ('uncloud_service', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='matrixserviceproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32), + ), + migrations.CreateModel( + name='GenericServiceProduct', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32)), + ('custom_description', models.TextField()), + ('custom_recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('custom_one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), + ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/matrixhosting/migrations/__init__.py b/uncloud_django_based/uncloud/uncloud_service/migrations/__init__.py similarity index 100% rename from matrixhosting/migrations/__init__.py rename to uncloud_django_based/uncloud/uncloud_service/migrations/__init__.py diff --git a/uncloud_service/models.py b/uncloud_django_based/uncloud/uncloud_service/models.py similarity index 75% rename from uncloud_service/models.py rename to uncloud_django_based/uncloud/uncloud_service/models.py index a37e42b..35a479e 100644 --- a/uncloud_service/models.py +++ b/uncloud_django_based/uncloud/uncloud_service/models.py @@ -1,9 +1,11 @@ +import uuid + from django.db import models from uncloud_pay.models import Product, RecurringPeriod, AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS from uncloud_vm.models import VMProduct, VMDiskImageProduct from django.core.validators import MinValueValidator -class MatrixServiceProduct(models.Model): +class MatrixServiceProduct(Product): monthly_managment_fee = 20 description = "Managed Matrix HomeServer" @@ -15,26 +17,25 @@ class MatrixServiceProduct(models.Model): domain = models.CharField(max_length=255, default='domain.tld') # Default recurring price is PER_MONT, see Product class. - # def recurring_price(self, recurring_period=RecurringPeriod.PER_30D): - # return self.monthly_managment_fee + def recurring_price(self, recurring_period=RecurringPeriod.PER_MONTH): + return self.monthly_managment_fee @staticmethod def base_image(): # TODO: find a way to safely reference debian 10 image. -#e return VMDiskImageProduct.objects.get(uuid="93e564c5-adb3-4741-941f-718f76075f02") - return False + return VMDiskImageProduct.objects.get(uuid="93e564c5-adb3-4741-941f-718f76075f02") - # @staticmethod - # def allowed_recurring_periods(): - # return list(filter( - # lambda pair: pair[0] in [RecurringPeriod.PER_30D], - # RecurringPeriod.choices)) + @staticmethod + def allowed_recurring_periods(): + return list(filter( + lambda pair: pair[0] in [RecurringPeriod.PER_MONTH], + RecurringPeriod.choices)) @property def one_time_price(self): return 30 -class GenericServiceProduct(models.Model): +class GenericServiceProduct(Product): custom_description = models.TextField() custom_recurring_price = models.DecimalField(default=0.0, max_digits=AMOUNT_MAX_DIGITS, diff --git a/uncloud_service/serializers.py b/uncloud_django_based/uncloud/uncloud_service/serializers.py similarity index 80% rename from uncloud_service/serializers.py rename to uncloud_django_based/uncloud/uncloud_service/serializers.py index bc6d753..6666a15 100644 --- a/uncloud_service/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_service/serializers.py @@ -12,13 +12,13 @@ class MatrixServiceProductSerializer(serializers.ModelSerializer): class Meta: model = MatrixServiceProduct - fields = ['order', 'owner', 'status', 'vm', 'domain', + fields = ['uuid', 'order', 'owner', 'status', 'vm', 'domain', 'recurring_period'] - read_only_fields = ['order', 'owner', 'status'] + read_only_fields = ['uuid', 'order', 'owner', 'status'] class OrderMatrixServiceProductSerializer(MatrixServiceProductSerializer): - # recurring_period = serializers.ChoiceField( - # choices=MatrixServiceProduct.allowed_recurring_periods()) + recurring_period = serializers.ChoiceField( + choices=MatrixServiceProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(OrderMatrixServiceProductSerializer, self).__init__(*args, **kwargs) @@ -37,13 +37,13 @@ class OrderMatrixServiceProductSerializer(MatrixServiceProductSerializer): class GenericServiceProductSerializer(serializers.ModelSerializer): class Meta: model = GenericServiceProduct - fields = ['order', 'owner', 'status', 'custom_recurring_price', + fields = ['uuid', 'order', 'owner', 'status', 'custom_recurring_price', 'custom_description', 'custom_one_time_price'] - read_only_fields = [ 'owner', 'status'] + read_only_fields = ['uuid', 'order', 'owner', 'status'] class OrderGenericServiceProductSerializer(GenericServiceProductSerializer): - # recurring_period = serializers.ChoiceField( - # choices=GenericServiceProduct.allowed_recurring_periods()) + recurring_period = serializers.ChoiceField( + choices=GenericServiceProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(OrderGenericServiceProductSerializer, self).__init__(*args, **kwargs) diff --git a/uncloud_storage/tests.py b/uncloud_django_based/uncloud/uncloud_service/tests.py similarity index 100% rename from uncloud_storage/tests.py rename to uncloud_django_based/uncloud/uncloud_service/tests.py diff --git a/uncloud_service/views.py b/uncloud_django_based/uncloud/uncloud_service/views.py similarity index 100% rename from uncloud_service/views.py rename to uncloud_django_based/uncloud/uncloud_service/views.py diff --git a/opennebula/__init__.py b/uncloud_django_based/uncloud/uncloud_storage/__init__.py similarity index 100% rename from opennebula/__init__.py rename to uncloud_django_based/uncloud/uncloud_storage/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/admin.py b/uncloud_django_based/uncloud/uncloud_storage/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_storage/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/uncloud_storage/apps.py b/uncloud_django_based/uncloud/uncloud_storage/apps.py similarity index 100% rename from uncloud_storage/apps.py rename to uncloud_django_based/uncloud/uncloud_storage/apps.py diff --git a/uncloud_storage/models.py b/uncloud_django_based/uncloud/uncloud_storage/models.py similarity index 100% rename from uncloud_storage/models.py rename to uncloud_django_based/uncloud/uncloud_storage/models.py diff --git a/uncloud_django_based/uncloud/uncloud_storage/tests.py b/uncloud_django_based/uncloud/uncloud_storage/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_storage/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/uncloud_storage/views.py b/uncloud_django_based/uncloud/uncloud_storage/views.py similarity index 100% rename from uncloud_storage/views.py rename to uncloud_django_based/uncloud/uncloud_storage/views.py diff --git a/opennebula/migrations/__init__.py b/uncloud_django_based/uncloud/uncloud_vm/__init__.py similarity index 100% rename from opennebula/migrations/__init__.py rename to uncloud_django_based/uncloud/uncloud_vm/__init__.py diff --git a/uncloud_django_based/uncloud/uncloud_vm/admin.py b/uncloud_django_based/uncloud/uncloud_vm/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/uncloud_vm/apps.py b/uncloud_django_based/uncloud/uncloud_vm/apps.py similarity index 100% rename from uncloud_vm/apps.py rename to uncloud_django_based/uncloud/uncloud_vm/apps.py diff --git a/uncloud_vm/management/commands/vm.py b/uncloud_django_based/uncloud/uncloud_vm/management/commands/vm.py similarity index 100% rename from uncloud_vm/management/commands/vm.py rename to uncloud_django_based/uncloud/uncloud_vm/management/commands/vm.py diff --git a/uncloud_vm/migrations/0001_initial.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0001_initial.py similarity index 53% rename from uncloud_vm/migrations/0001_initial.py rename to uncloud_django_based/uncloud/uncloud_vm/migrations/0001_initial.py index 4ec089a..f9f40d8 100644 --- a/uncloud_vm/migrations/0001_initial.py +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0001_initial.py @@ -1,8 +1,9 @@ -# Generated by Django 3.1 on 2020-12-13 10:38 +# Generated by Django 3.0.3 on 2020-03-05 10:34 from django.conf import settings from django.db import migrations, models import django.db.models.deletion +import uuid class Migration(migrations.Migration): @@ -10,76 +11,80 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('uncloud_pay', '0001_initial'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ - migrations.CreateModel( - name='VMCluster', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extra_data', models.JSONField(blank=True, editable=False, null=True)), - ('name', models.CharField(max_length=128, unique=True)), - ], - options={ - 'abstract': False, - }, - ), migrations.CreateModel( name='VMDiskImageProduct', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extra_data', models.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('name', models.CharField(max_length=256)), ('is_os_image', models.BooleanField(default=False)), - ('is_public', models.BooleanField(default=False, editable=False)), + ('is_public', models.BooleanField(default=False)), ('size_in_gb', models.FloatField(blank=True, null=True)), ('import_url', models.URLField(blank=True, null=True)), ('image_source', models.CharField(max_length=128, null=True)), ('image_source_type', models.CharField(max_length=128, null=True)), - ('storage_class', models.CharField(choices=[('HDD', 'HDD'), ('SSD', 'SSD')], default='SSD', max_length=32)), - ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32)), + ('storage_class', models.CharField(choices=[('hdd', 'HDD'), ('ssd', 'SSD')], default='ssd', max_length=32)), + ('status', models.CharField(choices=[('pending', 'Pending'), ('creating', 'Creating'), ('active', 'Active'), ('disabled', 'Disabled'), ('unusable', 'Unusable'), ('deleted', 'Deleted')], default='pending', max_length=32)), ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], - options={ - 'abstract': False, - }, ), migrations.CreateModel( name='VMHost', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('extra_data', models.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('hostname', models.CharField(max_length=253, unique=True)), ('physical_cores', models.IntegerField(default=0)), ('usable_cores', models.IntegerField(default=0)), ('usable_ram_in_gb', models.FloatField(default=0)), - ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32)), - ('vmcluster', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmcluster')), + ('status', models.CharField(choices=[('pending', 'Pending'), ('creating', 'Creating'), ('active', 'Active'), ('disabled', 'Disabled'), ('unusable', 'Unusable'), ('deleted', 'Deleted')], default='pending', max_length=32)), + ('vms', models.TextField(default='')), + ], + ), + migrations.CreateModel( + name='VMProduct', + fields=[ + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted')], default='PENDING', max_length=32)), + ('name', models.CharField(max_length=32)), + ('cores', models.IntegerField()), + ('ram_in_gb', models.FloatField()), + ('vmid', models.IntegerField(null=True)), + ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('vmhost', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMHost')), ], options={ 'abstract': False, }, ), migrations.CreateModel( - name='VMProduct', + name='VMWithOSProduct', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(blank=True, max_length=32, null=True)), - ('cores', models.IntegerField()), - ('ram_in_gb', models.FloatField()), - ('vmcluster', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmcluster')), - ('vmhost', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmhost')), + ('vmproduct_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='uncloud_vm.VMProduct')), ], + options={ + 'abstract': False, + }, + bases=('uncloud_vm.vmproduct',), ), migrations.CreateModel( name='VMSnapshotProduct', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted')], default='PENDING', max_length=32)), ('gb_ssd', models.FloatField(editable=False)), ('gb_hdd', models.FloatField(editable=False)), - ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snapshots', to='uncloud_vm.vmproduct')), + ('order', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')), ], + options={ + 'abstract': False, + }, ), migrations.CreateModel( name='VMNetworkCard', @@ -87,25 +92,17 @@ class Migration(migrations.Migration): ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('mac_address', models.BigIntegerField()), ('ip_address', models.GenericIPAddressField(blank=True, null=True)), - ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmproduct')), + ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')), ], ), migrations.CreateModel( name='VMDiskProduct', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), ('size_in_gb', models.FloatField(blank=True)), - ('disk_type', models.CharField(choices=[('ceph/ssd', 'Ceph Ssd'), ('ceph/hdd', 'Ceph Hdd'), ('local/ssd', 'Local Ssd'), ('local/hdd', 'Local Hdd')], default='ceph/ssd', max_length=20)), - ('image', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmdiskimageproduct')), - ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmproduct')), + ('image', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMDiskImageProduct')), + ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ('vm', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct')), ], ), - migrations.CreateModel( - name='VMWithOSProduct', - fields=[ - ('vmproduct_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='uncloud_vm.vmproduct')), - ('primary_disk', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.vmdiskproduct')), - ], - bases=('uncloud_vm.vmproduct',), - ), ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0002_auto_20200305_1321.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0002_auto_20200305_1321.py new file mode 100644 index 0000000..2711b33 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0002_auto_20200305_1321.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-03-05 13:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='vmdiskimageproduct', + name='storage_class', + field=models.CharField(choices=[('HDD', 'HDD'), ('SSD', 'SSD')], default='SSD', max_length=32), + ), + migrations.AlterField( + model_name='vmproduct', + name='name', + field=models.CharField(blank=True, max_length=32, null=True), + ), + ] diff --git a/matrixhosting/migrations/0008_remove_vminstance_ip.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0003_remove_vmhost_vms.py similarity index 52% rename from matrixhosting/migrations/0008_remove_vminstance_ip.py rename to uncloud_django_based/uncloud/uncloud_vm/migrations/0003_remove_vmhost_vms.py index 054359b..70ee863 100644 --- a/matrixhosting/migrations/0008_remove_vminstance_ip.py +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0003_remove_vmhost_vms.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.4 on 2021-07-10 14:29 +# Generated by Django 3.0.3 on 2020-03-05 13:58 from django.db import migrations @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('matrixhosting', '0007_vminstance'), + ('uncloud_vm', '0002_auto_20200305_1321'), ] operations = [ migrations.RemoveField( - model_name='vminstance', - name='ip', + model_name='vmhost', + name='vms', ), ] diff --git a/uncloud_pay/migrations/0022_remove_order_status.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py similarity index 51% rename from uncloud_pay/migrations/0022_remove_order_status.py rename to uncloud_django_based/uncloud/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py index 2b51be8..5f44b57 100644 --- a/uncloud_pay/migrations/0022_remove_order_status.py +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_remove_vmproduct_vmid.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.4 on 2021-07-11 08:32 +# Generated by Django 3.0.3 on 2020-03-17 14:40 from django.db import migrations @@ -6,12 +6,12 @@ from django.db import migrations class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0021_auto_20210709_0914'), + ('uncloud_vm', '0003_remove_vmhost_vms'), ] operations = [ migrations.RemoveField( - model_name='order', - name='status', + model_name='vmproduct', + name='vmid', ), ] diff --git a/uncloud_pay/migrations/0019_order_pricing_plan.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py similarity index 54% rename from uncloud_pay/migrations/0019_order_pricing_plan.py rename to uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py index 5392ce6..c78acc1 100644 --- a/uncloud_pay/migrations/0019_order_pricing_plan.py +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0004_vmproduct_primary_disk.py @@ -1,4 +1,4 @@ -# Generated by Django 3.2.4 on 2021-07-06 19:18 +# Generated by Django 3.0.3 on 2020-03-09 12:43 from django.db import migrations, models import django.db.models.deletion @@ -7,13 +7,13 @@ import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ - ('uncloud_pay', '0018_payment'), + ('uncloud_vm', '0004_remove_vmproduct_vmid'), ] operations = [ migrations.AddField( - model_name='order', - name='pricing_plan', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.pricingplan'), + model_name='vmproduct', + name='primary_disk', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMDiskProduct'), ), ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200309_1258.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200309_1258.py new file mode 100644 index 0000000..0356558 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200309_1258.py @@ -0,0 +1,25 @@ +# Generated by Django 3.0.3 on 2020-03-09 12:58 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_pay', '0001_initial'), + ('uncloud_vm', '0004_vmproduct_primary_disk'), + ] + + operations = [ + migrations.AddField( + model_name='vmdiskproduct', + name='order', + field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.Order'), + ), + migrations.AddField( + model_name='vmdiskproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted')], default='PENDING', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200321_1058.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200321_1058.py new file mode 100644 index 0000000..40eface --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0005_auto_20200321_1058.py @@ -0,0 +1,50 @@ +# Generated by Django 3.0.3 on 2020-03-21 10:58 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0005_auto_20200309_1258'), + ] + + operations = [ + migrations.AddField( + model_name='vmdiskimageproduct', + name='extra_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='vmdiskproduct', + name='extra_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='vmhost', + name='extra_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='vmproduct', + name='extra_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True), + ), + migrations.AddField( + model_name='vmsnapshotproduct', + name='extra_data', + field=django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True), + ), + migrations.AlterField( + model_name='vmdiskproduct', + name='vm', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='disks', to='uncloud_vm.VMProduct'), + ), + migrations.AlterField( + model_name='vmsnapshotproduct', + name='vm', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='snapshots', to='uncloud_vm.VMProduct'), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0006_auto_20200322_1758.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0006_auto_20200322_1758.py new file mode 100644 index 0000000..7726c9b --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0006_auto_20200322_1758.py @@ -0,0 +1,57 @@ +# Generated by Django 3.0.3 on 2020-03-22 17:58 + +import django.contrib.postgres.fields.jsonb +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0005_auto_20200321_1058'), + ] + + operations = [ + migrations.CreateModel( + name='VMCluster', + fields=[ + ('extra_data', django.contrib.postgres.fields.jsonb.JSONField(blank=True, editable=False, null=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=128, unique=True)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AlterField( + model_name='vmdiskimageproduct', + name='is_public', + field=models.BooleanField(default=False, editable=False), + ), + migrations.AlterField( + model_name='vmdiskimageproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmhost', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmsnapshotproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('ACTIVE', 'Active'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AddField( + model_name='vmproduct', + name='vmcluster', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMCluster'), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0007_vmhost_vmcluster.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0007_vmhost_vmcluster.py new file mode 100644 index 0000000..6766dd7 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0007_vmhost_vmcluster.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.3 on 2020-03-22 18:09 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0006_auto_20200322_1758'), + ] + + operations = [ + migrations.AddField( + model_name='vmhost', + name='vmcluster', + field=models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMCluster'), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0008_auto_20200403_1727.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0008_auto_20200403_1727.py new file mode 100644 index 0000000..5f4b494 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0008_auto_20200403_1727.py @@ -0,0 +1,33 @@ +# Generated by Django 3.0.5 on 2020-04-03 17:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0007_vmhost_vmcluster'), + ] + + operations = [ + migrations.AlterField( + model_name='vmdiskimageproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmhost', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmsnapshotproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py new file mode 100644 index 0000000..641f849 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_auto_20200417_0551.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.5 on 2020-04-17 05:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0008_auto_20200403_1727'), + ] + + operations = [ + migrations.AlterField( + model_name='vmproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32), + ), + migrations.AlterField( + model_name='vmsnapshotproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_merge_20200413_0857.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_merge_20200413_0857.py new file mode 100644 index 0000000..2a9d70c --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0009_merge_20200413_0857.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.5 on 2020-04-13 08:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0004_remove_vmproduct_vmid'), + ('uncloud_vm', '0008_auto_20200403_1727'), + ] + + operations = [ + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0010_auto_20200413_0924.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0010_auto_20200413_0924.py new file mode 100644 index 0000000..8883277 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0010_auto_20200413_0924.py @@ -0,0 +1,24 @@ +# Generated by Django 3.0.5 on 2020-04-13 09:24 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0009_merge_20200413_0857'), + ] + + operations = [ + migrations.AlterField( + model_name='vmdiskproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='PENDING', max_length=32), + ), + migrations.AlterField( + model_name='vmdiskproduct', + name='vm', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_vm.VMProduct'), + ), + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py new file mode 100644 index 0000000..c0d4c32 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0011_merge_20200418_0641.py @@ -0,0 +1,14 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0009_auto_20200417_0551'), + ('uncloud_vm', '0010_auto_20200413_0924'), + ] + + operations = [ + ] diff --git a/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py new file mode 100644 index 0000000..9af8649 --- /dev/null +++ b/uncloud_django_based/uncloud/uncloud_vm/migrations/0012_auto_20200418_0641.py @@ -0,0 +1,18 @@ +# Generated by Django 3.0.5 on 2020-04-18 06:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('uncloud_vm', '0011_merge_20200418_0641'), + ] + + operations = [ + migrations.AlterField( + model_name='vmdiskproduct', + name='status', + field=models.CharField(choices=[('PENDING', 'Pending'), ('AWAITING_PAYMENT', 'Awaiting payment'), ('BEING_CREATED', 'Being created'), ('SCHEDULED', 'Scheduled'), ('ACTIVE', 'Active'), ('MODIFYING', 'Modifying'), ('DELETED', 'Deleted'), ('DISABLED', 'Disabled'), ('UNUSABLE', 'Unusable')], default='AWAITING_PAYMENT', max_length=32), + ), + ] diff --git a/uncloud/migrations/__init__.py b/uncloud_django_based/uncloud/uncloud_vm/migrations/__init__.py similarity index 100% rename from uncloud/migrations/__init__.py rename to uncloud_django_based/uncloud/uncloud_vm/migrations/__init__.py diff --git a/uncloud_vm/models.py b/uncloud_django_based/uncloud/uncloud_vm/models.py similarity index 71% rename from uncloud_vm/models.py rename to uncloud_django_based/uncloud/uncloud_vm/models.py index c605779..5dacdbe 100644 --- a/uncloud_vm/models.py +++ b/uncloud_django_based/uncloud/uncloud_vm/models.py @@ -1,3 +1,5 @@ +import uuid + from django.db import models from django.contrib.auth import get_user_model @@ -8,9 +10,13 @@ import uncloud_pay.models as pay_models import uncloud_storage.models class VMCluster(UncloudModel): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=128, unique=True) + class VMHost(UncloudModel): + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + # 253 is the maximum DNS name length hostname = models.CharField(max_length=253, unique=True) @@ -48,8 +54,7 @@ class VMHost(UncloudModel): return self.usable_cores - sum([vm.cores for vm in self.vms ]) - -class VMProduct(models.Model): +class VMProduct(Product): vmhost = models.ForeignKey( VMHost, on_delete=models.CASCADE, editable=False, blank=True, null=True ) @@ -58,33 +63,38 @@ class VMProduct(models.Model): VMCluster, on_delete=models.CASCADE, editable=False, blank=True, null=True ) + # VM-specific. The name is only intended for customers: it's a pain to + # remember IDs (speaking from experience as ungleich customer)! name = models.CharField(max_length=32, blank=True, null=True) cores = models.IntegerField() ram_in_gb = models.FloatField() + primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) + + # Default recurring price is PER_MONTH, see uncloud_pay.models.Product. @property def recurring_price(self): return self.cores * 3 + self.ram_in_gb * 4 + def __str__(self): + return "VM {} ({}): {} cores {} gb ram".format(self.uuid, + self.name, + self.cores, + self.ram_in_gb) @property def description(self): return "Virtual machine '{}': {} core(s), {}GB memory".format( self.name, self.cores, self.ram_in_gb) - # @staticmethod - # def allowed_recurring_periods(): - # return list(filter( - # lambda pair: pair[0] in [RecurringPeriod.PER_365D, - # RecurringPeriod.PER_30D, RecurringPeriod.PER_HOUR], - # RecurringPeriod.choices)) - - - def __str__(self): - return f"VM id={self.id},name={self.name},cores={self.cores},ram_in_gb={self.ram_in_gb}" - + @staticmethod + def allowed_recurring_periods(): + return list(filter( + lambda pair: pair[0] in [RecurringPeriod.PER_YEAR, + RecurringPeriod.PER_MONTH, RecurringPeriod.PER_HOUR], + RecurringPeriod.choices)) class VMWithOSProduct(VMProduct): - primary_disk = models.ForeignKey('VMDiskProduct', on_delete=models.CASCADE, null=True) + pass class VMDiskImageProduct(UncloudModel): @@ -95,6 +105,9 @@ class VMDiskImageProduct(UncloudModel): """ + uuid = models.UUIDField( + primary_key=True, default=uuid.uuid4, editable=False + ) owner = models.ForeignKey( get_user_model(), on_delete=models.CASCADE, editable=False ) @@ -117,23 +130,13 @@ class VMDiskImageProduct(UncloudModel): ) def __str__(self): - return "VMDiskImage {} ({}): {} gb".format(self.id, + return "VMDiskImage {} ({}): {} gb".format(self.uuid, self.name, self.size_in_gb) -# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types -class VMDiskType(models.TextChoices): - """ - Types of disks that can be attached to VMs - """ - CEPH_SSD = 'ceph/ssd' - CEPH_HDD = 'ceph/hdd' - LOCAL_SSD = 'local/ssd' - LOCAL_HDD = 'local/hdd' - -class VMDiskProduct(models.Model): +class VMDiskProduct(Product): """ The VMDiskProduct is attached to a VM. @@ -143,32 +146,32 @@ class VMDiskProduct(models.Model): """ vm = models.ForeignKey(VMProduct, on_delete=models.CASCADE) - - image = models.ForeignKey(VMDiskImageProduct, on_delete=models.CASCADE, - blank=True, null=True) + image = models.ForeignKey(VMDiskImageProduct, on_delete=models.CASCADE) size_in_gb = models.FloatField(blank=True) - disk_type = models.CharField( - max_length=20, - choices=VMDiskType.choices, - default=VMDiskType.CEPH_SSD) - - def __str__(self): - return f"Disk {self.size_in_gb}GB ({self.disk_type}) for {self.vm}" + @property + def description(self): + return "Disk for VM '{}': {}GB".format(self.vm.name, self.size_in_gb) @property def recurring_price(self): - if self.disk_type == VMDiskType.CEPH_SSD: - price_per_gb = 3.5/10 - elif self.disk_type == VMDiskType.CEPH_HDD: - price_per_gb = 1.5/100 - elif self.disk_type == VMDiskType.LOCAL_SSD: - price_per_gb = 3.5/10 - elif self.disk_type == VMDiskType.CEPH_HDD: - price_per_gb = 1.5/100 + return (self.size_in_gb / 10) * 3.5 - return self.size_in_gb * price_per_gb + # Sample code for clean method + + # Ensures that a VMDiskProduct can only be created from a VMDiskImageProduct + # that is in status 'active' + + # def clean(self): + # if self.image.status != 'active': + # raise ValidationError({ + # 'image': 'VM Disk must be created from an active disk image.' + # }) + + def save(self, *args, **kwargs): + self.full_clean() + super().save(*args, **kwargs) class VMNetworkCard(models.Model): @@ -180,7 +183,7 @@ class VMNetworkCard(models.Model): null=True) -class VMSnapshotProduct(models.Model): +class VMSnapshotProduct(Product): gb_ssd = models.FloatField(editable=False) gb_hdd = models.FloatField(editable=False) diff --git a/uncloud_vm/serializers.py b/uncloud_django_based/uncloud/uncloud_vm/serializers.py similarity index 74% rename from uncloud_vm/serializers.py rename to uncloud_django_based/uncloud/uncloud_vm/serializers.py index a60d10b..92c7fe8 100644 --- a/uncloud_vm/serializers.py +++ b/uncloud_django_based/uncloud/uncloud_vm/serializers.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from rest_framework import serializers -from .models import * +from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct, VMCluster from uncloud_pay.models import RecurringPeriod, BillingAddress # XXX: does not seem to be used? @@ -67,7 +67,7 @@ class VMSnapshotProductSerializer(serializers.ModelSerializer): disks = VMDiskProduct.objects.filter(vm=value) if len(disks) == 0: - raise serializers.ValidationError("VM {} does not have any disks, cannot snapshot".format(value.id)) + raise serializers.ValidationError("VM {} does not have any disks, cannot snapshot".format(value.uuid)) return value @@ -86,46 +86,39 @@ class ManagedVMProductSerializer(serializers.ModelSerializer): """ primary_disk = CreateManagedVMDiskProductSerializer() class Meta: - model = VMWithOSProduct + model = VMProduct fields = [ 'cores', 'ram_in_gb', 'primary_disk'] -class VMProductSerializer(serializers.ModelSerializer): +class VMProductSerializer(serializers.HyperlinkedModelSerializer): primary_disk = CreateVMDiskProductSerializer() snapshots = VMSnapshotProductSerializer(many=True, read_only=True) disks = VMDiskProductSerializer(many=True, read_only=True) class Meta: - model = VMWithOSProduct - fields = ['order', 'owner', 'status', 'name', 'cores', + model = VMProduct + fields = ['uuid', 'order', 'owner', 'status', 'name', 'cores', 'ram_in_gb', 'primary_disk', 'snapshots', 'disks', 'extra_data'] - read_only_fields = ['order', 'owner', 'status'] + read_only_fields = ['uuid', 'order', 'owner', 'status'] class OrderVMProductSerializer(VMProductSerializer): - # recurring_period = serializers.ChoiceField( - # choices=VMWithOSProduct.allowed_recurring_periods()) + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) def __init__(self, *args, **kwargs): super(VMProductSerializer, self).__init__(*args, **kwargs) + self.fields['billing_address'] = serializers.ChoiceField( + choices=BillingAddress.get_addresses_for( + self.context['request'].user) + ) class Meta: model = VMProductSerializer.Meta.model - fields = VMProductSerializer.Meta.fields + [ 'recurring_period' ] + fields = VMProductSerializer.Meta.fields + [ + 'recurring_period', 'billing_address' + ] read_only_fields = VMProductSerializer.Meta.read_only_fields # Nico's playground. -class NicoVMProductSerializer(serializers.ModelSerializer): - snapshots = VMSnapshotProductSerializer(many=True, read_only=True) - order = serializers.StringRelatedField() - - class Meta: - model = VMProduct - read_only_fields = ['order', 'owner', 'status', - 'vmhost', 'vmcluster', 'snapshots', - 'extra_data' ] - fields = read_only_fields + [ 'name', - 'cores', - 'ram_in_gb' - ] class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): """ @@ -133,8 +126,8 @@ class DCLVMProductSerializer(serializers.HyperlinkedModelSerializer): """ # Custom field used at creation (= ordering) only. - # recurring_period = serializers.ChoiceField( - # choices=VMProduct.allowed_recurring_periods()) + recurring_period = serializers.ChoiceField( + choices=VMProduct.allowed_recurring_periods()) os_disk_uuid = serializers.UUIDField() # os_disk_size = diff --git a/uncloud_vm/tests.py b/uncloud_django_based/uncloud/uncloud_vm/tests.py similarity index 84% rename from uncloud_vm/tests.py rename to uncloud_django_based/uncloud/uncloud_vm/tests.py index e5d403f..1f47001 100644 --- a/uncloud_vm/tests.py +++ b/uncloud_django_based/uncloud/uncloud_vm/tests.py @@ -79,6 +79,22 @@ class VMTestCase(TestCase): # msg='VMDiskProduct created with disk image whose status is not active.' # ) + def test_vm_disk_product_creation(self): + """Ensure that a user can only create a VMDiskProduct for an existing VM""" + + disk_image = VMDiskImageProduct.objects.create( + owner=self.user, name='disk_image', is_os_image=True, is_public=True, size_in_gb=10, + status='active' + ) + + with self.assertRaises(ValidationError, msg='User created a VMDiskProduct for non-existing VM'): + # Create VMProduct object but don't save it in database + vm = VMProduct() + + vm_disk_product = VMDiskProduct.objects.create( + owner=self.user, vm=vm, image=disk_image, size_in_gb=10 + ) + # TODO: the logic tested by this test is not implemented yet. # def test_vm_disk_product_creation_for_someone_else(self): # """Ensure that a user can only create a VMDiskProduct for his/her own VM""" diff --git a/uncloud_vm/views.py b/uncloud_django_based/uncloud/uncloud_vm/views.py similarity index 84% rename from uncloud_vm/views.py rename to uncloud_django_based/uncloud/uncloud_vm/views.py index 67f8656..1dead62 100644 --- a/uncloud_vm/views.py +++ b/uncloud_django_based/uncloud/uncloud_vm/views.py @@ -10,7 +10,7 @@ from rest_framework.response import Response from rest_framework.exceptions import ValidationError from .models import VMHost, VMProduct, VMSnapshotProduct, VMDiskProduct, VMDiskImageProduct, VMCluster -from uncloud_pay.models import Order, BillingAddress +from uncloud_pay.models import Order from .serializers import * from uncloud_pay.helpers import ProductViewSet @@ -136,9 +136,9 @@ class VMProductViewSet(ProductViewSet): def get_queryset(self): if self.request.user.is_superuser: - obj = VMWithOSProduct.objects.all() + obj = VMProduct.objects.all() else: - obj = VMWithOSProduct.objects.filter(owner=self.request.user) + obj = VMProduct.objects.filter(owner=self.request.user) return obj @@ -156,55 +156,28 @@ class VMProductViewSet(ProductViewSet): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) order_recurring_period = serializer.validated_data.pop("recurring_period") + order_billing_address = serializer.validated_data.pop("billing_address") - # Create disk image. - disk = VMDiskProduct(owner=request.user, - **serializer.validated_data.pop("primary_disk")) - vm = VMWithOSProduct(owner=request.user, primary_disk=disk, - **serializer.validated_data) - disk.vm = vm # XXX: Is this really needed? - - # Create VM and Disk orders. - vm_order = Order.from_product( - vm, + # Create base order. + order = Order( recurring_period=order_recurring_period, + billing_address=order_billing_address, + owner=request.user, starting_date=timezone.now() ) + order.save() - disk_order = Order.from_product( - disk, - recurring_period=order_recurring_period, - starting_date=timezone.now(), - depends_on=vm_order - ) + # Create disk image. + disk = VMDiskProduct(owner=request.user, order=order, + **serializer.validated_data.pop("primary_disk")) - - # Commit to DB. - vm.order = vm_order - vm.save() - vm_order.save() - - disk.order = disk_order - disk_order.save() + # Create VM. + vm = serializer.save(owner=request.user, order=order, primary_disk=disk) + disk.vm = vm disk.save() return Response(VMProductSerializer(vm, context={'request': request}).data) -class NicoVMProductViewSet(ProductViewSet): - permission_classes = [permissions.IsAuthenticated] - serializer_class = NicoVMProductSerializer - - def get_queryset(self): - obj = VMProduct.objects.filter(owner=self.request.user) - return obj - - def create(self, request): - serializer = self.serializer_class(data=request.data, context={'request': request}) - serializer.is_valid(raise_exception=True) - vm = serializer.save(owner=request.user) - - return Response(serializer.data) - ### # Admin stuff. diff --git a/resources/vat-rates.csv b/uncloud_django_based/vat_rates.csv similarity index 100% rename from resources/vat-rates.csv rename to uncloud_django_based/vat_rates.csv diff --git a/archive/uncloud_etcd_based/bin/gen-version b/uncloud_etcd_based/bin/gen-version similarity index 100% rename from archive/uncloud_etcd_based/bin/gen-version rename to uncloud_etcd_based/bin/gen-version diff --git a/archive/uncloud_etcd_based/bin/uncloud b/uncloud_etcd_based/bin/uncloud similarity index 100% rename from archive/uncloud_etcd_based/bin/uncloud rename to uncloud_etcd_based/bin/uncloud diff --git a/archive/uncloud_etcd_based/bin/uncloud-run-reinstall b/uncloud_etcd_based/bin/uncloud-run-reinstall similarity index 100% rename from archive/uncloud_etcd_based/bin/uncloud-run-reinstall rename to uncloud_etcd_based/bin/uncloud-run-reinstall diff --git a/archive/uncloud_etcd_based/conf/uncloud.conf b/uncloud_etcd_based/conf/uncloud.conf similarity index 100% rename from archive/uncloud_etcd_based/conf/uncloud.conf rename to uncloud_etcd_based/conf/uncloud.conf diff --git a/archive/uncloud_etcd_based/docs/Makefile b/uncloud_etcd_based/docs/Makefile similarity index 100% rename from archive/uncloud_etcd_based/docs/Makefile rename to uncloud_etcd_based/docs/Makefile diff --git a/archive/uncloud_etcd_based/docs/README.md b/uncloud_etcd_based/docs/README.md similarity index 100% rename from archive/uncloud_etcd_based/docs/README.md rename to uncloud_etcd_based/docs/README.md diff --git a/uncloud_auth/__init__.py b/uncloud_etcd_based/docs/__init__.py similarity index 100% rename from uncloud_auth/__init__.py rename to uncloud_etcd_based/docs/__init__.py diff --git a/uncloud_auth/migrations/__init__.py b/uncloud_etcd_based/docs/source/__init__.py similarity index 100% rename from uncloud_auth/migrations/__init__.py rename to uncloud_etcd_based/docs/source/__init__.py diff --git a/archive/uncloud_etcd_based/docs/source/admin-guide.rst b/uncloud_etcd_based/docs/source/admin-guide.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/admin-guide.rst rename to uncloud_etcd_based/docs/source/admin-guide.rst diff --git a/archive/uncloud_etcd_based/docs/source/conf.py b/uncloud_etcd_based/docs/source/conf.py similarity index 100% rename from archive/uncloud_etcd_based/docs/source/conf.py rename to uncloud_etcd_based/docs/source/conf.py diff --git a/archive/uncloud_etcd_based/docs/source/diagram-code/ucloud b/uncloud_etcd_based/docs/source/diagram-code/ucloud similarity index 100% rename from archive/uncloud_etcd_based/docs/source/diagram-code/ucloud rename to uncloud_etcd_based/docs/source/diagram-code/ucloud diff --git a/archive/uncloud_etcd_based/docs/source/hacking.rst b/uncloud_etcd_based/docs/source/hacking.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/hacking.rst rename to uncloud_etcd_based/docs/source/hacking.rst diff --git a/archive/uncloud_etcd_based/docs/source/images/ucloud.svg b/uncloud_etcd_based/docs/source/images/ucloud.svg similarity index 100% rename from archive/uncloud_etcd_based/docs/source/images/ucloud.svg rename to uncloud_etcd_based/docs/source/images/ucloud.svg diff --git a/archive/uncloud_etcd_based/docs/source/index.rst b/uncloud_etcd_based/docs/source/index.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/index.rst rename to uncloud_etcd_based/docs/source/index.rst diff --git a/archive/uncloud_etcd_based/docs/source/introduction.rst b/uncloud_etcd_based/docs/source/introduction.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/introduction.rst rename to uncloud_etcd_based/docs/source/introduction.rst diff --git a/archive/uncloud_etcd_based/docs/source/misc/todo.rst b/uncloud_etcd_based/docs/source/misc/todo.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/misc/todo.rst rename to uncloud_etcd_based/docs/source/misc/todo.rst diff --git a/archive/uncloud_etcd_based/docs/source/setup-install.rst b/uncloud_etcd_based/docs/source/setup-install.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/setup-install.rst rename to uncloud_etcd_based/docs/source/setup-install.rst diff --git a/archive/uncloud_etcd_based/docs/source/theory/summary.rst b/uncloud_etcd_based/docs/source/theory/summary.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/theory/summary.rst rename to uncloud_etcd_based/docs/source/theory/summary.rst diff --git a/archive/uncloud_etcd_based/docs/source/troubleshooting.rst b/uncloud_etcd_based/docs/source/troubleshooting.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/troubleshooting.rst rename to uncloud_etcd_based/docs/source/troubleshooting.rst diff --git a/archive/uncloud_etcd_based/docs/source/user-guide.rst b/uncloud_etcd_based/docs/source/user-guide.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/user-guide.rst rename to uncloud_etcd_based/docs/source/user-guide.rst diff --git a/archive/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst b/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst rename to uncloud_etcd_based/docs/source/user-guide/how-to-create-an-os-image-for-ucloud.rst diff --git a/archive/uncloud_etcd_based/docs/source/vm-images.rst b/uncloud_etcd_based/docs/source/vm-images.rst similarity index 100% rename from archive/uncloud_etcd_based/docs/source/vm-images.rst rename to uncloud_etcd_based/docs/source/vm-images.rst diff --git a/archive/uncloud_etcd_based/scripts/uncloud b/uncloud_etcd_based/scripts/uncloud similarity index 100% rename from archive/uncloud_etcd_based/scripts/uncloud rename to uncloud_etcd_based/scripts/uncloud diff --git a/archive/uncloud_etcd_based/setup.py b/uncloud_etcd_based/setup.py similarity index 100% rename from archive/uncloud_etcd_based/setup.py rename to uncloud_etcd_based/setup.py diff --git a/uncloud_net/__init__.py b/uncloud_etcd_based/test/__init__.py similarity index 100% rename from uncloud_net/__init__.py rename to uncloud_etcd_based/test/__init__.py diff --git a/archive/uncloud_etcd_based/test/test_mac_local.py b/uncloud_etcd_based/test/test_mac_local.py similarity index 100% rename from archive/uncloud_etcd_based/test/test_mac_local.py rename to uncloud_etcd_based/test/test_mac_local.py diff --git a/archive/uncloud_etcd_based/uncloud/__init__.py b/uncloud_etcd_based/uncloud/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/__init__.py rename to uncloud_etcd_based/uncloud/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/api/README.md b/uncloud_etcd_based/uncloud/api/README.md similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/README.md rename to uncloud_etcd_based/uncloud/api/README.md diff --git a/archive/uncloud_etcd_based/uncloud/api/__init__.py b/uncloud_etcd_based/uncloud/api/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/__init__.py rename to uncloud_etcd_based/uncloud/api/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/api/common_fields.py b/uncloud_etcd_based/uncloud/api/common_fields.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/common_fields.py rename to uncloud_etcd_based/uncloud/api/common_fields.py diff --git a/archive/uncloud_etcd_based/uncloud/api/create_image_store.py b/uncloud_etcd_based/uncloud/api/create_image_store.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/create_image_store.py rename to uncloud_etcd_based/uncloud/api/create_image_store.py diff --git a/archive/uncloud_etcd_based/uncloud/api/helper.py b/uncloud_etcd_based/uncloud/api/helper.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/helper.py rename to uncloud_etcd_based/uncloud/api/helper.py diff --git a/archive/uncloud_etcd_based/uncloud/api/main.py b/uncloud_etcd_based/uncloud/api/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/main.py rename to uncloud_etcd_based/uncloud/api/main.py diff --git a/archive/uncloud_etcd_based/uncloud/api/schemas.py b/uncloud_etcd_based/uncloud/api/schemas.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/api/schemas.py rename to uncloud_etcd_based/uncloud/api/schemas.py diff --git a/uncloud_net/migrations/__init__.py b/uncloud_etcd_based/uncloud/cli/__init__.py similarity index 100% rename from uncloud_net/migrations/__init__.py rename to uncloud_etcd_based/uncloud/cli/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/helper.py b/uncloud_etcd_based/uncloud/cli/helper.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/helper.py rename to uncloud_etcd_based/uncloud/cli/helper.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/host.py b/uncloud_etcd_based/uncloud/cli/host.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/host.py rename to uncloud_etcd_based/uncloud/cli/host.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/image.py b/uncloud_etcd_based/uncloud/cli/image.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/image.py rename to uncloud_etcd_based/uncloud/cli/image.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/main.py b/uncloud_etcd_based/uncloud/cli/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/main.py rename to uncloud_etcd_based/uncloud/cli/main.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/network.py b/uncloud_etcd_based/uncloud/cli/network.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/network.py rename to uncloud_etcd_based/uncloud/cli/network.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/user.py b/uncloud_etcd_based/uncloud/cli/user.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/user.py rename to uncloud_etcd_based/uncloud/cli/user.py diff --git a/archive/uncloud_etcd_based/uncloud/cli/vm.py b/uncloud_etcd_based/uncloud/cli/vm.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/cli/vm.py rename to uncloud_etcd_based/uncloud/cli/vm.py diff --git a/uncloud_pay/migrations/__init__.py b/uncloud_etcd_based/uncloud/client/__init__.py similarity index 100% rename from uncloud_pay/migrations/__init__.py rename to uncloud_etcd_based/uncloud/client/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/client/main.py b/uncloud_etcd_based/uncloud/client/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/client/main.py rename to uncloud_etcd_based/uncloud/client/main.py diff --git a/archive/uncloud_etcd_based/uncloud/common/__init__.py b/uncloud_etcd_based/uncloud/common/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/__init__.py rename to uncloud_etcd_based/uncloud/common/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/common/classes.py b/uncloud_etcd_based/uncloud/common/classes.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/classes.py rename to uncloud_etcd_based/uncloud/common/classes.py diff --git a/archive/uncloud_etcd_based/uncloud/common/cli.py b/uncloud_etcd_based/uncloud/common/cli.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/cli.py rename to uncloud_etcd_based/uncloud/common/cli.py diff --git a/archive/uncloud_etcd_based/uncloud/common/counters.py b/uncloud_etcd_based/uncloud/common/counters.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/counters.py rename to uncloud_etcd_based/uncloud/common/counters.py diff --git a/archive/uncloud_etcd_based/uncloud/common/etcd_wrapper.py b/uncloud_etcd_based/uncloud/common/etcd_wrapper.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/etcd_wrapper.py rename to uncloud_etcd_based/uncloud/common/etcd_wrapper.py diff --git a/archive/uncloud_etcd_based/uncloud/common/host.py b/uncloud_etcd_based/uncloud/common/host.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/host.py rename to uncloud_etcd_based/uncloud/common/host.py diff --git a/archive/uncloud_etcd_based/uncloud/common/network.py b/uncloud_etcd_based/uncloud/common/network.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/network.py rename to uncloud_etcd_based/uncloud/common/network.py diff --git a/archive/uncloud_etcd_based/uncloud/common/parser.py b/uncloud_etcd_based/uncloud/common/parser.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/parser.py rename to uncloud_etcd_based/uncloud/common/parser.py diff --git a/archive/uncloud_etcd_based/uncloud/common/request.py b/uncloud_etcd_based/uncloud/common/request.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/request.py rename to uncloud_etcd_based/uncloud/common/request.py diff --git a/archive/uncloud_etcd_based/uncloud/common/schemas.py b/uncloud_etcd_based/uncloud/common/schemas.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/schemas.py rename to uncloud_etcd_based/uncloud/common/schemas.py diff --git a/archive/uncloud_etcd_based/uncloud/common/settings.py b/uncloud_etcd_based/uncloud/common/settings.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/settings.py rename to uncloud_etcd_based/uncloud/common/settings.py diff --git a/archive/uncloud_etcd_based/uncloud/common/shared.py b/uncloud_etcd_based/uncloud/common/shared.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/shared.py rename to uncloud_etcd_based/uncloud/common/shared.py diff --git a/archive/uncloud_etcd_based/uncloud/common/storage_handlers.py b/uncloud_etcd_based/uncloud/common/storage_handlers.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/storage_handlers.py rename to uncloud_etcd_based/uncloud/common/storage_handlers.py diff --git a/archive/uncloud_etcd_based/uncloud/common/vm.py b/uncloud_etcd_based/uncloud/common/vm.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/common/vm.py rename to uncloud_etcd_based/uncloud/common/vm.py diff --git a/uncloud_service/__init__.py b/uncloud_etcd_based/uncloud/configure/__init__.py similarity index 100% rename from uncloud_service/__init__.py rename to uncloud_etcd_based/uncloud/configure/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/configure/main.py b/uncloud_etcd_based/uncloud/configure/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/configure/main.py rename to uncloud_etcd_based/uncloud/configure/main.py diff --git a/archive/uncloud_etcd_based/uncloud/filescanner/__init__.py b/uncloud_etcd_based/uncloud/filescanner/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/filescanner/__init__.py rename to uncloud_etcd_based/uncloud/filescanner/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/filescanner/main.py b/uncloud_etcd_based/uncloud/filescanner/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/filescanner/main.py rename to uncloud_etcd_based/uncloud/filescanner/main.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/README.org b/uncloud_etcd_based/uncloud/hack/README.org similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/README.org rename to uncloud_etcd_based/uncloud/hack/README.org diff --git a/archive/uncloud_etcd_based/uncloud/hack/__init__.py b/uncloud_etcd_based/uncloud/hack/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/__init__.py rename to uncloud_etcd_based/uncloud/hack/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host b/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host rename to uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host diff --git a/archive/uncloud_etcd_based/uncloud/hack/config.py b/uncloud_etcd_based/uncloud/hack/config.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/config.py rename to uncloud_etcd_based/uncloud/hack/config.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/db.py b/uncloud_etcd_based/uncloud/hack/db.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/db.py rename to uncloud_etcd_based/uncloud/hack/db.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore b/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore rename to uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py b/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py rename to uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last b/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-last rename to uncloud_etcd_based/uncloud/hack/hackcloud/mac-last diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix b/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix rename to uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/net.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/net.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules b/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/nftrules rename to uncloud_etcd_based/uncloud/hack/hackcloud/nftrules diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf b/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf rename to uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh b/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh rename to uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh diff --git a/archive/uncloud_etcd_based/uncloud/hack/host.py b/uncloud_etcd_based/uncloud/hack/host.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/host.py rename to uncloud_etcd_based/uncloud/hack/host.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/mac.py b/uncloud_etcd_based/uncloud/hack/mac.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/mac.py rename to uncloud_etcd_based/uncloud/hack/mac.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/main.py b/uncloud_etcd_based/uncloud/hack/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/main.py rename to uncloud_etcd_based/uncloud/hack/main.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/net.py b/uncloud_etcd_based/uncloud/hack/net.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/net.py rename to uncloud_etcd_based/uncloud/hack/net.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/nftables.conf b/uncloud_etcd_based/uncloud/hack/nftables.conf similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/nftables.conf rename to uncloud_etcd_based/uncloud/hack/nftables.conf diff --git a/archive/uncloud_etcd_based/uncloud/hack/product.py b/uncloud_etcd_based/uncloud/hack/product.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/product.py rename to uncloud_etcd_based/uncloud/hack/product.py diff --git a/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api b/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api rename to uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api diff --git a/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host b/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host rename to uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host diff --git a/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata b/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata rename to uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-metadata diff --git a/archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler b/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler rename to uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-scheduler diff --git a/archive/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host b/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host rename to uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host diff --git a/archive/uncloud_etcd_based/uncloud/hack/uncloud-run-vm b/uncloud_etcd_based/uncloud/hack/uncloud-run-vm similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/uncloud-run-vm rename to uncloud_etcd_based/uncloud/hack/uncloud-run-vm diff --git a/archive/uncloud_etcd_based/uncloud/hack/vm.py b/uncloud_etcd_based/uncloud/hack/vm.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/hack/vm.py rename to uncloud_etcd_based/uncloud/hack/vm.py diff --git a/archive/uncloud_etcd_based/uncloud/host/__init__.py b/uncloud_etcd_based/uncloud/host/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/host/__init__.py rename to uncloud_etcd_based/uncloud/host/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/host/main.py b/uncloud_etcd_based/uncloud/host/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/host/main.py rename to uncloud_etcd_based/uncloud/host/main.py diff --git a/archive/uncloud_etcd_based/uncloud/host/virtualmachine.py b/uncloud_etcd_based/uncloud/host/virtualmachine.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/host/virtualmachine.py rename to uncloud_etcd_based/uncloud/host/virtualmachine.py diff --git a/archive/uncloud_etcd_based/uncloud/imagescanner/__init__.py b/uncloud_etcd_based/uncloud/imagescanner/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/imagescanner/__init__.py rename to uncloud_etcd_based/uncloud/imagescanner/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/imagescanner/main.py b/uncloud_etcd_based/uncloud/imagescanner/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/imagescanner/main.py rename to uncloud_etcd_based/uncloud/imagescanner/main.py diff --git a/archive/uncloud_etcd_based/uncloud/metadata/__init__.py b/uncloud_etcd_based/uncloud/metadata/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/metadata/__init__.py rename to uncloud_etcd_based/uncloud/metadata/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/metadata/main.py b/uncloud_etcd_based/uncloud/metadata/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/metadata/main.py rename to uncloud_etcd_based/uncloud/metadata/main.py diff --git a/archive/uncloud_etcd_based/uncloud/network/README b/uncloud_etcd_based/uncloud/network/README similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/README rename to uncloud_etcd_based/uncloud/network/README diff --git a/uncloud_storage/__init__.py b/uncloud_etcd_based/uncloud/network/__init__.py similarity index 100% rename from uncloud_storage/__init__.py rename to uncloud_etcd_based/uncloud/network/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/network/create-bridge.sh b/uncloud_etcd_based/uncloud/network/create-bridge.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/create-bridge.sh rename to uncloud_etcd_based/uncloud/network/create-bridge.sh diff --git a/archive/uncloud_etcd_based/uncloud/network/create-tap.sh b/uncloud_etcd_based/uncloud/network/create-tap.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/create-tap.sh rename to uncloud_etcd_based/uncloud/network/create-tap.sh diff --git a/archive/uncloud_etcd_based/uncloud/network/create-vxlan.sh b/uncloud_etcd_based/uncloud/network/create-vxlan.sh similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/create-vxlan.sh rename to uncloud_etcd_based/uncloud/network/create-vxlan.sh diff --git a/archive/uncloud_etcd_based/uncloud/network/radvd-template.conf b/uncloud_etcd_based/uncloud/network/radvd-template.conf similarity index 100% rename from archive/uncloud_etcd_based/uncloud/network/radvd-template.conf rename to uncloud_etcd_based/uncloud/network/radvd-template.conf diff --git a/archive/uncloud_etcd_based/uncloud/oneshot/__init__.py b/uncloud_etcd_based/uncloud/oneshot/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/oneshot/__init__.py rename to uncloud_etcd_based/uncloud/oneshot/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/oneshot/main.py b/uncloud_etcd_based/uncloud/oneshot/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/oneshot/main.py rename to uncloud_etcd_based/uncloud/oneshot/main.py diff --git a/archive/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py b/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/oneshot/virtualmachine.py rename to uncloud_etcd_based/uncloud/oneshot/virtualmachine.py diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/__init__.py b/uncloud_etcd_based/uncloud/scheduler/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/__init__.py rename to uncloud_etcd_based/uncloud/scheduler/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/helper.py b/uncloud_etcd_based/uncloud/scheduler/helper.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/helper.py rename to uncloud_etcd_based/uncloud/scheduler/helper.py diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/main.py b/uncloud_etcd_based/uncloud/scheduler/main.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/main.py rename to uncloud_etcd_based/uncloud/scheduler/main.py diff --git a/uncloud_vm/__init__.py b/uncloud_etcd_based/uncloud/scheduler/tests/__init__.py similarity index 100% rename from uncloud_vm/__init__.py rename to uncloud_etcd_based/uncloud/scheduler/tests/__init__.py diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py b/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py rename to uncloud_etcd_based/uncloud/scheduler/tests/test_basics.py diff --git a/archive/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py b/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py rename to uncloud_etcd_based/uncloud/scheduler/tests/test_dead_host_mechanism.py diff --git a/archive/uncloud_etcd_based/uncloud/version.py b/uncloud_etcd_based/uncloud/version.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/version.py rename to uncloud_etcd_based/uncloud/version.py diff --git a/archive/uncloud_etcd_based/uncloud/vmm/__init__.py b/uncloud_etcd_based/uncloud/vmm/__init__.py similarity index 100% rename from archive/uncloud_etcd_based/uncloud/vmm/__init__.py rename to uncloud_etcd_based/uncloud/vmm/__init__.py diff --git a/uncloud_net/admin.py b/uncloud_net/admin.py deleted file mode 100644 index ca6aaa1..0000000 --- a/uncloud_net/admin.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.contrib import admin - -from .models import * - - -for m in [ ReverseDNSEntry, WireGuardVPNPool, WireGuardVPN ]: - admin.site.register(m) diff --git a/uncloud_net/forms.py b/uncloud_net/forms.py deleted file mode 100644 index ad4e013..0000000 --- a/uncloud_net/forms.py +++ /dev/null @@ -1,11 +0,0 @@ -from django import forms - -from .models import * -from .selectors import * - -class WireGuardVPNForm(forms.ModelForm): - network_size = forms.ChoiceField(choices=allowed_vpn_network_reservation_size) - - class Meta: - model = WireGuardVPN - fields = [ "wireguard_public_key" ] diff --git a/uncloud_net/migrations/0001_initial.py b/uncloud_net/migrations/0001_initial.py deleted file mode 100644 index 6794156..0000000 --- a/uncloud_net/migrations/0001_initial.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 3.1 on 2020-12-13 13:42 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='MACAdress', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ], - ), - migrations.CreateModel( - name='WireGuardVPNPool', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('network', models.GenericIPAddressField(unique=True)), - ('network_mask', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])), - ('subnetwork_mask', models.IntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(128)])), - ('vpn_server_hostname', models.CharField(max_length=256)), - ('wireguard_private_key', models.CharField(max_length=48)), - ], - ), - migrations.CreateModel( - name='WireGuardVPNFreeLeases', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pool_index', models.IntegerField(unique=True)), - ('vpnpool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.wireguardvpnpool')), - ], - ), - migrations.CreateModel( - name='WireGuardVPN', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('pool_index', models.IntegerField(unique=True)), - ('wireguard_public_key', models.CharField(max_length=48)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('vpnpool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_net.wireguardvpnpool')), - ], - ), - migrations.CreateModel( - name='ReverseDNSEntry', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('ip_address', models.GenericIPAddressField(unique=True)), - ('name', models.CharField(max_length=253)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/uncloud_net/migrations/0002_wireguardvpnpool_wireguard_public_key.py b/uncloud_net/migrations/0002_wireguardvpnpool_wireguard_public_key.py deleted file mode 100644 index 479aba1..0000000 --- a/uncloud_net/migrations/0002_wireguardvpnpool_wireguard_public_key.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1 on 2020-12-13 17:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_net', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='wireguardvpnpool', - name='wireguard_public_key', - field=models.CharField(default='', max_length=48), - preserve_default=False, - ), - ] diff --git a/uncloud_net/migrations/0003_wireguardvpnpool_wg_name.py b/uncloud_net/migrations/0003_wireguardvpnpool_wg_name.py deleted file mode 100644 index 9ecf52c..0000000 --- a/uncloud_net/migrations/0003_wireguardvpnpool_wg_name.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1 on 2020-12-13 17:31 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_net', '0002_wireguardvpnpool_wireguard_public_key'), - ] - - operations = [ - migrations.AddField( - model_name='wireguardvpnpool', - name='wg_name', - field=models.CharField(default='wg0', max_length=15), - preserve_default=False, - ), - ] diff --git a/uncloud_net/migrations/0004_auto_20201213_1734.py b/uncloud_net/migrations/0004_auto_20201213_1734.py deleted file mode 100644 index 24e46e7..0000000 --- a/uncloud_net/migrations/0004_auto_20201213_1734.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.1 on 2020-12-13 17:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_net', '0003_wireguardvpnpool_wg_name'), - ] - - operations = [ - migrations.AddConstraint( - model_name='wireguardvpnpool', - constraint=models.UniqueConstraint(fields=('wg_name', 'vpn_server_hostname'), name='unique_interface_name_per_host'), - ), - ] diff --git a/uncloud_net/migrations/0005_auto_20201220_1837.py b/uncloud_net/migrations/0005_auto_20201220_1837.py deleted file mode 100644 index 1dbabe6..0000000 --- a/uncloud_net/migrations/0005_auto_20201220_1837.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2020-12-20 18:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_net', '0004_auto_20201213_1734'), - ] - - operations = [ - migrations.AlterField( - model_name='wireguardvpn', - name='wireguard_public_key', - field=models.CharField(max_length=48, unique=True), - ), - ] diff --git a/uncloud_net/migrations/0006_auto_20201224_1626.py b/uncloud_net/migrations/0006_auto_20201224_1626.py deleted file mode 100644 index c0dd2ef..0000000 --- a/uncloud_net/migrations/0006_auto_20201224_1626.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.1 on 2020-12-24 16:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_net', '0005_auto_20201220_1837'), - ] - - operations = [ - migrations.AddConstraint( - model_name='wireguardvpn', - constraint=models.UniqueConstraint(fields=('vpnpool', 'wireguard_public_key'), name='wg_key_unique_per_pool'), - ), - ] diff --git a/uncloud_net/models.py b/uncloud_net/models.py deleted file mode 100644 index 9865a08..0000000 --- a/uncloud_net/models.py +++ /dev/null @@ -1,208 +0,0 @@ -import uuid -import ipaddress - -from django.db import models -from django.contrib.auth import get_user_model -from django.core.validators import MinValueValidator, MaxValueValidator -from django.core.exceptions import FieldError, ValidationError - -from uncloud_pay.models import Order, Product - -class WireGuardVPNPool(models.Model): - """ - Network address pools from which VPNs can be created - """ - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['wg_name', 'vpn_server_hostname' ], - name='unique_interface_name_per_host') - ] - - - # Linux interface naming is restricing to max 15 characters - wg_name = models.CharField(max_length=15) - - network = models.GenericIPAddressField(unique=True) - network_mask = models.IntegerField(validators=[MinValueValidator(0), - MaxValueValidator(128)]) - - subnetwork_mask = models.IntegerField(validators=[ - MinValueValidator(0), - MaxValueValidator(128) - ]) - - vpn_server_hostname = models.CharField(max_length=256) - wireguard_private_key = models.CharField(max_length=48) - wireguard_public_key = models.CharField(max_length=48) - - @property - def max_pool_index(self): - """ - Return the highest possible network / last network id - """ - - bits = self.subnetwork_mask - self.network_mask - - return (2**bits)-1 - - @property - def ip_network(self): - """ - Return the IP network based on our address and mask - """ - return ipaddress.ip_network(f"{self.network}/{self.network_mask}") - - def __str__(self): - return f"{self.ip_network} (subnets: /{self.subnetwork_mask})" - - @property - def wireguard_config(self): - wireguard_config = [ f"[Interface]\nListenPort = 51820\nPrivateKey = {self.wireguard_private_key}\n" ] - - peers = [] - - for vpn in self.wireguardvpn_set.all(): - public_key = vpn.wireguard_public_key - peer_network = f"{vpn.address}/{self.subnetwork_mask}" - owner = vpn.owner - - peers.append(f"# Owner: {owner}\n[Peer]\nPublicKey = {public_key}\nAllowedIPs = {peer_network}\n\n") - - wireguard_config.extend(peers) - - return "\n".join(wireguard_config) - - -class WireGuardVPN(models.Model): - """ - Created VPNNetworks - """ - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - vpnpool = models.ForeignKey(WireGuardVPNPool, - on_delete=models.CASCADE) - - pool_index = models.IntegerField(unique=True) - - wireguard_public_key = models.CharField(max_length=48, unique=True) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['vpnpool', 'wireguard_public_key'], - name='wg_key_unique_per_pool') - ] - - - @property - def network_mask(self): - return self.vpnpool.subnetwork_mask - - @property - def vpn_server(self): - return self.vpnpool.vpn_server_hostname - - @property - def vpn_server_public_key(self): - return self.vpnpool.wireguard_public_key - - @property - def address(self): - """ - Locate the correct subnet in the supernet - - First get the network itself - - """ - - net = self.vpnpool.ip_network - subnet = net[(2**(128-self.vpnpool.subnetwork_mask)) * self.pool_index] - - return str(subnet) - - def __str__(self): - return f"{self.address} ({self.pool_index})" - - def create_product(self): - """ - Ensure we have a product for the WireguardVPN - """ - - pass - - # Product.objects.get_or_create( - # name="WireGuardVPN", - # description="Wireguard VPN", - # currency=Currency.CHF, - # config= - - -class WireGuardVPNFreeLeases(models.Model): - """ - Previously used VPNNetworks - """ - vpnpool = models.ForeignKey(WireGuardVPNPool, - on_delete=models.CASCADE) - - pool_index = models.IntegerField(unique=True) - -################################################################################ - -class MACAdress(models.Model): - default_prefix = 0x420000000000 - - -class ReverseDNSEntry(models.Model): - """ - A reverse DNS entry - """ - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - ip_address = models.GenericIPAddressField(null=False, unique=True) - - name = models.CharField(max_length=253, null=False) - - @property - def reverse_pointer(self): - return ipaddress.ip_address(self.ip_address).reverse_pointer - - def implement(self): - """ - The implement function implements the change - """ - - # Get all DNS entries (?) / update this DNS entry - # convert to DNS name - # - pass - - - def save(self, *args, **kwargs): - # Product.objects.filter(config__parameters__contains='reverse_dns_network') - # FIXME: check if order is still active / not replaced - - allowed = False - product = None - - for order in Order.objects.filter(config__parameters__reverse_dns_network__isnull=False, - owner=self.owner): - network = order.config['parameters']['reverse_dns_network'] - - net = ipaddress.ip_network(network) - addr = ipaddress.ip_address(self.ip_address) - - if addr in net: - allowed = True - product = order.product - break - - - if not allowed: - raise ValidationError(f"User {self.owner} does not have the right to create reverse DNS entry for {self.ip_address}") - - super().save(*args, **kwargs) - - - def __str__(self): - return f"{self.ip_address} - {self.name}" diff --git a/uncloud_net/selectors.py b/uncloud_net/selectors.py deleted file mode 100644 index 6e12e8b..0000000 --- a/uncloud_net/selectors.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import transaction -from django.db.models import Count, F -from .models import * - -def get_suitable_pools(subnetwork_mask): - """ - Find suitable pools for a certain network size. - - First, filter for all pools that offer the requested subnetwork_size. - - Then find those pools that are not fully exhausted: - - The number of available networks in a pool is 2^(subnetwork_size-network_size. - - The number of available networks in a pool is given by the number of VPNNetworkreservations. - - """ - - return WireGuardVPNPool.objects.annotate( - num_reservations=Count('wireguardvpn'), - max_reservations=2**(F('subnetwork_mask')-F('network_mask'))).filter( - num_reservations__lt=F('max_reservations'), - subnetwork_mask=subnetwork_mask) - - -def allowed_vpn_network_reservation_size(): - """ - Find all possible sizes of subnetworks that are available. - - Select all pools with free networks. - - Get their subnetwork sizes, reduce to a set - - """ - - pools = WireGuardVPNPool.objects.annotate(num_reservations=Count('wireguardvpn'), - max_reservations=2**(F('subnetwork_mask')-F('network_mask'))).filter( - num_reservations__lt=F('max_reservations')) - - # Need to return set of tuples, see - # https://docs.djangoproject.com/en/3.1/ref/models/fields/#field-choices -# return set([ (pool.subnetwork_mask, pool.subnetwork_mask) for pool in pools ]) - return set([pool.subnetwork_mask for pool in pools ]) diff --git a/uncloud_net/serializers.py b/uncloud_net/serializers.py deleted file mode 100644 index 09baa59..0000000 --- a/uncloud_net/serializers.py +++ /dev/null @@ -1,57 +0,0 @@ -import base64 - -from django.contrib.auth import get_user_model -from django.utils.translation import gettext_lazy as _ -from rest_framework import serializers - -from .models import * -from .services import * -from .selectors import * - - -class WireGuardVPNSerializer(serializers.ModelSerializer): - address = serializers.CharField(read_only=True) - vpn_server = serializers.CharField(read_only=True) - vpn_server_public_key = serializers.CharField(read_only=True) - network_mask = serializers.IntegerField() - - class Meta: - model = WireGuardVPN - fields = [ 'wireguard_public_key', 'address', 'network_mask', 'vpn_server', - 'vpn_server_public_key' ] - - extra_kwargs = { - 'network_mask': {'write_only': True } - } - - - def validate_network_mask(self, value): - msg = _(f"No pool for network size {value}") - sizes = allowed_vpn_network_reservation_size() - - if not value in sizes: - raise serializers.ValidationError(msg) - - return value - - def validate_wireguard_public_key(self, value): - msg = _("Supplied key is not a valid wireguard public key") - - """ - Verify wireguard key. - See https://lists.zx2c4.com/pipermail/wireguard/2020-December/006221.html - """ - - try: - decoded_key = base64.standard_b64decode(value) - except Exception as e: - raise serializers.ValidationError(msg) - - if not len(decoded_key) == 32: - raise serializers.ValidationError(msg) - - return value - - -class WireGuardVPNSizesSerializer(serializers.Serializer): - size = serializers.IntegerField(min_value=0, max_value=128) diff --git a/uncloud_net/services.py b/uncloud_net/services.py deleted file mode 100644 index 8449394..0000000 --- a/uncloud_net/services.py +++ /dev/null @@ -1,65 +0,0 @@ -from django.db import transaction - -from .models import * -from .selectors import * -from .tasks import * -from django_q.tasks import async_task, result - -@transaction.atomic -def create_wireguard_vpn(owner, public_key, network_mask): - # Check if the user has a membership. - #------------------------------------ - # If yes, user is eligible for API access and 2 VPNs - # If user already has 2 VPNs, we deduct from the credit - # If deduction is higher than the allowed credit, we fail - - - # - # Check if the user has suitable balance - # Create order - # - return create_wireguard_vpn_tech(owner, public_key, network_mask) - -@transaction.atomic -def create_wireguard_vpn_tech(owner, public_key, network_mask): - pool = get_suitable_pools(network_mask)[0] - count = pool.wireguardvpn_set.count() - - # Try re-using previously used networks first - try: - free_lease = WireGuardVPNFreeLeases.objects.get(vpnpool=pool) - - vpn = WireGuardVPN.objects.create(owner=owner, - vpnpool=pool, - pool_index=free_lease.pool_index, - wireguard_public_key=public_key) - - free_lease.delete() - - except WireGuardVPNFreeLeases.DoesNotExist: - # First object - if count == 0: - vpn = WireGuardVPN.objects.create(owner=owner, - vpnpool=pool, - pool_index=0, - wireguard_public_key=public_key) - - else: # Select last network and try +1 it - last_net = WireGuardVPN.objects.filter(vpnpool=pool).order_by('pool_index').last() - - next_index = last_net.pool_index + 1 - - if next_index <= pool.max_pool_index: - vpn = WireGuardVPN.objects.create(owner=owner, - vpnpool=pool, - pool_index=next_index, - wireguard_public_key=public_key) - - - config = pool.wireguard_config - server = pool.vpn_server_hostname - wg_name = pool.wg_name - - async_task(configure_wireguard_server_on_host, (wg_name, config), queue=server) - - return vpn diff --git a/uncloud_net/tasks.py b/uncloud_net/tasks.py deleted file mode 100644 index 5684871..0000000 --- a/uncloud_net/tasks.py +++ /dev/null @@ -1,74 +0,0 @@ -from .models import * - -import os -import subprocess -import logging -import uuid -from django_q.tasks import async_task, result - -log = logging.getLogger(__name__) - - -def configure_wireguard_server_on_host(wg_name, config): - """ - - Create wireguard config (DB query -> string) - - Submit config to cdist worker - - Change config locally on worker / commit / shared - """ - - # Write config - fname = f"/etc/wireguard/{wg_name}.conf" - with open(fname, "w") as fd: - fd.write(config) - - # Ensure the device exists - subprocess.run(f"ip link show {wg_name} >/dev/null || sudo ip link add {{wg_name}} type wireguard", - shell=True, check=True) - - # Ensure the config is correct - subprocess.run(f"sudo wg setconf {wg_name} {fname}", - shell=True, check=True) - - - -def configure_wireguard_server_via_cdist(wireguardvpnpool): - """ - - Create wireguard config (DB query -> string) - - Submit config to cdist worker - - Change config locally on worker / commit / shared - - """ - - config = wireguardvpnpool.wireguard_config - server = wireguardvpnpool.vpn_server_hostname - - log.info(f"Configuring VPN server {server} (async)") - - async_task(cdist_configure_wireguard_server,config, server).id - - -def cdist_configure_wireguard_server(config, server): - """ - Create config and configure server. - - To be executed on the cdist worker. - """ - - dirname= "/home/app/.cdist/type/__ungleich_wireguard/files/" - fname = os.path.join(dirname,server) - - log.info(f"Configuring VPN server {server} (on cdist host)") - with open(fname, "w") as fd: - fd.write(config) - - log.debug("git committing wireguard changes") - subprocess.run(f"cd {dirname} && git pull && git add {server} && git commit -m 'Updating config for {server}' && git push", - shell=True, check=True) - - log.debug(f"Configuring VPN server {server} with cdist") - subprocess.run(f"cdist config {server}", shell=True, check=True) - - # FIXME: - # ensure logs are on the server - # ensure exit codes are known - return True diff --git a/uncloud_net/templates/uncloud_net/wireguardvpn_form.html b/uncloud_net/templates/uncloud_net/wireguardvpn_form.html deleted file mode 100644 index 1463f41..0000000 --- a/uncloud_net/templates/uncloud_net/wireguardvpn_form.html +++ /dev/null @@ -1,25 +0,0 @@ -{% extends 'uncloud/base.html' %} - -{% block body %} -
-
-
-

-

Create a VPN Network

-

- Create a new wireguard based VPN network. -

- -
- -
-
- {% csrf_token %} - {{ form }} - -
-
-
-
- -{% endblock %} diff --git a/uncloud_net/tests.py b/uncloud_net/tests.py deleted file mode 100644 index 75bdafa..0000000 --- a/uncloud_net/tests.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.test import TestCase -from rest_framework.test import APIRequestFactory, force_authenticate - -from rest_framework.reverse import reverse -from django.contrib.auth import get_user_model -from django.core.exceptions import ValidationError, FieldError - -from .views import * -from .models import * - -from uncloud_pay.models import BillingAddress, Order -from uncloud.models import UncloudNetwork - -class UncloudNetworkTests(TestCase): - def test_invalid_IPv4_network(self): - with self.assertRaises(FieldError): - UncloudNetwork.objects.create(network_address="192.168.1.0", - network_mask=33) - -class VPNTests(TestCase): - def setUp(self): - self.user = get_user_model().objects.create_user('django-test-user', 'noreply@ungleich.ch') - self.admin_user = get_user_model().objects.create_user('django-test-adminuser', - 'noreply-admin@ungleich.ch') - - - - self.admin_user.is_staff = True - self.admin_user.save() - - self.pool_network = '2001:db8::' - self.pool_network2 = '2001:db8:1::' - self.pool_network_size = '48' - self.pool_subnetwork_size = '64' - self.pool_vpn_hostname = 'vpn.example.org' - self.pool_wireguard_private_key = 'MOz8kk0m4jhNtAXlge0qzexZh1MipIhu4HJwtdvZ2EY=' - - self.vpn_wireguard_public_key = 'B2b78eWBIXPMM1x4DDjkCDZepS0qDgcLN3T3PjcgXkY=' - - self.vpnpool = WireGuardVPNPool.objects.get_or_create(network=self.pool_network, - network_size=self.pool_network_size, - subnetwork_size=self.pool_subnetwork_size, - vpn_hostname=self.pool_vpn_hostname, - wireguard_private_key=self.pool_wireguard_private_key - ) - - self.factory = APIRequestFactory() - - - - def tearDown(self): - self.user.delete() - self.admin_user.delete() diff --git a/uncloud_net/views.py b/uncloud_net/views.py deleted file mode 100644 index 8e7e81b..0000000 --- a/uncloud_net/views.py +++ /dev/null @@ -1,63 +0,0 @@ -from django.views.generic.edit import CreateView -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.messages.views import SuccessMessageMixin -from rest_framework.response import Response - -from django.shortcuts import render - -from rest_framework import viewsets, permissions - -from .models import * -from .serializers import * -from .selectors import * -from .services import * -from .forms import * -from .tasks import * - -class WireGuardVPNViewSet(viewsets.ModelViewSet): - serializer_class = WireGuardVPNSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - if self.request.user.is_superuser: - obj = WireGuardVPN.objects.all() - else: - obj = WireGuardVPN.objects.filter(owner=self.request.user) - - return obj - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - vpn = create_wireguard_vpn( - owner=self.request.user, - public_key=serializer.validated_data['wireguard_public_key'], - network_mask=serializer.validated_data['network_mask'] - ) - - return Response(WireGuardVPNSerializer(vpn).data) - - -class WireGuardVPNCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView): - model = WireGuardVPN - - login_url = '/login/' - success_url = '/' - success_message = "%(network) was created successfully" - - form_class = WireGuardVPNForm - - def get_success_message(self, cleaned_data): - return self.success_message % dict(cleaned_data, - the_prefix = self.object.prefix) - -class WireGuardVPNSizes(viewsets.ViewSet): - def list(self, request): - sizes = allowed_vpn_network_reservation_size() - print(sizes) - - sizes = [ { 'size': size } for size in sizes ] - print(sizes) - - return Response(WireGuardVPNSizesSerializer(sizes, many=True).data) diff --git a/uncloud_pay/__init__.py b/uncloud_pay/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/uncloud_pay/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/uncloud_pay/admin.py b/uncloud_pay/admin.py deleted file mode 100644 index cb7b650..0000000 --- a/uncloud_pay/admin.py +++ /dev/null @@ -1,103 +0,0 @@ -from django.contrib import admin -from django.template.response import TemplateResponse -from django.urls import path -from django.shortcuts import render -from django.conf.urls import url - -from hardcopy import bytestring_to_pdf -from django.core.files.temp import NamedTemporaryFile -from django.http import FileResponse -from django.template.loader import render_to_string - -from uncloud_pay.models import * - -class BillRecordInline(admin.TabularInline): - model = BillRecord - -class RecurringPeriodInline(admin.TabularInline): - model = ProductToRecurringPeriod - -class ProductAdmin(admin.ModelAdmin): - inlines = [ RecurringPeriodInline ] - -class BillAdmin(admin.ModelAdmin): - inlines = [ BillRecordInline ] - - def get_urls(self): - """ - Create URLs for PDF view - """ - - info = "%s_%s" % (self.model._meta.app_label, self.model._meta.model_name) - pat = lambda regex, fn: url(regex, self.admin_site.admin_view(fn), name='%s_%s' % (info, fn.__name__)) - - url_patterns = [ - pat(r'^([0-9]+)/as_pdf/$', self.as_pdf), - pat(r'^([0-9]+)/as_html/$', self.as_html), - ] + super().get_urls() - - return url_patterns - - def as_pdf(self, request, object_id): - bill = self.get_object(request, object_id=object_id) - print(bill) - - if bill is None: - raise self._get_404_exception(object_id) - - output_file = NamedTemporaryFile() - bill_html = render_to_string( - "uncloud_pay/bill.html.j2", - { - 'bill': bill, - 'bill_records': bill.billrecord_set.all() - } - ) - - bytestring_to_pdf(bill_html.encode('utf-8'), output_file) - response = FileResponse(output_file, content_type="application/pdf") - response['Content-Disposition'] = f'filename="bill_{bill}.pdf"' - - return response - - def as_html(self, request, object_id): - bill = self.get_object(request, object_id=object_id) - - if bill is None: - raise self._get_404_exception(object_id) - - return render(request, 'uncloud_pay/bill.html.j2', - {'bill': bill, - 'bill_records': bill.billrecord_set.all() - }) - - - bill_html = render_to_string("bill.html.j2", {'bill': bill, - 'bill_records': bill.billrecord_set.all() - }) - - bytestring_to_pdf(bill_html.encode('utf-8'), output_file) - response = FileResponse(output_file, content_type="application/pdf") - - response['Content-Disposition'] = f'filename="bill_{bill}.pdf"' - - return HttpResponse(template.render(context, request)) - return response - - -admin.site.register(Bill, BillAdmin) -admin.site.register(Product, ProductAdmin) - -for m in [ - BillingAddress, - Order, - BillRecord, - Payment, - ProductToRecurringPeriod, - RecurringPeriod, - StripeCreditCard, - StripeCustomer, - PricingPlan, - VATRate -]: - admin.site.register(m) diff --git a/uncloud_pay/management/commands/.gitignore b/uncloud_pay/management/commands/.gitignore deleted file mode 100644 index cf5c7fa..0000000 --- a/uncloud_pay/management/commands/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Customer tests -customer-*.py diff --git a/uncloud_pay/management/commands/add-opennebula-vm-orders.py b/uncloud_pay/management/commands/add-opennebula-vm-orders.py deleted file mode 100644 index 1d66790..0000000 --- a/uncloud_pay/management/commands/add-opennebula-vm-orders.py +++ /dev/null @@ -1,140 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model - -from django.utils import timezone -from datetime import datetime, timedelta - -from uncloud_pay.models import * -from uncloud_vm.models import * - -import sys - -def vm_price_2020(cpu=1, ram=2, v6only=False): - if v6only: - discount = 9 - else: - discount = 0 - - return cpu*3 + ram*4 - discount - -def disk_price_2020(size_in_gb, disk_type): - if disk_type == VMDiskType.CEPH_SSD: - price = 3.5/10 - elif disk_type == VMDiskType.CEPH_HDD: - price = 1.5/100 - else: - raise Exception("not yet defined price") - - return size_in_gb * price - -class Command(BaseCommand): - help = 'Adding VMs / creating orders for user' - - def add_arguments(self, parser): - parser.add_argument('--username', type=str, required=True) - - def handle(self, *args, **options): - user = get_user_model().objects.get(username=options['username']) - - addr, created = BillingAddress.objects.get_or_create( - owner=user, - active=True, - defaults={'organization': 'Undefined organisation', - 'name': 'Undefined name', - 'street': 'Undefined Street', - 'city': 'Undefined city', - 'postal_code': '8750', - 'country': 'CH', - 'active': True - } - ) - - # 25206 + SSD - vm25206 = VMProduct.objects.create(name="one-25206", cores=1, ram_in_gb=4, owner=user) - vm25206.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) - - # vm25206_ssd = VMDiskProduct.objects.create(vm=vm25206, owner=user, size_in_gb=30) - # vm25206_ssd.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) - - # change 1 - vm25206.cores = 2 - vm25206.ram_in_gb = 8 - vm25206.save() - vm25206.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,4,17))) - - sys.exit(0) - - # change 2 - # vm25206_ssd.size_in_gb = 50 - # vm25206_ssd.save() - # vm25206_ssd.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,8,5))) - - # 25206 done. - - # 25615 - vm25615 = VMProduct.objects.create(name="one-25615", cores=1, ram_in_gb=4, owner=user) - vm25615.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) - - # Change 2020-04-17 - vm25615.cores = 2 - vm25615.ram_in_gb = 8 - vm25615.save() - vm25615.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,4,17))) - - # vm25615_ssd = VMDiskProduct(vm=vm25615, owner=user, size_in_gb=30) - # vm25615_ssd.create_order_at(timezone.make_aware(datetime.datetime(2020,3,3))) - # vm25615_ssd.save() - - vm25208 = VMProduct.objects.create(name="one-25208", cores=1, ram_in_gb=4, owner=user) - vm25208.create_order_at(timezone.make_aware(datetime.datetime(2020,3,5))) - - vm25208.cores = 2 - vm25208.ram_in_gb = 8 - vm25208.save() - vm25208.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,4,17))) - - Bill.create_next_bills_for_user(user, ending_date=end_of_month(timezone.make_aware(datetime.datetime(2020,7,31)))) - - sys.exit(0) - - - vm25615_ssd.size_in_gb = 50 - vm25615_ssd.save() - vm25615_ssd.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,8,5))) - - - - vm25208_ssd = VMDiskProduct.objects.create(vm=vm25208, - owner=user, - size_in_gb=30) - - - - vm25208_ssd.size_in_gb = 50 - vm25208_ssd.save() - vm25208_ssd.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,8,5))) - - - # 25207 - vm25207 = VMProduct.objects.create(name="OpenNebula 25207", - cores=1, - ram_in_gb=4, - owner=user) - - vm25207_ssd = VMDiskProduct.objects.create(vm=vm25207, - owner=user, - size_in_gb=30) - - vm25207_ssd.size_in_gb = 50 - vm25207_ssd.save() - vm25207_ssd.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,8,5))) - - - vm25207.cores = 2 - vm25207.ram_in_gb = 8 - vm25207.save() - vm25207.create_or_update_order(when_to_start=timezone.make_aware(datetime.datetime(2020,6,19))) - - - # FIXES: check starting times (they are slightly different) - # add vm 25236 diff --git a/uncloud_pay/management/commands/bootstrap-user.py b/uncloud_pay/management/commands/bootstrap-user.py deleted file mode 100644 index b78e80c..0000000 --- a/uncloud_pay/management/commands/bootstrap-user.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.core.management.base import BaseCommand -from django.contrib.auth import get_user_model -import datetime - -from uncloud_pay.models import * - -class Command(BaseCommand): - help = 'Bootstrap user (for testing)' - - def add_arguments(self, parser): - parser.add_argument('--username', type=str, required=True) - - def handle(self, *args, **options): - user = get_user_model().objects.get(username=options['username']) - - addr = BillingAddress.objects.get_or_create( - owner=user, - active=True, - defaults={'organization': 'ungleich', - 'name': 'Nico Schottelius', - 'street': 'Hauptstrasse 14', - 'city': 'Luchsingen', - 'postal_code': '8775', - 'country': 'CH' } - ) - - - bills = Bill.objects.filter(owner=user) - - # not even one bill? create! - if bills: - bill = bills[0] - else: - bill = Bill.objects.create(owner=user) - - # find any order that is associated to this bill - orders = Order.objects.filter(owner=user) - - print(f"Addr: {addr}") - print(f"Bill: {bill}") diff --git a/uncloud_pay/management/commands/import-vat-rates.py b/uncloud_pay/management/commands/import-vat-rates.py deleted file mode 100644 index a741740..0000000 --- a/uncloud_pay/management/commands/import-vat-rates.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.core.management.base import BaseCommand -from uncloud_pay.models import VATRate - -import logging -import urllib -import csv -import sys -import io - -logger = logging.getLogger(__name__) - -class Command(BaseCommand): - help = '''Imports VAT Rates. Assume vat rates of format https://github.com/kdeldycke/vat-rates/blob/master/vat_rates.csv''' - vat_url = "https://raw.githubusercontent.com/ungleich/vat-rates/main/vat_rates.csv" - - - def add_arguments(self, parser): - parser.add_argument('--vat-url', default=self.vat_url) - - def handle(self, *args, **options): - vat_url = options['vat_url'] - url_open = urllib.request.urlopen(vat_url) - - # map to fileio using stringIO - csv_file = io.StringIO(url_open.read().decode('utf-8')) - reader = csv.DictReader(csv_file) - - for row in reader: - if row["territory_codes"] and len(row["territory_codes"].splitlines()) > 1: - for code in row["territory_codes"].splitlines(): - VATRate.objects.get_or_create( - starting_date=row["start_date"], - ending_date=row["stop_date"] if row["stop_date"] != "" else None, - territory_codes=code, - currency_code=row["currency_code"], - rate=row["rate"], - rate_type=row["rate_type"], - description=row["description"] - ) - else: - VATRate.objects.get_or_create( - starting_date=row["start_date"], - ending_date=row["stop_date"] if row["stop_date"] != "" else None, - territory_codes=row["territory_codes"], - currency_code=row["currency_code"], - rate=row["rate"], - rate_type=row["rate_type"], - description=row["description"] - ) - logger.info('All VAT Rates have been added!') diff --git a/uncloud_pay/migrations/0001_initial.py b/uncloud_pay/migrations/0001_initial.py deleted file mode 100644 index e65f3dd..0000000 --- a/uncloud_pay/migrations/0001_initial.py +++ /dev/null @@ -1,202 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 22:19 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone -import uncloud.models -import uncloud_pay.models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_auth', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='Bill', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(auto_now_add=True)), - ('starting_date', models.DateTimeField(default=uncloud_pay.models.start_of_this_month)), - ('ending_date', models.DateTimeField()), - ('due_date', models.DateField(default=uncloud_pay.models.default_payment_delay)), - ('is_final', models.BooleanField(default=False)), - ], - ), - migrations.CreateModel( - name='BillingAddress', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('full_name', models.CharField(max_length=256)), - ('organization', models.CharField(blank=True, max_length=256, null=True)), - ('street', models.CharField(max_length=256)), - ('city', models.CharField(max_length=256)), - ('postal_code', models.CharField(max_length=64)), - ('country', uncloud.models.CountryField(blank=True, choices=[('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')], default='CH', max_length=2)), - ('vat_number', models.CharField(blank=True, default='', max_length=100)), - ('active', models.BooleanField(default=False)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=256, unique=True)), - ('description', models.CharField(max_length=1024)), - ('config', models.JSONField()), - ('currency', models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32)), - ], - ), - migrations.CreateModel( - name='RecurringPeriod', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100, unique=True)), - ('duration_seconds', models.IntegerField(unique=True)), - ], - ), - migrations.CreateModel( - name='StripeCustomer', - fields=[ - ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to='uncloud_auth.user')), - ('stripe_id', models.CharField(max_length=32)), - ], - ), - migrations.CreateModel( - name='VATRate', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('starting_date', models.DateField(blank=True, null=True)), - ('ending_date', models.DateField(blank=True, null=True)), - ('territory_codes', models.TextField(blank=True, default='')), - ('currency_code', models.CharField(max_length=10)), - ('rate', models.FloatField()), - ('rate_type', models.TextField(blank=True, default='')), - ('description', models.TextField(blank=True, default='')), - ], - ), - migrations.CreateModel( - name='StripeCreditCard', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('card_name', models.CharField(default='My credit card', max_length=128)), - ('card_id', models.CharField(max_length=32)), - ('last4', models.CharField(max_length=4)), - ('brand', models.CharField(max_length=64)), - ('expiry_date', models.DateField()), - ('owner', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='ProductToRecurringPeriod', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_default', models.BooleanField(default=False)), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product')), - ('recurring_period', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod')), - ], - ), - migrations.AddField( - model_name='product', - name='recurring_periods', - field=models.ManyToManyField(through='uncloud_pay.ProductToRecurringPeriod', to='uncloud_pay.RecurringPeriod'), - ), - migrations.CreateModel( - name='PaymentMethod', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('source', models.CharField(choices=[('stripe', 'Stripe'), ('unknown', 'Unknown')], default='stripe', max_length=256)), - ('description', models.TextField()), - ('primary', models.BooleanField(default=False, editable=False)), - ('stripe_payment_method_id', models.CharField(blank=True, max_length=32, null=True)), - ('stripe_setup_intent_id', models.CharField(blank=True, max_length=32, null=True)), - ('owner', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Payment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('source', models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral'), ('unknown', 'Unknown')], default='unknown', max_length=256)), - ('timestamp', models.DateTimeField(auto_now_add=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('description', models.TextField()), - ('config', models.JSONField()), - ('creation_date', models.DateTimeField(auto_now_add=True)), - ('starting_date', models.DateTimeField(default=django.utils.timezone.now)), - ('ending_date', models.DateTimeField(blank=True, null=True)), - ('one_time_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('recurring_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('currency', models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32)), - ('should_be_billed', models.BooleanField(default=True)), - ('billing_address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.billingaddress')), - ('depends_on', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='parent_of', to='uncloud_pay.order')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.product')), - ('recurring_period', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.recurringperiod')), - ('replaces', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='replaced_by', to='uncloud_pay.order')), - ], - ), - migrations.CreateModel( - name='Membership', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('starting_date', models.DateField(blank=True, null=True)), - ('ending_date', models.DateField(blank=True, null=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='BillRecord', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('creation_date', models.DateTimeField(auto_now_add=True)), - ('starting_date', models.DateTimeField()), - ('ending_date', models.DateTimeField()), - ('is_recurring_record', models.BooleanField()), - ('bill', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.bill')), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.order')), - ], - ), - migrations.AddField( - model_name='bill', - name='billing_address', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.billingaddress'), - ), - migrations.AddField( - model_name='bill', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - migrations.AddConstraint( - model_name='producttorecurringperiod', - constraint=models.UniqueConstraint(condition=models.Q(is_default=True), fields=('product',), name='one_default_recurring_period_per_product'), - ), - migrations.AddConstraint( - model_name='producttorecurringperiod', - constraint=models.UniqueConstraint(fields=('product', 'recurring_period'), name='recurring_period_once_per_product'), - ), - migrations.AddConstraint( - model_name='billingaddress', - constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('owner',), name='one_active_billing_address_per_user'), - ), - migrations.AddConstraint( - model_name='bill', - constraint=models.UniqueConstraint(fields=('owner', 'starting_date', 'ending_date'), name='one_bill_per_month_per_user'), - ), - ] diff --git a/uncloud_pay/migrations/0002_auto_20201228_2244.py b/uncloud_pay/migrations/0002_auto_20201228_2244.py deleted file mode 100644 index 4665553..0000000 --- a/uncloud_pay/migrations/0002_auto_20201228_2244.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 22:44 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='payment', - name='currency', - field=models.CharField(choices=[('CHF', 'Swiss Franc'), ('EUR', 'Euro'), ('USD', 'US Dollar')], default='CHF', max_length=32), - ), - migrations.AlterField( - model_name='payment', - name='amount', - field=models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)]), - ), - ] diff --git a/uncloud_pay/migrations/0003_auto_20201228_2256.py b/uncloud_pay/migrations/0003_auto_20201228_2256.py deleted file mode 100644 index b516bd5..0000000 --- a/uncloud_pay/migrations/0003_auto_20201228_2256.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 22:56 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0002_auto_20201228_2244'), - ] - - operations = [ - migrations.AlterField( - model_name='order', - name='currency', - field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), - ), - migrations.AlterField( - model_name='payment', - name='currency', - field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), - ), - migrations.AlterField( - model_name='product', - name='currency', - field=models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32), - ), - ] diff --git a/uncloud_pay/migrations/0004_stripecreditcard_active.py b/uncloud_pay/migrations/0004_stripecreditcard_active.py deleted file mode 100644 index 3fb8015..0000000 --- a/uncloud_pay/migrations/0004_stripecreditcard_active.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 23:34 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0003_auto_20201228_2256'), - ] - - operations = [ - migrations.AddField( - model_name='stripecreditcard', - name='active', - field=models.BooleanField(default=True), - ), - ] diff --git a/uncloud_pay/migrations/0005_auto_20201228_2335.py b/uncloud_pay/migrations/0005_auto_20201228_2335.py deleted file mode 100644 index 814752e..0000000 --- a/uncloud_pay/migrations/0005_auto_20201228_2335.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 23:35 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0004_stripecreditcard_active'), - ] - - operations = [ - migrations.AlterField( - model_name='stripecreditcard', - name='active', - field=models.BooleanField(default=False), - ), - ] diff --git a/uncloud_pay/migrations/0006_auto_20201228_2337.py b/uncloud_pay/migrations/0006_auto_20201228_2337.py deleted file mode 100644 index a164767..0000000 --- a/uncloud_pay/migrations/0006_auto_20201228_2337.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 23:37 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_pay', '0005_auto_20201228_2335'), - ] - - operations = [ - migrations.AlterField( - model_name='stripecreditcard', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/uncloud_pay/migrations/0007_auto_20201228_2338.py b/uncloud_pay/migrations/0007_auto_20201228_2338.py deleted file mode 100644 index 315a74b..0000000 --- a/uncloud_pay/migrations/0007_auto_20201228_2338.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 3.1 on 2020-12-28 23:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0006_auto_20201228_2337'), - ] - - operations = [ - migrations.AddConstraint( - model_name='stripecreditcard', - constraint=models.UniqueConstraint(condition=models.Q(active=True), fields=('owner',), name='one_active_card_per_user'), - ), - ] diff --git a/uncloud_pay/migrations/0008_payment_external_reference.py b/uncloud_pay/migrations/0008_payment_external_reference.py deleted file mode 100644 index 0de20b6..0000000 --- a/uncloud_pay/migrations/0008_payment_external_reference.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2020-12-29 00:04 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0007_auto_20201228_2338'), - ] - - operations = [ - migrations.AddField( - model_name='payment', - name='external_reference', - field=models.CharField(default='', max_length=256), - ), - ] diff --git a/uncloud_pay/migrations/0009_auto_20201229_0037.py b/uncloud_pay/migrations/0009_auto_20201229_0037.py deleted file mode 100644 index fc195e4..0000000 --- a/uncloud_pay/migrations/0009_auto_20201229_0037.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.1 on 2020-12-29 00:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0008_payment_external_reference'), - ] - - operations = [ - migrations.AlterField( - model_name='payment', - name='external_reference', - field=models.CharField(blank=True, default='', max_length=256, null=True), - ), - ] diff --git a/uncloud_pay/migrations/0010_auto_20201229_0042.py b/uncloud_pay/migrations/0010_auto_20201229_0042.py deleted file mode 100644 index 6dd6a60..0000000 --- a/uncloud_pay/migrations/0010_auto_20201229_0042.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1 on 2020-12-29 00:42 - -from django.db import migrations, models -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0009_auto_20201229_0037'), - ] - - operations = [ - migrations.AlterField( - model_name='payment', - name='timestamp', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - ] diff --git a/uncloud_pay/migrations/0011_auto_20210101_1308.py b/uncloud_pay/migrations/0011_auto_20210101_1308.py deleted file mode 100644 index 942f430..0000000 --- a/uncloud_pay/migrations/0011_auto_20210101_1308.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 3.1 on 2021-01-01 13:08 - -from django.db import migrations -import uncloud.models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0010_auto_20201229_0042'), - ] - - operations = [ - migrations.AlterField( - model_name='billingaddress', - name='country', - field=uncloud.models.CountryField(choices=[('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')], default='CH', max_length=2), - ), - ] diff --git a/uncloud_pay/migrations/0012_auto_20210630_0742.py b/uncloud_pay/migrations/0012_auto_20210630_0742.py deleted file mode 100644 index 45e3dfe..0000000 --- a/uncloud_pay/migrations/0012_auto_20210630_0742.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.4 on 2021-06-30 07:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0011_auto_20210101_1308'), - ] - - operations = [ - migrations.AddField( - model_name='billingaddress', - name='vat_number_verified', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='payment', - name='source', - field=models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral')], max_length=256), - ), - ] diff --git a/uncloud_pay/migrations/0013_alter_billingaddress_owner.py b/uncloud_pay/migrations/0013_alter_billingaddress_owner.py deleted file mode 100644 index 7597129..0000000 --- a/uncloud_pay/migrations/0013_alter_billingaddress_owner.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-03 15:23 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_pay', '0012_auto_20210630_0742'), - ] - - operations = [ - migrations.AlterField( - model_name='billingaddress', - name='owner', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='billing_addresses', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/uncloud_pay/migrations/0014_auto_20210703_1747.py b/uncloud_pay/migrations/0014_auto_20210703_1747.py deleted file mode 100644 index 1c004d0..0000000 --- a/uncloud_pay/migrations/0014_auto_20210703_1747.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-03 17:47 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0013_alter_billingaddress_owner'), - ] - - operations = [ - migrations.AddField( - model_name='billingaddress', - name='stripe_tax_id', - field=models.CharField(blank=True, default='', max_length=100), - ), - migrations.AddField( - model_name='billingaddress', - name='vat_number_validated_on', - field=models.DateTimeField(blank=True, null=True), - ), - ] diff --git a/uncloud_pay/migrations/0015_auto_20210705_0849.py b/uncloud_pay/migrations/0015_auto_20210705_0849.py deleted file mode 100644 index dfb6d80..0000000 --- a/uncloud_pay/migrations/0015_auto_20210705_0849.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-05 08:49 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0014_auto_20210703_1747'), - ] - - operations = [ - migrations.AddField( - model_name='order', - name='customer', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='uncloud_pay.stripecustomer'), - ), - migrations.AddField( - model_name='order', - name='status', - field=models.CharField(choices=[('draft', 'Draft'), ('declined', 'Declined'), ('approved', 'Approved')], default='draft', max_length=100), - ), - migrations.AddField( - model_name='order', - name='stripe_charge_id', - field=models.CharField(max_length=100, null=True), - ), - migrations.AddField( - model_name='order', - name='vm_id', - field=models.IntegerField(default=0), - ), - ] diff --git a/uncloud_pay/migrations/0016_pricingplan.py b/uncloud_pay/migrations/0016_pricingplan.py deleted file mode 100644 index 505c141..0000000 --- a/uncloud_pay/migrations/0016_pricingplan.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-06 13:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0015_auto_20210705_0849'), - ] - - operations = [ - migrations.CreateModel( - name='PricingPlan', - fields=[ - ('id', models.AutoField(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=2, default=0, max_digits=7)), - ('cores_unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=7)), - ('ram_unit_price', models.DecimalField(decimal_places=2, default=0, max_digits=7)), - ('storage_unit_price', models.DecimalField(decimal_places=2, 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)), - ], - ), - ] diff --git a/uncloud_pay/migrations/0017_auto_20210706_1728.py b/uncloud_pay/migrations/0017_auto_20210706_1728.py deleted file mode 100644 index 1571b10..0000000 --- a/uncloud_pay/migrations/0017_auto_20210706_1728.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-06 17:28 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0016_pricingplan'), - ] - - operations = [ - migrations.RemoveField( - model_name='paymentmethod', - name='owner', - ), - migrations.DeleteModel( - name='Payment', - ), - migrations.DeleteModel( - name='PaymentMethod', - ), - ] diff --git a/uncloud_pay/migrations/0018_payment.py b/uncloud_pay/migrations/0018_payment.py deleted file mode 100644 index 47d6e3a..0000000 --- a/uncloud_pay/migrations/0018_payment.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-06 17:47 - -from django.conf import settings -import django.core.validators -from django.db import migrations, models -import django.db.models.deletion -import django.utils.timezone - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('uncloud_pay', '0017_auto_20210706_1728'), - ] - - operations = [ - migrations.CreateModel( - name='Payment', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(0)])), - ('source', models.CharField(choices=[('wire', 'Wire Transfer'), ('stripe', 'Stripe'), ('voucher', 'Voucher'), ('referral', 'Referral')], max_length=256)), - ('timestamp', models.DateTimeField(default=django.utils.timezone.now)), - ('currency', models.CharField(choices=[('CHF', 'Swiss Franc')], default='CHF', max_length=32)), - ('external_reference', models.CharField(blank=True, default='', max_length=256, null=True)), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - ] diff --git a/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py b/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py deleted file mode 100644 index f3419eb..0000000 --- a/uncloud_pay/migrations/0020_rename_is_final_bill_is_closed.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-07 20:18 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0019_order_pricing_plan'), - ] - - operations = [ - migrations.RenameField( - model_name='bill', - old_name='is_final', - new_name='is_closed', - ), - ] diff --git a/uncloud_pay/migrations/0021_auto_20210709_0914.py b/uncloud_pay/migrations/0021_auto_20210709_0914.py deleted file mode 100644 index 66e3dcb..0000000 --- a/uncloud_pay/migrations/0021_auto_20210709_0914.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 3.2.4 on 2021-07-09 09:14 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('uncloud_pay', '0020_rename_is_final_bill_is_closed'), - ] - - operations = [ - migrations.RemoveField( - model_name='order', - name='stripe_charge_id', - ), - migrations.RemoveField( - model_name='order', - name='vm_id', - ), - ] diff --git a/uncloud_pay/models.py b/uncloud_pay/models.py deleted file mode 100644 index a4cf007..0000000 --- a/uncloud_pay/models.py +++ /dev/null @@ -1,1197 +0,0 @@ -import logging -import datetime -import json - -from math import ceil -from calendar import monthrange -from decimal import Decimal - -from django.conf import settings -from django.contrib.auth import get_user_model -from django.core.validators import MinValueValidator -from django.db import models -from django.db.models import Q -from django.utils.translation import gettext_lazy as _ -from django.utils import timezone -from django_q.tasks import schedule -from django_q.models import Schedule -# Verify whether or not to use them here -from django.core.exceptions import ObjectDoesNotExist, ValidationError - -import uncloud_pay -from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS -from uncloud.models import UncloudAddress, UncloudProvider -from uncloud.selectors import filter_for_when -from .services import * - -# Used to generate bill due dates. -BILL_PAYMENT_DELAY=datetime.timedelta(days=settings.BILL_PAYMENT_DELAY) - -# Initialize logger. -logger = logging.getLogger(__name__) - -def default_payment_delay(): - return timezone.now() + BILL_PAYMENT_DELAY - -class Currency(models.TextChoices): - """ - Possible currencies to be billed - """ - CHF = 'CHF', _('Swiss Franc') -# EUR = 'EUR', _('Euro') -# USD = 'USD', _('US Dollar') - - -### -# Stripe - -class StripeCustomer(models.Model): - owner = models.OneToOneField( get_user_model(), - primary_key=True, - on_delete=models.CASCADE) - stripe_id = models.CharField(max_length=32) - - def __str__(self): - return self.owner.username - - -class StripeCreditCard(models.Model): - owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - - card_name = models.CharField(null=False, max_length=128, default="My credit card") - card_id = models.CharField(null=False, max_length=32) - last4 = models.CharField(null=False, max_length=4) - brand = models.CharField(null=False, max_length=64) - expiry_date = models.DateField(null=False) - active = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['owner'], - condition=models.Q(active=True), - name='one_active_card_per_user') - ] - - - def __str__(self): - return f"{self.card_name}: {self.brand} {self.last4} ({self.expiry_date})" - -class Payment(models.Model): - owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) - - amount = models.DecimalField( - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - source = models.CharField(max_length=256, - choices = ( - ('wire', 'Wire Transfer'), - ('stripe', 'Stripe'), - ('voucher', 'Voucher'), - ('referral', 'Referral'), - )) - - timestamp = models.DateTimeField(default=timezone.now) - - currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) - - external_reference = models.CharField(max_length=256, default="", null=True, blank=True) - - def __str__(self): - return f"{self.amount}{self.currency} from {self.owner} via {self.source} on {self.timestamp}" - - def save(self, *args, **kwargs): - # Try to charge the user via the active card before saving otherwise throw payment Error - if self.source == 'stripe': - try: - result = uncloud_pay.stripe.charge_customer(self.owner, self.amount, self.currency,) - if not result.status or result.status != 'succeeded': - raise Exception("The payment has been failed, please try to activate another card") - super().save(*args, **kwargs) - except Exception as e: - raise e - - - -# See https://docs.djangoproject.com/en/dev/ref/models/fields/#field-choices-enum-types -class RecurringPeriodDefaultChoices(models.IntegerChoices): - """ - This is an old class and being superseeded by the database model below - """ - PER_365D = 365*24*3600, _('Per 365 days') - PER_30D = 30*24*3600, _('Per 30 days') - PER_WEEK = 7*24*3600, _('Per Week') - PER_DAY = 24*3600, _('Per Day') - PER_HOUR = 3600, _('Per Hour') - PER_MINUTE = 60, _('Per Minute') - PER_SECOND = 1, _('Per Second') - ONE_TIME = 0, _('Onetime') - -# RecurringPeriods -class RecurringPeriod(models.Model): - """ - Available recurring periods. - By default seeded from RecurringPeriodChoices - """ - - name = models.CharField(max_length=100, unique=True) - duration_seconds = models.IntegerField(unique=True) - - @classmethod - def populate_db_defaults(cls): - for (seconds, name) in RecurringPeriodDefaultChoices.choices: - obj, created = cls.objects.get_or_create(name=name, - defaults={ 'duration_seconds': seconds }) - - @staticmethod - def secs_to_name(secs): - name = "" - days = 0 - hours = 0 - - if secs > 24*3600: - days = secs // (24*3600) - secs -= (days*24*3600) - - if secs > 3600: - hours = secs // 3600 - secs -= hours*3600 - - return f"{days} days {hours} hours {secs} seconds" - - def __str__(self): - duration = self.secs_to_name(self.duration_seconds) - - return f"{self.name} ({duration})" - - -### -# Bills. - -class BillingAddress(UncloudAddress): - owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name='billing_addresses') - vat_number = models.CharField(max_length=100, default="", blank=True) - vat_number_verified = models.BooleanField(default=False) - vat_number_validated_on = models.DateTimeField(blank=True, null=True) - stripe_tax_id = models.CharField(max_length=100, default="", blank=True) - active = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['owner'], - condition=models.Q(active=True), - name='one_active_billing_address_per_user') - ] - - @classmethod - def populate_db_defaults(cls): - """ - Ensure we have at least one billing address that is associated with the uncloud-admin. - - This way we are sure that an UncloudProvider can be created. - - Cannot use get_or_create as that looks for exactly one. - - """ - - owner = get_user_model().objects.get(username=settings.UNCLOUD_ADMIN_NAME) - billing_address = cls.objects.filter(owner=owner).first() - - if not billing_address: - billing_address = cls.objects.create(owner=owner, - organization="uncloud admins", - full_name="Uncloud Admin", - street="Uncloudstreet. 42", - city="Luchsingen", - postal_code="8775", - country="CH", - active=True) - - def __str__(self): - return "{} - {}, {}, {} {}, {}".format( - self.owner, - self.full_name, self.street, self.postal_code, self.city, - self.country) - - @staticmethod - def get_address_for(user): - return BillingAddress.objects.get(owner=user) - -### -# VAT - -class VATRate(models.Model): - starting_date = models.DateField(blank=True, null=True) - ending_date = models.DateField(blank=True, null=True) - territory_codes = models.TextField(blank=True, default='') - currency_code = models.CharField(max_length=10) - rate = models.FloatField() - rate_type = models.TextField(blank=True, default='') - description = models.TextField(blank=True, default='') - - @staticmethod - def get_for_country(country_code): - vat_rate = None - try: - vat_rate = VATRate.objects.get( - territory_codes=country_code, start_date__isnull=False, stop_date=None - ) - return vat_rate.rate - except VATRate.DoesNotExist as dne: - logger.debug(str(dne)) - logger.debug("Did not find VAT rate for %s, returning 0" % country_code) - return 0 - - @staticmethod - def get_vat_rate(billing_address, when=None): - """ - Returns the VAT rate for business to customer. - - B2B is always 0% with the exception of trading within the own country - """ - - country = billing_address.country - - # Need to have a provider country - providers = UncloudProvider.objects.all() - vatrate = filter_for_when(VATRate.objects.filter(territory_codes=country), when).first() - - if not providers and not vatrate: - return 0 - - uncloud_provider = filter_for_when(providers).get() - - # By default we charge VAT. This affects: - # - Same country sales (VAT applied) - # - B2C to EU (VAT applied) - rate = vatrate.rate if vatrate else 0 - - # Exception: if... - # - the billing_address is in EU, - # - the vat_number has been set - # - the vat_number has been verified - # Then we do not charge VAT - - if uncloud_provider.country != country and billing_address.vat_number and billing_address.vat_number_verified: - rate = 0 - return rate - - - def __str__(self): - return f"{self.territory_codes}: {self.starting_date} - {self.ending_date or ''}: {self.rate_type}" - -### -# Products - -class Product(models.Model): - """ - A product is something a user can order. To record the pricing, we - create order that define a state in time. - - A product can have *one* one_time_order and/or *one* - recurring_order. - - If either of them needs to be updated, a new order of the same - type will be created and links to the previous order. - - """ - - name = models.CharField(max_length=256, unique=True) - description = models.CharField(max_length=1024) - config = models.JSONField() - recurring_periods = models.ManyToManyField(RecurringPeriod, through='ProductToRecurringPeriod') - currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) - - @property - def default_recurring_period(self): - """ - Return the default recurring Period - """ - return self.recurring_periods.get(producttorecurringperiod__is_default=True) - - @classmethod - def populate_db_defaults(cls): - recurring_period = RecurringPeriod.objects.get(name="Per 30 days") - - obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v1", - description="A standard virtual machine", - currency=Currency.CHF, - config={ - 'features': { - 'cores': - { 'min': 1, - 'max': 48 - }, - 'ram_gb': - { 'min': 1, - 'max': 256 - }, - 'ssd_gb': - { 'min': 10 - }, - 'hdd_gb': - { 'min': 0, - }, - 'additional_ipv4_address': - { 'min': 0, - }, - } - } - ) - - obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True }) - - obj, created = cls.objects.get_or_create(name="Dual Stack Virtual Machine v2", - description="A standard virtual machine", - currency=Currency.CHF, - config={ - 'features': { - 'base': - { 'min': 1, - 'max': 1, - }, - 'cores': - { 'min': 1, - 'max': 48, - }, - 'ram_gb': - { 'min': 1, - 'max': 256, - }, - 'ssd_gb': - { 'min': 10 - }, - 'hdd_gb': - { 'min': 0 - }, - 'additional_ipv4_address': - { 'min': 0,}, - } - } - ) - - obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True }) - - obj, created = cls.objects.get_or_create(name="reverse DNS", - description="Reverse DNS network", - currency=Currency.CHF, - config={ - 'parameters': [ - 'network' - ] - }) - obj.recurring_periods.add(recurring_period, through_defaults= { 'is_default': True }) - - - def __str__(self): - return f"{self.name} - {self.description}" - - @property - def recurring_orders(self): - return self.orders.order_by('id').exclude(recurring_price=0) - - @property - def last_recurring_order(self): - return self.recurring_orders.last() - - @property - def one_time_orders(self): - return self.orders.order_by('id').filter(recurring_price=0) - - @property - def last_one_time_order(self): - return self.one_time_orders.last() - - # FIXME: this could/should be part of Order (?) - def create_or_update_recurring_order(self, when_to_start=None, recurring_period=None): - if not self.recurring_price: - return - - if not recurring_period: - recurring_period = self.default_recurring_period - - if not when_to_start: - when_to_start = timezone.now() - - if self.last_recurring_order: - if self.recurring_price < self.last_recurring_order.price: - - if when_to_start < self.last_recurring_order.next_cancel_or_downgrade_date: - when_to_start = start_after(self.last_recurring_order.next_cancel_or_downgrade_date) - - when_to_end = end_before(when_to_start) - - new_order = Order.objects.create(owner=self.owner, - billing_address=self.last_recurring_order.billing_address, - starting_date=when_to_start, - price=self.recurring_price, - recurring_period=recurring_period, - description=str(self), - replaces=self.last_recurring_order) - - self.last_recurring_order.replace_with(new_order) - self.orders.add(new_order) - else: - self.create_order(when_to_start, recurring_period) - - @property - def is_recurring(self): - return self.recurring_price > 0 - - @property - def billing_address(self): - return self.order.billing_address - - def discounted_price_by_period(self, requested_period): - """ - Each product has a standard recurring period for which - we define a pricing. I.e. VPN is usually year, VM is usually monthly. - - The user can opt-in to use a different period, which influences the price: - The longer a user commits, the higher the discount. - - Products can also be limited in the available periods. For instance - a VPN only makes sense to be bought for at least one day. - - Rules are as follows: - - given a standard recurring period of ..., changing to ... modifies price ... - - - # One month for free if buying / year, compared to a month: about 8.33% discount - per_year -> per_month -> /11 - per_month -> per_year -> *11 - - # Month has 30.42 days on average. About 7.9% discount to go monthly - per_month -> per_day -> /28 - per_day -> per_month -> *28 - - # Day has 24h, give one for free - per_day -> per_hour -> /23 - per_hour -> per_day -> /23 - - - Examples - - VPN @ 120CHF/y becomes - - 10.91 CHF/month (130.91 CHF/year) - - 0.39 CHF/day (142.21 CHF/year) - - VM @ 15 CHF/month becomes - - 165 CHF/month (13.75 CHF/month) - - 0.54 CHF/day (16.30 CHF/month) - - """ - - # FIXME: This logic needs to be phased out / replaced by product specific (?) - # proportions. Maybe using the RecurringPeriod table to link the possible discounts/add ups - - if self.default_recurring_period == RecurringPeriod.PER_365D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price/11. - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/11./28. - - elif self.default_recurring_period == RecurringPeriod.PER_30D: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price/28. - - elif self.default_recurring_period == RecurringPeriod.PER_DAY: - if requested_period == RecurringPeriod.PER_365D: - return self.recurring_price*11*28 - if requested_period == RecurringPeriod.PER_30D: - return self.recurring_price*28 - if requested_period == RecurringPeriod.PER_DAY: - return self.recurring_price - else: - # FIXME: use the right type of exception here! - raise Exception("Did not implement the discounter for this case") - - - def save(self, *args, **kwargs): - # try: - # ba = BillingAddress.get_address_for(self.owner) - # except BillingAddress.DoesNotExist: - # raise ValidationError("User does not have a billing address") - - # if not ba.active: - # raise ValidationError("User does not have an active billing address") - - - # Verify the required JSON fields - - super().save(*args, **kwargs) - - -### -# Pricing -###### -import logging - -from django.db import models - -logger = logging.getLogger(__name__) - -class PricingPlan(models.Model): - name = models.CharField(max_length=255, unique=True) - vat_inclusive = models.BooleanField(default=True) - vat_percentage = models.DecimalField( - max_digits=7, decimal_places=5, blank=True, default=0 - ) - set_up_fees = models.DecimalField( - max_digits=7, decimal_places=2, default=0 - ) - cores_unit_price = models.DecimalField( - max_digits=7, decimal_places=2, default=0 - ) - ram_unit_price = models.DecimalField( - max_digits=7, decimal_places=2, default=0 - ) - storage_unit_price = models.DecimalField( - max_digits=7, decimal_places=2, default=0 - ) - discount_name = models.CharField(max_length=255, null=True, blank=True) - discount_amount = models.DecimalField( - max_digits=6, decimal_places=2, default=0 - ) - stripe_coupon_id = models.CharField(max_length=255, null=True, blank=True) - - def __str__(self): - display_str = self.name + ' => ' + ' - '.join([ - '{} Setup'.format(self.set_up_fees.normalize()), - '{}/Core'.format(self.cores_unit_price.normalize()), - '{}/GB RAM'.format(self.ram_unit_price.normalize()), - '{}/GB SSD'.format(self.storage_unit_price.normalize()), - '{}% VAT'.format(self.vat_percentage.normalize()) - if not self.vat_inclusive else 'VAT-Incl', - ]) - if self.discount_amount: - display_str = ' - '.join([ - display_str, - '{} {}'.format( - self.discount_amount, - self.discount_name if self.discount_name else 'Discount' - ) - ]) - return display_str - - @classmethod - def get_by_name(cls, name): - try: - pricing = PricingPlan.objects.get(name=name) - except Exception as e: - logger.error( - "Error getting VMPricing with name {name}. " - "Details: {details}. Attempting to return default" - "pricing.".format(name=name, details=str(e)) - ) - pricing = PricingPlan.get_default_pricing() - return pricing - - @classmethod - def get_default_pricing(cls): - """ Returns the default pricing or None """ - try: - default_pricing = PricingPlan.objects.get(name='default') - except Exception as e: - logger.error(str(e)) - default_pricing = None - return default_pricing - -### -# Orders. -class Order(models.Model): - """ - Order are assumed IMMUTABLE and used as SOURCE OF TRUST for generating - bills. Do **NOT** mutate then! - - An one time order is "closed" (does not need to be billed anymore) - if it has one bill record. Having more than one is a programming - error. - - A recurring order is closed if it has been replaced - (replaces__isnull=False) AND the ending_date is set AND it was - billed the last time it needed to be billed (how to check the last - item?) - - BOTH are closed, if they are ended/closed AND have been fully - charged. - - Fully charged == fully billed: sum_of_order_usage == sum_of_bill_records - - """ - - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE, - editable=True) - - billing_address = models.ForeignKey(BillingAddress, - on_delete=models.CASCADE) - - customer = models.ForeignKey(StripeCustomer, on_delete=models.CASCADE, null=True) - - description = models.TextField() - - product = models.ForeignKey(Product, blank=False, null=False, on_delete=models.CASCADE) - config = models.JSONField() - - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField(default=timezone.now) - ending_date = models.DateTimeField(blank=True, null=True) - - recurring_period = models.ForeignKey(RecurringPeriod, - on_delete=models.CASCADE, - editable=True) - - one_time_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - recurring_price = models.DecimalField(default=0.0, - max_digits=AMOUNT_MAX_DIGITS, - decimal_places=AMOUNT_DECIMALS, - validators=[MinValueValidator(0)]) - - currency = models.CharField(max_length=32, choices=Currency.choices, default=Currency.CHF) - - replaces = models.ForeignKey('self', - related_name='replaced_by', - on_delete=models.CASCADE, - blank=True, - null=True) - - depends_on = models.ForeignKey('self', - related_name='parent_of', - on_delete=models.CASCADE, - blank=True, - null=True) - pricing_plan = models.ForeignKey(PricingPlan, blank=False, null=True, on_delete=models.CASCADE) - - should_be_billed = models.BooleanField(default=True) - - @property - def earliest_ending_date(self): - """ - Recurring orders cannot end before finishing at least one recurring period. - - One time orders have a recurring period of 0, so this work universally - """ - - return self.starting_date + datetime.timedelta(seconds=self.recurring_period.duration_seconds) - - - def next_cancel_or_downgrade_date(self, until_when=None): - """ - Return the next proper ending date after n times the - recurring_period, where n is an integer that applies for downgrading - or cancelling. - """ - - if not until_when: - until_when = timezone.now() - - if until_when < self.starting_date: - raise ValidationError("Cannot end before start of start of order") - - if self.recurring_period.duration_seconds > 0: - delta = until_when - self.starting_date - - num_times = ceil(delta.total_seconds() / self.recurring_period.duration_seconds) - - next_date = self.starting_date + datetime.timedelta(seconds=num_times * self.recurring_period.duration_seconds) - else: - next_date = self.starting_date - - return next_date - - def get_ending_date_for_bill(self, bill): - """ - Determine the ending date given a specific bill - """ - - # If the order is quit, charge the final amount / finish (????) - # Probably not a good idea -- FIXME :continue until usual - if self.ending_date: - this_ending_date = self.ending_date - else: - if self.next_cancel_or_downgrade_date(bill.ending_date) > bill.ending_date: - this_ending_date = self.next_cancel_or_downgrade_date(bill.ending_date) - else: - this_ending_date = bill.ending_date - - return this_ending_date - - - @property - def count_billed(self): - """ - How many times this order was billed so far. - This logic is mainly thought to be for recurring bills, but also works for one time bills - """ - - return sum([ br.quantity for br in self.bill_records.all() ]) - - def cancel(self): - self.ending_date = timezone.now() - self.should_be_billed = False - self.save() - if self.instance_id: - last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last() - schedule('matrixhosting.tasks.delete_instance', - self.instance_id, - schedule_type=Schedule.ONCE, - next_run=last_bill_record.ending_date or (timezone.now() + datetime.timedelta(hours=1))) - - def count_used(self, when=None): - """ - How many times this order was billed so far. - This logic is mainly thought to be for recurring bills, but also works for one time bills - """ - - if self.is_one_time: - return 1 - - if not when: - when = timezone.now() - - # Cannot be used after it ended - if self.ending_date and when > self.ending_date: - when = self.ending_date - - return (when - self.starting_date) / self.default_recurring_period - - @property - def all_usage_billed(self, when=None): - """ - Returns true if this order does not need any further billing - ever. In other words: is this order "closed"? - """ - - if self.count_billed == self.count_used(when): - return True - else: - return False - - @property - def is_closed(self): - if self.all_usage_billed and self.ending_date: - return True - else: - return False - - @property - def is_recurring(self): - return self.recurring_price > 0 - - @property - def is_one_time(self): - return not self.is_recurring - - def replace_with(self, new_order): - new_order.replaces = self - self.ending_date = end_before(new_order.starting_date) - self.save() - - def update_order(self, config, starting_date=None): - """ - Updating an order means creating a new order and reference the previous order - """ - - if not starting_date: - starting_date = timezone.now() - - new_order = self.__class__(owner=self.owner, - billing_address=self.billing_address, - description=self.description, - product=self.product, - config=config, - pricing_plan=self.pricing_plan, - starting_date=starting_date, - currency=self.currency - ) - - new_order.recurring_price = new_order.calculate_recurring_price() - new_order.replaces = self - new_order.save() - - self.ending_date = end_before(new_order.starting_date) - self.save() - - return new_order - - - def create_bill_record(self, bill): - br = None - - if self.recurring_price != 0: - records = BillRecord.objects.filter(order=self).all() - if not records: - if self.one_time_price: - br = BillRecord.objects.create(bill=bill, - order=self, - starting_date=self.starting_date, - ending_date=bill.ending_date, - is_recurring_record=False) - else: - br = self.create_new_bill_record_for_recurring_order(bill) - else: - opened_recurring_record = BillRecord.objects.filter(bill=bill, order=self, is_recurring_record=True).first() - if opened_recurring_record: - br = opened_recurring_record - self.update_bill_record_for_recurring_order(br, bill) - else: - br = self.create_new_bill_record_for_recurring_order(bill) - return br - - def update_bill_record_for_recurring_order(self, - bill_record, - bill): - """ - Possibly update a bill record according to the information in the bill - """ - - # If the order has an ending date set, we might need to adjust the bill_record - if self.ending_date: - if bill_record.ending_date != self.ending_date: - bill_record.ending_date = self.ending_date - - else: - # recurring, not terminated, should go until at least end of bill - if bill_record.ending_date < bill.ending_date: - bill_record.ending_date = bill.ending_date - - bill_record.save() - - def create_new_bill_record_for_recurring_order(self, bill): - """ - Create a new bill record - """ - last_bill_record = BillRecord.objects.filter(order=self).order_by('id').last() - - starting_date=self.starting_date - - if last_bill_record: - # We already charged beyond the end of this bill's period - if last_bill_record.ending_date >= bill.ending_date: - return - - # This order is terminated or replaced - if self.ending_date: - # And the last bill record already covered us -> nothing to be done anymore - if last_bill_record.ending_date == self.ending_date: - return - - starting_date = start_after(last_bill_record.ending_date) - ending_date = self.get_ending_date_for_bill(bill) - - return BillRecord.objects.create(bill=bill, - order=self, - starting_date=starting_date, - ending_date=ending_date, - is_recurring_record=True) - - def calculate_recurring_price(self): - try: - config = json.loads(self.config) - recurring_price = 0 - if 'cores' in config: - recurring_price += self.pricing_plan.cores_unit_price * int(config['cores']) - if 'memory' in config: - recurring_price += self.pricing_plan.ram_unit_price * int(config['memory']) - if 'storage' in config: - recurring_price += self.pricing_plan.storage_unit_price * int(config['storage']) - - vat_rate = VATRate.get_vat_rate(self.billing_address) - vat_validation_status = "verified" if self.billing_address.vat_number_validated_on and self.billing_address.vat_number_verified else False - subtotal, subtotal_after_discount, price_after_discount_with_vat, vat, vat_percent, discount = uncloud_pay.utils.apply_vat_discount( - recurring_price, self.pricing_plan, - vat_rate=vat_rate * 100, vat_validation_status = vat_validation_status - ) - return price_after_discount_with_vat - except Exception as e: - logger.error("An error occurred while parsing the config obj", e) - return 0 - - def check_parameters(self): - if 'parameters' in self.product.config: - for parameter in self.product.config['parameters']: - if not parameter in self.config['parameters']: - raise ValidationError(f"Required parameter '{parameter}' is missing.") - - - def save(self, *args, **kwargs): - # Calculate the price of the order when we create it - # IMMUTABLE fields -- need to create new order to modify them - # However this is not enforced here... - if self._state.adding: - self.recurring_price = self.calculate_recurring_price() - - if self.recurring_period_id is None: - self.recurring_period = self.product.default_recurring_period - - try: - prod_period = self.product.recurring_periods.get(producttorecurringperiod__recurring_period=self.recurring_period) - except ObjectDoesNotExist: - raise ValidationError(f"Recurring Period {self.recurring_period} not allowed for product {self.product}") - - self.check_parameters() - - if self.ending_date and self.ending_date < self.starting_date: - raise ValidationError("End date cannot be before starting date") - - - super().save(*args, **kwargs) - - - def __str__(self): - return f"Order {self.id}: {self.description}" - -class Bill(models.Model): - """ - A bill is a representation of usage at a specific time - """ - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField(default=start_of_this_month) - ending_date = models.DateTimeField() - due_date = models.DateField(default=default_payment_delay) - - - billing_address = models.ForeignKey(BillingAddress, - on_delete=models.CASCADE, - editable=True, - null=False) - - # FIXME: editable=True -> is in the admin, but also editable in DRF - # Maybe filter fields in the serializer? - - is_closed = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['owner', - 'starting_date', - 'ending_date' ], - name='one_bill_per_month_per_user') - ] - - def close(self): - """ - Close/finish a bill - """ - self.is_closed = True - if not self.ending_date: - self.ending_date = timezone.now() - self.save() - - @property - def sum(self): - bill_records = BillRecord.objects.filter(bill=self) - return sum([ br.sum for br in bill_records ]) - - @property - def vat_rate(self): - return VATRate.get_vat_rate(self.billing_address, when=self.ending_date) - - - @classmethod - def create_bills_for_all_users(cls): - """ - Create next bill for each user - """ - - for owner in get_user_model().objects.all(): - cls.create_next_bills_for_user(owner) - - @classmethod - def create_next_bills_for_user(cls, owner, ending_date=None): - """ - Create one bill per billing address, as the VAT rates might be different - for each address - """ - - bills = [] - for billing_address in BillingAddress.objects.filter(owner=owner): - bill = cls.create_next_bill_for_user_address(billing_address, ending_date) - if bill: - bills.append(bill) - - return bills - - @classmethod - def create_next_bill_for_user_address(cls, billing_address, ending_date=None): - """ - Create the next bill for a specific billing address of a user - """ - - owner = billing_address.owner - - all_orders = Order.objects.filter(Q(owner__id=owner.id), Q(should_be_billed=True), - Q(billing_address__id=billing_address.id) - ).order_by('id') - - if len(all_orders) > 0: - bill = cls.get_or_create_bill(billing_address, ending_date=ending_date) - for order in all_orders: - order.create_bill_record(bill) - return bill - else: - # This Customer Hasn't any active orders - return False - - - @classmethod - def get_or_create_bill(cls, billing_address, ending_date=None): - """ - Get / reuse last bill if it is not yet closed - - Create bill, if there is no bill or if bill is closed. - """ - - last_bill = cls.objects.filter(billing_address=billing_address).order_by('id').last() - - all_orders = Order.objects.filter(billing_address=billing_address).order_by('id') - first_order = all_orders.first() - - bill = None - - # Get date & bill from previous bill, if it exists - if last_bill: - if not last_bill.is_closed: - bill = last_bill - starting_date = last_bill.starting_date - ending_date = bill.ending_date - else: - starting_date = last_bill.ending_date + datetime.timedelta(seconds=1) - else: - # Might be an idea to make this the start of the month, too - if first_order: - starting_date = first_order.starting_date - else: - starting_date = timezone.now() - - if not ending_date: - ending_date = end_of_month(starting_date) - - if not bill: - bill = cls.objects.create( - owner=billing_address.owner, - starting_date=starting_date, - ending_date=ending_date, - billing_address=billing_address) - - - return bill - - def __str__(self): - return f"{self.owner}-{self.id}" - - -class BillRecord(models.Model): - """ - Entry of a bill, dynamically generated from an order. - """ - - bill = models.ForeignKey(Bill, on_delete=models.CASCADE) - order = models.ForeignKey(Order, on_delete=models.CASCADE) - - creation_date = models.DateTimeField(auto_now_add=True) - starting_date = models.DateTimeField() - ending_date = models.DateTimeField() - - is_recurring_record = models.BooleanField(blank=False, null=False) - - @property - def quantity(self): - """ Determine the quantity by the duration""" - if not self.is_recurring_record: - return 1 - - record_delta = self.ending_date.date() - self.starting_date.date() - if self.order.recurring_period and self.order.recurring_period.duration_seconds > 0: - return int(record_delta.total_seconds() / self.order.recurring_period.duration_seconds) - else: - return 1 - - @property - def sum(self): - if self.is_recurring_record: - return self.order.recurring_price * Decimal(self.quantity) - else: - return self.order.one_time_price - - @property - def price(self): - if self.is_recurring_record: - return self.order.recurring_price - else: - return self.order.one_time_price - - def __str__(self): - if self.is_recurring_record: - bill_line = f"{self.starting_date} - {self.ending_date}: {self.quantity} x {self.order}" - else: - bill_line = f"{self.starting_date}: {self.order}" - - return bill_line - - def save(self, *args, **kwargs): - if self.ending_date < self.starting_date: - raise ValidationError("End date cannot be before starting date") - - super().save(*args, **kwargs) - - -class ProductToRecurringPeriod(models.Model): - """ - Intermediate manytomany mapping class that allows storing the default recurring period - for a product - """ - - recurring_period = models.ForeignKey(RecurringPeriod, on_delete=models.CASCADE) - product = models.ForeignKey(Product, on_delete=models.CASCADE) - - is_default = models.BooleanField(default=False) - - class Meta: - constraints = [ - models.UniqueConstraint(fields=['product'], - condition=models.Q(is_default=True), - name='one_default_recurring_period_per_product'), - models.UniqueConstraint(fields=['product', 'recurring_period'], - name='recurring_period_once_per_product') - ] - - def __str__(self): - return f"{self.product} - {self.recurring_period} (default: {self.is_default})" - - -class Membership(models.Model): - owner = models.ForeignKey(get_user_model(), - on_delete=models.CASCADE) - - starting_date = models.DateField(blank=True, null=True) - ending_date = models.DateField(blank=True, null=True) - - - @classmethod - def user_has_membership(user, when): - """ - Return true if user has membership at a point of time, - return false if that is not the case - """ - - pass - - # cls.objects.filter(owner=user, - # starting_date) diff --git a/uncloud_pay/selectors.py b/uncloud_pay/selectors.py deleted file mode 100644 index a634e30..0000000 --- a/uncloud_pay/selectors.py +++ /dev/null @@ -1,31 +0,0 @@ -from django.utils import timezone -from django.db import transaction -from .models import * - -def get_payments_for_user(user): - payments = [ payment.amount for payment in Payment.objects.filter(owner=user) ] - - return sum(payments) - -def get_spendings_for_user(user): - bills = Bill.objects.filter(owner=user) - - amount = 0 - for bill in bills: - amount += bill.sum - - return amount - -@transaction.atomic -def get_balance_for_user(user): - return get_payments_for_user(user) - get_spendings_for_user(user) - -@transaction.atomic -def has_enough_balance(user, due_amount): - balance = get_balance_for_user(user) - if balance >= due_amount: - return True - return False - -def get_billing_address_for_user(user): - return BillingAddress.objects.filter(owner=user, active=True).first() diff --git a/uncloud_pay/serializers.py b/uncloud_pay/serializers.py deleted file mode 100644 index 4ea4104..0000000 --- a/uncloud_pay/serializers.py +++ /dev/null @@ -1,127 +0,0 @@ -from django.contrib.auth import get_user_model -from rest_framework import serializers -from uncloud_auth.serializers import UserSerializer -from django.utils.translation import gettext_lazy as _ - -from .models import * -import uncloud_pay.stripe as uncloud_stripe -from uncloud import AMOUNT_DECIMALS, AMOUNT_MAX_DIGITS - -### -# 2020-12 Checked code - -class StripeCreditCardSerializer(serializers.HyperlinkedModelSerializer): - class Meta: - model = StripeCreditCard - exclude = [ "card_id", "owner" ] - read_only_fields = [ "last4", "brand", "expiry_date" ] - -class PaymentSerializer(serializers.ModelSerializer): - owner = serializers.HiddenField(default=serializers.CurrentUserDefault()) - - class Meta: - model = Payment - fields = '__all__' - read_only_fields = [ "external_reference", "source", "timestamp" ] - - def validate(self, data): - payment_intent = uncloud_stripe.charge_customer(data['owner'], - data['amount']) - - data["external_reference"] = payment_intent["id"] - data["source"] = "stripe" - - return data - -class BalanceSerializer(serializers.Serializer): - balance = serializers.DecimalField(max_digits=AMOUNT_MAX_DIGITS, decimal_places=AMOUNT_DECIMALS) - -class BillingAddressSerializer(serializers.ModelSerializer): - class Meta: - model = BillingAddress - exclude = [ "owner" ] - - -class VATRateSerializer(serializers.ModelSerializer): - - class Meta: - model = VATRate - fields = '__all__' - - -################################################################################ -# Unchecked code - - -### -# Orders & Products. - -class OrderSerializer(serializers.ModelSerializer): - owner = serializers.PrimaryKeyRelatedField(queryset=get_user_model().objects.all()) - - def __init__(self, *args, **kwargs): - # Don't pass the 'fields' arg up to the superclass - admin = kwargs.pop('admin', None) - - # Instantiate the superclass normally - super(OrderSerializer, self).__init__(*args, **kwargs) - - # Only allows owner in admin mode. - if not admin: - self.fields.pop('owner') - - def create(self, validated_data): - billing_address = BillingAddress.get_preferred_address_for(validated_data["owner"]) - instance = Order(billing_address=billing_address, **validated_data) - instance.save() - - return instance - - def validate_owner(self, value): - if BillingAddress.get_preferred_address_for(value) == None: - raise serializers.ValidationError("Owner does not have a valid billing address.") - - return value - - class Meta: - model = Order - read_only_fields = ['replaced_by', 'depends_on'] - fields = ['owner', 'description', 'creation_date', 'starting_date', 'ending_date', - 'recurring_period', 'recurring_price', 'one_time_price', - 'config', 'pricing_plan', 'should_be_billed'] + read_only_fields - - -### -# Bills - -# TODO: remove magic numbers for decimal fields -class BillRecordSerializer(serializers.Serializer): - order = serializers.HyperlinkedRelatedField( - view_name='order-detail', - read_only=True) - description = serializers.CharField() - one_time_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - recurring_price = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) -# recurring_period = serializers.ChoiceField() - recurring_count = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - vat_rate = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - vat_amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - amount = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - total = serializers.DecimalField(AMOUNT_MAX_DIGITS, AMOUNT_DECIMALS) - -class BillSerializer(serializers.ModelSerializer): - billing_address = BillingAddressSerializer(read_only=True) - records = BillRecordSerializer(many=True, read_only=True) - - class Meta: - model = Bill - fields = ['owner', 'sum', 'vat_rate', - 'due_date', 'creation_date', 'starting_date', 'ending_date', - 'records', 'is_closed', 'billing_address'] - -# We do not want users to mutate the country / VAT number of an address, as it -# will change VAT on existing bills. -class UpdateBillingAddressSerializer(serializers.ModelSerializer): - class Meta: - model = BillingAddress - fields = ['street', 'city', 'postal_code'] diff --git a/uncloud_pay/services.py b/uncloud_pay/services.py deleted file mode 100644 index 84a7c8d..0000000 --- a/uncloud_pay/services.py +++ /dev/null @@ -1,34 +0,0 @@ -import datetime -from calendar import monthrange -from django.utils import timezone - - -def start_of_month(a_day): - """ Returns first of the month of a given datetime object""" - return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) - -def end_of_month(a_day): - """ Returns first of the month of a given datetime object""" - - _, last_day = monthrange(a_day.year, a_day.month) - return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) - -def start_of_this_month(): - """ Returns first of this month""" - a_day = timezone.now() - return a_day.replace(day=1,hour=0,minute=0,second=0, microsecond=0) - -def end_of_this_month(): - """ Returns first of this month""" - a_day = timezone.now() - - _, last_day = monthrange(a_day.year, a_day.month) - return a_day.replace(day=last_day,hour=23,minute=59,second=59, microsecond=0) - -def end_before(a_date): - """ Return suitable datetimefield for ending just before a_date """ - return a_date - datetime.timedelta(seconds=1) - -def start_after(a_date): - """ Return suitable datetimefield for starting just after a_date """ - return a_date + datetime.timedelta(seconds=1) diff --git a/uncloud_pay/stripe.py b/uncloud_pay/stripe.py deleted file mode 100644 index a59456e..0000000 --- a/uncloud_pay/stripe.py +++ /dev/null @@ -1,249 +0,0 @@ -import stripe -import stripe.error -import logging -import datetime - -from django.core.exceptions import ObjectDoesNotExist, ValidationError -from django.conf import settings -from django.contrib.auth import get_user_model - -from .models import StripeCustomer, StripeCreditCard - -logger = logging.getLogger(__name__) - -CURRENCY = 'chf' - -stripe.api_key = settings.STRIPE_KEY - -def handle_stripe_error(f): - def handle_problems(*args, **kwargs): - response = { - 'paid': False, - 'response_object': None, - 'error': None - } - - common_message = "Currently it is not possible to make payments. Please try agin later." - try: - response_object = f(*args, **kwargs) - return response_object - except stripe.error.CardError as e: - # Since it's a decline, stripe.error.CardError will be caught - body = e.json_body - logging.error(str(e)) - - raise e # For error handling. - except stripe.error.RateLimitError: - logging.error("Too many requests made to the API too quickly.") - raise Exception(common_message) - except stripe.error.InvalidRequestError as e: - logging.error(str(e)) - raise Exception('Invalid parameters.') - except stripe.error.AuthenticationError as e: - # Authentication with Stripe's API failed - # (maybe you changed API keys recently) - logging.error(str(e)) - raise Exception(common_message) - except stripe.error.APIConnectionError as e: - logging.error(str(e)) - raise Exception(common_message) - except stripe.error.StripeError as e: - # XXX: maybe send email - logging.error(str(e)) - raise Exception(common_message) - - return handle_problems - -def public_api_key(): - return settings.STRIPE_PUBLIC_KEY - -def get_customer_id_for(user): - try: - # .get() raise if there is no matching entry. - return StripeCustomer.objects.get(owner=user).stripe_id - except ObjectDoesNotExist: - # No entry yet - making a new one. - try: - customer = create_customer(user.username, user.email) - uncloud_stripe_mapping = StripeCustomer.objects.create( - owner=user, stripe_id=customer.id) - return uncloud_stripe_mapping.stripe_id - except Exception as e: - return None - -@handle_stripe_error -def create_setup_intent(customer_id): - return stripe.SetupIntent.create(customer=customer_id) - -@handle_stripe_error -def get_setup_intent(setup_intent_id): - return stripe.SetupIntent.retrieve(setup_intent_id) - -@handle_stripe_error -def get_payment_method(payment_method_id): - return stripe.PaymentMethod.retrieve(payment_method_id) - -@handle_stripe_error -def get_card_from_payment(user, payment_method_id): - payment_method = stripe.PaymentMethod.retrieve(payment_method_id) - if payment_method: - if 'card' in payment_method: - sync_cards_for_user(user) - return payment_method['card'] - return False - - -@handle_stripe_error -def attach_payment_method(payment_method_id, customer_id): - return stripe.PaymentMethod.attach(payment_method_id, customer=customer_id) - -@handle_stripe_error -def create_customer(name, email): - return stripe.Customer.create(name=name, email=email) - -@handle_stripe_error -def get_customer(customer_id): - return stripe.Customer.retrieve(customer_id) - -@handle_stripe_error -def get_customer_cards(customer_id): - print(f"getting cards for: {customer_id}") - - cards = [] - stripe_cards = stripe.PaymentMethod.list( - customer=customer_id, - type="card", - ) - - for stripe_card in stripe_cards["data"]: - card = {} - card['brand'] = stripe_card["card"]["brand"] - card['last4'] = stripe_card["card"]["last4"] - card['month'] = stripe_card["card"]["exp_month"] - card['year'] = stripe_card["card"]["exp_year"] - card['id'] = stripe_card["id"] - - cards.append(card) - - return cards - -def sync_cards_for_user(user): - customer_id = get_customer_id_for(user) - cards = get_customer_cards(customer_id) - - active_cards = StripeCreditCard.objects.filter(owner=user, - active=True) - - if len(active_cards) > 0: - has_active_card = True - else: - has_active_card = False - - for card in cards: - active = False - - if not has_active_card: - active = True - has_active_card = True - - StripeCreditCard.objects.get_or_create(card_id=card['id'], - owner = user, - defaults = { - 'last4': card['last4'], - 'brand': card['brand'], - 'expiry_date': datetime.date(card['year'], - card['month'], - 1), - 'active': active - } - ) - -@handle_stripe_error -def charge_customer(user, amount, currency='CHF', card=False): - # Amount is in CHF but stripes requires smallest possible unit. - # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount - # FIXME: might need to be adjusted for other currencies - - if currency == 'CHF': - adjusted_amount = int(amount * 100) - else: - return Exception("Programming error: unsupported currency") - - try: - card = card or StripeCreditCard.objects.get(owner=user, - active=True) - - except StripeCreditCard.DoesNotExist: - raise ValidationError("No active credit card - cannot create payment") - - customer_id = get_customer_id_for(user) - - return stripe.PaymentIntent.create( - amount=adjusted_amount, - currency=currency, - customer=customer_id, - payment_method=card.card_id, - off_session=True, - confirm=True, - ) - -@handle_stripe_error -def get_payment_intent(user, amount, currency='CHF', card=False): - # Amount is in CHF but stripes requires smallest possible unit. - # https://stripe.com/docs/api/payment_intents/create#create_payment_intent-amount - # FIXME: might need to be adjusted for other currencies - - if currency == 'CHF': - adjusted_amount = int(amount * 100) - else: - return Exception("Programming error: unsupported currency") - - try: - card = card or StripeCreditCard.objects.get(owner=user, - active=True) - - except StripeCreditCard.DoesNotExist: - raise ValidationError("No active credit card - cannot create payment") - - customer_id = get_customer_id_for(user) - - return stripe.PaymentIntent.create( - amount=adjusted_amount, - currency=currency, - customer=customer_id, - payment_method=card.card_id, - setup_future_usage='off_session', - confirm=False, - ) - -@handle_stripe_error -def get_or_create_tax_id_for_user(stripe_customer_id, vat_number, - type="eu_vat", country=""): - def compare_vat_numbers(vat1, vat2): - _vat1 = vat1.replace(" ", "").replace(".", "").replace("-","") - _vat2 = vat2.replace(" ", "").replace(".", "").replace("-","") - return True if _vat1 == _vat2 else False - - tax_ids_list = stripe.Customer.list_tax_ids( - stripe_customer_id, - limit=100, - ) - for tax_id_obj in tax_ids_list.data: - if compare_vat_numbers(tax_id_obj.value, vat_number): - return tax_id_obj - else: - logger.debug( - "{val1} is not equal to {val2} or {con1} not same as " - "{con2}".format(val1=tax_id_obj.value, val2=vat_number, - con1=tax_id_obj.country.lower(), - con2=country.lower().strip())) - logger.debug( - "tax id obj does not exist for {val}. Creating a new one".format( - val=vat_number - )) - tax_id_obj = stripe.Customer.create_tax_id( - stripe_customer_id, - type=type, - value=vat_number, - ) - return tax_id_obj diff --git a/uncloud_pay/templates/uncloud_pay/register_stripe.html b/uncloud_pay/templates/uncloud_pay/register_stripe.html deleted file mode 100644 index 0eed76a..0000000 --- a/uncloud_pay/templates/uncloud_pay/register_stripe.html +++ /dev/null @@ -1,78 +0,0 @@ -{% extends 'uncloud/base.html' %} -{% block bootstrap5_extra_head %} - - -{% endblock %} -{% block bootstrap5_content %} -
- {% csrf_token %} -
-

Register Credit Card with Stripe

-

- By submitting I authorise to send instructions to - the financial institution that issued my card to take - payments from my card account in accordance with the - terms of my agreement with you. -

- - - - - -
-
- - -
-
The card will be registered with stripe.
- - -
- - - - -{% endblock %} diff --git a/uncloud_pay/tests.py b/uncloud_pay/tests.py deleted file mode 100644 index 0fde03d..0000000 --- a/uncloud_pay/tests.py +++ /dev/null @@ -1,532 +0,0 @@ -from django.test import TestCase -from django.contrib.auth import get_user_model -from datetime import datetime, date, timedelta -from django.utils import timezone - -from .models import * -from uncloud_service.models import GenericServiceProduct -from uncloud.models import UncloudProvider, UncloudNetwork - -import json - -vm_product_config = { - 'features': { - 'cores': - { 'min': 1, - 'max': 48 - }, - 'ram_gb': - { 'min': 1, - 'max': 256 - }, - }, -} - -vm_order_config = json.dumps({ - 'cores': 1, - 'memory': 2, - 'storage': 100 -}) - -vm_order_downgrade_config = { - 'features': { - 'cores': 1, - 'ram_gb': 1 - } -} - -vm_order_upgrade_config = { - 'features': { - 'cores': 4, - 'ram_gb': 4 - } -} - - -class ProductTestCase(TestCase): - """ - Test products and products <-> order interaction - """ - - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - self.ba = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="somewhere else", - active=True) - - RecurringPeriod.populate_db_defaults() - self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days") - - def test_create_product(self): - """ - Create a sample product - """ - p = Product.objects.create(name="Testproduct", - description="Only for testing", - config=vm_product_config) - - p.recurring_periods.add(self.default_recurring_period, - through_defaults= { 'is_default': True }) - - -class OrderTestCase(TestCase): - """ - The heart of ordering products - """ - - def setUp(self): - 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.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - 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) - - RecurringPeriod.populate_db_defaults() - 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_order_invalid_recurring_period(self): - """ - Order a products with a recurringperiod that is not added to the product - """ - - order_config = json.dumps({ - 'cores': 1, - 'memory':2, - 'storage': 100 - }) - o = Order.objects.create(owner=self.user, - billing_address=self.ba, - pricing_plan = self.pricing_plan, - product=self.product, - config=order_config) - - - def test_order_product(self): - """ - Order a product, ensure the order has correct price setup - """ - order_config = json.dumps({ - 'cores': 1, - 'memory':2, - 'storage': 100 - }) - o = Order.objects.create(owner=self.user, - billing_address=self.ba, - pricing_plan = self.pricing_plan, - product=self.product, - config=order_config) - - self.assertEqual(o.one_time_price, 0) - self.assertEqual(o.recurring_price, 13.0) - - def test_change_order(self): - """ - Change an order and ensure that - - a new order is created - - the price is correct in the new order - """ - order_config = json.dumps({ - 'cores': 2, - 'memory':4, - 'storage': 200 - }) - order1 = Order.objects.create(owner=self.user, - billing_address=self.ba, - pricing_plan = self.pricing_plan, - product=self.product, - config=order_config) - - self.assertEqual(order1.one_time_price, 0) - self.assertEqual(order1.recurring_price, 26.0) - - -class ModifyOrderTestCase(TestCase): - """ - Test typical order flows like - - cancelling - - downgrading - - upgrading - """ - - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - 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.order1_config = json.dumps({ - 'cores': 2, - 'memory':4, - 'storage': 200 - }) - self.order2_config = json.dumps({ - 'cores': 1, - 'memory':2, - 'storage': 100 - }) - 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) - - RecurringPeriod.populate_db_defaults() - 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_change_order(self): - """ - Test changing an order - - Expected result: - - - Old order should be closed before new order starts - - New order should start at starting data - """ - - user = self.user - - starting_price = 16 - downgrade_price = 8 - - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - ending1_date = starting_date + datetime.timedelta(days=15) - change1_date = start_after(ending1_date) - - bill_ending_date = change1_date + datetime.timedelta(days=1) - - - order1 = Order.objects.create(owner=self.user, - billing_address=BillingAddress.get_address_for(self.user), - product=self.product, - config=self.order1_config, - pricing_plan=self.pricing_plan, - starting_date=starting_date) - - order1.update_order(self.order2_config, starting_date=change1_date) - - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - - self.assertEqual(len(bill_records), 2) - - self.assertEqual(bill_records[0].starting_date, starting_date) - self.assertEqual(bill_records[0].ending_date, ending1_date) - - self.assertEqual(bill_records[1].starting_date, change1_date) - - - - def test_downgrade_product(self): - """ - Test downgrading behaviour: - - We create a recurring product (recurring time: 30 days) and downgrade after 15 days. - - We create the bill right AFTER the end of the first order. - - Expected result: - - - First bill record for 30 days - - Second bill record starting after 30 days - - Bill contains two bill records - - """ - - user = self.user - - starting_price = 16 - downgrade_price = 8 - - starting_date = timezone.make_aware(datetime.datetime(2019,3,3)) - first_order_should_end_at = starting_date + datetime.timedelta(days=30) - change1_date = start_after(starting_date + datetime.timedelta(days=15)) - bill_ending_date = change1_date + datetime.timedelta(days=1) - order1 = Order.objects.create(owner=self.user, - billing_address=BillingAddress.get_address_for(self.user), - product=self.product, - pricing_plan=self.pricing_plan, - config=self.order1_config, - starting_date=starting_date) - - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - - bill = bills[0] - bill_records = BillRecord.objects.filter(bill=bill) - - self.assertEqual(len(bill_records), 1) - self.assertEqual(bill_records[0].starting_date, starting_date) - - order1.update_order(self.order2_config, starting_date=change1_date) - bills = Bill.create_next_bills_for_user(user, ending_date=bill_ending_date) - bill_records = BillRecord.objects.filter(bill=bill) - self.assertEqual(len(bill_records), 2) - self.assertEqual(bill_records[0].order.ending_date.date(), change1_date.date()) - - -class BillTestCase(TestCase): - """ - Test aspects of billing / creating a bill - """ - - def setUp(self): - RecurringPeriod.populate_db_defaults() - - 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.user_without_address = get_user_model().objects.create( - username='no_home_person', - email='far.away@domain.tld') - - self.user = get_user_model().objects.create( - username='jdoe', - email='john.doe@domain.tld') - - self.recurring_user = get_user_model().objects.create( - username='recurrent_product_user', - email='jane.doe@domain.tld') - - self.user_addr = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - self.recurring_user_addr = BillingAddress.objects.create( - owner=self.recurring_user, - organization = 'Test org', - street="Somewhere", - city="Else", - postal_code="unknown", - active=True) - - self.order_meta = {} - self.order_meta[1] = { - 'starting_date': timezone.make_aware(datetime.datetime(2020,3,3)), - 'ending_date': timezone.make_aware(datetime.datetime(2020,4,17)), - 'price': 15, - 'description': '' - } - - self.product = Product.objects.create(name="Product Sample", - description="Not only for testing, but for joy", - config=vm_product_config) - - - self.vm = Product.objects.create(name="Super Fast VM", - description="Zooooom", - config=vm_product_config) - - - RecurringPeriod.populate_db_defaults() - self.default_recurring_period = RecurringPeriod.objects.get(name="Per 30 days") - - self.onetime_recurring_period = RecurringPeriod.objects.get(name="Onetime") - - self.product.recurring_periods.add(self.onetime_recurring_period, - through_defaults= { 'is_default': True }) - - self.vm.recurring_periods.add(self.default_recurring_period, - through_defaults= { 'is_default': True }) - - - # used for generating multiple bills - self.bill_dates = [ - timezone.make_aware(datetime.datetime(2020,3,31)), - timezone.make_aware(datetime.datetime(2020,4,30)), - timezone.make_aware(datetime.datetime(2020,5,31)), - ] - - - def order_product(self): - return Order.objects.create( - owner=self.user, - recurring_period=RecurringPeriod.objects.get(name="Onetime"), - product=self.product, - billing_address=BillingAddress.get_address_for(self.user), - starting_date=self.order_meta[1]['starting_date'], - ending_date=self.order_meta[1]['ending_date'], - pricing_plan=self.pricing_plan, - config=vm_order_config) - - def order_vm(self, owner=None): - - if not owner: - owner = self.recurring_user - - return Order.objects.create( - owner=owner, - product=self.vm, - config=vm_order_config, - pricing_plan=self.pricing_plan, - billing_address=BillingAddress.get_address_for(self.recurring_user), - starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), - ) - - def test_bill_one_time_with_recurring(self): - """ - Validate that if the order contains one_time_price and recurring_pricing - One Bill records should be created - """ - - order = Order.objects.create( - owner=self.user, - product=self.vm, - config=vm_order_config, - pricing_plan=self.pricing_plan, - one_time_price = 35, - billing_address=BillingAddress.get_address_for(self.user), - starting_date=timezone.make_aware(datetime.datetime(2020,3,3)), - ) - - bill = Bill.create_next_bill_for_user_address(self.user_addr) - - self.assertEqual(order.billrecord_set.count(), 1) - record = order.billrecord_set.first() - self.assertEqual(record.is_recurring_record, False) - self.assertEqual(record.price, 35) - self.assertEqual(record.quantity, 1) - self.assertEqual(record.sum, 35) - #close the bill as it has been paid - bill.close() - bill2 = Bill.create_next_bill_for_user_address(self.user_addr) - self.assertNotEqual(bill.id, bill2.id) - self.assertEqual(order.billrecord_set.count(), 2) - record = BillRecord.objects.filter(bill=bill2, order=order).first() - self.assertEqual(record.is_recurring_record, True) - self.assertEqual(record.price, 13) - self.assertEqual(record.quantity, 1) - self.assertEqual(record.sum, 13) - - def test_bill_one_time_one_bill_record(self): - """ - Ensure there is only 1 bill record per order - """ - - order = self.order_product() - - bill = Bill.create_next_bill_for_user_address(self.user_addr) - - self.assertEqual(order.billrecord_set.count(), 1) - - def test_bill_sum_onetime(self): - """ - Check the bill sum for a single one time order - """ - - order = self.order_product() - self.assertEqual(order.recurring_price, 13.0) - bill = Bill.create_next_bill_for_user_address(self.user_addr) - self.assertEqual(order.billrecord_set.count(), 1) - record = order.billrecord_set.first() - self.assertEqual(record.price, 13) - self.assertEqual(record.quantity, 1) - self.assertEqual(bill.sum, 13) - - - def test_bill_creates_record_for_recurring_order(self): - """ - Ensure there is only 1 bill record per order - """ - - order = self.order_vm() - bill = Bill.create_next_bill_for_user_address(self.recurring_user_addr) - - self.assertEqual(order.billrecord_set.count(), 1) - self.assertEqual(bill.billrecord_set.count(), 1) - - - def test_new_bill_after_closing(self): - """ - After closing a bill and the user has a recurring product, - the next bill run should create e new bill - """ - - order = self.order_vm() - - for ending_date in self.bill_dates: - b = Bill.create_next_bill_for_user_address(self.recurring_user_addr, ending_date) - b.close() - - bill_count = Bill.objects.filter(owner=self.recurring_user).count() - - self.assertEqual(len(self.bill_dates), bill_count) - - - -class BillingAddressTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - - def test_user_no_address(self): - """ - Raise an error, when there is no address - """ - - self.assertRaises(BillingAddress.DoesNotExist, - BillingAddress.get_address_for, - self.user) - -class VATRatesTestCase(TestCase): - def setUp(self): - self.user = get_user_model().objects.create( - username='random_user', - email='jane.random@domain.tld') - - self.user_addr = BillingAddress.objects.create( - owner=self.user, - organization = 'Test org', - street="unknown", - city="unknown", - postal_code="unknown", - active=True) - - UncloudNetwork.populate_db_defaults() - UncloudProvider.populate_db_defaults() - - - - def test_get_rate_for_user(self): - """ - Raise an error, when there is no address - """ diff --git a/uncloud_pay/utils.py b/uncloud_pay/utils.py deleted file mode 100644 index c80ae3a..0000000 --- a/uncloud_pay/utils.py +++ /dev/null @@ -1,155 +0,0 @@ -import logging -import decimal -import datetime - -from . import stripe as uncloud_stripe -import stripe -from .models import PricingPlan, BillingAddress - -logger = logging.getLogger(__name__) - -eu_countries = ['at', 'be', 'bg', 'ch', 'cy', 'cz', 'hr', 'dk', - 'ee', 'fi', 'fr', 'mc', 'de', 'gr', 'hu', 'ie', 'it', - 'lv', 'lu', 'mt', 'nl', 'po', 'pt', 'ro','sk', 'si', 'es', - 'se', 'gb'] - -def validate_vat_number(stripe_customer_id, billing_address_id): - try: - billing_address = BillingAddress.objects.get(id=billing_address_id) - except BillingAddress.DoesNotExist as dne: - billing_address = None - except BillingAddress.MultipleObjectsReturned as mor: - billing_address = BillingAddress.objects.filter(id=billing_address_id).order_by('-id').first() - if billing_address is not None: - logger.debug("BillingAddress found: %s %s" % ( - billing_address_id, str(billing_address))) - if billing_address.country.lower().strip() not in eu_countries: - return { - "validated_on": "", - "status": "not_needed" - } - if billing_address.vat_number_validated_on and billing_address.vat_number_verified: - return { - "validated_on": billing_address.vat_number_validated_on, - "status": "verified" - } - else: - if billing_address.stripe_tax_id: - logger.debug("We have a tax id %s" % billing_address.stripe_tax_id) - tax_id_obj = stripe.Customer.retrieve_tax_id( - stripe_customer_id, - billing_address.stripe_tax_id, - ) - if tax_id_obj.verification.status == "verified": - logger.debug("Latest status on Stripe=%s. Updating" % - tax_id_obj.verification.status) - # update billing address - billing_address.vat_number_validated_on = datetime.datetime.now() - billing_address.vat_number_verified = True - billing_address.save() - return { - "status": "verified", - "validated_on": billing_address.vat_number_validated_on - } - else: - billing_address.vat_number_validated_on = datetime.datetime.now() - billing_address.vat_number_verified = False - billing_address.save() - else: - logger.debug("Creating a tax id") - tax_id_obj = create_tax_id( - stripe_customer_id, billing_address_id, - "ch_vat" if billing_address.country.lower() == "ch" else "eu_vat", - ) - else: - logger.debug("invalid billing address") - return { - "status": "invalid billing address", - "validated_on": "" - } - return { - "status": tax_id_obj.verification.status if 'verification' in tax_id_obj else "unknown", - "validated_on": datetime.datetime.now() if tax_id_obj.verification.status == "verified" else "" - } - -def create_tax_id(stripe_customer_id, billing_address_id, type): - try: - billing_address = BillingAddress.objects.get(id=billing_address_id) - except BillingAddress.DoesNotExist as dne: - billing_address = None - logger.debug("BillingAddress does not exist for %s" % billing_address_id) - except BillingAddress.MultipleObjectsReturned as mor: - logger.debug("Multiple BillingAddress exist for %s" % billing_address_id) - billing_address = BillingAddress.objects.filter(billing_address_id).order_by('-id').first() - - tax_id_obj = None - if billing_address: - try: - tax_id_obj = uncloud_stripe.get_or_create_tax_id_for_user( - stripe_customer_id, - vat_number=billing_address.vat_number, - type=type, - country=billing_address.country - ) - billing_address.stripe_tax_id = tax_id_obj.id - billing_address.vat_number_verified = True if tax_id_obj.verification.status == "verified" else False - billing_address.save() - return tax_id_obj - except Exception as e: - logger.debug("Received none in tax_id_obj") - return { - 'verification': None, - 'error': str(e) - } - -def apply_vat_discount(subtotal, pricing_plan, vat_rate=False, vat_validation_status=False): - vat_percent = vat_rate or pricing_plan.vat_percentage - if pricing_plan.vat_inclusive or (vat_validation_status and vat_validation_status in ["verified", "not_needed"]): - vat_percent = decimal.Decimal(0) - vat = decimal.Decimal(0) - else: - vat = subtotal * decimal.Decimal(vat_rate) * decimal.Decimal(0.01) - discount_amount = 0 - discount_amount_with_vat = 0 - if pricing_plan.discount_amount: - discount_amount = round(float(pricing_plan.discount_amount), 2) - discount_amount_with_vat = decimal.Decimal(discount_amount) * (1 + decimal.Decimal(vat_rate) * decimal.Decimal(0.01)) - discount_amount_with_vat = discount_amount_with_vat - - subtotal = round(float(subtotal), 2) - vat_percent = round(float(vat_percent), 2) - discount = { - 'name': pricing_plan.discount_name, - 'amount': discount_amount, - 'amount_with_vat': round(float(discount_amount_with_vat), 2) - } - subtotal_after_discount = subtotal - discount["amount"] - price_after_discount_with_vat = round((subtotal - discount['amount']) * (1 + vat_percent * 0.01), 2) - - return (subtotal, round(float(subtotal_after_discount), 2), price_after_discount_with_vat, - round(float(vat), 2), vat_percent, discount) - - -def get_order_total_with_vat(cores, memory, storage, - pricing_name='default', vat_rate=False, vat_validation_status=False): - try: - pricing = PricingPlan.objects.get(name=pricing_name) - except Exception as ex: - logger.error( - "Error getting PricingPlan object for {pricing_name}." - "Details: {details}".format( - pricing_name=pricing_name, details=str(ex) - ) - ) - return None - - subtotal = ( - pricing.set_up_fees + - (decimal.Decimal(cores) * pricing.cores_unit_price) + - (decimal.Decimal(memory) * pricing.ram_unit_price) + - (decimal.Decimal(storage) * (pricing.storage_unit_price)) - ) - return apply_vat_discount(subtotal, pricing, vat_rate, vat_validation_status) - - - diff --git a/uncloud_pay/views.py b/uncloud_pay/views.py deleted file mode 100644 index e90dda1..0000000 --- a/uncloud_pay/views.py +++ /dev/null @@ -1,310 +0,0 @@ -from django.contrib.auth.mixins import LoginRequiredMixin -from django.views.generic.base import TemplateView -from django.shortcuts import render -from django.db import transaction -from django.contrib.auth import get_user_model -from rest_framework import viewsets, mixins, permissions, status, views -from rest_framework.renderers import TemplateHTMLRenderer -from rest_framework.response import Response -from rest_framework.decorators import action -from rest_framework.reverse import reverse -from rest_framework.decorators import renderer_classes -from vat_validator import validate_vat, vies -from vat_validator.countries import EU_COUNTRY_CODES -from hardcopy import bytestring_to_pdf -from django.core.files.temp import NamedTemporaryFile -from django.http import FileResponse -from django.template.loader import render_to_string -from copy import deepcopy - -import json -import logging - -from .models import * -from .serializers import * -from .selectors import * - -from datetime import datetime -from vat_validator import sanitize_vat -import uncloud_pay.stripe as uncloud_stripe -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator -from django.http import JsonResponse -import stripe - -logger = logging.getLogger(__name__) - -### -# 2020-12 checked code - -class RegisterCard(TemplateView): - template_name = "uncloud_pay/register_stripe.html" - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - - def get_context_data(self, **kwargs): - - customer_id = uncloud_stripe.get_customer_id_for(self.request.user) - setup_intent = uncloud_stripe.create_setup_intent(customer_id) - context = super().get_context_data(**kwargs) - context['client_secret'] = setup_intent.client_secret - context['username'] = self.request.user.username - context['stripe_pk'] = uncloud_stripe.public_api_key - return context - - -class CreditCardViewSet(mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.DestroyModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): - - serializer_class = StripeCreditCardSerializer - permission_classes = [permissions.IsAuthenticated] - - def list(self, request): - uncloud_stripe.sync_cards_for_user(self.request.user) - return super().list(request) - - def get_queryset(self): - return StripeCreditCard.objects.filter(owner=self.request.user) - -class PaymentViewSet(viewsets.ModelViewSet): - serializer_class = PaymentSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Payment.objects.filter(owner=self.request.user) - -class BalanceViewSet(viewsets.ViewSet): - permission_classes = [permissions.IsAuthenticated] - - def list(self, request): - serializer = BalanceSerializer(data={ - 'balance': get_balance_for_user(self.request.user) - }) - serializer.is_valid() - return Response(serializer.data) - - -class ListCards(TemplateView): - template_name = "uncloud_pay/list_stripe.html" - - @method_decorator(login_required) - def dispatch(self, *args, **kwargs): - return super().dispatch(*args, **kwargs) - - def get_context_data(self, **kwargs): - customer_id = uncloud_stripe.get_customer_id_for(self.request.user) - cards = uncloud_stripe.get_customer_cards(customer_id) - - context = super().get_context_data(**kwargs) - context['cards'] = cards - context['username'] = self.request.user - - return context - -### -# Bills and Orders. - -class BillViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = BillSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Bill.objects.filter(owner=self.request.user) - - - @action(detail=False, methods=['get']) - def unpaid(self, request): - serializer = self.get_serializer( - Bill.get_unpaid_for(self.request.user), - many=True) - return Response(serializer.data) - - @action(detail=True, methods=['get']) - def download(self, *args, **kwargs): - """ - Allow to download - """ - bill = self.get_object() - provider = UncloudProvider.get_provider() - output_file = NamedTemporaryFile() - bill_html = render_to_string("bill.html.j2", {'bill': bill}) - - bytestring_to_pdf(bill_html.encode('utf-8'), output_file) - response = FileResponse(output_file, content_type="application/pdf") - response['Content-Disposition'] = 'filename="{}_{}.pdf"'.format( - bill.reference, bill.uuid - ) - - return response - - -class OrderViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = OrderSerializer - permission_classes = [permissions.IsAuthenticated] - - def get_queryset(self): - return Order.objects.filter(owner=self.request.user) - -class VATRateViewSet(viewsets.ReadOnlyModelViewSet): - serializer_class = VATRateSerializer - permission_classes = [permissions.IsAuthenticated] - queryset = VATRate.objects.all() - -class BillingAddressViewSet(mixins.CreateModelMixin, - mixins.RetrieveModelMixin, - mixins.UpdateModelMixin, - mixins.ListModelMixin, - viewsets.GenericViewSet): - permission_classes = [permissions.IsAuthenticated] - - def get_serializer_class(self): - if self.action == 'update': - return UpdateBillingAddressSerializer - else: - return BillingAddressSerializer - - def get_queryset(self): - return self.request.user.billing_addresses.all() - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - # Validate VAT numbers. - country = serializer.validated_data["country"] - - # We ignore empty VAT numbers. - if 'vat_number' in serializer.validated_data and serializer.validated_data["vat_number"] != "": - vat_number = serializer.validated_data["vat_number"] - - if not validate_vat(country, vat_number): - return Response( - {'error': 'Malformed VAT number.'}, - status=status.HTTP_400_BAD_REQUEST) - elif country in EU_COUNTRY_CODES: - # XXX: make a synchroneous call to a third patry API here might not be a good idea.. - try: - vies_state = vies.check_vat(country, vat_number) - if not vies_state.valid: - return Response( - {'error': 'European VAT number does not exist in VIES.'}, - status=status.HTTP_400_BAD_REQUEST) - except Exception as e: - logger.warning(e) - return Response( - {'error': 'Could not validate EU VAT number against VIES. Try again later..'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - - serializer.save(owner=request.user) - return Response(serializer.data) - -### -# Admin stuff. - -class AdminPaymentViewSet(viewsets.ModelViewSet): - serializer_class = PaymentSerializer - permission_classes = [permissions.IsAdminUser] - - def get_queryset(self): - return Payment.objects.all() - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - serializer.save(timestamp=datetime.now()) - - headers = self.get_success_headers(serializer.data) - return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers) - -# Bills are generated from orders and should not be created or updated by hand. -class AdminBillViewSet(BillViewSet): - serializer_class = BillSerializer - permission_classes = [permissions.IsAdminUser] - - def get_queryset(self): - return Bill.objects.all() - - @action(detail=False, methods=['get']) - def unpaid(self, request): - unpaid_bills = [] - # XXX: works but we can do better than number of users + 1 SQL requests... - for user in get_user_model().objects.all(): - unpaid_bills = unpaid_bills + Bill.get_unpaid_for(self.request.user) - - serializer = self.get_serializer(unpaid_bills, many=True) - return Response(serializer.data) - - @action(detail=False, methods=['post']) - def generate(self, request): - users = get_user_model().objects.all() - - generated_bills = [] - for user in users: - now = timezone.now() - generated_bills = generated_bills + Bill.generate_for( - year=now.year, - month=now.month, - user=user) - - return Response( - map(lambda b: b.reference, generated_bills), - status=status.HTTP_200_OK) - -class AdminOrderViewSet(mixins.ListModelMixin, - mixins.RetrieveModelMixin, - mixins.CreateModelMixin, - mixins.UpdateModelMixin, - viewsets.GenericViewSet): - serializer_class = OrderSerializer - permission_classes = [permissions.IsAdminUser] - - def get_serializer(self, *args, **kwargs): - return self.serializer_class(*args, **kwargs, admin=True) - - def get_queryset(self): - return Order.objects.all() - - # Updates create a new order and terminate the 'old' one. - @transaction.atomic - def update(self, request, *args, **kwargs): - order = self.get_object() - partial = kwargs.pop('partial', False) - serializer = self.get_serializer(order, data=request.data, partial=partial) - serializer.is_valid(raise_exception=True) - - # Clone existing order for replacement. - replacing_order = deepcopy(order) - - # Yes, that's how you make a new entry in DB: - # https://docs.djangoproject.com/en/3.0/topics/db/queries/#copying-model-instances - replacing_order.pk = None - - for attr, value in serializer.validated_data.items(): - setattr(replacing_order, attr, value) - - # Save replacing order and terminate 'previous' one. - replacing_order.save() - order.replaced_by = replacing_order - order.save() - order.terminate() - - return Response(replacing_order) - - @action(detail=True, methods=['post']) - def terminate(self, request, pk): - order = self.get_object() - if order.is_terminated: - return Response( - {'error': 'Order is already terminated.'}, - status=status.HTTP_500_INTERNAL_SERVER_ERROR) - - else: - order.terminate() - return Response({}, status=status.HTTP_200_OK) diff --git a/uncloud_vm/admin.py b/uncloud_vm/admin.py deleted file mode 100644 index 6f3bc50..0000000 --- a/uncloud_vm/admin.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.contrib import admin - -# Register your models here. -from uncloud_vm.models import * -from uncloud_pay.models import Order - -class VMDiskInline(admin.TabularInline): - model = VMDiskProduct - -class OrderInline(admin.TabularInline): - model = Order - -class VMProductAdmin(admin.ModelAdmin): - inlines = [ - VMDiskInline - ] - -admin.site.register(VMProduct, VMProductAdmin) -admin.site.register(VMDiskProduct) diff --git a/uncloud_vm/migrations/__init__.py b/uncloud_vm/migrations/__init__.py deleted file mode 100644 index e69de29..0000000