Skip to content

Commit

Permalink
Merge pull request #20 from jiri-otoupal/ssh_processes
Browse files Browse the repository at this point in the history
Change of ssh and ssh pod
  • Loading branch information
jiri-otoupal committed Mar 11, 2024
2 parents 7cfb736 + 27bb817 commit eeda947
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 22 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pip3 install abst
deleted, will keep your,
leave context empty if you want to use default
connection alive till you kill this script
* `abst do forward/managed {context}` alias for `abst create`
* `abst clean` for removal all the saved credentials
* `abst use {context}` for using different config that you had filled, `default` is the default
context in `creds.json`
Expand All @@ -74,9 +75,17 @@ pip3 install abst
* `abst ctx list` to list contexts
* `abst ctx upgrade <context-name>` to upgrade context you can use `--all` flag to upgrade all contexts
* `abst ctx locate <context-name>` to get a full path of context
* `abst ctx share <context-name>` to copy context contents into clipboard and display it, you can use `--raw` to just get json
* `abst ctx share <context-name>` to copy context contents into clipboard and display it, you can use `--raw` to just
get json
* `abst ctx paste <context-name>` to paste contents into context file provided from name

### Session commands

* `abst ssh` facilitates selecting an instance to connect to. The optional `port` argument can be specified partially; a
connection is established if only one match is found. Use `-n <context-name>` to filter a connection, where the name
can be a partial match. `abst` employs the `in` operator for searching and proceeds with SSH if a single matching
instance exists.

### Parallel execution

If you are more demanding and need to have connection to your SSH Tunnels ready at all times
Expand Down Expand Up @@ -105,6 +114,7 @@ this program

### Kubectl commands

* `abst pod ssh <part of name>` will ssh into pod with similar name, if multiple found it will display select
* `abst cp secret secret_namespace target_namespace source_namespace(optional)` this will copy
secret to target namespace, without providing source namespace it will use current as a source

Expand Down
7 changes: 4 additions & 3 deletions abst/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
"CLI Command making OCI Bastion and kubernetes usage simple and fast"
)

__version__ = "2.3.37"
__version__ = "2.3.40"
__author__ = "Jiri Otoupal"
__author_email__ = "jiri-otoupal@ips-database.eu"
__license__ = "MIT"
__url__ = "https://github.com/jiri-otoupal/abst"
__pypi_repo__ = "https://pypi.org/project/abst/"

__version_name__ = "Formatted-Merged Giraffe"
__version_name__ = "SSHed Penguin"
__change_log__ = """
* Fixed try except for response being None\n
* 'abst ssh' will show screen with available instances for ssh from all abst processes\n
* moved old ssh to pod from 'abst ssh' to 'abst pod ssh'
"""
22 changes: 19 additions & 3 deletions abst/bastion_support/oci_bastion.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@

from abst.config import default_creds_path, \
default_contexts_location, default_conf_path, \
default_conf_contents, get_public_key, default_parallel_sets_location
default_conf_contents, get_public_key, default_parallel_sets_location, broadcast_shm_name
from abst.sharing.local_broadcast import LocalBroadcast
from abst.wrappers import mark_on_exit


Expand Down Expand Up @@ -46,6 +47,9 @@ def __init__(self, context_name=None, region=None, direct_json_path=None):
self.direct_json_path = direct_json_path
self.__mark_used__(direct_json_path)

self.lb = LocalBroadcast(broadcast_shm_name)
self.lb.store_json({context_name: {"region": self.region}})

def __mark_used__(self, path: Path | None = None):
if path is None:
cfg_path = Bastion.get_creds_path_resolve(self.context_name)
Expand All @@ -63,6 +67,7 @@ def current_status(self):
@current_status.setter
def current_status(self, value):
self._current_status = value
self.lb.store_json({self.context_name: {"status": value}})

def get_bastion_state(self) -> dict:
session_id = self.response["id"]
Expand Down Expand Up @@ -93,6 +98,8 @@ def kill(self):
print(f"Removed Session {self.get_print_name()}")
except Exception:
print(f"Looks like Bastion is already deleted {self.get_print_name()}")
finally:
self.lb.close()

@classmethod
def delete_bastion_session(cls, sess_id, region=None):
Expand Down Expand Up @@ -201,7 +208,16 @@ def create_forward_loop(self, shell: bool = False, force: bool = False):
Bastion.shell = shell
print(f"Loading Credentials for {self.get_print_name()}")
creds = self.load_self_creds()
title(f'{self.get_print_name()}:{creds["local-port"]}')
local_port = creds.get("local-port", 22)
username = creds.get("resource-os-username", None)
if username:
self.lb.store_json({self.context_name: {"port": local_port, "username": username}})
else:
rich.print(
"[yellow]No username in context json, please "
"specify resource-os-username for abst ssh to work[/yellow]")

title(f'{self.get_print_name()}:{local_port}')

self.current_status = "creating bastion session"

Expand Down Expand Up @@ -236,7 +252,7 @@ def create_forward_loop(self, shell: bool = False, force: bool = False):
user_custom_args = creds.get("ssh-custom-arguments", "")
ssh_tunnel_arg_str = self.run_ssh_tunnel_port_forward(bid, host, ip, port,
shell,
creds.get("local-port", 22),
local_port,
ssh_pub_key_path, force, user_custom_args)

while status := (sdata := self.get_bastion_state())[
Expand Down
9 changes: 9 additions & 0 deletions abst/cli_commands/create_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ def create():
pass


@click.group(name="do", help="Alias Group of commands for creating Bastion sessions")
def _do():
pass


@create.command(
"forward",
help="Creates and connects to Bastion session indefinitely until terminated by user",
Expand Down Expand Up @@ -72,3 +77,7 @@ def managed(shell, debug, context_name):
shell=shell)

sleep(1)


_do.add_command(forward)
_do.add_command(managed)
17 changes: 8 additions & 9 deletions abst/cli_commands/kubectl_cli/commands.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import os
import re
import subprocess

import click
import rich
Expand All @@ -10,7 +9,12 @@
from abst.utils.misc_funcs import setup_calls, fetch_pods


@click.command("ssh", help="Will SSH into pod with containing string name")
@click.group()
def pod():
pass


@pod.command("ssh", help="Will SSH into pod with containing string name")
@click.argument("pod_name")
@click.option("--debug", is_flag=True, default=False)
def ssh_pod(pod_name, debug):
Expand Down Expand Up @@ -49,19 +53,14 @@ def ssh_pod(pod_name, debug):
)


@click.command("logs", help="Will get logs from a pod with containing string name")
@pod.command("logs", help="Will get logs from a pod with containing string name")
@click.argument("pod_name")
@click.option("--debug", is_flag=True, default=False)
def log_pod(pod_name, debug):
setup_calls(debug)

try:
rich.print("Fetching pods")
pod_lines = (
subprocess.check_output(f"kubectl get pods -A".split(" "))
.decode()
.split("\n")
)
pod_lines = fetch_pods()
except FileNotFoundError:
rich.print("[red]kubectl not found on this machine[/red]")
return
Expand Down
Empty file.
42 changes: 42 additions & 0 deletions abst/cli_commands/ssh_cli/commands.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import click
import rich
from InquirerPy import inquirer

from abst.cli_commands.ssh_cli.utils import filter_keys_by_substring, filter_keys_by_port, do_ssh
from abst.config import broadcast_shm_name
from abst.sharing.local_broadcast import LocalBroadcast
from abst.utils.misc_funcs import setup_calls


@click.command("ssh", help="Will SSH into pod with containing string name")
@click.argument("port", required=False, default=None)
@click.option("-n", "--name", default=None, help="Name of context running, it can be just part of the name")
@click.option("--debug", is_flag=True, default=False)
def ssh_lin(port, name, debug):
setup_calls(debug)

lb = LocalBroadcast(broadcast_shm_name)
data = lb.retrieve_json()

if name and len(keys := filter_keys_by_substring(data, name)) == 1:
do_ssh(keys[0], data[keys[0]]["username"], data[keys[0]]["port"])
return

if port is not None and len(keys := filter_keys_by_port(data, port)) == 1:
do_ssh(keys[0], data[keys[0]]["username"], data[keys[0]]["port"])
return

longest_key = max(len(key) for key in data.keys())
questions = [{"name": f"{key.ljust(longest_key)} |= status: {data['status']}", "value": (key, data)} for key, data
in data.items()]

context_name, context = inquirer.select("Select context to ssh to:", questions).execute()

if "username" not in context:
rich.print("[red]Please fill resource-os-username into config for this feature to work[/red]")
return
elif "port" not in context:
rich.print("[red]Please fill local-port into config for this feature to work[/red]")
return

do_ssh(name, context["username"], context["port"])
22 changes: 22 additions & 0 deletions abst/cli_commands/ssh_cli/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os

import rich


def filter_keys_by_substring(dictionary: dict, substring: str) -> list:
# Convert substring to lower a case for case-insensitive comparison
substring_lower = substring.lower()
# Use a comprehension to filter keys
filtered_keys = [key for key in dictionary.keys() if substring_lower in key.lower()]
return filtered_keys


def filter_keys_by_port(data: dict, port: int):
"""Filter and return keys from the servers dictionary where the port matches the given input."""
return [key for key, data in data.items() if port in str(data.get('port', None))]


def do_ssh(context_name, username, port):
rich.print(f"[green]Running SSH to {context_name}[/green] "
f"[yellow]{username}@localhost:{port}[/yellow]")
os.system(f'ssh {username}@127.0.0.1 -p {port}')
4 changes: 4 additions & 0 deletions abst/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
# Share Config
share_excluded_keys: tuple = ("private-key-path", "ssh-pub-path", "last-time-used")

# Max Shared JSON size
max_json_shared = 1048576 # Bytes
broadcast_shm_name = "abst_shared_memory"


def get_public_key(ssh_path):
with open(str(Path(ssh_path).expanduser().resolve()), "r") as f:
Expand Down
10 changes: 6 additions & 4 deletions abst/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@
from abst.bastion_support.oci_bastion import Bastion
from abst.cli_commands.context.commands import context, ctx
from abst.cli_commands.cp_cli.commands import cp
from abst.cli_commands.create_cli.commands import create
from abst.cli_commands.create_cli.commands import create, _do
from abst.cli_commands.helm_cli.commands import helm
from abst.cli_commands.kubectl_cli.commands import ssh_pod, log_pod
from abst.cli_commands.kubectl_cli.commands import pod
from abst.cli_commands.parallel.commands import parallel, pl
from abst.cli_commands.ssh_cli.commands import ssh_lin
from abst.config import default_creds_path, default_contexts_location, default_conf_path
from abst.notifier.version_notifier import Notifier
from abst.utils.misc_funcs import setup_calls
Expand Down Expand Up @@ -122,9 +123,10 @@ def print_changelog(_config):
cli.add_command(ctx)
cli.add_command(helm)
cli.add_command(cp)
cli.add_command(ssh_pod)
cli.add_command(log_pod)
cli.add_command(pod)
cli.add_command(create)
cli.add_command(_do)
cli.add_command(ssh_lin)

if __name__ == "__main__":
main()
Empty file added abst/sharing/__init__.py
Empty file.
102 changes: 102 additions & 0 deletions abst/sharing/local_broadcast.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import json
import struct
from json import JSONDecodeError
from multiprocessing import shared_memory

from deepmerge import always_merger

from abst.config import max_json_shared


class LocalBroadcast:
_instance = None

def __new__(cls, name: str, size: int = max_json_shared):
if cls._instance is None:
cls._instance = super(LocalBroadcast, cls).__new__(cls)
cls._instance.__init_shared_memory(name[:14], size)
return cls._instance

def __init_shared_memory(self, name: str, size: int):
self._data_name = name
self._len_name = f"{name}_len"
self._size = size

try:
# Attempt to create the main shared memory block
self._data_shm = shared_memory.SharedMemory(name=self._data_name, create=True, size=size)
self._data_is_owner = True
except FileExistsError:
self._data_shm = shared_memory.SharedMemory(name=self._data_name, create=False)
self._data_is_owner = False

try:
self._len_shm = shared_memory.SharedMemory(name=self._len_name, create=True, size=8)
self._len_shm.buf[:8] = struct.pack('Q', 0)
self._len_is_owner = True
except FileExistsError:
self._len_shm = shared_memory.SharedMemory(name=self._len_name, create=False)
self._len_is_owner = False

def store_json(self, data: dict) -> int:
"""
Serialize and store JSON data in shared memory.
@return: Size of the serialized data in bytes
"""

data_before = self.retrieve_json()
for key, value in data.items():
for s_key in value.keys():
if type(data_before[key][s_key]) == type(data[key][s_key]):
data_before[key].pop(s_key)

data_copy = always_merger.merge(data, data_before)

serialized_data = json.dumps(data_copy).encode('utf-8')
if len(serialized_data) > self._size:
raise ValueError("Data exceeds allocated shared memory size.")

# Write the data length to the length shared memory
self._len_shm.buf[:8] = struct.pack('Q', len(serialized_data))

# Write data to the main shared memory
self._data_shm.buf[:len(serialized_data)] = serialized_data
return len(serialized_data)

def retrieve_json(self) -> dict:
"""
Retrieve and deserialize JSON data from shared memory.
"""
# Read the data length from the length shared memory
data_length = self.get_used_space()

if data_length == -1:
return {}

# Read data from the main shared memory
data = bytes(self._data_shm.buf[:data_length]).decode('utf-8')
try:
return json.loads(data)
except JSONDecodeError:
return {}

def get_used_space(self) -> int:
"""
Get the size of the shared memory
@return: Number of bytes used
"""
if self._len_shm.buf is None:
return -1
return struct.unpack('Q', self._len_shm.buf[:8])[0]

def close(self):
"""Close and unlink the shared memory blocks."""
try:
self._data_shm.close()
self._len_shm.close()
if self._data_is_owner:
self._data_shm.unlink()
if self._len_is_owner:
self._len_shm.unlink()
except FileNotFoundError:
pass
Empty file added abst/sharing/tests/__init__.py
Empty file.
Loading

0 comments on commit eeda947

Please sign in to comment.