Example #1
0
def update_discovered_state(new_discovered_state, temp_dir, discovery_id,
                            changed_subtree_path, subtree):

    discovered_state_file = os.path.join(
        temp_dir, 'project', f'discovered_state_{discovery_id}.yml')
    if os.path.exists(discovered_state_file):
        with open(discovered_state_file) as f:
            discovered_subtree_state = yaml.safe_load(f.read())
            print(changed_subtree_path)
            print(
                yaml.safe_dump(discovered_subtree_state,
                               default_flow_style=False))
            print(yaml.safe_dump(subtree, default_flow_style=False))

        # List case
        match_list = re.match(r"(.*)\[(\d+)\]$", changed_subtree_path)
        if match_list:
            parent_path = match_list.groups()[0]
            index = int(match_list.groups()[1])
            extract(new_discovered_state,
                    parent_path)[index] = discovered_subtree_state

        # Dict case
        match_dict = re.match(r"(.*)\['(\S+)'\]$", changed_subtree_path)
        if match_dict and not match_list:
            parent_path = match_dict.groups()[0]
            index = match_dict.groups()[1]
            extract(new_discovered_state,
                    parent_path)[index] = discovered_subtree_state

        if not match_dict and not match_list:
            assert False, f"type of changed_subtree_path not supported {changed_subtree_path}"

        print(yaml.safe_dump(new_discovered_state, default_flow_style=False))
Example #2
0
def destructure_vars(rule, subtree):

    destructured_vars = {}

    for name, extract_path in rule.get('vars', {}).items():
        destructured_vars[name] = extract(subtree, extract_path)

    return destructured_vars
Example #3
0
    def _search(
        self,
        fields: Dict[str, EntityID],
        models: OptionalModelOrModels[Entity] = None,
    ) -> List[Entity]:
        """Get the entities whose attributes match one or several conditions.

        Args:
            models: Entity class or classes to obtain.
            fields: Dictionary with the {key}:{value} to search.

        Returns:
            entities: List of Entity object that matches the search criteria.

        Raises:
            EntityNotFoundError: If the entities are not found.
        """
        models = self._build_models(models)
        if len(models) == 1:
            models = models[0]
        all_entities: List[Entity] = self.all(models)
        entities_dict = {entity.id_: entity for entity in all_entities}
        entity_attributes = {
            entity.id_: entity.dict()
            for entity in all_entities
        }

        for key, value in fields.items():
            # Get entities that have the value `value`
            entities_with_value = entity_attributes | grep(
                value, use_regexp=True, strict_checking=False)
            matching_entity_attributes = {}

            try:
                entities_with_value["matched_values"]
            except KeyError as error:
                raise self._model_not_found(
                    models,
                    f" that match the search filter {fields}") from error

            for path in entities_with_value["matched_values"]:
                entity_id = re.sub(r"root\['?(.*?)'?\]\[.*", r"\1", path)

                # Convert int ids from str to int
                try:
                    entity_id = int(entity_id)
                except ValueError:
                    entity_id = re.sub(r"'(.*)'", r"\1", entity_id)

                # Add the entity to the matching ones only if the value is of the
                # attribute `key`.
                if re.match(rf"root\['?{entity_id}'?\]\['{key}'\]", path):
                    matching_entity_attributes[entity_id] = extract(
                        entity_attributes, f"root[{entity_id}]")
            entity_attributes = matching_entity_attributes
        entities = [entities_dict[key] for key in entity_attributes.keys()]

        return entities
Example #4
0
def test_extract_and_modify():

    t1 = yaml.safe_load('''
    routers:
        - name: R1
        - name: R2
    ''')

    assert extract(t1, "root['routers'][0]") == {'name': 'R1'}
    extract(t1, "root['routers']")[0] = {'name': 'R3'}
    assert extract(t1, "root['routers'][0]") == {'name': 'R3'}

    # find parent and index of node

    # List case
    match = re.match(r"(.*)\[(\d+)\]$", "root['routers'][0]")
    assert match.groups()[0] == "root['routers']"
    assert match.groups()[1] == "0"
    parent = match.groups()[0]
    index = int(match.groups()[1])
    extract(t1, parent)[index] = {'name': 'R4'}

    assert extract(t1, "root['routers']") == [{'name': 'R4'}, {'name': 'R2'}]

    # Dictionary case
    match = re.match(r"(.*)\['(\S+)'\]$", "root['routers']")
    assert match.groups()[0] == "root"
    assert match.groups()[1] == "routers"
    parent = match.groups()[0]
    index = match.groups()[1]
    extract(t1, parent)[index] = [{'name': 'R5'}, {'name': 'R2'}]

    assert extract(t1, "root") == {'routers': [{'name': 'R5'}, {'name': 'R2'}]}
Example #5
0
def get_rule_action_subtree(matching_rule, current_desired_state,
                            new_desired_state):

    change_type, rule, match, value = matching_rule
    print('change_type', change_type)
    print('rule', rule)
    print('match', match)
    print('value', value)
    changed_subtree_path = match.groups()[0]
    print('changed_subtree_path', changed_subtree_path)
    try:
        new_subtree = extract(new_desired_state, changed_subtree_path)
        new_subtree_missing = False
    except (KeyError, IndexError, TypeError):
        new_subtree_missing = True
    try:
        old_subtree = extract(current_desired_state, changed_subtree_path)
        old_subtree_missing = False
    except (KeyError, IndexError, TypeError):
        old_subtree_missing = True
    print('new_subtree_missing', new_subtree_missing)
    print('old_subtree_missing', old_subtree_missing)

    if change_type == 'iterable_item_added':
        action = Action.CREATE
        subtree = new_subtree
    elif change_type == 'iterable_item_removed':
        action = Action.DELETE
        subtree = old_subtree
    elif new_subtree_missing is False and old_subtree_missing is False:
        action = Action.UPDATE
        subtree = new_subtree
    elif new_subtree_missing and old_subtree_missing is False:
        action = Action.DELETE
        subtree = old_subtree
    elif old_subtree_missing and new_subtree_missing is False:
        action = Action.CREATE
        subtree = new_subtree
    else:
        assert False, "Logic bug"
    print('action', action)

    return action, subtree
Example #6
0
    def _search(
        self,
        fields: Dict[str, EntityID],
        model: Type[EntityT],
    ) -> List[EntityT]:
        """Get the entities whose attributes match one or several conditions.

        Particular implementation of the database adapter.

        Args:
            model: Entity class to obtain.
            fields: Dictionary with the {key}:{value} to search.

        Returns:
            entities: List of Entity object that matches the search criteria.
        """
        all_entities = self.all(model)
        entities_dict = {entity.id_: entity for entity in all_entities}
        entity_attributes = {
            entity.id_: entity.dict()
            for entity in all_entities
        }

        for key, value in fields.items():
            # Get entities that have the value `value`
            entities_with_value = entity_attributes | grep(
                value, use_regexp=True, strict_checking=False)
            matching_entity_attributes = {}

            try:
                entities_with_value["matched_values"]
            except KeyError:
                return []

            for path in entities_with_value["matched_values"]:
                entity_id = re.sub(r"root\['?(.*?)'?\]\[.*", r"\1", path)

                # Convert int ids from str to int
                try:
                    # ignore: waiting for ADR-006 to be resolved
                    entity_id = int(entity_id)  # type: ignore
                except ValueError:
                    entity_id = re.sub(r"'(.*)'", r"\1", entity_id)

                # Add the entity to the matching ones only if the value is of the
                # attribute `key`.
                if re.match(rf"root\['?{entity_id}'?\]\['{key}'\]", path):
                    matching_entity_attributes[entity_id] = extract(
                        entity_attributes, f"root[{entity_id}]")
            # ignore: waiting for ADR-006 to be resolved
            entity_attributes = matching_entity_attributes  # type: ignore
        entities = [entities_dict[key] for key in entity_attributes.keys()]

        return entities
Example #7
0
def select_rules_recursive(diff, rules, current_desired_state,
                           new_desired_state):

    matching_rules = []
    matchers = [(make_matcher(build_rule_selector(rule['rule_selector'])),
                 rule) for rule in rules]

    for key, value in diff.get('values_changed', {}).items():
        for (matcher, rule) in matchers:
            match = re.match(matcher, key)
            if match:
                matching_rules.append(('values_changed', rule, match, value))

    for item in diff.get('dictionary_item_added', []):
        for (matcher, rule) in matchers:
            match = re.match(matcher, item)
            if match:
                matching_rules.append(
                    ('dictionary_item_added', rule, match, None))
            new_subtree = extract(new_desired_state, item)
            select_rules_recursive_helper(diff, matchers, matching_rules, item,
                                          new_subtree)

    for item in diff.get('dictionary_item_removed', []):
        for (matcher, rule) in matchers:
            match = re.match(matcher, item)
            if match:
                matching_rules.append(
                    ('dictionary_item_removed', rule, match, None))

    for item in diff.get('iterable_item_added', []):
        print(item)
        for (matcher, rule) in matchers:
            match = re.match(matcher, item)
            if match:
                matching_rules.append(
                    ('iterable_item_added', rule, match, None))

    for item in diff.get('iterable_item_removed', []):
        print(item)
        for (matcher, rule) in matchers:
            match = re.match(matcher, item)
            if match:
                matching_rules.append(
                    ('iterable_item_removed', rule, match, None))

    for key, value in diff.get('type_changes', {}).items():
        # Handles case in YAML where an empty list defaults to None type
        if value.get('old_type') == type(None) and value.get(
                'new_type') == list:
            # Add a single new element to the key
            # TODO: this should probably loop over all the new elements in the list not just one
            key += '[0]'
        elif value.get('old_type') == list and value.get('new_type') == type(
                None):
            # Add a single new element to the key
            key += '[0]'
        for (matcher, rule) in matchers:
            match = re.match(matcher, key)
            if match:
                matching_rules.append(('type_changes', rule, match, None))

        # Handles case in YAML where an empty dict defaults to None type on the old state
        if value.get('old_type') == type(None) and value.get(
                'new_type') == dict:
            # Try the matcher against all the keys in the dict
            for dict_key in value.get('new_value').keys():
                new_key = f"{key}['{dict_key}']"
                for (matcher, rule) in matchers:
                    match = re.match(matcher, new_key)
                    if match:
                        matching_rules.append(
                            ('type_changes', rule, match, None))
            select_rules_recursive_helper(diff, matchers, matching_rules, key,
                                          value.get('new_value'))

        # Handles case in YAML where an empty dict defaults to None type on the new state
        if value.get('old_type') == dict and value.get('new_type') == type(
                None):
            # Try the matcher against all the keys in the dict
            for dict_key in value.get('old_value').keys():
                old_key = f"{key}['{dict_key}']"
                for (matcher, rule) in matchers:
                    match = re.match(matcher, old_key)
                    if match:
                        matching_rules.append(
                            ('type_changes', rule, match, None))
            select_rules_recursive_helper(diff, matchers, matching_rules, key,
                                          value.get('old_value'))

    return matching_rules
Example #8
0
def desired_state_discovery(monitor, secrets, project_src,
                            current_desired_state, new_desired_state,
                            ran_rules, inventory, explain):

    # Discovers the state of a subset of a system

    diff = DeepDiff(current_desired_state,
                    new_desired_state,
                    ignore_order=True)

    # deep copy
    new_discovered_state = yaml.safe_load(yaml.safe_dump(new_desired_state))

    plays = []

    destructured_vars_list = []
    discovered_rules = []

    for discovery_id, (rule, changed_subtree_path, subtree,
                       inventory_name) in enumerate(ran_rules):

        # Experiment: Build the vars using destructuring
        destructured_vars = {}

        for name, extract_path in rule.get('vars', {}).items():
            destructured_vars[name] = extract(subtree, extract_path)

        # Experiment: Make the subtree available as node
        destructured_vars['node'] = subtree
        destructured_vars['discovery_id'] = discovery_id

        print('destructured_vars', destructured_vars)

        # Build a play using tasks or role from rule

        play = {
            'name':
            f'discovery for {inventory_name} discovery_id {discovery_id}',
            'hosts': inventory_name,
            'gather_facts': False,
            'tasks': []
        }

        if 'tasks' in rule.get(ACTION_RULES[Action.RETRIEVE], {}):
            play['tasks'].append({
                'include_tasks': {
                    'file':
                    find_tasks(
                        rule.get(ACTION_RULES[Action.RETRIEVE]).get('tasks'))
                },
                'name': 'include retrieve'
            })

            print(play)

            plays.append(play)
            destructured_vars_list.append(destructured_vars)
            discovered_rules.append(
                [discovery_id, changed_subtree_path, subtree])

    if not plays:
        return new_discovered_state

    def runner_process_message(data):
        monitor.stream.put_message(Stdout(0, now(), data.get('stdout', '')))

    runner = PlaybookRunner(runner_process_message, new_desired_state, diff,
                            destructured_vars_list, plays, secrets,
                            project_src, inventory)
    result = runner.run()

    if result:

        for discovery_id, changed_subtree_path, subtree in discovered_rules:
            update_discovered_state(new_discovered_state, runner.temp_dir,
                                    discovery_id, changed_subtree_path,
                                    subtree)

    return new_discovered_state
Example #9
0
def desired_state_diff(monitor, secrets, project_src, current_desired_state,
                       new_desired_state, rules, inventory, explain):
    '''
    desired_state_diff creates playbooks and runs them with ansible-runner to implement the differences
    between two version of state: current_desired_state and new_desired_state.
    '''

    # Find the difference between states

    diff = DeepDiff(current_desired_state,
                    new_desired_state,
                    ignore_order=True)
    print(diff)

    # Find matching rules

    matching_rules = select_rules_recursive(diff, rules['rules'],
                                            current_desired_state,
                                            new_desired_state)
    if explain:
        print('matching_rules')
        pprint(matching_rules)

    dedup_matching_rules = deduplicate_rules(matching_rules)

    if explain:
        print('dedup_matching_rules:')
        pprint(dedup_matching_rules)

    # Build up the set of ansible-runner executions to implement the changes using the rules

    ran_rules = []

    plays = []

    destructured_vars_list = []

    for matching_rule in dedup_matching_rules:
        change_type, rule, match, value = matching_rule
        changed_subtree_path = match.groups()[0]
        action, subtree = get_rule_action_subtree(matching_rule,
                                                  current_desired_state,
                                                  new_desired_state)
        print('action', action)

        print('rule action', rule.get(ACTION_RULES[action]))

        # Experiment: Build the vars using destructuring

        destructured_vars = {}

        for name, extract_path in rule.get('vars', {}).items():
            destructured_vars[name] = extract(subtree, extract_path)

        # Experiment: Make the subtree available as node
        destructured_vars['node'] = subtree

        print('destructured_vars', destructured_vars)

        # Determine the inventory to run on

        inventory_selector = build_inventory_selector(
            rule.get('inventory_selector'))
        if inventory_selector:
            try:
                inventory_name = extract(subtree, inventory_selector)
            except KeyError:
                raise Exception(
                    f'Invalid inventory_selector {inventory_selector}')

        print('inventory_name', inventory_name)

        # Build a play using tasks or role from rule

        play = {
            'name':
            "{0} {1} {2}".format(ACTION_RULES[action], changed_subtree_path,
                                 inventory_name),
            'hosts':
            inventory_name,
            'gather_facts':
            False,
            'tasks': []
        }

        if 'tasks' in rule.get(ACTION_RULES[action], {}):
            play['tasks'].append({
                'include_tasks': {
                    'file':
                    find_tasks(rule.get(ACTION_RULES[action]).get('tasks'))
                },
                'name':
                "{0} {1}".format(ACTION_RULES[action], changed_subtree_path)
            })

        if 'become' in rule:
            play['become'] = rule['become']

        if explain:
            print(yaml.dump(play))
        else:

            # Run the action play

            plays.append(play)
            destructured_vars_list.append(destructured_vars)

            ran_rules.append(
                (rule, changed_subtree_path, subtree, inventory_name))

    def runner_process_message(data):
        monitor.stream.put_message(Stdout(0, now(), data.get('stdout', '')))

    PlaybookRunner(runner_process_message, new_desired_state, diff,
                   destructured_vars_list, plays, secrets, project_src,
                   inventory).run()

    return ran_rules