class ReadNodesForm(FilterNodeForm): id = UnconstrainedMultipleChoiceField(label="System IDs to filter on", required=False) hostname = UnconstrainedMultipleChoiceField(label="Hostnames to filter on", required=False) mac_address = ValidatorMultipleChoiceField( validator=mac_validator, label="MAC addresses to filter on", required=False, error_messages={ "invalid_list": "Invalid parameter: invalid MAC address format" }, ) domain = UnconstrainedMultipleChoiceField( label="Domain names to filter on", required=False) agent_name = forms.CharField( label="Only include nodes with events matching the agent name", required=False, ) status = forms.ChoiceField( label="Only includes nodes with the specified status", choices=NODE_STATUS_SHORT_LABEL_CHOICES, required=False, ) def _apply_filters(self, nodes): nodes = super()._apply_filters(nodes) nodes = self.filter_by_ids(nodes) nodes = self.filter_by_hostnames(nodes) nodes = self.filter_by_mac_addresses(nodes) nodes = self.filter_by_domain(nodes) nodes = self.filter_by_agent_name(nodes) nodes = self.filter_by_status(nodes) return nodes def filter_by_ids(self, filtered_nodes): ids = self.cleaned_data.get(self.get_field_name("id")) if ids: filtered_nodes = filtered_nodes.filter(system_id__in=ids) return filtered_nodes def filter_by_hostnames(self, filtered_nodes): hostnames = self.cleaned_data.get(self.get_field_name("hostname")) if hostnames: filtered_nodes = filtered_nodes.filter(hostname__in=hostnames) return filtered_nodes def filter_by_mac_addresses(self, filtered_nodes): mac_addresses = self.cleaned_data.get( self.get_field_name("mac_address")) if mac_addresses: filtered_nodes = filtered_nodes.filter( interface__mac_address__in=mac_addresses) return filtered_nodes def filter_by_domain(self, filtered_nodes): domains = self.cleaned_data.get(self.get_field_name("domain")) if domains: filtered_nodes = filtered_nodes.filter(domain__name__in=domains) return filtered_nodes def filter_by_agent_name(self, filtered_nodes): field_name = self.get_field_name("agent_name") if field_name in self.data: agent_name = self.cleaned_data.get(field_name) filtered_nodes = filtered_nodes.filter(agent_name=agent_name) return filtered_nodes def filter_by_status(self, filtered_nodes): status = self.cleaned_data.get(self.get_field_name("status")) if status: status_id = getattr(NODE_STATUS, status.upper()) filtered_nodes = filtered_nodes.filter(status=status_id) return filtered_nodes
class FilterNodeForm(RenamableFieldsForm): """A form for filtering nodes.""" # This becomes a multiple-choice field during cleaning, to accommodate # architecture wildcards. arch = forms.CharField(label="Architecture", required=False) tags = UnconstrainedMultipleChoiceField(label="Tags", required=False) not_tags = UnconstrainedMultipleChoiceField(label="Not having tags", required=False) # XXX mpontillo 2015-10-30 need validators for fabric constraints fabrics = ValidatorMultipleChoiceField( validator=lambda x: True, label="Attached to fabrics", required=False, error_messages={ "invalid_list": "Invalid parameter: list of fabrics required." }, ) not_fabrics = ValidatorMultipleChoiceField( validator=lambda x: True, label="Not attached to fabrics", required=False, error_messages={ "invalid_list": "Invalid parameter: list of fabrics required." }, ) fabric_classes = ValidatorMultipleChoiceField( validator=lambda x: True, label="Attached to fabric with specified classes", required=False, error_messages={ "invalid_list": "Invalid parameter: list of fabric classes required." }, ) not_fabric_classes = ValidatorMultipleChoiceField( validator=lambda x: True, label="Not attached to fabric with specified classes", required=False, error_messages={ "invalid_list": "Invalid parameter: list of fabric classes required." }, ) subnets = ValidatorMultipleChoiceField( validator=Subnet.objects.validate_filter_specifiers, label="Attached to subnets", required=False, error_messages={ "invalid_list": "Invalid parameter: list of subnet specifiers required." }, ) not_subnets = ValidatorMultipleChoiceField( validator=Subnet.objects.validate_filter_specifiers, label="Not attached to subnets", required=False, error_messages={ "invalid_list": "Invalid parameter: list of subnet specifiers required." }, ) link_speed = forms.FloatField( label="Link speed", required=False, error_messages={"invalid": "Invalid link speed: number required."}, ) vlans = ValidatorMultipleChoiceField( validator=VLAN.objects.validate_filter_specifiers, label="Attached to VLANs", required=False, error_messages={ "invalid_list": "Invalid parameter: list of VLAN specifiers required." }, ) not_vlans = ValidatorMultipleChoiceField( validator=VLAN.objects.validate_filter_specifiers, label="Not attached to VLANs", required=False, error_messages={ "invalid_list": "Invalid parameter: list of VLAN specifiers required." }, ) connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Connected to", required=False, error_messages={ "invalid_list": "Invalid parameter: list of MAC addresses required." }, ) not_connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Not connected to", required=False, error_messages={ "invalid_list": "Invalid parameter: list of MAC addresses required." }, ) zone = forms.CharField(label="Physical zone", required=False) not_in_zone = ValidatorMultipleChoiceField( validator=MODEL_NAME_VALIDATOR, label="Not in zone", required=False, error_messages={ "invalid_list": "Invalid parameter: must list physical zones." }, ) pool = forms.CharField(label="Resource pool", required=False) not_in_pool = ValidatorMultipleChoiceField( validator=MODEL_NAME_VALIDATOR, label="Not in resource pool", required=False, error_messages={ "invalid_list": "Invalid parameter: must list resource pools." }, ) storage = forms.CharField(validators=[storage_validator], label="Storage", required=False) interfaces = LabeledConstraintMapField(validators=[interfaces_validator], label="Interfaces", required=False) cpu_count = forms.FloatField( label="CPU count", required=False, error_messages={"invalid": "Invalid CPU count: number required."}, ) mem = forms.FloatField( label="Memory", required=False, error_messages={"invalid": "Invalid memory: number of MiB required."}, ) pod = forms.CharField(label="The name of the desired pod", required=False) not_pod = forms.CharField(label="The name of the undesired pod", required=False) pod_type = forms.CharField(label="The power_type of the desired pod", required=False) not_pod_type = forms.CharField(label="The power_type of the undesired pod", required=False) ignore_unknown_constraints = False @classmethod def Strict(cls, *args, **kwargs): """A stricter version of the form which rejects unknown parameters.""" form = cls(*args, **kwargs) form.ignore_unknown_constraints = False return form def clean_arch(self): """Turn `arch` parameter into a list of architecture names. Even though `arch` is a single-value field, it turns into a list during cleaning. The architecture parameter may be a wildcard. """ # Import list_all_usable_architectures as part of its module, not # directly, so that patch_usable_architectures can easily patch it # for testing purposes. usable_architectures = maasserver_forms.list_all_usable_architectures() architecture_wildcards = get_architecture_wildcards( usable_architectures) value = self.cleaned_data[self.get_field_name("arch")] if value: if value in usable_architectures: # Full 'arch/subarch' specified directly. return [value] elif value in architecture_wildcards: # Try to expand 'arch' to all available 'arch/subarch' # matches. return architecture_wildcards[value] set_form_error( self, self.get_field_name("arch"), "Architecture not recognised.", ) return None def clean_tags(self): value = self.cleaned_data[self.get_field_name("tags")] if value: tag_names = parse_legacy_tags(value) # Validate tags. tag_names = set(tag_names) db_tag_names = set( Tag.objects.filter(name__in=tag_names).values_list("name", flat=True)) if len(tag_names) != len(db_tag_names): unknown_tags = tag_names.difference(db_tag_names) error_msg = "No such tag(s): %s." % ", ".join( "'%s'" % tag for tag in unknown_tags) set_form_error(self, self.get_field_name("tags"), error_msg) return None return tag_names return None def clean_zone(self): value = self.cleaned_data[self.get_field_name("zone")] if value: nonexistent_names = detect_nonexistent_names(Zone, [value]) if nonexistent_names: error_msg = "No such zone: '%s'." % value set_form_error(self, self.get_field_name("zone"), error_msg) return None return value return None def clean_not_in_zone(self): value = self.cleaned_data[self.get_field_name("not_in_zone")] if not value: return None nonexistent_names = detect_nonexistent_names(Zone, value) if nonexistent_names: error_msg = "No such zone(s): %s." % ", ".join(nonexistent_names) set_form_error(self, self.get_field_name("not_in_zone"), error_msg) return None return value def clean_pool(self): value = self.cleaned_data[self.get_field_name("pool")] if value: nonexistent_names = detect_nonexistent_names(ResourcePool, [value]) if nonexistent_names: error_msg = "No such pool: '%s'." % value set_form_error(self, self.get_field_name("pool"), error_msg) return None return value return None def clean_not_in_pool(self): value = self.cleaned_data[self.get_field_name("not_in_pool")] if not value: return None nonexistent_names = detect_nonexistent_names(ResourcePool, value) if nonexistent_names: error_msg = "No such pool(s): %s." % ", ".join(nonexistent_names) set_form_error(self, self.get_field_name("not_in_pool"), error_msg) return None return value def _clean_specifiers(self, model, specifiers): if not specifiers: return None objects = set(model.objects.filter_by_specifiers(specifiers)) if len(objects) == 0: raise ValidationError("No matching %s found." % model._meta.verbose_name_plural) return objects def clean_subnets(self): value = self.cleaned_data[self.get_field_name("subnets")] return self._clean_specifiers(Subnet, value) def clean_not_subnets(self): value = self.cleaned_data[self.get_field_name("not_subnets")] return self._clean_specifiers(Subnet, value) def clean_vlans(self): value = self.cleaned_data[self.get_field_name("vlans")] return self._clean_specifiers(VLAN, value) def clean_not_vlans(self): value = self.cleaned_data[self.get_field_name("not_vlans")] return self._clean_specifiers(VLAN, value) def clean(self): if not self.ignore_unknown_constraints: unknown_constraints = set(self.data).difference( set(self.field_mapping.values())) for constraint in unknown_constraints: if constraint not in IGNORED_FIELDS: msg = "No such constraint." self._errors[constraint] = self.error_class([msg]) return super().clean() def describe_constraint(self, field_name): """Return a human-readable representation of a constraint. Turns a constraint value as passed to the form into a Juju-like representation for display: `name=foo`. Multi-valued constraints are shown as comma-separated values, e.g. `tags=do,re,mi`. :param field_name: Name of the constraint on this form, e.g. `zone`. :return: A constraint string, or `None` if the constraint is not set. """ value = self.cleaned_data.get(field_name, None) if value is None: return None if isinstance(self.fields[field_name], MultipleChoiceField): output = describe_multi_constraint_value(value) elif field_name == "arch" and not isinstance(value, str): # The arch field is a special case. It's defined as a string # field, but may become a list/tuple/... of strings in cleaning. output = describe_multi_constraint_value(value) else: output = describe_single_constraint_value(value) if output is None: return None else: return "%s=%s" % (field_name, output) def describe_constraints(self): """Return a human-readable representation of the given constraints. The description is Juju-like, e.g. `arch=amd64 cpu=16 zone=rack3`. Constraints are listed in alphabetical order. """ constraints = (self.describe_constraint(name) for name in sorted(self.fields.keys())) return " ".join(constraint for constraint in constraints if constraint is not None) def filter_nodes(self, nodes): """Return the subset of nodes that match the form's constraints. :param nodes: The set of nodes on which the form should apply constraints. :type nodes: `django.db.models.query.QuerySet` :return: A QuerySet of the nodes that match the form's constraints. :rtype: `django.db.models.query.QuerySet` """ filtered_nodes = self._apply_filters(nodes) compatible_nodes, filtered_nodes = self.filter_by_storage( filtered_nodes) compatible_interfaces, filtered_nodes = self.filter_by_interfaces( filtered_nodes) return filtered_nodes, compatible_nodes, compatible_interfaces def _apply_filters(self, nodes): nodes = self.filter_by_arch(nodes) nodes = self.filter_by_tags(nodes) nodes = self.filter_by_zone(nodes) nodes = self.filter_by_pool(nodes) nodes = self.filter_by_subnets(nodes) nodes = self.filter_by_link_speed(nodes) nodes = self.filter_by_vlans(nodes) nodes = self.filter_by_fabrics(nodes) nodes = self.filter_by_fabric_classes(nodes) nodes = self.filter_by_cpu_count(nodes) nodes = self.filter_by_mem(nodes) nodes = self.filter_by_pod_or_pod_type(nodes) return nodes.distinct() def filter_by_interfaces(self, filtered_nodes): compatible_interfaces = {} interfaces_label_map = self.cleaned_data.get( self.get_field_name("interfaces")) if interfaces_label_map is not None: result = nodes_by_interface(interfaces_label_map) if result.node_ids is not None: filtered_nodes = filtered_nodes.filter(id__in=result.node_ids) compatible_interfaces = result.label_map return compatible_interfaces, filtered_nodes def filter_by_storage(self, filtered_nodes): compatible_nodes = {} # Maps node/storage to named storage constraints storage = self.cleaned_data.get(self.get_field_name("storage")) if storage: compatible_nodes = nodes_by_storage(storage) node_ids = list(compatible_nodes) if node_ids is not None: filtered_nodes = filtered_nodes.filter(id__in=node_ids) return compatible_nodes, filtered_nodes def filter_by_fabric_classes(self, filtered_nodes): fabric_classes = self.cleaned_data.get( self.get_field_name("fabric_classes")) if fabric_classes is not None and len(fabric_classes) > 0: filtered_nodes = filtered_nodes.filter( interface__vlan__fabric__class_type__in=fabric_classes) not_fabric_classes = self.cleaned_data.get( self.get_field_name("not_fabric_classes")) if not_fabric_classes is not None and len(not_fabric_classes) > 0: filtered_nodes = filtered_nodes.exclude( interface__vlan__fabric__class_type__in=not_fabric_classes) return filtered_nodes def filter_by_fabrics(self, filtered_nodes): fabrics = self.cleaned_data.get(self.get_field_name("fabrics")) if fabrics is not None and len(fabrics) > 0: # XXX mpontillo 2015-10-30 need to also handle fabrics whose name # is null (fabric-<id>). filtered_nodes = filtered_nodes.filter( interface__vlan__fabric__name__in=fabrics) not_fabrics = self.cleaned_data.get(self.get_field_name("not_fabrics")) if not_fabrics is not None and len(not_fabrics) > 0: # XXX mpontillo 2015-10-30 need to also handle fabrics whose name # is null (fabric-<id>). filtered_nodes = filtered_nodes.exclude( interface__vlan__fabric__name__in=not_fabrics) return filtered_nodes def filter_by_vlans(self, filtered_nodes): vlans = self.cleaned_data.get(self.get_field_name("vlans")) if vlans is not None and len(vlans) > 0: for vlan in set(vlans): filtered_nodes = filtered_nodes.filter(interface__vlan=vlan) not_vlans = self.cleaned_data.get(self.get_field_name("not_vlans")) if not_vlans is not None and len(not_vlans) > 0: for not_vlan in set(not_vlans): filtered_nodes = filtered_nodes.exclude( interface__vlan=not_vlan) return filtered_nodes def filter_by_subnets(self, filtered_nodes): subnets = self.cleaned_data.get(self.get_field_name("subnets")) if subnets is not None and len(subnets) > 0: for subnet in set(subnets): filtered_nodes = filtered_nodes.filter( interface__ip_addresses__subnet=subnet) not_subnets = self.cleaned_data.get(self.get_field_name("not_subnets")) if not_subnets is not None and len(not_subnets) > 0: for not_subnet in set(not_subnets): filtered_nodes = filtered_nodes.exclude( interface__ip_addresses__subnet=not_subnet) return filtered_nodes def filter_by_link_speed(self, filtered_nodes): link_speed = self.cleaned_data.get(self.get_field_name("link_speed")) if link_speed: filtered_nodes = filtered_nodes.filter( interface__link_speed__gte=link_speed) return filtered_nodes def filter_by_zone(self, filtered_nodes): zone = self.cleaned_data.get(self.get_field_name("zone")) if zone: zone_obj = Zone.objects.get(name=zone) filtered_nodes = filtered_nodes.filter(zone=zone_obj) not_in_zone = self.cleaned_data.get(self.get_field_name("not_in_zone")) if not_in_zone: not_in_zones = Zone.objects.filter(name__in=not_in_zone) filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) return filtered_nodes def filter_by_pool(self, filtered_nodes): pool_name = self.cleaned_data.get(self.get_field_name("pool")) if pool_name: pool = ResourcePool.objects.get(name=pool_name) filtered_nodes = filtered_nodes.filter(pool=pool) not_in_pool = self.cleaned_data.get(self.get_field_name("not_in_pool")) if not_in_pool: pools_to_exclude = ResourcePool.objects.filter( name__in=not_in_pool) filtered_nodes = filtered_nodes.exclude(pool__in=pools_to_exclude) return filtered_nodes def filter_by_tags(self, filtered_nodes): tags = self.cleaned_data.get(self.get_field_name("tags")) if tags: for tag in tags: filtered_nodes = filtered_nodes.filter(tags__name=tag) not_tags = self.cleaned_data.get(self.get_field_name("not_tags")) if len(not_tags) > 0: for not_tag in not_tags: filtered_nodes = filtered_nodes.exclude(tags__name=not_tag) return filtered_nodes def filter_by_mem(self, filtered_nodes): mem = self.cleaned_data.get(self.get_field_name("mem")) if mem: filtered_nodes = filtered_nodes.filter(memory__gte=mem) return filtered_nodes def filter_by_cpu_count(self, filtered_nodes): cpu_count = self.cleaned_data.get(self.get_field_name("cpu_count")) if cpu_count: filtered_nodes = filtered_nodes.filter(cpu_count__gte=cpu_count) return filtered_nodes def filter_by_arch(self, filtered_nodes): arch = self.cleaned_data.get(self.get_field_name("arch")) if arch: filtered_nodes = filtered_nodes.filter(architecture__in=arch) return filtered_nodes def filter_by_system_id(self, filtered_nodes): # Filter by system_id. system_id = self.cleaned_data.get(self.get_field_name("system_id")) if system_id: filtered_nodes = filtered_nodes.filter(system_id=system_id) return filtered_nodes def filter_by_pod_or_pod_type(self, filtered_nodes): # Filter by pod, pod type, not_pod or not_pod_type. # We are filtering for all of these to keep the query count down. pod = self.cleaned_data.get(self.get_field_name("pod")) not_pod = self.cleaned_data.get(self.get_field_name("not_pod")) pod_type = self.cleaned_data.get(self.get_field_name("pod_type")) not_pod_type = self.cleaned_data.get( self.get_field_name("not_pod_type")) if pod or pod_type or not_pod or not_pod_type: pods = Pod.objects.all() if pod: pods = pods.filter(name=pod) if not pod: pods = pods.exclude(name=not_pod) if pod_type: pods = pods.filter(power_type=pod_type) if not_pod_type: pods = pods.exclude(power_type=not_pod_type) filtered_nodes = filtered_nodes.filter( bmc_id__in=pods.values_list("id", flat=True)) return filtered_nodes.distinct()
class PackageRepositoryForm(MAASModelForm): """Package Repository creation/edition form.""" class Meta: model = PackageRepository fields = ( 'name', 'url', 'distributions', 'disabled_pockets', 'disabled_components', 'components', 'arches', 'key', 'enabled', ) name = forms.CharField(label="Name", required=True, help_text=("The name of the Package Repository.")) url = URLOrPPAFormField(label="Package Repository URL", required=True, help_text="The Package Repository URL") # Use UnconstrainedMultipleChoiceField fields for multiple-choices # fields instead of the default (djorm-ext-pgarray's ArrayFormField): # ArrayFormField deals with comma-separated lists and here we want to # handle multiple-values submissions. distributions = UnconstrainedMultipleChoiceField(label="Distribution list") disabled_pockets = UnconstrainedMultipleChoiceField( label="Disabled Pocket list") disabled_components = UnconstrainedMultipleChoiceField( label="Disabled Component list") components = UnconstrainedMultipleChoiceField(label="Component list") arches = UnconstrainedMultipleChoiceField(label="Architecture list") key = forms.CharField( label="Key", required=False, help_text=("The key used to authenticate the Package Repository.")) enabled = forms.BooleanField( label="Enabled", required=False, help_text=("Whether or not the Package Repository is enabled.")) def __init__(self, data=None, instance=None, request=None, **kwargs): super().__init__(data=data, instance=instance, **kwargs) if self.instance.id is None: self.initial['enabled'] = True else: self.fields['url'].initial = self.instance.url def clean(self): cleaned_data = super().clean() if self.instance.default and not cleaned_data.get('enabled', False): raise ValidationError("Default repositories may not be disabled.") def clean_arches(self): arches = [] for value in self.cleaned_data.get('arches', []): arches.extend([s.strip() for s in value.split(',')]) known_arches = set(PackageRepository.objects.get_known_architectures()) for value in arches: if value not in known_arches: raise ValidationError( "'%s' is not a valid architecture. Known architectures: " "%s" % (value, ", ".join(sorted(known_arches)))) # If no arches provided, use MAIN_ARCHES. if len(arches) == 0: arches = PackageRepository.MAIN_ARCHES return arches def clean_distributions(self): values = [] for value in self.cleaned_data.get('distributions', []): values.extend([s.strip() for s in value.split(',')]) return values def clean_disabled_pockets(self): values = [] for value in self.cleaned_data.get('disabled_pockets', []): values.extend([s.strip() for s in value.split(',')]) # This allows to reset the values of disabled_pockets if one of the # following is passed over the API: # disabled_pockets= # disabled_pockets='' # disabled_pockets=None # disabled_pockets=[] if values == [''] or values == ['None'] or values == ['[]']: return [] # Check that a valid pocket is being disable. for pocket in values: if pocket not in PackageRepository.POCKETS_TO_DISABLE: raise ValidationError( "'%s' is not a valid Ubuntu archive pocket. You " "can only disable %s." % (pocket, PackageRepository.POCKETS_TO_DISABLE)) return values def clean_disabled_components(self): values = [] for value in self.cleaned_data.get('disabled_components', []): values.extend([s.strip() for s in value.split(',')]) # This allows to reset the values of disabled_components if one of the # following is passed over the API: # disabled_components= # disabled_components='' # disabled_components=None # disabled_components=[] if values == [''] or values == ['None'] or values == ['[]']: return [] if self.instance is not None and not self.instance.default and values: raise ValidationError( "This is a custom repository. Please update 'components' " "instead.") # Check that a valid component is being passed. for component in values: if component not in PackageRepository.COMPONENTS_TO_DISABLE: raise ValidationError( "'%s' is not a valid Ubuntu archive component. You " "can only disable %s." % (component, PackageRepository.COMPONENTS_TO_DISABLE)) return values def clean_components(self): values = [] for value in self.cleaned_data.get('components', []): values.extend([s.strip() for s in value.split(',')]) if self.instance is not None and self.instance.default and values: raise ValidationError( "This is a default Ubuntu repository. Please update " "'disabled_components' instead.") return values
class AcquireNodeForm(RenamableFieldsForm): """A form handling the constraints used to acquire a node.""" name = forms.CharField(label="Host name", required=False) arch = forms.CharField(label="Architecture", required=False) cpu_count = forms.FloatField( label="CPU count", required=False, error_messages={'invalid': "Invalid CPU count: number required."}) mem = forms.FloatField( label="Memory", required=False, error_messages={'invalid': "Invalid memory: number of MB required."}) tags = UnconstrainedMultipleChoiceField(label="Tags", required=False) connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Connected to", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of MAC addresses required." }) not_connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Not connected to", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of MAC addresses required." }) zone = forms.CharField(label="Availability zone", required=False) ignore_unknown_constraints = True @classmethod def Strict(cls, *args, **kwargs): """A stricter version of the form which rejects unknown parameters.""" form = cls(*args, **kwargs) form.ignore_unknown_constraints = False return form def clean_arch(self): value = self.cleaned_data[self.get_field_name('arch')] if value: if value in ARCHITECTURE_CHOICES_DICT: # Full 'arch/subarch' specified directly. return [value] elif value in architecture_wildcards: # Try to expand 'arch' to all available 'arch/subarch' # matches. return architecture_wildcards[value] raise ValidationError({ self.get_field_name('arch'): ['Architecture not recognised.'] }) return None def clean_tags(self): value = self.cleaned_data[self.get_field_name('tags')] if value: tag_names = parse_legacy_tags(value) # Validate tags. tag_names = set(tag_names) db_tag_names = set( Tag.objects.filter(name__in=tag_names).values_list('name', flat=True)) if len(tag_names) != len(db_tag_names): unknown_tags = tag_names.difference(db_tag_names) error_msg = 'No such tag(s): %s.' % ', '.join( "'%s'" % tag for tag in unknown_tags) raise ValidationError( {self.get_field_name('arch'): [error_msg]}) return tag_names return None def clean_zone(self): value = self.cleaned_data[self.get_field_name('zone')] if value: zone_names = Zone.objects.all().values_list('name', flat=True) if value not in zone_names: error_msg = "No such zone: '%s'." % value raise ValidationError( {self.get_field_name('zone'): [error_msg]}) return value return None def clean(self): if not self.ignore_unknown_constraints: unknown_constraints = set(self.data).difference( set(self.field_mapping.values())) for constraint in unknown_constraints: msg = "No such constraint." self._errors[constraint] = self.error_class([msg]) return super(AcquireNodeForm, self).clean() def filter_nodes(self, nodes): filtered_nodes = nodes # Filter by hostname. hostname = self.cleaned_data.get(self.get_field_name('name')) if hostname: clause = Q(hostname=hostname) # If the given hostname has a domain part, try matching # against the nodes' FQDNs as well (the FQDN is built using # the nodegroup's name as the domain name). if "." in hostname: host, domain = hostname.split('.', 1) hostname_clause = (Q(hostname__startswith="%s." % host) | Q(hostname=host)) domain_clause = Q(nodegroup__name=domain) clause = clause | (hostname_clause & domain_clause) filtered_nodes = filtered_nodes.filter(clause) # Filter by architecture. arch = self.cleaned_data.get(self.get_field_name('arch')) if arch: filtered_nodes = filtered_nodes.filter(architecture__in=arch) # Filter by cpu_count. cpu_count = self.cleaned_data.get(self.get_field_name('cpu_count')) if cpu_count: filtered_nodes = filtered_nodes.filter(cpu_count__gte=cpu_count) # Filter by memory. mem = self.cleaned_data.get(self.get_field_name('mem')) if mem: filtered_nodes = filtered_nodes.filter(memory__gte=mem) # Filter by tags. tags = self.cleaned_data.get(self.get_field_name('tags')) if tags: for tag in tags: filtered_nodes = filtered_nodes.filter(tags__name=tag) # Filter by zone. zone = self.cleaned_data.get(self.get_field_name('zone')) if zone: filtered_nodes = filtered_nodes.filter(zone=zone) # Filter by connected_to. connected_to = self.cleaned_data.get( self.get_field_name('connected_to')) if connected_to: where, params = macs_contain("routers", connected_to) filtered_nodes = filtered_nodes.extra(where=[where], params=params) # Filter by not_connected_to. not_connected_to = self.cleaned_data.get( self.get_field_name('not_connected_to')) if not_connected_to: where, params = macs_do_not_contain("routers", not_connected_to) filtered_nodes = filtered_nodes.extra(where=[where], params=params) return filtered_nodes
def test_accepts_list(self): value = ['a', 'b'] instance = UnconstrainedMultipleChoiceField() self.assertEqual(value, instance.clean(value))
class PackageRepositoryForm(MAASModelForm): """Package Repository creation/edition form.""" class Meta: model = PackageRepository fields = ( "name", "url", "distributions", "disabled_pockets", "disabled_components", "disable_sources", "components", "arches", "key", "enabled", ) name = forms.CharField( label="Name", required=True, help_text="The name of the Package Repository.", ) url = URLOrPPAFormField( label="Package Repository URL", required=True, help_text="The Package Repository URL", ) # Use UnconstrainedMultipleChoiceField fields for multiple-choices # fields instead of the default as we want to handle # multiple-values submissions. distributions = UnconstrainedMultipleChoiceField(label="Distribution list") disabled_pockets = UnconstrainedMultipleChoiceField( label="Disabled Pocket list") disabled_components = UnconstrainedMultipleChoiceField( label="Disabled Component list") disable_sources = forms.BooleanField( label="Disable Sources", required=False, help_text="Whether or not deb-src lines are disabled.", ) components = UnconstrainedMultipleChoiceField(label="Component list") arches = UnconstrainedMultipleChoiceField(label="Architecture list") key = forms.CharField( label="Key", required=False, help_text="The key used to authenticate the Package Repository.", ) enabled = forms.BooleanField( label="Enabled", required=False, help_text="Whether or not the Package Repository is enabled.", ) def __init__(self, data=None, instance=None, request=None, **kwargs): super().__init__(data=data, instance=instance, **kwargs) if self.instance.id is None: self.initial["enabled"] = True else: self.fields["url"].initial = self.instance.url def clean(self): cleaned_data = super().clean() if self.instance.default and not cleaned_data.get("enabled", False): raise ValidationError("Default repositories may not be disabled.") def clean_arches(self): arches = [] for value in self.cleaned_data.get("arches", []): arches.extend([s.strip() for s in value.split(",")]) known_arches = set(PackageRepository.objects.get_known_architectures()) for value in arches: if value not in known_arches: raise ValidationError( "'%s' is not a valid architecture. Known architectures: " "%s" % (value, ", ".join(sorted(known_arches)))) # If no arches provided, use MAIN_ARCHES. if len(arches) == 0: arches = PackageRepository.MAIN_ARCHES return arches def clean_distributions(self): values = [] for value in self.cleaned_data.get("distributions", []): values.extend([s.strip() for s in value.split(",")]) return values def clean_disabled_pockets(self): values = [] for value in self.cleaned_data.get("disabled_pockets", []): values.extend([s.strip() for s in value.split(",")]) # This allows to reset the values of disabled_pockets if one of the # following is passed over the API: # disabled_pockets= # disabled_pockets='' # disabled_pockets=None # disabled_pockets=[] if values == [""] or values == ["None"] or values == ["[]"]: return [] # Check that a valid pocket is being disable. for pocket in values: if pocket not in PackageRepository.POCKETS_TO_DISABLE: raise ValidationError( "'%s' is not a valid Ubuntu archive pocket. You " "can only disable %s." % (pocket, PackageRepository.POCKETS_TO_DISABLE)) return values def clean_disabled_components(self): values = [] for value in self.cleaned_data.get("disabled_components", []): values.extend([s.strip() for s in value.split(",")]) # This allows to reset the values of disabled_components if one of the # following is passed over the API: # disabled_components= # disabled_components='' # disabled_components=None # disabled_components=[] if values == [""] or values == ["None"] or values == ["[]"]: return [] if self.instance is not None and not self.instance.default and values: raise ValidationError( "This is a custom repository. Please update 'components' " "instead.") # Check that a valid component is being passed. for component in values: if component not in PackageRepository.COMPONENTS_TO_DISABLE: raise ValidationError( "'%s' is not a valid Ubuntu archive component. You " "can only disable %s." % (component, PackageRepository.COMPONENTS_TO_DISABLE)) return values def clean_components(self): values = [] for value in self.cleaned_data.get("components", []): values.extend([s.strip() for s in value.split(",")]) if self.instance is not None and self.instance.default and values: raise ValidationError( "This is a default Ubuntu repository. Please update " "'disabled_components' instead.") return values def save(self, endpoint, request): package_repository = super().save() create_audit_event( EVENT_TYPES.SETTINGS, endpoint, request, None, description=("%s package repository '%s'." % ( "Updated" if self.is_update else "Created", package_repository.name, )), ) return package_repository
class AcquireNodeForm(RenamableFieldsForm): """A form handling the constraints used to acquire a node.""" name = forms.CharField(label="The hostname of the desired node", required=False) system_id = forms.CharField(label="The system_id of the desired node", required=False) # This becomes a multiple-choice field during cleaning, to accommodate # architecture wildcards. arch = forms.CharField(label="Architecture", required=False) cpu_count = forms.FloatField( label="CPU count", required=False, error_messages={'invalid': "Invalid CPU count: number required."}) mem = forms.FloatField( label="Memory", required=False, error_messages={'invalid': "Invalid memory: number of MiB required."}) tags = UnconstrainedMultipleChoiceField(label="Tags", required=False) not_tags = UnconstrainedMultipleChoiceField(label="Not having tags", required=False) # XXX mpontillo 2015-10-30 need validators for fabric constraints fabrics = ValidatorMultipleChoiceField( validator=lambda x: True, label="Attached to fabrics", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of fabrics required.", }) not_fabrics = ValidatorMultipleChoiceField( validator=lambda x: True, label="Not attached to fabrics", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of fabrics required.", }) fabric_classes = ValidatorMultipleChoiceField( validator=lambda x: True, label="Attached to fabric with specified classes", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of fabric classes required.", }) not_fabric_classes = ValidatorMultipleChoiceField( validator=lambda x: True, label="Not attached to fabric with specified classes", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of fabric classes required." }) subnets = ValidatorMultipleChoiceField( validator=Subnet.objects.validate_filter_specifiers, label="Attached to subnets", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of subnet specifiers required.", }) not_subnets = ValidatorMultipleChoiceField( validator=Subnet.objects.validate_filter_specifiers, label="Not attached to subnets", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of subnet specifiers required.", }) vlans = ValidatorMultipleChoiceField( validator=VLAN.objects.validate_filter_specifiers, label="Attached to VLANs", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of VLAN specifiers required.", }) not_vlans = ValidatorMultipleChoiceField( validator=VLAN.objects.validate_filter_specifiers, label="Not attached to VLANs", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of VLAN specifiers required.", }) connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Connected to", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of MAC addresses required." }) not_connected_to = ValidatorMultipleChoiceField( validator=mac_validator, label="Not connected to", required=False, error_messages={ 'invalid_list': "Invalid parameter: list of MAC addresses required." }) zone = forms.CharField(label="Physical zone", required=False) not_in_zone = ValidatorMultipleChoiceField( validator=MODEL_NAME_VALIDATOR, label="Not in zone", required=False, error_messages={ 'invalid_list': "Invalid parameter: must list physical zones.", }) storage = forms.CharField(validators=[storage_validator], label="Storage", required=False) interfaces = LabeledConstraintMapField(validators=[interfaces_validator], label="Interfaces", required=False) ignore_unknown_constraints = False pod = forms.CharField(label="The name of the desired pod", required=False) not_pod = forms.CharField(label="The name of the undesired pod", required=False) pod_type = forms.CharField(label="The power_type of the desired pod", required=False) not_pod_type = forms.CharField(label="The power_type of the undesired pod", required=False) @classmethod def Strict(cls, *args, **kwargs): """A stricter version of the form which rejects unknown parameters.""" form = cls(*args, **kwargs) form.ignore_unknown_constraints = False return form def clean_arch(self): """Turn `arch` parameter into a list of architecture names. Even though `arch` is a single-value field, it turns into a list during cleaning. The architecture parameter may be a wildcard. """ # Import list_all_usable_architectures as part of its module, not # directly, so that patch_usable_architectures can easily patch it # for testing purposes. usable_architectures = maasserver_forms.list_all_usable_architectures() architecture_wildcards = get_architecture_wildcards( usable_architectures) value = self.cleaned_data[self.get_field_name('arch')] if value: if value in usable_architectures: # Full 'arch/subarch' specified directly. return [value] elif value in architecture_wildcards: # Try to expand 'arch' to all available 'arch/subarch' # matches. return architecture_wildcards[value] set_form_error(self, self.get_field_name('arch'), 'Architecture not recognised.') return None def clean_tags(self): value = self.cleaned_data[self.get_field_name('tags')] if value: tag_names = parse_legacy_tags(value) # Validate tags. tag_names = set(tag_names) db_tag_names = set( Tag.objects.filter(name__in=tag_names).values_list('name', flat=True)) if len(tag_names) != len(db_tag_names): unknown_tags = tag_names.difference(db_tag_names) error_msg = 'No such tag(s): %s.' % ', '.join( "'%s'" % tag for tag in unknown_tags) set_form_error(self, self.get_field_name('tags'), error_msg) return None return tag_names return None def clean_zone(self): value = self.cleaned_data[self.get_field_name('zone')] if value: nonexistent_names = detect_nonexistent_names(Zone, [value]) if nonexistent_names: error_msg = "No such zone: '%s'." % value set_form_error(self, self.get_field_name('zone'), error_msg) return None return value return None def clean_not_in_zone(self): value = self.cleaned_data[self.get_field_name('not_in_zone')] if not value: return None nonexistent_names = detect_nonexistent_names(Zone, value) if nonexistent_names: error_msg = "No such zone(s): %s." % ', '.join(nonexistent_names) set_form_error(self, self.get_field_name('not_in_zone'), error_msg) return None return value def _clean_specifiers(self, model, specifiers): if not specifiers: return None objects = set(model.objects.filter_by_specifiers(specifiers)) if len(objects) == 0: raise ValidationError("No matching %s found." % model._meta.verbose_name_plural) return objects def clean_subnets(self): value = self.cleaned_data[self.get_field_name('subnets')] return self._clean_specifiers(Subnet, value) def clean_not_subnets(self): value = self.cleaned_data[self.get_field_name('not_subnets')] return self._clean_specifiers(Subnet, value) def clean_vlans(self): value = self.cleaned_data[self.get_field_name('vlans')] return self._clean_specifiers(VLAN, value) def clean_not_vlans(self): value = self.cleaned_data[self.get_field_name('not_vlans')] return self._clean_specifiers(VLAN, value) def __init__(self, *args, **kwargs): super(AcquireNodeForm, self).__init__(*args, **kwargs) def clean(self): if not self.ignore_unknown_constraints: unknown_constraints = set(self.data).difference( set(self.field_mapping.values())) for constraint in unknown_constraints: if constraint not in IGNORED_FIELDS: msg = "Unable to allocate a machine. No such constraint." self._errors[constraint] = self.error_class([msg]) return super(AcquireNodeForm, self).clean() def describe_constraint(self, field_name): """Return a human-readable representation of a constraint. Turns a constraint value as passed to the form into a Juju-like representation for display: `name=foo`. Multi-valued constraints are shown as comma-separated values, e.g. `tags=do,re,mi`. :param field_name: Name of the constraint on this form, e.g. `zone`. :return: A constraint string, or `None` if the constraint is not set. """ value = self.cleaned_data.get(field_name, None) if value is None: return None if isinstance(self.fields[field_name], MultipleChoiceField): output = describe_multi_constraint_value(value) elif field_name == 'arch' and not isinstance(value, str): # The arch field is a special case. It's defined as a string # field, but may become a list/tuple/... of strings in cleaning. output = describe_multi_constraint_value(value) else: output = describe_single_constraint_value(value) if output is None: return None else: return '%s=%s' % (field_name, output) def describe_constraints(self): """Return a human-readable representation of the given constraints. The description is Juju-like, e.g. `arch=amd64 cpu=16 zone=rack3`. Constraints are listed in alphabetical order. """ constraints = (self.describe_constraint(name) for name in sorted(self.fields.keys())) return ' '.join(constraint for constraint in constraints if constraint is not None) def filter_nodes(self, nodes): """Return the subset of nodes that match the form's constraints. :param nodes: The set of nodes on which the form should apply constraints. :type nodes: `django.db.models.query.QuerySet` :return: A QuerySet of the nodes that match the form's constraints. :rtype: `django.db.models.query.QuerySet` """ filtered_nodes = nodes filtered_nodes = self.filter_by_pod_or_pod_type(filtered_nodes) filtered_nodes = self.filter_by_hostname(filtered_nodes) filtered_nodes = self.filter_by_system_id(filtered_nodes) filtered_nodes = self.filter_by_arch(filtered_nodes) filtered_nodes = self.filter_by_cpu_count(filtered_nodes) filtered_nodes = self.filter_by_mem(filtered_nodes) filtered_nodes = self.filter_by_tags(filtered_nodes) filtered_nodes = self.filter_by_zone(filtered_nodes) filtered_nodes = self.filter_by_subnets(filtered_nodes) filtered_nodes = self.filter_by_vlans(filtered_nodes) filtered_nodes = self.filter_by_fabrics(filtered_nodes) filtered_nodes = self.filter_by_fabric_classes(filtered_nodes) compatible_nodes, filtered_nodes = self.filter_by_storage( filtered_nodes) compatible_interfaces, filtered_nodes = self.filter_by_interfaces( filtered_nodes) filtered_nodes = self.reorder_nodes_by_cost(filtered_nodes) return filtered_nodes, compatible_nodes, compatible_interfaces def reorder_nodes_by_cost(self, filtered_nodes): # This uses a very simple procedure to compute a machine's # cost. This procedure is loosely based on how ec2 computes # the costs of machines. This is here to give a hint to let # the call to acquire() decide which machine to return based # on the machine's cost when multiple machines match the # constraints. filtered_nodes = filtered_nodes.distinct().extra( select={'cost': "cpu_count + memory / 1024."}) return filtered_nodes.order_by("cost") def filter_by_interfaces(self, filtered_nodes): compatible_interfaces = {} interfaces_label_map = self.cleaned_data.get( self.get_field_name('interfaces')) if interfaces_label_map is not None: node_ids, compatible_interfaces = nodes_by_interface( interfaces_label_map) if node_ids is not None: filtered_nodes = filtered_nodes.filter(id__in=node_ids) return compatible_interfaces, filtered_nodes def filter_by_storage(self, filtered_nodes): compatible_nodes = {} # Maps node/storage to named storage constraints storage = self.cleaned_data.get(self.get_field_name('storage')) if storage: compatible_nodes = nodes_by_storage(storage) node_ids = list(compatible_nodes) if node_ids is not None: filtered_nodes = filtered_nodes.filter(id__in=node_ids) return compatible_nodes, filtered_nodes def filter_by_fabric_classes(self, filtered_nodes): fabric_classes = self.cleaned_data.get( self.get_field_name('fabric_classes')) if fabric_classes is not None and len(fabric_classes) > 0: filtered_nodes = filtered_nodes.filter( interface__vlan__fabric__class_type__in=fabric_classes) not_fabric_classes = self.cleaned_data.get( self.get_field_name('not_fabric_classes')) if not_fabric_classes is not None and len(not_fabric_classes) > 0: filtered_nodes = filtered_nodes.exclude( interface__vlan__fabric__class_type__in=not_fabric_classes) return filtered_nodes def filter_by_fabrics(self, filtered_nodes): fabrics = self.cleaned_data.get(self.get_field_name('fabrics')) if fabrics is not None and len(fabrics) > 0: # XXX mpontillo 2015-10-30 need to also handle fabrics whose name # is null (fabric-<id>). filtered_nodes = filtered_nodes.filter( interface__vlan__fabric__name__in=fabrics) not_fabrics = self.cleaned_data.get(self.get_field_name('not_fabrics')) if not_fabrics is not None and len(not_fabrics) > 0: # XXX mpontillo 2015-10-30 need to also handle fabrics whose name # is null (fabric-<id>). filtered_nodes = filtered_nodes.exclude( interface__vlan__fabric__name__in=not_fabrics) return filtered_nodes def filter_by_vlans(self, filtered_nodes): vlans = self.cleaned_data.get(self.get_field_name('vlans')) if vlans is not None and len(vlans) > 0: for vlan in set(vlans): filtered_nodes = filtered_nodes.filter(interface__vlan=vlan) not_vlans = self.cleaned_data.get(self.get_field_name('not_vlans')) if not_vlans is not None and len(not_vlans) > 0: for not_vlan in set(not_vlans): filtered_nodes = filtered_nodes.exclude( interface__vlan=not_vlan) return filtered_nodes def filter_by_subnets(self, filtered_nodes): subnets = self.cleaned_data.get(self.get_field_name('subnets')) if subnets is not None and len(subnets) > 0: for subnet in set(subnets): filtered_nodes = filtered_nodes.filter( interface__ip_addresses__subnet=subnet) not_subnets = self.cleaned_data.get(self.get_field_name('not_subnets')) if not_subnets is not None and len(not_subnets) > 0: for not_subnet in set(not_subnets): filtered_nodes = filtered_nodes.exclude( interface__ip_addresses__subnet=not_subnet) return filtered_nodes def filter_by_zone(self, filtered_nodes): zone = self.cleaned_data.get(self.get_field_name('zone')) if zone: zone_obj = Zone.objects.get(name=zone) filtered_nodes = filtered_nodes.filter(zone=zone_obj) not_in_zone = self.cleaned_data.get(self.get_field_name('not_in_zone')) if not_in_zone: not_in_zones = Zone.objects.filter(name__in=not_in_zone) filtered_nodes = filtered_nodes.exclude(zone__in=not_in_zones) return filtered_nodes def filter_by_tags(self, filtered_nodes): tags = self.cleaned_data.get(self.get_field_name('tags')) if tags: for tag in tags: filtered_nodes = filtered_nodes.filter(tags__name=tag) not_tags = self.cleaned_data.get(self.get_field_name('not_tags')) if len(not_tags) > 0: for not_tag in not_tags: filtered_nodes = filtered_nodes.exclude(tags__name=not_tag) return filtered_nodes def filter_by_mem(self, filtered_nodes): mem = self.cleaned_data.get(self.get_field_name('mem')) if mem: filtered_nodes = filtered_nodes.filter(memory__gte=mem) return filtered_nodes def filter_by_cpu_count(self, filtered_nodes): cpu_count = self.cleaned_data.get(self.get_field_name('cpu_count')) if cpu_count: filtered_nodes = filtered_nodes.filter(cpu_count__gte=cpu_count) return filtered_nodes def filter_by_arch(self, filtered_nodes): arch = self.cleaned_data.get(self.get_field_name('arch')) if arch: filtered_nodes = filtered_nodes.filter(architecture__in=arch) return filtered_nodes def filter_by_system_id(self, filtered_nodes): # Filter by system_id. system_id = self.cleaned_data.get(self.get_field_name('system_id')) if system_id: filtered_nodes = filtered_nodes.filter(system_id=system_id) return filtered_nodes def filter_by_hostname(self, filtered_nodes): # Filter by hostname. hostname = self.cleaned_data.get(self.get_field_name('name')) if hostname: # If the given hostname has a domain part, try matching # against the nodes' FQDN. if "." in hostname: host, domain = hostname.split('.', 1) hostname_clause = Q(hostname=host) domain_clause = Q(domain__name=domain) clause = (hostname_clause & domain_clause) else: clause = Q(hostname=hostname) filtered_nodes = filtered_nodes.filter(clause) return filtered_nodes def filter_by_pod_or_pod_type(self, filtered_nodes): # Filter by pod, pod type, not_pod or not_pod_type. # We are filtering for all of these to keep the query count down. pod = self.cleaned_data.get(self.get_field_name('pod')) not_pod = self.cleaned_data.get(self.get_field_name('not_pod')) pod_type = self.cleaned_data.get(self.get_field_name('pod_type')) not_pod_type = self.cleaned_data.get( self.get_field_name('not_pod_type')) if pod or pod_type or not_pod or not_pod_type: pods = Pod.objects.all() if pod: pods = pods.filter(name=pod) if not pod: pods = pods.exclude(name=not_pod) if pod_type: pods = pods.filter(power_type=pod_type) if not_pod_type: pods = pods.exclude(power_type=not_pod_type) filtered_nodes = filtered_nodes.filter( bmc_id__in=pods.values_list('id', flat=True)) return filtered_nodes