From 347843cb247e0c4c3710f65a486c502b994f7be1 Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 10:22:15 +0500 Subject: [PATCH 01/14] Sample config file added + uncloud dependency removed --- etcd_wrapper.py | 14 +++++++------- sample-pay.conf | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 sample-pay.conf diff --git a/etcd_wrapper.py b/etcd_wrapper.py index 9624677..0f55271 100644 --- a/etcd_wrapper.py +++ b/etcd_wrapper.py @@ -1,11 +1,9 @@ import etcd3 import json +import logging 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=True): @@ -26,12 +24,14 @@ def readable_errors(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.ConnectionFailedError as err: + raise etcd3.exceptions.ConnectionFailedError( + 'Cannot connect to etcd: is etcd running as configured in uncloud.conf?' + ) from err except etcd3.exceptions.ConnectionTimeoutError as err: raise etcd3.exceptions.ConnectionTimeoutError('etcd connection timeout.') from err - except Exception as err: - logger.exception('Some etcd error occured. See syslog for details.', err) + except Exception: + logging.exception('Some etcd error occured. See syslog for details.') return wrapper diff --git a/sample-pay.conf b/sample-pay.conf new file mode 100644 index 0000000..bed5dbe --- /dev/null +++ b/sample-pay.conf @@ -0,0 +1,19 @@ +[etcd] +host = 127.0.0.1 +port = 2379 + +[stripe] +private_key=stripe_private_key + +[app] +port = 5000 + +[ldap] +server = ldap_server_url +admin_dn = ldap_admin_dn +admin_password = ldap_admin_password +customer_dn = ldap_customer_dn +user_dn = ldap_user_dn + +internal_user_ou = users +customer_ou = customer \ No newline at end of file From e37592bdc6c2179c3d2c17951789f6fa5493715b Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 11:59:54 +0500 Subject: [PATCH 02/14] README.md updated and reorganized, Improved error handling for configparser and ldap manager, requirements.txt added --- .gitignore | 3 +-- README.md | 22 ++++++++++++++-------- config.py | 25 +++++++++++++++++++++---- ldap_manager.py | 11 +++++++++-- requirements.txt | 4 ++++ sample-pay.conf | 5 ----- schemas.py | 6 ++---- stripe_utils.py | 12 ++++++++---- ucloud_pay.py | 15 +++++++++++++-- 9 files changed, 72 insertions(+), 31 deletions(-) create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 77de841..304c492 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ __pycache__/ pay.conf -log.txt -test.py \ No newline at end of file +log.txt \ No newline at end of file diff --git a/README.md b/README.md index 1b50cf3..6dae6b9 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Currently handles very basic features, such as: #### 1. Adding of products ```shell script -http --json http://[::]:5000/product/add email=your_email_here password=your_password_here specs:=@ipv6-only-vm.json +http --json http://[::]:5000/product/add username=your_username_here password=your_password_here specs:=@ipv6-only-vm.json ``` #### 2. Listing of products @@ -24,20 +24,26 @@ http --json http://[::]:5000/product/add email=your_email_here password=your_pas http --json http://[::]:5000/product/list ``` -#### 3. Ordering products +#### 3. Registering user's payment method (credit card for now using Stripe) + ```shell script -http --json http://[::]:5000/product/order email=your_email_here password=your_password_here product_id=5332cb89453d495381e2b2167f32c842 cpu=1 ram=1gb os-disk-space=10gb os=alpine +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. Listing users orders +#### 4. Ordering products + +First of all, user have to buy the membership first. ```shell script -http --json GET http://[::]:5000/order/list email=your_email_here password=your_password_here +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. Registering user's payment method (credit card for now using Stripe) +#### 5. Listing users orders ```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" email=your_email_here password=your_password_here -``` \ No newline at end of file +http --json POST http://[::]:5000/order/list username=your_username_here password=your_password_here +``` diff --git a/config.py b/config.py index b951830..4d5e16a 100644 --- a/config.py +++ b/config.py @@ -1,11 +1,28 @@ import configparser +import sys +import os + from etcd_wrapper import EtcdWrapper from ldap_manager import LdapManager +config_file = os.environ.get('meow-pay-config-file', default='pay.conf') + config = configparser.ConfigParser() -config.read('pay.conf') -etcd_client = EtcdWrapper(host=config['etcd']['host'], port=config['etcd']['port']) +try: + successfully_read_files = config.read(config_file) +except configparser.Error as err: + sys.exit(err) -ldap_manager = LdapManager(server=config['ldap']['server'], admin_dn=config['ldap']['admin_dn'], - admin_password=config['ldap']['admin_password']) \ No newline at end of file +if not successfully_read_files: + sys.exit(f'Config file {config_file} couldn\'t be read.') + +try: + etcd_client = EtcdWrapper(host=config.get('etcd', 'host'), port=config.get('etcd', 'port')) + + ldap_manager = LdapManager( + server=config.get('ldap', 'server'), admin_dn=config.get('ldap', 'admin_dn'), + admin_password=config.get('ldap', 'admin_password') + ) +except configparser.Error as err: + sys.exit(f'{err} in config file {config_file}.') diff --git a/ldap_manager.py b/ldap_manager.py index 382afab..c0a793f 100644 --- a/ldap_manager.py +++ b/ldap_manager.py @@ -1,14 +1,22 @@ import hashlib import random import base64 +import sys from ldap3 import Server, Connection, ObjectDef, Reader, ALL +from ldap3.core import exceptions + +SALT_BYTES = 15 class LdapManager: def __init__(self, server, admin_dn, admin_password): self.server = Server(server, get_info=ALL) - self.conn = Connection(server, admin_dn, admin_password, auto_bind=True) + try: + self.conn = Connection(server, admin_dn, admin_password, auto_bind=True) + except exceptions.LDAPException as err: + sys.exit(f'LDAP Error: {err}') + self.person_obj_def = ObjectDef('inetOrgPerson', self.conn) def get(self, query=None, search_base='dc=ungleich,dc=ch'): @@ -57,7 +65,6 @@ class LdapManager: which can be used as LDAP value, e.g. after armoring it once more using base64 or decoding it to unicode from ``ascii``. """ - SALT_BYTES = 15 sha1 = hashlib.sha1() salt = random.SystemRandom().getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, 'little') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..843641e --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +ldap3 +etcd3 +stripe +flask diff --git a/sample-pay.conf b/sample-pay.conf index bed5dbe..7138838 100644 --- a/sample-pay.conf +++ b/sample-pay.conf @@ -12,8 +12,3 @@ port = 5000 server = ldap_server_url admin_dn = ldap_admin_dn admin_password = ldap_admin_password -customer_dn = ldap_customer_dn -user_dn = ldap_user_dn - -internal_user_ou = users -customer_ou = customer \ No newline at end of file diff --git a/schemas.py b/schemas.py index 106b591..25555f9 100644 --- a/schemas.py +++ b/schemas.py @@ -3,11 +3,9 @@ import config import json import math -from config import ldap_manager +from config import ldap_manager, etcd_client from helper import resolve_product -etcd_client = config.etcd_client - class ValidationException(Exception): """Validation Error""" @@ -105,7 +103,7 @@ class AddProductSchema(BaseSchema): 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['ldap']['internal_user_ou']: + 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) diff --git a/stripe_utils.py b/stripe_utils.py index 9474f74..1004b86 100644 --- a/stripe_utils.py +++ b/stripe_utils.py @@ -1,12 +1,16 @@ -import json import re import stripe import stripe.error import logging +import sys -from config import etcd_client as client, config as config +from configparser import Error as ConfigParserError +from config import etcd_client as client, config as config, config_file -stripe.api_key = config['stripe']['private_key'] +try: + stripe.api_key = config.get('stripe', 'private_key') +except ConfigParserError as err: + sys.exit(f'{err} in config file {config_file}') def handle_stripe_error(f): @@ -291,7 +295,7 @@ class StripeUtils(object): returns the new object. :param amount: The amount in CHF cents - :param name: The name of the Stripe plan to be created. + :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 diff --git a/ucloud_pay.py b/ucloud_pay.py index 09c5813..fc45951 100644 --- a/ucloud_pay.py +++ b/ucloud_pay.py @@ -5,7 +5,7 @@ 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 ( @@ -322,4 +322,15 @@ if __name__ == '__main__': api.add_resource(UserRegisterPayment, '/user/register_payment') api.add_resource(OrderList, '/order/list') - app.run(host='::', port=config['app']['port'], debug=True) + 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 From 5f1f451bc2569a7c7291dfdea112d5dbe541f4e0 Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 13:12:07 +0500 Subject: [PATCH 03/14] Added installation and getting started instructions in README.md --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6dae6b9..0f12883 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,17 @@ The pay module for the uncloud - uses [Stripe](https://stripe.com/docs/api) as the payment gateway. - uses [ldap3](https://github.com/cannatag/ldap3) for ldap authentication. -## Getting started +## Installation -**TODO** +```shell script +pip3 install -r requirements.txt +``` + +## Getting Started + +```shell script +python ucloud_pay.py +``` ## Usage From 7b9a970307e164abcc24ca7aac1f59f96027dfa3 Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 13:12:46 +0500 Subject: [PATCH 04/14] Update README.md --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 0f12883..bd2a663 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,6 @@ python ucloud_pay.py ## Usage -Currently handles very basic features, such as: - #### 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 From 519279ce6ffcf92ab119cd8a184fb6a1e2f9e24b Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 13:13:39 +0500 Subject: [PATCH 05/14] Update README.md --- README.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/README.md b/README.md index bd2a663..fe6a2a3 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,6 @@ # uncloud-pay -The pay module for the uncloud - -- uses [etcd3](https://coreos.com/blog/etcd3-a-new-etcd.html) for storage. -- uses [Stripe](https://stripe.com/docs/api) as the payment gateway. -- uses [ldap3](https://github.com/cannatag/ldap3) for ldap authentication. +The generic product/payment system. ## Installation From ce709c3b6f029a5486789b3772cf1d47a315eee2 Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 14:44:19 +0500 Subject: [PATCH 06/14] Add certificates option for etcd --- config.py | 8 ++++++-- sample-pay.conf | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/config.py b/config.py index 4d5e16a..4e000c9 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,7 @@ from ldap_manager import LdapManager config_file = os.environ.get('meow-pay-config-file', default='pay.conf') -config = configparser.ConfigParser() +config = configparser.ConfigParser(allow_no_value=True) try: successfully_read_files = config.read(config_file) @@ -18,7 +18,11 @@ if not successfully_read_files: sys.exit(f'Config file {config_file} couldn\'t be read.') try: - etcd_client = EtcdWrapper(host=config.get('etcd', 'host'), port=config.get('etcd', 'port')) + 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'), diff --git a/sample-pay.conf b/sample-pay.conf index 7138838..5d1fe61 100644 --- a/sample-pay.conf +++ b/sample-pay.conf @@ -1,6 +1,9 @@ [etcd] host = 127.0.0.1 port = 2379 +ca_cert +cert_cert +cert_key [stripe] private_key=stripe_private_key From cee92f2e9920d72d046021532ef2475c639c5865 Mon Sep 17 00:00:00 2001 From: meow Date: Thu, 20 Feb 2020 00:12:11 +0500 Subject: [PATCH 07/14] A lot of code moved to ungleich-common --- config.py | 39 ++++++++-------------- etcd_wrapper.py | 75 ----------------------------------------- ldap_manager.py | 76 ------------------------------------------ requirements.txt | 1 + schemas.py | 86 +----------------------------------------------- stripe_utils.py | 9 ++--- 6 files changed, 18 insertions(+), 268 deletions(-) delete mode 100644 etcd_wrapper.py delete mode 100644 ldap_manager.py diff --git a/config.py b/config.py index 4e000c9..d8092d4 100644 --- a/config.py +++ b/config.py @@ -1,32 +1,21 @@ -import configparser -import sys import os -from etcd_wrapper import EtcdWrapper -from ldap_manager import LdapManager +from ungleich_common.etcd_wrapper import EtcdWrapper +from ungleich_common.ldap_manager import LdapManager +from ungleich_common.config_parser import StrictConfigParser config_file = os.environ.get('meow-pay-config-file', default='pay.conf') -config = configparser.ConfigParser(allow_no_value=True) +config = StrictConfigParser(allow_no_value=True) +config.read(config_file) -try: - successfully_read_files = config.read(config_file) -except configparser.Error as err: - sys.exit(err) +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') +) -if not successfully_read_files: - sys.exit(f'Config file {config_file} couldn\'t be read.') - -try: - 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') - ) -except configparser.Error as err: - sys.exit(f'{err} in config file {config_file}.') +ldap_manager = LdapManager( + server=config.get('ldap', 'server'), admin_dn=config.get('ldap', 'admin_dn'), + admin_password=config.get('ldap', 'admin_password') +) diff --git a/etcd_wrapper.py b/etcd_wrapper.py deleted file mode 100644 index 0f55271..0000000 --- a/etcd_wrapper.py +++ /dev/null @@ -1,75 +0,0 @@ -import etcd3 -import json -import logging - -from functools import wraps - - -class EtcdEntry: - def __init__(self, meta_or_key, value, value_in_json=True): - 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 as err: - raise etcd3.exceptions.ConnectionFailedError( - 'Cannot connect to etcd: is etcd running as configured in uncloud.conf?' - ) from err - except etcd3.exceptions.ConnectionTimeoutError as err: - raise etcd3.exceptions.ConnectionTimeoutError('etcd connection timeout.') from err - except Exception: - logging.exception('Some etcd error occured. See syslog for details.') - - return wrapper - - -class EtcdWrapper: - @readable_errors - def __init__(self, *args, **kwargs): - self.client = etcd3.client(*args, **kwargs) - - @readable_errors - def get(self, *args, value_in_json=True, **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=True, **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=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, value_in_json=True): - event_iterator, cancel = self.client.watch_prefix(key) - for e in event_iterator: - if hasattr(e, '_event'): - e = getattr('e', '_event') - if e.type == e.PUT: - yield EtcdEntry(e.kv.key, e.kv.value, value_in_json=value_in_json) diff --git a/ldap_manager.py b/ldap_manager.py deleted file mode 100644 index c0a793f..0000000 --- a/ldap_manager.py +++ /dev/null @@ -1,76 +0,0 @@ -import hashlib -import random -import base64 -import sys - -from ldap3 import Server, Connection, ObjectDef, Reader, ALL -from ldap3.core import exceptions - -SALT_BYTES = 15 - - -class LdapManager: - def __init__(self, server, admin_dn, admin_password): - self.server = Server(server, get_info=ALL) - try: - self.conn = Connection(server, admin_dn, admin_password, auto_bind=True) - except exceptions.LDAPException as err: - sys.exit(f'LDAP Error: {err}') - - self.person_obj_def = ObjectDef('inetOrgPerson', self.conn) - - def get(self, query=None, search_base='dc=ungleich,dc=ch'): - kwargs = { - 'connection': self.conn, - 'object_def': self.person_obj_def, - 'base': search_base, - } - if query: - kwargs['query'] = query - r = Reader(**kwargs) - return r.search() - - def is_password_valid(self, query_value, password, query_key='mail', **kwargs): - entries = self.get(query='({}={})'.format(query_key, query_value), **kwargs) - if entries: - password_in_ldap = entries[0].userPassword.value - found = self._check_password(password_in_ldap, password) - if not found: - raise Exception('Invalid Password') - else: - return entries[0] - else: - raise ValueError('Such {}={} not found'.format(query_key, query_value)) - - @staticmethod - def _check_password(tagged_digest_salt, password): - digest_salt_b64 = tagged_digest_salt[6:] - digest_salt = base64.decodebytes(digest_salt_b64) - digest = digest_salt[:20] - salt = digest_salt[20:] - - sha = hashlib.sha1(password.encode('utf-8')) - sha.update(salt) - - return digest == sha.digest() - - @staticmethod - def ssha_password(password): - """ - Apply the SSHA password hashing scheme to the given *password*. - *password* must be a :class:`bytes` object, containing the utf-8 - encoded password. - - Return a :class:`bytes` object containing ``ascii``-compatible data - which can be used as LDAP value, e.g. after armoring it once more using - base64 or decoding it to unicode from ``ascii``. - """ - - sha1 = hashlib.sha1() - salt = random.SystemRandom().getrandbits(SALT_BYTES * 8).to_bytes(SALT_BYTES, 'little') - sha1.update(password) - sha1.update(salt) - - digest = sha1.digest() - passwd = b'{SSHA}' + base64.b64encode(digest + salt) - return passwd diff --git a/requirements.txt b/requirements.txt index 843641e..6b6c77b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ ldap3 etcd3 stripe flask +git+git://code.ungleich.ch/ahmedbilal/ungleich-common@master#egg=ungleich-common \ No newline at end of file diff --git a/schemas.py b/schemas.py index 25555f9..8285491 100644 --- a/schemas.py +++ b/schemas.py @@ -5,91 +5,7 @@ import math from config import ldap_manager, etcd_client from helper import resolve_product - - -class ValidationException(Exception): - """Validation Error""" - - -class Field: - def __init__(self, _name, _type, _value=None, validators=None, disable_validation=False): - self.validation_disabled = disable_validation - self.name = _name - self.value = _value - self.type = _type - self.validators = validators or [] - - def is_valid(self): - if not self.validation_disabled: - if not isinstance(self.value, self.type): - try: - self.value = self.type(self.value) - except Exception: - raise ValidationException("Incorrect Type for '{}' field".format(self.name)) - - for validator in self.validators: - validator() - - def __repr__(self): - return self.name - - -class BaseSchema: - def __init__(self): - self.objects = {} - - def validation(self): - # custom validation is optional - return True - - def get_fields(self): - return [getattr(self, field) for field in dir(self) if isinstance(getattr(self, field), Field)] - - def is_valid(self): - for field in self.get_fields(): - field.is_valid() - self.validation() - - def get_cleaned_values(self): - field_kv_dict = { - field.name: field.value - for field in self.get_fields() - } - cleaned_values = field_kv_dict - cleaned_values.update(self.objects) - - return cleaned_values - - def add_schema(self, schema, data, under_field_name=None): - s = schema(data) - s.is_valid() - - base = self - if under_field_name: - # Create a field in self - setattr(self, under_field_name, Field(under_field_name, dict, _value={}, disable_validation=True)) - base = getattr(self, under_field_name) - - for field in s.get_fields(): - if under_field_name: - getattr(base, 'value')[field.name] = field.value - else: - setattr(base, field.name, field) - - self.objects.update(s.objects) - - @staticmethod - def get(dictionary: dict, key: str, return_default=False, default=None): - if dictionary is None: - raise ValidationException('No data provided at all.') - try: - value = dictionary[key] - except KeyError: - if return_default: - return {'_value': default, 'disable_validation': True} - raise ValidationException("Missing data for '{}' field.".format(key)) - else: - return {'_value': value, 'disable_validation': False} +from ungleich_common.schemas import BaseSchema, Field, ValidationException class AddProductSchema(BaseSchema): diff --git a/stripe_utils.py b/stripe_utils.py index 1004b86..a125474 100644 --- a/stripe_utils.py +++ b/stripe_utils.py @@ -2,15 +2,10 @@ import re import stripe import stripe.error import logging -import sys -from configparser import Error as ConfigParserError -from config import etcd_client as client, config as config, config_file +from config import etcd_client as client, config as config -try: - stripe.api_key = config.get('stripe', 'private_key') -except ConfigParserError as err: - sys.exit(f'{err} in config file {config_file}') +stripe.api_key = config.get('stripe', 'private_key') def handle_stripe_error(f): From 9c7d458eecfd72de02608de7d92e9dcd3d17a9bc Mon Sep 17 00:00:00 2001 From: meow Date: Thu, 20 Feb 2020 13:57:32 +0500 Subject: [PATCH 08/14] use code from ungleich-common --- config.py | 7 +++---- requirements.txt | 5 ++++- schemas.py | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/config.py b/config.py index d8092d4..c3cd6a6 100644 --- a/config.py +++ b/config.py @@ -1,9 +1,8 @@ import os -from ungleich_common.etcd_wrapper import EtcdWrapper -from ungleich_common.ldap_manager import LdapManager -from ungleich_common.config_parser import StrictConfigParser - +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) diff --git a/requirements.txt b/requirements.txt index 6b6c77b..292cf99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,7 @@ ldap3 etcd3 stripe flask -git+git://code.ungleich.ch/ahmedbilal/ungleich-common@master#egg=ungleich-common \ No newline at end of file +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 \ No newline at end of file diff --git a/schemas.py b/schemas.py index 8285491..2e3aef7 100644 --- a/schemas.py +++ b/schemas.py @@ -5,7 +5,7 @@ import math from config import ldap_manager, etcd_client from helper import resolve_product -from ungleich_common.schemas import BaseSchema, Field, ValidationException +from ungleich_common.schemas.schemas import BaseSchema, Field, ValidationException class AddProductSchema(BaseSchema): From 00b35e0567de86360fe214c2b302a0d74fdfa8c0 Mon Sep 17 00:00:00 2001 From: meow Date: Thu, 20 Feb 2020 14:04:53 +0500 Subject: [PATCH 09/14] cleaned requirements.txt --- config.py | 1 + requirements.txt | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/config.py b/config.py index c3cd6a6..16804af 100644 --- a/config.py +++ b/config.py @@ -3,6 +3,7 @@ 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) diff --git a/requirements.txt b/requirements.txt index 292cf99..6b1ec6b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -ldap3 -etcd3 stripe flask git+https://code.ungleich.ch/ahmedbilal/ungleich-common/#egg=ungleich-common-etcd&subdirectory=etcd From bb18f6b0e93841ee80a54f136f7ffb0d9337cfcb Mon Sep 17 00:00:00 2001 From: meow Date: Thu, 20 Feb 2020 14:08:39 +0500 Subject: [PATCH 10/14] Flask-RESTful added in requirements.txt --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 6b1ec6b..cb4f2a8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ 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 From 8c353f277cb8158ec94b850bf8989a95bbfb9643 Mon Sep 17 00:00:00 2001 From: meow Date: Thu, 20 Feb 2020 15:23:15 +0500 Subject: [PATCH 11/14] is_order_valid added in helper.py --- helper.py | 21 +++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 22 insertions(+) diff --git a/helper.py b/helper.py index d1a5dd4..65a5155 100644 --- a/helper.py +++ b/helper.py @@ -1,5 +1,8 @@ import logging +import parsedatetime + +from datetime import datetime from stripe_utils import StripeUtils @@ -64,3 +67,21 @@ def calculate_charges(specification, data): 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 diff --git a/requirements.txt b/requirements.txt index cb4f2a8..0f5d0d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ +done stripe flask Flask-RESTful From d61a7e670f562af0ce0d858715b4bf14997c05e3 Mon Sep 17 00:00:00 2001 From: meow Date: Fri, 21 Feb 2020 20:33:37 +0500 Subject: [PATCH 12/14] opennebula vm sync/query application added --- nicohack202002/uncloud/opennebula/__init__.py | 0 nicohack202002/uncloud/opennebula/admin.py | 3 ++ nicohack202002/uncloud/opennebula/apps.py | 5 +++ .../opennebula/management/commands/syncvm.py | 40 +++++++++++++++++++ .../opennebula/migrations/0001_initial.py | 23 +++++++++++ .../migrations/0002_auto_20200221_1024.py | 23 +++++++++++ .../migrations/0003_auto_20200221_1113.py | 21 ++++++++++ .../uncloud/opennebula/migrations/__init__.py | 0 nicohack202002/uncloud/opennebula/models.py | 8 ++++ .../uncloud/opennebula/serializers.py | 8 ++++ nicohack202002/uncloud/opennebula/tests.py | 3 ++ nicohack202002/uncloud/opennebula/views.py | 14 +++++++ nicohack202002/uncloud/uncloud/settings.py | 11 ++--- nicohack202002/uncloud/uncloud/urls.py | 11 +++-- nicohack202002/uncloud/uncloud_auth/models.py | 1 + requirements.txt | 3 ++ 16 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 nicohack202002/uncloud/opennebula/__init__.py create mode 100644 nicohack202002/uncloud/opennebula/admin.py create mode 100644 nicohack202002/uncloud/opennebula/apps.py create mode 100644 nicohack202002/uncloud/opennebula/management/commands/syncvm.py create mode 100644 nicohack202002/uncloud/opennebula/migrations/0001_initial.py create mode 100644 nicohack202002/uncloud/opennebula/migrations/0002_auto_20200221_1024.py create mode 100644 nicohack202002/uncloud/opennebula/migrations/0003_auto_20200221_1113.py create mode 100644 nicohack202002/uncloud/opennebula/migrations/__init__.py create mode 100644 nicohack202002/uncloud/opennebula/models.py create mode 100644 nicohack202002/uncloud/opennebula/serializers.py create mode 100644 nicohack202002/uncloud/opennebula/tests.py create mode 100644 nicohack202002/uncloud/opennebula/views.py diff --git a/nicohack202002/uncloud/opennebula/__init__.py b/nicohack202002/uncloud/opennebula/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nicohack202002/uncloud/opennebula/admin.py b/nicohack202002/uncloud/opennebula/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/nicohack202002/uncloud/opennebula/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/nicohack202002/uncloud/opennebula/apps.py b/nicohack202002/uncloud/opennebula/apps.py new file mode 100644 index 0000000..0750576 --- /dev/null +++ b/nicohack202002/uncloud/opennebula/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OpennebulaConfig(AppConfig): + name = 'opennebula' diff --git a/nicohack202002/uncloud/opennebula/management/commands/syncvm.py b/nicohack202002/uncloud/opennebula/management/commands/syncvm.py new file mode 100644 index 0000000..5ea451d --- /dev/null +++ b/nicohack202002/uncloud/opennebula/management/commands/syncvm.py @@ -0,0 +1,40 @@ +import os +import json + +from django.core.management.base import BaseCommand +from django.contrib.auth import get_user_model +from xmlrpc.client import ServerProxy as RPCClient + +from xmltodict import parse + +from opennebula.models import VM as VMModel + +OCA_SESSION_STRING = os.environ.get('OCASECRETS', '') + + +class Command(BaseCommand): + help = 'Syncronize VM information from OpenNebula' + + def add_arguments(self, parser): + pass + + def handle(self, *args, **options): + with RPCClient('https://opennebula.ungleich.ch:2634/RPC2') as rpc_client: + success, response, *_ = rpc_client.one.vmpool.infoextended( + OCA_SESSION_STRING, -2, -1, -1, -1 + ) + if success: + vms = json.loads(json.dumps(parse(response)))['VM_POOL']['VM'] + for i, vm in enumerate(vms): + vm_id = vm['ID'] + vm_owner = vm['UNAME'] + try: + user = get_user_model().objects.get(username=vm_owner) + except get_user_model().DoesNotExist: + user = get_user_model().objects.create_user(username=vm_owner) + + vm_object = VMModel.objects.create(vmid=vm_id, owner=user, data=vm) + vm_object.save() + else: + print(response) + diff --git a/nicohack202002/uncloud/opennebula/migrations/0001_initial.py b/nicohack202002/uncloud/opennebula/migrations/0001_initial.py new file mode 100644 index 0000000..e2c6a1f --- /dev/null +++ b/nicohack202002/uncloud/opennebula/migrations/0001_initial.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-02-21 10:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='VM', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('vmid', models.IntegerField()), + ('owner', models.CharField(max_length=128)), + ('data', models.CharField(max_length=65536)), + ], + ), + ] diff --git a/nicohack202002/uncloud/opennebula/migrations/0002_auto_20200221_1024.py b/nicohack202002/uncloud/opennebula/migrations/0002_auto_20200221_1024.py new file mode 100644 index 0000000..43b7442 --- /dev/null +++ b/nicohack202002/uncloud/opennebula/migrations/0002_auto_20200221_1024.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.3 on 2020-02-21 10:24 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('opennebula', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='vm', + name='data', + field=models.CharField(max_length=65536, null=True), + ), + migrations.AlterField( + model_name='vm', + name='owner', + field=models.CharField(max_length=128, null=True), + ), + ] diff --git a/nicohack202002/uncloud/opennebula/migrations/0003_auto_20200221_1113.py b/nicohack202002/uncloud/opennebula/migrations/0003_auto_20200221_1113.py new file mode 100644 index 0000000..9ccc22e --- /dev/null +++ b/nicohack202002/uncloud/opennebula/migrations/0003_auto_20200221_1113.py @@ -0,0 +1,21 @@ +# Generated by Django 3.0.3 on 2020-02-21 11:13 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('opennebula', '0002_auto_20200221_1024'), + ] + + operations = [ + migrations.AlterField( + model_name='vm', + name='owner', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/nicohack202002/uncloud/opennebula/migrations/__init__.py b/nicohack202002/uncloud/opennebula/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/nicohack202002/uncloud/opennebula/models.py b/nicohack202002/uncloud/opennebula/models.py new file mode 100644 index 0000000..cd1a044 --- /dev/null +++ b/nicohack202002/uncloud/opennebula/models.py @@ -0,0 +1,8 @@ +from django.db import models +from django.contrib.auth import get_user_model + + +class VM(models.Model): + vmid = models.IntegerField() + owner = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + data = models.CharField(max_length=65536, null=True) diff --git a/nicohack202002/uncloud/opennebula/serializers.py b/nicohack202002/uncloud/opennebula/serializers.py new file mode 100644 index 0000000..c84f2ab --- /dev/null +++ b/nicohack202002/uncloud/opennebula/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from opennebula.models import VM + + +class VMSerializer(serializers.HyperlinkedModelSerializer): + class Meta: + model = VM + fields = ['vmid', 'owner', 'data'] diff --git a/nicohack202002/uncloud/opennebula/tests.py b/nicohack202002/uncloud/opennebula/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/nicohack202002/uncloud/opennebula/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/nicohack202002/uncloud/opennebula/views.py b/nicohack202002/uncloud/opennebula/views.py new file mode 100644 index 0000000..f706815 --- /dev/null +++ b/nicohack202002/uncloud/opennebula/views.py @@ -0,0 +1,14 @@ +from rest_framework import viewsets, generics +from .models import VM +from .serializers import VMSerializer + + +class VMList(generics.ListAPIView): + queryset = VM.objects.all() + serializer_class = VMSerializer + + +class VMDetail(generics.RetrieveAPIView): + lookup_field = 'vmid' + queryset = VM.objects.all() + serializer_class = VMSerializer diff --git a/nicohack202002/uncloud/uncloud/settings.py b/nicohack202002/uncloud/uncloud/settings.py index be38f8f..1e8f358 100644 --- a/nicohack202002/uncloud/uncloud/settings.py +++ b/nicohack202002/uncloud/uncloud/settings.py @@ -39,7 +39,8 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'rest_framework', 'uncloud_api', - 'uncloud_auth' + 'uncloud_auth', + 'opennebula' ] MIDDLEWARE = [ @@ -159,7 +160,7 @@ STATIC_URL = '/static/' # Uncommitted file -import uncloud.secrets - -import stripe -stripe.api_key = uncloud.secrets.STRIPE_KEY +# import uncloud.secrets +# +# import stripe +# stripe.api_key = uncloud.secrets.STRIPE_KEY diff --git a/nicohack202002/uncloud/uncloud/urls.py b/nicohack202002/uncloud/uncloud/urls.py index cb50432..f5804c9 100644 --- a/nicohack202002/uncloud/uncloud/urls.py +++ b/nicohack202002/uncloud/uncloud/urls.py @@ -19,6 +19,8 @@ from django.urls import path, include from rest_framework import routers from uncloud_api import views +from opennebula import views as oneviews + router = routers.DefaultRouter() router.register(r'users', views.UserViewSet) router.register(r'groups', views.GroupViewSet) @@ -28,10 +30,7 @@ router.register(r'groups', views.GroupViewSet) urlpatterns = [ path('', include(router.urls)), path('admin/', admin.site.urls), - path('api-auth/', include('rest_framework.urls', namespace='rest_framework')) + path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), + path('vm/list/', oneviews.VMList.as_view(), name='vm_list'), + path('vm/detail//', oneviews.VMDetail.as_view(), name='vm_detail'), ] - -#urlpatterns = [ -# path('admin/', admin.site.urls), -# path('api/', include('api.urls')), -#] diff --git a/nicohack202002/uncloud/uncloud_auth/models.py b/nicohack202002/uncloud/uncloud_auth/models.py index 4c9c171..3d30525 100644 --- a/nicohack202002/uncloud/uncloud_auth/models.py +++ b/nicohack202002/uncloud/uncloud_auth/models.py @@ -1,4 +1,5 @@ from django.contrib.auth.models import AbstractUser + class User(AbstractUser): pass diff --git a/requirements.txt b/requirements.txt index 29c21b4..1abfbed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,6 @@ +xmltodict +djangorestframework +django done stripe flask From b1bb6bc314c44ac028ae27ce664caec22c7ff3b2 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 22 Feb 2020 00:22:42 +0100 Subject: [PATCH 13/14] Make products available via getattr --- .../opennebula/management/commands/syncvm.py | 8 ++- nicohack202002/uncloud/requirements.txt | 1 + .../uncloud/uncloud/secrets_sample.py | 8 ++- nicohack202002/uncloud/uncloud/urls.py | 2 + .../uncloud_api/management/commands/hack.py | 26 +++++++++ nicohack202002/uncloud/uncloud_api/models.py | 54 ++++++++++++++++--- .../uncloud/uncloud_api/serializers.py | 3 ++ nicohack202002/uncloud/uncloud_api/views.py | 45 +++++++++++++++- notes-nico.org | 9 ++++ plan.org | 6 +++ 10 files changed, 147 insertions(+), 15 deletions(-) create mode 100644 nicohack202002/uncloud/uncloud_api/management/commands/hack.py create mode 100644 plan.org diff --git a/nicohack202002/uncloud/opennebula/management/commands/syncvm.py b/nicohack202002/uncloud/opennebula/management/commands/syncvm.py index 5ea451d..205b066 100644 --- a/nicohack202002/uncloud/opennebula/management/commands/syncvm.py +++ b/nicohack202002/uncloud/opennebula/management/commands/syncvm.py @@ -9,8 +9,7 @@ from xmltodict import parse from opennebula.models import VM as VMModel -OCA_SESSION_STRING = os.environ.get('OCASECRETS', '') - +import uncloud.secrets class Command(BaseCommand): help = 'Syncronize VM information from OpenNebula' @@ -19,9 +18,9 @@ class Command(BaseCommand): pass def handle(self, *args, **options): - with RPCClient('https://opennebula.ungleich.ch:2634/RPC2') as rpc_client: + with RPCClient(uncloud.secrets.OPENNEBULA_URL) as rpc_client: success, response, *_ = rpc_client.one.vmpool.infoextended( - OCA_SESSION_STRING, -2, -1, -1, -1 + uncloud.secrets.OPENNEBULA_USER_PASS, -2, -1, -1, -1 ) if success: vms = json.loads(json.dumps(parse(response)))['VM_POOL']['VM'] @@ -37,4 +36,3 @@ class Command(BaseCommand): vm_object.save() else: print(response) - diff --git a/nicohack202002/uncloud/requirements.txt b/nicohack202002/uncloud/requirements.txt index d81b59b..11ab309 100644 --- a/nicohack202002/uncloud/requirements.txt +++ b/nicohack202002/uncloud/requirements.txt @@ -2,3 +2,4 @@ django djangorestframework django-auth-ldap stripe +xmltodict diff --git a/nicohack202002/uncloud/uncloud/secrets_sample.py b/nicohack202002/uncloud/uncloud/secrets_sample.py index a895bc9..d145124 100644 --- a/nicohack202002/uncloud/uncloud/secrets_sample.py +++ b/nicohack202002/uncloud/uncloud/secrets_sample.py @@ -7,5 +7,11 @@ - +# Live/test key from stripe STRIPE_KEY="" + +# XML-RPC interface of opennebula +OPENNEBULA_URL='https://opennebula.ungleich.ch:2634/RPC2' + +# user:pass for accessing opennebula +OPENNEBULA_USER_PASS='user:password' diff --git a/nicohack202002/uncloud/uncloud/urls.py b/nicohack202002/uncloud/uncloud/urls.py index f5804c9..c7ce9b6 100644 --- a/nicohack202002/uncloud/uncloud/urls.py +++ b/nicohack202002/uncloud/uncloud/urls.py @@ -30,7 +30,9 @@ router.register(r'groups', views.GroupViewSet) urlpatterns = [ path('', include(router.urls)), path('admin/', admin.site.urls), + path('products/', views.ProductsView.as_view(), name='products'), path('api-auth/', include('rest_framework.urls', namespace='rest_framework')), path('vm/list/', oneviews.VMList.as_view(), name='vm_list'), path('vm/detail//', oneviews.VMDetail.as_view(), name='vm_detail'), + ] diff --git a/nicohack202002/uncloud/uncloud_api/management/commands/hack.py b/nicohack202002/uncloud/uncloud_api/management/commands/hack.py new file mode 100644 index 0000000..e129952 --- /dev/null +++ b/nicohack202002/uncloud/uncloud_api/management/commands/hack.py @@ -0,0 +1,26 @@ +import time +from django.conf import settings +from django.core.management.base import BaseCommand + +import uncloud_api.models + +import inspect +import sys +import re + +class Command(BaseCommand): + args = '' + help = 'hacking - only use if you are Nico' + + def add_arguments(self, parser): + parser.add_argument('command', type=str, help='Command') + + def handle(self, *args, **options): + getattr(self, options['command'])(**options) + + @classmethod + def classtest(cls, **_): + clsmembers = inspect.getmembers(sys.modules['uncloud_api.models'], inspect.isclass) + for name, c in clsmembers: + if re.match(r'.+Product$', name): + print("{} -> {}".format(name, c)) diff --git a/nicohack202002/uncloud/uncloud_api/models.py b/nicohack202002/uncloud/uncloud_api/models.py index 06e77ed..6df17c4 100644 --- a/nicohack202002/uncloud/uncloud_api/models.py +++ b/nicohack202002/uncloud/uncloud_api/models.py @@ -3,9 +3,38 @@ import uuid from django.db import models from django.contrib.auth import get_user_model +# Product in DB vs. product in code +# DB: +# - need to define params (+param types) in db -> messy? +# - get /products/ is easy / automatic +# +# code +# - can have serializer/verification of fields easily in DRF +# - can have per product side effects / extra code running +# - might (??) make features easier?? +# - how to setup / query the recurring period (?) +# - could get products list via getattr() + re ...Product() classes +# -> this could include the url for ordering => /order/vm_snapshot (params) +# ---> this would work with urlpatterns +# Combination: create specific product in DB (?) +# - a table per product (?) with 1 entry? + +# Orders +# define state in DB +# select a price from a product => product might change, order stays +# params: +# - the product uuid or name (?) => productuuid +# - the product parameters => for each feature +# + +# logs +# Should have a log = ... => 1:n field for most models! class Product(models.Model): + + description = "" + uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) name = models.CharField(max_length=256) @@ -21,10 +50,18 @@ class Product(models.Model): default="not_recurring" ) + # params = [ vmuuid, ... ] + # features -> required as defined + def __str__(self): return "{}".format(self.name) +class VMSnapshotProduct(Product): + # need to setup recurring_periodd + + description = "Create snapshot of a VM" + class Feature(models.Model): uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -35,6 +72,15 @@ class Feature(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE) + # params for "cpu": cpu_count -> int + # each feature can only have one parameters + # could call this "value" and set whether it is user usable + # has_value = True/False + # value = string -> int (?) + # value_int + # value_str + # value_float + def __str__(self): return "'{}' - '{}'".format(self.product, self.name) @@ -49,11 +95,5 @@ class Order(models.Model): on_delete=models.CASCADE) - -class OrderReference(models.Model): - """ - An order can references another product / relate to it. - This model is used for the relation - """ - +class VMSnapshotOrder(Order): pass diff --git a/nicohack202002/uncloud/uncloud_api/serializers.py b/nicohack202002/uncloud/uncloud_api/serializers.py index 57532f2..1573bf0 100644 --- a/nicohack202002/uncloud/uncloud_api/serializers.py +++ b/nicohack202002/uncloud/uncloud_api/serializers.py @@ -14,3 +14,6 @@ class GroupSerializer(serializers.HyperlinkedModelSerializer): class Meta: model = Group fields = ['url', 'name'] + +class VMSnapshotSerializer(serializers.Serializer): + pass diff --git a/nicohack202002/uncloud/uncloud_api/views.py b/nicohack202002/uncloud/uncloud_api/views.py index 88e0543..8cf76f2 100644 --- a/nicohack202002/uncloud/uncloud_api/views.py +++ b/nicohack202002/uncloud/uncloud_api/views.py @@ -2,9 +2,11 @@ from django.shortcuts import render from django.contrib.auth import get_user_model from django.contrib.auth.models import Group -from rest_framework import viewsets, permissions - +from rest_framework import viewsets, permissions, generics from .serializers import UserSerializer, GroupSerializer +from rest_framework.views import APIView +from rest_framework.response import Response + class CreditCardViewSet(viewsets.ModelViewSet): @@ -35,3 +37,42 @@ class GroupViewSet(viewsets.ModelViewSet): serializer_class = GroupSerializer permission_classes = [permissions.IsAuthenticated] + +class GroupViewSet(viewsets.ModelViewSet): + """ + API endpoint that allows groups to be viewed or edited. + """ + queryset = Group.objects.all() + serializer_class = GroupSerializer + + permission_classes = [permissions.IsAuthenticated] + + +# POST /vm/snapshot/ vmuuid=... => create snapshot, returns snapshot uuid +# GET /vm/snapshot => list +# DEL /vm/snapshot/ => delete +# create-list -> get, post => ListCreateAPIView +# del on other! +class VMSnapshotView(generics.ListCreateAPIView): + #lookup_field = 'uuid' + permission_classes = [permissions.IsAuthenticated] + +import inspect +import sys +import re + +class ProductsView(APIView): + def get(self, request, format=None): + clsmembers = inspect.getmembers(sys.modules['uncloud_api.models'], inspect.isclass) + products = [] + for name, c in clsmembers: + # Include everything that ends in Product, but not Product itself + if re.search(r'.+Product$', name): + products.append({ + 'name': name, + 'description': c.description + } + ) + + + return Response(products) diff --git a/notes-nico.org b/notes-nico.org index 21102f9..93e0c00 100644 --- a/notes-nico.org +++ b/notes-nico.org @@ -1,5 +1,14 @@ * 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=... +``` * steps ** DONE authenticate via ldap CLOSED: [2020-02-20 Thu 19:05] diff --git a/plan.org b/plan.org new file mode 100644 index 0000000..9f172c2 --- /dev/null +++ b/plan.org @@ -0,0 +1,6 @@ +* TODO register CC +* TODO list products +* ahmed +** schemas +*** field: is_valid? - used by schemas +*** definition of a "schema" From 4f4a4be8396316df064f4acd8f61a4dc184e3fb0 Mon Sep 17 00:00:00 2001 From: Nico Schottelius Date: Sat, 22 Feb 2020 00:50:06 +0100 Subject: [PATCH 14/14] good night commit - introducing status --- nicohack202002/uncloud/uncloud_api/models.py | 62 ++++++++++++++------ nicohack202002/uncloud/uncloud_api/views.py | 11 +++- 2 files changed, 51 insertions(+), 22 deletions(-) diff --git a/nicohack202002/uncloud/uncloud_api/models.py b/nicohack202002/uncloud/uncloud_api/models.py index 6df17c4..fafefe6 100644 --- a/nicohack202002/uncloud/uncloud_api/models.py +++ b/nicohack202002/uncloud/uncloud_api/models.py @@ -32,35 +32,59 @@ from django.contrib.auth import get_user_model # Should have a log = ... => 1:n field for most models! class Product(models.Model): - + # override these fields by default description = "" + recurring_period = "not_recurring" - uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=256) - - recurring_period = models.CharField(max_length=256, - choices = ( - ("per_year", "Per Year"), - ("per_month", "Per Month"), - ("per_week", "Per Week"), - ("per_day", "Per Day"), - ("per_hour", "Per Hour"), - ("not_recurring", "Not recurring") - ), - default="not_recurring" - ) - - # params = [ vmuuid, ... ] - # features -> required as defined + status = models.CharField(max_length=256, + choices = ( + ('pending', 'Pending'), + ('being_created', 'Being created'), + ('created_active', 'Created'), + ('deleted', 'Deleted') + ) def __str__(self): return "{}".format(self.name) class VMSnapshotProduct(Product): - # need to setup recurring_periodd + price_per_gb_ssd = 0.35 + price_per_gb_hdd = 1.5/100 + + sample_ssd = 10 + sample_hdd = 100 + + def recurring_price(self): + return 0 + + def one_time_price(self): + return 0 + + @classmethod + def sample_price(cls): + return cls.sample_ssd * cls.price_per_gb_ssd + cls.sample_hdd * cls.price_per_gb_hdd description = "Create snapshot of a VM" + recurring_period = "monthly" + + @classmethod + def pricing_model(cls): + return """ +Pricing is on monthly basis and storage prices are equivalent to the storage +price in the VM. + +Price per GB SSD is: {} +Price per GB HDD is: {} + + +Sample price for a VM with {} GB SSD and {} GB HDD VM is: {}. +""".format(cls.price_per_gb_ssd, cls.price_per_gb_hdd, + cls.sample_ssd, cls.sample_hdd, cls.sample_price()) + + gb_ssd = models.FloatField() + gb_hdd = models.FloatField() + class Feature(models.Model): diff --git a/nicohack202002/uncloud/uncloud_api/views.py b/nicohack202002/uncloud/uncloud_api/views.py index 8cf76f2..68963ff 100644 --- a/nicohack202002/uncloud/uncloud_api/views.py +++ b/nicohack202002/uncloud/uncloud_api/views.py @@ -61,16 +61,21 @@ import inspect import sys import re +# Next: create /order/ urls +# Next: strip off "Product" at the end class ProductsView(APIView): def get(self, request, format=None): clsmembers = inspect.getmembers(sys.modules['uncloud_api.models'], inspect.isclass) products = [] for name, c in clsmembers: # Include everything that ends in Product, but not Product itself - if re.search(r'.+Product$', name): + m = re.match(r'(?P.+)Product$', name) + if m: products.append({ - 'name': name, - 'description': c.description + 'name': m.group('pname'), + 'description': c.description, + 'recurring_period': c.recurring_period, + 'pricing_model': c.pricing_model() } )