Repository: KatharaFramework/Kathara (language: python)

Issue:
Env Regex Match failure
in the `machine.py` file
the regex for `env` is `^(?P<key>\w+)=(?P<value>\w+)$`
which does not accurately match IP addresses as a value. I don't think IP addresses will ever be a key.

the following worked for me

`^(?P<key>\w+)=(?P<value>\w+.*)$`
pyuv broken with python 3.10.x
**Describe the bug**
pyuv is currently broken for python 3.10.x

see upstream issue https://github.com/saghul/pyuv/issues/271 / PR https://github.com/saghul/pyuv/pull/275

**To Reproduce**
Steps to reproduce the behavior:
1. Clone repo
2. Make venv with python 3.10 & activate it (`python3 -m venv ./venv && source venv/bin/activate`)
3. run `pip install -r src/requirements.txt`
4. pip errors when trying to compile native parts of pyuv

**Temporary Workaround**
Use the master branch of pyuv as this has the changes merges from the PR mentioned above. This means to replace the pyuv line in `requirements.txt` with `git+https://github.com/saghul/pyuv@master#egg=pyuv`.




Discussion / hints:
Hi @zhangineer,
thanks for the issue. I'll open a branch and fix the regex.

Mariano.


Fix patch:
diff --git a/README.md b/README.md
index 2eba617a..435c1070 100644
--- a/README.md
+++ b/README.md
@@ -47,9 +47,9 @@ Kathará emulates network scenarios using either Docker or Kubernetes as backend
 ## Installation
 Install Docker and then run the installer specific for your Operating System. For a step by step guide check the [Wiki](https://github.com/KatharaFramework/Kathara/wiki).
 
-For further information on how to use Kathará on Kubernetes please refer to the [Wiki Page](https://github.com/KatharaFramework/Kathara/wiki/Megalos-(Kathara-over-Kubernetes)).
+For further information on how to use Kathará on Kubernetes (**Megalos**) please refer to the [Wiki Page](https://github.com/KatharaFramework/Kathara/wiki/Megalos-(Kathara-over-Kubernetes)).
 
-Kathará provides also a set of Python APIs.
+Kathará also provides a set of Python APIs.
 See the [Python APIs Tutorial](https://github.com/KatharaFramework/Kathara/wiki/Kathara-Python-API) and the [docs](https://github.com/KatharaFramework/Kathara/wiki/Kathara-API-Docs).
 
 ## Quick Example
@@ -63,18 +63,24 @@ See the [Python APIs Tutorial](https://github.com/KatharaFramework/Kathara/wiki/
 * Kathará will read the configuration of the scenario from `lab.conf` and the various `*.startup` files and start the devices, opening terminal windows to interact with them.
 * After you're done experimenting, simply run `kathara lclean` and wait until the network scenario closes.
 
-## Publications 
+## Publications and Presentations
 Kathará is developed by [Roma Tre Computer Networks and Security Research Group](https://compunet.ing.uniroma3.it/). 
-Several publications are related to this tool:
 
+Several publications are related to the tool:
 - <a href="https://ieeexplore.ieee.org/abstract/document/8406267/" target="_blank"> **Kathará: A container-based framework for implementing network function virtualization and software defined networks**</a> (at NOMS 2018)
-    - <a href="https://www.slideshare.net/GaetanoBonofiglio/kathar-noms-2018-106743047" target="_blank">Presentation</a>
+  - <a href="https://www.slideshare.net/GaetanoBonofiglio/kathar-noms-2018-106743047" target="_blank">Presentation</a>
 - <a href="https://ieeexplore.ieee.org/document/9110288" target="_blank"> **Megalos: A Scalable Architecture for the Virtualization of Network Scenarios** </a> (at NOMS 2020)
-    - <a href="https://www.youtube.com/watch?v=XvInh-kujrA&amp;feature=youtu.be" target="_blank">Presentation</a>
-- <a href="https://ieeexplore.ieee.org/document/9110351" target="_blank"> **Kathará: A Lightweight Network Emulation System** </a> (at NOMS 2020) 
-    - <a href="https://www.youtube.com/watch?v=ionEpKjv3Vk&amp;feature=youtu.be" target="_blank">Presentation</a>
-    - <a href="https://noms2020.ieee-noms.org/sites/noms2020.ieee-noms.org/files/NOMS2020_TPC_awards_signed.V2_Page_2_%20Best%20Nemo%20.jpg" target="_blank">Best Demo Paper Award</a>
-- <a href="https://www.mdpi.com/1999-5903/13/9/227" target="_blank"> **Megalos: A Scalable Architecture for the Virtualization of Large Network Scenarios** </a> (at Future Internet 2021)
+  - <a href="https://www.youtube.com/watch?v=XvInh-kujrA" target="_blank">Presentation</a>
+- <a href="https://ieeexplore.ieee.org/document/9110351" target="_blank"> **Kathará: A Lightweight Network Emulation System** </a> (at NOMS 2020)
+  - <a href="https://www.youtube.com/watch?v=ionEpKjv3Vk" target="_blank">Presentation</a>
+  - <a href="https://www.kathara.org/assets/images/awards/NOMS2020_TPC_awards_signed.V2_Page_2_%20Best%20Nemo%20.jpg" target="_blank">Best Demo Paper Award</a>
+- <a href="https://www.mdpi.com/1999-5903/13/9/227" target="_blank"> **Megalos: A Scalable Architecture for the Virtualization of Large Network Scenarios** </a> (in MDPI Future Internet Journal 2021)
+- <a href="https://ieeexplore.ieee.org/document/9789876">**Sibyl: a Framework for Evaluating the Implementation of Routing Protocols in Fat-Trees**</a> (at NOMS 2022)
+  - <a href="https://www.youtube.com/watch?v=FZjHjLZzXCY">Presentation</a>
+
+Kathará has been also presented in meetings and workshops:
+- RIFT Working Group Meeting (IETF 107 - March 2020)
+- <a href="https://www.youtube.com/watch?v=GVBOdNzwhBA" target="_blank">Kathará: A Lightweight Network Emulation System (Italian Audio)</a> (GraphRM - June 2022)
 
 ## External Tools
 
diff --git a/docs/footer.txt b/docs/footer.txt
index ba0ddaf9..ebb0ee60 100644
--- a/docs/footer.txt
+++ b/docs/footer.txt
@@ -2,7 +2,7 @@
 
 Report bugs opening an issue on the [official GitHub repository](https://github.com/KatharaFramework/Kathara) where the development and maintenance is primarily done.  
 Issues which are security relevant should be disclosed privately to the [Kathara mailing list](mailto:contact@kathara.org). You do not have to be subscribed to the list to send a message there.  
-When reporting a bug, remember to write used commands, eventually attach your lab, and include the output of `kathara-check`(1) in order to make possible to reproduce the bug.
+When reporting a bug, remember to write used commands, eventually attach your network scenario, and include the output of `kathara-check`(1) in order to make possible to reproduce the bug.
 
 ## AUTHORS
 
diff --git a/docs/kathara-connect.1.ronn b/docs/kathara-connect.1.ronn
index 6629f0be..c8c57125 100644
--- a/docs/kathara-connect.1.ronn
+++ b/docs/kathara-connect.1.ronn
@@ -18,10 +18,10 @@ Open a shell in device DEVICE_NAME.
     Show an help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    Connect to a Kathara device belonging to a lab that is located inside DIRECTORY.  
-	If no `-d` option is provided, assume the lab is located in the current directory.
+    Connect to a Kathara device belonging to a network scenario that is located inside DIRECTORY.  
+	If no `-d` option is provided, assume the network scenario is located in the current directory.
 	This option cannot be used in conjuction with `-v` or `--vdevice`.
 
 * `-v`, `--vdevice`:
@@ -51,7 +51,7 @@ Connects to a device called `pc1`, started with `kathara-vstart`(1).
 
 	kathara connect as1r1
 
-Connects to a device called `as1r1` belonging to a lab located in current folder and started with `kathara-lstart`(1).
+Connects to a device called `as1r1` belonging to a network scenario located in current folder and started with `kathara-lstart`(1).
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-exec.1.ronn b/docs/kathara-exec.1.ronn
index 84cf3d27..e1594b47 100644
--- a/docs/kathara-exec.1.ronn
+++ b/docs/kathara-exec.1.ronn
@@ -18,10 +18,10 @@ Execute a command in the Kathara device DEVICE_NAME.
     Show an help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    Execute the specified command in a Kathara device belonging to a lab that is located inside DIRECTORY.  
-	If no `-d` option is provided, assume the lab is located in the current directory.
+    Execute the specified command in a Kathara device belonging to a network scenario that is located inside DIRECTORY.  
+	If no `-d` option is provided, assume the network scenario is located in the current directory.
 	This option cannot be used in conjuction with `-v` or `--vmachine`.
 
 * `-v`, `--vmachine`:
@@ -49,7 +49,7 @@ Execute the command ping into a device called `pc1`, started with `kathara-vstar
 
 	kathara exec as1r1 "ping 127.0.0.1"
 
-Execute the command ping into a device called `as1r1` belonging to a lab located in current folder and started with `kathara-lstart`(1).
+Execute the command ping into a device called `as1r1` belonging to a network scenario located in current folder and started with `kathara-lstart`(1).
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-lab-dirs.7.ronn b/docs/kathara-lab-dirs.7.ronn
index 5be306be..de7dd975 100644
--- a/docs/kathara-lab-dirs.7.ronn
+++ b/docs/kathara-lab-dirs.7.ronn
@@ -1,10 +1,10 @@
 m4_changequote()
-kathara-lab-dirs(7) -- Lab directory structure
+kathara-lab-dirs(7) -- Network scenario directory structure
 =============================================
 
 ## DESCRIPTION
 
-A generic lab directory `lab-path` should have the following structure.
+A generic network scenario directory `lab-path` should have the following structure.
 
 * `lab-path/lab.conf`:
 	See `kathara-lab.conf`(5)
@@ -31,7 +31,7 @@ A generic lab directory `lab-path` should have the following structure.
 	Note that this script is executed by using the `device_shell` value specified in `kathara.conf`(5) as interpreter. If you would like to use a different interpreter, change it inside `kathara.conf`(5) or with `kathara-settings`(1).
 
 * `lab-path/shared/`:
-	This is a shared folder between the host and all the lab devices. This could be useful to transfer files between the host and a device or viceversa.
+	This is a shared folder between the host and all the network scenario devices. This could be useful to transfer files between the host and a device or viceversa.
 
 * `lab-path/shared.startup`
 	This is a shell script that will be run on every device just before `lab-path/device.startup`. It is not required to assign this script the executable permission.
@@ -44,7 +44,7 @@ A generic lab directory `lab-path` should have the following structure.
 	Note that this script is executed by using the `device_shell` value specified in `kathara.conf`(5) as interpreter. If you would like to use a different interpreter, change it inside `kathara.conf`(5) or with `kathara-settings`(1).
 
 * `lab-path/_test/`
-	This subdirectory contains information for performing a test of the lab. It may be automatically created by `kathara-ltest`(1) or may contain user created files describing customized tests. For more information about this directory, see `kathara-ltest`(1).
+	This subdirectory contains information for performing a test of the network scenario. It may be automatically created by `kathara-ltest`(1) or may contain user created files describing customized tests. For more information about this directory, see `kathara-ltest`(1).
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-lab.conf.5.ronn b/docs/kathara-lab.conf.5.ronn
index 3916bbd4..ff3e7d2b 100644
--- a/docs/kathara-lab.conf.5.ronn
+++ b/docs/kathara-lab.conf.5.ronn
@@ -1,10 +1,10 @@
 m4_changequote()
-kathara-lab.conf(5) -- Lab configuration file
+kathara-lab.conf(5) -- Network scenario configuration file
 =============================================
 
 ## DESCRIPTION
 
-The main lab configuration file. In this file you can specify the names of devices to be started, any option that should be used when launching them, and the topology of the network that connects them. Optionally, you can also provide some descriptive information for the lab, which will be displayed upon its startup. This file is not explicitly required, but running a lab without a lab.conf file is kind of useless...
+The main network scenario configuration file. In this file you can specify the names of devices to be started, any option that should be used when launching them, and the topology of the network that connects them. Optionally, you can also provide some descriptive information for the network scenario, which will be displayed upon its startup. This file is not explicitly required, but running a network scenario without a lab.conf file is kind of useless...
 
 This file is a list of `device[arg]=value` assignments, where `arg` can be an integer value or the name of an option (described below).
 
@@ -55,16 +55,16 @@ In order to establish a uniform convention, comment lines should always start wi
 * `num_terms` (integer):
     Choose the number of terminals to open for this device.
 
-## LAB META INFORMATION
+## NETWORK SCENARIO META INFORMATION
 
-It is also possible to provide descriptive information about a lab by using one of the following special assignments:
+It is also possible to provide descriptive information about a network scenario by using one of the following special assignments:
 
-+ LAB_NAME=string (a word as identifier for the lab)
-+ LAB_DESCRIPTION=string (a brief description of the purpose of the lab)
-+ LAB_VERSION=string (the version of the lab)
-+ LAB_AUTHOR=string (people who have written the lab)
-+ LAB_EMAIL=string (contacts of lab authors)
-+ LAB_WEB=string (useful web resources related to the lab)
++ LAB_NAME=string (a word as identifier for the network scenario)
++ LAB_DESCRIPTION=string (a brief description of the purpose of the network scenario)
++ LAB_VERSION=string (the version of the network scenario)
++ LAB_AUTHOR=string (people who have written the network scenario)
++ LAB_EMAIL=string (contacts of network scenario authors)
++ LAB_WEB=string (useful web resources related to the network scenario)
 
 ## EXAMPLE
 
diff --git a/docs/kathara-lab.dep.5.ronn b/docs/kathara-lab.dep.5.ronn
index f23a4e32..2e00ddb8 100644
--- a/docs/kathara-lab.dep.5.ronn
+++ b/docs/kathara-lab.dep.5.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-lab.dep(5) -- Lab dependencies file
+kathara-lab.dep(5) -- Network scenario dependencies file
 =============================================
 
 ## DESCRIPTION
diff --git a/docs/kathara-lab.ext.5.ronn b/docs/kathara-lab.ext.5.ronn
index c6a7fc05..e7bb1b98 100644
--- a/docs/kathara-lab.ext.5.ronn
+++ b/docs/kathara-lab.ext.5.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-lab.ext(5) -- Lab external networks file
+kathara-lab.ext(5) -- Network scenario external networks file
 =============================================
 
 ## DESCRIPTION
diff --git a/docs/kathara-lclean.1.ronn b/docs/kathara-lclean.1.ronn
index 3a6f047a..3d21e8ac 100644
--- a/docs/kathara-lclean.1.ronn
+++ b/docs/kathara-lclean.1.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-lclean(1) -- Stop a Kathara lab
+kathara-lclean(1) -- Stop a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
@@ -8,8 +8,8 @@ kathara-lclean(1) -- Stop a Kathara lab
 
 ## DESCRIPTION
 
-Gracefully shut down a running lab.
-By default, stops all the devices of the lab. If a list of <DEVICE_NAME>s is provided, then only devices with a matching name and that are part of the lab will be halted. 
+Gracefully shut down a running network scenario.
+By default, stops all the devices of the network scenario. If a list of <DEVICE_NAME>s is provided, then only devices with a matching name and that are part of the network scenario will be halted. 
 
 ## OPTIONS
 
@@ -17,13 +17,13 @@ By default, stops all the devices of the lab. If a list of <DEVICE_NAME>s is pro
     Show an help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-	Specify the folder containing the lab.
+	Specify the folder containing the network scenario.
 
-	Cleans the Kathara lab that is located inside <DIRECTORY>.  
-	If no `-d` option is provided, assume the lab is located in the current directory.
+	Cleans the Kathara network scenario that is located inside <DIRECTORY>.  
+	If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `DEVICE_NAME`:
-    A list of device names. Instead of shutting down the whole lab, only specified devices are stopped.
+    A list of device names. Instead of shutting down the whole network scenario, only specified devices are stopped.
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-lconfig.1.ronn b/docs/kathara-lconfig.1.ronn
index fe016df6..605d86f7 100644
--- a/docs/kathara-lconfig.1.ronn
+++ b/docs/kathara-lconfig.1.ronn
@@ -1,14 +1,14 @@
 m4_changequote()
-kathara-lconfig(1) -- Attach network interfaces to a running Kathara device of a Kathara lab
+kathara-lconfig(1) -- Attach network interfaces to a running Kathara device of a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
 
-`kathara lconfig` [`-h`] [`-d` <DIRECTORY>] `-n` <DEVICE_NAME> `--eth` <CD> [<CD> ...]  
+`kathara lconfig` [`-h`] [`-d` <DIRECTORY>] `-n` <DEVICE_NAME> (`--add` <CD> [<CD> ...] \| `--rm` <CD> [<CD> ...])  
 
 ## DESCRIPTION
 
-Attach network interfaces to running devices of a Kathara lab "on the fly". The affected device is identified by <DEVICE_NAME>, which is the name of a running device.
+Manage the network interfaces of a running Kathara device in a network scenario. The affected device is identified by <DEVICE_NAME>, which is the name of the running device in the network scenario. 
 
 ## OPTIONS
 
@@ -16,24 +16,33 @@ Attach network interfaces to running devices of a Kathara lab "on the fly". The
     Show a help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    Attach network interfaces to running devices of the Kathara lab that is located inside <DIRECTORY>.
-    If no `-d` option is provided, assume the lab is located in the current directory.
+    Manage the network interfaces of running devices in a Kathara network scenario that is located inside <DIRECTORY>.
+    If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `-n` <DEVICE_NAME>, `--name` <DEVICE_NAME>:
-    Name of the device to be connected on desired collision domains.
+    Name of the device to manage.
 
-* `--eth` <CD> [<CD> ...]:
-	Specify the collision domain for an interface.
+* `--add` <CD> [<CD> ...]:
+	Specify the collision domain to add.
 
-    Equip the device with an additional network interface attached to a (virtual) collision domain whose name is <CD>. The number of the resulting network interface is generated incrementing the number of the last network interface of the device.
+    Equip the device with an additional network interface attached to a (virtual) collision domain whose name is <CD>. The number of the resulting network interface is generated incrementing the number of the last network interface used by the device.
+
+* `--rm` <CD> [<CD> ...]:
+	Specify the collision domain to remove.
+
+    Disconnect the device from the collision domain whose name is <CD> and remove the corresponding interface.
 
 ## EXAMPLES
 
-    kathara lconfig -d path/to/lab -n pc1 --eth X Y
+    kathara lconfig -d path/to/network_scenario -n pc1 --add X Y
+
+Two new interfaces will be added to the device pc1 in the network scenario located in "path/to/network_scenario": the first one will be attached to the collision domain named X, while the other one to the collision domain named Y. Both the interfaces will have to be configured by hand inside the device (for example, by using ifconfig).
+
+    kathara vconfig -n pc1 --rm X
 
-Two new interfaces will be added to the device pc1 of the lab: the first one will be attached to the collision domain named X, while the other one to the collision domain named Y. Both the interfaces will have to be configured by hand inside the device (for example, by using ifconfig).
+pc1, in the network scenario located in "path/to/network_scenario", will be disconnected from the collision domain named X and the corresponding network interface will be removed.
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-linfo.1.ronn b/docs/kathara-linfo.1.ronn
index 23dd4851..141afe40 100644
--- a/docs/kathara-linfo.1.ronn
+++ b/docs/kathara-linfo.1.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-linfo(1) -- Show information about a Kathara lab
+kathara-linfo(1) -- Show information about a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
@@ -9,11 +9,11 @@ kathara-linfo(1) -- Show information about a Kathara lab
 
 ## DESCRIPTION
 
-Show exhaustive status of each device of the current lab. Can only be used when after the lab is started.
+Show exhaustive status of each device of the current network scenario. Can only be used when after the network scenario is started.
 
 The following information are displayed:
 
-    + `LAB_HASH`: autogenerated hash associated with a lab
+    + `LAB_HASH`: autogenerated hash associated with a network scenario
     + `DEVICE_NAME`: name of the device
     + `STATUS`: status of the device
     + `CPU %`: the percentage of the host's CPU the device is using
@@ -27,12 +27,12 @@ The following information are displayed:
     Show an help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    If no `-d` option is provided, assume the lab is located in the current directory.
+    If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `-l`, `--live`:
-    Live update of lab status.
+    Live update of network scenario status.
 
     To quit the live screen, pass the `SIGINT` `signal`(7) to the process (usually CTRL+C).
 
@@ -40,8 +40,8 @@ The following information are displayed:
     Read information from lab.conf and display the following:
 
     * Descriptive information (retrieved from `kathara-lab.conf`(5))
-    * Number of devices that make up the lab
-    * Number of networks that make up the lab
+    * Number of devices that make up the network scenario
+    * Number of networks that make up the network scenario
 
 * `-n` <DEVICE_NAME>, `--name` <DEVICE_NAME>:
     Show only info about a specified device.
diff --git a/docs/kathara-list.1.ronn b/docs/kathara-list.1.ronn
index e0050948..3829909a 100644
--- a/docs/kathara-list.1.ronn
+++ b/docs/kathara-list.1.ronn
@@ -13,7 +13,7 @@ Display a variety of information for all the devices of the current user.
 
 The following information are displayed:
 
-* `LAB_HASH`: autogenerated hash associated with a lab
+* `LAB_HASH`: autogenerated hash associated with a network scenario
 * `DEVICE_NAME`: name of the device
 * `STATUS`: status of the device 
 * `CPU %`: the percentage of the host's CPU the device is using
@@ -32,13 +32,13 @@ The following information are displayed:
     This option can be run only by `root` user.
 
 * `-l`, `--live`:
-    Live mode, can be used only when a lab is launched.
+    Live mode, can be used only when a network scenario is launched.
 
-    Show exhaustive live status of each device of the current lab. Can only be used when after the lab is started. This overrides the default behavior of the command described above.
+    Show exhaustive live status of each device of the current network scenario. Can only be used when after the network scenario is started. This overrides the default behavior of the command described above.
 
     The following information are displayed:
 
-        + `LAB_HASH`: autogenerated hash associated with a lab
+        + `LAB_HASH`: autogenerated hash associated with a network scenario
         + `DEVICE_NAME`: name of the device
         + `STATUS`: status of the device 
         + `CPU %`: the percentage of the host's CPU the device is using
diff --git a/docs/kathara-lrestart.1.ronn b/docs/kathara-lrestart.1.ronn
index aad0acf2..7c326b93 100644
--- a/docs/kathara-lrestart.1.ronn
+++ b/docs/kathara-lrestart.1.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-lrestart(1) -- Restart a Kathara lab
+kathara-lrestart(1) -- Restart a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
@@ -7,12 +7,12 @@ kathara-lrestart(1) -- Restart a Kathara lab
 `kathara lrestart` [`-h`] [`-F`] [`-l`]  
 [`--noterminals` | `--terminals` | `--privileged`] [`-d` <DIRECTORY>]  
 [`-o` [<OPTION> [<OPTION> ...]]] [`--xterm` <XTERM>]  
-[`-H`] [`-S`]
+[`-H`] [`-S`] [<DEVICE_NAME> [<DEVICE_NAME> ...]]  
 
 
 ## DESCRIPTION
 
-Restart a Kathara lab. It simply wraps a call to `kathara-lclean`(1) and then `kathara-lstart`(1).
+Restart a Kathara network scenario. It simply wraps a call to `kathara-lclean`(1) and then `kathara-lstart`(1).
 
 Mainly it has the same options of `lstart` with the lack of some options (e.g. `--print`).
 
@@ -22,32 +22,33 @@ Mainly it has the same options of `lstart` with the lack of some options (e.g. `
     Show an help message and exit.
 
 * `--noterminals`:
-    Start the lab devices without opening terminal windows. Override the default setting specified in `kathara.conf`(5) file.
+    Start the network scenario devices without opening terminal windows. Override the default setting specified in `kathara.conf`(5) file.
 
 * `--terminals`:
-    Start the lab devices opening their terminal windows. Override the default setting specified in `kathara.conf`(5) file.
+    Start the network scenario devices opening their terminal windows. Override the default setting specified in `kathara.conf`(5) file.
 
 * `--privileged`:
     Start the devices in privileged mode. MUST BE ROOT FOR THIS OPTION.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    Restart the Kathara lab that is located inside DIRECTORY.  
-    If no `-d` option is provided, assume the lab is located in the current directory.
+    Restart the Kathara network scenario that is located inside DIRECTORY.  
+    If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `-F`, `--force-lab`:
-    Force the lab to start without a lab.conf or lab.dep file.
+    Force the network scenario to start without a lab.conf or lab.dep file.
 
-    As a native behaviour, Kathara starts a device for each directory that it finds in a designated path, by default the current directory. In order to prevent the user from accidentally starting random devices from a directory that does not contain a lab, lstart requires the presence of the `kathara-lab.conf`(5) file in the affected directory. If this file is not required for your setting, and you still want to launch your lab without creating it, you can use this option to force Kathara starting a lab in the affected directory.
+    As a native behaviour, Kathara starts a device for each directory that it finds in a designated path, by default the current directory. In order to prevent the user from accidentally starting random devices from a directory that does not contain a network scenario, lstart requires the presence of the `kathara-lab.conf`(5) file in the affected directory. If this file is not required for your setting, and you still want to launch your network scenario without creating it, you can use this option to force Kathara starting a network scenario in the affected directory.
 
 * `-l`, `--list`:
-    Show informarion about running devices after the lab has been started.
+    Show informarion about running devices after the network scenario has been started.
 
 * `-o` [<OPTION> [<OPTION> ...]], `--pass` [<OPTION> [<OPTION> ...]]:
-    Apply options to all devices of a lab during startup.
+    Apply options to all devices of a network scenario during startup.
 
-    Should be a list of double quoted strings, like `--pass "mem=64m" "image=kathara/netkit_base"`.
+    Should be a list of double quoted strings, like `--pass "mem=64m" "image=kathara/frr"`.
+    You can specify the same device options described in `kathara-lab.conf`(5).
 
 * `--xterm` <XTERM>:
     Set a different terminal emulator application (Unix only).
@@ -62,7 +63,10 @@ Mainly it has the same options of `lstart` with the lack of some options (e.g. `
 * `-S`, `--no-shared`:
     `/shared` dir will not be mounted inside the device.
 
-    By default, the "shared" directory inside the lab folder is made available for reading/writing inside the device under the special directory `/shared`. This option disables this behaviour, thus not making the host filesystem accessible from inside the device. Override the default setting specified in `kathara.conf`(5) file.
+    By default, the "shared" directory inside the network scenario folder is made available for reading/writing inside the device under the special directory `/shared`. This option disables this behaviour, thus not making the host filesystem accessible from inside the device. Override the default setting specified in `kathara.conf`(5) file.
+
+* `DEVICE_NAME`:
+    A list of device names. Instead of restarting the whole network scenario, only specified devices are restarted.
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-lstart.1.ronn b/docs/kathara-lstart.1.ronn
index de75a605..c6b0462f 100644
--- a/docs/kathara-lstart.1.ronn
+++ b/docs/kathara-lstart.1.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-lstart(1) -- Start a Kathara lab
+kathara-lstart(1) -- Start a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
@@ -11,12 +11,12 @@ kathara-lstart(1) -- Start a Kathara lab
 
 ## DESCRIPTION
 
-In order to ease setting up complex network with Kathara it is possible to completely describe it into special configuration files, creating a so-called **lab**. The lab can be started with a single command as if it were a virtual network 'laboratory'.
+In order to ease setting up complex network with Kathara it is possible to completely describe it into special configuration files, creating a so-called **network scenario**. The network scenario can be started with a single command as if it were a virtual network 'laboratory'.
 
-Start a set of devices that are part of a Kathara lab and configures them according to the parameters contained in the lab description.
-By default, all the devices that make up the lab are started. If a list of <DEVICE_NAME>s is provided, then only devices with a matching name and that are part of the lab are started.
+Start a set of devices that are part of a Kathara network scenario and configures them according to the parameters contained in the network scenario description.
+By default, all the devices that make up the network scenario are started. If a list of <DEVICE_NAME>s is provided, then only devices with a matching name and that are part of the network scenario are started.
 
-The configuration of a Kathara lab consists of some files described in `kathara-lab.conf`(5), `kathara-lab.dep`(5), `kathara-lab.ext`(5) and directories whose names, locations and contents are described in `kathara-lab-dirs`(7).
+The configuration of a Kathara network scenario consists of some files described in `kathara-lab.conf`(5), `kathara-lab.dep`(5), `kathara-lab.ext`(5) and directories whose names, locations and contents are described in `kathara-lab-dirs`(7).
 
 `kathara-lab.ext`(5) is only available on Linux platforms and need root permissions to be used. In addition, terminals are not open automatically when this file is present. To connect to a device, use `kathara-connect`(1).
 
@@ -26,12 +26,12 @@ The configuration of a Kathara lab consists of some files described in `kathara-
     Show a help message and exit.
 
 * `--noterminals`:
-    Start the lab devices without opening terminal windows.
+    Start the network scenario devices without opening terminal windows.
 
     Override the default setting specified in `kathara.conf`(5) file.
 
 * `--terminals`:
-    Start the lab devices opening their terminal windows.
+    Start the network scenario devices opening their terminal windows.
 
     Override the default setting specified in `kathara.conf`(5) file.
 
@@ -39,23 +39,24 @@ The configuration of a Kathara lab consists of some files described in `kathara-
     Start the devices in privileged mode. MUST BE ROOT FOR THIS OPTION.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    Start the Kathara lab that is located inside <DIRECTORY>.  
-    If no `-d` option is provided, assume the lab is located in the current directory.
+    Start the Kathara network scenario that is located inside <DIRECTORY>.  
+    If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `-F`, `--force-lab`:
-    Force the lab to start without a lab.conf or lab.dep file.
+    Force the network scenario to start without a lab.conf or lab.dep file.
 
-    As a native behaviour, Kathara starts a device for each directory that it finds in a designated path, by default the current directory. In order to prevent the user from accidentally starting random devices from a directory that does not contain a lab, lstart requires the presence of the `kathara-lab.conf`(5) file in the affected directory. If this file is not required for your setting, and you still want to launch your lab without creating it, you can use this option to force Kathara starting a lab in the affected directory.
+    As a native behaviour, Kathara starts a device for each directory that it finds in a designated path, by default the current directory. In order to prevent the user from accidentally starting random devices from a directory that does not contain a network scenario, lstart requires the presence of the `kathara-lab.conf`(5) file in the affected directory. If this file is not required for your setting, and you still want to launch your network scenario without creating it, you can use this option to force Kathara starting a network scenario in the affected directory.
 
 * `-l`, `--list`:
-    Show information about running devices after the lab has been started.
+    Show information about running devices after the network scenario has been started.
 
 * `-o` [<OPTION> [<OPTION> ...]], `--pass` [<OPTION> [<OPTION> ...]]:
-    Apply options to all devices of a lab during startup.
+    Apply options to all devices of a network scenario during startup.
 
-    Should be a list of double quoted strings, like `--pass "mem=64m" "image=kathara/netkit_base"`.
+    Should be a list of double quoted strings, like `--pass "mem=64m" "image=kathara/frr"`.
+    You can specify the same device options described in `kathara-lab.conf`(5).
 
 * `--xterm` <XTERM>:
     Set a different terminal emulator application (Unix only).
@@ -73,10 +74,10 @@ The configuration of a Kathara lab consists of some files described in `kathara-
 * `-S`, `--no-shared`:
     `/shared` dir will not be mounted inside the devices.
 
-    By default, the "shared" directory inside the lab folder is made available for reading/writing inside the device under the special directory `/shared`. This option disables this behaviour, thus not making the host filesystem accessible from inside the device. Override the default setting specified in `kathara.conf`(5) file.
+    By default, the "shared" directory inside the network scenario folder is made available for reading/writing inside the device under the special directory `/shared`. This option disables this behaviour, thus not making the host filesystem accessible from inside the device. Override the default setting specified in `kathara.conf`(5) file.
 
 * `DEVICE_NAME`:
-    A list of device names. Instead of starting the whole lab, only specified devices are started.
+    A list of device names. Instead of starting the whole network scenario, only specified devices are started.
 
 m4_include(footer.txt)
 
diff --git a/docs/kathara-vconfig.1.ronn b/docs/kathara-vconfig.1.ronn
index 712d07fe..3d54987c 100644
--- a/docs/kathara-vconfig.1.ronn
+++ b/docs/kathara-vconfig.1.ronn
@@ -4,11 +4,11 @@ kathara-vconfig(1) -- Attach network interfaces to a running Kathara device
 
 ## SYNOPSIS
 
-`kathara vconfig` [`-h`] `-n` <DEVICE_NAME> `--eth` <CD> [<CD> ...]  
+`kathara vconfig` [`-h`] `-n` <DEVICE_NAME> (`--add` <CD> [<CD> ...] \| `--rm` <CD> [<CD> ...])  
 
 ## DESCRIPTION
 
-Attach network interfaces to running devices "on the fly". The affected device is identified by <DEVICE_NAME>, which is the name of a running device. 
+Manage the network interfaces of a running Kathara device. The affected device is identified by <DEVICE_NAME>, which is the name of the running device. 
 
 ## OPTIONS
 
@@ -16,19 +16,28 @@ Attach network interfaces to running devices "on the fly". The affected device i
     Show a help message and exit.
 
 * `-n` <DEVICE_NAME>, `--name` <DEVICE_NAME>:
-    Name of the device to be connected on desired collision domains.
+    Name of the device to manage.
 
-* `--eth` <CD> [<CD> ...]:
-	Specify the collision domain for an interface.
+* `--add` <CD> [<CD> ...]:
+	Specify the collision domain to add.
 
-    Equip the device with an additional network interface attached to a (virtual) collision domain whose name is <CD>. The number of the resulting network interface is generated incrementing the number of the last network interface of the device.
+    Equip the device with an additional network interface attached to a (virtual) collision domain whose name is <CD>. The number of the resulting network interface is generated incrementing the number of the last network interface used by the device.
+
+* `--rm` <CD> [<CD> ...]:
+	Specify the collision domain to remove.
+
+    Disconnect the device from the collision domain whose name is <CD> and remove the corresponding interface.
 
 ## EXAMPLES
 
-    kathara vconfig -n pc1 --eth X Y
+    kathara vconfig -n pc1 --add X Y
 
 Two new interfaces will be added to the device pc1: the first one will be attached to the collision domain named X, while the other one to the collision domain named Y. Both the interfaces will have to be configured by hand inside the device (for example, by using ifconfig).
 
+    kathara vconfig -n pc1 --rm X
+
+pc1 will be disconnected from the collision domain named X and the corresponding network interface will be removed.
+
 m4_include(footer.txt)
 
 ## SEE ALSO
diff --git a/docs/kathara.1.ronn b/docs/kathara.1.ronn
index e84882d0..5ee0497f 100644
--- a/docs/kathara.1.ronn
+++ b/docs/kathara.1.ronn
@@ -8,20 +8,18 @@ kathara(1) -- A network emulation tool
 
 ## DESCRIPTION
 
-Kathara is a network emulation tool.
+Kathara is a network emulation tool based on Docker containers. The tool can be really helpful in showing interactive demos/lessons, testing production networks in a sandbox environment, or developing new network protocols.
 
-It is an implementation of the notorious [Netkit](https://github.com/maxonthegit/netkit-core) using Python.  
-Ten times faster than Netkit and more than 100 times lighter, the framework has the performances to run in production.
+Kathara is the spiritual successor of the notorious [Netkit](https://github.com/maxonthegit/netkit-core), hence it is cross-compatible, and inherits its language and features.
 
 Within the Kathara environment each network device, called device, is implemented by a container, and each interconnection link is emulated by using a virtual network.
 
 Each device can be configured to have an arbitrary number of (virtual) network interfaces.
 
 By default, devices use a Docker Image which includes network oriented software such as routing daemons (RIP, OSPF, etc.), an HTTP server, firewalling utilities (`iptables`(8)), and diagnostic tools (`ping`(1), `traceroute`(1), `tcpdump`(1), etc.).  
-By configuring the appropriate software, it is possible to emulate with a device a specific network device (e.g., a router).  
-Other applications (such as SDN and P4) can be added using custom Docker Images.
+By configuring the appropriate software, it is possible to faithfully emulate a specific network device (e.g., a router).  
 
-Kathara provides two alternative interfaces to start and configure devices. A set of `v`-prefixed commands (vstart, vclean, vconfig), that allow to start and manage single devices while providing finegrained control on their configuration; and a set of `l`-prefixed commands (lstart, lclean, linfo, lrestart, ltest), that ease setting up preconfigured network laboratories consisting of several devices.
+Kathara provides two alternative interfaces to start and configure devices. A set of `v`-prefixed commands (vstart, vclean, vconfig), that allow to start and manage single devices while providing finegrained control on their configuration; and a set of `l`-prefixed commands (lstart, lclean, linfo, lrestart, lconfig, ltest), that ease setting up preconfigured network scenarios consisting of several devices.
 
 Kathara also provides a set of global commands (connect, info, wipe, settings, check).
 
@@ -36,25 +34,25 @@ The possible kathara commands are:
     Stop a single Kathara device
 
 * `kathara-vconfig`(1):
-    Attach network interfaces to a running Kathara device
+    Manage the network interfaces of a running Kathara device
 
 * `kathara-lstart`(1):
-    Start a Kathara lab
+    Start a Kathara network scenario
 
 * `kathara-lclean`(1):
-    Stop a Kathara lab
+    Stop a Kathara network scenario
 
 * `kathara-linfo`(1):
-    Show information about a Kathara lab
+    Show information about a Kathara network scenario
 
 * `kathara-lrestart`(1):
-    Restart a Kathara lab
+    Restart a Kathara network scenario
 
 * `kathara-ltest`(1):
-    Test a Kathara lab
+    Test a Kathara network scenario
 
 * `kathara-lconfig`(1):
-    Attach network interfaces to a running Kathara device in a Kathara lab
+    Manage the network interfaces of a running Kathara device in a network scenario
 
 * `kathara-connect`(1):
     Connect to a Kathara device
diff --git a/docs/kathara.conf.5.ronn b/docs/kathara.conf.5.ronn
index 7b17995e..a2665b5d 100644
--- a/docs/kathara.conf.5.ronn
+++ b/docs/kathara.conf.5.ronn
@@ -16,7 +16,7 @@ Checks on the correctness of the configuration are performed each time a Kathara
     Default to `kathara/quagga`.
 
 * `manager_type` (string):
-    Manager used to launch the lab.
+    Manager used to launch the network scenario.
 
     Default to `docker`.
 
@@ -79,7 +79,7 @@ Each Manager specifies additional parameters which are used only when the Manage
 	Default to `false`.
 
 * `shared_mount` (boolean):
-	This parameter specifies if the `/shared` dir will be mounted inside the device. The "shared" folder inside the lab folder is made available for reading/writing inside the device under the special directory `/shared`.
+	This parameter specifies if the `/shared` dir will be mounted inside the device. The "shared" folder inside the network scenario folder is made available for reading/writing inside the device under the special directory `/shared`.
 
 	Default to `true`.
 
diff --git a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template
index 31f0aaa1..b553e81f 100644
--- a/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template
+++ b/scripts/Linux-Deb/Docker-Linux-Build/Dockerfile_template
@@ -27,12 +27,11 @@ RUN apt update && \
 		
 RUN add-apt-repository ppa:deadsnakes/ppa \
 	&& DEBIAN_FRONTEND=noninteractive apt install -y \
-		python3.9 \
-		python3.9-dev \
-		python3.9-venv \
+		python3.10 \
+		python3.10-dev \
+		python3.10-venv \
 		python3-pip \
-		python3.9-distutils \
-	&& python3.9 -m pip install --upgrade pip
+		python3.10-distutils
 
 __NOKOGIRI__
 RUN gem install ronn-ng
diff --git a/scripts/Linux-Deb/Makefile b/scripts/Linux-Deb/Makefile
index 1e5373f7..3fa4f281 100644
--- a/scripts/Linux-Deb/Makefile
+++ b/scripts/Linux-Deb/Makefile
@@ -1,13 +1,13 @@
 #!/usr/bin/make -f
 
-VERSION=3.5.0
+VERSION=3.5.1
 DEBIAN_PACKAGE_VERSION=1
 LAUNCHPAD_NAME=user
 NO_BINARY_PACKAGES=pyroute2|pyuv|deepdiff
 
 .PHONY: allSigned docker-build-image prepare-deb-source unpack-deb-source clean-output-folder copy-debian-folder download-pip build-man build-deb-unsigned build-deb-signed ppa clean venv autocompletion
 
-allSigned: clean prepare-deb-source docker-signed_bionic docker-signed_focal docker-signed_impish docker-signed_jammy
+allSigned: clean prepare-deb-source docker-signed_bionic docker-signed_focal docker-signed_jammy
 
 docker-unsigned_%: clean prepare-deb-source docker-build-image_%
 	docker run -ti --rm -v `pwd`/../../:/opt/kathara kathara/linux-build-deb:$* /bin/bash -c "make build-deb-unsigned distro=$*"
@@ -49,22 +49,22 @@ copy-debian-folder:
 	sed -i -e 's/__VERSION__/$(VERSION)/g' Output/kathara-$(VERSION)/debian/changelog
 	
 venv:
-	python3.9 -m venv /root/3.9-env
+	python3.10 -m venv /root/3.10-env
 
 download-pip: copy-debian-folder
 	mkdir Output/kathara-$(VERSION)/debian/pythonLibs_amd64
 	mkdir Output/kathara-$(VERSION)/debian/pythonLibs_arm64
 	grep -Ev "$(NO_BINARY_PACKAGES)" Output/kathara-$(VERSION)/requirements.txt > Output/kathara-$(VERSION)/requirements-binary.txt
 	grep -E "$(NO_BINARY_PACKAGES)" Output/kathara-$(VERSION)/requirements.txt > Output/kathara-$(VERSION)/requirements-no-binary.txt
-	/root/3.9-env/bin/pip3.9 install --upgrade setuptools
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.9-env/bin/pip3.9 download flit_core
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.9-env/bin/pip3.9 download --no-binary=:all: -r ../../requirements-no-binary.txt
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.9-env/bin/pip3.9 download --platform manylinux2014_x86_64 --platform x86_64 --only-binary=:all: -r ../../requirements-binary.txt
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.9-env/bin/pip3.9 download nuitka
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.9-env/bin/pip3.9 download flit_core
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.9-env/bin/pip3.9 download --no-binary=:all: -r ../../requirements-no-binary.txt
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.9-env/bin/pip3.9 download --platform manylinux2014_aarch64 --platform arm64 --only-binary=:all: -r ../../requirements-binary.txt
-	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.9-env/bin/pip3.9 download nuitka
+	/root/3.10-env/bin/pip3.10 install --upgrade setuptools
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.10-env/bin/pip3.10 download flit_core
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.10-env/bin/pip3.10 download --no-binary=:all: -r ../../requirements-no-binary.txt
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.10-env/bin/pip3.10 download --platform manylinux2014_x86_64 --platform x86_64 --only-binary=:all: -r ../../requirements-binary.txt
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_amd64 && /root/3.10-env/bin/pip3.10 download nuitka
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.10-env/bin/pip3.10 download flit_core
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.10-env/bin/pip3.10 download --no-binary=:all: -r ../../requirements-no-binary.txt
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.10-env/bin/pip3.10 download --platform manylinux2014_aarch64 --platform arm64 --only-binary=:all: -r ../../requirements-binary.txt
+	cd Output/kathara-$(VERSION)/debian/pythonLibs_arm64 && /root/3.10-env/bin/pip3.10 download nuitka
 	rm -Rf Output/kathara-$(VERSION)/requirements-binary.txt Output/kathara-$(VERSION)/requirements-no-binary.txt
 	ls Output/kathara-$(VERSION)/debian/pythonLibs_amd64/ | awk '{print "debian/pythonLibs_amd64/"$$0}' > Output/kathara-$(VERSION)/debian/source/include-binaries
 	ls Output/kathara-$(VERSION)/debian/pythonLibs_arm64/ | awk '{print "debian/pythonLibs_arm64/"$$0}' >> Output/kathara-$(VERSION)/debian/source/include-binaries
@@ -76,8 +76,8 @@ build-man: copy-debian-folder
 	ls Output/kathara-$(VERSION)/debian/manpages/ | awk '{print "debian/manpages/"$$0}' > Output/kathara-$(VERSION)/debian/kathara.manpages
 
 autocompletion:
-	/root/3.9-env/bin/pip3.9 install -r ../autocompletion/requirements.txt
-	/root/3.9-env/bin/python3.9 ../autocompletion/generate_autocompletion.py Output/kathara-$(VERSION)/debian/kathara.bash-completion
+	/root/3.10-env/bin/pip3.10 install -r ../autocompletion/requirements.txt
+	/root/3.10-env/bin/python3.10 ../autocompletion/generate_autocompletion.py Output/kathara-$(VERSION)/debian/kathara.bash-completion
 
 build-deb-unsigned: venv build-man autocompletion download-pip
 	sed -i -e 's/__UBUNTU_VERSION__/$(distro)/g' Output/kathara-$(VERSION)/debian/changelog
diff --git a/scripts/Linux-Deb/debian/changelog b/scripts/Linux-Deb/debian/changelog
index 52db8231..059876d4 100644
--- a/scripts/Linux-Deb/debian/changelog
+++ b/scripts/Linux-Deb/debian/changelog
@@ -1,9 +1,12 @@
 kathara (__VERSION__-__DEBIAN_PACKAGE_VERSION____UBUNTU_VERSION__) __UBUNTU_VERSION__; urgency=low
 
-  * Fix lab.dep parser
-  * Refactor of Managers and add more expressive Python APIs
-  * Decouple UI Logic from Business Logic
-  * Add Settings option to select the Docker Image Update Policy
-  * Add possibility to declare container environment variables from vstart and lab.conf
+  * It is now possible to restart only specific devices using 'lrestart'
+  * Add possibility to disconnect a device from a collision domain using vconfig and lconfig commands (see man for further information)
+  * Add check to ensure that required Docker images are compatible with the host architecture
+  * More generic regular expression for device 'env' meta
+  * Network scenario hash is now computed from LAB_NAME meta, if present
+  * Improve exceptions handling and better error descriptions
+  * Add several utility methods to the 'Kathara' facade and model classes
+  * Change pyuv dependency with the Python 3.10 compatible version
 
  -- Mariano Scazzariello <******@gmail.com>  __DATE__
diff --git a/scripts/Linux-Deb/debian/control b/scripts/Linux-Deb/debian/control
index c725be9f..63ee2ab6 100644
--- a/scripts/Linux-Deb/debian/control
+++ b/scripts/Linux-Deb/debian/control
@@ -4,11 +4,11 @@ Section: misc
 Priority: optional
 Standards-Version: 3.9.8
 Build-Depends: debhelper (>= 10),
-	python3.9 (>= 3.9.0),
+	python3.10 (>= 3.10.0),
 	python3-pip (>= 9.0.0),
-	python3.9-dev (>= 3.9.0),
-	python3.9-venv (>= 3.9.0),
-	python3.9-distutils (>= 3.9.0) | python3-distutils (>= 3.8.0),
+	python3.10-dev (>= 3.10.0),
+	python3.10-venv (>= 3.10.0),
+	python3.10-distutils (>= 3.10.0) | python3-distutils (>= 3.8.0),
 	patchelf,
 	bash-completion,
 	zlib1g-dev
diff --git a/scripts/Linux-Deb/debian/rules b/scripts/Linux-Deb/debian/rules
index 2474a460..10a76c58 100755
--- a/scripts/Linux-Deb/debian/rules
+++ b/scripts/Linux-Deb/debian/rules
@@ -11,11 +11,11 @@ override_dh_auto_clean:
 	find . | grep -E "(__pycache__|\.pyc|\.pyo)$$" | xargs rm -rf
 
 override_dh_auto_configure:
-	python3.9 -m venv $(CURDIR)/venv
-	$(CURDIR)/venv/bin/pip3.9 install --no-index --find-links $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/ $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/*
+	python3.10 -m venv $(CURDIR)/venv
+	$(CURDIR)/venv/bin/pip3.10 install --no-index --find-links $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/ $(CURDIR)/debian/pythonLibs_$(ARCHITECTURE)/*
 
 override_dh_auto_build:
-	$(CURDIR)/venv/bin/python3.9 -m nuitka --lto=no --show-progress --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
+	$(CURDIR)/venv/bin/python3.10 -m nuitka --lto=no --show-progress --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
 
 override_dh_auto_install:
 	rm -rf $(CURDIR)/debian/kathara/usr/lib/kathara/*
diff --git a/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile b/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile
index e04d7c1c..7b229f60 100644
--- a/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile
+++ b/scripts/Linux-Pkg/Docker-Linux-Build/Dockerfile
@@ -19,15 +19,14 @@ RUN pacman -S --noconfirm wget \
 						  libffi \
 						  libnsl \
 						  libxcrypt \
-						  openssl \
 						  zlib && \
-	wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz && \
-	tar xfvz Python-3.9.9.tgz && cd Python-3.9.9 && \
+	wget https://www.python.org/ftp/python/3.10.7/Python-3.10.7.tgz && \
+	tar xfvz Python-3.10.7.tgz && cd Python-3.10.7 && \
 	./configure --enable-optimizations && \
 	make -j8 && \
 	make install && \
 	pacman -R --noconfirm wget && \
-    cd .. && rm -Rf Python-3.9.9*
+    cd .. && rm -Rf Python-3.10.7*
                           
 RUN useradd -m -s /bin/bash builduser
 
diff --git a/scripts/Linux-Pkg/Makefile b/scripts/Linux-Pkg/Makefile
index a3683243..0d452fe0 100644
--- a/scripts/Linux-Pkg/Makefile
+++ b/scripts/Linux-Pkg/Makefile
@@ -1,7 +1,7 @@
 #!/usr/bin/make -f
 
-VERSION=3.5.0
-PACKAGE_VERSION=1
+VERSION=3.5.1
+PACKAGE_VERSION=2
 AUR_NAME=user
 AUR_MAIL=your@mail.com
 
diff --git a/scripts/Linux-Pkg/pkginfo/PKGBUILD b/scripts/Linux-Pkg/pkginfo/PKGBUILD
index 37b19fdb..7033134a 100644
--- a/scripts/Linux-Pkg/pkginfo/PKGBUILD
+++ b/scripts/Linux-Pkg/pkginfo/PKGBUILD
@@ -7,25 +7,28 @@ url="https://www.kathara.org/"
 license=('GPL3')
 install="kathara.install"
 changelog="kathara.changelog"
-makedepends=('chrpath')
+makedepends=(
+    'chrpath'
+    'patchelf'
+)
 optdepends=(
-  'docker: for running network scenarios in a local environment'
-  'xterm: for opening devices terminals'
+    'docker: for running network scenarios in a local environment'
+    'xterm: for opening devices terminals'
 )
 source=("kathara.tar.gz")
 md5sums=('SKIP')
 
 prepare() {
   python3 -m venv $srcdir/venv
-  $srcdir/venv/bin/pip3.9 install --upgrade setuptools
-  $srcdir/venv/bin/pip3.9 install -r src/requirements.txt
-  $srcdir/venv/bin/pip3.9 install nuitka
-  $srcdir/venv/bin/pip3.9 install pytest
+  $srcdir/venv/bin/pip3.10 install --upgrade setuptools
+  $srcdir/venv/bin/pip3.10 install -r src/requirements.txt
+  $srcdir/venv/bin/pip3.10 install nuitka
+  $srcdir/venv/bin/pip3.10 install pytest
 }
 
 build() {
-  $srcdir/venv/bin/python3.9 -m pytest
-  cd src && $srcdir/venv/bin/python3.9 -m nuitka --lto=no --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
+  $srcdir/venv/bin/python3.10 -m pytest
+  cd src && $srcdir/venv/bin/python3.10 -m nuitka --lto=no --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
 }
 
 package() {
diff --git a/scripts/Linux-Pkg/pkginfo/kathara.changelog b/scripts/Linux-Pkg/pkginfo/kathara.changelog
index f7ba9866..9aa76ad3 100644
--- a/scripts/Linux-Pkg/pkginfo/kathara.changelog
+++ b/scripts/Linux-Pkg/pkginfo/kathara.changelog
@@ -1,8 +1,11 @@
 __DATE__ Kathara Team <******@kathara.org>
 
 * Release v__VERSION__
-    * Fix lab.dep parser
-    * Refactor of Managers and add more expressive Python APIs
-    * Decouple UI Logic from Business Logic
-    * Add Settings option to select the Docker Image Update Policy
-    * Add possibility to declare container environment variables from vstart and lab.conf
\ No newline at end of file
+    * It is now possible to restart only specific devices using 'lrestart'
+    * Add possibility to disconnect a device from a collision domain using vconfig and lconfig commands (see man for further information)
+    * Add check to ensure that required Docker images are compatible with the host architecture
+    * More generic regular expression for device 'env' meta
+    * Network scenario hash is now computed from LAB_NAME meta, if present
+    * Improve exceptions handling and better error descriptions
+    * Add several utility methods to the 'Kathara' facade and model classes
+    * Change pyuv dependency with the Python 3.10 compatible version
\ No newline at end of file
diff --git a/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile b/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile
index 8e4d05ab..01742cb2 100644
--- a/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile
+++ b/scripts/Linux-Rpm/Docker-Linux-Build/Dockerfile
@@ -20,13 +20,13 @@ RUN dnf install -y wget \
     ncurses-devel \
     sqlite-devel \
     libffi-devel && \
- wget https://www.python.org/ftp/python/3.9.9/Python-3.9.9.tgz && \
- tar xfvz Python-3.9.9.tgz && cd Python-3.9.9 && \
+ wget https://www.python.org/ftp/python/3.10.7/Python-3.10.7.tgz && \
+ tar xfvz Python-3.10.7.tgz && cd Python-3.10.7 && \
  ./configure --enable-optimizations && \
  make -j8 && \
  make install && \
  dnf remove -y wget && \
- cd .. && rm -Rf Python3.9.9*
+ cd .. && rm -Rf Python3.10.7*
 
 RUN gem install ronn-ng
 
diff --git a/scripts/Linux-Rpm/Makefile b/scripts/Linux-Rpm/Makefile
index ae577095..d1da611f 100644
--- a/scripts/Linux-Rpm/Makefile
+++ b/scripts/Linux-Rpm/Makefile
@@ -1,6 +1,6 @@
 #!/usr/bin/make -f
 
-VERSION=3.5.0
+VERSION=3.5.1
 PACKAGE_VERSION=1
 
 .PHONY: all clean docker-build-image prepare-source prepare-man-pages prepare-bash-completion pack-source build
diff --git a/scripts/Linux-Rpm/rpm/kathara.spec b/scripts/Linux-Rpm/rpm/kathara.spec
index 3db91f45..2141fd14 100644
--- a/scripts/Linux-Rpm/rpm/kathara.spec
+++ b/scripts/Linux-Rpm/rpm/kathara.spec
@@ -18,15 +18,15 @@ new network protocols.
 
 %prep
 %autosetup
-python3.9 -m venv %{_builddir}/venv
-%{_builddir}/venv/bin/pip3.9 install --upgrade pip
-%{_builddir}/venv/bin/pip3.9 install -r src/requirements.txt
-%{_builddir}/venv/bin/pip3.9 install nuitka
-%{_builddir}/venv/bin/pip3.9 install pytest
+python3.10 -m venv %{_builddir}/venv
+%{_builddir}/venv/bin/pip3.10 install --upgrade pip
+%{_builddir}/venv/bin/pip3.10 install -r src/requirements.txt
+%{_builddir}/venv/bin/pip3.10 install nuitka
+%{_builddir}/venv/bin/pip3.10 install pytest
 
 %build
-%{_builddir}/venv/bin/python3.9 -m pytest
-cd src && %{_builddir}/venv/bin/python3.9 -m nuitka --lto=no --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
+%{_builddir}/venv/bin/python3.10 -m pytest
+cd src && %{_builddir}/venv/bin/python3.10 -m nuitka --lto=no --plugin-enable=pylint-warnings --plugin-enable=multiprocessing --follow-imports --standalone --include-plugin-directory=Kathara kathara.py
 
 %install
 mv %{_builddir}/%{buildsubdir}/src/kathara.dist %{_builddir}/%{buildsubdir}/kathara.dist
@@ -68,8 +68,11 @@ chmod g+s %{_libdir}/kathara/kathara
 
 %changelog
 *  __DATE__ Kathara Team <******@kathara.org> - __VERSION__-__PACKAGE_VERSION__
-- Fix lab.dep parser
-- Refactor of Managers and add more expressive Python APIs
-- Decouple UI Logic from Business Logic
-- Add Settings option to select the Docker Image Update Policy
-- Add possibility to declare container environment variables from vstart and lab.conf
\ No newline at end of file
+- It is now possible to restart only specific devices using 'lrestart'
+- Add possibility to disconnect a device from a collision domain using vconfig and lconfig commands (see man for further information)
+- Add check to ensure that required Docker images are compatible with the host architecture
+- More generic regular expression for device 'env' meta
+- Network scenario hash is now computed from LAB_NAME meta, if present
+- Improve exceptions handling and better error descriptions
+- Add several utility methods to the 'Kathara' facade and model classes
+- Change pyuv dependency with the Python 3.10 compatible version
\ No newline at end of file
diff --git a/scripts/OSX/Makefile b/scripts/OSX/Makefile
index 3bcfc980..03838568 100644
--- a/scripts/OSX/Makefile
+++ b/scripts/OSX/Makefile
@@ -1,7 +1,7 @@
 #!/usr/bin/make -s
 
 PRODUCT=Kathara
-VERSION=3.5.0
+VERSION=3.5.1
 TARGET_DIRECTORY=Output
 APPLE_DEVELOPER_CERTIFICATE_ID=FakeID
 ROFF_DIR=../../docs/Roff
@@ -21,9 +21,9 @@ py_env:
 	python3 -m venv $(VENV_DIR)
 
 deps: py_env
-	$(VENV_DIR)/bin/pip3.9 install pyinstaller
-	$(VENV_DIR)/bin/pip3.9 install -r ../../src/requirements.txt
-	$(VENV_DIR)/bin/pip3.9 install pytest
+	$(VENV_DIR)/bin/pip3.10 install pyinstaller
+	$(VENV_DIR)/bin/pip3.10 install -r ../../src/requirements.txt
+	$(VENV_DIR)/bin/pip3.10 install pytest
 	gem install ronn-ng --user-install
 
 manpages:
diff --git a/scripts/OSX/README.md b/scripts/OSX/README.md
index 3555efff..4979d3d4 100644
--- a/scripts/OSX/README.md
+++ b/scripts/OSX/README.md
@@ -3,6 +3,7 @@
 1. Install `pkgbuild`, `productbuild` and `make` from Apple developer repository
     - If you have `XCode` installed you probably already have them installed
     - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action)
+    - **NOTE:** You need Python 3.10 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3107/))
 2. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files.
 3. Run `make all_x86` or `make all_arm64` to automatically compile and create the package for the desired architecture.
 You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU.
@@ -18,6 +19,7 @@ You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a
 2. Install `pkgbuild`, `productbuild` and `make` from Apple developer repository
     - If you have `XCode` installed you probably already have them installed
     - Otherwise, download them from [here](https://developer.apple.com/devcenter/mac/index.action)
+    - **NOTE:** You need Python 3.10 in order to compile the binary ([Quick link](https://www.python.org/downloads/release/python-3107/))
 3. Change the Kathara version number in both `src/Kathara/version.py` and `Makefile` files.
 4. Run `make allSigned_x86` or `make allSigned_arm64` to automatically compile and create the package for the desired architecture. 
 You can compile the `x86` package on a Mac with Intel CPU, and the `arm64` on a Mac with Apple CPU.
diff --git a/scripts/Windows/README.md b/scripts/Windows/README.md
index 367ddd06..e5520fbe 100644
--- a/scripts/Windows/README.md
+++ b/scripts/Windows/README.md
@@ -1,7 +1,8 @@
 # Compiling Kathara for Windows
 
-1. Download and Install Inno Setup [Quick link](http://www.jrsoftware.org/download.php/is.exe)
-2. Install pyinstaller `python -m pip install pyinstaller` (from an Administrator cmd or Powershell)
+1. Download and Install Inno Setup ([Quick link](http://www.jrsoftware.org/download.php/is.exe))
+2. Download and Install Python 3.10 ([Quick link](https://www.python.org/downloads/release/python-3107/))
+3. Install pyinstaller `python -m pip install pyinstaller` (from an Administrator cmd or Powershell)
 3. Install Kathara python dependencies `python -m pip install -r ..\..\src\requirements.txt` (from an Administrator cmd or Powershell)
 4. Change the Kathara version number in both `src/Kathara/version.py` and `installer.iss` files.
 5. Create binary package running `WindowsBuild.bat`
diff --git a/scripts/Windows/WindowsBuild.bat b/scripts/Windows/WindowsBuild.bat
index 705b7014..c87d215f 100644
--- a/scripts/Windows/WindowsBuild.bat
+++ b/scripts/Windows/WindowsBuild.bat
@@ -1,14 +1,12 @@
-@ECHO OFF
-
 set VENV_DIR=%cd%\venv
 
-python -m venv %VENV_DIR%
+python3.10 -m venv %VENV_DIR%
 CALL %VENV_DIR%\Scripts\activate
 
-pip install win_inet_pton
-pip install pyinstaller
-pip install -r ..\..\src\requirements.txt
-pip install pytest
+pip3.10 install win_inet_pton
+pip3.10 install pyinstaller
+pip3.10 install -r ..\..\src\requirements.txt
+pip3.10 install pytest
 if %errorlevel% neq 0 exit /b %errorlevel%
 
 copy Assets\kathara.spec ..\..\src\
@@ -22,8 +20,8 @@ cd src\
 
 pyinstaller --distpath=./kathara.dist --workpath=./kathara.build kathara.spec
 
-python -c "for p in __import__('pathlib').Path('.').rglob('*.py[co]'): p.unlink()"
-python -c "for p in __import__('pathlib').Path('.').rglob('__pycache__'): p.rmdir()"
+python3.10 -c "for p in __import__('pathlib').Path('.').rglob('*.py[co]'): p.unlink()"
+python3.10 -c "for p in __import__('pathlib').Path('.').rglob('__pycache__'): p.rmdir()"
 
 del kathara.spec
 del app_icon.ico
diff --git a/scripts/Windows/installer.iss b/scripts/Windows/installer.iss
index 8a0213a0..e3aead7c 100644
--- a/scripts/Windows/installer.iss
+++ b/scripts/Windows/installer.iss
@@ -2,7 +2,7 @@
 ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
 
 #define MyAppName "Kathara"
-#define MyAppVersion "3.5.0"
+#define MyAppVersion "3.5.1"
 #define MyAppPublisher "Kathara Team"
 #define MyAppURL "https://www.kathara.org"
 #define MyAppExeName "kathara.exe"
diff --git a/scripts/pydoc/Makefile b/scripts/pydoc/Makefile
index 606ba52c..5103d3fb 100644
--- a/scripts/pydoc/Makefile
+++ b/scripts/pydoc/Makefile
@@ -7,18 +7,18 @@ all: clean doc verify-doc
 doc: clean verify-doc
 	python3 generate_doc.py
 	cd docs && rm Kathara.model.md Kathara.setting.md Kathara.manager.md
-	sed -i "" '/manager.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/setting.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/docker.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/kubernetes.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/KubernetesSettingsAddon.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/DockerSettingsAddon.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/model.md/d' ./docs/Kathara-API-Docs.md
-	sed -i "" 's/\.\///g' ./docs/Kathara-API-Docs.md
-	sed -i "" 's/\.md//g' ./docs/Kathara-API-Docs.md
-	sed -i "" '/Modules/,/Classes/d' ./docs/Kathara-API-Docs.md
-	sed -i "" '/Functions/q' ./docs/Kathara-API-Docs.md
-	sed -i "" '/Functions/d' ./docs/Kathara-API-Docs.md
+	sed '/manager.md/d' ./docs/Kathara-API-Docs.md
+	sed '/setting.md/d' ./docs/Kathara-API-Docs.md
+	sed '/docker.md/d' ./docs/Kathara-API-Docs.md
+	sed '/kubernetes.md/d' ./docs/Kathara-API-Docs.md
+	sed '/KubernetesSettingsAddon.md/d' ./docs/Kathara-API-Docs.md
+	sed '/DockerSettingsAddon.md/d' ./docs/Kathara-API-Docs.md
+	sed '/model.md/d' ./docs/Kathara-API-Docs.md
+	sed 's/\.\///g' ./docs/Kathara-API-Docs.md
+	sed 's/\.md//g' ./docs/Kathara-API-Docs.md
+	sed '/Modules/,/Classes/d' ./docs/Kathara-API-Docs.md
+	sed '/Functions/q' ./docs/Kathara-API-Docs.md
+	sed '/Functions/d' ./docs/Kathara-API-Docs.md
 
 verify-doc:
 	python3 -m pip install lazydocs
diff --git a/setup.cfg b/setup.cfg
index 67f4a495..b7f02db6 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = kathara
-version = 3.5.0
+version = 3.5.1
 author = Kathara Framework
 author_email = contact@kathara.org
 description = A lightweight container based emulation system
@@ -14,4 +14,5 @@ classifiers =
     Intended Audience :: Developers
     Topic :: Software Development :: Build Tools
     License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
-    Programming Language :: Python :: 3.9
\ No newline at end of file
+    Programming Language :: Python :: 3.9
+    Programming Language :: Python :: 3.10
\ No newline at end of file
diff --git a/setup.py b/setup.py
index ac78d097..8a1a1a1e 100644
--- a/setup.py
+++ b/setup.py
@@ -7,13 +7,13 @@
     package_dir={'': 'src'},
     packages=find_packages('src'),
     py_modules=['kathara'],
-    version='3.5.0',
+    version='3.5.1',
     license='gpl-3.0',
     description='A lightweight container based emulation system.',
     author='Kathara Framework',
     author_email='contact@kathara.org',
     url='https://www.kathara.org',
-    download_url='https://github.com/KatharaFramework/Kathara/archive/refs/tags/3.5.0.tar.gz',
+    download_url='https://github.com/KatharaFramework/Kathara/archive/refs/tags/3.5.1.tar.gz',
     keywords=['NETWORK-EMULATION', 'CONTAINERS', 'NFV'],
     install_requires=[
         "binaryornot>=0.4.4",
@@ -27,7 +27,7 @@
         "pyroute2>=0.5.19",
         "progressbar2>=1.14.0",
         "libtmux>=0.8.2; platform_system == 'darwin' or platform_system == 'linux'",
-        "pyuv>=1.4.0",
+        "git+https://github.com/saghul/pyuv@master#egg=pyuv",
         "appscript>=1.1.0; platform_system == 'darwin'",
         "pypiwin32>=223; platform_system == 'win32'",
         "windows-curses>=2.1.0; platform_system == 'win32'"
@@ -38,5 +38,6 @@
         'Topic :: Software Development :: Build Tools',
         'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)',
         'Programming Language :: Python :: 3.9',
+        'Programming Language :: Python :: 3.10',
     ],
 )
diff --git a/src/Kathara/auth/PrivilegeHandler.py b/src/Kathara/auth/PrivilegeHandler.py
index 109ec5e4..b8b0353c 100644
--- a/src/Kathara/auth/PrivilegeHandler.py
+++ b/src/Kathara/auth/PrivilegeHandler.py
@@ -3,6 +3,8 @@
 import logging
 import os
 
+from ..exceptions import InstantiationError
+
 
 class PrivilegeHandler(object):
     __slots__ = ['user_uid', 'user_gid', 'effective_user_uid', 'effective_user_gid']
@@ -18,7 +20,7 @@ def get_instance() -> PrivilegeHandler:
 
     def __init__(self) -> None:
         if PrivilegeHandler.__instance is not None:
-            raise Exception("This class is a singleton!")
+            raise InstantiationError("This class is a singleton!")
         else:
             try:
                 self.user_uid: int = os.getuid()
diff --git a/src/Kathara/cli/command/CheckCommand.py b/src/Kathara/cli/command/CheckCommand.py
index d4799fb8..a1fcedb6 100644
--- a/src/Kathara/cli/command/CheckCommand.py
+++ b/src/Kathara/cli/command/CheckCommand.py
@@ -59,7 +59,7 @@ def linux_platform_info():
         args['no_shared'] = False
         args['no_hosthome'] = False
 
-        lab = Lab("kathara_vlab")
+        lab = Lab("kathara_test")
         lab.get_or_new_machine("hello_world")
 
         try:
diff --git a/src/Kathara/cli/command/ConnectCommand.py b/src/Kathara/cli/command/ConnectCommand.py
index 5d39b4d9..b7b6d032 100644
--- a/src/Kathara/cli/command/ConnectCommand.py
+++ b/src/Kathara/cli/command/ConnectCommand.py
@@ -5,6 +5,8 @@
 from ... import utils
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
+from ...model.Lab import Lab
+from ...parser.netkit.LabParser import LabParser
 from ...strings import strings, wiki_description
 
 
@@ -30,7 +32,7 @@ def __init__(self) -> None:
 
         group.add_argument(
             '-d', '--directory',
-            help='Specify the folder containing the lab.',
+            help='Specify the folder containing the network scenario.',
         )
         group.add_argument(
             '-v', '--vmachine',
@@ -59,14 +61,16 @@ def run(self, current_path: str, argv: List[str]) -> None:
         args = self.get_args()
 
         if args['vmachine']:
-            lab_path = "kathara_vlab"
+            lab = Lab("kathara_vlab")
         else:
             lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path
             lab_path = utils.get_absolute_path(lab_path)
+            try:
+                lab = LabParser.parse(lab_path)
+            except (Exception, IOError):
+                lab = Lab(None, path=lab_path)
 
-        logging.debug("Executing `connect` command in path `%s`..." % lab_path)
+        logging.debug("Executing `connect` command with hash `%s`..." % lab.hash)
 
-        lab_hash = utils.generate_urlsafe_hash(lab_path)
-
-        Kathara.get_instance().connect_tty(machine_name=args['machine_name'], lab_hash=lab_hash, shell=args['shell'],
+        Kathara.get_instance().connect_tty(machine_name=args['machine_name'], lab_hash=lab.hash, shell=args['shell'],
                                            logs=args['logs'])
diff --git a/src/Kathara/cli/command/ExecCommand.py b/src/Kathara/cli/command/ExecCommand.py
index 354b0041..3f6dff1f 100644
--- a/src/Kathara/cli/command/ExecCommand.py
+++ b/src/Kathara/cli/command/ExecCommand.py
@@ -5,6 +5,8 @@
 from ... import utils
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
+from ...model.Lab import Lab
+from ...parser.netkit.LabParser import LabParser
 from ...strings import strings, wiki_description
 
 
@@ -30,7 +32,7 @@ def __init__(self) -> None:
 
         group.add_argument(
             '-d', '--directory',
-            help='Specify the folder containing the lab.',
+            help='Specify the folder containing the network scenario.',
         )
         group.add_argument(
             '-v', '--vmachine',
@@ -67,13 +69,16 @@ def run(self, current_path: str, argv: List[str]) -> None:
         args = self.get_args()
 
         if args['vmachine']:
-            lab_path = "kathara_vlab"
+            lab = Lab("kathara_vlab")
         else:
             lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path
             lab_path = utils.get_absolute_path(lab_path)
+            try:
+                lab = LabParser.parse(lab_path)
+            except (Exception, IOError):
+                lab = Lab(None, path=lab_path)
 
-        lab_hash = utils.generate_urlsafe_hash(lab_path)
-        exec_output = Kathara.get_instance().exec(args['machine_name'], args['command'], lab_hash=lab_hash)
+        exec_output = Kathara.get_instance().exec(args['machine_name'], args['command'], lab_hash=lab.hash)
 
         try:
             while True:
diff --git a/src/Kathara/cli/command/LcleanCommand.py b/src/Kathara/cli/command/LcleanCommand.py
index 5e16bcdb..3d437302 100644
--- a/src/Kathara/cli/command/LcleanCommand.py
+++ b/src/Kathara/cli/command/LcleanCommand.py
@@ -6,6 +6,8 @@
 from ... import utils
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
+from ...model.Lab import Lab
+from ...parser.netkit.LabParser import LabParser
 from ...strings import strings, wiki_description
 
 
@@ -30,7 +32,7 @@ def __init__(self) -> None:
         self.parser.add_argument(
             '-d', '--directory',
             required=False,
-            help='Specify the folder containing the lab.'
+            help='Specify the folder containing the network scenario.'
         )
         self.parser.add_argument(
             'machine_names',
@@ -45,9 +47,11 @@ def run(self, current_path: str, argv: List[str]) -> None:
 
         lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path
         lab_path = utils.get_absolute_path(lab_path)
-
-        lab_hash = utils.generate_urlsafe_hash(lab_path)
+        try:
+            lab = LabParser.parse(lab_path)
+        except (Exception, IOError):
+            lab = Lab(None, path=lab_path)
 
         logging.info(format_headers("Stopping Network Scenario"))
 
-        Kathara.get_instance().undeploy_lab(lab_hash=lab_hash, selected_machines=set(args['machine_names']))
+        Kathara.get_instance().undeploy_lab(lab_hash=lab.hash, selected_machines=set(args['machine_names']))
diff --git a/src/Kathara/cli/command/LconfigCommand.py b/src/Kathara/cli/command/LconfigCommand.py
index 476c7ace..da2e4451 100644
--- a/src/Kathara/cli/command/LconfigCommand.py
+++ b/src/Kathara/cli/command/LconfigCommand.py
@@ -1,13 +1,13 @@
 import argparse
 import logging
-import re
-import sys
 from typing import List
 
 from ... import utils
+from ..ui.utils import alphanumeric
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
 from ...model.Lab import Lab
+from ...parser.netkit.LabParser import LabParser
 from ...strings import strings, wiki_description
 
 
@@ -32,7 +32,7 @@ def __init__(self) -> None:
             '-d', '--directory',
             metavar='LAB_PATH',
             required=False,
-            help='Path of the lab to configure, if not specified the current path is used'
+            help='Path of the network scenario to configure, if not specified the current path is used'
         )
         self.parser.add_argument(
             '-n', '--name',
@@ -40,13 +40,24 @@ def __init__(self) -> None:
             required=True,
             help='Name of the device to be connected on desired collision domains.'
         )
-        self.parser.add_argument(
-            '--eth',
-            dest='eths',
+
+        group = self.parser.add_mutually_exclusive_group(required=True)
+
+        group.add_argument(
+            '--add',
+            type=alphanumeric,
+            dest='to_add',
             metavar='CD',
             nargs='+',
-            required=True,
-            help='Specify the collision domain for an interface.'
+            help='Specify the collision domain to add.'
+        )
+        group.add_argument(
+            '--rm',
+            type=alphanumeric,
+            dest='to_remove',
+            metavar='CD',
+            nargs='+',
+            help='Specify the collision domain to remove.'
         )
 
     def run(self, current_path: str, argv: List[str]) -> None:
@@ -55,23 +66,26 @@ def run(self, current_path: str, argv: List[str]) -> None:
 
         lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path
         lab_path = utils.get_absolute_path(lab_path)
+        try:
+            lab = LabParser.parse(lab_path)
+        except (Exception, IOError):
+            lab = Lab(None, path=lab_path)
 
-        for eth in args['eths']:
-            # Only alphanumeric characters are allowed
-            matches = re.search(r"^\w+$", eth)
-
-            if not matches:
-                sys.stderr.write('Syntax error in --eth field.\n')
-                self.parser.print_help()
-                exit(1)
-
-        lab = Lab(None, path=lab_path)
+        Kathara.get_instance().update_lab_from_api(lab)
 
-        device = lab.get_or_new_machine(args['name'])
-        device.api_object = Kathara.get_instance().get_machine_api_object(args['name'], lab_hash=lab.hash)
+        machine_name = args['name']
+        device = lab.get_or_new_machine(machine_name)
 
-        for eth in args['eths']:
-            logging.info("Adding interface to device `%s` for collision domain `%s`..." % (args['name'], eth))
-            lab.connect_machine_to_link(args['name'], eth)
+        if args['to_add']:
+            for cd in args['to_add']:
+                logging.info(
+                    "Adding interface to device `%s` on collision domain `%s`..." % (machine_name, cd)
+                )
+                Kathara.get_instance().connect_machine_to_link(device, lab.get_or_new_link(cd))
 
-        Kathara.get_instance().update_lab(lab)
+        if args['to_remove']:
+            for cd in args['to_remove']:
+                logging.info(
+                    "Removing interface on collision domain `%s` from device `%s`..." % (cd, machine_name)
+                )
+                Kathara.get_instance().disconnect_machine_from_link(device, lab.get_or_new_link(cd))
diff --git a/src/Kathara/cli/command/LinfoCommand.py b/src/Kathara/cli/command/LinfoCommand.py
index 48d5146d..0d02fca8 100644
--- a/src/Kathara/cli/command/LinfoCommand.py
+++ b/src/Kathara/cli/command/LinfoCommand.py
@@ -6,6 +6,7 @@
 from ... import utils
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
+from ...model.Lab import Lab
 from ...model.Link import BRIDGE_LINK_NAME
 from ...parser.netkit.LabParser import LabParser
 from ...strings import strings, wiki_description
@@ -33,7 +34,7 @@ def __init__(self) -> None:
         self.parser.add_argument(
             '-d', '--directory',
             required=False,
-            help='Specify the folder containing the lab.'
+            help='Specify the folder containing the network scenario.'
         )
 
         group = self.parser.add_mutually_exclusive_group(required=False)
@@ -42,7 +43,7 @@ def __init__(self) -> None:
             '-l', '--live',
             required=False,
             action='store_true',
-            help='Live mode, can be used only when a lab is launched.'
+            help='Live mode, can be used only when a network scenario is launched.'
         )
 
         group.add_argument(
@@ -65,45 +66,48 @@ def run(self, current_path: str, argv: List[str]) -> None:
 
         lab_path = args['directory'].replace('"', '').replace("'", '') if args['directory'] else current_path
         lab_path = utils.get_absolute_path(lab_path)
-        lab_hash = utils.generate_urlsafe_hash(lab_path)
+        try:
+            lab = LabParser.parse(lab_path)
+        except (Exception, IOError):
+            lab = Lab(None, path=lab_path)
 
         if args['live']:
             if args['name']:
-                self._get_machine_live_info(lab_hash, args['name'])
+                self._get_machine_live_info(lab, args['name'])
             else:
-                self._get_lab_live_info(lab_hash)
+                self._get_lab_live_info(lab)
 
             return
 
         if args['conf']:
-            self._get_conf_info(lab_path, machine_name=args['name'])
+            self._get_conf_info(lab, machine_name=args['name'])
             return
 
         if args['name']:
             print(format_headers("Device Information"))
-            print(str(next(Kathara.get_instance().get_machine_stats(args['name'], lab_hash))))
+            print(str(next(Kathara.get_instance().get_machine_stats(args['name'], lab.hash))))
             print(format_headers())
         else:
-            machines_stats = Kathara.get_instance().get_machines_stats(lab_hash)
+            machines_stats = Kathara.get_instance().get_machines_stats(lab.hash)
             print(next(create_table(machines_stats)))
 
     @staticmethod
-    def _get_machine_live_info(lab_hash: str, machine_name: str) -> None:
+    def _get_machine_live_info(lab: Lab, machine_name: str) -> None:
         Curses.get_instance().init_window()
 
         try:
             while True:
                 Curses.get_instance().print_string(
                     format_headers("Device Information") + "\n" +
-                    str(next(Kathara.get_instance().get_machine_stats(machine_name, lab_hash))) + "\n" +
+                    str(next(Kathara.get_instance().get_machine_stats(machine_name, lab.hash))) + "\n" +
                     format_headers()
                 )
         finally:
             Curses.get_instance().close()
 
     @staticmethod
-    def _get_lab_live_info(lab_hash: str) -> None:
-        machines_stats = Kathara.get_instance().get_machines_stats(lab_hash)
+    def _get_lab_live_info(lab: Lab) -> None:
+        machines_stats = Kathara.get_instance().get_machines_stats(lab.hash)
         table = create_table(machines_stats)
 
         Curses.get_instance().init_window()
@@ -117,8 +121,7 @@ def _get_lab_live_info(lab_hash: str) -> None:
             Curses.get_instance().close()
 
     @staticmethod
-    def _get_conf_info(lab_path: str, machine_name: str = None) -> None:
-        lab = LabParser.parse(lab_path)
+    def _get_conf_info(lab: Lab, machine_name: str = None) -> None:
         if machine_name:
             print(format_headers("Device Information"))
             print(str(lab.machines[machine_name]))
diff --git a/src/Kathara/cli/command/ListCommand.py b/src/Kathara/cli/command/ListCommand.py
index 19951b5c..bb90ebab 100644
--- a/src/Kathara/cli/command/ListCommand.py
+++ b/src/Kathara/cli/command/ListCommand.py
@@ -53,7 +53,7 @@ def run(self, current_path: str, argv: List[str]) -> None:
         args = self.get_args()
 
         if args['all'] and not utils.is_admin():
-            raise Exception("You must be root in order to show all Kathara devices of all users.")
+            raise PrivilegeError("You must be root in order to show all Kathara devices of all users.")
 
         all_users = bool(args['all'])
 
diff --git a/src/Kathara/cli/command/LrestartCommand.py b/src/Kathara/cli/command/LrestartCommand.py
index 92276e22..5f312d2e 100644
--- a/src/Kathara/cli/command/LrestartCommand.py
+++ b/src/Kathara/cli/command/LrestartCommand.py
@@ -34,14 +34,14 @@ def __init__(self) -> None:
             dest="terminals",
             const=False,
             default=True,
-            help='Start the lab without opening terminal windows.'
+            help='Start the network scenario without opening terminal windows.'
         )
         group.add_argument(
             "--terminals",
             action="store_const",
             dest="terminals",
             const=True,
-            help='Start the lab opening terminal windows.'
+            help='Start the network scenario opening terminal windows.'
         )
         group.add_argument(
             "--privileged",
@@ -53,20 +53,20 @@ def __init__(self) -> None:
         self.parser.add_argument(
             '-d', '--directory',
             required=False,
-            help='Specify the folder containing the lab.'
+            help='Specify the folder containing the network scenario.'
         )
         self.parser.add_argument(
             '-F', '--force-lab',
             dest='force_lab',
             required=False,
             action='store_true',
-            help='Force the lab to start without a lab.conf or lab.dep file.'
+            help='Force the network scenario to start without a lab.conf or lab.dep file.'
         )
         self.parser.add_argument(
             '-l', '--list',
             required=False,
             action='store_true',
-            help='Show information about running devices after the lab has been started.'
+            help='Show information about running devices after the network scenario has been started.'
         )
         self.parser.add_argument(
             '-o', '--pass',
@@ -74,7 +74,7 @@ def __init__(self) -> None:
             metavar="OPTION",
             nargs='*',
             required=False,
-            help="Apply options to all devices of a lab during startup."
+            help="Apply options to all devices of a network scenario during startup."
         )
         self.parser.add_argument(
             '--xterm',
@@ -96,6 +96,13 @@ def __init__(self) -> None:
             help='/shared dir will not be mounted inside the devices.'
         )
 
+        self.parser.add_argument(
+            'machine_name',
+            metavar='DEVICE_NAME',
+            nargs='*',
+            help='Restarts only specified devices.'
+        )
+
     def run(self, current_path: str, argv: List[str]) -> None:
         self.parse_args(argv)
         args = self.get_args()
@@ -105,5 +112,8 @@ def run(self, current_path: str, argv: List[str]) -> None:
 
         lclean_argv = ['-d', args['directory']] if args['directory'] else []
 
+        if args['machine_name']:
+            lclean_argv.extend(args['machine_name'])
+
         LcleanCommand().run(lab_path, lclean_argv)
         LstartCommand().run(lab_path, argv)
diff --git a/src/Kathara/cli/command/LstartCommand.py b/src/Kathara/cli/command/LstartCommand.py
index 34141f71..97eadfad 100644
--- a/src/Kathara/cli/command/LstartCommand.py
+++ b/src/Kathara/cli/command/LstartCommand.py
@@ -7,7 +7,7 @@
 from ..ui.utils import create_table
 from ..ui.utils import format_headers
 from ... import utils
-from ...exceptions import PrivilegeError
+from ...exceptions import PrivilegeError, EmptyLabError
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
 from ...model.Lab import Lab
@@ -46,14 +46,14 @@ def __init__(self) -> None:
             dest="terminals",
             const=False,
             default=None,
-            help='Start the lab without opening terminal windows.'
+            help='Start the network scenario without opening terminal windows.'
         )
         group.add_argument(
             "--terminals",
             action="store_const",
             dest="terminals",
             const=True,
-            help='Start the lab opening terminal windows.'
+            help='Start the network scenario opening terminal windows.'
         )
         group.add_argument(
             "--privileged",
@@ -65,20 +65,20 @@ def __init__(self) -> None:
         self.parser.add_argument(
             '-d', '--directory',
             required=False,
-            help='Specify the folder containing the lab.'
+            help='Specify the folder containing the network scenario.'
         )
         self.parser.add_argument(
             '-F', '--force-lab',
             dest='force_lab',
             required=False,
             action='store_true',
-            help='Force the lab to start without a lab.conf or lab.dep file.'
+            help='Force the network scenario to start without a lab.conf or lab.dep file.'
         )
         self.parser.add_argument(
             '-l', '--list',
             required=False,
             action='store_true',
-            help='Show information about running devices after the lab has been started.'
+            help='Show information about running devices after the network scenario has been started.'
         )
         self.parser.add_argument(
             '-o', '--pass',
@@ -86,7 +86,7 @@ def __init__(self) -> None:
             metavar="OPTION",
             nargs='*',
             required=False,
-            help="Apply options to all devices of a lab during startup."
+            help="Apply options to all devices of a network scenario during startup."
         )
         self.parser.add_argument(
             '--xterm',
@@ -134,7 +134,7 @@ def run(self, current_path: str, argv: List[str]) -> Lab:
 
         if args['privileged']:
             if not utils.is_admin():
-                raise Exception("You must be root in order to start Kathara devices in privileged mode.")
+                raise PrivilegeError("You must be root in order to start Kathara devices in privileged mode.")
             else:
                 logging.warning("Running devices with privileged capabilities, terminals won't open!")
                 Setting.get_instance().open_terminals = False
@@ -148,7 +148,7 @@ def run(self, current_path: str, argv: List[str]) -> Lab:
             lab = LabParser.parse(lab_path)
         except IOError as e:
             if not args['force_lab']:
-                raise Exception(str(e))
+                raise e
             else:
                 lab = FolderParser.parse(lab_path)
 
@@ -164,7 +164,7 @@ def run(self, current_path: str, argv: List[str]) -> Lab:
             logging.info(format_headers())
 
         if len(lab.machines) <= 0:
-            raise Exception("No devices in the current lab. Exiting...")
+            raise EmptyLabError()
 
         try:
             options = OptionParser.parse(args['options'])
@@ -204,7 +204,7 @@ def run(self, current_path: str, argv: List[str]) -> Lab:
         lab.add_option('shared_mount', args['no_shared'])
         lab.add_option('privileged_machines', args['privileged'])
 
-        Kathara.get_instance().deploy_lab(lab, selected_machines=args['machine_name'])
+        Kathara.get_instance().deploy_lab(lab, selected_machines=set(args['machine_name']))
 
         if args['list']:
             machines_stats = Kathara.get_instance().get_machines_stats(lab_hash=lab.hash)
diff --git a/src/Kathara/cli/command/VcleanCommand.py b/src/Kathara/cli/command/VcleanCommand.py
index 5b61e9ea..822f63a8 100644
--- a/src/Kathara/cli/command/VcleanCommand.py
+++ b/src/Kathara/cli/command/VcleanCommand.py
@@ -2,9 +2,9 @@
 import logging
 from typing import List
 
-from ... import utils
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
+from ...model.Lab import Lab
 from ...strings import strings, wiki_description
 
 
@@ -37,8 +37,7 @@ def run(self, current_path: str, argv: List[str]) -> None:
         self.parse_args(argv)
         args = self.get_args()
 
-        lab_hash = utils.generate_urlsafe_hash("kathara_vlab")
-
-        Kathara.get_instance().undeploy_lab(lab_hash=lab_hash, selected_machines={args['name']})
+        lab = Lab("kathara_vlab")
+        Kathara.get_instance().undeploy_lab(lab_name=lab.name, selected_machines={args['name']})
 
         logging.info("Device `%s` deleted successfully!" % args['name'])
diff --git a/src/Kathara/cli/command/VconfigCommand.py b/src/Kathara/cli/command/VconfigCommand.py
index a5da8933..4383f6d9 100644
--- a/src/Kathara/cli/command/VconfigCommand.py
+++ b/src/Kathara/cli/command/VconfigCommand.py
@@ -1,9 +1,8 @@
 import argparse
 import logging
-import re
-import sys
 from typing import List
 
+from ..ui.utils import alphanumeric
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
 from ...model.Lab import Lab
@@ -33,35 +32,50 @@ def __init__(self) -> None:
             required=True,
             help='Name of the device to be connected on desired collision domains.'
         )
-        self.parser.add_argument(
-            '--eth',
-            dest='eths',
+
+        group = self.parser.add_mutually_exclusive_group(required=True)
+
+        group.add_argument(
+            '--add',
+            type=alphanumeric,
+            dest='to_add',
             metavar='CD',
             nargs='+',
-            required=True,
-            help='Specify the collision domain for an interface.'
+            help='Specify the collision domain to add.'
+        )
+        group.add_argument(
+            '--rm',
+            type=alphanumeric,
+            dest='to_remove',
+            metavar='CD',
+            nargs='+',
+            help='Specify the collision domain to remove.'
         )
 
     def run(self, current_path: str, argv: List[str]) -> None:
         self.parse_args(argv)
         args = self.get_args()
 
-        for eth in args['eths']:
-            # Only alphanumeric characters are allowed
-            matches = re.search(r"^\w+$", eth)
-
-            if not matches:
-                sys.stderr.write('Syntax error in --eth field.\n')
-                self.parser.print_help()
-                exit(1)
-
         lab = Lab("kathara_vlab")
 
-        device = lab.get_or_new_machine(args['name'])
-        device.api_object = Kathara.get_instance().get_machine_api_object(args['name'], lab_name=lab.name)
+        machine_name = args['name']
+        device = lab.get_or_new_machine(machine_name)
+        device.api_object = Kathara.get_instance().get_machine_api_object(machine_name, lab_name=lab.name)
+
+        if args['to_add']:
+            for cd in args['to_add']:
+                logging.info(
+                    "Adding interface to device `%s` on collision domain `%s`..." % (machine_name, cd)
+                )
+                link = lab.get_or_new_link(cd)
+                Kathara.get_instance().connect_machine_to_link(device, link)
 
-        for eth in args['eths']:
-            logging.info("Adding interface to device `%s` for collision domain `%s`..." % (args['name'], eth))
-            lab.connect_machine_to_link(args['name'], eth)
+        if args['to_remove']:
+            for cd in args['to_remove']:
+                logging.info(
+                    "Removing interface on collision domain `%s` from device `%s`..." % (cd, machine_name)
+                )
+                (_, link) = lab.connect_machine_to_link(machine_name, cd)
+                link.api_object = Kathara.get_instance().get_link_api_object(cd, lab_name=lab.name)
 
-        Kathara.get_instance().update_lab(lab)
+                Kathara.get_instance().disconnect_machine_from_link(device, link)
diff --git a/src/Kathara/cli/command/VstartCommand.py b/src/Kathara/cli/command/VstartCommand.py
index bfa90851..45c0bdf5 100644
--- a/src/Kathara/cli/command/VstartCommand.py
+++ b/src/Kathara/cli/command/VstartCommand.py
@@ -3,8 +3,9 @@
 import sys
 from typing import List
 
-from ..ui.utils import format_headers
+from ..ui.utils import format_headers, colon_separated
 from ... import utils
+from ...exceptions import PrivilegeError
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
 from ...model.Lab import Lab
@@ -68,6 +69,7 @@ def __init__(self) -> None:
         )
         self.parser.add_argument(
             '--eth',
+            type=colon_separated,
             dest='eths',
             metavar='N:CD',
             nargs='+',
@@ -168,7 +170,7 @@ def run(self, current_path: str, argv: List[str]) -> None:
 
         if args['privileged']:
             if not utils.is_admin():
-                raise Exception("You must be root in order to start this Kathara device in privileged mode.")
+                raise PrivilegeError("You must be root in order to start this Kathara device in privileged mode.")
             else:
                 logging.warning("Running device with privileged capabilities, terminal won't open!")
                 Setting.get_instance().open_terminals = False
@@ -183,12 +185,11 @@ def run(self, current_path: str, argv: List[str]) -> None:
         device = lab.get_or_new_machine(name, **args)
 
         if args['eths']:
-            for eth in args['eths']:
+            for iface_number, cd in args['eths']:
                 try:
-                    (iface_number, link_name) = eth.split(":")
-                    lab.connect_machine_to_link(device.name, link_name, machine_iface_number=int(iface_number))
+                    lab.connect_machine_to_link(device.name, cd, machine_iface_number=int(iface_number))
                 except ValueError:
-                    raise Exception("Interface number in `--eth %s` is not a number." % eth)
+                    raise SyntaxError("Interface number in `--eth %s:%s` is not a number." % (iface_number, cd))
 
         lab.check_integrity()
 
diff --git a/src/Kathara/cli/command/WipeCommand.py b/src/Kathara/cli/command/WipeCommand.py
index 29dfc8d5..c8e0abf9 100644
--- a/src/Kathara/cli/command/WipeCommand.py
+++ b/src/Kathara/cli/command/WipeCommand.py
@@ -5,6 +5,7 @@
 
 from ..ui.utils import confirmation_prompt
 from ... import utils
+from ...exceptions import PrivilegeError
 from ...foundation.cli.command.Command import Command
 from ...manager.Kathara import Kathara
 from ...setting.Setting import Setting
@@ -63,7 +64,7 @@ def run(self, current_path: str, argv: List[str]) -> None:
             Setting.wipe_from_disk()
         else:
             if args['all'] and not utils.is_admin():
-                raise Exception("You must be root in order to wipe all Kathara devices of all users.")
+                raise PrivilegeError("You must be root in order to wipe all Kathara devices of all users.")
 
             Kathara.get_instance().wipe(all_users=bool(args['all']))
 
diff --git a/src/Kathara/cli/ui/setting/CommonOptionsHandler.py b/src/Kathara/cli/ui/setting/CommonOptionsHandler.py
index 78452746..f50d475b 100644
--- a/src/Kathara/cli/ui/setting/CommonOptionsHandler.py
+++ b/src/Kathara/cli/ui/setting/CommonOptionsHandler.py
@@ -69,8 +69,7 @@ def add_items(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder
                                                    args=['image',
                                                          ImageValidator(),
                                                          'Write the name of a Docker image available on Docker Hub:',
-                                                         'Docker Image not available neither on Docker Hub nor '
-                                                         'in local repository!'
+                                                         None
                                                          ],
                                                    should_exit=True
                                                    )
diff --git a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py
index 456ed765..a17f0bd0 100644
--- a/src/Kathara/cli/ui/setting/DockerOptionsHandler.py
+++ b/src/Kathara/cli/ui/setting/DockerOptionsHandler.py
@@ -196,11 +196,11 @@ def remote_url_unix():
 
             return remote_url_item
 
-        remote_url_item = exec_by_platform(remote_url_unix, lambda: None, remote_url_unix)
+        platform_remote_url_item = exec_by_platform(remote_url_unix, lambda: None, remote_url_unix)
 
         current_menu.append_item(hosthome_item)
         current_menu.append_item(shared_item)
         current_menu.append_item(image_update_policy_item)
         current_menu.append_item(shared_cd_item)
-        if remote_url_item:
-            current_menu.append_item(remote_url_item)
+        if platform_remote_url_item:
+            current_menu.append_item(platform_remote_url_item)
diff --git a/src/Kathara/cli/ui/setting/utils.py b/src/Kathara/cli/ui/setting/utils.py
index 8b4ee0ee..7896e48d 100644
--- a/src/Kathara/cli/ui/setting/utils.py
+++ b/src/Kathara/cli/ui/setting/utils.py
@@ -76,7 +76,8 @@ def read_and_validate_value(prompt_msg: str, validator: BaseValidator, error_msg
                                 )
 
     if not answer.validation_result:
-        print(error_msg)
+        if error_msg:
+            print(error_msg)
 
     return answer
 
diff --git a/src/Kathara/cli/ui/utils.py b/src/Kathara/cli/ui/utils.py
index 80a2e16e..1a98cfc1 100644
--- a/src/Kathara/cli/ui/utils.py
+++ b/src/Kathara/cli/ui/utils.py
@@ -1,4 +1,6 @@
+import argparse
 import logging
+import re
 import subprocess
 import sys
 from datetime import datetime
@@ -78,7 +80,7 @@ def open_machine_terminal(machine) -> None:
     executable_path = utils.get_executable_path(sys.argv[0])
 
     if not executable_path:
-        raise Exception("Unable to find Kathara.")
+        raise FileNotFoundError("Unable to find Kathara.")
 
     is_vmachine = "-v" if machine.lab.path is None else ""
     connect_command = "%s connect %s -l %s" % (executable_path, is_vmachine, machine.name)
@@ -136,3 +138,20 @@ def osx_connect() -> None:
                 terminal_app.do_script(complete_osx_command)
 
     utils.exec_by_platform(unix_connect, windows_connect, osx_connect)
+
+
+# Types for argparse
+def alphanumeric(value, pat=re.compile(r"^\w+$")):
+    if not pat.match(value):
+        raise argparse.ArgumentTypeError("invalid alphanumeric value")
+
+    return value
+
+
+def colon_separated(value):
+    try:
+        (v1, v2) = value.split(':')
+    except ValueError:
+        raise argparse.ArgumentTypeError("invalid colon-separated value: %s" % value)
+
+    return v1, v2
diff --git a/src/Kathara/event/EventDispatcher.py b/src/Kathara/event/EventDispatcher.py
index 79f9e195..8c69dea4 100644
--- a/src/Kathara/event/EventDispatcher.py
+++ b/src/Kathara/event/EventDispatcher.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from typing import List, Callable, Any, Optional
+from ..exceptions import InstantiationError
 
 
 class EventDispatcher(object):
@@ -14,7 +15,10 @@ def get_instance() -> EventDispatcher:
         """Get an instance of the Dispatcher.
 
         Returns:
-            EventDispatcher: instance of the class.
+            EventDispatcher: An instance of the class.
+
+        Raises:
+            InstantiationError: If two instances of the class are created.
         """
         if EventDispatcher.__instance is None:
             EventDispatcher()
@@ -23,7 +27,7 @@ def get_instance() -> EventDispatcher:
 
     def __init__(self) -> None:
         if EventDispatcher.__instance is not None:
-            raise Exception("This class is a singleton!")
+            raise InstantiationError("This class is a singleton!")
         else:
             self.events = {}
 
diff --git a/src/Kathara/exceptions.py b/src/Kathara/exceptions.py
index 6e7e794a..54f6faca 100644
--- a/src/Kathara/exceptions.py
+++ b/src/Kathara/exceptions.py
@@ -7,23 +7,33 @@ class HTTPConnectionError(Exception):
     pass
 
 
+class InstantiationError(Exception):
+    pass
+
+
+class InvocationError(Exception):
+    pass
+
+
 # Settings Exceptions
 class SettingsError(Exception):
-    def __init__(self, message) -> None:
-        super().__init__("Settings file is not valid: %s\nFix it or delete it before launching." % message)
+    def __init__(self, message: str) -> None:
+        super().__init__(f"Settings file is not valid: {message} Fix it or delete it before launching.")
 
 
-class SettingsNotFound(Exception):
-    pass
+class SettingsNotFoundError(Exception):
+    def __init__(self, path: str) -> None:
+        super().__init__(f"Settings file not found in path `{path}`.")
 
 
 class DockerDaemonConnectionError(Exception):
-    pass
+    def __init__(self, message: str) -> None:
+        super().__init__(f"Cannot connect to Docker Daemon: {message}")
 
 
 class NotSupportedError(Exception):
-    def __init__(self, message) -> None:
-        super().__init__("Not Supported: %s" % message)
+    def __init__(self, message: str) -> None:
+        super().__init__(f"Not Supported: {message}")
 
 
 # OS Exceptions
@@ -31,29 +41,71 @@ class PrivilegeError(Exception):
     pass
 
 
+class InterfaceNotFoundError(Exception):
+    pass
+
+
+class HostArchitectureError(Exception):
+    def __init__(self, architecture: str) -> None:
+        super().__init__(f"Not implemented for host architecture `{architecture}`.")
+
+
 # Lab Exceptions
 class LabAlreadyExistsError(Exception):
     pass
 
 
+class LabNotFoundError(Exception):
+    pass
+
+
+class EmptyLabError(Exception):
+    def __init__(self) -> None:
+        super().__init__("No devices in the current network scenario.")
+
+
+class MachineDependencyError(Exception):
+    pass
+
+
 # Machine Exceptions
 class MountDeniedError(Exception):
     pass
 
 
 class MachineAlreadyExistsError(Exception):
-    pass
+    def __init__(self, machine_name: str) -> None:
+        super().__init__(f"Device with name `{machine_name}` already exists.")
 
 
 class NonSequentialMachineInterfaceError(Exception):
-    pass
+    def __init__(self, iface_num: int, machine_name: str) -> None:
+        super().__init__(f"Interface `{iface_num}` missing on device `{machine_name}`.")
 
 
 class MachineOptionError(Exception):
     pass
 
 
-class MachineCollisionDomainConflictError(Exception):
+class MachineCollisionDomainError(Exception):
+    pass
+
+
+class MachineNotFoundError(Exception):
+    pass
+
+
+class MachineNotReadyError(Exception):
+    def __init__(self, machine_name: str) -> None:
+        super().__init__(f"Device `{machine_name}` is not ready.")
+
+
+# Link Exceptions
+class LinkNotFoundError(Exception):
+    pass
+
+
+class LinkAlreadyExistsError(Exception):
     pass
 
 
@@ -63,4 +115,36 @@ class TestError(Exception):
 
 
 class MachineSignatureNotFoundError(TestError):
+    __slots__ = ['machine_name']
+
+    def __init__(self, machine_name: str):
+        self.machine_name: str = machine_name
+
+    def __str__(self):
+        return f"Signature for device `{self.machine_name}` not found!"
+
+
+# Docker Exceptions
+class InvalidImageArchitectureError(ValueError):
+    __slots__ = ['image_name', 'arch']
+
+    def __init__(self, image_name: str, arch: str):
+        self.image_name: str = image_name
+        self.arch: str = arch
+
+    def __str__(self):
+        return f"Docker Image `{self.image_name}` is not compatible with your host architecture `{self.arch}`"
+
+
+class DockerImageNotFoundError(Exception):
+    def __init__(self, image_name: str) -> None:
+        super().__init__(f"Docker Image `{image_name}` is not available neither on Docker Hub nor in local repository!")
+
+
+class DockerPluginError(Exception):
+    pass
+
+
+# Kubernetes Exceptions
+class KubernetesConfigMapError(Exception):
     pass
diff --git a/src/Kathara/foundation/cli/CliArgs.py b/src/Kathara/foundation/cli/CliArgs.py
index 58d195c5..d5ac9caa 100644
--- a/src/Kathara/foundation/cli/CliArgs.py
+++ b/src/Kathara/foundation/cli/CliArgs.py
@@ -2,6 +2,8 @@
 
 from typing import Any
 
+from ...exceptions import InstantiationError
+
 
 class CliArgs(object):
     __slots__ = ['args']
@@ -17,7 +19,7 @@ def get_instance() -> CliArgs:
 
     def __init__(self) -> None:
         if CliArgs.__instance is not None:
-            raise Exception("This class is a singleton!")
+            raise InstantiationError("This class is a singleton!")
         else:
             self.args = {}
 
diff --git a/src/Kathara/foundation/cli/ui/setting/OptionsHandler.py b/src/Kathara/foundation/cli/ui/setting/OptionsHandler.py
index ba9351d2..f70ae812 100644
--- a/src/Kathara/foundation/cli/ui/setting/OptionsHandler.py
+++ b/src/Kathara/foundation/cli/ui/setting/OptionsHandler.py
@@ -2,6 +2,7 @@
 from typing import Optional
 
 from .....cli.ui.setting import SettingsMenuFactory
+from .....exceptions import InvocationError
 from .....trdparty.consolemenu import ConsoleMenu, MenuFormatBuilder
 
 
@@ -13,7 +14,7 @@ def __init__(self) -> None:
 
     def add(self, current_menu: ConsoleMenu, menu_formatter: MenuFormatBuilder) -> None:
         if not self.menu_factory:
-            raise Exception("`add` called without a MenuFactory set.")
+            raise InvocationError("`add` called without a MenuFactory set.")
 
         self.add_items(current_menu, menu_formatter)
 
diff --git a/src/Kathara/foundation/manager/IManager.py b/src/Kathara/foundation/manager/IManager.py
index 9ffb1c7d..331f7ef5 100644
--- a/src/Kathara/foundation/manager/IManager.py
+++ b/src/Kathara/foundation/manager/IManager.py
@@ -5,11 +5,43 @@
 from .stats.ILinkStats import ILinkStats
 from .stats.IMachineStats import IMachineStats
 from ...model.Lab import Lab
+from ...model.Link import Link
 from ...model.Machine import Machine
 
 
 class IManager(ABC):
     """Interface to be implemented in the virtualization managers"""
+
+    @abstractmethod
+    def deploy_machine(self, machine: Machine) -> None:
+        """Deploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the specified device is not associated to any network scenario.
+        """
+        raise NotImplementedError("You must implement `deploy_machine` method.")
+
+    @abstractmethod
+    def deploy_link(self, link: Link) -> None:
+        """Deploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        raise NotImplementedError("You must implement `deploy_link` method.")
+
     @abstractmethod
     def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         """Deploy a Kathara network scenario.
@@ -24,16 +56,69 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         raise NotImplementedError("You must implement `deploy_lab` method.")
 
     @abstractmethod
-    def update_lab(self, lab: Lab) -> None:
-        """Update a running network scenario.
+    def connect_machine_to_link(self, machine: Machine, link: Link) -> None:
+        """Connect a Kathara device to a collision domain.
 
         Args:
-            lab (Kathara.model.Lab): A Kathara network scenario.
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is already connected to the collision domain.
+        """
+        raise NotImplementedError("You must implement `connect_machine_to_link` method.")
+
+    def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None:
+        """Disconnect a Kathara device from a collision domain.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): The Kathara collision domain from which disconnect the device.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is not connected to the collision domain.
+        """
+        raise NotImplementedError("You must implement `disconnect_machine_from_link` method.")
+
+    @abstractmethod
+    def undeploy_machine(self, machine: Machine) -> None:
+        """Undeploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
 
         Returns:
             None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
         """
-        raise NotImplementedError("You must implement `update_lab` method.")
+        raise NotImplementedError("You must implement `undeploy_machine` method.")
+
+    @abstractmethod
+    def undeploy_link(self, link: Link) -> None:
+        """Undeploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        raise NotImplementedError("You must implement `undeploy_link` method.")
 
     @abstractmethod
     def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None,
@@ -51,7 +136,7 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] =
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         raise NotImplementedError("You must implement `undeploy_lab` method.")
 
@@ -84,7 +169,7 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         raise NotImplementedError("You must implement `connect_tty` method.")
 
@@ -103,7 +188,7 @@ def exec(self, machine_name: str, command: str, lab_hash: Optional[str] = None,
             Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         raise NotImplementedError("You must implement `exec` method.")
 
@@ -136,6 +221,10 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
 
         Returns:
             Any: API object of the device specific for the current manager.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            MachineNotFoundError: If the specified device is not found.
         """
         raise NotImplementedError("You must implement `get_machine_api_object` method.")
 
@@ -169,6 +258,10 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
 
         Returns:
             Any: API object of the collision domain specific for the current manager.
+
+         Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            LinkNotFoundError: If the collision domain is not found.
         """
         raise NotImplementedError("You must implement `get_link_api_object` method.")
 
@@ -186,6 +279,31 @@ def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_
         """
         raise NotImplementedError("You must implement `get_links_api_objects` method.")
 
+    @abstractmethod
+    def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab:
+        """Return the network scenario (specified by the hash or name), building it from API objects.
+
+        Args:
+            lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name.
+            lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name.
+
+        Returns:
+            Lab: The built network scenario.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+        """
+        raise NotImplementedError("You must implement `get_lab_from_api` method.")
+
+    @abstractmethod
+    def update_lab_from_api(self, lab: Lab) -> None:
+        """Update the passed network scenario from API objects.
+
+        Args:
+            lab (Lab): The network scenario to update.
+        """
+        raise NotImplementedError("You must implement `update_lab_from_api` method.")
+
     @abstractmethod
     def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None,
                            all_users: bool = False) -> Generator[Dict[str, IMachineStats], None, None]:
@@ -218,8 +336,11 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None,
 
         Returns:
             IMachineStats: IMachineStats object containing the device info.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
         """
-        raise NotImplementedError("You must implement `get_machine_info` method.")
+        raise NotImplementedError("You must implement `get_machine_stats` method.")
 
     @abstractmethod
     def get_links_stats(self, lab_hash: str = None, lab_name: str = None, link_name: str = None,
@@ -254,6 +375,9 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N
         Returns:
              Generator[Dict[str, ILinkStats], None, None]: A generator containing dicts that has API Object
              identifier as keys and ILinksStats objects as values.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         raise NotImplementedError("You must implement `get_link_stats` method.")
 
@@ -268,8 +392,8 @@ def check_image(self, image_name: str) -> None:
             None
 
         Raises:
-            ConnectionError: The image is not locally available and there is no connection to a remote image repository.
-            Exception: The image is not found.
+            ConnectionError: If the image is not locally available and there is no connection to a remote image repository.
+            ImageNotFoundError: If the image is not found.
         """
         raise NotImplementedError("You must implement `check_image` method.")
 
diff --git a/src/Kathara/manager/Kathara.py b/src/Kathara/manager/Kathara.py
index 991fc0c6..5a87a6e1 100644
--- a/src/Kathara/manager/Kathara.py
+++ b/src/Kathara/manager/Kathara.py
@@ -3,11 +3,13 @@
 import io
 from typing import Set, Dict, Generator, Any, Tuple, List, Optional
 
+from ..exceptions import InstantiationError
 from ..foundation.manager.IManager import IManager
 from ..foundation.manager.ManagerFactory import ManagerFactory
 from ..foundation.manager.stats.ILinkStats import ILinkStats
 from ..foundation.manager.stats.IMachineStats import IMachineStats
 from ..model.Lab import Lab
+from ..model.Link import Link
 from ..model.Machine import Machine
 from ..setting.Setting import Setting, AVAILABLE_MANAGERS
 
@@ -24,6 +26,9 @@ def get_instance() -> Kathara:
     
         Returns:
             Kathara: instance of Kathara.
+
+        Raises:
+            InstantiationError: If two instances of the class are created.
         """
         if Kathara.__instance is None:
             Kathara()
@@ -32,7 +37,7 @@ def get_instance() -> Kathara:
 
     def __init__(self) -> None:
         if Kathara.__instance is not None:
-            raise Exception("This class is a singleton!")
+            raise InstantiationError("This class is a singleton!")
         else:
             manager_type = Setting.get_instance().manager_type
 
@@ -42,6 +47,34 @@ def __init__(self) -> None:
 
             Kathara.__instance = self
 
+    def deploy_machine(self, machine: Machine) -> None:
+        """Deploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the specified device is not associated to any network scenario.
+        """
+        self.manager.deploy_machine(machine)
+
+    def deploy_link(self, link: Link) -> None:
+        """Deploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        self.manager.deploy_link(link)
+
     def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         """Deploy a Kathara network scenario.
 
@@ -54,16 +87,67 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         """
         self.manager.deploy_lab(lab, selected_machines)
 
-    def update_lab(self, lab: Lab) -> None:
-        """Update a running network scenario.
+    def connect_machine_to_link(self, machine: Machine, link: Link) -> None:
+        """Connect a Kathara device to a collision domain.
 
         Args:
-            lab (Kathara.model.Lab): A Kathara network scenario.
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is already connected to the collision domain.
+        """
+        self.manager.connect_machine_to_link(machine, link)
+
+    def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None:
+        """Disconnect a Kathara device from a collision domain.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): The Kathara collision domain from which disconnect the device.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is not connected to the collision domain.
+        """
+        self.manager.disconnect_machine_from_link(machine, link)
+
+    def undeploy_machine(self, machine: Machine) -> None:
+        """Undeploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
 
         Returns:
             None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
         """
-        self.manager.update_lab(lab)
+        self.manager.undeploy_machine(machine)
+
+    def undeploy_link(self, link: Link) -> None:
+        """Undeploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        self.manager.undeploy_link(link)
 
     def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None,
                      selected_machines: Optional[Set[str]] = None) -> None:
@@ -80,7 +164,7 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] =
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         self.manager.undeploy_lab(lab_hash, lab_name, selected_machines)
 
@@ -111,7 +195,7 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         self.manager.connect_tty(machine_name, lab_hash, lab_name, shell, logs)
 
@@ -129,7 +213,7 @@ def exec(self, machine_name: str, command: str, lab_hash: Optional[str] = None,
             Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         return self.manager.exec(machine_name, command, lab_hash, lab_name)
 
@@ -160,6 +244,10 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
 
         Returns:
             Any: API object of the device specific for the current manager.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            MachineNotFoundError: If the specified device is not found.
         """
         return self.manager.get_machine_api_object(machine_name, lab_hash, lab_name, all_users)
 
@@ -191,6 +279,10 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
 
         Returns:
             Any: API object of the collision domain specific for the current manager.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            LinkNotFoundError: If the collision domain is not found.
         """
         return self.manager.get_link_api_object(link_name, lab_hash, lab_name, all_users)
 
@@ -207,6 +299,29 @@ def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_
         """
         return self.manager.get_links_api_objects(lab_hash, lab_name, all_users)
 
+    def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab:
+        """Return the network scenario (specified by the hash or name), building it from API objects.
+
+        Args:
+            lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name.
+            lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name.
+
+        Returns:
+            Lab: The built network scenario.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+        """
+        return self.manager.get_lab_from_api(lab_hash, lab_name)
+
+    def update_lab_from_api(self, lab: Lab) -> None:
+        """Update the passed network scenario from API objects.
+
+        Args:
+            lab (Lab): The network scenario to update.
+        """
+        self.manager.update_lab_from_api(lab)
+
     def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None,
                            all_users: bool = False) -> Generator[Dict[str, IMachineStats], None, None]:
         """Return information about the running devices.
@@ -239,7 +354,7 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None,
             IMachineStats: IMachineStats object containing the device info.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         return self.manager.get_machine_stats(machine_name, lab_hash, lab_name, all_users)
 
@@ -276,7 +391,7 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N
              identifier as keys and ILinksStats objects as values.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         return self.manager.get_link_stats(link_name, lab_hash, lab_name, all_users)
 
@@ -290,8 +405,8 @@ def check_image(self, image_name: str) -> None:
             None
 
         Raises:
-            ConnectionError: The image is not locally available and there is no connection to a remote image repository.
-            Exception: The image is not found.
+            ConnectionError: If the image is not locally available and there is no connection to a remote image repository.
+            ImageNotFoundError: If the image is not found.
         """
         self.manager.check_image(image_name)
 
diff --git a/src/Kathara/manager/docker/DockerImage.py b/src/Kathara/manager/docker/DockerImage.py
index f4e38813..20fb5be8 100644
--- a/src/Kathara/manager/docker/DockerImage.py
+++ b/src/Kathara/manager/docker/DockerImage.py
@@ -5,7 +5,9 @@
 from docker import DockerClient
 from docker.errors import APIError
 
+from ... import utils
 from ...event.EventDispatcher import EventDispatcher
+from ...exceptions import InvalidImageArchitectureError, DockerImageNotFoundError
 
 
 class DockerImage(object):
@@ -98,11 +100,15 @@ def check(self, image_name: str) -> None:
 
         Returns:
             None
+
+        Raises:
+            ConnectionError: If there is a connection error while pulling the Docker image from Docker Hub.
+            DockerImageNotFoundError: If the Docker image is not available neither on Docker Hub nor in local repository.
         """
         self._check_and_pull(image_name, pull=False)
 
-    def check_and_pull_from_list(self, images: Union[List[str], Set[str]]) -> None:
-        """Check and pull a list of specified images.
+    def check_from_list(self, images: Union[List[str], Set[str]]) -> None:
+        """Check a list of specified images.
 
         Args:
             images (Union[List[str], Set[str]]): A list of Docker images name to pull.
@@ -122,24 +128,63 @@ def _check_and_pull(self, image_name: str, pull: bool = True) -> None:
 
         Returns:
             None
+
+        Raises:
+            ConnectionError: If there is a connection error while pulling the Docker image from Docker Hub.
+            DockerImageNotFoundError: If the Docker image is not available neither on Docker Hub
+                nor in local repository.
+            InvalidImageArchitectureError: If the Docker image is not compatible with the host architecture.
         """
         try:
             # Tries to get the image from the local Docker repository.
-            self.get_local(image_name)
+            image = self.get_local(image_name)
+            self._check_image_architecture(image)
             try:
                 if pull:
                     self.check_for_updates(image_name)
             except APIError:
                 logging.debug("Cannot check updates, skipping...")
+        except InvalidImageArchitectureError as e:
+            raise e
         except APIError:
             # If not found, tries on Docker Hub.
             try:
                 # If the image exists on Docker Hub, pulls it.
-                self.get_remote(image_name)
+                registry_data = self.get_remote(image_name)
+                self._check_image_architecture(registry_data)
                 if pull:
                     self.pull(image_name)
             except APIError as e:
                 if e.response.status_code == 500 and 'dial tcp' in e.explanation:
-                    raise ConnectionError("Image `%s` not found in local and no Internet connection." % image_name)
+                    raise ConnectionError(
+                        "Docker Image `%s` is not available in local repository and "
+                        "no Internet connection is available to pull it from Docker Hub." % image_name
+                    )
                 else:
-                    raise Exception("Image `%s` does not exists either in local or on Docker Hub." % image_name)
+                    raise DockerImageNotFoundError(image_name)
+            except InvalidImageArchitectureError as e:
+                raise e
+
+    @staticmethod
+    def _check_image_architecture(image: Union[docker.models.images.Image, docker.models.images.RegistryData]) -> None:
+        """Check if the specified image is compatible with the host architecture.
+
+        Args:
+            image (Union[docker.models.images.Image, docker.models.images.RegistryData]): Docker Image object.
+
+        Returns:
+            None
+            
+        Raises: 
+            InvalidImageArchitectureError: If the Docker image is not compatible with the architecture.
+        """
+        host_arch = utils.get_architecture()
+
+        is_compatible = False
+        if isinstance(image, docker.models.images.Image):
+            is_compatible = (image.attrs['Architecture'] == host_arch)
+        elif isinstance(image, docker.models.images.RegistryData):
+            is_compatible = len(list(filter(lambda x: x['architecture'] == host_arch, image.attrs['Platforms']))) > 0
+
+        if not is_compatible:
+            raise InvalidImageArchitectureError(image, host_arch)
diff --git a/src/Kathara/manager/docker/DockerLink.py b/src/Kathara/manager/docker/DockerLink.py
index f67a9830..0e1e422c 100644
--- a/src/Kathara/manager/docker/DockerLink.py
+++ b/src/Kathara/manager/docker/DockerLink.py
@@ -1,7 +1,7 @@
 import logging
 import re
 from multiprocessing.dummy import Pool
-from typing import List, Union, Dict, Generator
+from typing import List, Union, Dict, Generator, Set, Optional
 
 import docker
 import docker.models.networks
@@ -12,6 +12,7 @@
 from ..docker.DockerPlugin import PLUGIN_NAME
 from ... import utils
 from ...event.EventDispatcher import EventDispatcher
+from ...exceptions import LinkNotFoundError
 from ...exceptions import PrivilegeError
 from ...model.ExternalLink import ExternalLink
 from ...model.Lab import Lab
@@ -27,17 +28,18 @@ class DockerLink(object):
     def __init__(self, client: DockerClient) -> None:
         self.client: DockerClient = client
 
-    def deploy_links(self, lab: Lab, selected_links: Dict[str, Link] = None) -> None:
+    def deploy_links(self, lab: Lab, selected_links: Set[str] = None) -> None:
         """Deploy all the lab collision domains as Docker networks.
 
         Args:
             lab (Kathara.model.Lab.Lab): A Kathara network scenario.
-            selected_links (Dict[str, Link]): Keys are collision domains names, values are Link objects.
+            selected_links (Set[str]): A set containing the name of the collision domains to deploy.
 
         Returns:
             None
         """
-        links = selected_links.items() if selected_links else lab.links.items()
+        links = {k: v for (k, v) in lab.links.items() if k in selected_links}.items() if selected_links \
+            else lab.links.items()
 
         if len(links) > 0:
             pool_size = utils.get_pool_size()
@@ -118,16 +120,20 @@ def create(self, link: Link) -> None:
             logging.debug("External Interfaces required, connecting them...")
             self._attach_external_interfaces(link.external, link.api_object)
 
-    def undeploy(self, lab_hash: str) -> None:
+    def undeploy(self, lab_hash: str, selected_links: Optional[Set[str]] = None) -> None:
         """Undeploy all the collision domains of the scenario specified by lab_hash.
 
         Args:
             lab_hash (str): The hash of the network scenario to undeploy.
+            selected_links (Set[str]): If specified, delete only the collision domains contained in the set.
 
         Returns:
             None
         """
         networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash)
+        if selected_links is not None and len(selected_links) > 0:
+            networks = [item for item in networks if item.attrs["Labels"]["name"] in selected_links]
+
         for item in networks:
             item.reload()
         networks = [item for item in networks if len(item.containers) <= 0]
@@ -225,13 +231,16 @@ def get_links_stats(self, lab_hash: str = None, link_name: str = None, user: str
         Returns:
            Generator[Dict[str, DockerMachineStats], None, None]: A generator containing network names as keys and
            DockerLinkStats as values.
+
+        Raises:
+            LinkNotFoundError: If the collision domains specified are not found.
         """
         networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name, user=user)
         if not networks:
             if not link_name:
-                raise Exception("No collision domains found.")
+                raise LinkNotFoundError("No collision domains found.")
             else:
-                raise Exception(f"Collision domains with name {link_name} not found.")
+                raise LinkNotFoundError(f"Collision domains with name {link_name} not found.")
 
         networks = sorted(networks, key=lambda x: x.name)
 
@@ -299,6 +308,9 @@ def _delete_link(network: docker.models.networks.Network) -> None:
 
         Args:
             network (docker.models.networks.Network): A Docker network.
+
+        Raises:
+            PrivilegeError: If you are not root while deleting an external VLAN Interface.
         """
         external_label = network.attrs['Labels']["external"]
         external_links = external_label.split(";") if external_label else None
diff --git a/src/Kathara/manager/docker/DockerMachine.py b/src/Kathara/manager/docker/DockerMachine.py
index c0ca6f88..5a75d551 100644
--- a/src/Kathara/manager/docker/DockerMachine.py
+++ b/src/Kathara/manager/docker/DockerMachine.py
@@ -12,9 +12,9 @@
 from .stats.DockerMachineStats import DockerMachineStats
 from ... import utils
 from ...event.EventDispatcher import EventDispatcher
-from ...exceptions import MountDeniedError, MachineAlreadyExistsError
+from ...exceptions import MountDeniedError, MachineAlreadyExistsError, MachineNotFoundError, DockerPluginError
 from ...model.Lab import Lab
-from ...model.Link import BRIDGE_LINK_NAME
+from ...model.Link import Link, BRIDGE_LINK_NAME
 from ...model.Machine import Machine
 from ...setting.Setting import Setting
 
@@ -34,7 +34,10 @@
     "(cd /hostlab/{machine_name} && tar c .) | (cd / && tar xhf -); fi",
 
     # If /etc/hosts is not configured by the user, add the localhost mapping
-    "if [ ! -s \"/etc/hosts\" ]; then echo '127.0.0.1 localhost' > /etc/hosts; fi",
+    "if [ ! -s \"/etc/hosts\" ]; then "
+    "echo '127.0.0.1 localhost' > /etc/hosts",
+    "echo '::1 localhost' >> /etc/hosts",
+    "fi",
 
     # Give proper permissions to /var/www
     "if [ -d \"/var/www\" ]; then "
@@ -104,9 +107,12 @@ def deploy_machines(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         Returns:
             None
         """
+        machines = {k: v for (k, v) in lab.machines.items() if k in selected_machines}.items() if selected_machines \
+            else lab.machines.items()
+
         # Check and pulling machine images
-        lab_images = set(map(lambda x: x.get_image(), lab.machines.values()))
-        self.docker_image.check_and_pull_from_list(lab_images)
+        lab_images = set(map(lambda x: x[1].get_image(), machines))
+        self.docker_image.check_from_list(lab_images)
 
         shared_mount = lab.general_options['shared_mount'] if 'shared_mount' in lab.general_options \
             else Setting.get_instance().shared_mount
@@ -117,9 +123,6 @@ def deploy_machines(self, lab: Lab, selected_machines: Set[str] = None) -> None:
             else:
                 lab.create_shared_folder()
 
-        machines = {k: v for (k, v) in lab.machines.items() if k in selected_machines}.items() if selected_machines \
-            else lab.machines.items()
-
         EventDispatcher.get_instance().dispatch("machines_deploy_started", items=machines)
 
         # Deploy all lab machines.
@@ -163,13 +166,17 @@ def create(self, machine: Machine) -> None:
 
         Returns:
             None
+
+        Raises:
+            MachineAlreadyExistsError: If a device with the name specified already exists.
+            APIError: If the Docker APIs return an error.
         """
         logging.debug("Creating device `%s`..." % machine.name)
 
         containers = self.get_machines_api_objects_by_filters(machine_name=machine.name, lab_hash=machine.lab.hash,
                                                               user=utils.get_current_user_name())
         if containers:
-            raise MachineAlreadyExistsError("Device with name `%s` already exists." % machine.name)
+            raise MachineAlreadyExistsError(machine.name)
 
         image = machine.get_image()
         memory = machine.get_mem()
@@ -284,32 +291,49 @@ def create(self, machine: Machine) -> None:
 
         machine.api_object = machine_container
 
-    def update(self, machine: Machine) -> None:
-        """Update the Docker container representing the machine.
-
-        Create a new Docker network for each collision domain contained in machine.interfaces that is not already
-        attached to the container.
+    @staticmethod
+    def connect_to_link(machine: Machine, link: Link) -> None:
+        """Connect the Docker container representing the machine to a specified collision domain.
 
         Args:
-            machine (Kathara.model.Machine.Machine): A Kathara device.
+            machine (Kathara.model.Machine): A Kathara device.
+            link (Kathara.model.Link): A Kathara collision domain object.
 
         Returns:
             None
+
+        Raises:
+            DockerPluginError: If Kathara has been left in an inconsistent state.
+            APIError: If the Docker APIs return an error.
         """
-        containers = self.get_machines_api_objects_by_filters(
-            machine_name=machine.name, lab_hash=machine.lab.hash, user=utils.get_current_user_name()
-        )
+        attached_networks = machine.api_object.attrs["NetworkSettings"]["Networks"]
 
-        if not containers:
-            raise Exception("The specified device `%s` is not running." % machine.name)
+        if link.api_object.name not in attached_networks:
+            try:
+                link.api_object.connect(machine.api_object)
+            except APIError as e:
+                if e.response.status_code == 500 and \
+                        ("network does not exist" in e.explanation or "endpoint does not exist" in e.explanation):
+                    raise DockerPluginError(
+                        "Kathara has been left in an inconsistent state! Please run `kathara wipe`.")
+                else:
+                    raise e
+
+    @staticmethod
+    def disconnect_from_link(machine: Machine, link: Link) -> None:
+        """Disconnect the Docker container representing the machine from a specified collision domain.
 
-        machine.api_object = containers.pop()
+        Args:
+            machine (Kathara.model.Machine): A Kathara device.
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+        """
         attached_networks = machine.api_object.attrs["NetworkSettings"]["Networks"]
 
-        # Connect the container to its new networks
-        for (_, machine_link) in machine.interfaces.items():
-            if machine_link.api_object.name not in attached_networks:
-                machine_link.api_object.connect(machine.api_object)
+        if link.api_object.name in attached_networks:
+            link.api_object.disconnect(machine.api_object)
 
     @staticmethod
     def start(machine: Machine) -> None:
@@ -319,6 +343,14 @@ def start(machine: Machine) -> None:
 
         Args:
            machine (Kathara.model.Machine.Machine): A Kathara device.
+
+        Returns:
+            None
+
+        Raises:
+            MountDeniedError: If the host drive is not shared with Docker.
+            DockerPluginError: If Kathara has been left in an inconsistent state.
+            APIError: If the Docker APIs return an error.
         """
         logging.debug("Starting device `%s`..." % machine.name)
 
@@ -327,6 +359,9 @@ def start(machine: Machine) -> None:
         except APIError as e:
             if e.response.status_code == 500 and e.explanation.startswith('Mounts denied'):
                 raise MountDeniedError("Host drive is not shared with Docker.")
+            elif e.response.status_code == 500 and \
+                    ("network does not exist" in e.explanation or "endpoint does not exist" in e.explanation):
+                raise DockerPluginError("Kathara has been left in an inconsistent state! Please run `kathara wipe`.")
             else:
                 raise e
 
@@ -339,8 +374,15 @@ def start(machine: Machine) -> None:
                                                                                                   iface_num
                                                                                                   )
                           )
-
-            machine_link.api_object.connect(machine.api_object)
+            try:
+                machine_link.api_object.connect(machine.api_object)
+            except APIError as e:
+                if e.response.status_code == 500 and \
+                        ("network does not exist" in e.explanation or "endpoint does not exist" in e.explanation):
+                    raise DockerPluginError(
+                        "Kathara has been left in an inconsistent state! Please run `kathara wipe`.")
+                else:
+                    raise e
 
         # Bridged connection required but not added in `deploy` method.
         if "bridge_connected" not in machine.meta and machine.meta['bridged']:
@@ -362,7 +404,7 @@ def start(machine: Machine) -> None:
         )
 
         # Execute the startup commands inside the container (without privileged flag so basic permissions are used)
-        machine.api_object.exec_run(cmd=[Setting.get_instance().device_shell, '-c', startup_commands_string],
+        machine.api_object.exec_run(cmd=[machine.api_object.labels['shell'], '-c', startup_commands_string],
                                     stdout=False,
                                     stderr=False,
                                     privileged=False,
@@ -444,10 +486,13 @@ def connect(self, lab_hash: str, machine_name: str, user: str = None, shell: str
 
         Returns:
             None
+
+        Raises:
+            MachineNotFoundError: If the specified device is not running.
         """
         containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user)
         if not containers:
-            raise Exception("The specified device `%s` is not running." % machine_name)
+            raise MachineNotFoundError("The specified device `%s` is not running." % machine_name)
         container = containers.pop()
 
         if not shell:
@@ -515,12 +560,15 @@ def exec(self, lab_hash: str, machine_name: str, command: str, user: str = None,
 
         Returns:
             Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes.
+
+        Raises:
+            MachineNotFoundError: If the specified device is not running.
         """
         logging.debug("Executing command `%s` to device with name: %s" % (command, machine_name))
 
         containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user)
         if not containers:
-            raise Exception("The specified device `%s` is not running." % machine_name)
+            raise MachineNotFoundError("The specified device `%s` is not running." % machine_name)
         container = containers.pop()
 
         exec_result = container.exec_run(cmd=command,
@@ -584,13 +632,16 @@ def get_machines_stats(self, lab_hash: str = None, machine_name: str = None, use
         Returns:
             Generator[Dict[str, DockerMachineStats], None, None]: A generator containing device names as keys and
             DockerMachineStats as values.
+
+        Raises:
+            MachineNotFoundError: If the specified devices are not running.
         """
         containers = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name, user=user)
         if not containers:
             if not machine_name:
-                raise Exception("No devices found.")
+                raise MachineNotFoundError("No devices found.")
             else:
-                raise Exception(f"Devices with name {machine_name} not found.")
+                raise MachineNotFoundError(f"Devices with name {machine_name} not found.")
 
         containers = sorted(containers, key=lambda x: x.name)
 
@@ -623,25 +674,25 @@ def get_container_name(name: str, lab_hash: str) -> str:
         return "%s_%s_%s_%s" % (Setting.get_instance().device_prefix, utils.get_current_user_name(), name, lab_hash)
 
     @staticmethod
-    def _delete_machine(machine: docker.models.containers.Container) -> None:
+    def _delete_machine(container: docker.models.containers.Container) -> None:
         """Remove a running Docker container.
 
         Args:
-            machine (docker.models.containers.Container): The Docker container to remove.
+            container (docker.models.containers.Container): The Docker container to remove.
 
         Returns:
             None
         """
         # Build the shutdown command string
-        shutdown_commands_string = "; ".join(SHUTDOWN_COMMANDS).format(machine_name=machine.labels["name"])
+        shutdown_commands_string = "; ".join(SHUTDOWN_COMMANDS).format(machine_name=container.labels["name"])
 
         # Execute the shutdown commands inside the container (only if it's running)
-        if machine.status == "running":
-            machine.exec_run(cmd=[Setting.get_instance().device_shell, '-c', shutdown_commands_string],
-                             stdout=False,
-                             stderr=False,
-                             privileged=True,
-                             detach=True
-                             )
-
-        machine.remove(force=True)
+        if container.status == "running":
+            container.exec_run(cmd=[container.labels['shell'], '-c', shutdown_commands_string],
+                               stdout=False,
+                               stderr=False,
+                               privileged=True,
+                               detach=True
+                               )
+
+        container.remove(force=True)
diff --git a/src/Kathara/manager/docker/DockerManager.py b/src/Kathara/manager/docker/DockerManager.py
index ceddbdcb..f0c38025 100644
--- a/src/Kathara/manager/docker/DockerManager.py
+++ b/src/Kathara/manager/docker/DockerManager.py
@@ -15,13 +15,15 @@
 from .stats.DockerMachineStats import DockerMachineStats
 from ... import utils
 from ...decorators import privileged
-from ...exceptions import DockerDaemonConnectionError
+from ...exceptions import DockerDaemonConnectionError, LinkNotFoundError, MachineCollisionDomainError, \
+    InvocationError, LabNotFoundError
 from ...foundation.manager.IManager import IManager
 from ...model.Lab import Lab
-from ...model.Link import BRIDGE_LINK_NAME
+from ...model.Link import Link
 from ...model.Machine import Machine
 from ...setting.Setting import Setting
 from ...utils import pack_files_for_tar
+from ...exceptions import MachineNotFoundError
 
 
 def pywin_import_stub():
@@ -53,9 +55,9 @@ def check_docker(*args, **kw):
         try:
             client.ping()
         except RequestsConnectionError as e:
-            raise DockerDaemonConnectionError("Can not connect to Docker Daemon. %s" % str(e))
+            raise DockerDaemonConnectionError(str(e))
         except pywintypes.error as e:
-            raise DockerDaemonConnectionError("Can not connect to Docker Daemon. %s" % str(e))
+            raise DockerDaemonConnectionError(str(e))
 
     return check_docker
 
@@ -83,6 +85,43 @@ def __init__(self) -> None:
         self.docker_machine: DockerMachine = DockerMachine(self.client, self.docker_image)
         self.docker_link: DockerLink = DockerLink(self.client)
 
+    @privileged
+    def deploy_machine(self, machine: Machine) -> None:
+        """Deploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the specified device is not associated to any network scenario.
+        """
+        if not machine.lab:
+            raise LabNotFoundError("Device `%s` is not associated to a network scenario." % machine.name)
+
+        self.docker_link.deploy_links(machine.lab, selected_links={x.name for x in machine.interfaces.values()})
+        self.docker_machine.deploy_machines(machine.lab, selected_machines={machine.name})
+
+    @privileged
+    def deploy_link(self, link: Link) -> None:
+        """Deploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        if not link.lab:
+            raise LabNotFoundError("Collision domain `%s` is not associated to a network scenario." % link.name)
+
+        self.docker_link.deploy_links(link.lab, selected_links={link.name})
+
     @privileged
     def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         """Deploy a Kathara network scenario.
@@ -93,7 +132,14 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
 
         Returns:
             None
+
+        Raises:
+            MachineNotFoundError: If the specified devices are not in the network scenario.
         """
+        if selected_machines and not lab.find_machines(selected_machines):
+            machines_not_in_lab = selected_machines - set(lab.machines.keys())
+            raise MachineNotFoundError(f"The following devices are not in the network scenario: {machines_not_in_lab}.")
+
         selected_links = None
         if selected_machines:
             selected_links = lab.get_links_from_machines(selected_machines)
@@ -105,30 +151,105 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         self.docker_machine.deploy_machines(lab, selected_machines=selected_machines)
 
     @privileged
-    def update_lab(self, lab: Lab) -> None:
-        """Update a running network scenario.
+    def connect_machine_to_link(self, machine: Machine, link: Link) -> None:
+        """Connect a Kathara device to a collision domain.
 
         Args:
-            lab (Kathara.model.Lab): A Kathara network scenario.
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): A Kathara collision domain object.
 
         Returns:
             None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is already connected to the collision domain.
+        """
+        if not machine.lab:
+            raise LabNotFoundError("Device `%s` is not associated to a network scenario." % machine.name)
+
+        if not link.lab:
+            raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.")
+
+        if machine.name in link.machines:
+            raise MachineCollisionDomainError(
+                f"Device `{machine.name}` is already connected to collision domain `{link.name}`."
+            )
+
+        machine.add_interface(link)
+
+        self.deploy_link(link)
+        self.docker_machine.connect_to_link(machine, link)
+
+    @privileged
+    def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None:
+        """Disconnect a Kathara device from a collision domain.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): The Kathara collision domain from which disconnect the device.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+            MachineCollisionDomainConflictError: If the device is not connected to the collision domain.
         """
-        # Deploy new links (if present)
-        for (_, link) in lab.links.items():
-            if link.name == BRIDGE_LINK_NAME:
-                continue
-
-            self.docker_link.create(link)
-
-        # Update lab devices.
-        for (_, machine) in lab.machines.items():
-            # Device is not deployed, deploy it
-            if machine.api_object is None:
-                self.deploy_lab(lab, selected_machines={machine.name})
-            else:
-                # Device already deployed, update it
-                self.docker_machine.update(machine)
+        if not machine.lab:
+            raise LabNotFoundError(f"Device `{machine.name}` is not associated to a network scenario.")
+
+        if not link.lab:
+            raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.")
+
+        if machine.name not in link.machines:
+            raise MachineCollisionDomainError(
+                f"Device `{machine.name}` is not connected to collision domain `{link.name}`."
+            )
+
+        machine.remove_interface(link)
+
+        self.docker_machine.disconnect_from_link(machine, link)
+        self.undeploy_link(link)
+
+    @privileged
+    def undeploy_machine(self, machine: Machine) -> None:
+        """Undeploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the device specified is not associated to any network scenario.
+        """
+        if not machine.lab:
+            raise LabNotFoundError(f"Device `{machine.name}` is not associated to a network scenario.")
+
+        self.docker_machine.undeploy(machine.lab.hash, selected_machines={machine.name})
+        self.docker_link.undeploy(machine.lab.hash, selected_links={x.name for x in machine.interfaces.values()})
+
+    @privileged
+    def undeploy_link(self, link: Link) -> None:
+        """Undeploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain is not associated to any network scenario.
+        """
+        if not link.lab:
+            raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.")
+
+        self.docker_link.undeploy(link.lab.hash, selected_links={link.name})
 
     @privileged
     def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None,
@@ -146,10 +267,10 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] =
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -196,10 +317,10 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -228,10 +349,10 @@ def exec(self, machine_name: str, command: str, lab_hash: Optional[str] = None,
             Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         user_name = utils.get_current_user_name()
         if lab_name:
@@ -275,10 +396,11 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
             docker.models.containers.Container: Docker API object of devices.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
+            MachineNotFoundError: If the specified device is not found.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         user_name = utils.get_current_user_name() if not all_users else None
         if lab_name:
@@ -290,7 +412,7 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
         if containers:
             return containers.pop()
 
-        raise Exception(f"Device {machine_name} not found.")
+        raise MachineNotFoundError(f"Device `{machine_name}` not found.")
 
     def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \
             List[docker.models.containers.Container]:
@@ -326,11 +448,11 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
             docker.models.networks.Network: Docker API object of the network.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
-            Exception: Collision Domain not found.
+            InvocationError: If a running network scenario hash or name is not specified.
+            LinkNotFoundError: If the collision domain is not found.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         user_name = utils.get_current_user_name() if not all_users else None
         if lab_name:
@@ -342,7 +464,7 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
         if networks:
             return networks.pop()
 
-        raise Exception(f"Collision Domain {link_name} not found.")
+        raise LinkNotFoundError(f"Collision Domain `{link_name}` not found.")
 
     def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \
             List[docker.models.networks.Network]:
@@ -362,6 +484,126 @@ def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_
 
         return self.docker_link.get_links_api_objects_by_filters(lab_hash=lab_hash, user=user_name)
 
+    @privileged
+    def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab:
+        """Return the network scenario (specified by the hash or name), building it from API objects.
+
+        Args:
+            lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name.
+            lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name.
+
+        Returns:
+            Lab: The built network scenario.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+        """
+        if not lab_hash and not lab_name:
+            raise InvocationError("You must specify a running network scenario hash or name.")
+
+        if lab_name:
+            reconstructed_lab = Lab(lab_name)
+        else:
+            reconstructed_lab = Lab("reconstructed_lab")
+            reconstructed_lab.hash = lab_hash
+
+        lab_containers = self.get_machines_api_objects(lab_hash=reconstructed_lab.hash)
+        lab_networks = dict(
+            map(lambda x: (x.name, x), self.get_links_api_objects(lab_hash=reconstructed_lab.hash))
+        )
+
+        for container in lab_containers:
+            container.reload()
+            device = reconstructed_lab.get_or_new_machine(container.labels["name"])
+            device.api_object = container
+
+            # Rebuild device metas
+            # NOTE: We cannot rebuild "exec", "ipv6" and "num_terms" meta.
+            device.add_meta("privileged", container.attrs["HostConfig"]["Privileged"])
+            device.add_meta("image", container.attrs["Config"]["Image"])
+            device.add_meta("shell", container.attrs["Config"]["Labels"]["shell"])
+
+            # Memory is always returned in MBytes
+            if container.attrs['HostConfig']['Memory'] > 0:
+                device.add_meta("mem", f"{int(container.attrs['HostConfig']['Memory'] / (1024 ** 2))}M")
+
+            # Reconvert nanocpus to a value passed by the user
+            if container.attrs["HostConfig"]["NanoCpus"] > 0:
+                device.add_meta("cpu", container.attrs["HostConfig"]["NanoCpus"] / 1000000000)
+
+            for env in container.attrs["Config"]["Env"]:
+                device.add_meta("env", env)
+
+            # Reconvert ports to the device format
+            if container.attrs['HostConfig']['PortBindings']:
+                for port_info, port_data in container.attrs['HostConfig']['PortBindings'].items():
+                    (guest_port, protocol) = port_info.split('/')
+                    host_port = port_data[0]["HostPort"]
+                    device.meta["ports"][(int(host_port), protocol)] = int(guest_port)
+
+            # Reassign sysctls directly
+            device.meta["sysctls"] = container.attrs["HostConfig"]["Sysctls"]
+
+            if "none" not in container.attrs["NetworkSettings"]["Networks"]:
+                for network_name in container.attrs["NetworkSettings"]["Networks"]:
+                    if network_name == "bridge":
+                        device.add_meta("bridged", True)
+                        continue
+
+                    network = lab_networks[network_name]
+                    link = reconstructed_lab.get_or_new_link(network.attrs["Labels"]["name"])
+                    link.api_object = network
+                    device.add_interface(link)
+
+        return reconstructed_lab
+
+    @privileged
+    def update_lab_from_api(self, lab: Lab) -> None:
+        """Update the passed network scenario from API objects.
+
+        Args:
+            lab (Lab): The network scenario to update.
+        """
+        running_containers = self.get_machines_api_objects(lab_hash=lab.hash)
+
+        deployed_networks = dict(
+            map(lambda x: (x.name, x), self.get_links_api_objects(lab_hash=lab.hash))
+        )
+        for network in deployed_networks.values():
+            network.reload()
+
+        deployed_networks_by_link_name = dict(
+            map(lambda x: (x.attrs["Labels"]["name"], x), self.get_links_api_objects(lab_hash=lab.hash))
+        )
+
+        for container in running_containers:
+            container.reload()
+            device = lab.get_or_new_machine(container.labels["name"])
+            device.api_object = container
+
+            # Collision domains declared in the network scenario
+            static_links = set(device.interfaces.values())
+            # Collision domains currently attached to the device
+            current_links = set(
+                map(lambda x: lab.get_or_new_link(deployed_networks[x].attrs["Labels"]["name"]),
+                    filter(lambda x: x != "bridge", container.attrs["NetworkSettings"]["Networks"]))
+            )
+            # Collision domains attached at runtime to the device
+            dynamic_links = current_links - static_links
+            # Static collision domains detached at runtime from the device
+            deleted_links = static_links - current_links
+
+            for link in static_links:
+                if link.name in deployed_networks_by_link_name:
+                    link.api_object = deployed_networks_by_link_name[link.name]
+
+            for link in dynamic_links:
+                link.api_object = deployed_networks_by_link_name[link.name]
+                device.add_interface(link)
+
+            for link in deleted_links:
+                device.remove_interface(link)
+
     @privileged
     def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine_name: str = None,
                            all_users: bool = False) -> Generator[Dict[str, DockerMachineStats], None, None]:
@@ -402,10 +644,10 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None,
             the device info.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -452,10 +694,10 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N
                 statistics.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -476,8 +718,8 @@ def check_image(self, image_name: str) -> None:
             None
 
         Raises:
-            ConnectionError: The image is not locally available and there is no connection to a remote image repository.
-            Exception: The image is not found.
+            ConnectionError: If the image is not locally available and there is no connection to a remote image repository.
+            DockerImageNotFoundError: If the image is not found.
         """
         self.docker_image.check(image_name)
 
diff --git a/src/Kathara/manager/docker/DockerPlugin.py b/src/Kathara/manager/docker/DockerPlugin.py
index 93c3bb4e..b0743e10 100644
--- a/src/Kathara/manager/docker/DockerPlugin.py
+++ b/src/Kathara/manager/docker/DockerPlugin.py
@@ -5,6 +5,7 @@
 from docker.errors import NotFound
 
 from ... import utils
+from ...exceptions import DockerPluginError
 from ...os.Networking import Networking
 from ...setting.Setting import Setting
 
@@ -25,6 +26,10 @@ def check_and_download_plugin(self) -> None:
 
         Returns:
             None
+
+        Raises:
+            DockerPluginError: If the Kathara Network Plugin is not found on remote Docker connection.
+            DockerPluginError: If the Kathara Network Plugin is not enabled on remote Docker connection.
         """
         try:
             logging.debug("Checking plugin `%s`..." % PLUGIN_NAME)
@@ -37,7 +42,7 @@ def check_and_download_plugin(self) -> None:
                 plugin = self.client.plugins.install(PLUGIN_NAME)
                 logging.info("Kathara Network Plugin installed successfully!")
             else:
-                raise Exception("Kathara Network Plugin not found on remote Docker connection.")
+                raise DockerPluginError("Kathara Network Plugin not found on remote Docker connection.")
 
         if Setting.get_instance().remote_url is None:
             xtables_lock_mount = self._get_xtables_lock_mount()
@@ -58,7 +63,7 @@ def check_and_download_plugin(self) -> None:
                     plugin.enable()
         else:
             if not plugin.enabled:
-                raise Exception("Kathara Network Plugin not enabled on remote Docker connection.")
+                raise DockerPluginError("Kathara Network Plugin not enabled on remote Docker connection.")
 
     def _get_xtables_lock_mount(self) -> Callable:
         def _mount_xtables_lock_linux():
diff --git a/src/Kathara/manager/kubernetes/KubernetesConfig.py b/src/Kathara/manager/kubernetes/KubernetesConfig.py
index f40bea6e..65abb7ce 100644
--- a/src/Kathara/manager/kubernetes/KubernetesConfig.py
+++ b/src/Kathara/manager/kubernetes/KubernetesConfig.py
@@ -29,7 +29,7 @@ def load_kube_config() -> None:
             None
 
         Raises:
-            ConnectionError: Cannot read Kubernetes configuration.
+            ConnectionError: If there is an error while reading the Kubernetes configuration.
         """
         try:
             config.load_kube_config()  # Try to load configuration if Megalos is launched on a k8s master.
diff --git a/src/Kathara/manager/kubernetes/KubernetesConfigMap.py b/src/Kathara/manager/kubernetes/KubernetesConfigMap.py
index c7da3b3e..3dde75da 100644
--- a/src/Kathara/manager/kubernetes/KubernetesConfigMap.py
+++ b/src/Kathara/manager/kubernetes/KubernetesConfigMap.py
@@ -3,7 +3,9 @@
 
 from kubernetes import client
 from kubernetes.client.api import core_v1_api
+from kubernetes.client.rest import ApiException
 
+from ...exceptions import KubernetesConfigMapError
 from ...model.Machine import Machine
 from ...utils import human_readable_bytes
 
@@ -34,7 +36,7 @@ def deploy_for_machine(self, machine: Machine) -> Optional[client.V1ConfigMap]:
         return self.client.create_namespaced_config_map(body=config_map, namespace=machine.lab.hash)
 
     def delete_for_machine(self, machine_name: str, machine_namespace: str) -> None:
-        """Delete the Kubernetes ConfigMap associated with the device.
+        """Delete the Kubernetes ConfigMap associated with the device, if it exists.
 
         Args:
             machine_name (str): The name of a Kathara device.
@@ -43,9 +45,12 @@ def delete_for_machine(self, machine_name: str, machine_namespace: str) -> None:
         Returns:
             None
         """
-        self.client.delete_namespaced_config_map(name=self.build_name_for_machine(machine_name, machine_namespace),
-                                                 namespace=machine_namespace
-                                                 )
+        try:
+            self.client.delete_namespaced_config_map(name=self.build_name_for_machine(machine_name, machine_namespace),
+                                                     namespace=machine_namespace
+                                                     )
+        except ApiException:
+            return
 
     @staticmethod
     def build_name_for_machine(machine_name: str, machine_namespace: str) -> str:
@@ -68,6 +73,9 @@ def _build_for_machine(self, machine: Machine) -> Optional[client.V1ConfigMap]:
 
         Returns:
             Optional[client.V1ConfigMap]: The Kubernetes ConfigMap for the device.
+
+        Raises:
+            KubernetesConfigMapError: If the device folder size exceeds the maximum supported size.
         """
         tar_data = machine.pack_data()
 
@@ -77,7 +85,7 @@ def _build_for_machine(self, machine: Machine) -> Optional[client.V1ConfigMap]:
             # Before creating the .tar.gz file, check if it is bigger than the maximum allowed size.
             tar_data_size = len(tar_data)
             if tar_data_size > MAX_FILE_SIZE:
-                raise Exception(
+                raise KubernetesConfigMapError(
                     'Unable to upload device folder. Maximum supported size: %s. Current: %s.' % (
                         human_readable_bytes(MAX_FILE_SIZE),
                         human_readable_bytes(tar_data_size)
diff --git a/src/Kathara/manager/kubernetes/KubernetesLink.py b/src/Kathara/manager/kubernetes/KubernetesLink.py
index 56722c73..02d1b213 100644
--- a/src/Kathara/manager/kubernetes/KubernetesLink.py
+++ b/src/Kathara/manager/kubernetes/KubernetesLink.py
@@ -15,6 +15,7 @@
 from .stats.KubernetesLinkStats import KubernetesLinkStats
 from ... import utils
 from ...event.EventDispatcher import EventDispatcher
+from ...exceptions import LinkNotFoundError
 from ...model.Lab import Lab
 from ...model.Link import Link
 from ...setting.Setting import Setting
@@ -37,17 +38,18 @@ def __init__(self, kubernetes_namespace: KubernetesNamespace) -> None:
 
         self.seed: str = KubernetesConfig.get_cluster_user()
 
-    def deploy_links(self, lab: Lab, selected_links: Dict[str, Link] = None) -> None:
+    def deploy_links(self, lab: Lab, selected_links: Set[str] = None) -> None:
         """Deploy all the links contained in lab.links.
 
         Args:
             lab (Kathara.model.Lab.Lab): A Kathara network scenario.
-            selected_links (Dict[str, Link]): Keys are collision domains names, values are Link objects.
+            selected_links (Set[str]): A set containing the name of the collision domains to deploy.
 
         Returns:
             None
         """
-        links = selected_links.items() if selected_links else lab.links.items()
+        links = {k: v for (k, v) in lab.links.items() if k in selected_links}.items() if selected_links \
+            else lab.links.items()
 
         if len(links) > 0:
             pool_size = utils.get_pool_size()
@@ -110,19 +112,19 @@ def create(self, link: Link, network_id: int) -> None:
         if link.external:
             logging.warning('External is not supported on Megalos. It will be ignored.')
 
-    def undeploy(self, lab_hash: str, networks_to_delete: Optional[Set] = None) -> None:
+    def undeploy(self, lab_hash: str, selected_links: Optional[Set[str]] = None) -> None:
         """Undeploy all the links of the scenario specified by lab_hash.
 
         Args:
             lab_hash (str): The hash of the network scenario to undeploy.
-            networks_to_delete (Set): If specified, delete only the networks that contained.
+            selected_links (Set[str]): If specified, delete only the collision domains contained in the set.
 
         Returns:
             None
         """
         networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash)
-        if networks_to_delete is not None and len(networks_to_delete) > 0:
-            networks = [item for item in networks if item["metadata"]["name"] in networks_to_delete]
+        if selected_links is not None and len(selected_links) > 0:
+            networks = [item for item in networks if item["metadata"]["name"] in selected_links]
 
         if len(networks) > 0:
             pool_size = utils.get_pool_size()
@@ -223,14 +225,17 @@ def get_links_stats(self, lab_hash: str = None, link_name: str = None) -> \
         Returns:
            Generator[Dict[str, KubernetesLinkStats], None, None]: A generator containing network names as keys and
                 KubernetesLinkStats as values.
+
+        Raises:
+            LinkNotFoundError: If the collision domains specified are not found.
         """
         while True:
             networks = self.get_links_api_objects_by_filters(lab_hash=lab_hash, link_name=link_name)
             if not networks:
                 if not link_name:
-                    raise Exception("No collision domains found.")
+                    raise LinkNotFoundError("No collision domains found.")
                 else:
-                    raise Exception(f"Collision domains with name {link_name} not found.")
+                    raise LinkNotFoundError(f"Collision domains with name {link_name} not found.")
 
             networks_stats = {}
 
diff --git a/src/Kathara/manager/kubernetes/KubernetesMachine.py b/src/Kathara/manager/kubernetes/KubernetesMachine.py
index 8f2702a9..7e5f5c74 100644
--- a/src/Kathara/manager/kubernetes/KubernetesMachine.py
+++ b/src/Kathara/manager/kubernetes/KubernetesMachine.py
@@ -19,7 +19,7 @@
 from .stats.KubernetesMachineStats import KubernetesMachineStats
 from ... import utils
 from ...event.EventDispatcher import EventDispatcher
-from ...exceptions import MachineAlreadyExistsError
+from ...exceptions import MachineAlreadyExistsError, MachineNotFoundError, MachineNotReadyError
 from ...model.Lab import Lab
 from ...model.Machine import Machine
 from ...setting.Setting import Setting
@@ -54,7 +54,10 @@
     "(cd /hostlab/{machine_name} && tar c .) | (cd / && tar xhf -); fi",
 
     # If /etc/hosts is not configured by the user, add the localhost mapping
-    "if [ ! -s \"/etc/hosts\" ]; then echo '127.0.0.1 localhost' > /etc/hosts; fi",
+    "if [ ! -s \"/etc/hosts\" ]; then "
+    "echo '127.0.0.1 localhost' > /etc/hosts",
+    "echo '::1 localhost' >> /etc/hosts",
+    "fi",
 
     # Give proper permissions to /var/www
     "if [ -d \"/var/www\" ]; then "
@@ -183,6 +186,9 @@ def create(self, machine: Machine) -> None:
 
         Returns:
             None
+
+        Raises:
+            MachineAlreadyExistsError: If a device with the name specified already exists.
         """
         logging.debug("Creating device `%s`..." % machine.name)
 
@@ -223,12 +229,11 @@ def create(self, machine: Machine) -> None:
                                                                           )
         except ApiException as e:
             if e.status == 409 and 'Conflict' in e.reason:
-                raise MachineAlreadyExistsError("Device with name `%s` already exists." % machine.name)
+                raise MachineAlreadyExistsError(machine.name)
             else:
                 raise e
 
-    @staticmethod
-    def _build_definition(machine: Machine, config_map: client.V1ConfigMap) -> client.V1Deployment:
+    def _build_definition(self, machine: Machine, config_map: client.V1ConfigMap) -> client.V1Deployment:
         """Return a Kubernetes deployment from a Kathara device and a Kubernetes ConfigMap.
 
         Args:
@@ -275,6 +280,8 @@ def _build_definition(machine: Machine, config_map: client.V1ConfigMap) -> clien
 
             resources = client.V1ResourceRequirements(limits=limits)
 
+        shell = machine.meta["shell"] if "shell" in machine.meta else Setting.get_instance().device_shell
+
         # postStart lifecycle hook is launched asynchronously by k8s master when the main container is Ready
         # On Ready state, the pod has volumes and network interfaces up, so this hook is used
         # to execute custom commands coming from .startup file and "exec" option
@@ -288,17 +295,14 @@ def _build_definition(machine: Machine, config_map: client.V1ConfigMap) -> clien
 
         post_start = client.V1LifecycleHandler(
             _exec=client.V1ExecAction(
-                command=[Setting.get_instance().device_shell, "-c", startup_commands_string]
+                command=[shell, "-c", startup_commands_string]
             )
         )
         lifecycle = client.V1Lifecycle(post_start=post_start)
 
         env = [
-            client.V1EnvVar("_MEGALOS_SHELL",
-                            machine.meta["shell"] if "shell" in machine.meta else Setting.get_instance().device_shell
-                            )
+            client.V1EnvVar("_MEGALOS_SHELL", shell)
         ]
-
         for env_var_name, env_var_value in machine.meta['envs'].items():
             env.append(client.V1EnvVar(env_var_name, env_var_value))
 
@@ -456,9 +460,11 @@ def _delete_machine(self, pod_api_object: client.V1Pod) -> None:
         shutdown_commands_string = "; ".join(SHUTDOWN_COMMANDS).format(machine_name=machine_name)
 
         try:
+            shell_env_value = self.get_env_var_value_from_pod(pod_api_object, "_MEGALOS_SHELL")
+            shell = shell_env_value if shell_env_value else Setting.get_instance().device_shell
             output = self.exec(machine_namespace,
                                machine_name,
-                               command=[Setting.get_instance().device_shell, '-c', shutdown_commands_string],
+                               command=[shell, '-c', shutdown_commands_string],
                                )
 
             try:
@@ -487,14 +493,17 @@ def connect(self, lab_hash: str, machine_name: str, shell: Union[str, List[str]]
 
         Returns:
             None
+
+        Raises:
+            MachineNotReadyError: If the device is not ready.
         """
         pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name)
         if not pods:
-            raise Exception("The specified device `%s` is not running." % machine_name)
+            raise MachineNotFoundError("The specified device `%s` is not running." % machine_name)
         deployment = pods.pop()
 
         if 'Running' not in deployment.status.phase:
-            raise Exception('Device `%s` is not ready.' % machine_name)
+            raise MachineNotReadyError(machine_name)
 
         if not shell:
             shell_env_value = self.get_env_var_value_from_pod(deployment, "_MEGALOS_SHELL")
@@ -590,7 +599,7 @@ def exec(self, lab_hash: str, machine_name: str, command: Union[str, List], tty:
             # Retrieve the pod of current Deployment
             pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name)
             if not pods:
-                raise Exception("The specified device `%s` is not running." % machine_name)
+                raise MachineNotFoundError("The specified device `%s` is not running." % machine_name)
             pod = pods.pop()
 
             response = stream(self.core_client.connect_get_namespaced_pod_exec,
@@ -710,9 +719,9 @@ def get_machines_stats(self, lab_hash: str = None, machine_name: str = None) ->
             pods = self.get_machines_api_objects_by_filters(lab_hash=lab_hash, machine_name=machine_name)
             if not pods:
                 if not machine_name:
-                    raise Exception("No devices found.")
+                    raise MachineNotFoundError("No devices found.")
                 else:
-                    raise Exception(f"Devices with name {machine_name} not found.")
+                    raise MachineNotFoundError(f"Devices with name {machine_name} not found.")
 
             machines_stats = {}
 
diff --git a/src/Kathara/manager/kubernetes/KubernetesManager.py b/src/Kathara/manager/kubernetes/KubernetesManager.py
index 03b7d65a..4be419f6 100644
--- a/src/Kathara/manager/kubernetes/KubernetesManager.py
+++ b/src/Kathara/manager/kubernetes/KubernetesManager.py
@@ -13,9 +13,11 @@
 from .stats.KubernetesLinkStats import KubernetesLinkStats
 from .stats.KubernetesMachineStats import KubernetesMachineStats
 from ... import utils
-from ...exceptions import NotSupportedError
+from ...exceptions import NotSupportedError, MachineNotFoundError, LinkNotFoundError, LabAlreadyExistsError, \
+    InvocationError, LabNotFoundError
 from ...foundation.manager.IManager import IManager
 from ...model.Lab import Lab
+from ...model.Link import Link
 from ...model.Machine import Machine
 from ...utils import pack_files_for_tar
 
@@ -32,6 +34,47 @@ def __init__(self) -> None:
         self.k8s_machine: KubernetesMachine = KubernetesMachine(self.k8s_namespace)
         self.k8s_link: KubernetesLink = KubernetesLink(self.k8s_namespace)
 
+    def deploy_machine(self, machine: Machine) -> None:
+        """Deploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the specified device is not associated to any network scenario.
+        """
+        if not machine.lab:
+            raise LabNotFoundError("Machine `%s` is not associated to a network scenario." % machine.name)
+
+        machine.lab.hash = machine.lab.hash.lower()
+
+        self.k8s_namespace.create(machine.lab)
+        self.k8s_link.deploy_links(machine.lab, selected_links={x.name for x in machine.interfaces.values()})
+        self.k8s_machine.deploy_machines(machine.lab, selected_machines={machine.name})
+
+    def deploy_link(self, link: Link) -> None:
+        """Deploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the collision domain specified is not associated to any network scenario.
+        """
+        if not link.lab:
+            raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.")
+
+        link.lab.hash = link.lab.hash.lower()
+
+        self.k8s_namespace.create(link.lab)
+        self.k8s_link.deploy_links(link.lab, selected_links={link.name})
+
     def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
         """Deploy a Kathara network scenario.
 
@@ -41,7 +84,16 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
 
         Returns:
             None
+
+        Raises:
+            MachineNotFoundError: If the specified devices are not in the network scenario specified.
+            LabAlreadyExistsError: If a network scenario is deployed while it is terminating its execution.
+            ApiError: If the Kubernetes APIs throw an exception.
         """
+        if selected_machines and not lab.find_machines(selected_machines):
+            machines_not_in_lab = selected_machines - set(lab.machines.keys())
+            raise MachineNotFoundError(f"The following devices are not in the network scenario: {machines_not_in_lab}.")
+
         # Kubernetes needs only lowercase letters for resources.
         # We force the hash to be lowercase
         lab.hash = lab.hash.lower()
@@ -57,20 +109,111 @@ def deploy_lab(self, lab: Lab, selected_machines: Set[str] = None) -> None:
             self.k8s_machine.deploy_machines(lab, selected_machines=selected_machines)
         except ApiException as e:
             if e.status == 403 and 'Forbidden' in e.reason:
-                raise Exception("Previous lab execution is still terminating. Please wait.")
+                raise LabAlreadyExistsError("Previous lab execution is still terminating. Please wait.")
             else:
                 raise e
 
-    def update_lab(self, lab: Lab) -> None:
-        """Update a running network scenario.
+    def connect_machine_to_link(self, machine: Machine, link: Link) -> None:
+        """Connect a Kathara device to a collision domain.
 
         Args:
-            lab (Kathara.model.Lab): A Kathara network scenario.
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
+
+        Raises:
+            NotSupportedError: Unable to update a running device on Kubernetes.
+        """
+        raise NotSupportedError("Unable to update a running device.")
+
+    def disconnect_machine_from_link(self, machine: Machine, link: Link) -> None:
+        """Disconnect a Kathara device from a collision domain.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+            link (Kathara.model.Link): The Kathara collision domain from which disconnect the device.
+
+        Returns:
+            None
+
+        Raises:
+            NotSupportedError: Unable to update a running device on Kubernetes.
+        """
+        raise NotSupportedError("Unable to update a running device.")
+
+    def undeploy_machine(self, machine: Machine) -> None:
+        """Undeploy a Kathara device.
+
+        Args:
+            machine (Kathara.model.Machine): A Kathara machine object.
+
+        Returns:
+            None
+
+        Raises:
+            LabNotFoundError: If the specified machine is not associated to a network scenario.
+        """
+        if not machine.lab:
+            raise LabNotFoundError(f"Machine `{machine.name}` is not associated to a network scenario.")
+
+        machine.lab.hash = machine.lab.hash.lower()
+
+        # Get all current running machines (not Terminating), removing the one that we're undeploying
+        running_machines = [running_machine for running_machine in
+                            self.k8s_machine.get_machines_api_objects_by_filters(lab_hash=machine.lab.hash)
+                            if 'Terminating' not in running_machine.status.phase and
+                            running_machine.metadata.labels["name"] != machine.name]
+
+        # From machines, save a set with all the attached networks (still needed)
+        running_networks = set()
+        for running_machine in running_machines:
+            network_annotation = json.loads(running_machine.metadata.annotations["k8s.v1.cni.cncf.io/networks"])
+            running_networks.update([net['name'] for net in network_annotation])
+
+        # Difference between all networks of the machine to undeploy, and attached networks are the ones to delete
+        machine_networks = {self.k8s_link.get_network_name(x.name) for x in machine.interfaces.values()}
+        networks_to_delete = machine_networks - running_networks
+
+        self.k8s_machine.undeploy(machine.lab.hash, selected_machines={machine.name})
+        self.k8s_link.undeploy(machine.lab.hash, selected_links=networks_to_delete)
+
+        if len(running_machines) <= 0:
+            self.k8s_namespace.undeploy(lab_hash=machine.lab.hash)
+
+    def undeploy_link(self, link: Link) -> None:
+        """Undeploy a Kathara collision domain.
+
+        Args:
+            link (Kathara.model.Link): A Kathara collision domain object.
+
+        Returns:
+            None
 
         Raises:
-            NotSupportedError: "Unable to update a running lab."
+            LabNotFoundError: If the collision domain specified is not associated to any network scenario.
         """
-        raise NotSupportedError("Unable to update a running lab.")
+        if not link.lab:
+            raise LabNotFoundError(f"Collision domain `{link.name}` is not associated to a network scenario.")
+
+        link.lab.hash = link.lab.hash.lower()
+
+        network_name = self.k8s_link.get_network_name(link.name)
+
+        # Get all current running machines (not Terminating)
+        running_machines = [running_machine for running_machine in
+                            self.k8s_machine.get_machines_api_objects_by_filters(lab_hash=link.lab.hash)
+                            if 'Terminating' not in running_machine.status.phase]
+
+        # From machines, save a set with all the attached networks (still needed)
+        for running_machine in running_machines:
+            network_annotation = json.loads(running_machine.metadata.annotations["k8s.v1.cni.cncf.io/networks"])
+            # If the collision domain to undeploy is still used, do nothing
+            if network_name in [net['name'] for net in network_annotation]:
+                return
+
+        self.k8s_link.undeploy(link.lab.hash, selected_links={network_name})
 
     def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] = None,
                      selected_machines: Optional[Set[str]] = None) -> None:
@@ -87,10 +230,10 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] =
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -128,7 +271,7 @@ def undeploy_lab(self, lab_hash: Optional[str] = None, lab_name: Optional[str] =
             running_machines = set()
 
         self.k8s_machine.undeploy(lab_hash, selected_machines=selected_machines)
-        self.k8s_link.undeploy(lab_hash, networks_to_delete=networks_to_delete)
+        self.k8s_link.undeploy(lab_hash, selected_links=networks_to_delete)
 
         # If no machines are selected or there are no running machines, undeploy the namespace
         if not selected_machines or len(running_machines - selected_machines) <= 0:
@@ -167,10 +310,10 @@ def connect_tty(self, machine_name: str, lab_hash: Optional[str] = None, lab_nam
             None
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -197,10 +340,10 @@ def exec(self, machine_name: str, command: str, lab_hash: Optional[str] = None,
             Generator[Tuple[bytes, bytes]]: A generator of tuples containing the stdout and stderr in bytes.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -238,12 +381,16 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
 
         Returns:
             client.V1Pod: A Kubernetes Pod.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            MachineNotFoundError: If the device is not found.
         """
         if all_users:
             logging.warning("User-specific options have no effect on Megalos.")
 
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -254,7 +401,7 @@ def get_machine_api_object(self, machine_name: str, lab_hash: str = None, lab_na
         if pods:
             return pods.pop()
 
-        raise Exception(f"Device {machine_name} not found.")
+        raise MachineNotFoundError(f"Device {machine_name} not found.")
 
     def get_machines_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \
             List[client.V1Pod]:
@@ -292,12 +439,16 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
 
         Returns:
             Any: Kubernetes API object of the network.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+            LinkNotFoundError: If the collision domain is not found.
         """
         if all_users:
             logging.warning("User-specific options have no effect on Megalos.")
 
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -308,7 +459,7 @@ def get_link_api_object(self, link_name: str, lab_hash: str = None, lab_name: st
         if networks:
             return networks.pop()
 
-        raise Exception(f"Collision Domain {link_name} not found.")
+        raise LinkNotFoundError(f"Collision Domain {link_name} not found.")
 
     def get_links_api_objects(self, lab_hash: str = None, lab_name: str = None, all_users: bool = False) -> \
             List[Any]:
@@ -356,6 +507,79 @@ def get_machines_stats(self, lab_hash: str = None, lab_name: str = None, machine
 
         return self.k8s_machine.get_machines_stats(lab_hash=lab_hash, machine_name=machine_name)
 
+    def get_lab_from_api(self, lab_hash: str = None, lab_name: str = None) -> Lab:
+        """Return the network scenario (specified by the hash or name), building it from API objects.
+
+        Args:
+            lab_hash (str): The hash of the network scenario. Can be used as an alternative to lab_name.
+            lab_name (str): The name of the network scenario. Can be used as an alternative to lab_name.
+
+        Returns:
+            Lab: The built network scenario.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
+        """
+        if not lab_hash and not lab_name:
+            raise InvocationError("You must specify a running network scenario hash or name.")
+
+        if lab_name:
+            reconstructed_lab = Lab(lab_name)
+        else:
+            reconstructed_lab = Lab("reconstructed_lab")
+            reconstructed_lab.hash = lab_hash
+
+        lab_pods = self.get_machines_api_objects(lab_hash=reconstructed_lab.hash)
+        lab_networks = dict(
+            map(lambda x: (x['metadata']['name'], x), self.get_links_api_objects(lab_hash=reconstructed_lab.hash))
+        )
+
+        for pod in lab_pods:
+            device = reconstructed_lab.get_or_new_machine(pod.metadata.labels["name"])
+            device.api_object = pod
+
+            # Rebuild device metas
+            # NOTE: "privileged" and "bridged" are not supported on Megalos
+            # NOTE: We cannot rebuild "sysctls", "exec", "ipv6" and "num_terms" meta.
+            container = pod.spec.containers[0]
+            device.add_meta("image", container.image.replace('docker.io/', ''))
+            device.add_meta("shell", self.k8s_machine.get_env_var_value_from_pod(pod, "_MEGALOS_SHELL"))
+
+            if container.resources.limits and 'memory' in container.resources.limits:
+                device.add_meta("mem", container.resources.limits['memory'].upper())
+
+            # Reconvert mcpus to a value passed by the user
+            if container.resources.limits and 'cpu' in container.resources.limits:
+                device.add_meta("cpu", int(container.resources.limits['cpu'].replace('m', '')) / 1000)
+
+            for env in container.env:
+                if env.name != "_MEGALOS_SHELL":
+                    device.meta["envs"][env.name] = env.value
+
+            # Reconvert ports to the device format
+            if container.ports:
+                for port in container.ports:
+                    device.meta["ports"][(port.host_port, port.protocol.lower())] = port.container_port
+
+            for network_conf in json.loads(pod.metadata.annotations["k8s.v1.cni.cncf.io/networks"]):
+                network = lab_networks[network_conf['name']]
+                link = reconstructed_lab.get_or_new_link(network['metadata']['labels']['name'])
+                link.api_object = network
+                device.add_interface(link)
+
+        return reconstructed_lab
+
+    def update_lab_from_api(self, lab: Lab) -> None:
+        """Update the passed network scenario from API objects.
+
+        Args:
+            lab (Lab): The network scenario to update.
+
+        Raises:
+            NotSupportedError: Unable to update a running network scenario on Kubernetes.
+        """
+        raise NotSupportedError("Unable to update a running network scenario.")
+
     def get_machine_stats(self, machine_name: str, lab_hash: str = None, lab_name: str = None,
                           all_users: bool = False) -> Generator[KubernetesMachineStats, None, None]:
         """Return information of the specified device in a specified network scenario.
@@ -370,9 +594,12 @@ def get_machine_stats(self, machine_name: str, lab_hash: str = None, lab_name: s
 
         Returns:
             KubernetesMachineStats: KubernetesMachineStats object containing the device info.
+
+        Raises:
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
@@ -425,10 +652,10 @@ def get_link_stats(self, link_name: str, lab_hash: str = None, lab_name: str = N
                 the network statistics.
 
         Raises:
-            Exception: You must specify a running network scenario hash or name.
+            InvocationError: If a running network scenario hash or name is not specified.
         """
         if not lab_hash and not lab_name:
-            raise Exception("You must specify a running network scenario hash or name.")
+            raise InvocationError("You must specify a running network scenario hash or name.")
 
         if lab_name:
             lab_hash = utils.generate_urlsafe_hash(lab_name)
diff --git a/src/Kathara/model/Lab.py b/src/Kathara/model/Lab.py
index 35449f85..64a5d006 100644
--- a/src/Kathara/model/Lab.py
+++ b/src/Kathara/model/Lab.py
@@ -1,12 +1,13 @@
 import collections
 import os
 from itertools import chain
-from typing import Dict, Set, Any, List, Union, Optional
+from typing import Dict, Set, Any, List, Union, Optional, Tuple
 
 from . import Machine as MachinePackage
 from .ExternalLink import ExternalLink
 from .Link import Link
 from .. import utils
+from ..exceptions import LinkNotFoundError, MachineNotFoundError, MachineAlreadyExistsError, LinkAlreadyExistsError
 
 
 class Lab(object):
@@ -31,11 +32,11 @@ class Lab(object):
         shared_shutdown_path(str) The path of the shared shutdown file, if exists.
         shared_folder(str) The path of the shared folder, if exists.
     """
-    __slots__ = ['name', 'description', 'version', 'author', 'email', 'web',
+    __slots__ = ['_name', 'description', 'version', 'author', 'email', 'web',
                  'path', 'hash', 'machines', 'links', 'general_options', 'has_dependencies',
                  'shared_startup_path', 'shared_shutdown_path', 'shared_folder']
 
-    def __init__(self, name: Optional[str], path: str = None) -> None:
+    def __init__(self, name: Optional[str], path: Optional[str] = None) -> None:
         """Create a new instance of a Kathara network scenario.
 
         Args:
@@ -45,7 +46,7 @@ def __init__(self, name: Optional[str], path: str = None) -> None:
         Returns:
             None
         """
-        self.name: Optional[str] = name
+        self._name: Optional[str] = name
         self.description: Optional[str] = None
         self.version: Optional[str] = None
         self.author: Optional[str] = None
@@ -63,21 +64,28 @@ def __init__(self, name: Optional[str], path: str = None) -> None:
         self.shared_startup_path: Optional[str] = None
         self.shared_shutdown_path: Optional[str] = None
 
-        self.hash: str = ""
+        self.hash: str = utils.generate_urlsafe_hash(self.path if self._name is None else self._name)
 
         if self.path:
-            self.hash = utils.generate_urlsafe_hash(self.path)
             shared_startup_file = os.path.join(self.path, 'shared.startup')
             self.shared_startup_path = shared_startup_file if os.path.exists(shared_startup_file) else None
 
             shared_shutdown_file = os.path.join(self.path, 'shared.shutdown')
             self.shared_shutdown_path = shared_shutdown_file if os.path.exists(shared_shutdown_file) else None
-        else:
-            self.hash = utils.generate_urlsafe_hash(self.name)
 
         self.shared_folder: Optional[str] = None
 
-    def connect_machine_to_link(self, machine_name: str, link_name: str, machine_iface_number: int = None) -> None:
+    @property
+    def name(self):
+        return self._name
+
+    @name.setter
+    def name(self, value):
+        self._name = value
+        self.hash = utils.generate_urlsafe_hash(value)
+
+    def connect_machine_to_link(self, machine_name: str, link_name: str, machine_iface_number: int = None) \
+            -> Tuple['MachinePackage.Machine', Link]:
         """Connect the specified device to the specified collision domain.
 
         Args:
@@ -87,7 +95,8 @@ def connect_machine_to_link(self, machine_name: str, link_name: str, machine_ifa
                 number is used.
 
         Returns:
-            None
+            Tuple[Kathara.model.Machine, Kathara.model.Link]: A tuple containing the Kathara device and collision domain
+                specified by their names.
 
         Raises:
             Exception: If an already used interface number is specified.
@@ -97,7 +106,9 @@ def connect_machine_to_link(self, machine_name: str, link_name: str, machine_ifa
 
         machine.add_interface(link, number=machine_iface_number)
 
-    def assign_meta_to_machine(self, machine_name: str, meta_name: str, meta_value: str) -> None:
+        return machine, link
+
+    def assign_meta_to_machine(self, machine_name: str, meta_name: str, meta_value: str) -> 'MachinePackage.Machine':
         """Assign meta information to the specified device.
 
         Args:
@@ -106,7 +117,7 @@ def assign_meta_to_machine(self, machine_name: str, meta_name: str, meta_value:
             meta_value (str): The value of the meta property.
 
         Returns:
-            None
+            Kathara.model.Machine: The Kathara device specified by the name.
 
         Raises:
             MachineOptionError: If invalid values are specified for meta properties.
@@ -115,6 +126,8 @@ def assign_meta_to_machine(self, machine_name: str, meta_name: str, meta_value:
 
         machine.add_meta(meta_name, meta_value)
 
+        return machine
+
     def attach_external_links(self, external_links: Dict[str, List[ExternalLink]]) -> None:
         """Attach external collision domains to the network scenario.
 
@@ -124,11 +137,14 @@ def attach_external_links(self, external_links: Dict[str, List[ExternalLink]]) -
 
         Returns:
             None
+
+        Raises:
+            LinkNotFoundError: If the external collision domain specified is not associated to the network scenario.
         """
         for (link_name, link_external_links) in external_links.items():
             if link_name not in self.links:
-                raise Exception("Collision domain `%s` (declared in lab.ext) not found in lab "
-                                "collision domains." % link_name)
+                raise LinkNotFoundError("Collision domain `%s` (declared in lab.ext) not found in lab "
+                                        "collision domains." % link_name)
 
             self.links[link_name].external += link_external_links
 
@@ -141,14 +157,14 @@ def check_integrity(self) -> None:
         for machine in self.machines:
             self.machines[machine].check()
 
-    def get_links_from_machines(self, selected_machines: Union[List[str], Set[str]]) -> Dict[str, Link]:
-        """Return the name of the collision domains connected to the selected_machines.
+    def get_links_from_machines(self, selected_machines: Union[List[str], Set[str]]) -> Set[str]:
+        """Return the name of the collision domains connected to the selected devices.
 
         Args:
             selected_machines (Set[str]): A set with selected devices names.
 
         Returns:
-            Dict[str, Link]: Keys are collision domains names, values are Link objects.
+            Set[str]: A set of names of collision domains to deploy.
         """
         # Intersect selected machines names with self.machines keys
         selected_machines = set(self.machines.keys()) & set(selected_machines)
@@ -157,7 +173,7 @@ def get_links_from_machines(self, selected_machines: Union[List[str], Set[str]])
 
         # Get only selected machines Link objects.
         selected_links = set(chain.from_iterable([machine.interfaces.values() for machine in machines]))
-        selected_links = {link.name: link for link in selected_links}
+        selected_links = {link.name for link in selected_links}
 
         return selected_links
 
@@ -170,6 +186,7 @@ def apply_dependencies(self, dependencies: List[str]) -> None:
         Returns:
             None
         """
+
         def dep_sort(item: str) -> int:
             try:
                 return dependencies.index(item) + 1
@@ -179,6 +196,44 @@ def dep_sort(item: str) -> int:
         self.machines = collections.OrderedDict(sorted(self.machines.items(), key=lambda t: dep_sort(t[0])))
         self.has_dependencies = True
 
+    def get_machine(self, name: str) -> 'MachinePackage.Machine':
+        """Get the specified device.
+
+        Args:
+            name (str): The name of the device
+
+        Returns:
+            Kathara.model.Machine: A Kathara device.
+
+        Raises:
+            MachineNotFoundError: If the device is not in the network scenario.
+        """
+        if name not in self.machines:
+            raise MachineNotFoundError(f"Device {name} not in the network scenario.")
+
+        return self.machines[name]
+
+    def new_machine(self, name: str, **kwargs: Dict[str, Any]) -> 'MachinePackage.Machine':
+        """Create and add the device to the devices list.
+
+        Args:
+            name (str): The name of the device
+            **kwargs (Dict[str, Any]): Contains device meta information.
+                Keys are meta property names, values are meta property values.
+
+        Returns:
+            Kathara.model.Machine: A Kathara device.
+
+        Raises:
+            MachineAlreadyExistsError: If the device is already in the network scenario.
+        """
+        if name in self.machines:
+            raise MachineAlreadyExistsError(f"Device {name} already in the network scenario.")
+
+        self.machines[name] = MachinePackage.Machine(self, name, **kwargs)
+
+        return self.machines[name]
+
     def get_or_new_machine(self, name: str, **kwargs: Dict[str, Any]) -> 'MachinePackage.Machine':
         """Get the specified device. If it not exists, create and add it to the devices list.
 
@@ -195,6 +250,42 @@ def get_or_new_machine(self, name: str, **kwargs: Dict[str, Any]) -> 'MachinePac
 
         return self.machines[name]
 
+    def get_link(self, name: str) -> Link:
+        """Get the specified collision domain.
+
+        Args:
+            name (str): The name of the collision domain.
+
+        Returns:
+            Kathara.model.Link: A Kathara collision domain.
+
+        Raises:
+            LinkNotFoundError: If the specified link is not in the network scenario.
+        """
+        if name not in self.links:
+            raise LinkNotFoundError(f"Collision domain {name} not found in the network scenario.")
+
+        return self.links[name]
+
+    def new_link(self, name: str) -> Link:
+        """Create the collision domain and add it to the collision domains list.
+
+        Args:
+            name (str): The name of the collision domain.
+
+        Returns:
+            Kathara.model.Link: A Kathara collision domain.
+
+        Raises:
+            LinkAlreadyExistsError: If the specified link is already in the network scenario.
+        """
+        if name in self.links:
+            raise LinkAlreadyExistsError(f"Collision domain {name} is already the network scenario.")
+
+        self.links[name] = Link(self, name)
+
+        return self.links[name]
+
     def get_or_new_link(self, name: str) -> Link:
         """Get the specified collision domain. If it not exists, create and add it to the collision domains list.
 
@@ -216,8 +307,8 @@ def create_shared_folder(self) -> None:
             None
 
         Raises:
-            Exception: The shared folder is a Symlink, delete it.
-            OSError: Permission error.
+            IOError: If the shared folder is a Symlink, delete it.
+            OSError: If there is a permission error.
         """
         if not self.has_path():
             return
@@ -226,7 +317,7 @@ def create_shared_folder(self) -> None:
             if not os.path.isdir(self.shared_folder):
                 os.mkdir(self.shared_folder)
             elif os.path.islink(self.shared_folder):
-                raise Exception("`shared` folder is a symlink, delete it.")
+                raise IOError("`shared` folder is a symlink, delete it.")
         except OSError:
             # Do not create shared folder if not permitted.
             return
@@ -235,7 +326,7 @@ def has_path(self) -> bool:
         """Check if the network scenario has a directory.
 
         Returns:
-            bool: True if it self.path is not None, else False.
+            bool: True if self.path is not None, else False.
         """
         return self.path is not None
 
@@ -252,14 +343,36 @@ def add_option(self, name: str, value: Any) -> None:
         if value is not None:
             self.general_options[name] = value
 
+    def find_machine(self, machine_name: str) -> bool:
+        """Check if the specified device is in the network scenario.
+
+        Args:
+            machine_name (str): The name of the device to search.
+
+        Returns:
+            bool: True if the device is in the network scenario, else False.
+        """
+        return machine_name in self.machines.keys()
+
+    def find_machines(self, machine_names: Set[str]) -> bool:
+        """Check if the specified devices are in the network scenario.
+
+        Args:
+            machine_names (Set[str]): A set of strings containing the names of the devices to search.
+
+        Returns:
+            bool: True if the devices are all in the network scenario, else False.
+        """
+        return all(map(lambda x: self.find_machine(x), machine_names))
+
     def __repr__(self) -> str:
         return "Lab(%s, %s, %s, %s)" % (self.path, self.hash, self.machines, self.links)
 
     def __str__(self) -> str:
         lab_info = ""
 
-        if self.name:
-            lab_info += "Name: %s\n" % self.name
+        if self._name:
+            lab_info += "Name: %s\n" % self._name
 
         if self.description:
             lab_info += "Description: %s\n" % self.description
@@ -274,6 +387,6 @@ def __str__(self) -> str:
             lab_info += "Email: %s\n" % self.email
 
         if self.web:
-            lab_info += "Website: %s" % self.web
+            lab_info += "Website: %s\n" % self.web
 
-        return lab_info
+        return lab_info[0:-1]  # Remove trailing new line
diff --git a/src/Kathara/model/Link.py b/src/Kathara/model/Link.py
index ed65ad28..af91ae2c 100644
--- a/src/Kathara/model/Link.py
+++ b/src/Kathara/model/Link.py
@@ -1,6 +1,7 @@
-from typing import List, Any
+from typing import List, Any, Dict
 
 from . import Lab as LabPackage
+from . import Machine as MachinePackage
 from .ExternalLink import ExternalLink
 
 BRIDGE_LINK_NAME = "kathara_host_bridge"
@@ -12,17 +13,19 @@ class Link(object):
     Contains information about the collision domain and the API object to interact with the Manager.
 
     Attributes:
-        lab (Kathara.model.Lab): The Kathara network Scenario of the collision domain.
+        lab (Kathara.model.Lab.Lab): The Kathara network scenario of the collision domain.
         name (str): The name of the collision domain.
-        external (List[Kathara.model.ExternalLink]): List of the ExternalLinks attached to this collision domain.
+        external (List[Kathara.model.ExternalLink.ExternalLink]): External links attached to this collision domain.
+        machines (Dict[str, Kathara.model.Machine.Machine]): Machines attached to this collision domain.
         api_object (Any): To interact with the current Kathara Manager.
     """
-    __slots__ = ['lab', 'name', 'external', 'api_object']
+    __slots__ = ['lab', 'name', 'external', 'machines', 'api_object']
 
     def __init__(self, lab: 'LabPackage.Lab', name) -> None:
         self.lab: 'LabPackage.Lab' = lab
         self.name: str = name
         self.external: List[ExternalLink] = []
+        self.machines: Dict[str, 'MachinePackage.Machine'] = {}
         self.api_object: Any = None
 
     def __repr__(self) -> str:
diff --git a/src/Kathara/model/Machine.py b/src/Kathara/model/Machine.py
index 8770e922..ca206725 100644
--- a/src/Kathara/model/Machine.py
+++ b/src/Kathara/model/Machine.py
@@ -13,7 +13,7 @@
 from . import Link as LinkPackage
 from .Link import Link
 from .. import utils
-from ..exceptions import NonSequentialMachineInterfaceError, MachineOptionError, MachineCollisionDomainConflictError
+from ..exceptions import NonSequentialMachineInterfaceError, MachineOptionError, MachineCollisionDomainError
 from ..setting.Setting import Setting
 
 
@@ -51,7 +51,7 @@ def __init__(self, lab: 'LabPackage.Lab', name: str, **kwargs) -> None:
         name = name.strip()
         matches = re.search(r"^[a-z0-9_]{1,30}$", name)
         if not matches:
-            raise Exception("Invalid device name `%s`." % name)
+            raise SyntaxError(f"Invalid device name `{name}`.")
 
         self.lab: LabPackage.Lab = lab
         self.name: str = name
@@ -98,20 +98,43 @@ def add_interface(self, link: 'LinkPackage.Link', number: int = None) -> None:
             None
 
         Raises:
-            Exception: The interface number specified is already used on the device.
+            MachineCollisionDomainConflictError: If the interface number specified is already used on the device.
+            MachineCollisionDomainConflictError: If the device is already connected to the collision domain.
         """
         if number is None:
             number = len(self.interfaces.keys())
 
         if number in self.interfaces:
-            raise Exception("Interface %d already set on device `%s`." % (number, self.name))
+            raise MachineCollisionDomainError(f"Interface {number} already set on device `{self.name}`.")
 
-        if any(x.name == link.name for x in self.interfaces.values()):
-            raise MachineCollisionDomainConflictError(
-                "Device `%s` is already connected to collision domain `%s`" % (self.name, link.name)
+        if self.name in link.machines:
+            raise MachineCollisionDomainError(
+                f"Device `{self.name}` is already connected to collision domain `{link.name}`."
             )
 
         self.interfaces[number] = link
+        link.machines[self.name] = self
+
+    def remove_interface(self, link: 'LinkPackage.Link') -> None:
+        """Disconnect the device from the specified collision domain.
+
+        Args:
+            link (Kathara.model.Link): The Kathara collision domain to disconnect.
+
+        Returns:
+            None
+
+        Raises:
+            MachineCollisionDomainConflictError: If the device is not connected to the collision domain.
+        """
+        if self.name not in link.machines:
+            raise MachineCollisionDomainError(
+                f"Device `{self.name}` is not connected to collision domain `{link.name}`.")
+
+        self.interfaces = collections.OrderedDict(
+            map(lambda x: x if x[1] is not None and x[1].name != link.name else (x[0], None), self.interfaces.items())
+        )
+        link.machines.pop(self.name)
 
     def add_meta(self, name: str, value: Any) -> None:
         """Add a meta property to the device.
@@ -124,7 +147,7 @@ def add_meta(self, name: str, value: Any) -> None:
             None
 
         Raises:
-            MachineOptionError: The specified value is not valid for the specified property.
+            MachineOptionError: If the specified value is not valid for the specified property.
         """
         if name == "exec":
             self.startup_commands.append(value)
@@ -152,7 +175,7 @@ def add_meta(self, name: str, value: Any) -> None:
             return
 
         if name == "env":
-            matches = re.search(r"^(?P<key>\w+)=(?P<value>\w+)$", value)
+            matches = re.search(r"^(?P<key>\w+)=(?P<value>\S+)$", value)
 
             # Check for valid kv-pair
             if matches:
@@ -202,7 +225,7 @@ def check(self) -> None:
 
         for i, (num_iface, _) in enumerate(sorted_interfaces):
             if num_iface != i:
-                raise NonSequentialMachineInterfaceError("Interface %d missing on device %s." % (i, self.name))
+                raise NonSequentialMachineInterfaceError(i, self.name)
 
         self.interfaces = collections.OrderedDict(sorted_interfaces)
 
@@ -294,6 +317,9 @@ def get_mem(self) -> str:
 
         Returns:
             str: The memory limit of the device.
+
+        Raises:
+            MachineOptionError: If the memory value specified is not valid.
         """
         memory = self.lab.general_options["mem"] if "mem" in self.lab.general_options else \
             self.meta["mem"] if "mem" in self.meta else None
@@ -317,13 +343,16 @@ def get_cpu(self, multiplier: int = 1) -> Optional[int]:
         """Get the CPU limit, multiplied by a specific multiplier.
 
         User should pass a float value ranging from 0 to max user CPUs.
-        Try to took it from options, or device meta. Otherwise, return None.
+        Try to take it from options, or device meta. Otherwise, return None.
 
         Args:
             multiplier (int): A numeric multiplier for the CPU limit value.
 
         Returns:
             Optional[int]: The CPU limit of the device.
+
+        Raises:
+            MachineOptionError: If the CPU value specified is not valid.
         """
         if "cpus" in self.lab.general_options:
             try:
@@ -354,6 +383,9 @@ def get_num_terms(self) -> int:
 
         Returns:
             int: The number of terminal to be opened.
+
+        Raises:
+            MachineOptionError: If the terminals number value specified is not valid.
         """
         num_terms = 1
 
@@ -377,6 +409,9 @@ def is_ipv6_enabled(self) -> bool:
 
         Returns:
             bool: True if it is enabled, else False.
+
+        Raises:
+            MachineOptionError: If the IPv6 value specified is not valid.
         """
         try:
             return bool(strtobool(self.lab.general_options["ipv6"])) if "ipv6" in self.lab.general_options else \
@@ -434,7 +469,8 @@ def __str__(self) -> str:
         if self.interfaces:
             formatted_machine += "\nInterfaces: "
             for (iface_num, link) in self.interfaces.items():
-                formatted_machine += f"\n\t- {iface_num}: {link.name}"
+                if link:
+                    formatted_machine += f"\n\t- {iface_num}: {link.name}"
 
         formatted_machine += f"\nBridged Connection: {self.meta['bridged']}"
 
diff --git a/src/Kathara/os/Networking.py b/src/Kathara/os/Networking.py
index c2578bd3..49a2d6e5 100644
--- a/src/Kathara/os/Networking.py
+++ b/src/Kathara/os/Networking.py
@@ -2,6 +2,7 @@
 import os
 import shutil
 from typing import Optional
+from ..exceptions import InterfaceNotFoundError
 
 
 class Networking(object):
@@ -20,6 +21,9 @@ def get_or_new_interface(full_interface_name: str, vlan_interface_name: str, vla
 
         Returns:
             int: The link index.
+
+        Raises:
+            InterfaceNotFoundError: If the specified interface is not found on the host machine.
         """
         # disable pyroute2 logging to avoid warning about project structure changes
         logging.getLogger('pyroute2').disabled = True
@@ -33,7 +37,7 @@ def get_or_new_interface(full_interface_name: str, vlan_interface_name: str, vla
         interface_indexes = ip.link_lookup(ifname=full_interface_name)
         # If not present, raise an error
         if not interface_indexes:
-            raise Exception("Interface `%s` not found." % full_interface_name)
+            raise InterfaceNotFoundError(f"Interface `{full_interface_name}` not found on the host machine.")
 
         interface_index = interface_indexes[0]
         logging.debug("Interface found with ID = %d" % interface_index)
@@ -148,7 +152,7 @@ def get_iptables_version() -> str:
                 iptables_binary = "/usr/sbin/iptables"
 
         if iptables_binary is None:
-            raise Exception("Cannot find `iptables` in the host.")
+            raise FileNotFoundError("Cannot find `iptables` in the host.")
 
         logging.debug("Found iptables binary in `%s`." % iptables_binary)
 
diff --git a/src/Kathara/parser/netkit/DepParser.py b/src/Kathara/parser/netkit/DepParser.py
index 9a4f1cc4..b574243e 100644
--- a/src/Kathara/parser/netkit/DepParser.py
+++ b/src/Kathara/parser/netkit/DepParser.py
@@ -1,10 +1,10 @@
 import logging
+import mmap
 import os
 import re
 from typing import List, Optional
 
-import mmap
-
+from ...exceptions import MachineDependencyError
 from ...trdparty.depgen import depgen
 
 
@@ -22,6 +22,11 @@ def parse(path: str) -> Optional[List[str]]:
         Returns:
             Optional[List[str]]: A List of string containing the names of the device ordered considering the
                 dependencies.
+
+        Raises:
+            IOError: If there is an error while opening lab.dep file.
+            SyntaxError: If there is a syntax error in lab.dep file.
+            MachineDependencyError: If there is a Machines dependency loop in lab.dep file.
         """
         lab_dep_path = os.path.join(path, 'lab.dep')
 
@@ -59,12 +64,12 @@ def parse(path: str) -> Optional[List[str]]:
                     # Dependencies are saved as dependencies[machine3] = [machine1, machine2]
                     dependencies[key] = deps
                 else:
-                    raise Exception("[ERROR] In lab.dep - line %d: Syntax error." % line_number)
+                    raise SyntaxError(f"In lab.dep - Line {line_number}.")
 
             line_number += 1
             line = dep_mem_file.readline().decode('utf-8')
 
         if depgen.has_loop(dependencies):
-            raise Exception("ERROR: Loop in lab.dep.\n")
+            raise MachineDependencyError("Machines' dependency loop in lab.dep file.")
 
         return depgen.flatten(dependencies)
diff --git a/src/Kathara/parser/netkit/ExtParser.py b/src/Kathara/parser/netkit/ExtParser.py
index a880700f..07bb59b7 100644
--- a/src/Kathara/parser/netkit/ExtParser.py
+++ b/src/Kathara/parser/netkit/ExtParser.py
@@ -21,6 +21,11 @@ def parse(path: str) -> Optional[Dict[str, List[ExternalLink]]]:
         Returns:
             Optional[Dict[str, List[ExternalLink]]]: Keys are name of collision domain and values are List of
                 ExternalLink attached to that interface.
+
+        Raises:
+            IOError: If lab.ext file cannot be opened.
+            ValueError: If the VLAN ID is not allowed.
+            SyntaxError: If lab.ext file is malformed.
         """
         lab_ext_path = os.path.join(path, 'lab.ext')
 
@@ -56,15 +61,14 @@ def parse(path: str) -> Optional[Dict[str, List[ExternalLink]]]:
 
                 if vlan:
                     if 0 <= vlan >= 4095:
-                        raise Exception("[ERROR] In file lab.ext, line %d: "
-                                        "VLAN ID must be in range [1, 4094]." % line_number)
+                        raise ValueError(f"In file lab.ext, line {line_number}: VLAN ID must be in range [1, 4094].")
 
                 if link not in external_links:
                     external_links[link] = []
 
                 external_links[link].append(ExternalLink(interface, vlan))
             elif not line.startswith('#') and line.strip():
-                raise Exception("[ERROR] In file lab.ext, line %d malformed." % line_number)
+                raise SyntaxError(f"In file lab.ext - Line {line_number}.")
 
             line_number += 1
             line = ext_mem_file.readline().decode('utf-8')
diff --git a/src/Kathara/parser/netkit/LabParser.py b/src/Kathara/parser/netkit/LabParser.py
index 6d2e61fb..5038bcd4 100644
--- a/src/Kathara/parser/netkit/LabParser.py
+++ b/src/Kathara/parser/netkit/LabParser.py
@@ -22,10 +22,10 @@ def parse(path: str) -> Lab:
         lab_conf_path = os.path.join(path, 'lab.conf')
 
         if not os.path.exists(lab_conf_path):
-            raise IOError("No lab.conf in given directory.\n")
+            raise IOError("No lab.conf in given directory.")
 
         if os.stat(lab_conf_path).st_size == 0:
-            raise IOError("lab.conf file is empty.\n")
+            raise IOError("lab.conf file is empty.")
 
         # Reads lab.conf in memory so it is faster.
         try:
@@ -40,8 +40,7 @@ def parse(path: str) -> Lab:
         line = lab_mem_file.readline().decode('utf-8')
         while line:
             matches = re.search(r"^(?P<key>[a-z0-9_]{1,30})\[(?P<arg>\w+)\]=(?P<value>\".+\"|\'.+\'|\w+)$",
-                                line.strip()
-                                )
+                                line.strip())
 
             if matches:
                 key = matches.group("key").strip()
@@ -49,8 +48,8 @@ def parse(path: str) -> Lab:
                 value = matches.group("value").replace('"', '').replace("'", '')
 
                 if key in RESERVED_MACHINE_NAMES:
-                    raise Exception("[ERROR] In line %d: "
-                                    "`%s` is a reserved name, you can not use it for a device." % (line_number, key))
+                    raise ValueError(f"In lab.conf - Line {line_number}: "
+                                     f"`{key}` is a reserved name, you can not use it for a device.")
 
                 try:
                     # It's an interface, handle it.
@@ -59,9 +58,8 @@ def parse(path: str) -> Lab:
                     if re.search(r"^\w+$", value):
                         lab.connect_machine_to_link(key, value, machine_iface_number=interface_number)
                     else:
-                        raise Exception("[ERROR] In line %d: "
-                                        "Collision domain `%s` contains non-alphanumeric characters." % (line_number,
-                                                                                                         value))
+                        raise ValueError(f"In lab.conf - Line {line_number}: "
+                                         f"Collision domain `{value}` contains non-alphanumeric characters.")
                 except ValueError:
                     # Not an interface, add it to the machine metas.
                     lab.assign_meta_to_machine(key, arg, value)
@@ -74,7 +72,7 @@ def parse(path: str) -> Lab:
                             not line.startswith("LAB_AUTHOR=") and \
                             not line.startswith("LAB_EMAIL=") and \
                             not line.startswith("LAB_WEB="):
-                        raise Exception("[ERROR] In line %d: Invalid characters `%s`." % (line_number, line))
+                        raise SyntaxError("In lab.conf - Line %d: Invalid characters `%s`." % (line_number, line))
                     else:
                         (key, value) = line.split("=")
                         key = key.replace("LAB_", "").lower()
diff --git a/src/Kathara/setting/Setting.py b/src/Kathara/setting/Setting.py
index ed37838b..c10836ea 100644
--- a/src/Kathara/setting/Setting.py
+++ b/src/Kathara/setting/Setting.py
@@ -8,10 +8,11 @@
 
 from .. import utils
 from .. import version
-from ..webhooks.GitHubApi import GitHubApi
-from ..exceptions import HTTPConnectionError, SettingsError, SettingsNotFound
+from ..exceptions import HTTPConnectionError, SettingsError, SettingsNotFoundError
+from ..exceptions import InstantiationError
 from ..foundation.setting.SettingsAddon import SettingsAddon
 from ..foundation.setting.SettingsAddonFactory import SettingsAddonFactory
+from ..webhooks.GitHubApi import GitHubApi
 
 AVAILABLE_DEBUG_LEVELS: List[str] = ["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "EXCEPTION"]
 AVAILABLE_MANAGERS: List[str] = ["docker", "kubernetes"]
@@ -48,6 +49,9 @@ def get_instance() -> Setting:
 
         Returns:
             Kathara.setting.Setting: An instance of Setting.
+
+        Raises:
+            InstantiationError: If two instances of the class are created.
         """
         if Setting.__instance is None:
             Setting()
@@ -56,7 +60,7 @@ def get_instance() -> Setting:
 
     def __init__(self) -> None:
         if Setting.__instance is not None:
-            raise Exception("This class is a singleton!")
+            raise InstantiationError("This class is a singleton!")
         else:
             # Load default settings to use
             for (name, value) in DEFAULTS.items():
@@ -89,12 +93,13 @@ def load_from_disk(self, path: Optional[str] = None) -> None:
             None
 
         Raises:
-            SettingsNotFound: Settings file not found in specified path.
+            SettingsNotFound: If the Settings file is not found in specified path.
+            SettingsError: If the specified file is not a valid JSON.
         """
         settings_path = os.path.join(path, SETTINGS_FILENAME) if path is not None else DEFAULT_SETTINGS_PATH
 
         if not os.path.exists(settings_path):  # Requested settings file doesn't exist, throw exception
-            raise SettingsNotFound()
+            raise SettingsNotFoundError(settings_path)
         else:  # Requested settings file exists, read it and check values
             settings = {}
             with open(settings_path, 'r') as settings_file:
@@ -173,6 +178,11 @@ def check(self) -> None:
 
         Returns:
             None
+
+        Raises:
+            SettingsError: If the Networks Prefix does not contain only lowercase letters and underscore.
+            SettingsError: If the Device Prefix does not contain only lowercase letters and underscore.
+            SettingsError: If the Debug Level specified is not allowed.
         """
         self._check_manager()
 
@@ -220,7 +230,7 @@ def _check_manager(self) -> None:
             None
 
         Raises:
-            SettingsError: "Manager Type not allowed."
+            SettingsError: If the Manager Type is not allowed.
         """
         from ..manager.Kathara import Kathara
         managers = Kathara.get_available_managers_name()
@@ -238,8 +248,8 @@ def check_image(self, image: str = None) -> None:
             None
 
         Raises:
-            ConnectionError: The image is not locally available and there is no connection to a remote image repository.
-            Exception: The image is not found.
+            ConnectionError: If the image is not locally available and there is no connection to a remote image repository.
+            ImageNotFoundError: If the image is not found.
         """
         image = self.image if not image else image
 
@@ -255,6 +265,9 @@ def check_terminal(self, terminal: str = None) -> bool:
 
         Returns:
             bool: True if the selected terminal is TMUX (that do not require path), else False.
+
+        Raises:
+            SettingError: If the terminal emulator specified is not found.
         """
         terminal = self.terminal if not terminal else terminal
 
diff --git a/src/Kathara/strings.py b/src/Kathara/strings.py
index f4a3df5a..1b30c173 100644
--- a/src/Kathara/strings.py
+++ b/src/Kathara/strings.py
@@ -3,13 +3,13 @@
 strings = {
     "vstart": "Start a new Kathara device",
     "vclean": "Stop a single Kathara device",
-    "vconfig": "Attach network interfaces to a running Kathara device",
-    "lstart": "Start a Kathara lab",
-    "lclean": "Stop a Kathara lab",
-    "linfo": "Show information about a Kathara lab",
-    "lrestart": "Restart a Kathara lab",
-    "ltest": "Test a Kathara lab",
-    "lconfig": "Attach network interfaces to a running Kathara device in a Kathara lab",
+    "vconfig": "Manage the network interfaces of a running Kathara device",
+    "lstart": "Start a Kathara network scenario",
+    "lclean": "Stop a Kathara network scenario",
+    "linfo": "Show information about a Kathara network scenario",
+    "lrestart": "Restart a Kathara network scenario",
+    "ltest": "Test a Kathara network scenario",
+    "lconfig": "Manage the network interfaces of a running Kathara device in a Kathara network scenario",
     "connect": "Connect to a Kathara device",
     "exec": "Execute a command in a Kathara device",
     "wipe": "Delete all Kathara devices and collision domains, optionally also delete settings",
diff --git a/src/Kathara/utils.py b/src/Kathara/utils.py
index 248952af..a90c4790 100644
--- a/src/Kathara/utils.py
+++ b/src/Kathara/utils.py
@@ -20,6 +20,8 @@
 from binaryornot.check import is_binary
 from slug import slug
 
+from .exceptions import HostArchitectureError
+
 # Platforms constants definition.
 MAC_OS: str = "darwin"
 WINDOWS: str = "win32"
@@ -35,8 +37,8 @@
 
 # Generic Functions
 def check_python_version() -> None:
-    if sys.version_info < (3, 0):
-        logging.critical("Python version should be greater than 3.0")
+    if sys.version_info < (3, 9):
+        logging.critical("Python version should be at least 3.9")
         sys.exit(1)
 
 
@@ -126,24 +128,33 @@ def get_architecture() -> str:
 
     logging.debug("Machine architecture is `%s`." % architecture)
 
-    if architecture == "x86_64" or architecture == "amd64":
+    if architecture in ["x86_64", "amd64"]:
         return "amd64"
     elif architecture == "i686":
         return "386"
-    elif architecture in ['arm64', 'aarch64']:
+    elif architecture in ["arm64", "aarch64"]:
         return "arm64"
     elif architecture == "armv7l":
         return "armv7"
     elif architecture == "armv6l":
         return "armv6"
     else:
-        raise Exception("Not implemented for %s." % architecture)
+        raise HostArchitectureError(architecture)
 
 
 def convert_win_2_linux(filename: str) -> bytes:
     if not is_binary(filename):
-        file_content = open(filename, mode='r', encoding='utf-8-sig').read()
-        return file_content.replace("\n\r", "\n").encode('utf-8')
+        file_obj = None
+        try:
+            file_obj = open(filename, mode='r', encoding='utf-8-sig')
+            file_content = file_obj.read()
+            return file_content.replace("\n\r", "\n").encode('utf-8')
+        except Exception:
+            # In any case the binaryornot heuristics fail (so the file is not a text file), close the stream and
+            # read the file as a standard binary file.
+            if file_obj:
+                file_obj.close()
+            pass
 
     return open(filename, mode='rb').read()
 
diff --git a/src/Kathara/validator/ImageValidator.py b/src/Kathara/validator/ImageValidator.py
index 92ccb481..78f47b1f 100644
--- a/src/Kathara/validator/ImageValidator.py
+++ b/src/Kathara/validator/ImageValidator.py
@@ -1,3 +1,4 @@
+from ..exceptions import DockerImageNotFoundError, InvalidImageArchitectureError
 from ..setting.Setting import Setting
 from ..trdparty.consolemenu.validators.base import BaseValidator
 
@@ -10,5 +11,6 @@ def validate(self, input_string: str) -> bool:
         try:
             Setting.get_instance().check_image(input_string)
             return True
-        except Exception:
+        except (ConnectionError, DockerImageNotFoundError, InvalidImageArchitectureError) as e:
+            print(str(e))
             return False
diff --git a/src/Kathara/validator/TerminalValidator.py b/src/Kathara/validator/TerminalValidator.py
index 8e578b08..ae42a274 100644
--- a/src/Kathara/validator/TerminalValidator.py
+++ b/src/Kathara/validator/TerminalValidator.py
@@ -1,5 +1,5 @@
+from ..exceptions import SettingsError
 from ..setting.Setting import Setting
-
 from ..trdparty.consolemenu.validators.base import BaseValidator
 
 
@@ -10,5 +10,5 @@ def __init__(self) -> None:
     def validate(self, input_string: str) -> bool:
         try:
             return Setting.get_instance().check_terminal(input_string)
-        except Exception:
+        except SettingsError:
             return False
diff --git a/src/Kathara/version.py b/src/Kathara/version.py
index 407a0f0a..acc4993a 100644
--- a/src/Kathara/version.py
+++ b/src/Kathara/version.py
@@ -1,6 +1,6 @@
 from typing import Tuple
 
-CURRENT_VERSION = "3.5.0"
+CURRENT_VERSION = "3.5.1"
 
 
 def parse(version: str) -> Tuple:
diff --git a/src/Kathara/webhooks/DockerHubApi.py b/src/Kathara/webhooks/DockerHubApi.py
index 4b7bb23f..a9046b7f 100644
--- a/src/Kathara/webhooks/DockerHubApi.py
+++ b/src/Kathara/webhooks/DockerHubApi.py
@@ -6,19 +6,34 @@
 
 DOCKER_HUB_KATHARA_URL = "https://hub.docker.com/v2/repositories/kathara/?page_size=-1"
 
-EXCLUDED_IMAGES = ['megalos-bgp-manager', 'katharanp']
+EXCLUDED_IMAGES = ['megalos-bgp-manager']
 
 
 class DockerHubApi(object):
     @staticmethod
     def get_images() -> filter:
+        """
+        Get the Kathara Docker Hub account image list, excluding:
+            - Private Images
+            - Plugins or other types of images
+            - Images in the EXCLUDED_IMAGES list
+
+        Returns:
+            filter: filtered list of currently available images that satisfy the previous conditions.
+
+        Raises:
+            HTTPConnectionError: If there is a connection error with the Docker Hub.
+        """
         try:
             result = requests.get(DOCKER_HUB_KATHARA_URL)
         except requests.exceptions.ConnectionError as e:
             raise HTTPConnectionError(str(e))
 
         if result.status_code != 200:
-            logging.debug("DockerHub replied with status code %s.", result.status_code)
-            raise HTTPConnectionError("DockerHub replied with status code %s." % result.status_code)
+            logging.debug("Docker Hub replied with status code %s.", result.status_code)
+            raise HTTPConnectionError("Docker Hub replied with status code %s." % result.status_code)
 
-        return filter(lambda x: not x['is_private'] and x['name'] not in EXCLUDED_IMAGES, result.json()['results'])
+        return filter(
+            lambda x: not x['is_private'] and x['repository_type'] == 'image' and x['name'] not in EXCLUDED_IMAGES,
+            result.json()['results']
+        )
diff --git a/src/kathara.py b/src/kathara.py
index dec7a92d..f83bd4d9 100755
--- a/src/kathara.py
+++ b/src/kathara.py
@@ -11,7 +11,7 @@
 from Kathara import utils
 from Kathara.auth.PrivilegeHandler import PrivilegeHandler
 from Kathara.cli.ui.event.register import register_cli_events
-from Kathara.exceptions import SettingsError, DockerDaemonConnectionError, ClassNotFoundError, SettingsNotFound
+from Kathara.exceptions import SettingsError, DockerDaemonConnectionError, ClassNotFoundError, SettingsNotFoundError
 from Kathara.foundation.cli.command.CommandFactory import CommandFactory
 from Kathara.setting.Setting import Setting
 from Kathara.strings import formatted_strings
@@ -69,17 +69,17 @@ def __init__(self) -> None:
             if "settings" not in args.command:
                 Setting.get_instance().check()
         except (SettingsError, DockerDaemonConnectionError) as e:
-            logging.critical(str(e))
+            logging.critical(f"({type(e).__name__}) {str(e)}")
             sys.exit(1)
 
         try:
             command_object = CommandFactory().create_instance(class_args=(args.command.capitalize(),))
         except ClassNotFoundError:
-            logging.error('Unrecognized command `%s`.\n' % args.command)
+            logging.error(f"Unrecognized command `{args.command}`.")
             parser.print_help()
             sys.exit(1)
         except ImportError as e:
-            logging.critical("`%s` is not installed in your system\n" % e.name)
+            logging.critical(f"({type(e).__name__}) `{e.name}` is not installed in your system")
             sys.exit(1)
 
         try:
@@ -88,14 +88,13 @@ def __init__(self) -> None:
         except KeyboardInterrupt:
             if args.command not in ['exec', 'linfo', 'list', 'settings']:
                 logging.warning("You interrupted Kathara during a command. The system may be in an inconsistent "
-                                "state!\n")
-                logging.warning("If you encounter any problem please run `kathara wipe`.")
+                                "state! If you encounter any problem please run `kathara wipe`.")
             sys.exit(0)
         except Exception as e:
             if Setting.get_instance().debug_level == "EXCEPTION":
-                logging.exception(str(e) + '\n')
+                logging.exception(f"({type(e).__name__}) {str(e)}")
             else:
-                logging.critical(str(e) + '\n')
+                logging.critical(f"({type(e).__name__}) {str(e)}")
             sys.exit(1)
 
 
@@ -110,7 +109,7 @@ def __init__(self) -> None:
 
     try:
         Setting.get_instance().load_from_disk()
-    except SettingsNotFound:
+    except SettingsNotFoundError:
         Setting.get_instance().save_to_disk()
 
     try:
diff --git a/src/requirements.txt b/src/requirements.txt
index 2c707231..99cf09e4 100644
--- a/src/requirements.txt
+++ b/src/requirements.txt
@@ -9,7 +9,7 @@ deepdiff>=4.0.9;
 pyroute2==0.5.19;
 progressbar2>=1.14.0;
 libtmux>=0.8.2; sys_platform == 'darwin' or sys_platform == 'linux'
-pyuv>=1.4.0;
+git+https://github.com/saghul/pyuv@master#egg=pyuv
 appscript>=1.1.0; sys_platform == 'darwin'
 pypiwin32>=223; sys_platform == 'win32'
 windows-curses>=2.1.0; sys_platform == 'win32'
\ No newline at end of file


Test patch:
diff --git a/docs/kathara-ltest.1.ronn b/docs/kathara-ltest.1.ronn
index 4cd004aa..2dc44fe0 100644
--- a/docs/kathara-ltest.1.ronn
+++ b/docs/kathara-ltest.1.ronn
@@ -1,5 +1,5 @@
 m4_changequote()
-kathara-ltest(1) -- Test a Kathara lab
+kathara-ltest(1) -- Test a Kathara network scenario
 =============================================
 
 ## SYNOPSIS
@@ -10,13 +10,13 @@ kathara-ltest(1) -- Test a Kathara lab
 
 ## DESCRIPTION
 
-Tool for retrieving and saving information about the state of running devices inside a Kathara lab. These information can then be distributed with the lab itself, so that users can easily test whether the lab still behaves in the expected way when it is being launched on a different host.
+Tool for retrieving and saving information about the state of running devices inside a Kathara network scenario. These information can then be distributed with the network scenario itself, so that users can easily test whether the network scenario still behaves in the expected way when it is being launched on a different host.
 
-Takes care of launching the lab in a test mode, running an ad-hoc set of scripts on devices, and storing their output on the host filesystem. If the test is being run for the first time, creates a signature of the test results that is used as a fingerprint of a correctly behaving lab. 
+Takes care of launching the network scenario in a test mode, running an ad-hoc set of scripts on devices, and storing their output on the host filesystem. If the test is being run for the first time, creates a signature of the test results that is used as a fingerprint of a correctly behaving network scenario. 
 
-When `ltest` is run with the `--verify` option, test results are compared with the signature to check that the lab still behaves as expected. After running the test, the lab is then automatically stopped.
+When `ltest` is run with the `--verify` option, test results are compared with the signature to check that the network scenario still behaves as expected. After running the test, the network scenario is then automatically stopped.
 
-A directory `lab-path/_test` is created inside and the results of the test are stored inside it. Any existing results of previous tests are overwritten. Subdirectory `lab-path/_test/signature` and `lab-path/_test/results` will contain the signature of the lab test and the outcome of the last executed test, respectively.
+A directory `lab-path/_test` is created inside and the results of the test are stored inside it. Any existing results of previous tests are overwritten. Subdirectory `lab-path/_test/signature` and `lab-path/_test/results` will contain the signature of the network scenario test and the outcome of the last executed test, respectively.
 
 **NOTE**: `ltest` runs devices without a terminal window. Therefore, no window will pop up and no output will be shown during a test.
 
@@ -28,18 +28,18 @@ A directory `lab-path/_test` is created inside and the results of the test are s
     Show an help message and exit.
 
 * `-d` <DIRECTORY>, `--directory` <DIRECTORY>:
-    Specify the folder containing the lab.
+    Specify the folder containing the network scenario.
 
-    If no `-d` option is provided, assume the lab is located in the current directory.
+    If no `-d` option is provided, assume the network scenario is located in the current directory.
 
 * `-R`, `--rebuild-signature`:
-    Force generating a new signature for the lab, even if one already exists. Overwrites any existing signature.
+    Force generating a new signature for the network scenario, even if one already exists. Overwrites any existing signature.
 
 * `--wait` <MINUTES>:
-    Minutes to wait from lab startup before running the tests (can be a decimal number).
+    Minutes to wait from network scenario startup before running the tests (can be a decimal number).
 
 * `--verify` {builtin,user,both}:
-    Compares current lab state with stored signature.
+    Compares current network scenario state with stored signature.
 
     After a signature has been generated, the outcome of any following tests is compared with information in the signature. Possible values are as follows:
 
diff --git a/src/Kathara/cli/command/LtestCommand.py b/src/Kathara/cli/command/LtestCommand.py
index eb213f63..82f427bc 100644
--- a/src/Kathara/cli/command/LtestCommand.py
+++ b/src/Kathara/cli/command/LtestCommand.py
@@ -37,27 +37,27 @@ def __init__(self) -> None:
         self.parser.add_argument(
             '-d', '--directory',
             required=False,
-            help='Specify the folder containing the lab.'
+            help='Specify the folder containing the network scenario.'
         )
         self.parser.add_argument(
             '-R', '--rebuild-signature',
             dest="rebuild_signature",
             required=False,
             action='store_true',
-            help='Force generating a new signature for the lab, even if one already exists. '
+            help='Force generating a new signature for the network scenario, even if one already exists. '
                  'Overwrites any existing signature.'
         )
         self.parser.add_argument(
             '--wait',
             required=False,
             metavar='MINUTES',
-            help='Minutes to wait from lab startup before running the tests (can be a decimal number).'
+            help='Minutes to wait from network scenario startup before running the tests (can be a decimal number).'
         )
         self.parser.add_argument(
             '--verify',
             required=False,
             choices=['builtin', 'user', 'both'],
-            help='Compares current lab state with stored signature.'
+            help='Compares current network scenario state with stored signature.'
         )
 
     def run(self, current_path: str, argv: List[str]) -> None:
@@ -72,7 +72,7 @@ def run(self, current_path: str, argv: List[str]) -> None:
             signature_test_path = os.path.join(lab_path, "_test", "signature")
 
             if os.path.exists(signature_test_path) and not args['rebuild_signature']:
-                logging.error("Signature for current lab already exists. Exiting...")
+                logging.error("Signature for current network scenario already exists. Exiting...")
                 sys.exit(1)
 
         # Tests run without terminals, no shared and /hosthome dirs.
diff --git a/src/Kathara/test/BuiltinTest.py b/src/Kathara/test/BuiltinTest.py
index c3feb0ac..9710bd2b 100644
--- a/src/Kathara/test/BuiltinTest.py
+++ b/src/Kathara/test/BuiltinTest.py
@@ -39,7 +39,7 @@ def test(self) -> bool:
                 with open(machine_signature_path, 'r') as machine_signature_file:
                     machine_signature = json.loads(machine_signature_file.read())
             else:
-                raise MachineSignatureNotFoundError("Signature for device `%s` not found! Exiting..." % machine_name)
+                raise MachineSignatureNotFoundError(machine_name)
 
             # Save machine state into result file
             machine_result_path = "%s/%s.builtin" % (self.results_path, machine.name)
@@ -101,7 +101,7 @@ def _get_machine_status(machine: Machine) -> Dict:
                                                          command="netstat -tuwln"
                                                          )
         # Remove Docker ports and header lines. Sort the array alphabetically.
-        net_stat = sorted([filter(lambda x: "127.0.0.11" not in x, net_stat.splitlines())][2:]) if net_stat else []
+        net_stat = sorted(list(filter(lambda x: "127.0.0.11" not in x, net_stat.splitlines()))[2:]) if net_stat else []
 
         # Machine processes
         (processes, _) = Test._get_machine_command_output(lab_hash=machine.lab.hash,
diff --git a/src/Kathara/test/UserTest.py b/src/Kathara/test/UserTest.py
index 379d7936..33dba4ca 100644
--- a/src/Kathara/test/UserTest.py
+++ b/src/Kathara/test/UserTest.py
@@ -47,9 +47,7 @@ def test(self) -> bool:
                     with open(machine_signature_path, 'r') as machine_signature_file:
                         machine_signature = machine_signature_file.read()
                 else:
-                    raise MachineSignatureNotFoundError("Signature for device `%s` not found! Exiting..." %
-                                                        machine_name
-                                                        )
+                    raise MachineSignatureNotFoundError(machine_name)
 
                 # Save machine state into result file
                 machine_result_path = "%s/%s.user" % (self.results_path, machine.name)
diff --git a/tests/manager/docker/docker_image_test.py b/tests/manager/docker/docker_image_test.py
index ea70a492..9f1eaa6a 100644
--- a/tests/manager/docker/docker_image_test.py
+++ b/tests/manager/docker/docker_image_test.py
@@ -1,6 +1,7 @@
 import sys
 from unittest import mock
 
+import docker.models.images
 import pytest
 from docker.errors import APIError
 
@@ -8,6 +9,7 @@
 
 from src.Kathara.event.EventDispatcher import EventDispatcher
 from src.Kathara.manager.docker.DockerImage import DockerImage
+from src.Kathara.exceptions import InvalidImageArchitectureError
 
 
 class MockPullEvent(object):
@@ -120,11 +122,15 @@ def test_check_for_updates_image_update(mock_local_image, mock_remote_image, moc
     mock_pull.assert_called_once_with("kathara/test")
 
 
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_image_architecture")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
-def test_check_and_pull_local_false(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull, docker_image):
+def test_check_and_pull_local_false(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                    mock_check_image_architecture, docker_image):
+    mock_check_image_architecture.return_value = None
+
     docker_image._check_and_pull("kathara/test", False)
     mock_get_local.assert_called_once_with("kathara/test")
     assert not mock_get_remote.called
@@ -132,11 +138,15 @@ def test_check_and_pull_local_false(mock_get_local, mock_check_for_updates, mock
     assert not mock_pull.called
 
 
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_image_architecture")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
-def test_check_and_pull_local_true(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull, docker_image):
+def test_check_and_pull_local_true(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                   mock_check_image_architecture, docker_image):
+    mock_check_image_architecture.return_value = None
+
     docker_image._check_and_pull("kathara/test", True)
     mock_get_local.assert_called_once_with("kathara/test")
     mock_check_for_updates.assert_called_once_with("kathara/test")
@@ -144,11 +154,15 @@ def test_check_and_pull_local_true(mock_get_local, mock_check_for_updates, mock_
     assert not mock_pull.called
 
 
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_image_architecture")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
-def test_check_and_pull_remote_false(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull, docker_image):
+def test_check_and_pull_remote_false(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                     mock_check_image_architecture, docker_image):
+    mock_check_image_architecture.return_value = None
+
     mock_get_local.side_effect = APIError("Fail")
     docker_image._check_and_pull("kathara/exception", False)
     mock_get_local.assert_called_once_with("kathara/exception")
@@ -157,11 +171,15 @@ def test_check_and_pull_remote_false(mock_get_local, mock_check_for_updates, moc
     assert not mock_pull.called
 
 
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_image_architecture")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
-def test_check_and_pull_remote_true(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull, docker_image):
+def test_check_and_pull_remote_true(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                    mock_check_image_architecture, docker_image):
+    mock_check_image_architecture.return_value = None
+
     mock_get_local.side_effect = APIError("Fail")
     docker_image._check_and_pull("kathara/test", True)
     mock_get_local.assert_called_once_with("kathara/test")
@@ -170,6 +188,52 @@ def test_check_and_pull_remote_true(mock_get_local, mock_check_for_updates, mock
     mock_pull.assert_called_once_with("kathara/test")
 
 
+@mock.patch("src.Kathara.utils.get_architecture")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
+def test_check_and_pull_local_incompatible_arch(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                                mock_get_architecture, docker_image):
+    image_obj = docker.models.images.Image()
+    image_obj.attrs['Architecture'] = 'amd64'
+    image_obj.image_name = "kathara/test"
+
+    mock_get_local.return_value = image_obj
+    mock_get_architecture.return_value = "arm64"
+
+    with pytest.raises(InvalidImageArchitectureError):
+        docker_image._check_and_pull("kathara/test", False)
+
+    mock_get_local.assert_called_once_with("kathara/test")
+    assert not mock_check_for_updates.called
+    assert not mock_get_remote.called
+    assert not mock_pull.called
+
+
+@mock.patch("src.Kathara.utils.get_architecture")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.pull")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_remote")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.check_for_updates")
+@mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage.get_local")
+def test_check_and_pull_remote_incompatible_arch(mock_get_local, mock_check_for_updates, mock_get_remote, mock_pull,
+                                                 mock_get_architecture, docker_image):
+    registry_data = docker.models.images.RegistryData("kathara/remote")
+    registry_data.attrs['Platforms'] = [{'os': 'linux', 'architecture': 'amd64'}]
+
+    mock_get_remote.return_value = registry_data
+    mock_get_architecture.return_value = "arm64"
+
+    mock_get_local.side_effect = APIError("Fail")
+
+    with pytest.raises(InvalidImageArchitectureError):
+        docker_image._check_and_pull("kathara/remote", False)
+    mock_get_local.assert_called_once_with("kathara/remote")
+    assert not mock_check_for_updates.called
+    mock_get_remote.assert_called_once_with("kathara/remote")
+    assert not mock_pull.called
+
+
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_and_pull")
 def test_check(mock_check_and_pull, docker_image):
     docker_image.check("kathara/test")
@@ -179,7 +243,7 @@ def test_check(mock_check_and_pull, docker_image):
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_and_pull")
 def test_check_and_pull_from_list_3_elem(mock_check_and_pull, docker_image):
     images = ["kathara/test1", "kathara/test2", "kathara/test3"]
-    docker_image.check_and_pull_from_list(images)
+    docker_image.check_from_list(images)
     mock_check_and_pull.assert_any_call("kathara/test1")
     mock_check_and_pull.assert_any_call("kathara/test2")
     mock_check_and_pull.assert_any_call("kathara/test3")
@@ -189,5 +253,5 @@ def test_check_and_pull_from_list_3_elem(mock_check_and_pull, docker_image):
 @mock.patch("src.Kathara.manager.docker.DockerImage.DockerImage._check_and_pull")
 def test_check_and_pull_from_list_0_elem(mock_check_and_pull, docker_image):
     images = []
-    docker_image.check_and_pull_from_list(images)
+    docker_image.check_from_list(images)
     assert not mock_check_and_pull.called
diff --git a/tests/manager/docker/docker_link_test.py b/tests/manager/docker/docker_link_test.py
index 7cb122d0..e3a430f1 100644
--- a/tests/manager/docker/docker_link_test.py
+++ b/tests/manager/docker/docker_link_test.py
@@ -10,7 +10,7 @@
 from src.Kathara.model.Lab import Lab
 from src.Kathara.manager.docker.DockerLink import DockerLink
 from src.Kathara import utils
-from src.Kathara.manager.docker.stats.DockerLinkStats import DockerLinkStats
+from src.Kathara.exceptions import LinkNotFoundError
 
 
 #
@@ -255,6 +255,27 @@ def test_undeploy_empty_lab(mock_get_links_by_filters, mock_undeploy_link, docke
     assert not mock_undeploy_link.called
 
 
+@mock.patch("docker.models.networks.Network")
+@mock.patch("docker.models.networks.Network")
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink._undeploy_link")
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters")
+def test_undeploy_selected_links(mock_get_links_by_filters, mock_undeploy_link, mock_net1, mock_net2, docker_link):
+    lab = Lab("Default scenario")
+    lab.get_or_new_link("A")
+    lab.get_or_new_link("B")
+
+    mock_net1.attrs = {"Labels": {"name": "A"}}
+    mock_net2.attrs = {"Labels": {"name": "B"}}
+
+    mock_get_links_by_filters.return_value = [mock_net1, mock_net2]
+
+    docker_link.undeploy("lab_hash", selected_links={"B"})
+    mock_get_links_by_filters.called_once_with(lab.hash)
+    assert mock_net1.reload.call_count == 0
+    assert mock_net2.reload.call_count == 1
+    assert mock_undeploy_link.call_count == 1
+
+
 #
 # TEST: wipe
 #
@@ -317,7 +338,7 @@ def test_get_links_stats_lab_hash_link_not_found(mock_get_links_api_objects_by_f
                                                  docker_network):
     docker_network.api_object.name = "test_network"
     mock_get_links_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         next(docker_link.get_links_stats(lab_hash="lab_hash"))
     mock_get_links_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", link_name=None, user=None)
 
@@ -327,7 +348,7 @@ def test_get_links_stats_lab_hash_link_name_not_found(mock_get_links_api_objects
                                                       docker_network):
     docker_network.api_object.name = "test_network"
     mock_get_links_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         next(docker_link.get_links_stats(lab_hash="lab_hash", link_name="test_network"))
     mock_get_links_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", link_name="test_network",
                                                                   user=None)
diff --git a/tests/manager/docker/docker_machine_test.py b/tests/manager/docker/docker_machine_test.py
index 55fd36b4..d90a5fb8 100644
--- a/tests/manager/docker/docker_machine_test.py
+++ b/tests/manager/docker/docker_machine_test.py
@@ -3,6 +3,7 @@
 from unittest.mock import Mock
 
 import pytest
+from docker.errors import APIError
 
 sys.path.insert(0, './')
 
@@ -10,6 +11,7 @@
 from src.Kathara.model.Link import Link
 from src.Kathara.model.Machine import Machine
 from src.Kathara.manager.docker.DockerMachine import DockerMachine
+from src.Kathara.exceptions import MachineNotFoundError, DockerPluginError
 
 
 #
@@ -32,6 +34,7 @@ def default_device(mock_docker_container):
     device.add_meta("image", "kathara/test")
     device.add_meta("bridged", False)
     device.api_object = mock_docker_container
+    device.api_object.attrs = {"NetworkSettings": {"Networks": []}}
     return device
 
 
@@ -226,6 +229,34 @@ def test_start(docker_machine, default_device, default_link, default_link_b):
     default_link_b.api_object.connect.assert_called_once()
 
 
+def test_start_plugin_error_endpoint_start(default_device, docker_machine):
+    default_device.api_object.start.side_effect = DockerPluginError("endpoint does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.start(default_device)
+
+
+def test_start_plugin_error_network_start(default_device, docker_machine):
+    default_device.api_object.start.side_effect = DockerPluginError("network does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.start(default_device)
+
+
+def test_start_plugin_error_endpoint_connect(default_device, default_link, default_link_b, docker_machine):
+    default_device.add_interface(default_link)
+    default_device.add_interface(default_link_b)
+    default_link_b.api_object.connect.side_effect = DockerPluginError("endpoint does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.start(default_device)
+
+
+def test_start_plugin_error_network_connect(default_device, default_link, default_link_b, docker_machine):
+    default_device.add_interface(default_link)
+    default_device.add_interface(default_link_b)
+    default_link_b.api_object.connect.side_effect = DockerPluginError("network does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.start(default_device)
+
+
 #
 # TEST: _deploy_and_start_machine
 #
@@ -248,30 +279,61 @@ def test_deploy_machines(mock_deploy_and_start, docker_machine):
     lab = Lab("Default scenario")
     lab.get_or_new_machine("pc1", **{'image': 'kathara/test1'})
     lab.get_or_new_machine("pc2", **{'image': 'kathara/test2'})
-    docker_machine.docker_image.check_and_pull_from_list.return_value = None
+    docker_machine.docker_image.check_from_list.return_value = None
     mock_deploy_and_start.return_value = None
     docker_machine.deploy_machines(lab)
-    docker_machine.docker_image.check_and_pull_from_list.assert_called_once_with({'kathara/test1', 'kathara/test2'})
+    docker_machine.docker_image.check_from_list.assert_called_once_with({'kathara/test1', 'kathara/test2'})
     assert mock_deploy_and_start.call_count == 2
 
 
 #
-# TEST: update
+# TEST: connect_to_link
 #
-@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters")
-def test_update(mock_get_machines_api_objects_by_filters, docker_machine, default_device, default_link, default_link_b):
-    mock_get_machines_api_objects_by_filters.return_value = default_device.api_object
-    default_device.api_object.connect.return_value = None
+def test_connect_to_link(docker_machine, default_device, default_link, default_link_b):
     default_device.api_object.attrs["NetworkSettings"] = {}
     default_device.api_object.attrs["NetworkSettings"]["Networks"] = ["A"]
     default_link.api_object.name = "A"
+
     default_device.add_interface(default_link)
     default_device.add_interface(default_link_b)
-    docker_machine.update(default_device)
-    default_link.api_object.connect.assert_called_once()
+
+    docker_machine.connect_to_link(default_device, default_link_b)
+
+    assert not default_link.api_object.connect.called
     default_link_b.api_object.connect.assert_called_once()
 
 
+def test_connect_to_link_plugin_error_network(default_device, default_link, docker_machine):
+    default_device.add_interface(default_link)
+    default_link.api_object.connect.side_effect = DockerPluginError("network does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.connect_to_link(default_device, default_link)
+
+
+def test_connect_to_link_plugin_error_endpoint(default_device, default_link, docker_machine):
+    default_device.add_interface(default_link)
+    default_link.api_object.connect.side_effect = DockerPluginError("endpoint does not exists")
+    with pytest.raises(DockerPluginError):
+        docker_machine.connect_to_link(default_device, default_link)
+
+
+#
+# TEST: disconnect_from_link
+#
+def test_disconnect_from_link(docker_machine, default_device, default_link, default_link_b):
+    default_device.api_object.attrs["NetworkSettings"] = {}
+    default_device.api_object.attrs["NetworkSettings"]["Networks"] = ["A", "B"]
+    default_link.api_object.name = "A"
+    default_link_b.api_object.name = "B"
+    default_device.add_interface(default_link)
+    default_device.add_interface(default_link_b)
+
+    docker_machine.disconnect_from_link(default_device, default_link_b)
+
+    assert not default_link.api_object.disconnect.called
+    default_link_b.api_object.disconnect.assert_called_once()
+
+
 #
 # TEST: undeploy
 #
@@ -514,7 +576,7 @@ def test_get_machines_stats_lab_hash_device_name_user(mock_get_machines_api_obje
 def test_get_machines_stats_lab_hash_device_not_found(mock_get_machines_api_objects_by_filters, docker_machine,
                                                       default_device):
     mock_get_machines_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         next(docker_machine.get_machines_stats(lab_hash="lab_hash"))
 
     mock_get_machines_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", machine_name=None,
@@ -526,7 +588,7 @@ def test_get_machines_stats_lab_hash_device_not_found(mock_get_machines_api_obje
 def test_get_machines_stats_lab_hash_and_name_device_not_found(mock_get_machines_api_objects_by_filters, docker_machine,
                                                                default_device):
     mock_get_machines_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         next(docker_machine.get_machines_stats(lab_hash="lab_hash", machine_name="test_device"))
 
     mock_get_machines_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", machine_name="test_device",
diff --git a/tests/manager/docker/docker_manager_test.py b/tests/manager/docker/docker_manager_test.py
index 3317e90c..e99d1e90 100644
--- a/tests/manager/docker/docker_manager_test.py
+++ b/tests/manager/docker/docker_manager_test.py
@@ -9,9 +9,11 @@
 from src.Kathara.manager.docker.DockerManager import DockerManager
 from src.Kathara.model.Lab import Lab
 from src.Kathara.model.Machine import Machine
+from src.Kathara.model.Link import Link
 from src.Kathara.utils import generate_urlsafe_hash
 from src.Kathara.manager.docker.stats.DockerLinkStats import DockerLinkStats
 from src.Kathara.manager.docker.stats.DockerMachineStats import DockerMachineStats
+from src.Kathara.exceptions import MachineNotFoundError, LabNotFoundError, InvocationError, LinkNotFoundError
 
 
 #
@@ -20,13 +22,9 @@
 @pytest.fixture()
 @mock.patch("src.Kathara.manager.docker.DockerPlugin.DockerPlugin.check_and_download_plugin")
 @mock.patch("docker.from_env")
-@mock.patch("docker.DockerClient")
-def docker_manager(mock_docker_client, mock_from_env, mock_check_and_download_plugin):
+def docker_manager(mock_from_env, mock_check_and_download_plugin):
     mock_check_and_download_plugin.return_value = True
-
-    client_mock = Mock()
-    mock_from_env = client_mock
-
+    mock_from_env.return_value = Mock()
     docker_manager = DockerManager()
 
     return docker_manager
@@ -56,6 +54,132 @@ def default_device(mock_docker_container):
     return device
 
 
+@pytest.fixture()
+def default_link():
+    from src.Kathara.model.Link import Link
+    return Link(Lab("default_scenario"), "A")
+
+
+@pytest.fixture()
+def default_link_b():
+    from src.Kathara.model.Link import Link
+    return Link(Lab("default_scenario"), "B")
+
+
+@pytest.fixture()
+def default_link_c():
+    from src.Kathara.model.Link import Link
+    return Link(Lab("default_scenario"), "C")
+
+
+@pytest.fixture()
+@mock.patch("docker.models.networks.Network")
+def docker_network(mock_network):
+    mock_network.name = "kathara_user_hash_test_network"
+    mock_network.attrs = {
+        "Labels": {
+            "name": "test_network",
+            "lab_hash": "lab_hash",
+            "user": "user",
+            "external": []
+        }
+    }
+    return mock_network
+
+
+@pytest.fixture()
+@mock.patch("docker.models.networks.Network")
+def docker_network_b(mock_network):
+    mock_network.name = "kathara_user_hash_test_network_b"
+    mock_network.attrs = {
+        "Labels": {
+            "name": "test_network_b",
+            "lab_hash": "lab_hash",
+            "user": "user",
+            "external": []
+        }
+    }
+    return mock_network
+
+
+@pytest.fixture()
+@mock.patch("docker.models.containers.Container")
+def docker_container(mock_container):
+    mock_container.reload = Mock()
+    mock_container.labels = {
+        "name": "test_container"
+    }
+    mock_container.attrs = {"HostConfig": {
+        "Privileged": True,
+        "Memory": 67108864,
+        "NanoCpus": 100000000,
+        "PortBindings": {
+            "55/udp": [
+                {
+                    "HostIp": "",
+                    "HostPort": "3000"
+                }
+            ]
+        },
+        "Sysctls": {
+            "sysctl.test": "0",
+        },
+    }, "Config": {
+        "Image": "test_image",
+        "Labels": {
+            "shell": "/bin/bash"
+        },
+        "Env": [
+            "test=path"
+        ]
+    }, "NetworkSettings": {
+        "Networks": {
+            "kathara_user_hash_test_network": {
+                "Links": None,
+            },
+            "bridge": {}
+        }
+    }}
+
+    return mock_container
+
+
+@pytest.fixture()
+@mock.patch("docker.models.containers.Container")
+def docker_container_empty_meta(mock_container):
+    mock_container.reload = Mock()
+    mock_container.labels = {
+        "name": "test_container"
+    }
+    mock_container.attrs = {
+        "HostConfig": {
+            "Privileged": False,
+            "Memory": 0,
+            "NanoCpus": 0,
+            "PortBindings": None,
+            "Sysctls": {
+                "sysctl.test": "0",
+            },
+        },
+        "Config": {
+            "Image": "test_image",
+            "Labels": {
+                "shell": "/bin/bash"
+            },
+            "Env": [
+                "test=path"
+            ]
+        },
+        "NetworkSettings": {
+            "Networks": {
+                "none": {}
+            }
+        }
+    }
+
+    return mock_container
+
+
 #
 # TEST: deploy_lab
 #
@@ -67,42 +191,208 @@ def test_deploy_lab(mock_deploy_links, mock_deploy_machines, docker_manager, two
     mock_deploy_machines.assert_called_once_with(two_device_scenario, selected_machines=None)
 
 
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.deploy_machines")
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.deploy_links")
+def test_deploy_lab_selected_machines(mock_deploy_links, mock_deploy_machines, docker_manager,
+                                      two_device_scenario: Lab):
+    docker_manager.deploy_lab(two_device_scenario, selected_machines={"pc1"})
+
+    mock_deploy_links.assert_called_once_with(two_device_scenario, selected_links={"A", "B"})
+    mock_deploy_machines.assert_called_once_with(two_device_scenario, selected_machines={"pc1"})
+
+
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.deploy_machines")
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.deploy_links")
+def test_deploy_lab_selected_machines_exception(mock_deploy_links, mock_deploy_machines, docker_manager,
+                                                two_device_scenario: Lab):
+    with pytest.raises(MachineNotFoundError):
+        docker_manager.deploy_lab(two_device_scenario, selected_machines={"pc3"})
+    assert not mock_deploy_machines.called
+    assert not mock_deploy_links.called
+
+
+#
+# TEST: deploy_machine
+#
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.deploy_links")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.deploy_machines")
+def test_deploy_machine(mock_deploy_machines, mock_deploy_links, docker_manager, default_device, default_link):
+    default_device.add_interface(default_link)
+
+    docker_manager.deploy_machine(default_device)
+    mock_deploy_links.assert_called_once_with(default_device.lab, selected_links={default_link.name})
+    mock_deploy_machines.assert_called_once_with(default_device.lab, selected_machines={default_device.name})
+
+
+def test_deploy_machine_no_lab(docker_manager, default_device):
+    default_device.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.deploy_machine(default_device)
+
+
+#
+# TEST: deploy_link
+#
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.deploy_links")
+def test_deploy_link(mock_deploy_links, docker_manager, default_link):
+    docker_manager.deploy_link(default_link)
+    mock_deploy_links.assert_called_once_with(default_link.lab, selected_links={default_link.name})
+
+
+def test_deploy_link_no_lab(docker_manager, default_link):
+    default_link.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.deploy_link(default_link)
+
+
+#
+# TEST: connect_machine_to_link
+#
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_link")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_to_link")
+def test_connect_machine_to_link_one_link(mock_connect_to_link_machine, mock_deploy_link, docker_manager,
+                                          default_device, default_link):
+    docker_manager.connect_machine_to_link(default_device, default_link)
+
+    mock_deploy_link.assert_called_once_with(default_link)
+    mock_connect_to_link_machine.assert_called_once_with(default_device, default_link)
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_link")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.connect_to_link")
+def test_connect_machine_to_link_two_links(mock_connect_to_link_machine, mock_deploy_link, docker_manager,
+                                           default_device, default_link, default_link_b):
+    docker_manager.connect_machine_to_link(default_device, default_link)
+
+    mock_deploy_link.assert_called_with(default_link)
+    mock_connect_to_link_machine.assert_called_with(default_device, default_link)
+
+    docker_manager.connect_machine_to_link(default_device, default_link_b)
+
+    mock_deploy_link.assert_called_with(default_link_b)
+    mock_connect_to_link_machine.assert_called_with(default_device, default_link_b)
+
+    assert mock_deploy_link.call_count == 2
+    assert mock_connect_to_link_machine.call_count == 2
+
+
+def test_connect_machine_to_link_no_machine_lab(docker_manager, default_device, default_link):
+    default_device.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.connect_machine_to_link(default_device, default_link)
+
+
+def test_connect_machine_to_link_no_link_lab(docker_manager, default_device, default_link):
+    default_link.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.connect_machine_to_link(default_device, default_link)
+
+
+#
+# TEST: disconnect_machine_from_link
+#
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.undeploy_link")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.disconnect_from_link")
+def test_disconnect_machine_from_link_one_link(mock_disconnect_from_link_machine, mock_undeploy_link, docker_manager,
+                                               default_device, default_link):
+    default_device.add_interface(default_link)
+
+    docker_manager.disconnect_machine_from_link(default_device, default_link)
+
+    mock_undeploy_link.assert_called_once_with(default_link)
+    mock_disconnect_from_link_machine.assert_called_once_with(default_device, default_link)
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.undeploy_link")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.disconnect_from_link")
+def test_disconnect_machine_from_link_two_links(mock_disconnect_from_link_machine, mock_undeploy_link, docker_manager,
+                                                default_device, default_link, default_link_b):
+    default_device.add_interface(default_link)
+    default_device.add_interface(default_link_b)
+
+    docker_manager.disconnect_machine_from_link(default_device, default_link)
+
+    mock_undeploy_link.assert_called_with(default_link)
+    mock_disconnect_from_link_machine.assert_called_with(default_device, default_link)
+
+    docker_manager.disconnect_machine_from_link(default_device, default_link_b)
+
+    mock_undeploy_link.assert_called_with(default_link_b)
+    mock_disconnect_from_link_machine.assert_called_with(default_device, default_link_b)
+
+    assert mock_undeploy_link.call_count == 2
+    assert mock_disconnect_from_link_machine.call_count == 2
+
+
+def test_disconnect_machine_from_link_no_machine_lab(docker_manager, default_device, default_link):
+    default_device.lab = None
+
+    default_device.add_interface(default_link)
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.disconnect_machine_from_link(default_device, default_link)
+
+
+def test_disconnect_machine_from_link_no_link_lab(docker_manager, default_device, default_link):
+    default_link.lab = None
+
+    default_device.add_interface(default_link)
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.disconnect_machine_from_link(default_device, default_link)
+
+
+#
+# TEST: undeploy_machine
+#
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.undeploy")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.undeploy")
+def test_undeploy_machine(mock_machine_undeploy, mock_link_undeploy, docker_manager, default_device, default_link):
+    default_device.add_interface(default_link)
+
+    docker_manager.undeploy_machine(default_device)
+
+    mock_machine_undeploy.assert_called_once_with(default_device.lab.hash, selected_machines={default_device.name})
+    mock_link_undeploy.assert_called_once_with(default_device.lab.hash, selected_links={default_link.name})
+
+
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.undeploy")
+@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.undeploy")
+def test_undeploy_machine_two_machines(mock_machine_undeploy, mock_link_undeploy, docker_manager, two_device_scenario):
+    device_1 = two_device_scenario.get_or_new_machine('pc1')
+    device_1_links = {x.name for x in device_1.interfaces.values()}
+
+    docker_manager.undeploy_machine(device_1)
+
+    mock_machine_undeploy.assert_called_once_with(two_device_scenario.hash, selected_machines={device_1.name})
+    mock_link_undeploy.assert_called_once_with(two_device_scenario.hash, selected_links=device_1_links)
+
+
+def test_undeploy_machine_no_lab(docker_manager, default_device):
+    default_device.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.undeploy_machine(default_device)
+
+
 #
-# TEST: update_lab
-#
-@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_lab")
-@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.update")
-@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.create")
-def test_update_lab_empty_lab(mock_create_link, mock_update_machine, mock_deploy_lab, docker_manager,
-                              two_device_scenario):
-    docker_manager.update_lab(two_device_scenario)
-    links = list(two_device_scenario.links.values())
-    mock_create_link.assert_any_call(links.pop())
-    mock_create_link.assert_any_call(links.pop())
-    assert mock_create_link.call_count == 2
-    assert len(links) == 0
-    machines = list(two_device_scenario.machines.values())
-    mock_deploy_lab.assert_any_call(two_device_scenario, selected_machines={machines.pop().name})
-    mock_deploy_lab.assert_any_call(two_device_scenario, selected_machines={machines.pop().name})
-    assert mock_deploy_lab.call_count == 2
-    assert len(machines) == 0
-
-
-@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.deploy_lab")
-@mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.update")
-@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.create")
-def test_update_lab_update_machine(mock_create_link, mock_update_machine, mock_deploy_lab, docker_manager,
-                                   two_device_scenario):
-    two_device_scenario.machines['pc1'].api_object = Mock()
-    docker_manager.update_lab(two_device_scenario)
-    links = list(two_device_scenario.links.values())
-    mock_create_link.assert_any_call(links.pop())
-    mock_create_link.assert_any_call(links.pop())
-    assert mock_create_link.call_count == 2
-    assert len(links) == 0
-    mock_deploy_lab.assert_called_once_with(two_device_scenario, selected_machines={"pc2"})
-    assert mock_deploy_lab.call_count == 1
-    mock_update_machine.assert_called_once_with(two_device_scenario.machines['pc1'])
+# TEST: undeploy_link
+#
+@mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.undeploy")
+def test_undeploy_link(mock_link_undeploy, docker_manager, default_link):
+    docker_manager.undeploy_link(default_link)
+    mock_link_undeploy.assert_called_once_with(default_link.lab.hash, selected_links={default_link.name})
+
+
+def test_undeploy_link_no_lab(docker_manager, default_link):
+    default_link.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        docker_manager.undeploy_link(default_link)
 
 
 #
@@ -220,7 +510,7 @@ def test_get_machine_api_object_lab_name_no_user(mock_get_machines_api_objects,
 
 @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters")
 def test_get_machine_api_object_no_name_no_hash(mock_get_machines_api_objects, docker_manager, default_device):
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         docker_manager.get_machine_api_object("test_device", all_users=True)
     assert not mock_get_machines_api_objects.called
 
@@ -228,7 +518,7 @@ def test_get_machine_api_object_no_name_no_hash(mock_get_machines_api_objects, d
 @mock.patch("src.Kathara.manager.docker.DockerMachine.DockerMachine.get_machines_api_objects_by_filters")
 def test_get_machine_api_object_device_not_found(mock_get_machines_api_objects, docker_manager, default_device):
     mock_get_machines_api_objects.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         docker_manager.get_machine_api_object("test_device", lab_name="lab_name", all_users=True)
     mock_get_machines_api_objects.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name"),
                                                           machine_name="test_device", user=None)
@@ -289,12 +579,6 @@ def test_get_machines_api_objects_device_not_found(mock_get_machines_api_objects
 #
 # TESTS: get_link_api_object
 #
-@pytest.fixture()
-@mock.patch("docker.models.networks.Network")
-def docker_network(mock_network):
-    return mock_network
-
-
 @mock.patch("src.Kathara.utils.get_current_user_name")
 @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters")
 def test_get_link_api_object_lab_hash_user(mock_get_links_api_objects, mock_get_current_user_name, docker_manager,
@@ -337,7 +621,7 @@ def test_get_link_api_object_lab_name_no_user(mock_get_links_api_objects,
 
 @mock.patch("src.Kathara.manager.docker.DockerLink.DockerLink.get_links_api_objects_by_filters")
 def test_get_link_api_object_no_name_no_hash(mock_get_links_api_objects, docker_manager):
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         docker_manager.get_link_api_object("test_link", all_users=True)
     assert not mock_get_links_api_objects.called
 
@@ -346,7 +630,7 @@ def test_get_link_api_object_no_name_no_hash(mock_get_links_api_objects, docker_
 def test_get_link_api_object_not_found(mock_get_links_api_objects,
                                        docker_manager):
     mock_get_links_api_objects.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         docker_manager.get_link_api_object("test_link", lab_name="lab_name_value", all_users=True)
     mock_get_links_api_objects.assert_called_once_with(lab_hash=generate_urlsafe_hash("lab_name_value"),
                                                        link_name="test_link", user=None)
@@ -407,6 +691,146 @@ def test_get_links_api_objects_not_found(mock_get_links_api_objects, docker_mana
     assert not links
 
 
+#
+# TESTS: get_lab_from_api
+#
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_get_lab_from_api_lab_name_all_info(mock_get_links_api_objects, mock_get_machines_api_objects, docker_container,
+                                            docker_network, docker_manager):
+    mock_get_machines_api_objects.return_value = [docker_container]
+    mock_get_links_api_objects.return_value = [docker_network]
+    lab = docker_manager.get_lab_from_api(lab_name="lab_test")
+    assert len(lab.machines) == 1
+    assert docker_container.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(docker_container.labels["name"])
+    assert reconstructed_device.meta["privileged"]
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert reconstructed_device.meta["mem"] == "64M"
+    assert reconstructed_device.meta["cpu"] == 0.1
+    assert reconstructed_device.meta["envs"]["test"] == "path"
+    assert reconstructed_device.meta["ports"][(3000, "udp")] == 55
+    assert reconstructed_device.meta["sysctls"]["sysctl.test"] == "0"
+    assert reconstructed_device.meta["bridged"]
+    assert len(lab.links) == 1
+    assert docker_network.attrs["Labels"]["name"] in lab.links
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_get_lab_from_api_lab_hash_all_info(mock_get_links_api_objects, mock_get_machines_api_objects, docker_container,
+                                            docker_network, docker_manager):
+    mock_get_machines_api_objects.return_value = [docker_container]
+    mock_get_links_api_objects.return_value = [docker_network]
+    lab = docker_manager.get_lab_from_api(lab_hash="lab_hash")
+    assert lab.hash == "lab_hash"
+    assert lab.name == "reconstructed_lab"
+    assert len(lab.machines) == 1
+    assert docker_container.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(docker_container.labels["name"])
+    assert reconstructed_device.meta["privileged"]
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert reconstructed_device.meta["mem"] == "64M"
+    assert reconstructed_device.meta["cpu"] == 0.1
+    assert reconstructed_device.meta["envs"]["test"] == "path"
+    assert reconstructed_device.meta["ports"][(3000, "udp")] == 55
+    assert reconstructed_device.meta["sysctls"]["sysctl.test"] == "0"
+    assert reconstructed_device.meta["bridged"]
+    assert len(lab.links) == 1
+    assert docker_network.attrs["Labels"]["name"] in lab.links
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_get_lab_from_api_lab_name_empty_meta(mock_get_links_api_objects, mock_get_machines_api_objects,
+                                              docker_container_empty_meta, docker_manager):
+    mock_get_machines_api_objects.return_value = [docker_container_empty_meta]
+    mock_get_links_api_objects.return_value = []
+    lab = docker_manager.get_lab_from_api(lab_name="lab_test")
+    assert len(lab.machines) == 1
+    assert docker_container_empty_meta.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(docker_container_empty_meta.labels["name"])
+    assert not reconstructed_device.meta["privileged"]
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert "mem" not in reconstructed_device.meta
+    assert "cpu" not in reconstructed_device.meta
+    assert reconstructed_device.meta["envs"]["test"] == "path"
+    assert reconstructed_device.meta["ports"] == {}
+    assert reconstructed_device.meta["sysctls"]["sysctl.test"] == "0"
+    assert not reconstructed_device.meta["bridged"]
+    assert len(lab.links) == 0
+
+
+def test_get_lab_from_api_exception(docker_manager):
+    with pytest.raises(InvocationError):
+        docker_manager.get_lab_from_api()
+
+
+#
+# TESTS: update_lab_from_api
+#
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_update_lab_from_api_add_link(mock_get_links_api_objects, mock_get_machines_api_objects, docker_container,
+                                      docker_network, docker_network_b, docker_manager):
+    lab = Lab("test")
+    device = lab.get_or_new_machine(docker_container.labels["name"])
+    link = Link(lab, docker_network.attrs["Labels"]["name"])
+    device.add_interface(link)
+    mock_get_machines_api_objects.return_value = [docker_container]
+    mock_get_links_api_objects.return_value = [docker_network, docker_network_b]
+    docker_container.attrs["NetworkSettings"]["Networks"] = ["kathara_user_hash_test_network",
+                                                             "kathara_user_hash_test_network_b"]
+    docker_manager.update_lab_from_api(lab)
+    assert len(lab.machines) == 1
+    assert docker_container.labels["name"] in lab.machines
+    assert len(lab.links) == 2
+    assert docker_network.attrs["Labels"]["name"] in lab.links
+    assert docker_network_b.attrs["Labels"]["name"] in lab.links
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_update_lab_from_api_remove_link(mock_get_links_api_objects, mock_get_machines_api_objects, docker_container,
+                                         docker_network, docker_manager):
+    lab = Lab("test")
+    device = lab.get_or_new_machine(docker_container.labels["name"])
+    link = Link(lab, docker_network.attrs["Labels"]["name"])
+    device.add_interface(link)
+    mock_get_machines_api_objects.return_value = [docker_container]
+    mock_get_links_api_objects.return_value = []
+    docker_container.attrs["NetworkSettings"]["Networks"] = []
+
+    docker_manager.update_lab_from_api(lab)
+    assert len(lab.machines) == 1
+    assert docker_container.labels["name"] in lab.machines
+    assert len(lab.links) == 0
+
+
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_api_objects")
+def test_update_lab_from_api_add_remove_link(mock_get_links_api_objects, mock_get_machines_api_objects,
+                                             docker_container,
+                                             docker_network, docker_network_b, docker_manager):
+    lab = Lab("test")
+    device = lab.get_or_new_machine(docker_container.labels["name"])
+    link = Link(lab, docker_network.attrs["Labels"]["name"])
+    device.add_interface(link)
+
+    mock_get_machines_api_objects.return_value = [docker_container]
+    mock_get_links_api_objects.return_value = [docker_network_b]
+    docker_container.attrs["NetworkSettings"]["Networks"] = ["kathara_user_hash_test_network_b"]
+
+    docker_manager.update_lab_from_api(lab)
+    assert len(lab.machines) == 1
+    assert docker_container.labels["name"] in lab.machines
+    assert len(lab.links) == 1
+    assert docker_network_b.attrs["Labels"]["name"] in lab.links
+
+
 #
 # TESTS: get_machines_stats
 #
@@ -469,7 +893,7 @@ def test_get_machine_stats_lab_hash_user(mock_get_machines_stats, default_device
 
 @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_machines_stats")
 def test_get_machine_stats_no_name_no_hash(mock_get_machines_stats, docker_manager):
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         next(docker_manager.get_machine_stats(machine_name="test_device", all_users=True))
     assert not mock_get_machines_stats.called
 
@@ -539,6 +963,6 @@ def test_get_link_stats_lab_hash_user(mock_get_links_stats, docker_network, dock
 @mock.patch("src.Kathara.manager.docker.DockerManager.DockerManager.get_links_stats")
 def test_get_link_stats_no_lab_hash_and_no_name(mock_get_links_stats, docker_network, docker_manager):
     mock_get_links_stats.return_value = iter([{"test_network": DockerLinkStats(docker_network)}])
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         next(docker_manager.get_link_stats(link_name="test_network"))
     assert not mock_get_links_stats.called
diff --git a/tests/manager/docker/docker_plugin_test.py b/tests/manager/docker/docker_plugin_test.py
index 2fe4d43a..a6e4dfe4 100644
--- a/tests/manager/docker/docker_plugin_test.py
+++ b/tests/manager/docker/docker_plugin_test.py
@@ -10,6 +10,7 @@
 sys.path.insert(0, './')
 
 from src.Kathara.manager.docker.DockerPlugin import DockerPlugin
+from src.Kathara.exceptions import DockerPluginError
 
 
 @pytest.fixture()
@@ -103,7 +104,7 @@ def test_check_and_download_remote_plugin_not_installed(mock_setting_get_instanc
     mock_setting_get_instance.return_value = mock_setting
     docker_plugin.client.plugins.get.return_value = None
     docker_plugin.client.plugins.get.side_effect = NotFound('Fail')
-    with pytest.raises(Exception) as e:
+    with pytest.raises(DockerPluginError) as e:
         docker_plugin.check_and_download_plugin()
 
     assert str(e.value) == "Kathara Network Plugin not found on remote Docker connection."
@@ -120,7 +121,7 @@ def test_check_and_download_remote_plugin_not_enabled(mock_setting_get_instance,
     mock_setting.remote_url = "http://remote-url.kt"
     mock_setting_get_instance.return_value = mock_setting
     docker_plugin.client.plugins.get.return_value = mock_plugin
-    with pytest.raises(Exception) as e:
+    with pytest.raises(DockerPluginError) as e:
         docker_plugin.check_and_download_plugin()
 
     assert str(e.value) == "Kathara Network Plugin not enabled on remote Docker connection."
diff --git a/tests/manager/kubernetes/kubernetes_link_test.py b/tests/manager/kubernetes/kubernetes_link_test.py
index 3fe6a970..36584f8a 100644
--- a/tests/manager/kubernetes/kubernetes_link_test.py
+++ b/tests/manager/kubernetes/kubernetes_link_test.py
@@ -1,3 +1,4 @@
+import copy
 import sys
 from unittest import mock
 from unittest.mock import Mock
@@ -9,6 +10,7 @@
 
 from src.Kathara.model.Lab import Lab
 from src.Kathara.manager.kubernetes.KubernetesLink import KubernetesLink
+from src.Kathara.exceptions import LinkNotFoundError
 
 
 class FakeConfig(object):
@@ -400,7 +402,7 @@ def test_get_links_by_filters_only_link_name(kubernetes_link):
 #
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink._undeploy_link")
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters")
-def undeploy(mock_get_links_by_filters, mock_undeploy_link, kubernetes_network):
+def test_undeploy(mock_get_links_by_filters, mock_undeploy_link, kubernetes_network, kubernetes_link):
     lab = Lab("Default scenario")
     lab.get_or_new_link("A")
     lab.get_or_new_link("B")
@@ -426,6 +428,27 @@ def test_undeploy_empty_lab(mock_get_links_by_filters, mock_undeploy_link, kuber
     assert not mock_undeploy_link.called
 
 
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink._undeploy_link")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters")
+def test_undeploy_selected_links(mock_get_links_by_filters, mock_undeploy_link, kubernetes_network, kubernetes_link):
+    lab = Lab("Default scenario")
+    lab.get_or_new_link("A")
+    lab.get_or_new_link("B")
+    lab.get_or_new_link("C")
+
+    kubernetes_network_1 = copy.deepcopy(kubernetes_network)
+    kubernetes_network_1["metadata"]["name"] = "netprefix-b"
+    kubernetes_network_2 = copy.deepcopy(kubernetes_network)
+    kubernetes_network_2["metadata"]["name"] = "netprefix-c"
+
+    mock_get_links_by_filters.return_value = [kubernetes_network, kubernetes_network_1, kubernetes_network_2]
+
+    kubernetes_link.undeploy("lab_hash", selected_links={"netprefix-b"})
+
+    mock_get_links_by_filters.called_once_with(lab.hash)
+    assert mock_undeploy_link.call_count == 1
+
+
 #
 # TEST: wipe
 #
@@ -473,7 +496,7 @@ def test_get_links_stats_lab_hash_link_not_found(mock_get_links_api_objects_by_f
                                                  kubernetes_network):
     kubernetes_network['metadata']['name'] = "test_network"
     mock_get_links_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         next(kubernetes_link.get_links_stats(lab_hash="lab_hash"))
     mock_get_links_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", link_name=None)
 
@@ -483,6 +506,6 @@ def test_get_links_stats_lab_hash_link_name_not_found(mock_get_links_api_objects
                                                       kubernetes_network):
     kubernetes_network['metadata']['name'] = "test_network"
     mock_get_links_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         next(kubernetes_link.get_links_stats(lab_hash="lab_hash", link_name="test_network"))
     mock_get_links_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash", link_name="test_network")
diff --git a/tests/manager/kubernetes/kubernetes_machine_test.py b/tests/manager/kubernetes/kubernetes_machine_test.py
index 47204e01..fafc0766 100644
--- a/tests/manager/kubernetes/kubernetes_machine_test.py
+++ b/tests/manager/kubernetes/kubernetes_machine_test.py
@@ -11,6 +11,7 @@
 from src.Kathara.model.Lab import Lab
 from src.Kathara.model.Machine import Machine
 from src.Kathara.manager.kubernetes.KubernetesMachine import KubernetesMachine, STARTUP_COMMANDS
+from src.Kathara.exceptions import MachineNotFoundError
 
 
 #
@@ -631,7 +632,7 @@ def test_get_machines_stats_no_hash_no_name(mock_get_machines_api_objects_by_fil
 def test_get_machines_stats_lab_hash_device_not_found(mock_get_machines_api_objects_by_filters, kubernetes_machine,
                                                       default_device):
     mock_get_machines_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         next(kubernetes_machine.get_machines_stats(lab_hash="lab_hash"))
     mock_get_machines_api_objects_by_filters.assert_called_once_with(lab_hash="lab_hash",
                                                                      machine_name=None)
@@ -641,7 +642,7 @@ def test_get_machines_stats_lab_hash_device_not_found(mock_get_machines_api_obje
 def test_get_machines_stats_device_not_found(mock_get_machines_api_objects_by_filters, kubernetes_machine,
                                              default_device):
     mock_get_machines_api_objects_by_filters.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         next(kubernetes_machine.get_machines_stats())
     mock_get_machines_api_objects_by_filters.assert_called_once_with(lab_hash=None,
                                                                      machine_name=None)
diff --git a/tests/manager/kubernetes/kubernetes_manager_test.py b/tests/manager/kubernetes/kubernetes_manager_test.py
index 81b97ca2..82364bff 100644
--- a/tests/manager/kubernetes/kubernetes_manager_test.py
+++ b/tests/manager/kubernetes/kubernetes_manager_test.py
@@ -1,58 +1,34 @@
+import json
 import sys
 from unittest import mock
 from unittest.mock import Mock
 
 import pytest
+from kubernetes import client
 
 sys.path.insert(0, './')
 
+from src.Kathara.exceptions import NotSupportedError, MachineNotFoundError, LabNotFoundError, InvocationError, \
+    LinkNotFoundError
 from src.Kathara.manager.kubernetes.KubernetesMachine import KubernetesMachine
 from src.Kathara.manager.kubernetes.KubernetesManager import KubernetesManager
 from src.Kathara.model.Lab import Lab
 from src.Kathara.model.Machine import Machine
 from src.Kathara.utils import generate_urlsafe_hash
-from src.Kathara.manager.kubernetes.KubernetesLink import KubernetesLink
 from src.Kathara.manager.kubernetes.stats.KubernetesMachineStats import KubernetesMachineStats
 from src.Kathara.manager.kubernetes.stats.KubernetesLinkStats import KubernetesLinkStats
 
 
-class FakeConfig(object):
-    def __init__(self):
-        self.api_key = {
-            'authorization': 'user123'
-        }
-
-
-class FakeManager(object):
-    def __init__(self, ctx):
-        pass
-
-    def start(self):
-        pass
-
-    def __enter__(self):
-        return self
-
-    def dict(self):
-        return {}
-
-    def __exit__(self, exit_type, value, traceback):
-        return True
-
-
-EXPECTED_NETWORK_ID = 4694369
-NETWORK_IDS_SINGLE = {
-    4694369: 1
-}
-NETWORK_IDS_DOUBLE = {
-    4694369: 1,
-    4694370: 1
-}
-
-
 #
 # FIXTURE
 #
+@pytest.fixture()
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesNamespace")
+def kubernetes_namespace(mock_kubernetes_namespace):
+    mock_kubernetes_namespace.create = Mock()
+    return mock_kubernetes_namespace
+
+
 @pytest.fixture()
 @mock.patch("kubernetes.client.api.apps_v1_api.AppsV1Api")
 @mock.patch("kubernetes.client.api.core_v1_api.CoreV1Api")
@@ -64,19 +40,17 @@ def kubernetes_machine(mock_kubernetes_namespace, mock_config_map, mock_core_v1_
 
 @pytest.fixture()
 @mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
-@mock.patch("src.Kathara.manager.kubernetes.KubernetesConfig.KubernetesConfig")
-@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine")
-@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink")
-@mock.patch("src.Kathara.manager.kubernetes.KubernetesNamespace")
-def kubernetes_manager(mock_kubernetes_namespace, mock_kubernetes_link, mock_kubernetes_machine,
-                       mock_kubernetes_load_config, mock_setting_get_instance):
+def kubernetes_manager(mock_setting_get_instance, kubernetes_namespace):
     mock_setting = Mock()
     mock_setting.configure_mock(**{
         'manager': 'kubernetes',
-        'device_prefix': 'devprefix'
+        'device_prefix': 'devprefix',
+        'net_prefix': 'netprefix'
     })
+    kube_manager = KubernetesManager()
+    kube_manager.k8s_namespace = kubernetes_namespace
     mock_setting_get_instance.return_value = mock_setting
-    return KubernetesManager()
+    return kube_manager
 
 
 @pytest.fixture()
@@ -101,6 +75,17 @@ def default_link():
     return Link(Lab("default_scenario"), "A")
 
 
+@pytest.fixture()
+def two_device_scenario():
+    lab = Lab("Default scenario")
+    pc1 = lab.get_or_new_machine("pc1", **{'image': 'kathara/test1'})
+    pc2 = lab.get_or_new_machine("pc2", **{'image': 'kathara/test2'})
+    lab.connect_machine_to_link(pc1.name, "A")
+    lab.connect_machine_to_link(pc1.name, "B")
+    lab.connect_machine_to_link(pc2.name, "A")
+    return lab
+
+
 @pytest.fixture()
 def kubernetes_network():
     return {
@@ -127,13 +112,344 @@ def kubernetes_network():
 
 
 @pytest.fixture()
-@mock.patch("kubernetes.client.api.custom_objects_api.CustomObjectsApi")
-@mock.patch("kubernetes.client.Configuration")
-@mock.patch("src.Kathara.manager.kubernetes.KubernetesNamespace")
-def kubernetes_link(kubernetes_namespace_mock, config_mock, _):
-    config_mock.get_default_copy.return_value = FakeConfig()
+def kubernetes_pod_1():
+    networks = [
+        {'name': 'netprefix-a'},
+        {'name': 'netprefix-b'}
+    ]
+
+    pod_metadata = client.V1ObjectMeta(deletion_grace_period_seconds=0,
+                                       annotations={"k8s.v1.cni.cncf.io/networks": json.dumps(networks)},
+                                       labels={"name": "pc1", "app": "kathara"}
+                                       )
+    pod_status = client.V1PodStatus(phase='Running')
+
+    return client.V1Pod(
+        metadata=pod_metadata,
+        status=pod_status
+    )
+
+
+@pytest.fixture()
+def kubernetes_pod_2():
+    networks = [
+        {'name': 'netprefix-a'}
+    ]
+
+    pod_metadata = client.V1ObjectMeta(deletion_grace_period_seconds=0,
+                                       annotations={"k8s.v1.cni.cncf.io/networks": json.dumps(networks)},
+                                       labels={"name": "pc2", "app": "kathara"}
+                                       )
+
+    pod_container_resources = client.V1ResourceRequirements(
+        limits={'memory': '64m', 'cpu': '1000'}
+    )
+    pod_container_env = [
+        client.V1EnvVar('_MEGALOS_SHELL', '/bin/bash'),
+        client.V1EnvVar('test', 'path')
+    ]
+    pod_container_ports = [
+        client.V1ContainerPort(name="port", container_port=55, host_port=3000, protocol="UDP")
+    ]
+
+    pod_container = client.V1Container(
+        name="pc1_container",
+        image="docker.io/test_image",
+        resources=pod_container_resources,
+        env=pod_container_env,
+        ports=pod_container_ports
+    )
+    pod_spec = client.V1PodSpec(
+        containers=[pod_container]
+    )
+
+    pod_status = client.V1PodStatus(phase='Running')
+
+    return client.V1Pod(
+        metadata=pod_metadata,
+        spec=pod_spec,
+        status=pod_status
+    )
+
+
+@pytest.fixture()
+def kubernetes_empty_pod():
+    pod_metadata = client.V1ObjectMeta(deletion_grace_period_seconds=0,
+                                       annotations={"k8s.v1.cni.cncf.io/networks": json.dumps([])},
+                                       labels={"name": "pc2", "app": "kathara"}
+                                       )
+
+    pod_container_resources = client.V1ResourceRequirements()
+    pod_container_env = [client.V1EnvVar('_MEGALOS_SHELL', '/bin/bash')]
+
+    pod_container = client.V1Container(
+        name="pc1_container",
+        image="docker.io/test_image",
+        resources=pod_container_resources,
+        env=pod_container_env,
+        ports=[]
+    )
+    pod_spec = client.V1PodSpec(
+        containers=[pod_container]
+    )
+
+    pod_status = client.V1PodStatus(phase='Running')
+
+    return client.V1Pod(
+        metadata=pod_metadata,
+        spec=pod_spec,
+        status=pod_status
+    )
+
+
+@pytest.fixture()
+def two_device_scenario(kubernetes_pod_1, kubernetes_pod_2):
+    lab = Lab("Default scenario")
+    pc1 = lab.get_or_new_machine("pc1", **{'image': 'kathara/test1'})
+    pc1.api_object = kubernetes_pod_1
+    pc2 = lab.get_or_new_machine("pc2", **{'image': 'kathara/test2'})
+    pc2.api_object = kubernetes_pod_2
+    lab.connect_machine_to_link(pc1.name, "A")
+    lab.connect_machine_to_link(pc1.name, "B")
+    lab.connect_machine_to_link(pc2.name, "A")
+    return lab
+
+
+#
+# TEST: deploy_lab
+#
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.deploy_machines")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.deploy_links")
+def test_deploy_lab(mock_deploy_links, mock_deploy_machines, kubernetes_manager, two_device_scenario):
+    kubernetes_manager.deploy_lab(two_device_scenario)
+    mock_deploy_links.assert_called_once_with(two_device_scenario, selected_links=None)
+    mock_deploy_machines.assert_called_once_with(two_device_scenario, selected_machines=None)
+
+
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesNamespace.KubernetesNamespace.create")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.deploy_machines")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.deploy_links")
+def test_deploy_lab_selected_machines(mock_deploy_links, mock_deploy_machines, mock_namespace_create,
+                                      kubernetes_manager, two_device_scenario: Lab):
+    kubernetes_manager.deploy_lab(two_device_scenario, selected_machines={"pc1"})
+
+    mock_deploy_links.assert_called_once_with(two_device_scenario, selected_links={"A", "B"})
+    mock_deploy_machines.assert_called_once_with(two_device_scenario, selected_machines={"pc1"})
+
+
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.deploy_machines")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.deploy_links")
+def test_deploy_lab_selected_machines_exception(mock_deploy_links, mock_deploy_machines, kubernetes_manager,
+                                                two_device_scenario: Lab):
+    with pytest.raises(MachineNotFoundError):
+        kubernetes_manager.deploy_lab(two_device_scenario, selected_machines={"pc3"})
+    assert not mock_deploy_machines.called
+    assert not mock_deploy_links.called
+
+
+#
+# TEST: deploy_machine
+#
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.deploy_links")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.deploy_machines")
+def test_deploy_machine(mock_deploy_machines, mock_deploy_links, kubernetes_manager,
+                        default_device, default_link):
+    default_device.add_interface(default_link)
+
+    kubernetes_manager.deploy_machine(default_device)
+    kubernetes_manager.k8s_namespace.create.assert_called_once_with(default_device.lab)
+    mock_deploy_links.assert_called_once_with(default_device.lab, selected_links={default_link.name})
+    mock_deploy_machines.assert_called_once_with(default_device.lab, selected_machines={default_device.name})
+
+
+def test_deploy_machine_no_lab(kubernetes_manager, default_device):
+    default_device.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        kubernetes_manager.deploy_machine(default_device)
+
+
+#
+# TEST: deploy_link
+#
+@mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.deploy_links")
+def test_deploy_link(mock_deploy_links, mock_setting_get_instance, kubernetes_manager, default_link):
+    mock_setting = Mock()
+    mock_setting.configure_mock(**{
+        'net_prefix': 'netprefix'
+    })
+    mock_setting_get_instance.return_value = mock_setting
+
+    kubernetes_manager.deploy_link(default_link)
+
+    kubernetes_manager.k8s_namespace.create.assert_called_once_with(default_link.lab)
+    mock_deploy_links.assert_called_once_with(default_link.lab, selected_links={default_link.name})
+
+
+def test_deploy_link_no_lab(kubernetes_manager, default_link):
+    default_link.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        kubernetes_manager.deploy_link(default_link)
+
+
+#
+# TEST: connect_machine_to_link
+#
+def test_connect_machine_to_link_not_supported(kubernetes_manager, default_device, default_link):
+    with pytest.raises(NotSupportedError):
+        kubernetes_manager.connect_machine_to_link(default_device, default_link)
+
+
+#
+# TEST: disconnect_machine_from_link
+#
+def test_disconnect_machine_from_link_not_supported(kubernetes_manager, default_device, default_link):
+    with pytest.raises(NotSupportedError):
+        kubernetes_manager.disconnect_machine_from_link(default_device, default_link)
+
+
+#
+# TEST: undeploy_machine
+#
+@mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.undeploy")
+def test_undeploy_machine(mock_machine_undeploy, mock_link_undeploy, mock_get_machines_api_objects,
+                          mock_setting_get_instance, kubernetes_manager, default_device, default_link):
+    mock_setting = Mock()
+    mock_setting.configure_mock(**{
+        'net_prefix': 'netprefix'
+    })
+    mock_setting_get_instance.return_value = mock_setting
 
-    return KubernetesLink(kubernetes_namespace_mock)
+    default_device.add_interface(default_link)
+
+    mock_get_machines_api_objects.return_value = []
+
+    kubernetes_manager.undeploy_machine(default_device)
+
+    mock_machine_undeploy.assert_called_once_with(default_device.lab.hash, selected_machines={default_device.name})
+    mock_link_undeploy.assert_called_once_with(default_device.lab.hash, selected_links={'netprefix-a'})
+    kubernetes_manager.k8s_namespace.undeploy.assert_called_once_with(lab_hash=default_device.lab.hash)
+
+
+@mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.undeploy")
+def test_undeploy_machine_two_machines(mock_machine_undeploy, mock_link_undeploy, mock_get_machines_api_objects,
+                                       mock_setting_get_instance, kubernetes_manager, two_device_scenario):
+    mock_setting = Mock()
+    mock_setting.configure_mock(**{
+        'net_prefix': 'netprefix'
+    })
+    mock_setting_get_instance.return_value = mock_setting
+
+    device_1 = two_device_scenario.get_or_new_machine('pc1')
+    device_2 = two_device_scenario.get_or_new_machine('pc2')
+
+    mock_get_machines_api_objects.return_value = [device_2.api_object]
+
+    kubernetes_manager.undeploy_machine(device_1)
+
+    mock_machine_undeploy.assert_called_with(two_device_scenario.hash, selected_machines={device_1.name})
+    mock_link_undeploy.assert_called_with(two_device_scenario.hash, selected_links={'netprefix-b'})
+    assert not kubernetes_manager.k8s_namespace.undeploy.called
+
+    mock_get_machines_api_objects.return_value = []
+
+    kubernetes_manager.undeploy_machine(device_2)
+
+    mock_machine_undeploy.assert_called_with(two_device_scenario.hash, selected_machines={device_2.name})
+    mock_link_undeploy.assert_called_with(two_device_scenario.hash, selected_links={'netprefix-a'})
+    kubernetes_manager.k8s_namespace.undeploy.assert_called_once_with(lab_hash=two_device_scenario.hash)
+
+
+def test_undeploy_machine_no_lab(kubernetes_manager, default_device):
+    default_device.lab = None
+
+    with pytest.raises(LabNotFoundError):
+        kubernetes_manager.undeploy_machine(default_device)
+
+
+#
+# TEST: undeploy_link
+#
+@mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+def test_undeploy_link(mock_link_undeploy, mock_get_machines_api_objects, mock_setting_get_instance, kubernetes_manager,
+                       default_link):
+    mock_setting = Mock()
+    mock_setting.configure_mock(**{
+        'net_prefix': 'netprefix'
+    })
+    mock_setting_get_instance.return_value = mock_setting
+
+    mock_get_machines_api_objects.return_value = []
+
+    kubernetes_manager.undeploy_link(default_link)
+
+    mock_link_undeploy.assert_called_once_with(default_link.lab.hash, selected_links={'netprefix-a'})
+
+
+@mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+def test_undeploy_link_machine_running(mock_link_undeploy, mock_get_machines_api_objects, mock_setting_get_instance,
+                                       kubernetes_manager, default_link, kubernetes_pod_2):
+    mock_setting = Mock()
+    mock_setting.configure_mock(**{
+        'net_prefix': 'netprefix'
+    })
+    mock_setting_get_instance.return_value = mock_setting
+
+    mock_get_machines_api_objects.return_value = [kubernetes_pod_2]
+
+    kubernetes_manager.undeploy_link(default_link)
+
+    assert not mock_link_undeploy.called
+
+
+def test_undeploy_link_no_lab(kubernetes_manager, default_link):
+    default_link.lab = None
+    with pytest.raises(LabNotFoundError):
+        kubernetes_manager.undeploy_link(default_link)
+
+
+#
+# TEST: undeploy_lab
+#
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.undeploy")
+def test_undeploy_lab(mock_undeploy_machine, mock_undeploy_link, kubernetes_manager):
+    kubernetes_manager.undeploy_lab('lab_hash')
+    mock_undeploy_machine.assert_called_once_with('lab_hash', selected_machines=None)
+    mock_undeploy_link.assert_called_once_with('lab_hash', selected_links=None)
+    kubernetes_manager.k8s_namespace.undeploy.assert_called_once_with(lab_hash='lab_hash')
+
+
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesNamespace.KubernetesNamespace.undeploy")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.undeploy")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.undeploy")
+def test_undeploy_lab_selected_machines(mock_undeploy_machine, mock_undeploy_link, mock_namespace_undeploy,
+                                        mock_get_machines_api_objects, mock_get_links_api_objects, kubernetes_manager,
+                                        kubernetes_pod_1, kubernetes_pod_2):
+    mock_get_links_api_objects.return_value = [{'metadata': {'name': 'netprefix-a'}},
+                                               {'metadata': {'name': 'netprefix-b'}}]
+    mock_get_machines_api_objects.return_value = [kubernetes_pod_1, kubernetes_pod_2]
+
+    kubernetes_manager.undeploy_lab('lab_hash', selected_machines={'pc1'})
+    mock_get_links_api_objects.assert_called_once_with(lab_hash='lab_hash')
+    mock_get_machines_api_objects.assert_called_once_with(lab_hash='lab_hash')
+    mock_undeploy_machine.assert_called_once_with('lab_hash', selected_machines={'pc1'})
+    mock_undeploy_link.assert_called_once_with('lab_hash', selected_links={'netprefix-b'})
+    assert not mock_namespace_undeploy.called
 
 
 #
@@ -169,7 +485,7 @@ def test_get_machine_api_object_lab_hash_and_name(mock_get_machines_api_objects,
 def test_get_machine_api_object_no_hash_no_name(mock_get_machines_api_objects, kubernetes_manager, default_device):
     default_device.api_object.name = "default_device"
     mock_get_machines_api_objects.return_value = [default_device.api_object]
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         kubernetes_manager.get_machine_api_object(machine_name="default_device")
     assert not mock_get_machines_api_objects.called
 
@@ -177,7 +493,7 @@ def test_get_machine_api_object_no_hash_no_name(mock_get_machines_api_objects, k
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesMachine.KubernetesMachine.get_machines_api_objects_by_filters")
 def test_get_machine_api_object_lab_hash_device_not_found(mock_get_machines_api_objects, kubernetes_manager):
     mock_get_machines_api_objects.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(MachineNotFoundError):
         kubernetes_manager.get_machine_api_object(lab_hash="lab_hash_value", machine_name="default_device")
     mock_get_machines_api_objects.assert_called_once_with(machine_name="default_device", lab_hash="lab_hash_value")
 
@@ -245,7 +561,7 @@ def test_get_link_api_object_lab_hash_and_name(mock_get_links_api_objects, kuber
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters")
 def test_get_link_api_object_no_hash_no_name(mock_get_links_api_objects, kubernetes_manager, kubernetes_network):
     mock_get_links_api_objects.return_value = [kubernetes_network]
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         kubernetes_manager.get_link_api_object(link_name="test_network")
     assert not mock_get_links_api_objects.called
 
@@ -253,11 +569,105 @@ def test_get_link_api_object_no_hash_no_name(mock_get_links_api_objects, kuberne
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_api_objects_by_filters")
 def test_get_link_api_object_lab_hash_cd_not_found(mock_get_links_api_objects, kubernetes_manager, kubernetes_network):
     mock_get_links_api_objects.return_value = []
-    with pytest.raises(Exception):
+    with pytest.raises(LinkNotFoundError):
         kubernetes_manager.get_link_api_object(link_name="test_network", lab_hash="lab_hash_value")
     mock_get_links_api_objects.assert_called_once_with(link_name="test_network", lab_hash="lab_hash_value")
 
 
+#
+# TESTS: get_lab_from_api
+#
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_links_api_objects")
+def test_get_lab_from_api_lab_name_all_info(mock_get_links_api_objects, mock_get_machines_api_objects,
+                                            kubernetes_pod_2, kubernetes_network, kubernetes_manager):
+    mock_get_machines_api_objects.return_value = [kubernetes_pod_2]
+    mock_get_links_api_objects.return_value = [kubernetes_network]
+
+    lab = kubernetes_manager.get_lab_from_api(lab_name="lab_test")
+    assert len(lab.machines) == 1
+
+    assert kubernetes_pod_2.metadata.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(kubernetes_pod_2.metadata.labels["name"])
+    assert "privileged" not in reconstructed_device.meta
+    assert not reconstructed_device.meta["bridged"]
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert reconstructed_device.meta["mem"] == "64M"
+    assert reconstructed_device.meta["cpu"] == 1.0
+    assert reconstructed_device.meta["envs"]["test"] == "path"
+    assert reconstructed_device.meta["ports"][(3000, "udp")] == 55
+    assert reconstructed_device.meta["sysctls"] == {}
+    assert len(lab.links) == 1
+    assert kubernetes_network["metadata"]["labels"]["name"] in lab.links
+
+
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_links_api_objects")
+def test_get_lab_from_api_lab_hash_all_info(mock_get_links_api_objects, mock_get_machines_api_objects,
+                                            kubernetes_pod_2, kubernetes_network, kubernetes_manager):
+    mock_get_machines_api_objects.return_value = [kubernetes_pod_2]
+    mock_get_links_api_objects.return_value = [kubernetes_network]
+
+    lab = kubernetes_manager.get_lab_from_api(lab_hash="lab_hash")
+
+    assert lab.hash == "lab_hash"
+    assert lab.name == "reconstructed_lab"
+    assert len(lab.machines) == 1
+
+    assert kubernetes_pod_2.metadata.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(kubernetes_pod_2.metadata.labels["name"])
+    assert "privileged" not in reconstructed_device.meta
+    assert not reconstructed_device.meta["bridged"]
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert reconstructed_device.meta["mem"] == "64M"
+    assert reconstructed_device.meta["cpu"] == 1.0
+    assert reconstructed_device.meta["envs"]["test"] == "path"
+    assert reconstructed_device.meta["ports"][(3000, "udp")] == 55
+    assert reconstructed_device.meta["sysctls"] == {}
+    assert len(lab.links) == 1
+    assert kubernetes_network["metadata"]["labels"]["name"] in lab.links
+
+
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_api_objects")
+@mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_links_api_objects")
+def test_get_lab_from_api_lab_name_empty_meta(mock_get_links_api_objects, mock_get_machines_api_objects,
+                                              kubernetes_empty_pod, kubernetes_manager):
+    mock_get_machines_api_objects.return_value = [kubernetes_empty_pod]
+    mock_get_links_api_objects.return_value = []
+
+    lab = kubernetes_manager.get_lab_from_api(lab_name="lab_test")
+    assert len(lab.machines) == 1
+    assert kubernetes_empty_pod.metadata.labels["name"] in lab.machines
+    reconstructed_device = lab.get_or_new_machine(kubernetes_empty_pod.metadata.labels["name"])
+    assert "privileged" not in reconstructed_device.meta
+    assert reconstructed_device.meta["image"] == "test_image"
+    assert reconstructed_device.meta["shell"] == "/bin/bash"
+    assert "mem" not in reconstructed_device.meta
+    assert "cpu" not in reconstructed_device.meta
+    assert reconstructed_device.meta["envs"] == {}
+    assert reconstructed_device.meta["ports"] == {}
+    assert reconstructed_device.meta["sysctls"] == {}
+    assert not reconstructed_device.meta["bridged"]
+    assert len(lab.links) == 0
+
+
+def test_get_lab_from_api_exception(kubernetes_manager):
+    with pytest.raises(Exception):
+        kubernetes_manager.get_lab_from_api()
+
+
+#
+# TESTS: update_lab_from_api
+#
+def test_update_lab_from_api_not_supported(kubernetes_manager):
+    lab = Lab("test")
+
+    with pytest.raises(NotSupportedError):
+        kubernetes_manager.update_lab_from_api(lab)
+
+
 #
 # TEST: get_machines_stats
 #
@@ -301,7 +711,7 @@ def test_get_machine_stats_lab_name(mock_get_machines_stats, default_device, kub
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesManager.KubernetesManager.get_machines_stats")
 def test_get_machine_stats_no_hash_no_name(mock_get_machines_stats, kubernetes_manager):
     mock_get_machines_stats.return_value = iter([])
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         next(kubernetes_manager.get_machine_stats(machine_name="test_device"))
     assert not mock_get_machines_stats.called
 
@@ -347,6 +757,6 @@ def test_get_link_stats_lab_name(mock_get_links_stats, kubernetes_network, kuber
 
 @mock.patch("src.Kathara.manager.kubernetes.KubernetesLink.KubernetesLink.get_links_stats")
 def test_get_link_stats_no_lab_hash_and_no_name(mock_get_links_stats, kubernetes_manager):
-    with pytest.raises(Exception):
+    with pytest.raises(InvocationError):
         next(kubernetes_manager.get_link_stats(link_name="test_network"))
     assert not mock_get_links_stats.called
diff --git a/tests/model/lab_test.py b/tests/model/lab_test.py
index ac00eebd..6cf37c4b 100644
--- a/tests/model/lab_test.py
+++ b/tests/model/lab_test.py
@@ -9,7 +9,8 @@
 from src.Kathara.model.Lab import Lab
 from src.Kathara import utils
 from tempfile import mkdtemp
-from src.Kathara.exceptions import MachineOptionError
+from src.Kathara.exceptions import MachineOptionError, MachineAlreadyExistsError, MachineNotFoundError, \
+    LinkAlreadyExistsError, LinkNotFoundError
 
 
 @pytest.fixture()
@@ -17,6 +18,11 @@ def default_scenario():
     return Lab("default_scenario")
 
 
+@pytest.fixture()
+def default_scenario_path():
+    return Lab(None, path="/lab/path")
+
+
 @pytest.fixture()
 def temporary_path():
     return mkdtemp("kathara_test")
@@ -47,6 +53,31 @@ def test_default_scenario_creation(default_scenario: Lab):
     assert default_scenario.hash == utils.generate_urlsafe_hash(default_scenario.name)
 
 
+def test_default_scenario_creation_with_path(default_scenario_path: Lab):
+    assert default_scenario_path.name is None
+    assert default_scenario_path.description is None
+    assert default_scenario_path.version is None
+    assert default_scenario_path.author is None
+    assert default_scenario_path.email is None
+    assert default_scenario_path.web is None
+    assert default_scenario_path.machines == {}
+    assert default_scenario_path.links == {}
+    assert default_scenario_path.general_options == {}
+    assert not default_scenario_path.has_dependencies
+    assert default_scenario_path.path == "/lab/path"
+    assert default_scenario_path.shared_shutdown_path is None
+    assert default_scenario_path.shared_startup_path is None
+    assert default_scenario_path.shared_folder is None
+    assert default_scenario_path.hash == utils.generate_urlsafe_hash(default_scenario_path.path)
+
+
+def test_default_scenario_creation_with_path_and_name(default_scenario_path: Lab):
+    default_scenario_path.name = "lab_with_path"
+    assert default_scenario_path.name == "lab_with_path"
+    assert default_scenario_path.path == "/lab/path"
+    assert default_scenario_path.hash == utils.generate_urlsafe_hash(default_scenario_path.name)
+
+
 def test_directory_scenario_creation_no_shared_files(directory_scenario: Lab, temporary_path: str):
     assert directory_scenario.name == "directory_scenario"
     assert directory_scenario.description is None
@@ -62,7 +93,31 @@ def test_directory_scenario_creation_no_shared_files(directory_scenario: Lab, te
     assert directory_scenario.shared_shutdown_path == os.path.join(temporary_path, 'shared.shutdown')
     assert directory_scenario.shared_startup_path == os.path.join(temporary_path, 'shared.startup')
     assert directory_scenario.shared_folder is None
-    assert directory_scenario.hash == utils.generate_urlsafe_hash(temporary_path)
+    assert directory_scenario.hash == utils.generate_urlsafe_hash(directory_scenario.name)
+
+
+def test_new_machine(default_scenario: Lab):
+    assert len(default_scenario.machines) == 0
+    default_scenario.new_machine("pc1")
+    assert len(default_scenario.machines) == 1
+    assert "pc1" in default_scenario.machines
+
+
+def test_new_machine_already_exists_error(default_scenario: Lab):
+    default_scenario.new_machine("pc1")
+    with pytest.raises(MachineAlreadyExistsError):
+        default_scenario.new_machine("pc1")
+
+
+def test_get_machine(default_scenario: Lab):
+    default_scenario.new_machine("pc1")
+    device = default_scenario.get_machine("pc1")
+    assert device.name == "pc1"
+
+
+def test_get_machine_not_found_error(default_scenario: Lab):
+    with pytest.raises(MachineNotFoundError):
+        default_scenario.get_machine("pc1")
 
 
 def test_get_or_new_machine_not_exist(default_scenario: Lab):
@@ -86,6 +141,30 @@ def test_get_or_new_machine_two_devices(default_scenario: Lab):
     assert default_scenario.machines['pc2']
 
 
+def test_new_link(default_scenario: Lab):
+    assert len(default_scenario.links) == 0
+    link = default_scenario.new_link("A")
+    assert len(default_scenario.links) == 1
+    assert link.name in default_scenario.links
+
+
+def test_new_link_already_exists_error(default_scenario: Lab):
+    default_scenario.new_link("A")
+    with pytest.raises(LinkAlreadyExistsError):
+        default_scenario.new_link("A")
+
+
+def test_get_link(default_scenario: Lab):
+    created_link = default_scenario.new_link("A")
+    link = default_scenario.get_link("A")
+    assert link == created_link
+
+
+def test_get_link_not_found_error(default_scenario: Lab):
+    with pytest.raises(LinkNotFoundError):
+        default_scenario.get_link("A")
+
+
 def test_get_or_new_link_not_exists(default_scenario: Lab):
     default_scenario.get_or_new_link("A")
     assert len(default_scenario.links) == 1
@@ -108,32 +187,35 @@ def test_get_or_new_link_two_cd(default_scenario: Lab):
 
 
 def test_connect_one_machine_to_link(default_scenario: Lab):
-    default_scenario.connect_machine_to_link("pc1", "A")
+    result_1 = default_scenario.connect_machine_to_link("pc1", "A")
     assert len(default_scenario.machines) == 1
     assert default_scenario.machines['pc1']
     assert len(default_scenario.links) == 1
     assert default_scenario.links['A']
     assert default_scenario.machines['pc1'].interfaces[0].name == 'A'
+    assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A'])
 
 
 def test_connect_two_machine_to_link(default_scenario: Lab):
-    default_scenario.connect_machine_to_link("pc1", "A")
+    result_1 = default_scenario.connect_machine_to_link("pc1", "A")
     assert len(default_scenario.machines) == 1
     assert default_scenario.machines['pc1']
     assert len(default_scenario.links) == 1
     assert default_scenario.links['A']
-    default_scenario.connect_machine_to_link("pc2", "A")
+    result_2 = default_scenario.connect_machine_to_link("pc2", "A")
     assert len(default_scenario.machines) == 2
     assert default_scenario.machines['pc2']
     assert len(default_scenario.links) == 1
     assert default_scenario.links['A']
     assert default_scenario.machines['pc1'].interfaces[0].name == 'A'
     assert default_scenario.machines['pc2'].interfaces[0].name == 'A'
+    assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A'])
+    assert result_2 == (default_scenario.machines['pc2'], default_scenario.links['A'])
 
 
 def test_connect_machine_to_two_links(default_scenario: Lab):
-    default_scenario.connect_machine_to_link("pc1", "A")
-    default_scenario.connect_machine_to_link("pc1", "B")
+    result_1 = default_scenario.connect_machine_to_link("pc1", "A")
+    result_2 = default_scenario.connect_machine_to_link("pc1", "B")
     assert len(default_scenario.machines) == 1
     assert default_scenario.machines['pc1']
     assert len(default_scenario.links) == 2
@@ -141,13 +223,16 @@ def test_connect_machine_to_two_links(default_scenario: Lab):
     assert default_scenario.links['B']
     assert default_scenario.machines['pc1'].interfaces[0].name == 'A'
     assert default_scenario.machines['pc1'].interfaces[1].name == 'B'
+    assert result_1 == (default_scenario.machines['pc1'], default_scenario.links['A'])
+    assert result_2 == (default_scenario.machines['pc1'], default_scenario.links['B'])
 
 
 def test_assign_meta_to_machine(default_scenario: Lab):
     default_scenario.get_or_new_machine("pc1")
-    default_scenario.assign_meta_to_machine("pc1", "test_meta", "test_value")
+    result = default_scenario.assign_meta_to_machine("pc1", "test_meta", "test_value")
     assert "test_meta" in default_scenario.machines['pc1'].meta
     assert default_scenario.machines['pc1'].meta["test_meta"] == "test_value"
+    assert result == default_scenario.machines['pc1']
 
 
 def test_assign_meta_to_machine_exception(default_scenario: Lab):
@@ -188,3 +273,30 @@ def test_apply_dependencies(default_scenario: Lab):
     assert default_scenario.machines.popitem()[0] == "pc2"
     assert default_scenario.machines.popitem()[0] == "pc1"
     assert default_scenario.machines.popitem()[0] == "pc3"
+
+
+def test_find_machine_true(default_scenario: Lab):
+    default_scenario.get_or_new_machine("pc1")
+
+    assert default_scenario.find_machine("pc1")
+
+
+def test_find_machine_false(default_scenario: Lab):
+    default_scenario.get_or_new_machine("pc1")
+
+    assert not default_scenario.find_machine("pc2")
+
+
+def test_find_machines_true(default_scenario: Lab):
+    default_scenario.get_or_new_machine("pc1")
+    default_scenario.get_or_new_machine("pc2")
+    default_scenario.get_or_new_machine("pc3")
+
+    assert default_scenario.find_machines({"pc1", "pc2", "pc3"})
+
+
+def test_find_machines_false(default_scenario: Lab):
+    default_scenario.get_or_new_machine("pc1")
+    default_scenario.get_or_new_machine("pc2")
+
+    assert not default_scenario.find_machines({"pc1", "pc2", "pc3"})
diff --git a/tests/model/machine_test.py b/tests/model/machine_test.py
index de985eac..1ae4fe11 100644
--- a/tests/model/machine_test.py
+++ b/tests/model/machine_test.py
@@ -6,7 +6,7 @@
 
 import pytest
 
-from src.Kathara.exceptions import MachineCollisionDomainConflictError
+from src.Kathara.exceptions import MachineCollisionDomainError
 
 sys.path.insert(0, './')
 
@@ -41,6 +41,9 @@ def test_default_device_parameters(default_device: Machine):
     assert not default_device.lab.has_path()
 
 
+#
+# TEST: add_interface
+#
 def test_add_interface(default_device: Machine):
     default_device.add_interface(Link(default_device.lab, "A"))
     assert len(default_device.interfaces) == 1
@@ -49,16 +52,85 @@ def test_add_interface(default_device: Machine):
 
 def test_add_interface_exception(default_device: Machine):
     default_device.add_interface(Link(default_device.lab, "A"))
-    with pytest.raises(Exception):
+    with pytest.raises(MachineCollisionDomainError):
         default_device.add_interface(Link(default_device.lab, "B"), number=0)
 
 
 def test_add_two_interfaces_on_same_cd(default_device: Machine):
-    default_device.add_interface(Link(default_device.lab, "A"))
-    with pytest.raises(MachineCollisionDomainConflictError):
-        default_device.add_interface(Link(default_device.lab, "A"))
+    link = Link(default_device.lab, "A")
+
+    default_device.add_interface(link)
+    with pytest.raises(MachineCollisionDomainError):
+        default_device.add_interface(link)
+
+
+#
+# TEST: remove_interface
+#
+def test_remove_interface(default_device: Machine):
+    link = Link(default_device.lab, "A")
+    default_device.add_interface(link)
+    assert len(default_device.interfaces) == 1
+    assert default_device.interfaces[0].name == "A"
+    assert default_device.name in link.machines
+    default_device.remove_interface(link)
+    assert len(default_device.interfaces) == 1
+    assert len(link.machines.keys()) == 0
+    assert default_device.interfaces[0] is None
+
+
+def test_remove_interface_one(default_device: Machine):
+    link_a = Link(default_device.lab, "A")
+    link_b = Link(default_device.lab, "B")
+    link_c = Link(default_device.lab, "C")
+    default_device.add_interface(link_a)
+    default_device.add_interface(link_b)
+    default_device.add_interface(link_c)
+    assert len(default_device.interfaces) == 3
+    assert default_device.interfaces[0].name == "A"
+    assert default_device.interfaces[1].name == "B"
+    assert default_device.interfaces[2].name == "C"
+    default_device.remove_interface(link_a)
+    assert len(default_device.interfaces) == 3
+    assert default_device.interfaces[0] is None
+    assert default_device.interfaces[1].name == "B"
+    assert default_device.interfaces[2].name == "C"
+    assert default_device.name not in link_a.machines
+
+
+def test_remove_interface_exception(default_device: Machine):
+    link = Link(default_device.lab, "A")
+    with pytest.raises(MachineCollisionDomainError):
+        default_device.remove_interface(link)
+
+
+def test_add_remove_add_interface(default_device: Machine):
+    link = Link(default_device.lab, "A")
+    default_device.add_interface(link)
+    default_device.remove_interface(link)
+    default_device.add_interface(link)
+    assert default_device.interfaces[1] == link
+
+
+def test_add_remove_three_interfaces(default_device: Machine):
+    link_a = Link(default_device.lab, "A")
+    link_b = Link(default_device.lab, "B")
+    link_c = Link(default_device.lab, "C")
+    link_d = Link(default_device.lab, "D")
+
+    default_device.add_interface(link_a)
+    default_device.add_interface(link_b)
+    default_device.add_interface(link_c)
+
+    default_device.remove_interface(link_c)
+
+    default_device.add_interface(link_d)
+    assert default_device.interfaces[3] == link_d
 
 
+#
+# TEST: add_meta
+#
 def test_add_meta_sysctl(default_device: Machine):
     default_device.add_meta("sysctl", "net.ipv4.tcp_syncookies=1")
     assert default_device.meta['sysctls']['net.ipv4.tcp_syncookies'] == 1
@@ -119,6 +191,9 @@ def test_add_meta_port_format_exception2(default_device: Machine):
         default_device.add_meta("port", ":2000")
 
 
+#
+# TEST: check
+#
 def test_check(default_device: Machine):
     default_device.add_interface(Link(default_device.lab, "A"), number=0)
     default_device.add_interface(Link(default_device.lab, "B"), number=1)
@@ -132,6 +207,9 @@ def test_check_exception(default_device: Machine):
         default_device.check()
 
 
+#
+# TEST: get_image
+#
 @mock.patch("src.Kathara.setting.Setting.Setting.get_instance")
 def test_get_image_default(mock_setting_get_instance, default_device: Machine):
     setting_mock = Mock()
@@ -149,6 +227,9 @@ def test_get_image():
     assert device.get_image() == "kathara/frr"
 
 
+#
+# TEST: get_mem
+#
 def test_get_mem_default(default_device: Machine):
     assert default_device.get_mem() is None
 
@@ -166,6 +247,9 @@ def test_get_mem_from_device_meta():
     assert device.get_mem() == "200m"
 
 
+#
+# TEST: get_cpu
+#
 def test_get_cpu_default(default_device: Machine):
     assert default_device.get_cpu() is None
 
@@ -183,6 +267,9 @@ def test_get_cpu_from_device_meta():
     assert device.get_cpu() == 1
 
 
+#
+# TEST: num_terms
+#
 def test_get_num_terms_default(default_device: Machine):
     assert default_device.get_num_terms() == 1
 
@@ -211,6 +298,9 @@ def test_get_num_terms_mix():
     assert device2.get_num_terms() == 2
 
 
+#
+# TEST: pack_data
+#
 @mock.patch("src.Kathara.utils.pack_file_for_tar")
 def test_pack_data_hidden_files(pack_file_for_tar_mock):
     lab_path = os.path.join("tests", "model", "hiddenfiles")
diff --git a/tests/parser/dep_parser_test.py b/tests/parser/dep_parser_test.py
index 5db5439a..48b23b89 100644
--- a/tests/parser/dep_parser_test.py
+++ b/tests/parser/dep_parser_test.py
@@ -4,6 +4,7 @@
 sys.path.insert(0, './')
 
 from src.Kathara.parser.netkit.DepParser import DepParser
+from src.Kathara.exceptions import MachineDependencyError
 
 
 def test_three_devices_dependencies():
@@ -12,7 +13,7 @@ def test_three_devices_dependencies():
 
 
 def test_devices_loop():
-    with pytest.raises(Exception):
+    with pytest.raises(MachineDependencyError):
         DepParser.parse("tests/parser/labdep/devices_loop")
 
 
@@ -22,5 +23,5 @@ def test_with_comment():
 
 
 def test_syntax_error():
-    with pytest.raises(Exception):
+    with pytest.raises(SyntaxError):
         DepParser.parse("tests/parser/labdep/syntax_error")
diff --git a/tests/parser/ext_parser_test.py b/tests/parser/ext_parser_test.py
index d8a8d99a..59b75f39 100644
--- a/tests/parser/ext_parser_test.py
+++ b/tests/parser/ext_parser_test.py
@@ -21,5 +21,10 @@ def test_external_link():
 
 
 def test_malformed_file():
-    with pytest.raises(Exception):
-        ExtParser.parse("tests/parser/labext/fail")
+    with pytest.raises(SyntaxError):
+        ExtParser.parse("tests/parser/labext/syntax_error")
+
+
+def test_io_error():
+    with pytest.raises(ValueError):
+        ExtParser.parse("tests/parser/labext/value_error")
diff --git a/tests/parser/lab_parser_test.py b/tests/parser/lab_parser_test.py
index efa00f54..9cc87cee 100644
--- a/tests/parser/lab_parser_test.py
+++ b/tests/parser/lab_parser_test.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from src.Kathara.exceptions import MachineCollisionDomainConflictError
+from src.Kathara.exceptions import MachineCollisionDomainError
 from src.Kathara.parser.netkit.LabParser import LabParser
 
 sys.path.insert(0, './')
@@ -46,7 +46,7 @@ def test_one_device_shared_name_error():
 
 
 def test_one_device_same_collision_domain_error():
-    with pytest.raises(MachineCollisionDomainConflictError):
+    with pytest.raises(MachineCollisionDomainError):
         LabParser.parse("tests/parser/labconf/one_device_same_collision_domain_error")
 
 
diff --git a/tests/parser/labext/fail/lab.conf b/tests/parser/labext/syntax_error/lab.conf
similarity index 100%
rename from tests/parser/labext/fail/lab.conf
rename to tests/parser/labext/syntax_error/lab.conf
diff --git a/tests/parser/labext/fail/lab.ext b/tests/parser/labext/syntax_error/lab.ext
similarity index 100%
rename from tests/parser/labext/fail/lab.ext
rename to tests/parser/labext/syntax_error/lab.ext
diff --git a/tests/parser/labext/value_error/lab.conf b/tests/parser/labext/value_error/lab.conf
new file mode 100644
index 00000000..f49e8efe
--- /dev/null
+++ b/tests/parser/labext/value_error/lab.conf
@@ -0,0 +1,9 @@
+r1[0]="A"
+r1[1]="B"
+
+r2[0]="C"
+r2[1]="B"
+
+pc1[0]="A"
+pc2[0]="C"
+
diff --git a/tests/parser/labext/value_error/lab.ext b/tests/parser/labext/value_error/lab.ext
new file mode 100644
index 00000000..fa8e34cf
--- /dev/null
+++ b/tests/parser/labext/value_error/lab.ext
@@ -0,0 +1,1 @@
+A enp0s25.100984
diff --git a/tests/parser/option_parser_test.py b/tests/parser/option_parser_test.py
index ce97be24..5c579328 100644
--- a/tests/parser/option_parser_test.py
+++ b/tests/parser/option_parser_test.py
@@ -20,9 +20,9 @@ def test_two_option():
 
 def test_option_error():
     with pytest.raises(ValueError):
-        parsed_options = OptionParser.parse(["option:fail"])
+        OptionParser.parse(["option:syntax_error"])
 
 
 def test_option_error2():
     with pytest.raises(ValueError):
-        parsed_options = OptionParser.parse(["option=fail=2"])
+        OptionParser.parse(["option=syntax_error=2"])