def test_node_list_without_zone_does_not_filter(self): nodes = [factory.make_Node(zone=factory.make_Zone()) for _ in range(3)] query = RequestFixture({}, '') node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual([node.system_id for node in nodes], extract_system_ids_from_nodes(node_list))
def test_node_list_with_agent_name_filters_with_empty_string(self): factory.make_Node(agent_name=factory.make_name("agent-name")) node = factory.make_Node(agent_name="") query = RequestFixture({"agent_name": ""}, "agent_name") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual([node.system_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_nonexistent_id_returns_empty_list(self): # Trying to list a nonexistent node id returns a list containing # no nodes -- even if other (non-matching) nodes exist. existing_id = factory.make_Node().system_id nonexistent_id = existing_id + factory.make_string() query = RequestFixture({"id": [nonexistent_id]}, "id") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertItemsEqual([], extract_system_ids_from_nodes(node_list))
def test_node_list_with_id_returns_matching_nodes(self): # The "list" operation takes optional "id" parameters. Only # nodes with matching ids will be returned. ids = [factory.make_Node().system_id for _ in range(3)] matching_id = ids[0] query = RequestFixture({"id": [matching_id]}, "id") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertItemsEqual([matching_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_pool_filters_by_pool(self): pool1 = factory.make_ResourcePool() pool2 = factory.make_ResourcePool() node1 = factory.make_Node(pool=pool1) factory.make_Node(pool=pool2) query = RequestFixture({"pool": pool1.name}, "pool", self.user) node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual([node1.system_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_some_matching_ids_returns_matching_nodes(self): # If some nodes match the requested ids and some don't, only the # matching ones are returned. existing_id = factory.make_Node().system_id nonexistent_id = existing_id + factory.make_string() query = RequestFixture({"id": [existing_id, nonexistent_id]}, "id") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertItemsEqual([existing_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_zone_filters_by_zone(self): non_listed_node = factory.make_Node(zone=factory.make_Zone( name="twilight")) ignore_unused(non_listed_node) zone = factory.make_Zone() node = factory.make_Node(zone=zone) query = RequestFixture({"zone": zone.name}, "zone") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual([node.system_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_agent_name_filters_by_agent_name(self): non_listed_node = factory.make_Node( agent_name=factory.make_name("agent_name")) ignore_unused(non_listed_node) agent_name = factory.make_name("agent-name") node = factory.make_Node(agent_name=agent_name) query = RequestFixture({"agent_name": agent_name}, "agent_name") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual([node.system_id], extract_system_ids_from_nodes(node_list))
def test_node_list_with_hostname_returns_matching_nodes(self): # The list operation takes optional "hostname" parameters. Only nodes # with matching hostnames will be returned. nodes = [factory.make_Node() for _ in range(3)] matching_hostname = nodes[0].hostname matching_system_id = nodes[0].system_id query = RequestFixture({"hostname": [matching_hostname]}, "hostname") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertItemsEqual([matching_system_id], extract_system_ids_from_nodes(node_list))
def test_node_list_without_pool_does_not_filter(self): nodes = [ factory.make_Node(pool=factory.make_ResourcePool()) for _ in range(3) ] query = RequestFixture({}, "", self.user) node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual( [node.system_id for node in nodes], extract_system_ids_from_nodes(node_list), )
def test_node_list_without_agent_name_does_not_filter(self): nodes = [ factory.make_Node(agent_name=factory.make_name("agent-name")) for _ in range(3) ] query = RequestFixture({}, "") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertSequenceEqual( [node.system_id for node in nodes], extract_system_ids_from_nodes(node_list), )
def test_node_list_with_macs_returns_matching_nodes(self): # The "list" operation takes optional "mac_address" parameters. Only # nodes with matching MAC addresses will be returned. interfaces = [ factory.make_Interface(INTERFACE_TYPE.PHYSICAL) for _ in range(3) ] matching_mac = str(interfaces[0].mac_address) matching_system_id = interfaces[0].node.system_id query = RequestFixture({"mac_address": [matching_mac]}, "mac_address") node_list = nodes_module.filtered_nodes_list_from_request(query) self.assertItemsEqual([matching_system_id], extract_system_ids_from_nodes(node_list))
def test_node_lists_list_devices(self): machines = [ factory.make_Node(agent_name=factory.make_name('agent-name')) for _ in range(3) ] # Create devices. devices = [factory.make_Device() for _ in range(3)] query = RequestFixture({}, '') node_list = nodes_module.filtered_nodes_list_from_request(query) system_ids = extract_system_ids_from_nodes(node_list) self.assertEqual([node.system_id for node in machines + devices], system_ids, "Node listing doesn't contain devices.")
def test_node_list_with_ids_orders_by_id(self): # Even when ids are passed to "list," nodes are returned in id # order, not necessarily in the order of the id arguments. all_nodes = [factory.make_Node() for _ in range(3)] system_ids = [node.system_id for node in all_nodes] random.shuffle(system_ids) query = RequestFixture({"id": list(system_ids)}, "id") node_list = nodes_module.filtered_nodes_list_from_request(query) sorted_system_ids = [ node.system_id for node in sorted(all_nodes, key=lambda node: node.id) ] self.assertSequenceEqual(sorted_system_ids, extract_system_ids_from_nodes(node_list))
def query(self, request): """@description-title List node events @description List node events, optionally filtered by various criteria via URL query parameters. @param (string) "hostname" [required=false] An optional hostname. Only events relating to the node with the matching hostname will be returned. This can be specified multiple times to get events relating to more than one node. @param (string) "mac_address" [required=false] An optional list of MAC addresses. Only nodes with matching MAC addresses will be returned. @param (string) "id" [required=false] An optional list of system ids. Only nodes with matching system ids will be returned. @param (string) "zone" [required=false] An optional name for a physical zone. Only nodes in the zone will be returned. @param (string) "agent_name" [required=false] An optional agent name. Only nodes with matching agent names will be returned. @param (string) "level" [required=false] Desired minimum log level of returned events. Returns this level of events and greater. Choose from: %(log_levels)s. The default is INFO. @param (string) "limit" [required=false] Optional number of events to return. Default 100. Maximum: 1000. @param (string) "before" [required=false] Optional event id. Defines where to start returning older events. @param (string) "after" [required=false] Optional event id. Defines where to start returning newer events. @param (string) "owner" [required=false] If specified, filters the list to show only events owned by the specified username. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a list of events objects. @success-example "success-json" [exkey=events-query] placeholder text """ # Extract & validate optional parameters from the request. after = get_optional_param(request.GET, 'after', None, Int) before = get_optional_param(request.GET, 'before', None, Int) level = get_optional_param(request.GET, 'level', 'INFO') limit = get_optional_param( request.GET, "limit", DEFAULT_EVENT_LOG_LIMIT, Int) owner = get_optional_param(request.GET, 'owner', default=None) # Limit what we'll return to avoid being swamped. if limit > MAX_EVENT_LOG_COUNT: raise MAASAPIBadRequest(( "Requested number of events %d is greater than" " limit: %d") % (limit, MAX_EVENT_LOG_COUNT)) else: # The limit should never be less than 1. limit = 1 if limit < 1 else limit # Filter first by optional node ID, hostname, MAC, etc. nodes = filtered_nodes_list_from_request(request) # Event lists aren't supported on devices. nodes = nodes.exclude(node_type=NODE_TYPE.DEVICE) # Check first for AUDIT level. if level == LOGGING_LEVELS[AUDIT]: events = Event.objects.filter(type__level=AUDIT) elif level in LOGGING_LEVELS_BY_NAME: events = Event.objects.filter(node__in=nodes) # Eliminate logs below the requested level. events = events.exclude( type__level__lt=LOGGING_LEVELS_BY_NAME[level]) elif level is not None: raise MAASAPIBadRequest( "Unrecognised log level: %s" % level) events = ( events.all() .select_related('type') .select_related('node')) # Filter events for owner. if owner is not None: events = events.filter(username=owner) # Future feature: # This is where we would filter for events 'since last node deployment' # using a query param like since_last_deployed=true, but we aren't # right now because we don't currently record a timestamp of the last # deployment, and we don't have an event subtype for node status # changes to filter for the deploying status event. if after is None and before is None: # Get `limit` events, newest first. events = events.order_by('-id') events = events[:limit] elif after is None: # Get `limit` events, newest first, all before `before`. events = events.filter(id__lt=before) events = events.order_by('-id') events = events[:limit] elif before is None: # Get `limit` events, OLDEST first, all after `after`, then # reverse the results. events = events.filter(id__gt=after) events = events.order_by('id') events = reversed(events[:limit]) else: raise MAASAPIBadRequest( "There is undetermined behaviour when both " "`after` and `before` are specified.") # We need to load all of these events at some point, so save them # into a list now so that len() is cheap. events = list(events) # Helper for building prev_uri and next_uri. def make_uri(params, base=reverse('events_handler')): query = urllib.parse.urlencode(params, doseq=True) url = urllib.parse.urlparse(base)._replace(query=query) return url.geturl() # Figure out a URI to obtain a set of newer events. next_uri_params = get_overridden_query_dict( request.GET, {"before": []}, self.all_params) if len(events) == 0: if before is None: # There are no newer events NOW, but there may be later. next_uri = make_uri(next_uri_params) else: # Without limiting to `before`, we might find some more events. next_uri_params["after"] = before - 1 next_uri = make_uri(next_uri_params) else: # The first event is the newest. next_uri_params["after"] = str(events[0].id) next_uri = make_uri(next_uri_params) # Figure out a URI to obtain a set of older events. prev_uri_params = get_overridden_query_dict( request.GET, {"after": []}, self.all_params) if len(events) == 0: if after is None: # There are no older events and never will be. prev_uri = None else: # Without limiting to `after`, we might find some more events. prev_uri_params["before"] = after + 1 prev_uri = make_uri(prev_uri_params) else: # The last event is the oldest. prev_uri_params["before"] = str(events[-1].id) prev_uri = make_uri(prev_uri_params) return { "count": len(events), "events": [event_to_dict(event) for event in events], "next_uri": next_uri, "prev_uri": prev_uri, }