1 # Unix SMB/CIFS implementation.
2 # Copyright (C) Jelmer Vernooij <jelmer@samba.org> 2009-2012
3 # Copyright (C) Theresa Halloran <theresahalloran@gmail.com> 2011
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 3 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 from ldb
import ERR_INVALID_CREDENTIALS
, ERR_INSUFFICIENT_ACCESS_RIGHTS
, LdbError
27 from samba
import colour
28 from samba
.auth
import system_session
29 from samba
.getopt
import Option
, OptionParser
30 from samba
.logger
import get_samba_logger
31 from samba
.samdb
import SamDB
32 from samba
.dcerpc
.security
import SDDLValueError
34 from .encoders
import JSONEncoder
37 class PlainHelpFormatter(optparse
.IndentedHelpFormatter
):
38 """This help formatter does text wrapping and preserves newlines."""
40 def format_description(self
, description
=""):
41 desc_width
= self
.width
- self
.current_indent
42 indent
= " " * self
.current_indent
43 paragraphs
= description
.split('\n')
44 wrapped_paragraphs
= [
47 initial_indent
=indent
,
48 subsequent_indent
=indent
)
50 result
= "\n".join(wrapped_paragraphs
) + "\n"
53 def format_epilog(self
, epilog
):
55 return "\n" + epilog
+ "\n"
60 class Command(object):
61 """A samba-tool command."""
63 def _get_short_description(self
):
64 return self
.__doc
__.splitlines()[0].rstrip("\n")
66 short_description
= property(_get_short_description
)
68 def _get_full_description(self
):
69 lines
= self
.__doc
__.split("\n")
70 return lines
[0] + "\n" + textwrap
.dedent("\n".join(lines
[1:]))
72 full_description
= property(_get_full_description
)
75 name
= self
.__class
__.__name
__
76 if name
.startswith("cmd_"):
80 name
= property(_get_name
)
82 # synopsis must be defined in all subclasses in order to provide the
87 takes_optiongroups
= {}
91 requested_colour
= None
96 preferred_output_format
= None
98 def _set_files(self
, outf
=None, errf
=None):
104 def __init__(self
, outf
=sys
.stdout
, errf
=sys
.stderr
):
105 self
._set
_files
(outf
, errf
)
107 def usage(self
, prog
=None):
108 parser
, _
= self
._create
_parser
(prog
or self
.command_name
)
111 def _print_error(self
, msg
, evalue
=None, klass
=None):
112 if self
.preferred_output_format
== 'json':
116 msg
= f
"{msg} - {evalue}"
117 if klass
is not None:
118 kwargs
= {'error class': klass
}
122 self
.print_json_status(evalue
, msg
, **kwargs
)
125 err
= colour
.c_DARK_RED("ERROR")
126 klass
= '' if klass
is None else f
'({klass})'
129 print(f
"{err}{klass}: {msg}", file=self
.errf
)
131 print(f
"{err}{klass}: {msg} - {evalue}", file=self
.errf
)
133 def _print_sddl_value_error(self
, e
):
134 generic_msg
, specific_msg
, position
, sddl
= e
.args
135 print(f
"{colour.c_DARK_RED('ERROR')}: {generic_msg}\n",
137 print(f
' {sddl}', file=self
.errf
)
138 # If the SDDL contains non-ascii characters, the byte offset
139 # provided by the exception won't agree with the visual offset
140 # because those characters will be encoded as multiple bytes.
142 # To account for this we'll attempt to measure the string
143 # length of the specified number of bytes. That is not quite
144 # the same as the visual length, because the SDDL could
145 # contain zero-width, full-width, or combining characters, but
148 position
= len((sddl
.encode()[:position
]).decode())
150 # use the original position
153 print(f
"{colour.c_DARK_YELLOW('^'):>{position + 2}}", file=self
.errf
)
154 print(f
' {specific_msg}', file=self
.errf
)
156 def ldb_connect(self
, hostopts
, sambaopts
, credopts
):
157 """Helper to connect to Ldb database using command line opts."""
158 lp
= sambaopts
.get_loadparm()
159 creds
= credopts
.get_credentials(lp
)
160 return SamDB(hostopts
.H
, credentials
=creds
,
161 session_info
=system_session(lp
), lp
=lp
)
163 def print_json(self
, data
):
164 """Print json on the screen using consistent formatting and sorting.
166 A custom JSONEncoder class is used to help with serializing unknown
167 objects such as Dn for example.
169 json
.dump(data
, self
.outf
, cls
=JSONEncoder
, indent
=2, sort_keys
=True)
170 self
.outf
.write("\n")
172 def print_json_status(self
, error
=None, message
=None, **kwargs
):
173 """For commands that really have nothing to say when they succeed
174 (`samba-tool foo delete --json`), we can still emit
175 '{"status": "OK"}\n'. And if they fail they can say:
176 '{"status": "error"}\n'.
177 This function hopes to keep things consistent.
179 If error is true-ish but not True, it is stringified and added
180 as a message. For example, if error is an LdbError with an
181 OBJECT_NOT_FOUND code, self.print_json_status(error) results
184 '{"status": "error", "message": "object not found"}\n'
186 unless an explicit message is added, in which case that is
187 used. A message can be provided on success, like this:
189 '{"status": "OK", "message": "thanks for asking!"}\n'
191 Extra keywords can be added too.
193 In summary, you might go:
197 except Exception as e:
204 data
['status'] = 'error'
205 if error
is not True:
206 data
['message'] = str(error
)
208 data
['status'] = 'OK'
210 if message
is not None:
211 data
['message'] = message
214 self
.print_json(data
)
216 def show_command_error(self
, e
):
217 """display a command error"""
218 if isinstance(e
, CommandError
):
219 (etype
, evalue
, etraceback
) = e
.exception_info
220 inner_exception
= e
.inner_exception
222 force_traceback
= False
224 (etype
, evalue
, etraceback
) = sys
.exc_info()
226 message
= "uncaught exception"
227 force_traceback
= True
229 if isinstance(e
, optparse
.OptParseError
):
230 print(evalue
, file=self
.errf
)
232 force_traceback
= False
234 elif isinstance(inner_exception
, LdbError
):
235 (ldb_ecode
, ldb_emsg
) = inner_exception
.args
236 if ldb_ecode
== ERR_INVALID_CREDENTIALS
:
237 print("Invalid username or password", file=self
.errf
)
238 force_traceback
= False
239 elif ldb_emsg
== 'LDAP client internal error: NT_STATUS_NETWORK_UNREACHABLE':
240 print("Could not reach remote server", file=self
.errf
)
241 force_traceback
= False
242 elif ldb_emsg
.startswith("Unable to open tdb "):
243 self
._print
_error
(message
, ldb_emsg
, 'ldb')
244 force_traceback
= False
245 elif ldb_ecode
== ERR_INSUFFICIENT_ACCESS_RIGHTS
:
246 self
._print
_error
("User has insufficient access rights")
247 force_traceback
= False
249 self
._print
_error
(message
, ldb_emsg
, 'ldb')
251 elif isinstance(inner_exception
, SDDLValueError
):
252 self
._print
_sddl
_value
_error
(inner_exception
)
253 force_traceback
= False
255 elif isinstance(inner_exception
, AssertionError):
256 self
._print
_error
(message
, klass
='assert')
257 force_traceback
= True
258 elif isinstance(inner_exception
, RuntimeError):
259 self
._print
_error
(message
, evalue
, 'runtime')
260 elif type(inner_exception
) is Exception:
261 self
._print
_error
(message
, evalue
, 'exception')
262 force_traceback
= True
263 elif inner_exception
is None:
264 self
._print
_error
(message
)
266 self
._print
_error
(message
, evalue
, str(etype
))
268 if force_traceback
or samba
.get_debug_level() >= 3:
269 traceback
.print_tb(etraceback
, file=self
.errf
)
271 def _create_parser(self
, prog
=None, epilog
=None):
272 parser
= OptionParser(
274 description
=self
.full_description
,
275 formatter
=PlainHelpFormatter(),
279 parser
.add_options(self
.takes_options
)
281 for name
in sorted(self
.takes_optiongroups
.keys()):
282 optiongroup
= self
.takes_optiongroups
[name
]
283 optiongroups
[name
] = optiongroup(parser
)
284 parser
.add_option_group(optiongroups
[name
])
286 parser
.add_option("--color",
287 help="use colour if available (default: auto)",
288 metavar
="always|never|auto",
291 return parser
, optiongroups
293 def message(self
, text
):
294 self
.outf
.write(text
+ "\n")
296 def _resolve(self
, path
, *argv
, outf
=None, errf
=None):
297 """This is a leaf node, the command that will actually run."""
298 self
._set
_files
(outf
, errf
)
299 self
.command_name
= path
302 def _run(self
, *argv
):
303 parser
, optiongroups
= self
._create
_parser
(self
.command_name
)
305 # Handle possible validation errors raised by parser
307 opts
, args
= parser
.parse_args(list(argv
))
308 except Exception as e
:
309 self
.show_command_error(e
)
312 # Filter out options from option groups
313 kwargs
= dict(opts
.__dict
__)
314 for option_group
in parser
.option_groups
:
315 for option
in option_group
.option_list
:
316 if option
.dest
is not None and option
.dest
in kwargs
:
317 del kwargs
[option
.dest
]
318 kwargs
.update(optiongroups
)
320 if kwargs
.get('output_format') == 'json':
321 self
.preferred_output_format
= 'json'
323 # we need to reset this for the tests that reuse the
325 self
.preferred_output_format
= None
328 self
.apply_colour_choice(kwargs
.pop('color', 'auto'))
330 # Check for a min a max number of allowed arguments, whenever possible
331 # The suffix "?" means zero or one occurrence
332 # The suffix "+" means at least one occurrence
333 # The suffix "*" means zero or more occurrences
336 undetermined_max_args
= False
337 for i
, arg
in enumerate(self
.takes_args
):
338 if arg
[-1] != "?" and arg
[-1] != "*":
340 if arg
[-1] == "+" or arg
[-1] == "*":
341 undetermined_max_args
= True
344 if (len(args
) < min_args
) or (not undetermined_max_args
and len(args
) > max_args
):
348 self
.raw_argv
= list(argv
)
350 self
.raw_kwargs
= kwargs
353 return self
.run(*args
, **kwargs
)
354 except Exception as e
:
355 self
.show_command_error(e
)
358 def run(self
, *args
, **kwargs
):
359 """Run the command. This should be overridden by all subclasses."""
360 raise NotImplementedError(f
"'{self.command_name}' run method not implemented")
362 def get_logger(self
, name
="", verbose
=False, quiet
=False, **kwargs
):
363 """Get a logger object."""
364 return get_samba_logger(
365 name
=name
or self
.name
, stream
=self
.errf
,
366 verbose
=verbose
, quiet
=quiet
,
369 def apply_colour_choice(self
, requested
):
370 """Heuristics to work out whether the user wants colour output, from a
371 --color=yes|no|auto option. This alters the ANSI 16 bit colour
372 "constants" in the colour module to be either real colours or empty
375 self
.requested_colour
= requested
377 colour
.colour_if_wanted(self
.outf
,
380 except ValueError as e
:
381 raise CommandError(f
"Unknown --color option: {requested} "
382 "please choose from always|never|auto")
385 class SuperCommand(Command
):
386 """A samba-tool command with subcommands."""
388 synopsis
= "%prog <subcommand>"
392 def _resolve(self
, path
, *args
, outf
=None, errf
=None):
393 """This is an internal node. We need to consume one of the args and
394 find the relevant child, returning an instance of that Command.
396 If there are no children, this SuperCommand will be returned
397 and its _run() will do a --help like thing.
399 self
.command_name
= path
400 self
._set
_files
(outf
, errf
)
402 # We collect up certain option arguments and pass them to the
403 # leaf, which is why we iterate over args, though we really
404 # expect to return in the first iteration.
407 for i
, a
in enumerate(args
):
408 if a
in self
.subcommands
:
409 sub_args
= args
[i
+ 1:] + tuple(deferred_args
)
410 sub_path
= f
'{path} {a}'
412 sub
= self
.subcommands
[a
]
413 return sub
._resolve
(sub_path
, *sub_args
, outf
=outf
, errf
=errf
)
415 elif a
in ['--help', 'help', None, '-h', '-V', '--version']:
416 # we pass these to the leaf node.
419 deferred_args
.append(a
)
422 # they are talking nonsense
423 print("%s: no such subcommand: %s\n" % (path
, a
), file=self
.outf
)
426 # We didn't find a subcommand, but maybe we found e.g. --version
427 print("%s: missing subcommand\n" % (path
), file=self
.outf
)
428 return (self
, deferred_args
)
430 def _run(self
, *argv
):
431 epilog
= "\nAvailable subcommands:\n"
433 subcmds
= sorted(self
.subcommands
.keys())
434 max_length
= max([len(c
) for c
in subcmds
], default
=0)
435 for cmd_name
in subcmds
:
436 cmd
= self
.subcommands
[cmd_name
]
439 epilog
+= " %*s - %s\n" % (
440 -max_length
, cmd_name
, cmd
.short_description
)
442 epilog
+= ("\nFor more help on a specific subcommand, please type: "
443 f
"{self.command_name} <subcommand> (-h|--help)\n")
445 parser
, optiongroups
= self
._create
_parser
(self
.command_name
, epilog
=epilog
)
446 opts
, args
= parser
.parse_args(list(argv
))
448 # note: if argv had --help, parser.parse_args() will have
449 # already done the .print_help() and attempted to exit with
450 # return code 0, so we won't get here.
455 class CommandError(Exception):
456 """An exception class for samba-tool Command errors."""
458 def __init__(self
, message
, inner_exception
=None):
459 self
.message
= message
460 self
.inner_exception
= inner_exception
461 self
.exception_info
= sys
.exc_info()
464 return "CommandError(%s)" % self
.message