diff --git a/app/main/forms.py b/app/main/forms.py
index e542e5b207f26b4dcc0561928f57efc28d864f5e..341bbb1231ee59bb5bf304ffedc345c7b995024a 100644
--- a/app/main/forms.py
+++ b/app/main/forms.py
@@ -11,7 +11,19 @@ This module defines the main forms.
 """
 from flask_wtf import FlaskForm
 from wtforms import SelectField, StringField, validators
-from .. import utils
+from .. import utils, models
+
+
+class NoValidateSelectField(SelectField):
+    """SelectField with no choices validation
+
+    By default a SelectField tries to validate the selected value
+    against the list of choices. This is not possible when the choices
+    are dynamically created on the browser side.
+    """
+
+    def pre_validate(self, form):
+        pass
 
 
 class QRCodeForm(FlaskForm):
@@ -23,3 +35,15 @@ class QRCodeForm(FlaskForm):
         self.kind.choices = utils.get_choices(
             ('Manufacturer', 'Model', 'Location')
         )
+
+
+class HostForm(FlaskForm):
+    network_id = SelectField('Network')
+    # The list of IPs is dynamically created on the browser side
+    # depending on the selected network
+    ip = NoValidateSelectField('IP', choices=[])
+    name = StringField('Hostname')
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.network_id.choices = [(str(network.id), network.prefix) for network in models.Network.query.all()]
diff --git a/app/main/views.py b/app/main/views.py
index bc1fb383c6a0224da9535d9d8a097bfd0419a14f..409b1c711f72190884a5b5b9292a389c56b2f4d0 100644
--- a/app/main/views.py
+++ b/app/main/views.py
@@ -11,13 +11,12 @@ This module implements the main blueprint.
 """
 import sqlalchemy as sa
 from flask import (Blueprint, render_template, jsonify,
-                   redirect, url_for, request, flash)
+                   redirect, url_for, request, flash, current_app)
 from flask_login import login_required
-from .forms import QRCodeForm
+from .forms import QRCodeForm, HostForm
 from ..extensions import db
-from ..models import Action, Manufacturer, Model, Location, Status, Item
 from ..decorators import login_groups_accepted
-from .. import utils
+from .. import utils, models
 
 bp = Blueprint('main', __name__)
 
@@ -43,7 +42,7 @@ def handle_csentry_error(error):
 @bp.route('/_retrieve_items')
 @login_required
 def retrieve_items():
-    items = Item.query.order_by(Item._created)
+    items = models.Item.query.order_by(models.Item._created)
     data = [[item.id,
              item.ics_id,
              utils.format_field(item._created),
@@ -67,7 +66,7 @@ def index():
 @bp.route('/view/<ics_id>')
 @login_required
 def view_item(ics_id):
-    item = Item.query.filter_by(ics_id=ics_id).first_or_404()
+    item = models.Item.query.filter_by(ics_id=ics_id).first_or_404()
     return render_template('view_item.html', item=item.to_dict(long=True))
 
 
@@ -75,7 +74,7 @@ def view_item(ics_id):
 @login_required
 def qrcodes():
     codes = {}
-    for model in (Action, Manufacturer, Model, Location, Status):
+    for model in (models.Action, models.Manufacturer, models.Model, models.Location, models.Status):
         items = db.session.query(model).order_by(model.name)
         images = [{'name': item.name,
                    'data': utils.image_to_base64(item.image())}
@@ -112,3 +111,47 @@ def retrieve_qrcodes_name(kind):
     items = db.session.query(model).order_by(model.name)
     data = [[item.name] for item in items]
     return jsonify(data=data)
+
+
+@bp.route('/hosts', methods=('GET', 'POST'))
+@login_groups_accepted('admin', 'create')
+def hosts_index():
+    # Try to get the network_id from the URL parameters
+    # to display the form with the same selected network
+    # when reloading the page (redirect after submit)
+    try:
+        network_id = request.args['network_id']
+    except KeyError:
+        # No need to pass request.form when no extra keywords are given
+        form = HostForm()
+    else:
+        form = HostForm(request.form, network_id=network_id)
+    if form.validate_on_submit():
+        host = models.Host(ip=form.ip.data,
+                           network_id=form.network_id.data,
+                           name=form.name.data)
+        current_app.logger.debug(f'Trying to create: {host!r}')
+        db.session.add(host)
+        try:
+            db.session.commit()
+        except sa.exc.IntegrityError as e:
+            db.session.rollback()
+            current_app.logger.warning(f'{e}')
+            flash(f'{e}', 'error')
+        return redirect(url_for('main.hosts_index', network_id=host.network_id))
+    return render_template('hosts.html', form=form)
+
+
+@bp.route('/_retrieve_hosts')
+@login_required
+def retrieve_hosts():
+    data = [(host.name, host.ip, host.network.prefix) for host in models.Host.query.all()]
+    return jsonify(data=data)
+
+
+@bp.route('/_retrieve_available_ips/<network_id>')
+@login_required
+def retrieve_available_ips(network_id):
+    network = models.Network.query.get(network_id)
+    data = [str(address) for address in network.available_ips()]
+    return jsonify(data=data)
diff --git a/app/models.py b/app/models.py
index 0710b0d6a5874e481a37a0618027ec97fc7e6014..c5b0969baeaf535a227a2f43d6e7d58a86158f03 100644
--- a/app/models.py
+++ b/app/models.py
@@ -377,7 +377,8 @@ class Host(db.Model):
     ip = db.Column(postgresql.INET, nullable=False, unique=True)
     name = db.Column(db.Text, unique=True)
 
-    mac = db.relationship('Mac', backref='host')
+    # This is a One To One relationship (set uselist to False)
+    mac = db.relationship('Mac', backref='host', uselist=False)
 
     def __init__(self, **kwargs):
         # Automatically convert network to an instance of Network if it was passed
@@ -401,6 +402,9 @@ class Host(db.Model):
     def __str__(self):
         return str(self.ip)
 
+    def __repr__(self):
+        return f'Host(id={self.id}, network_id={self.network_id}, ip={self.ip}, name={self.name}, mac={self.mac})'
+
     def to_dict(self, long=False):
         d = {
             'id': self.id,
diff --git a/app/static/js/hosts.js b/app/static/js/hosts.js
new file mode 100644
index 0000000000000000000000000000000000000000..38c7e994fa17adc41a2de729c78c42dbf2a63a40
--- /dev/null
+++ b/app/static/js/hosts.js
@@ -0,0 +1,38 @@
+$(document).ready(function() {
+
+  function update_available_ips() {
+    // Retrieve available IPs for the selected network
+    // and update the IP select field
+    var network_id = $("#network_id").val();
+    $.getJSON(
+      $SCRIPT_ROOT + "/_retrieve_available_ips/" + network_id,
+      function(json) {
+        var $ip = $("#ip");
+        $ip.empty();
+        $.map(json.data, function(option, index) {
+          $ip.append($("<option></option>").attr("value", option).text(option));
+        });
+      }
+    );
+  }
+
+  // Populate IP select field on first page load
+  update_available_ips();
+
+  // Update IP select field when changing network
+  $("#network_id").on('change', function() {
+    update_available_ips();
+  });
+
+  var hosts_table =  $("#hosts_table").DataTable({
+    "ajax": function(data, callback, settings) {
+      $.getJSON(
+        $SCRIPT_ROOT + "/_retrieve_hosts",
+        function(json) {
+          callback(json);
+        });
+    },
+    "paging": false
+  });
+
+});
diff --git a/app/templates/base.html b/app/templates/base.html
index 19ed9bd0abeb2383f3ac0a3ca93677790885412a..c60b5a8608ee7b02dae617113d196d043ba40229 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -23,7 +23,8 @@
       <div class="collapse navbar-collapse" id="navbarSupportedContent">
         <div class="navbar-nav mr-auto">
           {% set path = request.path %}
-          <a class="nav-item nav-link {{ is_active(path in ("/", "/index", "/index/")) }}" href="{{ url_for('main.index') }}">Index</a>
+          <a class="nav-item nav-link {{ is_active(path in ("/", "/index", "/index/")) }}" href="{{ url_for('main.index') }}">Items</a>
+          <a class="nav-item nav-link {{ is_active(path == "/hosts") }}" href="{{ url_for('main.hosts_index') }}">Hosts</a>
           <div class="dropdown {{ is_active(path.startswith("/qrcodes")) }}">
             <a class="nav-link dropdown-toggle" href="#" id="qrcodesDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
               QR Codes
diff --git a/app/templates/hosts.html b/app/templates/hosts.html
new file mode 100644
index 0000000000000000000000000000000000000000..14adf60ba46fa5dc0154180632f86726245098d4
--- /dev/null
+++ b/app/templates/hosts.html
@@ -0,0 +1,50 @@
+{%- extends "base.html" %}
+
+{% block title %}Hosts - CSEntry{% endblock %}
+
+{% block main %}
+  <h3>Host</h3>
+
+  <form id="hostForm" method="POST">
+    {{ form.hidden_tag() }}
+    <div class="form-group row">
+      {{ form.network_id.label(class_="col-sm-2 col-form-label") }}
+      <div class="col-sm-10">
+        {{ form.network_id(class_="form-control") }}
+      </div>
+    </div>
+    <div class="form-group row">
+      {{ form.ip.label(class_="col-sm-2 col-form-label") }}
+      <div class="col-sm-10">
+        {{ form.ip(class_="form-control") }}
+      </div>
+    </div>
+    <div class="form-group row">
+      {{ form.name.label(class_="col-sm-2 col-form-label") }}
+      <div class="col-sm-10">
+        {{ form.name(class_="form-control") }}
+      </div>
+    </div>
+    <div class="form-group row">
+      <div class="col-sm-10">
+        <button type="submit" class="btn btn-primary">Submit</button>
+      </div>
+    </div>
+  </form>
+
+  <hr class="separator">
+
+  <table id="hosts_table" class="table table-bordered table-hover table-sm">
+    <thead>
+      <tr>
+        <th>Hostname</th>
+        <th>IP</th>
+        <th>Network</th>
+      </tr>
+    </thead>
+  </table>
+{%- endblock %}
+
+{% block csentry_scripts %}
+  <script src="{{ url_for('static', filename='js/hosts.js') }}"></script>
+{% endblock %}