outerbounds 0.7.3__py3-none-any.whl → 0.8.0rc0__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.
- outerbounds/command_groups/workstations_cli.py +105 -1
- outerbounds/utils/kubectl_utils.py +68 -0
- outerbounds/utils/ssh_utils.py +540 -0
- {outerbounds-0.7.3.dist-info → outerbounds-0.8.0rc0.dist-info}/METADATA +4 -5
- {outerbounds-0.7.3.dist-info → outerbounds-0.8.0rc0.dist-info}/RECORD +7 -5
- {outerbounds-0.7.3.dist-info → outerbounds-0.8.0rc0.dist-info}/WHEEL +1 -1
- {outerbounds-0.7.3.dist-info → outerbounds-0.8.0rc0.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,8 @@ import json
|
|
8
8
|
import os
|
9
9
|
from os import path
|
10
10
|
from pathlib import Path
|
11
|
-
from ..utils import kubeconfig, metaflowconfig
|
11
|
+
from ..utils import kubeconfig, metaflowconfig, ssh_utils
|
12
|
+
from ..utils.kubectl_utils import exec_in_pod, cp_to_pod
|
12
13
|
from requests.exceptions import HTTPError
|
13
14
|
import platform
|
14
15
|
import subprocess
|
@@ -23,6 +24,7 @@ from .perimeters_cli import (
|
|
23
24
|
get_perimeters_from_api_or_fail_command,
|
24
25
|
confirm_user_has_access_to_perimeter_or_fail,
|
25
26
|
)
|
27
|
+
import sys
|
26
28
|
|
27
29
|
KUBECTL_INSTALL_MITIGATION = "Please install kubectl manually from https://kubernetes.io/docs/tasks/tools/#kubectl"
|
28
30
|
|
@@ -594,3 +596,105 @@ def show_relevant_links(config_dir=None, profile=None, perimeter_id="", output="
|
|
594
596
|
else:
|
595
597
|
click.secho("Failed to show relevant links", fg="red", err=True)
|
596
598
|
click.secho("Error: {}".format(str(e)), fg="red", err=True)
|
599
|
+
|
600
|
+
|
601
|
+
@cli.command(help="Prepare a workstation for SSH access")
|
602
|
+
@click.option(
|
603
|
+
"--workstation-id",
|
604
|
+
default="",
|
605
|
+
help="The name of the workstation on the Outerbounds UI",
|
606
|
+
)
|
607
|
+
@click.option(
|
608
|
+
"--setup-context",
|
609
|
+
"-c",
|
610
|
+
default="local",
|
611
|
+
help="The context to use for the setup command",
|
612
|
+
type=click.Choice(["local", "remote"]),
|
613
|
+
)
|
614
|
+
def prepare_for_ssh_access(workstation_id="", setup_context=""):
|
615
|
+
"""
|
616
|
+
Finds the pod whose WORKSTATION_NAME env var matches `workstation_name`
|
617
|
+
and opens an interactive bash shell in its "workstation" container.
|
618
|
+
"""
|
619
|
+
if setup_context == "local":
|
620
|
+
prepare_for_ssh_access_local(workstation_id)
|
621
|
+
elif setup_context == "remote":
|
622
|
+
prepare_for_ssh_access_remote()
|
623
|
+
|
624
|
+
|
625
|
+
def prepare_for_ssh_access_local(workstation_id):
|
626
|
+
"""
|
627
|
+
SSH connection requires both local instance and workstation pod to do some work.
|
628
|
+
This function takes care of the local instance. It does the following:
|
629
|
+
|
630
|
+
1. Creates a new ssh key pair, or re-uses an existing one.
|
631
|
+
2. Copies the public key of the key pair to the workstation.
|
632
|
+
3. Executes the `outerbounds prepare-ws-for-ssh-access` command on the workstation. This command would setup the workstation for remote access.
|
633
|
+
4. Ensures that the SSH config is setup with the right parameters.
|
634
|
+
"""
|
635
|
+
pod_name = f"{workstation_id}-0"
|
636
|
+
|
637
|
+
private_key_path, public_key_path = ssh_utils.create_ssh_key_pair()
|
638
|
+
|
639
|
+
# Create the .ssh directory if it doesn't exist
|
640
|
+
result, stdout, stderr = exec_in_pod(
|
641
|
+
pod_name,
|
642
|
+
"ws-69304f8188e31a0745bace40b9378c6b",
|
643
|
+
"mkdir -p /home/ob-workspace/.ssh",
|
644
|
+
)
|
645
|
+
if result != 0:
|
646
|
+
raise Exception(f"Failed to create .ssh directory: {stderr}")
|
647
|
+
|
648
|
+
# Copy the public key to the workstation
|
649
|
+
result, stdout, stderr = cp_to_pod(
|
650
|
+
pod_name,
|
651
|
+
"ws-69304f8188e31a0745bace40b9378c6b",
|
652
|
+
public_key_path,
|
653
|
+
f"/home/ob-workspace/.ssh/{ssh_utils.EXPECTED_PUBLIC_KEY_NAME}",
|
654
|
+
)
|
655
|
+
if result != 0:
|
656
|
+
raise Exception(f"Failed to copy public key to workstation: {stderr}")
|
657
|
+
|
658
|
+
# 4. Exec into the pod
|
659
|
+
result, stdout, stderr = exec_in_pod(
|
660
|
+
pod_name,
|
661
|
+
"ws-69304f8188e31a0745bace40b9378c6b",
|
662
|
+
"outerbounds prepare-for-ssh-access -c remote",
|
663
|
+
)
|
664
|
+
if result != 0:
|
665
|
+
raise Exception(f"Failed to exec into workstation: {stderr}")
|
666
|
+
|
667
|
+
# 5. Add the entry to the ssh config file
|
668
|
+
ok, msg = ssh_utils.add_entry_to_ssh_config(
|
669
|
+
workstation_id, "ws-69304f8188e31a0745bace40b9378c6b", private_key_path
|
670
|
+
)
|
671
|
+
if not ok:
|
672
|
+
raise Exception(f"Failed to add entry to ssh config: {msg}")
|
673
|
+
|
674
|
+
|
675
|
+
def prepare_for_ssh_access_remote():
|
676
|
+
"""
|
677
|
+
SSH connection requires both local instance and workstation pod to do some work.
|
678
|
+
This function takes care of the remote instance.
|
679
|
+
|
680
|
+
For now, it assumes:
|
681
|
+
|
682
|
+
1. The image of the workstation already has openssh-server, netcat installed.
|
683
|
+
"""
|
684
|
+
|
685
|
+
if "WORKSTATION_ID" not in os.environ:
|
686
|
+
raise Exception("This can only be run from a workstation!")
|
687
|
+
|
688
|
+
ok, message = ssh_utils.best_effort_install_ssh_netcat()
|
689
|
+
if not ok:
|
690
|
+
raise Exception(f"Failed to install ssh and netcat: {message}")
|
691
|
+
|
692
|
+
ok, msg = ssh_utils.configure_ssh_server()
|
693
|
+
if not ok:
|
694
|
+
raise Exception(f"Failed to configure ssh server: {msg}")
|
695
|
+
|
696
|
+
ok, msg = ssh_utils.ensure_public_key_registered_in_ssh_agent()
|
697
|
+
if not ok:
|
698
|
+
raise Exception(f"Failed to ensure public key registered in ssh agent: {msg}")
|
699
|
+
|
700
|
+
ssh_utils.add_env_loader_to_bashrc()
|
@@ -0,0 +1,68 @@
|
|
1
|
+
import os
|
2
|
+
import subprocess
|
3
|
+
from typing import Tuple
|
4
|
+
|
5
|
+
|
6
|
+
def exec_in_pod(pod_name: str, namespace: str, command: str) -> Tuple[int, str]:
|
7
|
+
"""
|
8
|
+
Executes a command using kubectl exec in the remote pod.
|
9
|
+
"""
|
10
|
+
|
11
|
+
try:
|
12
|
+
result = subprocess.run(
|
13
|
+
[
|
14
|
+
"kubectl",
|
15
|
+
"--context",
|
16
|
+
"outerbounds-workstations",
|
17
|
+
"exec",
|
18
|
+
"-i",
|
19
|
+
pod_name,
|
20
|
+
"-n",
|
21
|
+
namespace,
|
22
|
+
"-c",
|
23
|
+
"workstation",
|
24
|
+
"--",
|
25
|
+
"sh",
|
26
|
+
"-c",
|
27
|
+
command,
|
28
|
+
],
|
29
|
+
capture_output=True,
|
30
|
+
text=True,
|
31
|
+
check=True,
|
32
|
+
)
|
33
|
+
return result.returncode, result.stdout, result.stderr
|
34
|
+
except subprocess.CalledProcessError as e:
|
35
|
+
return e.returncode, "", f"Error executing command: {e.stderr}"
|
36
|
+
except Exception as e:
|
37
|
+
return 1, "", f"Error executing command: {e}"
|
38
|
+
|
39
|
+
|
40
|
+
def cp_to_pod(
|
41
|
+
pod_name: str, namespace: str, source_on_local: str, destination_on_pod: str
|
42
|
+
) -> Tuple[int, str]:
|
43
|
+
"""
|
44
|
+
Copies a file to a pod using kubectl cp.
|
45
|
+
"""
|
46
|
+
try:
|
47
|
+
result = subprocess.run(
|
48
|
+
[
|
49
|
+
"kubectl",
|
50
|
+
"--context",
|
51
|
+
"outerbounds-workstations",
|
52
|
+
"--container",
|
53
|
+
"workstation",
|
54
|
+
"cp",
|
55
|
+
source_on_local,
|
56
|
+
"-n",
|
57
|
+
namespace,
|
58
|
+
f"{pod_name}:{destination_on_pod}",
|
59
|
+
],
|
60
|
+
capture_output=True,
|
61
|
+
text=True,
|
62
|
+
check=True,
|
63
|
+
)
|
64
|
+
return result.returncode, result.stdout, result.stderr
|
65
|
+
except subprocess.CalledProcessError as e:
|
66
|
+
return e.returncode, "", f"Error copying file: {e.stderr}"
|
67
|
+
except Exception as e:
|
68
|
+
return 1, "", f"Error copying file: {e}"
|
@@ -0,0 +1,540 @@
|
|
1
|
+
import os
|
2
|
+
import shutil
|
3
|
+
import subprocess
|
4
|
+
from typing import Tuple
|
5
|
+
from pathlib import Path
|
6
|
+
import re
|
7
|
+
|
8
|
+
EXPECTED_PUBLIC_KEY_NAME = "ob_workstation_ed25519.pem.pub"
|
9
|
+
EXPECTED_PRIVATE_KEY_NAME = "ob_workstation_ed25519.pem"
|
10
|
+
|
11
|
+
|
12
|
+
def ensure_e25519_keypair_sshkeygen(
|
13
|
+
directory,
|
14
|
+
private_key_filename=EXPECTED_PRIVATE_KEY_NAME,
|
15
|
+
public_key_filename=EXPECTED_PUBLIC_KEY_NAME,
|
16
|
+
password=None,
|
17
|
+
):
|
18
|
+
"""
|
19
|
+
Creates an Ed25519 key pair using ssh-keygen.
|
20
|
+
"""
|
21
|
+
|
22
|
+
# Create directory if it doesn't exist
|
23
|
+
os.makedirs(directory, exist_ok=True)
|
24
|
+
|
25
|
+
private_key_path = os.path.join(directory, private_key_filename)
|
26
|
+
public_key_path = os.path.join(directory, public_key_filename)
|
27
|
+
|
28
|
+
# No need to regenerate keys if they already exist.
|
29
|
+
if os.path.exists(private_key_path) and os.path.exists(public_key_path):
|
30
|
+
return private_key_path, public_key_path
|
31
|
+
|
32
|
+
# Generate the Ed25519 key pair
|
33
|
+
result = subprocess.run(
|
34
|
+
["ssh-keygen", "-t", "ed25519", "-f", private_key_path, "-N", ""],
|
35
|
+
capture_output=True,
|
36
|
+
text=True,
|
37
|
+
)
|
38
|
+
if result.returncode != 0:
|
39
|
+
raise Exception(f"Failed to generate Ed25519 key pair: {result.stderr}")
|
40
|
+
|
41
|
+
return private_key_path, public_key_path
|
42
|
+
|
43
|
+
|
44
|
+
def create_ssh_key_pair():
|
45
|
+
location = os.environ.get("METAFLOW_HOME", os.path.expanduser("~/.metaflowconfig"))
|
46
|
+
return ensure_e25519_keypair_sshkeygen(
|
47
|
+
location, EXPECTED_PRIVATE_KEY_NAME, EXPECTED_PUBLIC_KEY_NAME
|
48
|
+
)
|
49
|
+
|
50
|
+
|
51
|
+
def ensure_public_key_registered_in_ssh_agent():
|
52
|
+
"""
|
53
|
+
This function ensures that the public key is registered in the ssh agent.
|
54
|
+
This means making sure that the public key is added to the list of AuthorizedKeysFile entries in the ssh_config file.
|
55
|
+
"""
|
56
|
+
WORKSPACE_DIR = "/home/ob-workspace/.ssh"
|
57
|
+
AUTHORIZED_KEYS = f"{WORKSPACE_DIR}/authorized_keys"
|
58
|
+
SOURCE_KEY = f"{WORKSPACE_DIR}/{EXPECTED_PUBLIC_KEY_NAME}"
|
59
|
+
|
60
|
+
try:
|
61
|
+
# Step 2: Handle authorized_keys file
|
62
|
+
print("Setting up authorized_keys...")
|
63
|
+
|
64
|
+
# Create .ssh directory if it doesn't exist
|
65
|
+
Path(WORKSPACE_DIR).mkdir(parents=True, exist_ok=True, mode=0o700)
|
66
|
+
|
67
|
+
# Check if authorized_keys exists, if not copy from ob-workstation-key
|
68
|
+
if not os.path.exists(AUTHORIZED_KEYS):
|
69
|
+
if os.path.exists(SOURCE_KEY):
|
70
|
+
shutil.copy2(SOURCE_KEY, AUTHORIZED_KEYS)
|
71
|
+
print(f"Copied {SOURCE_KEY} to {AUTHORIZED_KEYS}")
|
72
|
+
else:
|
73
|
+
return False, f"Source key file not found: {SOURCE_KEY}"
|
74
|
+
|
75
|
+
# Set correct permissions for authorized_keys
|
76
|
+
os.chmod(AUTHORIZED_KEYS, 0o600)
|
77
|
+
return restart_ssh_service()
|
78
|
+
except Exception as e:
|
79
|
+
return False, f"Error during configuration: {str(e)}"
|
80
|
+
|
81
|
+
|
82
|
+
def restart_ssh_service():
|
83
|
+
"""
|
84
|
+
This function restarts the ssh service.
|
85
|
+
"""
|
86
|
+
print("Restarting SSH service...")
|
87
|
+
|
88
|
+
# Try different service names and init systems
|
89
|
+
restart_commands = [
|
90
|
+
["systemctl", "restart", "sshd"],
|
91
|
+
["systemctl", "restart", "ssh"],
|
92
|
+
["service", "sshd", "restart"],
|
93
|
+
["service", "ssh", "restart"],
|
94
|
+
]
|
95
|
+
|
96
|
+
service_restarted = False
|
97
|
+
for cmd in restart_commands:
|
98
|
+
try:
|
99
|
+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
100
|
+
print(f"SSH service restarted using: {' '.join(cmd)}")
|
101
|
+
service_restarted = True
|
102
|
+
break
|
103
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
104
|
+
continue
|
105
|
+
|
106
|
+
if not service_restarted:
|
107
|
+
print("Warning: Could not restart SSH service automatically")
|
108
|
+
print("Please restart manually with: sudo systemctl restart sshd")
|
109
|
+
return False, f"SSH service could not be restarted automatically."
|
110
|
+
|
111
|
+
return True, "SSH configuration completed successfully"
|
112
|
+
|
113
|
+
|
114
|
+
def configure_ssh_server() -> Tuple[bool, str]:
|
115
|
+
"""
|
116
|
+
Configure SSH server to disable password authentication and set up authorized_keys.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
Tuple of (success: bool, message: str)
|
120
|
+
"""
|
121
|
+
|
122
|
+
# Configuration paths
|
123
|
+
SSHD_CONFIG = "/etc/ssh/sshd_config"
|
124
|
+
SSHD_RUN_DIR = "/run/sshd"
|
125
|
+
HOME_DIR = "/home/ob-workspace"
|
126
|
+
|
127
|
+
try:
|
128
|
+
# Step 1: Create and configure /run/sshd directory
|
129
|
+
print("Setting up /run/sshd directory...")
|
130
|
+
Path(SSHD_RUN_DIR).mkdir(parents=True, exist_ok=True, mode=0o755)
|
131
|
+
os.chmod(SSHD_RUN_DIR, 0o755)
|
132
|
+
print(f"Created {SSHD_RUN_DIR} with permissions 755")
|
133
|
+
|
134
|
+
os.chmod(HOME_DIR, 0o755)
|
135
|
+
|
136
|
+
# Step 3: Modify sshd_config
|
137
|
+
print("Modifying SSH configuration...")
|
138
|
+
|
139
|
+
# Check if we have permission to modify sshd_config
|
140
|
+
if not os.access(SSHD_CONFIG, os.W_OK):
|
141
|
+
return False, f"No write permission for {SSHD_CONFIG}. Run with sudo."
|
142
|
+
|
143
|
+
# Create backup if requested
|
144
|
+
backup_path = f"{SSHD_CONFIG}.backup"
|
145
|
+
shutil.copy2(SSHD_CONFIG, backup_path)
|
146
|
+
print(f"Backup created: {backup_path}")
|
147
|
+
|
148
|
+
# Read current configuration
|
149
|
+
with open(SSHD_CONFIG, "r") as f:
|
150
|
+
lines = f.readlines()
|
151
|
+
|
152
|
+
# Configuration settings to apply
|
153
|
+
config_settings = {
|
154
|
+
"PasswordAuthentication": "no",
|
155
|
+
"ChallengeResponseAuthentication": "no",
|
156
|
+
"PubkeyAuthentication": "yes",
|
157
|
+
"AuthorizedKeysFile": "/home/ob-workspace/.ssh/authorized_keys .ssh/authorized_keys",
|
158
|
+
}
|
159
|
+
|
160
|
+
# Track which settings we've modified
|
161
|
+
modified_settings = set()
|
162
|
+
new_lines = []
|
163
|
+
|
164
|
+
for line in lines:
|
165
|
+
modified = False
|
166
|
+
|
167
|
+
for setting, value in config_settings.items():
|
168
|
+
# Check if this line contains the setting (commented or not)
|
169
|
+
pattern = rf"^\s*#?\s*{setting}\s+"
|
170
|
+
if re.match(pattern, line, re.IGNORECASE):
|
171
|
+
# Replace with our setting
|
172
|
+
new_lines.append(f"{setting} {value}\n")
|
173
|
+
modified_settings.add(setting)
|
174
|
+
modified = True
|
175
|
+
break
|
176
|
+
|
177
|
+
if not modified:
|
178
|
+
new_lines.append(line)
|
179
|
+
|
180
|
+
# Add any settings that weren't found in the file
|
181
|
+
for setting, value in config_settings.items():
|
182
|
+
if setting not in modified_settings:
|
183
|
+
new_lines.append(f"\n{setting} {value}\n")
|
184
|
+
print(f"Added new setting: {setting} {value}")
|
185
|
+
|
186
|
+
# Write the modified configuration
|
187
|
+
with open(SSHD_CONFIG, "w") as f:
|
188
|
+
f.writelines(new_lines)
|
189
|
+
|
190
|
+
print("Configuration file updated successfully")
|
191
|
+
|
192
|
+
# Step 4: Test configuration
|
193
|
+
print("Testing SSH configuration...")
|
194
|
+
result = subprocess.run(["sshd", "-t"], capture_output=True, text=True)
|
195
|
+
|
196
|
+
if result.returncode != 0:
|
197
|
+
return False, f"SSH configuration test failed: {result.stderr}"
|
198
|
+
|
199
|
+
print("Configuration test passed")
|
200
|
+
|
201
|
+
# Step 5: Restart SSH service if requested
|
202
|
+
return restart_ssh_service()
|
203
|
+
|
204
|
+
except Exception as e:
|
205
|
+
return False, f"Error during configuration: {str(e)}"
|
206
|
+
|
207
|
+
|
208
|
+
def add_entry_to_ssh_config(
|
209
|
+
workstation_id: str, namespace: str, private_key_path: str
|
210
|
+
) -> Tuple[bool, str]:
|
211
|
+
"""
|
212
|
+
Adds an entry to the ssh config file if one doesn't already exist.
|
213
|
+
|
214
|
+
Example Entry:
|
215
|
+
Host ws-bae81a70-0
|
216
|
+
ProxyCommand kubectl exec -i --context outerbounds-workstations ws-bae81a70-0 -n ws-69304f8188e31a0745bace40b9378c6b --container workstation -- nc localhost 22
|
217
|
+
User root
|
218
|
+
StrictHostKeyChecking no
|
219
|
+
UserKnownHostsFile /dev/null
|
220
|
+
IdentityFile ~/.ssh/id_ed25519
|
221
|
+
ControlMaster auto
|
222
|
+
ControlPath ~/.ssh/control-%h-%p-%r
|
223
|
+
ControlPersist 10m
|
224
|
+
"""
|
225
|
+
|
226
|
+
with open(os.path.expanduser("~/.ssh/config"), "r") as f:
|
227
|
+
lines = f.readlines()
|
228
|
+
|
229
|
+
for line in lines:
|
230
|
+
if f"Host {workstation_id}-0" in line:
|
231
|
+
return True, "Entry already exists"
|
232
|
+
|
233
|
+
indent = " " * 2
|
234
|
+
with open(os.path.expanduser("~/.ssh/config"), "a") as f:
|
235
|
+
f.write("\n")
|
236
|
+
f.write(f"Host {workstation_id}-0\n")
|
237
|
+
f.write(
|
238
|
+
f"{indent}ProxyCommand kubectl exec -i --context outerbounds-workstations {workstation_id}-0 -n {namespace} --container workstation -- nc localhost 22\n"
|
239
|
+
)
|
240
|
+
f.write(f"{indent}User root\n")
|
241
|
+
f.write(f"{indent}StrictHostKeyChecking no\n")
|
242
|
+
f.write(f"{indent}UserKnownHostsFile /dev/null\n")
|
243
|
+
f.write(f"{indent}IdentityFile {private_key_path}\n")
|
244
|
+
f.write(f"{indent}ControlMaster auto\n")
|
245
|
+
f.write(f"{indent}ControlPath ~/.ssh/control-%h-%p-%r\n")
|
246
|
+
f.write(f"{indent}ControlPersist 10m\n")
|
247
|
+
|
248
|
+
return True, "Entry added to ssh config"
|
249
|
+
|
250
|
+
|
251
|
+
def add_env_loader_to_bashrc():
|
252
|
+
"""
|
253
|
+
Adds the environment variable loader block to ~/.bashrc if not already present.
|
254
|
+
Uses marker comments to identify the block.
|
255
|
+
"""
|
256
|
+
|
257
|
+
# Define the block content with marker comments
|
258
|
+
env_loader_block = """
|
259
|
+
# BEGIN WORKSTATION_ENV_LOADER
|
260
|
+
# Only proceed if WORKSTATION_ID is not already set
|
261
|
+
if [ -z "$WORKSTATION_ID" ]; then
|
262
|
+
# Read environment variables from /proc/1/environ (init process)
|
263
|
+
# This file contains null-separated env vars
|
264
|
+
if [ -r /proc/1/environ ]; then
|
265
|
+
while IFS= read -r -d '' var; do
|
266
|
+
# Extract the variable name (everything before the first '=')
|
267
|
+
var_name="${var%%=*}"
|
268
|
+
|
269
|
+
# Check if the variable name contains any of our keywords
|
270
|
+
if [[ "$var_name" == *"WORKSTATION"* ]] || \\
|
271
|
+
[[ "$var_name" == *"METAFLOW"* ]] || \\
|
272
|
+
[[ "$var_name" == *"AWS"* ]] || \\
|
273
|
+
[[ "$var_name" == *"GCP"* ]] || \\
|
274
|
+
[[ "$var_name" == *"CLOUDSDK"* ]] || \\
|
275
|
+
[[ "$var_name" == *"OBP"* ]]; then
|
276
|
+
# Export the variable to the current session
|
277
|
+
export "$var"
|
278
|
+
fi
|
279
|
+
done < /proc/1/environ
|
280
|
+
fi
|
281
|
+
fi
|
282
|
+
# END WORKSTATION_ENV_LOADER
|
283
|
+
"""
|
284
|
+
|
285
|
+
# Get the path to ~/.bashrc
|
286
|
+
bashrc_path = Path.home() / ".bashrc"
|
287
|
+
|
288
|
+
# Create .bashrc if it doesn't exist
|
289
|
+
if not bashrc_path.exists():
|
290
|
+
bashrc_path.touch()
|
291
|
+
print(f"Created {bashrc_path}")
|
292
|
+
|
293
|
+
# Read the current content
|
294
|
+
try:
|
295
|
+
with open(bashrc_path, "r") as f:
|
296
|
+
current_content = f.read()
|
297
|
+
except Exception as e:
|
298
|
+
print(f"Error reading {bashrc_path}: {e}")
|
299
|
+
return False
|
300
|
+
|
301
|
+
# Check if the marker is already present
|
302
|
+
marker = "BEGIN WORKSTATION_ENV_LOADER"
|
303
|
+
if marker in current_content:
|
304
|
+
print(f"Environment loader block already present in {bashrc_path}")
|
305
|
+
return True
|
306
|
+
|
307
|
+
# Add the block to the end of the file
|
308
|
+
try:
|
309
|
+
with open(bashrc_path, "a") as f:
|
310
|
+
# Add a newline before our block if file doesn't end with one
|
311
|
+
if current_content and not current_content.endswith("\n"):
|
312
|
+
f.write("\n")
|
313
|
+
f.write(env_loader_block)
|
314
|
+
print(f"Successfully added environment loader block to {bashrc_path}")
|
315
|
+
return True
|
316
|
+
except Exception as e:
|
317
|
+
print(f"Error writing to {bashrc_path}: {e}")
|
318
|
+
return False
|
319
|
+
|
320
|
+
|
321
|
+
def best_effort_install_ssh_netcat():
|
322
|
+
"""
|
323
|
+
Best effort installation of openssh-server and netcat.
|
324
|
+
|
325
|
+
Returns:
|
326
|
+
tuple: (success: bool, message: str)
|
327
|
+
"""
|
328
|
+
|
329
|
+
def run_command(cmd, check=False):
|
330
|
+
"""Helper to run shell commands"""
|
331
|
+
try:
|
332
|
+
result = subprocess.run(
|
333
|
+
cmd, shell=True, capture_output=True, text=True, timeout=30
|
334
|
+
)
|
335
|
+
return result.returncode == 0, result.stdout, result.stderr
|
336
|
+
except subprocess.TimeoutExpired:
|
337
|
+
return False, "", "Command timed out"
|
338
|
+
except Exception as e:
|
339
|
+
return False, "", str(e)
|
340
|
+
|
341
|
+
def check_command_exists(cmd):
|
342
|
+
"""Check if a command exists in PATH"""
|
343
|
+
return shutil.which(cmd) is not None
|
344
|
+
|
345
|
+
def check_ssh_server():
|
346
|
+
"""Check if SSH server is installed"""
|
347
|
+
# Check for sshd in common locations
|
348
|
+
sshd_paths = ["/usr/sbin/sshd", "/sbin/sshd"]
|
349
|
+
for path in sshd_paths:
|
350
|
+
if os.path.exists(path):
|
351
|
+
return True
|
352
|
+
# Also check if sshd is in PATH
|
353
|
+
return check_command_exists("sshd")
|
354
|
+
|
355
|
+
def check_sudo_access():
|
356
|
+
"""Check if we have sudo access"""
|
357
|
+
success, _, _ = run_command("sudo -n true 2>/dev/null")
|
358
|
+
if success:
|
359
|
+
return True
|
360
|
+
# Try with -v flag (might prompt for password)
|
361
|
+
success, _, _ = run_command("sudo -v 2>/dev/null")
|
362
|
+
return success
|
363
|
+
|
364
|
+
def detect_package_manager():
|
365
|
+
"""Detect which package manager is available"""
|
366
|
+
package_managers = {
|
367
|
+
"apt-get": {
|
368
|
+
"update": "apt-get update",
|
369
|
+
"install": "apt-get install -y",
|
370
|
+
"ssh_package": "openssh-server",
|
371
|
+
"nc_packages": ["netcat-openbsd", "netcat-traditional", "netcat"],
|
372
|
+
},
|
373
|
+
"apt": {
|
374
|
+
"update": "apt update",
|
375
|
+
"install": "apt install -y",
|
376
|
+
"ssh_package": "openssh-server",
|
377
|
+
"nc_packages": ["netcat-openbsd", "netcat-traditional", "netcat"],
|
378
|
+
},
|
379
|
+
"yum": {
|
380
|
+
"update": "yum makecache",
|
381
|
+
"install": "yum install -y",
|
382
|
+
"ssh_package": "openssh-server",
|
383
|
+
"nc_packages": ["nmap-ncat", "nc", "netcat"],
|
384
|
+
},
|
385
|
+
"dnf": {
|
386
|
+
"update": "dnf makecache",
|
387
|
+
"install": "dnf install -y",
|
388
|
+
"ssh_package": "openssh-server",
|
389
|
+
"nc_packages": ["nmap-ncat", "nc", "netcat"],
|
390
|
+
},
|
391
|
+
"zypper": {
|
392
|
+
"update": "zypper refresh",
|
393
|
+
"install": "zypper install -n",
|
394
|
+
"ssh_package": "openssh",
|
395
|
+
"nc_packages": ["netcat-openbsd", "gnu-netcat", "netcat"],
|
396
|
+
},
|
397
|
+
"pacman": {
|
398
|
+
"update": "pacman -Sy",
|
399
|
+
"install": "pacman -S --noconfirm",
|
400
|
+
"ssh_package": "openssh",
|
401
|
+
"nc_packages": ["gnu-netcat", "openbsd-netcat"],
|
402
|
+
},
|
403
|
+
"apk": {
|
404
|
+
"update": "apk update",
|
405
|
+
"install": "apk add --no-cache",
|
406
|
+
"ssh_package": "openssh-server",
|
407
|
+
"nc_packages": ["netcat-openbsd", "busybox"],
|
408
|
+
},
|
409
|
+
}
|
410
|
+
|
411
|
+
for pm, config in package_managers.items():
|
412
|
+
if check_command_exists(pm):
|
413
|
+
return pm, config
|
414
|
+
return None, None
|
415
|
+
|
416
|
+
def install_package(package, pm_config, use_sudo=True):
|
417
|
+
"""Try to install a package"""
|
418
|
+
install_cmd = pm_config["install"]
|
419
|
+
if use_sudo:
|
420
|
+
cmd = f"sudo {install_cmd} {package}"
|
421
|
+
else:
|
422
|
+
cmd = f"{install_cmd} {package}"
|
423
|
+
|
424
|
+
success, stdout, stderr = run_command(cmd)
|
425
|
+
return success
|
426
|
+
|
427
|
+
# Step 1: Check if both are already installed
|
428
|
+
ssh_installed = check_ssh_server()
|
429
|
+
nc_installed = (
|
430
|
+
check_command_exists("nc")
|
431
|
+
or check_command_exists("netcat")
|
432
|
+
or check_command_exists("ncat")
|
433
|
+
)
|
434
|
+
|
435
|
+
if ssh_installed and nc_installed:
|
436
|
+
return True, "Both openssh-server and netcat are already installed"
|
437
|
+
|
438
|
+
# Prepare status messages
|
439
|
+
status = []
|
440
|
+
if ssh_installed:
|
441
|
+
status.append("openssh-server is already installed")
|
442
|
+
if nc_installed:
|
443
|
+
status.append("netcat is already installed")
|
444
|
+
|
445
|
+
# Step 2 & 3: Check sudo availability and access
|
446
|
+
has_sudo = check_command_exists("sudo")
|
447
|
+
has_sudo_access = has_sudo and check_sudo_access()
|
448
|
+
|
449
|
+
# Step 4: Detect package manager
|
450
|
+
pm_name, pm_config = detect_package_manager()
|
451
|
+
|
452
|
+
if not pm_name:
|
453
|
+
missing = []
|
454
|
+
if not ssh_installed:
|
455
|
+
missing.append("openssh-server")
|
456
|
+
if not nc_installed:
|
457
|
+
missing.append("netcat")
|
458
|
+
return (
|
459
|
+
False,
|
460
|
+
f"No supported package manager found. Unable to install: {', '.join(missing)}",
|
461
|
+
)
|
462
|
+
|
463
|
+
# Step 5 & 6: Try to install missing packages
|
464
|
+
use_sudo = has_sudo and has_sudo_access
|
465
|
+
|
466
|
+
# Update package manager cache first
|
467
|
+
update_cmd = pm_config["update"]
|
468
|
+
if use_sudo:
|
469
|
+
update_cmd = f"sudo {update_cmd}"
|
470
|
+
|
471
|
+
update_success, _, _ = run_command(update_cmd)
|
472
|
+
if not update_success:
|
473
|
+
status.append(
|
474
|
+
f"Warning: Failed to update package cache with '{pm_config['update']}'"
|
475
|
+
)
|
476
|
+
|
477
|
+
# Install SSH server if needed
|
478
|
+
if not ssh_installed:
|
479
|
+
ssh_package = pm_config["ssh_package"]
|
480
|
+
if install_package(ssh_package, pm_config, use_sudo):
|
481
|
+
status.append(f"Successfully installed {ssh_package}")
|
482
|
+
ssh_installed = check_ssh_server()
|
483
|
+
else:
|
484
|
+
status.append(f"Failed to install {ssh_package}")
|
485
|
+
|
486
|
+
# Install netcat if needed
|
487
|
+
if not nc_installed:
|
488
|
+
# Try different netcat package names
|
489
|
+
nc_packages = pm_config["nc_packages"]
|
490
|
+
nc_install_success = False
|
491
|
+
|
492
|
+
for nc_package in nc_packages:
|
493
|
+
if install_package(nc_package, pm_config, use_sudo):
|
494
|
+
status.append(f"Successfully installed {nc_package}")
|
495
|
+
nc_install_success = True
|
496
|
+
break
|
497
|
+
|
498
|
+
if not nc_install_success:
|
499
|
+
status.append(f"Failed to install netcat (tried: {', '.join(nc_packages)})")
|
500
|
+
|
501
|
+
nc_installed = (
|
502
|
+
check_command_exists("nc")
|
503
|
+
or check_command_exists("netcat")
|
504
|
+
or check_command_exists("ncat")
|
505
|
+
)
|
506
|
+
|
507
|
+
# Step 7: Final check and return appropriate message
|
508
|
+
ssh_final = check_ssh_server()
|
509
|
+
nc_final = (
|
510
|
+
check_command_exists("nc")
|
511
|
+
or check_command_exists("netcat")
|
512
|
+
or check_command_exists("ncat")
|
513
|
+
)
|
514
|
+
|
515
|
+
if ssh_final and nc_final:
|
516
|
+
return True, "Successfully installed all required packages. " + " ".join(status)
|
517
|
+
elif ssh_final or nc_final:
|
518
|
+
installed = []
|
519
|
+
missing = []
|
520
|
+
if ssh_final:
|
521
|
+
installed.append("openssh-server")
|
522
|
+
else:
|
523
|
+
missing.append("openssh-server")
|
524
|
+
if nc_final:
|
525
|
+
installed.append("netcat")
|
526
|
+
else:
|
527
|
+
missing.append("netcat")
|
528
|
+
|
529
|
+
msg = f"Partial success. Installed: {', '.join(installed)}. "
|
530
|
+
msg += f"Failed to install: {', '.join(missing)}. "
|
531
|
+
if not use_sudo and has_sudo:
|
532
|
+
msg += "Try running with sudo privileges."
|
533
|
+
return False, msg + " " + " ".join(status)
|
534
|
+
else:
|
535
|
+
msg = "Failed to install both openssh-server and netcat. "
|
536
|
+
if not use_sudo and has_sudo:
|
537
|
+
msg += "No sudo access available. Try running with sudo privileges. "
|
538
|
+
elif not has_sudo:
|
539
|
+
msg += "sudo is not installed. May need root access. "
|
540
|
+
return False, msg + " ".join(status)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: outerbounds
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8.0rc0
|
4
4
|
Summary: More Data Science, Less Administration
|
5
5
|
License: Proprietary
|
6
6
|
Keywords: data science,machine learning,MLOps
|
@@ -14,7 +14,6 @@ Classifier: Programming Language :: Python :: 3.8
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.9
|
15
15
|
Classifier: Programming Language :: Python :: 3.10
|
16
16
|
Classifier: Programming Language :: Python :: 3.11
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
18
17
|
Provides-Extra: azure
|
19
18
|
Provides-Extra: gcp
|
20
19
|
Provides-Extra: otel
|
@@ -29,9 +28,9 @@ Requires-Dist: google-cloud-secret-manager (>=2.20.0,<3.0.0) ; extra == "gcp"
|
|
29
28
|
Requires-Dist: google-cloud-storage (>=2.14.0,<3.0.0) ; extra == "gcp"
|
30
29
|
Requires-Dist: metaflow_checkpoint (==0.2.4)
|
31
30
|
Requires-Dist: ob-metaflow (==2.17.1.0)
|
32
|
-
Requires-Dist: ob-metaflow-extensions (==1.4.
|
33
|
-
Requires-Dist: ob-metaflow-stubs (==6.0.7.
|
34
|
-
Requires-Dist: ob-project-utils (==0.1.
|
31
|
+
Requires-Dist: ob-metaflow-extensions (==1.4.4)
|
32
|
+
Requires-Dist: ob-metaflow-stubs (==6.0.7.0)
|
33
|
+
Requires-Dist: ob-project-utils (==0.1.16)
|
35
34
|
Requires-Dist: opentelemetry-distro (>=0.41b0) ; extra == "otel"
|
36
35
|
Requires-Dist: opentelemetry-exporter-otlp-proto-http (>=1.20.0) ; extra == "otel"
|
37
36
|
Requires-Dist: opentelemetry-instrumentation-requests (>=0.41b0) ; extra == "otel"
|
@@ -51,14 +51,16 @@ outerbounds/command_groups/local_setup_cli.py,sha256=tuuqJRXQ_guEwOuQSIf9wkUU0yg
|
|
51
51
|
outerbounds/command_groups/perimeters_cli.py,sha256=iF_Uw7ROiSctf6FgoJEy30iDBLVE1j9FKuR3shgJRmc,19050
|
52
52
|
outerbounds/command_groups/secrets_cli.py,sha256=Vgn_aiTo76a0s5hCJhNWEOrCVhyYeivD08ooQxz0y7c,2952
|
53
53
|
outerbounds/command_groups/tutorials_cli.py,sha256=UInFyiMqtscHFfi8YQwiY_6Sdw9quJOtRu5OukEBccw,3522
|
54
|
-
outerbounds/command_groups/workstations_cli.py,sha256=
|
54
|
+
outerbounds/command_groups/workstations_cli.py,sha256=GARk9r0ulsoNQytYxHipnWmUHprZSyd-b0ZZFHO2BOE,25631
|
55
55
|
outerbounds/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
56
56
|
outerbounds/utils/kubeconfig.py,sha256=yvcyRXGR4AhQuqUDqmbGxEOHw5ixMFV0AZIDg1LI_Qo,7981
|
57
|
+
outerbounds/utils/kubectl_utils.py,sha256=y01xZ2AGW3CCnCbX7t9pfrhWsR9pDAyzlYiKdnZ-CK8,1916
|
57
58
|
outerbounds/utils/metaflowconfig.py,sha256=l2vJbgPkLISU-XPGZFaC8ZKmYFyJemlD6bwB-EKUsAw,5770
|
58
59
|
outerbounds/utils/schema.py,sha256=lMUr9kNgn9wy-sO_t_Tlxmbt63yLeN4b0xQXbDUDj4A,2331
|
60
|
+
outerbounds/utils/ssh_utils.py,sha256=A6rgi307DmBTdhDj7V7Tm-OYgMj2xRyx770t2FKs9uw,18737
|
59
61
|
outerbounds/utils/utils.py,sha256=4Z8cszNob_8kDYCLNTrP-wWads_S_MdL3Uj3ju4mEsk,501
|
60
62
|
outerbounds/vendor.py,sha256=gRLRJNXtZBeUpPEog0LOeIsl6GosaFFbCxUvR4bW6IQ,5093
|
61
|
-
outerbounds-0.
|
62
|
-
outerbounds-0.
|
63
|
-
outerbounds-0.
|
64
|
-
outerbounds-0.
|
63
|
+
outerbounds-0.8.0rc0.dist-info/METADATA,sha256=fMzhxYpFyR-dUkbMw5AsEoiepHwWVwlnB-_0CsrFs7U,1825
|
64
|
+
outerbounds-0.8.0rc0.dist-info/entry_points.txt,sha256=AP6rZg7y5SK9e9a9iVq0Fi9Q2KPjPZSwtZ6R98rLw-8,56
|
65
|
+
outerbounds-0.8.0rc0.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88
|
66
|
+
outerbounds-0.8.0rc0.dist-info/RECORD,,
|
File without changes
|