diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d4f37167..2b2be5aa 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -27,6 +27,7 @@ use topology::{sym, Topology}; /// A partial representations of a TOSCA node template (enough for [solve()]) #[cfg_attr(feature = "python", derive(FromPyObject))] +#[derive(Debug)] pub struct Node { /// node template name pub name: String, @@ -141,22 +142,23 @@ fn add_field_to_topology( sym(n), )); } - CriteriaTerm::NodeMatch { query } => { + CriteriaTerm::NodeMatch { start_node, query } => { topology.result.push(( sym(node_name), sym(&f.name), 0, - sym(node_name), + sym(start_node), false, )); - for (index, (q, n)) in query.iter().enumerate() { + for (index, (q, n, param)) in query.iter().enumerate() { topology.query.push(( sym(node_name), sym(&f.name), index, *q, sym(n), + sym(param), index + 1 == query.len(), )); } @@ -394,6 +396,7 @@ pub fn solve( add_node_to_topology(node, &mut prog, &type_parents, true, false)?; } run_program(&mut prog, timeout)?; + // println!("term_match: {:#?}", prog.term_match.iter().map(|x| (x.0.clone(), x.1.clone(), x.3.clone(), x.4.clone())).collect::>()); // return requirement_match let mut requirements = RequirementMatches::new(); @@ -458,7 +461,8 @@ mod tests { n: "Service".into(), }, CriteriaTerm::NodeMatch { - query: vec![(QueryType::Sources, "connects_to".into())], + start_node: name.into(), + query: vec![(QueryType::Sources, "connects_to".into(), "".into())], }, ], tosca_type: Some("tosca.relationships.Root".into()), diff --git a/rust/src/topology.rs b/rust/src/topology.rs index 1f95cb23..66e80fcd 100644 --- a/rust/src/topology.rs +++ b/rust/src/topology.rs @@ -56,7 +56,8 @@ pub enum CriteriaTerm { constraints: Vec, }, NodeMatch { - query: Vec<(QueryType, Symbol)>, + start_node: Symbol, + query: Vec<(QueryType, Symbol, Symbol)>, }, } @@ -510,7 +511,7 @@ ascent! { term_match(source, req, criteria, ct, target, None) <-- result(source, req, q_id, target, true), requirement(source, req, criteria, restrictions), - req_term_query(node, req, ct, q_id); + req_term_query(source, req, ct, q_id); filtered(name, req_name, target, cn, criteria, Criteria::singleton(term.clone())) <-- term_match(name, req_name, criteria, term, target, cn); @@ -535,39 +536,41 @@ ascent! { transitive_match(x, r, z) <-- requirement_match(x, r, y, c), transitive_match(y, r, z); // querying - relation query(NodeName, ReqName, QueryId, QueryType, ReqName, bool); + relation query(NodeName, ReqName, QueryId, QueryType, ReqName, Symbol, bool); relation result(NodeName, ReqName, QueryId, NodeName, bool); // rules for generating for each query type: + + // include self in result + result(n, r, q_id + 1, s ,last) <-- + query(n, r, q_id, qt, ?req, sym("SELF"), last) if *qt != QueryType::PropSource, + result(n, r, q_id, s, false); + result(n, r, q_id + 1, t ,last) <-- transitive_match(s, a, t), - query(n, r, q_id, QueryType::TransitiveRelation, a, last), - result(n, r, q_id, s, false); + query(n, r, q_id, QueryType::TransitiveRelation, a, _, last), + result(n, r, q_id, s, false); result(n, r, q_id + 1, s, last) <-- required_by(s, a, t), - query(n, r, q_id, QueryType::RequiredBy, a, last), - result(n, r, q_id, t, false); + query(n, r, q_id, QueryType::RequiredBy, a, _, last), + result(n, r, q_id, t, false); result(n, r2, q_id + 1, s, last) <-- required_by(s, r2, t), - query(n, r, q_id, QueryType::RequiredByType, a, last), - relationship(n, r, a), - result(n, r, q_id, t, false); + query(n, r, q_id, QueryType::RequiredByType, a, _, last), + relationship(n, r, a), + result(n, r, q_id, t, false); result(n, r2, q_id + 1, t ,last) <-- transitive_match(s, r2, t), - query(n, r, q_id, QueryType::TransitiveRelationType, a, last), + query(n, r, q_id, QueryType::TransitiveRelationType, a, _, last), relationship(n, r, a), result(n, r, q_id, s, false); result(node_name, req_name, q_id + 1, source, last) <-- requirement_match(source, a, target, ?cap), - query(node_name, req_name, q_id, QueryType::Sources, a, last), - result(node_name, req_name, q_id, target, false); + query(node_name, req_name, q_id, QueryType::Sources, a, _, last), + result(node_name, req_name, q_id, target, false); result(node_name, req_name, q_id + 1, target, last) <-- requirement_match(source, a, target, ?cap), - query(node_name, req_name, q_id, QueryType::Targets, a, last), - result(node_name, req_name, q_id, source, false); - - // result(t, q_id, final) <-- property_source(s, None, prop_name, t), query(q_id, QueryType::PropertySource"), prop_name, last), result(s, q_id, false); - // given an expression like configured_by::property, generate: - // [result(source_node, 1, false), query(1, "required_by", "configured_by", false), property_source_query(1, property, true)] + query(node_name, req_name, q_id, QueryType::Targets, a, _, last), + result(node_name, req_name, q_id, source, false); } #[cfg(test)] diff --git a/unfurl/solver.py b/unfurl/solver.py index 46819ba0..ae522f35 100644 --- a/unfurl/solver.py +++ b/unfurl/solver.py @@ -1,8 +1,10 @@ # Copyright (c) 2024 Adam Souzis # SPDX-License-Identifier: MIT -from typing import Any, Dict, List, Optional, Tuple, cast +from typing import Any, Dict, List, Optional, Tuple, Union, cast import sys +from .support import resolve_function_keyword + # import types from rust extension from .tosca_solver import ( # type: ignore solve, @@ -276,30 +278,65 @@ def convert( return entity -def add_match(terms, match) -> None: +def add_match( + node_template: NodeTemplate, terms: list, match: Union[str, dict] +) -> None: + if isinstance(match, dict) and (get_property := match.get("get_property")): + assert isinstance(get_property, list), get_property + match = cast(str, resolve_function_keyword(get_property[0])) + if len(get_property) > 2: + match += "::.capabilities" # XXX or .targets (cf. .names) + match += "::" + "::".join(get_property[1:]) if isinstance(match, dict) and (node_type := match.get("get_nodes_of_type")): terms.append(CriteriaTerm.NodeType(node_type)) else: skip = False - query = [] + query: List[Tuple[QueryType, str, str]] = [] + start_node: str = node_template.name result = analyze_expr(match) if result: expr_list = result.get_keys() - # logger.warning(f"{match} {expr_list=}") + # logger.warning(f"expr_list: {node_template.name} with {match}:\n {expr_list=}") query_type = None + cap = "" for key in expr_list: if key == "$start": continue + if cap == ".capabilities": + cap = key # assume this key is capability name + continue if query_type is not None: - query.append((query_type, key)) + # Sources or Targets consume next key + query.append((query_type, key, "")) query_type = None else: + if key.startswith("::"): + query = [] + start_node = key[2:] if key.startswith("."): - if key == ".configured_by": + if key == ".": + continue + elif key == "..": + query.append((QueryType.Targets, "host", "")) + elif key.startswith(".root"): + query = [] + start_node = "root" + elif key == ".instances": + query.append((QueryType.Sources, "host", "")) + elif key == ".configured_by": query.append( ( QueryType.RequiredByType, "unfurl.relationships.Configures", + "", + ) + ) + elif key == ".parents" or key == ".ancestors": + query.append( + ( + QueryType.TransitiveRelation, + "host", + "SELF" if key == ".ancestors" else "", ) ) elif key == ".hosted_on": @@ -307,18 +344,23 @@ def add_match(terms, match) -> None: ( QueryType.TransitiveRelationType, "tosca.relationships.HostedOn", + "", ) ) + elif key == ".capabilities": + cap = ".capabilities" # assume next key is capability name else: + # matches Sources or Targets query_type = getattr(QueryType, key[1:].title(), None) if query_type is None: skip = True break else: # key is prop - query.append((QueryType.PropSource, key)) + query.append((QueryType.PropSource, key, cap)) + cap = "" # logger.warning(f"{skip} {query=}") if query and not skip: - terms.append(CriteriaTerm.NodeMatch(query)) + terms.append(CriteriaTerm.NodeMatch(start_node, query)) def get_req_terms( @@ -371,7 +413,7 @@ def get_req_terms( if node_filter: match = node_filter.get("match") if match: - add_match(terms, match) + add_match(node_template, terms, match) if not filter2term(terms, node_filter, None): return None, False # has an unsupported constraint, bail for cap_filters in node_filter.get("capabilities", []):