def _set_up_rack_fields(self): qs = RackController.objects.filter_by_vids([self.instance.vid]) self.fields["primary_rack"] = NodeChoiceField( required=False, initial=None, empty_label="No rack controller", queryset=qs, ) self.fields["secondary_rack"] = NodeChoiceField( required=False, initial=None, empty_label="No rack controller", queryset=qs, ) # Convert the initial values pulled from the database from id to # system_id so form validation doesn't complain primary_rack_id = self.initial.get("primary_rack") if primary_rack_id is not None: primary_rack = RackController.objects.get(id=primary_rack_id) self.initial["primary_rack"] = primary_rack.system_id secondary_rack_id = self.initial.get("secondary_rack") if secondary_rack_id is not None: secondary_rack = RackController.objects.get(id=secondary_rack_id) self.initial["secondary_rack"] = secondary_rack.system_id
def test_works_with_multiple_entries_in_queryset(self): # Regression test for lp:1551399 vlan = factory.make_VLAN() node = factory.make_Node_with_Interface_on_Subnet(vlan=vlan) factory.make_Interface(node=node, vlan=vlan) qs = Node.objects.filter_by_vids([vlan.vid]) node_field = NodeChoiceField(qs) # Double check that we have duplicated entires self.assertEqual(2, len(qs.filter(system_id=node.system_id))) self.assertEqual(node, node_field.clean(node.system_id))
class PodForm(MAASModelForm): class Meta: model = Pod fields = [ 'name', 'tags', 'zone', 'pool', 'cpu_over_commit_ratio', 'memory_over_commit_ratio', 'default_storage_pool', 'default_macvlan_mode', 'host', ] name = forms.CharField(label="Name", required=False, help_text=("The name of the pod")) zone = forms.ModelChoiceField(label="Physical zone", required=False, initial=Zone.objects.get_default_zone, queryset=Zone.objects.all(), to_field_name='name') pool = forms.ModelChoiceField( label="Default pool of created machines", required=False, initial=lambda: ResourcePool.objects.get_default_resource_pool().name, queryset=ResourcePool.objects.all(), to_field_name='name') cpu_over_commit_ratio = forms.FloatField(label="CPU over commit ratio", initial=1, required=False, min_value=0, max_value=10) memory_over_commit_ratio = forms.FloatField( label="Memory over commit ratio", initial=1, required=False, min_value=0, max_value=10) default_storage_pool = forms.ModelChoiceField( label="Default storage pool", required=False, queryset=PodStoragePool.objects.none(), to_field_name='pool_id') default_macvlan_mode = forms.ChoiceField(label="Default MACVLAN mode", required=False, choices=MACVLAN_MODE_CHOICES, initial=MACVLAN_MODE_CHOICES[0]) host = NodeChoiceField(label="Pod host", queryset=Node.objects.all(), required=False, initial=None, help_text=("The node that hosts this Pod.")) def __init__(self, data=None, instance=None, request=None, **kwargs): self.is_new = instance is None self.request = request super(PodForm, self).__init__(data=data, instance=instance, **kwargs) if data is None: data = {} type_value = data.get('type', self.initial.get('type')) self.drivers_orig = driver_parameters.get_all_power_types() self.drivers = { driver['name']: driver for driver in self.drivers_orig if driver['driver_type'] == 'pod' } if len(self.drivers) == 0: type_value = '' elif type_value not in self.drivers: type_value = ('' if self.instance is None else self.instance.power_type) choices = [(name, driver['description']) for name, driver in self.drivers.items()] self.fields['type'] = forms.ChoiceField(required=True, choices=choices, initial=type_value) if not self.is_new: if self.instance.power_type != '': self.initial['type'] = self.instance.power_type if instance is not None: self.initial['zone'] = instance.zone.name self.initial['pool'] = instance.pool.name self.fields['default_storage_pool'].queryset = ( instance.storage_pools.all()) if instance.default_storage_pool: self.initial['default_storage_pool'] = ( instance.default_storage_pool.pool_id) if instance.host is not None: self.initial['host'] = self.instance.host.system_id def _clean_fields(self): """Override to dynamically add fields based on the value of `type` field.""" # Process the built-in fields first. super(PodForm, self)._clean_fields() # If no errors then we re-process with the fields required by the # selected type for the pod. if len(self.errors) == 0: driver_fields = get_driver_parameters_from_json( self.drivers_orig, scope=SETTING_SCOPE.BMC) self.param_fields = ( driver_fields[self.cleaned_data['type']].field_dict) self.fields.update(self.param_fields) if not self.is_new: for key, value in self.instance.power_parameters.items(): if key not in self.data: self.data[key] = value super(PodForm, self)._clean_fields() def clean(self): cleaned_data = super(PodForm, self).clean() if len(self.drivers) == 0: set_form_error( self, 'type', "No rack controllers are connected, unable to validate.") elif (not self.is_new and self.instance.power_type != self.cleaned_data.get('type')): set_form_error( self, 'type', "Cannot change the type of a pod. Delete and re-create the " "pod with a different type.") return cleaned_data def save(self, *args, **kwargs): """Persist the pod into the database.""" def check_for_duplicate(power_type, power_parameters): # When the Pod is new try to get a BMC of the same type and # parameters to convert the BMC to a new Pod. When the Pod is not # new the form will use the already existing pod instance to update # those fields. If updating the fields causes a duplicate BMC then # a validation erorr will be raised from the model level. if self.is_new: bmc = BMC.objects.filter( power_type=power_type, power_parameters=power_parameters).first() if bmc is not None: if bmc.bmc_type == BMC_TYPE.BMC: # Convert the BMC to a Pod and set as the instance for # the PodForm. bmc.bmc_type = BMC_TYPE.POD bmc.pool = ( ResourcePool.objects.get_default_resource_pool()) return bmc.as_pod() else: # Pod already exists with the same power_type and # parameters. raise ValidationError("Pod %s with type and " "parameters already exist." % bmc.name) def update_obj(existing_obj): if existing_obj is not None: self.instance = existing_obj self.instance = super(PodForm, self).save(commit=False) self.instance.power_type = power_type self.instance.power_parameters = power_parameters if (self.data.get('host') is "" and self.instance.host is not None): # 'host' is being cleared. self.instance.host = None return self.instance power_type = self.cleaned_data['type'] # Set power_parameters to the generated param_fields. power_parameters = { param_name: self.cleaned_data[param_name] for param_name in self.param_fields.keys() if param_name in self.cleaned_data } if isInIOThread(): # Running in twisted reactor, do the work inside the reactor. d = deferToDatabase(transactional(check_for_duplicate), power_type, power_parameters) d.addCallback(update_obj) d.addCallback(lambda _: self.discover_and_sync_pod()) return d else: # Perform the actions inside the executing thread. existing_obj = check_for_duplicate(power_type, power_parameters) if existing_obj is not None: self.instance = existing_obj self.instance = update_obj(self.instance) return self.discover_and_sync_pod() def discover_and_sync_pod(self): """Discover and sync the pod information.""" def update_db(result): discovered_pod, discovered = result # When called with an instance that has no name, be sure to set # it before going any further. If this is a new instance this will # also create it in the database. if not self.instance.name: self.instance.set_random_name() self.instance.sync(discovered_pod, self.request.user) # Save which rack controllers can route and which cannot. discovered_rack_ids = [ rack_id for rack_id, _ in discovered[0].items() ] for rack_controller in RackController.objects.all(): routable = rack_controller.system_id in discovered_rack_ids bmc_route_model = BMCRoutableRackControllerRelationship relation, created = (bmc_route_model.objects.get_or_create( bmc=self.instance.as_bmc(), rack_controller=rack_controller, defaults={'routable': routable})) if not created and relation.routable != routable: relation.routable = routable relation.save() return self.instance if isInIOThread(): # Running in twisted reactor, do the work inside the reactor. d = discover_pod(self.instance.power_type, self.instance.power_parameters, pod_id=self.instance.id, name=self.instance.name) d.addCallback(lambda discovered: (get_best_discovered_result(discovered), discovered)) def catch_no_racks(result): discovered_pod, discovered = result if discovered_pod is None: raise PodProblem( "Unable to start the pod discovery process. " "No rack controllers connected.") return discovered_pod, discovered def wrap_errors(failure): if failure.check(PodProblem): return failure else: raise PodProblem(str(failure.value)) d.addCallback(catch_no_racks) d.addCallback(partial(deferToDatabase, transactional(update_db))) d.addErrback(wrap_errors) return d else: # Perform the actions inside the executing thread. try: discovered = discover_pod(self.instance.power_type, self.instance.power_parameters, pod_id=self.instance.id, name=self.instance.name) except Exception as exc: raise PodProblem(str(exc)) from exc # Use the first discovered pod object. All other objects are # ignored. The other rack controllers that also provided a result # can route to the pod. try: discovered_pod = get_best_discovered_result(discovered) except Exception as error: raise PodProblem(str(error)) if discovered_pod is None: raise PodProblem("Unable to start the pod discovery process. " "No rack controllers connected.") return update_db((discovered_pod, discovered))
class DHCPSnippetForm(MAASModelForm): """DHCP snippet creation/edition form.""" name = forms.CharField(label="Name", required=False, help_text="The name of the DHCP snippet.") value = VersionedTextFileField(label="DHCP Snippet", required=False, help_text="The DHCP Snippet") description = forms.CharField( label="Description", required=False, help_text="The description of what the DHCP snippet does.", ) enabled = forms.BooleanField( label="Enabled", required=False, help_text="Whether or not the DHCP snippet is enabled.", ) node = NodeChoiceField( label="Node", queryset=Node.objects.all(), required=False, initial=None, help_text="The node which the DHCP snippet is for.", ) subnet = SpecifierOrModelChoiceField( label="Subnet", queryset=Subnet.objects.all(), required=False, help_text="The subnet which the DHCP snippet is for.", ) global_snippet = forms.BooleanField( label="Global DHCP Snippet", required=False, help_text=( "Set the DHCP snippet to be global, removes links to nodes or " "subnets"), ) class Meta: model = DHCPSnippet fields = ( "name", "value", "description", "enabled", "node", "subnet", "global_snippet", ) def __init__(self, instance=None, request=None, **kwargs): super().__init__(instance=instance, **kwargs) if instance is None: for field in ["name", "value"]: self.fields[field].required = True self.initial["enabled"] = True else: self.fields["value"].initial = self.instance.value if instance is not None and instance.node is not None: self.initial["node"] = self.instance.node.system_id def clean(self): cleaned_data = super().clean() if cleaned_data.get("global_snippet", False): cleaned_data["node"] = None self.instance.node = None cleaned_data["subnet"] = None self.instance.subnet = None elif (self.instance.subnet == cleaned_data.get("subnet") and cleaned_data.get("node") is not None): cleaned_data["subnet"] = None self.instance.subnet = None elif (self.instance.node == cleaned_data.get("node") and cleaned_data.get("subnet") is not None): cleaned_data["node"] = None self.instance.node = None return cleaned_data def is_valid(self): valid = super().is_valid() if valid: # Often the first error can cause cascading errors. Showing all of # these errors can be confusing so only show the first if there is # one. first_error = None for error in validate_dhcp_config(self.instance): valid = False if first_error is None: first_error = error else: if error["line_num"] < first_error["line_num"]: first_error = error if first_error is not None: set_form_error(self, "value", first_error["error"]) # If the DHCPSnippet isn't valid cleanup the value if not valid and self.initial.get("value") != self.instance.value_id: self.instance.value.delete() return valid def save(self, endpoint, request): dhcp_snippet = super(DHCPSnippetForm, self).save() create_audit_event( EVENT_TYPES.SETTINGS, endpoint, request, None, description=("%s DHCP snippet '%s'." % ( "Updated" if self.is_update else "Created", dhcp_snippet.name, )), ) return dhcp_snippet
def test_raises_exception_when_not_found(self): for _ in range(3): factory.make_Node() node_field = NodeChoiceField(Node.objects.filter()) self.assertRaises(ValidationError, node_field.clean, factory.make_name("query"))
def test_allows_selecting_by_hostname(self): node = factory.make_Node() for _ in range(3): factory.make_Node() node_field = NodeChoiceField(Node.objects.filter()) self.assertEqual(node, node_field.clean(node.hostname))
class CloneForm(forms.Form): """Clone storage/interface form.""" source = NodeChoiceField( label="Source", queryset=Machine.objects.all(), required=True, initial=None, help_text="The source machine to clone from.", ) destinations = SimpleArrayField( NodeChoiceField(queryset=Machine.objects.all()), label="Destinations", min_length=1, error_messages={ "item_invalid": "Machine %(nth)s in the array did not validate:" }, help_text="The destinations to clone to.", ) storage = forms.BooleanField( label="Storage", required=False, help_text="Clone the storage configuration.", ) interfaces = forms.BooleanField( label="Interfaces", required=False, help_text="Clone the interfaces configuration.", ) def __init__(self, user, **kwargs): self.user = user super().__init__(**kwargs) self.fields["source"].queryset = Machine.objects.get_nodes( self.user, NodePermission.view) self.fields[ "destinations"].base_field.queryset = Machine.objects.get_nodes( self.user, NodePermission.admin, from_nodes=Machine.objects.filter( status__in={NODE_STATUS.READY, NODE_STATUS.FAILED_TESTING }), ) def clean(self): """Validate that the form is valid and that the destinations can accept the storage and/or interfaces configuration from the source.""" cleaned_data = super().clean() source = self.cleaned_data.get("source") if not source: # Django should be placing this automatically, but it does not # occur. So we force the setting of this error here. set_form_error(self, "source", "This field is required.") destinations = self.cleaned_data.get("destinations") destination_field = self.fields["destinations"] item_invalid = destination_field.error_messages["item_invalid"] storage = self.cleaned_data.get("storage", False) interfaces = self.cleaned_data.get("interfaces", False) if source and destinations: for index, dest in enumerate(destinations, 1): if source == dest: error = prefix_validation_error( ValidationError( "Source machine cannot be a destination machine."), prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) else: if storage: try: dest._get_storage_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) if interfaces: try: dest._get_interface_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=item_invalid, code="item_invalid", params={"nth": index}, ) set_form_error(self, "destinations", error) if not storage and not interfaces: set_form_error(self, "__all__", "Either storage or interfaces must be true.") return cleaned_data def save(self): """Clone the storage and/or interfaces configuration to the destinations.""" source = self.cleaned_data.get("source") destinations = self.cleaned_data.get("destinations") storage = self.cleaned_data.get("storage", False) interfaces = self.cleaned_data.get("interfaces", False) for dest in destinations: if storage: dest.set_storage_configuration_from_node(source) if interfaces: dest.set_networking_configuration_from_node(source)
class CloneForm(forms.Form): """Clone storage/interface form.""" source = NodeChoiceField( label="Source", queryset=Machine.objects.all(), required=True, initial=None, help_text="The source machine to clone from.", ) destinations = SimpleArrayField( NodeChoiceField(queryset=Machine.objects.all()), label="Destinations", min_length=1, error_messages={ "item_invalid": "Machine %(nth)s is invalid:", "is-source": "Source machine %(machine)s cannot be a destination machine.", "storage": "%(machine)s is invalid:", "networking": "%(machine)s is invalid:", }, help_text="The destinations to clone to.", ) storage = forms.BooleanField( label="Storage", required=False, help_text="Clone the storage configuration.", ) interfaces = forms.BooleanField( label="Interfaces", required=False, help_text="Clone the interfaces configuration.", ) def __init__(self, user, **kwargs): self.user = user super().__init__(**kwargs) self.fields["source"].queryset = Machine.objects.get_nodes( self.user, NodePermission.view) self.fields[ "destinations"].base_field.queryset = Machine.objects.get_nodes( self.user, NodePermission.admin, from_nodes=Machine.objects.filter( status__in={NODE_STATUS.READY, NODE_STATUS.FAILED_TESTING }), ) def clean(self): """Validate that the form is valid and that the destinations can accept the storage and/or interfaces configuration from the source.""" cleaned_data = super().clean() source = self.cleaned_data.get("source") if not source: # Django should be placing this automatically, but it does not # occur. So we force the setting of this error here. self.add_error("source", "This field is required.") destinations = self.cleaned_data.get("destinations") destination_field = self.fields["destinations"] storage = self.cleaned_data.get("storage", False) interfaces = self.cleaned_data.get("interfaces", False) if source and destinations: for dest in destinations: if source == dest: error = ValidationError( destination_field.error_messages["is-source"], code="is-source", params={ "machine": str(dest), "system_id": dest.system_id, }, ) self.add_error("destinations", error) else: if storage: try: dest._get_storage_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=destination_field. error_messages["storage"], code="storage", params={ "machine": str(dest), "system_id": dest.system_id, }, ) self.add_error("destinations", error) if interfaces: try: dest._get_interface_mapping_between_nodes(source) except ValidationError as exc: error = prefix_validation_error( exc, prefix=destination_field. error_messages["networking"], code="networking", params={ "machine": str(dest), "system_id": dest.system_id, }, ) self.add_error("destinations", error) if not storage and not interfaces: self.add_error( "__all__", ValidationError( "Either storage or interfaces must be true.", code="required", ), ) return cleaned_data def strip_failed_destinations(self): """Remove destinations that have errors.""" if "destinations" not in self.cleaned_data: submitted_destinations = self.data.get("destinations") # Don't try and manipulate empty submission if not submitted_destinations: return errors = self.errors.as_data() bogus_system_ids = { error.params["system_id"] for error in errors["destinations"] } return [ dest for dest in submitted_destinations if dest not in bogus_system_ids ] def save(self): """Clone the storage and/or interfaces configuration to the destinations.""" source = self.cleaned_data.get("source") destinations = self.cleaned_data.get("destinations", []) storage = self.cleaned_data.get("storage", False) interfaces = self.cleaned_data.get("interfaces", False) for dest in destinations: if storage: dest.set_storage_configuration_from_node(source) if interfaces: dest.set_networking_configuration_from_node(source)
class DHCPSnippetForm(MAASModelForm): """DHCP snippet creation/edition form.""" name = forms.CharField(label="Name", required=False, help_text=("The name of the DHCP snippet.")) value = VersionedTextFileField(label="DHCP Snippet", required=False, help_text="The DHCP Snippet") description = forms.CharField( label="Description", required=False, help_text=("The description of what the DHCP snippet does.")) enabled = forms.BooleanField( label="Enabled", required=False, help_text=("Whether or not the DHCP snippet is enabled.")) node = NodeChoiceField( label="Node", queryset=Node.objects.all(), required=False, initial=None, help_text=("The node which the DHCP snippet is for.")) subnet = SpecifierOrModelChoiceField( label="Subnet", queryset=Subnet.objects.all(), required=False, help_text="The subnet which the DHCP snippet is for.") global_snippet = forms.BooleanField( label="Global DHCP Snippet", required=False, help_text=( "Set the DHCP snippet to be global, removes links to nodes or " "subnets")) class Meta: model = DHCPSnippet fields = ( 'name', 'value', 'description', 'enabled', 'node', 'subnet', 'global_snippet', ) def __init__(self, instance=None, request=None, **kwargs): super().__init__(instance=instance, **kwargs) if instance is None: for field in ['name', 'value']: self.fields[field].required = True self.initial['enabled'] = True else: self.fields['value'].initial = self.instance.value if instance is not None and instance.node is not None: self.initial['node'] = self.instance.node.system_id def clean(self): cleaned_data = super().clean() if cleaned_data.get('global_snippet', False): cleaned_data['node'] = None self.instance.node = None cleaned_data['subnet'] = None self.instance.subnet = None elif (self.instance.subnet == cleaned_data.get('subnet') and cleaned_data.get('node') is not None): cleaned_data['subnet'] = None self.instance.subnet = None elif (self.instance.node == cleaned_data.get('node') and cleaned_data.get('subnet') is not None): cleaned_data['node'] = None self.instance.node = None return cleaned_data def is_valid(self): valid = super().is_valid() if valid: # Often the first error can cause cascading errors. Showing all of # these errors can be confusing so only show the first if there is # one. first_error = None for error in validate_dhcp_config(self.instance): valid = False if first_error is None: first_error = error else: if error['line_num'] < first_error['line_num']: first_error = error if first_error is not None: set_form_error(self, 'value', first_error['error']) # If the DHCPSnippet isn't valid cleanup the value if not valid and self.initial.get('value') != self.instance.value_id: self.instance.value.delete() return valid