// import("options") // import("mac") // import("ip") /////////////////////////////////////////////////////////// // Issues /////////////////////////////////////////////////////////// // Some actions invoke other actions (e.g. delete_network invokes // delete_subnet). There's a simple way of writing this, but // when we do projection, we would need to use a fixed-point // computation to compute all the changes. Seemingly simple to do // but haven't done it; hence, the delete_network example has this // fragment commented out. Note: this impacts how we store/represent // results. // Notion of 'results' is clunky and may be problematic down the road. // Need mapping from real neutron actions to these actions when simulating. // - Each Bulk operation maps to a sequence of our actions // - Subnet creation maps to one subnet_create and a series of // assign_subnet_allocation_pools // Not capturing all of the constraints on when an action can be execed // - create_subnet and delete_subnet require manipulating IP addresses // - see notes // Not properly handling access control policy -- additional conditions should // be grafted onto the policy below, after analyzing AC policy // Because we're not necessarily grounding everything, builtins/negation // may be encountered that are not ground. Perhaps the right answer is // to delay their evaluation until they are ground. For builtins, it // is probably okay to just return the unground ones, e.g. that way // skolemization turns into constraint solving. For negatives, // it is an error if we never get to evaluate them. I suppose this is // just a variant of abduction. But it means that we should always be using // abduction when processing the action theory. // Should be 'import'ing modules that we care about. Implementationally, // this is straightforward to implement using theory includes. Need // to enable cycles through imports, which would cause an infinite loop // during top_down_evaluation. But since we // are always querying the top-level action theory, we should be // able to eliminate cycles. Or we could modify top_down_includes // to avoid the infinite loop. /////////////////////////////////////////////////////////// // Congress-defined "actions"/builtins /////////////////////////////////////////////////////////// // Representing optional parameters to API calls with options:value // Maybe use 'support' instead of 'action' so that during // abduction we can save both 'support' and 'action' but // we do not get confused about what the legit actions are. action("options:value") // hacks to avoid dealing with builtins action("sys:userid") // the name of the user currently logged in action("sys:user") // a list of all users action("sys:mac_address") // 'list' of all mac addresses action("cms:admin") // group of users considered administrators // Options module // Check if a key is present options:present(options, key) :- options:value(options, key, value) // Compute value of one of the options, providing a default if not present // options:lookup(options, key, default, result) options:lookup(options, key, old, old) :- not options:present(options, key) options:lookup(options, key, old, new) :- options:value(options, key, new) // Mac address module // mac:mac_address(mac) //builtin // IP module // ip:ip_address(ip) // builtin // ip:ip_in_range(ip, start, end) //builtin /////////////////////////////////////////////////////////// // Neutron helpers /////////////////////////////////////////////////////////// // tenant_id creation // If not present or not admin, login ID // Otherwise, supplied value neutron:compute.create.tenant(options, userid) :- sys:user(userid), not options:present(options, "tenant_id") neutron:compute.create.tenant(options, userid) :- sys:user(userid), not cms:admin(userid) neutron:compute.create.tenant(options, value) :- sys:user(userid), cms:admin(userid), options:value(options, "tenant_id", value) // tenant_id update // Cannot update tenant_id unless user requesting action is admin neutron:compute.update.tenant(options, old, old) :- not options:present(options, "tenant_id") neutron:compute.update.tenant(options, old, tenant_op) :- options:present(options, "tenant_id"), options:value(options, "tenant_id", tenant_op), sys:user(userid), cms:admin(userid) /////////////////////////////////////////////////////////// // Ports /////////////////////////////////////////////////////////// ///////////////////////////// // Create port // Modeling differences from reality: // - Fixed_ips and security_groups cannot be provided at creation-time // - Fixed_ips and security_groups can only be updated after creation // - Storing fixed_ips and security_groups in separate tables, keyed // on the port's ID. // Tables we're manipulating: // neutron:port(id, name, network_id, mac_address, device_id, device_owner, status, admin_state_up, tenant_id) // neutron:port.ip(id, ip) // neutron:available_ip(ip) // neutron:port.security_group(id, group_id) action("neutron:create_port") result(id) :- neutron:port+(id, network_id, name, mac_address, "null", "null", status, admin_state_up, tenant_id) neutron:port+(id, network_id, name, mac_address, "null", "null", status, admin_state_up, tenant_id) :- neutron:create_port(network_id, options), options:lookup(options, "name", "", name), neutron:create_port.compute.mac_address(options, mac_address), neutron:compute.create.tenant(options, tenant_id) // If given value but not mac_address, it's a noop. neutron:create_port.compute.mac_address(options, mac_address) :- not options:present(options, "mac_address") neutron:create_port.compute.mac_address(options, value) :- options:value(options, "mac_address", value), mac:mac_address(value) ///////////////////////////// // Update port // Note: updating a port is not the same as deleting old and adding new // but that's how we have to model it. If we generate a remediation // with both a delete and an add, we need to postprocess it to create // an update. Of course, that requires knowing what the add/delete/update // actions are and how to combine an add and a delete to produce an update. // Annoying, but no way around it I can see. Maybe for the common case // we'd want a macro language that spits out the code above and below. // Semantics: first delete and then insert action("neutron:update_port") result(id) :- neutron:port+(id, network_id, name, mac_address, device_id, device_owner, status, admin_state_up, tenant_id) // delete the old neutron:port-(id, network_id_old, name_old, mac_address_old, device_id_old, device_owner_old, status_old, admin_state_up_old, tenant_id_old) :- neutron:update_port(id, options), neutron:port(id, network_id_old, name_old, mac_address_old, device_id_old, device_owner_old, status_old, admin_state_up_old, tenant_id_old) // add the new neutron:port+(id, network_id, name, mac_address, device_id, device_owner, status, admin_state_up, tenant_id) :- neutron:update_port(id, options), neutron:port(id, network_id_old, name_old, mac_address_old, device_id_old, device_owner_old, status_old, admin_state_up_old, tenant_id_old), // look up optional params -- old if not present options:lookup(options, "network_id", network_id_old, network_id), options:lookup(options, "name", name_old, name), options:lookup(options, "mac_address", mac_address_old, mac_address), options:lookup(options, "device_id", device_id_old, device_id), options:lookup(options, "device_owner", device_owner_old, device_owner), neutron:compute.update.tenant(options, tenant_id_old, tenant_id) // Since options:value is a relation, we can handle sets natively. // We don't have an ordering on those sets, though I could imagine // making options:value ternary so that we at least have the index. // This seems like a bad idea though--since we're then writing code // that updates the index of port.ip, for example. // However, we won't *generate* a single API call that adds multiple // ports. Again, post-processing should be able to help; we just need // to know how to combine multiple API calls into a single one: // create_port(1, x), options:value(x,"ports","a") // + create_port(1, y), options:value(y,"ports","b") // -------------------------------------------------- // create_port(1,z), options:value(z, "ports", "a"), // options:value(z, "ports", "b") // Should be more careful here so as to not depend on the conflict // resolution semantics: delete before insert. Arguably, if an action // results in both the insertion and deletion of a tuple, then // there's something wrong with the policy. // delete old IPs, if value included neutron:port.ip-(id, ip) :- neutron:update_port(id, options), options:present(options, "ip"), neutron:port.ip(id, ip) // add new ports neutron:port.ip+(id, ip) :- neutron:update_port(id, options), options:value(options, "ip", ip), neutron:available_ip(ip) // old IPs become available neutron:available_ip+(ip) :- neutron:update_port(id, options), options:present(options, "ip"), neutron:port.ip(id, ip) // new IPs become unavailable neutron:available_ip-(ip) :- neutron:update_port(id, options), options:value(options, "ip", ip) // delete old groups, if value included neutron:port.security_group-(id, group) :- neutron:update_port(id, options), options:present(options, "security_group"), neutron:port.security_group(id, group) // add new ports neutron:port.security_group+(id, group) :- neutron:update_port(id, options), options:value(options, "security_group", group) ///////////////////////////// // Delete port action("neutron:delete_port") neutron:port-(id, network_id, name, mac_address, device_id, device_owner, status, admin_state_up, tenant_id) :- neutron:delete_port(id), neutron:port(id, network_id, name, mac_address, device_id, device_owner, status, admin_state_up, tenant_id) neutron:port.ip-(id,ip) :- neutron:delete_port(id), neutron:port.ip(id, ip) neutron:available_ip+(ip) :- neutron:delete_port(id), neutron:port.ip(id, ip) neutron:port.security_group-(id, group) :- neutron:delete_port(id), neutron:port.security_group(id, group) /////////////////////////////////////////////////////////// // Subnets /////////////////////////////////////////////////////////// // Tables: // neutron:subnet(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) // neutron:subnet.allocation_pool(subnetid, start, end) // Modeling approximation: instead of creating a subnet and including // unchangeable allocation pools, we create a subnet and then execute // the action "neutron:extend_subnet_allocation_pools" several times ///////////////////////////// // Create subnet action("neutron:create_subnet") result(id) :- neutron:subnet+(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) // What do we do about the fact that in reality you must supply // a valid IP if you provide a gateway_ip. Could add that constraint // but then our simulation will be *generating* // an IP address here WITHIN the logic. One option: // we extract constraints (builtins that are unground) // and then satisfy them when skolemizing. // We can always bail out since this is an approximation anyway. neutron:subnet+(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) :- neutron:create_subnet(network_id, cidr, options), options:lookup(options, "name", "", name), options:lookup(options, "gateway_ip", gateway_ip, gateway_ip), options:lookup(options, "ip_version", 4, ip_version), options:lookup(options, "enable_dhcp", "true", enable_dhcp), neutron:compute.create.tenant(options, tenant_id) action("neutron:assign_subnet_allocation_pools") neutron:subnet.allocation_pool+(id, start, end) :- neutron:assign_subnet_allocation_pools(id, start, end) ///////////////////////////// // Update subnet action("neutron:update_subnet") neutron:subnet-(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) :- neutron:update_subnet(id, options), neutron:subnet(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) neutron:subnet+(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) :- neutron:update_subnet(id, options), neutron:subnet(id, name_old, network_id_old, gateway_ip_old, ip_version, cidr, enable_dhcp_old, tenant_id_old), options:lookup(options, "name", name_old, name), options:lookup(options, "network_id", network_id_old, network_id), options:lookup(options, "gateway_ip", gateway_ip_old, gateway_ip), options:lookup(options, "enable_dhcp", enable_dhcp_old, enable_dhcp), neutron:compute.update.tenant(options, tenant_id_old, tenant_id) ///////////////////////////// // Delete subnet action("neutron:delete_subnet") neutron:subnet-(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) :- neutron:delete_subnet(id), not neutron:some_allocated_ip(id), neutron:subnet(id, name, network_id, gateway_ip, ip_version, cidr, enable_dhcp, tenant_id) some_allocated_ip(subnet_id) :- neutron:port(port_id, ip), neutron:subnet.allocation_pool(subnet_id, start, end), ip:ip_in_range(ip, start, end) // empty until we attach it /////////////////////////////////////////////////////////// // Networks /////////////////////////////////////////////////////////// // Tables: // neutron:network(id, name, status, admin_state, shared, tenant_ID) // neutron:network.subnets(id, subnet) ///////////////////////////// // Create network result(id) :- neutron:network+(id, name, status, admin_state, shared, tenant_id) action("neutron:create_network") neutron:network+(id, name, status, admin_state, shared, tenant_id) :- neutron:create_network(options), options:lookup(options, "name", "", name), options:lookup(options, "admin_state", "true", admin_state), options:lookup(options, "shared", "true", shared), neutron:compute.create.tenant(options, tenant_id) ///////////////////////////// // Update network // Note: updating a port is not the same as deleting old and adding new // but that's how we have to model it. If we generate a remediation // with both a delete and an add, we need to postprocess it to create // an update. Of course, that requires knowing what the add/delete/update // actions are and how to combine an add and a delete to produce an update. // Annoying, but no way around it I can see. Maybe for the common case // we'd want a macro language that spits out the code above and below. // Semantics: first delete and then insert action("neutron:update_network") result(id) :- neutron:network+(id, name, status, admin_state, shared, tenant_id) // delete the old neutron:network-(id, name, status, admin_state, shared, tenant_id) :- neutron:update_network(id, options), neutron:network(id, name, status, admin_state, shared, tenant_id) // add the new neutron:network+(id, name, status, admin_state, shared, tenant_id) :- neutron:update_network(id, options), neutron:network(id, name_old, status, admin_state_old, shared_old, tenant_id_old), // look up optional params -- old if not present options:lookup(options, "name", name_old, name), options:lookup(options, "admin_state", admin_state_old, admin_state), options:lookup(options, "shared", shared_old, shared), neutron:compute.update.tenant(options, tenant_id_old, tenant_id) // Should be more careful here so as to not depend on the conflict // resolution semantics: delete before insert. Arguably, if a change // results in both the insertion and deletion of a tuple, then // there's something wrong with the policy. // delete old subnets, if value included neutron:network.subnet-(id, subnet) :- neutron:update_network(id, options), options:present(options, "subnet"), neutron:network.subnet(id, subnet) // add new subnets neutron:network.subnet+(id, subnet) :- neutron:update_network(id, options), options:value(options, "subnet", subnet) ///////////////////////////// // Delete network // Can only be executed if no ports configured for network action("neutron:delete_network") neutron:network-(id, name, status, admin_state, shared, tenant_id) :- neutron:delete_network(id), not neutron:some_port_configured(id), neutron:network(id, name, status, admin_state, shared, tenant_id) neutron:network.subnet-(id,subnet) :- neutron:delete_network(id), not some_port_configured(id), neutron:network.subnet(id, subnet) // CASCADING DELETE -- WANT TO BE ABLE TO SAY THAT DELETE_NETWORK CAUSES // DELETE_SUBNET. CAN'T REALLY DO THAT. MAYBE WE WANT TO DO IT OUTSIDE. // neutron:delete_subnet*(subnet_id) :- // neutron:delete_network(id), // not some_port_configured(id), // neutron:network.subnet(id, subnet_id) neutron:some_port_configured(network) :- neutron:port+(id, network, name, mac_address, device_id, device_owner, status, admin_state_up, tenant_id)