aws-ssh-utils 0.1.1__tar.gz → 0.2.0__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.
- aws_ssh_utils-0.2.0/LICENSE +21 -0
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/PKG-INFO +7 -2
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/README.md +4 -0
- aws_ssh_utils-0.2.0/aws_ssh_utils/__init__.py +1 -0
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/aws_ssh_utils/emr_utils.py +0 -10
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/aws_ssh_utils/ssh.py +61 -13
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/pyproject.toml +1 -1
- aws_ssh_utils-0.1.1/aws_ssh_utils/__init__.py +0 -1
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/.gitignore +0 -0
- {aws_ssh_utils-0.1.1 → aws_ssh_utils-0.2.0}/aws_ssh_utils/interactive_ssh.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 mvanderlee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aws-ssh-utils
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Easy AWS SSHing
|
|
5
|
-
Project-URL: Homepage, https://github.com/mvanderlee/
|
|
5
|
+
Project-URL: Homepage, https://github.com/mvanderlee/aws-ssh-utils
|
|
6
6
|
Author-email: Michiel Vanderlee <jmt.vanderlee@gmail.com>
|
|
7
7
|
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
8
9
|
Classifier: Intended Audience :: Developers
|
|
9
10
|
Classifier: Intended Audience :: Information Technology
|
|
10
11
|
Classifier: Intended Audience :: System Administrators
|
|
@@ -37,6 +38,10 @@ Description-Content-Type: text/markdown
|
|
|
37
38
|
|
|
38
39
|
# AWS SSH Utils
|
|
39
40
|
|
|
41
|
+
[](https://pypi.org/project/aws-ssh-utils/)
|
|
42
|
+
[](#)
|
|
43
|
+
[](https://pypi.org/project/aws-ssh-utils/)
|
|
44
|
+
|
|
40
45
|
```shell
|
|
41
46
|
pip install aws-ssh-utils
|
|
42
47
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# AWS SSH Utils
|
|
2
2
|
|
|
3
|
+
[](https://pypi.org/project/aws-ssh-utils/)
|
|
4
|
+
[](#)
|
|
5
|
+
[](https://pypi.org/project/aws-ssh-utils/)
|
|
6
|
+
|
|
3
7
|
```shell
|
|
4
8
|
pip install aws-ssh-utils
|
|
5
9
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.2.0"
|
|
@@ -161,13 +161,3 @@ def get_emr_instance_ips(
|
|
|
161
161
|
]
|
|
162
162
|
for k, v in get_emr_instances(emr, cluster_id).items()
|
|
163
163
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
def get_instance_key_name(
|
|
167
|
-
emr: "EMRClient",
|
|
168
|
-
cluster_id: str,
|
|
169
|
-
) -> str:
|
|
170
|
-
cluster = emr.describe_cluster(ClusterId=cluster_id)
|
|
171
|
-
|
|
172
|
-
key_name = cluster["Cluster"]["Ec2InstanceAttributes"]["Ec2KeyName"]
|
|
173
|
-
return key_name
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import datetime as dt
|
|
1
2
|
import logging
|
|
2
3
|
import os
|
|
4
|
+
import subprocess
|
|
3
5
|
import sys
|
|
4
6
|
import textwrap
|
|
5
|
-
from dataclasses import dataclass
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from hashlib import sha1
|
|
6
9
|
from typing import TYPE_CHECKING
|
|
7
10
|
|
|
8
11
|
import boto3
|
|
9
12
|
import click
|
|
10
13
|
import click_spinner
|
|
11
14
|
import paramiko
|
|
15
|
+
import paramiko.pkey
|
|
12
16
|
import questionary
|
|
13
17
|
from botocore.exceptions import ClientError
|
|
14
18
|
from environs import Env
|
|
@@ -17,7 +21,6 @@ from loguru import logger
|
|
|
17
21
|
from .emr_utils import (
|
|
18
22
|
IP,
|
|
19
23
|
get_emr_instance_ips,
|
|
20
|
-
get_instance_key_name,
|
|
21
24
|
prompt_for_emr_cluster,
|
|
22
25
|
prompt_for_emr_instance_group,
|
|
23
26
|
)
|
|
@@ -28,6 +31,7 @@ if TYPE_CHECKING:
|
|
|
28
31
|
from mypy_boto3_emr import EMRClient
|
|
29
32
|
|
|
30
33
|
Env().read_env() # Load .env file
|
|
34
|
+
OPKSSH_PROVIDER_TAG = 'opkssh_provider'
|
|
31
35
|
|
|
32
36
|
|
|
33
37
|
class ShellError(Exception):
|
|
@@ -248,13 +252,6 @@ def emr_ssh_all(
|
|
|
248
252
|
exit(1)
|
|
249
253
|
|
|
250
254
|
|
|
251
|
-
@cli.command('test')
|
|
252
|
-
def test(**kwargs):
|
|
253
|
-
tmux('new-session -d -s "test" -n "window 1" "echo hello && $SHELL -i"')
|
|
254
|
-
tmux('new-window -n "window 2" -t "test:" "echo hello && $SHELL -i"')
|
|
255
|
-
tmux('switch-client -t "test:window 1"')
|
|
256
|
-
|
|
257
|
-
|
|
258
255
|
@dataclass
|
|
259
256
|
class SSHShell:
|
|
260
257
|
hostname: str
|
|
@@ -262,6 +259,17 @@ class SSHShell:
|
|
|
262
259
|
key_filename: str
|
|
263
260
|
terminal_title: str = None
|
|
264
261
|
|
|
262
|
+
private_key: paramiko.PKey = field(init=False)
|
|
263
|
+
|
|
264
|
+
def __post_init__(self):
|
|
265
|
+
if not os.path.isfile(self.key_filename):
|
|
266
|
+
raise ValueError(f'File {self.key_filename} does not exist')
|
|
267
|
+
|
|
268
|
+
self.private_key = paramiko.PKey.from_path(self.key_filename)
|
|
269
|
+
public_key_path = f'{self.key_filename}.pub'
|
|
270
|
+
if os.path.isfile(public_key_path):
|
|
271
|
+
self.private_key.load_certificate(public_key_path)
|
|
272
|
+
|
|
265
273
|
def connect(self):
|
|
266
274
|
self._open()
|
|
267
275
|
self._launch()
|
|
@@ -278,7 +286,11 @@ class SSHShell:
|
|
|
278
286
|
ssh_client.load_host_keys(host_key_path)
|
|
279
287
|
ssh_client.set_missing_host_key_policy(ConfirmAddPolicy())
|
|
280
288
|
try:
|
|
281
|
-
ssh_client.connect(
|
|
289
|
+
ssh_client.connect(
|
|
290
|
+
hostname=self.hostname,
|
|
291
|
+
username=self.username,
|
|
292
|
+
pkey=self.private_key,
|
|
293
|
+
)
|
|
282
294
|
except paramiko.BadHostKeyException as e:
|
|
283
295
|
error_message = f'''
|
|
284
296
|
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
|
|
@@ -359,8 +371,18 @@ def get_ec2_ssh_options(
|
|
|
359
371
|
key_file: str = None
|
|
360
372
|
) -> tuple[str, str, str, str]:
|
|
361
373
|
instance = prompt_for_ec2_instance(b3s)
|
|
374
|
+
instance_tags = {
|
|
375
|
+
tag['Key'].lower(): tag['Value']
|
|
376
|
+
for tag in instance.tags
|
|
377
|
+
}
|
|
378
|
+
|
|
362
379
|
if key_file is None:
|
|
363
|
-
|
|
380
|
+
# Support https://github.com/openpubkey/opkssh via tags
|
|
381
|
+
if OPKSSH_PROVIDER_TAG in instance_tags:
|
|
382
|
+
key_file = get_opkssh_key_file(instance_tags[OPKSSH_PROVIDER_TAG])
|
|
383
|
+
|
|
384
|
+
else:
|
|
385
|
+
key_file = try_to_find_ssh_key_file(instance.key_name)
|
|
364
386
|
|
|
365
387
|
if user is None:
|
|
366
388
|
logger.info('No user specified, attempting to detect required user...')
|
|
@@ -406,8 +428,17 @@ def get_emr_ssh_key_file_for_cluster(
|
|
|
406
428
|
cluster_id: str,
|
|
407
429
|
) -> str:
|
|
408
430
|
with click_spinner.spinner():
|
|
409
|
-
|
|
410
|
-
|
|
431
|
+
cluster = emr.describe_cluster(ClusterId=cluster_id)
|
|
432
|
+
cluster_tags = {
|
|
433
|
+
tag['Key'].lower(): tag['Value']
|
|
434
|
+
for tag in cluster['Cluster']['Tags']
|
|
435
|
+
}
|
|
436
|
+
# Support https://github.com/openpubkey/opkssh via tags
|
|
437
|
+
if OPKSSH_PROVIDER_TAG in cluster_tags:
|
|
438
|
+
key_file = get_opkssh_key_file(cluster_tags[OPKSSH_PROVIDER_TAG])
|
|
439
|
+
else:
|
|
440
|
+
key_name = cluster["Cluster"]["Ec2InstanceAttributes"]["Ec2KeyName"]
|
|
441
|
+
key_file = try_to_find_ssh_key_file(key_name)
|
|
411
442
|
|
|
412
443
|
if key_file is None:
|
|
413
444
|
should_continue = questionary.confirm(f'Could not find the ssh key {key_name}, would you like to continue?').unsafe_ask()
|
|
@@ -472,6 +503,23 @@ def get_ec2_name(ec2_instance: "Instance") -> str:
|
|
|
472
503
|
return ec2_instance.instance_id
|
|
473
504
|
|
|
474
505
|
|
|
506
|
+
def get_opkssh_key_file(provider: str) -> str:
|
|
507
|
+
opkssh_key_file = os.path.join(os.path.expanduser('~/.ssh/'), f'opkssh_{sha1(provider.encode()).hexdigest()}')
|
|
508
|
+
|
|
509
|
+
if os.path.exists(opkssh_key_file) and os.path.getmtime(opkssh_key_file) > dt.datetime.now().timestamp() - 86400:
|
|
510
|
+
logger.info('Found existing opkssh key')
|
|
511
|
+
else:
|
|
512
|
+
logger.info('Detected opkssh, logging in')
|
|
513
|
+
cmd = f"opkssh login --provider '{provider}' -i '{opkssh_key_file}'"
|
|
514
|
+
process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
|
|
515
|
+
process.wait()
|
|
516
|
+
if process.returncode != 0:
|
|
517
|
+
logger.error('Failed to authenticate using opkssh')
|
|
518
|
+
sys.exit(process.returncode)
|
|
519
|
+
|
|
520
|
+
return opkssh_key_file
|
|
521
|
+
|
|
522
|
+
|
|
475
523
|
class ConfirmAddPolicy(paramiko.client.MissingHostKeyPolicy):
|
|
476
524
|
"""
|
|
477
525
|
Policy for automatically adding the hostname and new host key to the
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.1.1"
|
|
File without changes
|
|
File without changes
|