samba-tool: fix replication after dns partition fsmo role transfer
[metze/samba/wip.git] / python / samba / netcmd / fsmo.py
1 # Changes a FSMO role owner
2 #
3 # Copyright Nadezhda Ivanova 2009
4 # Copyright Jelmer Vernooij 2009
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 3 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14 # GNU General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
18 #
19
20 import samba
21 import samba.getopt as options
22 import ldb
23 from ldb import LdbError
24 from samba.dcerpc import drsuapi, misc
25 from samba.auth import system_session
26 from samba.netcmd import (
27     Command,
28     CommandError,
29     SuperCommand,
30     Option,
31 )
32 from samba.samdb import SamDB
33
34
35 def get_fsmo_roleowner(samdb, roledn, role):
36     """Gets the owner of an FSMO role
37
38     :param roledn: The DN of the FSMO role
39     :param role: The FSMO role
40     """
41     try:
42         res = samdb.search(roledn,
43                            scope=ldb.SCOPE_BASE, attrs=["fSMORoleOwner"])
44     except LdbError as e7:
45         (num, msg) = e7.args
46         if num == ldb.ERR_NO_SUCH_OBJECT:
47             raise CommandError("The '%s' role is not present in this domain" % role)
48         raise
49
50     if 'fSMORoleOwner' in res[0]:
51         master_owner = (ldb.Dn(samdb, res[0]["fSMORoleOwner"][0].decode('utf8')))
52     else:
53         master_owner = None
54
55     return master_owner
56
57
58 def transfer_dns_role(outf, sambaopts, credopts, role, samdb):
59     """Transfer dns FSMO role. """
60
61     if role == "domaindns":
62         domain_dn = samdb.domain_dn()
63         role_object = "CN=Infrastructure,DC=DomainDnsZones," + domain_dn
64     elif role == "forestdns":
65         forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
66         role_object = "CN=Infrastructure,DC=ForestDnsZones," + forest_dn
67
68     new_host_dns_name = samdb.host_dns_name()
69
70     res = samdb.search(role_object,
71                        attrs=["fSMORoleOwner"],
72                        scope=ldb.SCOPE_BASE,
73                        controls=["extended_dn:1:1"])
74
75     if 'fSMORoleOwner' in res[0]:
76         try:
77             master_guid = str(misc.GUID(ldb.Dn(samdb,
78                                                res[0]['fSMORoleOwner'][0].decode('utf8'))
79                                         .get_extended_component('GUID')))
80             master_owner = str(ldb.Dn(samdb, res[0]['fSMORoleOwner'][0].decode('utf8')))
81         except LdbError as e3:
82             (num, msg) = e3.args
83             raise CommandError("No GUID found in naming master DN %s : %s \n" %
84                                (res[0]['fSMORoleOwner'][0], msg))
85     else:
86         outf.write("* The '%s' role does not have an FSMO roleowner\n" % role)
87         return False
88
89     if role == "domaindns":
90         master_dns_name = '%s._msdcs.%s' % (master_guid,
91                                             samdb.domain_dns_name())
92         new_dns_name = '%s._msdcs.%s' % (samdb.get_ntds_GUID(),
93                                          samdb.domain_dns_name())
94     elif role == "forestdns":
95         master_dns_name = '%s._msdcs.%s' % (master_guid,
96                                             samdb.forest_dns_name())
97         new_dns_name = '%s._msdcs.%s' % (samdb.get_ntds_GUID(),
98                                          samdb.forest_dns_name())
99
100     new_owner = samdb.get_dsServiceName()
101
102     if master_dns_name != new_dns_name:
103         lp = sambaopts.get_loadparm()
104         creds = credopts.get_credentials(lp, fallback_machine=True)
105         samdb = SamDB(url="ldap://%s" % (master_dns_name),
106                       session_info=system_session(),
107                       credentials=creds, lp=lp)
108
109         m = ldb.Message()
110         m.dn = ldb.Dn(samdb, role_object)
111         m["fSMORoleOwner"] = ldb.MessageElement(master_owner,
112                                                 ldb.FLAG_MOD_DELETE,
113                                                 "fSMORoleOwner")
114
115         try:
116             samdb.modify(m)
117         except LdbError as e4:
118             (num, msg) = e4.args
119             raise CommandError("Failed to delete role '%s': %s" %
120                                (role, msg))
121
122         m = ldb.Message()
123         m.dn = ldb.Dn(samdb, role_object)
124         m["fSMORoleOwner"] = ldb.MessageElement(new_owner,
125                                                 ldb.FLAG_MOD_ADD,
126                                                 "fSMORoleOwner")
127         try:
128             samdb.modify(m)
129         except LdbError as e5:
130             (num, msg) = e5.args
131             raise CommandError("Failed to add role '%s': %s" % (role, msg))
132
133         try:
134             connection = samba.drs_utils.drsuapi_connect(new_host_dns_name,
135                                                          lp, creds)
136         except samba.drs_utils.drsException as e:
137             raise CommandError("Drsuapi Connect failed", e)
138
139         try:
140             drsuapi_connection = connection[0]
141             drsuapi_handle = connection[1]
142             req_options = drsuapi.DRSUAPI_DRS_WRIT_REP
143             NC = role_object[18:]
144             samba.drs_utils.sendDsReplicaSync(drsuapi_connection,
145                                               drsuapi_handle,
146                                               master_guid,
147                                               NC, req_options)
148         except samba.drs_utils.drsException as estr:
149             raise CommandError("Replication failed", estr)
150
151         outf.write("FSMO transfer of '%s' role successful\n" % role)
152         return True
153     else:
154         outf.write("This DC already has the '%s' FSMO role\n" % role)
155         return False
156
157
158 def transfer_role(outf, role, samdb):
159     """Transfer standard FSMO role. """
160
161     domain_dn = samdb.domain_dn()
162     rid_dn = "CN=RID Manager$,CN=System," + domain_dn
163     naming_dn = "CN=Partitions,%s" % samdb.get_config_basedn()
164     infrastructure_dn = "CN=Infrastructure," + domain_dn
165     schema_dn = str(samdb.get_schema_basedn())
166     new_owner = ldb.Dn(samdb, samdb.get_dsServiceName())
167     m = ldb.Message()
168     m.dn = ldb.Dn(samdb, "")
169     if role == "rid":
170         master_owner = get_fsmo_roleowner(samdb, rid_dn, role)
171         m["becomeRidMaster"] = ldb.MessageElement(
172             "1", ldb.FLAG_MOD_REPLACE,
173             "becomeRidMaster")
174     elif role == "pdc":
175         master_owner = get_fsmo_roleowner(samdb, domain_dn, role)
176
177         res = samdb.search(domain_dn,
178                            scope=ldb.SCOPE_BASE, attrs=["objectSid"])
179         assert len(res) == 1
180         sid = res[0]["objectSid"][0]
181         m["becomePdc"] = ldb.MessageElement(
182             sid, ldb.FLAG_MOD_REPLACE,
183             "becomePdc")
184     elif role == "naming":
185         master_owner = get_fsmo_roleowner(samdb, naming_dn, role)
186         m["becomeDomainMaster"] = ldb.MessageElement(
187             "1", ldb.FLAG_MOD_REPLACE,
188             "becomeDomainMaster")
189     elif role == "infrastructure":
190         master_owner = get_fsmo_roleowner(samdb, infrastructure_dn, role)
191         m["becomeInfrastructureMaster"] = ldb.MessageElement(
192             "1", ldb.FLAG_MOD_REPLACE,
193             "becomeInfrastructureMaster")
194     elif role == "schema":
195         master_owner = get_fsmo_roleowner(samdb, schema_dn, role)
196         m["becomeSchemaMaster"] = ldb.MessageElement(
197             "1", ldb.FLAG_MOD_REPLACE,
198             "becomeSchemaMaster")
199     else:
200         raise CommandError("Invalid FSMO role.")
201
202     if master_owner is None:
203         outf.write("Cannot transfer, no DC assigned to the %s role.  Try 'seize' instead\n" % role)
204         return False
205
206     if master_owner != new_owner:
207         try:
208             samdb.modify(m)
209         except LdbError as e6:
210             (num, msg) = e6.args
211             raise CommandError("Transfer of '%s' role failed: %s" %
212                                (role, msg))
213
214         outf.write("FSMO transfer of '%s' role successful\n" % role)
215         return True
216     else:
217         outf.write("This DC already has the '%s' FSMO role\n" % role)
218         return False
219
220
221 class cmd_fsmo_seize(Command):
222     """Seize the role."""
223
224     synopsis = "%prog [options]"
225
226     takes_optiongroups = {
227         "sambaopts": options.SambaOptions,
228         "credopts": options.CredentialsOptions,
229         "versionopts": options.VersionOptions,
230     }
231
232     takes_options = [
233         Option("-H", "--URL", help="LDB URL for database or target server",
234                type=str, metavar="URL", dest="H"),
235         Option("--force",
236                help="Force seizing of role without attempting to transfer.",
237                action="store_true"),
238         Option("--role", type="choice", choices=["rid", "pdc", "infrastructure",
239                                                  "schema", "naming", "domaindns", "forestdns", "all"],
240                help="""The FSMO role to seize or transfer.\n
241 rid=RidAllocationMasterRole\n
242 schema=SchemaMasterRole\n
243 pdc=PdcEmulationMasterRole\n
244 naming=DomainNamingMasterRole\n
245 infrastructure=InfrastructureMasterRole\n
246 domaindns=DomainDnsZonesMasterRole\n
247 forestdns=ForestDnsZonesMasterRole\n
248 all=all of the above\n
249 You must provide an Admin user and password."""),
250     ]
251
252     takes_args = []
253
254     def seize_role(self, role, samdb, force):
255         """Seize standard fsmo role. """
256
257         serviceName = samdb.get_dsServiceName()
258         domain_dn = samdb.domain_dn()
259         self.infrastructure_dn = "CN=Infrastructure," + domain_dn
260         self.naming_dn = "CN=Partitions,%s" % samdb.get_config_basedn()
261         self.schema_dn = str(samdb.get_schema_basedn())
262         self.rid_dn = "CN=RID Manager$,CN=System," + domain_dn
263
264         m = ldb.Message()
265         if role == "rid":
266             m.dn = ldb.Dn(samdb, self.rid_dn)
267         elif role == "pdc":
268             m.dn = ldb.Dn(samdb, domain_dn)
269         elif role == "naming":
270             m.dn = ldb.Dn(samdb, self.naming_dn)
271         elif role == "infrastructure":
272             m.dn = ldb.Dn(samdb, self.infrastructure_dn)
273         elif role == "schema":
274             m.dn = ldb.Dn(samdb, self.schema_dn)
275         else:
276             raise CommandError("Invalid FSMO role.")
277         # first try to transfer to avoid problem if the owner is still active
278         seize = False
279         master_owner = get_fsmo_roleowner(samdb, m.dn, role)
280         # if there is a different owner
281         if master_owner is not None:
282             # if there is a different owner
283             if master_owner != serviceName:
284                 # if --force isn't given, attempt transfer
285                 if force is None:
286                     self.message("Attempting transfer...")
287                     try:
288                         transfer_role(self.outf, role, samdb)
289                     except:
290                         # transfer failed, use the big axe...
291                         seize = True
292                         self.message("Transfer unsuccessful, seizing...")
293                     else:
294                         self.message("Transfer successful, not seizing role")
295                         return True
296             else:
297                 self.outf.write("This DC already has the '%s' FSMO role\n" %
298                                 role)
299                 return False
300         else:
301             seize = True
302
303         if force is not None or seize:
304             self.message("Seizing %s FSMO role..." % role)
305             m["fSMORoleOwner"] = ldb.MessageElement(
306                 serviceName, ldb.FLAG_MOD_REPLACE,
307                 "fSMORoleOwner")
308
309             samdb.transaction_start()
310             try:
311                 samdb.modify(m)
312                 if role == "rid":
313                     # We may need to allocate the initial RID Set
314                     samdb.create_own_rid_set()
315
316             except LdbError as e1:
317                 (num, msg) = e1.args
318                 if role == "rid" and num == ldb.ERR_ENTRY_ALREADY_EXISTS:
319
320                     # Try again without the RID Set allocation
321                     # (normal).  We have to manage the transaction as
322                     # we do not have nested transactions and creating
323                     # a RID set touches multiple objects. :-(
324                     samdb.transaction_cancel()
325                     samdb.transaction_start()
326                     try:
327                         samdb.modify(m)
328                     except LdbError as e:
329                         (num, msg) = e.args
330                         samdb.transaction_cancel()
331                         raise CommandError("Failed to seize '%s' role: %s" %
332                                            (role, msg))
333
334                 else:
335                     samdb.transaction_cancel()
336                     raise CommandError("Failed to seize '%s' role: %s" %
337                                        (role, msg))
338             samdb.transaction_commit()
339             self.outf.write("FSMO seize of '%s' role successful\n" % role)
340
341             return True
342
343     def seize_dns_role(self, role, samdb, credopts, sambaopts,
344                        versionopts, force):
345         """Seize DNS FSMO role. """
346
347         serviceName = samdb.get_dsServiceName()
348         domain_dn = samdb.domain_dn()
349         forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
350         self.domaindns_dn = "CN=Infrastructure,DC=DomainDnsZones," + domain_dn
351         self.forestdns_dn = "CN=Infrastructure,DC=ForestDnsZones," + forest_dn
352
353         m = ldb.Message()
354         if role == "domaindns":
355             m.dn = ldb.Dn(samdb, self.domaindns_dn)
356         elif role == "forestdns":
357             m.dn = ldb.Dn(samdb, self.forestdns_dn)
358         else:
359             raise CommandError("Invalid FSMO role.")
360         # first try to transfer to avoid problem if the owner is still active
361         seize = False
362         master_owner = get_fsmo_roleowner(samdb, m.dn, role)
363         if master_owner is not None:
364             # if there is a different owner
365             if master_owner != serviceName:
366                 # if --force isn't given, attempt transfer
367                 if force is None:
368                     self.message("Attempting transfer...")
369                     try:
370                         transfer_dns_role(self.outf, sambaopts, credopts, role,
371                                           samdb)
372                     except:
373                         # transfer failed, use the big axe...
374                         seize = True
375                         self.message("Transfer unsuccessful, seizing...")
376                     else:
377                         self.message("Transfer successful, not seizing role\n")
378                         return True
379             else:
380                 self.outf.write("This DC already has the '%s' FSMO role\n" %
381                                 role)
382                 return False
383         else:
384             seize = True
385
386         if force is not None or seize:
387             self.message("Seizing %s FSMO role..." % role)
388             m["fSMORoleOwner"] = ldb.MessageElement(
389                 serviceName, ldb.FLAG_MOD_REPLACE,
390                 "fSMORoleOwner")
391             try:
392                 samdb.modify(m)
393             except LdbError as e2:
394                 (num, msg) = e2.args
395                 raise CommandError("Failed to seize '%s' role: %s" %
396                                    (role, msg))
397             self.outf.write("FSMO seize of '%s' role successful\n" % role)
398             return True
399
400     def run(self, force=None, H=None, role=None,
401             credopts=None, sambaopts=None, versionopts=None):
402
403         lp = sambaopts.get_loadparm()
404         creds = credopts.get_credentials(lp, fallback_machine=True)
405
406         samdb = SamDB(url=H, session_info=system_session(),
407                       credentials=creds, lp=lp)
408
409         if role == "all":
410             self.seize_role("rid", samdb, force)
411             self.seize_role("pdc", samdb, force)
412             self.seize_role("naming", samdb, force)
413             self.seize_role("infrastructure", samdb, force)
414             self.seize_role("schema", samdb, force)
415             self.seize_dns_role("domaindns", samdb, credopts, sambaopts,
416                                 versionopts, force)
417             self.seize_dns_role("forestdns", samdb, credopts, sambaopts,
418                                 versionopts, force)
419         else:
420             if role == "domaindns" or role == "forestdns":
421                 self.seize_dns_role(role, samdb, credopts, sambaopts,
422                                     versionopts, force)
423             else:
424                 self.seize_role(role, samdb, force)
425
426
427 class cmd_fsmo_show(Command):
428     """Show the roles."""
429
430     synopsis = "%prog [options]"
431
432     takes_optiongroups = {
433         "sambaopts": options.SambaOptions,
434         "credopts": options.CredentialsOptions,
435         "versionopts": options.VersionOptions,
436     }
437
438     takes_options = [
439         Option("-H", "--URL", help="LDB URL for database or target server",
440                type=str, metavar="URL", dest="H"),
441     ]
442
443     takes_args = []
444
445     def run(self, H=None, credopts=None, sambaopts=None, versionopts=None):
446         lp = sambaopts.get_loadparm()
447         creds = credopts.get_credentials(lp, fallback_machine=True)
448
449         samdb = SamDB(url=H, session_info=system_session(),
450                       credentials=creds, lp=lp)
451
452         domain_dn = samdb.domain_dn()
453         forest_dn = samba.dn_from_dns_name(samdb.forest_dns_name())
454         infrastructure_dn = "CN=Infrastructure," + domain_dn
455         naming_dn = "CN=Partitions,%s" % samdb.get_config_basedn()
456         schema_dn = samdb.get_schema_basedn()
457         rid_dn = "CN=RID Manager$,CN=System," + domain_dn
458         domaindns_dn = "CN=Infrastructure,DC=DomainDnsZones," + domain_dn
459         forestdns_dn = "CN=Infrastructure,DC=ForestDnsZones," + forest_dn
460
461         masters = [(schema_dn, "schema", "SchemaMasterRole"),
462                    (infrastructure_dn, "infrastructure", "InfrastructureMasterRole"),
463                    (rid_dn, "rid", "RidAllocationMasterRole"),
464                    (domain_dn, "pdc", "PdcEmulationMasterRole"),
465                    (naming_dn, "naming", "DomainNamingMasterRole"),
466                    (domaindns_dn, "domaindns", "DomainDnsZonesMasterRole"),
467                    (forestdns_dn, "forestdns", "ForestDnsZonesMasterRole"),
468                    ]
469
470         for master in masters:
471             (dn, short_name, long_name) = master
472             try:
473                 master = get_fsmo_roleowner(samdb, dn, short_name)
474                 if master is not None:
475                     self.message("%s owner: %s" % (long_name, str(master)))
476                 else:
477                     self.message("%s has no current owner" % (long_name))
478             except CommandError as e:
479                 self.message("%s: * %s" % (long_name, e.message))
480
481
482 class cmd_fsmo_transfer(Command):
483     """Transfer the role."""
484
485     synopsis = "%prog [options]"
486
487     takes_optiongroups = {
488         "sambaopts": options.SambaOptions,
489         "credopts": options.CredentialsOptions,
490         "versionopts": options.VersionOptions,
491     }
492
493     takes_options = [
494         Option("-H", "--URL", help="LDB URL for database or target server",
495                type=str, metavar="URL", dest="H"),
496         Option("--role", type="choice", choices=["rid", "pdc", "infrastructure",
497                                                  "schema", "naming", "domaindns", "forestdns", "all"],
498                help="""The FSMO role to seize or transfer.\n
499 rid=RidAllocationMasterRole\n
500 schema=SchemaMasterRole\n
501 pdc=PdcEmulationMasterRole\n
502 naming=DomainNamingMasterRole\n
503 infrastructure=InfrastructureMasterRole\n
504 domaindns=DomainDnsZonesMasterRole\n
505 forestdns=ForestDnsZonesMasterRole\n
506 all=all of the above\n
507 You must provide an Admin user and password."""),
508     ]
509
510     takes_args = []
511
512     def run(self, force=None, H=None, role=None,
513             credopts=None, sambaopts=None, versionopts=None):
514
515         lp = sambaopts.get_loadparm()
516         creds = credopts.get_credentials(lp, fallback_machine=True)
517
518         samdb = SamDB(url=H, session_info=system_session(),
519                       credentials=creds, lp=lp)
520
521         if role == "all":
522             transfer_role(self.outf, "rid", samdb)
523             transfer_role(self.outf, "pdc", samdb)
524             transfer_role(self.outf, "naming", samdb)
525             transfer_role(self.outf, "infrastructure", samdb)
526             transfer_role(self.outf, "schema", samdb)
527             transfer_dns_role(self.outf, sambaopts, credopts,
528                               "domaindns", samdb)
529             transfer_dns_role(self.outf, sambaopts, credopts, "forestdns",
530                               samdb)
531         else:
532             if role == "domaindns" or role == "forestdns":
533                 transfer_dns_role(self.outf, sambaopts, credopts, role, samdb)
534             else:
535                 transfer_role(self.outf, role, samdb)
536
537
538 class cmd_fsmo(SuperCommand):
539     """Flexible Single Master Operations (FSMO) roles management."""
540
541     subcommands = {}
542     subcommands["seize"] = cmd_fsmo_seize()
543     subcommands["show"] = cmd_fsmo_show()
544     subcommands["transfer"] = cmd_fsmo_transfer()