diff --git a/src/tests/ofc22/descriptors_emulated_xr.json b/src/tests/ofc22/descriptors_emulated_xr.json
index 4e247bb30d4df25fa75d30a3baa94f1348c0a6d9..b873d31143406a5f6cedbf19c1b357f2223d42d9 100644
--- a/src/tests/ofc22/descriptors_emulated_xr.json
+++ b/src/tests/ofc22/descriptors_emulated_xr.json
@@ -96,6 +96,18 @@
             "device_operational_status": 1,
             "device_drivers": [6],
             "device_endpoints": []
+        },
+        {
+            "device_id": {"device_uuid": {"uuid": "X2-XR-CONSTELLATION"}},
+            "device_type": "xr-constellation",
+            "device_config": {"config_rules": [
+                {"action": 1, "custom": {"resource_key": "_connect/address", "resource_value": "172.19.219.44"}},
+                {"action": 1, "custom": {"resource_key": "_connect/port", "resource_value": "443"}},
+                {"action": 1, "custom": {"resource_key": "_connect/settings", "resource_value": "{\"username\": \"xr-user-1\", \"password\": \"xr-user-1\", \"hub_module_name\": \"XR HUB 2\", \"consistency-mode\": \"lifecycle\"}"}}
+            ]},
+            "device_operational_status": 1,
+            "device_drivers": [6],
+            "device_endpoints": []
         }
     ],
     "links": [
diff --git a/src/webui/service/service/routes.py b/src/webui/service/service/routes.py
index defbe2cb003cc97830d6ec24db01bf8734a7f530..70a5b5bad41df6520cb2facdad94cfee04f726cd 100644
--- a/src/webui/service/service/routes.py
+++ b/src/webui/service/service/routes.py
@@ -12,27 +12,56 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+from contextlib import contextmanager
+import json
 import grpc
-from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for
+from collections import defaultdict
+from flask import current_app, redirect, render_template, Blueprint, flash, session, url_for, request
 from common.proto.context_pb2 import (
-    IsolationLevelEnum, Service, ServiceId, ServiceTypeEnum, ServiceStatusEnum, Connection)
+    IsolationLevelEnum, Service, ServiceId, ServiceTypeEnum, ServiceStatusEnum, Connection, Empty, DeviceDriverEnum, ConfigActionEnum, Device, DeviceList)
 from common.tools.context_queries.Context import get_context
+from common.tools.context_queries.Topology import get_topology
 from common.tools.context_queries.EndPoint import get_endpoint_names
 from common.tools.context_queries.Service import get_service
 from context.client.ContextClient import ContextClient
 from service.client.ServiceClient import ServiceClient
+from typing import Optional, Set
+from common.tools.object_factory.Topology import json_topology_id
+from common.tools.object_factory.ConfigRule import json_config_rule_set
+from common.tools.object_factory.Context import json_context_id
 
 service = Blueprint('service', __name__, url_prefix='/service')
 
 context_client = ContextClient()
 service_client = ServiceClient()
 
+@contextmanager
+def connected_client(c):
+    try:
+        c.connect()
+        yield c
+    finally:
+        c.close()
+
+# Context client must be in connected state when calling this function
+def get_device_drivers_in_use(topology_uuid: str, context_uuid: str) -> Set[str]:
+    active_drivers = set()
+    grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
+    topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
+    grpc_devices: DeviceList = context_client.ListDevices(Empty())
+    for device in grpc_devices.devices:
+        if device.device_id.device_uuid.uuid in topo_device_uuids:
+            for driver in device.device_drivers:
+                active_drivers.add(DeviceDriverEnum.Name(driver))
+    return active_drivers
+
 @service.get('/')
 def home():
     if 'context_uuid' not in session or 'topology_uuid' not in session:
         flash("Please select a context!", "warning")
         return redirect(url_for("main.home"))
     context_uuid = session['context_uuid']
+    topology_uuid = session['topology_uuid']
 
     context_client.connect()
 
@@ -44,10 +73,12 @@ def home():
         try:
             services = context_client.ListServices(context_obj.context_id)
             services = services.services
+            active_drivers = get_device_drivers_in_use(topology_uuid, context_uuid)
         except grpc.RpcError as e:
             if e.code() != grpc.StatusCode.NOT_FOUND: raise
             if e.details() != 'Context({:s}) not found'.format(context_uuid): raise
             services, device_names, endpoints_data = list(), dict(), dict()
+            active_drivers = set()
         else:
             endpoint_ids = list()
             for service_ in services:
@@ -57,7 +88,7 @@ def home():
     context_client.close()
     return render_template(
         'service/home.html', services=services, device_names=device_names, endpoints_data=endpoints_data,
-        ste=ServiceTypeEnum, sse=ServiceStatusEnum)
+        ste=ServiceTypeEnum, sse=ServiceStatusEnum, active_drivers=active_drivers)
 
 
 @service.route('add', methods=['GET', 'POST'])
@@ -67,6 +98,151 @@ def add():
     #return render_template('service/home.html')
 
 
+def get_hub_module_name(dev: Device) -> Optional[str]:
+    for cr in dev.device_config.config_rules:
+        if cr.action == ConfigActionEnum.CONFIGACTION_SET and cr.custom and cr.custom.resource_key == "_connect/settings":
+            try:
+                cr_dict = json.loads(cr.custom.resource_value)
+                if "hub_module_name" in cr_dict:
+                    return cr_dict["hub_module_name"]
+            except json.JSONDecodeError:
+                pass
+    return None
+
+@service.route('add-xr', methods=['GET', 'POST'])
+def add_xr():
+    ### FIXME: copypaste
+    if 'context_uuid' not in session or 'topology_uuid' not in session:
+        flash("Please select a context!", "warning")
+        return redirect(url_for("main.home"))
+
+    context_uuid = session['context_uuid']
+    topology_uuid = session['topology_uuid']
+
+    context_client.connect()
+    grpc_topology = get_topology(context_client, topology_uuid, context_uuid=context_uuid, rw_copy=False)
+    if grpc_topology is None:
+        flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger')
+        return redirect(url_for("main.home"))
+    else:
+        topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids}
+        grpc_devices= context_client.ListDevices(Empty())
+        devices = [
+            device for device in grpc_devices.devices
+            if device.device_id.device_uuid.uuid in topo_device_uuids and DeviceDriverEnum.DEVICEDRIVER_XR in device.device_drivers
+        ]
+        devices.sort(key=lambda dev: dev.name)
+
+        hub_interfaces_by_device = defaultdict(list)
+        leaf_interfaces_by_device = defaultdict(list)
+        constellation_name_to_uuid = {}
+        dev_ep_to_uuid = {}
+        ep_uuid_to_name = {}
+        for d in devices:
+            constellation_name_to_uuid[d.name] = d.device_id.device_uuid.uuid
+            hm_name = get_hub_module_name(d)
+            if hm_name is not None:
+                hm_if_prefix= hm_name + "|"
+                for ep in d.device_endpoints:
+                    dev_ep_to_uuid[(d.name, ep.name)] = ep.endpoint_id.endpoint_uuid.uuid
+                    if ep.name.startswith(hm_if_prefix):
+                        hub_interfaces_by_device[d.name].append(ep.name)
+                    else:
+                        leaf_interfaces_by_device[d.name].append(ep.name)
+                    ep_uuid_to_name[ep.endpoint_id.endpoint_uuid.uuid] = (d.name, ep.name)
+                hub_interfaces_by_device[d.name].sort()
+                leaf_interfaces_by_device[d.name].sort()
+
+        # Find out what endpoints are already used so that they can be disabled
+        # in the create screen
+        context_obj = get_context(context_client, context_uuid, rw_copy=False)
+        if context_obj is None:
+            flash('Context({:s}) not found'.format(str(context_uuid)), 'danger')
+            return redirect(request.url)
+        
+        services = context_client.ListServices(context_obj.context_id)
+        ep_used_by={}
+        for service in services.services:
+            if  service.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE:
+                for ep in service.service_endpoint_ids:
+                    ep_uuid = ep.endpoint_uuid.uuid
+                    if ep_uuid in ep_uuid_to_name:
+                        dev_name, ep_name = ep_uuid_to_name[ep_uuid]
+                        ep_used_by[f"{ep_name}@{dev_name}"] = service.name
+
+    context_client.close()
+
+    if request.method != 'POST':
+        return render_template('service/add-xr.html', devices=devices, hub_if=hub_interfaces_by_device, leaf_if=leaf_interfaces_by_device, ep_used_by=ep_used_by)
+    else:
+        service_name = request.form["service_name"]
+        if service_name == "":
+            flash(f"Service name must be specified", 'danger')
+
+        constellation = request.form["constellation"]
+        constellation_uuid = constellation_name_to_uuid.get(constellation, None)
+        if constellation_uuid is None:
+            flash(f"Invalid constellation \"{constellation}\"", 'danger')
+
+        hub_if = request.form["hubif"]
+        hub_if_uuid = dev_ep_to_uuid.get((constellation, hub_if), None)
+        if hub_if_uuid is None:
+            flash(f"Invalid hub interface \"{hub_if}\"", 'danger')
+
+        leaf_if = request.form["leafif"]
+        leaf_if_uuid = dev_ep_to_uuid.get((constellation, leaf_if), None)
+        if leaf_if_uuid is None:
+            flash(f"Invalid leaf interface \"{leaf_if}\"", 'danger')
+        
+        if service_name == "" or constellation_uuid is None or hub_if_uuid is None or leaf_if_uuid is None:
+            return redirect(request.url)
+        
+        
+        json_context_uuid=json_context_id(context_uuid)
+        sr = {
+            "name": service_name,
+            "service_id": {
+                 "context_id": {"context_uuid": {"uuid": context_uuid}},
+                 "service_uuid": {"uuid": service_name}
+            },
+            'service_type'        : ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE,
+            "service_endpoint_ids": [
+                {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': hub_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)},
+                {'device_id': {'device_uuid': {'uuid': constellation_uuid}}, 'endpoint_uuid': {'uuid': leaf_if_uuid}, 'topology_id': json_topology_id("admin", context_id=json_context_uuid)}
+            ],
+            'service_status'      : {'service_status': ServiceStatusEnum.SERVICESTATUS_PLANNED},
+            'service_constraints' : [],
+        }
+
+        json_tapi_settings = {
+            'capacity_value'  : 50.0,
+            'capacity_unit'   : 'GHz',
+            'layer_proto_name': 'PHOTONIC_MEDIA',
+            'layer_proto_qual': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC',
+            'direction'       : 'UNIDIRECTIONAL',
+        }
+        config_rule = json_config_rule_set('/settings', json_tapi_settings)
+
+        with connected_client(service_client) as sc:
+            endpoints, sr['service_endpoint_ids'] = sr['service_endpoint_ids'], []
+            try:
+                create_response = sc.CreateService(Service(**sr))
+            except Exception as e:
+                flash(f'Failure to update service name {service_name} with endpoints and configuration, exception {str(e)}', 'danger')
+                return redirect(request.url)
+            
+            sr['service_endpoint_ids'] = endpoints
+            sr['service_config'] = {'config_rules': [config_rule]}
+
+            try:
+                update_response = sc.UpdateService(Service(**sr))
+                flash(f'Created service {update_response.service_uuid.uuid}', 'success')
+            except Exception as e: 
+                flash(f'Failure to update service {create_response.service_uuid.uuid} with endpoints and configuration, exception {str(e)}', 'danger')
+                return redirect(request.url)
+
+            return redirect(url_for('service.home'))
+
 @service.get('<path:service_uuid>/detail')
 def detail(service_uuid: str):
     if 'context_uuid' not in session or 'topology_uuid' not in session:
diff --git a/src/webui/service/templates/service/add-xr.html b/src/webui/service/templates/service/add-xr.html
new file mode 100644
index 0000000000000000000000000000000000000000..36fe132caa7df1e3c72fa09ff2c39e2a92a7a357
--- /dev/null
+++ b/src/webui/service/templates/service/add-xr.html
@@ -0,0 +1,105 @@
+<!--
+ Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/)
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+{% extends 'base.html' %}
+
+{% block content %}
+    <script>
+        var js_hub_if = JSON.parse('{{hub_if | tojson | safe}}');
+        var js_leaf_if = JSON.parse('{{leaf_if | tojson | safe}}');
+        var js_ep_used_by = JSON.parse('{{ep_used_by | tojson | safe}}');
+
+        function clear_select_except_first(s) {
+            while (s.options.length > 1) {                
+                s.remove(1);
+            }        
+        }
+
+        function add_ep_to_select(sel, dev_name, ep_name) {
+            used_by = js_ep_used_by[ep_name + "@" + dev_name];
+            var o;
+            if (used_by === undefined) {
+                o = new Option(ep_name, ep_name)
+            } else {
+                o = new Option(ep_name + " (used by " + used_by + ")", ep_name)
+                o.disabled=true
+            }
+            sel.add(o);
+        }
+
+        function constellationSelected() {
+            const constellation_select = document.getElementById('constellation');
+            const hubif_select = document.getElementById('hubif');
+            const leafif_select = document.getElementById('leafif');
+
+            clear_select_except_first(hubif_select)
+            clear_select_except_first(leafif_select)
+            if (constellation_select.value) {
+                const hub_ifs=js_hub_if[constellation_select.value]
+                for (const hi of hub_ifs) {
+                    add_ep_to_select(hubif_select, constellation_select.value, hi);
+                }
+
+                const leaf_ifs=js_leaf_if[constellation_select.value]
+                for (const li of leaf_ifs) {
+                    add_ep_to_select(leafif_select, constellation_select.value, li);
+                }
+            }
+        }
+    </script>
+
+    <h1>Add XR Service</h1>
+        <form action="#" method="post">
+            <fieldset class="form-group row mb-3">
+                <label for="service_name" class="col-sm-3 col-form-label">Service name:</label>
+                <div class="col-sm-9">
+                    <input type="text" id="service_name" name="service_name" class="form-control">
+                </div>
+            </fieldset>
+
+            <fieldset class="form-group row mb-3">
+                <label for="constellation" class="col-sm-3 col-form-label">Constellation:</label>
+                <div class="col-sm-9">
+                    <select name="constellation" id="constellation" onchange="constellationSelected()" class="form-select">
+                        <option value="">(choose constellation)</option>
+                        {% for dev in devices %}
+                            <option value="{{dev.name}}">{{dev.name}}</option>
+                        {% endfor %}
+                    </select>
+                </div>
+            </fieldset>
+
+            <fieldset class="form-group row mb-3">
+                <label for="hubif" class="col-sm-3 col-form-label">Hub Endpoint:</label>
+                <div class="col-sm-9">
+                    <select name="hubif" id="hubif" class="col-sm-8 form-select">
+                        <option value="">(choose hub endpoint)</option>
+                    </select>
+                </div>
+            </fieldset>
+
+            <fieldset class="form-group row mb-3">
+                <label for="leafif" class="col-sm-3 col-form-label">Leaf Endpoint:</label>
+                <div class="col-sm-9">
+                    <select name="leafif" id="leafif" class="col-sm-8 form-select">
+                        <option value="">(choose leaf endpoint)</option>
+                    </select>
+                </div>
+            </fieldset>
+
+            <input type="submit" class="btn btn-primary" value="Create">
+        </form>
+{% endblock %}
diff --git a/src/webui/service/templates/service/home.html b/src/webui/service/templates/service/home.html
index 79b55c962dcdd0af4a380928c180f6c9def75ba7..00feaff59128dd026ab2bdb369229a9d0aaae805 100644
--- a/src/webui/service/templates/service/home.html
+++ b/src/webui/service/templates/service/home.html
@@ -26,6 +26,18 @@
                 Add New Service
             </a>
         </div> -->
+
+        <!-- Only display XR service addition button if there are XR constellations. Otherwise it might confuse
+             user, as other service types do not have GUI to add service yet. -->
+        {% if "DEVICEDRIVER_XR" in active_drivers %}
+            <div class="col">
+                <a href="{{ url_for('service.add_xr') }}" class="btn btn-primary" style="margin-bottom: 10px;">
+                    <i class="bi bi-plus"></i>
+                    Add New XR Service
+                </a>
+            </div>
+        {% endif %}
+
         <div class="col">
             {{ services | length }} services found in context <i>{{ session['context_uuid'] }}</i>
         </div>