Skip to content

Commit 39865e0

Browse files
committed
Implement parallelism on post server validations
Avoid failing on alias record validation when others pass
1 parent 5f76c6e commit 39865e0

11 files changed

Lines changed: 114 additions & 58 deletions

File tree

.pre-commit-config.yaml

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
fail_fast: true
2+
exclude: ^docs/
23
repos:
3-
-
4-
repo: https://github.com/PyCQA/flake8
5-
rev: '6.1.0'
4+
- repo: https://github.com/pre-commit/pre-commit-hooks
5+
rev: v4.4.0
66
hooks:
7-
-
8-
id: flake8
9-
additional_dependencies:
10-
- flake8-docstrings
11-
- flake8-sfs
12-
args: [--max-line-length=120, --extend-ignore=SFS3 D107 SFS301 D100 D104 D401 SFS101 SFS201]
7+
- id: check-added-large-files
8+
- id: check-ast
9+
- id: check-byte-order-marker
10+
- id: check-builtin-literals
11+
- id: check-case-conflict
12+
- id: check-docstring-first
13+
- id: check-executables-have-shebangs
14+
- id: check-shebang-scripts-are-executable
15+
- id: check-merge-conflict
16+
- id: check-toml
17+
- id: check-vcs-permalinks
18+
- id: check-xml
19+
- id: debug-statements
20+
- id: destroyed-symlinks
21+
- id: detect-aws-credentials
22+
- id: detect-private-key
23+
- id: end-of-file-fixer
24+
- id: fix-byte-order-marker
25+
- id: mixed-line-ending
26+
- id: name-tests-test
27+
- id: requirements-txt-fixer
28+
- id: trailing-whitespace
1329

14-
-
15-
repo: https://github.com/PyCQA/isort
16-
rev: '5.12.0'
30+
- repo: https://github.com/PyCQA/isort
31+
rev: 5.12.0
1732
hooks:
18-
-
19-
id: isort
33+
- id: isort
2034

21-
-
22-
repo: local
35+
- repo: local
2336
hooks:
2437
-
2538
id: docs

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ vpn_server.delete_vpn_server()
4747
## Coding Standards
4848
Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) <br>
4949
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/) <br>
50-
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
50+
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
5151
[`isort`](https://pycqa.github.io/isort/)
5252

5353
### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst)

docs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ vpn_server.delete_vpn_server()
4747
## Coding Standards
4848
Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) <br>
4949
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/) <br>
50-
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
50+
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
5151
[`isort`](https://pycqa.github.io/isort/)
5252

5353
### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst)

docs/_sources/README.md.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ vpn_server.delete_vpn_server()
4747
## Coding Standards
4848
Docstring format: [`Google`](https://google.github.io/styleguide/pyguide.html#38-comments-and-docstrings) <br>
4949
Styling conventions: [`PEP 8`](https://www.python.org/dev/peps/pep-0008/) <br>
50-
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
50+
Clean code with pre-commit hooks: [`flake8`](https://flake8.pycqa.org/en/latest/) and
5151
[`isort`](https://pycqa.github.io/isort/)
5252

5353
### [Release Notes](https://github.com/thevickypedia/vpn-server/blob/main/release_notes.rst)

docs/index.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -310,20 +310,27 @@ <h1>Welcome to VPN Server’s documentation!<a class="headerlink" href="#welcome
310310

311311
<dl class="py method">
312312
<dt class="sig sig-object py" id="vpn.main.VPNServer._test_get">
313-
<span class="sig-name descname"><span class="pre">_test_get</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">server_hostname</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">timeout</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Tuple</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">(3,</span> <span class="pre">3)</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">Response</span></span></span><a class="headerlink" href="#vpn.main.VPNServer._test_get" title="Permalink to this definition"></a></dt>
313+
<span class="sig-name descname"><span class="pre">_test_get</span></span><span class="sig-paren">(</span><em class="sig-param"><span class="n"><span class="pre">host</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">str</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">timeout</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">Tuple</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">(3,</span> <span class="pre">3)</span></span></em>, <em class="sig-param"><span class="n"><span class="pre">retries</span></span><span class="p"><span class="pre">:</span></span><span class="w"> </span><span class="n"><span class="pre">int</span></span><span class="w"> </span><span class="o"><span class="pre">=</span></span><span class="w"> </span><span class="default_value"><span class="pre">5</span></span></em><span class="sig-paren">)</span> <span class="sig-return"><span class="sig-return-icon">&#x2192;</span> <span class="sig-return-typehint"><span class="pre">Response</span></span></span><a class="headerlink" href="#vpn.main.VPNServer._test_get" title="Permalink to this definition"></a></dt>
314314
<dd><p>Test GET connection with multiple hostnames.</p>
315315
<dl class="field-list simple">
316316
<dt class="field-odd">Parameters<span class="colon">:</span></dt>
317317
<dd class="field-odd"><ul class="simple">
318-
<li><p><strong>server_hostname</strong> – Public IP address or DNS name or alias record (entrypoint)</p></li>
318+
<li><p><strong>host</strong> – Public IP address or DNS name or alias record (entrypoint)</p></li>
319319
<li><p><strong>timeout</strong> – Tuple of connection timeout and read timeout.</p></li>
320+
<li><p><strong>retries</strong> – Number of times to retry in case of connection errors.</p></li>
320321
</ul>
321322
</dd>
322-
<dt class="field-even">Returns<span class="colon">:</span></dt>
323-
<dd class="field-even"><p>Response object.</p>
323+
</dl>
324+
<div class="admonition seealso">
325+
<p class="admonition-title">See also</p>
326+
<p>Retries with exponential intervals between each attempt in case of a failure.</p>
327+
</div>
328+
<dl class="field-list simple">
329+
<dt class="field-odd">Returns<span class="colon">:</span></dt>
330+
<dd class="field-odd"><p>Response object.</p>
324331
</dd>
325-
<dt class="field-odd">Return type<span class="colon">:</span></dt>
326-
<dd class="field-odd"><p>Response</p>
332+
<dt class="field-even">Return type<span class="colon">:</span></dt>
333+
<dd class="field-even"><p>Response</p>
327334
</dd>
328335
</dl>
329336
</dd></dl>
@@ -339,11 +346,13 @@ <h1>Welcome to VPN Server’s documentation!<a class="headerlink" href="#welcome
339346
</dl>
340347
<div class="admonition seealso">
341348
<p class="admonition-title">See also</p>
349+
<p>All the tests run in parallel to improve runtime.</p>
342350
<ul class="simple">
343351
<li><p>GET request against the public IP of the ec2 instance.</p></li>
344352
<li><p>GET request against the public DNS of the ec2 instance.</p></li>
345353
<li><p>SSH connection with the OpenVPN Access Server.</p></li>
346354
<li><p>Test <code class="docutils literal notranslate"><span class="pre">openvpnas</span></code> service availability on the server.</p></li>
355+
<li><p>Test alias record if values for <code class="docutils literal notranslate"><span class="pre">hosted_zone</span></code> and <code class="docutils literal notranslate"><span class="pre">subdomain</span></code> were provided</p></li>
347356
</ul>
348357
</div>
349358
<dl class="field-list simple">

docs/searchindex.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pre_commit.sh

100644100755
File mode changed.

release_notes.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
Release Notes
22
=============
33

4+
1.6 (01/29/2024)
5+
----------------
6+
- Includes speed and stability improvements for server validations
7+
48
1.5.2 (09/28/2023)
59
------------------
610
- Includes instance information in return statements during creation and deletion

vpn/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
from vpn.models import (config, exceptions, image_factory, # noqa: F401
55
logger, route53, server, util)
66

7-
version = "1.5.2"
7+
version = "1.6"

vpn/main.py

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import time
44
import warnings
55
from logging import Logger
6+
from multiprocessing.pool import ThreadPool
67
from typing import Dict, Tuple, Union
78

89
import boto3
@@ -90,6 +91,8 @@ def __init__(self, **kwargs: Unpack[Union[EnvConfig, Logger]]):
9091
self.image_id = None
9192
self.zone_id = None
9293

94+
self.engine = inflect.engine()
95+
9396
def _init(self,
9497
start: Union[bool, int]) -> None:
9598
"""Initializer function.
@@ -403,24 +406,35 @@ def _terminate_ec2_instance(self,
403406
self.logger.warning('API call to terminate the instance has failed.')
404407
self.logger.error(error)
405408

406-
def _test_get(self, server_hostname: str, timeout: Tuple = (3, 3)) -> Response:
409+
def _test_get(self, host: str, timeout: Tuple = (3, 3), retries: int = 5) -> Response:
407410
"""Test GET connection with multiple hostnames.
408411
409412
Args:
410-
server_hostname: Public IP address or DNS name or alias record (entrypoint)
413+
host: Public IP address or DNS name or alias record (entrypoint)
411414
timeout: Tuple of connection timeout and read timeout.
415+
retries: Number of times to retry in case of connection errors.
416+
417+
See Also:
418+
Retries with exponential intervals between each attempt in case of a failure.
412419
413420
Returns:
414421
Response:
415422
Response object.
416423
"""
417-
try:
418-
response = requests.get(url=f"https://{server_hostname}:{self.env.vpn_port}",
419-
verify=False, timeout=timeout)
420-
self.logger.debug(response)
421-
return response
422-
except requests.RequestException as error:
423-
self.logger.error(error)
424+
for i in range(1, retries + 1):
425+
try:
426+
response = requests.get(url=f"https://{host}:{self.env.vpn_port}",
427+
verify=False, timeout=timeout)
428+
self.logger.debug(response)
429+
return response
430+
except requests.RequestException as error:
431+
if i < retries:
432+
exponent = 2 ** i
433+
self.logger.info("Failed to validate %s in %s attempt, next attempt in %d seconds",
434+
host, self.engine.ordinal(i), exponent)
435+
time.sleep(exponent)
436+
else:
437+
self.logger.error(error)
424438

425439
def _tester(self, data: Dict[str, Union[str, int]]) -> None:
426440
"""Tests ``GET`` and ``SSH`` connections on the existing server.
@@ -429,31 +443,44 @@ def _tester(self, data: Dict[str, Union[str, int]]) -> None:
429443
data: Takes the instance information in a dictionary format as an argument.
430444
431445
See Also:
446+
All the tests run in parallel to improve runtime.
447+
432448
- GET request against the public IP of the ec2 instance.
433449
- GET request against the public DNS of the ec2 instance.
434450
- SSH connection with the OpenVPN Access Server.
435451
- Test ``openvpnas`` service availability on the server.
452+
- Test alias record if values for ``hosted_zone`` and ``subdomain`` were provided
436453
437454
Raises:
438455
AssertionError:
439456
When any of the tests fail.
440457
"""
441458
urllib3.disable_warnings(InsecureRequestWarning) # Disable warnings for self-signed certificates
442-
self.logger.info(f"Testing GET connection to https://{data.get('public_ip')}:{self.env.vpn_port}")
443-
ip_check = self._test_get(data.get('public_ip'))
444-
host_check = self._test_get(data.get('public_dns'))
445-
alias_check = Response()
446-
alias_check.status_code = 200
459+
alias_thread = None
447460
if self.settings.entrypoint:
448-
alias_check = self._test_get(self.settings.entrypoint)
449-
assert all((ip_check, host_check, alias_check)) and all((ip_check.ok, host_check.ok, alias_check.ok)), \
461+
alias_thread = ThreadPool(processes=1).apply_async(self._test_get,
462+
args=(self.settings.entrypoint,))
463+
self.logger.info("Testing GET connections to VPN server, via hostname and IP address.")
464+
ip_thread = ThreadPool(processes=1).apply_async(self._test_get,
465+
kwds=dict(host=data.get('public_ip'), retries=2))
466+
host_thread = ThreadPool(processes=1).apply_async(self._test_get,
467+
kwds=dict(host=data.get('public_dns'), retries=2))
468+
ip_check = ip_thread.get()
469+
host_check = host_thread.get()
470+
assert all((ip_check, host_check)) and all((ip_check.ok, host_check.ok)), \
450471
"One or more tests for GET connection has failed. Please check the logs for more information."
451-
self.logger.info(f"Testing SSH connection to {data.get('public_dns')}")
472+
self.logger.info("Connections to VPN server, via hostname and IP address were successful.")
473+
self.logger.info("Testing SSH connection to %s", data.get('public_dns'))
452474
test_ssh = Server(username=self.env.vpn_username, hostname=data.get('public_dns'), logger=self.logger,
453475
env=self.env, settings=self.settings)
454476
test_ssh.test_service(display=False, timeout=5)
455-
self.logger.info(f"Connection to https://{data.get('public_ip')}:{self.env.vpn_port} and "
456-
f"SSH to {data.get('public_dns')} was successful.")
477+
self.logger.info(f"SSH to {data.get('public_dns')} was successful.")
478+
if alias_thread:
479+
if (alias_response := alias_thread.get()) and alias_response.ok:
480+
self.logger.info("Connection to VPN server, via alias record %s was successful.",
481+
self.settings.entrypoint)
482+
else:
483+
self.logger.error("Failed to test A record, it may be DNS propagation delay. ")
457484

458485
def test_vpn(self) -> None:
459486
"""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]:
548575

549576
self._configure_vpn(instance.public_dns_name)
550577
if self.settings.entrypoint:
551-
change_record_set(source=self.settings.entrypoint,
552-
destination=instance.public_ip_address,
553-
logger=self.logger,
554-
client=self.route53_client,
555-
zone_id=self.zone_id, action='UPSERT')
556-
instance_info['entrypoint'] = self.settings.entrypoint
557-
with open(self.env.vpn_info, 'w') as file:
558-
json.dump(instance_info, file, indent=2)
559-
file.flush()
578+
if change_record_set(source=self.settings.entrypoint,
579+
destination=instance.public_ip_address,
580+
logger=self.logger,
581+
client=self.route53_client,
582+
zone_id=self.zone_id, action='UPSERT'):
583+
instance_info['entrypoint'] = self.settings.entrypoint
584+
with open(self.env.vpn_info, 'w') as file:
585+
json.dump(instance_info, file, indent=2)
586+
file.flush()
587+
else:
588+
self.logger.error("Failed to add entrypoint as alias")
589+
self.settings.entrypoint = None
560590

561591
try:
562592
self._tester(data=instance_info)
@@ -580,7 +610,7 @@ def _configure_vpn(self, public_dns: str) -> None:
580610
try:
581611
server = Server(hostname=public_dns, username='openvpnas', logger=self.logger,
582612
env=self.env, settings=self.settings)
583-
self.logger.info("Connection established on %s attempt", inflect.engine().ordinal(i + 1))
613+
self.logger.info("Connection established on %s attempt", self.engine.ordinal(i + 1))
584614
break
585615
except Exception as error:
586616
self.logger.error(error)

0 commit comments

Comments
 (0)