pyntcli 0.1.29__py3-none-any.whl → 0.1.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- build/lib/build/lib/build/lib/pyntcli/__init__.py +1 -0
- build/lib/build/lib/build/lib/pyntcli/analytics/__init__.py +0 -0
- build/lib/build/lib/build/lib/pyntcli/analytics/send.py +70 -0
- build/lib/build/lib/build/lib/pyntcli/auth/__init__.py +0 -0
- build/lib/build/lib/build/lib/pyntcli/auth/login.py +156 -0
- build/lib/build/lib/build/lib/pyntcli/commands/__init__.py +0 -0
- build/lib/build/lib/build/lib/pyntcli/commands/har.py +68 -0
- build/lib/build/lib/build/lib/pyntcli/commands/id_command.py +28 -0
- build/lib/build/lib/build/lib/pyntcli/commands/newman.py +85 -0
- build/lib/build/lib/build/lib/pyntcli/commands/postman.py +58 -0
- build/lib/build/lib/build/lib/pyntcli/commands/proxy.py +145 -0
- build/lib/build/lib/build/lib/pyntcli/commands/pynt_cmd.py +47 -0
- build/lib/build/lib/build/lib/pyntcli/commands/root.py +57 -0
- build/lib/build/lib/build/lib/pyntcli/commands/sub_command.py +15 -0
- build/lib/build/lib/build/lib/pyntcli/commands/util.py +26 -0
- build/lib/build/lib/build/lib/pyntcli/main.py +77 -0
- build/lib/build/lib/build/lib/pyntcli/pynt_docker/__init__.py +1 -0
- build/lib/build/lib/build/lib/pyntcli/pynt_docker/pynt_container.py +178 -0
- build/lib/build/lib/build/lib/pyntcli/store/__init__.py +1 -0
- build/lib/build/lib/build/lib/pyntcli/store/json_connector.py +23 -0
- build/lib/build/lib/build/lib/pyntcli/store/store.py +55 -0
- build/lib/build/lib/build/lib/pyntcli/store/store_connector.py +17 -0
- build/lib/build/lib/build/lib/pyntcli/ui/__init__.py +0 -0
- build/lib/build/lib/build/lib/pyntcli/ui/progress.py +31 -0
- build/lib/build/lib/build/lib/pyntcli/ui/ui_thread.py +168 -0
- build/lib/build/lib/build/lib/tests/auth/test_login.py +96 -0
- build/lib/build/lib/build/lib/tests/conftest.py +24 -0
- build/lib/build/lib/build/lib/tests/store/test_cred_store.py +11 -0
- build/lib/build/lib/pyntcli/__init__.py +1 -0
- build/lib/build/lib/pyntcli/analytics/__init__.py +0 -0
- build/lib/build/lib/pyntcli/analytics/send.py +70 -0
- build/lib/build/lib/pyntcli/auth/__init__.py +0 -0
- build/lib/build/lib/pyntcli/auth/login.py +156 -0
- build/lib/build/lib/pyntcli/commands/__init__.py +0 -0
- build/lib/build/lib/pyntcli/commands/har.py +68 -0
- build/lib/build/lib/pyntcli/commands/id_command.py +28 -0
- build/lib/build/lib/pyntcli/commands/newman.py +85 -0
- build/lib/build/lib/pyntcli/commands/postman.py +58 -0
- build/lib/build/lib/pyntcli/commands/proxy.py +147 -0
- build/lib/build/lib/pyntcli/commands/pynt_cmd.py +47 -0
- build/lib/build/lib/pyntcli/commands/root.py +57 -0
- build/lib/build/lib/pyntcli/commands/sub_command.py +15 -0
- build/lib/build/lib/pyntcli/commands/util.py +26 -0
- build/lib/build/lib/pyntcli/main.py +77 -0
- build/lib/build/lib/pyntcli/pynt_docker/__init__.py +1 -0
- build/lib/build/lib/pyntcli/pynt_docker/pynt_container.py +178 -0
- build/lib/build/lib/pyntcli/store/__init__.py +1 -0
- build/lib/build/lib/pyntcli/store/json_connector.py +23 -0
- build/lib/build/lib/pyntcli/store/store.py +55 -0
- build/lib/build/lib/pyntcli/store/store_connector.py +17 -0
- build/lib/build/lib/pyntcli/ui/__init__.py +0 -0
- build/lib/build/lib/pyntcli/ui/progress.py +31 -0
- build/lib/build/lib/pyntcli/ui/ui_thread.py +168 -0
- build/lib/build/lib/tests/auth/test_login.py +96 -0
- build/lib/build/lib/tests/conftest.py +24 -0
- build/lib/build/lib/tests/store/test_cred_store.py +11 -0
- build/lib/pyntcli/__init__.py +1 -1
- build/lib/pyntcli/commands/proxy.py +1 -0
- pyntcli/__init__.py +1 -1
- pyntcli/commands/proxy.py +0 -2
- {pyntcli-0.1.29.dist-info → pyntcli-0.1.31.dist-info}/METADATA +1 -1
- pyntcli-0.1.31.dist-info/RECORD +117 -0
- pyntcli-0.1.29.dist-info/RECORD +0 -61
- {pyntcli-0.1.29.dist-info → pyntcli-0.1.31.dist-info}/WHEEL +0 -0
- {pyntcli-0.1.29.dist-info → pyntcli-0.1.31.dist-info}/entry_points.txt +0 -0
- {pyntcli-0.1.29.dist-info → pyntcli-0.1.31.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
import os
|
|
4
|
+
import webbrowser
|
|
5
|
+
from http import HTTPStatus
|
|
6
|
+
import requests
|
|
7
|
+
import time
|
|
8
|
+
from subprocess import Popen, PIPE
|
|
9
|
+
from build.lib.pyntcli.commands import proxy
|
|
10
|
+
|
|
11
|
+
from pyntcli.store.store import CredStore
|
|
12
|
+
from pyntcli.pynt_docker import pynt_container
|
|
13
|
+
from pyntcli.ui import ui_thread
|
|
14
|
+
from pyntcli.ui.progress import connect_progress_ws, wrap_ws_progress
|
|
15
|
+
from pyntcli.commands import util, sub_command
|
|
16
|
+
|
|
17
|
+
def proxy_usage():
|
|
18
|
+
return ui_thread.PrinterText("Command integration to Pynt. Run a security scan with a given command.") \
|
|
19
|
+
.with_line("") \
|
|
20
|
+
.with_line("Usage:",style=ui_thread.PrinterText.HEADER) \
|
|
21
|
+
.with_line("\tpynt command [OPTIONS]") \
|
|
22
|
+
.with_line("") \
|
|
23
|
+
.with_line("Options:",style=ui_thread.PrinterText.HEADER) \
|
|
24
|
+
.with_line("\t--cmd - The command that runs the functional tests") \
|
|
25
|
+
.with_line("\t--port - Set the port pynt will listen to (DEFAULT: 5001)") \
|
|
26
|
+
.with_line("\t--allow-errors - If present will allow command to fail and continue execution") \
|
|
27
|
+
.with_line("\t--ca-path - The path to the CA file in PEM format") \
|
|
28
|
+
.with_line("\t--proxy-port - Set the port proxied traffic should be routed to (DEFAULT: 6666)") \
|
|
29
|
+
.with_line("\t--report - If present will save the generated report in this path.") \
|
|
30
|
+
.with_line("\t--insecure - use when target uses self signed certificates")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ProxyCommand(sub_command.PyntSubCommand):
|
|
34
|
+
def __init__(self, name) -> None:
|
|
35
|
+
super().__init__(name)
|
|
36
|
+
self.scan_id = ""
|
|
37
|
+
self.proxy_sleep_interval = 2
|
|
38
|
+
self.proxy_healthcheck_buffer = 10
|
|
39
|
+
self.proxy_server_base_url = "http://localhost:{}/api"
|
|
40
|
+
|
|
41
|
+
def print_usage(self, *args):
|
|
42
|
+
ui_thread.print(proxy_usage())
|
|
43
|
+
|
|
44
|
+
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
45
|
+
proxy_cmd = parent.add_parser(self.name)
|
|
46
|
+
proxy_cmd.add_argument("--port", "-p", help="", type=int, default=5001)
|
|
47
|
+
proxy_cmd.add_argument("--proxy-port", help="", type=int, default=6666)
|
|
48
|
+
proxy_cmd.add_argument("--cmd", help="", default="", required=True)
|
|
49
|
+
proxy_cmd.add_argument("--allow-errors", action="store_true")
|
|
50
|
+
proxy_cmd.add_argument("--ca-path", type=str, default="")
|
|
51
|
+
proxy_cmd.add_argument("--report", type=str, default="")
|
|
52
|
+
proxy_cmd.print_usage = self.print_usage
|
|
53
|
+
proxy_cmd.print_help = self.print_usage
|
|
54
|
+
return proxy_cmd
|
|
55
|
+
|
|
56
|
+
def _updated_environment(self, args):
|
|
57
|
+
env_copy = deepcopy(os.environ)
|
|
58
|
+
return env_copy.update({"HTTP_PROXY": "http://localhost:{}".format(args.proxy_port),
|
|
59
|
+
"HTTPS_PROXY": "http://localhost:{}".format(args.proxy_port)})
|
|
60
|
+
|
|
61
|
+
def _start_proxy(self, args):
|
|
62
|
+
res = requests.put(self.proxy_server_base_url.format(args.port) + "/proxy/start")
|
|
63
|
+
res.raise_for_status()
|
|
64
|
+
self.scan_id = res.json()["scanId"]
|
|
65
|
+
|
|
66
|
+
def _stop_proxy(self, args):
|
|
67
|
+
start = time.time()
|
|
68
|
+
while start + self.proxy_healthcheck_buffer > time.time():
|
|
69
|
+
res = requests.put(self.proxy_server_base_url.format(args.port) + "/proxy/stop", json={"scanId": self.scan_id})
|
|
70
|
+
if res.status_code == HTTPStatus.OK:
|
|
71
|
+
return
|
|
72
|
+
time.sleep(self.proxy_sleep_interval)
|
|
73
|
+
raise TimeoutError()
|
|
74
|
+
|
|
75
|
+
def _get_report(self, args):
|
|
76
|
+
while True:
|
|
77
|
+
res = requests.get(self.proxy_server_base_url.format(args.port) + "/report", params={"scanId": self.scan_id})
|
|
78
|
+
if res.status_code == HTTPStatus.OK:
|
|
79
|
+
return res.text
|
|
80
|
+
if res.status_code == HTTPStatus.ACCEPTED:
|
|
81
|
+
time.sleep(self.proxy_sleep_interval)
|
|
82
|
+
continue
|
|
83
|
+
if res.status_code == 517: #pynt did not recieve any requests
|
|
84
|
+
ui_thread.print(ui_thread.PrinterText(res.json()["message"], ui_thread.PrinterText.WARNING))
|
|
85
|
+
return
|
|
86
|
+
ui_thread.print("Error in polling for scan report: {}".format(res.text))
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
def run_cmd(self, args: argparse.Namespace):
|
|
90
|
+
docker_type, docker_arguments = pynt_container.get_container_with_arguments(pynt_container.PyntDockerPort(args.port, args.port, "--port"),
|
|
91
|
+
pynt_container.PyntDockerPort(args.proxy_port, args.proxy_port, "--proxy-port"))
|
|
92
|
+
|
|
93
|
+
if "insecure" in args and args.insecure:
|
|
94
|
+
docker_arguments.append("--insecure")
|
|
95
|
+
|
|
96
|
+
if "dev_flags" in args:
|
|
97
|
+
docker_arguments += args.dev_flags.split(" ")
|
|
98
|
+
|
|
99
|
+
mounts = []
|
|
100
|
+
if "ca_path" in args and args.ca_path:
|
|
101
|
+
if not os.path.isfile(args.ca_path):
|
|
102
|
+
ui_thread.print(ui_thread.PrinterText("Could not find the provided ca path, please provide with a valid path", ui_thread.PrinterText.WARNING))
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
ca_name = os.path.basename(args.ca_path)
|
|
106
|
+
docker_arguments += ["--ca-path", ca_name]
|
|
107
|
+
mounts.append(pynt_container.create_mount(os.path.abspath(args.ca_path), "/etc/pynt/{}".format(ca_name)))
|
|
108
|
+
|
|
109
|
+
creds_path = CredStore().get_path()
|
|
110
|
+
mounts.append(pynt_container.create_mount(creds_path, "/app/creds.json"))
|
|
111
|
+
|
|
112
|
+
proxy_docker = pynt_container.PyntContainer(image_name=pynt_container.PYNT_DOCKER_IMAGE,
|
|
113
|
+
tag="proxy-latest",
|
|
114
|
+
mounts=mounts,
|
|
115
|
+
detach=True,
|
|
116
|
+
args=docker_arguments)
|
|
117
|
+
proxy_docker.run(docker_type)
|
|
118
|
+
ui_thread.print_generator(proxy_docker.stdout)
|
|
119
|
+
|
|
120
|
+
util.wait_for_healthcheck("http://localhost:{}".format(args.port))
|
|
121
|
+
self._start_proxy(args)
|
|
122
|
+
|
|
123
|
+
user_process = Popen(args.cmd, shell=True, stdout=PIPE, stderr=PIPE, env=self._updated_environment(args))
|
|
124
|
+
ui_thread.print_generator(user_process.stdout)
|
|
125
|
+
ui_thread.print_generator(user_process.stderr)
|
|
126
|
+
rc = user_process.wait()
|
|
127
|
+
if rc != 0 and not args.allow_errors:
|
|
128
|
+
proxy_docker.stop()
|
|
129
|
+
ui_thread.print(ui_thread.PrinterText("Command finished with error return code {}, If you wish Pynt to run anyway, run with --allow-errors".format(rc)))
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
self._stop_proxy(args)
|
|
133
|
+
|
|
134
|
+
report = ""
|
|
135
|
+
with ui_thread.progress(wrap_ws_progress(connect_progress_ws("ws://localhost:{}/progress?scanId={}".format(args.port, self.scan_id))), "scan in progress..."):
|
|
136
|
+
report = self._get_report(args)
|
|
137
|
+
if not report:
|
|
138
|
+
proxy_docker.stop()
|
|
139
|
+
return
|
|
140
|
+
report_path = os.path.join(os.getcwd(), "report_{}.html".format(int(time.time())))
|
|
141
|
+
if "report" in args and args.report:
|
|
142
|
+
report_path = os.path.abspath(args.report)
|
|
143
|
+
with open(report_path, "w") as f:
|
|
144
|
+
f.write(report)
|
|
145
|
+
|
|
146
|
+
webbrowser.open("file://{}".format(report_path))
|
|
147
|
+
proxy_docker.stop()
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
from typing import Dict, List
|
|
3
|
+
|
|
4
|
+
from . import postman, root, sub_command, id_command, proxy , newman, har
|
|
5
|
+
|
|
6
|
+
avail_sub_commands = [
|
|
7
|
+
postman.PostmanSubCommand("postman"),
|
|
8
|
+
id_command.PyntShowIdCommand("pynt-id"),
|
|
9
|
+
newman.NewmanSubCommand("newman"),
|
|
10
|
+
har.HarSubCommand("har"),
|
|
11
|
+
proxy.ProxyCommand("command"),
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
class PyntCommandException(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
class BadArgumentsException(PyntCommandException):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
class NoSuchCommandException(PyntCommandException):
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PyntCommand:
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self.base: root.BaseCommand = root.BaseCommand()
|
|
27
|
+
self.sub_commands: Dict[str, sub_command.PyntSubCommand] = {sc.get_name(): sc for sc in avail_sub_commands}
|
|
28
|
+
self._start_command()
|
|
29
|
+
|
|
30
|
+
def _start_command(self):
|
|
31
|
+
self.base.cmd()
|
|
32
|
+
for sc in self.sub_commands.values():
|
|
33
|
+
self.base.add_base_arguments(sc.add_cmd(self.base.get_subparser()))
|
|
34
|
+
|
|
35
|
+
def parse_args(self, args_from_cmd: List[str]):
|
|
36
|
+
return self.base.cmd().parse_args(args_from_cmd)
|
|
37
|
+
|
|
38
|
+
def run_cmd(self, args: argparse.Namespace):
|
|
39
|
+
if not "command" in args:
|
|
40
|
+
raise BadArgumentsException()
|
|
41
|
+
|
|
42
|
+
command = getattr(args, "command")
|
|
43
|
+
if not command in self.sub_commands:
|
|
44
|
+
raise NoSuchCommandException()
|
|
45
|
+
|
|
46
|
+
self.base.run_cmd(args)
|
|
47
|
+
self.sub_commands[command].run_cmd(args)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from pyntcli.auth import login
|
|
5
|
+
from pyntcli.ui import ui_thread
|
|
6
|
+
from pyntcli.analytics import send as analytics
|
|
7
|
+
|
|
8
|
+
def root_usage():
|
|
9
|
+
return ui_thread.PrinterText("Usage:",style=ui_thread.PrinterText.HEADER) \
|
|
10
|
+
.with_line("\tpynt [COMMAND] [OPTIONS]") \
|
|
11
|
+
.with_line("") \
|
|
12
|
+
.with_line("Commands:",style=ui_thread.PrinterText.HEADER) \
|
|
13
|
+
.with_line("\tpostman - integration with postman, run scan from pynt postman collection") \
|
|
14
|
+
.with_line("\tnewman - run postman collection from the CLI") \
|
|
15
|
+
.with_line("\tpynt-id - view your pynt-id to use when running pynt in CI pipeline") \
|
|
16
|
+
.with_line("") \
|
|
17
|
+
.with_line("Run pynt [COMMAND] -h to get help on a specific command",style=ui_thread.PrinterText.INFO)
|
|
18
|
+
|
|
19
|
+
def usage(*args):
|
|
20
|
+
ui_thread.print(root_usage())
|
|
21
|
+
|
|
22
|
+
class BaseCommand():
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self.base: argparse.ArgumentParser = None
|
|
25
|
+
self.subparser: argparse._SubParsersAction = None
|
|
26
|
+
|
|
27
|
+
def cmd(self):
|
|
28
|
+
if self.base:
|
|
29
|
+
return self.base
|
|
30
|
+
|
|
31
|
+
self.base = argparse.ArgumentParser(prog="pynt")
|
|
32
|
+
self.base.print_usage = usage
|
|
33
|
+
self.base.print_help = usage
|
|
34
|
+
return self.base
|
|
35
|
+
|
|
36
|
+
def add_base_arguments(self, parser):
|
|
37
|
+
parser.add_argument("--insecure", default=False, required=False, action='store_true',help="use when target uses self signed certificates")
|
|
38
|
+
parser.add_argument("--dev-flags", type=str,default="", help=argparse.SUPPRESS)
|
|
39
|
+
|
|
40
|
+
def get_subparser(self) -> argparse._SubParsersAction:
|
|
41
|
+
if self.subparser is None:
|
|
42
|
+
self.subparser = self.base.add_subparsers(help="", dest="command")
|
|
43
|
+
|
|
44
|
+
return self.subparser
|
|
45
|
+
|
|
46
|
+
def run_cmd(self, args: argparse.Namespace):
|
|
47
|
+
analytics.emit(analytics.LOGIN_START)
|
|
48
|
+
|
|
49
|
+
if login.should_login():
|
|
50
|
+
l = login.Login().login()
|
|
51
|
+
else:
|
|
52
|
+
login.refresh_token()
|
|
53
|
+
|
|
54
|
+
analytics.emit(analytics.LOGIN_DONE)
|
|
55
|
+
user_id = login.user_id()
|
|
56
|
+
if user_id:
|
|
57
|
+
analytics.set_user_id(user_id)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
class PyntSubCommand:
|
|
4
|
+
def __init__(self, name) -> None:
|
|
5
|
+
self.name = name
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
def get_name(self):
|
|
9
|
+
return self.name
|
|
10
|
+
|
|
11
|
+
def add_cmd(self, parent: argparse._SubParsersAction) -> argparse.ArgumentParser:
|
|
12
|
+
raise NotImplemented()
|
|
13
|
+
|
|
14
|
+
def run_cmd(self, args: argparse.Namespace):
|
|
15
|
+
raise NotImplemented()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import time
|
|
3
|
+
import socket
|
|
4
|
+
|
|
5
|
+
def is_port_in_use(port: int) -> bool:
|
|
6
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
7
|
+
return s.connect_ex(('localhost', port)) == 0
|
|
8
|
+
|
|
9
|
+
def find_open_port() -> int:
|
|
10
|
+
with socket.socket() as s:
|
|
11
|
+
s.bind(('', 0))
|
|
12
|
+
return s.getsockname()[1]
|
|
13
|
+
|
|
14
|
+
HEALTHCHECK_TIMEOUT = 10
|
|
15
|
+
HEALTHCHECK_INTERVAL = 2
|
|
16
|
+
|
|
17
|
+
def wait_for_healthcheck(address):
|
|
18
|
+
start = time.time()
|
|
19
|
+
while start + HEALTHCHECK_TIMEOUT > time.time():
|
|
20
|
+
try:
|
|
21
|
+
res = requests.get(address + "/healthcheck")
|
|
22
|
+
if res.status_code == 418:
|
|
23
|
+
return
|
|
24
|
+
except:
|
|
25
|
+
time.sleep(HEALTHCHECK_INTERVAL)
|
|
26
|
+
raise TimeoutError()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
from sys import argv, exit
|
|
2
|
+
import signal
|
|
3
|
+
|
|
4
|
+
from pyntcli import __version__ as cli_version
|
|
5
|
+
from pyntcli.commands import pynt_cmd
|
|
6
|
+
from pyntcli.pynt_docker import pynt_container
|
|
7
|
+
from pyntcli.ui import ui_thread
|
|
8
|
+
from pyntcli.auth import login
|
|
9
|
+
from pyntcli.analytics import send as analytics
|
|
10
|
+
|
|
11
|
+
def signal_handler(signal_number, frame):
|
|
12
|
+
ui_thread.print(ui_thread.PrinterText("Exiting..."))
|
|
13
|
+
|
|
14
|
+
analytics.stop()
|
|
15
|
+
pynt_container.PyntContainerRegistery.instance().stop_all_containers()
|
|
16
|
+
ui_thread.stop()
|
|
17
|
+
|
|
18
|
+
exit(0)
|
|
19
|
+
|
|
20
|
+
import requests
|
|
21
|
+
from distutils.version import StrictVersion
|
|
22
|
+
VERSION_CHECK_URL = "https://pypi.org/pypi/pyntcli/json"
|
|
23
|
+
|
|
24
|
+
def check_is_latest_version(current_version):
|
|
25
|
+
res = requests.get(VERSION_CHECK_URL)
|
|
26
|
+
res.raise_for_status()
|
|
27
|
+
|
|
28
|
+
avail_versions = list(res.json()["releases"].keys())
|
|
29
|
+
avail_versions.sort(key=StrictVersion)
|
|
30
|
+
|
|
31
|
+
if current_version != avail_versions[-1]:
|
|
32
|
+
ui_thread.print(ui_thread.PrinterText("""Pynt CLI new version is available, upgrade now with:
|
|
33
|
+
python3 -m pip install --upgrade pyntcli""",ui_thread.PrinterText.WARNING))
|
|
34
|
+
|
|
35
|
+
def check_for_dependecies():
|
|
36
|
+
pynt_container.get_docker_type()
|
|
37
|
+
|
|
38
|
+
def print_header():
|
|
39
|
+
ui_thread.print(ui_thread.PrinterText(*ui_thread.pynt_header()) \
|
|
40
|
+
.with_line(*ui_thread.pynt_version()) \
|
|
41
|
+
.with_line(""))
|
|
42
|
+
def start_analytics():
|
|
43
|
+
user_id = login.user_id()
|
|
44
|
+
if user_id:
|
|
45
|
+
analytics.set_user_id(user_id)
|
|
46
|
+
|
|
47
|
+
analytics.emit(analytics.CLI_START)
|
|
48
|
+
|
|
49
|
+
def main():
|
|
50
|
+
print_header()
|
|
51
|
+
try:
|
|
52
|
+
start_analytics()
|
|
53
|
+
check_for_dependecies()
|
|
54
|
+
check_is_latest_version(cli_version)
|
|
55
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
56
|
+
cli = pynt_cmd.PyntCommand()
|
|
57
|
+
cli.run_cmd(cli.parse_args(argv[1:]))
|
|
58
|
+
analytics.stop()
|
|
59
|
+
except pynt_cmd.PyntCommandException:
|
|
60
|
+
pynt_cmd.root.usage()
|
|
61
|
+
except pynt_container.DockerNotAvailableException:
|
|
62
|
+
ui_thread.print(ui_thread.PrinterText("Docker was unavailable, please make sure docker is installed and running.", ui_thread.PrinterText.WARNING))
|
|
63
|
+
analytics.emit(analytics.ERROR, {"error": "docker unavailable"})
|
|
64
|
+
except requests.exceptions.SSLError:
|
|
65
|
+
ui_thread.print(ui_thread.PrinterText("We encountered SSL issues and could not proceed, this may be the cause of a VPN or a Firewall in place.", ui_thread.PrinterText.WARNING))
|
|
66
|
+
analytics.emit(analytics.ERROR, {"error": "ssl issue"})
|
|
67
|
+
except login.Timeout:
|
|
68
|
+
ui_thread.print(ui_thread.PrinterText("Pynt CLI exited due to incomplete registration, please try again.", ui_thread.PrinterText.WARNING))
|
|
69
|
+
analytics.emit(analytics.ERROR, {"error": "login timeout"})
|
|
70
|
+
except login.InvalidTokenInEnvVarsException:
|
|
71
|
+
ui_thread.print(ui_thread.PrinterText("Pynt CLI exited due to malformed credentials provided in env vars.", ui_thread.PrinterText.WARNING))
|
|
72
|
+
analytics.emit(analytics.ERROR, {"error": "invalid pynt cli credentials in env vars"})
|
|
73
|
+
finally:
|
|
74
|
+
ui_thread.stop()
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .pynt_container import *
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import docker
|
|
2
|
+
from docker.errors import DockerException
|
|
3
|
+
from docker.types import Mount
|
|
4
|
+
import os
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from pyntcli.ui import ui_thread
|
|
8
|
+
|
|
9
|
+
PYNT_DOCKER_IMAGE = "ghcr.io/pynt-io/pynt"
|
|
10
|
+
|
|
11
|
+
def create_mount(src, destination, mount_type="bind"):
|
|
12
|
+
return Mount(target=destination, source=src, type=mount_type)
|
|
13
|
+
|
|
14
|
+
class DockerNotAvailableException(Exception):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
def get_docker_type():
|
|
18
|
+
try:
|
|
19
|
+
c = docker.from_env()
|
|
20
|
+
version_data = c.version()
|
|
21
|
+
platform = version_data.get("Platform")
|
|
22
|
+
|
|
23
|
+
if platform and platform.get("Name"):
|
|
24
|
+
return platform.get("Name")
|
|
25
|
+
|
|
26
|
+
return ""
|
|
27
|
+
|
|
28
|
+
except DockerException:
|
|
29
|
+
raise DockerNotAvailableException()
|
|
30
|
+
except Exception: #TODO: This is since windows is not behaving nice
|
|
31
|
+
raise DockerNotAvailableException()
|
|
32
|
+
|
|
33
|
+
class PyntDockerPort:
|
|
34
|
+
def __init__(self, src, dest, name) -> None:
|
|
35
|
+
self.src = src
|
|
36
|
+
self.dest = dest
|
|
37
|
+
self.name = name
|
|
38
|
+
|
|
39
|
+
def get_container_with_arguments(*args: PyntDockerPort):
|
|
40
|
+
if "desktop" in get_docker_type().lower():
|
|
41
|
+
ports = {}
|
|
42
|
+
for p in args:
|
|
43
|
+
ports[str(p.src)] = int(p.dest)
|
|
44
|
+
docker_type = PyntDockerDesktopContainer(ports=ports)
|
|
45
|
+
return docker_type , []
|
|
46
|
+
|
|
47
|
+
docker_arguments = []
|
|
48
|
+
docker_type = PyntNativeContainer(network="host")
|
|
49
|
+
|
|
50
|
+
for p in args:
|
|
51
|
+
docker_arguments.append(p.name)
|
|
52
|
+
docker_arguments.append(str(p.dest))
|
|
53
|
+
|
|
54
|
+
return docker_type, docker_arguments
|
|
55
|
+
|
|
56
|
+
def _container_image_from_tag(tag: str) -> str:
|
|
57
|
+
if ":" in tag:
|
|
58
|
+
return tag.split(":")[0]
|
|
59
|
+
|
|
60
|
+
return tag
|
|
61
|
+
|
|
62
|
+
class PyntContainer():
|
|
63
|
+
def __init__(self, image_name, tag, mounts, detach, args) -> None:
|
|
64
|
+
self.docker_client: docker.DockerClient = None
|
|
65
|
+
self.image = image_name if not os.environ.get("IMAGE") else os.environ.get("IMAGE")
|
|
66
|
+
self.tag = tag if not os.environ.get("TAG") else os.environ.get("TAG")
|
|
67
|
+
self.mounts = mounts
|
|
68
|
+
self.detach = detach
|
|
69
|
+
self.stdout = None
|
|
70
|
+
self.running = False
|
|
71
|
+
self.args = args
|
|
72
|
+
self.container_name = ""
|
|
73
|
+
|
|
74
|
+
def _create_docker_client(self):
|
|
75
|
+
self.docker_client = docker.from_env()
|
|
76
|
+
pat = os.environ.get("DOCKER_PASSWORD")
|
|
77
|
+
username = os.environ.get("DOCKER_USERNAME")
|
|
78
|
+
registry = os.environ.get("DOCKER_REGISTRY")
|
|
79
|
+
if pat and username and registry:
|
|
80
|
+
self.docker_client.login(username=username, password=pat, registry=registry)
|
|
81
|
+
|
|
82
|
+
def _is_docker_image_up_to_date(self, image):
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def _handle_outdated_docker_image(self, image):
|
|
86
|
+
return image
|
|
87
|
+
|
|
88
|
+
def kill_other_instances(self):
|
|
89
|
+
for c in self.docker_client.containers.list():
|
|
90
|
+
if _container_image_from_tag(c.image.tags[0]) == self.image:
|
|
91
|
+
c.kill()
|
|
92
|
+
|
|
93
|
+
def stop(self):
|
|
94
|
+
if not self.running:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
self.kill_other_instances()
|
|
98
|
+
|
|
99
|
+
self.docker_client.close()
|
|
100
|
+
self.docker_client = None
|
|
101
|
+
self.running = False
|
|
102
|
+
|
|
103
|
+
def is_alive(self):
|
|
104
|
+
if not self.docker_client or not self.container_name:
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
l = self.docker_client.containers.list(filters={"name": self.container_name})
|
|
108
|
+
if len(l) != 1:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
return l[0].status == "running"
|
|
112
|
+
|
|
113
|
+
def run(self, integration_docker):
|
|
114
|
+
if not self.docker_client:
|
|
115
|
+
self._create_docker_client()
|
|
116
|
+
|
|
117
|
+
self.running = True
|
|
118
|
+
self.kill_other_instances()
|
|
119
|
+
|
|
120
|
+
ui_thread.print(ui_thread.PrinterText("Pulling latest docker", ui_thread.PrinterText.INFO))
|
|
121
|
+
image = self.docker_client.images.pull(self.image, tag=self.tag)
|
|
122
|
+
if not self._is_docker_image_up_to_date(image):
|
|
123
|
+
image = self._handle_outdated_docker_image(image)
|
|
124
|
+
ui_thread.print(ui_thread.PrinterText("Docker pull done", ui_thread.PrinterText.INFO))
|
|
125
|
+
|
|
126
|
+
args = self.args if self.args else None
|
|
127
|
+
|
|
128
|
+
run_arguments = {
|
|
129
|
+
"image":image,
|
|
130
|
+
"detach":self.detach,
|
|
131
|
+
"mounts":self.mounts,
|
|
132
|
+
"stream": True,
|
|
133
|
+
"remove": True,
|
|
134
|
+
"command": args
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
run_arguments.update(integration_docker.get_argumets())
|
|
138
|
+
|
|
139
|
+
c = self.docker_client.containers.run(**run_arguments)
|
|
140
|
+
self.container_name = c.name
|
|
141
|
+
self.stdout = c.logs(stream=True)
|
|
142
|
+
|
|
143
|
+
PyntContainerRegistery.instance().register_container(self)
|
|
144
|
+
|
|
145
|
+
class PyntDockerDesktopContainer():
|
|
146
|
+
def __init__(self, ports) -> None:
|
|
147
|
+
self.ports = ports
|
|
148
|
+
|
|
149
|
+
def get_argumets(self):
|
|
150
|
+
return {"ports": self.ports} if self.ports else {}
|
|
151
|
+
|
|
152
|
+
class PyntNativeContainer():
|
|
153
|
+
def __init__(self, network) -> None:
|
|
154
|
+
self.network = network
|
|
155
|
+
|
|
156
|
+
def get_argumets(self):
|
|
157
|
+
return {"network": self.network} if self.network else {}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class PyntContainerRegistery():
|
|
161
|
+
_instance = None
|
|
162
|
+
|
|
163
|
+
def __init__(self) -> None:
|
|
164
|
+
self.containers: List[PyntContainer] = []
|
|
165
|
+
|
|
166
|
+
@staticmethod
|
|
167
|
+
def instance():
|
|
168
|
+
if not PyntContainerRegistery._instance:
|
|
169
|
+
PyntContainerRegistery._instance = PyntContainerRegistery()
|
|
170
|
+
|
|
171
|
+
return PyntContainerRegistery._instance
|
|
172
|
+
|
|
173
|
+
def register_container(self, c: PyntContainer):
|
|
174
|
+
self.containers.append(c)
|
|
175
|
+
|
|
176
|
+
def stop_all_containers(self):
|
|
177
|
+
for c in self.containers:
|
|
178
|
+
c.stop()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .store import CredStore
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from .store_connector import StoreConnector
|
|
4
|
+
|
|
5
|
+
class JsonStoreConnector(StoreConnector):
|
|
6
|
+
def __init__(self, data) -> None:
|
|
7
|
+
if not data:
|
|
8
|
+
data = JsonStoreConnector.default_value()
|
|
9
|
+
super().__init__(data)
|
|
10
|
+
self.json = json.loads(self._data)
|
|
11
|
+
|
|
12
|
+
def get(self, key):
|
|
13
|
+
return self.json.get(key, None)
|
|
14
|
+
|
|
15
|
+
def put(self, key, value):
|
|
16
|
+
self.json[key] = value
|
|
17
|
+
|
|
18
|
+
def dump(self):
|
|
19
|
+
return json.dumps(self.json, indent=2)
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def default_value():
|
|
23
|
+
return json.dumps({})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from typing import Type
|
|
2
|
+
import os
|
|
3
|
+
import platform
|
|
4
|
+
|
|
5
|
+
from .json_connector import JsonStoreConnector
|
|
6
|
+
from .store_connector import StoreConnector
|
|
7
|
+
|
|
8
|
+
class Store():
|
|
9
|
+
def __init__(self, file_location: str, connector_type: Type[StoreConnector]) -> None :
|
|
10
|
+
self.file_location = file_location
|
|
11
|
+
self.connector:StoreConnector = None
|
|
12
|
+
self._file = None
|
|
13
|
+
self._connector_tpye = connector_type
|
|
14
|
+
|
|
15
|
+
def _get_file_data(self):
|
|
16
|
+
if self.connector:
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
dirname = os.path.dirname(self.file_location)
|
|
20
|
+
if not os.path.exists(dirname):
|
|
21
|
+
os.makedirs(dirname)
|
|
22
|
+
|
|
23
|
+
if not os.path.exists(self.file_location):
|
|
24
|
+
with open(self.file_location, "w") as f:
|
|
25
|
+
self.connector = self._connector_tpye(self._connector_tpye.default_value())
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
with open(self.file_location, "r+") as f:
|
|
29
|
+
self.connector = self._connector_tpye(f.read())
|
|
30
|
+
|
|
31
|
+
def get(self, key):
|
|
32
|
+
self._get_file_data()
|
|
33
|
+
return self.connector.get(key)
|
|
34
|
+
|
|
35
|
+
def put(self, key, value):
|
|
36
|
+
self._get_file_data()
|
|
37
|
+
self.connector.put(key, value)
|
|
38
|
+
|
|
39
|
+
def get_path(self):
|
|
40
|
+
return self.file_location
|
|
41
|
+
|
|
42
|
+
def __enter__(self):
|
|
43
|
+
self._get_file_data()
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
def __exit__(self, type, value, traceback):
|
|
47
|
+
with open(self.file_location, "w") as f:
|
|
48
|
+
f.write(self.connector.dump())
|
|
49
|
+
|
|
50
|
+
class CredStore(Store):
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
dir = ".pynt"
|
|
53
|
+
super().__init__(file_location=os.path.join(os.path.expanduser("~"), dir, "creds.json"),
|
|
54
|
+
connector_type=JsonStoreConnector)
|
|
55
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
class StoreConnector():
|
|
2
|
+
def __init__(self, data) -> None:
|
|
3
|
+
self._data = data
|
|
4
|
+
|
|
5
|
+
def get(self, key):
|
|
6
|
+
raise NotImplemented()
|
|
7
|
+
|
|
8
|
+
def put(self, key, value):
|
|
9
|
+
raise NotImplementedError()
|
|
10
|
+
|
|
11
|
+
def dump(self):
|
|
12
|
+
raise NotImplementedError()
|
|
13
|
+
|
|
14
|
+
@classmethod
|
|
15
|
+
def default_value(cls):
|
|
16
|
+
raise NotImplementedError()
|
|
17
|
+
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import websocket
|
|
2
|
+
|
|
3
|
+
def connect_progress_ws(address):
|
|
4
|
+
ws = websocket.WebSocket()
|
|
5
|
+
ws.connect(address)
|
|
6
|
+
return ws
|
|
7
|
+
|
|
8
|
+
def wrap_ws_progress(ws):
|
|
9
|
+
prev = 0
|
|
10
|
+
while ws.connected:
|
|
11
|
+
try:
|
|
12
|
+
current = int(ws.recv())
|
|
13
|
+
yield current - prev
|
|
14
|
+
prev = current
|
|
15
|
+
except websocket.WebSocketConnectionClosedException:
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
class PyntProgress():
|
|
19
|
+
def __init__(self, what_to_track, total, description) -> None:
|
|
20
|
+
self.running = False
|
|
21
|
+
self.trackable = what_to_track
|
|
22
|
+
self.total = total
|
|
23
|
+
self.description = description
|
|
24
|
+
|
|
25
|
+
def __enter__(self):
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
29
|
+
self.running = False
|
|
30
|
+
|
|
31
|
+
|