bashhub 3.0.2__tar.gz → 3.0.3__tar.gz
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.
- {bashhub-3.0.2/bashhub.egg-info → bashhub-3.0.3}/PKG-INFO +8 -2
- {bashhub-3.0.2 → bashhub-3.0.3}/README.md +1 -1
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/bashhub.py +41 -42
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/bashhub_globals.py +15 -11
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/bashhub_setup.py +155 -53
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/bh.py +23 -11
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/i_search.py +31 -25
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/interactive_search.py +22 -19
- bashhub-3.0.3/bashhub/model/__init__.py +11 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/command.py +18 -19
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/command_form.py +6 -3
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/min_command.py +4 -4
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/serializable.py +16 -9
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/status_view.py +11 -3
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/model/system.py +13 -13
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/rest_client.py +113 -56
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell_utils.py +7 -5
- bashhub-3.0.3/bashhub/validation.py +54 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/version.py +1 -1
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/view/status.py +6 -2
- {bashhub-3.0.2 → bashhub-3.0.3/bashhub.egg-info}/PKG-INFO +8 -2
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub.egg-info/SOURCES.txt +5 -1
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub.egg-info/requires.txt +7 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/pyproject.toml +28 -0
- bashhub-3.0.3/tests/test_bashhub_setup.py +418 -0
- bashhub-3.0.3/tests/test_bh.py +32 -0
- bashhub-3.0.3/tests/test_rest_client.py +207 -0
- bashhub-3.0.3/tests/test_validation.py +76 -0
- bashhub-3.0.2/bashhub/model/__init__.py +0 -6
- bashhub-3.0.2/tests/test_bashhub_setup.py +0 -62
- {bashhub-3.0.2 → bashhub-3.0.3}/LICENSE.md +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/MANIFEST.in +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/__init__.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell/bashhub.fish +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell/bashhub.sh +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell/bashhub.zsh +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell/deps/bash-preexec.sh +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/shell/deps/lib-bashhub.sh +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub/view/__init__.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub.egg-info/dependency_links.txt +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub.egg-info/entry_points.txt +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/bashhub.egg-info/top_level.txt +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/setup.cfg +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_bashhub.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_bashhub_globals.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_command.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_command_form.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_interactive_search.py +0 -0
- {bashhub-3.0.2 → bashhub-3.0.3}/tests/test_shell_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bashhub
|
|
3
|
-
Version: 3.0.
|
|
3
|
+
Version: 3.0.3
|
|
4
4
|
Summary: Bashhub.com python client
|
|
5
5
|
Author-email: Ryan Caloras <ryan@bashhub.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -24,9 +24,15 @@ Requires-Dist: inflection>=0.5.1
|
|
|
24
24
|
Requires-Dist: humanize>=4.13.0
|
|
25
25
|
Provides-Extra: test
|
|
26
26
|
Requires-Dist: pytest>=3.3.1; extra == "test"
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: pytest>=3.3.1; extra == "dev"
|
|
29
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
31
|
+
Requires-Dist: types-requests; extra == "dev"
|
|
32
|
+
Requires-Dist: types-python-dateutil; extra == "dev"
|
|
27
33
|
Dynamic: license-file
|
|
28
34
|
|
|
29
|
-
<img src="https://bashhub.com/
|
|
35
|
+
<img src="https://bashhub.com/static/web/images/bashhub-logo.png" alt="Bashhub Logo">
|
|
30
36
|
|
|
31
37
|
Bashhub saves every terminal command entered across all sessions and systems and provides powerful querying across all commands.
|
|
32
38
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<img src="https://bashhub.com/
|
|
1
|
+
<img src="https://bashhub.com/static/web/images/bashhub-logo.png" alt="Bashhub Logo">
|
|
2
2
|
|
|
3
3
|
Bashhub saves every terminal command entered across all sessions and systems and provides powerful querying across all commands.
|
|
4
4
|
|
|
@@ -1,29 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
|
-
from
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from time import mktime
|
|
11
|
+
|
|
3
12
|
import click
|
|
4
|
-
import traceback
|
|
5
13
|
import dateutil.parser
|
|
6
|
-
import
|
|
7
|
-
import os
|
|
8
|
-
import io
|
|
14
|
+
import requests
|
|
9
15
|
|
|
16
|
+
from . import bashhub_globals, bashhub_setup, rest_client, shell_utils
|
|
17
|
+
from .bashhub_globals import BH_FILTER, BH_HOME, write_to_config_file
|
|
10
18
|
from .model import CommandForm
|
|
11
|
-
from . import rest_client
|
|
12
|
-
from . import bashhub_setup
|
|
13
|
-
from . import bashhub_globals
|
|
14
|
-
from .bashhub_globals import BH_FILTER, BH_HOME, BH_SAVE_COMMANDS
|
|
15
|
-
from .bashhub_globals import write_to_config_file
|
|
16
19
|
from .version import version_str
|
|
17
|
-
import
|
|
18
|
-
import requests
|
|
19
|
-
import subprocess
|
|
20
|
-
from . import shell_utils
|
|
21
|
-
import re
|
|
22
|
-
from .view.status import *
|
|
20
|
+
from .view.status import build_status_view
|
|
23
21
|
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
def print_version(ctx: click.Context, param: click.Parameter,
|
|
24
|
+
value: bool) -> None:
|
|
27
25
|
if not value or ctx.resilient_parsing:
|
|
28
26
|
return
|
|
29
27
|
click.echo(version_str)
|
|
@@ -42,13 +40,13 @@ CONTEXT_SETTINGS = dict(help_option_names=['-h', '--help'])
|
|
|
42
40
|
help='Display version',
|
|
43
41
|
expose_value=False,
|
|
44
42
|
is_eager=True)
|
|
45
|
-
def bashhub():
|
|
43
|
+
def bashhub() -> None:
|
|
46
44
|
"""Bashhub command line client"""
|
|
47
45
|
pass
|
|
48
46
|
|
|
49
47
|
|
|
50
48
|
@bashhub.command()
|
|
51
|
-
def version():
|
|
49
|
+
def version() -> None:
|
|
52
50
|
"""Display version"""
|
|
53
51
|
click.echo(version_str)
|
|
54
52
|
|
|
@@ -60,7 +58,7 @@ def version():
|
|
|
60
58
|
default=False,
|
|
61
59
|
is_flag=True,
|
|
62
60
|
help="Turn off saving commands for all sessions.")
|
|
63
|
-
def off(is_global):
|
|
61
|
+
def off(is_global: bool) -> None:
|
|
64
62
|
"""Turn off saving commands to Bashhub. Applies for this current session."""
|
|
65
63
|
if is_global:
|
|
66
64
|
write_to_config_file('save_commands', 'False')
|
|
@@ -74,7 +72,7 @@ def off(is_global):
|
|
|
74
72
|
"--local",
|
|
75
73
|
help="Turn on saving commands for only this session.",
|
|
76
74
|
is_flag=True)
|
|
77
|
-
def on(local):
|
|
75
|
+
def on(local: bool) -> None:
|
|
78
76
|
"""Turn on saving commands to Bashhub. Applies globally."""
|
|
79
77
|
f = io.open(BH_HOME + '/script.bh', 'w+', encoding='utf-8')
|
|
80
78
|
|
|
@@ -91,22 +89,23 @@ def on(local):
|
|
|
91
89
|
@click.argument('pid', type=int)
|
|
92
90
|
@click.argument('process_start_time', type=int)
|
|
93
91
|
@click.argument('exit_status', type=int)
|
|
94
|
-
def save(command, path, pid, process_start_time,
|
|
92
|
+
def save(command: str, path: str, pid: int, process_start_time: int,
|
|
93
|
+
exit_status: int) -> None:
|
|
95
94
|
"""Save a command to Bashhub"""
|
|
96
95
|
pid_start_time = unix_time_to_epoc_millis(process_start_time)
|
|
97
|
-
|
|
96
|
+
command_text = command.strip()
|
|
98
97
|
|
|
99
98
|
# Check if we have commands saving turned on
|
|
100
99
|
if not bashhub_globals.BH_SAVE_COMMANDS:
|
|
101
100
|
return
|
|
102
101
|
|
|
103
102
|
# Check if we should ignore this command.
|
|
104
|
-
if "#ignore" in
|
|
103
|
+
if "#ignore" in command_text:
|
|
105
104
|
return
|
|
106
105
|
|
|
107
106
|
# Check if we should filter this command.
|
|
108
107
|
bh_filter = bashhub_globals.BH_FILTER
|
|
109
|
-
if bh_filter and re.findall(bh_filter,
|
|
108
|
+
if bh_filter and re.findall(bh_filter, command_text):
|
|
110
109
|
return
|
|
111
110
|
|
|
112
111
|
# Check that we have an auth token.
|
|
@@ -114,18 +113,18 @@ def save(command, path, pid, process_start_time, exit_status):
|
|
|
114
113
|
print("No auth token found. Run 'bashhub setup' to login.")
|
|
115
114
|
return
|
|
116
115
|
|
|
117
|
-
|
|
118
|
-
rest_client.save_command(
|
|
116
|
+
command_form = CommandForm(command_text, path, exit_status, pid, pid_start_time)
|
|
117
|
+
rest_client.save_command(command_form)
|
|
119
118
|
|
|
120
119
|
|
|
121
120
|
@bashhub.command()
|
|
122
|
-
def setup():
|
|
121
|
+
def setup() -> None:
|
|
123
122
|
"""Run Bashhub user and system setup"""
|
|
124
123
|
bashhub_setup.main()
|
|
125
124
|
|
|
126
125
|
|
|
127
126
|
@bashhub.command()
|
|
128
|
-
def status():
|
|
127
|
+
def status() -> None:
|
|
129
128
|
"""Stats for this session and user"""
|
|
130
129
|
# Get our user and session information from our context
|
|
131
130
|
(ppid, start_time) = shell_utils.get_session_information()
|
|
@@ -136,9 +135,10 @@ def status():
|
|
|
136
135
|
|
|
137
136
|
@bashhub.command()
|
|
138
137
|
@click.pass_context
|
|
139
|
-
def help(ctx):
|
|
138
|
+
def help(ctx: click.Context) -> None:
|
|
140
139
|
"""Show this message and exit"""
|
|
141
|
-
|
|
140
|
+
if ctx.parent is not None:
|
|
141
|
+
click.echo(ctx.parent.get_help())
|
|
142
142
|
|
|
143
143
|
# Dynamic help text containing the BH_FILTER variable.
|
|
144
144
|
filtered_text = "BH_FILTER={0}".format(
|
|
@@ -157,7 +157,7 @@ is configured via a regex exported as BH_FILTER.
|
|
|
157
157
|
'--regex',
|
|
158
158
|
default=BH_FILTER,
|
|
159
159
|
help='Regex to filter against')
|
|
160
|
-
def filter(command, regex):
|
|
160
|
+
def filter(command: str, regex: str) -> None:
|
|
161
161
|
|
|
162
162
|
# Check if the regex we receive is valid
|
|
163
163
|
if not bashhub_globals.is_valid_regex(regex):
|
|
@@ -178,7 +178,7 @@ def filter(command, regex):
|
|
|
178
178
|
|
|
179
179
|
@bashhub.command()
|
|
180
180
|
@click.argument('version', type=str, default='')
|
|
181
|
-
def update(version):
|
|
181
|
+
def update(version: str) -> None:
|
|
182
182
|
"""Update your Bashhub installation"""
|
|
183
183
|
|
|
184
184
|
if version != '':
|
|
@@ -201,40 +201,39 @@ def update(version):
|
|
|
201
201
|
|
|
202
202
|
|
|
203
203
|
@bashhub.group()
|
|
204
|
-
def util():
|
|
204
|
+
def util() -> None:
|
|
205
205
|
"""Misc utils used by Bashhub"""
|
|
206
206
|
pass
|
|
207
207
|
|
|
208
208
|
|
|
209
209
|
@util.command()
|
|
210
|
-
def update_system_info():
|
|
210
|
+
def update_system_info() -> None:
|
|
211
211
|
"""Updates system info for Bashhub"""
|
|
212
212
|
result = bashhub_setup.update_system_info()
|
|
213
213
|
# Exit code based on if our update call was successful
|
|
214
|
-
sys.exit(0) if result
|
|
214
|
+
sys.exit(0) if result is not None else sys.exit(1)
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
@util.command()
|
|
218
218
|
@click.argument('date_string', type=str)
|
|
219
|
-
def parsedate(date_string):
|
|
219
|
+
def parsedate(date_string: str) -> None:
|
|
220
220
|
"""date string to seconds since the unix epoch"""
|
|
221
221
|
try:
|
|
222
222
|
date = dateutil.parser.parse(date_string)
|
|
223
223
|
unix_time = int(mktime(date.timetuple()))
|
|
224
224
|
click.echo(unix_time)
|
|
225
|
-
except Exception
|
|
225
|
+
except Exception:
|
|
226
226
|
# Should really log an error here
|
|
227
227
|
click.echo(0)
|
|
228
228
|
|
|
229
229
|
|
|
230
|
-
def unix_time_to_epoc_millis(unix_time):
|
|
230
|
+
def unix_time_to_epoc_millis(unix_time: int) -> int:
|
|
231
231
|
return int(unix_time) * 1000
|
|
232
232
|
|
|
233
233
|
|
|
234
|
-
def main():
|
|
234
|
+
def main() -> None:
|
|
235
235
|
try:
|
|
236
236
|
bashhub()
|
|
237
237
|
except Exception as e:
|
|
238
|
-
formatted = traceback.format_exc(e)
|
|
239
238
|
click.echo("Oops, looks like an exception occured: " + str(e))
|
|
240
239
|
sys.exit(1)
|
|
@@ -2,23 +2,25 @@
|
|
|
2
2
|
This file should be used for declaring any global variables that need to be
|
|
3
3
|
pulled in from environment variables or are just used across multiple files.
|
|
4
4
|
"""
|
|
5
|
+
from __future__ import annotations
|
|
5
6
|
|
|
7
|
+
import configparser
|
|
6
8
|
import os
|
|
7
9
|
import re
|
|
8
|
-
import time
|
|
9
10
|
import stat
|
|
11
|
+
import time as _time
|
|
12
|
+
from configparser import NoOptionError, NoSectionError
|
|
10
13
|
|
|
11
|
-
import configparser
|
|
12
|
-
from configparser import NoSectionError, NoOptionError
|
|
13
14
|
|
|
14
15
|
# Current time in milleseconds to use across app.
|
|
15
|
-
current_milli_time
|
|
16
|
+
def current_milli_time() -> int:
|
|
17
|
+
return int(round(_time.time() * 1000))
|
|
16
18
|
|
|
17
19
|
BH_HOME = '~/.bashhub' if 'HOME' not in list(os.environ.keys()) \
|
|
18
20
|
else os.environ['HOME'] + '/.bashhub'
|
|
19
21
|
|
|
20
22
|
|
|
21
|
-
def write_to_config_file(section, value):
|
|
23
|
+
def write_to_config_file(section: str, value: str) -> bool:
|
|
22
24
|
exists = os.path.exists(BH_HOME)
|
|
23
25
|
file_path = BH_HOME + '/config'
|
|
24
26
|
permissions = stat.S_IRUSR | stat.S_IWUSR
|
|
@@ -39,14 +41,14 @@ def write_to_config_file(section, value):
|
|
|
39
41
|
return False
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def get_from_config(key, default=''):
|
|
44
|
+
def get_from_config(key: str, default: str = '') -> str:
|
|
43
45
|
try:
|
|
44
46
|
config = configparser.ConfigParser()
|
|
45
47
|
config.read(BH_HOME + '/config')
|
|
46
48
|
return config.get('bashhub', key)
|
|
47
|
-
except NoSectionError
|
|
49
|
+
except NoSectionError:
|
|
48
50
|
return default
|
|
49
|
-
except NoOptionError
|
|
51
|
+
except NoOptionError:
|
|
50
52
|
return default
|
|
51
53
|
|
|
52
54
|
# Optional environment variable to configure for development
|
|
@@ -58,6 +60,8 @@ BH_SAVE_COMMANDS = os.getenv('BH_SAVE_COMMANDS', \
|
|
|
58
60
|
|
|
59
61
|
BH_SYSTEM_NAME = get_from_config("system_name")
|
|
60
62
|
|
|
63
|
+
BH_MAC = get_from_config("mac")
|
|
64
|
+
|
|
61
65
|
# Check if debug mode is enabled
|
|
62
66
|
BH_DEBUG = os.getenv('BH_DEBUG', get_from_config("debug"))
|
|
63
67
|
|
|
@@ -65,18 +69,18 @@ BH_DEBUG = os.getenv('BH_DEBUG', get_from_config("debug"))
|
|
|
65
69
|
# Get our token from the environment if one is present
|
|
66
70
|
# otherwise retrieve it from our config. Needs to
|
|
67
71
|
# be a function since we may change our token during setup
|
|
68
|
-
def BH_AUTH():
|
|
72
|
+
def BH_AUTH() -> str:
|
|
69
73
|
return os.getenv('BH_ACCESS_TOKEN', get_from_config("access_token"))
|
|
70
74
|
|
|
71
75
|
|
|
72
|
-
def is_valid_regex(regex):
|
|
76
|
+
def is_valid_regex(regex: str) -> bool:
|
|
73
77
|
try:
|
|
74
78
|
re.compile(regex)
|
|
75
79
|
return True
|
|
76
80
|
except re.error:
|
|
77
81
|
return False
|
|
78
82
|
|
|
79
|
-
def get_bh_filter():
|
|
83
|
+
def get_bh_filter() -> str:
|
|
80
84
|
filter = os.getenv('BH_FILTER', get_from_config('filter'))
|
|
81
85
|
return filter if is_valid_regex(filter) else '__invalid__'
|
|
82
86
|
|
|
@@ -1,26 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/python
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
import requests
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
7
4
|
import getpass
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
8
7
|
import traceback
|
|
9
8
|
import uuid
|
|
10
|
-
import
|
|
11
|
-
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
12
11
|
from . import rest_client
|
|
13
|
-
from .version import __version__
|
|
14
|
-
from .model import *
|
|
15
12
|
from .bashhub_globals import *
|
|
16
|
-
import
|
|
17
|
-
from
|
|
18
|
-
from
|
|
19
|
-
import collections
|
|
20
|
-
import configparser
|
|
13
|
+
from .model import *
|
|
14
|
+
from .validation import validate_email, validate_password, validate_username
|
|
15
|
+
from .version import __version__
|
|
21
16
|
|
|
22
17
|
|
|
23
|
-
def query_yes_no(question, default="yes"):
|
|
18
|
+
def query_yes_no(question: str, default: str = "yes") -> bool:
|
|
24
19
|
"""Ask a yes/no question via input() and return their answer.
|
|
25
20
|
|
|
26
21
|
"question" is a string that is presented to the user.
|
|
@@ -31,7 +26,7 @@ def query_yes_no(question, default="yes"):
|
|
|
31
26
|
The "answer" return value is one of "yes" or "no".
|
|
32
27
|
"""
|
|
33
28
|
valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False}
|
|
34
|
-
if default
|
|
29
|
+
if default is None:
|
|
35
30
|
prompt = " [y/n] "
|
|
36
31
|
elif default == "yes":
|
|
37
32
|
prompt = " [Y/n] "
|
|
@@ -52,10 +47,39 @@ def query_yes_no(question, default="yes"):
|
|
|
52
47
|
"(or 'y' or 'n').\n")
|
|
53
48
|
|
|
54
49
|
|
|
55
|
-
def
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
def prompt_until_valid(
|
|
51
|
+
prompt: str,
|
|
52
|
+
validator: Callable[[str], str | None],
|
|
53
|
+
secret: bool = False,
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Prompt the user until validator() returns None. Re-prompts with the
|
|
56
|
+
error message on failure."""
|
|
57
|
+
while True:
|
|
58
|
+
value = getpass.getpass(prompt) if secret else input(prompt)
|
|
59
|
+
error = validator(value)
|
|
60
|
+
if error is None:
|
|
61
|
+
return value
|
|
62
|
+
print(error)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def prompt_password_with_confirmation() -> str:
|
|
66
|
+
"""Prompt for a password, validate it, then require a matching
|
|
67
|
+
confirmation. Re-prompts from the start if the confirmation doesn't
|
|
68
|
+
match."""
|
|
69
|
+
while True:
|
|
70
|
+
password = prompt_until_valid(
|
|
71
|
+
"What password? ", validate_password, secret=True)
|
|
72
|
+
confirm = getpass.getpass("Confirm password: ")
|
|
73
|
+
if password == confirm:
|
|
74
|
+
return password
|
|
75
|
+
print("Passwords did not match. Please try again.")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def get_new_user_information() -> RegisterUser:
|
|
79
|
+
email = prompt_until_valid("What's your email? ", validate_email)
|
|
80
|
+
username = prompt_until_valid(
|
|
81
|
+
"What username would you like? ", validate_username)
|
|
82
|
+
password = prompt_password_with_confirmation()
|
|
59
83
|
print("\nEmail: " + email + " Username: " + username)
|
|
60
84
|
all_good = query_yes_no("Are these correct?")
|
|
61
85
|
|
|
@@ -65,59 +89,121 @@ def get_new_user_information():
|
|
|
65
89
|
return get_new_user_information()
|
|
66
90
|
|
|
67
91
|
|
|
68
|
-
def get_user_information_and_login(
|
|
92
|
+
def get_user_information_and_login(
|
|
93
|
+
username: str | None = None,
|
|
94
|
+
password: str | None = None,
|
|
95
|
+
attempts: int = 0,
|
|
96
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
69
97
|
if attempts == 4:
|
|
70
98
|
print("Too many bad attempts.")
|
|
71
|
-
return None
|
|
99
|
+
return (None, None, None)
|
|
72
100
|
|
|
73
101
|
# Only collect user information if we don't already have it
|
|
74
102
|
# i.e. if we didn't just register a new user.
|
|
75
|
-
if username
|
|
103
|
+
if username is None and password is None:
|
|
76
104
|
print("Please enter your bashhub credentials")
|
|
77
105
|
username = input("Username: ")
|
|
78
106
|
password = getpass.getpass("Password: ")
|
|
79
107
|
|
|
80
108
|
# login once we have all of our information
|
|
109
|
+
if username is None or password is None:
|
|
110
|
+
return (None, None, None)
|
|
81
111
|
access_token = rest_client.login_user(LoginForm(username, password))
|
|
82
112
|
|
|
83
113
|
# Package our result to include our credentials to later login our system.
|
|
84
114
|
if access_token:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
115
|
+
return (username, password, access_token)
|
|
116
|
+
return get_user_information_and_login(attempts=attempts + 1) or (
|
|
117
|
+
None, None, None)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_mac_address() -> str:
|
|
121
|
+
"""Get the mac address for our system as a fingerprint.
|
|
122
|
+
|
|
123
|
+
The mac is our stable, opaque identifier for a machine — the
|
|
124
|
+
server keys systems on ``(user, mac)``. To avoid drift between
|
|
125
|
+
runs (multi-interface boxes, VPN/Docker churn, random-mac
|
|
126
|
+
fallbacks), we prefer a value that we've previously agreed on
|
|
127
|
+
with the server and persisted in ``~/.bashhub/config``.
|
|
128
|
+
|
|
129
|
+
Resolution order:
|
|
130
|
+
1. ``mac`` from the config file, if present. This is the
|
|
131
|
+
source of truth once we've successfully registered or
|
|
132
|
+
reconciled with the server.
|
|
133
|
+
2. ``uuid.getnode()`` — today's behavior, used only on the
|
|
134
|
+
very first run before anything has been written to config.
|
|
135
|
+
3. Hostname, if ``getnode()`` synthesized a random multicast
|
|
136
|
+
value (indicating it couldn't find any hardware MAC).
|
|
137
|
+
|
|
138
|
+
Note: we do *not* persist the computed value here. Persisting
|
|
139
|
+
happens only after the server confirms the value (on successful
|
|
140
|
+
register / PATCH / name-fallback resolve), so a transient bad
|
|
141
|
+
value never gets pinned into config.
|
|
142
|
+
"""
|
|
92
143
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
cached = get_from_config("mac")
|
|
145
|
+
if cached:
|
|
146
|
+
return cached
|
|
96
147
|
|
|
97
|
-
|
|
148
|
+
mac_int = uuid.getnode()
|
|
98
149
|
# check if getnode fails
|
|
99
|
-
if (
|
|
150
|
+
if (mac_int >> 40) & 1:
|
|
100
151
|
hostname = socket.gethostname()
|
|
101
152
|
print("warning: cannot find MAC. Using hostname (%s) to identify system" % hostname)
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
mac = str(mac)
|
|
105
|
-
return mac
|
|
153
|
+
return hostname
|
|
154
|
+
return str(mac_int)
|
|
106
155
|
|
|
107
156
|
|
|
108
|
-
# Update our hostname
|
|
109
|
-
|
|
157
|
+
# Update our hostname in case it changed, and opportunistically pin
|
|
158
|
+
# the mac into local config / reconcile server-side if it drifted.
|
|
159
|
+
def update_system_info() -> int | None:
|
|
110
160
|
mac = get_mac_address()
|
|
111
161
|
hostname = socket.gethostname()
|
|
112
162
|
patch = SystemPatch(hostname=hostname, client_version=__version__)
|
|
113
|
-
|
|
163
|
+
result = rest_client.patch_system(patch, mac)
|
|
164
|
+
|
|
165
|
+
if result is not None:
|
|
166
|
+
# Happy path. Pin mac into config if it wasn't already there
|
|
167
|
+
# so future runs are immune to uuid.getnode() drift.
|
|
168
|
+
if not get_from_config("mac"):
|
|
169
|
+
write_to_config_file("mac", mac)
|
|
170
|
+
return result
|
|
171
|
+
|
|
172
|
+
# PATCH failed. If we have a stored system_name, try to self-heal
|
|
173
|
+
# by looking the system up by name and rewriting its mac on the
|
|
174
|
+
# server to match the one we just computed. This covers the
|
|
175
|
+
# common `bashhub update` scenario where uuid.getnode() returned
|
|
176
|
+
# a different value than what was originally registered.
|
|
177
|
+
system_name = get_from_config("system_name")
|
|
178
|
+
if not system_name:
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
system = rest_client.get_system_information(name=system_name)
|
|
182
|
+
if system is None:
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
reconcile = SystemPatch(
|
|
186
|
+
mac=mac, hostname=hostname, client_version=__version__)
|
|
187
|
+
reconciled = rest_client.patch_system(reconcile, system.mac)
|
|
188
|
+
if reconciled is not None:
|
|
189
|
+
write_to_config_file("mac", mac)
|
|
190
|
+
return reconciled
|
|
114
191
|
|
|
115
192
|
|
|
116
|
-
def handle_system_information(
|
|
193
|
+
def handle_system_information(
|
|
194
|
+
username: str,
|
|
195
|
+
password: str,
|
|
196
|
+
attempts: int = 0,
|
|
197
|
+
) -> tuple[str | None, str | None]:
|
|
117
198
|
|
|
118
199
|
mac = get_mac_address()
|
|
119
|
-
|
|
120
|
-
|
|
200
|
+
# Pass both mac and the previously-stored system_name (if any) so
|
|
201
|
+
# the server's mac->name fallback can recognize this box in one
|
|
202
|
+
# round-trip when the mac has drifted since last registration.
|
|
203
|
+
stored_system_name = get_from_config("system_name") or None
|
|
204
|
+
system = rest_client.get_system_information(
|
|
205
|
+
mac=mac, name=stored_system_name)
|
|
206
|
+
system_name: str | None = None
|
|
121
207
|
# Register a new System if this one isn't recognized
|
|
122
208
|
if system is None:
|
|
123
209
|
hostname = socket.gethostname()
|
|
@@ -130,9 +216,12 @@ def handle_system_information(username, password, attempts=0):
|
|
|
130
216
|
name, mac, hostname, __version__))
|
|
131
217
|
if system_name:
|
|
132
218
|
print("Registered a new system " + name)
|
|
219
|
+
# Pin the mac we just registered into local config so
|
|
220
|
+
# future runs read it back instead of recomputing.
|
|
221
|
+
write_to_config_file("mac", mac)
|
|
133
222
|
else:
|
|
134
223
|
if attempts < 3:
|
|
135
|
-
print("Looks like registering your system failed.
|
|
224
|
+
print("Looks like registering your system failed. Let's retry.")
|
|
136
225
|
return handle_system_information(username, password, attempts + 1)
|
|
137
226
|
else:
|
|
138
227
|
return (None, None)
|
|
@@ -149,11 +238,21 @@ def handle_system_information(username, password, attempts=0):
|
|
|
149
238
|
system_name = system.name
|
|
150
239
|
print("Welcome back! Looks like this box is already registered as " +
|
|
151
240
|
system.name + ".")
|
|
241
|
+
# The server may have matched by name rather than mac (the
|
|
242
|
+
# "drifted" case). If so, PATCH the server's mac to the
|
|
243
|
+
# current client value so both sides converge. Either way,
|
|
244
|
+
# pin the mac we agreed on into config.
|
|
245
|
+
if system.mac != mac:
|
|
246
|
+
hostname = socket.gethostname()
|
|
247
|
+
reconcile = SystemPatch(
|
|
248
|
+
mac=mac, hostname=hostname, client_version=__version__)
|
|
249
|
+
rest_client.patch_system(reconcile, system.mac)
|
|
250
|
+
write_to_config_file("mac", mac)
|
|
152
251
|
|
|
153
252
|
return (access_token, system_name)
|
|
154
253
|
|
|
155
254
|
|
|
156
|
-
def main():
|
|
255
|
+
def main() -> None:
|
|
157
256
|
try:
|
|
158
257
|
|
|
159
258
|
ascii_art = r"""
|
|
@@ -171,9 +270,9 @@ def main():
|
|
|
171
270
|
is_new_user = query_yes_no("Are you a new user?")
|
|
172
271
|
|
|
173
272
|
# Initialize variaous Credentials for logging in.
|
|
174
|
-
username = None
|
|
175
|
-
password = None
|
|
176
|
-
access_token = None
|
|
273
|
+
username: str | None = None
|
|
274
|
+
password: str | None = None
|
|
275
|
+
access_token: str | None = None
|
|
177
276
|
|
|
178
277
|
# If this is a new user walk them through the registration flow
|
|
179
278
|
if is_new_user:
|
|
@@ -193,7 +292,7 @@ def main():
|
|
|
193
292
|
|
|
194
293
|
(username, password, access_token) = get_user_information_and_login(
|
|
195
294
|
username, password)
|
|
196
|
-
if access_token
|
|
295
|
+
if access_token is None:
|
|
197
296
|
print("\nSorry looks like logging in failed.")
|
|
198
297
|
print("If you forgot your password please reset it. "
|
|
199
298
|
"https://bashhub.com/password-reset")
|
|
@@ -208,11 +307,14 @@ def main():
|
|
|
208
307
|
print("Writing your config file failed.")
|
|
209
308
|
sys.exit(1)
|
|
210
309
|
|
|
310
|
+
if username is None or password is None:
|
|
311
|
+
print("Sorry looks like getting your credentials failed. Exiting...")
|
|
312
|
+
sys.exit(1)
|
|
211
313
|
(access_token, system_name) = handle_system_information(username,
|
|
212
314
|
password)
|
|
213
315
|
|
|
214
|
-
if access_token
|
|
215
|
-
print("Sorry looks like getting your
|
|
316
|
+
if access_token is None or system_name is None:
|
|
317
|
+
print("Sorry looks like getting your access_token or system_name failed.\
|
|
216
318
|
Exiting...")
|
|
217
319
|
sys.exit(0)
|
|
218
320
|
|