From 347843cb247e0c4c3710f65a486c502b994f7be1 Mon Sep 17 00:00:00 2001 From: meow Date: Wed, 19 Feb 2020 10:22:15 +0500 Subject: [PATCH 01/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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/12] 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