targetcli 3.0.0.dev0__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 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
+ try:
312
+ while not shell._exit:
313
+ shell.run_interactive()
314
+ except (RTSLibError, ExecutionError) as msg:
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()
@@ -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()