diff --git a/app/models.py b/app/models.py index 54b0db77495ec61887d85b1f2d496be991cc14ab..d574e9b5e70ddaaded255759959b4673e0ae87ed 100644 --- a/app/models.py +++ b/app/models.py @@ -943,6 +943,21 @@ class Network(CreatedMixin, db.Model): raise ValidationError(r"Vlan name shall match [A-Za-z0-9\-]{3,25}") return string + @validates("vlan_id") + def validate_vlan_id(self, key, value): + """Ensure the vlan_id is in the scope range""" + if value is None or self.scope is None: + # If scope is None, we can't do any validation + # This will occur when vlan_id is passed before scope + # We could ensure it's not the case but main use case + # is when editing network. This won't happen then. + return value + if int(value) not in self.scope.vlan_range(): + raise ValidationError( + f"Vlan id shall be in the range [{self.scope.first_vlan} - {self.scope.last_vlan}]" + ) + return value + def to_dict(self, recursive=False): d = super().to_dict() d.update( diff --git a/app/network/forms.py b/app/network/forms.py index 71180a6f40dcbbc302a7ce873d4c5d002178ff99..a5df1888d33aeb41d1754594e407d4dd8e176298 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -126,6 +126,31 @@ class NetworkForm(CSEntryForm): self.domain_id.choices = utils.get_model_choices(models.Domain, attr="name") +class EditNetworkForm(CSEntryForm): + vlan_name = StringField( + "Vlan name", + description="vlan name must be 3-25 characters long and contain only letters, numbers and dash", + validators=[ + validators.InputRequired(), + validators.Regexp(VLAN_NAME_RE), + Unique(models.Network, column="vlan_name"), + ], + ) + vlan_id = IntegerField("Vlan id") + description = TextAreaField("Description") + address = StringField("Address") + first_ip = StringField("First IP") + last_ip = StringField("Last IP") + gateway = StringField("Gateway IP") + domain_id = SelectField("Domain") + admin_only = BooleanField("Admin only") + sensitive = BooleanField("Sensitive") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.domain_id.choices = utils.get_model_choices(models.Domain, attr="name") + + class HostForm(CSEntryForm): name = StringField( "Hostname", diff --git a/app/network/views.py b/app/network/views.py index 1e69490eaec0e3ce1bf4fc11320c788b2f16f51c..ae678730969be148a1d5a76444b7db7d75460939 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -30,6 +30,7 @@ from .forms import ( InterfaceForm, HostInterfaceForm, NetworkForm, + EditNetworkForm, NetworkScopeForm, DomainForm, CreateVMForm, @@ -700,6 +701,44 @@ def create_network(): return render_template("network/create_network.html", form=form) +@bp.route("/networks/edit/<vlan_name>", methods=("GET", "POST")) +@login_groups_accepted("admin") +def edit_network(vlan_name): + network = models.Network.query.filter_by(vlan_name=vlan_name).first_or_404() + form = EditNetworkForm(request.form, obj=network) + if form.validate_on_submit(): + try: + for field in ( + "vlan_name", + "vlan_id", + "description", + "address", + "first_ip", + "last_ip", + "gateway", + "domain_id", + "admin_only", + "sensitive", + ): + setattr(network, field, getattr(form, field).data) + except ValidationError as e: + # Check for error raised by model validation (not implemented in form validation) + current_app.logger.warning(f"{e}") + flash(f"{e}", "error") + return render_template("network/edit_network.html", form=form) + current_app.logger.debug(f"Trying to update: {network!r}") + try: + db.session.commit() + except sa.exc.IntegrityError as e: + db.session.rollback() + current_app.logger.warning(f"{e}") + flash(f"{e}", "error") + else: + flash(f"Network {network} updated!", "success") + return redirect(url_for("network.view_network", vlan_name=network.vlan_name)) + return render_template("network/edit_network.html", form=form) + + @bp.route("/_retrieve_scope_defaults/<int:scope_id>") @login_required def retrieve_scope_defaults(scope_id): diff --git a/app/templates/network/edit_network.html b/app/templates/network/edit_network.html new file mode 100644 index 0000000000000000000000000000000000000000..d06182f2c445ff65bc0d9633ca4841958f9e7641 --- /dev/null +++ b/app/templates/network/edit_network.html @@ -0,0 +1,34 @@ +{% extends "network/networks.html" %} +{% from "_helpers.html" import render_field %} + +{% block title %}Edit Network - CSEntry{% endblock %} + +{% block networks_nav %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.view_network', vlan_name=form.vlan_name.data) }}">View network</a> + </li> + <li class="nav-item"> + <a class="nav-link active" href="{{ url_for('network.edit_network', vlan_name=form.vlan_name.data) }}">Edit network</a> + </li> +{% endblock %} + +{% block networks_main %} + <form id="editNetworkForm" method="POST"> + {{ form.hidden_tag() }} + {{ render_field(form.vlan_name) }} + {{ render_field(form.vlan_id) }} + {{ render_field(form.description) }} + {{ render_field(form.address) }} + {{ render_field(form.first_ip) }} + {{ render_field(form.last_ip) }} + {{ render_field(form.gateway) }} + {{ render_field(form.domain_id) }} + {{ render_field(form.admin_only) }} + {{ render_field(form.sensitive) }} + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-primary">Submit</button> + </div> + </div> + </form> +{%- endblock %} diff --git a/app/templates/network/view_network.html b/app/templates/network/view_network.html index 31276ac049fdb4b48aef233e03f65ecabb6fd301..b5676f0096b34a0c98d59a8b1f9db25776015532 100644 --- a/app/templates/network/view_network.html +++ b/app/templates/network/view_network.html @@ -7,6 +7,9 @@ <li class="nav-item"> <a class="nav-link active" href="{{ url_for('network.view_network', vlan_name=network.vlan_name) }}">View network</a> </li> +<li class="nav-item"> + <a class="nav-link" href="{{ url_for('network.edit_network', vlan_name=network.vlan_name) }}">Edit network</a> +</li> {% endblock %} {% block networks_main %} diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index 92410ea71ae5aeb3f509423021a9d6648cfb9241..460db6d351bf70f6994683a2e0cccb14cf82bc2c 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -750,6 +750,55 @@ def test_create_network_no_vlan( assert network.vlan_id is None +def test_edit_network( + logged_admin_client, domain_factory, network_scope_factory, network_factory +): + domain = domain_factory(name="lab.example.org") + scope = network_scope_factory( + name="MyNetworks", + first_vlan=100, + last_vlan=200, + supernet="192.168.0.0/16", + domain_id=domain.id, + ) + vlan_name = "my-network" + network = network_factory( + vlan_name=vlan_name, + domain=domain, + scope=scope, + vlan_id=100, + address="192.168.0.0/24", + first_ip="192.168.0.11", + last_ip="192.168.0.249", + gateway="192.168.0.254", + admin_only=False, + sensitive=False, + ) + new_first_ip = "192.168.0.10" + form = { + "vlan_name": vlan_name, + "vlan_id": network.vlan_id, + "address": network.address, + "first_ip": new_first_ip, + "last_ip": network.last_ip, + "gateway": network.gateway, + "domain_id": network.domain_id, + "admin_only": True, + "sensitive": True, + } + response = logged_admin_client.post( + f"/network/networks/edit/{vlan_name}", data=form + ) + assert response.status_code == 302 + # The network was updated + network = models.Network.query.filter_by(vlan_name=vlan_name).first() + assert network is not None + assert network.vlan_name == vlan_name + assert network.first_ip == new_first_ip + assert network.admin_only is True + assert network.sensitive is True + + def test_create_item_invalid_ics_id(logged_rw_client): ics_id = "AAA1100" form = {"ics_id": ics_id, "serial_number": "12345"}