Skip to content

Commit 42d7baf

Browse files
author
Simca
committed
Add new command 'ssh'
Allow user to login to ssh using the tool, without any need to look for IP address or update known_host each time
1 parent d776322 commit 42d7baf

File tree

5 files changed

+194
-6
lines changed

5 files changed

+194
-6
lines changed

src/openstack_cli/commands/info.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,13 @@
2424

2525
__module__ = CommandMetaInfo("info", "Shows the detailed information about requested VMs")
2626
__args__ = __module__.arg_builder\
27-
.add_default_argument("search_pattern", str, "Search query", default="")
27+
.add_default_argument("search_pattern", str, "Search query", default="") \
28+
.add_argument("own", bool, "Display only owned by user items", default=False)
2829

2930
from openstack_cli.modules.openstack.objects import ServerPowerState
3031

3132

32-
def __init__(conf: Configuration, search_pattern: str, debug: bool):
33+
def __init__(conf: Configuration, search_pattern: str, debug: bool, own: bool):
3334
__run_ico = Symbols.PLAY.green()
3435
__pause_ico = Symbols.PAUSE.yellow()
3536
__stop_ico = Symbols.STOP.red()
@@ -39,7 +40,7 @@ def __init__(conf: Configuration, search_pattern: str, debug: bool):
3940
}
4041

4142
ostack = OpenStack(conf, debug=debug)
42-
clusters = ostack.get_server_by_cluster(search_pattern=search_pattern, sort=True)
43+
clusters = ostack.get_server_by_cluster(search_pattern=search_pattern, sort=True, only_owned=own)
4344
max_fqdn_len = ostack.servers.max_host_len + ostack.servers.max_domain_len + 5
4445

4546
to = TableOutput(

src/openstack_cli/commands/list.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@
2727

2828
__module__ = CommandMetaInfo("list", "Shows information about available clusters")
2929
__args__ = __module__.arg_builder\
30-
.add_default_argument("search_pattern", str, "Search query", default="")
30+
.add_default_argument("search_pattern", str, "Search query", default="")\
31+
.add_argument("own", bool, "Display only owned by user items", default=False)
3132

3233

3334
def get_lifetime(timestamp: datetime):
@@ -75,9 +76,9 @@ def print_cluster(servers: Dict[str, List[OpenStackVMInfo]]):
7576
)
7677

7778

78-
def __init__(conf: Configuration, search_pattern: str):
79+
def __init__(conf: Configuration, search_pattern: str, own: bool):
7980
ostack = OpenStack(conf)
80-
clusters = ostack.get_server_by_cluster(search_pattern=search_pattern, sort=True)
81+
clusters = ostack.get_server_by_cluster(search_pattern=search_pattern, sort=True, only_owned=own)
8182

8283
if search_pattern and len(clusters) == 0:
8384
print(f"Query '{search_pattern}' returned no match")

src/openstack_cli/commands/ssh.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one or more
2+
# contributor license agreements. See the NOTICE file distributed with
3+
# this work for additional information regarding copyright ownership.
4+
# The ASF licenses this file to You under the Apache License, Version 2.0
5+
# (the "License"); you may not use this file except in compliance with
6+
# the License. You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
import os
17+
import sys
18+
import subprocess
19+
from typing import Dict, List
20+
21+
from openstack_cli.modules.apputils.terminal import TableOutput, TableColumn, Console
22+
from openstack_cli.modules.openstack.objects import OpenStackVMInfo
23+
from openstack_cli.modules.openstack import OpenStack
24+
from openstack_cli.core.config import Configuration
25+
from openstack_cli.modules.apputils.discovery import CommandMetaInfo
26+
27+
__module__ = CommandMetaInfo("ssh", "Establish an SSH connection to a cluster node")
28+
__args__ = __module__.arg_builder\
29+
.add_default_argument("name", str, "full node name or cluster name")\
30+
.add_default_argument("node_number", int, "node number of the cluster to connect to", default=-1)\
31+
.add_argument("user_name", str, "User name to login", alias="user-name", default="root")\
32+
.add_argument("use_password", bool, "Use password auth instead of key", alias="use-password", default=False)\
33+
.add_argument("use_key", str, "Private key to use instead of one detected automatically", alias="use-key", default="None")\
34+
.add_argument("own", bool, "Display only own clusters", default=False)\
35+
.add_argument("port", int, "SSH Port", default=22)
36+
37+
38+
IS_WIN: bool = sys.platform == "win32"
39+
40+
41+
def _locate_binary(name: str) -> str:
42+
_default: str = "c:\\windows\\system32\\openssh" if IS_WIN else "/usr/bin/ssh"
43+
name = f"{name}.exe" if IS_WIN else name
44+
path_list = os.getenv("PATH").split(os.path.pathsep)
45+
for path in path_list:
46+
if name in os.listdir(path):
47+
return os.path.join(path, name)
48+
49+
return os.path.join(_default, name)
50+
51+
52+
def _open_console(host: str, port: int = 22, user_name: str = "root", password: bool = False, key_file: str = None):
53+
args = [
54+
f"{user_name}@{host}",
55+
"-p",
56+
str(port),
57+
"-oStrictHostKeyChecking=no",
58+
f"-oUserKnownHostsFile={'NUL' if IS_WIN else '/dev/null'}"
59+
]
60+
61+
if key_file:
62+
args.extend([
63+
"-oPreferredAuthentications=publickey",
64+
"-oPasswordAuthentication=no",
65+
"-oIdentitiesOnly=yes",
66+
"-i",
67+
key_file
68+
])
69+
elif password:
70+
args.extend([
71+
"-oPreferredAuthentications=password",
72+
"-oPasswordAuthentication=yes"
73+
])
74+
75+
_bin = _locate_binary("ssh")
76+
if IS_WIN:
77+
args = [_bin] + args
78+
p = subprocess.Popen(args, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
79+
p.wait()
80+
else:
81+
os.execv(_bin, [_bin] + args)
82+
83+
84+
def __init__(conf: Configuration, name: str, node_number: int, user_name: str, use_password: bool, use_key: str,
85+
own: bool, port: int):
86+
ostack = OpenStack(conf)
87+
if name == "None":
88+
name = ""
89+
90+
if use_key == "None":
91+
use_key = None
92+
93+
if name and node_number == -1:
94+
_name, _, _node_number = name.rpartition("-")
95+
96+
try:
97+
node_number = int(_node_number)
98+
name = _name
99+
except (ValueError, TypeError):
100+
pass
101+
102+
if "." in name:
103+
name, _ = name.split(".")
104+
105+
search_result: Dict[str, List[OpenStackVMInfo]] = ostack.get_server_by_cluster(name, sort=True, only_owned=own)
106+
to = TableOutput(
107+
TableColumn("Cluster name", 40),
108+
print_row_number=True
109+
)
110+
if len(search_result.keys()) > 1:
111+
to.print_header()
112+
for cluster_name in search_result.keys():
113+
to.print_row(cluster_name)
114+
115+
selection: int = Console.ask("Choose cluster from the list", int)
116+
try:
117+
name = list(search_result.keys())[selection:][0]
118+
except IndexError:
119+
raise ValueError("Wrong selection, please select item within an provided range")
120+
121+
nodes: List[OpenStackVMInfo] = search_result[name]
122+
if node_number == -1:
123+
if len(nodes) > 1:
124+
to = TableOutput(
125+
TableColumn("IP", 18),
126+
TableColumn("Host name", 40),
127+
128+
print_row_number=True
129+
)
130+
to.print_header()
131+
for node in nodes:
132+
to.print_row(node.ip_address, node.fqdn)
133+
node_number: int = Console.ask("Choose host from the list", int)
134+
if node_number > len(nodes):
135+
raise ValueError("Wrong selection, please select item within an provided range")
136+
else:
137+
node_number = 0
138+
else:
139+
node_number -= 1 # the node name starts for 1, while list from 0
140+
141+
try:
142+
node: OpenStackVMInfo = nodes[node_number]
143+
except IndexError:
144+
raise ValueError("Unknown host name, please check the name")
145+
146+
print(f"Establishing connection to {node.fqdn}({node.ip_address}) as '{user_name}' user...")
147+
if use_password:
148+
_open_console(node.ip_address, port=port, user_name=user_name, password=True)
149+
else:
150+
if not os.path.exists(conf.local_key_dir):
151+
os.makedirs(conf.local_key_dir, exist_ok=True)
152+
153+
if not use_key and node.key_name and node.key_name in conf.key_names and conf.get_key(node.key_name).private_key:
154+
key = conf.get_key(node.key_name).private_key
155+
use_key = os.path.join(conf.local_key_dir, node.key_name) + ".key"
156+
with open(use_key, "w+", encoding="UTF-8") as f:
157+
f.write(key)
158+
else:
159+
raise ValueError("No custom key provided nor private key found in the key storage. Please add private key to"
160+
" storage or use custom one with 'use-key' argument")
161+
162+
_open_console(node.ip_address, user_name=user_name, port=port, key_file=use_key)

src/openstack_cli/core/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16+
import os
1617
import json
1718
from typing import List
1819

@@ -135,6 +136,14 @@ def auth_token(self):
135136
def auth_token(self, value: str):
136137
self._storage.set_text_property(self._options_table, "auth_token", value, True)
137138

139+
@property
140+
def user_id(self):
141+
return self._storage.get_property(self._options_table, "user_id").value
142+
143+
@user_id.setter
144+
def user_id(self, value: str):
145+
self._storage.set_text_property(self._options_table, "user_id", value, encrypted=True)
146+
138147
@property
139148
def default_vm_password(self):
140149
_pass = self._storage.get_property(self._options_table, "default_vm_password").value
@@ -166,3 +175,7 @@ def region(self):
166175
@property
167176
def supported_os_names(self) -> List[str]:
168177
return ["sles", "rhel", "debian", "ubuntu", "centos", "opensuse"]
178+
179+
@property
180+
def local_key_dir(self):
181+
return os.path.join(self._storage.configuration_dir, "keys")

src/openstack_cli/modules/openstack/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ def __check_token(self) -> bool:
146146

147147
l_resp = LoginResponse(serialized_obj=r.content)
148148
self.__endpoints__ = OpenStackEndpoints(self._conf, l_resp)
149+
self._conf.user_id = l_resp.token.user.id
149150
return True
150151

151152
def __auth(self, _type: AuthRequestType = AuthRequestType.SCOPED) -> bool:
@@ -180,6 +181,8 @@ def __auth(self, _type: AuthRequestType = AuthRequestType.SCOPED) -> bool:
180181
l_resp = LoginResponse(serialized_obj=r.from_json())
181182
if _type == AuthRequestType.UNSCOPED:
182183
l_resp.token = Token(catalog=[])
184+
else:
185+
self._conf.user_id = l_resp.token.user.id
183186

184187
self.__endpoints__ = OpenStackEndpoints(self._conf, l_resp)
185188

@@ -611,6 +614,7 @@ def get_server_by_cluster(self,
611614
sort: bool = False,
612615
filter_func: Callable[[OpenStackVMInfo], bool] = None,
613616
no_cache: bool = False,
617+
only_owned: bool = False,
614618
) -> Dict[str, List[OpenStackVMInfo]]:
615619
"""
616620
:param search_pattern: vm search pattern list
@@ -619,6 +623,7 @@ def get_server_by_cluster(self,
619623
:param filter_func: if return true - item would be filtered, false not
620624
"""
621625
_servers: Dict[str, List[OpenStackVMInfo]] = {}
626+
user_id = self._conf.user_id
622627
# if no cached queries available, execute limited query
623628
if no_cache or self.__get_local_cache(LocalCacheType.SERVERS) is None:
624629
servers = self.get_servers(arguments={
@@ -629,6 +634,9 @@ def get_server_by_cluster(self,
629634
if filter_func and filter_func(server):
630635
continue
631636

637+
if only_owned and server.owner_id != user_id:
638+
continue
639+
632640
if server.cluster_name not in _servers:
633641
_servers[server.cluster_name] = []
634642

@@ -642,6 +650,9 @@ def get_server_by_cluster(self,
642650
if filter_func and filter_func(server):
643651
continue
644652

653+
if only_owned and server.owner_id != user_id:
654+
continue
655+
645656
if server.cluster_name not in _servers:
646657
_servers[server.cluster_name] = []
647658

0 commit comments

Comments
 (0)