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
28 from samba import sd_utils
29 from samba.dcerpc import dnsserver, dnsp, security
30 from samba.dnsserver import ARecord, AAAARecord
31 from samba.ndr import ndr_unpack, ndr_pack, ndr_print
32 from samba.remove_dc import remove_dns_references
33 from samba.auth import system_session
34 from samba.samdb import SamDB
44 from samba.netcmd import (
52 def _is_valid_ip(ip_string, address_families=None):
53 """Check ip string is valid address"""
54 # by default, check both ipv4 and ipv6
55 if not address_families:
56 address_families = [socket.AF_INET, socket.AF_INET6]
58 for address_family in address_families:
60 socket.inet_pton(address_family, ip_string)
61 return True # if no error, return directly
63 continue # Otherwise, check next family
67 def _is_valid_ipv4(ip_string):
68 """Check ip string is valid ipv4 address"""
69 return _is_valid_ip(ip_string, address_families=[socket.AF_INET])
72 def _is_valid_ipv6(ip_string):
73 """Check ip string is valid ipv6 address"""
74 return _is_valid_ip(ip_string, address_families=[socket.AF_INET6])
78 samdb, name, dns_conn, change_owner_sd,
79 server, ip_address_list, logger):
80 """Add DNS A or AAAA records while creating computer. """
81 name = name.rstrip('$')
82 client_version = dnsserver.DNS_CLIENT_VERSION_LONGHORN
83 select_flags = dnsserver.DNS_RPC_VIEW_AUTHORITY_DATA | dnsserver.DNS_RPC_VIEW_NO_CHILDREN
84 zone = samdb.domain_dns_name()
86 sd_helper = sd_utils.SDUtils(samdb)
89 buflen, res = dns_conn.DnssrvEnumRecords2(
101 except WERRORError as e:
102 if e.args[0] == werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
108 for record in rec.records:
109 if record.wType == dnsp.DNS_TYPE_A or record.wType == dnsp.DNS_TYPE_AAAA:
111 del_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
112 del_rec_buf.rec = record
114 dns_conn.DnssrvUpdateRecord2(
123 except WERRORError as e:
124 if e.args[0] != werror.WERR_DNS_ERROR_NAME_DOES_NOT_EXIST:
127 for ip_address in ip_address_list:
128 if _is_valid_ipv6(ip_address):
129 logger.info("Adding DNS AAAA record %s.%s for IPv6 IP: %s" % (
130 name, zone, ip_address))
131 rec = AAAARecord(ip_address)
132 elif _is_valid_ipv4(ip_address):
133 logger.info("Adding DNS A record %s.%s for IPv4 IP: %s" % (
134 name, zone, ip_address))
135 rec = ARecord(ip_address)
137 raise ValueError('Invalid IP: {}'.format(ip_address))
140 add_rec_buf = dnsserver.DNS_RPC_RECORD_BUF()
141 add_rec_buf.rec = rec
143 dns_conn.DnssrvUpdateRecord2(
153 if (len(ip_address_list) > 0):
154 domaindns_zone_dn = ldb.Dn(
156 'DC=DomainDnsZones,%s' % samdb.get_default_basedn(),
159 dns_a_dn, ldap_record = samdb.dns_lookup(
160 "%s.%s" % (name, zone),
161 dns_partition=domaindns_zone_dn,
164 # Make the DC own the DNS record, not the administrator
165 sd_helper.modify_sd_on_dn(
168 controls=["sd_flags:1:%d" % (security.SECINFO_OWNER | security.SECINFO_GROUP)],
172 class cmd_computer_create(Command):
173 """Create a new computer.
175 This command creates a new computer account in the Active Directory domain.
176 The computername specified on the command is the sAMaccountName without the
177 trailing $ (dollar sign).
179 User accounts may represent physical entities, such as workstations. Computer
180 accounts are also referred to as security principals and are assigned a
181 security identifier (SID).
184 samba-tool computer create Computer1 -H ldap://samba.samdom.example.com \
185 -Uadministrator%passw1rd
187 Example1 shows how to create a new computer in the domain against a remote LDAP
188 server. The -H parameter is used to specify the remote target server. The -U
189 option is used to pass the userid and password authorized to issue the command
193 sudo samba-tool computer create Computer2
195 Example2 shows how to create a new computer in the domain against the local
196 server. sudo is used so a user may run the command as root.
199 samba-tool computer create Computer3 --computerou='OU=OrgUnit'
201 Example3 shows how to create a new computer in the OrgUnit organizational unit.
204 synopsis = "%prog <computername> [options]"
207 Option("-H", "--URL", help="LDB URL for database or target server",
208 type=str, metavar="URL", dest="H"),
209 Option("--computerou",
210 help=("DN of alternative location (with or without domainDN "
211 "counterpart) to default CN=Computers in which new "
212 "computer object will be created. E.g. 'OU=<OU name>'"),
214 Option("--description", help="Computers's description", type=str),
215 Option("--prepare-oldjoin",
216 help="Prepare enabled machine account for oldjoin mechanism",
217 action="store_true"),
218 Option("--ip-address",
219 dest='ip_address_list',
220 help=("IPv4 address for the computer's A record, or IPv6 "
221 "address for AAAA record, can be provided multiple "
224 Option("--service-principal-name",
225 dest='service_principal_name_list',
226 help=("Computer's Service Principal Name, can be provided "
231 takes_args = ["computername"]
233 takes_optiongroups = {
234 "sambaopts": options.SambaOptions,
235 "credopts": options.CredentialsOptions,
236 "versionopts": options.VersionOptions,
239 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
240 H=None, computerou=None, description=None, prepare_oldjoin=False,
241 ip_address_list=None, service_principal_name_list=None):
243 if ip_address_list is None:
246 if service_principal_name_list is None:
247 service_principal_name_list = []
249 # check each IP address if provided
250 for ip_address in ip_address_list:
251 if not _is_valid_ip(ip_address):
252 raise CommandError('Invalid IP address {}'.format(ip_address))
254 lp = sambaopts.get_loadparm()
255 creds = credopts.get_credentials(lp)
258 samdb = SamDB(url=H, session_info=system_session(),
259 credentials=creds, lp=lp)
260 samdb.newcomputer(computername, computerou=computerou,
261 description=description,
262 prepare_oldjoin=prepare_oldjoin,
263 ip_address_list=ip_address_list,
264 service_principal_name_list=service_principal_name_list,
268 # if ip_address_list provided, then we need to create DNS
269 # records for this computer.
271 hostname = re.sub(r"\$$", "", computername)
272 if hostname.count('$'):
273 raise CommandError('Illegal computername "%s"' % computername)
275 filters = '(&(sAMAccountName={}$)(objectclass=computer))'.format(
276 ldb.binary_encode(hostname))
279 base=samdb.domain_dn(),
280 scope=ldb.SCOPE_SUBTREE,
282 attrs=['primaryGroupID', 'objectSid'])
284 group = recs[0]['primaryGroupID'][0]
285 owner = ndr_unpack(security.dom_sid, recs[0]["objectSid"][0])
287 dns_conn = dnsserver.dnsserver(
288 "ncacn_ip_tcp:{}[sign]".format(samdb.host_dns_name()),
291 change_owner_sd = security.descriptor()
292 change_owner_sd.owner_sid = owner
293 change_owner_sd.group_sid = security.dom_sid(
294 "{}-{}".format(samdb.get_domain_sid(), group),
298 samdb, hostname, dns_conn,
299 change_owner_sd, samdb.host_dns_name(),
300 ip_address_list, self.get_logger())
301 except Exception as e:
302 raise CommandError("Failed to create computer '%s': " %
305 self.outf.write("Computer '%s' created successfully\n" % computername)
308 class cmd_computer_delete(Command):
309 """Delete a computer.
311 This command deletes a computer account from the Active Directory domain. The
312 computername specified on the command is the sAMAccountName without the
313 trailing $ (dollar sign).
315 Once the account is deleted, all permissions and memberships associated with
316 that account are deleted. If a new computer account is added with the same name
317 as a previously deleted account name, the new computer does not have the
318 previous permissions. The new account computer will be assigned a new security
319 identifier (SID) and permissions and memberships will have to be added.
321 The command may be run from the root userid or another authorized
322 userid. The -H or --URL= option can be used to execute the command against
326 samba-tool computer delete Computer1 -H ldap://samba.samdom.example.com \
327 -Uadministrator%passw1rd
329 Example1 shows how to delete a computer in the domain against a remote LDAP
330 server. The -H parameter is used to specify the remote target server. The
331 --computername= and --password= options are used to pass the computername and
332 password of a computer that exists on the remote server and is authorized to
333 issue the command on that server.
336 sudo samba-tool computer delete Computer2
338 Example2 shows how to delete a computer in the domain against the local server.
339 sudo is used so a computer may run the command as root.
342 synopsis = "%prog <computername> [options]"
345 Option("-H", "--URL", help="LDB URL for database or target server",
346 type=str, metavar="URL", dest="H"),
349 takes_args = ["computername"]
350 takes_optiongroups = {
351 "sambaopts": options.SambaOptions,
352 "credopts": options.CredentialsOptions,
353 "versionopts": options.VersionOptions,
356 def run(self, computername, credopts=None, sambaopts=None,
357 versionopts=None, H=None):
358 lp = sambaopts.get_loadparm()
359 creds = credopts.get_credentials(lp, fallback_machine=True)
361 samdb = SamDB(url=H, session_info=system_session(),
362 credentials=creds, lp=lp)
364 samaccountname = computername
365 if not computername.endswith('$'):
366 samaccountname = "%s$" % computername
368 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
369 (ldb.binary_encode(samaccountname),
370 dsdb.ATYPE_WORKSTATION_TRUST))
372 res = samdb.search(base=samdb.domain_dn(),
373 scope=ldb.SCOPE_SUBTREE,
375 attrs=["userAccountControl", "dNSHostName"])
376 computer_dn = res[0].dn
377 computer_ac = int(res[0]["userAccountControl"][0])
378 if "dNSHostName" in res[0]:
379 computer_dns_host_name = res[0]["dNSHostName"][0]
381 computer_dns_host_name = None
383 raise CommandError('Unable to find computer "%s"' % computername)
385 computer_is_workstation = (
386 computer_ac & dsdb.UF_WORKSTATION_TRUST_ACCOUNT)
387 if computer_is_workstation == False:
388 raise CommandError('Failed to remove computer "%s": '
389 'Computer is not a workstation - removal denied'
392 samdb.delete(computer_dn)
393 if computer_dns_host_name:
394 remove_dns_references(
395 samdb, self.get_logger(), computer_dns_host_name,
397 except Exception as e:
398 raise CommandError('Failed to remove computer "%s"' %
400 self.outf.write("Deleted computer %s\n" % computername)
403 class cmd_computer_list(Command):
404 """List all computers."""
406 synopsis = "%prog [options]"
409 Option("-H", "--URL", help="LDB URL for database or target server",
410 type=str, metavar="URL", dest="H"),
413 takes_optiongroups = {
414 "sambaopts": options.SambaOptions,
415 "credopts": options.CredentialsOptions,
416 "versionopts": options.VersionOptions,
419 def run(self, sambaopts=None, credopts=None, versionopts=None, H=None):
420 lp = sambaopts.get_loadparm()
421 creds = credopts.get_credentials(lp, fallback_machine=True)
423 samdb = SamDB(url=H, session_info=system_session(),
424 credentials=creds, lp=lp)
426 filter = "(sAMAccountType=%u)" % (dsdb.ATYPE_WORKSTATION_TRUST)
428 domain_dn = samdb.domain_dn()
429 res = samdb.search(domain_dn, scope=ldb.SCOPE_SUBTREE,
431 attrs=["samaccountname"])
436 self.outf.write("%s\n" % msg.get("samaccountname", idx=0))
438 class cmd_computer_show(Command):
439 """Display a computer AD object.
441 This command displays a computer account and it's attributes in the Active
443 The computername specified on the command is the sAMAccountName.
445 The command may be run from the root userid or another authorized
448 The -H or --URL= option can be used to execute the command against a remote
452 samba-tool computer show Computer1 -H ldap://samba.samdom.example.com \
455 Example1 shows how display a computers attributes in the domain against a
458 The -H parameter is used to specify the remote target server.
461 samba-tool computer show Computer2
463 Example2 shows how to display a computers attributes in the domain against a
467 samba-tool computer show Computer2 --attributes=objectSid,operatingSystem
469 Example3 shows how to display a computers objectSid and operatingSystem
472 synopsis = "%prog <computername> [options]"
475 Option("-H", "--URL", help="LDB URL for database or target server",
476 type=str, metavar="URL", dest="H"),
477 Option("--attributes",
478 help=("Comma separated list of attributes, "
479 "which will be printed."),
480 type=str, dest="computer_attrs"),
483 takes_args = ["computername"]
484 takes_optiongroups = {
485 "sambaopts": options.SambaOptions,
486 "credopts": options.CredentialsOptions,
487 "versionopts": options.VersionOptions,
490 def run(self, computername, credopts=None, sambaopts=None, versionopts=None,
491 H=None, computer_attrs=None):
493 lp = sambaopts.get_loadparm()
494 creds = credopts.get_credentials(lp, fallback_machine=True)
495 samdb = SamDB(url=H, session_info=system_session(),
496 credentials=creds, lp=lp)
500 attrs = computer_attrs.split(",")
502 samaccountname = computername
503 if not computername.endswith('$'):
504 samaccountname = "%s$" % computername
506 filter = ("(&(sAMAccountType=%d)(sAMAccountName=%s))" %
507 (dsdb.ATYPE_WORKSTATION_TRUST,
508 ldb.binary_encode(samaccountname)))
510 domaindn = samdb.domain_dn()
513 res = samdb.search(base=domaindn, expression=filter,
514 scope=ldb.SCOPE_SUBTREE, attrs=attrs)
515 computer_dn = res[0].dn
517 raise CommandError('Unable to find computer "%s"' %
521 computer_ldif = samdb.write_ldif(msg, ldb.CHANGETYPE_NONE)
522 self.outf.write(computer_ldif)
524 class cmd_computer_move(Command):
525 """Move a computer to an organizational unit/container."""
527 synopsis = "%prog computername <new_ou_dn> [options]"
530 Option("-H", "--URL", help="LDB URL for database or target server",
531 type=str, metavar="URL", dest="H"),
534 takes_args = [ "computername", "new_ou_dn" ]
535 takes_optiongroups = {
536 "sambaopts": options.SambaOptions,
537 "credopts": options.CredentialsOptions,
538 "versionopts": options.VersionOptions,
541 def run(self, computername, new_ou_dn, credopts=None, sambaopts=None,
542 versionopts=None, H=None):
543 lp = sambaopts.get_loadparm()
544 creds = credopts.get_credentials(lp, fallback_machine=True)
545 samdb = SamDB(url=H, session_info=system_session(),
546 credentials=creds, lp=lp)
547 domain_dn = ldb.Dn(samdb, samdb.domain_dn())
549 samaccountname = computername
550 if not computername.endswith('$'):
551 samaccountname = "%s$" % computername
553 filter = ("(&(sAMAccountName=%s)(sAMAccountType=%u))" %
554 (ldb.binary_encode(samaccountname),
555 dsdb.ATYPE_WORKSTATION_TRUST))
557 res = samdb.search(base=domain_dn,
559 scope=ldb.SCOPE_SUBTREE)
560 computer_dn = res[0].dn
562 raise CommandError('Unable to find computer "%s"' % (computername))
564 full_new_ou_dn = ldb.Dn(samdb, new_ou_dn)
565 if not full_new_ou_dn.is_child_of(domain_dn):
566 full_new_ou_dn.add_base(domain_dn)
567 new_computer_dn = ldb.Dn(samdb, str(computer_dn))
568 new_computer_dn.remove_base_components(len(computer_dn)-1)
569 new_computer_dn.add_base(full_new_ou_dn)
571 samdb.rename(computer_dn, new_computer_dn)
572 except Exception as e:
573 raise CommandError('Failed to move computer "%s"' % computername, e)
574 self.outf.write('Moved computer "%s" to "%s"\n' %
575 (computername, new_ou_dn))
578 class cmd_computer(SuperCommand):
579 """Computer management."""
582 subcommands["create"] = cmd_computer_create()
583 subcommands["delete"] = cmd_computer_delete()
584 subcommands["list"] = cmd_computer_list()
585 subcommands["show"] = cmd_computer_show()
586 subcommands["move"] = cmd_computer_move()