blob: 1f101b8e2f3ee805961901f8f4ada5683c431aef [file]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package onhost
import (
"strings"
"time"
"chromium.googlesource.com/enterprise/cel/go/common"
"chromium.googlesource.com/enterprise/cel/go/gcp/onhost"
assetpb "chromium.googlesource.com/enterprise/cel/go/schema/asset"
"github.com/pkg/errors"
)
type AdDomainResolver struct{}
func (*AdDomainResolver) ResolveOnHost(ctx common.Context, ad *assetpb.ActiveDirectoryDomain) error {
d, ok := ctx.(*deployer)
if !ok {
return errors.New("ctx is not Deployer")
}
if ad.DomainController[0].WindowsMachine == d.instanceName {
return setupADDomain(d, ad)
} else {
return nil
}
}
func setupADDomain(d *deployer, ad *assetpb.ActiveDirectoryDomain) error {
d.Logf("Configuring Ad Domain: %+v\n", ad.Name)
// Check status from Runtime Config (for DM) or instance metadata (for IM)
configVar := onhost.GetActiveDirectoryRuntimeConfigVariableName(ad.Name)
metadataKey := onhost.GetADDomainStatusMetadataKey(ad.Name)
// Check Runtime Config first (DM), fall back to instance metadata (IM)
status := d.getRuntimeConfigVariableValue(configVar)
if status == "" {
status = d.getInstanceMetadataFromInstance(d.instanceName, metadataKey)
}
d.Logf("Status of AD domain %s is [%s]", ad.Name, status)
if status == statusReady {
d.Logf("AD Domain already configured")
return nil
}
if status == statusError {
return errors.New("AD Domain configuration failed")
}
// Write to both Runtime Config (for DM watcher) and instance metadata (for IM + client coordination)
d.setRuntimeConfigVariable(configVar, statusInProgress)
d.setInstanceMetadata(metadataKey, statusInProgress)
// There are 3 cases:
// No ancestor -> The root domain of a new forest
// Ancestor is ParentName -> The child domain of the parent domain
// Ancestor is Forest -> Adding a new tree domain to an existing root domain
var err error
if ad.Ancestor == nil {
err = createRootDomain(d, ad)
} else {
parent, ok := ad.Ancestor.(*assetpb.ActiveDirectoryDomain_ParentName)
if ok {
err = createChildDomain(d, ad, parent.ParentName)
} else {
err = createTreeDomain(d, ad)
}
}
if err == nil {
// Write to both Runtime Config (for DM watcher) and instance metadata (for IM + client coordination)
d.setRuntimeConfigVariable(configVar, statusReady)
d.setInstanceMetadata(metadataKey, statusReady)
} else if err != ErrRebootNeeded && !IsRestarting(d) {
d.setRuntimeConfigVariable(configVar, statusError)
d.setInstanceMetadata(metadataKey, statusError)
}
return err
}
const maxRetriesCreateActiveDirectory = 5
// waitForADDomainReady waits for an AD domain to be ready.
// For Infrastructure Manager deployments, it uses instance metadata directly.
// For Deployment Manager deployments, it uses Runtime Config.
func waitForADDomainReady(d *deployer, adName string, dcInstanceName string, timeout time.Duration) error {
// For Infrastructure Manager, use instance metadata directly (no Runtime Config exists)
if d.isInfrastructureManager() {
d.Logf("Infrastructure Manager deployment: waiting for AD domain %s via instance metadata", adName)
metadataKey := onhost.GetADDomainStatusMetadataKey(adName)
return d.waitForInstanceMetadata(dcInstanceName, metadataKey, timeout)
}
// For Deployment Manager, use Runtime Config
d.Logf("Deployment Manager deployment: waiting for AD domain %s via Runtime Config", adName)
depVar := onhost.GetActiveDirectoryRuntimeConfigVariableName(adName)
return d.waitForDependency(depVar, timeout)
}
func createRootDomain(d *deployer, ad *assetpb.ActiveDirectoryDomain) error {
fileToRun := ""
addDnsForwardFile := ""
if d.IsWindows2012() || d.IsWindows2016() {
fileToRun = d.GetSupportingFilePath("create_ad_win2012.ps1")
addDnsForwardFile = d.GetSupportingFilePath("add_dns_forwarder_win2012.ps1")
} else if d.IsWindows2008() {
fileToRun = d.GetSupportingFilePath("create_ad_win2008.ps1")
addDnsForwardFile = d.GetSupportingFilePath("add_dns_forwarder_win2008.ps1")
} else {
return errors.New("unsupported windows version")
}
retries := 0
for {
// normal domain
if err := d.RunConfigCommand("powershell.exe", "-File", fileToRun, "-domainName", ad.Name,
"-netbiosName", ad.NetbiosName, "-adminName", "administrator",
"-adminPassword", string(ad.SafeModeAdminPassword.Final)); err == nil {
break
} else if err == ErrTransient && retries <= maxRetriesCreateActiveDirectory {
retries++
d.Logf("Script returned a transient error. Will wait a minute and try again.")
time.Sleep(1 * time.Minute)
continue
} else {
return err
}
}
// create DNS forwarder for tree domains whose root is this domain.
for _, dn := range d.configuration.AssetManifest.GetAdDomain() {
_, ok := dn.Ancestor.(*assetpb.ActiveDirectoryDomain_Forest)
if !ok {
continue
}
rootAd, err := getRootDomain(d, dn)
if err != nil || rootAd != ad {
continue
}
// create DNS forwarder
if err := d.RunConfigCommand(
"powershell.exe", "-File", addDnsForwardFile,
"-name", dn.Name,
"-masterServer", dn.DomainController[0].WindowsMachine); err != nil {
return err
}
}
// TODO(feiling): create DNS forwarder if there are tree domains underneath it.
d.Logf("AD Domain config is finished.")
return nil
}
// Returns the root domain of the forest
func getRootDomain(d *deployer, ad *assetpb.ActiveDirectoryDomain) (*assetpb.ActiveDirectoryDomain, error) {
for {
if ad.Ancestor == nil {
return ad, nil
}
parent, ok := ad.Ancestor.(*assetpb.ActiveDirectoryDomain_ParentName)
if ok {
parentAd, err := d.getAdDomainAsset(parent.ParentName)
if err != nil {
return nil, err
}
ad = parentAd
} else {
forest := ad.Ancestor.(*assetpb.ActiveDirectoryDomain_Forest)
forestAd, err := d.getAdDomainAsset(forest.Forest)
if err != nil {
return nil, err
}
ad = forestAd
}
}
}
func createChildDomain(d *deployer, ad *assetpb.ActiveDirectoryDomain, parentAdName string) error {
parentAd, err := d.getAdDomainAsset(parentAdName)
if err != nil {
return err
}
rootAd, err := getRootDomain(d, ad)
if err != nil {
return err
}
// name check
if !(strings.HasSuffix(ad.Name, parentAd.Name) &&
ad.Name[len(ad.Name)-len(parentAd.Name)-1] == '.') {
return errors.Errorf(
"Parent and child domain names are not valid. Parent %s, child %s",
parentAd.Name, ad.Name)
}
// wait for parent domain to be ready
parentDcInstanceName := parentAd.DomainController[0].WindowsMachine
err = waitForADDomainReady(d, parentAd.Name, parentDcInstanceName, time.Duration(60)*time.Minute)
if err != nil {
return err
}
fileToRun := ""
if d.IsWindows2012() || d.IsWindows2016() {
fileToRun = d.GetSupportingFilePath("create_child_domain_win2012.ps1")
} else if d.IsWindows2008() {
fileToRun = d.GetSupportingFilePath("create_child_domain_win2008.ps1")
} else {
return errors.New("unsupported windows version")
}
for count := 1; count <= 5; count++ {
err = d.RunConfigCommand("powershell.exe", "-File", fileToRun,
"-domainName", ad.Name[0:len(ad.Name)-len(parentAd.Name)-1],
"-netbiosName", ad.NetbiosName,
"-parentDomainName", parentAd.Name,
// use parent domain controller as DNS, though,
// it can be any DC in the forest where DNS is installed.
"-dnsServer", parentAd.DomainController[0].WindowsMachine,
// note that the admin is the admin on the forest root domain, not on the parent domain.
"-adminName", rootAd.Name+"\\administrator",
"-adminPassword", string(rootAd.SafeModeAdminPassword.Final),
"-localAdminPassword", string(ad.SafeModeAdminPassword.Final))
if err == nil || err == ErrRebootNeeded {
// success or reboot needed
return err
}
// retry
d.Logf("Child domain creation failed. This might be caused " +
"by DNS replication not done yet. Retry")
time.Sleep(5 * time.Minute)
}
return err
}
func createTreeDomain(d *deployer, ad *assetpb.ActiveDirectoryDomain) error {
rootAd, err := getRootDomain(d, ad)
if err != nil {
return err
}
// wait for root domain to be ready
rootDcInstanceName := rootAd.DomainController[0].WindowsMachine
err = waitForADDomainReady(d, rootAd.Name, rootDcInstanceName, time.Duration(60)*time.Minute)
if err != nil {
return err
}
fileToRun := ""
if d.IsWindows2012() || d.IsWindows2016() {
fileToRun = d.GetSupportingFilePath("create_tree_domain_win2012.ps1")
} else if d.IsWindows2008() {
fileToRun = d.GetSupportingFilePath("create_tree_domain_win2008.ps1")
} else {
return errors.New("unsupported windows version")
}
if err := d.RunConfigCommand("powershell.exe", "-File", fileToRun,
"-domainName", ad.Name,
"-netbiosName", ad.NetbiosName,
"-rootDomainName", rootAd.Name,
"-dnsServer", rootAd.DomainController[0].WindowsMachine,
"-adminName", rootAd.Name+"\\administrator",
"-adminPassword", string(rootAd.SafeModeAdminPassword.Final),
"-localAdminPassword", string(ad.SafeModeAdminPassword.Final)); err != nil {
return err
}
d.Logf("Tree domain config is finished.")
return nil
}
func init() {
common.RegisterResolverClass(&AdDomainResolver{})
}