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
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.compat import get_bytes
38 from subprocess import check_call, CalledProcessError
48 from samba.netcmd import (
55 def _is_valid_ip(ip_string, address_families=None):
56 """Check ip string is valid address"""
57 # by default, check both ipv4 and ipv6
58 if not address_families:
59 address_families = [socket.AF_INET, socket.AF_INET6]
61 for address_family in address_families:
63 socket.inet_pton(address_family, ip_string)
64 return True # if no error, return directly
66 continue # Otherwise, check next family
70 def _is_valid_ipv4(ip_string):
71 """Check ip string is valid ipv4 address"""
72 return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
75 def _is_valid_ipv6(ip_string):
76 """Check ip string is valid ipv6 address"""
77 return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
81 samdb, name, dns_conn, change_owner_sd,
82 server, ip_address_list, logger):
83 """Add DNS A or AAAA records while creating computer. """
84 name = name.rstrip('$')
85 client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
86 select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
87 zone = samdb.domain_dns_name()
89 sd_helper = sd_utils.SDUtils(samdb)
92 buflen, res = dns_conn.DnssrvEnumRecords2(
104 except WERRORError as e:
105 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
111 for record in rec.records:
112 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
114 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
115 del_rec_buf.rec = record
117 dns_conn.DnssrvUpdateRecord2(
126 except WERRORError as e:
127 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
130 for ip_address in ip_address_list:
131 if _is_valid_ipv6(ip_address):
132 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
133 name, zone, ip_address))
134 rec = AAAARecord(ip_address)
135 elif _is_valid_ipv4(ip_address):
136 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
137 name, zone, ip_address))
138 rec = ARecord(ip_address)
140 raise ValueError('Invalid IP: {}'.format(ip_address))
143 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
144 add_rec_buf.rec = rec
146 dns_conn.DnssrvUpdateRecord2(
156 if (len(ip_address_list) > 0):
157 domaindns_zone_dn = ldb.Dn(
159 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
162 dns_a_dn, ldap_record = samdb.dns_lookup(
163 "%s.%s" % (name, zone),
164 dns_partition=domaindns_zone_dn,
167 # Make the DC own the DNS record, not the administrator
168 sd_helper.modify_sd_on_dn(
171 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
175 class cmd_computer_create(Command):
176 """Create a new computer.
178 This command creates a new computer account in the Active Directory domain.
179 The computername specified on the command is the sAMaccountName without the
180 trailing $ (dollar sign).
182 User accounts may represent physical entities, such as workstations. Computer
183 accounts are also referred to as security principals and are assigned a
184 security identifier (SID).
187 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \\
188 -Uadministrator%passw1rd
190 Example1 shows how to create a new computer in the domain against a remote LDAP
191 server. The -H parameter is used to specify the remote target server. The -U
192 option is used to pass the userid and password authorized to issue the command
196 sudo samba-tool computer create Computer2
198 Example2 shows how to create a new computer in the domain against the local
199 server. sudo is used so a user may run the command as root.
202 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
204 Example3 shows how to create a new computer in the OrgUnit organizational unit.
207 synopsis = "%prog <computername> [options]"
210 Option("-H", "--URL", help="LDB URL for database or target server",
211 type=str, metavar="URL", dest="H"),
212 Option("--computerou",
213 help=("DN of alternative location (with or without domainDN "
214 "counterpart) to default CN=Computers in which new "
215 "computer object will be created. E.g. 'OU=<OU name>'"),
217 Option("--description", help="Computers's description", type=str),
218 Option("--prepare-oldjoin",
219 help="Prepare enabled machine account for oldjoin mechanism",
220 action="store_true"),
221 Option("--ip-address",
222 dest='ip_address_list',
223 help=("IPv4 address for the computer's A record, or IPv6 "
224 "address for AAAA record, can be provided multiple "
227 Option("--service-principal-name",
228 dest='service_principal_name_list',
229 help=("Computer's Service Principal Name, can be provided "
234 takes_args = ["computername"]
236 takes_optiongroups = {
237 "sambaopts": options.SambaOptions,
238 "credopts": options.CredentialsOptions,
239 "versionopts": options.VersionOptions,
242 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
243 H=None, computerou=None, description=None, prepare_oldjoin=False,
244 ip_address_list=None, service_principal_name_list=None):
246 if ip_address_list is None:
249 if service_principal_name_list is None:
250 service_principal_name_list = []
252 # check each IP address if provided
253 for ip_address in ip_address_list:
254 if not _is_valid_ip(ip_address):
255 raise CommandError('Invalid IP address {}'.format(ip_address))
257 lp = sambaopts.get_loadparm()
258 creds = credopts.get_credentials(lp)
261 samdb = SamDB(url=H, session_info=system_session(),
262 credentials=creds, lp=lp)
263 samdb.newcomputer(computername, computerou=computerou,
264 description=description,
265 prepare_oldjoin=prepare_oldjoin,
266 ip_address_list=ip_address_list,
267 service_principal_name_list=service_principal_name_list,
271 # if ip_address_list provided, then we need to create DNS
272 # records for this computer.
274 hostname = re.sub(r"\$$", "", computername)
275 if hostname.count('$'):
276 raise CommandError('Illegal computername "%s"' % computername)
278 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
279 ldb.binary_encode(hostname))
282 base=samdb.domain_dn(),
283 scope=ldb.SCOPE_SUBTREE,
285 attrs=['primaryGroupID', 'objectSid'])
287 group = recs[0]['primaryGroupID'][0]
288 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
290 dns_conn = dnsserver.dnsserver(
291 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
294 change_owner_sd = security.descriptor()
295 change_owner_sd.owner_sid = owner
296 change_owner_sd.group_sid = security.dom_sid(
297 "{}-{}".format(samdb.get_domain_sid(), group),
301 samdb, hostname, dns_conn,
302 change_owner_sd, samdb.host_dns_name(),
303 ip_address_list, self.get_logger())
304 except Exception as e:
305 raise CommandError("Failed to create computer '%s': " %
308 self.outf.write("Computer '%s' created successfully\n" % computername)
311 class cmd_computer_delete(Command):
312 """Delete a computer.
314 This command deletes a computer account from the Active Directory domain. The
315 computername specified on the command is the sAMAccountName without the
316 trailing $ (dollar sign).
318 Once the account is deleted, all permissions and memberships associated with
319 that account are deleted. If a new computer account is added with the same name
320 as a previously deleted account name, the new computer does not have the
321 previous permissions. The new account computer will be assigned a new security
322 identifier (SID) and permissions and memberships will have to be added.
324 The command may be run from the root userid or another authorized
325 userid. The -H or --URL= option can be used to execute the command against
329 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \\
330 -Uadministrator%passw1rd
332 Example1 shows how to delete a computer in the domain against a remote LDAP
333 server. The -H parameter is used to specify the remote target server. The
334 --computername= and --password= options are used to pass the computername and
335 password of a computer that exists on the remote server and is authorized to
336 issue the command on that server.
339 sudo samba-tool computer delete Computer2
341 Example2 shows how to delete a computer in the domain against the local server.
342 sudo is used so a computer may run the command as root.
345 synopsis = "%prog <computername> [options]"
348 Option("-H", "--URL", help="LDB URL for database or target server",
349 type=str, metavar="URL", dest="H"),
352 takes_args = ["computername"]
353 takes_optiongroups = {
354 "sambaopts": options.SambaOptions,
355 "credopts": options.CredentialsOptions,
356 "versionopts": options.VersionOptions,
359 def run(self, computername, credopts=None, sambaopts=None,
360 versionopts=None, H=None):
361 lp = sambaopts.get_loadparm()
362 creds = credopts.get_credentials(lp, fallback_machine=True)
364 samdb = SamDB(url=H, session_info=system_session(),
365 credentials=creds, lp=lp)
367 samaccountname = computername
368 if not computername.endswith('$'):
369 samaccountname = "%s$" % computername
371 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
372 (ldb.binary_encode(samaccountname),
373 dsdb.ATYPE_WORKSTATION_TRUST))
375 res = samdb.search(base=samdb.domain_dn(),
376 scope=ldb.SCOPE_SUBTREE,
378 attrs=["userAccountControl", "dNSHostName"])
379 computer_dn = res[0].dn
380 computer_ac = int(res[0]["userAccountControl"][0])
381 if "dNSHostName" in res[0]:
382 computer_dns_host_name = str(res[0]["dNSHostName"][0])
384 computer_dns_host_name = None
386 raise CommandError('Unable to find computer "%s"' % computername)
388 computer_is_workstation = (
389 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
390 if not computer_is_workstation:
391 raise CommandError('Failed to remove computer "%s": '
392 'Computer is not a workstation - removal denied'
395 samdb.delete(computer_dn)
396 if computer_dns_host_name:
397 remove_dns_references(
398 samdb, self.get_logger(), computer_dns_host_name,
400 except Exception as e:
401 raise CommandError('Failed to remove computer "%s"' %
403 self.outf.write("Deleted computer %s\n" % computername)
406 class cmd_computer_edit(Command):
407 """Modify Computer AD object.
409 This command will allow editing of a computer account in the Active
410 Directory domain. You will then be able to add or change attributes and
413 The computername specified on the command is the sAMaccountName with or
414 without the trailing $ (dollar sign).
416 The command may be run from the root userid or another authorized userid.
418 The -H or --URL= option can be used to execute the command against a remote
422 samba-tool computer edit Computer1 -H ldap://samba.samdom.example.com \\
423 -U administrator --password=passw1rd
425 Example1 shows how to edit a computers attributes in the domain against a
428 The -H parameter is used to specify the remote target server.
431 samba-tool computer edit Computer2
433 Example2 shows how to edit a computers attributes in the domain against a
437 samba-tool computer edit Computer3 --editor=nano
439 Example3 shows how to edit a computers attributes in the domain against a
440 local LDAP server using the 'nano' editor.
442 synopsis = "%prog <computername> [options]"
445 Option("-H", "--URL", help="LDB URL for database or target server",
446 type=str, metavar="URL", dest="H"),
447 Option("--editor", help="Editor to use instead of the system default,"
448 " or 'vi' if no system default is set.", type=str),
451 takes_args = ["computername"]
452 takes_optiongroups = {
453 "sambaopts": options.SambaOptions,
454 "credopts": options.CredentialsOptions,
455 "versionopts": options.VersionOptions,
458 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
459 H=None, editor=None):
462 lp = sambaopts.get_loadparm()
463 creds = credopts.get_credentials(lp, fallback_machine=True)
464 samdb = SamDB(url=H, session_info=system_session(),
465 credentials=creds, lp=lp)
467 samaccountname = computername
468 if not computername.endswith('$'):
469 samaccountname = "%s$" % computername
471 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
472 (dsdb.ATYPE_WORKSTATION_TRUST,
473 ldb.binary_encode(samaccountname)))
475 domaindn = samdb.domain_dn()
478 res = samdb.search(base=domaindn,
480 scope=ldb.SCOPE_SUBTREE)
481 computer_dn = res[0].dn
483 raise CommandError('Unable to find computer "%s"' % (computername))
486 raise CommandError('Invalid number of results: for "%s": %d' %
487 ((computername), len(res)))
490 result_ldif = common.get_ldif_for_editor(samdb, msg)
493 editor = os.environ.get('EDITOR')
497 with tempfile.NamedTemporaryFile(suffix=".tmp") as t_file:
498 t_file.write(get_bytes(result_ldif))
501 check_call([editor, t_file.name])
502 except CalledProcessError as e:
503 raise CalledProcessError("ERROR: ", e)
504 with open(t_file.name) as edited_file:
505 edited_message = edited_file.read()
507 msgs_edited = samdb.parse_ldif(edited_message)
508 msg_edited = next(msgs_edited)[1]
510 res_msg_diff = samdb.msg_diff(msg, msg_edited)
511 if len(res_msg_diff) == 0:
512 self.outf.write("Nothing to do\n")
516 samdb.modify(res_msg_diff)
517 except Exception as e:
518 raise CommandError("Failed to modify computer '%s': " %
521 self.outf.write("Modified computer '%s' successfully\n" % computername)
523 class cmd_computer_list(Command):
524 """List all computers."""
526 synopsis = "%prog [options]"
529 Option("-H", "--URL", help="LDB URL for database or target server",
530 type=str, metavar="URL", dest="H"),
533 takes_optiongroups = {
534 "sambaopts": options.SambaOptions,
535 "credopts": options.CredentialsOptions,
536 "versionopts": options.VersionOptions,
539 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
540 lp = sambaopts.get_loadparm()
541 creds = credopts.get_credentials(lp, fallback_machine=True)
543 samdb = SamDB(url=H, session_info=system_session(),
544 credentials=creds, lp=lp)
546 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
548 domain_dn = samdb.domain_dn()
549 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
551 attrs=["samaccountname"])
556 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
559 class cmd_computer_show(Command):
560 """Display a computer AD object.
562 This command displays a computer account and it's attributes in the Active
564 The computername specified on the command is the sAMAccountName.
566 The command may be run from the root userid or another authorized
569 The -H or --URL= option can be used to execute the command against a remote
573 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \\
576 Example1 shows how display a computers attributes in the domain against a
579 The -H parameter is used to specify the remote target server.
582 samba-tool computer show Computer2
584 Example2 shows how to display a computers attributes in the domain against a
588 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
590 Example3 shows how to display a computers objectSid and operatingSystem
593 synopsis = "%prog <computername> [options]"
596 Option("-H", "--URL", help="LDB URL for database or target server",
597 type=str, metavar="URL", dest="H"),
598 Option("--attributes",
599 help=("Comma separated list of attributes, "
600 "which will be printed."),
601 type=str, dest="computer_attrs"),
604 takes_args = ["computername"]
605 takes_optiongroups = {
606 "sambaopts": options.SambaOptions,
607 "credopts": options.CredentialsOptions,
608 "versionopts": options.VersionOptions,
611 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
612 H=None, computer_attrs=None):
614 lp = sambaopts.get_loadparm()
615 creds = credopts.get_credentials(lp, fallback_machine=True)
616 samdb = SamDB(url=H, session_info=system_session(),
617 credentials=creds, lp=lp)
621 attrs = computer_attrs.split(",")
623 samaccountname = computername
624 if not computername.endswith('$'):
625 samaccountname = "%s$" % computername
627 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
628 (dsdb.ATYPE_WORKSTATION_TRUST,
629 ldb.binary_encode(samaccountname)))
631 domaindn = samdb.domain_dn()
634 res = samdb.search(base=domaindn, expression=filter,
635 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
636 computer_dn = res[0].dn
638 raise CommandError('Unable to find computer "%s"' %
642 computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
643 self.outf.write(computer_ldif)
646 class cmd_computer_move(Command):
647 """Move a computer to an organizational unit/container."""
649 synopsis = "%prog computername <new_ou_dn> [options]"
652 Option("-H", "--URL", help="LDB URL for database or target server",
653 type=str, metavar="URL", dest="H"),
656 takes_args = ["computername", "new_ou_dn"]
657 takes_optiongroups = {
658 "sambaopts": options.SambaOptions,
659 "credopts": options.CredentialsOptions,
660 "versionopts": options.VersionOptions,
663 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
664 versionopts=None, H=None):
665 lp = sambaopts.get_loadparm()
666 creds = credopts.get_credentials(lp, fallback_machine=True)
667 samdb = SamDB(url=H, session_info=system_session(),
668 credentials=creds, lp=lp)
669 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
671 samaccountname = computername
672 if not computername.endswith('$'):
673 samaccountname = "%s$" % computername
675 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
676 (ldb.binary_encode(samaccountname),
677 dsdb.ATYPE_WORKSTATION_TRUST))
679 res = samdb.search(base=domain_dn,
681 scope=ldb.SCOPE_SUBTREE)
682 computer_dn = res[0].dn
684 raise CommandError('Unable to find computer "%s"' % (computername))
686 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
687 if not full_new_ou_dn.is_child_of(domain_dn):
688 full_new_ou_dn.add_base(domain_dn)
689 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
690 new_computer_dn.remove_base_components(len(computer_dn) -1)
691 new_computer_dn.add_base(full_new_ou_dn)
693 samdb.rename(computer_dn, new_computer_dn)
694 except Exception as e:
695 raise CommandError('Failed to move computer "%s"' % computername, e)
696 self.outf.write('Moved computer "%s" to "%s"\n' %
697 (computername, new_ou_dn))
700 class cmd_computer(SuperCommand):
701 """Computer management."""
704 subcommands["create"] = cmd_computer_create()
705 subcommands["delete"] = cmd_computer_delete()
706 subcommands["edit"] = cmd_computer_edit()
707 subcommands["list"] = cmd_computer_list()
708 subcommands["show"] = cmd_computer_show()
709 subcommands["move"] = cmd_computer_move()