netcmd: Incorrect arguments to Exception constructor
[Samba.git] / python / samba / netcmd / computer.py
blob8fa0ab8d1e979b453d300bd3ca7d1d59e0d01031
1 # machine account (computer) management
3 # Copyright Bjoern Baumbch <bb@sernet.de> 2018
5 # based on user management
6 # Copyright Jelmer Vernooij 2010 <jelmer@samba.org>
7 # Copyright Theresa Halloran 2011 <theresahalloran@gmail.com>
9 # This program is free software; you can redistribute it and/or modify
10 # it under the terms of the GNU General Public License as published by
11 # the Free Software Foundation; either version 3 of the License, or
12 # (at your option) any later version.
14 # This program is distributed in the hope that it will be useful,
15 # but WITHOUT ANY WARRANTY; without even the implied warranty of
16 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 # GNU General Public License for more details.
19 # You should have received a copy of the GNU General Public License
20 # along with this program. If not, see <http://www.gnu.org/licenses/>.
23 import samba.getopt as options
24 import ldb
25 import socket
26 import samba
27 import re
28 import os
29 import tempfile
30 from samba import sd_utils
31 from samba.dcerpc import dnsserver, dnsp, security
32 from samba.dnsserver import ARecord, AAAARecord
33 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
34 from samba.remove_dc import remove_dns_references
35 from samba.auth import system_session
36 from samba.samdb import SamDB
37 from samba.common import get_bytes
38 from subprocess import check_call, CalledProcessError
39 from . import common
41 from samba import (
42 credentials,
43 dsdb,
44 Ldb,
45 werror,
46 WERRORError
49 from samba.netcmd import (
50 Command,
51 CommandError,
52 SuperCommand,
53 Option,
56 def _is_valid_ip(ip_string, address_families=None):
57 """Check ip string is valid address"""
58 # by default, check both ipv4 and ipv6
59 if not address_families:
60 address_families = [socket.AF_INET, socket.AF_INET6]
62 for address_family in address_families:
63 try:
64 socket.inet_pton(address_family, ip_string)
65 return True # if no error, return directly
66 except socket.error:
67 continue # Otherwise, check next family
68 return False
71 def _is_valid_ipv4(ip_string):
72 """Check ip string is valid ipv4 address"""
73 return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
76 def _is_valid_ipv6(ip_string):
77 """Check ip string is valid ipv6 address"""
78 return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
81 def add_dns_records(
82 samdb, name, dns_conn, change_owner_sd,
83 server, ip_address_list, logger):
84 """Add DNS A or AAAA records while creating computer. """
85 name = name.rstrip('$')
86 client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
87 select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
88 zone = samdb.domain_dns_name()
89 name_found = True
90 sd_helper = sd_utils.SDUtils(samdb)
92 try:
93 buflen, res = dns_conn.DnssrvEnumRecords2(
94 client_version,
96 server,
97 zone,
98 name,
99 None,
100 dnsp.DNS_TYPE_ALL,
101 select_flags,
102 None,
103 None,
105 except WERRORError as e:
106 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
107 name_found = False
108 pass
110 if name_found:
111 for rec in res.rec:
112 for record in rec.records:
113 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
114 # delete record
115 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
116 del_rec_buf.rec = record
117 try:
118 dns_conn.DnssrvUpdateRecord2(
119 client_version,
121 server,
122 zone,
123 name,
124 None,
125 del_rec_buf,
127 except WERRORError as e:
128 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
129 raise
131 for ip_address in ip_address_list:
132 if _is_valid_ipv6(ip_address):
133 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
134 name, zone, ip_address))
135 rec = AAAARecord(ip_address)
136 elif _is_valid_ipv4(ip_address):
137 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
138 name, zone, ip_address))
139 rec = ARecord(ip_address)
140 else:
141 raise ValueError('Invalid IP: {}'.format(ip_address))
143 # Add record
144 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
145 add_rec_buf.rec = rec
147 dns_conn.DnssrvUpdateRecord2(
148 client_version,
150 server,
151 zone,
152 name,
153 add_rec_buf,
154 None,
157 if (len(ip_address_list) > 0):
158 domaindns_zone_dn = ldb.Dn(
159 samdb,
160 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
163 dns_a_dn, ldap_record = samdb.dns_lookup(
164 "%s.%s" % (name, zone),
165 dns_partition=domaindns_zone_dn,
168 # Make the DC own the DNS record, not the administrator
169 sd_helper.modify_sd_on_dn(
170 dns_a_dn,
171 change_owner_sd,
172 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
176 class cmd_computer_add(Command):
177 """Add a new computer.
179 This command adds a new computer account to the Active Directory domain.
180 The computername specified on the command is the sAMaccountName without the
181 trailing $ (dollar sign).
183 Computer accounts may represent physical entities, such as workstations. Computer
184 accounts are also referred to as security principals and are assigned a
185 security identifier (SID).
187 Example1:
188 samba-tool computer add Computer1 -H ldap://samba.samdom.example.com \\
189 -Uadministrator%passw1rd
191 Example1 shows how to add a new computer to the domain against a remote LDAP
192 server. The -H parameter is used to specify the remote target server. The -U
193 option is used to pass the userid and password authorized to issue the command
194 remotely.
196 Example2:
197 sudo samba-tool computer add Computer2
199 Example2 shows how to add a new computer to the domain against the local
200 server. sudo is used so a user may run the command as root.
202 Example3:
203 samba-tool computer add Computer3 --computerou='OU=OrgUnit'
205 Example3 shows how to add a new computer in the OrgUnit organizational unit.
208 synopsis = "%prog <computername> [options]"
210 takes_options = [
211 Option("-H", "--URL", help="LDB URL for database or target server",
212 type=str, metavar="URL", dest="H"),
213 Option("--computerou",
214 help=("DN of alternative location (with or without domainDN "
215 "counterpart) to default CN=Computers in which new "
216 "computer object will be created. E.g. 'OU=<OU name>'"),
217 type=str),
218 Option("--description", help="Computers's description", type=str),
219 Option("--prepare-oldjoin",
220 help="Prepare enabled machine account for oldjoin mechanism",
221 action="store_true"),
222 Option("--ip-address",
223 dest='ip_address_list',
224 help=("IPv4 address for the computer's A record, or IPv6 "
225 "address for AAAA record, can be provided multiple "
226 "times"),
227 action='append'),
228 Option("--service-principal-name",
229 dest='service_principal_name_list',
230 help=("Computer's Service Principal Name, can be provided "
231 "multiple times"),
232 action='append')
235 takes_args = ["computername"]
237 takes_optiongroups = {
238 "sambaopts": options.SambaOptions,
239 "credopts": options.CredentialsOptions,
240 "versionopts": options.VersionOptions,
243 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
244 H=None, computerou=None, description=None, prepare_oldjoin=False,
245 ip_address_list=None, service_principal_name_list=None):
247 if ip_address_list is None:
248 ip_address_list = []
250 if service_principal_name_list is None:
251 service_principal_name_list = []
253 # check each IP address if provided
254 for ip_address in ip_address_list:
255 if not _is_valid_ip(ip_address):
256 raise CommandError('Invalid IP address {}'.format(ip_address))
258 lp = sambaopts.get_loadparm()
259 creds = credopts.get_credentials(lp)
261 try:
262 samdb = SamDB(url=H, session_info=system_session(),
263 credentials=creds, lp=lp)
264 samdb.newcomputer(computername, computerou=computerou,
265 description=description,
266 prepare_oldjoin=prepare_oldjoin,
267 ip_address_list=ip_address_list,
268 service_principal_name_list=service_principal_name_list,
271 if ip_address_list:
272 # if ip_address_list provided, then we need to create DNS
273 # records for this computer.
275 hostname = re.sub(r"\$$", "", computername)
276 if hostname.count('$'):
277 raise CommandError('Illegal computername "%s"' % computername)
279 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
280 ldb.binary_encode(hostname))
282 recs = samdb.search(
283 base=samdb.domain_dn(),
284 scope=ldb.SCOPE_SUBTREE,
285 expression=filters,
286 attrs=['primaryGroupID', 'objectSid'])
288 group = recs[0]['primaryGroupID'][0]
289 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
291 dns_conn = dnsserver.dnsserver(
292 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
293 lp, creds)
295 change_owner_sd = security.descriptor()
296 change_owner_sd.owner_sid = owner
297 change_owner_sd.group_sid = security.dom_sid(
298 "{}-{}".format(samdb.get_domain_sid(), group),
301 add_dns_records(
302 samdb, hostname, dns_conn,
303 change_owner_sd, samdb.host_dns_name(),
304 ip_address_list, self.get_logger())
305 except Exception as e:
306 raise CommandError("Failed to add computer '%s': " %
307 computername, e)
309 self.outf.write("Computer '%s' added successfully\n" % computername)
312 class cmd_computer_delete(Command):
313 """Delete a computer.
315 This command deletes a computer account from the Active Directory domain. The
316 computername specified on the command is the sAMAccountName without the
317 trailing $ (dollar sign).
319 Once the account is deleted, all permissions and memberships associated with
320 that account are deleted. If a new computer account is added with the same name
321 as a previously deleted account name, the new computer does not have the
322 previous permissions. The new account computer will be assigned a new security
323 identifier (SID) and permissions and memberships will have to be added.
325 The command may be run from the root userid or another authorized
326 userid. The -H or --URL= option can be used to execute the command against
327 a remote server.
329 Example1:
330 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
331 -Uadministrator%passw1rd
333 Example1 shows how to delete a computer in the domain against a remote LDAP
334 server. The -H parameter is used to specify the remote target server. The
335 --computername= and --password= options are used to pass the computername and
336 password of a computer that exists on the remote server and is authorized to
337 issue the command on that server.
339 Example2:
340 sudo samba-tool computer delete Computer2
342 Example2 shows how to delete a computer in the domain against the local server.
343 sudo is used so a computer may run the command as root.
346 synopsis = "%prog <computername> [options]"
348 takes_options = [
349 Option("-H", "--URL", help="LDB URL for database or target server",
350 type=str, metavar="URL", dest="H"),
353 takes_args = ["computername"]
354 takes_optiongroups = {
355 "sambaopts": options.SambaOptions,
356 "credopts": options.CredentialsOptions,
357 "versionopts": options.VersionOptions,
360 def run(self, computername, credopts=None, sambaopts=None,
361 versionopts=None, H=None):
362 lp = sambaopts.get_loadparm()
363 creds = credopts.get_credentials(lp, fallback_machine=True)
365 samdb = SamDB(url=H, session_info=system_session(),
366 credentials=creds, lp=lp)
368 samaccountname = computername
369 if not computername.endswith('$'):
370 samaccountname = "%s$" % computername
372 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
373 (ldb.binary_encode(samaccountname),
374 dsdb.ATYPE_WORKSTATION_TRUST))
375 try:
376 res = samdb.search(base=samdb.domain_dn(),
377 scope=ldb.SCOPE_SUBTREE,
378 expression=filter,
379 attrs=["userAccountControl", "dNSHostName"])
380 computer_dn = res[0].dn
381 computer_ac = int(res[0]["userAccountControl"][0])
382 if "dNSHostName" in res[0]:
383 computer_dns_host_name = str(res[0]["dNSHostName"][0])
384 else:
385 computer_dns_host_name = None
386 except IndexError:
387 raise CommandError('Unable to find computer "%s"' % computername)
389 computer_is_workstation = (
390 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
391 if not computer_is_workstation:
392 raise CommandError('Failed to remove computer "%s": '
393 'Computer is not a workstation - removal denied'
394 % computername)
395 try:
396 samdb.delete(computer_dn)
397 if computer_dns_host_name:
398 remove_dns_references(
399 samdb, self.get_logger(), computer_dns_host_name,
400 ignore_no_name=True)
401 except Exception as e:
402 raise CommandError('Failed to remove computer "%s"' %
403 samaccountname, e)
404 self.outf.write("Deleted computer %s\n" % computername)
407 class cmd_computer_edit(Command):
408 """Modify Computer AD object.
410 This command will allow editing of a computer account in the Active
411 Directory domain. You will then be able to add or change attributes and
412 their values.
414 The computername specified on the command is the sAMaccountName with or
415 without the trailing $ (dollar sign).
417 The command may be run from the root userid or another authorized userid.
419 The -H or --URL= option can be used to execute the command against a remote
420 server.
422 Example1:
423 samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
424 -U administrator --password=passw1rd
426 Example1 shows how to edit a computers attributes in the domain against a
427 remote LDAP server.
429 The -H parameter is used to specify the remote target server.
431 Example2:
432 samba-tool computer edit Computer2
434 Example2 shows how to edit a computers attributes in the domain against a
435 local LDAP server.
437 Example3:
438 samba-tool computer edit Computer3 --editor=nano
440 Example3 shows how to edit a computers attributes in the domain against a
441 local LDAP server using the 'nano' editor.
443 synopsis = "%prog <computername> [options]"
445 takes_options = [
446 Option("-H", "--URL", help="LDB URL for database or target server",
447 type=str, metavar="URL", dest="H"),
448 Option("--editor", help="Editor to use instead of the system default,"
449 " or 'vi' if no system default is set.", type=str),
452 takes_args = ["computername"]
453 takes_optiongroups = {
454 "sambaopts": options.SambaOptions,
455 "credopts": options.CredentialsOptions,
456 "versionopts": options.VersionOptions,
459 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
460 H=None, editor=None):
461 lp = sambaopts.get_loadparm()
462 creds = credopts.get_credentials(lp, fallback_machine=True)
463 samdb = SamDB(url=H, session_info=system_session(),
464 credentials=creds, lp=lp)
466 samaccountname = computername
467 if not computername.endswith('$'):
468 samaccountname = "%s$" % computername
470 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
471 (dsdb.ATYPE_WORKSTATION_TRUST,
472 ldb.binary_encode(samaccountname)))
474 domaindn = samdb.domain_dn()
476 try:
477 res = samdb.search(base=domaindn,
478 expression=filter,
479 scope=ldb.SCOPE_SUBTREE)
480 computer_dn = res[0].dn
481 except IndexError:
482 raise CommandError('Unable to find computer "%s"' % (computername))
484 if len(res) != 1:
485 raise CommandError('Invalid number of results: for "%s": %d' %
486 ((computername), len(res)))
488 msg = res[0]
489 result_ldif = common.get_ldif_for_editor(samdb, msg)
491 if editor is None:
492 editor = os.environ.get('EDITOR')
493 if editor is None:
494 editor = 'vi'
496 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
497 t_file.write(get_bytes(result_ldif))
498 t_file.flush()
499 try:
500 check_call([editor, t_file.name])
501 except CalledProcessError as e:
502 raise CalledProcessError("ERROR: ", e)
503 with open(t_file.name) as edited_file:
504 edited_message = edited_file.read()
506 msgs_edited = samdb.parse_ldif(edited_message)
507 msg_edited = next(msgs_edited)[1]
509 res_msg_diff = samdb.msg_diff(msg, msg_edited)
510 if len(res_msg_diff) == 0:
511 self.outf.write("Nothing to do\n")
512 return
514 try:
515 samdb.modify(res_msg_diff)
516 except Exception as e:
517 raise CommandError("Failed to modify computer '%s': " %
518 computername, e)
520 self.outf.write("Modified computer '%s' successfully\n" % computername)
522 class cmd_computer_list(Command):
523 """List all computers."""
525 synopsis = "%prog [options]"
527 takes_options = [
528 Option("-H", "--URL", help="LDB URL for database or target server",
529 type=str, metavar="URL", dest="H"),
530 Option("-b", "--base-dn",
531 help="Specify base DN to use",
532 type=str),
533 Option("--full-dn", dest="full_dn",
534 default=False,
535 action="store_true",
536 help="Display DN instead of the sAMAccountName.")
539 takes_optiongroups = {
540 "sambaopts": options.SambaOptions,
541 "credopts": options.CredentialsOptions,
542 "versionopts": options.VersionOptions,
545 def run(self,
546 sambaopts=None,
547 credopts=None,
548 versionopts=None,
549 H=None,
550 base_dn=None,
551 full_dn=False):
552 lp = sambaopts.get_loadparm()
553 creds = credopts.get_credentials(lp, fallback_machine=True)
555 samdb = SamDB(url=H, session_info=system_session(),
556 credentials=creds, lp=lp)
558 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
560 search_dn = samdb.domain_dn()
561 if base_dn:
562 search_dn = samdb.normalize_dn_in_domain(base_dn)
564 res = samdb.search(search_dn,
565 scope=ldb.SCOPE_SUBTREE,
566 expression=filter,
567 attrs=["samaccountname"])
568 if (len(res) == 0):
569 return
571 for msg in res:
572 if full_dn:
573 self.outf.write("%s\n" % msg.get("dn"))
574 continue
576 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
579 class cmd_computer_show(Command):
580 """Display a computer AD object.
582 This command displays a computer account and it's attributes in the Active
583 Directory domain.
584 The computername specified on the command is the sAMAccountName.
586 The command may be run from the root userid or another authorized
587 userid.
589 The -H or --URL= option can be used to execute the command against a remote
590 server.
592 Example1:
593 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
594 -U administrator
596 Example1 shows how display a computers attributes in the domain against a
597 remote LDAP server.
599 The -H parameter is used to specify the remote target server.
601 Example2:
602 samba-tool computer show Computer2
604 Example2 shows how to display a computers attributes in the domain against a
605 local LDAP server.
607 Example3:
608 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
610 Example3 shows how to display a computers objectSid and operatingSystem
611 attribute.
613 synopsis = "%prog <computername> [options]"
615 takes_options = [
616 Option("-H", "--URL", help="LDB URL for database or target server",
617 type=str, metavar="URL", dest="H"),
618 Option("--attributes",
619 help=("Comma separated list of attributes, "
620 "which will be printed."),
621 type=str, dest="computer_attrs"),
624 takes_args = ["computername"]
625 takes_optiongroups = {
626 "sambaopts": options.SambaOptions,
627 "credopts": options.CredentialsOptions,
628 "versionopts": options.VersionOptions,
631 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
632 H=None, computer_attrs=None):
634 lp = sambaopts.get_loadparm()
635 creds = credopts.get_credentials(lp, fallback_machine=True)
636 samdb = SamDB(url=H, session_info=system_session(),
637 credentials=creds, lp=lp)
639 attrs = None
640 if computer_attrs:
641 attrs = computer_attrs.split(",")
643 samaccountname = computername
644 if not computername.endswith('$'):
645 samaccountname = "%s$" % computername
647 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
648 (dsdb.ATYPE_WORKSTATION_TRUST,
649 ldb.binary_encode(samaccountname)))
651 domaindn = samdb.domain_dn()
653 try:
654 res = samdb.search(base=domaindn, expression=filter,
655 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
656 computer_dn = res[0].dn
657 except IndexError:
658 raise CommandError('Unable to find computer "%s"' %
659 samaccountname)
661 for msg in res:
662 computer_ldif = common.get_ldif_for_editor(samdb, msg)
663 self.outf.write(computer_ldif)
666 class cmd_computer_move(Command):
667 """Move a computer to an organizational unit/container."""
669 synopsis = "%prog <computername> <new_ou_dn> [options]"
671 takes_options = [
672 Option("-H", "--URL", help="LDB URL for database or target server",
673 type=str, metavar="URL", dest="H"),
676 takes_args = ["computername", "new_ou_dn"]
677 takes_optiongroups = {
678 "sambaopts": options.SambaOptions,
679 "credopts": options.CredentialsOptions,
680 "versionopts": options.VersionOptions,
683 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
684 versionopts=None, H=None):
685 lp = sambaopts.get_loadparm()
686 creds = credopts.get_credentials(lp, fallback_machine=True)
687 samdb = SamDB(url=H, session_info=system_session(),
688 credentials=creds, lp=lp)
689 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
691 samaccountname = computername
692 if not computername.endswith('$'):
693 samaccountname = "%s$" % computername
695 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
696 (ldb.binary_encode(samaccountname),
697 dsdb.ATYPE_WORKSTATION_TRUST))
698 try:
699 res = samdb.search(base=domain_dn,
700 expression=filter,
701 scope=ldb.SCOPE_SUBTREE)
702 computer_dn = res[0].dn
703 except IndexError:
704 raise CommandError('Unable to find computer "%s"' % (computername))
706 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
707 if not full_new_ou_dn.is_child_of(domain_dn):
708 full_new_ou_dn.add_base(domain_dn)
709 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
710 new_computer_dn.remove_base_components(len(computer_dn) -1)
711 new_computer_dn.add_base(full_new_ou_dn)
712 try:
713 samdb.rename(computer_dn, new_computer_dn)
714 except Exception as e:
715 raise CommandError('Failed to move computer "%s"' % computername, e)
716 self.outf.write('Moved computer "%s" to "%s"\n' %
717 (computername, new_ou_dn))
720 class cmd_computer(SuperCommand):
721 """Computer management."""
723 subcommands = {}
724 subcommands["add"] = cmd_computer_add()
725 subcommands["create"] = cmd_computer_add()
726 subcommands["delete"] = cmd_computer_delete()
727 subcommands["edit"] = cmd_computer_edit()
728 subcommands["list"] = cmd_computer_list()
729 subcommands["show"] = cmd_computer_show()
730 subcommands["move"] = cmd_computer_move()