Repository: Wenzel/libmicrovmi (language: python)

Issue:
libmicrovmi python bindings
Create Python bindings to call libmicrovmi from Python

TODO (updated):

- [x] decide how to integrate this new library in our build system
- [x] expose `DriverType` and `DriverInitParam`
- [x] expose `read_physical`
- [ ] package and upload to PyPI
- [x] update `setup.py` to accept `--features` parameter, and set features in `RustExtension`
- [x] create new Dockerfile based on `quay.io/pypa/manylinux2014_x86_64` and install all the drivers required development headers
- [x] update `build-wheels.sh` to accept `--features` and pass them to `setup.py`
- [x] read all relevant fields of `python/Cargo.toml` to fill `setup.py` metadata



We should use the [`rust-cpython` project](https://github.com/dgrunwald/rust-cpython#example-library-with-python-bindings)

rust-cpython will help us to generate a Python extension module, wrapping the libmicrovmi Rust API.


# how to integrate this new library

1. create a separate repository
2. use cargo workspace to manage both Rust library and Python extension in the same git repository, in tandem.
3. maybe generate both with the same cargo.toml (I doubt this is possible)

@rageagainsthepc if you have an opinion


Discussion / hints:
After looking at rust-cpython, a couple of things:

- there is no need to create a workspace, we can produce the same `libmicrovmi.so` that will be considered both the official C API, and the native Python extension
- [PyO3](https://github.com/PyO3/PyO3) is a fork of `rust-cpython`, at the time when it was maintained, and it proposes a better abstraction in my opinion. the only drawback was that it required nightly rust, but 6 months ago, it [became available](https://github.com/PyO3/pyo3/issues/5#issuecomment-647094806) with stable rust
> the only drawback was that it required nightly rust, but 6 months ago, it [became available](https://github.com/PyO3/pyo3/issues/5#issuecomment-647094806) with stable rust

that's awesome, my info was not up to date then, because I thought it was still nightly only. 👍


Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ac63cf8d..d5b6ba8d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -222,6 +222,51 @@ jobs:
           token: ${{ secrets.GITHUB_TOKEN }}
           args: --examples -- -D warnings
 
+  python:
+    runs-on: ubuntu-20.04
+    steps:
+      - name: install stable toolchain with clippy
+        uses: actions-rs/toolchain@v1
+        with:
+          profile: minimal
+          toolchain: stable
+          components: clippy
+
+      - name: Set up Python 3.7 🐍
+        uses: actions/setup-python@v1
+        with:
+          python-version: '3.7'
+
+      - uses: actions/checkout@v1
+
+      - name: install nox and check formatting, linting, type checking and testing
+        run: |
+          pip install nox
+          nox -r
+        working-directory: python
+
+      - name: build Python extension
+        uses: actions-rs/cargo@v1
+        with:
+          command: build
+          args: -p pymicrovmi
+
+      - name: annotate commit with clippy warnings
+        uses: actions-rs/clippy-check@v1
+        with:
+          name: clippy python
+          token: ${{ secrets.GITHUB_TOKEN }}
+          args: -p pymicrovmi -- -D warnings
+
+      - name: install Python package
+        run: |
+          python -m pip install -r requirements.txt
+          ./setup.py install
+        working-directory: python
+
+      - name: smoke test
+        run: python3 -c 'from microvmi import Microvmi, DriverType, DriverInitParam'
+
   debian_package:
     # create a Debian package with cargo deb
     needs: [xen, kvm, virtualbox, hyper-v]
@@ -290,7 +335,7 @@ jobs:
     # only when
     # - push on master
     # - tag starts with 'v*'
-    needs: [format, xen, kvm, virtualbox, hyper-v, c_api, examples]
+    needs: [format, xen, kvm, virtualbox, hyper-v, c_api, examples, python]
     runs-on: ubuntu-20.04
     # output these value to be used by other jobs so they can add assets
     outputs:
@@ -413,3 +458,25 @@ jobs:
         shell: bash
         run: |
           cargo publish --features xen --token ${{ secrets.CRATES_TOKEN }}
+
+
+  publish_pypi:
+    needs: release
+    runs-on: ubuntu-20.04
+
+    steps:
+      - uses: actions/checkout@v1
+
+      - name: install nox
+        run: pip install nox
+
+      - name: Build Wheels with manylinux
+        run: nox -r -s generate_wheels -- --features xen,kvm,virtualbox --release
+        working-directory: python
+
+      - name: Publish on PyPI 🚀
+        uses: pypa/gh-action-pypi-publish@v1.3.1
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_ACCESS_TOKEN }}
+          packages_dir: python/dist/manylinux
diff --git a/.gitignore b/.gitignore
index 45867809..d827baba 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
 # IntelliJ IDEA
-/.idea/
-/*.iml
+.idea/
+*.iml
 
 # Generated by Cargo
 # will have compiled files and executables
@@ -20,3 +20,6 @@ Cargo.lock
 
 /target
 #**/*.rs.bk
+#
+# venvs
+venv
diff --git a/Cargo.toml b/Cargo.toml
index 0c00f65d..ce5fe902 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -45,7 +45,6 @@ widestring = { version = "0.4", optional = true }
 ntapi = { version = "0.3", optional = true }
 vid-sys = { version = "=0.3.0", features = ["deprecated-apis"], optional = true }
 
-
 [dev-dependencies]
 ctrlc = "3.1"
 clap = "2.33"
@@ -71,3 +70,8 @@ assets = [
     ["target/release/libmicrovmi.so", "usr/lib/libmicrovmi.so", "644"],
     ["target/release/capi/libmicrovmi.h", "usr/include/libmicrovmi.h", "644"],
 ]
+
+[workspace]
+members = [
+    "python"
+]
diff --git a/c_examples/mem-dump.c b/c_examples/mem-dump.c
index dc01bd1d..af08f6ed 100644
--- a/c_examples/mem-dump.c
+++ b/c_examples/mem-dump.c
@@ -24,7 +24,7 @@ void dump_memory(void* driver, const char* vm_name) {
     uint8_t buffer[PAGE_SIZE];
     for (int i = 0; i <= max_address / PAGE_SIZE; i++) {
         memset(buffer, 0, PAGE_SIZE);
-        if (microvmi_read_physical(driver, i * PAGE_SIZE, buffer, PAGE_SIZE)) {
+        if (microvmi_read_physical(driver, i * PAGE_SIZE, buffer, PAGE_SIZE, NULL)) {
             fwrite(buffer, sizeof(uint8_t), PAGE_SIZE, dump_file);
         }
     }
diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md
index 6d882bf1..3e6c3730 100644
--- a/doc/src/SUMMARY.md
+++ b/doc/src/SUMMARY.md
@@ -1,18 +1,30 @@
 - [Introduction](./README.md)
 
+
 # Tutorial
 
-- [Introduction](./tutorial/intro.md)
-- [Memory dump example in Rust](./tutorial/rust.md)
-- [Memory dump example in C](./tutorial/c.md)
+- [Installation](./tutorial/installation.md)
+  - [Packaged](./tutorial/packaged.md)
+  - [Source](./tutorial/source.md)
+- [Memory dump example on Xen](./tutorial/mem-dump-example.md)
+  - [Rust](./tutorial/rust.md)
+  - [C](./tutorial/c.md)
+  - [Python](./tutorial/python.md)
+- [Integration](./tutorial/integration.md)
+  - [volatility3](./tutorial/integration/volatility3.md)
 
 # Reference
 
 - [API](./reference/api.md)
-- [Build Options](./reference/build_options.md)
+- [Python API](./reference/python_api.md)
+- [Drivers](./reference/drivers.md)
 - [Integration Status](./reference/integration_status.md)
 
 # Explanation
 
 - [VMI API](./explanation/vmi_api.md)
 - [VMI Fragmentation](./explanation/vmi_ecosystem.md)
+
+# Developer
+
+- [Python](./developer/python.md)
diff --git a/doc/src/developer/python.md b/doc/src/developer/python.md
new file mode 100644
index 00000000..5a8ea40c
--- /dev/null
+++ b/doc/src/developer/python.md
@@ -0,0 +1,42 @@
+# Python Bindings
+
+## Nox
+
+The project uses [Nox](https://nox.thea.codes/en/stable/) to facilite and automate the developer workflow.
+Please install this tool before you start
+
+Running `nox` without any argument will run the default sessions.
+
+## Generating the Wheels
+
+Distributing a Python native extension compatible with many systems and a large set of Python interpreters is a challenging task.
+
+The [manylinux](https://github.com/pypa/manylinux) project comes to the rescue here.
+
+The extension is built based on the [`manylinux2014`](https://www.python.org/dev/peps/pep-0599/) platform tag.
+
+Generation of the wheels is managed by `nox` and requires [`Docker`](https://www.docker.com/) to build a custom manylinux2014 CentOS image, and
+execute the script inside it.
+
+To start the generation:
+
+~~~
+$ cd libmicrovmi/python
+$ nox -r -s generate_wheels -- --features xen
+~~~
+
+you can activate more drivers
+
+~~~
+$ nox -r -s generate_wheels -- --features xen,kvm,virtualbox
+~~~
+
+and enable the release mode as well
+
+~~~
+nox -r -s generate_wheels -- --features xen --release
+~~~
+
+After the execution, the wheels will be available in `libmicrovmi/python/dist/manylinux`.
+
+
diff --git a/doc/src/reference/build_options.md b/doc/src/reference/build_options.md
deleted file mode 100644
index 138e4924..00000000
--- a/doc/src/reference/build_options.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# Build Options
-
-| Option     | Description                 |
-|------------|-----------------------------|
-| xen        | Build the Xen driver        |
-| kvm        | Build the KVM driver        |
-| virtualbox | Build the VirtualBox driver |
-| hyper-v    | Build the Hyper-V driver    |
-
-## Example
-
-~~~
-$ cargo build --features xen,kvm
-~~~
\ No newline at end of file
diff --git a/doc/src/reference/drivers.md b/doc/src/reference/drivers.md
new file mode 100644
index 00000000..c78b0436
--- /dev/null
+++ b/doc/src/reference/drivers.md
@@ -0,0 +1,51 @@
+# Drivers
+
+This section documents the drivers available and the requirements to compile them.
+
+## Features
+
+| Feature       | Description                 |
+|--------------|-----------------------------|
+| `xen`        | Build the Xen driver        |
+| `kvm`        | Build the KVM driver        |
+| `virtualbox` | Build the VirtualBox driver |
+| `hyper-v`    | Build the Hyper-V driver    |
+
+Example
+~~~
+$ cargo build --features xen,kvm
+~~~
+
+## Xen
+
+~~~
+$ sudo apt install clang libxen-dev
+~~~
+
+Compatibility: Xen >= 4.11.0
+
+## KVM
+
+The KVM driver depends on [libkvmi](https://github.com/bitdefender/libkvmi)
+
+~~~
+$ git clone https://github.com/bitdefender/libkvmi.git
+$ cd libkvmi
+$ git checkout bf5776319e1801b59125c994c459446f0ed6837e
+$ ./bootstrap
+$ ./configure
+$ make
+$ sudo make install
+~~~
+
+## VirtualBox
+
+The VirtualBox driver depends on [libFDP](https://github.com/thalium/icebox/tree/master/src/FDP)
+
+~~~
+$ git clone --depth 1 https://github.com/thalium/icebox
+$ cd icebox/src/FDP
+$ g++ -std=c++11 -shared -fPIC FDP.cpp -o libFDP.so
+$ sudo mv include/* /usr/local/include/
+$ sudo mv libFDP.so /usr/local/lib/
+~~~
\ No newline at end of file
diff --git a/doc/src/reference/integration_status.md b/doc/src/reference/integration_status.md
index 9756c242..f0ecae09 100644
--- a/doc/src/reference/integration_status.md
+++ b/doc/src/reference/integration_status.md
@@ -3,11 +3,17 @@
 This section gives a status overview of libmicrovmi's integration into
 other applications and libraries based on _virtual machine introspection_.
 
+|   Project   | Supported |
+|:-----------:|:---------:|
+|    LibVMI   |     🟠    |
+| volatility3 |     ✅     |
+
 ## LibVMI
 
 - [project](https://github.com/libvmi/libvmi)
 - [issue](https://github.com/Wenzel/libmicrovmi/issues/137)
 - [fork](https://github.com/Wenzel/libvmi/tree/libmicrovmi) (Note: use the `libmicrovmi` branch)
+- compatibility: 🟠
 
 ### API Compatibility Status
 
@@ -72,3 +78,14 @@ The API is used in the following files:
 | `driver_set_access_required()`            |            |              |
 | `get_data()`                              |     ✅     |              |
 | `release_data()`                          |     ✅     |              |
+
+## volatility3
+
+volatility3 could inspect and run its forensics plugins on live memory by
+adding a new URL handler.
+
+- [project](https://github.com/volatilityfoundation/volatility3)
+- [issue](https://github.com/Wenzel/libmicrovmi/issues/161)
+- [fork](https://github.com/Wenzel/volatility3/tree/vm_introspection_handler) (Note: use the `vm_introspection_handler` branch)
+- compatibility: ✅
+
diff --git a/doc/src/reference/python_api.md b/doc/src/reference/python_api.md
new file mode 100644
index 00000000..9294ad85
--- /dev/null
+++ b/doc/src/reference/python_api.md
@@ -0,0 +1,26 @@
+# Python API
+
+### Initializing libmicrovmi
+
+~~~Python
+from microvmi import Microvmi
+
+micro = Microvmi("Windows10")
+~~~
+
+### Specifying the hypervisor
+
+~~~Python
+from microvmi import Microvmi, DriverType
+
+micro = Microvmi("Windows10", DriverType.XEN)
+~~~
+
+### Adding driver initialization parameters
+
+~~~Python
+from microvmi import Microvmi, DriverType, DriverInitParam
+
+init_param = DriverInitParam.kvmi_unix_socket("/tmp/introspector")
+micro = Microvmi("Windows10", DriverType.KVM, init_param)
+~~~
diff --git a/doc/src/tutorial/installation.md b/doc/src/tutorial/installation.md
new file mode 100644
index 00000000..7a35f7fa
--- /dev/null
+++ b/doc/src/tutorial/installation.md
@@ -0,0 +1,5 @@
+# Installation
+
+This section will teach you how to:
+
+- install libmicrovmi either from official repositories or source code
\ No newline at end of file
diff --git a/doc/src/tutorial/integration.md b/doc/src/tutorial/integration.md
new file mode 100644
index 00000000..7435808c
--- /dev/null
+++ b/doc/src/tutorial/integration.md
@@ -0,0 +1,7 @@
+# Integration
+
+libmicrovmi aims to be combined with a great diversity of VMI projects.
+
+In this section you will find detailed tutorials to integrate and exploit your favorite tools on top of libmicrovmi.
+
+Please refer to the [Integration Status](https://wenzel.github.io/libmicrovmi/reference/integration_status.html) page for an overview of libmicrovmi's compatibility with existing integrations.
\ No newline at end of file
diff --git a/doc/src/tutorial/integration/volatility3-demo.png b/doc/src/tutorial/integration/volatility3-demo.png
new file mode 100644
index 00000000..66896149
Binary files /dev/null and b/doc/src/tutorial/integration/volatility3-demo.png differ
diff --git a/doc/src/tutorial/integration/volatility3.md b/doc/src/tutorial/integration/volatility3.md
new file mode 100644
index 00000000..a3931685
--- /dev/null
+++ b/doc/src/tutorial/integration/volatility3.md
@@ -0,0 +1,73 @@
+# volatility3
+
+[volatility3](https://github.com/volatilityfoundation/volatility3) is a framework for extracting digital artifacts and performing forensic investigation
+on RAM samples.
+
+Combined with libmicrovmi, you can run volatility3 on top of a live virtual machine's physical memory.
+
+Thanks to volatility3's modular architecture the libmicrovmi integration doesn't require any upstream modification.
+Instead we need to indicate to volatility3 how to locate our plugin.
+
+This guide assumes you already have a working installation of libmicrovmi Python in a virtualenv.
+Please refer to the [documentation](https://wenzel.github.io/libmicrovmi/tutorial/installation.html).
+
+# Setup
+
+We need the development version of volatility3, from git:
+
+~~~
+(venv) $ git clone https://github.com/volatilityfoundation/volatility3
+(venv) $ cd volatility3
+(venv) $ pip install .
+~~~
+
+The `microvmi` python package comes with a [`volatility`](https://github.com/Wenzel/libmicrovmi/tree/master/python/microvmi/volatility) directory which contains the connection plugin.
+
+We need to add this directory to volatility's search path.
+
+To locate the volatility directory in your `venv`:
+
+~~~
+(venv) $ find venv/ -type d -wholename '*microvmi/volatility*'
+venv/lib/python3.8/site-packages/microvmi-0.0.2-py3.8-linux-x86_64.egg/microvmi/volatility
+~~~
+
+# Usage
+
+To run volatility3 with libmicrovmi, specify the additional plugin path with flag `-p`.
+
+## VMI scheme URL
+
+The libmicrovmi handler for volatility is a URL handler with the following syntax:
+
+    vmi://[hypervisor]/vm_name
+
+The hypervisor part is optional. If not specified, it will default to try every builtin driver available.
+
+Additional driver parameters can be specified.
+
+To pass the KVMi socket:
+
+    vmi:///vm_name?kvmi_unix_socket=/tmp/introspector
+
+## Running volatility3
+
+Let's put all of this together and run volatility3 combined with libmicrovmi.
+
+- `-p <plugin_dir>`
+- `--single-location vmi://` url
+
+~~~
+(venv) $ vol -p <plugin_dir> --single-location vmi:///vm_name <volatility plugin>
+~~~
+
+Example listing the processes on Xen:
+
+~~~bash
+(venv) $ sudo -E ./venv/bin/vol \  # running volatility3 as root (required by the Xen driver)
+    -p ./venv/lib/python3.8/site-packages/microvmi-0.0.2-py3.8-linux-x86_64.egg/microvmi/volatility \  # path to the microvmi connection plugin
+    --single-location vmi:///winxp \  # specify the resource location
+    windows.pslist.PsList  # volatility's pslist plugin
+~~~
+
+![demo](./volatility3-demo.png)
diff --git a/doc/src/tutorial/mem-dump-example.md b/doc/src/tutorial/mem-dump-example.md
new file mode 100644
index 00000000..f9a1e090
--- /dev/null
+++ b/doc/src/tutorial/mem-dump-example.md
@@ -0,0 +1,23 @@
+# Memory dump example on Xen
+
+This tutorial will walk you through the steps required to compile libmicrovmi and run
+the `mem-dump` Rust example code on a Xen domain in order to dump its physical memory.
+
+This tutorial assumes the following:
+- you have a working cargo stable toolchain
+- you have a running VM supervised by **Xen 4.11.0** or above.
+- you are running on **Ubuntu 20.04**
+
+## Requirements
+
+- `clang` (bindgen)
+- Xen development headers
+
+To install the additional dependencies:
+
+~~~
+$ sudo apt install clang libxen-dev
+~~~
+
+⚠️ Note: Accessing Xen's introspection APIs will require high privileges as we are talking to `Dom0`,
+hence we have to run `cargo` as `root` when actually running and testing example code.
\ No newline at end of file
diff --git a/doc/src/tutorial/packaged.md b/doc/src/tutorial/packaged.md
new file mode 100644
index 00000000..48df69ac
--- /dev/null
+++ b/doc/src/tutorial/packaged.md
@@ -0,0 +1,40 @@
+# Packaged installation
+
+This section details how to install the officially distributed version of libmicrovmi.
+
+libmicrovmi is accessible through the following languages
+- Rust
+- C
+- Python
+
+It comes bundled by default with the following Linux drivers:
+- Xen
+- KVM
+- VirtualBox
+
+# Rust - Crates.io
+
+The crates is available on crates.io:
+
+[![Crates.io](https://img.shields.io/crates/v/microvmi?logo=rust&style=for-the-badge)](https://crates.io/crates/microvmi)
+
+# C - Debian package
+
+A Debian package is available on [Github releases](https://github.com/Wenzel/libmicrovmi/releases):
+
+![GitHub release (latest by date)](https://img.shields.io/github/v/release/Wenzel/libmicrovmi?color=yellowgreen&logo=github&style=for-the-badge)
+
+It contains both the library and the headers to get started with C development
+
+- `/usr/lib/libmicrovmi.so`
+- `/usr/include/libmicrovmi.h`
+
+# Python - PyPI
+
+libmicrovmi is exposing a Python interface through a native extension.
+
+The package `microvmi` is available on [PyPI](https://pypi.org/project/microvmi/):
+
+[![PyPI](https://img.shields.io/pypi/v/microvmi?color=blue&logo=pypi&logoColor=white&style=for-the-badge)](https://pypi.org/project/microvmi/)
+
+Note: this extension is completely independant from any existing `libmicrovmi.so` installation on you system. Hence you don't have to install the debian package above.
diff --git a/doc/src/tutorial/python.md b/doc/src/tutorial/python.md
new file mode 100644
index 00000000..d55eb5ab
--- /dev/null
+++ b/doc/src/tutorial/python.md
@@ -0,0 +1,36 @@
+# Memory-dump example in Rust
+
+In this tutorial chapter, we will run the `mem-dump.py` Python example to dump
+the physical memory of a domain, thanks to libmicrovmi Python bindings.
+
+First, let's create a virtualenv and install the bindings with the Xen driver enabled:
+
+~~~
+$ cd libmicrovmi/python
+$ python3 -m venv venv
+$ source venv/bin/activate
+(venv) $ python -m pip install --upgrade setuptools
+(venv) $ python -m pip install -r requirements.txt
+(venv) $ ./setup.py install --features xen
+~~~
+
+## Running the example
+
+The `examples/mem-dump.py` script is based on the [rich](https://github.com/willmcgugan/rich) library for an enhanced output.
+As they are only needed for example code, the requirements are kept in a separate file.
+
+Let's install them:
+
+~~~
+cd libmicrovmi/python
+(venv) $ python -m pip install -r examples/requirements.txt
+~~~
+
+Now you can run the example on your domain `<vm_name>`:
+~~~
+$ sudo -E ./venv/bin/python examples/mem-dump.py <vm_name>
+~~~
+
+Expected output:
+
+![mem-dump output](./python_mem_dump.png)
diff --git a/doc/src/tutorial/python_mem_dump.png b/doc/src/tutorial/python_mem_dump.png
new file mode 100644
index 00000000..25c54a34
Binary files /dev/null and b/doc/src/tutorial/python_mem_dump.png differ
diff --git a/doc/src/tutorial/source.md b/doc/src/tutorial/source.md
new file mode 100644
index 00000000..8d945758
--- /dev/null
+++ b/doc/src/tutorial/source.md
@@ -0,0 +1,64 @@
+# Compilation from Source
+
+This sections details how to compile libmicrovmi from source.
+
+You might want to dive into it if you are interested by:
+
+- compiling a specific driver that is not available by default in the officially distributed version
+- developing libmicrovmi
+  - add a new driver
+  - extend the APIs
+
+We assume that you have a working Cargo stable toolchain.
+
+⚠️ Note: each driver depends on development headers to be install in order
+to generate the bindings from C to Rust.
+
+For any driver, `clang` is a required dependency:
+~~~
+$ sudo apt-get install clang
+~~~
+
+Please look at the [driver reference](https://wenzel.github.io/libmicrovmi/reference/drivers.html) section of the documentation.
+
+## Cloning libmicrovmi
+
+Clone the [repo](https://github.com/Wenzel/libmicrovmi):
+
+~~~
+$ git clone https://github.com/Wenzel/libmicrovmi
+~~~
+
+## Rust
+
+Compilation for a Rust crate is as simple as using cargo:
+
+~~~
+$ cargo build --features xen,kvm
+~~~
+
+## C
+
+The C library is generated by default when building the microvmi crate.
+
+The output files will be located at:
+`target/<debug|release>/libmicrovmi.so` : the C library
+`target/<debug|release>/capi/libmicrovmi.h` : the development header
+
+## Python
+
+To build the native Python extension, create a virtualenv and run `./setup.py`:
+
+~~~
+$ cd libmicrovmi/python
+$ python3 -m venv venv
+$ source venv/bin/activate
+(venv) $ python -m pip install --upgrade setuptools
+(venv) $ python -m pip install -r requirements.txt
+(venv) $ ./setup.py install --features xen,kvm
+~~~
+
+Note:
+- `./setup.py install` : will create a release build
+- `./setup.py develop` : will create a debug build
+- `pip install .` is **NOT** available due a to an issue in [setuptools-rust/Maturin](https://github.com/PyO3/maturin/issues/330). Do not use `pip`.
\ No newline at end of file
diff --git a/examples/mem-dump.rs b/examples/mem-dump.rs
index 517b3074..71cd62be 100644
--- a/examples/mem-dump.rs
+++ b/examples/mem-dump.rs
@@ -84,7 +84,8 @@ fn main() {
         );
         // reset buffer each loop
         let mut buffer: [u8; PAGE_SIZE] = [0; PAGE_SIZE];
-        drv.read_physical(cur_addr, &mut buffer)
+        let mut _bytes_read = 0;
+        drv.read_physical(cur_addr, &mut buffer, &mut _bytes_read)
             .unwrap_or_else(|_| debug!("failed to read memory at {:#X}", cur_addr));
         dump_file
             .write_all(&buffer)
diff --git a/python/.gitignore b/python/.gitignore
new file mode 100644
index 00000000..a7742a9b
--- /dev/null
+++ b/python/.gitignore
@@ -0,0 +1,12 @@
+*.egg-info
+__pycache__/
+*.so
+build/
+dist/
+
+# nox
+.nox/
+
+# coverage
+.coverage
+.coverage_html/
diff --git a/python/Cargo.toml b/python/Cargo.toml
new file mode 100644
index 00000000..94e5d5f3
--- /dev/null
+++ b/python/Cargo.toml
@@ -0,0 +1,37 @@
+[package]
+name = "pymicrovmi"
+# note "pymicrovmi" is just a conflict free name for Cargo
+# the Python package is named "microvmi" (setup.py)
+version = "0.0.2"
+authors = ["Mathieu Tarral <mathieu.tarral@protonmail.com>"]
+edition = "2018"
+description = "Python bindings for libmicrovmi"
+readme = "README.md"
+homepage = "https://github.com/Wenzel/libmicrovmi"
+repository = "https://github.com/Wenzel/libmicrovmi"
+license = "GPL-3.0-only"
+keywords = ["introspection", "VMI"]
+
+[lib]
+name = "pymicrovmi"
+crate-type = ["cdylib"]
+
+[features]
+# Xen driver
+xen = ["microvmi/xen"]
+# KVM driver
+kvm = ["microvmi/kvm"]
+# VirtualBox driver
+virtualbox = ["microvmi/virtualbox"]
+# Hyper-v driver
+hyper-v = ["microvmi/hyper-v"]
+
+[dependencies]
+log = "0.4"
+env_logger = "0.8"
+thiserror = "1.0"
+microvmi = { path = "../" }
+
+[dependencies.pyo3]
+version = "0.13.1"
+features = ["extension-module"]
diff --git a/python/Dockerfile b/python/Dockerfile
new file mode 100644
index 00000000..73b16e40
--- /dev/null
+++ b/python/Dockerfile
@@ -0,0 +1,43 @@
+FROM quay.io/pypa/manylinux2014_x86_64
+
+# install clang
+RUN yum -y install clang
+
+# Xen
+# Note: the first sed disables a compiler flag that would be treated as an error
+# the second sed disabled the compilation of qemu, which is very long and also
+# had errors, and we don't need it anyway
+RUN git clone -b RELEASE-4.11.0 --depth 1 https://github.com/xen-project/xen \
+        && cd xen \
+        && yum install -y dev86 xz-devel python-devel gettext-devel iasl \
+                ncurses-devel pixman-devel wget yajl-devel zlib-devel \
+                glibc-devel.i686 \
+        && ./configure --disable-xen --disable-docs --disable-stubdom \
+                --enable-tools --disable-rombios \
+        && sed -i '/$(call cc-option-add,CFLAGS,CC,-Wno-unused-local-typedefs)/a $(call cc-option-add,CFLAGS,CC,-Wno-address-of-packed-member)' Config.mk \
+        && sed -i 28,32d tools/Makefile \
+        && make -j4 dist-tools \
+        && make install-tools \
+        && cd .. \
+        && rm -rf xen
+
+# libkvmi v7
+RUN git clone https://github.com/bitdefender/libkvmi.git \
+    && cd libkvmi \
+    && git checkout bf5776319e1801b59125c994c459446f0ed6837e \
+    && ./bootstrap \
+    && ./configure \
+    && make \
+    && make install \
+    && cd .. \
+    && rm -rf libkvmi
+
+# libFDP.so
+RUN git clone --depth 1 https://github.com/thalium/icebox \
+    && cd icebox/src/FDP \
+    && g++ -std=c++11 -shared -fPIC FDP.cpp -o libFDP.so \
+    && mv include/* /usr/local/include/ \
+    && mv libFDP.so /usr/local/lib/ \
+    && cd - \
+    && rm -rf icebox
+
diff --git a/python/README.md b/python/README.md
new file mode 100644
index 00000000..37d3a9e6
--- /dev/null
+++ b/python/README.md
@@ -0,0 +1,5 @@
+# Microvmi Python bindings
+
+> Python bindings for libmicrovmi
+
+[![documentation](https://img.shields.io/badge/Online-Documentation-green?style=for-the-badge&logo=gitbook)](https://wenzel.github.io/libmicrovmi/tutorial/python.html)
diff --git a/python/build-wheels.sh b/python/build-wheels.sh
new file mode 100755
index 00000000..638e4d73
--- /dev/null
+++ b/python/build-wheels.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+
+# This script is used to build multiple wheels, with the manylinux docker container
+
+set -ex
+
+curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain stable -y
+export PATH="$HOME/.cargo/bin:$PATH"
+
+# map libmicrovmi root dir to /io
+cd /io/python
+
+# note: removed 3.5
+for PYBIN in /opt/python/cp{36,37,38,39}*/bin; do
+    "${PYBIN}/pip" install -r requirements.txt
+    "${PYBIN}/python" setup.py bdist_wheel $@
+done
+
+for whl in dist/*.whl; do
+    auditwheel repair "$whl" -w dist/manylinux
+done
diff --git a/python/examples/mem-dump.py b/python/examples/mem-dump.py
new file mode 100755
index 00000000..08848436
--- /dev/null
+++ b/python/examples/mem-dump.py
@@ -0,0 +1,68 @@
+#!/usr/bin/env python3
+
+import argparse
+import logging
+from contextlib import contextmanager
+
+from microvmi import Microvmi
+from rich import print
+from rich.progress import BarColumn, Progress, TextColumn
+
+READ_SIZE = 1024 * 1024
+
+
+@contextmanager
+def pause_ctxt(micro: Microvmi):
+    micro.pause()
+    try:
+        yield micro
+    finally:
+        micro.resume()
+
+
+def parse_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("vm_name", help="The VM name to dump memory from")
+    parser.add_argument("-o", "--output-file", help="Dump file path", default="{vm_name}.dump")
+    args = parser.parse_args()
+    return args
+
+
+def dump_mem(vm_name, output_file):
+    micro = Microvmi(vm_name)
+    destination = output_file.format(vm_name=vm_name)
+    with pause_ctxt(micro):
+        max_addr = micro.max_addr
+        print(f"Dumping physical memory on {vm_name} until 0x{max_addr:X} to {destination}")
+        with open(destination, "wb") as f:
+            with Progress(
+                "[progress.description]{task.description}",
+                TextColumn("[bold yellow]0x{task.completed:X}"),
+                BarColumn(bar_width=None),
+                "[progress.percentage]{task.percentage:>3.0f}%",
+                transient=True,
+            ) as progress:
+                dump_task = progress.add_task("Dumping ", total=max_addr)
+                mem = micro.padded_memory
+                for addr in range(0, max_addr, READ_SIZE):
+                    logging.debug("dumping at 0x%x", addr)
+                    current_chunk_size = min(READ_SIZE, max_addr - addr)
+                    buffer = mem.read(current_chunk_size)
+                    f.write(buffer)
+                    progress.update(dump_task, advance=READ_SIZE)
+
+
+def main():
+    logging.basicConfig(level=logging.INFO)
+    args = parse_args()
+
+    try:
+        vm_name = args.vm_name
+        output_file = args.output_file
+        dump_mem(vm_name, output_file)
+    except KeyboardInterrupt:
+        logging.critical("Cancelled by CTRL-C")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/python/examples/requirements.txt b/python/examples/requirements.txt
new file mode 100644
index 00000000..e3cb5cf6
--- /dev/null
+++ b/python/examples/requirements.txt
@@ -0,0 +1,2 @@
+rich==9.10.0
+
diff --git a/python/microvmi/__init__.py b/python/microvmi/__init__.py
new file mode 100644
index 00000000..bc8e2553
--- /dev/null
+++ b/python/microvmi/__init__.py
@@ -0,0 +1,3 @@
+from microvmi.microvmi import DriverType, Microvmi
+
+from .pymicrovmi import DriverInitParam
diff --git a/python/microvmi/memory.py b/python/microvmi/memory.py
new file mode 100644
index 00000000..3002d1ea
--- /dev/null
+++ b/python/microvmi/memory.py
@@ -0,0 +1,106 @@
+import logging
+from array import ArrayType
+from io import RawIOBase
+from mmap import mmap
+from os import SEEK_CUR, SEEK_END, SEEK_SET
+from typing import Optional, Union
+
+from .pymicrovmi import MicrovmiExt
+
+PAGE_SIZE = 4096
+
+
+class AbstractPhysicalMemoryIO(RawIOBase):
+    """This class provide a basic implementation common
+    to PhysicalMemoryIO and PhysicalMemoryPaddedIO
+    """
+
+    def __init__(self, m: MicrovmiExt):
+        self._log = logging.getLogger(f"{self.__module__}.{self.__class__.__name__}")
+        self._m: MicrovmiExt = m
+        self._max_addr: int = self._m.get_max_physical_addr()
+        # current seek position in memory (physical address)
+        self._cur_pos: int = 0
+
+    def seek(self, offset: int, whence: int = SEEK_SET) -> int:
+        self._log.debug("seek: offset: %s, whence: %s, pos: %s", offset, whence, hex(self._cur_pos))
+        if whence == SEEK_SET:
+            # seek from start of physical memory
+            assert offset >= 0 or offset <= self._max_addr
+            self._cur_pos = offset
+        elif whence == SEEK_END:
+            # seek to the end of physical memory
+            # offset must be zero
+            assert offset == 0
+            self._cur_pos = self._max_addr
+        elif whence == SEEK_CUR:
+            # seek to the current position
+            self._cur_pos += offset
+            if self._cur_pos < 0:
+                self._cur_pos = 0
+            elif self._cur_pos > self._max_addr:
+                self._cur_pos = self._max_addr
+
+        else:
+            raise RuntimeError(f"seek: unexpected whence value {whence}")
+        return self._cur_pos
+
+    def seekable(self) -> bool:
+        return True
+
+    def tell(self) -> int:
+        return self._cur_pos
+
+    def writable(self) -> bool:
+        return False
+
+
+class PhysicalMemoryIO(AbstractPhysicalMemoryIO):
+    """This class provides a Python IO object to work
+    with a VM's physical memory as a binary stream
+
+    The stream is unbuffered, as we are dealing with live memory.
+    Also, it allows randoms access (seeks) in memory"""
+
+    def read(self, size: int = ...) -> Optional[bytes]:  # type: ignore
+        self._log.debug("read: size: %s", size)
+        if size < 0:
+            # -1: read all bytes until EOF
+            raise NotImplementedError
+        data = bytearray(size)
+        bytes_read = self._m.read_physical_into(self._cur_pos, data)
+        self._log.debug("read return: len: %s, content: %s", len(data), data[:100])
+        return bytes(data[:bytes_read])
+
+    def readinto(self, buffer: Union[bytearray, memoryview, ArrayType, mmap]) -> Optional[int]:
+        bytes_read = self._m.read_physical_into(self._cur_pos, buffer)
+        return bytes_read
+
+
+class PaddedPhysicalMemoryIO(AbstractPhysicalMemoryIO):
+    """This class provides a Python IO object to work
+    with a VM's physical memory as a binary stream.
+
+    The stream is unbuffered, as we are dealing with live memory.
+    Also, it allows randoms access (seeks) in memory
+
+    The read operations are padded. so read(n) will always
+    return n bytes.
+    It has been designed to be used mainly by Volatility3,
+    which requires padded reads."""
+
+    def read(self, size: int = ...) -> Optional[bytes]:  # type: ignore
+        self._log.debug("read: size: %s", size)
+        if size < 0:
+            # -1: read all bytes until EOF
+            raise NotImplementedError
+        data = bytearray(size)
+        for offset in range(0, size, PAGE_SIZE):
+            read_len = min(PAGE_SIZE, size - offset)
+            pos = self.tell()
+            chunk, _ = self._m.read_physical(pos, read_len)
+            end_offset = offset + read_len
+            data[offset:end_offset] = chunk
+            self.seek(read_len, SEEK_CUR)
+        self._log.debug("read return: len: %s, content: %s", len(data), data[:100])
+        return bytes(data)
diff --git a/python/microvmi/microvmi.py b/python/microvmi/microvmi.py
new file mode 100644
index 00000000..ee670ad7
--- /dev/null
+++ b/python/microvmi/microvmi.py
@@ -0,0 +1,85 @@
+from enum import Enum
+from typing import Optional
+
+from microvmi.memory import PaddedPhysicalMemoryIO, PhysicalMemoryIO
+
+from .pymicrovmi import DriverInitParam, MicrovmiExt
+
+
+class DriverType(Enum):
+    """Types of hypervisors supported by libmicrovmi"""
+
+    HyperV = 0
+    KVM = 1
+    VirtualBox = 2
+    Xen = 3
+
+
+class Microvmi:
+    """This is the main class to interact with libmicrovmi"""
+
+    def __init__(
+        self,
+        domain_name: str,
+        driver_type: DriverType = None,
+        drv_init_param: DriverInitParam = None,
+    ):
+        """
+        Initialize a Microvmi instance
+
+        Args:
+            domain_name (str): the domain name
+            driver_type (int, optional): the hypervisor driver type on which the library should be initialized
+            init_param (DriverInitParam, optional): additional initialization parameters for driver initialization
+        """
+        drv_type_ext: Optional[int] = driver_type.value if driver_type is not None else None
+        self._micro = MicrovmiExt(domain_name, drv_type_ext, drv_init_param)
+        self._memory = PhysicalMemoryIO(self._micro)
+        self._padded_memory = PaddedPhysicalMemoryIO(self._micro)
+
+    @property
+    def memory(self) -> PhysicalMemoryIO:
+        """Return a file object to interact with the VM's physical memory"""
+        return self._memory
+
+    @property
+    def padded_memory(self) -> PaddedPhysicalMemoryIO:
+        """Return a file object to interact with the VM's physical memory"""
+        return self._padded_memory
+
+    @property
+    def max_addr(self) -> int:
+        """Return the maximum physical address"""
+        return self._micro.get_max_physical_addr()
+
+    def read_physical(self, paddr: int, size: int) -> bytes:
+        """Read size bytes of physical memory at paddr
+
+        Args:
+            paddr (int): the physical address to start reading from
+            size (int): the length of the read operation
+
+        Return:
+            bytes: the block of physical memory read
+        """
+        return self._micro.read_physical(paddr, size)
+
+    def read_physical_into(self, paddr: int, buffer: bytearray) -> int:
+        """Read the buffer size of physical memory at paddr
+
+        Args:
+            paddr (int): the physical address to start reading from
+            buffer (bytearray): the buffer to read into
+
+        Return
+            int: the amount of bytes read
+        """
+        return self._micro.read_physical_into(paddr, buffer)
+
+    def pause(self):
+        """Pause the VM"""
+        self._micro.pause()
+
+    def resume(self):
+        """Resume the VM"""
+        self._micro.resume()
diff --git a/python/microvmi/volatility/__init__.py b/python/microvmi/volatility/__init__.py
new file mode 100644
index 00000000..66aa9f57
--- /dev/null
+++ b/python/microvmi/volatility/__init__.py
@@ -0,0 +1,8 @@
+"""
+This package regroups the volatility plugins developed on top of the Microvmi package
+
+It is intended to be used as a volatility3 plugins directory:
+
+Example:
+    vol.py -p /path/to/microvmi/volatility windows.pslist.PsList
+"""
diff --git a/python/microvmi/volatility/vmi_handler.py b/python/microvmi/volatility/vmi_handler.py
new file mode 100644
index 00000000..f0cd7a6b
--- /dev/null
+++ b/python/microvmi/volatility/vmi_handler.py
@@ -0,0 +1,118 @@
+import logging
+from typing import Any, List, Optional, Tuple
+from urllib.parse import parse_qs, urlparse
+from urllib.request import BaseHandler, Request
+
+from microvmi import DriverInitParam, DriverType, Microvmi
+
+# to be used by volatility, the VMIHandler should inherit from VolatilityHandler
+# in order to be non cacheable
+# if we find volatility, use VolatilityHandler, otherwise use the BaseHandler from stdlib
+try:
+    from volatility3.framework.layers.resources import VolatilityHandler
+except ImportError:
+    # define dummy class
+    # mypy checking: ignore name already defined
+    class VolatilityHandler(BaseHandler):  # type: ignore
+        pass
+
+
+micro: Optional[Microvmi] = None
+
+
+class MicrovmiHandlerError(Exception):
+    pass
+
+
+class VMIHandler(VolatilityHandler):
+    """
+    Handles the Virtual Machine Introspection URL scheme based on libmicrovmi
+
+    Documentation: https://wenzel.github.io/libmicrovmi/
+
+    Syntax is defined here:
+        vmi://<hypervisor>/<vm_name>?param1=value1&param2=value2
+    """
+
+    SCHEME = "vmi"
+
+    """Map of driver initialization parameter keys to the associated DriverInitParam functions
+    exposed by the pymicrovmi python extension"""
+    DRIVER_INIT_PARAM_MAP = {"kvmi_unix_socket": DriverInitParam.kvmi_unix_socket}
+
+    @classmethod
+    def non_cached_schemes(cls) -> List[str]:
+        return [VMIHandler.SCHEME]
+
+    @staticmethod
+    def vmi_open(req: Request) -> Optional[Any]:
+        """Handles the request if it's the VMI scheme"""
+        logging.getLogger("microvmi").setLevel(logging.WARNING)
+        vm_name, driver_type, init_param = url_to_driver_parameters(req.full_url)
+        # this method is called multiple times
+        # just return if already initialized instance
+        global micro
+        if micro is not None:
+            return micro.padded_memory
+        # init Microvmi
+        micro = Microvmi(vm_name, driver_type, init_param)
+        return micro.padded_memory
+
+
+def url_to_driver_parameters(url: str) -> Tuple[str, Optional[DriverType], Optional[DriverInitParam]]:
+    """Parses a given request and extracts the Microvmi driver initialization parameters"""
+    parsed_url = urlparse(url)
+    # scheme
+    _validate_scheme(parsed_url.scheme)
+    # domain
+    vm_name: str = _parse_vm_name(parsed_url.path)
+    # hypervisor
+    driver_type: Optional[DriverType] = _parse_hypervisor(parsed_url.netloc)
+    # init params
+    init_param: Optional[DriverInitParam] = _parse_driver_init_params(parsed_url.query)
+    return vm_name, driver_type, init_param
+
+
+def _validate_scheme(scheme: str):
+    if scheme != VMIHandler.SCHEME:
+        raise MicrovmiHandlerError(f"Scheme error: Got {scheme}, expected {VMIHandler.SCHEME}")
+
+
+def _parse_vm_name(path: str) -> str:
+    # remove /
+    vm_name = path[1:]
+    if not vm_name:
+        raise MicrovmiHandlerError("Empty vm_name")
+    return vm_name
+
+
+def _parse_hypervisor(netloc: str) -> Optional[DriverType]:
+    if netloc:
+        try:
+            drv_type = DriverType[netloc]
+        except KeyError:
+            raise MicrovmiHandlerError(
+                f"Invalid driver type. Valid driver types: {' '.join([d.name for d in DriverType])}"
+            )
+        else:
+            return drv_type
+    return None
+
+
+def _parse_driver_init_params(query: str) -> Optional[DriverInitParam]:
+    if not query:
+        return None
+    url_params = parse_qs(query, strict_parsing=True)
+    if not url_params:
+        return None
+    if len(url_params) > 1:
+        raise MicrovmiHandlerError("Only one driver initialization parameter is supported")
+    try:
+        key = list(url_params.keys())[0]
+        init_param_func = VMIHandler.DRIVER_INIT_PARAM_MAP[key]
+    except KeyError as e:
+        raise MicrovmiHandlerError(
+            f"Unknown driver initialization parameter. Allow init parameters: {VMIHandler.DRIVER_INIT_PARAM_MAP.keys()}"
+        ) from e
+    else:
+        return init_param_func(url_params[key][0])
diff --git a/python/noxfile.py b/python/noxfile.py
new file mode 100644
index 00000000..4c3836ff
--- /dev/null
+++ b/python/noxfile.py
@@ -0,0 +1,70 @@
+import subprocess
+from pathlib import Path
+
+import nox
+
+CUR_DIR = Path(__file__).parent
+
+# default sessions for nox
+nox.options.sessions = ["fmt", "lint", "type", "test"]
+
+
+@nox.session
+def fmt(session):
+    session.install("black==20.8b1")
+    # note: black doesn't support setup.cfg
+    # so we hardcode the config here
+    session.run("black", "--line-length", "120", ".")
+
+
+@nox.session
+def lint(session):
+    session.install("flake8", "flake8-bugbear", "isort")
+    session.run("flake8", "--show-source", "--statistics")
+    session.run("isort", "--line-length", "120", ".")
+
+
+@nox.session
+def type(session):
+    session.install("-r", "requirements.txt")
+    session.install("mypy")
+    session.run("mypy", "-p", "microvmi")
+
+
+@nox.session
+def test(session):
+    # run unit tests
+    args = session.posargs
+    # we need to compile and install the extension
+    session.install("-r", "requirements.txt")
+    # can't use pip install
+    # see: https://github.com/PyO3/maturin/issues/330
+    session.run(f'{CUR_DIR / "setup.py"}', "develop")
+    session.install("pytest==6.0.2", "coverage==5.3")
+    session.run("coverage", "run", "-m", "pytest", "-v", *args)
+    session.run("coverage", "report")
+
+
+@nox.session
+def coverage_html(session):
+    session.install("coverage==5.3")
+    session.run("coverage", "html", "--dir", ".coverage_html")
+    session.run("xdg-open", ".coverage_html/index.html")
+
+
+@nox.session
+def generate_wheels(session):
+    # you can pass additional argument
+    # example:
+    #   nox -r -s generate_wheels -- --features xen
+    #   nox -r -s generate_wheels -- --features xen,kvm,virtualbox --release
+    args = session.posargs
+    image_name = "manylinux2014_microvmi"
+    # ensure custom image is up to date
+    subprocess.check_call(["docker", "build", "-t", image_name, str(CUR_DIR)])
+    root_dir = CUR_DIR.parent
+    # maps libmicrovmi root dir to container /io
+    # executes /io/python/build-wheels.sh
+    subprocess.check_call(
+        ["docker", "run", "--rm", "-v", f"{str(root_dir)}:/io", image_name, "/io/python/build-wheels.sh", *args]
+    )
diff --git a/python/requirements.txt b/python/requirements.txt
new file mode 100644
index 00000000..53fb7779
--- /dev/null
+++ b/python/requirements.txt
@@ -0,0 +1,4 @@
+setuptools-rust
+setuptools==46.1
+wheel
+toml==0.10.2
diff --git a/python/setup.cfg b/python/setup.cfg
new file mode 100644
index 00000000..a6ef5bfd
--- /dev/null
+++ b/python/setup.cfg
@@ -0,0 +1,29 @@
+[metadata]
+description-file = README.md
+
+[aliases]
+test=pytest
+
+[flake8]
+max-line-length=120
+exclude = .git, .nox, .venv, venv, build, dist
+# black compatibility
+extend-ignore = E203
+# ignore import unused in __init__
+# ignore unable to detect undefined names
+per-file-ignores = __init__.py:F401,F403
+
+[coverage:run]
+# measure branch coverage in addition to statement coverage
+branch = True
+# restrict coverage on the package only
+source = microvmi
+
+# mandatory section, despite what the docs says
+[mypy]
+
+[mypy-microvmi.pymicrovmi.*]
+ignore_missing_imports = True
+
+[mypy-volatility3.framework.layers.resources.*]
+ignore_missing_imports = True
diff --git a/python/setup.py b/python/setup.py
new file mode 100755
index 00000000..269ec67d
--- /dev/null
+++ b/python/setup.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+
+import sys
+from pathlib import Path
+
+import toml
+from setuptools import setup, find_packages
+from setuptools_rust import Binding, RustExtension
+
+CUR_DIR = Path(__file__).resolve().parent
+
+# check for --features
+try:
+    index = sys.argv.index("--features")
+except ValueError:
+    features = []
+else:
+    # remove --features
+    sys.argv.pop(index)
+    # remove and retieve --features arg
+    try:
+        features_str = sys.argv.pop(index)
+    except IndexError:
+        print("invalid --features argument")
+        sys.exit(1)
+    else:
+        features = features_str.split(",")
+
+# check for --release
+debug = False
+try:
+    index = sys.argv.index("--release")
+except ValueError:
+    debug = True
+else:
+    debug = False
+    sys.argv.pop(index)
+
+# python package version is taken from Cargo.toml to avoid duplication
+with open(str(CUR_DIR / "Cargo.toml"), "r", encoding="utf-8") as f:
+    cargo = f.read()
+    cargo_toml = toml.loads(cargo)
+    author = cargo_toml["package"]["authors"][0].split("<")[0].strip()
+    author_email = cargo_toml["package"]["authors"][0].split("<")[1][:-1].strip()
+
+with open(str(CUR_DIR / cargo_toml["package"]["readme"]), "r", encoding="utf-8") as fh:
+    long_description = fh.read()
+
+with open(str(CUR_DIR / "requirements.txt")) as f:
+    requirements = f.read().splitlines()
+
+setup(
+    name="microvmi",
+    version=cargo_toml["package"]["version"],
+    author=author,
+    author_email=author_email,
+    description=cargo_toml["package"]["description"],
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    url="https://github.com/Wenzel/libmicrovmi",
+    install_requires=requirements,
+    rust_extensions=[RustExtension("microvmi.pymicrovmi", binding=Binding.PyO3, features=features, debug=debug)],
+    packages=find_packages(),
+    # rust extensions are not zip safe, just like C-extensions.
+    zip_safe=False,
+    classifiers=[
+        "Programming Language :: Python :: 3.6",
+        "Development Status :: 4 - Beta",
+        "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
+    ],
+    python_requires=">=3.6",
+)
diff --git a/python/src/errors.rs b/python/src/errors.rs
new file mode 100644
index 00000000..7f8a698a
--- /dev/null
+++ b/python/src/errors.rs
@@ -0,0 +1,29 @@
+use std::convert::From;
+use std::error::Error;
+
+use pyo3::exceptions::PyValueError;
+use pyo3::PyErr;
+
+use microvmi::errors::MicrovmiError;
+
+// create a NewType for MicrovmiError, since we cannot implement
+// From trait on external types
+#[derive(thiserror::Error, Debug)]
+pub enum PyMicrovmiError {
+    #[error("{source}")]
+    Microvmi {
+        #[from]
+        source: MicrovmiError,
+    },
+    #[error("{source}")]
+    Other {
+        #[from]
+        source: Box<dyn Error>,
+    },
+}
+
+impl From<PyMicrovmiError> for PyErr {
+    fn from(err: PyMicrovmiError) -> PyErr {
+        PyErr::new::<PyValueError, String>(err.to_string())
+    }
+}
diff --git a/python/src/lib.rs b/python/src/lib.rs
new file mode 100644
index 00000000..1e841140
--- /dev/null
+++ b/python/src/lib.rs
@@ -0,0 +1,192 @@
+mod errors;
+
+use log::{debug, info};
+
+use pyo3::exceptions::PyValueError;
+use pyo3::prelude::*;
+
+use errors::PyMicrovmiError;
+use microvmi::api as rapi; // rust api
+use microvmi::init;
+use pyo3::types::{PyByteArray, PyBytes};
+
+/// microvmi Python module declaration
+#[pymodule]
+fn pymicrovmi(_py: Python, m: &PyModule) -> PyResult<()> {
+    // init the env logger at module init
+    env_logger::init();
+
+    m.add_class::<MicrovmiExt>()?;
+    m.add_class::<DriverType>()?;
+    m.add_class::<DriverInitParam>()?;
+
+    Ok(())
+}
+
+// fake enum impl for driver type
+#[pyclass]
+struct DriverType {}
+
+#[pymethods]
+impl DriverType {
+    #[classattr]
+    const HYPERV: u32 = 0;
+    #[classattr]
+    const KVM: u32 = 1;
+    #[classattr]
+    const VIRTUALBOX: u32 = 2;
+    #[classattr]
+    const XEN: u32 = 3;
+}
+
+// this will not be exported to Python
+#[derive(Debug, Copy, Clone)]
+enum DriverInitParamType {
+    KVMiUnixSocket = 0,
+}
+
+// exposing DriverInitParam to Python is bit more complicated
+// as it's an enum with associated data
+// the proposed implementation is exposing static method returning an
+// initialized instance of DriverInitParam struct
+/// Manages additional driver initialization parameters
+#[pyclass]
+#[derive(Debug, Clone)]
+struct DriverInitParam {
+    pub param_type: DriverInitParamType,
+    #[pyo3(get)]
+    pub param_data_string: String,
+}
+
+#[pymethods]
+impl DriverInitParam {
+    /// initialize a DriverInitParam for the KVM driver, with a Unix socket
+    #[staticmethod]
+    fn kvmi_unix_socket(socket: &str) -> Self {
+        DriverInitParam {
+            param_type: DriverInitParamType::KVMiUnixSocket,
+            param_data_string: socket.to_string(),
+        }
+    }
+}
+
+/// Main class to interact with libmicrovmi
+// A class marked as unsendable will panic when accessed by another thread.
+// TODO: make Introspectable trait inherit Send trait, and make the drivers implementation
+// compatible
+#[pyclass(unsendable)]
+struct MicrovmiExt {
+    driver: Box<dyn rapi::Introspectable>,
+}
+
+#[pymethods]
+impl MicrovmiExt {
+    // enums are not available in PyO3 (yet)
+    // TODO: docstring is not exposed in Python (bug ?)
+    /// initializes libmicrovmi from the specified domain name
+    /// if driver_type is None, every driver compiled in libmicrovmi will be tested,
+    /// and the first one that succeeds will be returned, or an error
+    ///
+    /// Args:
+    ///     domain_name (str): the domain name
+    ///     driver_type (int, optional): the hypervisor driver type on which the library should be initialized.
+    ///     init_param (DriverInitParam, optional): additional initialization parameters for driver initialization
+    #[new]
+    #[args(domain_name, driver_type = "None", init_param = "None")]
+    fn new(
+        domain_name: &str,
+        driver_type: Option<u32>,
+        init_param: Option<DriverInitParam>,
+    ) -> PyResult<Self> {
+        info!("Microvmi Python init");
+        // convert Python DriverType to rust API DriverType
+        debug!(
+            "Microvmi Python init driver_type: {:?}, init_param: {:?}",
+            driver_type, init_param
+        );
+        let rust_driver_type = driver_type
+            .map(|drv_type| match drv_type {
+                DriverType::HYPERV => Ok(rapi::DriverType::HyperV),
+                DriverType::KVM => Ok(rapi::DriverType::KVM),
+                DriverType::VIRTUALBOX => Ok(rapi::DriverType::VirtualBox),
+                DriverType::XEN => Ok(rapi::DriverType::Xen),
+                _ => Err(PyValueError::new_err(format!(
+                    "Invalid value for DriverType: {}",
+                    drv_type
+                ))),
+            })
+            .transpose()?;
+        // convert Python DriverInitParam to rust API DriverinitParam
+        let rust_init_param: Option<rapi::DriverInitParam> =
+            init_param.map(|param| match param.param_type {
+                DriverInitParamType::KVMiUnixSocket => {
+                    rapi::DriverInitParam::KVMiSocket(param.param_data_string)
+                }
+            });
+        let driver =
+            init(domain_name, rust_driver_type, rust_init_param).map_err(PyMicrovmiError::from)?;
+        Ok(MicrovmiExt { driver })
+    }
+
+    /// read VM physical memory starting from paddr, of a given size
+    ///
+    /// Args:
+    ///     paddr: (int) physical address from where the read operation should start
+    ///     size: (int) size of the read operation
+    ///
+    /// Returns:
+    ///     Tuple[bytes, int]: the read operation result and the amount bytes read
+    fn read_physical<'p>(
+        &self,
+        py: Python<'p>,
+        paddr: u64,
+        size: usize,
+    ) -> PyResult<(&'p PyBytes, u64)> {
+        let mut bytes_read: u64 = 0;
+        let pybuffer: &PyBytes = PyBytes::new_with(py, size, |mut buffer| {
+            self.driver
+                .read_physical(paddr, &mut buffer, &mut bytes_read)
+                .ok();
+            Ok(())
+        })?;
+
+        Ok((pybuffer, bytes_read))
+    }
+
+    /// read VM physical memory starting from paddr into the given buffer
+    ///
+    /// Args:
+    ///     paddr (int): the physical address to start reading from
+    ///     buffer (bytearray): the buffer to read into
+    fn read_physical_into(&self, paddr: u64, buffer: &PyByteArray) -> u64 {
+        let mut_buf: &mut [u8] = unsafe { buffer.as_bytes_mut() };
+        let mut bytes_read: u64 = 0;
+        // ignore read error
+        self.driver
+            .read_physical(paddr, mut_buf, &mut bytes_read)
+            .ok();
+        bytes_read
+    }
+
+    /// pause the VM
+    fn pause(&mut self) -> PyResult<()> {
+        Ok(self.driver.pause().map_err(PyMicrovmiError::from)?)
+    }
+
+    /// resume the VM
+    fn resume(&mut self) -> PyResult<()> {
+        Ok(self.driver.resume().map_err(PyMicrovmiError::from)?)
+    }
+
+    /// get maximum physical address
+    ///
+    /// Returns:
+    ///     int: the maximum physical address
+    fn get_max_physical_addr(&self) -> PyResult<u64> {
+        let max_addr = self
+            .driver
+            .get_max_physical_addr()
+            .map_err(PyMicrovmiError::from)?;
+        Ok(max_addr)
+    }
+}
diff --git a/src/api.rs b/src/api.rs
index 9135bc2b..305f63e3 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -186,8 +186,14 @@ pub trait Introspectable {
     ///
     /// * 'paddr' - the physical address to read from
     /// * 'buf' - the data read from memory
+    /// * 'bytes_read' - the number of bytes read
     ///
-    fn read_physical(&self, _paddr: u64, _buf: &mut [u8]) -> Result<(), Box<dyn Error>> {
+    fn read_physical(
+        &self,
+        _paddr: u64,
+        _buf: &mut [u8],
+        _bytes_read: &mut u64,
+    ) -> Result<(), Box<dyn Error>> {
         unimplemented!();
     }
 
diff --git a/src/capi.rs b/src/capi.rs
index 1c88b72a..e1869521 100644
--- a/src/capi.rs
+++ b/src/capi.rs
@@ -95,11 +95,23 @@ pub unsafe extern "C" fn microvmi_read_physical(
     physical_address: uint64_t,
     buffer: *mut uint8_t,
     size: size_t,
+    bytes_read: *mut uint64_t,
 ) -> bool {
     let driver = get_driver_mut_ptr(context);
-    (*driver)
-        .read_physical(physical_address, slice::from_raw_parts_mut(buffer, size))
-        .is_ok()
+
+    let mut bytes_read_local = 0;
+    let res = (*driver)
+        .read_physical(
+            physical_address,
+            slice::from_raw_parts_mut(buffer, size),
+            &mut bytes_read_local,
+        )
+        .is_ok();
+    // update bytes_read if not NULL
+    if !bytes_read.is_null() {
+        bytes_read.write(bytes_read_local);
+    }
+    res
 }
 
 #[allow(clippy::missing_safety_doc)]
diff --git a/src/driver/kvm.rs b/src/driver/kvm.rs
index 6e3cd78d..447d403d 100644
--- a/src/driver/kvm.rs
+++ b/src/driver/kvm.rs
@@ -147,13 +147,20 @@ impl<T: KVMIntrospectable> Introspectable for Kvm<T> {
         Ok(self.kvmi.get_vcpu_count()?.try_into()?)
     }
 
-    fn read_physical(&self, paddr: u64, buf: &mut [u8]) -> Result<(), Box<dyn Error>> {
+    fn read_physical(
+        &self,
+        paddr: u64,
+        buf: &mut [u8],
+        bytes_read: &mut u64,
+    ) -> Result<(), Box<dyn Error>> {
         // kvmi read_physical can only handle a 4K buf request
         // any buffer bigger than that will result in an IOError (KVM_EINVAL)
         // need to chunk the read in 4K
         for (i, chunk) in buf.chunks_mut(PAGE_SIZE).enumerate() {
-            let cur_paddr = paddr + (i * PAGE_SIZE) as u64;
+            let offset = i * PAGE_SIZE;
+            let cur_paddr = paddr + offset as u64;
             self.kvmi.read_physical(cur_paddr, chunk)?;
+            *bytes_read = offset as u64;
         }
         Ok(())
     }
diff --git a/src/driver/virtualbox.rs b/src/driver/virtualbox.rs
index 6df4f4f1..01e98700 100644
--- a/src/driver/virtualbox.rs
+++ b/src/driver/virtualbox.rs
@@ -30,8 +30,15 @@ impl Introspectable for VBox {
         Ok(1)
     }
 
-    fn read_physical(&self, paddr: u64, buf: &mut [u8]) -> Result<(), Box<dyn Error>> {
-        self.fdp.read_physical_memory(paddr, buf)
+    fn read_physical(
+        &self,
+        paddr: u64,
+        buf: &mut [u8],
+        bytes_read: &mut u64,
+    ) -> Result<(), Box<dyn Error>> {
+        self.fdp.read_physical_memory(paddr, buf)?;
+        *bytes_read = buf.len() as u64;
+        Ok(())
     }
 
     fn get_max_physical_addr(&self) -> Result<u64, Box<dyn Error>> {
diff --git a/src/driver/xen.rs b/src/driver/xen.rs
index 5f7652d3..4466ab4d 100644
--- a/src/driver/xen.rs
+++ b/src/driver/xen.rs
@@ -112,34 +112,45 @@ impl Xen {
 }
 
 impl Introspectable for Xen {
-    fn read_physical(&self, paddr: u64, buf: &mut [u8]) -> Result<(), Box<dyn Error>> {
+    fn read_physical(
+        &self,
+        paddr: u64,
+        buf: &mut [u8],
+        bytes_read: &mut u64,
+    ) -> Result<(), Box<dyn Error>> {
         let mut cur_paddr: u64;
-        let mut offset: u64 = 0;
         let mut count_mut: u64 = buf.len() as u64;
         let mut buf_offset: u64 = 0;
+        *bytes_read = 0;
         while count_mut > 0 {
             // compute new paddr
-            cur_paddr = paddr + offset;
+            cur_paddr = paddr + buf_offset;
             // get the current gfn
             let gfn = cur_paddr >> PAGE_SHIFT;
-            offset = u64::from(PAGE_SIZE - 1) & cur_paddr;
+            let page_offset = u64::from(PAGE_SIZE - 1) & cur_paddr;
             // map gfn
             let page = self
                 .xen_fgn
                 .map(self.domid, PROT_READ, gfn)
                 .map_err(XenDriverError::from)?;
             // determine how much we can read
-            let read_len = if (offset + count_mut as u64) > u64::from(PAGE_SIZE) {
-                u64::from(PAGE_SIZE) - offset
+            let read_len = if (page_offset + count_mut as u64) > u64::from(PAGE_SIZE) {
+                u64::from(PAGE_SIZE) - page_offset
             } else {
-                buf.len() as u64
+                count_mut
             };
 
+            // prepare offsets
+            let buf_start = buf_offset as usize;
+            let buf_end = (buf_offset + read_len) as usize;
+            let page_start = page_offset as usize;
+            let page_end = (page_offset + read_len) as usize;
             // do the read
-            buf[buf_offset as usize..].copy_from_slice(&page[..read_len as usize]);
+            buf[buf_start..buf_end].copy_from_slice(&page[page_start..page_end]);
             // update loop variables
             count_mut -= read_len;
             buf_offset += read_len;
+            *bytes_read += read_len;
             // unmap page
             self.xen_fgn.unmap(page).map_err(XenDriverError::from)?;
         }


Test patch:
diff --git a/python/tests/test_handler.py b/python/tests/test_handler.py
new file mode 100644
index 00000000..60fb05e3
--- /dev/null
+++ b/python/tests/test_handler.py
@@ -0,0 +1,78 @@
+import pytest
+from microvmi import DriverType
+from microvmi.volatility.vmi_handler import MicrovmiHandlerError, url_to_driver_parameters
+
+
+def test_valid_scheme():
+    url = "vmi:///vm_name"
+    url_to_driver_parameters(url)
+
+
+def test_invalid_scheme():
+    url = "http:///vm_name"
+    with pytest.raises(MicrovmiHandlerError):
+        url_to_driver_parameters(url)
+
+
+def test_empty_vm_name():
+    url = "vmi:///"
+    with pytest.raises(MicrovmiHandlerError):
+        url_to_driver_parameters(url)
+
+
+def test_parse_vm_name():
+    expected = "windows10"
+    url = f"vmi:///{expected}"
+    value, *rest = url_to_driver_parameters(url)
+    assert expected == value
+
+
+def test_parse_vm_name_spaces():
+    expected = "windows 10 20H1"
+    url = f"vmi:///{expected}"
+    value, *rest = url_to_driver_parameters(url)
+    assert expected == value
+
+
+def test_parse_hypervisor_none():
+    expected = None
+    url = "vmi:///vm_name"
+    _, value, _ = url_to_driver_parameters(url)
+    assert expected == value
+
+
+@pytest.mark.parametrize("drv_type_enum_member", [d for d in DriverType])
+def test_parse_hypervisor_each(drv_type_enum_member):
+    expected = drv_type_enum_member
+    url = f"vmi://{drv_type_enum_member.name}/vm_name"
+    _, value, _ = url_to_driver_parameters(url)
+    assert expected == value
+
+
+def test_parse_hypervisor_bad():
+    url = "vmi://UNKNOWN_DRIVER/vm_name"
+    with pytest.raises(MicrovmiHandlerError):
+        url_to_driver_parameters(url)
+
+
+def test_parse_init_param_none():
+    expected = None
+    url = "vmi:///vm_name"
+    _, _, value = url_to_driver_parameters(url)
+    assert expected == value
+
+
+def test_parse_init_param_kvmi_socket():
+    socket = "/tmp/introspector"
+    # TODO: missing getter to test init_param type
+    url = f"vmi:///vm_name?kvmi_unix_socket={socket}"
+    _, _, value = url_to_driver_parameters(url)
+    assert socket == value.param_data_string
+
+
+def test_parse_init_param_unkown_key():
+    key = "unkown_config_key"
+    socket = "/tmp/introspector"
+    url = f"vmi:///vm_name?{key}={socket}"
+    with pytest.raises(MicrovmiHandlerError):
+        url_to_driver_parameters(url)