diff --git a/README.md b/README.md
index 092e7113c9655caafba53e15493d7e4a77ca0f1c..0bcf92ec1a7baab2025a40f6c7f005756044e083 100644
--- a/README.md
+++ b/README.md
@@ -1,11 +1,65 @@
-## netbox-proxmox-sync
+# NetBox Proxmox Sync (Plugin)
+FIXME: ta feio askasjkaksajks
 
-TODO: explain this properly (also add a changelog)
+This NetBox plugin integrates with a Proxmox cluster to import information
+about virtual machines (VMs) and containers (CTs) into NetBox's virtualization
+tab.
 
-Simple way to import a Proxmox cluster information into NetBox.
+## Installation
 
-Steps:
-1. Configure
-2. Run sync
-3. ???
-4. Happiness?
+1. Clone the repository into your NetBox installation's plugins directory:
+
+   ```bash
+   git clone https://github.com/your/repository.git /opt/netbox/netbox/plugins/netbox_proxmox
+
+    Install the required dependencies (if any):
+
+    bash
+
+pip install -r /opt/netbox/netbox/plugins/netbox_proxmox/requirements.txt
+
+Add 'netbox_proxmox' to the PLUGINS list in your NetBox configuration file (/opt/netbox/netbox/netbox/configuration.py).
+
+Perform database migrations to apply the plugin's models to your database:
+
+bash
+
+sudo /opt/netbox/netbox/manage.py migrate
+
+Restart your NetBox application server:
+
+bash
+
+    sudo systemctl restart netbox
+
+Configuration
+
+    Configure the plugin by editing the NetBox configuration file (/opt/netbox/netbox/netbox/configuration.py). Here's an example configuration:
+
+    python
+
+    # Proxmox API settings
+    PROXMOX_HOST = 'https://your-proxmox-host.example.com'
+    PROXMOX_API_USER = 'your_proxmox_api_user'
+    PROXMOX_API_PASSWORD = 'your_proxmox_api_password'
+
+    # NetBox settings
+    NETBOX_PROXMOX_CLUSTER_NAME = 'Your Proxmox Cluster Name'
+
+    Optionally, configure any additional settings required by the plugin.
+
+Usage
+
+    Once the plugin is installed and configured, navigate to the NetBox web interface.
+
+    Go to the virtualization tab to view imported VM and CT information from your Proxmox cluster.
+
+Contributing
+
+Contributions are welcome! Please fork the repository and submit pull requests to contribute improvements or new features.
+License
+
+This plugin is licensed under the GPL-3.0 License. See the LICENSE file for details.
+Support
+
+For support or questions about this plugin, please open an issue on the GitHub repository.
diff --git a/build/lib/netbox_proxmox_sync/__init__.py b/build/lib/netbox_proxmox_sync/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..29948087aef9ab10f486f6e0f5b76391a0e1e1b0
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/__init__.py
@@ -0,0 +1,12 @@
+from netbox.plugins import PluginConfig
+
+
+class NetBoxProxmoxSyncConfig(PluginConfig):
+    name = 'netbox_proxmox_sync'
+    verbose_name = 'NetBox Proxmox Sync'
+    description = 'Import cluster information from Proxmox into NetBox'
+    version = '2.0.0'
+    base_url = 'netbox-proxmox-sync'
+
+
+config = NetBoxProxmoxSyncConfig
diff --git a/build/lib/netbox_proxmox_sync/api/__init__.py b/build/lib/netbox_proxmox_sync/api/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/netbox_proxmox_sync/api/proxmox/config.py b/build/lib/netbox_proxmox_sync/api/config.py
similarity index 98%
rename from netbox_proxmox_sync/api/proxmox/config.py
rename to build/lib/netbox_proxmox_sync/api/config.py
index 6fcb37e40db0c1f024c9862b81513648471af3b0..66474ce850bcae34dbda607c73f77601e4513e22 100644
--- a/netbox_proxmox_sync/api/proxmox/config.py
+++ b/build/lib/netbox_proxmox_sync/api/config.py
@@ -62,7 +62,7 @@ NETBOX_DEFAULT_TAG_COLOR = NETBOX_SETTINGS.get('default_tag_color_hex', NETBOX_D
 # Create connections
 # TODO: more descriptive errors
 try:
-    PROXMOX_API = ProxmoxAPI(
+    PX_API = ProxmoxAPI(
         PROXMOX_DOMAIN,
         user=PROXMOX_USER,
         port=PROXMOX_PORT,
@@ -79,7 +79,7 @@ try:
         url = f'https://{NETBOX_DOMAIN}:{NETBOX_PORT}'
     else:
         url = f'http://{NETBOX_DOMAIN}:{NETBOX_PORT}'
-    NETBOX_API = NetboxAPI(
+    NB_API = NetboxAPI(
         url,
         token=NETBOX_TOKEN,
         threading=True,
diff --git a/build/lib/netbox_proxmox_sync/api/netbox/create.py b/build/lib/netbox_proxmox_sync/api/netbox/create.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c0fc06b501a2d56666fd4ead327d19f019e2d88
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/netbox/create.py
@@ -0,0 +1,135 @@
+# TODO: more descriptive errors
+# FIXME: fetch_* is a shit name
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+from .delete import all as delete_everything
+
+
+def _assert_nodes_exist(proxmox_nodes):
+    node_names = set([node['name'] for node in proxmox_nodes])
+    existing_names = set([
+        node.name for node in NB_API.dcim.devices.filter(name=node_names)
+    ])
+    missing_nodes = list(node_names - existing_names)
+    if len(missing_nodes) > 0:
+        missing = [f'Node "{node_name}" is missing!' for node_name in missing_nodes]
+        raise ValidationError('Not all cluster nodes registered in NetBox!', missing)
+
+
+def _assert_cluster_does_not_exist(proxmox_cluster):
+    cluster_name = proxmox_cluster['name']
+    netbox_cluster_list = NB_API.virtualization.clusters.filter(name=cluster_name)
+    if len(netbox_cluster_list) != 0:
+        raise ValidationError(f'Virtualization cluster "{cluster_name}" already exists!')
+
+
+def cluster(proxmox_cluster, proxmox_nodes):
+    try:
+        # Already formatted the data for this :)
+        netbox_cluster = NB_API.virtualization.clusters.create(**proxmox_cluster)
+    except Exception as e:
+        raise APIError(e)
+    # Update nodes' cluster
+    node_names = [node['name'] for node in proxmox_nodes]
+    netbox_nodes = list(NB_API.dcim.devices.filter(name=[node_names]))
+    for node in netbox_nodes:
+        node.cluster = {'name': proxmox_cluster['name']}
+    if not NB_API.dcim.devices.update(netbox_nodes):
+        # In case of error "rollback"
+        netbox_cluster.delete()
+        raise APIError('Failed to set Nodes\' cluster!')
+    # Return JSON serializable dict :)
+    return dict(netbox_cluster)
+
+
+def tags():
+    proxmox_tags = Proxmox.fetch_tags()
+    try:
+        existing_tags = NB_API.extras.tags.filter(content_types=[
+            'virtualization.virtualmachines',
+        ])
+    except Exception as e:
+        raise APIError(e)
+    # Tags for VMs currently should only be managed by this plugin
+    # TODO: we could add a custom field to mark tags managed by the plugin, for instance
+    if len(existing_tags) > 0:
+        names = [tag.name for tag in existing_tags]
+        errors = [f'Tag "{name}" should not exist!' for name in names]
+        raise ValidationError('Some VM tags already exist!', errors)
+    try:
+        netbox_tags = NB_API.extras.tags.create(proxmox_tags)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(tag) for tag in netbox_tags]
+
+
+def custom_fields():
+    try:
+        vmid_already_exists = len(NB_API.extras.custom_fields.filter(
+            name='vmid', object_types='virtualization.virtualmachine'
+        )) > 0
+    except Exception as e:
+        raise APIError(e)
+    if vmid_already_exists:
+        raise ValidationError('Custom field "vmid" already exists!')
+    custom_fields = [{
+        'name': 'vmid',
+        'label': 'VMID',
+        'description': '[Proxmox] VM/CT ID',
+        'ui_editable': 'no',
+        'ui_visible': 'always',
+        'filter_logic': 'exact',
+        'type': 'integer',
+        'object_types': ['virtualization.virtualmachine']
+    }]
+    # Create stuff :)
+    try:
+        netbox_custom_fields = NB_API.extras.custom_fields.create(custom_fields)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(custom_field) for custom_field in netbox_custom_fields]
+
+
+def vms_and_interfaces():
+    proxmox_vms, proxmox_interfaces = Proxmox.fetch_virtual_machines_and_interfaces()
+    try:
+        netbox_vms = NB_API.virtualization.virtual_machines.create(proxmox_vms)
+        netbox_interfaces = NB_API.virtualization.interfaces.create(proxmox_interfaces)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable lists :)
+    return [dict(x) for x in netbox_vms], [dict(x) for x in netbox_interfaces]
+
+
+def all():
+    proxmox_cluster, proxmox_nodes = Proxmox.fetch_cluster_and_nodes()
+    _assert_nodes_exist(proxmox_nodes)
+    _assert_cluster_does_not_exist(proxmox_cluster)
+    # Leave cluster out first so the first error comes quickly, useful for API
+    #  errors like invalid token for instance, which might be common.
+    # (netbox takes tooooo long to delete <stuff>, even if <stuff> does not exist)
+    netbox_cluster = cluster(proxmox_cluster, proxmox_nodes)
+    # In case the cluster() call succeeds we are sure that we have write access
+    #  for the API, so any error that comes out of the following calls must be
+    #  something else (or you messed up things).
+    # This is useful because we can be pretty sure that a "rollback" is possible
+    # (the delete_everything() call below *should* NOT raise an exception).
+    try:
+        netbox_tags = tags()
+        netbox_custom_fields = custom_fields()
+        netbox_vms, netbox_interfaces = vms_and_interfaces()
+    except Exception as e:
+        # "rollback" in case of any failure
+        # (if this function fails you probably messed up in a very interesting way)
+        delete_everything()
+        raise e
+    return {
+        'cluster': netbox_cluster,
+        'tags': netbox_tags,
+        'custom_fields': netbox_custom_fields,
+        'vms': netbox_vms,
+        'interfaces': netbox_interfaces,
+    }
diff --git a/build/lib/netbox_proxmox_sync/api/netbox/delete.py b/build/lib/netbox_proxmox_sync/api/netbox/delete.py
new file mode 100644
index 0000000000000000000000000000000000000000..c01a199a2401b322f5af8c0ce9173aa9d441b1fe
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/netbox/delete.py
@@ -0,0 +1,91 @@
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+
+
+def cluster(proxmox_cluster):
+    cluster_name = proxmox_cluster['name']
+    try:
+        netbox_clusters = NB_API.virtualization.clusters.filter(name=cluster_name)
+    except Exception as e:
+        raise APIError(e)
+    if (len(netbox_clusters)) == 0:
+        raise ValidationError(f'Cluster "{cluster_name}" does not exist!')
+    netbox_cluster = list(netbox_clusters)[0]
+    # Return JSON serializable dict :)
+    return dict(netbox_cluster)
+
+
+def tags():
+    try:
+        netbox_tags = NB_API.extras.tags.filter(content_types=[
+            'virtualization.virtualmachines',
+        ])
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(tag) for tag in netbox_tags]
+
+
+def custom_fields():
+    try:
+        netbox_custom_fields = NB_API.extras.custom_fields.filter(
+            name='vmid',
+            object_types='virtualization.virtualmachine',
+        )
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(custom_field) for custom_field in netbox_custom_fields]
+
+
+def vms_and_interfaces(netbox_cluster):
+    try:
+        cluster_id = netbox_cluster['id']
+        netbox_vms = NB_API.virtualization.virtual_machines.filter(cluster_id=cluster_id)
+        netbox_interfaces = NB_API.virtualization.interfaces.filter(cluster_id=cluster_id)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable lists :)
+    return [dict(x) for x in netbox_vms], [dict(x) for x in netbox_interfaces]
+
+
+def delete_stuff(cluster, tags, custom_fields, vms, interfaces):
+    # This order is important
+    rt = NB_API.extras.tags.delete([tag['id'] for tag in tags])
+    rc = NB_API.extras.custom_fields.delete([field['id'] for field in custom_fields])
+    ri = NB_API.virtualization.interfaces.delete([i['id'] for i in interfaces])
+    rv = NB_API.virtualization.virtual_machines.delete([vm['id'] for vm in vms])
+    rf = NB_API.virtualization.clusters.delete([cluster['id']])
+    errors = []
+    if rt is None:
+        errors.append('Could not delete tags!')
+    if rf is None:
+        errors.append('Could not delete custom_fields!')
+    if rc is None:
+        errors.append('Could not delete cluster!')
+    if rv is None:
+        errors.append('Could not delete virtual machines!')
+    if ri is None:
+        errors.append('Could not delete interfaces!')
+    if len(errors) > 0:
+        raise APIError('Could not delete plugin information!', errors=errors)
+
+
+def all():
+    proxmox_cluster, _ = Proxmox.fetch_cluster_and_nodes()
+    netbox_cluster = cluster(proxmox_cluster)
+    netbox_tags = tags()
+    netbox_custom_fields = custom_fields()
+    netbox_vms, netbox_interfaces = vms_and_interfaces(netbox_cluster)
+    delete_stuff(
+        netbox_cluster, netbox_tags, netbox_custom_fields,
+        netbox_vms, netbox_interfaces
+    )
+    return {
+        'cluster': netbox_cluster,
+        'tags': netbox_tags,
+        'custom_fields': netbox_custom_fields,
+        'vms': netbox_vms,
+        'interfaces': netbox_interfaces,
+    }
diff --git a/build/lib/netbox_proxmox_sync/api/netbox/extract.py b/build/lib/netbox_proxmox_sync/api/netbox/extract.py
new file mode 100644
index 0000000000000000000000000000000000000000..077b70391c5f8052fbcb8a1b3d329ba2da722ec2
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/netbox/extract.py
@@ -0,0 +1,55 @@
+from netbox_proxmox_sync.api.config import (
+    NB_API,
+    NETBOX_SITE_ID,
+    NETBOX_CLUSTER_TYPE_ID,
+)
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.utils.models import (
+    tag_from_netbox,
+    vm_from_netbox,
+    interface_from_netbox,
+)
+
+
+class NetBox:
+    @staticmethod
+    def fetch_cluster(cluster_name):
+        try:
+            netbox_cluster = NB_API.virtualization.clusters.filter(name=cluster_name)
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        return {
+            'id': netbox_cluster.id,
+            'name': netbox_cluster.name,
+            'type': NETBOX_CLUSTER_TYPE_ID,
+            'description': netbox_cluster.description,
+            'site': NETBOX_SITE_ID,
+        }
+
+    @staticmethod
+    def fetch_tags():
+        try:
+            netbox_tags = [dict(tag) for tag in NB_API.extras.tags.filter(
+                content_types='virtualization.virtualmachines',
+            )]
+        except Exception as e:
+            raise APIError(e)
+
+        return [tag_from_netbox(tag) for tag in netbox_tags]
+
+    @staticmethod
+    def fetch_virtual_machines_and_interfaces(cluster_id):
+        try:
+            netbox_vms = [
+                dict(vm) for vm in NB_API.virtualization.virtual_machines.filter(
+                    cluster_id=cluster_id
+                )
+            ]
+            netbox_interfaces = [dict(i) for i in NB_API.virtualization.interfaces.all()]
+        except Exception as e:
+            raise APIError(e)
+
+        vms = [vm_from_netbox(vm) for vm in netbox_vms]
+        interfaces = [interface_from_netbox(i) for i in netbox_interfaces]
+
+        return vms, interfaces
diff --git a/build/lib/netbox_proxmox_sync/api/netbox/update.py b/build/lib/netbox_proxmox_sync/api/netbox/update.py
new file mode 100644
index 0000000000000000000000000000000000000000..368105da90e4222fd8f100273682e689f1e64639
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/netbox/update.py
@@ -0,0 +1,123 @@
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+from netbox_proxmox_sync.api.netbox.extract import NetBox
+
+
+# MERGE, get created_*, updated_*, deleted_*
+def tags():
+    proxmox_tags = Proxmox.fetch_tags()
+    netbox_tags = NetBox.fetch_tags()
+    px_tags = {tag['name']: tag for tag in proxmox_tags}
+    nb_tags = {tag['name']: tag for tag in netbox_tags}
+    # "merge" information, extract stuff to C/U/D
+    created = [tag for tag in proxmox_tags if tag['name'] not in nb_tags]
+    deleted = [tag for tag in netbox_tags if tag['name'] not in px_tags]
+    updated = []
+    for name in px_tags:
+        # only thing that can really change for tags is their color
+        if name in nb_tags and px_tags[name]['color'] != nb_tags[name]['color']:
+            nb_tags[name]['color'] = px_tags[name]['color']
+            updated.append(nb_tags[name])
+    # FIXME: error handling
+    NB_API.extras.tags.create(created)
+    NB_API.extras.tags.update(updated)
+    NB_API.extras.tags.delete([d['id'] for d in deleted])
+    return created, updated, deleted
+
+
+# TODO: allow for updates of custom fields in case the plugin is updated to use more
+# def custom_fields():
+
+
+def vms(proxmox_vms, netbox_vms):
+    px_vms = {vm['custom_fields']['vmid']: vm for vm in proxmox_vms}
+    nb_vms = {vm['custom_fields']['vmid']: vm for vm in netbox_vms}
+    # "merge" information, extract stuff to C/U/D
+    created = [vm for vm in proxmox_vms if vm['custom_fields']['vmid'] not in nb_vms]
+    deleted = [vm for vm in netbox_vms if vm['custom_fields']['vmid'] not in px_vms]
+    updated = []
+    for name in px_vms:
+        changed = False
+        px_vm = px_vms[name]
+        if nb_vms.get(name) is None:
+            continue
+        nb_vm = nb_vms[name]
+        # Update all fields
+        for field in px_vm:
+            changed |= nb_vm[field] != px_vm[field]
+            nb_vm[field] = px_vm[field]
+        if changed:
+            updated.append(nb_vm)
+    return created, updated, deleted
+
+
+def interfaces(proxmox_interfaces, netbox_interfaces):
+    # Note: if the MAC changes the IP for the VM is lost :)
+    px_interfaces = {i['mac_address']: i for i in proxmox_interfaces}
+    nb_interfaces = {i['mac_address']: i for i in netbox_interfaces}
+    created = [i for i in proxmox_interfaces if i['mac_address'] not in nb_interfaces]
+    deleted = [i for i in netbox_interfaces if i['mac_address'] not in px_interfaces]
+    updated = []
+    for name in px_interfaces:
+        changed = False
+        # Update all fields
+        px_interface = px_interfaces[name]
+        if nb_interfaces.get(name) is None:
+            continue
+        nb_interface = nb_interfaces[name]
+        for field in px_interface:
+            changed |= nb_interface[field] != px_interface[field]
+            nb_interface[field] = px_interface[field]
+        if changed:
+            updated.append(nb_interface)
+    return created, updated, deleted
+
+
+def vms_and_interfaces(cluster_id):
+    proxmox_vms, proxmox_interfaces = Proxmox.fetch_virtual_machines_and_interfaces()
+    netbox_vms, netbox_interfaces = NetBox.fetch_virtual_machines_and_interfaces(cluster_id)
+
+    created_vms, updated_vms, deleted_vms = vms(proxmox_vms, netbox_vms)
+    created_i, updated_i, deleted_i = interfaces(proxmox_interfaces, netbox_interfaces)
+
+    # FIXME: error handling
+    NB_API.virtualization.virtual_machines.create(created_vms)
+    NB_API.virtualization.virtual_machines.update(updated_vms)
+    NB_API.virtualization.virtual_machines.delete([d['id'] for d in deleted_vms])
+    NB_API.virtualization.interfaces.create(created_i)
+    NB_API.virtualization.interfaces.update(updated_i)
+    # NB_API.virtualization.interfaces.delete([d['id'] for d in deleted])
+
+    return created_vms, updated_vms, deleted_vms, \
+        created_i, updated_i, deleted_i
+
+
+def all():
+    proxmox_cluster = Proxmox.fetch_cluster()
+    cluster_name = proxmox_cluster['name']
+    clusters = list(NB_API.virtualization.clusters.filter(name=cluster_name))
+    if (len(clusters)) == 0:
+        raise ValidationError(f'Cluster "{cluster_name}" does not exist!')
+    netbox_cluster = clusters[0]
+    created_tags, updated_tags, deleted_tags = tags()
+    created_vms, updated_vms, deleted_vms, \
+        created_interfaces, updated_interfaces, deleted_interfaces = \
+        vms_and_interfaces(cluster_id=netbox_cluster.id)
+    return {
+        'tags': {
+            'created': created_tags,
+            'updated': updated_tags,
+            'deleted': deleted_tags,
+        },
+        'virtual_machines': {
+            'created': created_vms,
+            'updated': updated_vms,
+            'deleted': deleted_vms,
+        },
+        'interfaces': {
+            'created': created_interfaces,
+            'updated': updated_interfaces,
+            'deleted': deleted_interfaces,
+        }
+    }
diff --git a/build/lib/netbox_proxmox_sync/api/proxmox/extract.py b/build/lib/netbox_proxmox_sync/api/proxmox/extract.py
new file mode 100644
index 0000000000000000000000000000000000000000..34590706509b564cc57125f92c70cf54f4b26c4a
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/proxmox/extract.py
@@ -0,0 +1,119 @@
+from netbox_proxmox_sync.api.config import (
+    PX_API,
+    NETBOX_SITE_ID,
+    NETBOX_CLUSTER_DESCRIPTION,
+    NETBOX_CLUSTER_TYPE_ID,
+)
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.utils.models import (
+    tag_from_proxmox,
+    vm_from_proxmox,
+    interface_from_proxmox,
+)
+
+
+# Utils for proper data extraction
+def extract_vm_data(cluster_name, node_name, vm_status):
+    try:
+        vm_config = PX_API.nodes(node_name).qemu(vm_status['vmid']).config.get()
+    except Exception:
+        raise APIError('Failed to connect to Proxmox Cluster!')
+    tags = vm_config['tags'].split(';')
+    vm_data = vm_from_proxmox(cluster_name, node_name, vm_status, tags)
+    interfaces_data = extract_vm_interfaces(vm_config)
+
+    return vm_data, interfaces_data
+
+
+def extract_vm_interfaces(proxmox_vm_config):
+    vm_name = proxmox_vm_config['name']
+    interfaces = []
+    for interface_name in [key for key in proxmox_vm_config if key.startswith('net')]:
+        interface_info = proxmox_vm_config[interface_name]
+        new_interface = interface_from_proxmox(vm_name, interface_name, interface_info)
+        interfaces.append(new_interface)
+    return interfaces
+
+
+class Proxmox:
+    @staticmethod
+    def fetch_cluster():
+        cluster, _ = Proxmox.fetch_cluster_and_nodes()
+        return cluster
+
+    @staticmethod
+    def fetch_cluster_and_nodes():
+        try:
+            proxmox_cluster_info = PX_API.cluster.status.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        cluster = {
+            'name': proxmox_cluster_info[0]['name'],
+            'type': NETBOX_CLUSTER_TYPE_ID,
+            'description': NETBOX_CLUSTER_DESCRIPTION,
+            'site': NETBOX_SITE_ID,
+        }
+        return cluster, proxmox_cluster_info[1:]
+
+    @staticmethod
+    def fetch_tags():
+        try:
+            proxmox_tag_info = PX_API.cluster.options.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        allowed_tags = proxmox_tag_info['allowed-tags']
+        tag_colormap = proxmox_tag_info['tag-style']['color-map'].split(';')
+        tags = []
+        # Create tags with no color defined
+        for tag_name in allowed_tags:
+            tags.append(tag_from_proxmox(tag_name))
+        # Remap defined tag colors
+        for tag_info in tag_colormap:
+            tag_name = tag_info.split(':')[0]
+            tag_slug = tag_name.lower().replace(' ', '-').replace('.', '_')
+            tag_color = tag_info.split(':')[1].lower()
+            found = False
+            # Find existing tag and update its color
+            for tag in tags:
+                if tag['name'] == tag_name:
+                    found = True
+                    tag['color'] = tag_color
+                    break
+            # ???
+            if not found:
+                tags.append({
+                    'name': tag_name,
+                    'slug': tag_slug,
+                    'color': tag_color,
+                    'object_types': ['virtualization.virtualmachine']
+                })
+        return tags
+
+    @staticmethod
+    def fetch_virtual_machines_and_interfaces():
+        try:
+            cluster_status = PX_API.cluster.status.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        cluster_name = cluster_status[0]['name']
+        proxmox_nodes = [node['name'] for node in cluster_status[1:]]
+        # List, because node information is stored in the VMs themselves
+        # (same for interfaces: vm info is stored in the interface itself)
+        virtual_machines = []
+        interfaces = []
+        for node_name in proxmox_nodes:
+            try:
+                node_vms = PX_API.nodes(node_name).qemu.get()
+            except Exception:
+                raise APIError('Failed to connect to Proxmox Cluster!')
+            for vm_status in node_vms:
+                # If the update runs as the VM is being cloned the vm_config will be
+                # messed up and our update will fail.
+                if vm_status.get('lock') is not None and vm_status['lock'] == 'clone':
+                    continue
+                new_vm, new_interfaces = extract_vm_data(
+                    cluster_name, node_name, vm_status
+                )
+                virtual_machines.append(new_vm)
+                interfaces.extend(new_interfaces)
+        return virtual_machines, interfaces
diff --git a/build/lib/netbox_proxmox_sync/api/urls.py b/build/lib/netbox_proxmox_sync/api/urls.py
new file mode 100644
index 0000000000000000000000000000000000000000..5acc2eee3044bd82d59eb564322762d26f45a688
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/urls.py
@@ -0,0 +1,10 @@
+from django.urls import path
+from . import views
+
+app_name = 'netbox_proxmox_sync'
+
+urlpatterns = [
+    path('create/', views.CreateCluster.as_view(), name='proxmoxsync_create'),
+    path('update/', views.UpdateCluster.as_view(), name='proxmoxsync_update'),
+    path('delete/', views.DeleteCluster.as_view(), name='proxmoxsync_delete'),
+]
diff --git a/netbox_proxmox_sync/api/errors.py b/build/lib/netbox_proxmox_sync/api/utils/errors.py
similarity index 74%
rename from netbox_proxmox_sync/api/errors.py
rename to build/lib/netbox_proxmox_sync/api/utils/errors.py
index 2b49c48ff96402ed5b40e5a8851052f8cde343be..9b16ad5edb3ade134a6a7d39aa5c876c4ed1e07d 100644
--- a/netbox_proxmox_sync/api/errors.py
+++ b/build/lib/netbox_proxmox_sync/api/utils/errors.py
@@ -1,13 +1,13 @@
 class APIError(Exception):
-    def __init__(self, message, status=500):
+    def __init__(self, message, status=500, errors=[]):
         super().__init__(message)
         self.status = status
+        self.errors = errors
 
 
 class ValidationError(APIError):
     def __init__(self, message, errors=[]):
-        super().__init__(message, 422)
-        self.errors = errors
+        super().__init__(message, 422, errors)
 
 
 class UnauthorizedError(APIError):
diff --git a/build/lib/netbox_proxmox_sync/api/utils/models.py b/build/lib/netbox_proxmox_sync/api/utils/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd974a3aaf0716c056911b5899562b666d33e9cd
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/utils/models.py
@@ -0,0 +1,167 @@
+from netbox_proxmox_sync.api.config import (
+    NETBOX_DEFAULT_TAG_COLOR,
+    NETBOX_VM_ROLE_ID,
+)
+# Classes to ensure NetBox and Proxmox data have the exact same format and fields,
+# which makes updating stuff correctly easier
+
+
+class VirtualMachine:
+    def __init__(self, vmid, name, status, node_name, cluster_name, vcpus, maxmem,
+                 role_id, tags=[], maxdisk=None, description=None, id=None):
+        self.vmid = vmid
+        self.name = name
+        self.status = status
+        self.node_name = node_name
+        self.cluster_name = cluster_name
+        self.role_id = role_id
+        self.vcpus = vcpus
+        self.maxmem = maxmem
+        self.tags = sorted(tags)
+        self.maxdisk = maxdisk
+        self.description = description
+        self.id = id
+
+    def to_dict(self):
+        # Trust me, it's important that they're sorted
+        vm_data = {
+            'name': self.name,
+            'status': self.status,
+            'device': {'name': self.node_name},
+            'cluster': {'name': self.cluster_name},
+            'vcpus': self.vcpus,
+            'memory': self.maxmem,
+            'role': self.role_id,
+            'tags': [{'name': tag} for tag in self.tags],
+            'custom_fields': {'vmid': self.vmid}
+        }
+        # Both these fields may be None, so we have to set them separately
+        if self.maxdisk is not None and self.maxdisk > 0:
+            vm_data['disk'] = int(self.maxdisk)
+        if self.description is not None:
+            # NetBox only allows 200 char description, but our VMs have more
+            # so we store the description in the "comments" instead
+            vm_data['comments'] = self.description
+        if self.id is not None:
+            vm_data['id'] = self.id
+        return vm_data
+
+
+class VirtualInterface:
+    def __init__(self, name, vm_name, mac_address, vlan_id, id=None):
+        self.name = name
+        self.vm_name = vm_name
+        self.mac_address = mac_address
+        self.vlan_id = vlan_id
+        self.id = id
+
+    def to_dict(self):
+        interface = {
+            'name': self.name,
+            'virtual_machine': {'name': self.vm_name},
+            'mac_address': self.mac_address,
+            'mode': 'access',
+            'untagged_vlan': {'vid': self.vlan_id},
+            # 'bridge': bridge
+        }
+        if self.id is not None:
+            interface['id'] = self.id
+        return interface
+
+
+class VirtualMachineTag:
+    def __init__(self, name, id=None, color=NETBOX_DEFAULT_TAG_COLOR):
+        self.name = name
+        self.slug = name.lower().replace(' ', '-').replace('.', '_')
+        self.color = color
+        self.id = id
+
+    def to_dict(self):
+        vm_tag = {
+            'name': self.name,
+            'slug': self.slug,
+            'color': self.color,
+            'object_types': ['virtualization.virtualmachine'],
+        }
+        if self.id is not None:
+            vm_tag['id'] = self.id
+        return vm_tag
+
+
+def tag_from_netbox(netbox_tag):
+    return VirtualMachineTag(
+        id=netbox_tag['id'],
+        name=netbox_tag['name'],
+        # slug=netbox_tag.slug,
+        color=netbox_tag['color'],
+    ).to_dict()
+
+
+def vm_from_netbox(netbox_vm):
+    role_id = None
+    if netbox_vm.get('role'):
+        role_id = netbox_vm['role']['id']
+    return VirtualMachine(
+        id=netbox_vm['id'],
+        vmid=netbox_vm['custom_fields']['vmid'],
+        name=netbox_vm['name'],
+        status=netbox_vm['status']['value'],
+        # FIXME: some of these are tricky
+        node_name=netbox_vm['device']['name'],
+        cluster_name=netbox_vm['cluster']['name'],
+        role_id=role_id,
+        vcpus=netbox_vm['vcpus'],
+        maxmem=netbox_vm['memory'],
+        tags=[tag['name'] for tag in netbox_vm['tags']],
+        maxdisk=netbox_vm.get('disk'),
+        description=netbox_vm.get('comments'),
+    ).to_dict()
+
+
+def interface_from_netbox(netbox_interface):
+    return VirtualInterface(
+        id=netbox_interface['id'],
+        name=netbox_interface['name'],
+        vm_name=netbox_interface['virtual_machine']['name'],
+        mac_address=netbox_interface['mac_address'].upper(),
+        vlan_id=netbox_interface['untagged_vlan']['vid'],
+    ).to_dict()
+
+
+def tag_from_proxmox(tag_name, color=NETBOX_DEFAULT_TAG_COLOR):
+    return VirtualMachineTag(
+        tag_name,
+        color,
+    ).to_dict()
+
+
+def vm_from_proxmox(cluster_name, proxmox_node_name, proxmox_vm, tags=[]):
+    maxdisk = proxmox_vm.get('maxdisk')
+    if maxdisk is not None:
+        maxdisk = int(maxdisk) / 2 ** 30  # B -> GB
+    memory = int(proxmox_vm['maxmem']) / 2**20  # B -> MB
+    return VirtualMachine(
+        vmid=proxmox_vm['vmid'],
+        name=proxmox_vm['name'],
+        status='active' if proxmox_vm['status'] == 'running' else 'offline',
+        node_name=proxmox_node_name,
+        cluster_name=cluster_name,
+        vcpus=proxmox_vm['cpus'],
+        role_id=NETBOX_VM_ROLE_ID,
+        maxmem=memory,
+        tags=tags,
+        maxdisk=maxdisk,
+        description=proxmox_vm.get('description'),
+    ).to_dict()
+
+
+def interface_from_proxmox(proxmox_vm_name, interface_name, proxmox_interface):
+    # net[0-9]+: 'virtio=00:00:00:00:00:00,bridge=vmbr<VID>'
+    mac = proxmox_interface.split('virtio=')[1].split(',')[0]
+    vlan_id = int(proxmox_interface.split('bridge=vmbr')[1].split(',firewall')[0])
+    return VirtualInterface(
+        name=f'{proxmox_vm_name}:{interface_name}',
+        vm_name=proxmox_vm_name,
+        mac_address=mac.upper(),
+        vlan_id=vlan_id,
+    ).to_dict()
diff --git a/build/lib/netbox_proxmox_sync/api/views.py b/build/lib/netbox_proxmox_sync/api/views.py
new file mode 100644
index 0000000000000000000000000000000000000000..67f07669dd8969fbe3ec7e8bdf8931c632eeb9ef
--- /dev/null
+++ b/build/lib/netbox_proxmox_sync/api/views.py
@@ -0,0 +1,57 @@
+from django.http import HttpResponse
+from django.views import View
+from django.contrib.auth.mixins import PermissionRequiredMixin
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.netbox import create, update, delete
+import json
+import traceback
+# TODO: fix HTTP status codes
+# TODO: proper error handling
+
+
+class CreateCluster(PermissionRequiredMixin, View):
+    permission_required = "netbox_proxbox.sync_proxmox_cluster"
+
+    def get(self, _):
+        try:
+            result = create.all()
+            json_result = json.dumps(result)
+            return HttpResponse(json_result, content_type='application/json')
+        except APIError as e:
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
+            return HttpResponse(
+                json_result, status=e.status, content_type='application/json'
+            )
+
+
+class UpdateCluster(PermissionRequiredMixin, View):
+    permission_required = "netbox_proxbox.sync_proxmox_cluster"
+
+    def get(self, _):
+        try:
+            result = update.all()
+            json_result = json.dumps(result)
+            return HttpResponse(json_result, status=201, content_type='application/json')
+        except APIError as e:
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
+            return HttpResponse(
+                json_result, status=e.status, content_type='application/json'
+            )
+
+
+class DeleteCluster(PermissionRequiredMixin, View):
+    permission_required = "netbox_proxbox.reset_promox_cluster"
+
+    def get(self, _):
+        try:
+            result = delete.all()
+            json_result = json.dumps(result)
+            return HttpResponse(json_result, status=201, content_type='application/json')
+        except APIError as e:
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
+            return HttpResponse(
+                json_result, status=e.status, content_type='application/json'
+            )
diff --git a/netbox_proxmox_sync.egg-info/PKG-INFO b/netbox_proxmox_sync.egg-info/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..2169862fc9d7a335baf32ee5de0bf6fb1b3f851c
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/PKG-INFO
@@ -0,0 +1,4 @@
+Metadata-Version: 2.1
+Name: netbox-proxmox-sync
+Version: 2.0.0
+Summary: Import Proxmox cluster info into NetBox.
diff --git a/netbox_proxmox_sync.egg-info/SOURCES.txt b/netbox_proxmox_sync.egg-info/SOURCES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..d91c4afd16667b62eaaa32a10638c4d2bf362f46
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/SOURCES.txt
@@ -0,0 +1,20 @@
+README.md
+setup.py
+netbox_proxmox_sync/__init__.py
+netbox_proxmox_sync.egg-info/PKG-INFO
+netbox_proxmox_sync.egg-info/SOURCES.txt
+netbox_proxmox_sync.egg-info/dependency_links.txt
+netbox_proxmox_sync.egg-info/not-zip-safe
+netbox_proxmox_sync.egg-info/requires.txt
+netbox_proxmox_sync.egg-info/top_level.txt
+netbox_proxmox_sync/api/__init__.py
+netbox_proxmox_sync/api/config.py
+netbox_proxmox_sync/api/urls.py
+netbox_proxmox_sync/api/views.py
+netbox_proxmox_sync/api/netbox/create.py
+netbox_proxmox_sync/api/netbox/delete.py
+netbox_proxmox_sync/api/netbox/extract.py
+netbox_proxmox_sync/api/netbox/update.py
+netbox_proxmox_sync/api/proxmox/extract.py
+netbox_proxmox_sync/api/utils/errors.py
+netbox_proxmox_sync/api/utils/models.py
\ No newline at end of file
diff --git a/netbox_proxmox_sync.egg-info/dependency_links.txt b/netbox_proxmox_sync.egg-info/dependency_links.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/netbox_proxmox_sync.egg-info/not-zip-safe b/netbox_proxmox_sync.egg-info/not-zip-safe
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/not-zip-safe
@@ -0,0 +1 @@
+
diff --git a/netbox_proxmox_sync.egg-info/requires.txt b/netbox_proxmox_sync.egg-info/requires.txt
new file mode 100644
index 0000000000000000000000000000000000000000..3240c1cb61a0381a70c378909e3c5be729663ab4
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/requires.txt
@@ -0,0 +1,2 @@
+proxmoxer
+pynetbox
diff --git a/netbox_proxmox_sync.egg-info/top_level.txt b/netbox_proxmox_sync.egg-info/top_level.txt
new file mode 100644
index 0000000000000000000000000000000000000000..0bd3fead4416e20df81238981c3bbeae84dcb467
--- /dev/null
+++ b/netbox_proxmox_sync.egg-info/top_level.txt
@@ -0,0 +1 @@
+netbox_proxmox_sync
diff --git a/netbox_proxmox_sync/__init__.py b/netbox_proxmox_sync/__init__.py
index ece8f09482f91b00bc96784f82e7deb4733e6bd5..29948087aef9ab10f486f6e0f5b76391a0e1e1b0 100644
--- a/netbox_proxmox_sync/__init__.py
+++ b/netbox_proxmox_sync/__init__.py
@@ -5,7 +5,7 @@ class NetBoxProxmoxSyncConfig(PluginConfig):
     name = 'netbox_proxmox_sync'
     verbose_name = 'NetBox Proxmox Sync'
     description = 'Import cluster information from Proxmox into NetBox'
-    version = '1.0.2'
+    version = '2.0.0'
     base_url = 'netbox-proxmox-sync'
 
 
diff --git a/netbox_proxmox_sync/api/config.py b/netbox_proxmox_sync/api/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..66474ce850bcae34dbda607c73f77601e4513e22
--- /dev/null
+++ b/netbox_proxmox_sync/api/config.py
@@ -0,0 +1,88 @@
+from proxmoxer import ProxmoxAPI
+from pynetbox import api as NetboxAPI
+from netbox.settings import PLUGINS_CONFIG
+
+
+# Set default values
+NETBOX_DEFAULT_CONFIG = {
+    'domain': 'localhost',
+    'port': 8001,
+    'token': 'this should be set',
+    'ssl': True,
+    'settings': {
+        'vm_role_id': 0,
+        'site_id': 0,
+        'cluster_type_id': 0,
+        'cluster_description': 'A Proxmox Cluster.',
+        'default_tag_color_hex': 'ffffff',
+    },
+}
+PROXMOX_DEFAULT_CONFIG = {
+    'domain': 'this should be set',
+    'port': 8006,
+    'user': 'this should be set',
+    'token': {
+        'name': 'this should be set',
+        'value': 'this should be set',
+    },
+    'ssl': True,
+}
+
+USER_PLUGINS_CONFIG = PLUGINS_CONFIG.get('netbox_proxmox_sync', {})
+PROXMOX_CONFIG = USER_PLUGINS_CONFIG.get('proxmox', {})
+NETBOX_CONFIG = USER_PLUGINS_CONFIG.get('netbox', {})
+
+# TODO: throw errors for missing required fields (like token/user/etc)
+#       -> honestly... every field is required here (except maybe ssl)
+# PROXMOX
+# - Main
+PROXMOX_DOMAIN = PROXMOX_CONFIG.get('domain', PROXMOX_DEFAULT_CONFIG['domain'])
+PROXMOX_PORT = PROXMOX_CONFIG.get('port', PROXMOX_DEFAULT_CONFIG['port'])
+PROXMOX_USER = PROXMOX_CONFIG.get('user', PROXMOX_DEFAULT_CONFIG['user'])
+PROXMOX_SSL = PROXMOX_CONFIG.get('ssl', PROXMOX_DEFAULT_CONFIG['ssl'])
+# - Token
+PROXMOX_TOKEN = PROXMOX_CONFIG.get('token', PROXMOX_DEFAULT_CONFIG['token'])
+PROXMOX_TOKEN_NAME = PROXMOX_TOKEN.get('name', PROXMOX_DEFAULT_CONFIG['token']['name'])
+PROXMOX_TOKEN_VALUE = PROXMOX_TOKEN.get('value', PROXMOX_DEFAULT_CONFIG['token']['value'])
+
+# NETBOX
+# - Main
+NETBOX_DOMAIN = NETBOX_CONFIG.get('domain', NETBOX_DEFAULT_CONFIG['domain'])
+NETBOX_PORT = NETBOX_CONFIG.get('port', NETBOX_DEFAULT_CONFIG['port'])
+NETBOX_TOKEN = NETBOX_CONFIG.get('token', NETBOX_DEFAULT_CONFIG['token'])
+NETBOX_SSL = NETBOX_CONFIG.get('ssl', NETBOX_DEFAULT_CONFIG['ssl'])
+# - Settings
+NETBOX_SETTINGS = NETBOX_CONFIG.get('settings', NETBOX_DEFAULT_CONFIG['settings'])
+NETBOX_CLUSTER_TYPE_ID = NETBOX_SETTINGS.get('cluster_type_id', NETBOX_DEFAULT_CONFIG['settings']['cluster_type_id'])
+NETBOX_CLUSTER_DESCRIPTION = NETBOX_SETTINGS.get('cluster_description', NETBOX_DEFAULT_CONFIG['settings']['cluster_description'])
+NETBOX_VM_ROLE_ID = NETBOX_SETTINGS.get('vm_role_id', NETBOX_DEFAULT_CONFIG['settings']['vm_role_id'])
+NETBOX_SITE_ID = NETBOX_SETTINGS.get('site_id', NETBOX_DEFAULT_CONFIG['settings']['site_id'])
+NETBOX_DEFAULT_TAG_COLOR = NETBOX_SETTINGS.get('default_tag_color_hex', NETBOX_DEFAULT_CONFIG['settings']['default_tag_color_hex'])
+
+# Create connections
+# TODO: more descriptive errors
+try:
+    PX_API = ProxmoxAPI(
+        PROXMOX_DOMAIN,
+        user=PROXMOX_USER,
+        port=PROXMOX_PORT,
+        token_name=PROXMOX_TOKEN_NAME,
+        token_value=PROXMOX_TOKEN_VALUE,
+        verify_ssl=PROXMOX_SSL,
+    )
+except Exception:
+    raise RuntimeError('Could not connect to Proxmox Cluster! Verify your credentials!')
+
+try:
+    # TODO: allow to change default base path?
+    if NETBOX_SSL:
+        url = f'https://{NETBOX_DOMAIN}:{NETBOX_PORT}'
+    else:
+        url = f'http://{NETBOX_DOMAIN}:{NETBOX_PORT}'
+    NB_API = NetboxAPI(
+        url,
+        token=NETBOX_TOKEN,
+        threading=True,
+    )
+except Exception:
+    raise RuntimeError('Could not connect to NetBox! Verify your credentials!')
diff --git a/netbox_proxmox_sync/api/netbox/create.py b/netbox_proxmox_sync/api/netbox/create.py
new file mode 100644
index 0000000000000000000000000000000000000000..5c0fc06b501a2d56666fd4ead327d19f019e2d88
--- /dev/null
+++ b/netbox_proxmox_sync/api/netbox/create.py
@@ -0,0 +1,135 @@
+# TODO: more descriptive errors
+# FIXME: fetch_* is a shit name
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+from .delete import all as delete_everything
+
+
+def _assert_nodes_exist(proxmox_nodes):
+    node_names = set([node['name'] for node in proxmox_nodes])
+    existing_names = set([
+        node.name for node in NB_API.dcim.devices.filter(name=node_names)
+    ])
+    missing_nodes = list(node_names - existing_names)
+    if len(missing_nodes) > 0:
+        missing = [f'Node "{node_name}" is missing!' for node_name in missing_nodes]
+        raise ValidationError('Not all cluster nodes registered in NetBox!', missing)
+
+
+def _assert_cluster_does_not_exist(proxmox_cluster):
+    cluster_name = proxmox_cluster['name']
+    netbox_cluster_list = NB_API.virtualization.clusters.filter(name=cluster_name)
+    if len(netbox_cluster_list) != 0:
+        raise ValidationError(f'Virtualization cluster "{cluster_name}" already exists!')
+
+
+def cluster(proxmox_cluster, proxmox_nodes):
+    try:
+        # Already formatted the data for this :)
+        netbox_cluster = NB_API.virtualization.clusters.create(**proxmox_cluster)
+    except Exception as e:
+        raise APIError(e)
+    # Update nodes' cluster
+    node_names = [node['name'] for node in proxmox_nodes]
+    netbox_nodes = list(NB_API.dcim.devices.filter(name=[node_names]))
+    for node in netbox_nodes:
+        node.cluster = {'name': proxmox_cluster['name']}
+    if not NB_API.dcim.devices.update(netbox_nodes):
+        # In case of error "rollback"
+        netbox_cluster.delete()
+        raise APIError('Failed to set Nodes\' cluster!')
+    # Return JSON serializable dict :)
+    return dict(netbox_cluster)
+
+
+def tags():
+    proxmox_tags = Proxmox.fetch_tags()
+    try:
+        existing_tags = NB_API.extras.tags.filter(content_types=[
+            'virtualization.virtualmachines',
+        ])
+    except Exception as e:
+        raise APIError(e)
+    # Tags for VMs currently should only be managed by this plugin
+    # TODO: we could add a custom field to mark tags managed by the plugin, for instance
+    if len(existing_tags) > 0:
+        names = [tag.name for tag in existing_tags]
+        errors = [f'Tag "{name}" should not exist!' for name in names]
+        raise ValidationError('Some VM tags already exist!', errors)
+    try:
+        netbox_tags = NB_API.extras.tags.create(proxmox_tags)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(tag) for tag in netbox_tags]
+
+
+def custom_fields():
+    try:
+        vmid_already_exists = len(NB_API.extras.custom_fields.filter(
+            name='vmid', object_types='virtualization.virtualmachine'
+        )) > 0
+    except Exception as e:
+        raise APIError(e)
+    if vmid_already_exists:
+        raise ValidationError('Custom field "vmid" already exists!')
+    custom_fields = [{
+        'name': 'vmid',
+        'label': 'VMID',
+        'description': '[Proxmox] VM/CT ID',
+        'ui_editable': 'no',
+        'ui_visible': 'always',
+        'filter_logic': 'exact',
+        'type': 'integer',
+        'object_types': ['virtualization.virtualmachine']
+    }]
+    # Create stuff :)
+    try:
+        netbox_custom_fields = NB_API.extras.custom_fields.create(custom_fields)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(custom_field) for custom_field in netbox_custom_fields]
+
+
+def vms_and_interfaces():
+    proxmox_vms, proxmox_interfaces = Proxmox.fetch_virtual_machines_and_interfaces()
+    try:
+        netbox_vms = NB_API.virtualization.virtual_machines.create(proxmox_vms)
+        netbox_interfaces = NB_API.virtualization.interfaces.create(proxmox_interfaces)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable lists :)
+    return [dict(x) for x in netbox_vms], [dict(x) for x in netbox_interfaces]
+
+
+def all():
+    proxmox_cluster, proxmox_nodes = Proxmox.fetch_cluster_and_nodes()
+    _assert_nodes_exist(proxmox_nodes)
+    _assert_cluster_does_not_exist(proxmox_cluster)
+    # Leave cluster out first so the first error comes quickly, useful for API
+    #  errors like invalid token for instance, which might be common.
+    # (netbox takes tooooo long to delete <stuff>, even if <stuff> does not exist)
+    netbox_cluster = cluster(proxmox_cluster, proxmox_nodes)
+    # In case the cluster() call succeeds we are sure that we have write access
+    #  for the API, so any error that comes out of the following calls must be
+    #  something else (or you messed up things).
+    # This is useful because we can be pretty sure that a "rollback" is possible
+    # (the delete_everything() call below *should* NOT raise an exception).
+    try:
+        netbox_tags = tags()
+        netbox_custom_fields = custom_fields()
+        netbox_vms, netbox_interfaces = vms_and_interfaces()
+    except Exception as e:
+        # "rollback" in case of any failure
+        # (if this function fails you probably messed up in a very interesting way)
+        delete_everything()
+        raise e
+    return {
+        'cluster': netbox_cluster,
+        'tags': netbox_tags,
+        'custom_fields': netbox_custom_fields,
+        'vms': netbox_vms,
+        'interfaces': netbox_interfaces,
+    }
diff --git a/netbox_proxmox_sync/api/netbox/delete.py b/netbox_proxmox_sync/api/netbox/delete.py
new file mode 100644
index 0000000000000000000000000000000000000000..c01a199a2401b322f5af8c0ce9173aa9d441b1fe
--- /dev/null
+++ b/netbox_proxmox_sync/api/netbox/delete.py
@@ -0,0 +1,91 @@
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+
+
+def cluster(proxmox_cluster):
+    cluster_name = proxmox_cluster['name']
+    try:
+        netbox_clusters = NB_API.virtualization.clusters.filter(name=cluster_name)
+    except Exception as e:
+        raise APIError(e)
+    if (len(netbox_clusters)) == 0:
+        raise ValidationError(f'Cluster "{cluster_name}" does not exist!')
+    netbox_cluster = list(netbox_clusters)[0]
+    # Return JSON serializable dict :)
+    return dict(netbox_cluster)
+
+
+def tags():
+    try:
+        netbox_tags = NB_API.extras.tags.filter(content_types=[
+            'virtualization.virtualmachines',
+        ])
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(tag) for tag in netbox_tags]
+
+
+def custom_fields():
+    try:
+        netbox_custom_fields = NB_API.extras.custom_fields.filter(
+            name='vmid',
+            object_types='virtualization.virtualmachine',
+        )
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable list :)
+    return [dict(custom_field) for custom_field in netbox_custom_fields]
+
+
+def vms_and_interfaces(netbox_cluster):
+    try:
+        cluster_id = netbox_cluster['id']
+        netbox_vms = NB_API.virtualization.virtual_machines.filter(cluster_id=cluster_id)
+        netbox_interfaces = NB_API.virtualization.interfaces.filter(cluster_id=cluster_id)
+    except Exception as e:
+        raise APIError(e)
+    # Return JSON serializable lists :)
+    return [dict(x) for x in netbox_vms], [dict(x) for x in netbox_interfaces]
+
+
+def delete_stuff(cluster, tags, custom_fields, vms, interfaces):
+    # This order is important
+    rt = NB_API.extras.tags.delete([tag['id'] for tag in tags])
+    rc = NB_API.extras.custom_fields.delete([field['id'] for field in custom_fields])
+    ri = NB_API.virtualization.interfaces.delete([i['id'] for i in interfaces])
+    rv = NB_API.virtualization.virtual_machines.delete([vm['id'] for vm in vms])
+    rf = NB_API.virtualization.clusters.delete([cluster['id']])
+    errors = []
+    if rt is None:
+        errors.append('Could not delete tags!')
+    if rf is None:
+        errors.append('Could not delete custom_fields!')
+    if rc is None:
+        errors.append('Could not delete cluster!')
+    if rv is None:
+        errors.append('Could not delete virtual machines!')
+    if ri is None:
+        errors.append('Could not delete interfaces!')
+    if len(errors) > 0:
+        raise APIError('Could not delete plugin information!', errors=errors)
+
+
+def all():
+    proxmox_cluster, _ = Proxmox.fetch_cluster_and_nodes()
+    netbox_cluster = cluster(proxmox_cluster)
+    netbox_tags = tags()
+    netbox_custom_fields = custom_fields()
+    netbox_vms, netbox_interfaces = vms_and_interfaces(netbox_cluster)
+    delete_stuff(
+        netbox_cluster, netbox_tags, netbox_custom_fields,
+        netbox_vms, netbox_interfaces
+    )
+    return {
+        'cluster': netbox_cluster,
+        'tags': netbox_tags,
+        'custom_fields': netbox_custom_fields,
+        'vms': netbox_vms,
+        'interfaces': netbox_interfaces,
+    }
diff --git a/netbox_proxmox_sync/api/netbox/extract.py b/netbox_proxmox_sync/api/netbox/extract.py
new file mode 100644
index 0000000000000000000000000000000000000000..077b70391c5f8052fbcb8a1b3d329ba2da722ec2
--- /dev/null
+++ b/netbox_proxmox_sync/api/netbox/extract.py
@@ -0,0 +1,55 @@
+from netbox_proxmox_sync.api.config import (
+    NB_API,
+    NETBOX_SITE_ID,
+    NETBOX_CLUSTER_TYPE_ID,
+)
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.utils.models import (
+    tag_from_netbox,
+    vm_from_netbox,
+    interface_from_netbox,
+)
+
+
+class NetBox:
+    @staticmethod
+    def fetch_cluster(cluster_name):
+        try:
+            netbox_cluster = NB_API.virtualization.clusters.filter(name=cluster_name)
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        return {
+            'id': netbox_cluster.id,
+            'name': netbox_cluster.name,
+            'type': NETBOX_CLUSTER_TYPE_ID,
+            'description': netbox_cluster.description,
+            'site': NETBOX_SITE_ID,
+        }
+
+    @staticmethod
+    def fetch_tags():
+        try:
+            netbox_tags = [dict(tag) for tag in NB_API.extras.tags.filter(
+                content_types='virtualization.virtualmachines',
+            )]
+        except Exception as e:
+            raise APIError(e)
+
+        return [tag_from_netbox(tag) for tag in netbox_tags]
+
+    @staticmethod
+    def fetch_virtual_machines_and_interfaces(cluster_id):
+        try:
+            netbox_vms = [
+                dict(vm) for vm in NB_API.virtualization.virtual_machines.filter(
+                    cluster_id=cluster_id
+                )
+            ]
+            netbox_interfaces = [dict(i) for i in NB_API.virtualization.interfaces.all()]
+        except Exception as e:
+            raise APIError(e)
+
+        vms = [vm_from_netbox(vm) for vm in netbox_vms]
+        interfaces = [interface_from_netbox(i) for i in netbox_interfaces]
+
+        return vms, interfaces
diff --git a/netbox_proxmox_sync/api/netbox/update.py b/netbox_proxmox_sync/api/netbox/update.py
new file mode 100644
index 0000000000000000000000000000000000000000..368105da90e4222fd8f100273682e689f1e64639
--- /dev/null
+++ b/netbox_proxmox_sync/api/netbox/update.py
@@ -0,0 +1,123 @@
+from netbox_proxmox_sync.api.config import NB_API
+from netbox_proxmox_sync.api.utils.errors import APIError, ValidationError
+from netbox_proxmox_sync.api.proxmox.extract import Proxmox
+from netbox_proxmox_sync.api.netbox.extract import NetBox
+
+
+# MERGE, get created_*, updated_*, deleted_*
+def tags():
+    proxmox_tags = Proxmox.fetch_tags()
+    netbox_tags = NetBox.fetch_tags()
+    px_tags = {tag['name']: tag for tag in proxmox_tags}
+    nb_tags = {tag['name']: tag for tag in netbox_tags}
+    # "merge" information, extract stuff to C/U/D
+    created = [tag for tag in proxmox_tags if tag['name'] not in nb_tags]
+    deleted = [tag for tag in netbox_tags if tag['name'] not in px_tags]
+    updated = []
+    for name in px_tags:
+        # only thing that can really change for tags is their color
+        if name in nb_tags and px_tags[name]['color'] != nb_tags[name]['color']:
+            nb_tags[name]['color'] = px_tags[name]['color']
+            updated.append(nb_tags[name])
+    # FIXME: error handling
+    NB_API.extras.tags.create(created)
+    NB_API.extras.tags.update(updated)
+    NB_API.extras.tags.delete([d['id'] for d in deleted])
+    return created, updated, deleted
+
+
+# TODO: allow for updates of custom fields in case the plugin is updated to use more
+# def custom_fields():
+
+
+def vms(proxmox_vms, netbox_vms):
+    px_vms = {vm['custom_fields']['vmid']: vm for vm in proxmox_vms}
+    nb_vms = {vm['custom_fields']['vmid']: vm for vm in netbox_vms}
+    # "merge" information, extract stuff to C/U/D
+    created = [vm for vm in proxmox_vms if vm['custom_fields']['vmid'] not in nb_vms]
+    deleted = [vm for vm in netbox_vms if vm['custom_fields']['vmid'] not in px_vms]
+    updated = []
+    for name in px_vms:
+        changed = False
+        px_vm = px_vms[name]
+        if nb_vms.get(name) is None:
+            continue
+        nb_vm = nb_vms[name]
+        # Update all fields
+        for field in px_vm:
+            changed |= nb_vm[field] != px_vm[field]
+            nb_vm[field] = px_vm[field]
+        if changed:
+            updated.append(nb_vm)
+    return created, updated, deleted
+
+
+def interfaces(proxmox_interfaces, netbox_interfaces):
+    # Note: if the MAC changes the IP for the VM is lost :)
+    px_interfaces = {i['mac_address']: i for i in proxmox_interfaces}
+    nb_interfaces = {i['mac_address']: i for i in netbox_interfaces}
+    created = [i for i in proxmox_interfaces if i['mac_address'] not in nb_interfaces]
+    deleted = [i for i in netbox_interfaces if i['mac_address'] not in px_interfaces]
+    updated = []
+    for name in px_interfaces:
+        changed = False
+        # Update all fields
+        px_interface = px_interfaces[name]
+        if nb_interfaces.get(name) is None:
+            continue
+        nb_interface = nb_interfaces[name]
+        for field in px_interface:
+            changed |= nb_interface[field] != px_interface[field]
+            nb_interface[field] = px_interface[field]
+        if changed:
+            updated.append(nb_interface)
+    return created, updated, deleted
+
+
+def vms_and_interfaces(cluster_id):
+    proxmox_vms, proxmox_interfaces = Proxmox.fetch_virtual_machines_and_interfaces()
+    netbox_vms, netbox_interfaces = NetBox.fetch_virtual_machines_and_interfaces(cluster_id)
+
+    created_vms, updated_vms, deleted_vms = vms(proxmox_vms, netbox_vms)
+    created_i, updated_i, deleted_i = interfaces(proxmox_interfaces, netbox_interfaces)
+
+    # FIXME: error handling
+    NB_API.virtualization.virtual_machines.create(created_vms)
+    NB_API.virtualization.virtual_machines.update(updated_vms)
+    NB_API.virtualization.virtual_machines.delete([d['id'] for d in deleted_vms])
+    NB_API.virtualization.interfaces.create(created_i)
+    NB_API.virtualization.interfaces.update(updated_i)
+    # NB_API.virtualization.interfaces.delete([d['id'] for d in deleted])
+
+    return created_vms, updated_vms, deleted_vms, \
+        created_i, updated_i, deleted_i
+
+
+def all():
+    proxmox_cluster = Proxmox.fetch_cluster()
+    cluster_name = proxmox_cluster['name']
+    clusters = list(NB_API.virtualization.clusters.filter(name=cluster_name))
+    if (len(clusters)) == 0:
+        raise ValidationError(f'Cluster "{cluster_name}" does not exist!')
+    netbox_cluster = clusters[0]
+    created_tags, updated_tags, deleted_tags = tags()
+    created_vms, updated_vms, deleted_vms, \
+        created_interfaces, updated_interfaces, deleted_interfaces = \
+        vms_and_interfaces(cluster_id=netbox_cluster.id)
+    return {
+        'tags': {
+            'created': created_tags,
+            'updated': updated_tags,
+            'deleted': deleted_tags,
+        },
+        'virtual_machines': {
+            'created': created_vms,
+            'updated': updated_vms,
+            'deleted': deleted_vms,
+        },
+        'interfaces': {
+            'created': created_interfaces,
+            'updated': updated_interfaces,
+            'deleted': deleted_interfaces,
+        }
+    }
diff --git a/netbox_proxmox_sync/api/proxmox/create.py b/netbox_proxmox_sync/api/proxmox/create.py
deleted file mode 100644
index d14b3b4bc93eb3786dde4ed4134fc847f39376a5..0000000000000000000000000000000000000000
--- a/netbox_proxmox_sync/api/proxmox/create.py
+++ /dev/null
@@ -1,109 +0,0 @@
-from netbox_proxmox_sync.api.errors import APIError, ValidationError
-from .extract import fetch_cluster_and_nodes, fetch_tags, fetch_virtual_machines_and_interfaces
-from .config import NETBOX_API
-
-
-def assert_cluster_does_not_exist(cluster_name):
-    netbox_cluster_list = NETBOX_API.virtualization.clusters.filter(name=cluster_name)
-    if len(netbox_cluster_list) != 0:
-        raise ValidationError(f'Virtualization cluster "{cluster_name}" already exists!')
-
-
-def assert_nodes_exist(proxmox_nodes):
-    node_names = set([node['name'] for node in proxmox_nodes])
-    existing_names = set([
-        node.name for node in NETBOX_API.dcim.devices.filter(name=node_names)
-    ])
-    missing_nodes = list(node_names - existing_names)
-    if len(missing_nodes) > 0:
-        missing = [f'Node "{node_name}" is missing!' for node_name in missing_nodes]
-        raise ValidationError('Not all cluster nodes registered in NetBox!', missing)
-
-
-def create_cluster(proxmox_cluster, proxmox_nodes):
-    try:
-        # Already formatted the data for this :)
-        netbox_cluster = NETBOX_API.virtualization.clusters.create(**proxmox_cluster)
-        node_names = [node['name'] for node in proxmox_nodes]
-        # Update nodes' cluster
-        netbox_nodes = list(NETBOX_API.dcim.devices.filter(name=[node_names]))
-        for node in netbox_nodes:
-            node.cluster = {'name': proxmox_cluster['name']}
-        # In case of error "rollback"
-        if not NETBOX_API.dcim.devices.update(netbox_nodes):
-            netbox_cluster.delete()
-            raise APIError('Failed to set Nodes\' cluster!')
-    except Exception as e:
-        raise APIError(e)
-    # Return JSON serializable dict :)
-    return dict(netbox_cluster)
-
-
-def create_tags_and_custom_fields(proxmox_tags):
-    try:
-        existing_tags = NETBOX_API.extras.tags.filter(content_types=[
-            'virtualization.virtualmachines',
-        ])
-    except Exception as e:
-        raise APIError(e)
-    # If tags for VMs already exist complain
-    if len(existing_tags) > 0:
-        names = [tag.name for tag in existing_tags]
-        errors = [f'Tag "{name}" should not exist!' for name in names]
-        raise ValidationError('Some VM tags already exist!', errors)
-    # If vmid custom field already exist complain
-    custom_field_exists = len(NETBOX_API.extras.custom_fields.filter(name='vmid')) > 0
-    if custom_field_exists:
-        raise ValidationError('Custom field "vmid" already exists!')
-    custom_fields = [{
-        'name': 'vmid',
-        'label': 'VMID',
-        'description': '[Proxmox] VM/CT ID',
-        'ui_editable': 'no',
-        'ui_visible': 'always',
-        'filter_logic': 'exact',
-        'type': 'integer',
-        'object_types': ['virtualization.virtualmachine']
-    }]
-    # Create stuff :)
-    try:
-        netbox_tags = NETBOX_API.extras.tags.create(proxmox_tags)
-        netbox_custom_fields = NETBOX_API.extras.custom_fields.create(custom_fields)
-    except Exception as e:
-        raise APIError(e)
-    # Return JSON serializable lists :)
-    return [dict(x) for x in netbox_tags], [dict(x) for x in netbox_custom_fields]
-
-
-def create_virtual_machines(proxmox_vms, proxmox_interfaces):
-    # Just create stuff :)
-    try:
-        netbox_vms = NETBOX_API.virtualization.virtual_machines.create(proxmox_vms)
-        netbox_interfaces = NETBOX_API.virtualization.interfaces.create(proxmox_interfaces)
-    except Exception as e:
-        raise APIError(e)
-    # Return JSON serializable lists :)
-    return [dict(x) for x in netbox_vms], [dict(x) for x in netbox_interfaces]
-
-
-# FIXME: should try a full rollback in case of any error
-def all():
-    cluster, nodes = fetch_cluster_and_nodes()
-    tags = fetch_tags()
-    vms, interfaces = fetch_virtual_machines_and_interfaces()
-    # Assert some stuff first
-    assert_cluster_does_not_exist(cluster['name'])
-    assert_nodes_exist(nodes)
-    # Create basic info
-    netbox_cluster = create_cluster(cluster, nodes)
-    netbox_tags, netbox_custom_fields = create_tags_and_custom_fields(tags)
-    # Do the first "sync" (creates VMs and their respective interfaces)
-    netbox_vms, netbox_interfaces = create_virtual_machines(vms, interfaces)
-    return {
-        'cluster': netbox_cluster,
-        'tags': netbox_tags,
-        'custom_fields': netbox_custom_fields,
-        # 'nodes': netbox_nodes,
-        'virtual_machines': netbox_vms,
-        'interfaces': netbox_interfaces
-    }
diff --git a/netbox_proxmox_sync/api/proxmox/delete.py b/netbox_proxmox_sync/api/proxmox/delete.py
deleted file mode 100644
index fca5c60bdc0100292f631cf7805008f493bd5734..0000000000000000000000000000000000000000
--- a/netbox_proxmox_sync/api/proxmox/delete.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from netbox_proxmox_sync.api.errors import APIError, ValidationError
-from .extract import fetch_cluster_and_nodes
-from .config import NETBOX_API
-
-
-def all():
-    # Fetch all data (for proper output)
-    cluster, _ = fetch_cluster_and_nodes()
-    netbox_clusters = list(NETBOX_API.virtualization.clusters.filter(
-        name=cluster['name']
-    ))
-    if len(netbox_clusters) == 0:
-        raise ValidationError(f'Cluster "{cluster["name"]}" does not exist!')
-    netbox_cluster = dict(netbox_clusters[0])
-    try:
-        netbox_interfaces = [dict(i) for i in NETBOX_API.virtualization.interfaces.all()]
-        netbox_vms = [dict(i) for i in NETBOX_API.virtualization.virtual_machines.filter(
-            cluster_id=netbox_cluster['id'],
-        )]
-        netbox_tags = [dict(f) for f in NETBOX_API.extras.tags.filter(
-            content_types=['virtualization.virtualmachines'],
-        )]
-        netbox_custom_fields = [dict(f) for f in NETBOX_API.extras.custom_fields.filter(
-            object_types=['virtualization.virtualmachine'],
-        )]
-    except Exception:
-        raise APIError(f'Failed to fetch all "{cluster["name"]}" cluster information!')
-    # Actually delete stuff
-    si = NETBOX_API.virtualization.interfaces.delete([
-        interface['id'] for interface in netbox_interfaces
-    ])
-    sv = NETBOX_API.virtualization.virtual_machines.delete([
-        vm['id'] for vm in netbox_vms
-    ])
-    st = NETBOX_API.extras.tags.delete([
-        tag['id'] for tag in netbox_tags
-    ])
-    sf = NETBOX_API.extras.custom_fields.delete([
-        custom_field['id'] for custom_field in netbox_custom_fields
-    ])
-    sc = NETBOX_API.virtualization.clusters.delete([netbox_cluster['id']])
-    if not (si and sv and st and sf and sc):
-        raise APIError(f'Failed to delete all "{cluster["name"]}" cluster information!')
-
-    return {
-        'cluster': netbox_cluster,
-        'tags': netbox_tags,
-        'custom_fields': netbox_custom_fields,
-        # 'nodes': netbox_nodes,
-        'virtual_machines': netbox_vms,
-        'interfaces': netbox_interfaces
-    }
diff --git a/netbox_proxmox_sync/api/proxmox/extract.py b/netbox_proxmox_sync/api/proxmox/extract.py
index 203bab2b60723b5e0312a0637b64d37ad4110717..34590706509b564cc57125f92c70cf54f4b26c4a 100644
--- a/netbox_proxmox_sync/api/proxmox/extract.py
+++ b/netbox_proxmox_sync/api/proxmox/extract.py
@@ -1,132 +1,119 @@
-from netbox_proxmox_sync.api.errors import APIError
-from ..proxmox.config import (
-    PROXMOX_API,
+from netbox_proxmox_sync.api.config import (
+    PX_API,
     NETBOX_SITE_ID,
     NETBOX_CLUSTER_DESCRIPTION,
     NETBOX_CLUSTER_TYPE_ID,
-    NETBOX_DEFAULT_TAG_COLOR,
+)
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.utils.models import (
+    tag_from_proxmox,
+    vm_from_proxmox,
+    interface_from_proxmox,
 )
 
 
-def fetch_cluster_and_nodes():
-    try:
-        proxmox_cluster_info = PROXMOX_API.cluster.status.get()
-    except Exception:
-        raise APIError('Failed to connect to Proxmox Cluster!')
-    cluster = {
-        'name': proxmox_cluster_info[0]['name'],
-        'type': NETBOX_CLUSTER_TYPE_ID,
-        'description': NETBOX_CLUSTER_DESCRIPTION,
-        'site': NETBOX_SITE_ID,
-    }
-    return cluster, proxmox_cluster_info[1:]
-
-
-def fetch_tags():
-    try:
-        proxmox_tag_info = PROXMOX_API.cluster.options.get()
-    except Exception:
-        raise APIError('Failed to connect to Proxmox Cluster!')
-    allowed_tags = proxmox_tag_info['allowed-tags']
-    tag_colormap = proxmox_tag_info['tag-style']['color-map'].split(';')
-    tags = []
-    # Create tags with no color defined
-    for tag_name in allowed_tags:
-        tag_slug = tag_name.lower().replace(' ', '-').replace('.', '_')
-        tag_color = NETBOX_DEFAULT_TAG_COLOR.lower()
-        tags.append({
-            'name': tag_name,
-            'slug': tag_slug,
-            'color': tag_color,
-            'object_types': ['virtualization.virtualmachine']
-        })
-    # Remap defined tag colors
-    for tag_info in tag_colormap:
-        tag_name = tag_info.split(':')[0]
-        tag_slug = tag_name.lower().replace(' ', '-').replace('.', '_')
-        tag_color = tag_info.split(':')[1].lower()
-        found = False
-        # Find existing tag and update its color
-        for tag in tags:
-            if tag['name'] == tag_name:
-                found = True
-                tag['color'] = tag_color
-                break
-        # ???
-        if not found:
-            tags.append({
-                'name': tag_name,
-                'slug': tag_slug,
-                'color': tag_color,
-                'object_types': ['virtualization.virtualmachine']
-            })
-    return tags
-
-
-def fetch_virtual_machines_and_interfaces():
-    try:
-        cluster_status = PROXMOX_API.cluster.status.get()
-    except Exception:
-        raise APIError('Failed to connect to Proxmox Cluster!')
-    cluster_name = cluster_status[0]['name']
-    proxmox_nodes = [node['name'] for node in cluster_status[1:]]
-    # List, because node information is stored in the VMs themselves
-    virtual_machines = []
-    interfaces = []
-    for node_name in proxmox_nodes:
-        try:
-            node_vms = PROXMOX_API.nodes(node_name).qemu.get()
-        except Exception:
-            raise APIError('Failed to connect to Proxmox Cluster!')
-        for vm_status in node_vms:
-            new_vm, new_interface = extract_vm_data(cluster_name, node_name, vm_status)
-            virtual_machines.append(new_vm)
-            interfaces.extend(new_interface)
-    return virtual_machines, interfaces
-
-
+# Utils for proper data extraction
 def extract_vm_data(cluster_name, node_name, vm_status):
     try:
-        vm_config = PROXMOX_API.nodes(node_name).qemu(vm_status['vmid']).config.get()
+        vm_config = PX_API.nodes(node_name).qemu(vm_status['vmid']).config.get()
     except Exception:
         raise APIError('Failed to connect to Proxmox Cluster!')
-    # Get full VM info for creation
-    # (specially "maxdisk" is unreliable in the original API call)
-    # plus we need the VM's interfaces
-    vm_data = {
-        'name': vm_status['name'],
-        'status': 'active' if vm_status['status'] == 'running' else 'offline',
-        'device': {'name': node_name},
-        'cluster': {'name': cluster_name},
-        'vcpus': vm_status['cpus'],
-        'memory': vm_status['maxmem'] / 2**20,
-        'tags': [{'name': tag} for tag in vm_config['tags'].split(';')],
-        'custom_fields': {'vmid': vm_status['vmid']}
-    }
+    tags = vm_config['tags'].split(';')
+    vm_data = vm_from_proxmox(cluster_name, node_name, vm_status, tags)
     interfaces_data = extract_vm_interfaces(vm_config)
-    # Some VMs either have no disk or their disk isn't explicitly set as the bootdisk
-    if vm_status.get('maxdisk') is not None and vm_status['maxdisk'] > 0:
-        vm_data['disk'] = int(vm_status['maxdisk'] / 2**30)
-    if vm_config.get('description') is not None:
-        # NetBox only allows 200 char description, but our VMs have more
-        # so we store the description in the "comments" instead
-        vm_data['comments'] = vm_config['description']
+
     return vm_data, interfaces_data
 
 
 def extract_vm_interfaces(proxmox_vm_config):
+    vm_name = proxmox_vm_config['name']
     interfaces = []
     for interface_name in [key for key in proxmox_vm_config if key.startswith('net')]:
         interface_info = proxmox_vm_config[interface_name]
-        # net[0-9]+: 'virtio=00:00:00:00:00:00,bridge=vmbr<VID>'
-        mac = interface_info.split('virtio=')[1].split(',')[0]
-        vlan_id = int(interface_info.split('bridge=vmbr')[1].split(',firewall')[0])
-        interfaces.append({
-            'name': interface_name,
-            'virtual_machine': {'name': proxmox_vm_config['name']},
-            'mac_address': mac.upper(),  # NetBox saves uppercase anyways
-            'mode': 'access',
-            'untagged_vlan': {'vid': vlan_id},
-            # 'bridge': bridge
-        })
+        new_interface = interface_from_proxmox(vm_name, interface_name, interface_info)
+        interfaces.append(new_interface)
     return interfaces
+
+
+class Proxmox:
+    @staticmethod
+    def fetch_cluster():
+        cluster, _ = Proxmox.fetch_cluster_and_nodes()
+        return cluster
+
+    @staticmethod
+    def fetch_cluster_and_nodes():
+        try:
+            proxmox_cluster_info = PX_API.cluster.status.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        cluster = {
+            'name': proxmox_cluster_info[0]['name'],
+            'type': NETBOX_CLUSTER_TYPE_ID,
+            'description': NETBOX_CLUSTER_DESCRIPTION,
+            'site': NETBOX_SITE_ID,
+        }
+        return cluster, proxmox_cluster_info[1:]
+
+    @staticmethod
+    def fetch_tags():
+        try:
+            proxmox_tag_info = PX_API.cluster.options.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        allowed_tags = proxmox_tag_info['allowed-tags']
+        tag_colormap = proxmox_tag_info['tag-style']['color-map'].split(';')
+        tags = []
+        # Create tags with no color defined
+        for tag_name in allowed_tags:
+            tags.append(tag_from_proxmox(tag_name))
+        # Remap defined tag colors
+        for tag_info in tag_colormap:
+            tag_name = tag_info.split(':')[0]
+            tag_slug = tag_name.lower().replace(' ', '-').replace('.', '_')
+            tag_color = tag_info.split(':')[1].lower()
+            found = False
+            # Find existing tag and update its color
+            for tag in tags:
+                if tag['name'] == tag_name:
+                    found = True
+                    tag['color'] = tag_color
+                    break
+            # ???
+            if not found:
+                tags.append({
+                    'name': tag_name,
+                    'slug': tag_slug,
+                    'color': tag_color,
+                    'object_types': ['virtualization.virtualmachine']
+                })
+        return tags
+
+    @staticmethod
+    def fetch_virtual_machines_and_interfaces():
+        try:
+            cluster_status = PX_API.cluster.status.get()
+        except Exception:
+            raise APIError('Failed to connect to Proxmox Cluster!')
+        cluster_name = cluster_status[0]['name']
+        proxmox_nodes = [node['name'] for node in cluster_status[1:]]
+        # List, because node information is stored in the VMs themselves
+        # (same for interfaces: vm info is stored in the interface itself)
+        virtual_machines = []
+        interfaces = []
+        for node_name in proxmox_nodes:
+            try:
+                node_vms = PX_API.nodes(node_name).qemu.get()
+            except Exception:
+                raise APIError('Failed to connect to Proxmox Cluster!')
+            for vm_status in node_vms:
+                # If the update runs as the VM is being cloned the vm_config will be
+                # messed up and our update will fail.
+                if vm_status.get('lock') is not None and vm_status['lock'] == 'clone':
+                    continue
+                new_vm, new_interfaces = extract_vm_data(
+                    cluster_name, node_name, vm_status
+                )
+                virtual_machines.append(new_vm)
+                interfaces.extend(new_interfaces)
+        return virtual_machines, interfaces
diff --git a/netbox_proxmox_sync/api/proxmox/update.py b/netbox_proxmox_sync/api/proxmox/update.py
deleted file mode 100644
index 007bafa3046c418768c462fe04d1ded904357529..0000000000000000000000000000000000000000
--- a/netbox_proxmox_sync/api/proxmox/update.py
+++ /dev/null
@@ -1,241 +0,0 @@
-from netbox_proxmox_sync.api.errors import APIError, ValidationError
-from .extract import fetch_cluster_and_nodes, fetch_tags, fetch_virtual_machines_and_interfaces
-from .config import NETBOX_API
-
-
-def assert_cluster_and_nodes_exist(proxmox_cluster, proxmox_nodes):
-    cluster_name = proxmox_cluster['name']
-    netbox_cluster_list = NETBOX_API.virtualization.clusters.filter(name=cluster_name)
-    if len(netbox_cluster_list) != 1:
-        raise ValidationError(
-            f'A single "{cluster_name}" virtualization cluster should exist!'
-        )
-    node_names = set([node['name'] for node in proxmox_nodes])
-    existing_names = set([
-        node.name for node in NETBOX_API.dcim.devices.filter(name=node_names)
-    ])
-    missing_nodes = list(node_names - existing_names)
-    if len(missing_nodes) > 0:
-        missing = [f'Node "{node_name}" is missing!' for node_name in missing_nodes]
-        raise ValidationError('Not all cluster nodes registered in NetBox!', missing)
-    return list(netbox_cluster_list)[0].id
-
-
-def update_tags(proxmox_tags):
-    try:
-        netbox_tags = [dict(tag) for tag in NETBOX_API.extras.tags.filter(
-            content_types=['virtualization.virtualmachines'],
-        )]
-    except Exception:
-        raise APIError('Failed to connect to NetBox Cluster!')
-    new_tags = {tag['name']: tag for tag in proxmox_tags}
-    old_tags = {tag['name']: tag for tag in netbox_tags}
-
-    deleted = [tag for tag in netbox_tags if tag['name'] not in new_tags]
-    to_create = [tag for tag in proxmox_tags if tag['name'] not in old_tags]
-    updated = []
-    for name in new_tags:
-        # if it already exists in netbox but something changed: it's updated
-        # (only thing that can really change is the color)
-        if name in old_tags and new_tags[name]['color'] != old_tags[name]['color']:
-            # Better to update the new dict
-            old_tags[name]['color'] = new_tags[name]['color']
-            updated.append(old_tags[name])
-    try:
-        created = [dict(x) for x in NETBOX_API.extras.tags.create(to_create)]
-    except Exception:
-        raise APIError('Error updating tags!')
-    deleted_ids = [tag['id'] for tag in deleted]
-    result_update = len(updated) == 0 or NETBOX_API.extras.tags.update(updated)
-    result_delete = len(deleted) == 0 or NETBOX_API.extras.tags.delete(deleted_ids)
-    if not result_update or not result_delete:
-        raise APIError('Error updating tags!')
-
-    return created, updated, deleted
-
-
-def update_old_vms(old_vms, new_vms):
-    updated = []
-    for name in new_vms:
-        # if it already exists in netbox but something changed: we need to update it
-        if name not in old_vms:
-            continue
-        something_changed = False
-        # Without this the update is messed, because it's a dict, same for all the "del"s
-        # below these ifs (long story, just believe me here)
-        del old_vms[name]['site']
-        if old_vms[name].get('primary_ip4') is not None:
-            ip = old_vms[name]['primary_ip4']['address']
-            old_vms[name]['primary_ip4'] = {'address': ip}
-        if old_vms[name].get('primary_ip') is not None:
-            ip = old_vms[name]['primary_ip']['address']
-            old_vms[name]['primary_ip'] = {'address': ip}
-        if old_vms[name].get('primary_ip6') is not None:
-            ip = old_vms[name]['primary_ip6']['address']
-            old_vms[name]['primary_ip6'] = {'address': ip}
-        # Iterate over properties, set new stuff
-        for key in new_vms[name]:
-            # Edge cases... 4 of them...
-            if key == 'maxdisk' or key == 'description':
-                # These two keys may be None sometimes, which is annoying
-                # If it's None now, delete key
-                if new_vms[name].get(key) is None and old_vms[name].get(key) is not None:
-                    del old_vms[name][key]
-                    something_changed = True
-                # If it was None and isn't anymore, set key
-                if new_vms[name].get(key) is not None and old_vms[name].get(key) is None:
-                    old_vms[name][key] = new_vms[name][key]
-                    something_changed = True
-            elif key == 'status':
-                # Netbox returns status via 'value' key... (WHY????)
-                if old_vms[name][key]['value'] != new_vms[name][key]:
-                    old_vms[name][key] = new_vms[name][key]
-                    something_changed = True
-                else:
-                    del old_vms[name][key]
-            elif key == 'device' or key == 'cluster':
-                # Device & cluster are set by name
-                # (i don't think I made a cluster name change possible... but whatever)
-                if old_vms[name][key]['name'] != new_vms[name][key]['name']:
-                    old_vms[name][key] = new_vms[name][key]
-                    something_changed = True
-                else:
-                    del old_vms[name][key]
-            elif key == 'tags':
-                # Tags is tecnically a list, so we update the whole thing
-                old_tags = set([tag['name'] for tag in old_vms[name][key]])
-                new_tags = set([tag['name'] for tag in new_vms[name][key]])
-                # Reattribute tags
-                old_vms[name][key] = new_vms[name][key]
-                if old_tags != new_tags:
-                    something_changed = True
-            # General case: value for key is different means it changed
-            elif old_vms[name][key] != new_vms[name][key]:
-                old_vms[name][key] = new_vms[name][key]
-                something_changed = True
-        if something_changed:
-            updated.append(old_vms[name])
-    return updated
-
-
-def update_old_interfaces(proxmox_interfaces, netbox_interfaces):
-    created = []
-    updated = []
-    deleted = []
-    # check something changed: create/update/delete interface
-    # Note: interfaces is a list for each VM
-    # for each proxmox_interface not in deleted, search for it in netbox
-    for pi in proxmox_interfaces:
-        found = False
-        # if found: update/do nothing
-        for ni in netbox_interfaces:
-            updated_interface = False
-            # same VM, same interface name, see if it's necessary to update
-            if ni.virtual_machine['name'] == pi['virtual_machine']['name']:
-                if ni.name == pi['name']:
-                    found = True
-                    updated_interface = ni.untagged_vlan.vid != pi['untagged_vlan']['vid'] or \
-                        ni.mac_address.upper() != pi['mac_address'].upper()
-                    ni.mac_address = pi['mac_address'].upper()
-                    ni.untagged_vlan = pi['untagged_vlan']
-                    if updated_interface:
-                        ui = dict(ni)
-                        # Virtual machine won't change, nor mode
-                        del ui['virtual_machine']
-                        del ui['mode']
-                        # better to be a dict here
-                        updated.append(ui)
-                    break
-        # if not found: create
-        if not found:
-            created.append(pi)
-    for ni in netbox_interfaces:
-        found = False
-        # if not found in proxmox: delete (seem simple?)
-        for pi in proxmox_interfaces:
-            if ni.virtual_machine['name'] == pi['virtual_machine']['name']:
-                if ni.name == pi['name']:
-                    found = True
-                    break
-        if not found:
-            deleted.append(dict(ni))
-    return created, updated, deleted
-
-
-def update_virtual_machines(cluster_id, proxmox_vms, proxmox_interfaces):
-    try:
-        netbox_vms = list(NETBOX_API.virtualization.virtual_machines.filter(
-            cluster_id=cluster_id
-        ))
-        netbox_interfaces = list(NETBOX_API.virtualization.interfaces.all())
-    except Exception:
-        raise APIError('Failed to connect to NetBox Cluster!')
-    # TODO: redo this with vmid, which is more stable, name may change more
-    new_vms = {vm['name']: vm for vm in proxmox_vms}
-    old_vms = {vm.name: dict(vm) for vm in netbox_vms}
-    # Find out which ones to delete, which to create and which to update
-    deleted = [dict(vm) for vm in netbox_vms if vm.name not in new_vms]
-    to_create = [vm for vm in proxmox_vms if vm['name'] not in old_vms]
-    updated = update_old_vms(old_vms, new_vms)
-
-    for interface in netbox_interfaces:
-        interface.virtual_machine = {'name': interface.virtual_machine['name']}
-    to_create_i, updated_i, deleted_i = update_old_interfaces(
-        proxmox_interfaces, netbox_interfaces
-    )
-
-    # First create
-    try:
-        created = [
-            dict(v) for v in NETBOX_API.virtualization.virtual_machines.create(to_create)
-        ]
-        created_i = [
-            dict(i) for i in NETBOX_API.virtualization.interfaces.create(to_create_i)
-        ]
-    except Exception:
-        raise APIError('Error creating VMs & interfaces!')
-    # Then delete (interfaces first)
-    deleted_ids = [vm['id'] for vm in deleted]
-    deleted_i_ids = [i['id'] for i in deleted_i]
-    result_delete_i = len(deleted_i) == 0 or NETBOX_API.virtualization.interfaces.delete(deleted_i_ids)
-    result_delete = len(deleted) == 0 or NETBOX_API.virtualization.virtual_machines.delete(deleted_ids)
-    if not result_delete or not result_delete_i:
-        raise APIError('Error deleting VMs & interfaces!')
-    # Then update (whatever order)
-    result_update = len(updated) == 0 or NETBOX_API.virtualization.virtual_machines.update(updated)
-    result_update_i = len(updated_i) == 0 or NETBOX_API.virtualization.interfaces.update(updated_i)
-    if not result_update or not result_update_i:
-        raise APIError('Error updating VMs & interfaces!')
-    return created, updated, deleted, created_i, updated_i, deleted_i
-    # return to_create, updated, deleted, to_create_i, updated_i, deleted_i
-
-
-def all():
-    cluster, nodes = fetch_cluster_and_nodes()
-    tags = fetch_tags()
-    vms, interfaces = fetch_virtual_machines_and_interfaces()
-    # Assert some stuff first
-    cluster_id = assert_cluster_and_nodes_exist(cluster, nodes)
-    # In case tags have changed
-    created_tags, updated_tags, deleted_tags = update_tags(tags)
-    # Do the first "sync" (creates VMs and their respective interfaces)
-    created_vms, updated_vms, deleted_vms, \
-        created_interfaces, updated_interfaces, deleted_interfaces = \
-        update_virtual_machines(cluster_id, vms, interfaces)
-    return {
-        'tags': {
-            'created': created_tags,
-            'updated': updated_tags,
-            'deleted': deleted_tags,
-        },
-        'virtual_machines': {
-            'created': created_vms,
-            'updated': updated_vms,
-            'deleted': deleted_vms,
-        },
-        'interfaces': {
-            'created': created_interfaces,
-            'updated': updated_interfaces,
-            'deleted': deleted_interfaces,
-        }
-    }
diff --git a/netbox_proxmox_sync/api/utils/errors.py b/netbox_proxmox_sync/api/utils/errors.py
new file mode 100644
index 0000000000000000000000000000000000000000..9b16ad5edb3ade134a6a7d39aa5c876c4ed1e07d
--- /dev/null
+++ b/netbox_proxmox_sync/api/utils/errors.py
@@ -0,0 +1,15 @@
+class APIError(Exception):
+    def __init__(self, message, status=500, errors=[]):
+        super().__init__(message)
+        self.status = status
+        self.errors = errors
+
+
+class ValidationError(APIError):
+    def __init__(self, message, errors=[]):
+        super().__init__(message, 422, errors)
+
+
+class UnauthorizedError(APIError):
+    def __init__(self, message):
+        super().__init__(message, 401)
diff --git a/netbox_proxmox_sync/api/utils/models.py b/netbox_proxmox_sync/api/utils/models.py
new file mode 100644
index 0000000000000000000000000000000000000000..cd974a3aaf0716c056911b5899562b666d33e9cd
--- /dev/null
+++ b/netbox_proxmox_sync/api/utils/models.py
@@ -0,0 +1,167 @@
+from netbox_proxmox_sync.api.config import (
+    NETBOX_DEFAULT_TAG_COLOR,
+    NETBOX_VM_ROLE_ID,
+)
+# Classes to ensure NetBox and Proxmox data have the exact same format and fields,
+# which makes updating stuff correctly easier
+
+
+class VirtualMachine:
+    def __init__(self, vmid, name, status, node_name, cluster_name, vcpus, maxmem,
+                 role_id, tags=[], maxdisk=None, description=None, id=None):
+        self.vmid = vmid
+        self.name = name
+        self.status = status
+        self.node_name = node_name
+        self.cluster_name = cluster_name
+        self.role_id = role_id
+        self.vcpus = vcpus
+        self.maxmem = maxmem
+        self.tags = sorted(tags)
+        self.maxdisk = maxdisk
+        self.description = description
+        self.id = id
+
+    def to_dict(self):
+        # Trust me, it's important that they're sorted
+        vm_data = {
+            'name': self.name,
+            'status': self.status,
+            'device': {'name': self.node_name},
+            'cluster': {'name': self.cluster_name},
+            'vcpus': self.vcpus,
+            'memory': self.maxmem,
+            'role': self.role_id,
+            'tags': [{'name': tag} for tag in self.tags],
+            'custom_fields': {'vmid': self.vmid}
+        }
+        # Both these fields may be None, so we have to set them separately
+        if self.maxdisk is not None and self.maxdisk > 0:
+            vm_data['disk'] = int(self.maxdisk)
+        if self.description is not None:
+            # NetBox only allows 200 char description, but our VMs have more
+            # so we store the description in the "comments" instead
+            vm_data['comments'] = self.description
+        if self.id is not None:
+            vm_data['id'] = self.id
+        return vm_data
+
+
+class VirtualInterface:
+    def __init__(self, name, vm_name, mac_address, vlan_id, id=None):
+        self.name = name
+        self.vm_name = vm_name
+        self.mac_address = mac_address
+        self.vlan_id = vlan_id
+        self.id = id
+
+    def to_dict(self):
+        interface = {
+            'name': self.name,
+            'virtual_machine': {'name': self.vm_name},
+            'mac_address': self.mac_address,
+            'mode': 'access',
+            'untagged_vlan': {'vid': self.vlan_id},
+            # 'bridge': bridge
+        }
+        if self.id is not None:
+            interface['id'] = self.id
+        return interface
+
+
+class VirtualMachineTag:
+    def __init__(self, name, id=None, color=NETBOX_DEFAULT_TAG_COLOR):
+        self.name = name
+        self.slug = name.lower().replace(' ', '-').replace('.', '_')
+        self.color = color
+        self.id = id
+
+    def to_dict(self):
+        vm_tag = {
+            'name': self.name,
+            'slug': self.slug,
+            'color': self.color,
+            'object_types': ['virtualization.virtualmachine'],
+        }
+        if self.id is not None:
+            vm_tag['id'] = self.id
+        return vm_tag
+
+
+def tag_from_netbox(netbox_tag):
+    return VirtualMachineTag(
+        id=netbox_tag['id'],
+        name=netbox_tag['name'],
+        # slug=netbox_tag.slug,
+        color=netbox_tag['color'],
+    ).to_dict()
+
+
+def vm_from_netbox(netbox_vm):
+    role_id = None
+    if netbox_vm.get('role'):
+        role_id = netbox_vm['role']['id']
+    return VirtualMachine(
+        id=netbox_vm['id'],
+        vmid=netbox_vm['custom_fields']['vmid'],
+        name=netbox_vm['name'],
+        status=netbox_vm['status']['value'],
+        # FIXME: some of these are tricky
+        node_name=netbox_vm['device']['name'],
+        cluster_name=netbox_vm['cluster']['name'],
+        role_id=role_id,
+        vcpus=netbox_vm['vcpus'],
+        maxmem=netbox_vm['memory'],
+        tags=[tag['name'] for tag in netbox_vm['tags']],
+        maxdisk=netbox_vm.get('disk'),
+        description=netbox_vm.get('comments'),
+    ).to_dict()
+
+
+def interface_from_netbox(netbox_interface):
+    return VirtualInterface(
+        id=netbox_interface['id'],
+        name=netbox_interface['name'],
+        vm_name=netbox_interface['virtual_machine']['name'],
+        mac_address=netbox_interface['mac_address'].upper(),
+        vlan_id=netbox_interface['untagged_vlan']['vid'],
+    ).to_dict()
+
+
+def tag_from_proxmox(tag_name, color=NETBOX_DEFAULT_TAG_COLOR):
+    return VirtualMachineTag(
+        tag_name,
+        color,
+    ).to_dict()
+
+
+def vm_from_proxmox(cluster_name, proxmox_node_name, proxmox_vm, tags=[]):
+    maxdisk = proxmox_vm.get('maxdisk')
+    if maxdisk is not None:
+        maxdisk = int(maxdisk) / 2 ** 30  # B -> GB
+    memory = int(proxmox_vm['maxmem']) / 2**20  # B -> MB
+    return VirtualMachine(
+        vmid=proxmox_vm['vmid'],
+        name=proxmox_vm['name'],
+        status='active' if proxmox_vm['status'] == 'running' else 'offline',
+        node_name=proxmox_node_name,
+        cluster_name=cluster_name,
+        vcpus=proxmox_vm['cpus'],
+        role_id=NETBOX_VM_ROLE_ID,
+        maxmem=memory,
+        tags=tags,
+        maxdisk=maxdisk,
+        description=proxmox_vm.get('description'),
+    ).to_dict()
+
+
+def interface_from_proxmox(proxmox_vm_name, interface_name, proxmox_interface):
+    # net[0-9]+: 'virtio=00:00:00:00:00:00,bridge=vmbr<VID>'
+    mac = proxmox_interface.split('virtio=')[1].split(',')[0]
+    vlan_id = int(proxmox_interface.split('bridge=vmbr')[1].split(',firewall')[0])
+    return VirtualInterface(
+        name=f'{proxmox_vm_name}:{interface_name}',
+        vm_name=proxmox_vm_name,
+        mac_address=mac.upper(),
+        vlan_id=vlan_id,
+    ).to_dict()
diff --git a/netbox_proxmox_sync/api/views.py b/netbox_proxmox_sync/api/views.py
index 57eff190973956f9b442aa6be70c6056921ea41d..67f07669dd8969fbe3ec7e8bdf8931c632eeb9ef 100644
--- a/netbox_proxmox_sync/api/views.py
+++ b/netbox_proxmox_sync/api/views.py
@@ -1,9 +1,10 @@
 from django.http import HttpResponse
 from django.views import View
 from django.contrib.auth.mixins import PermissionRequiredMixin
-from netbox_proxmox_sync.api.errors import APIError
-from netbox_proxmox_sync.api.proxmox import create, update, delete
+from netbox_proxmox_sync.api.utils.errors import APIError
+from netbox_proxmox_sync.api.netbox import create, update, delete
 import json
+import traceback
 # TODO: fix HTTP status codes
 # TODO: proper error handling
 
@@ -17,7 +18,8 @@ class CreateCluster(PermissionRequiredMixin, View):
             json_result = json.dumps(result)
             return HttpResponse(json_result, content_type='application/json')
         except APIError as e:
-            json_result = json.dumps({'error': str(e)})
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
             return HttpResponse(
                 json_result, status=e.status, content_type='application/json'
             )
@@ -32,7 +34,8 @@ class UpdateCluster(PermissionRequiredMixin, View):
             json_result = json.dumps(result)
             return HttpResponse(json_result, status=201, content_type='application/json')
         except APIError as e:
-            json_result = json.dumps({'error': str(e)})
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
             return HttpResponse(
                 json_result, status=e.status, content_type='application/json'
             )
@@ -47,7 +50,8 @@ class DeleteCluster(PermissionRequiredMixin, View):
             json_result = json.dumps(result)
             return HttpResponse(json_result, status=201, content_type='application/json')
         except APIError as e:
-            json_result = json.dumps({'error': str(e)})
+            raise e
+            json_result = json.dumps({'error': str(e), 'trace': traceback.print_exc()})
             return HttpResponse(
                 json_result, status=e.status, content_type='application/json'
             )
diff --git a/setup.py b/setup.py
index d9b13f8e789e41f6d93b482fe148fbc9ebe12021..b59b584a527fb943a9e5275b27f0e474dc859826 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
 
 setup(
     name='netbox-proxmox-sync',
-    version='1.0.2',
+    version='2.0.0',
     description='Import Proxmox cluster info into NetBox.',
     install_requires=['pynetbox', 'proxmoxer'],
     include_package_data=True,