From 39865e0b5d7f911b28fc6ec45c2650df93d0d657 Mon Sep 17 00:00:00 2001 From: thevickypedia Date: Mon, 29 Jan 2024 17:33:38 -0600 Subject: [PATCH] Implement parallelism on post server validations Avoid failing on alias record validation when others pass --- .pre-commit-config.yaml | 45 ++++++++++++------- README.md | 2 +- docs/README.md | 2 +- docs/_sources/README.md.txt | 2 +- docs/index.html | 21 ++++++--- docs/searchindex.js | 2 +- pre_commit.sh | 0 release_notes.rst | 4 ++ vpn/__init__.py | 2 +- vpn/main.py | 88 +++++++++++++++++++++++++------------ vpn/requirements.txt | 4 +- 11 files changed, 114 insertions(+), 58 deletions(-) mode change 100644 => 100755 pre_commit.sh diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5246c56..384d402 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,25 +1,38 @@ fail_fast: true +exclude: ^docs/ repos: - - - repo: https://github.com/PyCQA/flake8 - rev: '6.1.0' + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 hooks: - - - id: flake8 - additional_dependencies: - - flake8-docstrings - - flake8-sfs - args: [--max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401 SFS101 SFS201] + - id: check-added-large-files + - id: check-ast + - id: check-byte-order-marker + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-executables-have-shebangs + - id: check-shebang-scripts-are-executable + - id: check-merge-conflict + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: debug-statements + - id: destroyed-symlinks + - id: detect-aws-credentials + - id: detect-private-key + - id: end-of-file-fixer + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: name-tests-test + - id: requirements-txt-fixer + - id: trailing-whitespace - - - repo: https://github.com/PyCQA/isort - rev: '5.12.0' + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - - - id: isort + - id: isort - - - repo: local + - repo: local hooks: - id: docs diff --git a/README.md b/README.md index 607ac74..620bd81 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ vpn_server.delete_vpn_server() ## Coding Standards Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
-Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and +Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and [`isort`](https://pycqa.github.io/isort/) ### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst) diff --git a/docs/README.md b/docs/README.md index 607ac74..620bd81 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,7 +47,7 @@ vpn_server.delete_vpn_server() ## Coding Standards Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
-Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and +Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and [`isort`](https://pycqa.github.io/isort/) ### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst) diff --git a/docs/_sources/README.md.txt b/docs/_sources/README.md.txt index 607ac74..620bd81 100644 --- a/docs/_sources/README.md.txt +++ b/docs/_sources/README.md.txt @@ -47,7 +47,7 @@ vpn_server.delete_vpn_server() ## Coding Standards Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings)
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/)
-Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and +Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and [`isort`](https://pycqa.github.io/isort/) ### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst) diff --git a/docs/index.html b/docs/index.html index 90994d5..029b1d0 100644 --- a/docs/index.html +++ b/docs/index.html @@ -310,20 +310,27 @@

Welcome to VPN Server’s documentation!
-_test_get(server_hostname: str, timeout: Tuple = (3, 3)) Response
+_test_get(host: str, timeout: Tuple = (3, 3), retries: int = 5) Response

Test GET connection with multiple hostnames.

Parameters:
    -
  • server_hostname – Public IP address or DNS name or alias record (entrypoint)

  • +
  • host – Public IP address or DNS name or alias record (entrypoint)

  • timeout – Tuple of connection timeout and read timeout.

  • +
  • retries – Number of times to retry in case of connection errors.

-
Returns:
-

Response object.

+
+
+

See also

+

Retries with exponential intervals between each attempt in case of a failure.

+
+
+
Returns:
+

Response object.

-
Return type:
-

Response

+
Return type:
+

Response

@@ -339,11 +346,13 @@

Welcome to VPN Server’s documentation!

See also

+

All the tests run in parallel to improve runtime.

  • GET request against the public IP of the ec2 instance.

  • GET request against the public DNS of the ec2 instance.

  • SSH connection with the OpenVPN Access Server.

  • Test openvpnas service availability on the server.

  • +
  • Test alias record if values for hosted_zone and subdomain were provided

diff --git a/docs/searchindex.js b/docs/searchindex.js index c3d98c3..26b81e5 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Platform Supported", "Welcome to VPN Server\u2019s documentation!"], "terms": {"establish": 0, "scalabl": 0, "demand": 0, "power": 0, "openvpn": [0, 1], "aw": [0, 1], "ec2": [0, 1], "python": 0, "m": 0, "pip": 0, "import": 0, "instanti": [0, 1], "object": [0, 1], "vpn_server": 0, "vpnserver": [0, 1], "creat": [0, 1], "create_vpn_serv": [0, 1], "test": [0, 1], "an": [0, 1], "exist": [0, 1], "test_vpn": [0, 1], "delet": [0, 1], "delete_vpn_serv": [0, 1], "bulb": 0, "pleas": [0, 1], "refer": [0, 1], "wiki": 0, "page": [0, 1], "more": [0, 1], "instruct": 0, "payload": 0, "requir": [0, 1], "docstr": 0, "format": [0, 1], "googl": [0, 1], "style": 0, "convent": 0, "pep": 0, "8": 0, "clean": 0, "pre": [0, 1], "commit": 0, "hook": 0, "flake8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": [0, 1], "precommit": 0, "ensur": 0, "doc": [0, 1], "creation": 0, "ar": 0, "run": [0, 1], "everi": 0, "sphinx": 0, "5": 0, "1": 0, "recommonmark": 0, "all": [0, 1], "file": [0, 1], "repositori": 0, "runbook": 0, "packag": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "platform": 1, "support": 1, "repo": 1, "stat": 1, "deploy": 1, "instal": 1, "usag": 1, "code": 1, "standard": 1, "project": 1, "link": 1, "licens": 1, "copyright": 1, "class": 1, "main": 1, "kwarg": 1, "unpack": 1, "initi": 1, "spin": 1, "up": 1, "instanc": 1, "ami": 1, "which": 1, "serv": 1, "load": 1, "envconfig": 1, "subclass": 1, "baseset": 1, "valid": 1, "us": 1, "pydant": 1, "paramet": 1, "dictionari": 1, "keyword": 1, "argument": 1, "follow": 1, "kei": 1, "valu": 1, "pair": 1, "vpn_usernam": 1, "usernam": 1, "access": 1, "client": 1, "vpn_password": 1, "password": 1, "vpn_port": 1, "port": 1, "number": 1, "web": 1, "interfac": 1, "aws_profile_nam": 1, "name": 1, "profil": 1, "session": 1, "aws_access_kei": 1, "token": 1, "account": 1, "aws_secret_kei": 1, "secret": 1, "id": 1, "aws_region_nam": 1, "region": 1, "where": 1, "should": 1, "live": 1, "default": 1, "image_id": 1, "instance_typ": 1, "type": 1, "host": 1, "key_pair": 1, "store": 1, "connect": 1, "security_group": 1, "secur": 1, "group": 1, "vpn_info": 1, "json": 1, "dump": 1, "inform": 1, "domain": 1, "zone": 1, "hosted_zon": 1, "subdomain": 1, "bring": 1, "your": 1, "own": 1, "point": 1, "combin": 1, "exampl": 1, "If": 1, "you": 1, "have": 1, "com": 1, "want": 1, "entrypoint": 1, "note": 1, "also": 1, "regist": 1, "cannot": 1, "simpli": 1, "expect": 1, "work": 1, "The": 1, "alia": 1, "record": 1, "fail": 1, "duplic": 1, "dn": 1, "resolut": 1, "level": 1, "unless": 1, "i": 1, "same": 1, "_authorize_security_group": 1, "security_group_id": 1, "str": 1, "bool": 1, "author": 1, "certain": 1, "ingress": 1, "list": 1, "take": 1, "securitygroup": 1, "firewal": 1, "open": 1, "tcp": 1, "22": 1, "443": 1, "943": 1, "can": 1, "dynam": 1, "945": 1, "cluster": 1, "control": 1, "channel": 1, "udp": 1, "1194": 1, "return": 1, "flag": 1, "call": 1, "function": 1, "whether": 1, "wa": 1, "successfulli": 1, "_configure_vpn": 1, "public_dn": 1, "none": 1, "traffic": 1, "from": 1, "localhost": 1, "tunnel": 1, "public": 1, "_create_ec2_inst": 1, "option": 1, "tupl": 1, "success": 1, "union": 1, "_create_key_pair": 1, "keypair": 1, "rsa": 1, "pem": 1, "_create_security_group": 1, "get": 1, "vpc": 1, "re": 1, "sg": 1, "case": 1, "alreadi": 1, "_delete_key_pair": 1, "_delete_security_group": 1, "_disassociate_security_group": 1, "serviceresourc": 1, "instance_id": 1, "disassoci": 1, "assign": 1, "unavail": 1, "_get_vpc_id": 1, "fetch": 1, "_init": 1, "start": 1, "int": 1, "its": 1, "startup": 1, "shutdown": 1, "_terminate_ec2_inst": 1, "termin": 1, "request": 1, "_test_get": 1, "server_hostnam": 1, "timeout": 1, "3": 1, "respons": 1, "multipl": 1, "hostnam": 1, "ip": 1, "address": 1, "_tester": 1, "data": 1, "dict": 1, "against": 1, "openvpna": 1, "servic": 1, "avail": 1, "rais": 1, "assertionerror": 1, "when": 1, "ani": 1, "public_ip": 1, "disabl": 1, "remov": 1, "resourc": 1, "acquir": 1, "ha": 1, "A": 1, "doesn": 1, "long": 1, "neither": 1, "nor": 1, "modifi": 1, "manual": 1, "http": 1, "boto3": 1, "amazonaw": 1, "v1": 1, "api": 1, "latest": 1, "wait_until_termin": 1, "html": 1, "model": 1, "config": 1, "configurationset": 1, "basemodel": 1, "set": 1, "interact": 1, "new": 1, "pars": 1, "input": 1, "validationerror": 1, "pydantic_cor": 1, "form": 1, "__init__": 1, "__pydantic_self__": 1, "instead": 1, "common": 1, "self": 1, "first": 1, "arg": 1, "allow": 1, "field": 1, "amibas": 1, "imag": 1, "model_post_init": 1, "__context": 1, "thi": 1, "meant": 1, "behav": 1, "like": 1, "method": 1, "initialis": 1, "privat": 1, "attribut": 1, "It": 1, "context": 1, "sinc": 1, "what": 1, "core": 1, "pass": 1, "pydantic_set": 1, "env": 1, "dev": 1, "2": 1, "migrat": 1, "nullabl": 1, "extra": 1, "classmethod": 1, "validate_instance_typ": 1, "v": 1, "make": 1, "sure": 1, "nano": 1, "validate_vpn_password": 1, "per": 1, "regex": 1, "wrapper": 1, "awsresourceerror": 1, "status_cod": 1, "error_msg": 1, "custom": 1, "error": 1, "notimplementedwarn": 1, "implement": 1, "warn": 1, "image_factori": 1, "handl": 1, "retriev": 1, "sourc": 1, "ssm": 1, "origin": 1, "get_ami_id_nam": 1, "get_ami_id_product_cod": 1, "product": 1, "get_ami_id_ssm": 1, "get_image_id": 1, "tri": 1, "sequenti": 1, "execut": 1, "sequenc": 1, "fastest": 1, "singl": 1, "possibli": 1, "contain": 1, "lookup": 1, "specif": 1, "mani": 1, "so": 1, "grab": 1, "most": 1, "recent": 1, "one": 1, "unabl": 1, "deprecation_warn": 1, "deprecation_tim": 1, "deprec": 1, "chosen": 1, "nearing": 1, "deprecationtim": 1, "streamhandl": 1, "debug": 1, "mode": 1, "change_record_set": 1, "destin": 1, "zone_id": 1, "action": 1, "chang": 1, "within": 1, "perform": 1, "upsert": 1, "get_zone_id": 1, "init": 1, "fals": 1, "boolean": 1, "miss": 1, "rsakei": 1, "gener": 1, "add_formatt": 1, "add": 1, "formatt": 1, "were": 1, "dure": 1, "remove_formatt": 1, "log": 1, "room": 1, "restart_servic": 1, "restart": 1, "run_interactive_ssh": 1, "displai": 1, "true": 1, "30": 1, "command": 1, "screen": 1, "complet": 1, "test_servic": 1, "check": 1, "statu": 1, "remot": 1, "available_instance_typ": 1, "loop": 1, "through": 1, "describ": 1, "yield": 1, "available_region": 1, "index": 1, "modul": 1, "search": 1}, "objects": {"vpn": [[1, 0, 0, "-", "main"]], "vpn.main": [[1, 1, 1, "", "VPNServer"]], "vpn.main.VPNServer": [[1, 2, 1, "", "_authorize_security_group"], [1, 2, 1, "", "_configure_vpn"], [1, 2, 1, "", "_create_ec2_instance"], [1, 2, 1, "", "_create_key_pair"], [1, 2, 1, "", "_create_security_group"], [1, 2, 1, "", "_delete_key_pair"], [1, 2, 1, "", "_delete_security_group"], [1, 2, 1, "", "_disassociate_security_group"], [1, 2, 1, "", "_get_vpc_id"], [1, 2, 1, "", "_init"], [1, 2, 1, "", "_terminate_ec2_instance"], [1, 2, 1, "", "_test_get"], [1, 2, 1, "", "_tester"], [1, 2, 1, "", "create_vpn_server"], [1, 2, 1, "", "delete_vpn_server"], [1, 2, 1, "", "test_vpn"]], "vpn.models.config": [[1, 1, 1, "", "AMIBase"], [1, 1, 1, "", "ConfigurationSettings"], [1, 1, 1, "", "EnvConfig"], [1, 1, 1, "", "Settings"]], "vpn.models.config.AMIBase": [[1, 2, 1, "", "model_post_init"]], "vpn.models.config.EnvConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "validate_instance_type"], [1, 2, 1, "", "validate_vpn_password"]], "vpn.models": [[1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "image_factory"], [1, 0, 0, "-", "logger"], [1, 0, 0, "-", "route53"], [1, 0, 0, "-", "server"], [1, 0, 0, "-", "util"]], "vpn.models.exceptions": [[1, 3, 1, "", "AWSResourceError"], [1, 3, 1, "", "NotImplementedWarning"]], "vpn.models.image_factory": [[1, 1, 1, "", "ImageFactory"], [1, 4, 1, "", "deprecation_warning"]], "vpn.models.image_factory.ImageFactory": [[1, 2, 1, "", "get_ami_id_name"], [1, 2, 1, "", "get_ami_id_product_code"], [1, 2, 1, "", "get_ami_id_ssm"], [1, 2, 1, "", "get_image_id"]], "vpn.models.route53": [[1, 4, 1, "", "change_record_set"], [1, 4, 1, "", "get_zone_id"]], "vpn.models.server": [[1, 1, 1, "", "Server"]], "vpn.models.server.Server": [[1, 2, 1, "", "add_formatter"], [1, 2, 1, "", "remove_formatter"], [1, 2, 1, "", "restart_service"], [1, 2, 1, "", "run_interactive_ssh"], [1, 2, 1, "", "test_service"]], "vpn.models.util": [[1, 4, 1, "", "available_instance_types"], [1, 4, 1, "", "available_regions"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:exception", "4": "py:function"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "exception", "Python exception"], "4": ["py", "function", "Python function"]}, "titleterms": {"platform": 0, "support": 0, "repo": 0, "stat": 0, "deploy": 0, "vpn": [0, 1], "server": [0, 1], "instal": 0, "usag": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "project": 0, "link": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "read": 1, "me": 1, "configur": 1, "except": 1, "imagefactori": 1, "logger": 1, "route53": 1, "ssh": 1, "util": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["Platform Supported", "Welcome to VPN Server\u2019s documentation!"], "terms": {"establish": 0, "scalabl": 0, "demand": 0, "power": 0, "openvpn": [0, 1], "aw": [0, 1], "ec2": [0, 1], "python": 0, "m": 0, "pip": 0, "import": 0, "instanti": [0, 1], "object": [0, 1], "vpn_server": 0, "vpnserver": [0, 1], "creat": [0, 1], "create_vpn_serv": [0, 1], "test": [0, 1], "an": [0, 1], "exist": [0, 1], "test_vpn": [0, 1], "delet": [0, 1], "delete_vpn_serv": [0, 1], "bulb": 0, "pleas": [0, 1], "refer": [0, 1], "wiki": 0, "page": [0, 1], "more": [0, 1], "instruct": 0, "payload": 0, "requir": [0, 1], "docstr": 0, "format": [0, 1], "googl": [0, 1], "style": 0, "convent": 0, "pep": 0, "8": 0, "clean": 0, "pre": [0, 1], "commit": 0, "hook": 0, "flake8": 0, "isort": 0, "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": [0, 1], "precommit": 0, "ensur": 0, "doc": [0, 1], "creation": 0, "ar": 0, "run": [0, 1], "everi": 0, "sphinx": 0, "5": [0, 1], "1": 0, "recommonmark": 0, "all": [0, 1], "file": [0, 1], "repositori": 0, "runbook": 0, "packag": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "platform": 1, "support": 1, "repo": 1, "stat": 1, "deploy": 1, "instal": 1, "usag": 1, "code": 1, "standard": 1, "project": 1, "link": 1, "licens": 1, "copyright": 1, "class": 1, "main": 1, "kwarg": 1, "unpack": 1, "initi": 1, "spin": 1, "up": 1, "instanc": 1, "ami": 1, "which": 1, "serv": 1, "load": 1, "envconfig": 1, "subclass": 1, "baseset": 1, "valid": 1, "us": 1, "pydant": 1, "paramet": 1, "dictionari": 1, "keyword": 1, "argument": 1, "follow": 1, "kei": 1, "valu": 1, "pair": 1, "vpn_usernam": 1, "usernam": 1, "access": 1, "client": 1, "vpn_password": 1, "password": 1, "vpn_port": 1, "port": 1, "number": 1, "web": 1, "interfac": 1, "aws_profile_nam": 1, "name": 1, "profil": 1, "session": 1, "aws_access_kei": 1, "token": 1, "account": 1, "aws_secret_kei": 1, "secret": 1, "id": 1, "aws_region_nam": 1, "region": 1, "where": 1, "should": 1, "live": 1, "default": 1, "image_id": 1, "instance_typ": 1, "type": 1, "host": 1, "key_pair": 1, "store": 1, "connect": 1, "security_group": 1, "secur": 1, "group": 1, "vpn_info": 1, "json": 1, "dump": 1, "inform": 1, "domain": 1, "zone": 1, "hosted_zon": 1, "subdomain": 1, "bring": 1, "your": 1, "own": 1, "point": 1, "combin": 1, "exampl": 1, "If": 1, "you": 1, "have": 1, "com": 1, "want": 1, "entrypoint": 1, "note": 1, "also": 1, "regist": 1, "cannot": 1, "simpli": 1, "expect": 1, "work": 1, "The": 1, "alia": 1, "record": 1, "fail": 1, "duplic": 1, "dn": 1, "resolut": 1, "level": 1, "unless": 1, "i": 1, "same": 1, "_authorize_security_group": 1, "security_group_id": 1, "str": 1, "bool": 1, "author": 1, "certain": 1, "ingress": 1, "list": 1, "take": 1, "securitygroup": 1, "firewal": 1, "open": 1, "tcp": 1, "22": 1, "443": 1, "943": 1, "can": 1, "dynam": 1, "945": 1, "cluster": 1, "control": 1, "channel": 1, "udp": 1, "1194": 1, "return": 1, "flag": 1, "call": 1, "function": 1, "whether": 1, "wa": 1, "successfulli": 1, "_configure_vpn": 1, "public_dn": 1, "none": 1, "traffic": 1, "from": 1, "localhost": 1, "tunnel": 1, "public": 1, "_create_ec2_inst": 1, "option": 1, "tupl": 1, "success": 1, "union": 1, "_create_key_pair": 1, "keypair": 1, "rsa": 1, "pem": 1, "_create_security_group": 1, "get": 1, "vpc": 1, "re": 1, "sg": 1, "case": 1, "alreadi": 1, "_delete_key_pair": 1, "_delete_security_group": 1, "_disassociate_security_group": 1, "serviceresourc": 1, "instance_id": 1, "disassoci": 1, "assign": 1, "unavail": 1, "_get_vpc_id": 1, "fetch": 1, "_init": 1, "start": 1, "int": 1, "its": 1, "startup": 1, "shutdown": 1, "_terminate_ec2_inst": 1, "termin": 1, "request": 1, "_test_get": 1, "timeout": 1, "3": 1, "retri": 1, "respons": 1, "multipl": 1, "hostnam": 1, "ip": 1, "address": 1, "time": 1, "error": 1, "exponenti": 1, "interv": 1, "between": 1, "each": 1, "attempt": 1, "failur": 1, "_tester": 1, "data": 1, "dict": 1, "parallel": 1, "improv": 1, "runtim": 1, "against": 1, "openvpna": 1, "servic": 1, "avail": 1, "were": 1, "provid": 1, "rais": 1, "assertionerror": 1, "when": 1, "ani": 1, "public_ip": 1, "disabl": 1, "remov": 1, "resourc": 1, "acquir": 1, "ha": 1, "A": 1, "doesn": 1, "long": 1, "neither": 1, "nor": 1, "modifi": 1, "manual": 1, "http": 1, "boto3": 1, "amazonaw": 1, "v1": 1, "api": 1, "latest": 1, "wait_until_termin": 1, "html": 1, "model": 1, "config": 1, "configurationset": 1, "basemodel": 1, "set": 1, "interact": 1, "new": 1, "pars": 1, "input": 1, "validationerror": 1, "pydantic_cor": 1, "form": 1, "__init__": 1, "__pydantic_self__": 1, "instead": 1, "common": 1, "self": 1, "first": 1, "arg": 1, "allow": 1, "field": 1, "amibas": 1, "imag": 1, "model_post_init": 1, "__context": 1, "thi": 1, "meant": 1, "behav": 1, "like": 1, "method": 1, "initialis": 1, "privat": 1, "attribut": 1, "It": 1, "context": 1, "sinc": 1, "what": 1, "core": 1, "pass": 1, "pydantic_set": 1, "env": 1, "dev": 1, "2": 1, "migrat": 1, "nullabl": 1, "extra": 1, "classmethod": 1, "validate_instance_typ": 1, "v": 1, "make": 1, "sure": 1, "nano": 1, "validate_vpn_password": 1, "per": 1, "regex": 1, "wrapper": 1, "awsresourceerror": 1, "status_cod": 1, "error_msg": 1, "custom": 1, "notimplementedwarn": 1, "implement": 1, "warn": 1, "image_factori": 1, "handl": 1, "retriev": 1, "sourc": 1, "ssm": 1, "origin": 1, "get_ami_id_nam": 1, "get_ami_id_product_cod": 1, "product": 1, "get_ami_id_ssm": 1, "get_image_id": 1, "tri": 1, "sequenti": 1, "execut": 1, "sequenc": 1, "fastest": 1, "singl": 1, "possibli": 1, "contain": 1, "lookup": 1, "specif": 1, "mani": 1, "so": 1, "grab": 1, "most": 1, "recent": 1, "one": 1, "unabl": 1, "deprecation_warn": 1, "deprecation_tim": 1, "deprec": 1, "chosen": 1, "nearing": 1, "deprecationtim": 1, "streamhandl": 1, "debug": 1, "mode": 1, "change_record_set": 1, "destin": 1, "zone_id": 1, "action": 1, "chang": 1, "within": 1, "perform": 1, "upsert": 1, "get_zone_id": 1, "init": 1, "fals": 1, "boolean": 1, "miss": 1, "rsakei": 1, "gener": 1, "add_formatt": 1, "add": 1, "formatt": 1, "dure": 1, "remove_formatt": 1, "log": 1, "room": 1, "restart_servic": 1, "restart": 1, "run_interactive_ssh": 1, "displai": 1, "true": 1, "30": 1, "command": 1, "screen": 1, "complet": 1, "test_servic": 1, "check": 1, "statu": 1, "remot": 1, "available_instance_typ": 1, "loop": 1, "through": 1, "describ": 1, "yield": 1, "available_region": 1, "index": 1, "modul": 1, "search": 1}, "objects": {"vpn": [[1, 0, 0, "-", "main"]], "vpn.main": [[1, 1, 1, "", "VPNServer"]], "vpn.main.VPNServer": [[1, 2, 1, "", "_authorize_security_group"], [1, 2, 1, "", "_configure_vpn"], [1, 2, 1, "", "_create_ec2_instance"], [1, 2, 1, "", "_create_key_pair"], [1, 2, 1, "", "_create_security_group"], [1, 2, 1, "", "_delete_key_pair"], [1, 2, 1, "", "_delete_security_group"], [1, 2, 1, "", "_disassociate_security_group"], [1, 2, 1, "", "_get_vpc_id"], [1, 2, 1, "", "_init"], [1, 2, 1, "", "_terminate_ec2_instance"], [1, 2, 1, "", "_test_get"], [1, 2, 1, "", "_tester"], [1, 2, 1, "", "create_vpn_server"], [1, 2, 1, "", "delete_vpn_server"], [1, 2, 1, "", "test_vpn"]], "vpn.models.config": [[1, 1, 1, "", "AMIBase"], [1, 1, 1, "", "ConfigurationSettings"], [1, 1, 1, "", "EnvConfig"], [1, 1, 1, "", "Settings"]], "vpn.models.config.AMIBase": [[1, 2, 1, "", "model_post_init"]], "vpn.models.config.EnvConfig": [[1, 1, 1, "", "Config"], [1, 2, 1, "", "validate_instance_type"], [1, 2, 1, "", "validate_vpn_password"]], "vpn.models": [[1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "image_factory"], [1, 0, 0, "-", "logger"], [1, 0, 0, "-", "route53"], [1, 0, 0, "-", "server"], [1, 0, 0, "-", "util"]], "vpn.models.exceptions": [[1, 3, 1, "", "AWSResourceError"], [1, 3, 1, "", "NotImplementedWarning"]], "vpn.models.image_factory": [[1, 1, 1, "", "ImageFactory"], [1, 4, 1, "", "deprecation_warning"]], "vpn.models.image_factory.ImageFactory": [[1, 2, 1, "", "get_ami_id_name"], [1, 2, 1, "", "get_ami_id_product_code"], [1, 2, 1, "", "get_ami_id_ssm"], [1, 2, 1, "", "get_image_id"]], "vpn.models.route53": [[1, 4, 1, "", "change_record_set"], [1, 4, 1, "", "get_zone_id"]], "vpn.models.server": [[1, 1, 1, "", "Server"]], "vpn.models.server.Server": [[1, 2, 1, "", "add_formatter"], [1, 2, 1, "", "remove_formatter"], [1, 2, 1, "", "restart_service"], [1, 2, 1, "", "run_interactive_ssh"], [1, 2, 1, "", "test_service"]], "vpn.models.util": [[1, 4, 1, "", "available_instance_types"], [1, 4, 1, "", "available_regions"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:exception", "4": "py:function"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "exception", "Python exception"], "4": ["py", "function", "Python function"]}, "titleterms": {"platform": 0, "support": 0, "repo": 0, "stat": 0, "deploy": 0, "vpn": [0, 1], "server": [0, 1], "instal": 0, "usag": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "project": 0, "link": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "read": 1, "me": 1, "configur": 1, "except": 1, "imagefactori": 1, "logger": 1, "route53": 1, "ssh": 1, "util": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/pre_commit.sh b/pre_commit.sh old mode 100644 new mode 100755 diff --git a/release_notes.rst b/release_notes.rst index e93e521..e11e3f2 100644 --- a/release_notes.rst +++ b/release_notes.rst @@ -1,6 +1,10 @@ Release Notes ============= +1.6 (01/29/2024) +---------------- +- Includes speed and stability improvements for server validations + 1.5.2 (09/28/2023) ------------------ - Includes instance information in return statements during creation and deletion diff --git a/vpn/__init__.py b/vpn/__init__.py index c5d243a..3602491 100644 --- a/vpn/__init__.py +++ b/vpn/__init__.py @@ -4,4 +4,4 @@ from vpn.models import (config, exceptions, image_factory, # noqa: F401 logger, route53, server, util) -version = "1.5.2" +version = "1.6" diff --git a/vpn/main.py b/vpn/main.py index 06a79ec..8efd406 100644 --- a/vpn/main.py +++ b/vpn/main.py @@ -3,6 +3,7 @@ import time import warnings from logging import Logger +from multiprocessing.pool import ThreadPool from typing import Dict, Tuple, Union import boto3 @@ -90,6 +91,8 @@ def __init__(self, **kwargs: Unpack[Union[EnvConfig, Logger]]): self.image_id = None self.zone_id = None + self.engine = inflect.engine() + def _init(self, start: Union[bool, int]) -> None: """Initializer function. @@ -403,24 +406,35 @@ def _terminate_ec2_instance(self, self.logger.warning('API call to terminate the instance has failed.') self.logger.error(error) - def _test_get(self, server_hostname: str, timeout: Tuple = (3, 3)) -> Response: + def _test_get(self, host: str, timeout: Tuple = (3, 3), retries: int = 5) -> Response: """Test GET connection with multiple hostnames. Args: - server_hostname: Public IP address or DNS name or alias record (entrypoint) + host: Public IP address or DNS name or alias record (entrypoint) timeout: Tuple of connection timeout and read timeout. + retries: Number of times to retry in case of connection errors. + + See Also: + Retries with exponential intervals between each attempt in case of a failure. Returns: Response: Response object. """ - try: - response = requests.get(url=f"https://{server_hostname}:{self.env.vpn_port}", - verify=False, timeout=timeout) - self.logger.debug(response) - return response - except requests.RequestException as error: - self.logger.error(error) + for i in range(1, retries + 1): + try: + response = requests.get(url=f"https://{host}:{self.env.vpn_port}", + verify=False, timeout=timeout) + self.logger.debug(response) + return response + except requests.RequestException as error: + if i < retries: + exponent = 2 ** i + self.logger.info("Failed to validate %s in %s attempt, next attempt in %d seconds", + host, self.engine.ordinal(i), exponent) + time.sleep(exponent) + else: + self.logger.error(error) def _tester(self, data: Dict[str, Union[str, int]]) -> None: """Tests ``GET`` and ``SSH`` connections on the existing server. @@ -429,31 +443,44 @@ def _tester(self, data: Dict[str, Union[str, int]]) -> None: data: Takes the instance information in a dictionary format as an argument. See Also: + All the tests run in parallel to improve runtime. + - GET request against the public IP of the ec2 instance. - GET request against the public DNS of the ec2 instance. - SSH connection with the OpenVPN Access Server. - Test ``openvpnas`` service availability on the server. + - Test alias record if values for ``hosted_zone`` and ``subdomain`` were provided Raises: AssertionError: When any of the tests fail. """ urllib3.disable_warnings(InsecureRequestWarning) # Disable warnings for self-signed certificates - self.logger.info(f"Testing GET connection to https://{data.get('public_ip')}:{self.env.vpn_port}") - ip_check = self._test_get(data.get('public_ip')) - host_check = self._test_get(data.get('public_dns')) - alias_check = Response() - alias_check.status_code = 200 + alias_thread = None if self.settings.entrypoint: - alias_check = self._test_get(self.settings.entrypoint) - assert all((ip_check, host_check, alias_check)) and all((ip_check.ok, host_check.ok, alias_check.ok)), \ + alias_thread = ThreadPool(processes=1).apply_async(self._test_get, + args=(self.settings.entrypoint,)) + self.logger.info("Testing GET connections to VPN server, via hostname and IP address.") + ip_thread = ThreadPool(processes=1).apply_async(self._test_get, + kwds=dict(host=data.get('public_ip'), retries=2)) + host_thread = ThreadPool(processes=1).apply_async(self._test_get, + kwds=dict(host=data.get('public_dns'), retries=2)) + ip_check = ip_thread.get() + host_check = host_thread.get() + assert all((ip_check, host_check)) and all((ip_check.ok, host_check.ok)), \ "One or more tests for GET connection has failed. Please check the logs for more information." - self.logger.info(f"Testing SSH connection to {data.get('public_dns')}") + self.logger.info("Connections to VPN server, via hostname and IP address were successful.") + self.logger.info("Testing SSH connection to %s", data.get('public_dns')) test_ssh = Server(username=self.env.vpn_username, hostname=data.get('public_dns'), logger=self.logger, env=self.env, settings=self.settings) test_ssh.test_service(display=False, timeout=5) - self.logger.info(f"Connection to https://{data.get('public_ip')}:{self.env.vpn_port} and " - f"SSH to {data.get('public_dns')} was successful.") + self.logger.info(f"SSH to {data.get('public_dns')} was successful.") + if alias_thread: + if (alias_response := alias_thread.get()) and alias_response.ok: + self.logger.info("Connection to VPN server, via alias record %s was successful.", + self.settings.entrypoint) + else: + self.logger.error("Failed to test A record, it may be DNS propagation delay. ") def test_vpn(self) -> None: """Tests the ``GET`` and ``SSH`` connections to an existing VPN server.""" @@ -548,15 +575,18 @@ def create_vpn_server(self) -> Union[Dict[str, Union[str, int]], None]: self._configure_vpn(instance.public_dns_name) if self.settings.entrypoint: - change_record_set(source=self.settings.entrypoint, - destination=instance.public_ip_address, - logger=self.logger, - client=self.route53_client, - zone_id=self.zone_id, action='UPSERT') - instance_info['entrypoint'] = self.settings.entrypoint - with open(self.env.vpn_info, 'w') as file: - json.dump(instance_info, file, indent=2) - file.flush() + if change_record_set(source=self.settings.entrypoint, + destination=instance.public_ip_address, + logger=self.logger, + client=self.route53_client, + zone_id=self.zone_id, action='UPSERT'): + instance_info['entrypoint'] = self.settings.entrypoint + with open(self.env.vpn_info, 'w') as file: + json.dump(instance_info, file, indent=2) + file.flush() + else: + self.logger.error("Failed to add entrypoint as alias") + self.settings.entrypoint = None try: self._tester(data=instance_info) @@ -580,7 +610,7 @@ def _configure_vpn(self, public_dns: str) -> None: try: server = Server(hostname=public_dns, username='openvpnas', logger=self.logger, env=self.env, settings=self.settings) - self.logger.info("Connection established on %s attempt", inflect.engine().ordinal(i + 1)) + self.logger.info("Connection established on %s attempt", self.engine.ordinal(i + 1)) break except Exception as error: self.logger.error(error) diff --git a/vpn/requirements.txt b/vpn/requirements.txt index 1f6e3f3..290fd70 100644 --- a/vpn/requirements.txt +++ b/vpn/requirements.txt @@ -1,8 +1,8 @@ boto3 -inflect botocore -requests +inflect paramiko==3.3.1 paramiko-expect==0.3.5 pydantic==2.4.0 pydantic-settings==2.0.3 +requests