diff --git a/app/models.py b/app/models.py index ed5d6ed62f343d7d546a2783fc754a843c0ebcbb..8ce4e5d7aae736a456bbabc0652da5ad25e1def6 100644 --- a/app/models.py +++ b/app/models.py @@ -840,7 +840,7 @@ class ItemComment(CreatedMixin, db.Model): class Network(CreatedMixin, db.Model): vlan_name = db.Column(CIText, nullable=False, unique=True) - vlan_id = db.Column(db.Integer, nullable=False, unique=True) + vlan_id = db.Column(db.Integer, nullable=True, unique=True) address = db.Column(postgresql.CIDR, nullable=False, unique=True) first_ip = db.Column(postgresql.INET, nullable=False, unique=True) last_ip = db.Column(postgresql.INET, nullable=False, unique=True) @@ -1571,8 +1571,8 @@ class Domain(CreatedMixin, db.Model): class NetworkScope(CreatedMixin, db.Model): __tablename__ = "network_scope" name = db.Column(CIText, nullable=False, unique=True) - first_vlan = db.Column(db.Integer, nullable=False, unique=True) - last_vlan = db.Column(db.Integer, nullable=False, unique=True) + first_vlan = db.Column(db.Integer, nullable=True, unique=True) + last_vlan = db.Column(db.Integer, nullable=True, unique=True) supernet = db.Column(postgresql.CIDR, nullable=False, unique=True) domain_id = db.Column(db.Integer, db.ForeignKey("domain.id"), nullable=False) description = db.Column(db.Text) @@ -1603,6 +1603,8 @@ class NetworkScope(CreatedMixin, db.Model): The range is defined by the first and last vlan """ + if self.first_vlan is None or self.last_vlan is None: + return [] return range(self.first_vlan, self.last_vlan + 1) def used_vlans(self): @@ -1610,6 +1612,8 @@ class NetworkScope(CreatedMixin, db.Model): The list is sorted """ + if self.first_vlan is None or self.last_vlan is None: + return [] return sorted(network.vlan_id for network in self.networks) def available_vlans(self): diff --git a/app/network/forms.py b/app/network/forms.py index ad7b7a1e7c0bbe6574b132acba1eb37561d7897e..2b5422e8c56d9fa3946a1ef2d6fb717057dfb2ac 100644 --- a/app/network/forms.py +++ b/app/network/forms.py @@ -92,8 +92,8 @@ class NetworkScopeForm(CSEntryForm): ], ) description = TextAreaField("Description") - first_vlan = IntegerField("First vlan") - last_vlan = IntegerField("Last vlan") + first_vlan = IntegerField("First vlan", validators=[validators.optional()]) + last_vlan = IntegerField("Last vlan", validators=[validators.optional()]) supernet = StringField( "Supernet", validators=[validators.InputRequired(), IPNetwork()] ) @@ -115,7 +115,9 @@ class NetworkForm(CSEntryForm): Unique(models.Network, column="vlan_name"), ], ) - vlan_id = NoValidateSelectField("Vlan id", choices=[]) + vlan_id = NoValidateSelectField( + "Vlan id", choices=[], coerce=utils.coerce_to_str_or_none + ) description = TextAreaField("Description") prefix = NoValidateSelectField("Prefix", choices=[]) address = NoValidateSelectField("Address", choices=[]) diff --git a/app/network/views.py b/app/network/views.py index 60860d444c04417cbcd6f4154472b457fc65e874..7b4895eec541ba5d8ac2418a0d6d63f8f45d0f67 100644 --- a/app/network/views.py +++ b/app/network/views.py @@ -665,6 +665,10 @@ def retrieve_scope_defaults(scope_id): } else: vlans = [vlan_id for vlan_id in scope.available_vlans()] + if vlans: + selected_vlan = vlans[0] + else: + selected_vlan = "" prefixes = scope.prefix_range() default_prefix = current_app.config["NETWORK_DEFAULT_PREFIX"] if default_prefix in prefixes: @@ -674,7 +678,7 @@ def retrieve_scope_defaults(scope_id): data = { "vlans": vlans, "prefixes": prefixes, - "selected_vlan": vlans[0], + "selected_vlan": selected_vlan, "selected_prefix": selected_prefix, "domain_id": scope.domain_id, } diff --git a/app/static/js/csentry.js b/app/static/js/csentry.js index 78d6775491a2d224231aff537be39298f40051cf..d23fe0e018b330572645c20e9a43601b647b4fed 100644 --- a/app/static/js/csentry.js +++ b/app/static/js/csentry.js @@ -71,7 +71,12 @@ function update_selectfield(field_id, data, selected_value) { } $field.append($("<option></option>").attr("value", option).text(text)); }); - $field.val(selected_value); + if( selected_value == "" ) { + $field.prop("disabled", true); + } else { + $field.val(selected_value); + $field.prop("disabled", false); + } } // Function to populate dynamically the stack_member field diff --git a/migrations/versions/33720bfb353a_allow_null_vlan_id.py b/migrations/versions/33720bfb353a_allow_null_vlan_id.py new file mode 100644 index 0000000000000000000000000000000000000000..a2a736527b00d99e5673084c915a45539e2b1c7a --- /dev/null +++ b/migrations/versions/33720bfb353a_allow_null_vlan_id.py @@ -0,0 +1,36 @@ +"""Allow null vlan id + +Revision ID: 33720bfb353a +Revises: cb777d44627f +Create Date: 2019-06-10 17:03:44.867111 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "33720bfb353a" +down_revision = "cb777d44627f" +branch_labels = None +depends_on = None + + +def upgrade(): + op.alter_column("network", "vlan_id", existing_type=sa.INTEGER(), nullable=True) + op.alter_column( + "network_scope", "first_vlan", existing_type=sa.INTEGER(), nullable=True + ) + op.alter_column( + "network_scope", "last_vlan", existing_type=sa.INTEGER(), nullable=True + ) + + +def downgrade(): + op.alter_column( + "network_scope", "last_vlan", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column( + "network_scope", "first_vlan", existing_type=sa.INTEGER(), nullable=False + ) + op.alter_column("network", "vlan_id", existing_type=sa.INTEGER(), nullable=False) diff --git a/tests/functional/test_web.py b/tests/functional/test_web.py index bf7f787e6cf09cff6fb2a824142e11705503cdb4..5907be5d9a318f7de209ca7650b331ffe8dca9f1 100644 --- a/tests/functional/test_web.py +++ b/tests/functional/test_web.py @@ -542,3 +542,113 @@ def test_delete_host_as_normal_user(logged_rw_client, host_factory, user_factory ) assert response.status_code == 403 assert len(models.Host.query.all()) == 1 + + +def test_create_network_scope(logged_admin_client, domain_factory): + domain = domain_factory(name="prod.example.org") + name = "MyNetworks" + first_vlan = 200 + last_vlan = 300 + supernet = "192.168.0.0/16" + form = { + "name": name, + "first_vlan": first_vlan, + "last_vlan": last_vlan, + "supernet": supernet, + "domain_id": domain.id, + } + response = logged_admin_client.post("/network/scopes/create", data=form) + assert response.status_code == 302 + # The network scope was created + scope = models.NetworkScope.query.filter_by(name=name).first() + assert scope is not None + assert scope.name == name + assert scope.first_vlan == first_vlan + assert scope.last_vlan == last_vlan + assert scope.supernet == supernet + + +def test_create_network_scope_no_vlan(logged_admin_client, domain_factory): + domain = domain_factory(name="lab.example.org") + name = "NoVlan" + supernet = "192.168.0.0/16" + form = { + "name": name, + "first_vlan": "", + "last_vlan": None, + "supernet": supernet, + "domain_id": domain.id, + } + response = logged_admin_client.post("/network/scopes/create", data=form) + assert response.status_code == 302 + # The network scope was created + scope = models.NetworkScope.query.filter_by(name=name).first() + assert scope is not None + assert scope.name == name + assert scope.first_vlan is None + assert scope.last_vlan is None + assert scope.supernet == supernet + + +def test_create_network(logged_admin_client, domain_factory, network_scope_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" + form = { + "scope_id": scope.id, + "vlan_name": vlan_name, + "vlan_id": 101, + "address": "192.168.0.0/24", + "first_ip": "192.168.0.11", + "last_ip": "192.168.0.249", + "gateway": "192.168.0.254", + "domain_id": domain.id, + "admin_only": False, + } + response = logged_admin_client.post("/network/networks/create", data=form) + assert response.status_code == 302 + # The network was created + network = models.Network.query.filter_by(vlan_name=vlan_name).first() + assert network is not None + assert network.vlan_name == vlan_name + assert network.address == form["address"] + assert network.vlan_id == form["vlan_id"] + + +def test_create_network_no_vlan( + logged_admin_client, domain_factory, network_scope_factory +): + domain = domain_factory(name="lab.example.org") + scope = network_scope_factory( + name="NoVlanNetworks", + first_vlan=None, + last_vlan=None, + supernet="192.168.0.0/16", + domain_id=domain.id, + ) + vlan_name = "my-network" + form = { + "scope_id": scope.id, + "vlan_name": vlan_name, + "vlan_id": "", + "address": "192.168.0.0/24", + "first_ip": "192.168.0.11", + "last_ip": "192.168.0.249", + "gateway": "192.168.0.254", + "domain_id": domain.id, + "admin_only": False, + } + response = logged_admin_client.post("/network/networks/create", data=form) + assert response.status_code == 302 + # The network was created + network = models.Network.query.filter_by(vlan_name=vlan_name).first() + assert network is not None + assert network.vlan_name == vlan_name + assert network.address == form["address"] + assert network.vlan_id is None