def filtered_nodes_list_from_request(request, model=None): """List Nodes visible to the user, optionally filtered by criteria. Nodes are sorted by id (i.e. most recent last). :param hostname: 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 mac_address: An optional MAC address. Only events relating to the node owning the specified MAC address will be returned. This can be specified multiple times to get events relating to more than one node. :param id: An optional list of system ids. Only events relating to the nodes with matching system ids will be returned. :param domain: An optional name for a dns domain. Only events relating to the nodes in the domain will be returned. :param zone: An optional name for a physical zone. Only events relating to the nodes in the zone will be returned. :param pool: An optional name for a resource pool. Only nodes belonging to the pool will be returned. :param agent_name: An optional agent name. Only events relating to the nodes with matching agent names will be returned. """ # Get filters from request. match_ids = get_optional_list(request.GET, "id") match_macs = get_optional_list(request.GET, "mac_address") if match_macs is not None: invalid_macs = [mac for mac in match_macs if MAC_RE.match(mac) is None] if len(invalid_macs) != 0: raise MAASAPIValidationError("Invalid MAC address(es): %s" % ", ".join(invalid_macs)) if model is None: model = Node # Fetch nodes and apply filters. nodes = model.objects.get_nodes(request.user, NodePermission.view, ids=match_ids) if match_macs is not None: nodes = nodes.filter(interface__mac_address__in=match_macs) match_hostnames = get_optional_list(request.GET, "hostname") if match_hostnames is not None: nodes = nodes.filter(hostname__in=match_hostnames) match_domains = get_optional_list(request.GET, "domain") if match_domains is not None: nodes = nodes.filter(domain__name__in=match_domains) match_zone_name = request.GET.get("zone", None) if match_zone_name is not None: nodes = nodes.filter(zone__name=match_zone_name) match_pool_name = request.GET.get("pool", None) if match_pool_name is not None: nodes = nodes.filter(pool__name=match_pool_name) match_agent_name = request.GET.get("agent_name", None) if match_agent_name is not None: nodes = nodes.filter(agent_name=match_agent_name) return nodes.order_by("id")
def list(self, request): """List Nodes visible to the user, optionally filtered by criteria. :param hostname: An optional list of hostnames. Only nodes with matching hostnames will be returned. :type hostname: iterable :param mac_address: An optional list of MAC addresses. Only nodes with matching MAC addresses will be returned. :type mac_address: iterable :param id: An optional list of system ids. Only nodes with matching system ids will be returned. :type id: iterable :param zone: An optional name for a physical zone. Only nodes in the zone will be returned. :type zone: unicode :param agent_name: An optional agent name. Only nodes with matching agent names will be returned. :type agent_name: unicode """ # Get filters from request. match_ids = get_optional_list(request.GET, 'id') match_macs = get_optional_list(request.GET, 'mac_address') if match_macs is not None: invalid_macs = [ mac for mac in match_macs if MAC_RE.match(mac) is None] if len(invalid_macs) != 0: raise ValidationError( "Invalid MAC address(es): %s" % ", ".join(invalid_macs)) # Fetch nodes and apply filters. nodes = Node.objects.get_nodes( request.user, NODE_PERMISSION.VIEW, ids=match_ids) if match_macs is not None: nodes = nodes.filter(macaddress__mac_address__in=match_macs) match_hostnames = get_optional_list(request.GET, 'hostname') if match_hostnames is not None: nodes = nodes.filter(hostname__in=match_hostnames) match_zone_name = request.GET.get('zone', None) if match_zone_name is not None: nodes = nodes.filter(zone__name=match_zone_name) match_agent_name = request.GET.get('agent_name', None) if match_agent_name is not None: nodes = nodes.filter(agent_name=match_agent_name) # Prefetch related objects that are needed for rendering the result. nodes = nodes.prefetch_related('macaddress_set__node') nodes = nodes.prefetch_related('macaddress_set__ip_addresses') nodes = nodes.prefetch_related('tags') nodes = nodes.select_related('nodegroup') nodes = nodes.prefetch_related('nodegroup__dhcplease_set') nodes = nodes.prefetch_related('nodegroup__nodegroupinterface_set') nodes = nodes.prefetch_related('zone') return nodes.order_by('id')
def set_zone(self, request): """@description-title Assign nodes to a zone @description Assigns a given node to a given zone. @param (string) "zone" [required=true] The zone name. @param (string) "nodes" [required=true] The node to add. @success (http-status-code) "204" 204 @error (http-status-code) "403" 403 @error (content) "no-perms" The user does not have set the zone. @error-example "no-perms" This method is reserved for admin users. @error (http-status-code) "400" 400 @error (content) "bad-param" The given parameters were not correct. """ data = { "zone": request.data.get("zone"), "system_id": get_optional_list(request.data, "nodes"), } form = BulkNodeSetZoneForm(request.user, data=data) if not form.is_valid(): raise MAASAPIValidationError(form.errors) form.save()
def power_parameters(self, request): """@description-title Get power parameters @description Get power parameters for multiple machines. To request power parameters for a specific machine or more than one machine: ``op=power_parameters&id=abc123&id=def456``. @param (url-string) "id" [required=false] A system ID. To request more than one machine, provide multiple ``id`` arguments in the request. Only machines with matching system ids will be returned. @param-example "id" op=power_parameters&id=abc123&id=def456 @success (http-status-code) "200" 200 @success (json) "success_json" A JSON object containing a list of power parameters with system_ids as keys. @success-example "success_json" [exkey=get-power-params] placeholder text @error (http-status-code) "403" 403 @error (content) "no-perms" The user is not authorized to view the power parameters. """ match_ids = get_optional_list(request.GET, "id") if match_ids is None: machines = self.base_model.objects.all() else: machines = self.base_model.objects.filter(system_id__in=match_ids) return { machine.system_id: machine.power_parameters for machine in machines }
def set_zone(self, request): """Assign multiple nodes to a physical zone at once. :param zone: Zone name. If omitted, the zone is "none" and the nodes will be taken out of their physical zones. :param nodes: system_ids of the nodes whose zones are to be set. (An empty list is acceptable). Raises 403 if the user is not an admin. """ data = { 'action': 'set_zone', 'zone': request.data.get('zone'), 'system_id': get_optional_list(request.data, 'nodes'), } form = BulkNodeActionForm(request.user, data=data) if not form.is_valid(): raise MAASAPIValidationError(form.errors) form.save()
def power_parameters(self, request): """Retrieve power parameters for multiple nodes. :param id: An optional list of system ids. Only nodes with matching system ids will be returned. :type id: iterable :return: A dictionary of power parameters, keyed by node system_id. Raises 403 if the user is not an admin. """ match_ids = get_optional_list(request.GET, 'id') if match_ids is None: nodes = Node.objects.all() else: nodes = Node.objects.filter(system_id__in=match_ids) return {node.system_id: node.power_parameters for node in nodes}
def set_zone(self, request): """Assign multiple nodes to a physical zone at once. :param zone: Zone name. If omitted, the zone is "none" and the nodes will be taken out of their physical zones. :param nodes: system_ids of the nodes whose zones are to be set. (An empty list is acceptable). Raises 403 if the user is not an admin. """ data = { 'action': 'set_zone', 'zone': request.data.get('zone'), 'system_id': get_optional_list(request.data, 'nodes'), } form = BulkNodeActionForm(request.user, data=data) if not form.is_valid(): raise ValidationError(form.errors) form.save()
def power_parameters(self, request): """Retrieve power parameters for multiple machines. :param id: An optional list of system ids. Only machines with matching system ids will be returned. :type id: iterable :return: A dictionary of power parameters, keyed by machine system_id. Raises 403 if the user is not an admin. """ match_ids = get_optional_list(request.GET, 'id') if match_ids is None: machines = self.base_model.objects.all() else: machines = self.base_model.objects.filter(system_id__in=match_ids) return {machine.system_id: machine.power_parameters for machine in machines}
def scan(self, request, **kwargs): """@description-title Run discovery scan on rack networks @description Immediately run a neighbour discovery scan on all rack networks. This command causes each connected rack controller to execute the 'maas-rack scan-network' command, which will scan all CIDRs configured on the rack controller using 'nmap' (if it is installed) or 'ping'. Network discovery must not be set to 'disabled' for this command to be useful. Scanning will be started in the background, and could take a long time on rack controllers that do not have 'nmap' installed and are connected to large networks. If the call is a success, this method will return a dictionary of results with the following keys: ``result``: A human-readable string summarizing the results. ``scan_attempted_on``: A list of rack system_id values where a scan was attempted. (That is, an RPC connection was successful and a subsequent call was intended.) ``failed_to_connect_to``: A list of rack system_id values where the RPC connection failed. ``scan_started_on``: A list of rack system_id values where a scan was successfully started. ``scan_failed_on``: A list of rack system_id values where a scan was attempted, but failed because a scan was already in progress. ``rpc_call_timed_out_on``: A list of rack system_id values where the RPC connection was made, but the call timed out before a ten second timeout elapsed. @param (string) "cidr" [required=false] The subnet CIDR(s) to scan (can be specified multiple times). If not specified, defaults to all networks. @param (boolean) "force" [required=false] If True, will force the scan, even if all networks are specified. (This may not be the best idea, depending on acceptable use agreements, and the politics of the organization that owns the network.) Note that this parameter is required if all networks are specified. Default: False. @param (string) "always_use_ping" [required=false] If True, will force the scan to use 'ping' even if 'nmap' is installed. Default: False. @param (string) "slow" [required=false] If True, and 'nmap' is being used, will limit the scan to nine packets per second. If the scanner is 'ping', this option has no effect. Default: False. @param (string) "threads" [required=false] The number of threads to use during scanning. If 'nmap' is the scanner, the default is one thread per 'nmap' process. If 'ping' is the scanner, the default is four threads per CPU. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a dictionary of results. @success-example "success-json" [exkey=discovery-scan] placeholder text """ cidrs = get_optional_list(request.POST, 'cidr', default=[], validator=CIDR) # The RPC call requires a list of CIDRs. ipnetworks = [IPNetwork(cidr) for cidr in cidrs] force = get_optional_param(request.POST, 'force', default=False, validator=StringBool) always_use_ping = get_optional_param(request.POST, 'always_use_ping', default=False, validator=StringBool) slow = get_optional_param(request.POST, 'slow', default=False, validator=StringBool) threads = get_optional_param(request.POST, 'threads', default=None, validator=Number) if threads is not None: threads = int(threads) # Could be a floating point. if len(cidrs) == 0 and force is not True: error = ( "Bad request: scanning all subnets is not allowed unless " "force=True is specified.\n\n**WARNING: This will scan ALL " "networks attached to MAAS rack controllers.\nCheck with your " "internet service provider or IT department to be sure this " "is\nallowed before proceeding.**\n") return HttpResponseBadRequest(content_type="text/plain", content=error) elif len(cidrs) == 0 and force is True: # No CIDRs specified and force==True, so scan all networks. results = scan_all_rack_networks(scan_all=True, ping=always_use_ping, slow=slow, threads=threads) else: results = scan_all_rack_networks(cidrs=ipnetworks, ping=always_use_ping, slow=slow, threads=threads) return user_friendly_scan_results(results)
def read(self, request): """@description-title Read commissioning results @description Read the commissioning results per node visible to the user, optionally filtered. @param (string) "system_id" [required=false] An optional list of system ids. Only the results related to the nodes with these system ids will be returned. @param (string) "name" [required=false] An optional list of names. Only the results with the specified names will be returned. @param (string) "result_type" [required=false] An optional result_type. Only the results with the specified result_type will be returned. @success (http-status-code) "server-success" 200 @success (json) "success-json" A JSON object containing a list of the requested installation-result objects. @success-example "success-json" [exkey=installation-results] placeholder text """ # Get filters from request. system_ids = get_optional_list(request.GET, "system_id") names = get_optional_list(request.GET, "name") result_type = get_optional_param(request.GET, "result_type", None, Int) nodes = Node.objects.get_nodes(request.user, NodePermission.view, ids=system_ids) script_sets = [] for node in nodes: if node.current_commissioning_script_set is not None: script_sets.append(node.current_commissioning_script_set) if node.current_installation_script_set is not None: script_sets.append(node.current_installation_script_set) if node.current_testing_script_set is not None: script_sets.append(node.current_testing_script_set) if names is not None: # Convert to a set; it's used for membership testing. names = set(names) resource_uri = reverse("commissioning_scripts_handler") results = [] for script_set in script_sets: if (result_type is not None and script_set.result_type != result_type): continue for script_result in script_set.scriptresult_set.filter( status__in=( SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED, )): if names is not None and script_result.name not in names: continue # MAAS stores stdout, stderr, and the combined output. The # metadata API determine which field uploaded data should go # into based on the extention of the uploaded file. .out goes # to stdout, .err goes to stderr, otherwise its assumed the # data is combined. Curtin uploads the installation log as # install.log so its stored as a combined result. This ensures # a result is always returned. if script_result.stdout != b"": data = b64encode(script_result.stdout) else: data = b64encode(script_result.output) results.append({ "created": script_result.created, "updated": script_result.updated, "id": script_result.id, "name": script_result.name, "script_result": script_result.exit_status, "result_type": script_set.result_type, "node": { "system_id": script_set.node.system_id }, "data": data, "resource_uri": resource_uri, }) if script_result.stderr != b"": results.append({ "created": script_result.created, "updated": script_result.updated, "id": script_result.id, "name": "%s.err" % script_result.name, "script_result": script_result.exit_status, "result_type": script_set.result_type, "node": { "system_id": script_set.node.system_id }, "data": b64encode(script_result.stderr), "resource_uri": resource_uri, }) return results
def read(self, request): """List NodeResult visible to the user, optionally filtered. :param system_id: An optional list of system ids. Only the results related to the nodes with these system ids will be returned. :type system_id: iterable :param name: An optional list of names. Only the results with the specified names will be returned. :type name: iterable :param result_type: An optional result_type. Only the results with the specified result_type will be returned. :type name: iterable """ # Get filters from request. system_ids = get_optional_list(request.GET, 'system_id') names = get_optional_list(request.GET, 'name') result_type = get_optional_param(request.GET, 'result_type', None, Int) nodes = Node.objects.get_nodes(request.user, NODE_PERMISSION.VIEW, ids=system_ids) script_sets = [] for node in nodes: if node.current_commissioning_script_set is not None: script_sets.append(node.current_commissioning_script_set) if node.current_installation_script_set is not None: script_sets.append(node.current_installation_script_set) if node.current_testing_script_set is not None: script_sets.append(node.current_testing_script_set) if names is not None: # Convert to a set; it's used for membership testing. names = set(names) resource_uri = reverse('commissioning_scripts_handler') results = [] for script_set in script_sets: if (result_type is not None and script_set.result_type != result_type): continue for script_result in script_set.scriptresult_set.filter( status__in=(SCRIPT_STATUS.PASSED, SCRIPT_STATUS.FAILED, SCRIPT_STATUS.TIMEDOUT, SCRIPT_STATUS.ABORTED)): if names is not None and script_result.name not in names: continue # MAAS stores stdout, stderr, and the combined output. The # metadata API determine which field uploaded data should go # into based on the extention of the uploaded file. .out goes # to stdout, .err goes to stderr, otherwise its assumed the # data is combined. Curtin uploads the installation log as # install.log so its stored as a combined result. This ensures # a result is always returned. if script_result.stdout != b'': data = b64encode(script_result.stdout) else: data = b64encode(script_result.output) results.append({ 'created': script_result.created, 'updated': script_result.updated, 'id': script_result.id, 'name': script_result.name, 'script_result': script_result.exit_status, 'result_type': script_set.result_type, 'node': { 'system_id': script_set.node.system_id }, 'data': data, 'resource_uri': resource_uri, }) if script_result.stderr != b'': results.append({ 'created': script_result.created, 'updated': script_result.updated, 'id': script_result.id, 'name': '%s.err' % script_result.name, 'script_result': script_result.exit_status, 'result_type': script_set.result_type, 'node': { 'system_id': script_set.node.system_id }, 'data': b64encode(script_result.stderr), 'resource_uri': resource_uri, }) return results
def list_allocated(self, request): """Fetch Nodes that were allocated to the User/oauth token.""" token = get_oauth_token(request) match_ids = get_optional_list(request.GET, 'id') nodes = Node.objects.get_allocated_visible_nodes(token, match_ids) return nodes.order_by('id')