targetcli 3.0.0__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.
- targetcli/__init__.py +21 -0
- targetcli/targetcli_shell.py +326 -0
- targetcli/targetclid.py +281 -0
- targetcli/ui_backstore.py +787 -0
- targetcli/ui_node.py +212 -0
- targetcli/ui_root.py +329 -0
- targetcli/ui_target.py +1450 -0
- targetcli-3.0.0.dist-info/METADATA +65 -0
- targetcli-3.0.0.dist-info/RECORD +12 -0
- targetcli-3.0.0.dist-info/WHEEL +4 -0
- targetcli-3.0.0.dist-info/entry_points.txt +3 -0
- targetcli-3.0.0.dist-info/licenses/COPYING +175 -0
targetcli/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'''
|
|
2
|
+
This file is part of targetcli.
|
|
3
|
+
Copyright (c) 2011-2013 by Datera, Inc
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
6
|
+
not use this file except in compliance with the License. You may obtain
|
|
7
|
+
a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
13
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
14
|
+
License for the specific language governing permissions and limitations
|
|
15
|
+
under the License.
|
|
16
|
+
'''
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
import importlib.metadata
|
|
20
|
+
|
|
21
|
+
__version__ = importlib.metadata.version('targetcli')
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Starts the targetcli CLI shell.
|
|
3
|
+
|
|
4
|
+
This file is part of targetcli.
|
|
5
|
+
Copyright (c) 2011-2013 by Datera, Inc
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
8
|
+
not use this file except in compliance with the License. You may obtain
|
|
9
|
+
a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
15
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
16
|
+
License for the specific language governing permissions and limitations
|
|
17
|
+
under the License.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
import contextlib
|
|
22
|
+
import fcntl
|
|
23
|
+
import readline
|
|
24
|
+
import socket
|
|
25
|
+
import struct
|
|
26
|
+
import sys
|
|
27
|
+
from os import getenv, getuid
|
|
28
|
+
|
|
29
|
+
from configshell_fb import ConfigShell, ExecutionError
|
|
30
|
+
from rtslib_fb import RTSLibError
|
|
31
|
+
|
|
32
|
+
from targetcli import __version__ as targetcli_version
|
|
33
|
+
from targetcli.ui_root import UIRoot
|
|
34
|
+
|
|
35
|
+
err = sys.stderr
|
|
36
|
+
# lockfile for serializing multiple targetcli requests
|
|
37
|
+
lock_file = '/var/run/targetcli.lock'
|
|
38
|
+
socket_path = '/var/run/targetclid.sock'
|
|
39
|
+
hints = ['/', 'backstores/', 'iscsi/', 'loopback/', 'vhost/', 'xen-pvscsi/',
|
|
40
|
+
'cd', 'pwd', 'ls', 'set', 'get', 'help', 'refresh', 'status',
|
|
41
|
+
'clearconfig', 'restoreconfig', 'saveconfig', 'exit']
|
|
42
|
+
|
|
43
|
+
class TargetCLI(ConfigShell):
|
|
44
|
+
default_prefs = {'color_path': 'magenta',
|
|
45
|
+
'color_command': 'cyan',
|
|
46
|
+
'color_parameter': 'magenta',
|
|
47
|
+
'color_keyword': 'cyan',
|
|
48
|
+
'completions_in_columns': True,
|
|
49
|
+
'logfile': None,
|
|
50
|
+
'loglevel_console': 'info',
|
|
51
|
+
'loglevel_file': 'debug9',
|
|
52
|
+
'color_mode': True,
|
|
53
|
+
'prompt_length': 30,
|
|
54
|
+
'tree_max_depth': 0,
|
|
55
|
+
'tree_status_mode': True,
|
|
56
|
+
'tree_round_nodes': True,
|
|
57
|
+
'tree_show_root': True,
|
|
58
|
+
'export_backstore_name_as_model': True,
|
|
59
|
+
'auto_enable_tpgt': True,
|
|
60
|
+
'auto_add_mapped_luns': True,
|
|
61
|
+
'auto_cd_after_create': False,
|
|
62
|
+
'auto_save_on_exit': True,
|
|
63
|
+
'max_backup_files': '10',
|
|
64
|
+
'auto_add_default_portal': True,
|
|
65
|
+
'auto_use_daemon': False,
|
|
66
|
+
'daemon_use_batch_mode': False,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def usage():
|
|
70
|
+
print(f"Usage: {sys.argv[0]} [--version|--help|CMD|--disable-daemon]", file=err)
|
|
71
|
+
print(" --version\t\tPrint version", file=err)
|
|
72
|
+
print(" --help\t\tPrint this information", file=err)
|
|
73
|
+
print(" CMD\t\t\tRun targetcli shell command and exit", file=err)
|
|
74
|
+
print(" <nothing>\t\tEnter configuration shell", file=err)
|
|
75
|
+
print(" --disable-daemon\tTurn-off the global auto use daemon flag", file=err)
|
|
76
|
+
print("See man page for more information.", file=err)
|
|
77
|
+
sys.exit(-1)
|
|
78
|
+
|
|
79
|
+
def version():
|
|
80
|
+
print(f"{sys.argv[0]} version {targetcli_version}", file=err)
|
|
81
|
+
sys.exit(0)
|
|
82
|
+
|
|
83
|
+
def usage_version(cmd):
|
|
84
|
+
if cmd in {"help", "--help", "-h"}:
|
|
85
|
+
usage()
|
|
86
|
+
|
|
87
|
+
if cmd in {"version", "--version", "-v"}:
|
|
88
|
+
version()
|
|
89
|
+
|
|
90
|
+
def try_op_lock(shell, lkfd):
|
|
91
|
+
'''
|
|
92
|
+
acquire a blocking lock on lockfile, to serialize multiple requests
|
|
93
|
+
'''
|
|
94
|
+
try:
|
|
95
|
+
fcntl.flock(lkfd, fcntl.LOCK_EX) # wait here until ongoing request is finished
|
|
96
|
+
except Exception as e:
|
|
97
|
+
shell.con.display(
|
|
98
|
+
shell.con.render_text(
|
|
99
|
+
f"taking lock on lockfile failed: {e!s}",
|
|
100
|
+
'red'))
|
|
101
|
+
sys.exit(1)
|
|
102
|
+
|
|
103
|
+
def release_op_lock(shell, lkfd):
|
|
104
|
+
'''
|
|
105
|
+
release blocking lock on lockfile, which can allow other requests process
|
|
106
|
+
'''
|
|
107
|
+
try:
|
|
108
|
+
fcntl.flock(lkfd, fcntl.LOCK_UN) # allow other requests now
|
|
109
|
+
except Exception as e:
|
|
110
|
+
shell.con.display(
|
|
111
|
+
shell.con.render_text(
|
|
112
|
+
f"unlock on lockfile failed: {e!s}",
|
|
113
|
+
'red'))
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
lkfd.close()
|
|
116
|
+
|
|
117
|
+
def completer(text, state):
|
|
118
|
+
options = [x for x in hints if x.startswith(text)]
|
|
119
|
+
try:
|
|
120
|
+
return options[state]
|
|
121
|
+
except IndexError:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
def call_daemon(shell, req, interactive):
|
|
125
|
+
try:
|
|
126
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
127
|
+
except OSError as err:
|
|
128
|
+
shell.con.display(shell.con.render_text(err, 'red'))
|
|
129
|
+
sys.exit(1)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
sock.connect(socket_path)
|
|
133
|
+
except OSError as err:
|
|
134
|
+
shell.con.display(shell.con.render_text(err, 'red'))
|
|
135
|
+
shell.con.display(
|
|
136
|
+
shell.con.render_text("Currently auto_use_daemon is true, "
|
|
137
|
+
"hence please make sure targetclid daemon is running ...\n"
|
|
138
|
+
"(or)\nIncase if you wish to turn auto_use_daemon to false "
|
|
139
|
+
"then run '#targetcli --disable-daemon'", 'red'))
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
|
|
142
|
+
# Two cases where we want to get pwd:
|
|
143
|
+
# 1. Before starting shell in interactive mode, needed for setting terminal
|
|
144
|
+
# 2. And only in Interactive mode, having command 'cd'
|
|
145
|
+
get_pwd = False
|
|
146
|
+
if interactive:
|
|
147
|
+
if not req:
|
|
148
|
+
req = "pwd"
|
|
149
|
+
get_pwd = True
|
|
150
|
+
elif "cd " in req:
|
|
151
|
+
req += "%pwd"
|
|
152
|
+
get_pwd = True
|
|
153
|
+
else:
|
|
154
|
+
req = "cd /%" + req # Non-interactive modes always consider start at '/'
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# send request
|
|
158
|
+
sock.sendall(req.encode())
|
|
159
|
+
except OSError as err:
|
|
160
|
+
shell.con.display(shell.con.render_text(err, 'red'))
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
var = sock.recv(4) # get length of data
|
|
164
|
+
sending = struct.unpack('i', var)
|
|
165
|
+
amount_expected = sending[0]
|
|
166
|
+
amount_received = 0
|
|
167
|
+
|
|
168
|
+
# get the actual data in chunks
|
|
169
|
+
output = ""
|
|
170
|
+
path = ""
|
|
171
|
+
while amount_received < amount_expected:
|
|
172
|
+
data = sock.recv(1024)
|
|
173
|
+
data = data.decode()
|
|
174
|
+
amount_received += len(data)
|
|
175
|
+
output += data
|
|
176
|
+
|
|
177
|
+
if get_pwd:
|
|
178
|
+
output_split = output.splitlines()
|
|
179
|
+
lines = len(output_split)
|
|
180
|
+
for i in range(lines):
|
|
181
|
+
if i == lines - 1:
|
|
182
|
+
path = str(output_split[i])
|
|
183
|
+
else:
|
|
184
|
+
print(str(output_split[i]), end="\n")
|
|
185
|
+
else:
|
|
186
|
+
print(output, end="")
|
|
187
|
+
|
|
188
|
+
sock.send(b'-END@OF@DATA-')
|
|
189
|
+
sock.close()
|
|
190
|
+
|
|
191
|
+
return path
|
|
192
|
+
|
|
193
|
+
def switch_to_daemon(shell, interactive):
|
|
194
|
+
readline.set_completer(completer)
|
|
195
|
+
readline.set_completer_delims('')
|
|
196
|
+
|
|
197
|
+
if 'libedit' in readline.__doc__:
|
|
198
|
+
readline.parse_and_bind("bind ^I rl_complete")
|
|
199
|
+
else:
|
|
200
|
+
readline.parse_and_bind("tab: complete")
|
|
201
|
+
|
|
202
|
+
if len(sys.argv) > 1:
|
|
203
|
+
command = " ".join(sys.argv[1:])
|
|
204
|
+
call_daemon(shell, command, False)
|
|
205
|
+
sys.exit(0)
|
|
206
|
+
|
|
207
|
+
if interactive:
|
|
208
|
+
shell.con.display(f"targetcli shell version {targetcli_version}\n"
|
|
209
|
+
"Entering targetcli interactive mode for daemonized approach.\n"
|
|
210
|
+
"Type 'exit' to quit.\n")
|
|
211
|
+
else:
|
|
212
|
+
shell.con.display(f"targetcli shell version {targetcli_version}\n"
|
|
213
|
+
"Entering targetcli batch mode for daemonized approach.\n"
|
|
214
|
+
"Enter multiple commands separated by newline and "
|
|
215
|
+
"type 'exit' to run them all in one go.\n")
|
|
216
|
+
|
|
217
|
+
prompt_path = "/"
|
|
218
|
+
if interactive:
|
|
219
|
+
prompt_path = call_daemon(shell, None, interactive) # get the initial path
|
|
220
|
+
|
|
221
|
+
inputs = []
|
|
222
|
+
real_exit = False
|
|
223
|
+
while True:
|
|
224
|
+
command = input(f"{prompt_path}> ")
|
|
225
|
+
if command.lower() == "exit":
|
|
226
|
+
real_exit = True
|
|
227
|
+
elif not command:
|
|
228
|
+
continue
|
|
229
|
+
if not interactive:
|
|
230
|
+
inputs.append(command)
|
|
231
|
+
if real_exit:
|
|
232
|
+
command = '%'.join(inputs) # delimit multiple commands with '%'
|
|
233
|
+
call_daemon(shell, command, interactive)
|
|
234
|
+
break
|
|
235
|
+
else:
|
|
236
|
+
if real_exit:
|
|
237
|
+
break
|
|
238
|
+
path = call_daemon(shell, command, interactive)
|
|
239
|
+
if path:
|
|
240
|
+
if path[0] == "/":
|
|
241
|
+
prompt_path = path
|
|
242
|
+
else:
|
|
243
|
+
print(path) # Error No Path ...
|
|
244
|
+
|
|
245
|
+
sys.exit(0)
|
|
246
|
+
|
|
247
|
+
def main():
|
|
248
|
+
'''
|
|
249
|
+
Start the targetcli shell.
|
|
250
|
+
'''
|
|
251
|
+
shell = TargetCLI(getenv("TARGETCLI_HOME", '~/.targetcli'))
|
|
252
|
+
|
|
253
|
+
is_root = False
|
|
254
|
+
if getuid() == 0:
|
|
255
|
+
is_root = True
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
lkfd = open(lock_file, 'w+') # noqa: SIM115
|
|
259
|
+
except OSError as e:
|
|
260
|
+
shell.con.display(
|
|
261
|
+
shell.con.render_text(f"opening lockfile failed: {e!s}",
|
|
262
|
+
'red'))
|
|
263
|
+
sys.exit(1)
|
|
264
|
+
|
|
265
|
+
try_op_lock(shell, lkfd)
|
|
266
|
+
|
|
267
|
+
use_daemon = False
|
|
268
|
+
if shell.prefs['auto_use_daemon']:
|
|
269
|
+
use_daemon = True
|
|
270
|
+
|
|
271
|
+
disable_daemon = False
|
|
272
|
+
if len(sys.argv) > 1:
|
|
273
|
+
usage_version(sys.argv[1])
|
|
274
|
+
if sys.argv[1] in {"disable-daemon", "--disable-daemon"}:
|
|
275
|
+
disable_daemon = True
|
|
276
|
+
|
|
277
|
+
interactive_mode = True
|
|
278
|
+
if shell.prefs['daemon_use_batch_mode']:
|
|
279
|
+
interactive_mode = False
|
|
280
|
+
|
|
281
|
+
if use_daemon and not disable_daemon:
|
|
282
|
+
switch_to_daemon(shell, interactive_mode)
|
|
283
|
+
# does not return
|
|
284
|
+
|
|
285
|
+
try:
|
|
286
|
+
root_node = UIRoot(shell, as_root=is_root)
|
|
287
|
+
root_node.refresh()
|
|
288
|
+
except Exception as error:
|
|
289
|
+
shell.con.display(shell.con.render_text(str(error), 'red'))
|
|
290
|
+
if not is_root:
|
|
291
|
+
shell.con.display(shell.con.render_text("Retry as root.", 'red'))
|
|
292
|
+
sys.exit(-1)
|
|
293
|
+
|
|
294
|
+
if len(sys.argv) > 1:
|
|
295
|
+
try:
|
|
296
|
+
if disable_daemon:
|
|
297
|
+
shell.run_cmdline('set global auto_use_daemon=false')
|
|
298
|
+
else:
|
|
299
|
+
shell.run_cmdline(" ".join(sys.argv[1:]))
|
|
300
|
+
except Exception as e:
|
|
301
|
+
print(str(e), file=sys.stderr)
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
sys.exit(0)
|
|
304
|
+
|
|
305
|
+
shell.con.display(f"targetcli shell version {targetcli_version}\n"
|
|
306
|
+
"Copyright 2011-2013 by Datera, Inc and others.\n"
|
|
307
|
+
"For help on commands, type 'help'.\n")
|
|
308
|
+
if not is_root:
|
|
309
|
+
shell.con.display("You are not root, disabling privileged commands.\n")
|
|
310
|
+
|
|
311
|
+
while not shell._exit:
|
|
312
|
+
try:
|
|
313
|
+
shell.run_interactive()
|
|
314
|
+
except (RTSLibError, ExecutionError) as msg: # noqa: PERF203 - would otherwise exit shell
|
|
315
|
+
shell.log.error(str(msg))
|
|
316
|
+
|
|
317
|
+
if shell.prefs['auto_save_on_exit'] and is_root:
|
|
318
|
+
shell.log.info("Global pref auto_save_on_exit=true")
|
|
319
|
+
root_node.ui_command_saveconfig()
|
|
320
|
+
|
|
321
|
+
release_op_lock(shell, lkfd)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
if __name__ == "__main__":
|
|
325
|
+
with contextlib.suppress(KeyboardInterrupt):
|
|
326
|
+
main()
|
targetcli/targetclid.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'''
|
|
2
|
+
targetclid
|
|
3
|
+
|
|
4
|
+
This file is part of targetcli-fb.
|
|
5
|
+
Copyright (c) 2019 by Red Hat, Inc.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
8
|
+
not use this file except in compliance with the License. You may obtain
|
|
9
|
+
a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
15
|
+
WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
16
|
+
License for the specific language governing permissions and limitations
|
|
17
|
+
under the License.
|
|
18
|
+
'''
|
|
19
|
+
|
|
20
|
+
import contextlib
|
|
21
|
+
import errno
|
|
22
|
+
import fcntl
|
|
23
|
+
import os
|
|
24
|
+
import signal
|
|
25
|
+
import socket
|
|
26
|
+
import stat
|
|
27
|
+
import struct
|
|
28
|
+
import sys
|
|
29
|
+
import tempfile
|
|
30
|
+
from os import getenv, getuid
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
from threading import Thread
|
|
33
|
+
|
|
34
|
+
from configshell_fb import ConfigShell
|
|
35
|
+
|
|
36
|
+
from targetcli import __version__ as targetcli_version
|
|
37
|
+
from targetcli.ui_root import UIRoot
|
|
38
|
+
|
|
39
|
+
err = sys.stderr
|
|
40
|
+
|
|
41
|
+
class TargetCLI:
|
|
42
|
+
def __init__(self):
|
|
43
|
+
'''
|
|
44
|
+
initializer
|
|
45
|
+
'''
|
|
46
|
+
# socket for unix communication
|
|
47
|
+
self.socket_path = '/var/run/targetclid.sock'
|
|
48
|
+
# pid file for defending on multiple daemon runs
|
|
49
|
+
self.pid_file = '/var/run/targetclid.pid'
|
|
50
|
+
|
|
51
|
+
self.NoSignal = True
|
|
52
|
+
self.sock = None
|
|
53
|
+
|
|
54
|
+
# shell console methods
|
|
55
|
+
self.shell = ConfigShell(getenv("TARGETCLI_HOME", '~/.targetcli'))
|
|
56
|
+
self.con = self.shell.con
|
|
57
|
+
self.display = self.shell.con.display
|
|
58
|
+
self.render = self.shell.con.render_text
|
|
59
|
+
|
|
60
|
+
# Handle SIGINT SIGTERM SIGHUP gracefully
|
|
61
|
+
signal.signal(signal.SIGINT, self.signal_handler)
|
|
62
|
+
signal.signal(signal.SIGTERM, self.signal_handler)
|
|
63
|
+
signal.signal(signal.SIGHUP, self.signal_handler)
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
self.pfd = open(self.pid_file, 'w+') # noqa: SIM115
|
|
67
|
+
except OSError as e:
|
|
68
|
+
self.display(
|
|
69
|
+
self.render(f"opening pidfile failed: {e!s}", 'red'),
|
|
70
|
+
)
|
|
71
|
+
sys.exit(1)
|
|
72
|
+
|
|
73
|
+
self.try_pidfile_lock()
|
|
74
|
+
|
|
75
|
+
is_root = False
|
|
76
|
+
if getuid() == 0:
|
|
77
|
+
is_root = True
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
root_node = UIRoot(self.shell, as_root=is_root)
|
|
81
|
+
root_node.refresh()
|
|
82
|
+
except Exception as error:
|
|
83
|
+
self.display(self.render(str(error), 'red'))
|
|
84
|
+
if not is_root:
|
|
85
|
+
self.display(self.render("Retry as root.", 'red'))
|
|
86
|
+
self.pfd.close()
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
# Keep track, for later use
|
|
90
|
+
self.con_stdout_ = self.con._stdout
|
|
91
|
+
self.con_stderr_ = self.con._stderr
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def __del__(self):
|
|
95
|
+
'''
|
|
96
|
+
destructor
|
|
97
|
+
'''
|
|
98
|
+
if hasattr(self, 'pfd'):
|
|
99
|
+
self.pfd.close()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def signal_handler(self):
|
|
103
|
+
'''
|
|
104
|
+
signal handler
|
|
105
|
+
'''
|
|
106
|
+
self.NoSignal = False
|
|
107
|
+
if self.sock:
|
|
108
|
+
self.sock.close()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def try_pidfile_lock(self):
|
|
112
|
+
'''
|
|
113
|
+
get lock on pidfile, which is to check if targetclid is running
|
|
114
|
+
'''
|
|
115
|
+
# check if targetclid is already running
|
|
116
|
+
lock = struct.pack('hhllhh', fcntl.F_WRLCK, 0, 0, 0, 0, 0)
|
|
117
|
+
try:
|
|
118
|
+
fcntl.fcntl(self.pfd, fcntl.F_SETLK, lock)
|
|
119
|
+
except Exception:
|
|
120
|
+
self.display(self.render("targetclid is already running...", 'red'))
|
|
121
|
+
self.pfd.close()
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def release_pidfile_lock(self):
|
|
126
|
+
'''
|
|
127
|
+
release lock on pidfile
|
|
128
|
+
'''
|
|
129
|
+
lock = struct.pack('hhllhh', fcntl.F_UNLCK, 0, 0, 0, 0, 0)
|
|
130
|
+
try:
|
|
131
|
+
fcntl.fcntl(self.pfd, fcntl.F_SETLK, lock)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self.display(
|
|
134
|
+
self.render(f"fcntl(UNLCK) on pidfile failed: {e!s}", 'red'),
|
|
135
|
+
)
|
|
136
|
+
self.pfd.close()
|
|
137
|
+
sys.exit(1)
|
|
138
|
+
self.pfd.close()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def client_thread(self, connection):
|
|
142
|
+
'''
|
|
143
|
+
Handle commands from client
|
|
144
|
+
'''
|
|
145
|
+
# load the prefs
|
|
146
|
+
self.shell.prefs.load()
|
|
147
|
+
|
|
148
|
+
still_listen = True
|
|
149
|
+
# Receive the data in small chunks and retransmit it
|
|
150
|
+
while still_listen:
|
|
151
|
+
data = connection.recv(65535)
|
|
152
|
+
if b'-END@OF@DATA-' in data:
|
|
153
|
+
connection.close()
|
|
154
|
+
still_listen = False
|
|
155
|
+
else:
|
|
156
|
+
self.con._stdout = self.con._stderr = f = tempfile.NamedTemporaryFile(mode='w', delete=False)
|
|
157
|
+
try:
|
|
158
|
+
# extract multiple commands delimited with '%'
|
|
159
|
+
list_data = data.decode().split('%')
|
|
160
|
+
for cmd in list_data:
|
|
161
|
+
self.shell.run_cmdline(cmd)
|
|
162
|
+
except Exception as e:
|
|
163
|
+
print(str(e), file=f) # push error to stream
|
|
164
|
+
|
|
165
|
+
# Restore
|
|
166
|
+
self.con._stdout = self.con_stdout_
|
|
167
|
+
self.con._stderr = self.con_stderr_
|
|
168
|
+
f.close()
|
|
169
|
+
|
|
170
|
+
with open(f.name) as f:
|
|
171
|
+
output = f.read()
|
|
172
|
+
var = struct.pack('i', len(output))
|
|
173
|
+
connection.sendall(var) # length of string
|
|
174
|
+
if len(output):
|
|
175
|
+
connection.sendall(output.encode()) # actual string
|
|
176
|
+
|
|
177
|
+
Path(f.name).unlink()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def usage():
|
|
181
|
+
print(f"Usage: {sys.argv[0]} [--version|--help]", file=err)
|
|
182
|
+
print(" --version\t\tPrint version", file=err)
|
|
183
|
+
print(" --help\t\tPrint this information", file=err)
|
|
184
|
+
sys.exit(0)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def version():
|
|
188
|
+
print(f"{sys.argv[0]} version {targetcli_version}", file=err)
|
|
189
|
+
sys.exit(0)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def usage_version(cmd):
|
|
193
|
+
if cmd in {"help", "--help", "-h"}:
|
|
194
|
+
usage()
|
|
195
|
+
|
|
196
|
+
if cmd in {"version", "--version", "-v"}:
|
|
197
|
+
version()
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main():
|
|
201
|
+
'''
|
|
202
|
+
start targetclid
|
|
203
|
+
'''
|
|
204
|
+
if len(sys.argv) > 1:
|
|
205
|
+
usage_version(sys.argv[1])
|
|
206
|
+
print(f"unrecognized option: {sys.argv[1]}")
|
|
207
|
+
sys.exit(-1)
|
|
208
|
+
|
|
209
|
+
to = TargetCLI()
|
|
210
|
+
|
|
211
|
+
if getenv('LISTEN_PID'):
|
|
212
|
+
# the systemd-activation path, using next available FD
|
|
213
|
+
fn = sys.stderr.fileno() + 1
|
|
214
|
+
try:
|
|
215
|
+
sock = socket.fromfd(fn, socket.AF_UNIX, socket.SOCK_STREAM)
|
|
216
|
+
except OSError as err:
|
|
217
|
+
to.display(to.render(err.strerror, 'red'))
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
|
|
220
|
+
# save socket so a signal can clea it up
|
|
221
|
+
to.sock = sock
|
|
222
|
+
else:
|
|
223
|
+
# Make sure file doesn't exist already
|
|
224
|
+
with contextlib.suppress(FileNotFoundError):
|
|
225
|
+
Path(to.socket_path).unlink()
|
|
226
|
+
|
|
227
|
+
# Create a TCP/IP socket
|
|
228
|
+
try:
|
|
229
|
+
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
230
|
+
except OSError as err:
|
|
231
|
+
to.display(to.render(err.strerror, 'red'))
|
|
232
|
+
sys.exit(1)
|
|
233
|
+
|
|
234
|
+
# save socket so a signal can clea it up
|
|
235
|
+
to.sock = sock
|
|
236
|
+
|
|
237
|
+
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
|
|
238
|
+
umask = 0o777 ^ mode # Prevents always downgrading umask to 0
|
|
239
|
+
umask_original = os.umask(umask)
|
|
240
|
+
# Bind the socket path
|
|
241
|
+
try:
|
|
242
|
+
sock.bind(to.socket_path)
|
|
243
|
+
except OSError as err:
|
|
244
|
+
to.display(to.render(err.strerror, 'red'))
|
|
245
|
+
sys.exit(1)
|
|
246
|
+
finally:
|
|
247
|
+
os.umask(umask_original)
|
|
248
|
+
|
|
249
|
+
# Listen for incoming connections
|
|
250
|
+
try:
|
|
251
|
+
sock.listen(1)
|
|
252
|
+
except OSError as err:
|
|
253
|
+
to.display(to.render(err.strerror, 'red'))
|
|
254
|
+
sys.exit(1)
|
|
255
|
+
|
|
256
|
+
while to.NoSignal:
|
|
257
|
+
try:
|
|
258
|
+
# Wait for a connection
|
|
259
|
+
connection, _client_address = sock.accept()
|
|
260
|
+
except OSError as err:
|
|
261
|
+
if err.errno != errno.EBADF or to.NoSignal:
|
|
262
|
+
to.display(to.render(err.strerror, 'red'))
|
|
263
|
+
break
|
|
264
|
+
|
|
265
|
+
thread = Thread(target=to.client_thread, args=(connection,))
|
|
266
|
+
thread.start()
|
|
267
|
+
try:
|
|
268
|
+
thread.join()
|
|
269
|
+
except Exception as error:
|
|
270
|
+
to.display(to.render(str(error), 'red'))
|
|
271
|
+
|
|
272
|
+
to.release_pidfile_lock()
|
|
273
|
+
|
|
274
|
+
if not to.NoSignal:
|
|
275
|
+
to.display(to.render("Signal received, quiting gracefully!", 'green'))
|
|
276
|
+
sys.exit(0)
|
|
277
|
+
sys.exit(1)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
if __name__ == "__main__":
|
|
281
|
+
main()
|