Compare commits
529 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8287e73f6b | |||
| b7aa1c6971 | |||
|
|
e205d8d07c | ||
|
|
a463bcf7bd | ||
|
|
d872357dd1 | ||
|
|
485f08e25c | ||
|
|
745abc48ef | ||
|
|
49f52fd41d | ||
|
|
c8ce7dbb40 | ||
|
|
a920887100 | ||
|
|
6b9b15e663 | ||
|
|
48ce21f833 | ||
|
|
6c15d2086e | ||
|
|
1b06d8ee03 | ||
|
|
e225bf1cc0 | ||
|
|
e2c4a19049 | ||
|
|
74749bf07c | ||
|
|
93e5d39c7b | ||
|
|
18d4c99571 | ||
|
|
e51edab2f5 | ||
|
|
f7c68b5ca5 | ||
|
|
6efedcb381 | ||
|
|
df4c0c3060 | ||
|
|
8dd4b712fb | ||
|
|
50a395c8ec | ||
|
|
663d72269a | ||
|
|
a0fbe2d6ed | ||
|
|
858aabb5ba | ||
|
|
ece2bca831 | ||
|
|
cdab685269 | ||
|
|
689375a2fe | ||
|
|
8f83679c48 | ||
|
|
5e870f04b1 | ||
|
|
63191c0a88 | ||
|
|
03c0b34446 | ||
|
|
1922a0d92d | ||
|
|
2e6c72c093 | ||
|
|
b3626369a2 | ||
|
|
179baee96d | ||
|
|
054886fd9c | ||
|
|
e2b36c8bca | ||
|
|
372fe800cd | ||
|
|
16f3adef93 | ||
|
|
2d62388eb1 | ||
|
|
aec79cba74 | ||
|
|
cd19c47fdb | ||
|
|
cf948b03a8 | ||
|
|
5716cae900 | ||
|
|
10d5a72c5a | ||
|
|
074cffcbd7 | ||
|
|
7f32d05cd4 | ||
|
|
0fd5ac18cd | ||
|
|
ad0c2f1e9d | ||
|
|
0b1c2cc168 | ||
| 4845ab1e39 | |||
|
|
ecc9e6f734 | ||
|
|
20c7c86703 | ||
|
|
8959bc6ad5 | ||
|
|
0cd8a3a787 | ||
|
|
bbc7625550 | ||
|
|
fe4e200dc0 | ||
|
|
e03cdf214a | ||
|
|
50fd9e1f37 | ||
|
|
2e74661702 | ||
|
|
c26ff253de | ||
|
|
9623a77907 | ||
|
|
c435639241 | ||
|
|
992c7c551e | ||
|
|
58883765d7 | ||
|
|
8d8c4d660c | ||
|
|
c32499199a | ||
|
|
c6bacab35a | ||
|
|
1aead50170 | ||
|
|
d8a7964fed | ||
|
|
077c665c53 | ||
|
|
f7274fe967 | ||
|
|
1c7d81762d | ||
|
|
18f9a3848a | ||
|
|
9211894b23 | ||
|
|
b8b15704a3 | ||
|
|
ab412cb877 | ||
|
|
7b83efe995 | ||
|
|
4d5ca58b2a | ||
|
|
f693dd3d18 | ||
|
|
5ceaaf7c90 | ||
|
|
2b29e300dd | ||
|
|
8df1d8dc7c | ||
|
|
ef02cb61fd | ||
|
|
70c450afc8 | ||
|
|
0dd1093812 | ||
|
|
6a928a2b2a | ||
|
|
89519e48a9 | ||
|
|
e169b8c1d1 | ||
|
|
d7c0c40926 | ||
|
|
fd39526350 | ||
|
|
78d1de9031 | ||
|
|
db1a69561b | ||
|
|
9bf0a99f6a | ||
|
|
c9be8cc50b | ||
|
|
8da6a1e19c | ||
|
|
ff8fdb76b1 | ||
|
|
9b00ef11fb | ||
|
|
d2bd6ba200 | ||
|
|
165dacb7bf | ||
|
|
ee79877a27 | ||
|
|
2ce667e8c7 | ||
|
|
e563780142 | ||
|
|
66233a1ce5 | ||
|
|
9d5d8657cb | ||
|
|
a091079677 | ||
|
|
c9a941e290 | ||
|
|
f7b14bf507 | ||
|
|
ed40b21d16 | ||
|
|
880e4d046b | ||
|
|
cbd5a08ae7 | ||
|
|
64780bfc6c | ||
|
|
9c1b4ab275 | ||
|
|
2771a7518a | ||
|
|
9b3493a661 | ||
|
|
932ac06cea | ||
|
|
05a897db70 | ||
|
|
011096f152 | ||
|
|
55ba61e36b | ||
|
|
1265e23750 | ||
|
|
bc0c77a393 | ||
|
|
bdba3bffe2 | ||
|
|
3745a0e2b1 | ||
|
|
7ce28b0b60 | ||
|
|
662e706eab | ||
|
|
11de455d23 | ||
|
|
126d9da764 | ||
|
|
8a17ee6de5 | ||
|
|
721472b416 | ||
|
|
424ceafbb8 | ||
|
|
1e68539ed8 | ||
|
|
3ef19610f3 | ||
|
|
8decfe1b16 | ||
|
|
662845128f | ||
|
|
95011c2058 | ||
|
|
a3f3ca8cf9 | ||
|
|
5d1eaaf0af | ||
|
|
bcd141730d | ||
|
|
15535433e8 | ||
|
|
18b862c2e1 | ||
|
|
b8652c921e | ||
|
|
8bbcc5df5f | ||
|
|
0202f80a37 | ||
|
|
caedf874e4 | ||
|
|
f17f9060b0 | ||
|
|
04fac71a85 | ||
|
|
ef76304bae | ||
|
|
ec447e0dc4 | ||
|
|
ca2065a94d | ||
|
|
dc7a465a8c | ||
| 65440ab2ef | |||
| d794b24c86 | |||
| df059fb00d | |||
| 67af7b5465 | |||
| beb5bd7ee4 | |||
| 9574d69f4c | |||
| 74e2168529 | |||
| 444d6ded28 | |||
| cbba1f4169 | |||
| d47c94ba84 | |||
| 89e853b490 | |||
|
|
1b97fc8fc7 | ||
|
|
41a2f18453 | ||
| a086d46136 | |||
| ae2bad5754 | |||
| 3874165189 | |||
| 56d98cbb55 | |||
| 718abab9d2 | |||
| 268e08c4db | |||
| b8ac99acb6 | |||
| 221d98af4b | |||
| ebd4e6fa1b | |||
| b512d42058 | |||
| 1245c191c0 | |||
| 95d43f002f | |||
| 0560063326 | |||
| db3c29d17e | |||
| 892b2b6f13 | |||
|
|
aa8ade4730 | ||
|
|
594f1a9b69 | ||
|
|
e3b28354fe | ||
|
|
99a18232aa | ||
|
|
c835c874d5 | ||
|
|
028f1ebe6e | ||
|
|
9ef5309b91 | ||
|
|
4097c2ce13 | ||
|
|
736fe27493 | ||
|
|
7d708cfbb6 | ||
|
|
927fb20671 | ||
|
|
eea654a9f8 | ||
|
|
2cda6441f4 | ||
|
|
62d9ccbbef | ||
|
|
1cf20a2cb6 | ||
| 94932edebe | |||
| a15952862a | |||
| cec4263621 | |||
| f61b91dab2 | |||
| b3afad5d5d | |||
| 3a03717b12 | |||
| db9ff5d18b | |||
| a49fe6ff51 | |||
| dd0c1cba94 | |||
| c0e12884e1 | |||
| 9bbe3b3b56 | |||
| e6eba7542b | |||
| 0522927c50 | |||
| 3fa1d5753e | |||
| c6ca94800e | |||
| ad187c02da | |||
| 7afb3f8793 | |||
| 86775af4c8 | |||
| 83d2cd465d | |||
| b6c976b722 | |||
| d1e993140c | |||
| c57780fb4d | |||
| 83a0ca0e4e | |||
| 5d5bf486b5 | |||
| 839bd5f8a9 | |||
| aa0702faba | |||
| 9a57153c4e | |||
| f5897ed4b1 | |||
| 14f59430bb | |||
| 05f8f1f6c0 | |||
| 1a58508f8b | |||
| e67bd03997 | |||
| a4cc4304f9 | |||
| d3b7470294 | |||
| a7e9f3c09d | |||
|
|
ff133e81b7 | ||
|
|
85b4d70592 | ||
|
|
b55254b9b1 | ||
|
|
bc033a9087 | ||
|
|
05f38d157e | ||
|
|
bab59b1879 | ||
|
|
50b8b7a5f6 | ||
| f1bba63f6f | |||
| 276c7e9901 | |||
| 3a37343a73 | |||
| 3588ae88f9 | |||
|
|
9431f11284 | ||
|
|
e64c2b8ddb | ||
|
|
d9473e8f33 | ||
| cb3346303b | |||
|
|
7d892daff9 | ||
|
|
08b9886ce3 | ||
| d6bdf5c991 | |||
| cc7056c87c | |||
| a8b81b074b | |||
| 89c705f7d2 | |||
| f2a797874a | |||
|
|
d3f2a3e071 | ||
|
|
3d2f8574d3 | ||
|
|
1838eaf7dd | ||
|
|
8986835c7e | ||
|
|
938f0a3390 | ||
|
|
913e992a48 | ||
|
|
096f7e05c0 | ||
|
|
06c4a5643c | ||
|
|
198aaea48a | ||
|
|
d537e9e2f0 | ||
|
|
5d084a5716 | ||
|
|
8fb3ad7fe8 | ||
|
|
c44faa7a73 | ||
|
|
fa0ca2d9c1 | ||
|
|
7a6c8739f6 | ||
|
|
833d570472 | ||
|
|
3cf3439f1c | ||
|
|
23203ff418 | ||
|
|
9961ca0446 | ||
|
|
105142f76a | ||
|
|
08fe3e689e | ||
|
|
10c5257f90 | ||
|
|
a32f7522b5 | ||
|
|
4b4cbbf009 | ||
|
|
c6a9bd4363 | ||
|
|
cd01f62fde | ||
|
|
2f1aee8181 | ||
|
|
6a382fab23 | ||
|
|
b9473c1803 | ||
|
|
cc2efa5c14 | ||
|
|
5d840de55c | ||
|
|
9f4b927c74 | ||
|
|
55bd42fe64 | ||
|
|
8634d667d5 | ||
|
|
ac7ea86668 | ||
|
|
8356404fe4 | ||
|
|
723d2a99cc | ||
|
|
8f4e7cca1b | ||
| b15a12dc71 | |||
| 923102af24 | |||
| f4ebbb79ce | |||
| 122bc586b4 | |||
| ae6548e168 | |||
| 0e4068cea8 | |||
| 623d3ae5c4 | |||
| fe0e6d98bf | |||
| 41e35c1af0 | |||
| 948391ab2e | |||
| d089d06264 | |||
| c086dbd357 | |||
| 545727afe7 | |||
| 7bbc729b87 | |||
| b10cae472e | |||
| 952cf8fd13 | |||
| 7e278228bd | |||
| 31507c0f1a | |||
| 7e58a8ace2 | |||
| a4fa0def4b | |||
| 80fe28588e | |||
| 4e658d2d77 | |||
| 5161a74354 | |||
| 0e62ccff3b | |||
| bf83b750de | |||
|
|
47148454f6 | ||
|
|
0032c272e7 | ||
|
|
263125048d | ||
| 658262c599 | |||
| c41b55573a | |||
| 4016c28c5f | |||
|
|
aa8336b7e4 | ||
|
|
0e6a6afd88 | ||
|
|
efbe1c0596 | ||
| 6c7f0e98b3 | |||
|
|
b8c2f80e45 | ||
|
|
139aca6a61 | ||
|
|
2a73f0e767 | ||
|
|
66e224e926 | ||
|
|
10f09c7115 | ||
|
|
ec7a2a3c3a | ||
| b07df26eb2 | |||
| b958cc77ea | |||
| e9b6a6f277 | |||
| 546667d117 | |||
| b88dfa4bfe | |||
| d6ee806467 | |||
| a41184d83d | |||
| 2f70418f4d | |||
| 21e1a3d220 | |||
| 4cc19e1e6e | |||
| 08bf7cd320 | |||
| 7e9f2ea561 | |||
| 929211162d | |||
|
|
cf17373b3f | ||
|
|
14a4fa8cc1 | ||
|
|
4fc1c36ae9 | ||
| 371c5ccf00 | |||
| faca104459 | |||
| 9e8149135b | |||
| 02b287eff8 | |||
| 9aabc66e57 | |||
| a662b1fe29 | |||
| 88c10e2e4a | |||
| fea0568bb9 | |||
| e0cb6ac670 | |||
|
|
ebc9238845 | ||
| 94a39ed81d | |||
| 5c2d2a5b94 | |||
|
|
e9ef2acb06 | ||
| 3846e49395 | |||
| a849e642dd | |||
| 28407bf3e3 | |||
|
|
ea00e81b1e | ||
| 53baf0d9f3 | |||
| 11e22f5001 | |||
|
|
e176ad0817 | ||
|
|
a50095f873 | ||
| a40da40169 | |||
| 2eaaad49db | |||
| 9fdf66ed74 | |||
| b31aa72f84 | |||
| 5559d600c7 | |||
| 4e51670a90 | |||
| 4ad737ed90 | |||
| c651c4ddaa | |||
| 531bfa1768 | |||
| 750d8c8cbf | |||
| 0c3e6d10ae | |||
| afdba3d7d9 | |||
| 52e74c22cc | |||
| 6c9c63e0da | |||
| 9e9018060e | |||
| 9e253d497b | |||
| 81bd54116a | |||
| 8e41b894c0 | |||
|
|
3228b91038 | ||
|
|
028fd6789f | ||
| 4f25086a63 | |||
| be2b0a8855 | |||
|
|
4115eed2a8 | ||
|
|
5c33bc5c02 | ||
|
|
6a38e4e0a4 | ||
|
|
bcbd6f6f83 | ||
| e319d1d151 | |||
| af1265003e | |||
| eaa483e018 | |||
| 181005ad6c | |||
| b3bbfafa04 | |||
| 33cc2b2111 | |||
| 3b87a47430 | |||
| b5a242f176 | |||
| 1cb1de4876 | |||
| c0512e54b0 | |||
| e12575e1de | |||
| adb57c55ca | |||
|
|
89215e47b6 | ||
| 37ed126bc1 | |||
| 4bed53c8a8 | |||
| 059791e2f2 | |||
| ef5e7e8035 | |||
| b1649a6228 | |||
| 0e28e50bac | |||
| 38d3a3a5d3 | |||
| 809a55e1dd | |||
| b2fe5014d8 | |||
| 1dd3324275 | |||
| 36fcff5149 | |||
|
|
288a65f219 | ||
|
|
bd6008462d | ||
|
|
70a4fe4d90 | ||
|
|
e89fb45f9c | ||
| 1ff5702ce3 | |||
|
|
b722f30ea5 | ||
| f5eadd6ddb | |||
|
|
1445acb77a | ||
|
|
7bf4f2adb2 | ||
|
|
a9aac39486 | ||
| 225f20c91b | |||
| b9b605f407 | |||
| f358acca05 | |||
|
|
41a5eae879 | ||
|
|
657dfc541e | ||
|
|
fd648ade65 | ||
| 06ab21c577 | |||
|
|
11d629bb51 | ||
|
|
aa59b05a2d | ||
|
|
a58a361254 | ||
|
|
1ca247148c | ||
| 033b1e846a | |||
|
|
c0bf4d96c4 | ||
|
|
0a6fa031d3 | ||
|
|
0c7ca1147a | ||
|
|
df851bee08 | ||
|
|
0b60765e2b | ||
|
|
bd3d21faa9 | ||
|
|
c7ded96658 | ||
|
|
d4b170f813 | ||
|
|
446c13b77c | ||
|
|
defe36bfb6 | ||
|
|
e790063d5a | ||
|
|
cc3d2f2d42 | ||
| d658b9635d | |||
|
|
7d1c8df84d | ||
|
|
cc9e5905eb | ||
| c7252cde53 | |||
| a72bc142a6 | |||
| 739bd72526 | |||
|
|
15b0fe3dc9 | ||
|
|
50df7050d6 | ||
|
|
734c406245 | ||
| b3e505d37c | |||
|
|
98f84f1cf5 | ||
|
|
8c6e4eee00 | ||
|
|
2900844f63 | ||
|
|
46921c43ad | ||
| fa4d7a1d70 | |||
|
|
edbfb7964e | ||
| e4f2f446f5 | |||
|
|
e2b5b5d102 | ||
| 7b09f0a373 | |||
|
|
cee45b5227 | ||
|
|
94633d6cc8 | ||
|
|
1d1ae6fb3e | ||
|
|
f8c29aa1d6 | ||
|
|
fc4ec7b0f8 | ||
|
|
f588691f0d | ||
|
|
581865460b | ||
|
|
7f821b4d5a | ||
|
|
ce0da4b827 | ||
|
|
de06b9ee22 | ||
|
|
26449d3159 | ||
| 71a764ce1e | |||
| 5f28e9630c | |||
|
|
2aa22803f4 | ||
|
|
dc34c0ecd4 | ||
|
|
4f4a4be839 | ||
|
|
b1bb6bc314 | ||
|
|
b67f41cc35 | ||
|
|
dc5092be71 | ||
|
|
4df7c761d3 | ||
| d61a7e670f | |||
| 5de973b204 | |||
|
|
0708a1e1fd | ||
|
|
a5695ffa48 | ||
|
|
6ba224638a | ||
|
|
2cda4dd57b | ||
|
|
c456355059 | ||
|
|
118c66799c | ||
|
|
f8182e00e8 | ||
|
|
9fd445e947 | ||
|
|
254429db55 | ||
|
|
e472d20ae0 | ||
|
|
315aaded41 | ||
|
|
13292db39e | ||
|
|
0a1ccadda2 | ||
|
|
8160d01471 | ||
| 8c353f277c | |||
| bb18f6b0e9 | |||
| 00b35e0567 | |||
| 9c7d458eec | |||
|
|
074efffaa7 | ||
| cee92f2e99 | |||
| ce709c3b6f | |||
| 519279ce6f | |||
| 7b9a970307 | |||
| 5f1f451bc2 | |||
| e37592bdc6 | |||
| 347843cb24 | |||
|
|
aa9548e753 | ||
|
|
c1f384fb9a | ||
|
|
b38c9b6060 | ||
|
|
a80a279ba5 | ||
|
|
5ef009cc9b | ||
| 200a7672f2 | |||
| 1a76d2b5f3 | |||
| da54a59ca2 |
329 changed files with 18038 additions and 117 deletions
13
.gitignore
vendored
13
.gitignore
vendored
|
|
@ -1,6 +1,12 @@
|
||||||
.idea
|
.idea/
|
||||||
.vscode
|
.vscode/
|
||||||
|
__pycache__/
|
||||||
|
|
||||||
|
pay.conf
|
||||||
|
log.txt
|
||||||
|
test.py
|
||||||
|
STRIPE
|
||||||
|
venv/
|
||||||
|
|
||||||
uncloud/docs/build
|
uncloud/docs/build
|
||||||
logs.txt
|
logs.txt
|
||||||
|
|
@ -16,5 +22,6 @@ uncloud/version.py
|
||||||
build/
|
build/
|
||||||
venv/
|
venv/
|
||||||
dist/
|
dist/
|
||||||
|
.history/
|
||||||
*.iso
|
*.iso
|
||||||
|
*.sqlite3
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,18 @@
|
||||||
image: python:3
|
stages:
|
||||||
|
- lint
|
||||||
|
- test
|
||||||
|
|
||||||
before_script:
|
run-tests:
|
||||||
- python setup.py install
|
stage: test
|
||||||
|
image: code.ungleich.ch:5050/uncloud/uncloud/uncloud-ci:latest
|
||||||
python_tests:
|
services:
|
||||||
script:
|
- postgres:latest
|
||||||
- python -m unittest -v test/test_mac_local.py
|
variables:
|
||||||
|
DATABASE_HOST: postgres
|
||||||
|
DATABASE_USER: postgres
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
coverage: /^TOTAL.+?(\d+\%)$/
|
||||||
|
script:
|
||||||
|
- pip install -r requirements.txt
|
||||||
|
- coverage run --source='.' ./manage.py test
|
||||||
|
- coverage report
|
||||||
|
|
|
||||||
71
README.md
71
README.md
|
|
@ -1,3 +1,70 @@
|
||||||
# ucloud
|
# Uncloud
|
||||||
|
|
||||||
Checkout https://ungleich.ch/ucloud/ for the documentation of ucloud.
|
Cloud management platform, the ungleich way.
|
||||||
|
|
||||||
|
|
||||||
|
[](https://code.ungleich.ch/uncloud/uncloud/commits/master)
|
||||||
|
[](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`
|
||||||
|
|
|
||||||
6
archive/issues.org
Normal file
6
archive/issues.org
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
* 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
|
||||||
55
archive/uncloud_django_based/hacks/abk-hacks.py
Normal file
55
archive/uncloud_django_based/hacks/abk-hacks.py
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
"""
|
||||||
|
investigate into a simple python function that maps an ldap user to a vat percentage. Basically you need to
|
||||||
|
lookup the customer address, check if she is a business/registered tax number and if not apply the local
|
||||||
|
vat
|
||||||
|
"""
|
||||||
|
|
||||||
|
import iso3166
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from csv import DictReader
|
||||||
|
|
||||||
|
|
||||||
|
def get_vat(street_address, city, postal_code, country, vat_number=None):
|
||||||
|
vat = {
|
||||||
|
'Austria': [
|
||||||
|
{'period': '1984-01-01/', 'rate': 0.2},
|
||||||
|
{'period': '1976-01-01/1984-01-01', 'rate': 0.18},
|
||||||
|
{'period': '1973-01-01/1976-01-01', 'rate': 0.16},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
return iso3166.countries.get(country)
|
||||||
|
|
||||||
|
# return iso3166.countries_by_name[country]
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# vat = get_vat(
|
||||||
|
# street_address='82 Nasheman-e-Iqbal near Wapda Town',
|
||||||
|
# city='Lahore',
|
||||||
|
# postal_code=53700,
|
||||||
|
# country='Pakistan',
|
||||||
|
# )
|
||||||
|
# print(vat)
|
||||||
|
vat_rates = {}
|
||||||
|
with open('vat_rates.csv', newline='') as csvfile:
|
||||||
|
reader = DictReader(csvfile)
|
||||||
|
for row in reader:
|
||||||
|
territory_codes = row['territory_codes'].split('\n')
|
||||||
|
for code in territory_codes:
|
||||||
|
if code not in vat_rates:
|
||||||
|
vat_rates[code] = {}
|
||||||
|
|
||||||
|
start_date = row['start_date']
|
||||||
|
stop_data = row['stop_date']
|
||||||
|
time_period = f'{start_date}|{stop_data}'
|
||||||
|
r = row.copy()
|
||||||
|
del r['start_date']
|
||||||
|
del r['stop_date']
|
||||||
|
del r['territory_codes']
|
||||||
|
vat_rates[code][time_period] = r
|
||||||
|
print(vat_rates)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
import importlib
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
from os.path import join as join_path
|
||||||
|
from xmlrpc.client import ServerProxy as RPCClient
|
||||||
|
|
||||||
|
root = os.path.dirname(os.getcwd())
|
||||||
|
sys.path.append(join_path(root, 'uncloud'))
|
||||||
|
secrets = importlib.import_module('uncloud.secrets')
|
||||||
|
|
||||||
|
|
||||||
|
class OpenNebula:
|
||||||
|
def __init__(self, url, session_string):
|
||||||
|
self.session_string = session_string
|
||||||
|
self.client = RPCClient(secrets.OPENNEBULA_URL)
|
||||||
|
|
||||||
|
def create_user(self, username, password, authentication_driver='', group_id=None):
|
||||||
|
# https://docs.opennebula.org/5.10/integration/system_interfaces/api.html#one-user-allocate
|
||||||
|
|
||||||
|
if group_id is None:
|
||||||
|
group_id = []
|
||||||
|
|
||||||
|
return self.client.one.user.allocate(
|
||||||
|
self.session_string,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
authentication_driver,
|
||||||
|
group_id
|
||||||
|
)
|
||||||
|
|
||||||
|
def chmod(self, vm_id, user_id=-1, group_id=-1):
|
||||||
|
# https://docs.opennebula.org/5.10/integration/system_interfaces/api.html#one-vm-chown
|
||||||
|
|
||||||
|
return self.client.one.vm.chown(self.session_string, vm_id, user_id, group_id)
|
||||||
|
|
||||||
|
|
||||||
|
one = OpenNebula(secrets.OPENNEBULA_URL, secrets.OPENNEBULA_USER_PASS)
|
||||||
|
|
||||||
|
# Create User in OpenNebula
|
||||||
|
# success, response, *_ = one.create_user(username='meow12345', password='hello_world')
|
||||||
|
# print(success, response)
|
||||||
|
|
||||||
|
# Change owner of a VM
|
||||||
|
# success, response, *_ = one.chmod(vm_id=25589, user_id=706)
|
||||||
|
# print(success, response)
|
||||||
18
archive/uncloud_django_based/hacks/command-wrapper.sh
Normal file
18
archive/uncloud_django_based/hacks/command-wrapper.sh
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/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 ..."
|
||||||
51
archive/uncloud_django_based/meow-payv1/README.md
Normal file
51
archive/uncloud_django_based/meow-payv1/README.md
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
# uncloud-pay
|
||||||
|
|
||||||
|
The generic product/payment system.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
pip3 install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
python ucloud_pay.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
#### 1. Adding of products
|
||||||
|
```shell script
|
||||||
|
http --json http://[::]:5000/product/add username=your_username_here password=your_password_here specs:=@ipv6-only-vm.json
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Listing of products
|
||||||
|
```shell script
|
||||||
|
http --json http://[::]:5000/product/list
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Registering user's payment method (credit card for now using Stripe)
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
http --json http://[::]:5000/user/register_payment card_number=4111111111111111 cvc=123 expiry_year=2020 expiry_month=8 card_holder_name="The test user" username=your_username_here password=your_password_here line1="your_billing_address" city="your_city" country="your_country"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Ordering products
|
||||||
|
|
||||||
|
First of all, user have to buy the membership first.
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
http --json http://[::]:5000/product/order username=your_username_here password=your_password_here product_id=membership pay=True
|
||||||
|
```
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
http --json http://[::]:5000/product/order username=your_username_here password=your_password_here product_id=ipv6-only-vm cpu=1 ram=1 os-disk-space=10 os=alpine pay=True
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 5. Listing users orders
|
||||||
|
|
||||||
|
```shell script
|
||||||
|
http --json POST http://[::]:5000/order/list username=your_username_here password=your_password_here
|
||||||
|
```
|
||||||
21
archive/uncloud_django_based/meow-payv1/config.py
Normal file
21
archive/uncloud_django_based/meow-payv1/config.py
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ungleich_common.ldap.ldap_manager import LdapManager
|
||||||
|
from ungleich_common.std.configparser import StrictConfigParser
|
||||||
|
from ungleich_common.etcd.etcd_wrapper import EtcdWrapper
|
||||||
|
|
||||||
|
config_file = os.environ.get('meow-pay-config-file', default='pay.conf')
|
||||||
|
|
||||||
|
config = StrictConfigParser(allow_no_value=True)
|
||||||
|
config.read(config_file)
|
||||||
|
|
||||||
|
etcd_client = EtcdWrapper(
|
||||||
|
host=config.get('etcd', 'host'), port=config.get('etcd', 'port'),
|
||||||
|
ca_cert=config.get('etcd', 'ca_cert'), cert_key=config.get('etcd', 'cert_key'),
|
||||||
|
cert_cert=config.get('etcd', 'cert_cert')
|
||||||
|
)
|
||||||
|
|
||||||
|
ldap_manager = LdapManager(
|
||||||
|
server=config.get('ldap', 'server'), admin_dn=config.get('ldap', 'admin_dn'),
|
||||||
|
admin_password=config.get('ldap', 'admin_password')
|
||||||
|
)
|
||||||
213
archive/uncloud_django_based/meow-payv1/hack-a-vpn.py
Normal file
213
archive/uncloud_django_based/meow-payv1/hack-a-vpn.py
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
from flask import Flask, request
|
||||||
|
from flask_restful import Resource, Api
|
||||||
|
import etcd3
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
|
from ldaptest import is_valid_ldap_user
|
||||||
|
|
||||||
|
def authenticate(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
if not getattr(func, 'authenticated', True):
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
# pass in username/password !
|
||||||
|
acct = basic_authentication() # custom account lookup function
|
||||||
|
|
||||||
|
if acct:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
flask_restful.abort(401)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
def readable_errors(func):
|
||||||
|
@wraps(func)
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
except etcd3.exceptions.ConnectionFailedError as e:
|
||||||
|
raise UncloudException('Cannot connect to etcd: is etcd running and reachable? {}'.format(e))
|
||||||
|
except etcd3.exceptions.ConnectionTimeoutError as e:
|
||||||
|
raise UncloudException('etcd connection timeout. {}'.format(e))
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
class DB(object):
|
||||||
|
def __init__(self, config, prefix="/"):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
# Root for everything
|
||||||
|
self.base_prefix= '/nicohack'
|
||||||
|
|
||||||
|
# Can be set from outside
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
self.connect()
|
||||||
|
|
||||||
|
@readable_errors
|
||||||
|
def connect(self):
|
||||||
|
self._db_clients = []
|
||||||
|
for endpoint in self.config.etcd_hosts:
|
||||||
|
client = etcd3.client(host=endpoint, **self.config.etcd_args)
|
||||||
|
self._db_clients.append(client)
|
||||||
|
|
||||||
|
def realkey(self, key):
|
||||||
|
return "{}{}/{}".format(self.base_prefix,
|
||||||
|
self.prefix,
|
||||||
|
key)
|
||||||
|
|
||||||
|
@readable_errors
|
||||||
|
def get(self, key, as_json=False, **kwargs):
|
||||||
|
value, _ = self._db_clients[0].get(self.realkey(key), **kwargs)
|
||||||
|
|
||||||
|
if as_json:
|
||||||
|
value = json.loads(value)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
@readable_errors
|
||||||
|
def set(self, key, value, as_json=False, **kwargs):
|
||||||
|
if as_json:
|
||||||
|
value = json.dumps(value)
|
||||||
|
|
||||||
|
# FIXME: iterate over clients in case of failure ?
|
||||||
|
return self._db_clients[0].put(self.realkey(key), value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class Membership(Resource):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
print("{} {}".format(data, config))
|
||||||
|
return {'message': 'Order successful' }, 200
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
print("{} {}".format(data, config))
|
||||||
|
return {'message': 'Order 2x successful' }, 200
|
||||||
|
|
||||||
|
|
||||||
|
class Order(Resource):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
print("{} {}".format(data, config))
|
||||||
|
|
||||||
|
|
||||||
|
class Product(Resource):
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
|
||||||
|
self.products = []
|
||||||
|
self.products.append(
|
||||||
|
{ "name": "membership-free",
|
||||||
|
"description": """
|
||||||
|
This membership gives you access to the API and includes a VPN
|
||||||
|
with 1 IPv6 address.
|
||||||
|
See https://redmine.ungleich.ch/issues/7747?
|
||||||
|
""",
|
||||||
|
"uuid": "a3883466-0012-4d01-80ff-cbf7469957af",
|
||||||
|
"recurring": True,
|
||||||
|
"recurring_time_frame": "per_year",
|
||||||
|
"features": [
|
||||||
|
{ "name": "membership",
|
||||||
|
"price_one_time": 0,
|
||||||
|
"price_recurring": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.products.append(
|
||||||
|
{ "name": "membership-standard",
|
||||||
|
"description": """
|
||||||
|
This membership gives you access to the API and includes an IPv6-VPN with
|
||||||
|
one IPv6 address ("Road warrior")
|
||||||
|
See https://redmine.ungleich.ch/issues/7747?
|
||||||
|
""",
|
||||||
|
"uuid": "1d85296b-0863-4dd6-a543-a6d5a4fbe4a6",
|
||||||
|
"recurring": True,
|
||||||
|
"recurring_time_frame": "per_month",
|
||||||
|
"features": [
|
||||||
|
{ "name": "membership",
|
||||||
|
"price_one_time": 0,
|
||||||
|
"price_recurring": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.products.append(
|
||||||
|
{ "name": "membership-premium",
|
||||||
|
"description": """
|
||||||
|
This membership gives you access to the API and includes an
|
||||||
|
IPv6-VPN with a /48 IPv6 network.
|
||||||
|
See https://redmine.ungleich.ch/issues/7747?
|
||||||
|
""",
|
||||||
|
"uuid": "bfd63fd2-d227-436f-a8b8-600de74dd6ce",
|
||||||
|
"recurring": True,
|
||||||
|
"recurring_time_frame": "per_month",
|
||||||
|
"features": [
|
||||||
|
{ "name": "membership",
|
||||||
|
"price_one_time": 0,
|
||||||
|
"price_recurring": 5
|
||||||
|
}
|
||||||
|
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.products.append(
|
||||||
|
{ "name": "ipv6-vpn-with-/48",
|
||||||
|
"description": """
|
||||||
|
An IPv6 VPN with a /48 network included.
|
||||||
|
""",
|
||||||
|
"uuid": "fe5753f8-6fe1-4dc4-9b73-7b803de4c597",
|
||||||
|
"recurring": True,
|
||||||
|
"recurring_time_frame": "per_year",
|
||||||
|
"features": [
|
||||||
|
{ "name": "vpn",
|
||||||
|
"price_one_time": 0,
|
||||||
|
"price_recurring": 120
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
print("{} {}".format(data, config))
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
print("{} {}".format(data, config))
|
||||||
|
|
||||||
|
return self.products
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
config['etcd_url']="https://etcd1.ungleich.ch"
|
||||||
|
config['ldap_url']="ldaps://ldap1.ungleich.ch"
|
||||||
|
|
||||||
|
api = Api(app)
|
||||||
|
api.add_resource(Order, '/orders', resource_class_args=( config, ))
|
||||||
|
api.add_resource(Product, '/products', resource_class_args=( config, ))
|
||||||
|
api.add_resource(Membership, '/membership', resource_class_args=( config, ))
|
||||||
|
|
||||||
|
app.run(host='::', port=5000, debug=True)
|
||||||
87
archive/uncloud_django_based/meow-payv1/helper.py
Normal file
87
archive/uncloud_django_based/meow-payv1/helper.py
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import parsedatetime
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from stripe_utils import StripeUtils
|
||||||
|
|
||||||
|
|
||||||
|
def get_plan_id_from_product(product):
|
||||||
|
plan_id = 'ucloud-v1-'
|
||||||
|
plan_id += product['name'].strip().replace(' ', '-')
|
||||||
|
return plan_id
|
||||||
|
|
||||||
|
|
||||||
|
def get_pricing(price_in_chf_cents, product_type, recurring_period):
|
||||||
|
if product_type == 'recurring':
|
||||||
|
return 'CHF {}/{}'.format(price_in_chf_cents/100, recurring_period)
|
||||||
|
elif product_type == 'one-time':
|
||||||
|
return 'CHF {} (One time charge)'.format(price_in_chf_cents/100)
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_friendly_product(product_dict):
|
||||||
|
uf_product = {
|
||||||
|
'name': product_dict['name'],
|
||||||
|
'description': product_dict['description'],
|
||||||
|
'product_id': product_dict['usable-id'],
|
||||||
|
'pricing': get_pricing(
|
||||||
|
product_dict['price'], product_dict['type'], product_dict['recurring_period']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if product_dict['type'] == 'recurring':
|
||||||
|
uf_product['minimum_subscription_period'] = product_dict['minimum_subscription_period']
|
||||||
|
return uf_product
|
||||||
|
|
||||||
|
|
||||||
|
def get_token(card_number, cvc, exp_month, exp_year):
|
||||||
|
stripe_utils = StripeUtils()
|
||||||
|
token_response = stripe_utils.get_token_from_card(
|
||||||
|
card_number, cvc, exp_month, exp_year
|
||||||
|
)
|
||||||
|
if token_response['response_object']:
|
||||||
|
return token_response['response_object'].id
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_product(usable_id, etcd_client):
|
||||||
|
products = etcd_client.get_prefix('/v1/products/', value_in_json=True)
|
||||||
|
for p in products:
|
||||||
|
if p.value['usable-id'] == usable_id:
|
||||||
|
return p.value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_charges(specification, data):
|
||||||
|
logging.debug('Calculating charges for specs:{} and data:{}'.format(specification, data))
|
||||||
|
one_time_charge = 0
|
||||||
|
recurring_charge = 0
|
||||||
|
for feature_name, feature_detail in specification['features'].items():
|
||||||
|
if feature_detail['constant']:
|
||||||
|
data[feature_name] = 1
|
||||||
|
|
||||||
|
if feature_detail['unit']['type'] != 'str':
|
||||||
|
one_time_charge += feature_detail['one_time_fee']
|
||||||
|
recurring_charge += (
|
||||||
|
feature_detail['price_per_unit_per_period'] * data[feature_name] /
|
||||||
|
feature_detail['unit']['value']
|
||||||
|
)
|
||||||
|
return one_time_charge, recurring_charge
|
||||||
|
|
||||||
|
|
||||||
|
def is_order_valid(order_timestamp, renewal_period):
|
||||||
|
"""
|
||||||
|
Sample Code Usage
|
||||||
|
|
||||||
|
>> current_datetime, status = cal.parse('Now')
|
||||||
|
>> current_datetime = datetime(*current_datetime[:6])
|
||||||
|
|
||||||
|
>> print('Is order valid: ', is_order_valid(current_datetime, '1 month'))
|
||||||
|
>> True
|
||||||
|
"""
|
||||||
|
cal = parsedatetime.Calendar()
|
||||||
|
|
||||||
|
renewal_datetime, status = cal.parse(renewal_period)
|
||||||
|
renewal_datetime = datetime(*renewal_datetime[:6])
|
||||||
|
|
||||||
|
return order_timestamp <= renewal_datetime
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"usable-id": "ipv6-only-django-hosting",
|
||||||
|
"active": true,
|
||||||
|
"name": "IPv6 Only Django Hosting",
|
||||||
|
"description": "Host your Django application on our shiny IPv6 Only VM",
|
||||||
|
"recurring_period": "month",
|
||||||
|
"quantity": "inf",
|
||||||
|
"features": {
|
||||||
|
"cpu": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 3,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": false
|
||||||
|
},
|
||||||
|
"ram": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 4,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": false
|
||||||
|
},
|
||||||
|
"os-disk-space": {
|
||||||
|
"unit": {"value": 10, "type":"int"},
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"price_per_unit_per_period": 3.5,
|
||||||
|
"constant": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"usable-id": "ipv6-only-vm",
|
||||||
|
"active": true,
|
||||||
|
"name": "IPv6 Only VM",
|
||||||
|
"description": "IPv6 Only VM are accessible to only those having IPv6 for themselves",
|
||||||
|
"recurring_period": "month",
|
||||||
|
"quantity": "inf",
|
||||||
|
"features": {
|
||||||
|
"cpu": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 3,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": false
|
||||||
|
},
|
||||||
|
"ram": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 4,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": false
|
||||||
|
},
|
||||||
|
"os-disk-space": {
|
||||||
|
"unit": {"value": 10, "type":"int"},
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"price_per_unit_per_period": 4,
|
||||||
|
"constant": false
|
||||||
|
},
|
||||||
|
"os": {
|
||||||
|
"unit": {"value": 1, "type":"str"},
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"price_per_unit_per_period": 0,
|
||||||
|
"constant": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"usable-id": "ipv6-only-vpn",
|
||||||
|
"active": true,
|
||||||
|
"name": "IPv6 Only VPN",
|
||||||
|
"description": "IPv6 VPN enable you to access IPv6 only websites and more",
|
||||||
|
"recurring_period": "month",
|
||||||
|
"quantity": "inf",
|
||||||
|
"features": {
|
||||||
|
"vpn": {
|
||||||
|
"unit": {"value": 1, "type": "int"},
|
||||||
|
"price_per_unit_per_period": 10,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"usable-id": "ipv6-box",
|
||||||
|
"active": true,
|
||||||
|
"name": "IPv6 Box",
|
||||||
|
"description": "A ready-to-go IPv6 Box: it creates a VPN to ungleich and distributes IPv6 addresses to all your computers.",
|
||||||
|
"recurring_period": "eternity",
|
||||||
|
"quantity": 4,
|
||||||
|
"features": {
|
||||||
|
"ipv6-box": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 0,
|
||||||
|
"one_time_fee": 250,
|
||||||
|
"constant": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"usable-id": "membership",
|
||||||
|
"active": true,
|
||||||
|
"name": "Membership",
|
||||||
|
"description": "Membership to use uncloud-pay",
|
||||||
|
"recurring_period": "month",
|
||||||
|
"quantity": "inf",
|
||||||
|
"features": {
|
||||||
|
"membership": {
|
||||||
|
"unit": {"value": 1, "type":"int"},
|
||||||
|
"price_per_unit_per_period": 5,
|
||||||
|
"one_time_fee": 0,
|
||||||
|
"constant": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"max_per_user": "1"
|
||||||
|
}
|
||||||
7
archive/uncloud_django_based/meow-payv1/requirements.txt
Normal file
7
archive/uncloud_django_based/meow-payv1/requirements.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
stripe
|
||||||
|
flask
|
||||||
|
Flask-RESTful
|
||||||
|
git+https://code.ungleich.ch/ahmedbilal/ungleich-common/#egg=ungleich-common-etcd&subdirectory=etcd
|
||||||
|
git+https://code.ungleich.ch/ahmedbilal/ungleich-common/#egg=ungleich-common-ldap&subdirectory=ldap
|
||||||
|
git+https://code.ungleich.ch/ahmedbilal/ungleich-common/#egg=ungleich-common-std&subdirectory=std
|
||||||
|
git+https://code.ungleich.ch/ahmedbilal/ungleich-common/#egg=ungleich-common-schemas&subdirectory=schemas
|
||||||
17
archive/uncloud_django_based/meow-payv1/sample-pay.conf
Normal file
17
archive/uncloud_django_based/meow-payv1/sample-pay.conf
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
[etcd]
|
||||||
|
host = 127.0.0.1
|
||||||
|
port = 2379
|
||||||
|
ca_cert
|
||||||
|
cert_cert
|
||||||
|
cert_key
|
||||||
|
|
||||||
|
[stripe]
|
||||||
|
private_key=stripe_private_key
|
||||||
|
|
||||||
|
[app]
|
||||||
|
port = 5000
|
||||||
|
|
||||||
|
[ldap]
|
||||||
|
server = ldap_server_url
|
||||||
|
admin_dn = ldap_admin_dn
|
||||||
|
admin_password = ldap_admin_password
|
||||||
136
archive/uncloud_django_based/meow-payv1/schemas.py
Normal file
136
archive/uncloud_django_based/meow-payv1/schemas.py
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
import logging
|
||||||
|
import config
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
|
||||||
|
from config import ldap_manager, etcd_client
|
||||||
|
from helper import resolve_product
|
||||||
|
from ungleich_common.schemas.schemas import BaseSchema, Field, ValidationException
|
||||||
|
|
||||||
|
|
||||||
|
class AddProductSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
self.add_schema(UserCredentialSchema, data)
|
||||||
|
self.specs = Field('specs', dict, **self.get(data, 'specs'))
|
||||||
|
self.update = Field('update', bool, **self.get(data, 'update', return_default=True, default=False))
|
||||||
|
|
||||||
|
def validation(self):
|
||||||
|
user = self.objects['user']
|
||||||
|
user = json.loads(user.entry_to_json())
|
||||||
|
uid, ou, *dc = user['dn'].replace('ou=', '').replace('dc=', '').replace('uid=', '').split(',')
|
||||||
|
if ou != config.config.get('ldap', 'internal_user_ou', fallback='users'):
|
||||||
|
raise ValidationException('You do not have access to create product.')
|
||||||
|
|
||||||
|
product = resolve_product(self.specs.value['usable-id'], etcd_client)
|
||||||
|
if product:
|
||||||
|
self.objects['product'] = product
|
||||||
|
|
||||||
|
|
||||||
|
class AddressSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
self.line1 = Field('line1', str, **self.get(data, 'line1'))
|
||||||
|
self.line2 = Field('line2', str, **self.get(data, 'line2', return_default=True))
|
||||||
|
self.city = Field('city', str, **self.get(data, 'city'))
|
||||||
|
self.country = Field('country', str, **self.get(data, 'country'))
|
||||||
|
self.state = Field('state', str, **self.get(data, 'state', return_default=True))
|
||||||
|
self.postal_code = Field('postal_code', str, **self.get(data, 'postal_code', return_default=True))
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterPaymentSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.add_schema(UserCredentialSchema, data)
|
||||||
|
self.add_schema(AddressSchema, data, under_field_name='address')
|
||||||
|
|
||||||
|
self.card_number = Field('card_number', str, **self.get(data, 'card_number'))
|
||||||
|
self.cvc = Field('cvc', str, **self.get(data, 'cvc'))
|
||||||
|
self.expiry_year = Field('expiry_year', int, **self.get(data, 'expiry_year'))
|
||||||
|
self.expiry_month = Field('expiry_month', int, **self.get(data, 'expiry_month'))
|
||||||
|
self.card_holder_name = Field('card_holder_name', str, **self.get(data, 'card_holder_name'))
|
||||||
|
|
||||||
|
|
||||||
|
class UserCredentialSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
self.username = Field('username', str, **self.get(data, 'username'))
|
||||||
|
self.password = Field('password', str, **self.get(data, 'password'))
|
||||||
|
|
||||||
|
def validation(self):
|
||||||
|
try:
|
||||||
|
entry = ldap_manager.is_password_valid(self.username.value, self.password.value, query_key='uid')
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationException('No user with \'{}\' username found. You can create account at '
|
||||||
|
'https://account.ungleich.ch'.format(self.username.value))
|
||||||
|
except Exception:
|
||||||
|
raise ValidationException('Invalid username/password.')
|
||||||
|
else:
|
||||||
|
self.objects['user'] = entry
|
||||||
|
|
||||||
|
|
||||||
|
class ProductOrderSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
self.product_id = Field(
|
||||||
|
'product_id', str, **self.get(data, 'product_id'), validators=[self.product_id_validation]
|
||||||
|
)
|
||||||
|
self.pay_consent = Field('pay', bool, **self.get(data, 'pay', return_default=True, default=False))
|
||||||
|
self.add_schema(UserCredentialSchema, data)
|
||||||
|
|
||||||
|
def product_id_validation(self):
|
||||||
|
product = resolve_product(self.product_id.value, etcd_client)
|
||||||
|
if product:
|
||||||
|
product['quantity'] = float(product['quantity'])
|
||||||
|
self.product_id.value = product['uuid']
|
||||||
|
self.objects['product'] = product
|
||||||
|
logging.debug('Got product {}'.format(product))
|
||||||
|
|
||||||
|
if not product['active']:
|
||||||
|
raise ValidationException('Product is not active at the moment.')
|
||||||
|
|
||||||
|
if product['quantity'] <= 0:
|
||||||
|
raise ValidationException('Out of stock.')
|
||||||
|
else:
|
||||||
|
raise ValidationException('No such product exists.')
|
||||||
|
|
||||||
|
def validation(self):
|
||||||
|
username = self.objects['user'].uid
|
||||||
|
customer_previous_orders = etcd_client.get_prefix('/v1/user/{}'.format(username), value_in_json=True)
|
||||||
|
customer_previous_orders = [o.value for o in customer_previous_orders]
|
||||||
|
membership = next(filter(lambda o: o['product'] == 'membership', customer_previous_orders), None)
|
||||||
|
if membership is None and self.objects['product']['usable-id'] != 'membership':
|
||||||
|
raise ValidationException('Please buy membership first to use this facility')
|
||||||
|
max_quantity_user_can_order = float(self.objects['product'].get('max_per_user', math.inf))
|
||||||
|
previous_order_of_same_product = [
|
||||||
|
o for o in customer_previous_orders if o['product'] == self.objects['product']['usable-id']
|
||||||
|
]
|
||||||
|
if len(previous_order_of_same_product) >= max_quantity_user_can_order:
|
||||||
|
raise ValidationException(
|
||||||
|
'You cannot buy {} more than {} times'.format(
|
||||||
|
self.objects['product']['name'], int(max_quantity_user_can_order)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OrderListSchema(BaseSchema):
|
||||||
|
def __init__(self, data):
|
||||||
|
super().__init__()
|
||||||
|
self.add_schema(UserCredentialSchema, data)
|
||||||
|
|
||||||
|
|
||||||
|
def make_return_message(err, status_code=200):
|
||||||
|
logging.debug('message: {}'.format(str(err)))
|
||||||
|
return {'message': str(err)}, status_code
|
||||||
|
|
||||||
|
|
||||||
|
def create_schema(specification, data):
|
||||||
|
fields = {}
|
||||||
|
for feature_name, feature_detail in specification['features'].items():
|
||||||
|
if not feature_detail['constant']:
|
||||||
|
fields[feature_name] = Field(
|
||||||
|
feature_name, eval(feature_detail['unit']['type']), **BaseSchema.get(data, feature_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
return type('{}Schema'.format(specification['name']), (BaseSchema,), fields)
|
||||||
7
archive/uncloud_django_based/meow-payv1/stripe_hack.py
Normal file
7
archive/uncloud_django_based/meow-payv1/stripe_hack.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import stripe_utils
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
s = stripe_utils.StripeUtils(os.environ['STRIPE_PRIVATE_KEY'])
|
||||||
|
print(s.get_stripe_customer_from_email('coder.purple+2002@gmail.com'))
|
||||||
491
archive/uncloud_django_based/meow-payv1/stripe_utils.py
Normal file
491
archive/uncloud_django_based/meow-payv1/stripe_utils.py
Normal file
|
|
@ -0,0 +1,491 @@
|
||||||
|
import re
|
||||||
|
import stripe
|
||||||
|
import stripe.error
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from config import etcd_client as client, config as config
|
||||||
|
|
||||||
|
stripe.api_key = config.get('stripe', 'private_key')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_stripe_error(f):
|
||||||
|
def handle_problems(*args, **kwargs):
|
||||||
|
response = {
|
||||||
|
'paid': False,
|
||||||
|
'response_object': None,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
|
||||||
|
common_message = "Currently it's not possible to make payments."
|
||||||
|
try:
|
||||||
|
response_object = f(*args, **kwargs)
|
||||||
|
response = {
|
||||||
|
'response_object': response_object,
|
||||||
|
'error': None
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
except stripe.error.CardError as e:
|
||||||
|
# Since it's a decline, stripe.error.CardError will be caught
|
||||||
|
body = e.json_body
|
||||||
|
err = body['error']
|
||||||
|
response.update({'error': err['message']})
|
||||||
|
logging.error(str(e))
|
||||||
|
return response
|
||||||
|
except stripe.error.RateLimitError:
|
||||||
|
response.update(
|
||||||
|
{'error': "Too many requests made to the API too quickly"})
|
||||||
|
return response
|
||||||
|
except stripe.error.InvalidRequestError as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
response.update({'error': "Invalid parameters"})
|
||||||
|
return response
|
||||||
|
except stripe.error.AuthenticationError as e:
|
||||||
|
# Authentication with Stripe's API failed
|
||||||
|
# (maybe you changed API keys recently)
|
||||||
|
logging.error(str(e))
|
||||||
|
response.update({'error': common_message})
|
||||||
|
return response
|
||||||
|
except stripe.error.APIConnectionError as e:
|
||||||
|
logging.error(str(e))
|
||||||
|
response.update({'error': common_message})
|
||||||
|
return response
|
||||||
|
except stripe.error.StripeError as e:
|
||||||
|
# maybe send email
|
||||||
|
logging.error(str(e))
|
||||||
|
response.update({'error': common_message})
|
||||||
|
return response
|
||||||
|
except Exception as e:
|
||||||
|
# maybe send email
|
||||||
|
logging.error(str(e))
|
||||||
|
response.update({'error': common_message})
|
||||||
|
return response
|
||||||
|
|
||||||
|
return handle_problems
|
||||||
|
|
||||||
|
|
||||||
|
class StripeUtils(object):
|
||||||
|
CURRENCY = 'chf'
|
||||||
|
INTERVAL = 'month'
|
||||||
|
SUCCEEDED_STATUS = 'succeeded'
|
||||||
|
STRIPE_PLAN_ALREADY_EXISTS = 'Plan already exists'
|
||||||
|
STRIPE_NO_SUCH_PLAN = 'No such plan'
|
||||||
|
PLAN_EXISTS_ERROR_MSG = 'Plan {} exists already.\nCreating a local StripePlan now.'
|
||||||
|
PLAN_DOES_NOT_EXIST_ERROR_MSG = 'Plan {} does not exist.'
|
||||||
|
|
||||||
|
def __init__(self, private_key):
|
||||||
|
self.stripe = stripe
|
||||||
|
stripe.api_key = private_key
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def card_exists(self, customer, cc_number, exp_month, exp_year, cvc):
|
||||||
|
token_obj = stripe.Token.create(
|
||||||
|
card={
|
||||||
|
'number': cc_number,
|
||||||
|
'exp_month': exp_month,
|
||||||
|
'exp_year': exp_year,
|
||||||
|
'cvc': cvc,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cards = stripe.Customer.list_sources(
|
||||||
|
customer,
|
||||||
|
limit=20,
|
||||||
|
object='card'
|
||||||
|
)
|
||||||
|
|
||||||
|
for card in cards.data:
|
||||||
|
if (card.fingerprint == token_obj.card.fingerprint and
|
||||||
|
int(card.exp_month) == int(exp_month) and int(card.exp_year) == int(exp_year)):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_stripe_customer_from_email(email):
|
||||||
|
customer = stripe.Customer.list(limit=1, email=email)
|
||||||
|
return customer.data[0] if len(customer.data) == 1 else None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_customer_token(customer, token):
|
||||||
|
customer.source = token
|
||||||
|
customer.save()
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_token_from_card(self, cc_number, cvc, expiry_month, expiry_year):
|
||||||
|
token_obj = stripe.Token.create(
|
||||||
|
card={
|
||||||
|
'number': cc_number,
|
||||||
|
'exp_month': expiry_month,
|
||||||
|
'exp_year': expiry_year,
|
||||||
|
'cvc': cvc,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return token_obj
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def associate_customer_card(self, stripe_customer_id, token,
|
||||||
|
set_as_default=False):
|
||||||
|
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||||
|
card = customer.sources.create(source=token)
|
||||||
|
if set_as_default:
|
||||||
|
customer.default_source = card.id
|
||||||
|
customer.save()
|
||||||
|
return True
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def dissociate_customer_card(self, stripe_customer_id, card_id):
|
||||||
|
customer = stripe.Customer.retrieve(stripe_customer_id)
|
||||||
|
card = customer.sources.retrieve(card_id)
|
||||||
|
card.delete()
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def update_customer_card(self, customer_id, token):
|
||||||
|
customer = stripe.Customer.retrieve(customer_id)
|
||||||
|
current_card_token = customer.default_source
|
||||||
|
customer.sources.retrieve(current_card_token).delete()
|
||||||
|
customer.source = token
|
||||||
|
customer.save()
|
||||||
|
credit_card_raw_data = customer.sources.data.pop()
|
||||||
|
new_card_data = {
|
||||||
|
'last4': credit_card_raw_data.last4,
|
||||||
|
'brand': credit_card_raw_data.brand
|
||||||
|
}
|
||||||
|
return new_card_data
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_card_details(self, customer_id):
|
||||||
|
customer = stripe.Customer.retrieve(customer_id)
|
||||||
|
credit_card_raw_data = customer.sources.data.pop()
|
||||||
|
card_details = {
|
||||||
|
'last4': credit_card_raw_data.last4,
|
||||||
|
'brand': credit_card_raw_data.brand,
|
||||||
|
'exp_month': credit_card_raw_data.exp_month,
|
||||||
|
'exp_year': credit_card_raw_data.exp_year,
|
||||||
|
'fingerprint': credit_card_raw_data.fingerprint,
|
||||||
|
'card_id': credit_card_raw_data.id
|
||||||
|
}
|
||||||
|
return card_details
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_all_invoices(self, customer_id, created_gt):
|
||||||
|
return_list = []
|
||||||
|
has_more_invoices = True
|
||||||
|
starting_after = False
|
||||||
|
while has_more_invoices:
|
||||||
|
if starting_after:
|
||||||
|
invoices = stripe.Invoice.list(
|
||||||
|
limit=10, customer=customer_id, created={'gt': created_gt},
|
||||||
|
starting_after=starting_after
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
invoices = stripe.Invoice.list(
|
||||||
|
limit=10, customer=customer_id, created={'gt': created_gt}
|
||||||
|
)
|
||||||
|
has_more_invoices = invoices.has_more
|
||||||
|
for invoice in invoices.data:
|
||||||
|
sub_ids = []
|
||||||
|
for line in invoice.lines.data:
|
||||||
|
if line.type == 'subscription':
|
||||||
|
sub_ids.append(line.id)
|
||||||
|
elif line.type == 'invoiceitem':
|
||||||
|
sub_ids.append(line.subscription)
|
||||||
|
else:
|
||||||
|
sub_ids.append('')
|
||||||
|
invoice_details = {
|
||||||
|
'created': invoice.created,
|
||||||
|
'receipt_number': invoice.receipt_number,
|
||||||
|
'invoice_number': invoice.number,
|
||||||
|
'paid_at': invoice.status_transitions.paid_at if invoice.paid else 0,
|
||||||
|
'period_start': invoice.period_start,
|
||||||
|
'period_end': invoice.period_end,
|
||||||
|
'billing_reason': invoice.billing_reason,
|
||||||
|
'discount': invoice.discount.coupon.amount_off if invoice.discount else 0,
|
||||||
|
'total': invoice.total,
|
||||||
|
# to see how many line items we have in this invoice and
|
||||||
|
# then later check if we have more than 1
|
||||||
|
'lines_data_count': len(invoice.lines.data) if invoice.lines.data is not None else 0,
|
||||||
|
'invoice_id': invoice.id,
|
||||||
|
'lines_meta_data_csv': ','.join(
|
||||||
|
[line.metadata.VM_ID if hasattr(line.metadata, 'VM_ID') else '' for line in invoice.lines.data]
|
||||||
|
),
|
||||||
|
'subscription_ids_csv': ','.join(sub_ids),
|
||||||
|
'line_items': invoice.lines.data
|
||||||
|
}
|
||||||
|
starting_after = invoice.id
|
||||||
|
return_list.append(invoice_details)
|
||||||
|
return return_list
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_cards_details_from_token(self, token):
|
||||||
|
stripe_token = stripe.Token.retrieve(token)
|
||||||
|
card_details = {
|
||||||
|
'last4': stripe_token.card.last4,
|
||||||
|
'brand': stripe_token.card.brand,
|
||||||
|
'exp_month': stripe_token.card.exp_month,
|
||||||
|
'exp_year': stripe_token.card.exp_year,
|
||||||
|
'fingerprint': stripe_token.card.fingerprint,
|
||||||
|
'card_id': stripe_token.card.id
|
||||||
|
}
|
||||||
|
return card_details
|
||||||
|
|
||||||
|
def check_customer(self, stripe_cus_api_id, user, token):
|
||||||
|
try:
|
||||||
|
customer = stripe.Customer.retrieve(stripe_cus_api_id)
|
||||||
|
except stripe.error.InvalidRequestError:
|
||||||
|
customer = self.create_customer(token, user.email, user.name)
|
||||||
|
user.stripecustomer.stripe_id = customer.get(
|
||||||
|
'response_object').get('id')
|
||||||
|
user.stripecustomer.save()
|
||||||
|
if type(customer) is dict:
|
||||||
|
customer = customer['response_object']
|
||||||
|
return customer
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_customer(self, stripe_api_cus_id):
|
||||||
|
customer = stripe.Customer.retrieve(stripe_api_cus_id)
|
||||||
|
# data = customer.get('response_object')
|
||||||
|
return customer
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def create_customer(self, token, email, name=None, address=None):
|
||||||
|
if name is None or name.strip() == "":
|
||||||
|
name = email
|
||||||
|
customer = self.stripe.Customer.create(
|
||||||
|
source=token,
|
||||||
|
description=name,
|
||||||
|
email=email,
|
||||||
|
address=address
|
||||||
|
)
|
||||||
|
return customer
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def make_charge(self, amount=None, customer=None):
|
||||||
|
_amount = float(amount)
|
||||||
|
amount = int(_amount * 100) # stripe amount unit, in cents
|
||||||
|
charge = self.stripe.Charge.create(
|
||||||
|
amount=amount, # in cents
|
||||||
|
currency=self.CURRENCY,
|
||||||
|
customer=customer
|
||||||
|
)
|
||||||
|
return charge
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_all_stripe_plans():
|
||||||
|
all_stripe_plans = client.get("/v1/stripe_plans")
|
||||||
|
all_stripe_plans_set = set()
|
||||||
|
if all_stripe_plans:
|
||||||
|
all_stripe_plans_obj = all_stripe_plans.value
|
||||||
|
if all_stripe_plans_obj and len(all_stripe_plans_obj['plans']) > 0:
|
||||||
|
all_stripe_plans_set = set(all_stripe_plans_obj["plans"])
|
||||||
|
return all_stripe_plans_set
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _save_all_stripe_plans(stripe_plans):
|
||||||
|
client.put("/v1/stripe_plans", {"plans": list(stripe_plans)})
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def get_or_create_stripe_plan(self, product_name, amount, stripe_plan_id,
|
||||||
|
interval=INTERVAL):
|
||||||
|
"""
|
||||||
|
This function checks if a StripePlan with the given
|
||||||
|
stripe_plan_id already exists. If it exists then the function
|
||||||
|
returns this object otherwise it creates a new StripePlan and
|
||||||
|
returns the new object.
|
||||||
|
|
||||||
|
:param amount: The amount in CHF cents
|
||||||
|
:param product_name: The name of the Stripe plan (product) to be created.
|
||||||
|
:param stripe_plan_id: The id of the Stripe plan to be
|
||||||
|
created. Use get_stripe_plan_id_string function to
|
||||||
|
obtain the name of the plan to be created
|
||||||
|
:param interval: The interval for subscription {month, year}. Defaults
|
||||||
|
to month if not provided
|
||||||
|
:return: The StripePlan object if it exists else creates a
|
||||||
|
Plan object in Stripe and a local StripePlan and
|
||||||
|
returns it. Returns None in case of Stripe error
|
||||||
|
"""
|
||||||
|
_amount = float(amount)
|
||||||
|
amount = int(_amount * 100) # stripe amount unit, in cents
|
||||||
|
all_stripe_plans = self._get_all_stripe_plans()
|
||||||
|
if stripe_plan_id in all_stripe_plans:
|
||||||
|
logging.debug("{} plan exists in db.".format(stripe_plan_id))
|
||||||
|
else:
|
||||||
|
logging.debug(("{} plan DOES NOT exist in db. "
|
||||||
|
"Creating").format(stripe_plan_id))
|
||||||
|
try:
|
||||||
|
plan_obj = self.stripe.Plan.retrieve(id=stripe_plan_id)
|
||||||
|
logging.debug("{} plan exists in Stripe".format(stripe_plan_id))
|
||||||
|
all_stripe_plans.add(stripe_plan_id)
|
||||||
|
except stripe.error.InvalidRequestError as e:
|
||||||
|
if "No such plan" in str(e):
|
||||||
|
logging.debug("Plan {} does not exist in Stripe, Creating")
|
||||||
|
plan_obj = self.stripe.Plan.create(
|
||||||
|
amount=amount,
|
||||||
|
product={'name': product_name},
|
||||||
|
interval=interval,
|
||||||
|
currency=self.CURRENCY,
|
||||||
|
id=stripe_plan_id)
|
||||||
|
logging.debug(self.PLAN_EXISTS_ERROR_MSG.format(stripe_plan_id))
|
||||||
|
all_stripe_plans.add(stripe_plan_id)
|
||||||
|
self._save_all_stripe_plans(all_stripe_plans)
|
||||||
|
return stripe_plan_id
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def delete_stripe_plan(self, stripe_plan_id):
|
||||||
|
"""
|
||||||
|
Deletes the Plan in Stripe and also deletes the local db copy
|
||||||
|
of the plan if it exists
|
||||||
|
|
||||||
|
:param stripe_plan_id: The stripe plan id that needs to be
|
||||||
|
deleted
|
||||||
|
:return: True if the plan was deleted successfully from
|
||||||
|
Stripe, False otherwise.
|
||||||
|
"""
|
||||||
|
return_value = False
|
||||||
|
try:
|
||||||
|
plan = self.stripe.Plan.retrieve(stripe_plan_id)
|
||||||
|
plan.delete()
|
||||||
|
return_value = True
|
||||||
|
all_stripe_plans = self._get_all_stripe_plans()
|
||||||
|
all_stripe_plans.remove(stripe_plan_id)
|
||||||
|
self._save_all_stripe_plans(all_stripe_plans)
|
||||||
|
except stripe.error.InvalidRequestError as e:
|
||||||
|
if self.STRIPE_NO_SUCH_PLAN in str(e):
|
||||||
|
logging.debug(
|
||||||
|
self.PLAN_DOES_NOT_EXIST_ERROR_MSG.format(stripe_plan_id))
|
||||||
|
return return_value
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def subscribe_customer_to_plan(self, customer, plans, trial_end=None):
|
||||||
|
"""
|
||||||
|
Subscribes the given customer to the list of given plans
|
||||||
|
|
||||||
|
:param customer: The stripe customer identifier
|
||||||
|
:param plans: A list of stripe plans.
|
||||||
|
:param trial_end: An integer representing when the Stripe subscription
|
||||||
|
is supposed to end
|
||||||
|
Ref: https://stripe.com/docs/api/python#create_subscription-items
|
||||||
|
e.g.
|
||||||
|
plans = [
|
||||||
|
{
|
||||||
|
"plan": "dcl-v1-cpu-2-ram-5gb-ssd-10gb",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
:return: The subscription StripeObject
|
||||||
|
"""
|
||||||
|
|
||||||
|
subscription_result = self.stripe.Subscription.create(
|
||||||
|
customer=customer, items=plans, trial_end=trial_end
|
||||||
|
)
|
||||||
|
return subscription_result
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def set_subscription_metadata(self, subscription_id, metadata):
|
||||||
|
subscription = stripe.Subscription.retrieve(subscription_id)
|
||||||
|
subscription.metadata = metadata
|
||||||
|
subscription.save()
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def unsubscribe_customer(self, subscription_id):
|
||||||
|
"""
|
||||||
|
Cancels a given subscription
|
||||||
|
|
||||||
|
:param subscription_id: The Stripe subscription id string
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
sub = stripe.Subscription.retrieve(subscription_id)
|
||||||
|
return sub.delete()
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def make_payment(self, customer, amount, token):
|
||||||
|
charge = self.stripe.Charge.create(
|
||||||
|
amount=amount, # in cents
|
||||||
|
currency=self.CURRENCY,
|
||||||
|
customer=customer
|
||||||
|
)
|
||||||
|
return charge
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_stripe_plan_id(cpu, ram, ssd, version, app='dcl', hdd=None,
|
||||||
|
price=None):
|
||||||
|
"""
|
||||||
|
Returns the Stripe plan id string of the form
|
||||||
|
`dcl-v1-cpu-2-ram-5gb-ssd-10gb` based on the input parameters
|
||||||
|
|
||||||
|
:param cpu: The number of cores
|
||||||
|
:param ram: The size of the RAM in GB
|
||||||
|
:param ssd: The size of ssd storage in GB
|
||||||
|
:param hdd: The size of hdd storage in GB
|
||||||
|
:param version: The version of the Stripe plans
|
||||||
|
:param app: The application to which the stripe plan belongs
|
||||||
|
to. By default it is 'dcl'
|
||||||
|
:param price: The price for this plan
|
||||||
|
:return: A string of the form `dcl-v1-cpu-2-ram-5gb-ssd-10gb`
|
||||||
|
"""
|
||||||
|
dcl_plan_string = 'cpu-{cpu}-ram-{ram}gb-ssd-{ssd}gb'.format(cpu=cpu,
|
||||||
|
ram=ram,
|
||||||
|
ssd=ssd)
|
||||||
|
if hdd is not None:
|
||||||
|
dcl_plan_string = '{dcl_plan_string}-hdd-{hdd}gb'.format(
|
||||||
|
dcl_plan_string=dcl_plan_string, hdd=hdd)
|
||||||
|
stripe_plan_id_string = '{app}-v{version}-{plan}'.format(
|
||||||
|
app=app,
|
||||||
|
version=version,
|
||||||
|
plan=dcl_plan_string
|
||||||
|
)
|
||||||
|
if price is not None:
|
||||||
|
stripe_plan_id_string_with_price = '{}-{}chf'.format(
|
||||||
|
stripe_plan_id_string,
|
||||||
|
round(price, 2)
|
||||||
|
)
|
||||||
|
return stripe_plan_id_string_with_price
|
||||||
|
else:
|
||||||
|
return stripe_plan_id_string
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_vm_config_from_stripe_id(stripe_id):
|
||||||
|
"""
|
||||||
|
Given a string like "dcl-v1-cpu-2-ram-5gb-ssd-10gb" return different
|
||||||
|
configuration params as a dict
|
||||||
|
|
||||||
|
:param stripe_id|str
|
||||||
|
:return: dict
|
||||||
|
"""
|
||||||
|
pattern = re.compile(r'^dcl-v(\d+)-cpu-(\d+)-ram-(\d+\.?\d*)gb-ssd-(\d+)gb-?(\d*\.?\d*)(chf)?$')
|
||||||
|
match_res = pattern.match(stripe_id)
|
||||||
|
if match_res is not None:
|
||||||
|
price = None
|
||||||
|
try:
|
||||||
|
price = match_res.group(5)
|
||||||
|
except IndexError:
|
||||||
|
logging.debug("Did not find price in {}".format(stripe_id))
|
||||||
|
return {
|
||||||
|
'version': match_res.group(1),
|
||||||
|
'cores': match_res.group(2),
|
||||||
|
'ram': match_res.group(3),
|
||||||
|
'ssd': match_res.group(4),
|
||||||
|
'price': price
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_stripe_plan_name(cpu, memory, disk_size, price):
|
||||||
|
"""
|
||||||
|
Returns the Stripe plan name
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return "{cpu} Cores, {memory} GB RAM, {disk_size} GB SSD, " \
|
||||||
|
"{price} CHF".format(
|
||||||
|
cpu=cpu,
|
||||||
|
memory=memory,
|
||||||
|
disk_size=disk_size,
|
||||||
|
price=round(price, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
@handle_stripe_error
|
||||||
|
def set_subscription_meta_data(self, subscription_id, meta_data):
|
||||||
|
"""
|
||||||
|
Adds VM metadata to a subscription
|
||||||
|
:param subscription_id: Stripe identifier for the subscription
|
||||||
|
:param meta_data: A dict of meta data to be added
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
subscription = stripe.Subscription.retrieve(subscription_id)
|
||||||
|
subscription.metadata = meta_data
|
||||||
|
subscription.save()
|
||||||
338
archive/uncloud_django_based/meow-payv1/ucloud_pay.py
Normal file
338
archive/uncloud_django_based/meow-payv1/ucloud_pay.py
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
from flask import Flask, request
|
||||||
|
from flask_restful import Resource, Api
|
||||||
|
from werkzeug.exceptions import HTTPException
|
||||||
|
from config import etcd_client as client, config as config
|
||||||
|
from stripe_utils import StripeUtils
|
||||||
|
from schemas import (
|
||||||
|
make_return_message, ValidationException, UserRegisterPaymentSchema,
|
||||||
|
AddProductSchema, ProductOrderSchema, OrderListSchema, create_schema
|
||||||
|
)
|
||||||
|
from helper import get_plan_id_from_product, calculate_charges
|
||||||
|
|
||||||
|
|
||||||
|
class ListProducts(Resource):
|
||||||
|
@staticmethod
|
||||||
|
def get():
|
||||||
|
products = client.get_prefix('/v1/products/')
|
||||||
|
products = [
|
||||||
|
product
|
||||||
|
for product in [p.value for p in products]
|
||||||
|
if product['active']
|
||||||
|
]
|
||||||
|
prod_dict = {}
|
||||||
|
for p in products:
|
||||||
|
prod_dict[p['usable-id']] = {
|
||||||
|
'name': p['name'],
|
||||||
|
'description': p['description'],
|
||||||
|
}
|
||||||
|
logger.debug('Products = {}'.format(prod_dict))
|
||||||
|
return prod_dict, 200
|
||||||
|
|
||||||
|
class AddProduct(Resource):
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug('Got data: {}'.format(str(data)))
|
||||||
|
validator = AddProductSchema(data)
|
||||||
|
validator.is_valid()
|
||||||
|
except ValidationException as err:
|
||||||
|
return make_return_message(err, 400)
|
||||||
|
else:
|
||||||
|
cleaned_values = validator.get_cleaned_values()
|
||||||
|
previous_product = cleaned_values.get('product', None)
|
||||||
|
if previous_product:
|
||||||
|
if not cleaned_values['update']:
|
||||||
|
return make_return_message('Product already exists. Pass --update to update the product.')
|
||||||
|
else:
|
||||||
|
product_uuid = previous_product.pop('uuid')
|
||||||
|
else:
|
||||||
|
product_uuid = uuid4().hex
|
||||||
|
|
||||||
|
product_value = cleaned_values['specs']
|
||||||
|
|
||||||
|
product_key = '/v1/products/{}'.format(product_uuid)
|
||||||
|
product_value['uuid'] = product_uuid
|
||||||
|
|
||||||
|
logger.debug('Adding product data: {}'.format(str(product_value)))
|
||||||
|
client.put(product_key, product_value)
|
||||||
|
if not previous_product:
|
||||||
|
return make_return_message('Product created.')
|
||||||
|
else:
|
||||||
|
return make_return_message('Product updated.')
|
||||||
|
|
||||||
|
################################################################################
|
||||||
|
# Nico-ok-marker
|
||||||
|
|
||||||
|
|
||||||
|
class UserRegisterPayment(Resource):
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.debug('Got data: {}'.format(str(data)))
|
||||||
|
validator = UserRegisterPaymentSchema(data)
|
||||||
|
validator.is_valid()
|
||||||
|
except ValidationException as err:
|
||||||
|
return make_return_message(err, 400)
|
||||||
|
else:
|
||||||
|
cleaned_values = validator.get_cleaned_values()
|
||||||
|
last4 = data['card_number'].strip()[-4:]
|
||||||
|
|
||||||
|
stripe_utils = StripeUtils()
|
||||||
|
|
||||||
|
# Does customer already exist ?
|
||||||
|
stripe_customer = stripe_utils.get_stripe_customer_from_email(cleaned_values['user']['mail'])
|
||||||
|
|
||||||
|
# Does customer already exist ?
|
||||||
|
if stripe_customer is not None:
|
||||||
|
logger.debug('Customer {}-{} exists already'.format(
|
||||||
|
cleaned_values['username'], cleaned_values['user']['mail'])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if the card already exists
|
||||||
|
ce_response = stripe_utils.card_exists(
|
||||||
|
stripe_customer.id, cc_number=data['card_number'],
|
||||||
|
exp_month=int(data['expiry_month']),
|
||||||
|
exp_year=int(data['expiry_year']),
|
||||||
|
cvc=data['cvc'])
|
||||||
|
|
||||||
|
if ce_response['response_object']:
|
||||||
|
message = 'The given card ending in {} exists already.'.format(last4)
|
||||||
|
return make_return_message(message, 400)
|
||||||
|
|
||||||
|
elif ce_response['response_object'] is False:
|
||||||
|
# Associate card with user
|
||||||
|
logger.debug('Adding card ending in {}'.format(last4))
|
||||||
|
token_response = stripe_utils.get_token_from_card(
|
||||||
|
data['card_number'], data['cvc'], data['expiry_month'],
|
||||||
|
data['expiry_year']
|
||||||
|
)
|
||||||
|
if token_response['response_object']:
|
||||||
|
logger.debug('Token {}'.format(token_response['response_object'].id))
|
||||||
|
resp = stripe_utils.associate_customer_card(
|
||||||
|
stripe_customer.id, token_response['response_object'].id
|
||||||
|
)
|
||||||
|
if resp['response_object']:
|
||||||
|
return make_return_message(
|
||||||
|
'Card ending in {} registered as your payment source'.format(last4)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return make_return_message('Error with payment gateway. Contact support', 400)
|
||||||
|
else:
|
||||||
|
return make_return_message('Error: {}'.format(ce_response['error']), 400)
|
||||||
|
else:
|
||||||
|
# Stripe customer does not exist, create a new one
|
||||||
|
logger.debug(
|
||||||
|
'Customer {} does not exist, creating new'.format(cleaned_values['user']['mail'])
|
||||||
|
)
|
||||||
|
token_response = stripe_utils.get_token_from_card(
|
||||||
|
cleaned_values['card_number'], cleaned_values['cvc'],
|
||||||
|
cleaned_values['expiry_month'], cleaned_values['expiry_year']
|
||||||
|
)
|
||||||
|
if token_response['response_object']:
|
||||||
|
logger.debug('Token {}'.format(token_response['response_object'].id))
|
||||||
|
|
||||||
|
# Create stripe customer
|
||||||
|
stripe_customer_resp = stripe_utils.create_customer(
|
||||||
|
name=cleaned_values['card_holder_name'],
|
||||||
|
token=token_response['response_object'].id,
|
||||||
|
email=cleaned_values['user']['mail'],
|
||||||
|
address=cleaned_values['address']
|
||||||
|
)
|
||||||
|
stripe_customer = stripe_customer_resp['response_object']
|
||||||
|
|
||||||
|
if stripe_customer:
|
||||||
|
logger.debug('Created stripe customer {}'.format(stripe_customer.id))
|
||||||
|
return make_return_message(
|
||||||
|
'Card ending in {} registered as your payment source'.format(last4)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return make_return_message('Error with card. Contact support', 400)
|
||||||
|
else:
|
||||||
|
return make_return_message('Error with payment gateway. Contact support', 400)
|
||||||
|
|
||||||
|
|
||||||
|
class ProductOrder(Resource):
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
validator = ProductOrderSchema(data)
|
||||||
|
validator.is_valid()
|
||||||
|
except ValidationException as err:
|
||||||
|
return make_return_message(err, 400)
|
||||||
|
else:
|
||||||
|
cleaned_values = validator.get_cleaned_values()
|
||||||
|
stripe_utils = StripeUtils()
|
||||||
|
|
||||||
|
product = cleaned_values['product']
|
||||||
|
|
||||||
|
# Check the user has a payment source added
|
||||||
|
stripe_customer = stripe_utils.get_stripe_customer_from_email(cleaned_values['user']['mail'])
|
||||||
|
|
||||||
|
if not stripe_customer or len(stripe_customer.sources) == 0:
|
||||||
|
return make_return_message('Please register your payment method first.', 400)
|
||||||
|
|
||||||
|
try:
|
||||||
|
product_schema = create_schema(product, data)
|
||||||
|
product_schema = product_schema()
|
||||||
|
product_schema.is_valid()
|
||||||
|
except ValidationException as err:
|
||||||
|
return make_return_message(err, 400)
|
||||||
|
else:
|
||||||
|
transformed_data = product_schema.get_cleaned_values()
|
||||||
|
logger.debug('Tranformed data: {}'.format(transformed_data))
|
||||||
|
one_time_charge, recurring_charge = calculate_charges(product, transformed_data)
|
||||||
|
recurring_charge = int(recurring_charge)
|
||||||
|
|
||||||
|
if not cleaned_values['pay']:
|
||||||
|
return make_return_message(
|
||||||
|
'You would be charged {} CHF one time and {} CHF every {}. '
|
||||||
|
'Add --pay to command to order.'.format(
|
||||||
|
one_time_charge, recurring_charge, product['recurring_period']
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
with client.client.lock('product-order') as _:
|
||||||
|
# Initiate a one-time/subscription based on product type
|
||||||
|
if recurring_charge > 0:
|
||||||
|
logger.debug('Product {} is recurring payment'.format(product['name']))
|
||||||
|
plan_id = get_plan_id_from_product(product)
|
||||||
|
res = stripe_utils.get_or_create_stripe_plan(
|
||||||
|
product_name=product['name'],
|
||||||
|
stripe_plan_id=plan_id, amount=recurring_charge,
|
||||||
|
interval=product['recurring_period'],
|
||||||
|
)
|
||||||
|
if res['response_object']:
|
||||||
|
logger.debug('Obtained plan {}'.format(plan_id))
|
||||||
|
subscription_res = stripe_utils.subscribe_customer_to_plan(
|
||||||
|
stripe_customer.id,
|
||||||
|
[{'plan': plan_id}]
|
||||||
|
)
|
||||||
|
subscription_obj = subscription_res['response_object']
|
||||||
|
if subscription_obj is None or subscription_obj.status != 'active':
|
||||||
|
return make_return_message(
|
||||||
|
'Error subscribing to plan. Detail: {}'.format(subscription_res['error']), 400
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
order_obj = {
|
||||||
|
'order-id': uuid4().hex,
|
||||||
|
'ordered-at': datetime.now().isoformat(),
|
||||||
|
'product': product['usable-id'],
|
||||||
|
'one-time-price': one_time_charge,
|
||||||
|
'recurring-price': recurring_charge,
|
||||||
|
'recurring-period': product['recurring_period']
|
||||||
|
}
|
||||||
|
client.put(
|
||||||
|
'/v1/user/{}/orders/{}'.format(
|
||||||
|
cleaned_values['username'], order_obj['order-id']
|
||||||
|
), order_obj
|
||||||
|
)
|
||||||
|
product['quantity'] -= 1
|
||||||
|
client.put('/v1/products/{}'.format(product['uuid']), product)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'message': 'Order Successful.',
|
||||||
|
**order_obj
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.error('Could not create plan {}'.format(plan_id))
|
||||||
|
return make_return_message('Something wrong happened. Contact administrator', 400)
|
||||||
|
|
||||||
|
elif recurring_charge == 0 and one_time_charge > 0:
|
||||||
|
logger.debug('Product {} is one-time payment'.format(product['name']))
|
||||||
|
charge_response = stripe_utils.make_charge(
|
||||||
|
amount=one_time_charge,
|
||||||
|
customer=stripe_customer.id
|
||||||
|
)
|
||||||
|
stripe_onetime_charge = charge_response.get('response_object')
|
||||||
|
|
||||||
|
# Check if the payment was approved
|
||||||
|
if not stripe_onetime_charge:
|
||||||
|
msg = charge_response.get('error')
|
||||||
|
return make_return_message('Error subscribing to plan. Details: {}'.format(msg), 400)
|
||||||
|
|
||||||
|
order_obj = {
|
||||||
|
'order-id': uuid4().hex,
|
||||||
|
'ordered-at': datetime.now().isoformat(),
|
||||||
|
'product': product['usable-id'],
|
||||||
|
'one-time-price': one_time_charge,
|
||||||
|
}
|
||||||
|
client.put(
|
||||||
|
'/v1/user/{}/orders/{}'.format(cleaned_values['username'], order_obj['order-id']),
|
||||||
|
order_obj
|
||||||
|
)
|
||||||
|
product['quantity'] -= 1
|
||||||
|
client.put('/v1/products/{}'.format(product['uuid']), product)
|
||||||
|
|
||||||
|
return {'message': 'Order successful', **order_obj}, 200
|
||||||
|
|
||||||
|
|
||||||
|
class OrderList(Resource):
|
||||||
|
@staticmethod
|
||||||
|
def post():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
validator = OrderListSchema(data)
|
||||||
|
validator.is_valid()
|
||||||
|
except ValidationException as err:
|
||||||
|
return make_return_message(err, 400)
|
||||||
|
else:
|
||||||
|
cleaned_values = validator.get_cleaned_values()
|
||||||
|
orders = client.get_prefix('/v1/user/{}/orders'.format(cleaned_values['username']))
|
||||||
|
orders_dict = {
|
||||||
|
order.value['order-id']: {
|
||||||
|
**order.value
|
||||||
|
}
|
||||||
|
for order in orders
|
||||||
|
}
|
||||||
|
logger.debug('Orders = {}'.format(orders_dict))
|
||||||
|
return {'orders': orders_dict}, 200
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
logger = logging.getLogger()
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
log_formater = logging.Formatter('[%(filename)s:%(lineno)d] %(message)s')
|
||||||
|
|
||||||
|
stream_logger = logging.StreamHandler()
|
||||||
|
stream_logger.setFormatter(log_formater)
|
||||||
|
|
||||||
|
# file_logger = logging.FileHandler('log.txt')
|
||||||
|
# file_logger.setLevel(logging.DEBUG)
|
||||||
|
# file_logger.setFormatter(log_formater)
|
||||||
|
|
||||||
|
logger.addHandler(stream_logger)
|
||||||
|
# logger.addHandler(file_logger)
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
api = Api(app)
|
||||||
|
api.add_resource(ListProducts, '/product/list')
|
||||||
|
api.add_resource(AddProduct, '/product/add')
|
||||||
|
api.add_resource(ProductOrder, '/product/order')
|
||||||
|
api.add_resource(UserRegisterPayment, '/user/register_payment')
|
||||||
|
api.add_resource(OrderList, '/order/list')
|
||||||
|
|
||||||
|
app.run(host='::', port=config.get('app', 'port', fallback=5000), debug=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.errorhandler(Exception)
|
||||||
|
def handle_exception(e):
|
||||||
|
app.logger.error(e)
|
||||||
|
# pass through HTTP errors
|
||||||
|
if isinstance(e, HTTPException):
|
||||||
|
return e
|
||||||
|
|
||||||
|
# now you're handling non-HTTP exceptions only
|
||||||
|
return {'message': 'Server Error'}, 500
|
||||||
11
archive/uncloud_django_based/notes-abk.md
Normal file
11
archive/uncloud_django_based/notes-abk.md
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
## TODO 2020-02-22
|
||||||
|
|
||||||
|
* ~~move the current rest api to /opennebula~~
|
||||||
|
* ~~make the /opennebula api only accessible by an admin account~~
|
||||||
|
* ~~create a new filtered api on /vm/list that~~
|
||||||
|
* ~~a) requires authentication~~
|
||||||
|
* ~~b) only shows the VMs of the current user~~
|
||||||
|
* ~~the new api should not contain all details, but: cpus (as read by the vcpu field), ram, ips, disks~~
|
||||||
|
* ~~also make a (random) uuid the primary key for VMs - everything in this uncloud hack will use uuids as the id~~
|
||||||
|
* ~~still expose the opennebula id as opennebula_id~~
|
||||||
|
* ~~note put all secrets/configs into uncloud.secrets - I added a sample file into the repo~~
|
||||||
102
archive/uncloud_django_based/notes-nico.org
Normal file
102
archive/uncloud_django_based/notes-nico.org
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
* snapshot feature
|
||||||
|
** product: vm-snapshot
|
||||||
|
** flow
|
||||||
|
*** list all my VMs
|
||||||
|
**** get the uuid of the VM I want to take a snapshot of
|
||||||
|
*** request a snapshot
|
||||||
|
```
|
||||||
|
vmuuid=$(http nicocustomer
|
||||||
|
http -a nicocustomer:xxx http://uncloud.ch/vm/create_snapshot uuid=
|
||||||
|
password=...
|
||||||
|
```
|
||||||
|
** backend realisation
|
||||||
|
*** list snapshots
|
||||||
|
- have them in the DB
|
||||||
|
- create an entry on create
|
||||||
|
*** creating snapshots
|
||||||
|
- vm sync / fsync?
|
||||||
|
- rbd snapshot
|
||||||
|
- host/cluster mapping?
|
||||||
|
- need image(s)
|
||||||
|
|
||||||
|
* steps
|
||||||
|
** DONE authenticate via ldap
|
||||||
|
CLOSED: [2020-02-20 Thu 19:05]
|
||||||
|
** DONE Make classes / views require authentication
|
||||||
|
CLOSED: [2020-02-20 Thu 19:05]
|
||||||
|
** TODO register credit card
|
||||||
|
*** TODO find out what saving with us
|
||||||
|
*** Info
|
||||||
|
**** should not be fully saved in the DB
|
||||||
|
**** model needs to be a bit different
|
||||||
|
* Decide where to save sensitive data
|
||||||
|
** stripe access key, etc.
|
||||||
|
* python requirements (nicohack202002)
|
||||||
|
django djangorestframework django-auth-ldap stripe
|
||||||
|
* os package requirements (alpine)
|
||||||
|
openldap-dev
|
||||||
|
* VPN case
|
||||||
|
** put on /orders with uuid
|
||||||
|
** register cc
|
||||||
|
* CC
|
||||||
|
** TODO check whether we can register or not at stripe
|
||||||
|
* membership
|
||||||
|
** required for "smaller" / "shorter" products
|
||||||
|
|
||||||
|
* TODO Membership missing
|
||||||
|
* Flows to be implemented - see https://redmine.ungleich.ch/issues/7609
|
||||||
|
** Membership
|
||||||
|
*** 5 CHF
|
||||||
|
** Django Hosting
|
||||||
|
*** One time payment 35 CHF
|
||||||
|
*** Monthly payment depends on VM size
|
||||||
|
*** Parameters: same as IPv6 only VM
|
||||||
|
** IPv6 VPN
|
||||||
|
*** Parameters: none
|
||||||
|
*** Is for free if the customer has an active VM
|
||||||
|
** IPv6 only VM
|
||||||
|
*** Parameters: cores, ram, os_disk_size, OS
|
||||||
|
* Django rest framework
|
||||||
|
** viewset: .list and .create
|
||||||
|
** view: .get .post
|
||||||
|
* TODO register CC
|
||||||
|
* DONE list products
|
||||||
|
CLOSED: [2020-02-24 Mon 20:15]
|
||||||
|
* An ungleich account - can be registered for free on
|
||||||
|
https://account.ungleich.ch
|
||||||
|
* httpie installed (provides the http command)
|
||||||
|
|
||||||
|
## Get a membership
|
||||||
|
|
||||||
|
|
||||||
|
## Registering a payment method
|
||||||
|
|
||||||
|
To be able to pay for the membership, you will need to register a
|
||||||
|
credit card or apply for payment on bill (TO BE IMPLEMENTED).
|
||||||
|
|
||||||
|
### Register credit card
|
||||||
|
|
||||||
|
```
|
||||||
|
http POST https://api.ungleich.ch/membership \
|
||||||
|
username=nico password=yourpassword \
|
||||||
|
cc_number=.. \
|
||||||
|
cc_
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Request payment via bill
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Create the membership
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
http POST https://api.ungleich.ch/membership username=nico password=yourpassword
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## List available products
|
||||||
6
archive/uncloud_django_based/plan.org
Normal file
6
archive/uncloud_django_based/plan.org
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
* TODO register CC
|
||||||
|
* TODO list products
|
||||||
|
* ahmed
|
||||||
|
** schemas
|
||||||
|
*** field: is_valid? - used by schemas
|
||||||
|
*** definition of a "schema"
|
||||||
4
archive/uncloud_django_based/uncloud/.gitignore
vendored
Normal file
4
archive/uncloud_django_based/uncloud/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
db.sqlite3
|
||||||
|
uncloud/secrets.py
|
||||||
|
debug.log
|
||||||
|
uncloud/local_settings.py
|
||||||
|
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
2
archive/uncloud_etcd_based/uncloud/__init__.py
Normal file
2
archive/uncloud_etcd_based/uncloud/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
class UncloudException(Exception):
|
||||||
|
pass
|
||||||
23
archive/uncloud_etcd_based/uncloud/client/main.py
Normal file
23
archive/uncloud_etcd_based/uncloud/client/main.py
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import argparse
|
||||||
|
import etcd3
|
||||||
|
from uncloud.common.etcd_wrapper import Etcd3Wrapper
|
||||||
|
|
||||||
|
arg_parser = argparse.ArgumentParser('client', add_help=False)
|
||||||
|
arg_parser.add_argument('--dump-etcd-contents-prefix', help="Dump contents below the given prefix")
|
||||||
|
|
||||||
|
def dump_etcd_contents(prefix):
|
||||||
|
etcd = Etcd3Wrapper()
|
||||||
|
for k,v in etcd.get_prefix_raw(prefix):
|
||||||
|
k = k.decode('utf-8')
|
||||||
|
v = v.decode('utf-8')
|
||||||
|
print("{} = {}".format(k,v))
|
||||||
|
# print("{} = {}".format(k,v))
|
||||||
|
|
||||||
|
# for k,v in etcd.get_prefix(prefix):
|
||||||
|
#
|
||||||
|
print("done")
|
||||||
|
|
||||||
|
|
||||||
|
def main(arguments):
|
||||||
|
if 'dump_etcd_contents_prefix' in arguments:
|
||||||
|
dump_etcd_contents(prefix=arguments['dump_etcd_contents_prefix'])
|
||||||
|
|
@ -23,12 +23,21 @@
|
||||||
import etcd3
|
import etcd3
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from uncloud import UncloudException
|
from uncloud import UncloudException
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def db_logentry(message):
|
||||||
|
timestamp = datetime.datetime.now()
|
||||||
|
return {
|
||||||
|
"timestamp": str(timestamp),
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def readable_errors(func):
|
def readable_errors(func):
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
|
|
@ -81,9 +90,13 @@ class DB(object):
|
||||||
|
|
||||||
@readable_errors
|
@readable_errors
|
||||||
def get_prefix(self, key, as_json=False, **kwargs):
|
def get_prefix(self, key, as_json=False, **kwargs):
|
||||||
key_range = self._db_clients[0].get_prefix(self.realkey(key), **kwargs)
|
for value, meta in self._db_clients[0].get_prefix(self.realkey(key), **kwargs):
|
||||||
|
k = meta.key.decode("utf-8")
|
||||||
|
value = value.decode("utf-8")
|
||||||
|
if as_json:
|
||||||
|
value = json.loads(value)
|
||||||
|
|
||||||
return key_range
|
yield (k, value)
|
||||||
|
|
||||||
|
|
||||||
@readable_errors
|
@readable_errors
|
||||||
|
|
@ -95,6 +108,19 @@ class DB(object):
|
||||||
# FIXME: iterate over clients in case of failure ?
|
# FIXME: iterate over clients in case of failure ?
|
||||||
return self._db_clients[0].put(self.realkey(key), value, **kwargs)
|
return self._db_clients[0].put(self.realkey(key), value, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
@readable_errors
|
||||||
|
def list_and_filter(self, key, filter_key=None, filter_regexp=None):
|
||||||
|
for k,v in self.get_prefix(key, as_json=True):
|
||||||
|
|
||||||
|
if filter_key and filter_regexp:
|
||||||
|
if filter_key in v:
|
||||||
|
if re.match(filter_regexp, v[filter_key]):
|
||||||
|
yield v
|
||||||
|
else:
|
||||||
|
yield v
|
||||||
|
|
||||||
|
|
||||||
@readable_errors
|
@readable_errors
|
||||||
def increment(self, key, **kwargs):
|
def increment(self, key, **kwargs):
|
||||||
print(self.realkey(key))
|
print(self.realkey(key))
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue