Пример #1
0
 def lock(self):
     """
     Trys to write a lock file. Returns True if we get the lock, else return
     False.
     """
     try:
         if not os.path.exists(os.path.dirname(self.LOCK_FILE)):
             os.makedirs(os.path.dirname(self.LOCK_FILE))
         self.log("Attempting acquire mutext "
                  "({0})...".format(self.LOCK_FILE))
         self.lock_fd = open(self.LOCK_FILE, 'w+')
         fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
         self.log(self.format_title("Mutex Acquired"))
         return True
     except IOError, exc_value:
         self.lock_fd = None
         #  IOError: [Errno 11] Resource temporarily unavailable
         if exc_value[0] == 11:
             self.log(
                 "DNS build script attempted to acquire the "
                 "build mutux but another process already has it."
             )
             fail_mail(
                 "An attempt was made to start the DNS build script "
                 "while an instance of the script was already running. "
                 "The attempt was denied.",
                 subject="Concurrent DNS builds attempted.")
             return False
         else:
             raise
Пример #2
0
def main():
    parser = argparse.ArgumentParser(description='DNS Build scripts')
    parser.add_argument('--stage-only', dest='STAGE_ONLY', action='store_true',
                        default=False, help="Just build staging and don't "
                        "copy to prod. named-checkzone will still be run.")
    parser.add_argument('--clobber-stage', dest='CLOBBER_STAGE',
                        action='store_true', default=False, help="If stage "
                        "already exists delete it before running the build "
                        "script.")
    parser.add_argument('--ship-it', dest='PUSH_TO_PROD',
                        action='store_true', default=False, help="Check files "
                        "into rcs and push upstream.")
    parser.add_argument('--preserve-stage', dest='PRESERVE_STAGE',
                        action='store_true', default=False, help="Do not "
                        "remove staging area after build completes.")
    parser.add_argument('--no-build', dest='BUILD_ZONES',
                        action='store_false', default=True, help="Do not "
                        "build zone files.")
    parser.add_argument('--no-syslog', dest='LOG_SYSLOG',
                        action='store_false', default=True, help="Do not "
                        "log to syslog.")
    parser.add_argument('--debug', dest='DEBUG',
                        action='store_true', default=False, help="Print "
                        "copious amounts of text.")
    parser.add_argument('--force', dest='FORCE',
                        action='store_true', default=False, help="Ignore "
                        "all change delta thresholds, clobber stagig area, "
                        "and build even when no tasks are scheduled.")
    parser.add_argument('--status', dest='STATUS',
                        action='store_true', default=False, help="Display "
                        "info about the build configuration and exit.")
    nas = parser.parse_args(sys.argv[1:])
    b = DNSBuilder(**dict(nas._get_kwargs()))

    if nas.STATUS:
        b.status()
        return
    message = "DNS Build Error. Error: '{0}'. The build was unsuccessful."

    def write_stop_update(error):
        if os.path.exists(STOP_UPDATE_FILE):
            return
        with open(STOP_UPDATE_FILE, 'w+') as fd:
            msg = ("This file was placed here because there was an error:\n"
                   "=============== ERROR MESSAGE ======================+\n")
            fd.write(msg)
            fd.write(error)
    try:
        with open(LAST_RUN_FILE, 'w+') as fd:
            fd.write(str(int(time.time())))
        b.build_dns()
    except BuildError as why:
        b.log(why, log_level='LOG_ERR')
        write_stop_update(str(why))
        fail_mail(message.format(why))
    except Exception as err:
        fail_mail(message.format(err))
        b.log(err, log_level='LOG_CRIT')
        error_msg = "{0}\n{1}".format(str(err), traceback.format_exc())
        write_stop_update(error_msg)
 def lock(self):
     """
     Trys to write a lock file. Returns True if we get the lock, else return
     False.
     """
     try:
         if not os.path.exists(os.path.dirname(self.LOCK_FILE)):
             os.makedirs(os.path.dirname(self.LOCK_FILE))
         self.log("Attempting acquire mutext "
                  "({0})...".format(self.LOCK_FILE))
         self.lock_fd = open(self.LOCK_FILE, 'w+')
         fcntl.flock(self.lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
         self.log(self.format_title("Mutex Acquired"))
         return True
     except IOError, exc_value:
         self.lock_fd = None
         #  IOError: [Errno 11] Resource temporarily unavailable
         if exc_value[0] == 11:
             self.log("DNS build script attempted to acquire the "
                      "build mutux but another process already has it.")
             fail_mail(
                 "An attempt was made to start the DNS build script "
                 "while an instance of the script was already running. "
                 "The attempt was denied.",
                 subject="Concurrent DNS builds attempted.")
             return False
         else:
             raise
def build_zone_data(view, root_domain, soa, logf=None):
    """
    This function does the heavy lifting of building a zone. It coordinates
    getting all of the data out of the db into BIND format.

    :param soa: The SOA corresponding to the zone being built.
    :type soa: SOA

    :param root_domain: The root domain of this zone.
    :type root_domain: str

    :returns public_file_path: The path to the zone file in the STAGEING dir
    :type public_file_path: str
    :returns public_data: The data that should be written to public_file_path
    :type public_data: str

    :returns view_zone_file: The path to the zone file in the STAGEING dir
    :type view_zone_file: str
    :param view_data: The data that should be written to view_zone_file
    :type view_data: str
    """
    ztype = "reverse" if root_domain.is_reverse else "forward"
    if soa.has_record_set(view=view, exclude_ns=True) and not root_domain.nameserver_set.filter(views=view).exists():
        msg = (
            "The {0} zone has a records in the {1} view, but there are "
            "no nameservers in that view. A zone file for {1} won't be "
            "built. Use the search string 'zone=:{0} view=:{1}' to find "
            "the troublesome records".format(root_domain, view.name)
        )
        fail_mail(msg, subject="Shitty edge case detected.")
        logf("LOG_WARNING", msg)
        return ""

    domains = soa.domain_set.all().order_by("name")

    # Bulid the mega filter!
    domain_mega_filter = Q(domain=root_domain)
    for domain in domains:
        domain_mega_filter = domain_mega_filter | Q(domain=domain)

    rdomain_mega_filter = Q(reverse_domain=root_domain)
    for reverse_domain in domains:
        rdomain_mega_filter = rdomain_mega_filter | Q(reverse_domain=reverse_domain)

    soa_data = render_soa_only(soa=soa, root_domain=root_domain)
    try:
        if ztype == "forward":
            view_data = render_forward_zone(view, domain_mega_filter)
        else:
            view_data = render_reverse_zone(view, domain_mega_filter, rdomain_mega_filter)
    except View.DoesNotExist:
        view_data = ""

    if view_data:
        view_data = soa_data + view_data

    return view_data
 def stop_update_exists(self):
     """
     Look for a file referenced by `STOP_UPDATE_FILE` and if it exists,
     cancel the build.
     """
     if os.path.exists(self.STOP_UPDATE_FILE):
         msg = ("The STOP_UPDATE_FILE ({0}) exists. Build canceled. \n"
                "Reason for skipped build: \n"
                "{1}".format(self.STOP_UPDATE_FILE,
                             open(self.STOP_UPDATE_FILE).read()))
         fail_mail(msg, subject="DNS builds have stoped")
         self.log(msg)
         return True
     else:
         return False
Пример #6
0
 def stop_update_exists(self):
     """
     Look for a file referenced by `STOP_UPDATE_FILE` and if it exists,
     cancel the build.
     """
     if os.path.exists(self.STOP_UPDATE_FILE):
         msg = ("The STOP_UPDATE_FILE ({0}) exists. Build canceled. \n"
                "Reason for skipped build: \n"
                "{1}".format(self.STOP_UPDATE_FILE,
                             open(self.STOP_UPDATE_FILE).read()))
         fail_mail(msg, subject="DNS builds have stoped")
         self.log(msg)
         return True
     else:
         return False
Пример #7
0
def build_zone_data(view, root_domain, soa, logf=None):
    """
    This function does the heavy lifting of building a zone. It coordinates
    getting all of the data out of the db into BIND format.

    :param soa: The SOA corresponding to the zone being built.
    :type soa: SOA

    :param root_domain: The root domain of this zone.
    :type root_domain: str
    """
    ztype = 'reverse' if root_domain.is_reverse else 'forward'
    if (soa.has_record_set(view=view, exclude_ns=True) and
            not root_domain.nameserver_set.filter(views=view).exists()):
        msg = ("The {0} zone has a records in the {1} view, but there are "
               "no nameservers in that view. Use the search string 'zone=:{0} "
               "view=:{1}' to find the troublesome records".format(
                   root_domain, view.name)
               )
        fail_mail(msg, subject="Shitty edge case detected.")
        logf('LOG_WARNING', msg)

    domains = soa.domain_set.all().order_by('name')

    # Bulid the mega filter!
    domain_mega_filter = Q(domain=root_domain)
    for domain in domains:
        domain_mega_filter = domain_mega_filter | Q(domain=domain)

    rdomain_mega_filter = Q(reverse_domain=root_domain)
    for reverse_domain in domains:
        rdomain_mega_filter = rdomain_mega_filter | Q(
            reverse_domain=reverse_domain)

    soa_data = render_soa_only(soa=soa, root_domain=root_domain)
    try:
        if ztype == "forward":
            view_data = render_forward_zone(view, domain_mega_filter)
        else:
            view_data = render_reverse_zone(view, domain_mega_filter,
                                            rdomain_mega_filter)
    except View.DoesNotExist:
        view_data = ""

    if view_data:
        view_data = soa_data + view_data

    return view_data
def build_zone_data(view, root_domain, soa, logf=None):
    """
    This function does the heavy lifting of building a zone. It coordinates
    getting all of the data out of the db into BIND format.

    :param soa: The SOA corresponding to the zone being built.
    :type soa: SOA

    :param root_domain: The root domain of this zone.
    :type root_domain: str
    """
    ztype = 'reverse' if root_domain.is_reverse else 'forward'
    if (soa.has_record_set(view=view, exclude_ns=True)
            and not root_domain.nameserver_set.filter(views=view).exists()):
        msg = ("The {0} zone has a records in the {1} view, but there are "
               "no nameservers in that view. Use the search string 'zone=:{0} "
               "view=:{1}' to find the troublesome records".format(
                   root_domain, view.name))
        fail_mail(msg, subject="Shitty edge case detected.")
        logf('LOG_WARNING', msg)

    domains = soa.domain_set.all().order_by('name')

    # Bulid the mega filter!
    domain_mega_filter = Q(domain=root_domain)
    for domain in domains:
        domain_mega_filter = domain_mega_filter | Q(domain=domain)

    rdomain_mega_filter = Q(reverse_domain=root_domain)
    for reverse_domain in domains:
        rdomain_mega_filter = rdomain_mega_filter | Q(
            reverse_domain=reverse_domain)

    soa_data = render_soa_only(soa=soa, root_domain=root_domain)
    try:
        if ztype == "forward":
            view_data = render_forward_zone(view, domain_mega_filter)
        else:
            view_data = render_reverse_zone(view, domain_mega_filter,
                                            rdomain_mega_filter)
    except View.DoesNotExist:
        view_data = ""

    if view_data:
        view_data = soa_data + view_data

    return view_data
            scope.delete()
        except IndexError:
            scope.delete()
    if len(dhcp_scopes) > 0 or always_push_svn:
        os.chdir(output_dir)
        os.system('/usr/bin/svn update')
        os.system('/usr/bin/svn add * --force')
        os.system(
            '/usr/bin/svn commit -m "Autogenerated addition from inventory"')
    #os.system('/usr/bin/git push origin master')


def write_stop_update(error):
    if os.path.exists(STOP_UPDATE_FILE):
        return
    with open(STOP_UPDATE_FILE, 'w+') as fd:
        msg = ("This file was placed here because there was an error:\n"
               "=============== ERROR MESSAGE ======================+\n")
        fd.write(msg)
        fd.write(error)


if __name__ == '__main__':
    try:
        main()
    except Exception as err:
        message = "DHCP Build Error. Error: '{0}'. The build was unsuccessful."
        fail_mail(message.format(err))
        error_msg = "{0}\n{1}".format(str(err), traceback.format_exc())
        write_stop_update(error_msg)
            d = DHCPFile(dhcp_scope=dhcp_scope,file_text=output_text)
            d.save()
            scope.delete()
        except IndexError:
            scope.delete()
    if len(dhcp_scopes) > 0 or always_push_svn:
        os.chdir(output_dir)
        os.system('/usr/bin/svn update')
        os.system('/usr/bin/svn add * --force')
        os.system('/usr/bin/svn commit -m "Autogenerated addition from inventory"')
    #os.system('/usr/bin/git push origin master')


def write_stop_update(error):
    if os.path.exists(STOP_UPDATE_FILE):
        return
    with open(STOP_UPDATE_FILE, 'w+') as fd:
        msg = ("This file was placed here because there was an error:\n"
               "=============== ERROR MESSAGE ======================+\n")
        fd.write(msg)
        fd.write(error)

if __name__ == '__main__':
    try:
        main()
    except Exception as err:
        message = "DHCP Build Error. Error: '{0}'. The build was unsuccessful."
        fail_mail(message.format(err))
        error_msg = "{0}\n{1}".format(str(err), traceback.format_exc())
        write_stop_update(error_msg)
def main():
    parser = argparse.ArgumentParser(description='DNS Build scripts')
    parser.add_argument('--stage-only',
                        dest='STAGE_ONLY',
                        action='store_true',
                        default=False,
                        help="Just build staging and don't "
                        "copy to prod. named-checkzone will still be run.")
    parser.add_argument('--clobber-stage',
                        dest='CLOBBER_STAGE',
                        action='store_true',
                        default=False,
                        help="If stage "
                        "already exists delete it before running the build "
                        "script.")
    parser.add_argument('--ship-it',
                        dest='PUSH_TO_PROD',
                        action='store_true',
                        default=False,
                        help="Check files "
                        "into rcs and push upstream.")
    parser.add_argument('--preserve-stage',
                        dest='PRESERVE_STAGE',
                        action='store_true',
                        default=False,
                        help="Do not "
                        "remove staging area after build completes.")
    parser.add_argument('--no-build',
                        dest='BUILD_ZONES',
                        action='store_false',
                        default=True,
                        help="Do not "
                        "build zone files.")
    parser.add_argument('--no-syslog',
                        dest='LOG_SYSLOG',
                        action='store_false',
                        default=True,
                        help="Do not "
                        "log to syslog.")
    parser.add_argument('--debug',
                        dest='DEBUG',
                        action='store_true',
                        default=False,
                        help="Print "
                        "copious amounts of text.")
    parser.add_argument('--force-checkin',
                        dest='FORCE',
                        action='store_true',
                        default=False,
                        help="Ignore "
                        "all change delta thresholds, clobber stagig area, "
                        "and build even when no tasks are scheduled.")
    parser.add_argument('--force-build',
                        dest='FORCE_BUILD',
                        action='store_true',
                        default=False,
                        help="Do a buil "
                        "even if no zones are requesting to be rebuilt.")
    parser.add_argument('--status',
                        dest='STATUS',
                        action='store_true',
                        default=False,
                        help="Display "
                        "info about the build configuration and exit.")
    nas = parser.parse_args(sys.argv[1:])
    b = DNSBuilder(**dict(nas._get_kwargs()))

    if nas.STATUS:
        b.status()
        return
    message = "DNS Build Error. Error: '{0}'. The build was unsuccessful."

    def write_stop_update(error):
        if os.path.exists(STOP_UPDATE_FILE):
            return
        with open(STOP_UPDATE_FILE, 'w+') as fd:
            msg = ("This file was placed here because there was an error:\n"
                   "=============== ERROR MESSAGE ======================+\n")
            fd.write(msg)
            fd.write(error)

    try:
        with open(LAST_RUN_FILE, 'w+') as fd:
            fd.write(str(int(time.time())))
        b.build_dns()
    except BuildError as why:
        b.log(why, log_level='LOG_ERR')
        write_stop_update(str(why))
        fail_mail(message.format(why))
    except Exception as err:
        fail_mail(message.format(err))
        b.log(err, log_level='LOG_CRIT')
        error_msg = "{0}\n{1}".format(str(err), traceback.format_exc())
        write_stop_update(error_msg)
    def build_zone_files(self, soas, soa_pks_to_rebuild, gen_config=True):
        """
        For every soa in soas, build a views that are valid for its zone. If
        gen_config is true, generate a config statement for a view and store it
        in zone_stmts (returned by this function). When an soa's pk is in
        `soa_pks_to_rebuild` calculate a new serial for the zone.

        :param soas: The soa objects that will have config statments made
            with gen_config is True
        :param soa_pks_to_rebuild: The primary keys of soa objects that have
            been flagged as needing to be rebuilt. These soas will have their
            serial numbers recalculated.
        :param gen_config: Defaults to True, this parameter decides whether
            or not zone_statements will be be calculated for the soa objects
            found in the `soas` parameter
        :returns zone_stmts: A dictionary mapping zone names to config
            statements
        """
        zone_stmts = {}

        for soa in soas:
            # If anything happens during this soa's build we need to mark
            # it as dirty so it can be rebuild
            try:
                root_domain = soa.root_domain  # This is an expensive lookup
                if not root_domain:
                    # TODO, figure out how to send a nagios alert on this case.
                    self.log("No root domain found in zone {0}".format(soa))
                    fail_mail("{0} is an orphan. It's being skipped in the "
                              "builds".format(soa),
                              subject="{0} is "
                              "orphaned".format(soa))
                    continue

                force_rebuild = soa.pk in soa_pks_to_rebuild or soa.dirty
                if force_rebuild and self.PUSH_TO_PROD:
                    soa.dirty = False
                    soa.save()

                self.log("====== Processing {0} {1} ======".format(
                    root_domain, soa.serial))

                view_contents = self.build_views(soa, root_domain, gen_config,
                                                 force_rebuild)

                next_serial = soa.get_incremented_serial()

                if force_rebuild:
                    # Bypass save so we don't have to save a possible stale
                    # 'dirty' value to the db.
                    SOA.objects.filter(pk=soa.pk).update(serial=next_serial)
                    self.log("Zone will be rebuilt at serial {0}".format(
                        next_serial),
                             root_domain=root_domain)
                else:
                    self.log("Zone is stable at serial {0}".format(soa.serial),
                             root_domain=root_domain)

                self.do_build(zone_stmts, soa, view_contents, root_domain,
                              gen_config, force_rebuild, next_serial)
            except Exception:
                soa.schedule_full_rebuild()
                raise

        return zone_stmts
Пример #13
0
    def build_zone_files(self, soa_pks_to_rebuild):
        zone_stmts = {}

        for soa in SOA.objects.all():
            # If anything happens during this soa's build we need to mark
            # it as dirty so it can be rebuild
            try:
                root_domain = soa.root_domain  # This is an expensive lookup
                if not root_domain:
                    # TODO, figure out how to send a nagios alert on this case.
                    self.log("No root domain found in zone {0}".format(soa))
                    fail_mail(
                        "{0} is an orphan. It's being skipped in "
                        "the builds".format(soa), subject="{0} is "
                        "orphaned".format(soa))
                    continue

                """
                General order of things:
                * Find which views should have a zone file built and add them
                  to a list.
                * If any of the view's zone file have been tampered with or
                  the zone is new, trigger the rebuilding of all the zone's
                  view files. (rebuil all views in a zone keeps the serial
                  synced across all views)
                * Either rebuild all of a zone's view files because one view
                  needed to be rebuilt due to tampering or the zone was dirty
                  (again, this is to keep their serial synced) or just call
                  named-checkzone on the existing zone files for good measure.
                  Also generate a zone statement and add it to a dictionary for
                  later use during BIND configuration generation.
                """
                force_rebuild = soa.pk in soa_pks_to_rebuild or soa.dirty
                if force_rebuild:
                    soa.dirty = False
                    soa.save()

                self.log('====== Processing {0} {1} ======'.format(
                    root_domain, soa.serial)
                )
                views_to_build = []
                self.log(
                    "SOA was seen with dirty == {0}".format(force_rebuild),
                    root_domain=root_domain
                )

                # This for loop decides which views will be canidates for
                # rebuilding.
                for view in View.objects.all():
                    self.log("++++++ Looking at < {0} > view ++++++".
                             format(view.name), root_domain=root_domain)
                    t_start = time.time()  # tic
                    view_data = build_zone_data(view, root_domain, soa,
                                                logf=self.log)
                    build_time = time.time() - t_start  # toc
                    self.log('< {0} > Built {1} data in {2} seconds'
                             .format(view.name, soa, build_time),
                             root_domain=root_domain, build_time=build_time)
                    if not view_data:
                        self.log('< {0} > No data found in this view. '
                                 'No zone file will be made or included in any'
                                 ' config for this view.'.format(view.name),
                                 root_domain=root_domain)
                        continue
                    self.log('< {0} > Non-empty data set for this '
                             'view. Its zone file will be included in the '
                             'config.'.format(view.name),
                             root_domain=root_domain)
                    file_meta = self.get_file_meta(view, root_domain, soa)
                    was_bad_prev, safe_serial = self.verify_previous_build(
                        file_meta, view, root_domain, soa
                    )

                    if was_bad_prev:
                        soa.serial = safe_serial
                        force_rebuild = True

                    views_to_build.append(
                        (view, file_meta, view_data)
                    )

                self.log(
                    '----- Building < {0} > ------'.format(
                        ' | '.join([v.name for v, _, _ in views_to_build])
                    ), root_domain=root_domain
                )

                next_serial = soa.get_incremented_serial()
                if force_rebuild:
                    # Bypass save so we don't have to save a possible stale
                    # 'dirty' value to the db.
                    SOA.objects.filter(pk=soa.pk).update(serial=next_serial)
                    self.log('Zone will be rebuilt at serial {0}'
                             .format(next_serial), root_domain=root_domain)
                else:
                    self.log('Zone is stable at serial {0}'
                             .format(soa.serial), root_domain=root_domain)

                for view, file_meta, view_data in views_to_build:
                    if (root_domain.name, view.name) in ZONES_WITH_NO_CONFIG:
                        self.log(
                            '!!! Not going to emit zone statements for '
                            '{0}\n'.format(root_domain.name),
                            root_domain=root_domain
                        )
                    else:
                        view_zone_stmts = zone_stmts.setdefault(view.name, [])

                        # If we see a view in this loop it's going to end up in
                        # the config
                        view_zone_stmts.append(
                            self.render_zone_stmt(soa, root_domain, file_meta)
                        )

                    # If it's dirty or we are rebuilding another view, rebuild
                    # the zone
                    if force_rebuild:
                        self.log(
                            'Rebuilding < {0} > view file {1}'
                            .format(view.name, file_meta['prod_fname']),
                            root_domain=root_domain)
                        prod_fname = self.build_zone(
                            view, file_meta,
                            # Lazy string evaluation
                            view_data.format(serial=next_serial),
                            root_domain
                        )
                        assert prod_fname == file_meta['prod_fname']
                    else:
                        self.log(
                            'NO REBUILD needed for < {0} > view file {1}'
                            .format(view.name, file_meta['prod_fname']),
                            root_domain=root_domain
                        )
                    # run named-checkzone for good measure
                        if self.STAGE_ONLY:
                            self.log("Not calling named-checkconf.",
                                     root_domain=root_domain)
                        else:
                            self.named_checkzone(
                                file_meta['prod_fname'], root_domain
                            )
            except Exception:
                soa.schedule_rebuild()
                raise

        return zone_stmts
Пример #14
0
    def build_zone_files(self, soas, soa_pks_to_rebuild, gen_config=True):
        """
        For every soa in soas, build a views that are valid for its zone. If
        gen_config is true, generate a config statement for a view and store it
        in zone_stmts (returned by this function). When an soa's pk is in
        `soa_pks_to_rebuild` calculate a new serial for the zone.

        :param soas: The soa objects that will have config statments made
            with gen_config is True
        :param soa_pks_to_rebuild: The primary keys of soa objects that have
            been flagged as needing to be rebuilt. These soas will have their
            serial numbers recalculated.
        :param gen_config: Defaults to True, this parameter decides whether
            or not zone_statements will be be calculated for the soa objects
            found in the `soas` parameter
        :returns zone_stmts: A dictionary mapping zone names to config
            statements
        """
        zone_stmts = {}

        for soa in soas:
            # If anything happens during this soa's build we need to mark
            # it as dirty so it can be rebuild
            try:
                root_domain = soa.root_domain  # This is an expensive lookup
                if not root_domain:
                    # TODO, figure out how to send a nagios alert on this case.
                    self.log("No root domain found in zone {0}".format(soa))
                    fail_mail(
                        "{0} is an orphan. It's being skipped in the "
                        "builds".format(soa), subject="{0} is "
                        "orphaned".format(soa))
                    continue

                force_rebuild = soa.pk in soa_pks_to_rebuild or soa.dirty
                if force_rebuild and self.PUSH_TO_PROD:
                    soa.dirty = False
                    soa.save()

                self.log("====== Processing {0} {1} ======".format(
                    root_domain, soa.serial)
                )

                view_contents = self.build_views(
                    soa, root_domain, gen_config, force_rebuild
                )

                next_serial = soa.get_incremented_serial()

                if force_rebuild:
                    # Bypass save so we don't have to save a possible stale
                    # 'dirty' value to the db.
                    SOA.objects.filter(pk=soa.pk).update(serial=next_serial)
                    self.log(
                        "Zone will be rebuilt at serial {0}"
                        .format(next_serial),
                        root_domain=root_domain
                    )
                else:
                    self.log(
                        "Zone is stable at serial {0}".format(soa.serial),
                        root_domain=root_domain
                    )

                self.do_build(
                    zone_stmts, soa, view_contents, root_domain,
                    gen_config, force_rebuild, next_serial
                )
            except Exception:
                soa.schedule_full_rebuild()
                raise

        return zone_stmts