Compare commits
	
		
			16 Commits
		
	
	
		
			docker-api
			...
			mock-hub-c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 6dbe2b5c86 | |||
| f7461d5a28 | |||
| 9f23638959 | |||
| 8f2becb9ee | |||
| f848eda931 | |||
| 46f49e8d8f | |||
| 36329796f0 | |||
| 28271ee852 | |||
| 7923f3a99f | |||
| 7ed847251f | |||
| e3a4776a5d | |||
| 357d99cb91 | |||
| b8279d7491 | |||
| 908d02803f | |||
| 6e6bd2b143 | |||
| 47fbaab403 | 
							
								
								
									
										14
									
								
								.drone.yml
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								.drone.yml
									
									
									
									
									
								
							| @ -1,14 +0,0 @@ | |||||||
| --- |  | ||||||
| kind: pipeline |  | ||||||
| name: publish docker image |  | ||||||
| steps: |  | ||||||
|   - name: build and publish |  | ||||||
|     image: plugins/docker |  | ||||||
|     settings: |  | ||||||
|       username: |  | ||||||
|         from_secret: docker_reg_username_3wc |  | ||||||
|       password: |  | ||||||
|         from_secret: docker_reg_passwd_3wc |  | ||||||
|       repo: 3wordchant/capsul-flask |  | ||||||
|       tags: latest |  | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								Dockerfile
									
									
									
									
									
								
							| @ -1,32 +0,0 @@ | |||||||
| FROM python:3.8-alpine as build |  | ||||||
|  |  | ||||||
| RUN apk add gettext git gcc python3-dev musl-dev \ |  | ||||||
|     libffi-dev zlib-dev jpeg-dev libjpeg postgresql-dev build-base \ |  | ||||||
|     --virtual .build-dependencies |  | ||||||
|  |  | ||||||
| RUN mkdir -p /app/{code,venv} |  | ||||||
| WORKDIR /app/code |  | ||||||
| COPY Pipfile Pipfile.lock /app/code/ |  | ||||||
|  |  | ||||||
| RUN python3 -m venv /app/venv |  | ||||||
| RUN pip install pipenv setuptools |  | ||||||
| ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv" |  | ||||||
| RUN pip install wheel cppy |  | ||||||
| # Install dependencies into the virtual environment with Pipenv |  | ||||||
| RUN pipenv install --deploy --verbose |  | ||||||
|  |  | ||||||
| FROM python:3.8-alpine |  | ||||||
|  |  | ||||||
| RUN apk add --no-cache libpq libstdc++ libjpeg libvirt-client |  | ||||||
|  |  | ||||||
| COPY . /app/code/ |  | ||||||
| WORKDIR /app/code |  | ||||||
|  |  | ||||||
| COPY --from=build /app/venv /app/venv |  | ||||||
| ENV PATH="/app/venv/bin:$PATH" VIRTUAL_ENV="/app/venv" |  | ||||||
|  |  | ||||||
| CMD ["gunicorn", "--bind", "0.0.0.0:5000", "-k", "gevent", "--worker-connections", "1000", "app:app"] |  | ||||||
|  |  | ||||||
| VOLUME /app/code |  | ||||||
|  |  | ||||||
| EXPOSE 5000 |  | ||||||
| @ -31,7 +31,6 @@ load_dotenv(find_dotenv()) | |||||||
| app = Flask(__name__) | app = Flask(__name__) | ||||||
|  |  | ||||||
| app.config.from_mapping( | app.config.from_mapping( | ||||||
|    |  | ||||||
|   BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"), |   BASE_URL=os.environ.get("BASE_URL", default="http://localhost:5000"), | ||||||
|   SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"), |   SECRET_KEY=os.environ.get("SECRET_KEY", default="dev"), | ||||||
|   HUB_MODE_ENABLED=os.environ.get("HUB_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], |   HUB_MODE_ENABLED=os.environ.get("HUB_MODE_ENABLED", default="True").lower() in ['true', '1', 't', 'y', 'yes'], | ||||||
| @ -72,7 +71,7 @@ app.config.from_mapping( | |||||||
|   #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") |   #STRIPE_WEBHOOK_SECRET=os.environ.get("STRIPE_WEBHOOK_SECRET", default="") | ||||||
|  |  | ||||||
|   BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"), |   BTCPAY_PRIVATE_KEY=os.environ.get("BTCPAY_PRIVATE_KEY", default="").replace("\\n", "\n"), | ||||||
|   BTCPAY_URL=os.environ.get("BTCPAY_URL", default="https://btcpay.cyberia.club") |   BTCPAY_URL=os.environ.get("BTCPAY_URL", default="") | ||||||
| ) | ) | ||||||
|  |  | ||||||
| app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL']) | app.config['HUB_URL'] = os.environ.get("HUB_URL", default=app.config['BASE_URL']) | ||||||
| @ -140,8 +139,11 @@ else: | |||||||
|  |  | ||||||
| app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) | app.config['HTTP_CLIENT'] = MyHTTPClient(timeout_seconds=int(app.config['INTERNAL_HTTP_TIMEOUT_SECONDS'])) | ||||||
|  |  | ||||||
|  | app.config['BTCPAY_ENABLED'] = False | ||||||
|  | if app.config['BTCPAY_URL'] != "": | ||||||
|   try: |   try: | ||||||
|     app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY']) |     app.config['BTCPAY_CLIENT'] = btcpay.Client(api_uri=app.config['BTCPAY_URL'], pem=app.config['BTCPAY_PRIVATE_KEY']) | ||||||
|  |     app.config['BTCPAY_ENABLED'] = True | ||||||
|   except: |   except: | ||||||
|     app.logger.warning("unable to create btcpay client. Capsul will work fine except cryptocurrency payments will not work. The error was: " + my_exec_info_message(sys.exc_info())) |     app.logger.warning("unable to create btcpay client. Capsul will work fine except cryptocurrency payments will not work. The error was: " + my_exec_info_message(sys.exc_info())) | ||||||
|  |  | ||||||
| @ -153,6 +155,7 @@ is_running_server = ('flask run' in command_line) or ('gunicorn' in command_line | |||||||
| app.logger.info(f"is_running_server: {is_running_server}") | app.logger.info(f"is_running_server: {is_running_server}") | ||||||
|  |  | ||||||
| if app.config['HUB_MODE_ENABLED']: | if app.config['HUB_MODE_ENABLED']: | ||||||
|  |  | ||||||
|   if app.config['HUB_MODEL'] == "capsul-flask": |   if app.config['HUB_MODEL'] == "capsul-flask": | ||||||
|     app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub() |     app.config['HUB_MODEL'] = hub_model.CapsulFlaskHub() | ||||||
|  |  | ||||||
| @ -174,9 +177,7 @@ if app.config['HUB_MODE_ENABLED']: | |||||||
|   from capsulflask import db |   from capsulflask import db | ||||||
|   db.init_app(app, is_running_server) |   db.init_app(app, is_running_server) | ||||||
|  |  | ||||||
|   from capsulflask import ( |   from capsulflask import auth, landing, console, payment, metrics, cli, hub_api, admin | ||||||
|     auth, landing, console, payment, metrics, cli, hub_api, publicapi, admin |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|   app.register_blueprint(landing.bp) |   app.register_blueprint(landing.bp) | ||||||
|   app.register_blueprint(auth.bp) |   app.register_blueprint(auth.bp) | ||||||
| @ -186,13 +187,13 @@ if app.config['HUB_MODE_ENABLED']: | |||||||
|   app.register_blueprint(cli.bp) |   app.register_blueprint(cli.bp) | ||||||
|   app.register_blueprint(hub_api.bp) |   app.register_blueprint(hub_api.bp) | ||||||
|   app.register_blueprint(admin.bp) |   app.register_blueprint(admin.bp) | ||||||
|   app.register_blueprint(publicapi.bp) |  | ||||||
|  |  | ||||||
|   app.add_url_rule("/", endpoint="index") |   app.add_url_rule("/", endpoint="index") | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if app.config['SPOKE_MODE_ENABLED']: | if app.config['SPOKE_MODE_ENABLED']: | ||||||
|  |  | ||||||
|   if app.config['SPOKE_MODEL'] == "shell-scripts": |   if app.config['SPOKE_MODEL'] == "shell-scripts": | ||||||
|     app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke() |     app.config['SPOKE_MODEL'] = spoke_model.ShellScriptSpoke() | ||||||
|   else: |   else: | ||||||
|  | |||||||
| @ -1,4 +1,3 @@ | |||||||
| from base64 import b64decode |  | ||||||
| import functools | import functools | ||||||
| import re | import re | ||||||
|  |  | ||||||
| @ -25,15 +24,6 @@ def account_required(view): | |||||||
|  |  | ||||||
|     @functools.wraps(view) |     @functools.wraps(view) | ||||||
|     def wrapped_view(**kwargs): |     def wrapped_view(**kwargs): | ||||||
|         api_token = request.headers.get('authorization', None) |  | ||||||
|         if api_token is not None: |  | ||||||
|             email = get_model().authenticate_token(b64decode(api_token).decode('utf-8')) |  | ||||||
|  |  | ||||||
|             if email is not None: |  | ||||||
|                 session.clear() |  | ||||||
|                 session["account"] = email |  | ||||||
|                 session["csrf-token"] = generate() |  | ||||||
|  |  | ||||||
|         if session.get("account") is None or session.get("csrf-token") is None : |         if session.get("account") is None or session.get("csrf-token") is None : | ||||||
|             return redirect(url_for("auth.login")) |             return redirect(url_for("auth.login")) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,9 +1,7 @@ | |||||||
| from base64 import b64encode |  | ||||||
| from datetime import datetime, timedelta |  | ||||||
| import json |  | ||||||
| import re | import re | ||||||
| import sys | import sys | ||||||
|  | import json | ||||||
|  | from datetime import datetime, timedelta | ||||||
| from flask import Blueprint | from flask import Blueprint | ||||||
| from flask import flash | from flask import flash | ||||||
| from flask import current_app | from flask import current_app | ||||||
| @ -100,6 +98,7 @@ def index(): | |||||||
| @bp.route("/<string:id>", methods=("GET", "POST")) | @bp.route("/<string:id>", methods=("GET", "POST")) | ||||||
| @account_required | @account_required | ||||||
| def detail(id): | def detail(id): | ||||||
|  |  | ||||||
|   duration=request.args.get('duration') |   duration=request.args.get('duration') | ||||||
|   if not duration: |   if not duration: | ||||||
|     duration = "5m" |     duration = "5m" | ||||||
| @ -189,13 +188,23 @@ def detail(id): | |||||||
|       duration=duration |       duration=duration | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
| def _create(vm_sizes, operating_systems, public_keys_for_account, server_data): |  | ||||||
|  | @bp.route("/create", methods=("GET", "POST")) | ||||||
|  | @account_required | ||||||
|  | def create(): | ||||||
|  |   vm_sizes = get_model().vm_sizes_dict() | ||||||
|  |   operating_systems = get_model().operating_systems_dict() | ||||||
|  |   public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"]) | ||||||
|  |   account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow()) | ||||||
|  |   capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024) | ||||||
|   errors = list() |   errors = list() | ||||||
|  |  | ||||||
|   size = server_data.get("size") |   if request.method == "POST": | ||||||
|   os = server_data.get("os") |     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: | ||||||
|   posted_keys_count = int(server_data.get("ssh_authorized_key_count")) |       return abort(418, f"u want tea") | ||||||
|  |  | ||||||
|  |     size = request.form["size"] | ||||||
|  |     os = request.form["os"] | ||||||
|     if not size: |     if not size: | ||||||
|       errors.append("Size is required") |       errors.append("Size is required") | ||||||
|     elif size not in vm_sizes: |     elif size not in vm_sizes: | ||||||
| @ -206,14 +215,15 @@ def _create(vm_sizes, operating_systems, public_keys_for_account, server_data): | |||||||
|     elif os not in operating_systems: |     elif os not in operating_systems: | ||||||
|       errors.append(f"Invalid os {os}") |       errors.append(f"Invalid os {os}") | ||||||
|  |  | ||||||
|  |     posted_keys_count = int(request.form["ssh_authorized_key_count"]) | ||||||
|     posted_keys = list() |     posted_keys = list() | ||||||
|  |  | ||||||
|     if posted_keys_count > 1000: |     if posted_keys_count > 1000: | ||||||
|       errors.append("something went wrong with ssh keys") |       errors.append("something went wrong with ssh keys") | ||||||
|     else: |     else: | ||||||
|       for i in range(0, posted_keys_count): |       for i in range(0, posted_keys_count): | ||||||
|       if f"ssh_key_{i}" in server_data: |         if f"ssh_key_{i}" in request.form: | ||||||
|         posted_name = server_data.get(f"ssh_key_{i}") |           posted_name = request.form[f"ssh_key_{i}"] | ||||||
|           key = None |           key = None | ||||||
|           for x in public_keys_for_account: |           for x in public_keys_for_account: | ||||||
|             if x['name'] == posted_name: |             if x['name'] == posted_name: | ||||||
| @ -226,9 +236,7 @@ def _create(vm_sizes, operating_systems, public_keys_for_account, server_data): | |||||||
|     if len(posted_keys) == 0: |     if len(posted_keys) == 0: | ||||||
|       errors.append("At least one SSH Public Key is required") |       errors.append("At least one SSH Public Key is required") | ||||||
|  |  | ||||||
|   capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable( |     capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(vm_sizes[size]['memory_mb']*1024*1024) | ||||||
|     vm_sizes[size]['memory_mb']*1024*1024 |  | ||||||
|   ) |  | ||||||
|  |  | ||||||
|     if not capacity_avaliable: |     if not capacity_avaliable: | ||||||
|       errors.append(""" |       errors.append(""" | ||||||
| @ -237,45 +245,19 @@ def _create(vm_sizes, operating_systems, public_keys_for_account, server_data): | |||||||
|  |  | ||||||
|     if len(errors) == 0: |     if len(errors) == 0: | ||||||
|       id = make_capsul_id() |       id = make_capsul_id() | ||||||
|     get_model().create_vm( |       # we can't create the vm record in the DB yet because its IP address needs to be allocated first. | ||||||
|       email=session["account"],  |       # so it will be created when the allocation happens inside the hub_api. | ||||||
|       id=id,  |  | ||||||
|       size=size,  |  | ||||||
|       os=os, |  | ||||||
|       ssh_authorized_keys=list(map(lambda x: x["name"], posted_keys)) |  | ||||||
|     ) |  | ||||||
|       current_app.config["HUB_MODEL"].create( |       current_app.config["HUB_MODEL"].create( | ||||||
|         email = session["account"], |         email = session["account"], | ||||||
|         id=id, |         id=id, | ||||||
|  |         os=os, | ||||||
|  |         size=size, | ||||||
|         template_image_file_name=operating_systems[os]['template_image_file_name'], |         template_image_file_name=operating_systems[os]['template_image_file_name'], | ||||||
|         vcpus=vm_sizes[size]['vcpus'], |         vcpus=vm_sizes[size]['vcpus'], | ||||||
|         memory_mb=vm_sizes[size]['memory_mb'], |         memory_mb=vm_sizes[size]['memory_mb'], | ||||||
|       ssh_authorized_keys=list(map(lambda x: x["content"], posted_keys)) |         ssh_authorized_keys=list(map(lambda x: dict(name=x['name'], content=x['content']), posted_keys))  | ||||||
|       ) |       ) | ||||||
|     return id, errors |  | ||||||
|        |        | ||||||
|   return None, errors |  | ||||||
|  |  | ||||||
| @bp.route("/create", methods=("GET", "POST")) |  | ||||||
| @account_required |  | ||||||
| def create(): |  | ||||||
|   vm_sizes = get_model().vm_sizes_dict() |  | ||||||
|   operating_systems = get_model().operating_systems_dict() |  | ||||||
|   public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"]) |  | ||||||
|   account_balance = get_account_balance(get_vms(), get_payments(), datetime.utcnow()) |  | ||||||
|   capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024) |  | ||||||
|  |  | ||||||
|   if request.method == "POST": |  | ||||||
|     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: |  | ||||||
|       return abort(418, f"u want tea") |  | ||||||
|     id, errors = _create( |  | ||||||
|       vm_sizes,  |  | ||||||
|       operating_systems, |  | ||||||
|       public_keys_for_account, |  | ||||||
|       request.form) |  | ||||||
|     if len(errors) == 0:  |  | ||||||
|       for error in errors: |  | ||||||
|         flash(error) |  | ||||||
|       return redirect(f"{url_for('console.index')}?created={id}") |       return redirect(f"{url_for('console.index')}?created={id}") | ||||||
|    |    | ||||||
|   affordable_vm_sizes = dict() |   affordable_vm_sizes = dict() | ||||||
| @ -286,6 +268,9 @@ def create(): | |||||||
|     if vm_size["dollars_per_month"] <= account_balance+0.25: |     if vm_size["dollars_per_month"] <= account_balance+0.25: | ||||||
|       affordable_vm_sizes[key] = vm_size |       affordable_vm_sizes[key] = vm_size | ||||||
|  |  | ||||||
|  |   for error in errors: | ||||||
|  |     flash(error) | ||||||
|  |  | ||||||
|   if not capacity_avaliable: |   if not capacity_avaliable: | ||||||
|     current_app.logger.warning(f"when capsul capacity is restored, send an email to {session['account']}") |     current_app.logger.warning(f"when capsul capacity is restored, send an email to {session['account']}") | ||||||
|  |  | ||||||
| @ -302,25 +287,23 @@ def create(): | |||||||
|     vm_sizes=affordable_vm_sizes |     vm_sizes=affordable_vm_sizes | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
| @bp.route("/keys", methods=("GET", "POST")) | @bp.route("/ssh", methods=("GET", "POST")) | ||||||
| @account_required | @account_required | ||||||
| def ssh_api_keys(): | def ssh_public_keys(): | ||||||
|   errors = list() |   errors = list() | ||||||
|  |  | ||||||
|   token = None |  | ||||||
|  |  | ||||||
|   if request.method == "POST": |   if request.method == "POST": | ||||||
|     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: |     if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: | ||||||
|       return abort(418, f"u want tea") |       return abort(418, f"u want tea") | ||||||
|        |        | ||||||
|     action = request.form["action"] |     method = request.form["method"] | ||||||
|  |  | ||||||
|     if action == 'upload_ssh_key': |  | ||||||
|     content = None |     content = None | ||||||
|  |     if method == "POST": | ||||||
|       content = request.form["content"].replace("\r", " ").replace("\n", " ").strip() |       content = request.form["content"].replace("\r", " ").replace("\n", " ").strip() | ||||||
|        |        | ||||||
|     name = request.form["name"] |     name = request.form["name"] | ||||||
|     if not name or len(name.strip()) < 1: |     if not name or len(name.strip()) < 1: | ||||||
|  |       if method == "POST": | ||||||
|         parts = re.split(" +", content) |         parts = re.split(" +", content) | ||||||
|         if len(parts) > 2 and len(parts[2].strip()) > 0: |         if len(parts) > 2 and len(parts[2].strip()) > 0: | ||||||
|           name = parts[2].strip() |           name = parts[2].strip() | ||||||
| @ -331,6 +314,7 @@ def ssh_api_keys(): | |||||||
|     if not re.match(r"^[0-9A-Za-z_@:. -]+$", name): |     if not re.match(r"^[0-9A-Za-z_@:. -]+$", name): | ||||||
|       errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"") |       errors.append(f"Key name '{name}' must match \"^[0-9A-Za-z_@:. -]+$\"") | ||||||
|  |  | ||||||
|  |     if method == "POST": | ||||||
|       if not content or len(content.strip()) < 1: |       if not content or len(content.strip()) < 1: | ||||||
|         errors.append("Content is required") |         errors.append("Content is required") | ||||||
|       else: |       else: | ||||||
| @ -343,36 +327,24 @@ def ssh_api_keys(): | |||||||
|       if len(errors) == 0: |       if len(errors) == 0: | ||||||
|         get_model().create_ssh_public_key(session["account"], name, content) |         get_model().create_ssh_public_key(session["account"], name, content) | ||||||
|  |  | ||||||
|     elif action == "delete_ssh_key": |     elif method == "DELETE": | ||||||
|  |  | ||||||
|  |       if len(errors) == 0: | ||||||
|         get_model().delete_ssh_public_key(session["account"], name) |         get_model().delete_ssh_public_key(session["account"], name) | ||||||
|  |  | ||||||
|     elif action == "generate_api_token": |  | ||||||
|       name = request.form["name"] |  | ||||||
|       if name == '': |  | ||||||
|           name = datetime.utcnow().strftime('%y-%m-%d %H:%M:%S') |  | ||||||
|       token = b64encode( |  | ||||||
|         get_model().generate_api_token(session["account"], name).encode('utf-8') |  | ||||||
|       ).decode('utf-8') |  | ||||||
|  |  | ||||||
|     elif action == "delete_api_token": |  | ||||||
|       get_model().delete_api_token(session["account"], request.form["id"]) |  | ||||||
|  |  | ||||||
|   for error in errors: |   for error in errors: | ||||||
|     flash(error) |     flash(error) | ||||||
|  |  | ||||||
|   ssh_keys_list=list(map( |   keys_list=list(map( | ||||||
|     lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),  |     lambda x: dict(name=x['name'], content=f"{x['content'][:20]}...{x['content'][len(x['content'])-20:]}"),  | ||||||
|     get_model().list_ssh_public_keys_for_account(session["account"]) |     get_model().list_ssh_public_keys_for_account(session["account"]) | ||||||
|   )) |   )) | ||||||
|  |  | ||||||
|   api_tokens_list = get_model().list_api_tokens(session["account"]) |  | ||||||
|  |  | ||||||
|   return render_template( |   return render_template( | ||||||
|     "keys.html",  |     "ssh-public-keys.html",  | ||||||
|     csrf_token = session["csrf-token"], |     csrf_token = session["csrf-token"], | ||||||
|     api_tokens=api_tokens_list,  |     ssh_public_keys=keys_list,  | ||||||
|     ssh_public_keys=ssh_keys_list, |     has_ssh_public_keys=len(keys_list) > 0 | ||||||
|     generated_api_token=token, |  | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
| def get_vms(): | def get_vms(): | ||||||
| @ -396,6 +368,7 @@ def get_vm_months_float(vm, as_of): | |||||||
|   return days / average_number_of_days_in_a_month |   return days / average_number_of_days_in_a_month | ||||||
|  |  | ||||||
| def get_account_balance(vms, payments, as_of): | def get_account_balance(vms, payments, as_of): | ||||||
|  |  | ||||||
|   vm_cost_dollars = 0.0 |   vm_cost_dollars = 0.0 | ||||||
|   for vm in vms: |   for vm in vms: | ||||||
|     vm_months = get_vm_months_float(vm, as_of) |     vm_months = get_vm_months_float(vm, as_of) | ||||||
| @ -408,6 +381,7 @@ def get_account_balance(vms, payments, as_of): | |||||||
| @bp.route("/account-balance") | @bp.route("/account-balance") | ||||||
| @account_required | @account_required | ||||||
| def account_balance(): | def account_balance(): | ||||||
|  |  | ||||||
|   payment_sessions = get_model().list_payment_sessions_for_account(session['account']) |   payment_sessions = get_model().list_payment_sessions_for_account(session['account']) | ||||||
|   for payment_session in payment_sessions: |   for payment_session in payment_sessions: | ||||||
|     if payment_session['type'] == 'btcpay': |     if payment_session['type'] == 'btcpay': | ||||||
| @ -449,6 +423,7 @@ def account_balance(): | |||||||
|     has_vms=len(vms_billed)>0,  |     has_vms=len(vms_billed)>0,  | ||||||
|     vms_billed=vms_billed, |     vms_billed=vms_billed, | ||||||
|     warning_text=warning_text, |     warning_text=warning_text, | ||||||
|  |     btcpay_enabled=current_app.config["BTCPAY_ENABLED"], | ||||||
|     payments=list(map( |     payments=list(map( | ||||||
|       lambda x: dict( |       lambda x: dict( | ||||||
|         dollars=x["dollars"],  |         dollars=x["dollars"],  | ||||||
|  | |||||||
| @ -33,7 +33,7 @@ def init_app(app, is_running_server): | |||||||
|     result = re.search(r"^\d+_(up|down)", filename) |     result = re.search(r"^\d+_(up|down)", filename) | ||||||
|     if not result: |     if not result: | ||||||
|       app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.") |       app.logger.error(f"schemaVersion {filename} must match ^\\d+_(up|down). exiting.") | ||||||
|       continue |       exit(1) | ||||||
|     key = result.group() |     key = result.group() | ||||||
|     with open(join(schemaMigrationsPath, filename), 'rb') as file: |     with open(join(schemaMigrationsPath, filename), 'rb') as file: | ||||||
|       schemaMigrations[key] = file.read().decode("utf8") |       schemaMigrations[key] = file.read().decode("utf8") | ||||||
| @ -128,3 +128,4 @@ def close_db(e=None): | |||||||
|   if db_model is not None: |   if db_model is not None: | ||||||
|     db_model.cursor.close() |     db_model.cursor.close() | ||||||
|     current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection) |     current_app.config['PSYCOPG2_CONNECTION_POOL'].putconn(db_model.connection) | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,8 +1,8 @@ | |||||||
|  |  | ||||||
| import re | import re | ||||||
|  |  | ||||||
| # I was never able to get this type hinting to work correctly  | # I was never able to get this type hinting to work correctly  | ||||||
| # from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor | # from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor | ||||||
| import hashlib |  | ||||||
| from nanoid import generate | from nanoid import generate | ||||||
| from flask import current_app | from flask import current_app | ||||||
| from typing import List | from typing import List | ||||||
| @ -17,6 +17,7 @@ class DBModel: | |||||||
|     self.cursor = cursor |     self.cursor = cursor | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|   #     ------    LOGIN     ---------      |   #     ------    LOGIN     ---------      | ||||||
|  |  | ||||||
|  |  | ||||||
| @ -43,16 +44,6 @@ class DBModel: | |||||||
|  |  | ||||||
|     return (token, ignoreCaseMatches) |     return (token, ignoreCaseMatches) | ||||||
|      |      | ||||||
|   def authenticate_token(self, token): |  | ||||||
|     m = hashlib.md5() |  | ||||||
|     m.update(token.encode('utf-8')) |  | ||||||
|     hash_token = m.hexdigest() |  | ||||||
|     self.cursor.execute("SELECT email FROM api_tokens WHERE token = %s", (hash_token, )) |  | ||||||
|     result = self.cursor.fetchall() |  | ||||||
|     if len(result) == 1: |  | ||||||
|       return result[0] |  | ||||||
|     return None |  | ||||||
|      |  | ||||||
|   def consume_token(self, token): |   def consume_token(self, token): | ||||||
|     self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) |     self.cursor.execute("SELECT email FROM login_tokens WHERE token = %s and created > (NOW() - INTERVAL '20 min')", (token, )) | ||||||
|     row = self.cursor.fetchone() |     row = self.cursor.fetchone() | ||||||
| @ -141,32 +132,6 @@ class DBModel: | |||||||
|     self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) ) |     self.cursor.execute( "DELETE FROM ssh_public_keys where email = %s AND name = %s", (email, name) ) | ||||||
|     self.connection.commit() |     self.connection.commit() | ||||||
|  |  | ||||||
|   def list_api_tokens(self, email): |  | ||||||
|     self.cursor.execute( |  | ||||||
|       "SELECT id, token, name, created FROM api_tokens WHERE email = %s",  |  | ||||||
|       (email, ) |  | ||||||
|     ) |  | ||||||
|     return list(map( |  | ||||||
|       lambda x: dict(id=x[0], token=x[1], name=x[2], created=x[3]),  |  | ||||||
|       self.cursor.fetchall() |  | ||||||
|     )) |  | ||||||
|  |  | ||||||
|   def generate_api_token(self, email, name): |  | ||||||
|     token = generate() |  | ||||||
|     m = hashlib.md5() |  | ||||||
|     m.update(token.encode('utf-8')) |  | ||||||
|     hash_token = m.hexdigest() |  | ||||||
|     self.cursor.execute( |  | ||||||
|       "INSERT INTO api_tokens (email, name, token) VALUES (%s, %s, %s)",  |  | ||||||
|       (email, name, hash_token) |  | ||||||
|     ) |  | ||||||
|     self.connection.commit() |  | ||||||
|     return token |  | ||||||
|  |  | ||||||
|   def delete_api_token(self, email, id_): |  | ||||||
|     self.cursor.execute( "DELETE FROM api_tokens where email = %s AND id = %s", (email, id_)) |  | ||||||
|     self.connection.commit() |  | ||||||
|  |  | ||||||
|   def list_vms_for_account(self, email): |   def list_vms_for_account(self, email): | ||||||
|     self.cursor.execute("""  |     self.cursor.execute("""  | ||||||
|       SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month |       SELECT vms.id, vms.public_ipv4, vms.public_ipv6, vms.size, vms.os, vms.created, vms.deleted, vm_sizes.dollars_per_month | ||||||
| @ -514,3 +479,8 @@ class DBModel: | |||||||
|     #cursor.close() |     #cursor.close() | ||||||
|  |  | ||||||
|     return to_return |     return to_return | ||||||
|  |        | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -160,7 +160,10 @@ def can_claim_create(payload, host_id) -> (str, str): | |||||||
|   if allocated_network_name is None or allocated_ipv4_address is None: |   if allocated_network_name is None or allocated_ipv4_address is None: | ||||||
|     return "", f"host \"{host_id}\" does not have any avaliable IP addresses on any of its networks." |     return "", f"host \"{host_id}\" does not have any avaliable IP addresses on any of its networks." | ||||||
|  |  | ||||||
|   payload["network_name"] = allocated_network_name |   # payload["network_name"] = allocated_network_name | ||||||
|  |   # hard-code the network name for now until we can fix the phantom dhcp lease issues. | ||||||
|  |  | ||||||
|  |   payload["network_name"] = 'public3' | ||||||
|   payload["public_ipv4"] = allocated_ipv4_address |   payload["public_ipv4"] = allocated_ipv4_address | ||||||
|  |  | ||||||
|   return payload, "" |   return payload, "" | ||||||
|  | |||||||
| @ -17,6 +17,10 @@ from capsulflask.http_client import HTTPResult | |||||||
| from capsulflask.shared import VirtualizationInterface, VirtualMachine, OnlineHost, validate_capsul_id, my_exec_info_message | from capsulflask.shared import VirtualizationInterface, VirtualMachine, OnlineHost, validate_capsul_id, my_exec_info_message | ||||||
|  |  | ||||||
| class MockHub(VirtualizationInterface): | class MockHub(VirtualizationInterface): | ||||||
|  |   def __init__(self): | ||||||
|  |     self.default_network = "public1" | ||||||
|  |     self.default_ipv4 = "1.1.1.1" | ||||||
|  |  | ||||||
|   def capacity_avaliable(self, additional_ram_bytes): |   def capacity_avaliable(self, additional_ram_bytes): | ||||||
|     return True |     return True | ||||||
|  |  | ||||||
| @ -29,9 +33,9 @@ class MockHub(VirtualizationInterface): | |||||||
|         {"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"}, |         {"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"}, | ||||||
|         {"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"} |         {"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"} | ||||||
|       ]""") |       ]""") | ||||||
|       return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1", ssh_host_keys=ssh_host_keys) |       return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4, ssh_host_keys=ssh_host_keys) | ||||||
|  |  | ||||||
|     return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1") |     return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=self.default_ipv4) | ||||||
|  |  | ||||||
|   def list_ids(self) -> list: |   def list_ids(self) -> list: | ||||||
|     return get_model().all_non_deleted_vm_ids() |     return get_model().all_non_deleted_vm_ids() | ||||||
| @ -40,6 +44,16 @@ class MockHub(VirtualizationInterface): | |||||||
|     validate_capsul_id(id) |     validate_capsul_id(id) | ||||||
|     current_app.logger.info(f"mock create: {id} for {email}") |     current_app.logger.info(f"mock create: {id} for {email}") | ||||||
|     sleep(1) |     sleep(1) | ||||||
|  |     get_model().create_vm( | ||||||
|  |       email=email,  | ||||||
|  |       id=id,  | ||||||
|  |       size=size,  | ||||||
|  |       os=os, | ||||||
|  |       host=current_app.config["SPOKE_HOST_ID"], | ||||||
|  |       network_name=self.default_network, | ||||||
|  |       public_ipv4=self.default_ipv4, | ||||||
|  |       ssh_authorized_keys=list(map(lambda x: x["name"], ssh_authorized_keys)), | ||||||
|  |     ) | ||||||
|  |  | ||||||
|   def destroy(self, email: str, id: str): |   def destroy(self, email: str, id: str): | ||||||
|     current_app.logger.info(f"mock destroy: {id} for {email}") |     current_app.logger.info(f"mock destroy: {id} for {email}") | ||||||
| @ -49,7 +63,6 @@ class MockHub(VirtualizationInterface): | |||||||
|  |  | ||||||
|  |  | ||||||
| class CapsulFlaskHub(VirtualizationInterface): | class CapsulFlaskHub(VirtualizationInterface): | ||||||
|  |  | ||||||
|   def synchronous_operation(self, hosts: List[OnlineHost], email: str, payload: str) -> List[HTTPResult]: |   def synchronous_operation(self, hosts: List[OnlineHost], email: str, payload: str) -> List[HTTPResult]: | ||||||
|     return self.generic_operation(hosts, email, payload, True)[1] |     return self.generic_operation(hosts, email, payload, True)[1] | ||||||
|    |    | ||||||
| @ -262,4 +275,3 @@ class CapsulFlaskHub(VirtualizationInterface): | |||||||
|  |  | ||||||
|     if not result_status == "success": |     if not result_status == "success": | ||||||
|       raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") |       raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") | ||||||
|  |  | ||||||
|  | |||||||
| @ -12,9 +12,11 @@ def index(): | |||||||
|  |  | ||||||
| @bp.route("/pricing") | @bp.route("/pricing") | ||||||
| def pricing(): | def pricing(): | ||||||
|  |   vm_sizes = get_model().vm_sizes_dict() | ||||||
|   operating_systems = get_model().operating_systems_dict() |   operating_systems = get_model().operating_systems_dict() | ||||||
|   return render_template( |   return render_template( | ||||||
|     "pricing.html",  |     "pricing.html",  | ||||||
|  |     vm_sizes=vm_sizes, | ||||||
|     operating_systems=operating_systems |     operating_systems=operating_systems | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | |||||||
| @ -48,6 +48,10 @@ def validate_dollars(): | |||||||
| def btcpay_payment(): | def btcpay_payment(): | ||||||
|   errors = list() |   errors = list() | ||||||
|  |  | ||||||
|  |   if not current_app.config['BTCPAY_ENABLED']: | ||||||
|  |     flash("BTCPay is not enabled on this server") | ||||||
|  |     return redirect(url_for("console.account_balance")) | ||||||
|  |  | ||||||
|   if request.method == "POST": |   if request.method == "POST": | ||||||
|     result = validate_dollars() |     result = validate_dollars() | ||||||
|     errors = result[0] |     errors = result[0] | ||||||
|  | |||||||
| @ -1,40 +0,0 @@ | |||||||
| import datetime |  | ||||||
|  |  | ||||||
| from flask import Blueprint |  | ||||||
| from flask import current_app |  | ||||||
| from flask import jsonify |  | ||||||
| from flask import request |  | ||||||
| from flask import session |  | ||||||
| from nanoid import generate |  | ||||||
|  |  | ||||||
| from capsulflask.auth import account_required |  | ||||||
| from capsulflask.db import get_model |  | ||||||
|  |  | ||||||
| bp = Blueprint("publicapi", __name__, url_prefix="/api") |  | ||||||
|  |  | ||||||
| @bp.route("/capsul/create", methods=["POST"]) |  | ||||||
| @account_required |  | ||||||
| def capsul_create(): |  | ||||||
|     email = session["account"] |  | ||||||
|      |  | ||||||
|     from .console import _create,get_account_balance, get_payments, get_vms |  | ||||||
|  |  | ||||||
|     vm_sizes = get_model().vm_sizes_dict() |  | ||||||
|     operating_systems = get_model().operating_systems_dict() |  | ||||||
|     public_keys_for_account = get_model().list_ssh_public_keys_for_account(session["account"]) |  | ||||||
|     account_balance = get_account_balance(get_vms(), get_payments(), datetime.datetime.utcnow()) |  | ||||||
|     capacity_avaliable = current_app.config["HUB_MODEL"].capacity_avaliable(512*1024*1024) |  | ||||||
|  |  | ||||||
|     request.json['ssh_authorized_key_count'] = 1 |  | ||||||
|  |  | ||||||
|     id, errors = _create( |  | ||||||
|       vm_sizes,  |  | ||||||
|       operating_systems, |  | ||||||
|       public_keys_for_account, |  | ||||||
|       request.json) |  | ||||||
|  |  | ||||||
|     if id is not None: |  | ||||||
|         return jsonify( |  | ||||||
|             id=id, |  | ||||||
|         ) |  | ||||||
|     return jsonify(errors=errors) |  | ||||||
| @ -1,2 +0,0 @@ | |||||||
| DROP TABLE api_keys; |  | ||||||
| UPDATE schemaversion SET version = 16; |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| CREATE TABLE api_tokens ( |  | ||||||
|   id                 SERIAL PRIMARY KEY, |  | ||||||
|   email              TEXT REFERENCES accounts(email) ON DELETE RESTRICT, |  | ||||||
|   name               TEXT NOT NULL, |  | ||||||
|   created            TIMESTAMP NOT NULL DEFAULT NOW(), |  | ||||||
|   token              TEXT NOT NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| UPDATE schemaversion SET version = 17; |  | ||||||
| @ -1,2 +0,0 @@ | |||||||
| DROP TABLE api_keys; |  | ||||||
| UPDATE schemaversion SET version = 16; |  | ||||||
| @ -1,9 +0,0 @@ | |||||||
| CREATE TABLE api_tokens ( |  | ||||||
|   id                 SERIAL PRIMARY KEY, |  | ||||||
|   email              TEXT REFERENCES accounts(email) ON DELETE RESTRICT, |  | ||||||
|   name               TEXT NOT NULL, |  | ||||||
|   created            TIMESTAMP NOT NULL DEFAULT NOW(), |  | ||||||
|   token              TEXT NOT NULL |  | ||||||
| ); |  | ||||||
|  |  | ||||||
| UPDATE schemaversion SET version = 17; |  | ||||||
| @ -241,7 +241,6 @@ thead { | |||||||
|   background: #bdc7b812; |   background: #bdc7b812; | ||||||
| } | } | ||||||
| td, th { | td, th { | ||||||
|  |  | ||||||
|   padding: 0.1em 1em; |   padding: 0.1em 1em; | ||||||
| } | } | ||||||
| table.small td, table.small th { | table.small td, table.small th { | ||||||
|  | |||||||
| @ -46,7 +46,9 @@ | |||||||
|             <a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a> |             <a href="/payment/stripe">Add funds with Credit/Debit (stripe)</a> | ||||||
|             <ul><li>notice: stripe will load nonfree javascript </li></ul> |             <ul><li>notice: stripe will load nonfree javascript </li></ul> | ||||||
|           </li> |           </li> | ||||||
|  |           {% if btcpay_enabled %} | ||||||
|           <li><a href="/payment/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li> |           <li><a href="/payment/btcpay">Add funds with Bitcoin/Litecoin/Monero (btcpay)</a></li> | ||||||
|  |           {% endif %} | ||||||
|      |      | ||||||
|           <li>Cash: email <a href="mailto:treasurer@cyberia.club">treasurer@cyberia.club</a></li> |           <li>Cash: email <a href="mailto:treasurer@cyberia.club">treasurer@cyberia.club</a></li> | ||||||
|         </ul> |         </ul> | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ | |||||||
|  |  | ||||||
|     {% if session["account"] %}  |     {% if session["account"] %}  | ||||||
|       <a href="/console">Capsuls</a> |       <a href="/console">Capsuls</a> | ||||||
|       <a href="/console/keys">SSH & API Keys</a> |       <a href="/console/ssh">SSH Public Keys</a> | ||||||
|       <a href="/console/account-balance">Account Balance</a> |       <a href="/console/account-balance">Account Balance</a> | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  | |||||||
| @ -101,7 +101,7 @@ | |||||||
|       </div> |       </div> | ||||||
|       <div class="row justify-start"> |       <div class="row justify-start"> | ||||||
|         <label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label> |         <label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label> | ||||||
|         <a id="ssh_authorized_keys" href="/console/keys">{{ vm['ssh_authorized_keys'] }}</a> |         <a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a> | ||||||
|       </div> |       </div> | ||||||
|        |        | ||||||
|     </div> |     </div> | ||||||
|  | |||||||
| @ -31,7 +31,7 @@ | |||||||
|     <p>(At least one month of funding is required)</p> |     <p>(At least one month of funding is required)</p> | ||||||
|   {% elif no_ssh_public_keys %} |   {% elif no_ssh_public_keys %} | ||||||
|     <p>You don't have any ssh public keys yet.</p>  |     <p>You don't have any ssh public keys yet.</p>  | ||||||
|     <p>You must <a href="/console/keys">upload one</a> before you can create a Capsul.</p> |     <p>You must <a href="/console/ssh">upload one</a> before you can create a Capsul.</p> | ||||||
|   {% elif not capacity_avaliable %} |   {% elif not capacity_avaliable %} | ||||||
|     <p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p>  |     <p>Host(s) at capacity. No capsuls can be created at this time. sorry. </p>  | ||||||
|   {% else %} |   {% else %} | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ | |||||||
|      How do I log in? |      How do I log in? | ||||||
|      <p>ssh to the ip provided to you using the cyberian user.</p> |      <p>ssh to the ip provided to you using the cyberian user.</p> | ||||||
|      <pre class='code'>$ ssh cyberian@1.2.3.4</pre> |      <pre class='code'>$ ssh cyberian@1.2.3.4</pre> | ||||||
|  |      <p>For more information, see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>.</p> | ||||||
|    </li> |    </li> | ||||||
|    <li> |    <li> | ||||||
|      How do I change to the root user? |      How do I change to the root user? | ||||||
|  | |||||||
| @ -6,22 +6,38 @@ | |||||||
|   <div class="row third-margin"> |   <div class="row third-margin"> | ||||||
|     <h1>CAPSUL TYPES & PRICING</h1> |     <h1>CAPSUL TYPES & PRICING</h1> | ||||||
|   </div> |   </div> | ||||||
|  |   <div class="row half-margin"> | ||||||
|  |     <table> | ||||||
|  |       <thead> | ||||||
|  |         <tr> | ||||||
|  |           <th>type</th> | ||||||
|  |           <th>monthly*</th> | ||||||
|  |           <th>cpus</th> | ||||||
|  |           <th>mem</th> | ||||||
|  |           <th>ssd</th> | ||||||
|  |           <th>net</th> | ||||||
|  |         </tr> | ||||||
|  |       </thead> | ||||||
|  |       <tbody> | ||||||
|  |       {% for vm_size_key, vm_size in vm_sizes.items() %} | ||||||
|  |         <tr> | ||||||
|  |           <td>{{ vm_size_key }}</td> | ||||||
|  |           <td>${{ vm_size['dollars_per_month'] }}</td> | ||||||
|  |           <td>{{ vm_size['vcpus'] }}</td> | ||||||
|  |           <td>{{ vm_size['memory_mb'] }}</td> | ||||||
|  |           <td>25G</td> | ||||||
|  |           <td>{{ vm_size['bandwidth_gb_per_month'] }}</td> | ||||||
|  |         </tr> | ||||||
|  |       {% endfor %} | ||||||
|  |       </tbody> | ||||||
|  |     </table> | ||||||
|  |   </div> | ||||||
|   <div class="row half-margin"> |   <div class="row half-margin"> | ||||||
|     <pre> |     <pre> | ||||||
|     type     monthly*  cpus  mem     ssd   net* |  | ||||||
|     -----    -------   ----  ---     ---   --- |  | ||||||
|     f1-xs    $5.00     1     512M    25G   .5TB |  | ||||||
|     f1-s     $7.50     1     1024M   25G   1TB |  | ||||||
|     f1-m     $12.50    1     2048M   25G   2TB |  | ||||||
|     f1-l     $20.00    2     3072M   25G   3TB |  | ||||||
|     f1-x     $27.50    3     4096M   25G   4TB |  | ||||||
|     f1-xx    $50.00    4     8192M   25G   5TB |  | ||||||
|  |  | ||||||
|     * net is calculated as a per-month average |     * net is calculated as a per-month average | ||||||
|     * vms are billed for a minimum of 24 hours upon creation |     * vms are billed for a minimum of 24 hours upon creation | ||||||
|     * all VMs come standard with one public IPv4 address |     * all VMs come standard with one public IPv4 address | ||||||
|      |      | ||||||
|      |  | ||||||
|     SUPPORTED OPERATING SYSTEMS: |     SUPPORTED OPERATING SYSTEMS: | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,18 +1,17 @@ | |||||||
| {% extends 'base.html' %} | {% extends 'base.html' %} | ||||||
| 
 | 
 | ||||||
| {% block title %}SSH & API Keys{% endblock %} | {% block title %}SSH Public Keys{% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block content %} | {% block content %} | ||||||
| <div class="row third-margin"> | <div class="row third-margin"> | ||||||
|   <h1>SSH PUBLIC KEYS</h1> |   <h1>SSH PUBLIC KEYS</h1> | ||||||
| </div> | </div> | ||||||
| <div class="row third-margin"><div> | <div class="row third-margin"><div> | ||||||
|     {% if ssh_public_keys|length > 0 %} <hr/> {% endif %} |   {% if has_ssh_public_keys %} <hr/> {% endif %} | ||||||
|    |    | ||||||
|   {% for ssh_public_key in ssh_public_keys %} |   {% for ssh_public_key in ssh_public_keys %} | ||||||
|     <form method="post"> |     <form method="post"> | ||||||
|       <input type="hidden" name="method" value="DELETE"></input>  |       <input type="hidden" name="method" value="DELETE"></input>  | ||||||
|       <input type="hidden" name="action" value="delete_ssh_key"></input> |  | ||||||
|       <input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input>  |       <input type="hidden" name="name" value="{{ ssh_public_key['name'] }}"></input>  | ||||||
|       <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> |       <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||||
|       <div class="row"> |       <div class="row"> | ||||||
| @ -23,14 +22,13 @@ | |||||||
|     </form> |     </form> | ||||||
|   {% endfor %} |   {% endfor %} | ||||||
| 
 | 
 | ||||||
|   {% if ssh_public_keys|length > 0 %} <hr/> {% endif %} |   {% if has_ssh_public_keys %} <hr/> {% endif %} | ||||||
| 
 | 
 | ||||||
|   <div class="third-margin"> |   <div class="third-margin"> | ||||||
|     <h1>UPLOAD A NEW SSH PUBLIC KEY</h1> |     <h1>UPLOAD A NEW SSH PUBLIC KEY</h1> | ||||||
|   </div> |   </div> | ||||||
|   <form method="post"> |   <form method="post"> | ||||||
|     <input type="hidden" name="method" value="POST"></input> |     <input type="hidden" name="method" value="POST"></input> | ||||||
|     <input type="hidden" name="action" value="upload_ssh_key"></input> |  | ||||||
|     <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> |     <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> | ||||||
|     <div class="row justify-start"> |     <div class="row justify-start"> | ||||||
|       <label class="align" for="content">File Contents</label> |       <label class="align" for="content">File Contents</label> | ||||||
| @ -56,51 +54,6 @@ | |||||||
|     </div> |     </div> | ||||||
|   </form> |   </form> | ||||||
| </div></div> | </div></div> | ||||||
| <hr/> |  | ||||||
| <div class="row third-margin"> |  | ||||||
|   <h1>API KEYS</h1> |  | ||||||
| </div> |  | ||||||
| <div class="row third-margin"><div> |  | ||||||
|   {% if generated_api_token %} |  | ||||||
|     <hr/> |  | ||||||
|     Generated key: |  | ||||||
|     <span class="code">{{ generated_api_token }}</span> |  | ||||||
|   {% endif %} |  | ||||||
|   {% if api_tokens|length >0 %} <hr/>{% endif %} |  | ||||||
|   {% for api_token in api_tokens %} |  | ||||||
|     <form method="post"> |  | ||||||
|       <input type="hidden" name="method" value="DELETE"></input>  |  | ||||||
|       <input type="hidden" name="action" value="delete_api_token"></input> |  | ||||||
|       <input type="hidden" name="id" value="{{ api_token['id'] }}"></input>  |  | ||||||
|       <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> |  | ||||||
|       <div class="row"> |  | ||||||
|         <span class="code">{{ api_token['name'] }}</span> |  | ||||||
|         created {{ api_token['created'].strftime("%b %d %Y") }} |  | ||||||
|         <input type="submit" value="Delete"> |  | ||||||
|       </div> |  | ||||||
|     </form> |  | ||||||
|   {% endfor %} |  | ||||||
|   {% if api_tokens|length >0 %} <hr/>{% endif %} |  | ||||||
| 
 |  | ||||||
|   <div class="third-margin"> |  | ||||||
|     <h1>GENERATE A NEW API KEY</h1> |  | ||||||
|   </div> |  | ||||||
|   <form method="post"> |  | ||||||
|     <input type="hidden" name="method" value="POST"></input> |  | ||||||
|     <input type="hidden" name="action" value="generate_api_token"></input> |  | ||||||
|     <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> |  | ||||||
|     <div class="smalltext"> |  | ||||||
|       <p>Generate a new API key, to integrate with other systems.</p> |  | ||||||
|     </div> |  | ||||||
|     <div class="row justify-start"> |  | ||||||
|       <label class="align" for="name">Key Name</label> |  | ||||||
|       <input type="text" id="name" name="name"></input> (defaults to creation time) |  | ||||||
|     </div> |  | ||||||
|     <div class="row justify-end"> |  | ||||||
|       <input type="submit" value="Generate"> |  | ||||||
|     </div> |  | ||||||
|   </form> |  | ||||||
| </div></div> |  | ||||||
| {% endblock %} | {% endblock %} | ||||||
| 
 | 
 | ||||||
| {% block pagesource %}/templates/ssh-public-keys.html{% endblock %} | {% block pagesource %}/templates/ssh-public-keys.html{% endblock %} | ||||||
| @ -1,29 +0,0 @@ | |||||||
| --- |  | ||||||
| version: "3.8" |  | ||||||
|  |  | ||||||
| services: |  | ||||||
|   app: |  | ||||||
|     image: 3wordchant/capsul-flask:latest |  | ||||||
|     build: . |  | ||||||
|     volumes: |  | ||||||
|       - "./:/app/code" |  | ||||||
|     depends_on: |  | ||||||
|       - db |  | ||||||
|     ports: |  | ||||||
|       - "5000:5000" |  | ||||||
|     environment: |  | ||||||
|       - "POSTGRES_CONNECTION_PARAMETERS=host=db port=5432 user=capsul password=capsul dbname=capsul" |  | ||||||
|     # The image uses gunicorn by default, let's override it with Flask's |  | ||||||
|     # built-in development server |  | ||||||
|     command: ["flask", "run", "-h", "0.0.0.0", "-p", "5000"] |  | ||||||
|   db: |  | ||||||
|     image: "postgres:9.6.5" |  | ||||||
|     volumes: |  | ||||||
|       - "postgres:/var/lib/postgresql/data" |  | ||||||
|     environment: |  | ||||||
|       POSTGRES_USER: capsul |  | ||||||
|       POSTGRES_PASSWORD: capsul |  | ||||||
|       POSTGRES_DB: capsul |  | ||||||
|  |  | ||||||
| volumes: |  | ||||||
|   postgres: |  | ||||||
		Reference in New Issue
	
	Block a user
	