Compare commits

...

735 commits

Author SHA1 Message Date
8287e73f6b Merge branch 'master' into 'master'
- Implement a complete cycle for buying a Matrix Chat Host

See merge request uncloud/uncloud!11
2021-07-19 16:36:11 +02:00
b7aa1c6971 - Added PricingPlan Model
- Implement a complete cycle for buying a Matrix Chat Host
- Refactor the Payement cycle and stripe related methods
2021-07-19 16:36:10 +02:00
Nico Schottelius
e205d8d07c Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2021-06-20 11:58:23 +02:00
Nico Schottelius
a463bcf7bd Late commits 2021-06-20 11:51:27 +02:00
Nico Schottelius
d872357dd1 Fix login -> move to bootstrap5 2021-05-25 20:22:18 +02:00
Nico Schottelius
485f08e25c Cleanup views 2021-05-25 19:55:33 +02:00
Nico Schottelius
745abc48ef Add balance if user is logged in 2021-05-25 19:55:13 +02:00
Nico Schottelius
49f52fd41d [bootstrap] update to bootstrap5 2021-02-13 18:50:28 +01:00
Nico Schottelius
c8ce7dbb40 do not touch local_settings.py on deploy 2021-01-17 15:54:16 +01:00
Nico Schottelius
a920887100 ++bridge update 2021-01-17 15:53:30 +01:00
Nico Schottelius
6b9b15e663 Add deploy.sh 2021-01-17 15:47:37 +01:00
Nico Schottelius
48ce21f833 integrate bootstrap 2021-01-01 13:25:52 +01:00
Nico Schottelius
6c15d2086e implement balance getting 2021-01-01 12:41:54 +01:00
Nico Schottelius
1b06d8ee03 [credit card] implement payment 2020-12-29 01:43:33 +01:00
Nico Schottelius
e225bf1cc0 implement credit card listing 2020-12-28 23:35:34 +01:00
Nico Schottelius
e2c4a19049 Less verbose 2020-12-26 14:48:10 +01:00
Nico Schottelius
74749bf07c fix templating 2020-12-26 14:45:28 +01:00
Nico Schottelius
93e5d39c7b moving vpn to direct configuration 2020-12-26 14:42:53 +01:00
Nico Schottelius
18d4c99571 [doc] workers need access to the database 2020-12-26 13:42:20 +01:00
Nico Schottelius
e51edab2f5 cleanup/in between commit 2020-12-26 11:22:51 +01:00
Nico Schottelius
f7c68b5ca5 Rename template 2020-12-25 17:33:01 +01:00
Nico Schottelius
6efedcb381 hackish way of registering works 2020-12-25 17:29:17 +01:00
Nico Schottelius
df4c0c3060 in between commit to update for cc tests 2020-12-25 10:31:42 +01:00
Nico Schottelius
8dd4b712fb [views] add index view for uncloud 2020-12-25 10:11:13 +01:00
Nico Schottelius
50a395c8ec sort requirements.txt 2020-12-25 10:10:57 +01:00
Nico Schottelius
663d72269a [wireguard] verify key length 2020-12-25 10:08:34 +01:00
Nico Schottelius
a0fbe2d6ed [wireguard] add unique constrain for keys in pool 2020-12-24 17:26:53 +01:00
Nico Schottelius
858aabb5ba Return value from validation 2020-12-20 22:03:43 +01:00
Nico Schottelius
ece2bca831 add new /sizes endpoint 2020-12-20 21:45:47 +01:00
Nico Schottelius
cdab685269 [vpn/doc] update docs 2020-12-20 19:37:12 +01:00
Nico Schottelius
689375a2fe Fix the config task 2020-12-20 19:17:03 +01:00
Nico Schottelius
8f83679c48 test cleaning tasks in a task fails:
[2020-12-20 18:01:50,264: WARNING/ForkPoolWorker-7] Pruning UncloudTask object (571ffc76-8b40-4cb6-9658-87030834bc6c)...
[2020-12-20 18:01:50,265: ERROR/ForkPoolWorker-7] Task uncloud.tasks.cleanup_tasks[f9fb1480-f122-41c9-bec1-3d6d0f92a22e] raised unexpected: RuntimeError('Never call result.get() within a task!\nSee http://docs.celeryq.org/en/latest/userguide/tasks.html#task-synchronous-subtasks\n')
Traceback (most recent call last):
  File "/home/nico/vcs/uncloud/venv/lib/python3.8/site-packages/celery/app/trace.py", line 405, in trace_task
    R = retval = fun(*args, **kwargs)
  File "/home/nico/vcs/uncloud/venv/lib/python3.8/site-packages/celery/app/trace.py", line 697, in __protected_call__
    return self.run(*args, **kwargs)
  File "/home/nico/vcs/uncloud/uncloud/tasks.py", line 13, in cleanup_tasks
    print(res.get())
  File "/home/nico/vcs/uncloud/venv/lib/python3.8/site-packages/celery/result.py", line 209, in get
    assert_will_not_block()
  File "/home/nico/vcs/uncloud/venv/lib/python3.8/site-packages/celery/result.py", line 37, in assert_will_not_block
    raise RuntimeError(E_WOULDBLOCK)
RuntimeError: Never call result.get() within a task!
See http://docs.celeryq.org/en/latest/userguide/tasks.html#task-synchronous-subtasks
2020-12-20 19:01:37 +01:00
Nico Schottelius
5e870f04b1 ++celery/tasks 2020-12-20 18:36:46 +01:00
Nico Schottelius
63191c0a88 Remove $ that is not needed in python... 2020-12-20 13:24:55 +01:00
Nico Schottelius
03c0b34446 ++config vpn server 2020-12-20 13:00:36 +01:00
Nico Schottelius
1922a0d92d ++routing tests 2020-12-20 12:54:02 +01:00
Nico Schottelius
2e6c72c093 wireguard/celery fixes 2020-12-20 12:45:36 +01:00
Nico Schottelius
b3626369a2 --syntax error 2020-12-20 12:24:35 +01:00
Nico Schottelius
179baee96d fix celery task routes syntax error 2020-12-20 12:22:50 +01:00
Nico Schottelius
054886fd9c begin phasing in config of vpn via cdist 2020-12-20 12:20:54 +01:00
Nico Schottelius
e2b36c8bca celery test 2020-12-13 19:50:36 +01:00
Nico Schottelius
372fe800cd fill in template values for settings 2020-12-13 19:06:22 +01:00
Nico Schottelius
16f3adef93 [doc] ++requirements alpine 2020-12-13 18:56:47 +01:00
Nico Schottelius
2d62388eb1 phasing in celery
for configuring the vpn server
2020-12-13 18:34:43 +01:00
Nico Schottelius
aec79cba74 [vpn] include vpn server public key 2020-12-13 18:05:48 +01:00
Nico Schottelius
cd19c47fdb [vpn] implement creating vpns 2020-12-13 17:59:35 +01:00
Nico Schottelius
cf948b03a8 ++vpn network 2020-12-13 13:28:43 +01:00
Nico Schottelius
5716cae900 [vpn] add selector for size 2020-12-13 11:43:49 +01:00
Nico Schottelius
10d5a72c5a [refactor] cleaning up uncloud_net for Wireguardvpn 2020-12-13 11:38:41 +01:00
Nico Schottelius
074cffcbd7 Add selection for vpnnetworkreservations 2020-12-09 21:20:33 +01:00
Nico Schottelius
7f32d05cd4 begin phasing in vpn support [poc] 2020-12-09 20:22:33 +01:00
Nico Schottelius
0fd5ac18cd do not import pay->auth
Try to keep common things in the "uncloud" module
2020-12-06 11:53:37 +01:00
Nico Schottelius
ad0c2f1e9d Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-11-17 11:47:53 +01:00
Nico Schottelius
0b1c2cc168 Cleanup code so that *most* test work again
Still need to solve the downgrade test
2020-11-15 15:43:11 +01:00
4845ab1e39 Create account using api
Registration and change_email is backed by ldap
2020-11-14 14:50:43 +05:00
Nico Schottelius
ecc9e6f734 [reverseDNS] add basic logic 2020-10-25 22:43:34 +01:00
Nico Schottelius
20c7c86703 restructure to move uncloudnetwork into core 2020-10-25 21:00:30 +01:00
Nico Schottelius
8959bc6ad5 various updates 2020-10-25 13:52:36 +01:00
Nico Schottelius
0cd8a3a787 ++update ungleich_provider 2020-10-11 22:36:01 +02:00
Nico Schottelius
bbc7625550 phase in configuration - move address to base 2020-10-11 22:32:08 +02:00
Nico Schottelius
fe4e200dc0 Begin phasing in the uncloudprovider 2020-10-11 17:45:25 +02:00
Nico Schottelius
e03cdf214a update VAT importer 2020-10-08 19:54:04 +02:00
Nico Schottelius
50fd9e1f37 ++work 2020-10-07 00:54:56 +02:00
Nico Schottelius
2e74661702 Fix first test case / billing 2020-10-06 23:14:32 +02:00
Nico Schottelius
c26ff253de One step furter to allow saving of orders w/o explicit recurringperiod 2020-10-06 19:21:37 +02:00
Nico Schottelius
9623a77907 Updating for products/recurring periods 2020-10-06 18:53:13 +02:00
Nico Schottelius
c435639241 gitignore some tests 2020-10-06 16:13:03 +02:00
Nico Schottelius
992c7c551e Make recurring period a database model
- For easier handling (foreignkeys, many2many)
- For higher flexibility (users can define their own periods)
2020-10-06 15:46:22 +02:00
Nico Schottelius
58883765d7 [tests] back to 5 working tests! 2020-09-28 23:16:17 +02:00
Nico Schottelius
8d8c4d660c Can order a generic product now 2020-09-28 21:59:35 +02:00
Nico Schottelius
c32499199a Add JSON support for product description 2020-09-28 21:34:24 +02:00
Nico Schottelius
c6bacab35a Phasing out Product model
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-09-28 20:59:08 +02:00
Nico Schottelius
1aead50170 remove big mistake: orders from product
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-09-28 20:44:50 +02:00
Nico Schottelius
d8a7964fed Continue to refactor for shifting logic into the order 2020-09-09 00:35:55 +02:00
Nico Schottelius
077c665c53 ++update 2020-09-03 17:16:18 +02:00
Nico Schottelius
f7274fe967 Adding logic to order to find out whether its closed 2020-09-03 16:38:51 +02:00
Nico Schottelius
1c7d81762d begin splitting bill record creation function 2020-09-02 16:02:28 +02:00
Nico Schottelius
18f9a3848a Implement ending/replacing date logic 2020-08-27 22:00:54 +02:00
Nico Schottelius
9211894b23 implement basic logic for updating a recurring order
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-27 14:45:37 +02:00
Nico Schottelius
b8b15704a3 begin testing bill sums
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-25 21:53:25 +02:00
Nico Schottelius
ab412cb877 Test that creating products w/o correct billing address fails 2020-08-25 21:31:12 +02:00
Nico Schottelius
7b83efe995 [pay] make sample products more modular 2020-08-25 21:11:28 +02:00
Nico Schottelius
4d5ca58b2a [tests] cleanup old tests
Finally manage.py tests runs through
2020-08-25 20:40:33 +02:00
Nico Schottelius
f693dd3d18 ++notes 2020-08-09 21:10:43 +02:00
Nico Schottelius
5ceaaf7c90 bill cleanup, note next step 2020-08-09 14:52:42 +02:00
Nico Schottelius
2b29e300dd [product] migrate orders to ManyToManyField 2020-08-09 14:44:29 +02:00
Nico Schottelius
8df1d8dc7c begin refactor product to user orders instead of single order
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-09 14:38:10 +02:00
Nico Schottelius
ef02cb61fd Refine tests for bills, multiple bills 2020-08-09 12:34:25 +02:00
Nico Schottelius
70c450afc8 fix tests for Product()
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-09 11:44:22 +02:00
Nico Schottelius
0dd1093812 add sample products and improve testing for Product 2020-08-09 11:02:45 +02:00
Nico Schottelius
6a928a2b2a Fix tests for billing 2020-08-09 10:18:15 +02:00
Nico Schottelius
89519e48a9 Various updates 2020-08-09 10:14:49 +02:00
Nico Schottelius
e169b8c1d1 Implement the whole billing logic
The major part has been written!
2020-08-09 10:14:31 +02:00
Nico Schottelius
d7c0c40926 first bill generation works
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-09 00:37:27 +02:00
Nico Schottelius
fd39526350 Improve billing address testing 2020-08-08 23:02:24 +02:00
Nico Schottelius
78d1de9031 Remove orderrecord
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-08 22:37:00 +02:00
Nico Schottelius
db1a69561b pass first 2 bill tests
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-08 22:31:43 +02:00
Nico Schottelius
9bf0a99f6a uncloud pay cleanups
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-08 22:20:49 +02:00
Nico Schottelius
c9be8cc50b orders only have 1 price
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-08 21:54:44 +02:00
Nico Schottelius
8da6a1e19c update models/new django
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-08 21:54:07 +02:00
Nico Schottelius
ff8fdb76b1 ++ test 2020-08-08 19:30:25 +02:00
Nico Schottelius
9b00ef11fb ++stuff 2020-08-04 18:56:36 +02:00
Nico Schottelius
d2bd6ba200 [test] update for uncloud_net 2020-08-04 12:41:15 +02:00
Nico Schottelius
165dacb7bf update to use new JSONFIELD
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-04 12:01:44 +02:00
Nico Schottelius
ee79877a27 +migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-04 11:40:34 +02:00
Nico Schottelius
2ce667e8c7 model changes
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-04 11:26:42 +02:00
Nico Schottelius
e563780142 Add some sample VMs 2020-08-02 00:55:07 +02:00
Nico Schottelius
66233a1ce5 [doc] more cleanup 2020-08-01 23:44:10 +02:00
Nico Schottelius
9d5d8657cb [doc] Move install and co. into the main documentation 2020-08-01 23:42:36 +02:00
Nico Schottelius
a091079677 in between commit
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-01 23:20:14 +02:00
Nico Schottelius
c9a941e290 [vm] add disks inlined to VMs 2020-08-01 18:48:51 +02:00
Nico Schottelius
f7b14bf507 cleanup migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-01 18:38:38 +02:00
Nico Schottelius
ed40b21d16 [doc] begin describe replacing orders 2020-08-01 18:31:27 +02:00
Nico Schottelius
880e4d046b [vm] cleanup 2020-08-01 18:30:40 +02:00
Nico Schottelius
cbd5a08ae7 make price a real property of order
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-01 18:27:12 +02:00
Nico Schottelius
64780bfc6c [models] update / doc 2020-08-01 16:29:24 +02:00
Nico Schottelius
9c1b4ab275 [db] add migration for vmdiskproduct 2020-08-01 16:29:11 +02:00
Nico Schottelius
2771a7518a [doc] cleanup 2020-08-01 16:28:19 +02:00
Nico Schottelius
9b3493a661 [doc] integrate vpn into manual 2020-08-01 16:24:21 +02:00
Nico Schottelius
932ac06cea begin doc in views 2020-08-01 14:05:56 +02:00
Nico Schottelius
05a897db70 +whitespace 2020-08-01 14:05:50 +02:00
Nico Schottelius
011096f152 [bootstrap user] remove syntax errors 2020-08-01 14:05:36 +02:00
Nico Schottelius
55ba61e36b [urls] remove obsolete comment 2020-08-01 14:05:26 +02:00
Nico Schottelius
1265e23750 [doc] add bootstrap 2020-08-01 14:05:12 +02:00
Nico Schottelius
bc0c77a393 add sql migration for opennebula
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-08-01 14:03:22 +02:00
Nico Schottelius
bdba3bffe2 [opennebula] turn VM into a product 2020-08-01 14:02:54 +02:00
Nico Schottelius
3745a0e2b1 [doc] ignore generated files 2020-08-01 13:24:51 +02:00
Nico Schottelius
7ce28b0b60 Begin new uncloud manual 2020-08-01 13:07:44 +02:00
Nico Schottelius
662e706eab begin to fix bill view 2020-06-21 23:54:57 +02:00
Nico Schottelius
11de455d23 begin to add pdf view of bill into admin 2020-06-21 23:46:26 +02:00
Nico Schottelius
126d9da764 Use quantity instead of usage_count 2020-06-21 16:42:55 +02:00
Nico Schottelius
8a17ee6de5 Include BillRecords in the admin 2020-06-21 16:08:00 +02:00
Nico Schottelius
721472b416 Fix constraint to active = True 2020-06-21 14:45:05 +02:00
Nico Schottelius
424ceafbb8 Enhance make-admin to allow making superuser 2020-06-21 14:44:55 +02:00
Nico Schottelius
1e68539ed8 remove uuid primary key
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-06-21 14:35:12 +02:00
Nico Schottelius
3ef19610f3 add script to reset migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-06-21 14:34:48 +02:00
Nico Schottelius
8decfe1b16 Phase in admin, remove uuid from bills 2020-06-21 13:46:54 +02:00
Nico Schottelius
662845128f ++ billing details 2020-06-20 23:47:26 +02:00
Nico Schottelius
95011c2058 Add readme for billing 2020-06-20 22:15:15 +02:00
Nico Schottelius
a3f3ca8cf9 in the middle of restructering 2020-05-24 13:45:03 +02:00
Nico Schottelius
5d1eaaf0af Add new models backup - before major refactoring 2020-05-24 12:46:11 +02:00
Nico Schottelius
bcd141730d convert recurring period into an integerfield
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-05-23 23:38:34 +02:00
Nico Schottelius
15535433e8 begin to change to day based differences
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-05-23 23:32:45 +02:00
Nico Schottelius
18b862c2e1 fix syntax errors 2020-05-23 23:08:59 +02:00
Nico Schottelius
b8652c921e Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-05-23 21:33:04 +02:00
Nico Schottelius
8bbcc5df5f ++dev 2020-05-23 21:32:56 +02:00
Nico Schottelius
0202f80a37 in between pay commit 2020-05-23 21:23:06 +02:00
Nico Schottelius
caedf874e4 [vpn] add tests 2020-05-20 21:00:08 +02:00
Nico Schottelius
f17f9060b0 [config] add path to chrome 2020-05-17 23:34:13 +02:00
Nico Schottelius
04fac71a85 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-05-17 22:38:49 +02:00
Nico Schottelius
ef76304bae + old stuff 2020-05-17 22:35:25 +02:00
Nico Schottelius
ec447e0dc4 Add support for primary address in user.
Closes #35

Fixes #35
2020-05-10 21:47:44 +02:00
Nico Schottelius
ca2065a94d gitignore for local settings 2020-05-10 14:37:20 +02:00
Nico Schottelius
dc7a465a8c Fix exception if first order does not have billing address 2020-05-10 14:22:09 +02:00
65440ab2ef Add depends_on relation on orders 2020-05-08 16:47:32 +02:00
d794b24c86 Make VM order-able again 2020-05-08 16:31:33 +02:00
df059fb00d Speed-up CI with pre-built image, add resources directory 2020-05-08 12:15:40 +02:00
67af7b5465 Add missing billnico migration 2020-05-08 11:43:01 +02:00
beb5bd7ee4 Fix existing uncloud_pay tests 2020-05-08 11:33:59 +02:00
9574d69f4c Move VAT rate CSV out of archive/ 2020-05-08 11:23:09 +02:00
74e2168529 Fix floating-point issue on bills (Fix #31) 2020-05-08 11:13:11 +02:00
444d6ded28 Move AMOUNT_* and COUNTRIES to uncloud_pay/init.py 2020-05-08 10:56:03 +02:00
cbba1f4169 Add admin bill generation endpoint 2020-05-08 10:42:04 +02:00
d47c94ba84 Implement non-destructive order updates 2020-05-08 10:07:44 +02:00
89e853b490 Add order termination logic 2020-05-08 09:31:46 +02:00
Nico Schottelius
1b97fc8fc7 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-05-07 20:22:49 +02:00
Nico Schottelius
41a2f18453 In between commit 2020-05-07 20:22:42 +02:00
a086d46136 Allow bill download from admin bill endpoint 2020-05-07 15:45:04 +02:00
ae2bad5754 Generate bill PDFs from /my/bill 2020-05-07 15:38:49 +02:00
3874165189 Fix bill generation 2020-05-07 14:24:04 +02:00
56d98cbb55 Implement Orders/Bills permissions, unpaid bill views 2020-05-07 13:12:38 +02:00
718abab9d2 Add make-admin command to uncloud_auth 2020-05-07 12:45:06 +02:00
268e08c4db Adapt README for SQLite 2020-05-07 12:31:59 +02:00
b8ac99acb6 On more small commit to fix README formatting 2020-05-07 12:25:05 +02:00
221d98af4b Inline CI badges 2020-05-07 12:24:17 +02:00
ebd4e6fa1b Add fancy CI badges to README 2020-05-07 12:23:17 +02:00
b512d42058 Add devel environment setup instructions 2020-05-07 12:21:49 +02:00
1245c191c0 Adapt CI to new structure 2020-05-07 12:13:48 +02:00
95d43f002f Move django-based uncloud to top-level 2020-05-07 12:12:35 +02:00
0560063326 Add description field to Orders 2020-05-07 12:08:18 +02:00
db3c29d17e Fix admin order creation 2020-05-07 12:05:26 +02:00
892b2b6f13 Revert "Disable vat validator to get project back running"
This reverts commit 1cf20a2cb6.
2020-05-07 12:03:28 +02:00
Nico Schottelius
aa8ade4730 Add readme about identifiers 2020-05-05 16:01:47 +02:00
Nico Schottelius
594f1a9b69 +hacks 2020-05-05 15:19:50 +02:00
Nico Schottelius
e3b28354fe ++notes 2020-05-05 15:19:04 +02:00
Nico Schottelius
99a18232aa VMs now properly set their pricing 2020-05-02 23:44:20 +02:00
Nico Schottelius
c835c874d5 [BREAKING] make Order a stand-alone version
I think that while the idea of an Orderrecord is good, we might get
away / have a simpler implementation if we only use orders and
reference them where needed.

I saved the previous Order model for easy rollback, if my assumption
is wrong.
2020-05-02 22:48:05 +02:00
Nico Schottelius
028f1ebe6e Entry point /beta/vm/ works for creating VM + order 2020-05-02 22:03:34 +02:00
Nico Schottelius
9ef5309b91 +db migrations for pay/vm
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-05-02 21:21:29 +02:00
Nico Schottelius
4097c2ce13 BillingAddress: make mget_preferred_address a classmethod 2020-05-02 21:20:14 +02:00
Nico Schottelius
736fe27493 Add issues.org as a shortcut for registering issues 2020-05-02 20:45:19 +02:00
Nico Schottelius
7d708cfbb6 Fix empty vat number bug 2020-05-02 20:42:09 +02:00
Nico Schottelius
927fb20671 Versionise API and cleanups 2020-05-02 20:31:36 +02:00
Nico Schottelius
eea654a9f8 Phase in new beta/vm view for creating vms + orders + bills 2020-05-02 19:15:48 +02:00
Nico Schottelius
2cda6441f4 Refactor secret / local settings handling 2020-05-02 00:16:29 +02:00
Nico Schottelius
62d9ccbbef [vpn] begin to introduce save() method
The save() and delete() method will create/manage the orders
2020-04-27 18:25:44 +02:00
Nico Schottelius
1cf20a2cb6 Disable vat validator to get project back running 2020-04-27 18:25:27 +02:00
94932edebe Add user admin endpoint, import from LDAP 2020-04-18 15:11:52 +02:00
a15952862a Make VM order-able again 2020-04-18 15:11:46 +02:00
cec4263621 Merge branch 'vat-handling' into 'master'
VAT support handling

See merge request uncloud/uncloud!7
2020-04-18 11:57:16 +02:00
f61b91dab2 Catch any exception from VIES VAT check 2020-04-18 11:51:13 +02:00
b3afad5d5d Compute VAT rate and amount on bill generation 2020-04-18 11:43:55 +02:00
3a03717b12 Split bills between orders of the same billing address 2020-04-18 11:21:11 +02:00
db9ff5d18b Display allr elevant values on Bill serializer/page 2020-04-18 10:43:23 +02:00
a49fe6ff51 Properly wire billing addresses to uncloud_service 2020-04-18 10:40:11 +02:00
dd0c1cba94 Remove legacy ungleich_service migrations 2020-04-18 10:39:57 +02:00
c0e12884e1 Sync migrations - again! 2020-04-18 09:38:12 +02:00
9bbe3b3b56 Adapt uncloud_pay tests to support billing addresses 2020-04-18 09:26:34 +02:00
e6eba7542b Minor fixes, DB sync after rebase 2020-04-18 09:26:21 +02:00
0522927c50 Start wiring BillingAddresses to bills & orders 2020-04-18 09:13:52 +02:00
3fa1d5753e Minimal VAT validation on billing address registration 2020-04-18 09:13:04 +02:00
c6ca94800e Add BillingAddress structure to users 2020-04-18 09:13:04 +02:00
ad187c02da Import VAT rates "importer" from dynamicweb 2020-04-18 09:13:04 +02:00
7afb3f8793 Merge branch 'uncloud-product-activation' into 'master'
Handle product activation on payment

See merge request uncloud/uncloud!6
2020-04-18 09:08:32 +02:00
86775af4c8 Fix product activation tests after rebase 2020-04-18 09:02:33 +02:00
83d2cd465d Sync migrations after rebase 2020-04-18 08:42:50 +02:00
b6c976b722 Commit autp-generated migrations (missing from master?) 2020-04-18 08:36:41 +02:00
d1e993140c Add simple product activation test 2020-04-18 08:36:41 +02:00
c57780fb4d Add naive GenericServiceProduct 2020-04-18 08:36:41 +02:00
83a0ca0e4e Adapt billing tests to product activation structure 2020-04-18 08:35:22 +02:00
5d5bf486b5 Initial product activation implementation 2020-04-18 08:34:41 +02:00
839bd5f8a9 Merge branch 'vm-ordering-disk' into 'master'
Wire disk images to VM creation/ordering, make Order records transparents

See merge request uncloud/uncloud!4
2020-04-18 08:32:07 +02:00
aa0702faba Add chromium to path on CI environment 2020-04-13 12:06:03 +02:00
9a57153c4e Commit missing migrations 2020-04-13 12:02:49 +02:00
f5897ed4b1 Adapt recurring price of VM and Matrix to new scheme 2020-04-13 12:00:59 +02:00
14f59430bb Restore Order.add_record, used by uncloud_pay tests 2020-04-13 11:54:41 +02:00
05f8f1f6c0 Fix dependency issue in CI job 2020-04-13 11:40:19 +02:00
1a58508f8b Rename ungleich_service into uncloud_service 2020-04-13 11:39:49 +02:00
e67bd03997 Migration fix after rebase 2020-04-13 11:19:18 +02:00
a4cc4304f9 Adapt managed service to create VMDiskProduct 2020-04-13 11:19:18 +02:00
d3b7470294 Wire disk images to VM creation/ordering 2020-04-13 11:19:18 +02:00
a7e9f3c09d Move Order.add_record to save hook in abstract Product 2020-04-13 10:42:44 +02:00
Nico Schottelius
ff133e81b7 [vpn] update to show reservations, create wireguard config 2020-04-12 22:55:22 +02:00
Nico Schottelius
85b4d70592 [vpn] make a vpn creat-able!
[15:40] line:~% http -a nicoschottelius:$(pass ungleich.ch/nico.schottelius@ungleich.ch)  http://localhost:8000/net/vpn/ network_size=48  wireguard_public_key=$(wg genkey | wg pubkey)
HTTP/1.1 201 Created
Allow: GET, POST, HEAD, OPTIONS
Content-Length: 206
Content-Type: application/json
Date: Sun, 12 Apr 2020 13:40:26 GMT
Server: WSGIServer/0.2 CPython/3.7.3
Vary: Accept
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "extra_data": null,
    "network": "2a0a:e5c1:203::",
    "order": null,
    "owner": 30,
    "status": "PENDING",
    "uuid": "8f977a8f-e06a-4346-94ae-8f525df58b7b",
    "wireguard_public_key": "JvCuUTZHm9unasJkGsLKN0Bf/hu6ZSIv7dnIGPyJ6xA="
}
2020-04-12 15:40:39 +02:00
Nico Schottelius
b55254b9b1 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-04-12 14:28:39 +02:00
Nico Schottelius
bc033a9087 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-04-12 11:36:51 +02:00
Nico Schottelius
05f38d157e add a discounter function to the product model 2020-04-12 11:35:37 +02:00
Nico Schottelius
bab59b1879 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-04-11 21:37:50 +02:00
Nico Schottelius
50b8b7a5f6 in-between commit 2020-04-11 21:37:36 +02:00
f1bba63f6f Merge branch 'yearly-billing' into 'master'
Implement yearly billing, general billing tests

See merge request uncloud/uncloud!5
2020-04-11 21:37:13 +02:00
276c7e9901 Set VM order starting date on creation 2020-04-09 14:52:56 +02:00
3a37343a73 Set default value for vpnnetworkreservation, rebuild migrations 2020-04-09 14:28:46 +02:00
3588ae88f9 Merge branch 'master' into HEAD 2020-04-09 14:10:01 +02:00
Nico Schottelius
9431f11284 ++notes 2020-04-09 12:09:38 +02:00
Nico Schottelius
e64c2b8ddb Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-04-09 12:08:17 +02:00
Nico Schottelius
d9473e8f33 ++ doc 2020-04-09 12:08:11 +02:00
cb3346303b Fix typo in migration dependencies for uncloud_pay 2020-04-09 12:06:05 +02:00
Nico Schottelius
7d892daff9 [db] stay on psql+socket 2020-04-09 11:59:49 +02:00
Nico Schottelius
08b9886ce3 Remove sample secret key in secrets_sample
No need to worry, this was just a testing key
2020-04-09 11:59:25 +02:00
d6bdf5c991 Merge branch 'sync-old-meowpay-patches' into 'master'
Revamped payment pipeline (imported from nico/mew-cloud)

See merge request uncloud/uncloud!3
2020-04-09 11:31:49 +02:00
cc7056c87c Remove old Stripe settings from secrets_sample.py 2020-04-08 17:55:48 +02:00
a8b81b074b Remove user view from uncloud_pay 2020-04-08 17:40:44 +02:00
89c705f7d2 Set one payment method as primary, allow updates 2020-04-08 17:22:53 +02:00
f2a797874a Merge branch 'master' into stripe-js 2020-04-08 17:08:09 +02:00
Nico Schottelius
d3f2a3e071 in between commit
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-04-08 16:24:39 +02:00
Nico Schottelius
3d2f8574d3 [db] use tcp -> support ssh 2020-04-08 13:09:17 +02:00
Nico Schottelius
1838eaf7dd Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-04-08 12:07:11 +02:00
Nico Schottelius
8986835c7e Add readme for postgresql support 2020-04-08 12:03:18 +02:00
Nico Schottelius
938f0a3390 update to work on different computer
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-04-07 19:45:16 +02:00
Nico Schottelius
913e992a48 [vpn] fix urls 2020-04-06 22:30:01 +02:00
Nico Schottelius
096f7e05c0 [migration] new models for uncloud_net 2020-04-06 22:29:41 +02:00
Nico Schottelius
06c4a5643c [doc] move readme to subdir 2020-04-06 22:08:29 +02:00
Nico Schottelius
198aaea48a Remove unused ldaptest 2020-04-06 22:06:48 +02:00
Nico Schottelius
d537e9e2f0 [doc] add new readme's 2020-04-06 22:06:34 +02:00
Nico Schottelius
5d084a5716 phase in vpn
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-04-03 19:27:49 +02:00
Nico Schottelius
8fb3ad7fe8 inline all pictures 2020-04-03 18:51:09 +02:00
Nico Schottelius
c44faa7a73 Begin to include bill output 2020-04-03 18:41:17 +02:00
Nico Schottelius
fa0ca2d9c1 Merge remote-tracking branch 'meowpaylocal/master' 2020-04-02 19:31:57 +02:00
Nico Schottelius
7a6c8739f6 Rename / prepare for merge with uncloud repo 2020-04-02 19:31:03 +02:00
Nico Schottelius
833d570472 sync .gitignore 2020-04-02 19:30:47 +02:00
Nico Schottelius
3cf3439f1c Move all files to _etc_based 2020-04-02 19:29:08 +02:00
Nico Schottelius
23203ff418 vmsnapshot progress 2020-03-22 20:55:11 +01:00
Nico Schottelius
9961ca0446 add new migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-22 18:59:59 +01:00
Nico Schottelius
105142f76a Add template for creating VMs 2020-03-22 18:52:31 +01:00
Nico Schottelius
08fe3e689e Add debug to opennebula, create VM disks from opennebula correctly 2020-03-22 17:30:55 +01:00
Nico Schottelius
10c5257f90 Introduce "extra_data" jsonfield 2020-03-21 11:59:04 +01:00
Nico Schottelius
a32f7522b5 Relate VM to disks and snapshots 2020-03-18 15:43:01 +01:00
Nico Schottelius
4b4cbbf009 Also list snapshots for a VM 2020-03-18 15:19:06 +01:00
Nico Schottelius
c6a9bd4363 Make balance a user attribute + decimalfield 2020-03-18 14:53:26 +01:00
Nico Schottelius
cd01f62fde Move user view to uncloud_auth 2020-03-18 14:36:40 +01:00
Nico Schottelius
2f1aee8181 Can create a VMSnapshot w/ order (bugs to be removed) 2020-03-17 19:53:14 +01:00
Nico Schottelius
6a382fab23 [vmhost] add used_ram_in_gb 2020-03-17 19:07:00 +01:00
Nico Schottelius
b9473c1803 ++ fix opennebula migration 2020-03-17 16:03:41 +01:00
Nico Schottelius
cc2efa5c14 Remove old opennebula view, remove vmid field 2020-03-17 15:40:08 +01:00
Nico Schottelius
5d840de55c [opennebula] refresh formula, cleanup vm import/migration to uncloud 2020-03-17 15:39:24 +01:00
Nico Schottelius
9f4b927c74 Introduce mirations to ungleich_service to make tests work 2020-03-17 14:50:28 +01:00
Nico Schottelius
55bd42fe64 List all VMs for admins 2020-03-17 14:50:14 +01:00
Nico Schottelius
8634d667d5 update requirements for graphing 2020-03-17 14:49:59 +01:00
Nico Schottelius
ac7ea86668 rename opennebula commands 2020-03-17 14:49:49 +01:00
Nico Schottelius
8356404fe4 ++ product readme 2020-03-17 14:49:36 +01:00
Nico Schottelius
723d2a99cc add django…extensions to support "graph_models" 2020-03-17 13:30:48 +01:00
Nico Schottelius
8f4e7cca1b add migrations to ungleich_service so tests don't fail
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-17 12:46:02 +01:00
b15a12dc71 Missing import for DCLVMProductSerializer 2020-03-13 14:22:49 +05:00
923102af24 Fix DCLVMProductSerializer import following rebase 2020-03-09 13:17:40 +01:00
f4ebbb79ce Add test coverage parsing to CI 2020-03-09 12:25:12 +01:00
122bc586b4 Disable 'old' broken/not-yet-implemented ungleich_vm tests
To make CI happy on merge requests from now on :-)
2020-03-09 12:25:12 +01:00
ae6548e168 Read database host and user from environment (used by CI pipeline) 2020-03-09 12:25:12 +01:00
0e4068cea8 Add minimal CI running django tests 2020-03-09 12:25:12 +01:00
623d3ae5c4 Fix various billing issues discovered by testing 2020-03-09 12:25:12 +01:00
fe0e6d98bf Add simple tests for billing 2020-03-09 12:25:11 +01:00
41e35c1af0 Add missing migration and dependency to run tests 2020-03-09 12:25:11 +01:00
948391ab2e Dump test placeholder for uncloud_pay 2020-03-09 12:25:11 +01:00
d089d06264 Initial yearly billing implementation 2020-03-09 12:25:11 +01:00
c086dbd357 Rebuild paymentmethod/stripe migrations from master 2020-03-09 12:25:11 +01:00
545727afe7 Move STRIPE_PUBLIC_KEY to secrets (i.e. local configuration) 2020-03-09 12:25:11 +01:00
7bbc729b87 Fix duplicates in payment method creation 2020-03-09 12:25:11 +01:00
b10cae472e Fix migration dependencies after rebase 2020-03-09 12:25:11 +01:00
952cf8fd13 Remove unused empty migration 2020-03-09 12:25:11 +01:00
7e278228bd Fix payment update updates 2020-03-09 12:25:11 +01:00
31507c0f1a Fix error in stripe get_customer_id_for 2020-03-09 12:25:11 +01:00
7e58a8ace2 Fix generate-bills, remove debug print in charge method 2020-03-09 12:25:11 +01:00
a4fa0def4b Fix dumb logic errors/typo from last commit 2020-03-09 12:25:11 +01:00
80fe28588e Revamp stripe error handling 2020-03-09 12:25:11 +01:00
4e658d2d77 Remove legacy credit card support 2020-03-09 12:25:11 +01:00
5161a74354 Add STRIPE_PUBLIC_KEY setting 2020-03-09 12:25:11 +01:00
0e62ccff3b Cleanup/reorder uncloud_pay views 2020-03-09 12:25:11 +01:00
bf83b750de Replace legacy Stripe Charge API by Payment{setup, intent} 2020-03-09 12:24:14 +01:00
Nico Schottelius
47148454f6 s/_/-/ for bill id 2020-03-06 11:11:16 +01:00
Nico Schottelius
0032c272e7 Merge branch 'bill-id' of code.ungleich.ch:nico/meow-pay 2020-03-06 11:10:47 +01:00
Nico Schottelius
263125048d Begin to introduce a DCL alike view for VMs 2020-03-06 11:10:20 +01:00
658262c599 Add human readable reference to bills 2020-03-06 09:39:41 +01:00
c41b55573a Fix order link in BillRecordSerializer 2020-03-06 09:32:25 +01:00
4016c28c5f Fix generate-bills 2020-03-06 09:17:31 +01:00
Nico Schottelius
aa8336b7e4 VM: def __str__ 2020-03-05 23:55:33 +01:00
Nico Schottelius
0e6a6afd88 [opennebula] Fix fields/serializers 2020-03-05 23:18:07 +01:00
Nico Schottelius
efbe1c0596 Merge commands into the "vm" command 2020-03-05 17:52:01 +01:00
6c7f0e98b3 Rebuild paymentmethod/stripe migrations from master 2020-03-05 16:24:45 +01:00
Nico Schottelius
b8c2f80e45 [vmhost] add available_ram_in_gb and available_cores 2020-03-05 15:06:34 +01:00
Nico Schottelius
139aca6a61 Remove vms field from vmhost
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-05 14:58:45 +01:00
Nico Schottelius
2a73f0e767 [migration] make vm name optional, use storage class choices
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-05 14:22:56 +01:00
Nico Schottelius
66e224e926 [storage] move choices to uncloud_storage 2020-03-05 14:21:10 +01:00
Nico Schottelius
10f09c7115 add an old client hack (just for reference) 2020-03-05 14:15:33 +01:00
Nico Schottelius
ec7a2a3c3a Correct pricing for VMProduct 2020-03-05 14:00:14 +01:00
b07df26eb2 Move STRIPE_PUBLIC_KEY to secrets (i.e. local configuration) 2020-03-05 11:51:08 +01:00
b958cc77ea Fix duplicates in payment method creation 2020-03-05 11:45:37 +01:00
e9b6a6f277 Fix migration dependencies after rebase 2020-03-05 11:43:07 +01:00
546667d117 Remove unused empty migration 2020-03-05 11:39:48 +01:00
b88dfa4bfe Fix payment update updates 2020-03-05 11:39:48 +01:00
d6ee806467 Fix error in stripe get_customer_id_for 2020-03-05 11:39:48 +01:00
a41184d83d Fix generate-bills, remove debug print in charge method 2020-03-05 11:39:48 +01:00
2f70418f4d Fix dumb logic errors/typo from last commit 2020-03-05 11:39:48 +01:00
21e1a3d220 Revamp stripe error handling 2020-03-05 11:39:48 +01:00
4cc19e1e6e Remove legacy credit card support 2020-03-05 11:39:48 +01:00
08bf7cd320 Add STRIPE_PUBLIC_KEY setting 2020-03-05 11:39:48 +01:00
7e9f2ea561 Cleanup/reorder uncloud_pay views 2020-03-05 11:39:48 +01:00
929211162d Replace legacy Stripe Charge API by Payment{setup, intent} 2020-03-05 11:39:48 +01:00
Nico Schottelius
cf17373b3f Fix ahmed introduced migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-05 11:35:00 +01:00
Nico Schottelius
14a4fa8cc1 Merge remote-tracking branch 'ahmed/migrate-one-to-regular-vm' 2020-03-05 11:34:15 +01:00
Nico Schottelius
4fc1c36ae9 fix incorrect migrations from fnux-stable branch
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-05 11:17:30 +01:00
371c5ccf00 Merge remote-tracking branch 'origin/fnux-hacks' 2020-03-04 12:12:22 +01:00
faca104459 Fix stripe import in uncloud_pay.models 2020-03-04 11:05:21 +01:00
9e8149135b Move bill generation logic to Bill class, initial work for prepaid 2020-03-04 10:55:12 +01:00
02b287eff8 small cleaning 2020-03-04 14:44:41 +05:00
9aabc66e57 Pay: move some model-related methods from helpers to models
Otherwise we end up in circular dependency hell
2020-03-04 09:39:18 +01:00
a662b1fe29 Make migrate-one-vm-to-regular command idempotent 2020-03-04 13:25:46 +05:00
88c10e2e4a improve readability 2020-03-03 23:53:45 +05:00
fea0568bb9 init commit 2020-03-03 23:46:39 +05:00
e0cb6ac670 Allow for charging customers 2020-03-03 18:16:25 +01:00
Nico Schottelius
ebc9238845 recreate all migrations
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-03 17:50:52 +01:00
94a39ed81d Properly wire stripe card to payment methods 2020-03-03 16:56:42 +01:00
5c2d2a5b94 Document relations for Orders and Managed Services 2020-03-03 13:14:51 +01:00
Nico Schottelius
e9ef2acb06 Add readme for objects 2020-03-03 12:15:05 +01:00
3846e49395 Fix migration issue introduced in previous merge 2020-03-03 11:40:37 +01:00
a849e642dd Merge remote-tracking branch 'origin/master' into fnux-hacks 2020-03-03 11:36:08 +01:00
28407bf3e3 Quickly document OrderRecord class 2020-03-03 11:34:47 +01:00
Nico Schottelius
ea00e81b1e Move all stripe stuff to stripe.py 2020-03-03 11:31:32 +01:00
53baf0d9f3 Fix typo in BillRecord 2020-03-03 11:29:57 +01:00
11e22f5001 Consistently use one_time_price instead of setup_fee 2020-03-03 11:27:35 +01:00
Nico Schottelius
e176ad0817 Remove second stripe key definition 2020-03-03 11:26:16 +01:00
Nico Schottelius
a50095f873 Merge remote-tracking branch 'origin/fnux-stable'
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-03 11:23:29 +01:00
a40da40169 Add recurring_count to bills 2020-03-03 11:15:48 +01:00
2eaaad49db Handle setup fee in bills 2020-03-03 10:59:21 +01:00
9fdf66ed74 Fix MatrixService ordering 2020-03-03 10:51:16 +01:00
b31aa72f84 Allow to select billing period when registering VM 2020-03-03 10:14:56 +01:00
5559d600c7 Move things around for readability in uncloud_pay models and serializer 2020-03-03 09:13:04 +01:00
4e51670a90 Expand recurring period billing logic for DD/MM/hh/month 2020-03-03 08:53:19 +01:00
4ad737ed90 Initial stripe playground 2020-03-02 22:29:50 +01:00
c651c4ddaa Cleanup a bit BillRecord 2020-03-02 16:41:49 +01:00
531bfa1768 actual thing name is replaced by pseudo names 2020-03-02 19:20:12 +05:00
750d8c8cbf Use fictional hostname for VMHost 2020-03-02 17:42:54 +05:00
0c3e6d10ae Indentation/Spacing fixes 2020-03-02 17:20:30 +05:00
afdba3d7d9 Remove duplicate code 2020-03-02 17:17:30 +05:00
52e74c22cc Merge branch 'nico/meow-pay-master' into HEAD 2020-03-02 17:05:23 +05:00
6c9c63e0da Add sample clean() for model + Add tests for uncloud_vm 2020-03-02 16:54:36 +05:00
9e9018060e Wire order records to bills, fix user balance 2020-03-02 10:46:04 +01:00
9e253d497b Wrap VM creation in database transaction 2020-03-02 09:30:51 +01:00
81bd54116a Add records to orders 2020-03-02 09:25:03 +01:00
8e41b894c0 Add OrderRecord model 2020-03-02 08:09:42 +01:00
Ahmed Bilal
3228b91038 Merge branch 'master' into 'master'
Merge nico/meow-pay into ahmedbilal/meow-pay

See merge request ahmedbilal/meow-pay!7
2020-03-02 07:17:04 +01:00
Ahmed Bilal
028fd6789f ++cleanup
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-03-02 07:17:04 +01:00
4f25086a63 Only generate bill if no overlap 2020-03-01 15:47:27 +01:00
be2b0a8855 Fix a few errors on preview billing rework
Another WIP commit to sync with laptop, do not forget to rebase!
2020-03-01 12:23:04 +01:00
Nico Schottelius
4115eed2a8 +migration
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-29 17:58:10 +01:00
Nico Schottelius
5c33bc5c02 support creating disks
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-29 17:57:57 +01:00
Nico Schottelius
6a38e4e0a4 add url for importing disk image
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-29 17:00:13 +01:00
Nico Schottelius
bcbd6f6f83 Introduce disk->image relationship
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-29 16:45:52 +01:00
e319d1d151 WIP revamped bill logic 2020-02-29 09:08:55 +01:00
af1265003e Define custom fields and serializer for MatrixServiceProduct 2020-02-28 16:26:45 +01:00
eaa483e018 Commit forgottem uncloud_vm migrations 2020-02-28 15:08:45 +01:00
181005ad6c Cleanup VMProduct serializer, add name field to VMProduct 2020-02-28 15:08:00 +01:00
b3bbfafa04 Introduce custom ProductViewSet preventing customer from updating
products
2020-02-28 14:57:45 +01:00
33cc2b2111 Add uncloud_storage template app 2020-02-28 14:48:01 +01:00
3b87a47430 Add initial ungleich_service app with MatrixServiceProduct shell 2020-02-28 14:46:33 +01:00
b5a242f176 Merge branch 'master' into fnux-hacks 2020-02-28 14:06:29 +01:00
1cb1de4876 Add (broken) charge method to payment method endpoint 2020-02-28 11:10:31 +01:00
c0512e54b0 Add handle-overdue-bills 2020-02-28 10:18:24 +01:00
e12575e1de Commit forgotten migration on Orders (Float->Decimal) 2020-02-28 09:59:13 +01:00
adb57c55ca Revamp generate-bills logic to avoid overlapping 2020-02-28 09:58:01 +01:00
Nico Schottelius
89215e47b6 phase in mac 2020-02-28 09:34:29 +01:00
37ed126bc1 Create payment on strip charging 2020-02-28 09:26:18 +01:00
4bed53c8a8 Wire charge-negative-balance to payment methods 2020-02-28 09:10:36 +01:00
059791e2f2 Add initial generate-bills and charge-negative-balance uncloud-pay
commands
2020-02-28 08:59:32 +01:00
ef5e7e8035 Quickly wire vm creation to orders 2020-02-28 07:26:34 +01:00
b1649a6228 Remove product resolution from /order endpoint 2020-02-28 07:25:56 +01:00
0e28e50bac Revert "Commit WIP changes for /order, if needed at any point"
This reverts commit 83794a1781a1b84506100b39a6997882c654b4f3.
2020-02-28 07:25:18 +01:00
38d3a3a5d3 Commit WIP changes for /order, if needed at any point 2020-02-28 07:25:18 +01:00
809a55e1dd Wire VMProduct creation to order 2020-02-28 07:25:18 +01:00
b2fe5014d8 Make recurring_period an Enum, VMProduct a Product, initial wire for
order
2020-02-28 07:25:05 +01:00
1dd3324275 Wiring initial user balance 2020-02-28 07:24:51 +01:00
36fcff5149 Add initial structure for payment methods 2020-02-28 07:24:51 +01:00
Nico Schottelius
288a65f219 ++update
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-27 15:29:15 +01:00
Nico Schottelius
bd6008462d add template for uncloud_net
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-27 15:29:05 +01:00
Nico Schottelius
70a4fe4d90 order: serialize all fields 2020-02-27 12:45:54 +01:00
Nico Schottelius
e89fb45f9c Merge remote-tracking branch 'origin/fnux-hacks' 2020-02-27 12:43:24 +01:00
1ff5702ce3 Expose Order model 2020-02-27 12:42:24 +01:00
Nico Schottelius
b722f30ea5 ++doc 2020-02-27 12:42:09 +01:00
f5eadd6ddb Move user view to uncloud_pay 2020-02-27 12:38:04 +01:00
Nico Schottelius
1445acb77a Merge remote-tracking branch 'origin/fnux-hacks' 2020-02-27 12:36:40 +01:00
Nico Schottelius
7bf4f2adb2 --debug
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-27 12:36:33 +01:00
Nico Schottelius
a9aac39486 Create a vmsnapshot + associated order 2020-02-27 12:31:20 +01:00
225f20c91b Fix typo in payment source model 2020-02-27 12:21:52 +01:00
b9b605f407 Add ADMIN endpoints for bills and payments 2020-02-27 12:21:25 +01:00
f358acca05 Fix payment creation 2020-02-27 12:11:13 +01:00
Nico Schottelius
41a5eae879 cleanup views/vmsnapshot 2020-02-27 12:09:29 +01:00
Nico Schottelius
657dfc541e Merge remote-tracking branch 'origin/fnux-hacks'
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-27 12:06:02 +01:00
Nico Schottelius
fd648ade65 ++cleanup
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-27 12:02:41 +01:00
06ab21c577 Fix python errors on latest hack commits => make runserver happy again 2020-02-27 11:59:28 +01:00
Nico Schottelius
11d629bb51 [uncloud_api] completely remove it 2020-02-27 11:42:42 +01:00
Nico Schottelius
aa59b05a2d cleanup urls 2020-02-27 11:40:36 +01:00
Nico Schottelius
a58a361254 Move snapshot to _pay and _vm 2020-02-27 11:36:50 +01:00
Nico Schottelius
1ca247148c [uncloud_pay] add "prototype" 2020-02-27 11:21:38 +01:00
033b1e846a Merge branch 'nico/meow-pay-master' into HEAD 2020-02-27 14:35:52 +05:00
Nico Schottelius
c0bf4d96c4 ++ debian/devuan notes 2020-02-26 21:13:30 +01:00
Ahmed Bilal
0a6fa031d3 Merge branch 'master' into 'master'
Merge nico/meow-pay into ahmedbilal/meow-pay

See merge request ahmedbilal/meow-pay!5
2020-02-26 11:31:18 +01:00
Ahmed Bilal
0c7ca1147a fix migrations the ugly way
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-26 11:31:17 +01:00
Nico Schottelius
df851bee08 Merge branch 'master' of code.ungleich.ch:nico/meow-pay 2020-02-26 11:16:46 +01:00
Nico Schottelius
0b60765e2b in between commit 2020-02-26 11:16:42 +01:00
Nico Schottelius
bd3d21faa9 add thoughts for health checking 2020-02-25 22:04:04 +01:00
Nico Schottelius
c7ded96658 vmhosts, restructure urls, etc. 2020-02-25 22:01:55 +01:00
Nico Schottelius
d4b170f813 phase in vmhost
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-25 20:53:12 +01:00
Nico Schottelius
446c13b77c fix/simplify syncvm 2020-02-25 19:23:39 +01:00
Nico Schottelius
defe36bfb6 Merge branch 'master' of code.ungleich.ch:nico/meow-pay 2020-02-25 18:28:19 +01:00
Nico Schottelius
e790063d5a Merge remote-tracking branch 'ahmed/master' into ahmed_merge
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-25 18:19:55 +01:00
Nico Schottelius
cc3d2f2d42 in-between-commit
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-25 18:15:22 +01:00
d658b9635d Replace (vmid,uuid) with id in VM model + Add last_host and graphics in VM model + Fixed retrieve view in uncloud.opennebula 2020-02-25 21:03:20 +05:00
Nico Schottelius
7d1c8df84d ++ postgres requirement 2020-02-25 14:20:03 +01:00
Nico Schottelius
cc9e5905eb update
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-25 14:12:23 +01:00
c7252cde53 Introduced local settings in meow-pay/uncloud django app 2020-02-25 13:09:54 +05:00
a72bc142a6 Fixed issues in opennebula/views.py + syncvm now behaves correctly and print users which are not in ldap as per their email address 2020-02-25 11:50:49 +05:00
739bd72526 Migration fixed + opennebula/views.py fixed 2020-02-23 23:00:42 +05:00
Nico Schottelius
15b0fe3dc9 fix migrations the ugly way
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-23 18:11:14 +01:00
Nico Schottelius
50df7050d6 vmhost: add status field 2020-02-23 17:46:30 +01:00
Nico Schottelius
734c406245 Extend uncloud VM models 2020-02-23 17:43:06 +01:00
b3e505d37c migration fix 2020-02-23 21:34:22 +05:00
Ahmed Bilal
98f84f1cf5 Merge branch 'master' into 'master'
Merge nico/meow-pay into ahmedbilal/meow-pay

See merge request ahmedbilal/meow-pay!4
2020-02-23 17:23:57 +01:00
Nico Schottelius
8c6e4eee00 -- merge conflict 2020-02-23 17:20:28 +01:00
Nico Schottelius
2900844f63 Merge remote-tracking branch 'ahmed/master' 2020-02-23 17:17:08 +01:00
Nico Schottelius
46921c43ad update ldap, update syncvm 2020-02-23 17:11:05 +01:00
fa4d7a1d70 opennebula_hacks added i.e create one user and chown of vm 2020-02-23 21:00:18 +05:00
Nico Schottelius
edbfb7964e [ldap] bind with admin to get attributes 2020-02-23 16:52:30 +01:00
e4f2f446f5 Merge branch 'nico/meow-pay-master' into HEAD 2020-02-23 20:25:35 +05:00
Nico Schottelius
e2b5b5d102 opennebula -> router 2020-02-23 15:33:26 +01:00
7b09f0a373 abk-hacks added 2020-02-23 19:18:51 +05:00
Nico Schottelius
cee45b5227 -typo 2020-02-23 15:09:58 +01:00
Nico Schottelius
94633d6cc8 move uncloud a layer up
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-23 14:07:37 +01:00
Nico Schottelius
1d1ae6fb3e Force uniqueness on uuid 2020-02-23 11:59:09 +01:00
Nico Schottelius
f8c29aa1d6 add uuid() to opennebula VM 2020-02-23 11:55:57 +01:00
Nico Schottelius
fc4ec7b0f8 update readme + api 2020-02-23 11:42:15 +01:00
Nico Schottelius
f588691f0d [opennebula] add json, add helper functions 2020-02-23 11:42:03 +01:00
Nico Schottelius
581865460b Mess with migrations 2020-02-23 11:41:51 +01:00
Nico Schottelius
7f821b4d5a add readme 2020-02-23 10:31:28 +01:00
Nico Schottelius
ce0da4b827 + bracket 2020-02-23 09:44:55 +01:00
Nico Schottelius
de06b9ee22 Merge branch 'master' of code.ungleich.ch:nico/meow-pay 2020-02-23 09:18:27 +01:00
Nico Schottelius
26449d3159 ++snapshot ideas 2020-02-23 09:18:16 +01:00
71a764ce1e Move vm/{detail,list} under opennebula/vm/{detail,list} and make it admin accessible only + Created vm/list that list currently authenticated user's VMs 2020-02-22 15:49:00 +05:00
5f28e9630c Remove unneccessary requirements from {repo_root}/requirements.txt + uncloud/secret_sample.py minor changes 2020-02-22 11:36:18 +05:00
Ahmed Bilal
2aa22803f4 Merge branch 'master' into 'master'
Merge nico/meow-pay into ahmedbilal/meow-pay

See merge request ahmedbilal/meow-pay!2
2020-02-22 07:32:52 +01:00
Ahmed Bilal
dc34c0ecd4 Merge nico/meow-pay into ahmedbilal/meow-pay 2020-02-22 07:32:52 +01:00
Nico Schottelius
4f4a4be839 good night commit - introducing status 2020-02-22 00:50:06 +01:00
Nico Schottelius
b1bb6bc314 Make products available via getattr 2020-02-22 00:22:42 +01:00
Nico Schottelius
b67f41cc35 Merge remote-tracking branch 'ahmed/master' 2020-02-21 22:04:26 +01:00
Nico Schottelius
dc5092be71 Add sample secrets 2020-02-21 21:41:51 +01:00
Nico Schottelius
4df7c761d3 ++stuff 2020-02-21 20:51:04 +01:00
d61a7e670f opennebula vm sync/query application added 2020-02-21 20:33:37 +05:00
5de973b204 Merge branch 'nico/meow-pay-master' into HEAD 2020-02-21 20:06:21 +05:00
Nico Schottelius
0708a1e1fd add requirements.txt 2020-02-21 15:05:17 +01:00
Nico Schottelius
a5695ffa48 two more related user problems
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-21 11:43:17 +01:00
Nico Schottelius
6ba224638a fix migrations / custom user late introduce
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-21 11:42:54 +01:00
Nico Schottelius
2cda4dd57b [auth] add customer user model
Best practice

See

https://docs.djangoproject.com/en/3.0/topics/auth/customizing/#using-a-custom-user-model-when-starting-a-project
2020-02-21 11:32:41 +01:00
Nico Schottelius
c456355059 begin to introduce product
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-21 10:41:22 +01:00
Nico Schottelius
118c66799c ++views/permissions 2020-02-20 19:38:43 +01:00
Nico Schottelius
f8182e00e8 import secrets 2020-02-20 19:38:30 +01:00
Nico Schottelius
9fd445e947 add ldap support + tutorial example 2020-02-20 18:58:07 +01:00
Nico Schottelius
254429db55 .gitignore & more 2020-02-20 16:55:01 +01:00
Nico Schottelius
e472d20ae0 hacking uncloud v202002
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-20 16:52:50 +01:00
Nico Schottelius
315aaded41 Focus on creating a VPN as a first test case 2020-02-20 16:05:58 +01:00
Nico Schottelius
13292db39e +old notes 2020-02-20 11:57:03 +01:00
Nico Schottelius
0a1ccadda2 +ldaptest 2020-02-20 11:56:47 +01:00
Nico Schottelius
8160d01471 Merge remote-tracking branch 'origin/master' 2020-02-20 11:51:14 +01:00
8c353f277c is_order_valid added in helper.py 2020-02-20 15:23:15 +05:00
bb18f6b0e9 Flask-RESTful added in requirements.txt 2020-02-20 14:08:39 +05:00
00b35e0567 cleaned requirements.txt 2020-02-20 14:04:53 +05:00
9c7d458eec use code from ungleich-common 2020-02-20 13:57:32 +05:00
Nico Schottelius
074efffaa7 ++ hack 2020-02-20 09:44:30 +01:00
cee92f2e99 A lot of code moved to ungleich-common 2020-02-20 00:12:11 +05:00
ce709c3b6f Add certificates option for etcd 2020-02-19 14:44:19 +05:00
519279ce6f Update README.md 2020-02-19 13:13:39 +05:00
7b9a970307 Update README.md 2020-02-19 13:12:46 +05:00
5f1f451bc2 Added installation and getting started instructions in README.md 2020-02-19 13:12:07 +05:00
e37592bdc6 README.md updated and reorganized, Improved error handling for configparser and ldap manager, requirements.txt added 2020-02-19 11:59:54 +05:00
347843cb24 Sample config file added + uncloud dependency removed 2020-02-19 10:22:15 +05:00
Nico Schottelius
aa9548e753 +gitignore
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-15 11:15:26 +01:00
Nico Schottelius
c1f384fb9a so many notes&hacks! 2020-02-15 09:38:33 +01:00
Nico Schottelius
b38c9b6060 Ad capability to add and list hosts 2020-02-09 19:27:24 +01:00
Nico Schottelius
a80a279ba5 Add filtering support:
(venv) [12:54] diamond:uncloud% ./bin/uncloud-run-reinstall hack --product 'dualstack-vm' --os-image-name alpine311 --username nicocustomer --password '...' --hackprefix ~/vcs/uncloud/uncloud/hack/hackcloud/ --etcd-host etcd1.ungleich.ch --etcd-ca-cert ~/vcs/ungleich-dot-cdist/files/etcd/ca.pem --etcd-cert-cert ~/vcs/ungleich-dot-cdist/files/etcd/nico.pem --etcd-cert-key ~/vcs/ungleich-dot-cdist/files/etcd/nico-key.pem --list-orders --filter-order-key "status" --filter-order-regexp NEW
2020-02-09 12:54:52 +01:00
Nico Schottelius
5ef009cc9b Begin to phase in features and processing orders 2020-02-09 12:12:15 +01:00
Nico Schottelius
5da6dbb32e ++hack / list products
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2020-02-09 11:14:50 +01:00
Nico Schottelius
3b508fc87d phase in notion of a product 2020-02-09 09:36:50 +01:00
Nico Schottelius
55a2de72c8 [hack] begin to add ldap authentication 2020-02-09 08:51:35 +01:00
Nico Schottelius
f99d0a0b64 [requirements] add ldap3 2020-02-09 08:43:56 +01:00
Nico Schottelius
d9a756b50e Catch filenotfound errors when launching etcd 2020-02-06 15:33:01 +01:00
Nico Schottelius
592b745cea exit if an exception happened 2020-02-06 15:32:48 +01:00
Nico Schottelius
aaf0114df1 add image format option 2020-02-06 15:13:08 +01:00
0e667b5262 Fix UUID variable in oneshot/vm/get_name 2020-01-30 09:00:28 +01:00
f2337a14eb Yet another forgotten CLI parameter in oneshot... 2020-01-30 08:55:56 +01:00
8797e93baf Fix --name support in oneshot 2020-01-30 08:54:58 +01:00
9e2751c41e Remove deplicate vm definition in oneshot --stop 2020-01-30 08:52:24 +01:00
17d0c61407 Fix --accel parameter for oneshot 2020-01-30 08:47:23 +01:00
Nico Schottelius
3171ab8ccb [hack/vm] add self.vm dict 2020-01-29 19:55:55 +01:00
Nico Schottelius
56565ac7f7 Fix AttributeError: 'VM' object has no attribute 'vm'
ERROR:uncloud.vmm:Error occurred while starting VM.
Detail qemu-system-x86_64: -drive file=/home/nico/vcs/uncloud/uncloud/hack/hackcloud/alpine-virt-3.11.2-x86_64.iso,format=qcow2,if=virtio: Image is not in qcow2 format
Traceback (most recent call last):
  File "/home/nico/vcs/uncloud/uncloud/vmm/__init__.py", line 186, in start
    sp.check_output(command, stderr=sp.PIPE)
  File "/usr/lib/python3.8/subprocess.py", line 411, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
  File "/usr/lib/python3.8/subprocess.py", line 512, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['sudo', '-p', 'Enter password to start VM 87230168-1b74-49f7-97c3-c968a26fc65e: ', '/usr/bin/qemu-system-x86_64', '-name', 'uncoud-87230168-1b74-49f7-97c3-c968a26fc65e', '-machine', 'pc,accel=kvm', '-drive', 'file=/home/nico/vcs/uncloud/uncloud/hack/hackcloud/alpine-virt-3.11.2-x86_64.iso,format=qcow2,if=virtio', '-device', 'virtio-rng-pci', '-m', '1024M', '-smp', 'cores=1,threads=1', '-netdev', 'tap,id=netmain,script=/home/nico/vcs/uncloud/uncloud/hack/hackcloud/ifup.sh,downscript=/home/nico/vcs/uncloud/uncloud/hack/hackcloud/ifdown.sh,ifname=uc000000000000', '-device', 'virtio-net-pci,netdev=netmain,id=net0,mac=42:00:00:00:00:01', '-qmp', 'unix:/home/nico/vcs/uncloud/uncloud/hack/hackcloud/sock/87230168-1b74-49f7-97c3-c968a26fc65e,server,nowait', '-vnc', 'unix:/tmp/tmpep71nz1f', '-daemonize']' returned non-zero exit status 1.
ERROR:root:'VM' object has no attribute 'vm'
Traceback (most recent call last):
  File "./bin/../scripts/uncloud", line 82, in <module>
    main(arguments)
  File "/home/nico/vcs/uncloud/uncloud/hack/main.py", line 47, in main
    vm.create()
  File "/home/nico/vcs/uncloud/uncloud/hack/vm.py", line 115, in create
    self.vm['mac'] = self.mac
AttributeError: 'VM' object has no attribute 'vm'
(venv) [18:49] diamond:uncloud% ./bin/uncloud-run-reinstall hack --create-vm --hackprefix ~/vcs/uncloud/uncloud/hack/hackcloud/ --image alpine-virt-3.11.2-x86_64.iso --no-db
2020-01-29 19:30:19 +01:00
Nico Schottelius
1b08a49aef Do not background dnsmasq 2020-01-29 18:45:50 +01:00
Dominique Roux
7e36b0c067 Debugging pipeline 2020-01-29 17:25:29 +01:00
Dominique Roux
1ca2f8670d Wrote first unit tests 2020-01-29 17:15:34 +01:00
Dominique Roux
d8a465bca4 Changed Exception in MAC class 2020-01-29 17:06:54 +01:00
Dominique Roux
dfa4e16806 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-29 17:06:14 +01:00
Dominique Roux
bdbf26cfd4 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-29 17:06:11 +01:00
Dominique Roux
5969d3b13d accessed the mac class with the correct function 2020-01-29 17:04:59 +01:00
3e69fb275f Oneshot: cleanup CLI, initial networking support 2020-01-29 10:08:15 +01:00
618fecb73f Initial implementation (no networking) of uncloud-oneshot 2020-01-28 16:30:43 +01:00
e2cd44826b Fix typo in hack/vm.py 2020-01-28 13:45:20 +01:00
1758629ca1 Add minimal doc to hack/vm.py 2020-01-28 12:33:36 +01:00
a759b8aa39 VMM: make use of socket_dir 2020-01-28 12:24:26 +01:00
4c6a126d8b Hack/VM: wire get_vnc and list_vms 2020-01-28 11:02:18 +01:00
2b71c1807d Wire uncloud-hack vm module to VMM 2020-01-28 09:25:25 +01:00
200a7672f2 make value_in_json=True 2020-01-27 14:55:26 +05:00
1a76d2b5f3 Many more changes 2020-01-27 13:40:57 +05:00
cbcaf63650 Update VM images documentation (upstream images, uncloud-init) 2020-01-26 12:04:37 +01:00
Dominique Roux
5d05e91335 added hackerprefix argument, changed the commandline structure of vm to work better with sudo 2020-01-24 17:12:50 +01:00
Dominique Roux
8cc58726d0 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-24 14:34:36 +01:00
Dominique Roux
5711bf4770 bugfixes in vm 2020-01-24 14:34:34 +01:00
Nico Schottelius
ae3482cc71 Fix and break some VM stuff 2020-01-24 14:21:38 +01:00
Nico Schottelius
b1319d654a Make me and Dominique happy (aka add vxlan to bridge) 2020-01-24 14:15:48 +01:00
Nico Schottelius
93d7a409b1 Fix Dominique's sudo bug
Totally not related to my previous commit
2020-01-24 14:10:49 +01:00
Nico Schottelius
7e91f60c0a sudo fix 2020-01-24 14:10:08 +01:00
Dominique Roux
58daf8191e refactored vm.py to create a VM 2020-01-24 13:56:08 +01:00
Nico Schottelius
b5409552d8 prepare vm.py for dominique 2020-01-23 21:20:16 +01:00
Dominique Roux
550937630c Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-23 21:17:30 +01:00
Dominique Roux
46a04048b5 small changes in vm.py to make it more generic 2020-01-23 21:17:09 +01:00
Nico Schottelius
3ddd27a08d Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-23 21:16:10 +01:00
Nico Schottelius
c881c7ce4d hack mac: be a proper python class 2020-01-23 21:15:26 +01:00
Dominique Roux
d5a7f8ef59 Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-23 18:43:42 +01:00
Dominique Roux
0982927c1b Added DNSmasq ability for RA 2020-01-23 18:43:41 +01:00
Nico Schottelius
8e839aeb44 commit stuff before dominique does 2020-01-23 18:41:59 +01:00
da54a59ca2 initial commit 2020-01-20 12:30:12 +05:00
Nico Schottelius
8888f5d9f7 add logging 2020-01-19 12:55:06 +01:00
Nico Schottelius
bd9dbb12b7 Cleanup networking 2020-01-19 11:30:41 +01:00
Nico Schottelius
30be791312 Be less verbose when reinstalling 2020-01-19 11:30:30 +01:00
Nico Schottelius
2b8831784a [pep440] improve versioning name for python 2020-01-19 11:30:16 +01:00
Nico Schottelius
b847260768 ++network 2020-01-19 09:16:29 +01:00
Nico Schottelius
1b5a3f6d2e Progress with networking 2020-01-15 13:26:05 +01:00
Nico Schottelius
8a451ff4ff [hack] phase in networking 2020-01-15 12:40:37 +01:00
Nico Schottelius
bd03f95e99 [docs] move one level higher 2020-01-15 11:32:23 +01:00
Nico Schottelius
26d5c91625 Update hacking docs 2020-01-15 10:53:22 +01:00
Nico Schottelius
b877ab13b3 add hack code 2020-01-15 10:02:37 +01:00
Nico Schottelius
12e8ccd01c Cleanups for mac handling 2020-01-14 19:10:59 +01:00
Nico Schottelius
8078ffae5a Add working --last-used-mac
{'create_vm': False, 'last_used_mac': True, 'get_new_mac': False, 'debug': False, 'conf_dir': '/home/nico/uncloud', 'etcd_host': 'etcd1.ungleich.ch', 'etcd_port': None, 'etcd_ca_cert': '/home/nico/vcs/ungleich-dot-cdist/files/etcd/ca.pem', 'etcd_cert_cert': '/home/nico/vcs/ungleich-dot-cdist/files/etcd/nico.pem', 'etcd_cert_key': '/home/nico/vcs/ungleich-dot-cdist/files/etcd/nico-key.pem'}
00:20:00:00:00:00
(venv) [19:02] diamond:uncloud% ./bin/uncloud-run-reinstall hack  --etcd-host etcd1.ungleich.ch --etcd-ca-cert /home/nico/vcs/ungleich-dot-cdist/files/etcd/ca.pem --etcd-cert-cert /home/nico/vcs/ungleich-dot-cdist/files/etcd/nico.pem --etcd-cert-key /home/nico/vcs/ungleich-dot-cdist/files/etcd/nico-key.pem --last-used-mac
2020-01-14 19:02:15 +01:00
Nico Schottelius
1b36c2f96f Write VM to etcd 2020-01-14 14:23:26 +01:00
Nico Schottelius
c0e6d6a0d8 Begin further integration of code into hack 2020-01-14 11:25:06 +01:00
Nico Schottelius
083ba43918 Integrate hack + vm create into python code 2020-01-14 11:22:04 +01:00
Nico Schottelius
22531a7459 Disable cli / otp reading for the moment
Imho this should clearly not leak into scripts/uncloud and
additionally it is broken at the moment
2020-01-14 11:09:45 +01:00
Nico Schottelius
b96e56b453 Begin to integrate hack into the main script 2020-01-14 11:05:42 +01:00
Nico Schottelius
9f02b31b1b Add hacky etcd client 2020-01-13 12:54:02 +01:00
Nico Schottelius
10c8dc85ba Begin hacky database handling 2020-01-13 12:14:30 +01:00
Nico Schottelius
091131d350 dummy 2020-01-13 11:52:40 +01:00
Ahmed Bilal
ab65349047 Merge branch 'conf-dir' into 'master'
Adding conf-dir and etcd-* arguments to command-line-interface

See merge request uncloud/uncloud!2
2020-01-13 05:57:42 +01:00
Ahmed Bilal
c3b42aabc6 Added --conf-dir, --etcd-{host,port,ca_cert,cert_cert,cert_key} parameters to cli and settings is now accessbile through uncloud.shared.shared.settings 2020-01-13 05:57:41 +01:00
Nico Schottelius
e6d22a73c5 ++ cleanup 2020-01-12 14:44:53 +01:00
Nico Schottelius
02526baaf9 add ifdown support 2020-01-12 14:43:06 +01:00
Nico Schottelius
3188787c2a ++mac change 2020-01-12 14:38:01 +01:00
Nico Schottelius
94dad7c9b6 Add script to generate mac addresses 2020-01-12 14:35:59 +01:00
Nico Schottelius
53c6a14d60 mac: begin to downstrip 2020-01-12 14:03:04 +01:00
Nico Schottelius
64ab011299 import mac.py from cinv 2020-01-12 13:41:54 +01:00
Nico Schottelius
b017df4879 ignore iso, update nft rules 2020-01-12 13:20:38 +01:00
Nico Schottelius
aaf29adcbb + mac prefix 2020-01-12 00:41:31 +01:00
Nico Schottelius
6d51e2a8c4 [metadata] change default port to 1234 2020-01-12 00:32:17 +01:00
Nico Schottelius
c6b7152464 update nftrules example 2020-01-11 21:21:30 +01:00
Nico Schottelius
8544df8bad don't use tcg 2020-01-11 16:36:41 +01:00
Nico Schottelius
708e3ebb97 cleanup ifup.sh 2020-01-11 16:20:29 +01:00
Nico Schottelius
3b68a589d4 cleanup vm.sh 2020-01-11 16:17:35 +01:00
Nico Schottelius
029ef36d62 net +debug 2020-01-11 15:54:19 +01:00
Nico Schottelius
3cf4807f7c Merge branch 'master' of code.ungleich.ch:uncloud/uncloud
flush ruleset
2020-01-11 02:43:39 +01:00
Nico Schottelius
c1cabb7220 add working nft 2020-01-11 02:42:04 +01:00
Nico Schottelius
5d95f11b3d Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-11 00:24:25 +01:00
Nico Schottelius
23d805f04f ++stuff 2020-01-11 00:24:17 +01:00
Nico Schottelius
3825c7c210 Add vxlan into the bridge 2020-01-11 00:23:55 +01:00
Nico Schottelius
7c9e3d747a Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-11 00:06:29 +01:00
Nico Schottelius
b9c9a5e0ec add working network 2020-01-10 23:55:21 +01:00
Nico Schottelius
ebcb1680d7 add hack scripts 2020-01-10 23:27:21 +01:00
cf4930ee84 cli enabled again 2020-01-10 16:42:07 +05:00
00d876aea1 Do not break if client section/or OTP creds missing from conf file 2020-01-10 16:39:40 +05:00
Nico Schottelius
e91fd9e24a disable cli until bug #25 is fixed 2020-01-10 12:00:02 +01:00
Nico Schottelius
469d03467d Merge branch 'master' of code.ungleich.ch:uncloud/uncloud 2020-01-10 11:56:56 +01:00
Nico Schottelius
ec66a756a0 ++confdir 2020-01-10 11:56:47 +01:00
b4f47adb4f print message removed 2020-01-10 15:47:38 +05:00
31ec024be6 passing arguments dict to componenets instead of **kwargs 2020-01-10 15:45:48 +05:00
Nico Schottelius
82a69701ce catch etcd in scripts/ 2020-01-10 11:43:53 +01:00
Nico Schottelius
d9dd6b48dc No try: needed for pop/importlib/getattr 2020-01-10 11:35:04 +01:00
Nico Schottelius
b7596e071a begin phasing in arguments instead of **arguments 2020-01-10 11:30:23 +01:00
Nico Schottelius
71fd0ca7d9 Remove double try/except blocks (with wraps) 2020-01-10 11:00:00 +01:00
Nico Schottelius
92f985c857 Handle etcd connection error 2020-01-10 10:10:37 +01:00
Nico Schottelius
feb334cf04 Exit code == 1 in case we died with an exception 2020-01-10 10:07:01 +01:00
48efcdf08c 1. mp.set_start_method('spawn') commented out from scripts/uncloud
2. uncloud.shared moved under uncloud.common
3. Refactoring in etcd_wrapper e.g timeout mechanism removed and few other things
4. uncloud-{scheduler,host} now better handle etcd events in their block state (waiting for requests to come)
2020-01-09 00:40:05 +05:00
f8f790e7fc nested dict doesn't play well with configparser 2020-01-07 22:18:13 +05:00
5a646aeac9 prefix is renamed to base_prefix, uncloud now respects base_prefix and put things under it 2020-01-07 21:45:11 +05:00
6046015c3d Add base prefix option for uncloud so that we can run independent instance on uncloud 2020-01-07 20:26:10 +05:00
b4292615de Display more info about tracked files to user e.g creation_date, host on which it is stored, size etc 2020-01-07 18:27:22 +05:00
48cc37c438 add hostname to file entry (uncloud filescanner) 2020-01-07 17:57:44 +05:00
6086fec633 move settings under uncloud.common 2020-01-06 12:25:59 +05:00
Nico Schottelius
388127bd11 [hack] add scripts to start VM 2020-01-05 18:32:14 +01:00
ef0f13534a bug fixed that add extra space in QEMU command when there is no network to be attached 2020-01-05 21:59:24 +05:00
ec40d6b1e0 don't suppress error when changing permissions in uncloud vmm 2020-01-05 20:20:00 +05:00
b7f3ba1a34 remove cache=none from QEMU args as it is not supported on tmpfs/rootfs 2020-01-05 19:46:38 +05:00
6f51ddbb36 renamed argument, and changed destination and make it required (uncloud.cli.image.create_image_from_file) 2020-01-05 18:31:48 +05:00
7fff280c79 uncloud filescanner os.path.getsize expects str given Path instead 2020-01-05 18:00:05 +05:00
6847a0d323 base dir reverted back to str path 2020-01-05 17:56:42 +05:00
180f6f4989 No longer using xattrs as they don't work on tmpfs/rootfs 2020-01-05 17:21:26 +05:00
344a957a3f Removed duplicate add_help from argument parsers in cli/image and cli/network 2020-01-03 18:42:20 +05:00
3296e524cc uncloud cli converted to argparse 2020-01-03 18:38:59 +05:00
50fb135726 uncloud cli converted to argparse, code isn't beautiful yet. Would make it soom 2020-01-03 15:02:39 +05:00
cd2f0aaa0d Using click instead of argparse in uncloud script 2020-01-01 14:59:47 +05:00
2afb37daca get() methods converted to post() 2019-12-31 20:33:55 +05:00
Nico Schottelius
b95037f624 [metadata] allow passing in the port 2019-12-31 15:35:49 +01:00
Nico Schottelius
eb19b10333 [scheduler] partial debug support 2019-12-31 14:22:44 +01:00
Nico Schottelius
2566e86f1e [host] get ourselves from etcd 2019-12-31 14:13:08 +01:00
Nico Schottelius
e775570884 Make uncloud host running 2019-12-31 14:06:51 +01:00
Nico Schottelius
9662e02eb7 Allow to not have keys in etcd 2019-12-31 13:50:56 +01:00
Nico Schottelius
71c3f9d978 begin adding port support, catch OSError from Flask 2019-12-31 13:13:19 +01:00
Nico Schottelius
29dfacfadb Update .gitignore for uncloud 2019-12-31 12:15:50 +01:00
Nico Schottelius
bff12ed930 ++ exception handling 2019-12-31 12:15:05 +01:00
Nico Schottelius
1fba79ca31 remove syslog handler (cruft), add debug flag 2019-12-31 11:56:28 +01:00
Nico Schottelius
4c7678618d Also fix setup.py and the configuration file 2019-12-31 11:37:52 +01:00
Nico Schottelius
6682f127f1 Remove colors, remove sophisticated logging
Go back to simple
2019-12-31 11:35:51 +01:00
Nico Schottelius
433a3b9817 refactor #2
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2019-12-31 11:30:02 +01:00
Nico Schottelius
7b6c02b3ab find ucloud -name \*.py -exec sed -i "s/ucloud/uncloud/g" {} \; 2019-12-31 11:29:08 +01:00
Nico Schottelius
70c8da544e [refactor] rename scripts to uncloud 2019-12-30 20:06:15 +01:00
6a40a7f12f sshtunnel, sphinx, sphinx-rtd-theme, werkzeug removed from dependencies 2019-12-30 23:22:00 +05:00
27e780b359 Remove unneccassary sudo from ssh forwarding command 2019-12-30 21:30:59 +05:00
4b7d6d5099 Bug fixed in migration code 2019-12-30 21:14:08 +05:00
d13a4bcc37 Remove pending vm handling mechanism from scheduler + fixed issue that update VM's hostname even on migration failure 2019-12-30 20:05:12 +05:00
d2d6c6bf5c Use UTC time for heartbeat mechanism 2019-12-30 15:30:26 +05:00
9963e9c62d Slow down heartbeat update 2019-12-30 15:18:25 +05:00
52867614df Remove unused code + Increase frequeuncy of host heartbeat update 2019-12-30 14:58:05 +05:00
9bdf4d2180 Shutdown Source VM (PAUSED) on successfull migration + blackened all .py files 2019-12-30 14:35:07 +05:00
29e938dc74 Destination Host of VM during migration now notify Source host of exact socket path 2019-12-29 23:48:04 +05:00
f980cdb464 Better error handling, Efforts to run non-root with occasional sudo 2019-12-29 23:14:39 +05:00
808271f3e0 Return nice message when etcd section is missing 2019-12-28 16:35:55 +05:00
ba515f0b48 Refactoring, VMM added, uncloud-host mostly new, migration is better now 2019-12-28 15:39:11 +05:00
cd9d4cb78c Fix bug that cause failure of image resizing when creating vm 2019-12-26 14:30:15 +05:00
ec3cf49799 Create radvd configuration and start it <--> VM's which is being started has IPv6 network which is global 2019-12-26 12:24:19 +05:00
f79097cae9 Fix logging 2019-12-24 15:27:21 +05:00
972bb5a920 - Better error reporting.
- Flask now uses application's logger instead of its own.
- ucloud file scanner refactored.
2019-12-23 12:58:04 +05:00
eea6c1568e colored error output 2019-12-22 13:47:16 +05:00
e4d2c98fb5 Better logging. Errors without stacktrace are now printed to stderr 2019-12-22 13:14:42 +05:00
88b4d34e1a workaround of setuptools bug that fails to install Flask without version 2019-12-22 12:33:59 +05:00
04993e4106 Refactoring, Removal of most global vars, config default path is ~/ucloud/ 2019-12-22 12:26:48 +05:00
bc58a6ed9c Configuration/Setting module added 2019-12-21 14:36:55 +05:00
71279a968f Fix issues in naming and few other things 2019-12-14 20:23:31 +05:00
Ahmed Bilal
f919719b1e Update ucloud.conf 2019-12-08 15:03:49 +01:00
Nico Schottelius
8afd524c55 [config] inline etcd3
to get things moving faster - cleanup later
2019-12-08 14:55:26 +01:00
Nico Schottelius
e79f1b4de9 ++notes 2019-12-08 14:22:56 +01:00
Nico Schottelius
23c7604a3e Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 14:20:36 +01:00
Nico Schottelius
8e159c8be1 add hacking template 2019-12-08 14:20:26 +01:00
Nico Schottelius
0283894ba2 remove non-unknown vars 2019-12-08 14:16:22 +01:00
Nico Schottelius
9206d8ed1d Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 14:15:40 +01:00
Nico Schottelius
5b44034602 cleanup 2019-12-08 14:15:36 +01:00
dee0a29c91 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 14:14:37 +01:00
8b90755015 removed unwanted file 2019-12-08 14:14:32 +01:00
bfbf08c7cd [conf] added unkown values 2019-12-08 14:11:44 +01:00
2a1e80dbc5 [imagescanner] main.py refactored from env_vars to config 2019-12-08 14:11:19 +01:00
Nico Schottelius
26aeb78f61 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 14:08:42 +01:00
Nico Schottelius
9ec9083c57 conf update
Signed-off-by: Nico Schottelius <nico@nico-notebook.schottelius.org>
2019-12-08 14:08:40 +01:00
c6fe2cb1c4 [host] virtualmachine.py refactored from env_vars to config 2019-12-08 14:06:15 +01:00
1e70d0183d [conf] added ssh dictionary 2019-12-08 14:05:38 +01:00
34a6e99525 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 14:04:50 +01:00
fee1cfd4ff [conf] added ssh dictionary 2019-12-08 14:01:41 +01:00
Nico Schottelius
b2de277244 [conf] add values for filescanner 2019-12-08 13:51:52 +01:00
Nico Schottelius
00563c7dc2 [filescanner] use configparser 2019-12-08 13:51:40 +01:00
b235f0833c Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:46:40 +01:00
Nico Schottelius
42bb7bc609 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:46:57 +01:00
012f3cb3b5 [conf] added storage dictionary 2019-12-08 13:46:33 +01:00
Nico Schottelius
0d38a66a34 add a wrapper to re install ucloud and then run it 2019-12-08 13:46:24 +01:00
Nico Schottelius
72af426b3a update config x2 2019-12-08 13:41:42 +01:00
871aa5347b : 2019-12-08 13:41:37 +01:00
608d1eb280 [host] main.py refactored from env_vars to config 2019-12-08 13:41:32 +01:00
Nico Schottelius
537a5b01f1 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:36:49 +01:00
Nico Schottelius
c37bf19f92 ++conf 2019-12-08 13:36:45 +01:00
Nico Schottelius
431a6f6d9b [metadata] -> configparser 2019-12-08 13:32:06 +01:00
6c56a7a7c6 Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:32:01 +01:00
dd33b89941 [scheduler] helper.py refactored from env_vars to config 2019-12-08 13:31:56 +01:00
Nico Schottelius
cff6a4021f Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:29:35 +01:00
Nico Schottelius
76f63633ca [api] done -> configparser 2019-12-08 13:29:24 +01:00
Nico Schottelius
adddba518c Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:25:42 +01:00
787b236305 fixed brackets 2019-12-08 13:25:03 +01:00
c1b0c5301e Merge branch 'master' of code.ungleich.ch:ucloud/ucloud 2019-12-08 13:23:32 +01:00
7486fafbaa [scheduler] refactored from env_vars to config 2019-12-08 13:23:26 +01:00
Nico Schottelius
cdbfb96e71 [api] config updates and add default values 2019-12-08 13:09:52 +01:00
Nico Schottelius
a4bedb01f6 [api] begin to move to configparser 2019-12-08 13:00:42 +01:00
Nico Schottelius
6d0ce65f5c begin to switch to configparser
To not have unwanted environment influence
2019-12-08 12:59:18 +01:00
Nico Schottelius
e459434b91 add sample ucloud.conf 2019-12-08 12:58:26 +01:00
Nico Schottelius
cfb09c29de simplify main script 2019-12-08 12:28:25 +01:00
Nico Schottelius
9517e73233 Migrate sanity_check.py into the respective daemons 2019-12-07 14:25:21 +01:00
Nico Schottelius
f9dbdc730a Remove logging configuration
Leave it to the OS/env to set this up.

Fixes #6
2019-12-07 14:15:48 +01:00
Nico Schottelius
2244b94fd8 Fix another UndefinedValueError: VM_DIR
decouple.UndefinedValueError: VM_DIR not found. Declare it as envvar or define a default value.
2019-12-07 14:10:16 +01:00
Nico Schottelius
9ae75f20e8 Generate version from git
Fixes #3
2019-12-07 14:01:44 +01:00
Nico Schottelius
6d715e8348 [config] setup default values to remove startup failures 2019-12-07 13:51:50 +01:00
345 changed files with 22373 additions and 2762 deletions

19
.gitignore vendored
View file

@ -1,18 +1,27 @@
.idea
.vscode
.idea/
.vscode/
__pycache__/
pay.conf
log.txt
test.py
STRIPE
venv/
ucloud/docs/build
uncloud/docs/build
logs.txt
ucloud.egg-info
uncloud.egg-info
# run artefacts
default.etcd
__pycache__
# build artefacts
ucloud/version.py
uncloud/version.py
build/
venv/
dist/
.history/
*.iso
*.sqlite3

18
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,18 @@
stages:
- lint
- test
run-tests:
stage: test
image: code.ungleich.ch:5050/uncloud/uncloud/uncloud-ci:latest
services:
- postgres:latest
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

View file

@ -1,3 +1,70 @@
# ucloud
# Uncloud
Checkout https://ungleich.ch/ucloud/ for the documentation of 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`

6
archive/issues.org Normal file
View 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

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

View file

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

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

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

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

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

View 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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View 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

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

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

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

View 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

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

View 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

View file

@ -0,0 +1,6 @@
* TODO register CC
* TODO list products
* ahmed
** schemas
*** field: is_valid? - used by schemas
*** definition of a "schema"

View file

@ -0,0 +1,4 @@
db.sqlite3
uncloud/secrets.py
debug.log
uncloud/local_settings.py

View file

@ -0,0 +1,29 @@
#!/bin/sh
# -*- coding: utf-8 -*-
#
# 2019-2020 Nico Schottelius (nico-uncloud at schottelius.org)
#
# This file is part of uncloud.
#
# uncloud is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# uncloud is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
#
#
# Wrapper for real script to allow execution from checkout
dir=${0%/*}
# Ensure version is present - the bundled/shipped version contains a static version,
# the git version contains a dynamic version
printf "VERSION = \"%s\"\n" "$(git describe --tags --abbrev=0)" > ${dir}/../uncloud/version.py

View file

@ -25,9 +25,9 @@ dir=${0%/*}
# Ensure version is present - the bundled/shipped version contains a static version,
# the git version contains a dynamic version
printf "VERSION = \"%s\"\n" "$(git describe)" > ${dir}/../ucloud/version.py
${dir}/gen-version
libdir=$(cd "${dir}/../" && pwd -P)
export PYTHONPATH="${libdir}"
"$dir/../scripts/ucloud" "$@"
"$dir/../scripts/uncloud" "$@"

View file

@ -0,0 +1,29 @@
#!/bin/sh
# -*- coding: utf-8 -*-
#
# 2012-2019 Nico Schottelius (nico-ucloud at schottelius.org)
#
# This file is part of ucloud.
#
# ucloud is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# ucloud is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with ucloud. If not, see <http://www.gnu.org/licenses/>.
#
#
# Wrapper for real script to allow execution from checkout
dir=${0%/*}
${dir}/gen-version;
pip uninstall -y uncloud >/dev/null
python setup.py install >/dev/null
${dir}/uncloud "$@"

View file

@ -0,0 +1,13 @@
[etcd]
url = localhost
port = 2379
base_prefix = /
ca_cert
cert_cert
cert_key
[client]
name = replace_me
realm = replace_me
seed = replace_me
api_server = http://localhost:5000

View file

@ -7,7 +7,7 @@ SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source/
BUILDDIR = build/
DESTINATION=root@staticweb.ungleich.ch:/home/services/www/ungleichstatic/staticcms.ungleich.ch/www/ucloud/
DESTINATION=root@staticweb.ungleich.ch:/home/services/www/ungleichstatic/staticcms.ungleich.ch/www/uncloud/
.PHONY: all build clean

View file

@ -0,0 +1,12 @@
# uncloud docs
## Requirements
1. Python3
2. Sphinx
## Usage
Run `make build` to build docs.
Run `make clean` to remove build directory.
Run `make publish` to push build dir to https://ungleich.ch/ucloud/

View file

@ -56,40 +56,13 @@ To start host we created earlier, execute the following command
ucloud host ungleich.ch
Create OS Image
---------------
File & image scanners
--------------------------
Create ucloud-init ready OS image (Optional)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This step is optional if you just want to test ucloud. However, sooner or later
you want to create OS images with ucloud-init to properly
contexualize VMs.
1. Start a VM with OS image on which you want to install ucloud-init
2. Execute the following command on the started VM
.. code-block:: sh
apk add git
git clone https://code.ungleich.ch/ucloud/ucloud-init.git
cd ucloud-init
sh ./install.sh
3. Congratulations. Your image is now ucloud-init ready.
Upload Sample OS Image
~~~~~~~~~~~~~~~~~~~~~~
Execute the following to get the sample OS image file.
.. code-block:: sh
mkdir /var/www/admin
(cd /var/www/admin && wget https://cloud.ungleich.ch/s/qTb5dFYW5ii8KsD/download)
Run File Scanner and Image Scanner
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Currently, our uploaded file *alpine-untouched.qcow2* is not tracked by ucloud. We can only make
images from tracked files. So, we need to track the file by running File Scanner
Let's assume we have uploaded an *alpine-uploaded.qcow2* disk images to our
uncloud server. Currently, our *alpine-untouched.qcow2* is not tracked by
ucloud. We can only make images from tracked files. So, we need to track the
file by running File Scanner
.. code-block:: sh

View file

@ -17,9 +17,9 @@
# -- Project information -----------------------------------------------------
project = 'ucloud'
copyright = '2019, ungleich'
author = 'ungleich'
project = "uncloud"
copyright = "2019, ungleich"
author = "ungleich"
# -- General configuration ---------------------------------------------------
@ -27,12 +27,12 @@ author = 'ungleich'
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinx_rtd_theme',
"sphinx.ext.autodoc",
"sphinx_rtd_theme",
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
@ -50,4 +50,4 @@ html_theme = "sphinx_rtd_theme"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]

View file

@ -0,0 +1,36 @@
Hacking
=======
Using uncloud in hacking (aka development) mode.
Get the code
------------
.. code-block:: sh
:linenos:
git clone https://code.ungleich.ch/uncloud/uncloud.git
Install python requirements
---------------------------
You need to have python3 installed.
.. code-block:: sh
:linenos:
cd uncloud!
python -m venv venv
. ./venv/bin/activate
./bin/uncloud-run-reinstall
Install os requirements
-----------------------
Install the following software packages: **dnsmasq**.
If you already have a working IPv6 SLAAC and DNS setup,
this step can be skipped.
Note that you need at least one /64 IPv6 network to run uncloud.

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

View file

@ -11,12 +11,12 @@ Welcome to ucloud's documentation!
:caption: Contents:
introduction
user-guide
setup-install
vm-images
user-guide
admin-guide
user-guide/how-to-create-an-os-image-for-ucloud
troubleshooting
hacking
Indices and tables
==================

View file

@ -0,0 +1,66 @@
VM images
==================================
Overview
---------
ucloud tries to be least invasise towards VMs and only require
strictly necessary changes for running in a virtualised
environment. This includes configurations for:
* Configuring the network
* Managing access via ssh keys
* Resizing the attached disk(s)
Upstream images
---------------
The 'official' uncloud images are defined in the `uncloud/images
<https://code.ungleich.ch/uncloud/images>`_ repository.
How to make you own Uncloud images
----------------------------------
.. note::
It is fairly easy to create your own images for uncloud, as the common
operations (which are detailed below) can be automatically handled by the
`uncloud/uncloud-init <https://code.ungleich.ch/uncloud/uncloud-init>`_ tool.
Network configuration
~~~~~~~~~~~~~~~~~~~~~
All VMs in ucloud are required to support IPv6. The primary network
configuration is always done using SLAAC. A VM thus needs only to be
configured to
* accept router advertisements on all network interfaces
* use the router advertisements to configure the network interfaces
* accept the DNS entries from the router advertisements
Configuring SSH keys
~~~~~~~~~~~~~~~~~~~~
To be able to access the VM, ucloud support provisioning SSH keys.
To accept ssh keys in your VM, request the URL
*http://metadata/ssh_keys*. Add the content to the appropriate user's
**authorized_keys** file. Below you find sample code to accomplish
this task:
.. code-block:: sh
tmp=$(mktemp)
curl -s http://metadata/ssk_keys > "$tmp"
touch ~/.ssh/authorized_keys # ensure it exists
cat ~/.ssh/authorized_keys >> "$tmp"
sort "$tmp" | uniq > ~/.ssh/authorized_keys
Disk resize
~~~~~~~~~~~
In virtualised environments, the disk sizes might grow. The operating
system should detect disks that are bigger than the existing partition
table and resize accordingly. This task is os specific.
ucloud does not support shrinking disks due to the complexity and
intra OS dependencies.

View file

@ -0,0 +1,89 @@
#!/usr/bin/env python3
import logging
import sys
import importlib
import argparse
import os
from etcd3.exceptions import ConnectionFailedError
from uncloud.common import settings
from uncloud import UncloudException
from uncloud.common.cli import resolve_otp_credentials
# Components that use etcd
ETCD_COMPONENTS = ['api', 'scheduler', 'host', 'filescanner',
'imagescanner', 'metadata', 'configure', 'hack']
ALL_COMPONENTS = ETCD_COMPONENTS.copy()
ALL_COMPONENTS.append('oneshot')
#ALL_COMPONENTS.append('cli')
if __name__ == '__main__':
arg_parser = argparse.ArgumentParser()
subparsers = arg_parser.add_subparsers(dest='command')
parent_parser = argparse.ArgumentParser(add_help=False)
parent_parser.add_argument('--debug', '-d', action='store_true', default=False,
help='More verbose logging')
parent_parser.add_argument('--conf-dir', '-c', help='Configuration directory',
default=os.path.expanduser('~/uncloud'))
etcd_parser = argparse.ArgumentParser(add_help=False)
etcd_parser.add_argument('--etcd-host')
etcd_parser.add_argument('--etcd-port')
etcd_parser.add_argument('--etcd-ca-cert', help='CA that signed the etcd certificate')
etcd_parser.add_argument('--etcd-cert-cert', help='Path to client certificate')
etcd_parser.add_argument('--etcd-cert-key', help='Path to client certificate key')
for component in ALL_COMPONENTS:
mod = importlib.import_module('uncloud.{}.main'.format(component))
parser = getattr(mod, 'arg_parser')
if component in ETCD_COMPONENTS:
subparsers.add_parser(name=parser.prog, parents=[parser, parent_parser, etcd_parser])
else:
subparsers.add_parser(name=parser.prog, parents=[parser, parent_parser])
arguments = vars(arg_parser.parse_args())
etcd_arguments = [key for key, value in arguments.items() if key.startswith('etcd_') and value]
etcd_arguments = {
'etcd': {
key.replace('etcd_', ''): arguments[key]
for key in etcd_arguments
}
}
if not arguments['command']:
arg_parser.print_help()
else:
# Initializing Settings and resolving otp_credentials
# It is neccessary to resolve_otp_credentials after argument parsing is done because
# previously we were reading config file which was fixed to ~/uncloud/uncloud.conf and
# providing the default values for --name, --realm and --seed arguments from the values
# we read from file. But, now we are asking user about where the config file lives. So,
# to providing default value is not possible before parsing arguments. So, we are doing
# it after..
# settings.settings = settings.Settings(arguments['conf_dir'], seed_value=etcd_arguments)
# resolve_otp_credentials(arguments)
name = arguments.pop('command')
mod = importlib.import_module('uncloud.{}.main'.format(name))
main = getattr(mod, 'main')
if arguments['debug']:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
log = logging.getLogger()
try:
main(arguments)
except UncloudException as err:
log.error(err)
sys.exit(1)
# except ConnectionFailedError as err:
# log.error('Cannot connect to etcd: {}'.format(err))
except Exception as err:
log.exception(err)

View file

@ -0,0 +1,51 @@
import os
from setuptools import setup, find_packages
with open("README.md", "r") as fh:
long_description = fh.read()
try:
import uncloud.version
version = uncloud.version.VERSION
except:
import subprocess
c = subprocess.check_output(["git", "describe"])
version = c.decode("utf-8").strip()
setup(
name="uncloud",
version=version,
description="uncloud cloud management",
url="https://code.ungleich.ch/uncloud/uncloud",
long_description=long_description,
long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 3 - Alpha",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Programming Language :: Python :: 3",
],
author="ungleich",
author_email="technik@ungleich.ch",
packages=find_packages(),
install_requires=[
"requests",
"Flask>=1.1.1",
"flask-restful",
"bitmath",
"pyotp",
"pynetbox",
"colorama",
"etcd3 @ https://github.com/kragniz/python-etcd3/tarball/master#egg=etcd3",
"marshmallow",
"ldap3"
],
scripts=["scripts/uncloud"],
data_files=[
(os.path.expanduser("~/uncloud/"), ["conf/uncloud.conf"])
],
zip_safe=False,
)

View file

@ -0,0 +1,37 @@
import unittest
from unittest.mock import Mock
from uncloud.hack.mac import MAC
from uncloud import UncloudException
class TestMacLocal(unittest.TestCase):
def setUp(self):
self.config = Mock()
self.config.arguments = {"no_db":True}
self.mac = MAC(self.config)
self.mac.create()
def testMacInt(self):
self.assertEqual(self.mac.__int__(), int("0x420000000001",0), "wrong first MAC index")
def testMacRepr(self):
self.assertEqual(self.mac.__repr__(), '420000000001', "wrong first MAC index")
def testMacStr(self):
self.assertEqual(self.mac.__str__(), '42:00:00:00:00:01', "wrong first MAC index")
def testValidationRaise(self):
with self.assertRaises(UncloudException):
self.mac.validate_mac("2")
def testValidation(self):
self.assertTrue(self.mac.validate_mac("42:00:00:00:00:01"), "Validation of a given MAC not working properly")
def testNextMAC(self):
self.mac.create()
self.assertEqual(self.mac.__repr__(), '420000000001', "wrong second MAC index")
self.assertEqual(self.mac.__int__(), int("0x420000000001",0), "wrong second MAC index")
self.assertEqual(self.mac.__str__(), '42:00:00:00:00:01', "wrong second MAC index")
if __name__ == '__main__':
unittest.main()

View file

@ -0,0 +1,2 @@
class UncloudException(Exception):
pass

View file

@ -1,6 +1,6 @@
import os
from ucloud.config import etcd_client, env_vars
from uncloud.common.shared import shared
class Optional:
@ -19,12 +19,16 @@ class Field:
def is_valid(self):
if self.value == KeyError:
self.add_error("'{}' field is a required field".format(self.name))
self.add_error(
"'{}' field is a required field".format(self.name)
)
else:
if isinstance(self.value, Optional):
pass
elif not isinstance(self.value, self.type):
self.add_error("Incorrect Type for '{}' field".format(self.name))
self.add_error(
"Incorrect Type for '{}' field".format(self.name)
)
else:
self.validation()
@ -48,6 +52,8 @@ class VmUUIDField(Field):
self.validation = self.vm_uuid_validation
def vm_uuid_validation(self):
r = etcd_client.get(os.path.join(env_vars.get('VM_PREFIX'), self.uuid))
r = shared.etcd_client.get(
os.path.join(shared.settings["etcd"]["vm_prefix"], self.uuid)
)
if not r:
self.add_error("VM with uuid {} does not exists".format(self.uuid))

View file

@ -0,0 +1,19 @@
import json
import os
from uuid import uuid4
from uncloud.common.shared import shared
data = {
'is_public': True,
'type': 'ceph',
'name': 'images',
'description': 'first ever public image-store',
'attributes': {'list': [], 'key': [], 'pool': 'images'},
}
shared.etcd_client.put(
os.path.join(shared.settings['etcd']['image_store_prefix'], uuid4().hex),
json.dumps(data),
)

View file

@ -1,48 +1,51 @@
import binascii
import ipaddress
import random
import subprocess as sp
import logging
import requests
from pyotp import TOTP
from ucloud.config import vm_pool, env_vars
from uncloud.common.shared import shared
logger = logging.getLogger(__name__)
def check_otp(name, realm, token):
try:
data = {
"auth_name": env_vars.get("AUTH_NAME"),
"auth_token": TOTP(env_vars.get("AUTH_SEED")).now(),
"auth_realm": env_vars.get("AUTH_REALM"),
"auth_name": shared.settings["otp"]["auth_name"],
"auth_token": TOTP(shared.settings["otp"]["auth_seed"]).now(),
"auth_realm": shared.settings["otp"]["auth_realm"],
"name": name,
"realm": realm,
"token": token,
}
except binascii.Error:
except binascii.Error as err:
logger.error(
"Cannot compute OTP for seed: {}".format(
shared.settings["otp"]["auth_seed"]
)
)
return 400
response = requests.post(
"{OTP_SERVER}{OTP_VERIFY_ENDPOINT}".format(
OTP_SERVER=env_vars.get("OTP_SERVER", ""),
OTP_VERIFY_ENDPOINT=env_vars.get("OTP_VERIFY_ENDPOINT", "verify/"),
),
json=data,
shared.settings["otp"]["verification_controller_url"], json=data
)
return response.status_code
def resolve_vm_name(name, owner):
"""Return UUID of Virtual Machine of name == name and owner == owner
Input: name of vm, owner of vm.
Output: uuid of vm if found otherwise None
"""
result = next(
filter(
lambda vm: vm.value["owner"] == owner and vm.value["name"] == name,
vm_pool.vms,
lambda vm: vm.value["owner"] == owner
and vm.value["name"] == name,
shared.vm_pool.vms,
),
None,
)
@ -54,7 +57,7 @@ def resolve_vm_name(name, owner):
def resolve_image_name(name, etcd_client):
"""Return image uuid given its name and its store
* If the provided name is not in correct format
i.e {store_name}:{image_name} return ValueError
* If no such image found then return KeyError
@ -70,26 +73,35 @@ def resolve_image_name(name, etcd_client):
"""
Examples, where it would work and where it would raise exception
"images:alpine" --> ["images", "alpine"]
"images" --> ["images"] it would raise Exception as non enough value to unpack
"images:alpine:meow" --> ["images", "alpine", "meow"] it would raise Exception
as too many values to unpack
"""
store_name, image_name = store_name_and_image_name
except Exception:
raise ValueError("Image name not in correct format i.e {store_name}:{image_name}")
raise ValueError(
"Image name not in correct format i.e {store_name}:{image_name}"
)
images = etcd_client.get_prefix(env_vars.get('IMAGE_PREFIX'), value_in_json=True)
images = etcd_client.get_prefix(
shared.settings["etcd"]["image_prefix"], value_in_json=True
)
# Try to find image with name == image_name and store_name == store_name
try:
image = next(filter(lambda im: im.value['name'] == image_name
and im.value['store_name'] == store_name, images))
image = next(
filter(
lambda im: im.value["name"] == image_name
and im.value["store_name"] == store_name,
images,
)
)
except StopIteration:
raise KeyError("No image with name {} found.".format(name))
else:
image_uuid = image.key.split('/')[-1]
image_uuid = image.key.split("/")[-1]
return image_uuid
@ -98,7 +110,7 @@ def random_bytes(num=6):
return [random.randrange(256) for _ in range(num)]
def generate_mac(uaa=False, multicast=False, oui=None, separator=':', byte_fmt='%02x'):
def generate_mac(uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"):
mac = random_bytes()
if oui:
if type(oui) == str:
@ -116,36 +128,6 @@ def generate_mac(uaa=False, multicast=False, oui=None, separator=':', byte_fmt='
return separator.join(byte_fmt % b for b in mac)
def get_ip_addr(mac_address, device):
"""Return IP address of a device provided its mac address / link local address
and the device with which it is connected.
For Example, if we call get_ip_addr(mac_address="52:54:00:12:34:56", device="br0")
the following two scenarios can happen
1. It would return None if we can't be able to find device whose mac_address is equal
to the arg:mac_address or the mentioned arg:device does not exists or the ip address
we found is local.
2. It would return ip_address of device whose mac_address is equal to arg:mac_address
and is connected/neighbor of arg:device
"""
try:
output = sp.check_output(['ip', '-6', 'neigh', 'show', 'dev', device], stderr=sp.PIPE)
except sp.CalledProcessError:
return None
else:
result = []
output = output.strip().decode("utf-8")
output = output.split("\n")
for entry in output:
entry = entry.split()
if entry:
ip = ipaddress.ip_address(entry[0])
mac = entry[2]
if ip.is_global and mac_address == mac:
result.append(ip)
return result
def mac2ipv6(mac, prefix):
# only accept MACs separated by a colon
parts = mac.split(":")
@ -158,8 +140,9 @@ def mac2ipv6(mac, prefix):
# format output
ipv6_parts = [str(0)] * 4
for i in range(0, len(parts), 2):
ipv6_parts.append("".join(parts[i:i + 2]))
ipv6_parts.append("".join(parts[i : i + 2]))
lower_part = ipaddress.IPv6Address(":".join(ipv6_parts))
prefix = ipaddress.IPv6Address(prefix)
return str(prefix + int(lower_part))

View file

@ -0,0 +1,600 @@
import json
import pynetbox
import logging
import argparse
from uuid import uuid4
from os.path import join as join_path
from flask import Flask, request
from flask_restful import Resource, Api
from werkzeug.exceptions import HTTPException
from uncloud.common.shared import shared
from uncloud.common import counters
from uncloud.common.vm import VMStatus
from uncloud.common.request import RequestEntry, RequestType
from uncloud.api import schemas
from uncloud.api.helper import generate_mac, mac2ipv6
from uncloud import UncloudException
logger = logging.getLogger(__name__)
app = Flask(__name__)
api = Api(app)
app.logger.handlers.clear()
arg_parser = argparse.ArgumentParser('api', add_help=False)
arg_parser.add_argument('--port', '-p')
@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
class CreateVM(Resource):
"""API Request to Handle Creation of VM"""
@staticmethod
def post():
data = request.json
validator = schemas.CreateVMSchema(data)
if validator.is_valid():
vm_uuid = uuid4().hex
vm_key = join_path(shared.settings['etcd']['vm_prefix'], vm_uuid)
specs = {
'cpu': validator.specs['cpu'],
'ram': validator.specs['ram'],
'os-ssd': validator.specs['os-ssd'],
'hdd': validator.specs['hdd'],
}
macs = [generate_mac() for _ in range(len(data['network']))]
tap_ids = [
counters.increment_etcd_counter(
shared.etcd_client, shared.settings['etcd']['tap_counter']
)
for _ in range(len(data['network']))
]
vm_entry = {
'name': data['vm_name'],
'owner': data['name'],
'owner_realm': data['realm'],
'specs': specs,
'hostname': '',
'status': VMStatus.stopped,
'image_uuid': validator.image_uuid,
'log': [],
'vnc_socket': '',
'network': list(zip(data['network'], macs, tap_ids)),
'metadata': {'ssh-keys': []},
'in_migration': False,
}
shared.etcd_client.put(vm_key, vm_entry, value_in_json=True)
# Create ScheduleVM Request
r = RequestEntry.from_scratch(
type=RequestType.ScheduleVM,
uuid=vm_uuid,
request_prefix=shared.settings['etcd']['request_prefix'],
)
shared.request_pool.put(r)
return {'message': 'VM Creation Queued'}, 200
return validator.get_errors(), 400
class VmStatus(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.VMStatusSchema(data)
if validator.is_valid():
vm = shared.vm_pool.get(
join_path(shared.settings['etcd']['vm_prefix'], data['uuid'])
)
vm_value = vm.value.copy()
vm_value['ip'] = []
for network_mac_and_tap in vm.network:
network_name, mac, tap = network_mac_and_tap
network = shared.etcd_client.get(
join_path(
shared.settings['etcd']['network_prefix'],
data['name'],
network_name,
),
value_in_json=True,
)
ipv6_addr = (
network.value.get('ipv6').split('::')[0] + '::'
)
vm_value['ip'].append(mac2ipv6(mac, ipv6_addr))
vm.value = vm_value
return vm.value
else:
return validator.get_errors(), 400
class CreateImage(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.CreateImageSchema(data)
if validator.is_valid():
file_entry = shared.etcd_client.get(
join_path(shared.settings['etcd']['file_prefix'], data['uuid'])
)
file_entry_value = json.loads(file_entry.value)
image_entry_json = {
'status': 'TO_BE_CREATED',
'owner': file_entry_value['owner'],
'filename': file_entry_value['filename'],
'name': data['name'],
'store_name': data['image_store'],
'visibility': 'public',
}
shared.etcd_client.put(
join_path(
shared.settings['etcd']['image_prefix'], data['uuid']
),
json.dumps(image_entry_json),
)
return {'message': 'Image queued for creation.'}
return validator.get_errors(), 400
class ListPublicImages(Resource):
@staticmethod
def get():
images = shared.etcd_client.get_prefix(
shared.settings['etcd']['image_prefix'], value_in_json=True
)
r = {'images': []}
for image in images:
image_key = '{}:{}'.format(
image.value['store_name'], image.value['name']
)
r['images'].append(
{'name': image_key, 'status': image.value['status']}
)
return r, 200
class VMAction(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.VmActionSchema(data)
if validator.is_valid():
vm_entry = shared.vm_pool.get(
join_path(shared.settings['etcd']['vm_prefix'], data['uuid'])
)
action = data['action']
if action == 'start':
action = 'schedule'
if action == 'delete' and vm_entry.hostname == '':
if shared.storage_handler.is_vm_image_exists(
vm_entry.uuid
):
r_status = shared.storage_handler.delete_vm_image(
vm_entry.uuid
)
if r_status:
shared.etcd_client.client.delete(vm_entry.key)
return {'message': 'VM successfully deleted'}
else:
logger.error(
'Some Error Occurred while deleting VM'
)
return {'message': 'VM deletion unsuccessfull'}
else:
shared.etcd_client.client.delete(vm_entry.key)
return {'message': 'VM successfully deleted'}
r = RequestEntry.from_scratch(
type='{}VM'.format(action.title()),
uuid=data['uuid'],
hostname=vm_entry.hostname,
request_prefix=shared.settings['etcd']['request_prefix'],
)
shared.request_pool.put(r)
return (
{'message': 'VM {} Queued'.format(action.title())},
200,
)
else:
return validator.get_errors(), 400
class VMMigration(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.VmMigrationSchema(data)
if validator.is_valid():
vm = shared.vm_pool.get(data['uuid'])
r = RequestEntry.from_scratch(
type=RequestType.InitVMMigration,
uuid=vm.uuid,
hostname=join_path(
shared.settings['etcd']['host_prefix'],
validator.destination.value,
),
request_prefix=shared.settings['etcd']['request_prefix'],
)
shared.request_pool.put(r)
return (
{'message': 'VM Migration Initialization Queued'},
200,
)
else:
return validator.get_errors(), 400
class ListUserVM(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.OTPSchema(data)
if validator.is_valid():
vms = shared.etcd_client.get_prefix(
shared.settings['etcd']['vm_prefix'], value_in_json=True
)
return_vms = []
user_vms = filter(
lambda v: v.value['owner'] == data['name'], vms
)
for vm in user_vms:
return_vms.append(
{
'name': vm.value['name'],
'vm_uuid': vm.key.split('/')[-1],
'specs': vm.value['specs'],
'status': vm.value['status'],
'hostname': vm.value['hostname'],
'vnc_socket': vm.value.get('vnc_socket', None),
}
)
if return_vms:
return {'message': return_vms}, 200
return {'message': 'No VM found'}, 404
else:
return validator.get_errors(), 400
class ListUserFiles(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.OTPSchema(data)
if validator.is_valid():
files = shared.etcd_client.get_prefix(
shared.settings['etcd']['file_prefix'], value_in_json=True
)
return_files = []
user_files = [f for f in files if f.value['owner'] == data['name']]
for file in user_files:
file_uuid = file.key.split('/')[-1]
file = file.value
file['uuid'] = file_uuid
file.pop('sha512sum', None)
file.pop('owner', None)
return_files.append(file)
return {'message': return_files}, 200
else:
return validator.get_errors(), 400
class CreateHost(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.CreateHostSchema(data)
if validator.is_valid():
host_key = join_path(
shared.settings['etcd']['host_prefix'], uuid4().hex
)
host_entry = {
'specs': data['specs'],
'hostname': data['hostname'],
'status': 'DEAD',
'last_heartbeat': '',
}
shared.etcd_client.put(
host_key, host_entry, value_in_json=True
)
return {'message': 'Host Created'}, 200
return validator.get_errors(), 400
class ListHost(Resource):
@staticmethod
def get():
hosts = shared.host_pool.hosts
r = {
host.key: {
'status': host.status,
'specs': host.specs,
'hostname': host.hostname,
}
for host in hosts
}
return r, 200
class GetSSHKeys(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.GetSSHSchema(data)
if validator.is_valid():
if not validator.key_name.value:
# {user_prefix}/{realm}/{name}/key/
etcd_key = join_path(
shared.settings['etcd']['user_prefix'],
data['realm'],
data['name'],
'key',
)
etcd_entry = shared.etcd_client.get_prefix(
etcd_key, value_in_json=True
)
keys = {
key.key.split('/')[-1]: key.value
for key in etcd_entry
}
return {'keys': keys}
else:
# {user_prefix}/{realm}/{name}/key/{key_name}
etcd_key = join_path(
shared.settings['etcd']['user_prefix'],
data['realm'],
data['name'],
'key',
data['key_name'],
)
etcd_entry = shared.etcd_client.get(
etcd_key, value_in_json=True
)
if etcd_entry:
return {
'keys': {
etcd_entry.key.split('/')[
-1
]: etcd_entry.value
}
}
else:
return {'keys': {}}
else:
return validator.get_errors(), 400
class AddSSHKey(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.AddSSHSchema(data)
if validator.is_valid():
# {user_prefix}/{realm}/{name}/key/{key_name}
etcd_key = join_path(
shared.settings['etcd']['user_prefix'],
data['realm'],
data['name'],
'key',
data['key_name'],
)
etcd_entry = shared.etcd_client.get(
etcd_key, value_in_json=True
)
if etcd_entry:
return {
'message': 'Key with name "{}" already exists'.format(
data['key_name']
)
}
else:
# Key Not Found. It implies user' haven't added any key yet.
shared.etcd_client.put(
etcd_key, data['key'], value_in_json=True
)
return {'message': 'Key added successfully'}
else:
return validator.get_errors(), 400
class RemoveSSHKey(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.RemoveSSHSchema(data)
if validator.is_valid():
# {user_prefix}/{realm}/{name}/key/{key_name}
etcd_key = join_path(
shared.settings['etcd']['user_prefix'],
data['realm'],
data['name'],
'key',
data['key_name'],
)
etcd_entry = shared.etcd_client.get(
etcd_key, value_in_json=True
)
if etcd_entry:
shared.etcd_client.client.delete(etcd_key)
return {'message': 'Key successfully removed.'}
else:
return {
'message': 'No Key with name "{}" Exists at all.'.format(
data['key_name']
)
}
else:
return validator.get_errors(), 400
class CreateNetwork(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.CreateNetwork(data)
if validator.is_valid():
network_entry = {
'id': counters.increment_etcd_counter(
shared.etcd_client, shared.settings['etcd']['vxlan_counter']
),
'type': data['type'],
}
if validator.user.value:
try:
nb = pynetbox.api(
url=shared.settings['netbox']['url'],
token=shared.settings['netbox']['token'],
)
nb_prefix = nb.ipam.prefixes.get(
prefix=shared.settings['network']['prefix']
)
prefix = nb_prefix.available_prefixes.create(
data={
'prefix_length': int(
shared.settings['network']['prefix_length']
),
'description': '{}\'s network "{}"'.format(
data['name'], data['network_name']
),
'is_pool': True,
}
)
except Exception as err:
app.logger.error(err)
return {
'message': 'Error occured while creating network.'
}
else:
network_entry['ipv6'] = prefix['prefix']
else:
network_entry['ipv6'] = 'fd00::/64'
network_key = join_path(
shared.settings['etcd']['network_prefix'],
data['name'],
data['network_name'],
)
shared.etcd_client.put(
network_key, network_entry, value_in_json=True
)
return {'message': 'Network successfully added.'}
else:
return validator.get_errors(), 400
class ListUserNetwork(Resource):
@staticmethod
def post():
data = request.json
validator = schemas.OTPSchema(data)
if validator.is_valid():
prefix = join_path(
shared.settings['etcd']['network_prefix'], data['name']
)
networks = shared.etcd_client.get_prefix(
prefix, value_in_json=True
)
user_networks = []
for net in networks:
net.value['name'] = net.key.split('/')[-1]
user_networks.append(net.value)
return {'networks': user_networks}, 200
else:
return validator.get_errors(), 400
api.add_resource(CreateVM, '/vm/create')
api.add_resource(VmStatus, '/vm/status')
api.add_resource(VMAction, '/vm/action')
api.add_resource(VMMigration, '/vm/migrate')
api.add_resource(CreateImage, '/image/create')
api.add_resource(ListPublicImages, '/image/list-public')
api.add_resource(ListUserVM, '/user/vms')
api.add_resource(ListUserFiles, '/user/files')
api.add_resource(ListUserNetwork, '/user/networks')
api.add_resource(AddSSHKey, '/user/add-ssh')
api.add_resource(RemoveSSHKey, '/user/remove-ssh')
api.add_resource(GetSSHKeys, '/user/get-ssh')
api.add_resource(CreateHost, '/host/create')
api.add_resource(ListHost, '/host/list')
api.add_resource(CreateNetwork, '/network/create')
def main(arguments):
debug = arguments['debug']
port = arguments['port']
try:
image_stores = list(
shared.etcd_client.get_prefix(
shared.settings['etcd']['image_store_prefix'], value_in_json=True
)
)
except KeyError:
image_stores = False
# Do not inject default values that might be very wrong
# fail when required, not before
#
# if not image_stores:
# data = {
# 'is_public': True,
# 'type': 'ceph',
# 'name': 'images',
# 'description': 'first ever public image-store',
# 'attributes': {'list': [], 'key': [], 'pool': 'images'},
# }
# shared.etcd_client.put(
# join_path(
# shared.settings['etcd']['image_store_prefix'], uuid4().hex
# ),
# json.dumps(data),
# )
try:
app.run(host='::', port=port, debug=debug)
except OSError as e:
raise UncloudException('Failed to start Flask: {}'.format(e))

View file

@ -1,6 +1,6 @@
"""
This module contain classes thats validates and intercept/modify
data coming from ucloud-cli (user)
data coming from uncloud-cli (user)
It was primarily developed as an alternative to argument parser
of Flask_Restful which is going to be deprecated. I also tried
@ -19,10 +19,10 @@ import os
import bitmath
from ucloud.common.host import HostStatus
from ucloud.common.vm import VMStatus
from ucloud.config import etcd_client, env_vars, vm_pool, host_pool
from . import helper
from uncloud.common.host import HostStatus
from uncloud.common.vm import VMStatus
from uncloud.common.shared import shared
from . import helper, logger
from .common_fields import Field, VmUUIDField
from .helper import check_otp, resolve_vm_name
@ -79,7 +79,12 @@ class OTPSchema(BaseSchema):
super().__init__(data=data, fields=_fields)
def validation(self):
if check_otp(self.name.value, self.realm.value, self.token.value) != 200:
if (
check_otp(
self.name.value, self.realm.value, self.token.value
)
!= 200
):
self.add_error("Wrong Credentials")
@ -91,7 +96,9 @@ class CreateImageSchema(BaseSchema):
# Fields
self.uuid = Field("uuid", str, data.get("uuid", KeyError))
self.name = Field("name", str, data.get("name", KeyError))
self.image_store = Field("image_store", str, data.get("image_store", KeyError))
self.image_store = Field(
"image_store", str, data.get("image_store", KeyError)
)
# Validations
self.uuid.validation = self.file_uuid_validation
@ -102,34 +109,51 @@ class CreateImageSchema(BaseSchema):
super().__init__(data, fields)
def file_uuid_validation(self):
file_entry = etcd_client.get(os.path.join(env_vars.get('FILE_PREFIX'), self.uuid.value))
file_entry = shared.etcd_client.get(
os.path.join(
shared.shared.shared.shared.shared.settings["etcd"]["file_prefix"], self.uuid.value
)
)
if file_entry is None:
self.add_error(
"Image File with uuid '{}' Not Found".format(self.uuid.value)
"Image File with uuid '{}' Not Found".format(
self.uuid.value
)
)
def image_store_name_validation(self):
image_stores = list(etcd_client.get_prefix(env_vars.get('IMAGE_STORE_PREFIX')))
image_stores = list(
shared.etcd_client.get_prefix(
shared.shared.shared.shared.shared.settings["etcd"]["image_store_prefix"]
)
)
image_store = next(
filter(
lambda s: json.loads(s.value)["name"] == self.image_store.value,
lambda s: json.loads(s.value)["name"]
== self.image_store.value,
image_stores,
),
None,
)
if not image_store:
self.add_error("Store '{}' does not exists".format(self.image_store.value))
self.add_error(
"Store '{}' does not exists".format(
self.image_store.value
)
)
# Host Operations
class CreateHostSchema(OTPSchema):
def __init__(self, data):
self.parsed_specs = {}
# Fields
self.specs = Field("specs", dict, data.get("specs", KeyError))
self.hostname = Field("hostname", str, data.get("hostname", KeyError))
self.hostname = Field(
"hostname", str, data.get("hostname", KeyError)
)
# Validation
self.specs.validation = self.specs_validation
@ -141,22 +165,28 @@ class CreateHostSchema(OTPSchema):
def specs_validation(self):
ALLOWED_BASE = 10
_cpu = self.specs.value.get('cpu', KeyError)
_ram = self.specs.value.get('ram', KeyError)
_os_ssd = self.specs.value.get('os-ssd', KeyError)
_hdd = self.specs.value.get('hdd', KeyError)
_cpu = self.specs.value.get("cpu", KeyError)
_ram = self.specs.value.get("ram", KeyError)
_os_ssd = self.specs.value.get("os-ssd", KeyError)
_hdd = self.specs.value.get("hdd", KeyError)
if KeyError in [_cpu, _ram, _os_ssd, _hdd]:
self.add_error("You must specify CPU, RAM and OS-SSD in your specs")
self.add_error(
"You must specify CPU, RAM and OS-SSD in your specs"
)
return None
try:
parsed_ram = bitmath.parse_string_unsafe(_ram)
parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd)
if parsed_ram.base != ALLOWED_BASE:
self.add_error("Your specified RAM is not in correct units")
self.add_error(
"Your specified RAM is not in correct units"
)
if parsed_os_ssd.base != ALLOWED_BASE:
self.add_error("Your specified OS-SSD is not in correct units")
self.add_error(
"Your specified OS-SSD is not in correct units"
)
if _cpu < 1:
self.add_error("CPU must be atleast 1")
@ -171,7 +201,9 @@ class CreateHostSchema(OTPSchema):
for hdd in _hdd:
_parsed_hdd = bitmath.parse_string_unsafe(hdd)
if _parsed_hdd.base != ALLOWED_BASE:
self.add_error("Your specified HDD is not in correct units")
self.add_error(
"Your specified HDD is not in correct units"
)
break
else:
parsed_hdd.append(str(_parsed_hdd))
@ -182,15 +214,17 @@ class CreateHostSchema(OTPSchema):
else:
if self.get_errors():
self.specs = {
'cpu': _cpu,
'ram': str(parsed_ram),
'os-ssd': str(parsed_os_ssd),
'hdd': parsed_hdd
"cpu": _cpu,
"ram": str(parsed_ram),
"os-ssd": str(parsed_os_ssd),
"hdd": parsed_hdd,
}
def validation(self):
if self.realm.value != "ungleich-admin":
self.add_error("Invalid Credentials/Insufficient Permission")
self.add_error(
"Invalid Credentials/Insufficient Permission"
)
# VM Operations
@ -198,13 +232,15 @@ class CreateHostSchema(OTPSchema):
class CreateVMSchema(OTPSchema):
def __init__(self, data):
self.parsed_specs = {}
# Fields
self.specs = Field("specs", dict, data.get("specs", KeyError))
self.vm_name = Field("vm_name", str, data.get("vm_name", KeyError))
self.vm_name = Field(
"vm_name", str, data.get("vm_name", KeyError)
)
self.image = Field("image", str, data.get("image", KeyError))
self.network = Field("network", list, data.get("network", KeyError))
self.network = Field(
"network", list, data.get("network", KeyError)
)
# Validation
self.image.validation = self.image_validation
@ -218,16 +254,25 @@ class CreateVMSchema(OTPSchema):
def image_validation(self):
try:
image_uuid = helper.resolve_image_name(self.image.value, etcd_client)
image_uuid = helper.resolve_image_name(
self.image.value, shared.etcd_client
)
except Exception as e:
logger.exception(
"Cannot resolve image name = %s", self.image.value
)
self.add_error(str(e))
else:
self.image_uuid = image_uuid
def vm_name_validation(self):
if resolve_vm_name(name=self.vm_name.value, owner=self.name.value):
if resolve_vm_name(
name=self.vm_name.value, owner=self.name.value
):
self.add_error(
'VM with same name "{}" already exists'.format(self.vm_name.value)
'VM with same name "{}" already exists'.format(
self.vm_name.value
)
)
def network_validation(self):
@ -235,34 +280,48 @@ class CreateVMSchema(OTPSchema):
if _network:
for net in _network:
network = etcd_client.get(os.path.join(env_vars.get('NETWORK_PREFIX'),
self.name.value,
net), value_in_json=True)
network = shared.etcd_client.get(
os.path.join(
shared.shared.shared.shared.shared.settings["etcd"]["network_prefix"],
self.name.value,
net,
),
value_in_json=True,
)
if not network:
self.add_error("Network with name {} does not exists" \
.format(net))
self.add_error(
"Network with name {} does not exists".format(
net
)
)
def specs_validation(self):
ALLOWED_BASE = 10
_cpu = self.specs.value.get('cpu', KeyError)
_ram = self.specs.value.get('ram', KeyError)
_os_ssd = self.specs.value.get('os-ssd', KeyError)
_hdd = self.specs.value.get('hdd', KeyError)
_cpu = self.specs.value.get("cpu", KeyError)
_ram = self.specs.value.get("ram", KeyError)
_os_ssd = self.specs.value.get("os-ssd", KeyError)
_hdd = self.specs.value.get("hdd", KeyError)
if KeyError in [_cpu, _ram, _os_ssd, _hdd]:
self.add_error("You must specify CPU, RAM and OS-SSD in your specs")
self.add_error(
"You must specify CPU, RAM and OS-SSD in your specs"
)
return None
try:
parsed_ram = bitmath.parse_string_unsafe(_ram)
parsed_os_ssd = bitmath.parse_string_unsafe(_os_ssd)
if parsed_ram.base != ALLOWED_BASE:
self.add_error("Your specified RAM is not in correct units")
self.add_error(
"Your specified RAM is not in correct units"
)
if parsed_os_ssd.base != ALLOWED_BASE:
self.add_error("Your specified OS-SSD is not in correct units")
self.add_error(
"Your specified OS-SSD is not in correct units"
)
if _cpu < 1:
if int(_cpu) < 1:
self.add_error("CPU must be atleast 1")
if parsed_ram < bitmath.GB(1):
@ -275,7 +334,9 @@ class CreateVMSchema(OTPSchema):
for hdd in _hdd:
_parsed_hdd = bitmath.parse_string_unsafe(hdd)
if _parsed_hdd.base != ALLOWED_BASE:
self.add_error("Your specified HDD is not in correct units")
self.add_error(
"Your specified HDD is not in correct units"
)
break
else:
parsed_hdd.append(str(_parsed_hdd))
@ -286,21 +347,24 @@ class CreateVMSchema(OTPSchema):
else:
if self.get_errors():
self.specs = {
'cpu': _cpu,
'ram': str(parsed_ram),
'os-ssd': str(parsed_os_ssd),
'hdd': parsed_hdd
"cpu": _cpu,
"ram": str(parsed_ram),
"os-ssd": str(parsed_os_ssd),
"hdd": parsed_hdd,
}
class VMStatusSchema(OTPSchema):
def __init__(self, data):
data["uuid"] = (
resolve_vm_name(
name=data.get("vm_name", None),
owner=(data.get("in_support_of", None) or data.get("name", None)),
)
or KeyError
resolve_vm_name(
name=data.get("vm_name", None),
owner=(
data.get("in_support_of", None)
or data.get("name", None)
),
)
or KeyError
)
self.uuid = VmUUIDField(data)
@ -309,9 +373,10 @@ class VMStatusSchema(OTPSchema):
super().__init__(data, fields)
def validation(self):
vm = vm_pool.get(self.uuid.value)
vm = shared.vm_pool.get(self.uuid.value)
if not (
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
vm.value["owner"] == self.name.value
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
@ -319,11 +384,14 @@ class VMStatusSchema(OTPSchema):
class VmActionSchema(OTPSchema):
def __init__(self, data):
data["uuid"] = (
resolve_vm_name(
name=data.get("vm_name", None),
owner=(data.get("in_support_of", None) or data.get("name", None)),
)
or KeyError
resolve_vm_name(
name=data.get("vm_name", None),
owner=(
data.get("in_support_of", None)
or data.get("name", None)
),
)
or KeyError
)
self.uuid = VmUUIDField(data)
self.action = Field("action", str, data.get("action", KeyError))
@ -338,20 +406,23 @@ class VmActionSchema(OTPSchema):
allowed_actions = ["start", "stop", "delete"]
if self.action.value not in allowed_actions:
self.add_error(
"Invalid Action. Allowed Actions are {}".format(allowed_actions)
"Invalid Action. Allowed Actions are {}".format(
allowed_actions
)
)
def validation(self):
vm = vm_pool.get(self.uuid.value)
vm = shared.vm_pool.get(self.uuid.value)
if not (
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
vm.value["owner"] == self.name.value
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
if (
self.action.value == "start"
and vm.status == VMStatus.running
and vm.hostname != ""
self.action.value == "start"
and vm.status == VMStatus.running
and vm.hostname != ""
):
self.add_error("VM Already Running")
@ -365,15 +436,20 @@ class VmActionSchema(OTPSchema):
class VmMigrationSchema(OTPSchema):
def __init__(self, data):
data["uuid"] = (
resolve_vm_name(
name=data.get("vm_name", None),
owner=(data.get("in_support_of", None) or data.get("name", None)),
)
or KeyError
resolve_vm_name(
name=data.get("vm_name", None),
owner=(
data.get("in_support_of", None)
or data.get("name", None)
),
)
or KeyError
)
self.uuid = VmUUIDField(data)
self.destination = Field("destination", str, data.get("destination", KeyError))
self.destination = Field(
"destination", str, data.get("destination", KeyError)
)
self.destination.validation = self.destination_validation
@ -382,31 +458,47 @@ class VmMigrationSchema(OTPSchema):
def destination_validation(self):
hostname = self.destination.value
host = next(filter(lambda h: h.hostname == hostname, host_pool.hosts), None)
host = next(
filter(
lambda h: h.hostname == hostname, shared.host_pool.hosts
),
None,
)
if not host:
self.add_error("No Such Host ({}) exists".format(self.destination.value))
self.add_error(
"No Such Host ({}) exists".format(
self.destination.value
)
)
elif host.status != HostStatus.alive:
self.add_error("Destination Host is dead")
else:
self.destination.value = host.key
def validation(self):
vm = vm_pool.get(self.uuid.value)
vm = shared.vm_pool.get(self.uuid.value)
if not (
vm.value["owner"] == self.name.value or self.realm.value == "ungleich-admin"
vm.value["owner"] == self.name.value
or self.realm.value == "ungleich-admin"
):
self.add_error("Invalid User")
if vm.status != VMStatus.running:
self.add_error("Can't migrate non-running VM")
if vm.hostname == os.path.join(env_vars.get('HOST_PREFIX'), self.destination.value):
self.add_error("Destination host couldn't be same as Source Host")
if vm.hostname == os.path.join(
shared.shared.shared.shared.shared.settings["etcd"]["host_prefix"], self.destination.value
):
self.add_error(
"Destination host couldn't be same as Source Host"
)
class AddSSHSchema(OTPSchema):
def __init__(self, data):
self.key_name = Field("key_name", str, data.get("key_name", KeyError))
self.key_name = Field(
"key_name", str, data.get("key_name", KeyError)
)
self.key = Field("key", str, data.get("key_name", KeyError))
fields = [self.key_name, self.key]
@ -415,7 +507,9 @@ class AddSSHSchema(OTPSchema):
class RemoveSSHSchema(OTPSchema):
def __init__(self, data):
self.key_name = Field("key_name", str, data.get("key_name", KeyError))
self.key_name = Field(
"key_name", str, data.get("key_name", KeyError)
)
fields = [self.key_name]
super().__init__(data=data, fields=fields)
@ -423,7 +517,9 @@ class RemoveSSHSchema(OTPSchema):
class GetSSHSchema(OTPSchema):
def __init__(self, data):
self.key_name = Field("key_name", str, data.get("key_name", None))
self.key_name = Field(
"key_name", str, data.get("key_name", None)
)
fields = [self.key_name]
super().__init__(data=data, fields=fields)
@ -442,15 +538,20 @@ class CreateNetwork(OTPSchema):
super().__init__(data, fields=fields)
def network_name_validation(self):
network = etcd_client.get(os.path.join(env_vars.get('NETWORK_PREFIX'),
self.name.value,
self.network_name.value),
value_in_json=True)
key = os.path.join(shared.shared.shared.shared.shared.settings["etcd"]["network_prefix"], self.name.value, self.network_name.value)
network = shared.etcd_client.get(key, value_in_json=True)
if network:
self.add_error("Network with name {} already exists" \
.format(self.network_name.value))
self.add_error(
"Network with name {} already exists".format(
self.network_name.value
)
)
def network_type_validation(self):
supported_network_types = ["vxlan"]
if self.type.value not in supported_network_types:
self.add_error("Unsupported Network Type. Supported network types are {}".format(supported_network_types))
self.add_error(
"Unsupported Network Type. Supported network types are {}".format(
supported_network_types
)
)

View file

@ -0,0 +1,46 @@
import requests
import json
import argparse
import binascii
from pyotp import TOTP
from os.path import join as join_path
from uncloud.common.shared import shared
def get_otp_parser():
otp_parser = argparse.ArgumentParser('otp')
otp_parser.add_argument('--name')
otp_parser.add_argument('--realm')
otp_parser.add_argument('--seed', type=get_token, dest='token', metavar='SEED')
return otp_parser
def load_dump_pretty(content):
if isinstance(content, bytes):
content = content.decode('utf-8')
parsed = json.loads(content)
return json.dumps(parsed, indent=4, sort_keys=True)
def make_request(*args, data=None, request_method=requests.post):
try:
r = request_method(join_path(shared.settings['client']['api_server'], *args), json=data)
except requests.exceptions.RequestException:
print('Error occurred while connecting to API server.')
else:
try:
print(load_dump_pretty(r.content))
except Exception:
print('Error occurred while getting output from api server.')
def get_token(seed):
if seed is not None:
try:
token = TOTP(seed).now()
except binascii.Error:
raise argparse.ArgumentTypeError('Invalid seed')
else:
return token

View file

@ -0,0 +1,45 @@
import requests
from uncloud.cli.helper import make_request, get_otp_parser
from uncloud.common.parser import BaseParser
class HostParser(BaseParser):
def __init__(self):
super().__init__('host')
def create(self, **kwargs):
p = self.subparser.add_parser('create', parents=[get_otp_parser()], **kwargs)
p.add_argument('--hostname', required=True)
p.add_argument('--cpu', required=True, type=int)
p.add_argument('--ram', required=True)
p.add_argument('--os-ssd', required=True)
p.add_argument('--hdd', default=list())
def list(self, **kwargs):
self.subparser.add_parser('list', **kwargs)
parser = HostParser()
arg_parser = parser.arg_parser
def main(**kwargs):
subcommand = kwargs.pop('host_subcommand')
if not subcommand:
arg_parser.print_help()
else:
request_method = requests.post
data = None
if subcommand == 'create':
kwargs['specs'] = {
'cpu': kwargs.pop('cpu'),
'ram': kwargs.pop('ram'),
'os-ssd': kwargs.pop('os_ssd'),
'hdd': kwargs.pop('hdd')
}
data = kwargs
elif subcommand == 'list':
request_method = requests.get
make_request('host', subcommand, data=data, request_method=request_method)

View file

@ -0,0 +1,38 @@
import requests
from uncloud.cli.helper import make_request
from uncloud.common.parser import BaseParser
class ImageParser(BaseParser):
def __init__(self):
super().__init__('image')
def create(self, **kwargs):
p = self.subparser.add_parser('create', **kwargs)
p.add_argument('--name', required=True)
p.add_argument('--uuid', required=True)
p.add_argument('--image-store', required=True, dest='image_store')
def list(self, **kwargs):
self.subparser.add_parser('list', **kwargs)
parser = ImageParser()
arg_parser = parser.arg_parser
def main(**kwargs):
subcommand = kwargs.pop('image_subcommand')
if not subcommand:
arg_parser.print_help()
else:
data = None
request_method = requests.post
if subcommand == 'list':
subcommand = 'list-public'
request_method = requests.get
elif subcommand == 'create':
data = kwargs
make_request('image', subcommand, data=data, request_method=request_method)

View file

@ -0,0 +1,23 @@
#!/usr/bin/env python3
import argparse
import importlib
arg_parser = argparse.ArgumentParser('cli', add_help=False)
subparser = arg_parser.add_subparsers(dest='subcommand')
for component in ['user', 'host', 'image', 'network', 'vm']:
module = importlib.import_module('uncloud.cli.{}'.format(component))
parser = getattr(module, 'arg_parser')
subparser.add_parser(name=parser.prog, parents=[parser])
def main(arguments):
if not arguments['subcommand']:
arg_parser.print_help()
else:
name = arguments.pop('subcommand')
arguments.pop('debug')
mod = importlib.import_module('uncloud.cli.{}'.format(name))
_main = getattr(mod, 'main')
_main(**arguments)

View file

@ -0,0 +1,32 @@
import requests
from uncloud.cli.helper import make_request, get_otp_parser
from uncloud.common.parser import BaseParser
class NetworkParser(BaseParser):
def __init__(self):
super().__init__('network')
def create(self, **kwargs):
p = self.subparser.add_parser('create', parents=[get_otp_parser()], **kwargs)
p.add_argument('--network-name', required=True)
p.add_argument('--network-type', required=True, dest='type')
p.add_argument('--user', action='store_true')
parser = NetworkParser()
arg_parser = parser.arg_parser
def main(**kwargs):
subcommand = kwargs.pop('network_subcommand')
if not subcommand:
arg_parser.print_help()
else:
data = None
request_method = requests.post
if subcommand == 'create':
data = kwargs
make_request('network', subcommand, data=data, request_method=request_method)

View file

@ -0,0 +1,41 @@
from uncloud.cli.helper import make_request, get_otp_parser
from uncloud.common.parser import BaseParser
class UserParser(BaseParser):
def __init__(self):
super().__init__('user')
def files(self, **kwargs):
self.subparser.add_parser('files', parents=[get_otp_parser()], **kwargs)
def vms(self, **kwargs):
self.subparser.add_parser('vms', parents=[get_otp_parser()], **kwargs)
def networks(self, **kwargs):
self.subparser.add_parser('networks', parents=[get_otp_parser()], **kwargs)
def add_ssh(self, **kwargs):
p = self.subparser.add_parser('add-ssh', parents=[get_otp_parser()], **kwargs)
p.add_argument('--key-name', required=True)
p.add_argument('--key', required=True)
def get_ssh(self, **kwargs):
p = self.subparser.add_parser('get-ssh', parents=[get_otp_parser()], **kwargs)
p.add_argument('--key-name', default='')
def remove_ssh(self, **kwargs):
p = self.subparser.add_parser('remove-ssh', parents=[get_otp_parser()], **kwargs)
p.add_argument('--key-name', required=True)
parser = UserParser()
arg_parser = parser.arg_parser
def main(**kwargs):
subcommand = kwargs.pop('user_subcommand')
if not subcommand:
arg_parser.print_help()
else:
make_request('user', subcommand, data=kwargs)

View file

@ -0,0 +1,62 @@
from uncloud.common.parser import BaseParser
from uncloud.cli.helper import make_request, get_otp_parser
class VMParser(BaseParser):
def __init__(self):
super().__init__('vm')
def start(self, **args):
p = self.subparser.add_parser('start', parents=[get_otp_parser()], **args)
p.add_argument('--vm-name', required=True)
def stop(self, **args):
p = self.subparser.add_parser('stop', parents=[get_otp_parser()], **args)
p.add_argument('--vm-name', required=True)
def status(self, **args):
p = self.subparser.add_parser('status', parents=[get_otp_parser()], **args)
p.add_argument('--vm-name', required=True)
def delete(self, **args):
p = self.subparser.add_parser('delete', parents=[get_otp_parser()], **args)
p.add_argument('--vm-name', required=True)
def migrate(self, **args):
p = self.subparser.add_parser('migrate', parents=[get_otp_parser()], **args)
p.add_argument('--vm-name', required=True)
p.add_argument('--destination', required=True)
def create(self, **args):
p = self.subparser.add_parser('create', parents=[get_otp_parser()], **args)
p.add_argument('--cpu', required=True)
p.add_argument('--ram', required=True)
p.add_argument('--os-ssd', required=True)
p.add_argument('--hdd', action='append', default=list())
p.add_argument('--image', required=True)
p.add_argument('--network', action='append', default=[])
p.add_argument('--vm-name', required=True)
parser = VMParser()
arg_parser = parser.arg_parser
def main(**kwargs):
subcommand = kwargs.pop('vm_subcommand')
if not subcommand:
arg_parser.print_help()
else:
data = kwargs
endpoint = subcommand
if subcommand in ['start', 'stop', 'delete']:
endpoint = 'action'
data['action'] = subcommand
elif subcommand == 'create':
kwargs['specs'] = {
'cpu': kwargs.pop('cpu'),
'ram': kwargs.pop('ram'),
'os-ssd': kwargs.pop('os_ssd'),
'hdd': kwargs.pop('hdd')
}
make_request('vm', endpoint, data=data)

View 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'])

View file

@ -1,4 +1,4 @@
from etcd3_wrapper import EtcdEntry
from .etcd_wrapper import EtcdEntry
class SpecificEtcdEntryBase:

View file

@ -0,0 +1,26 @@
from uncloud.common.shared import shared
from pyotp import TOTP
def get_token(seed):
if seed is not None:
try:
token = TOTP(seed).now()
except Exception:
raise Exception('Invalid seed')
else:
return token
def resolve_otp_credentials(kwargs):
d = {
'name': shared.settings['client']['name'],
'realm': shared.settings['client']['realm'],
'token': get_token(shared.settings['client']['seed'])
}
for k, v in d.items():
if k in kwargs and kwargs[k] is None:
kwargs.update({k: v})
return d

View file

@ -1,4 +1,4 @@
from etcd3_wrapper import Etcd3Wrapper
from .etcd_wrapper import Etcd3Wrapper
def increment_etcd_counter(etcd_client: Etcd3Wrapper, key):

View file

@ -0,0 +1,75 @@
import etcd3
import json
from functools import wraps
from uncloud import UncloudException
from uncloud.common import logger
class EtcdEntry:
def __init__(self, meta_or_key, value, value_in_json=False):
if hasattr(meta_or_key, 'key'):
# if meta has attr 'key' then get it
self.key = meta_or_key.key.decode('utf-8')
else:
# otherwise meta is the 'key'
self.key = meta_or_key
self.value = value.decode('utf-8')
if value_in_json:
self.value = json.loads(self.value)
def readable_errors(func):
@wraps(func)
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
except etcd3.exceptions.ConnectionFailedError:
raise UncloudException('Cannot connect to etcd: is etcd running as configured in uncloud.conf?')
except etcd3.exceptions.ConnectionTimeoutError as err:
raise etcd3.exceptions.ConnectionTimeoutError('etcd connection timeout.') from err
except Exception:
logger.exception('Some etcd error occured. See syslog for details.')
return wrapper
class Etcd3Wrapper:
@readable_errors
def __init__(self, *args, **kwargs):
self.client = etcd3.client(*args, **kwargs)
@readable_errors
def get(self, *args, value_in_json=False, **kwargs):
_value, _key = self.client.get(*args, **kwargs)
if _key is None or _value is None:
return None
return EtcdEntry(_key, _value, value_in_json=value_in_json)
@readable_errors
def put(self, *args, value_in_json=False, **kwargs):
_key, _value = args
if value_in_json:
_value = json.dumps(_value)
if not isinstance(_key, str):
_key = _key.decode('utf-8')
return self.client.put(_key, _value, **kwargs)
@readable_errors
def get_prefix(self, *args, value_in_json=False, raise_exception=True, **kwargs):
event_iterator = self.client.get_prefix(*args, **kwargs)
for e in event_iterator:
yield EtcdEntry(*e[::-1], value_in_json=value_in_json)
@readable_errors
def watch_prefix(self, key, raise_exception=True, value_in_json=False):
event_iterator, cancel = self.client.watch_prefix(key)
for e in event_iterator:
if hasattr(e, '_event'):
e = e._event
if e.type == e.PUT:
yield EtcdEntry(e.kv.key, e.kv.value, value_in_json=value_in_json)

View file

@ -7,7 +7,7 @@ from .classes import SpecificEtcdEntryBase
class HostStatus:
"""Possible Statuses of ucloud host."""
"""Possible Statuses of uncloud host."""
alive = "ALIVE"
dead = "DEAD"
@ -26,11 +26,13 @@ class HostEntry(SpecificEtcdEntryBase):
def update_heartbeat(self):
self.status = HostStatus.alive
self.last_heartbeat = time.strftime("%Y-%m-%d %H:%M:%S")
self.last_heartbeat = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
def is_alive(self):
last_heartbeat = datetime.strptime(self.last_heartbeat, "%Y-%m-%d %H:%M:%S")
delta = datetime.now() - last_heartbeat
last_heartbeat = datetime.strptime(
self.last_heartbeat, "%Y-%m-%d %H:%M:%S"
)
delta = datetime.utcnow() - last_heartbeat
if delta.total_seconds() > 60:
return False
return True

View file

@ -0,0 +1,70 @@
import subprocess as sp
import random
import logging
logger = logging.getLogger(__name__)
def random_bytes(num=6):
return [random.randrange(256) for _ in range(num)]
def generate_mac(
uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"
):
mac = random_bytes()
if oui:
if type(oui) == str:
oui = [int(chunk) for chunk in oui.split(separator)]
mac = oui + random_bytes(num=6 - len(oui))
else:
if multicast:
mac[0] |= 1 # set bit 0
else:
mac[0] &= ~1 # clear bit 0
if uaa:
mac[0] &= ~(1 << 1) # clear bit 1
else:
mac[0] |= 1 << 1 # set bit 1
return separator.join(byte_fmt % b for b in mac)
def create_dev(script, _id, dev, ip=None):
command = [
"sudo",
"-p",
"Enter password to create network devices for vm: ",
script,
str(_id),
dev,
]
if ip:
command.append(ip)
try:
output = sp.check_output(command, stderr=sp.PIPE)
except Exception:
logger.exception("Creation of interface %s failed.", dev)
return None
else:
return output.decode("utf-8").strip()
def delete_network_interface(iface):
try:
sp.check_output(
[
"sudo",
"-p",
"Enter password to remove {} network device: ".format(
iface
),
"ip",
"link",
"del",
iface,
],
stderr=sp.PIPE,
)
except Exception:
logger.exception("Interface %s Deletion failed", iface)

View file

@ -0,0 +1,13 @@
import argparse
class BaseParser:
def __init__(self, command):
self.arg_parser = argparse.ArgumentParser(command, add_help=False)
self.subparser = self.arg_parser.add_subparsers(dest='{}_subcommand'.format(command))
self.common_args = {'add_help': False}
methods = [attr for attr in dir(self) if not attr.startswith('__')
and type(getattr(self, attr)).__name__ == 'method']
for method in methods:
getattr(self, method)(**self.common_args)

View file

@ -2,9 +2,8 @@ import json
from os.path import join
from uuid import uuid4
from etcd3_wrapper.etcd3_wrapper import PsuedoEtcdEntry
from .classes import SpecificEtcdEntryBase
from uncloud.common.etcd_wrapper import EtcdEntry
from uncloud.common.classes import SpecificEtcdEntryBase
class RequestType:
@ -18,8 +17,9 @@ class RequestType:
class RequestEntry(SpecificEtcdEntryBase):
def __init__(self, e):
self.destination_sock_path = None
self.destination_host_key = None
self.type = None # type: str
self.migration = None # type: bool
self.destination = None # type: str
@ -29,8 +29,8 @@ class RequestEntry(SpecificEtcdEntryBase):
@classmethod
def from_scratch(cls, request_prefix, **kwargs):
e = PsuedoEtcdEntry(join(request_prefix, uuid4().hex),
value=json.dumps(kwargs).encode("utf-8"), value_in_json=True)
e = EtcdEntry(meta_or_key=join(request_prefix, uuid4().hex),
value=json.dumps(kwargs).encode('utf-8'), value_in_json=True)
return cls(e)

View file

@ -0,0 +1,41 @@
import bitmath
from marshmallow import fields, Schema
class StorageUnit(fields.Field):
def _serialize(self, value, attr, obj, **kwargs):
return str(value)
def _deserialize(self, value, attr, data, **kwargs):
return bitmath.parse_string_unsafe(value)
class SpecsSchema(Schema):
cpu = fields.Int()
ram = StorageUnit()
os_ssd = StorageUnit(data_key="os-ssd", attribute="os-ssd")
hdd = fields.List(StorageUnit())
class VMSchema(Schema):
name = fields.Str()
owner = fields.Str()
owner_realm = fields.Str()
specs = fields.Nested(SpecsSchema)
status = fields.Str()
log = fields.List(fields.Str())
vnc_socket = fields.Str()
image_uuid = fields.Str()
hostname = fields.Str()
metadata = fields.Dict()
network = fields.List(
fields.Tuple((fields.Str(), fields.Str(), fields.Int()))
)
in_migration = fields.Bool()
class NetworkSchema(Schema):
_id = fields.Int(data_key="id", attribute="id")
_type = fields.Str(data_key="type", attribute="type")
ipv6 = fields.Str()

View file

@ -0,0 +1,136 @@
import configparser
import logging
import sys
import os
from datetime import datetime
from uncloud.common.etcd_wrapper import Etcd3Wrapper
from os.path import join as join_path
logger = logging.getLogger(__name__)
settings = None
class CustomConfigParser(configparser.RawConfigParser):
def __getitem__(self, key):
try:
result = super().__getitem__(key)
except KeyError as err:
raise KeyError(
'Key \'{}\' not found in configuration. Make sure you configure uncloud.'.format(
key
)
) from err
else:
return result
class Settings(object):
def __init__(self, conf_dir, seed_value=None):
conf_name = 'uncloud.conf'
self.config_file = join_path(conf_dir, conf_name)
# this is used to cache config from etcd for 1 minutes. Without this we
# would make a lot of requests to etcd which slows down everything.
self.last_config_update = datetime.fromtimestamp(0)
self.config_parser = CustomConfigParser(allow_no_value=True)
self.config_parser.add_section('etcd')
self.config_parser.set('etcd', 'base_prefix', '/')
if os.access(self.config_file, os.R_OK):
self.config_parser.read(self.config_file)
else:
raise FileNotFoundError('Config file %s not found!', self.config_file)
self.config_key = join_path(self['etcd']['base_prefix'] + 'uncloud/config/')
self.read_internal_values()
if seed_value is None:
seed_value = dict()
self.config_parser.read_dict(seed_value)
def get_etcd_client(self):
args = tuple()
try:
kwargs = {
'host': self.config_parser.get('etcd', 'url'),
'port': self.config_parser.get('etcd', 'port'),
'ca_cert': self.config_parser.get('etcd', 'ca_cert'),
'cert_cert': self.config_parser.get('etcd', 'cert_cert'),
'cert_key': self.config_parser.get('etcd', 'cert_key'),
}
except configparser.Error as err:
raise configparser.Error(
'{} in config file {}'.format(
err.message, self.config_file
)
) from err
else:
try:
wrapper = Etcd3Wrapper(*args, **kwargs)
except Exception as err:
logger.error(
'etcd connection not successfull. Please check your config file.'
'\nDetails: %s\netcd connection parameters: %s',
err,
kwargs,
)
sys.exit(1)
else:
return wrapper
def read_internal_values(self):
base_prefix = self['etcd']['base_prefix']
self.config_parser.read_dict(
{
'etcd': {
'file_prefix': join_path(base_prefix, 'files/'),
'host_prefix': join_path(base_prefix, 'hosts/'),
'image_prefix': join_path(base_prefix, 'images/'),
'image_store_prefix': join_path(base_prefix, 'imagestore/'),
'network_prefix': join_path(base_prefix, 'networks/'),
'request_prefix': join_path(base_prefix, 'requests/'),
'user_prefix': join_path(base_prefix, 'users/'),
'vm_prefix': join_path(base_prefix, 'vms/'),
'vxlan_counter': join_path(base_prefix, 'counters/vxlan'),
'tap_counter': join_path(base_prefix, 'counters/tap')
}
}
)
def read_config_file_values(self, config_file):
try:
# Trying to read configuration file
with open(config_file) as config_file_handle:
self.config_parser.read_file(config_file_handle)
except FileNotFoundError:
sys.exit('Configuration file {} not found!'.format(config_file))
except Exception as err:
logger.exception(err)
sys.exit('Error occurred while reading configuration file')
def read_values_from_etcd(self):
etcd_client = self.get_etcd_client()
if (datetime.utcnow() - self.last_config_update).total_seconds() > 60:
config_from_etcd = etcd_client.get(self.config_key, value_in_json=True)
if config_from_etcd:
self.config_parser.read_dict(config_from_etcd.value)
self.last_config_update = datetime.utcnow()
else:
raise KeyError('Key \'{}\' not found in etcd. Please configure uncloud.'.format(self.config_key))
def __getitem__(self, key):
# Allow failing to read from etcd if we have
# it locally
if key not in self.config_parser.sections():
try:
self.read_values_from_etcd()
except KeyError:
pass
return self.config_parser[key]
def get_settings():
return settings

View file

@ -0,0 +1,34 @@
from uncloud.common.settings import get_settings
from uncloud.common.vm import VmPool
from uncloud.common.host import HostPool
from uncloud.common.request import RequestPool
import uncloud.common.storage_handlers as storage_handlers
class Shared:
@property
def settings(self):
return get_settings()
@property
def etcd_client(self):
return self.settings.get_etcd_client()
@property
def host_pool(self):
return HostPool(self.etcd_client, self.settings["etcd"]["host_prefix"])
@property
def vm_pool(self):
return VmPool(self.etcd_client, self.settings["etcd"]["vm_prefix"])
@property
def request_pool(self):
return RequestPool(self.etcd_client, self.settings["etcd"]["request_prefix"])
@property
def storage_handler(self):
return storage_handlers.get_storage_handler()
shared = Shared()

View file

@ -6,17 +6,20 @@ import stat
from abc import ABC
from . import logger
from os.path import join as join_path
import uncloud.common.shared as shared
class ImageStorageHandler(ABC):
handler_name = "base"
def __init__(self, image_base, vm_base):
self.image_base = image_base
self.vm_base = vm_base
def import_image(self, image_src, image_dest, protect=False):
"""Put an image at the destination
:param src: An Image file
:param dest: A path where :param src: is to be put.
:param image_src: An Image file
:param image_dest: A path where :param src: is to be put.
:param protect: If protect is true then the dest is protect (readonly etc)
The obj must exist on filesystem.
"""
@ -26,8 +29,8 @@ class ImageStorageHandler(ABC):
def make_vm_image(self, image_path, path):
"""Copy image from src to dest
:param src: A path
:param dest: A path
:param image_path: A path
:param path: A path
src and destination must be on same storage system i.e both on file system or both on CEPH etc.
"""
@ -43,14 +46,17 @@ class ImageStorageHandler(ABC):
def delete_vm_image(self, path):
raise NotImplementedError()
def execute_command(self, command, report=True):
def execute_command(self, command, report=True, error_origin=None):
if not error_origin:
error_origin = self.handler_name
command = list(map(str, command))
try:
output = sp.check_output(command, stderr=sp.PIPE)
except Exception as e:
sp.check_output(command, stderr=sp.PIPE)
except sp.CalledProcessError as e:
_stderr = e.stderr.decode("utf-8").strip()
if report:
print(e)
logger.exception(e)
logger.exception("%s:- %s", error_origin, _stderr)
return False
return True
@ -65,12 +71,16 @@ class ImageStorageHandler(ABC):
class FileSystemBasedImageStorageHandler(ImageStorageHandler):
handler_name = "Filesystem"
def import_image(self, src, dest, protect=False):
dest = join_path(self.image_base, dest)
try:
shutil.copy(src, dest)
if protect:
os.chmod(dest, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
os.chmod(
dest, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
)
except Exception as e:
logger.exception(e)
return False
@ -80,7 +90,7 @@ class FileSystemBasedImageStorageHandler(ImageStorageHandler):
src = join_path(self.image_base, src)
dest = join_path(self.vm_base, dest)
try:
shutil.copy(src, dest)
shutil.copyfile(src, dest)
except Exception as e:
logger.exception(e)
return False
@ -88,7 +98,14 @@ class FileSystemBasedImageStorageHandler(ImageStorageHandler):
def resize_vm_image(self, path, size):
path = join_path(self.vm_base, path)
command = ["qemu-img", "resize", "-f", "raw", path, "{}M".format(size)]
command = [
"qemu-img",
"resize",
"-f",
"raw",
path,
"{}M".format(size),
]
if self.execute_command(command):
return True
else:
@ -117,17 +134,33 @@ class FileSystemBasedImageStorageHandler(ImageStorageHandler):
class CEPHBasedImageStorageHandler(ImageStorageHandler):
handler_name = "Ceph"
def import_image(self, src, dest, protect=False):
dest = join_path(self.image_base, dest)
command = ["rbd", "import", src, dest]
import_command = ["rbd", "import", src, dest]
commands = [import_command]
if protect:
snap_create_command = ["rbd", "snap", "create", "{}@protected".format(dest)]
snap_protect_command = ["rbd", "snap", "protect", "{}@protected".format(dest)]
snap_create_command = [
"rbd",
"snap",
"create",
"{}@protected".format(dest),
]
snap_protect_command = [
"rbd",
"snap",
"protect",
"{}@protected".format(dest),
]
commands.append(snap_create_command)
commands.append(snap_protect_command)
return self.execute_command(command) and self.execute_command(snap_create_command) and\
self.execute_command(snap_protect_command)
result = True
for command in commands:
result = result and self.execute_command(command)
return self.execute_command(command)
return result
def make_vm_image(self, src, dest):
src = join_path(self.image_base, src)
@ -156,3 +189,19 @@ class CEPHBasedImageStorageHandler(ImageStorageHandler):
path = join_path(self.vm_base, path)
command = ["rbd", "info", path]
return self.execute_command(command, report=False)
def get_storage_handler():
__storage_backend = shared.shared.settings["storage"]["storage_backend"]
if __storage_backend == "filesystem":
return FileSystemBasedImageStorageHandler(
vm_base=shared.shared.settings["storage"]["vm_dir"],
image_base=shared.shared.settings["storage"]["image_dir"],
)
elif __storage_backend == "ceph":
return CEPHBasedImageStorageHandler(
vm_base=shared.shared.settings["storage"]["ceph_vm_pool"],
image_base=shared.shared.settings["storage"]["ceph_image_pool"],
)
else:
raise Exception("Unknown Image Storage Handler")

View file

@ -12,8 +12,13 @@ class VMStatus:
error = "ERROR" # An error occurred that cannot be resolved automatically
class VMEntry(SpecificEtcdEntryBase):
def declare_stopped(vm):
vm["hostname"] = ""
vm["in_migration"] = False
vm["status"] = VMStatus.stopped
class VMEntry(SpecificEtcdEntryBase):
def __init__(self, e):
self.owner = None # type: str
self.specs = None # type: dict
@ -42,7 +47,9 @@ class VMEntry(SpecificEtcdEntryBase):
def add_log(self, msg):
self.log = self.log[:5]
self.log.append("{} - {}".format(datetime.now().isoformat(), msg))
self.log.append(
"{} - {}".format(datetime.now().isoformat(), msg)
)
class VmPool:

View file

@ -0,0 +1,57 @@
import os
import argparse
from uncloud.common.shared import shared
arg_parser = argparse.ArgumentParser('configure', add_help=False)
configure_subparsers = arg_parser.add_subparsers(dest='subcommand')
otp_parser = configure_subparsers.add_parser('otp')
otp_parser.add_argument('--verification-controller-url', required=True, metavar='URL')
otp_parser.add_argument('--auth-name', required=True, metavar='OTP-NAME')
otp_parser.add_argument('--auth-realm', required=True, metavar='OTP-REALM')
otp_parser.add_argument('--auth-seed', required=True, metavar='OTP-SEED')
network_parser = configure_subparsers.add_parser('network')
network_parser.add_argument('--prefix-length', required=True, type=int)
network_parser.add_argument('--prefix', required=True)
network_parser.add_argument('--vxlan-phy-dev', required=True)
netbox_parser = configure_subparsers.add_parser('netbox')
netbox_parser.add_argument('--url', required=True)
netbox_parser.add_argument('--token', required=True)
ssh_parser = configure_subparsers.add_parser('ssh')
ssh_parser.add_argument('--username', default='root')
ssh_parser.add_argument('--private-key-path', default=os.path.expanduser('~/.ssh/id_rsa'),)
storage_parser = configure_subparsers.add_parser('storage')
storage_parser.add_argument('--file-dir', required=True)
storage_parser_subparsers = storage_parser.add_subparsers(dest='storage_backend')
filesystem_storage_parser = storage_parser_subparsers.add_parser('filesystem')
filesystem_storage_parser.add_argument('--vm-dir', required=True)
filesystem_storage_parser.add_argument('--image-dir', required=True)
ceph_storage_parser = storage_parser_subparsers.add_parser('ceph')
ceph_storage_parser.add_argument('--ceph-vm-pool', required=True)
ceph_storage_parser.add_argument('--ceph-image-pool', required=True)
def update_config(section, kwargs):
uncloud_config = shared.etcd_client.get(shared.settings.config_key, value_in_json=True)
if not uncloud_config:
uncloud_config = {}
else:
uncloud_config = uncloud_config.value
uncloud_config[section] = kwargs
shared.etcd_client.put(shared.settings.config_key, uncloud_config, value_in_json=True)
def main(arguments):
subcommand = arguments['subcommand']
if not subcommand:
arg_parser.print_help()
else:
update_config(subcommand, arguments)

View file

@ -0,0 +1,85 @@
import glob
import os
import pathlib
import subprocess as sp
import time
import argparse
import bitmath
from uuid import uuid4
from . import logger
from uncloud.common.shared import shared
arg_parser = argparse.ArgumentParser('filescanner', add_help=False)
arg_parser.add_argument('--hostname', required=True)
def sha512sum(file: str):
"""Use sha512sum utility to compute sha512 sum of arg:file
IF arg:file does not exists:
raise FileNotFoundError exception
ELSE IF sum successfully computer:
return computed sha512 sum
ELSE:
return None
"""
if not isinstance(file, str):
raise TypeError
try:
output = sp.check_output(['sha512sum', file], stderr=sp.PIPE)
except sp.CalledProcessError as e:
error = e.stderr.decode('utf-8')
if 'No such file or directory' in error:
raise FileNotFoundError from None
else:
output = output.decode('utf-8').strip()
output = output.split(' ')
return output[0]
return None
def track_file(file, base_dir, host):
file_path = file.relative_to(base_dir)
file_str = str(file)
# Get Username
try:
owner = file_path.parts[0]
except IndexError:
pass
else:
file_path = file_path.relative_to(owner)
creation_date = time.ctime(os.stat(file_str).st_ctime)
entry_key = os.path.join(shared.settings['etcd']['file_prefix'], str(uuid4()))
entry_value = {
'filename': str(file_path),
'owner': owner,
'sha512sum': sha512sum(file_str),
'creation_date': creation_date,
'size': str(bitmath.Byte(os.path.getsize(file_str)).to_MB()),
'host': host
}
logger.info('Tracking %s', file_str)
shared.etcd_client.put(entry_key, entry_value, value_in_json=True)
def main(arguments):
hostname = arguments['hostname']
base_dir = shared.settings['storage']['file_dir']
# Recursively Get All Files and Folder below BASE_DIR
files = glob.glob('{}/**'.format(base_dir), recursive=True)
files = [pathlib.Path(f) for f in files if pathlib.Path(f).is_file()]
# Files that are already tracked
tracked_files = [
pathlib.Path(os.path.join(base_dir, f.value['owner'], f.value['filename']))
for f in shared.etcd_client.get_prefix(shared.settings['etcd']['file_prefix'], value_in_json=True)
if f.value['host'] == hostname
]
untracked_files = set(files) - set(tracked_files)
for file in untracked_files:
track_file(file, base_dir, hostname)

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,39 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
#
# This file is part of uncloud.
#
# uncloud is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# uncloud is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
#
#
class Config(object):
def __init__(self, arguments):
""" read arguments dicts as a base """
self.arguments = arguments
# Split them so *etcd_args can be used and we can
# iterate over etcd_hosts
self.etcd_hosts = [ arguments['etcd_host'] ]
self.etcd_args = {
'ca_cert': arguments['etcd_ca_cert'],
'cert_cert': arguments['etcd_cert_cert'],
'cert_key': arguments['etcd_cert_key'],
# 'user': None,
# 'password': None
}
self.etcd_prefix = '/nicohack/'

View file

@ -0,0 +1,149 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
#
# This file is part of uncloud.
#
# uncloud is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# uncloud is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
#
#
import etcd3
import json
import logging
import datetime
import re
from functools import wraps
from uncloud import UncloudException
log = logging.getLogger(__name__)
def db_logentry(message):
timestamp = datetime.datetime.now()
return {
"timestamp": str(timestamp),
"message": message
}
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
try:
self.connect()
except FileNotFoundError as e:
raise UncloudException("Is the path to the etcd certs correct? {}".format(e))
@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 get_prefix(self, key, as_json=False, **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)
yield (k, value)
@readable_errors
def set(self, key, value, as_json=False, **kwargs):
if as_json:
value = json.dumps(value)
log.debug("Setting {} = {}".format(self.realkey(key), value))
# FIXME: iterate over clients in case of failure ?
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
def increment(self, key, **kwargs):
print(self.realkey(key))
print("prelock")
lock = self._db_clients[0].lock('/nicohack/foo')
print("prelockacq")
lock.acquire()
print("prelockrelease")
lock.release()
with self._db_clients[0].lock("/nicohack/mac/last_used_index") as lock:
print("in lock")
pass
# with self._db_clients[0].lock(self.realkey(key)) as lock:# value = int(self.get(self.realkey(key), **kwargs))
# self.set(self.realkey(key), str(value + 1), **kwargs)
if __name__ == '__main__':
endpoints = [ "https://etcd1.ungleich.ch:2379",
"https://etcd2.ungleich.ch:2379",
"https://etcd3.ungleich.ch:2379" ]
db = DB(url=endpoints)

View file

@ -0,0 +1,3 @@
*.iso
radvdpid
foo

View file

@ -0,0 +1,6 @@
#!/bin/sh
etcdctl --cert=$HOME/vcs/ungleich-dot-cdist/files/etcd/nico.pem \
--key=/home/nico/vcs/ungleich-dot-cdist/files/etcd/nico-key.pem \
--cacert=$HOME/vcs/ungleich-dot-cdist/files/etcd/ca.pem \
--endpoints https://etcd1.ungleich.ch:2379,https://etcd2.ungleich.ch:2379,https://etcd3.ungleich.ch:2379 "$@"

View file

@ -0,0 +1,3 @@
#!/bin/sh
echo $@

View file

@ -0,0 +1,7 @@
#!/bin/sh
dev=$1; shift
# bridge is setup from outside
ip link set dev "$dev" master ${bridge}
ip link set dev "$dev" up

View file

@ -0,0 +1 @@
000000000252

View file

@ -0,0 +1 @@
02:00

Some files were not shown because too many files have changed in this diff Show more