def update(self, data=None, read_from_netbox=False, source=None):

        # don't change the name of the VLAN if it already exists
        if read_from_netbox is False and grab(self, "data.name") is not None:
            data["name"] = grab(self, "data.name")

        super().update(data=data,
                       read_from_netbox=read_from_netbox,
                       source=source)
    def update_tags(self, tags, remove=False):
        """
        Update list of object tags

        Parameters
        ----------
        tags: (str, list, dict, NBTag)
            tags to parse and add/remove to/from current list of object tags
        remove: bool
            True if tags shall be removed, otherwise they will be added

        Returns
        -------
        None
        """

        if tags is None or NBTagList not in self.data_model.values():
            return

        action = "Adding" if remove is False else "Removing"

        log.debug2(f"{action} Tags: {tags}")

        current_tags = grab(self, "data.tags", fallback=NBTagList())

        new_tags = self.compile_tags(tags, remove=remove)

        if str(current_tags.get_display_name()) != str(
                new_tags.get_display_name()):

            self.data["tags"] = new_tags
            self.updated_items.append("tags")

            log.info(
                f"{self.name.capitalize()} '{self.get_display_name()}' attribute 'tags' changed from "
                f"'{current_tags.get_display_name()}' to '{new_tags.get_display_name()}'"
            )
Example #3
0
def main():

    start_time = datetime.now()

    # parse command line
    args = parse_command_line(
        self_description=self_description,
        version=__version__,
        version_date=__version_date__,
        default_config_file_path=default_config_file_path)

    # get config file path
    config_file = get_config_file(args.config_file)

    # get config handler
    config_handler = open_config_file(config_file)

    # get logging configuration

    # set log level
    log_level = default_log_level
    # config overwrites default
    log_level = config_handler.get("common", "log_level", fallback=log_level)
    # cli option overwrites config file
    log_level = grab(args, "log_level", fallback=log_level)

    log_file = None
    if bool(config_handler.getboolean("common", "log_to_file",
                                      fallback=False)) is True:
        log_file = config_handler.get("common", "log_file", fallback=None)

    # setup logging
    log = setup_logging(log_level, log_file)

    # now we are ready to go
    log.info("Starting " + __description__)
    log.debug(f"Using config file: {config_file}")

    # initialize an empty inventory which will be used to hold and reference all objects
    inventory = NetBoxInventory()

    # get config for NetBox handler
    netbox_settings = get_config(config_handler,
                                 section="netbox",
                                 valid_settings=NetBoxHandler.settings)

    # establish NetBox connection
    nb_handler = NetBoxHandler(settings=netbox_settings, inventory=inventory)

    # if purge was selected we go ahead and remove all items which were managed by this tools
    if args.purge is True:

        if args.dry_run is True:
            do_error_exit("Purge not available with option 'dry_run'")

        nb_handler.just_delete_all_the_things()

        # that's it, we are done here
        exit(0)

    # instantiate source handlers and get attributes
    log.info("Initializing sources")
    sources = instantiate_sources(config_handler, inventory)

    # all sources are unavailable
    if len(sources) == 0:
        log.error("No working sources found. Exit.")
        exit(1)

    # collect all dependent object classes
    log.info(
        "Querying necessary objects from Netbox. This might take a while.")
    for source in sources:
        nb_handler.query_current_data(source.dependent_netbox_objects)

    log.info("Finished querying necessary objects from Netbox")

    # resolve object relations within the initial inventory
    inventory.resolve_relations()

    # initialize basic data needed for syncing
    nb_handler.initialize_basic_data()

    # loop over sources and patch netbox data
    for source in sources:
        source.apply()

    # add/remove tags to/from all inventory items
    inventory.tag_all_the_things(nb_handler)

    # update all IP addresses
    inventory.query_ptr_records_for_all_ips()

    if args.dry_run is True:
        log.info("This is a dry run and we stop here. Running time: %s" %
                 get_relative_time(datetime.now() - start_time))
        exit(0)

    # update data in NetBox
    nb_handler.update_instance()

    # prune orphaned objects from NetBox
    nb_handler.prune_data()

    # finish
    log.info("Completed NetBox Sync in %s" %
             get_relative_time(datetime.now() - start_time))
Example #4
0
    def prune_data(self):
        """
        Prune objects in NetBox if they are no longer present in any source.
        First they will be marked as Orphaned and after X days they will be
        deleted from NetBox.
        """

        if self.prune_enabled is False:
            log.debug("Pruning disabled. Skipping")
            return

        log.info("Pruning orphaned data in NetBox")

        # update all items in NetBox accordingly
        today = datetime.now()
        for nb_object_sub_class in reversed(NetBoxObject.__subclasses__()):

            if getattr(nb_object_sub_class, "prune", False) is False:
                continue

            for this_object in self.inventory.get_all_items(
                    nb_object_sub_class):

                if this_object.source is not None:
                    continue

                if self.orphaned_tag not in this_object.get_tags():
                    continue

                date_last_update = grab(this_object, "data.last_updated")

                if date_last_update is None:
                    continue

                # already deleted
                if getattr(this_object, "deleted", False) is True:
                    continue

                # only need the date including seconds
                date_last_update = date_last_update[0:19]

                log.debug2(
                    f"Object '{this_object.name}' '{this_object.get_display_name()}' is Orphaned. "
                    f"Last time changed: {date_last_update}")

                # check prune delay.
                # noinspection PyBroadException
                try:
                    last_updated = datetime.strptime(date_last_update,
                                                     "%Y-%m-%dT%H:%M:%S")
                except Exception:
                    continue

                days_since_last_update = (today - last_updated).days

                # it seems we need to delete this object
                if last_updated is not None and days_since_last_update >= self.prune_delay_in_days:

                    log.info(
                        f"{nb_object_sub_class.name.capitalize()} '{this_object.get_display_name()}' is orphaned "
                        f"for {days_since_last_update} days and will be deleted."
                    )

                    # delete device/VM interfaces first. interfaces have no last_updated attribute
                    if isinstance(this_object, (NBVM, NBDevice)):

                        log.info(
                            f"Before the '{this_object.name}' can be deleted, all interfaces must be deleted."
                        )

                        for object_interface in self.inventory.get_all_interfaces(
                                this_object):

                            # already deleted
                            if getattr(object_interface, "deleted",
                                       False) is True:
                                continue

                            log.info(
                                f"Deleting interface '{object_interface.get_display_name()}'"
                            )

                            ret = self.request(object_interface.__class__,
                                               req_type="DELETE",
                                               nb_id=object_interface.nb_id)

                            if ret is True:
                                object_interface.deleted = True

                    ret = self.request(nb_object_sub_class,
                                       req_type="DELETE",
                                       nb_id=this_object.nb_id)

                    if ret is True:
                        this_object.deleted = True

        return
Example #5
0
    def update_object(self, nb_object_sub_class, unset=False, last_run=False):
        """
        Iterate over all objects of a certain NetBoxObject sub class and add/update them.
        But first update objects which this object class depends on.
        If some dependencies are unresolvable then these will be removed from the request
        and re added later to the object to try update object in a third run.

        Parameters
        ----------
        nb_object_sub_class: NetBoxObject sub class
            NetBox objects to update
        unset: bool
            True if only unset items should be deleted
        last_run: bool
            True if this will be the last update run. Needed to assign primary_ip4/6 properly

        """

        for this_object in self.inventory.get_all_items(nb_object_sub_class):

            # resolve dependencies
            for dependency in this_object.get_dependencies():
                if dependency not in self.resolved_dependencies:
                    log.debug2("Resolving dependency: %s" % dependency.name)
                    self.update_object(dependency)

            # unset data if requested
            if unset is True:

                if len(this_object.unset_items) == 0:
                    continue

                unset_data = dict()
                for unset_item in this_object.unset_items:

                    key_data_type = grab(this_object,
                                         f"data_model.{unset_item}")
                    if key_data_type in NBObjectList.__subclasses__():
                        unset_data[unset_item] = []
                    else:
                        unset_data[unset_item] = None

                log.info("Updating NetBox '%s' object '%s' with data: %s" %
                         (this_object.name, this_object.get_display_name(),
                          unset_data))

                returned_object_data = self.request(nb_object_sub_class,
                                                    req_type="PATCH",
                                                    data=unset_data,
                                                    nb_id=this_object.nb_id)

                if returned_object_data is not None:

                    this_object.update(data=returned_object_data,
                                       read_from_netbox=True)

                    this_object.resolve_relations()

                else:
                    log.error(
                        f"Request Failed for {nb_object_sub_class.name}. Used data: {unset_data}"
                    )

                continue

            data_to_patch = dict()
            unresolved_dependency_data = dict()

            for key, value in this_object.data.items():
                if key in this_object.updated_items:

                    if isinstance(value, (NetBoxObject, NBObjectList)):

                        # resolve dependency issues in last run
                        # primary IP always set in last run
                        if value.get_nb_reference() is None or \
                                (key.startswith("primary_ip") and last_run is False):
                            unresolved_dependency_data[key] = value
                        else:
                            data_to_patch[key] = value.get_nb_reference()

                    else:
                        data_to_patch[key] = value

            issued_request = False
            returned_object_data = None
            if len(data_to_patch.keys()) > 0:

                # default is a new object
                nb_id = None
                req_type = "POST"
                action = "Creating new"

                # if its not a new object then update it
                if this_object.is_new is False:
                    nb_id = this_object.nb_id
                    req_type = "PATCH"
                    action = "Updating"

                log.info("%s NetBox '%s' object '%s' with data: %s" %
                         (action, this_object.name,
                          this_object.get_display_name(), data_to_patch))

                returned_object_data = self.request(nb_object_sub_class,
                                                    req_type=req_type,
                                                    data=data_to_patch,
                                                    nb_id=nb_id)

                issued_request = True

            if returned_object_data is not None:

                this_object.update(data=returned_object_data,
                                   read_from_netbox=True)

            elif issued_request is True:
                log.error(
                    f"Request Failed for {nb_object_sub_class.name}. Used data: {data_to_patch}"
                )

            # add unresolved dependencies back to object
            if len(unresolved_dependency_data.keys()) > 0:
                log.debug2(
                    "Adding unresolved dependencies back to object: %s" %
                    list(unresolved_dependency_data.keys()))
                this_object.update(data=unresolved_dependency_data)

            this_object.resolve_relations()

        # add class to resolved dependencies
        self.resolved_dependencies.add(nb_object_sub_class)
Example #6
0
    def request(self,
                object_class,
                req_type="GET",
                data=None,
                params=None,
                nb_id=None):
        """
        Perform a NetBox request for a certain object.

        Parameters
        ----------
        object_class: NetBoxObject sub class
            class definition of the desired NetBox object
        req_type: str
            GET, PATCH, PUT, DELETE
        data: dict
            data which shall be send to NetBox
        params: dict
            dict of URL params which should be passed to NetBox
        nb_id: int
            ID of the NetBox object which will be appended to the requested NetBox URL

        Returns
        -------
        (dict, bool, None): of returned NetBox data. If object was requested to be deleted and it was
                            successful then True will be returned. None if request failed or was empty
        """

        result = None

        request_url = f"{self.url}{object_class.api_path}/"

        # append NetBox ID
        if nb_id is not None:
            request_url += f"{nb_id}/"

        if params is not None and not isinstance(params, dict):
            log.debug(
                f"Params passed to NetBox request need to be a dict, got: {params}"
            )
            params = dict()

        if req_type == "GET":

            if params is None:
                params = dict()

            if "limit" not in params.keys():
                params["limit"] = self.default_netbox_result_limit

            # always exclude config context
            params["exclude"] = "config_context"

        # prepare request
        this_request = self.session.prepare_request(
            requests.Request(req_type, request_url, params=params, json=data))

        # issue request
        response = self.single_request(this_request)

        try:
            result = response.json()
        except json.decoder.JSONDecodeError:
            pass

        if response.status_code == 200:

            # retrieve paginated results
            if this_request.method == "GET" and result is not None:
                while response.json().get("next") is not None:
                    this_request.url = response.json().get("next")
                    log.debug2(
                        "NetBox results are paginated. Getting next page")

                    response = self.single_request(this_request)
                    result["results"].extend(response.json().get("results"))

        elif response.status_code in [201, 204]:

            action = "created" if response.status_code == 201 else "deleted"

            if req_type == "DELETE":
                object_name = self.inventory.get_by_id(object_class, nb_id)
                if object_name is not None:
                    object_name = object_name.get_display_name()
            else:
                object_name = result.get(object_class.primary_key)

            log.info(
                f"NetBox successfully {action} {object_class.name} object '{object_name}'."
            )

            if response.status_code == 204:
                result = True

        # token issues
        elif response.status_code == 403:

            do_error_exit("NetBox returned: %s: %s" %
                          (response.reason, grab(result, "detail")))

        # we screw up something else
        elif 400 <= response.status_code < 500:

            log.error(
                f"NetBox returned: {this_request.method} {this_request.path_url} {response.reason}"
            )
            log.error(f"NetBox returned body: {result}")
            result = None

        elif response.status_code >= 500:

            do_error_exit(
                f"NetBox returned: {response.status_code} {response.reason}")

        return result
    def compile_tags(self, tags, remove=False):
        """

        Parameters
        ----------
        tags: (str, list, dict, NBTag)
            tags to parse and add/remove to/from current list of object tags
        remove: bool
            True if tags shall be removed, otherwise they will be added

        Returns
        -------
        NBTagList: with added/removed tags
        """

        if tags is None or NBTagList not in self.data_model.values():
            return

        # list of parsed tag strings
        sanitized_tag_strings = list()

        log.debug2(f"Compiling TAG list")

        new_tag_list = NBTagList()

        def extract_tags(this_tags):
            if isinstance(this_tags, NBTag):
                sanitized_tag_strings.append(this_tags.get_display_name())
            elif isinstance(this_tags, str):
                sanitized_tag_strings.append(this_tags)
            elif isinstance(this_tags,
                            dict) and this_tags.get("name") is not None:
                sanitized_tag_strings.append(this_tags.get("name"))

        if isinstance(tags, list):
            for tag in tags:
                extract_tags(tag)
        else:
            extract_tags(tags)

        # current list of tag strings
        current_tag_strings = self.get_tags()

        new_tags = list()
        removed_tags = list()

        for tag_name in sanitized_tag_strings:

            # add tag
            if tag_name not in current_tag_strings and remove is False:

                tag = self.inventory.add_update_object(NBTag,
                                                       data={"name": tag_name})

                new_tags.append(tag)

            if tag_name in current_tag_strings and remove is True:

                tag = self.inventory.get_by_data(NBTag,
                                                 data={"name": tag_name})

                removed_tags.append(tag)

        current_tags = grab(self, "data.tags", fallback=NBTagList())

        if len(new_tags) > 0:

            for tag in new_tags + current_tags:
                new_tag_list.append(tag)

        elif len(removed_tags) > 0:

            for tag in current_tags:
                if tag not in removed_tags:
                    new_tag_list.append(tag)
        else:
            new_tag_list = current_tags

        return new_tag_list