From 05f90b14f2f65d3bfecbe862a5ff53b2af5e5a56 Mon Sep 17 00:00:00 2001 From: ge Date: Thu, 23 Nov 2023 02:34:02 +0300 Subject: [PATCH] various improvements --- COPYING | 674 +++++++++++++ Makefile | 38 +- README.md | 104 ++ compute/__init__.py | 17 +- compute/__main__.py | 19 +- compute/cli/control.py | 127 ++- compute/common.py | 30 + compute/exceptions.py | 29 +- compute/instance/__init__.py | 15 + compute/instance/guest_agent.py | 15 + compute/instance/instance.py | 51 +- compute/instance/schemas.py | 15 + compute/session.py | 15 + compute/storage/__init__.py | 15 + compute/storage/pool.py | 15 + compute/storage/volume.py | 24 +- compute/utils/config_loader.py | 15 + compute/utils/ids.py | 15 + compute/utils/units.py | 15 + docs/source/conf.py | 4 +- packaging/Dockerfile | 23 + packaging/Makefile | 24 + packaging/build.sh | 15 + packaging/build/compute-0.1.0.dev1.tar.gz | Bin 0 -> 20824 bytes .../compute-0.1.0.dev1.dist-info/METADATA | 81 ++ .../build/compute-0.1.0.dev1.dist-info/RECORD | 23 + .../build/compute-0.1.0.dev1.dist-info/WHEEL | 4 + .../entry_points.txt | 3 + .../cpython3_3.11/build/compute/__init__.py | 22 + .../cpython3_3.11/build/compute/__main__.py | 21 + .../build/compute/cli/__init__.py | 0 .../build/compute/cli/control.py | 501 ++++++++++ .../cpython3_3.11/build/compute/common.py | 30 + .../cpython3_3.11/build/compute/exceptions.py | 80 ++ .../build/compute/instance/__init__.py | 18 + .../build/compute/instance/guest_agent.py | 208 ++++ .../build/compute/instance/instance.py | 675 +++++++++++++ .../build/compute/instance/schemas.py | 165 ++++ .../cpython3_3.11/build/compute/session.py | 286 ++++++ .../build/compute/storage/__init__.py | 17 + .../build/compute/storage/pool.py | 124 +++ .../build/compute/storage/volume.py | 138 +++ .../build/compute/utils/__init__.py | 0 .../build/compute/utils/config_loader.py | 56 ++ .../cpython3_3.11/build/compute/utils/ids.py | 33 + .../build/compute/utils/units.py | 54 ++ .../compute-0.1.0.dev1-py3-none-any.whl | Bin 0 -> 30693 bytes .../.pybuild/cpython3_3.11/scripts/compute | 8 + packaging/build/compute-0.1.0.dev1/PKG-INFO | 81 ++ packaging/build/compute-0.1.0.dev1/README.md | 65 ++ .../compute-0.1.0.dev1/compute/__init__.py | 22 + .../compute-0.1.0.dev1/compute/__main__.py | 21 + .../compute/cli/__init__.py | 0 .../compute-0.1.0.dev1/compute/cli/control.py | 501 ++++++++++ .../compute-0.1.0.dev1/compute/common.py | 30 + .../compute-0.1.0.dev1/compute/exceptions.py | 80 ++ .../compute/instance/__init__.py | 18 + .../compute/instance/guest_agent.py | 208 ++++ .../compute/instance/instance.py | 675 +++++++++++++ .../compute/instance/schemas.py | 165 ++++ .../compute-0.1.0.dev1/compute/session.py | 286 ++++++ .../compute/storage/__init__.py | 17 + .../compute/storage/pool.py | 124 +++ .../compute/storage/volume.py | 138 +++ .../compute/utils/__init__.py | 0 .../compute/utils/config_loader.py | 56 ++ .../compute-0.1.0.dev1/compute/utils/ids.py | 33 + .../compute-0.1.0.dev1/compute/utils/units.py | 54 ++ .../dh_installchangelogs.dch.trimmed | 5 + .../compute-doc/installed-by-dh_installdocs | 0 .../compute/dh_installchangelogs.dch.trimmed | 5 + .../compute/installed-by-dh_installdocs | 1 + .../build/compute-0.1.0.dev1/debian/changelog | 5 + .../debian/compute-doc.debhelper.log | 1 + .../debian/compute-doc.substvars | 4 + .../debian/compute-doc/DEBIAN/control | 11 + .../debian/compute-doc/DEBIAN/md5sums | 40 + .../share/doc/compute-doc/changelog.Debian.gz | Bin 0 -> 176 bytes .../usr/share/doc/compute-doc/copyright | 32 + .../compute-doc/html/_sources/index.rst.txt | 16 + .../html/_sources/pyapi/exceptions.rst.txt | 5 + .../html/_sources/pyapi/index.rst.txt | 49 + .../pyapi/instance/guest_agent.rst.txt | 6 + .../_sources/pyapi/instance/index.rst.txt | 10 + .../_sources/pyapi/instance/instance.rst.txt | 6 + .../_sources/pyapi/instance/schemas.rst.txt | 5 + .../html/_sources/pyapi/session.rst.txt | 6 + .../html/_sources/pyapi/storage/index.rst.txt | 9 + .../html/_sources/pyapi/storage/pool.rst.txt | 6 + .../_sources/pyapi/storage/volume.rst.txt | 6 + .../html/_sources/pyapi/utils.rst.txt | 14 + .../_sphinx_javascript_frameworks_compat.js | 1 + .../compute-doc/html/_static/alabaster.css | 701 ++++++++++++++ .../doc/compute-doc/html/_static/basic.css | 900 ++++++++++++++++++ .../doc/compute-doc/html/_static/custom.css | 1 + .../doc/compute-doc/html/_static/doctools.js | 1 + .../html/_static/documentation_options.js | 14 + .../doc/compute-doc/html/_static/file.png | Bin 0 -> 286 bytes .../_static/forkme_right_darkblue_121621.png | Bin 0 -> 7791 bytes .../doc/compute-doc/html/_static/jquery.js | 1 + .../compute-doc/html/_static/language_data.js | 1 + .../doc/compute-doc/html/_static/minus.png | Bin 0 -> 90 bytes .../doc/compute-doc/html/_static/plus.png | Bin 0 -> 90 bytes .../doc/compute-doc/html/_static/pygments.css | 83 ++ .../compute-doc/html/_static/searchtools.js | 1 + .../html/_static/sphinx_highlight.js | 1 + .../compute-doc/html/_static/underscore.js | 1 + .../share/doc/compute-doc/html/genindex.html | 614 ++++++++++++ .../usr/share/doc/compute-doc/html/index.html | 122 +++ .../share/doc/compute-doc/html/objects.inv | Bin 0 -> 1463 bytes .../doc/compute-doc/html/py-modindex.html | 165 ++++ .../compute-doc/html/pyapi/exceptions.html | 183 ++++ .../doc/compute-doc/html/pyapi/index.html | 342 +++++++ .../html/pyapi/instance/guest_agent.html | 266 ++++++ .../html/pyapi/instance/index.html | 120 +++ .../html/pyapi/instance/instance.html | 490 ++++++++++ .../html/pyapi/instance/schemas.html | 187 ++++ .../doc/compute-doc/html/pyapi/session.html | 331 +++++++ .../compute-doc/html/pyapi/storage/index.html | 119 +++ .../compute-doc/html/pyapi/storage/pool.html | 201 ++++ .../html/pyapi/storage/volume.html | 210 ++++ .../doc/compute-doc/html/pyapi/utils.html | 144 +++ .../share/doc/compute-doc/html/search.html | 124 +++ .../share/doc/compute-doc/html/searchindex.js | 1 + .../debian/compute.bash-completion | 93 ++ .../debian/compute.debhelper.log | 1 + .../debian/compute.postinst.debhelper | 10 + .../debian/compute.prerm.debhelper | 10 + .../debian/compute.substvars | 3 + .../debian/compute/DEBIAN/control | 12 + .../debian/compute/DEBIAN/md5sums | 27 + .../debian/compute/DEBIAN/postinst | 12 + .../debian/compute/DEBIAN/prerm | 12 + .../debian/compute/usr/bin/compute | 8 + .../compute-0.1.0.dev1.dist-info/METADATA | 81 ++ .../compute-0.1.0.dev1.dist-info/RECORD | 23 + .../compute-0.1.0.dev1.dist-info/WHEEL | 4 + .../entry_points.txt | 3 + .../python3/dist-packages/compute/__init__.py | 22 + .../python3/dist-packages/compute/__main__.py | 21 + .../dist-packages/compute/cli/__init__.py | 0 .../dist-packages/compute/cli/control.py | 501 ++++++++++ .../python3/dist-packages/compute/common.py | 30 + .../dist-packages/compute/exceptions.py | 80 ++ .../compute/instance/__init__.py | 18 + .../compute/instance/guest_agent.py | 208 ++++ .../compute/instance/instance.py | 675 +++++++++++++ .../dist-packages/compute/instance/schemas.py | 165 ++++ .../python3/dist-packages/compute/session.py | 286 ++++++ .../dist-packages/compute/storage/__init__.py | 17 + .../dist-packages/compute/storage/pool.py | 124 +++ .../dist-packages/compute/storage/volume.py | 138 +++ .../dist-packages/compute/utils/__init__.py | 0 .../compute/utils/config_loader.py | 56 ++ .../dist-packages/compute/utils/ids.py | 33 + .../dist-packages/compute/utils/units.py | 54 ++ .../share/bash-completion/completions/compute | 93 ++ .../compute/usr/share/doc/compute/README.md | 65 ++ .../usr/share/doc/compute/changelog.Debian.gz | Bin 0 -> 176 bytes .../compute/usr/share/doc/compute/copyright | 32 + .../build/compute-0.1.0.dev1/debian/control | 48 + .../build/compute-0.1.0.dev1/debian/copyright | 32 + .../debian/debhelper-build-stamp | 2 + .../build/compute-0.1.0.dev1/debian/docs | 1 + .../build/compute-0.1.0.dev1/debian/files | 3 + .../build/compute-0.1.0.dev1/debian/rules | 20 + .../compute-0.1.0.dev1/debian/source/format | 1 + .../compute-0.1.0.dev1/debian/source/options | 1 + .../debian/upstream/metadata.ex | 10 + .../build/compute-0.1.0.dev1/pyproject.toml | 61 ++ .../build/compute-doc_0.1.0.dev1-1_all.deb | Bin 0 -> 40424 bytes .../build/compute_0.1.0.dev1-1.debian.tar.xz | Bin 0 -> 2660 bytes packaging/build/compute_0.1.0.dev1-1.dsc | 21 + packaging/build/compute_0.1.0.dev1-1_all.deb | Bin 0 -> 21644 bytes .../compute_0.1.0.dev1-1_amd64.buildinfo | 270 ++++++ .../build/compute_0.1.0.dev1-1_amd64.changes | 38 + .../build/compute_0.1.0.dev1.orig.tar.gz | Bin 0 -> 20824 bytes packaging/build/docs/Makefile | 20 + packaging/build/docs/make.bat | 35 + .../docs/source/_templates/versioning.html | 8 + packaging/build/docs/source/conf.py | 33 + packaging/build/docs/source/index.rst | 16 + .../build/docs/source/pyapi/exceptions.rst | 5 + packaging/build/docs/source/pyapi/index.rst | 49 + .../source/pyapi/instance/guest_agent.rst | 6 + .../docs/source/pyapi/instance/index.rst | 10 + .../docs/source/pyapi/instance/instance.rst | 6 + .../docs/source/pyapi/instance/schemas.rst | 5 + packaging/build/docs/source/pyapi/session.rst | 6 + .../build/docs/source/pyapi/storage/index.rst | 9 + .../build/docs/source/pyapi/storage/pool.rst | 6 + .../docs/source/pyapi/storage/volume.rst | 6 + packaging/build/docs/source/pyapi/utils.rst | 14 + packaging/files/compute.bash-completion | 93 ++ packaging/files/control | 48 + packaging/files/copyright | 32 + packaging/files/docs | 1 + packaging/files/rules | 20 + pyproject.toml | 11 +- requirements.txt | 186 ++++ 200 files changed, 15968 insertions(+), 84 deletions(-) create mode 100644 COPYING create mode 100644 compute/common.py create mode 100644 packaging/Dockerfile create mode 100644 packaging/Makefile create mode 100644 packaging/build.sh create mode 100644 packaging/build/compute-0.1.0.dev1.tar.gz create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py create mode 100644 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl create mode 100755 packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute create mode 100644 packaging/build/compute-0.1.0.dev1/PKG-INFO create mode 100644 packaging/build/compute-0.1.0.dev1/README.md create mode 100644 packaging/build/compute-0.1.0.dev1/compute/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/__main__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/cli/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/cli/control.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/common.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/exceptions.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/instance/instance.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/session.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/storage/pool.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/storage/volume.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/utils/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/utils/ids.py create mode 100644 packaging/build/compute-0.1.0.dev1/compute/utils/units.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed create mode 100644 packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/installed-by-dh_installdocs create mode 100644 packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed create mode 100644 packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs create mode 100644 packaging/build/compute-0.1.0.dev1/debian/changelog create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/jquery.js create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/language_data.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/minus.png create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js create mode 120000 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/py-modindex.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute.substvars create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums create mode 100755 packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst create mode 100755 packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm create mode 100755 packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/__init__.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz create mode 100644 packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright create mode 100644 packaging/build/compute-0.1.0.dev1/debian/control create mode 100644 packaging/build/compute-0.1.0.dev1/debian/copyright create mode 100644 packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp create mode 100644 packaging/build/compute-0.1.0.dev1/debian/docs create mode 100644 packaging/build/compute-0.1.0.dev1/debian/files create mode 100755 packaging/build/compute-0.1.0.dev1/debian/rules create mode 100644 packaging/build/compute-0.1.0.dev1/debian/source/format create mode 100644 packaging/build/compute-0.1.0.dev1/debian/source/options create mode 100644 packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex create mode 100644 packaging/build/compute-0.1.0.dev1/pyproject.toml create mode 100644 packaging/build/compute-doc_0.1.0.dev1-1_all.deb create mode 100644 packaging/build/compute_0.1.0.dev1-1.debian.tar.xz create mode 100644 packaging/build/compute_0.1.0.dev1-1.dsc create mode 100644 packaging/build/compute_0.1.0.dev1-1_all.deb create mode 100644 packaging/build/compute_0.1.0.dev1-1_amd64.buildinfo create mode 100644 packaging/build/compute_0.1.0.dev1-1_amd64.changes create mode 100644 packaging/build/compute_0.1.0.dev1.orig.tar.gz create mode 100644 packaging/build/docs/Makefile create mode 100644 packaging/build/docs/make.bat create mode 100644 packaging/build/docs/source/_templates/versioning.html create mode 100644 packaging/build/docs/source/conf.py create mode 100644 packaging/build/docs/source/index.rst create mode 100644 packaging/build/docs/source/pyapi/exceptions.rst create mode 100644 packaging/build/docs/source/pyapi/index.rst create mode 100644 packaging/build/docs/source/pyapi/instance/guest_agent.rst create mode 100644 packaging/build/docs/source/pyapi/instance/index.rst create mode 100644 packaging/build/docs/source/pyapi/instance/instance.rst create mode 100644 packaging/build/docs/source/pyapi/instance/schemas.rst create mode 100644 packaging/build/docs/source/pyapi/session.rst create mode 100644 packaging/build/docs/source/pyapi/storage/index.rst create mode 100644 packaging/build/docs/source/pyapi/storage/pool.rst create mode 100644 packaging/build/docs/source/pyapi/storage/volume.rst create mode 100644 packaging/build/docs/source/pyapi/utils.rst create mode 100644 packaging/files/compute.bash-completion create mode 100644 packaging/files/control create mode 100644 packaging/files/copyright create mode 100644 packaging/files/docs create mode 100755 packaging/files/rules create mode 100644 requirements.txt diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/Makefile b/Makefile index 952372e..0992fee 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,44 @@ -SRC = compute/ -DIST = dist/ -DOCS_SRC = docs/source/ -DOCS_BUILD = docs/build/ +SRCDIR = compute +DISTDIR = dist +DOCS_SRCDIR = docs/source +DOCS_BUILDDIR = docs/build .PHONY: docs all: build +requirements.txt: + poetry export -f requirements.txt -o requirements.txt + build: format lint + awk '/^version/{print $$3}' pyproject.toml \ + | xargs -I {} sed "s/__version__ =.*/__version__ = '{}'/" -i $(SRCDIR)/__init__.py poetry build +build-deb: build + cd packaging && $(MAKE) + format: - poetry run isort $(SRC) - poetry run ruff format $(SRC) + poetry run isort $(SRCDIR) + poetry run ruff format $(SRCDIR) lint: - poetry run ruff check $(SRC) + poetry run ruff check $(SRCDIR) docs: - poetry run sphinx-build $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-build $(DOCS_SRCDIR) $(DOCS_BUILDDIR) docs-versions: - poetry run sphinx-multiversion $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-multiversion $(DOCS_SRCDIR) $(DOCS_BUILDDIR) serve-docs: - poetry run sphinx-autobuild $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-autobuild $(DOCS_SRCDIR) $(DOCS_BUILDDIR) clean: - [ -d $(DIST) ] && rm -rf $(DIST) || true - [ -d $(DOCS_BUILD) ] && rm -rf $(DOCS_BUILD) || true + [ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true + [ -d $(DOCS_BUILDDIR) ] && rm -rf $(DOCS_BUILDDIR) || true find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true + cd packaging && $(MAKE) clean -test-build: - poetry build - scp $(DIST)/*.tar.gz vm:~ +test-build: build-deb + scp packaging/build/compute*.deb vm:~ diff --git a/README.md b/README.md index 723b03c..d2236a4 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,107 @@ Install [poetry](https://python-poetry.org/), clone this repository and run: ``` poetry install --with dev --with docs ``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. + +# Installation + +Packages can be installed via `dpkg` or `apt-get`: + +``` +# apt-get install ./compute*.deb +``` + +After installation prepare environment, run following command to start libvirtd and create required storage pools: + +``` +# systemctl enable --now libvirtd.service +# virsh net-start default +# virsh net-autostart default +# for pool in images volumes; do + virsh pool-define-as $pool dir - - - - "/$pool" + virsh pool-build $pool + virsh pool-start $pool +done +``` + +Then set environment variables in your `~/.profile`, `~/.bashrc` or global in `/etc/profile.d/compute` or `/etc/bash.bashrc`: + +``` +export CMP_IMAGES_POOL=images +export CMP_VOLUMES_POOL=volumes +``` + +Configuration file is yet not supported. + +Make sure the variables are exported to the environment: + +``` +printenv | grep CMP_ +``` + +If the command didn't show anything _source_ your rc files or relogin. + + +# Basic usage + +To get help run: + +``` +compute --help +``` + +Also you can use `compute` as generic Python library. For example: + +```python +from compute import Session + +with Session() as session: + instance = session.get_instance('myinstance') + if not instance.is_running(): + instance.start() + else: + print('instance is already running') +``` + +# Create compute instances + +Place your qcow2 image in `/volumes` directory. For example `debian_12.qcow2`. + +Create `instance.yaml` file with following content: + +```yaml +name: myinstance +memory: 2048 # memory in MiB +vcpus: 2 +image: debian_12.qcow2 +volumes: + - type: file + is_system: true + target: vda + capacity: + value: 10 + unit: GiB +``` + +Refer to `Instance` class docs for more info. Full `instance.yaml` example will be provided later. + +To initialise instance run: + +``` +compute -l debug init instance.yaml +``` + +Start instance: + +``` +compute start myinstance +``` diff --git a/compute/__init__.py b/compute/__init__.py index 07940b8..ffe06d7 100644 --- a/compute/__init__.py +++ b/compute/__init__.py @@ -1,6 +1,21 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Compute instances management library.""" -__version__ = '0.1.0' +__version__ = '0.1.0-dev1' from .instance import Instance, InstanceConfig, InstanceSchema from .session import Session diff --git a/compute/__main__.py b/compute/__main__.py index c6467ef..4995fbd 100644 --- a/compute/__main__.py +++ b/compute/__main__.py @@ -1,6 +1,21 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Command line interface for compute module.""" -from compute.cli import control +from compute.cli import main -control.cli() +main.cli() diff --git a/compute/cli/control.py b/compute/cli/control.py index 93ad959..f5a5b91 100644 --- a/compute/cli/control.py +++ b/compute/cli/control.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Command line interface.""" import argparse @@ -15,10 +30,7 @@ import yaml from pydantic import ValidationError from compute import __version__ -from compute.exceptions import ( - ComputeServiceError, - GuestAgentTimeoutExceededError, -) +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError from compute.instance import GuestAgent from compute.session import Session from compute.utils import ids @@ -198,10 +210,23 @@ def _create_instance(session: Session, file: io.TextIOWrapper) -> None: sys.exit() +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + def main(session: Session, args: argparse.Namespace) -> None: """Perform actions.""" match args.command: - case 'create': + case 'init': _create_instance(session, args.file) case 'exec': _exec_guest_agent_command(session, args) @@ -211,14 +236,16 @@ def main(session: Session, args: argparse.Namespace) -> None: instance = session.get_instance(args.instance) instance.start() case 'shutdown': - instance = session.get_instance(args.instance) - instance.shutdown(args.method) + _shutdown_instance(session, args) case 'reboot': instance = session.get_instance(args.instance) instance.reboot() case 'reset': instance = session.get_instance(args.instance) instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() case 'pause': instance = session.get_instance(args.instance) instance.pause() @@ -234,7 +261,7 @@ def main(session: Session, args: argparse.Namespace) -> None: case 'setmem': instance = session.get_instance(args.instance) instance.set_memory(args.memory, live=True) - case 'setpasswd': + case 'setpass': instance = session.get_instance(args.instance) instance.set_user_password( args.username, @@ -261,7 +288,6 @@ def cli() -> None: # noqa: PLR0915 '-c', '--connect', metavar='URI', - default='qemu:///system', help='libvirt connection URI', ) root.add_argument( @@ -270,7 +296,7 @@ def cli() -> None: # noqa: PLR0915 type=str.lower, metavar='LEVEL', choices=log_levels, - help='log level [envvar: CMP_LOG]', + help='log level', ) root.add_argument( '-V', @@ -280,13 +306,16 @@ def cli() -> None: # noqa: PLR0915 ) subparsers = root.add_subparsers(dest='command', metavar='COMMAND') - # create command - create = subparsers.add_parser( - 'create', help='create new instance from YAML config file' + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' ) - create.add_argument( + init.add_argument( 'file', type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', ) # exec subcommand @@ -307,14 +336,14 @@ def cli() -> None: # noqa: PLR0915 default=60, help=( 'waiting time in seconds for a command to be executed ' - 'in guest, 60 sec by default' + 'in guest [default: 60]' ), ) execute.add_argument( '-x', '--executable', default='/bin/sh', - help='path to executable in guest, /bin/sh by default', + help='path to executable in guest [default: /bin/sh]', ) execute.add_argument( '-e', @@ -352,12 +381,36 @@ def cli() -> None: # noqa: PLR0915 # shutdown subcommand shutdown = subparsers.add_parser('shutdown', help='shutdown instance') shutdown.add_argument('instance') - shutdown.add_argument( - '-m', - '--method', - choices=['soft', 'normal', 'hard', 'unsafe'], - default='normal', - help='use shutdown method', + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), ) # reboot subcommand @@ -368,6 +421,10 @@ def cli() -> None: # noqa: PLR0915 reset = subparsers.add_parser('reset', help='reset instance') reset.add_argument('instance') + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + # pause subcommand pause = subparsers.add_parser('pause', help='pause instance') pause.add_argument('instance') @@ -390,15 +447,15 @@ def cli() -> None: # noqa: PLR0915 setmem.add_argument('instance') setmem.add_argument('memory', type=int, help='memory in MiB') - # setpasswd subcommand - setpasswd = subparsers.add_parser( - 'setpasswd', + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', help='set user password in guest', ) - setpasswd.add_argument('instance') - setpasswd.add_argument('username') - setpasswd.add_argument('password') - setpasswd.add_argument( + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( '-e', '--encrypted', action='store_true', @@ -419,10 +476,18 @@ def cli() -> None: # noqa: PLR0915 ) log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + try: - with Session(args.connect) as session: + with Session(connect_uri) as session: main(session, args) - except ComputeServiceError as e: + except ComputeError as e: sys.exit(f'error: {e}') except KeyboardInterrupt: sys.exit() diff --git a/compute/common.py b/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/compute/exceptions.py b/compute/exceptions.py index 0528afd..1eef8de 100644 --- a/compute/exceptions.py +++ b/compute/exceptions.py @@ -1,19 +1,34 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Exceptions.""" -class ComputeServiceError(Exception): - """Basic exception class for Compute.""" +class ComputeError(Exception): + """Basic exception class.""" -class ConfigLoaderError(ComputeServiceError): +class ConfigLoaderError(ComputeError): """Something went wrong when loading configuration.""" -class SessionError(ComputeServiceError): +class SessionError(ComputeError): """Something went wrong while connecting to libvirtd.""" -class GuestAgentError(ComputeServiceError): +class GuestAgentError(ComputeError): """Something went wring when QEMU Guest Agent call.""" @@ -33,7 +48,7 @@ class GuestAgentCommandNotSupportedError(GuestAgentError): """Guest agent command is not supported or blacklisted on guest.""" -class StoragePoolError(ComputeServiceError): +class StoragePoolError(ComputeError): """Something went wrong when operating with storage pool.""" @@ -53,7 +68,7 @@ class VolumeNotFoundError(StoragePoolError): super().__init__(f"storage volume '{msg}' not found") -class InstanceError(ComputeServiceError): +class InstanceError(ComputeError): """Something went wrong while interacting with the domain.""" diff --git a/compute/instance/__init__.py b/compute/instance/__init__.py index 100c1c5..6e2b150 100644 --- a/compute/instance/__init__.py +++ b/compute/instance/__init__.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + from .guest_agent import GuestAgent from .instance import Instance, InstanceConfig from .schemas import InstanceSchema diff --git a/compute/instance/guest_agent.py b/compute/instance/guest_agent.py index 5355ae3..4381591 100644 --- a/compute/instance/guest_agent.py +++ b/compute/instance/guest_agent.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Interacting with the QEMU Guest Agent.""" import json diff --git a/compute/instance/instance.py b/compute/instance/instance.py index 143caed..5b806e6 100644 --- a/compute/instance/instance.py +++ b/compute/instance/instance.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Manage compute instances.""" __all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] @@ -9,6 +24,7 @@ import libvirt from lxml import etree from lxml.builder import E +from compute.common import DeviceConfig, EntityConfig from compute.exceptions import ( GuestAgentCommandNotSupportedError, InstanceError, @@ -28,8 +44,8 @@ from .schemas import ( log = logging.getLogger(__name__) -class InstanceConfig: - """Compute instance config builder.""" +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" def __init__(self, schema: InstanceSchema): """ @@ -181,10 +197,6 @@ class InstanceInfo(NamedTuple): cputime: int -class DeviceConfig: - """Abstract device config class.""" - - class Instance: """Manage compute instances.""" @@ -492,7 +504,7 @@ class Instance: return child[0].getparent() if child else None def attach_device( - self, device: 'DeviceConfig', *, live: bool = False + self, device: DeviceConfig, *, live: bool = False ) -> None: """ Attach device to compute instance. @@ -517,7 +529,7 @@ class Instance: self.domain.attachDeviceFlags(device.to_xml(), flags=flags) def detach_device( - self, device: 'DeviceConfig', *, live: bool = False + self, device: DeviceConfig, *, live: bool = False ) -> None: """ Dettach device from compute instance. @@ -545,8 +557,8 @@ class Instance: """ Detach disk device by target name. - There is no ``attach_disk()`` method. Use :method:`attach_device` - with :class:`DiskConfig` as parameter. + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. :param name: Disk name e.g. 'vda', 'sda', etc. This name may not match the name of the disk inside the guest OS. @@ -574,14 +586,14 @@ class Instance: raise InstanceError(msg) self.detach_device(DiskConfig(**disk_params), live=True) - def resize_volume( + def resize_disk( self, name: str, capacity: int, unit: units.DataUnit ) -> None: """ Resize attached block device. :param name: Disk device name e.g. `vda`, `sda`, etc. - :param capacity: New volume capacity. + :param capacity: New capacity. :param unit: Capacity unit. """ self.domain.blockResize( @@ -590,6 +602,10 @@ class Instance: flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, ) + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + def pause(self) -> None: """Pause instance.""" if not self.is_running(): @@ -600,9 +616,9 @@ class Instance: """Resume paused instance.""" self.domain.resume() - def list_ssh_keys(self, user: str) -> list[str]: + def get_ssh_keys(self, user: str) -> list[str]: """ - Get list of SSH keys on guest for specific user. + Return list of SSH keys on guest for specific user. :param user: Username. """ @@ -617,7 +633,7 @@ class Instance: """ raise NotImplementedError - def remove_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: """ Remove SSH keys from guest for specific user. @@ -632,7 +648,7 @@ class Instance: """ Set new user password in guest OS. - This action performs by guest agent inside guest. + This action performs by guest agent inside the guest. :param user: Username. :param password: Password. @@ -653,6 +669,7 @@ class Instance: return self.domain.XMLDesc(flags) def delete(self) -> None: - """Undefine instance and delete local volumes.""" + """Undefine instance.""" + # TODO @ge: delete local disks self.shutdown(method='HARD') self.domain.undefine() diff --git a/compute/instance/schemas.py b/compute/instance/schemas.py index 684a72d..f5a677c 100644 --- a/compute/instance/schemas.py +++ b/compute/instance/schemas.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Compute instance related objects schemas.""" import re diff --git a/compute/session.py b/compute/session.py index 7506fc5..de5f900 100644 --- a/compute/session.py +++ b/compute/session.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Hypervisor session manager.""" import logging diff --git a/compute/storage/__init__.py b/compute/storage/__init__.py index 5090edd..34aae30 100644 --- a/compute/storage/__init__.py +++ b/compute/storage/__init__.py @@ -1,2 +1,17 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + from .pool import StoragePool from .volume import DiskConfig, Volume, VolumeConfig diff --git a/compute/storage/pool.py b/compute/storage/pool.py index 0a11d3a..cb17494 100644 --- a/compute/storage/pool.py +++ b/compute/storage/pool.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Manage storage pools.""" import logging diff --git a/compute/storage/volume.py b/compute/storage/volume.py index b7dfaea..11a1dc4 100644 --- a/compute/storage/volume.py +++ b/compute/storage/volume.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Manage storage volumes.""" from dataclasses import dataclass @@ -8,13 +23,14 @@ import libvirt from lxml import etree from lxml.builder import E +from compute.common import DeviceConfig, EntityConfig from compute.utils import units @dataclass -class VolumeConfig: +class VolumeConfig(EntityConfig): """ - Storage volume config builder. + Storage volume XML config builder. Generate XML config for creating a volume in a libvirt storage pool. @@ -48,9 +64,9 @@ class VolumeConfig: @dataclass -class DiskConfig: +class DiskConfig(DeviceConfig): """ - Disk config builder. + Disk XML config builder. Generate XML config for attaching or detaching storage volumes to compute instances. diff --git a/compute/utils/config_loader.py b/compute/utils/config_loader.py index 5763d03..aaeb0fe 100644 --- a/compute/utils/config_loader.py +++ b/compute/utils/config_loader.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Configuration loader.""" import tomllib diff --git a/compute/utils/ids.py b/compute/utils/ids.py index 335017f..8a6454a 100644 --- a/compute/utils/ids.py +++ b/compute/utils/ids.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Random identificators.""" # ruff: noqa: S311, C417 diff --git a/compute/utils/units.py b/compute/utils/units.py index 7e4b632..57a4583 100644 --- a/compute/utils/units.py +++ b/compute/utils/units.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + """Tools for data units convertion.""" from enum import StrEnum diff --git a/docs/source/conf.py b/docs/source/conf.py index c2c53c3..38e4a1f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ -# Add ../../compute to path for autodoc +# Add ../.. to path for autodoc Sphinx extension import os import sys -sys.path.insert(0, os.path.abspath('../../compute')) +sys.path.insert(0, os.path.abspath('../..')) # Project information project = 'Compute' diff --git a/packaging/Dockerfile b/packaging/Dockerfile new file mode 100644 index 0000000..4659618 --- /dev/null +++ b/packaging/Dockerfile @@ -0,0 +1,23 @@ +FROM debian:bookworm-slim +WORKDIR /mnt/build +RUN apt-get update; \ + env DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ + build-essential \ + bash-completion \ + debhelper \ + quilt \ + dh-make \ + dh-python \ + pybuild-plugin-pyproject \ + python3-poetry-core \ + python3-all \ + python3-setuptools \ + python3-sphinx \ + python3-sphinx-multiversion \ + python3-libvirt \ + python3-lxml \ + python3-yaml \ + python3-pydantic; \ + apt clean; \ + echo "alias ll='ls -alFh'" >> /etc/bash.bashrc +USER 1000:1000 diff --git a/packaging/Makefile b/packaging/Makefile new file mode 100644 index 0000000..d92de39 --- /dev/null +++ b/packaging/Makefile @@ -0,0 +1,24 @@ +DOCKER_CMD ?= docker +DOCKER_IMG = pybuilder:bookworm +DEBBUILDDIR = build + +all: docker-build build + +clean: + test -d $(DEBBUILDDIR) && rm -rf $(DEBBUILDDIR) || true + +docker-build: + $(DOCKER_CMD) build -f Dockerfile -t $(DOCKER_IMG) . + +build: clean + mkdir -p $(DEBBUILDDIR) + cp -v ../dist/compute-*[.tar.gz] $(DEBBUILDDIR)/ + cp -r ../docs $(DEBBUILDDIR)/ + if [ -f build.sh.bak ]; then mv build.sh{.bak,}; fi + cp build.sh{,.bak} + awk '/authors/{gsub(/[\[\]]/,"");print $$3" "$$4}' ../pyproject.toml \ + | sed "s/['<>]//g" \ + | tr ' ' '\n' \ + | xargs -I {} sed "0,/%placeholder%/s//{}/" -i build.sh + $(DOCKER_CMD) run --rm -i -v $$PWD:/mnt $(DOCKER_IMG) bash < build.sh + mv build.sh{.bak,} diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100644 index 0000000..575c5d5 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,15 @@ +set -o errexit +set -o xtrace +export USER=build +export HOME=/mnt +export DEBFULLNAME='%placeholder%' +export DEBEMAIL='%placeholder%' +mkdir -p /mnt/build && cd /mnt/build +tar xf compute-*[.tar.gz] +cd compute-*[^.tar.gz] +sed -e "s%\.\./\.\.%$PWD%" -i ../docs/source/conf.py +dh_make --copyright gpl3 --yes --python --file ../compute-*[.tar.gz] +rm debian/*.ex debian/README.{Debian,source} debian/*.docs +sed -e 's/\* Initial release.*/\* This is the development build, see commits in upstream repo for info./' -i debian/changelog +cp -v ../../files/{control,rules,copyright,docs,compute.bash-completion} debian/ +dpkg-buildpackage -us -uc diff --git a/packaging/build/compute-0.1.0.dev1.tar.gz b/packaging/build/compute-0.1.0.dev1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fcb6882313d06126099819d8417de54da00684f0 GIT binary patch literal 20824 zcmV)7K*zryiwFn+00002|6^}$aCLNLEif)IE-)@+Wp*(xbYXG;?Y(Pz+c=UaJfHO| zxaoHuN9W{I|_wDp-`9LW$^N^$Kk{KC>%!VAAXZhi$7I=TdlSA;yb3dTdVEWKX?y+_#Hm8 zEDuxY{trLqXZ4jg&f{^^ZEv+&o3GbbH`_t$)#lb#d#m<`<>&wT&mb93XL+>J3SixX zVRYAidD7e7J?I7F;rHDCo9pXU`+s$FYx587^^Mi7jn?|w<_7Hl)zyuSKX|R>{{PKC zKY2Th0kxX=;>8mH$z%{^-Z-3uH_(I}Z>BNz3bhQ+UjcLiVXxt)_F9FD_jt+wKw zKU{b_X%y-s!DJ7TcTbKE#P`7{nGILsNt}DvN$O40*^Nj#h{g$k!%Tdi>ZuH{Lp*52dk0i01pWUSc(`X8RmI7xP^2xc^GNQ(9Am+$TZBd{5DNy zH@DjOZ)RZ{PVzV+FeS4TAs4~FfG62(I!#gu*njUGeDL1Q00Q3j4Q895_cYMUmNIXa zK@ks7!we7r1_`~&VeB0JeU(-w(+3DG|HsFp zeJklO%At`@&f7L~*;2+; zIQTT1%H;ij?C|a`c5AJ6{FvXuJlBGD`%gH#H2T*pPNN}gU}|PG^3JD8l&6mu_1iq3 zW}TNWr&MQ!lY=C^dD&><&YeVFeoJjklPt~?L0Hn+q*JS1US8I?25)3&cxB~2&Tj!l z@6=Z$JXo^!llNv8kA^VVt2hK8!At<8#h`#heFEgTj?*k}LbuUG>xYQe71m!FMpxY6 zr8pBWX-x7L617X&dgk57&@@)ena*(rpyEqv4X`JT1~{?6JC=0@FubcsP6`m>E)KoR z;q=qZr3WPJGMwfsH&K2m#wLC$AOtVPP<{^NSihbA2TT5c$^UEo|9WdRXs^G1)mq3IZ$R-S|Np!2|7Um*U&kY2S*Br{ zd¶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 literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD new file mode 100644 index 0000000..5f97163 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD @@ -0,0 +1,23 @@ +../scripts/compute,sha256=b-Gj6H6ssfbGalpouUMSX5pmsjqDnN9xMdTwnU-UfZY,216 +compute/__init__.py,sha256=x4zp_CoVPKgDT6AqhometspAyinGxJUXO48duJ5aHUM,873 +compute/__main__.py,sha256=zJyKJul6pCbguFPtVLZBoAuZl9RXibn4CCMn46jIgUQ,745 +compute/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/cli/control.py,sha256=83wnR21pHOPyyk1i1n_YBIDz6dCFB6hmuIFguIk68rs,14634 +compute/common.py,sha256=G1qwC1EybG5LEJtyoux9ymiqB2ZOsgKXlCpbuhHv55Y,948 +compute/exceptions.py,sha256=Ga59L55qSAPeyDfjANPuMh4yVSRWHDYi9xqq5o4_7-0,2452 +compute/instance/__init__.py,sha256=kHN8jVamyrBZYZgi62tPtJ7rS73gUPhfswLalmPA5Zs,772 +compute/instance/guest_agent.py,sha256=fq89kQbcV5X5eFCsMmujRuwTOSghWO4ZhAjvxyUu84M,7018 +compute/instance/instance.py,sha256=WP6oTJfdAf6QlefwVLqdC8J6XoKHum6nZhwwHOEtjNk,23297 +compute/instance/schemas.py,sha256=B51ytPlxhnx0MrkR2WYhd49RaRT7Is7NsIM9OrMUpvI,4288 +compute/session.py,sha256=znYOIzoiCbSG62k-ViaXti_lOnw88wD8Syp3nCXAJ28,10050 +compute/storage/__init__.py,sha256=zNaVjZ2925DxrVUFWwVRsGU6bSYbF46sb4L6NsaiKbw,736 +compute/storage/pool.py,sha256=9z99bBDbb4ATGpfMkEWpxAO4fEQHNVOxxf0iUln9cN0,4197 +compute/storage/volume.py,sha256=_TbK9Y4d3NAeknPUiuhldAT3ZaN1sZgjy4QzC-Sw4Io,4110 +compute/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/utils/config_loader.py,sha256=ul1J3sZg0D9R0HbOz5Pg9JmL4nFaMahAzQEdGaWFABU,1989 +compute/utils/ids.py,sha256=fg6Xsg4OMM-BIaU3DPu0L91ICwx-L3qNoELEwQZz2s0,1007 +compute/utils/units.py,sha256=UkwD0zQ-rlpSpkbfezCcvJx4D8iZlI9M-oXXvdVEvy0,1549 +compute-0.1.0.dev1.dist-info/METADATA,sha256=tbX8xp92Jwqf44sOwPB-HqKHLezab5dU9DrQDYFitDQ,1944 +compute-0.1.0.dev1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88 +compute-0.1.0.dev1.dist-info/entry_points.txt,sha256=xHhg-Fo9Z5gJnIahbG8pVIGNDqlH5Eordn8hnXUwscw,51 +compute-0.1.0.dev1.dist-info/RECORD,, diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL new file mode 100644 index 0000000..4ba7671 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.4.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt new file mode 100644 index 0000000..4130f9f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +compute=compute.cli.control:cli + diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..acf2d647ea5aed7660809b61bc9dc2491bbe1720 GIT binary patch literal 30693 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 literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute new file mode 100755 index 0000000..56e33f2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from compute.cli.control import cli +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(cli()) diff --git a/packaging/build/compute-0.1.0.dev1/PKG-INFO b/packaging/build/compute-0.1.0.dev1/PKG-INFO new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/PKG-INFO @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/README.md b/packaging/build/compute-0.1.0.dev1/README.md new file mode 100644 index 0000000..0131e8e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/README.md @@ -0,0 +1,65 @@ +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` diff --git a/packaging/build/compute-0.1.0.dev1/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/compute/common.py b/packaging/build/compute-0.1.0.dev1/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/compute/session.py b/packaging/build/compute-0.1.0.dev1/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/installed-by-dh_installdocs b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs new file mode 100644 index 0000000..c2dd0c3 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs @@ -0,0 +1 @@ +./README.md diff --git a/packaging/build/compute-0.1.0.dev1/debian/changelog b/packaging/build/compute-0.1.0.dev1/debian/changelog new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/changelog @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log new file mode 100644 index 0000000..8dc2028 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log @@ -0,0 +1 @@ +dh_sphinxdoc diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars new file mode 100644 index 0000000..c41bfd3 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars @@ -0,0 +1,4 @@ +sphinxdoc:Depends=libjs-sphinxdoc (>= 1.0), libjs-sphinxdoc (>= 2.4.3-5~), libjs-sphinxdoc (>= 5.0), libjs-sphinxdoc (>= 5.2) +sphinxdoc:Built-Using=alabaster (= 0.7.12-1), sphinx (= 5.3.0-4) +misc:Depends= +misc:Pre-Depends= diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control new file mode 100644 index 0000000..72814c9 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control @@ -0,0 +1,11 @@ +Package: compute-doc +Source: compute +Version: 0.1.0.dev1-1 +Architecture: all +Maintainer: ge +Installed-Size: 376 +Depends: libjs-sphinxdoc (>= 5.2) +Section: doc +Priority: optional +Homepage: https://git.lulzette.ru/hstack/compute +Description: Compute instances management library and tools (documentation) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums new file mode 100644 index 0000000..5ab5be6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums @@ -0,0 +1,40 @@ +6845278a102bd147f30f770ed1134ce5 usr/share/doc/compute-doc/changelog.Debian.gz +fb1a6c11d7a8fa5f238617c20b13b6a1 usr/share/doc/compute-doc/copyright +705113edf19bbf7f9d406fccd98ebef9 usr/share/doc/compute-doc/html/_sources/index.rst.txt +91934f7b742b8395043e25cfa73682af usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt +de8bc1c2c00774ddee5363aef80c0775 usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt +2a0040e0a150de53ed929e963af635a8 usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt +dd6324cb85dc57ef37c4f8161aa2d233 usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt +c594567565cc48a247932409d9adcc4a usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt +e6a69ab447e455dba6e7b865a3d872d2 usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt +ba27654c086857e64d58468b13bc31c4 usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt +801ccc953fc57199b06ec122e10f784c usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt +324ae7c877f3cf7895b2a5d3af579345 usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt +db91c0d83c2c80e9f9323a8943eeeff4 usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt +572ed749dd8924c36f1afe9e8e14d4d3 usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt +4fc9d553e40384beedf38e21f205d2a7 usr/share/doc/compute-doc/html/_static/alabaster.css +23ffe661f835b08e157d492a86aae74d usr/share/doc/compute-doc/html/_static/basic.css +dad0c9b31e59069c83018ce87594ed65 usr/share/doc/compute-doc/html/_static/custom.css +5e103d51310d4e0c065325d795cc9def usr/share/doc/compute-doc/html/_static/documentation_options.js +ba0c95766a77a6c598a7ca542f1db738 usr/share/doc/compute-doc/html/_static/file.png +5b6b3233153feca50a94aa6c60873a5f usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png +36b1a4b05451c7acde7ced60b2f6bc21 usr/share/doc/compute-doc/html/_static/minus.png +0d7849fd4d4148b7f78cab60a087633a usr/share/doc/compute-doc/html/_static/plus.png +4f81be1c1dd97a6ec76af15b8f926189 usr/share/doc/compute-doc/html/_static/pygments.css +fd297228a19ece7e38824d0704f3635d usr/share/doc/compute-doc/html/genindex.html +3e038e6169c721ebacf889ea4ac5c1bf usr/share/doc/compute-doc/html/index.html +b8e4906e5136e907ab0d7ae826720603 usr/share/doc/compute-doc/html/objects.inv +2658558520c0c9f209dd4c69516facfd usr/share/doc/compute-doc/html/py-modindex.html +4254a2ecc3e154f52646febebd0ef6e6 usr/share/doc/compute-doc/html/pyapi/exceptions.html +bf4609f321d2c60399574c3e52dd6a44 usr/share/doc/compute-doc/html/pyapi/index.html +730aab71986cb938e9aff03ba203c9a9 usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html +fad8eba8a9cb9b1befd8e0ecdf1bbe5f usr/share/doc/compute-doc/html/pyapi/instance/index.html +781272676f0b35c52f43b99f2ca86647 usr/share/doc/compute-doc/html/pyapi/instance/instance.html +ede88501ec628083bb1ad1cb86cdec9f usr/share/doc/compute-doc/html/pyapi/instance/schemas.html +4c8d372d298068aba7272d11feb2cc52 usr/share/doc/compute-doc/html/pyapi/session.html +000f86f6184a455843017772ff2fec9d usr/share/doc/compute-doc/html/pyapi/storage/index.html +a2b63c0194a1e55be8d7036b46851986 usr/share/doc/compute-doc/html/pyapi/storage/pool.html +8d4e9081b213585aad36b4daadc37e26 usr/share/doc/compute-doc/html/pyapi/storage/volume.html +307d7a44f4343b0f34ee758e4ab20d88 usr/share/doc/compute-doc/html/pyapi/utils.html +5999199d4710213969f7fb1b50647f4a usr/share/doc/compute-doc/html/search.html +148b182d3691ae88c629783c3623007d usr/share/doc/compute-doc/html/searchindex.js diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz new file mode 100644 index 0000000000000000000000000000000000000000..40eae6fedeef605a8bc33ff14a672c4eec8e46c4 GIT binary patch literal 176 zcmV;h08jrPiwFP!000020}YJJ4uUWgME88fEPc_GVqD-On#j($@DbNoF3?C@LR+GL zZzr>woH^3!A$Y=!vy5?8)0Cyz9M9{myp*SVdEO$7EgAXSYpPYyNdheJ=#)dO?+Ecj zy&W_ek9Sagy@Dfxv|1}4DT6RLKT@SJ(qPfpF^-L8QI)1>3A>h#Mt!?VejGF855S9} eMhIN(1i?iPkr#YZtaB`RO!5cgr!y+J0000<1Wum- literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt new file mode 100644 index 0000000..81222c2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt @@ -0,0 +1,16 @@ +Compute +======= + +Compute instances management library. + +.. toctree:: + :maxdepth: 1 + + pyapi/index + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt new file mode 100644 index 0000000..3912721 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt @@ -0,0 +1,5 @@ +``exceptions`` +============== + +.. automodule:: compute.exceptions + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt new file mode 100644 index 0000000..e0cebb8 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt @@ -0,0 +1,49 @@ +Python API +========== + +The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the `myinstance` instance. + +.. code-block:: python + + import logging + + from compute import Session + + logging.basicConfig(level=logging.DEBUG) + + with Session() as session: + instance = session.get_instance('myinstance') + instance.set_vcpus(4) + instance.start() + instance.set_autostart(enabled=True) + + +:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect` +and returns objects of other classes of the present library. + +Entity representation +--------------------- + +Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +:class:`Volume`. + +The configuration files of various libvirt objects in `compute` are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the ``to_xml()`` method. For example :class:`VolumeConfig`. + +`Pydantic `_ models are used to validate input data. +For example :class:`VolumeSchema`. + +Modules documentation +--------------------- + +.. toctree:: + :maxdepth: 4 + + session + instance/index + storage/index + utils + exceptions diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt new file mode 100644 index 0000000..1305140 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt @@ -0,0 +1,6 @@ +``guest_agent`` +=============== + +.. automodule:: compute.instance.guest_agent + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt new file mode 100644 index 0000000..659ffc2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt @@ -0,0 +1,10 @@ +``instance`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + instance + guest_agent + schemas diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt new file mode 100644 index 0000000..3c58f1f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt @@ -0,0 +1,6 @@ +``instance`` +============ + +.. automodule:: compute.instance.instance + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt new file mode 100644 index 0000000..7dacabf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt @@ -0,0 +1,5 @@ +``schemas`` +=========== + +.. automodule:: compute.instance.schemas + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt new file mode 100644 index 0000000..2dec16e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt @@ -0,0 +1,6 @@ +``session`` +=========== + +.. automodule:: compute.session + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt new file mode 100644 index 0000000..e9ea734 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt @@ -0,0 +1,9 @@ +``storage`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + pool + volume diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt new file mode 100644 index 0000000..398124e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt @@ -0,0 +1,6 @@ +``pool`` +======== + +.. automodule:: compute.storage.pool + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt new file mode 100644 index 0000000..e1ba8d0 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt @@ -0,0 +1,6 @@ +``volume`` +========== + +.. automodule:: compute.storage.volume + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt new file mode 100644 index 0000000..b5ab60a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt @@ -0,0 +1,14 @@ +``utils`` +========= + +``utils.units`` +--------------- + +.. automodule:: compute.utils.units + :members: + +``utils.ids`` +------------- + +.. automodule:: compute.utils.ids + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js new file mode 120000 index 0000000..e04de6d --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/_sphinx_javascript_frameworks_compat.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css new file mode 100644 index 0000000..0eddaeb --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css @@ -0,0 +1,701 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css new file mode 100644 index 0000000..4e9a9f1 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css @@ -0,0 +1,900 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js new file mode 120000 index 0000000..e51872e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/doctools.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js new file mode 100644 index 0000000..e49ed18 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.1.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png new file mode 100644 index 0000000000000000000000000000000000000000..a858a410e4faa62ce324d814e4b816fff83a6fb3 GIT binary patch literal 286 zcmV+(0pb3MP)s`hMrGg#P~ix$^RISR_I47Y|r1 z_CyJOe}D1){SET-^Amu_i71Lt6eYfZjRyw@I6OQAIXXHDfiX^GbOlHe=Ae4>0m)d(f|Me07*qoM6N<$f}vM^LjV8( literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png new file mode 100644 index 0000000000000000000000000000000000000000..146ef8a800602169cf78c686fc5a6d138a76bc0a GIT binary patch literal 7791 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 literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png new file mode 100644 index 0000000000000000000000000000000000000000..7107cec93a979b9a5f64843235a16651d563ce2d GIT binary patch literal 90 zcmeAS@N?(olHy`uVBq!ia0vp^+#t*WBp7;*Yy1LIik>cxAr*|t7R?Mi>2?kWtu>-2 m3q%Vub%g%s<8sJhVPMczOq}xhg9DJoz~JfX=d#Wzp$Pyb1r*Kz literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css new file mode 100644 index 0000000..9abe04b --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css @@ -0,0 +1,83 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #004461; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #582800 } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #745334 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #990000 } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #004461 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #888888 } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ +.highlight .mb { color: #990000 } /* Literal.Number.Bin */ +.highlight .mf { color: #990000 } /* Literal.Number.Float */ +.highlight .mh { color: #990000 } /* Literal.Number.Hex */ +.highlight .mi { color: #990000 } /* Literal.Number.Integer */ +.highlight .mo { color: #990000 } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js new file mode 120000 index 0000000..2d33672 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/searchtools.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js new file mode 120000 index 0000000..75db705 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/sphinx_highlight.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js new file mode 120000 index 0000000..94df107 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/underscore.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html new file mode 100644 index 0000000..9434347 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html @@ -0,0 +1,614 @@ + + + + + + + + Index — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | G + | I + | L + | M + | N + | P + | R + | S + | T + | U + | V + +
+

_

+ + +
+ +

A

+ + + +
+ +

B

+ + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

G

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + +
+ +

V

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html new file mode 100644 index 0000000..dfb6fab --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html @@ -0,0 +1,122 @@ + + + + + + + + + Compute — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Compute

+

Compute instances management library.

+
+ +
+
+

Indices and tables

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv new file mode 100644 index 0000000000000000000000000000000000000000..353c123751df5365109b8d8b5dcc61c9dc93921b GIT binary patch literal 1463 zcmV;o1xWfMAX9K?X>NERX>N99Zgg*Qc_4OWa&u{KZXhxWBOp+6Z)#;@bUGkIZ*6dO zbY%)7AXa5^b7^mGIv@%oAXI2&AaZ4GVQFq;WpW^IW*~HEX>%ZEX>4U6X>%ZBZ*6dL zWpi_7WFU2OX>MmAdTeQ8E(&-cWzZqAusEj<4})?5G% zs&^_d7glR`s!p(QTdoW-?Sz>Cd$5);9*A9Bb&jR)8B9($?*nAIQumBg!Pga(_3ya| zVH2}Qqb1vbnT;`P1vHczOio))p%3|OS>+w z(SR>bn}K*^9x$UtAz8Bz4=hu2`TZ9W_;}WC&*sp3HZ|^_yueux)rx)PdK20-j}b3I zUE-y#hXzk5uSEXZYjD7FiPELJUXzK54W{TSFd`ay7*+G`IV1xIMJd}RrjGy9`G&KF z*wTXM9pl6+&I@4nC%o>AUrf(wVFiMV9NtrH^3QgWfFiy5lyp*mHZOXR1B!tVxNFwTDkz`yCv8_L?5PlkRx#!@zpkgNDC6 zE|TyQuQ5X4Az0eWNRI9p0=?;%drFs=1ggW|Tn&q|Jijd1mSD3ZEVGUlMdm3s~>_35NHv5xB<=3Ck%6AjI&)4 + + + + + + Python Module Index — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ c +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ c
+ compute +
    + compute.exceptions +
    + compute.instance.guest_agent +
    + compute.instance.instance +
    + compute.instance.schemas +
    + compute.session +
    + compute.storage.pool +
    + compute.storage.volume +
    + compute.utils.ids +
    + compute.utils.units +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html new file mode 100644 index 0000000..1868528 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html @@ -0,0 +1,183 @@ + + + + + + + + + exceptions — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

exceptions

+

Exceptions.

+
+
+exception compute.exceptions.ComputeError
+

Basic exception class.

+
+ +
+
+exception compute.exceptions.ConfigLoaderError
+

Something went wrong when loading configuration.

+
+ +
+
+exception compute.exceptions.GuestAgentCommandNotSupportedError
+

Guest agent command is not supported or blacklisted on guest.

+
+ +
+
+exception compute.exceptions.GuestAgentError
+

Something went wring when QEMU Guest Agent call.

+
+ +
+
+exception compute.exceptions.GuestAgentTimeoutExceededError(msg: int)
+

QEMU timeout exceeded.

+
+ +
+
+exception compute.exceptions.GuestAgentUnavailableError
+

Guest agent is not connected or is unavailable.

+
+ +
+
+exception compute.exceptions.InstanceError
+

Something went wrong while interacting with the domain.

+
+ +
+
+exception compute.exceptions.InstanceNotFoundError(msg: str)
+

Virtual machine or container not found on compute node.

+
+ +
+
+exception compute.exceptions.SessionError
+

Something went wrong while connecting to libvirtd.

+
+ +
+
+exception compute.exceptions.StoragePoolError
+

Something went wrong when operating with storage pool.

+
+ +
+
+exception compute.exceptions.StoragePoolNotFoundError(msg: str)
+

Storage pool not found.

+
+ +
+
+exception compute.exceptions.VolumeNotFoundError(msg: str)
+

Storage volume not found.

+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html new file mode 100644 index 0000000..7162de0 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html @@ -0,0 +1,342 @@ + + + + + + + + + Python API — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Python API

+

The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the myinstance instance.

+
import logging
+
+from compute import Session
+
+logging.basicConfig(level=logging.DEBUG)
+
+with Session() as session:
+    instance = session.get_instance('myinstance')
+    instance.set_vcpus(4)
+    instance.start()
+    instance.set_autostart(enabled=True)
+
+
+

Session context manager provides an abstraction over libvirt.virConnect +and returns objects of other classes of the present library.

+
+

Entity representation

+

Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +Volume.

+

The configuration files of various libvirt objects in compute are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the to_xml() method. For example VolumeConfig.

+

Pydantic models are used to validate input data. +For example VolumeSchema.

+
+
+

Modules documentation

+
+ +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html new file mode 100644 index 0000000..a8cca32 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html @@ -0,0 +1,266 @@ + + + + + + + + + guest_agent — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

guest_agent

+

Interacting with the QEMU Guest Agent.

+
+
+class compute.instance.guest_agent.GuestAgent(domain: virDomain, timeout: int = 60)
+

Class for interacting with QEMU guest agent.

+
+
+__init__(domain: virDomain, timeout: int = 60)
+

Initialise GuestAgent.

+
+
Parameters:
+
    +
  • domain – Libvirt domain object

  • +
  • timeout – QEMU timeout

  • +
+
+
+
+ +
+
+execute(command: dict) dict
+

Execute QEMU guest agent command.

+

See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html

+
+
Parameters:
+

command – QEMU guest agent command as dict

+
+
Returns:
+

Command output

+
+
Return type:
+

dict

+
+
+
+ +
+
+get_supported_commands() set[str]
+

Return set of supported guest agent commands.

+
+ +
+
+guest_exec(path: str, args: list[str] | None = None, env: list[str] | None = None, stdin: str | None = None, *, capture_output: bool = False, decode_output: bool = False, poll: bool = False) GuestExecOutput
+

Execute qemu-exec command and return output.

+
+
Parameters:
+
    +
  • path – Path ot executable on guest.

  • +
  • arg – List of arguments to pass to executable.

  • +
  • env – List of environment variables to pass to executable. +For example: ['LANG=C', 'TERM=xterm']

  • +
  • stdin – Data to pass to executable STDIN.

  • +
  • capture_output – Capture command output.

  • +
  • decode_output – Use base64_decode() to decode command output. +Affects only if capture_output is True.

  • +
  • poll – Poll command output. Uses self.timeout and +POLL_INTERVAL constant.

  • +
+
+
Returns:
+

Command output

+
+
Return type:
+

GuestExecOutput

+
+
+
+ +
+
+guest_exec_status(pid: int, *, poll: bool = False, poll_interval: float = 0.3) dict
+

Execute guest-exec-status and return output.

+
+
Parameters:
+
    +
  • pid – PID in guest.

  • +
  • poll – If True poll command status.

  • +
  • poll_interval – Time between attempts to obtain command status.

  • +
+
+
Returns:
+

Command output

+
+
Return type:
+

dict

+
+
+
+ +
+
+is_available() bool
+

Execute guest-ping.

+
+
Returns:
+

True or False if guest agent is unreachable.

+
+
Return type:
+

bool

+
+
+
+ +
+
+raise_for_commands(commands: list[str]) None
+

Raise exception if QEMU GA command is not available.

+
+
Parameters:
+

commands – List of required commands

+
+
Raise:
+

GuestAgentCommandNotSupportedError

+
+
+
+ +
+ +
+
+class compute.instance.guest_agent.GuestExecOutput(exited: bool | None = None, exitcode: int | None = None, stdout: str | None = None, stderr: str | None = None)
+

QEMU guest-exec command output.

+
+
+exitcode: int | None
+

Alias for field number 1

+
+ +
+
+exited: bool | None
+

Alias for field number 0

+
+ +
+
+stderr: str | None
+

Alias for field number 3

+
+ +
+
+stdout: str | None
+

Alias for field number 2

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html new file mode 100644 index 0000000..87e074f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html @@ -0,0 +1,120 @@ + + + + + + + + + instance — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

instance

+
+

Contents:

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html new file mode 100644 index 0000000..9f34c04 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html @@ -0,0 +1,490 @@ + + + + + + + + + instance — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

instance

+

Manage compute instances.

+
+
+class compute.instance.instance.Instance(domain: virDomain)
+

Manage compute instances.

+
+
+__init__(domain: virDomain)
+

Initialise Instance.

+
+
Variables:
+
    +
  • domain (libvirt.virDomain) – domain object

  • +
  • connection (libvirt.virConnect) – connection object

  • +
  • name (str) – domain name

  • +
  • guest_agent (GuestAgent) – GuestAgent object

  • +
+
+
Parameters:
+

domain – libvirt domain object

+
+
+
+ +
+
+attach_device(device: EntityConfig, *, live: bool = False) None
+

Attach device to compute instance.

+
+
Parameters:
+
    +
  • device – Object with device description e.g. DiskConfig

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+delete() None
+

Undefine instance.

+
+ +
+
+delete_ssh_keys(user: str, ssh_keys: list[str]) None
+

Remove SSH keys from guest for specific user.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • ssh_keys – List of public SSH keys.

  • +
+
+
+
+ +
+
+detach_device(device: EntityConfig, *, live: bool = False) None
+

Dettach device from compute instance.

+
+
Parameters:
+
    +
  • device – Object with device description e.g. DiskConfig

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+detach_disk(name: str) None
+

Detach disk device by target name.

+

There is no attach_disk() method. Use attach_device() +with DiskConfig as argument.

+
+
Parameters:
+

name – Disk name e.g. ‘vda’, ‘sda’, etc. This name may +not match the name of the disk inside the guest OS.

+
+
+
+ +
+
+dump_xml(*, inactive: bool = False) str
+

Return instance XML description.

+
+ +
+
+get_disks() list[compute.storage.volume.DiskConfig]
+

Return list of attached disks.

+
+ +
+
+get_info() InstanceInfo
+

Return instance info.

+
+ +
+
+get_max_memory() int
+

Maximum memory value for domain in KiB.

+
+ +
+
+get_max_vcpus() int
+

Maximum vCPUs number for domain.

+
+ +
+
+get_ssh_keys(user: str) list[str]
+

Return list of SSH keys on guest for specific user.

+
+
Parameters:
+

user – Username.

+
+
+
+ +
+
+get_status() str
+

Return instance state: ‘running’, ‘shutoff’, etc.

+

Reference: +https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState

+
+ +
+
+is_autostart() bool
+

Return True if instance autostart is enabled, else return False.

+
+ +
+
+is_running() bool
+

Return True if instance is running, else return False.

+
+ +
+
+pause() None
+

Pause instance.

+
+ +
+
+power_reset() None
+

Shutdown instance and start.

+

By analogy with real hardware, this is a normal server shutdown, +and then turning off from the power supply and turning it on again.

+

This method is applicable in cases where there has been a +configuration change in libvirt and you need to restart the +instance to apply the new configuration.

+
+ +
+
+reboot() None
+

Send ACPI signal to guest OS to reboot. OS may ignore this.

+
+ +
+
+reset() None
+

Reset instance.

+

Copypaste from libvirt doc:

+

Reset a domain immediately without any guest OS shutdown. +Reset emulates the power reset button on a machine, where all +hardware sees the RST line set and reinitializes internal state.

+

Note that there is a risk of data loss caused by reset without any +guest OS shutdown.

+
+ +
+
+resize_disk(name: str, capacity: int, unit: DataUnit) None
+

Resize attached block device.

+
+
Parameters:
+
    +
  • name – Disk device name e.g. vda, sda, etc.

  • +
  • capacity – New capacity.

  • +
  • unit – Capacity unit.

  • +
+
+
+
+ +
+
+resume() None
+

Resume paused instance.

+
+ +
+
+set_autostart(*, enabled: bool) None
+

Set autostart flag for instance.

+
+
Parameters:
+

enabled – Bool argument to set or unset autostart flag.

+
+
+
+ +
+
+set_memory(memory: int, *, live: bool = False) None
+

Set memory.

+

If live is True and instance is not currently running set memory +in config and will applied when instance boot.

+
+
Parameters:
+
    +
  • memory – Memory value in mebibytes

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+set_ssh_keys(user: str, ssh_keys: list[str]) None
+

Add SSH keys to guest for specific user.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • ssh_keys – List of public SSH keys.

  • +
+
+
+
+ +
+
+set_user_password(user: str, password: str, *, encrypted: bool = False) None
+

Set new user password in guest OS.

+

This action performs by guest agent inside the guest.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • password – Password.

  • +
  • encrypted – Set it to True if password is already encrypted. +Right encryption method depends on guest OS.

  • +
+
+
+
+ +
+
+set_vcpus(nvcpus: int, *, live: bool = False) None
+

Set vCPU number.

+

If live is True and instance is not currently running vCPUs +will set in config and will applied when instance boot.

+

NB: Note that if this call is executed before the guest has +finished booting, the guest may fail to process the change.

+
+
Parameters:
+
    +
  • nvcpus – Number of vCPUs

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+shutdown(method: str | None = None) None
+

Shutdown instance.

+

Shutdown methods:

+
+
SOFT

Use guest agent to shutdown. If guest agent is unavailable +NORMAL method will be used.

+
+
NORMAL

Use method choosen by hypervisor to shutdown. Usually send ACPI +signal to guest OS. OS may ignore ACPI e.g. if guest is hanged.

+
+
HARD

Shutdown instance without any guest OS shutdown. This is simular +to unplugging machine from power. Internally send SIGTERM to +instance process and destroy it gracefully.

+
+
UNSAFE

Force shutdown. Internally send SIGKILL to instance process. +There is high data corruption risk!

+
+
+

If method is None NORMAL method will used.

+
+
Parameters:
+

method – Method used to shutdown instance

+
+
+
+ +
+
+start() None
+

Start defined instance.

+
+ +
+ +
+
+class compute.instance.instance.InstanceConfig(schema: InstanceSchema)
+

Compute instance XML config builder.

+
+
+__init__(schema: InstanceSchema)
+

Initialise InstanceConfig.

+
+
Parameters:
+

schema – InstanceSchema object

+
+
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+
+class compute.instance.instance.InstanceInfo(state: str, max_memory: int, memory: int, nproc: int, cputime: int)
+

Store compute instance info.

+

Reference: +https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo

+
+
+cputime: int
+

Alias for field number 4

+
+ +
+
+max_memory: int
+

Alias for field number 1

+
+ +
+
+memory: int
+

Alias for field number 2

+
+ +
+
+nproc: int
+

Alias for field number 3

+
+ +
+
+state: str
+

Alias for field number 0

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html new file mode 100644 index 0000000..ef15eb2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html @@ -0,0 +1,187 @@ + + + + + + + + + schemas — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

schemas

+

Compute instance related objects schemas.

+
+
+class compute.instance.schemas.BootOptionsSchema(*, order: tuple)
+

Instance boot settings.

+
+ +
+
+class compute.instance.schemas.CPUEmulationMode(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

CPU emulation mode enumerated.

+
+ +
+
+class compute.instance.schemas.CPUFeaturesSchema(*, require: list[str], disable: list[str])
+

CPU features model.

+
+ +
+
+class compute.instance.schemas.CPUSchema(*, emulation_mode: CPUEmulationMode, model: str | None = None, vendor: str | None = None, topology: compute.instance.schemas.CPUTopologySchema | None = None, features: compute.instance.schemas.CPUFeaturesSchema | None = None)
+

CPU model.

+
+ +
+
+class compute.instance.schemas.CPUTopologySchema(*, sockets: int, cores: int, threads: int, dies: int = 1)
+

CPU topology model.

+
+ +
+
+class compute.instance.schemas.EntityModel
+

Basic entity model.

+
+
+class Config
+

Do not allow extra fields.

+
+ +
+ +
+
+class compute.instance.schemas.InstanceSchema(*, name: str, title: str | None = None, description: str | None = None, memory: int, max_memory: int, vcpus: int, max_vcpus: int, cpu: CPUSchema, machine: str, emulator: Path, arch: str, boot: BootOptionsSchema, volumes: list[compute.instance.schemas.VolumeSchema], network_interfaces: list[compute.instance.schemas.NetworkInterfaceSchema], image: str | None = None)
+

Compute instance model.

+
+ +
+
+class compute.instance.schemas.NetworkInterfaceSchema(*, source: str, mac: str)
+

Network inerface model.

+
+ +
+
+class compute.instance.schemas.VolumeCapacitySchema(*, value: int, unit: DataUnit)
+

Storage volume capacity field model.

+
+ +
+
+class compute.instance.schemas.VolumeSchema(*, type: VolumeType, target: str, capacity: VolumeCapacitySchema, source: str | None = None, is_readonly: bool = False, is_system: bool = False)
+

Storage volume model.

+
+ +
+
+class compute.instance.schemas.VolumeType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

Storage volume types enumeration.

+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html new file mode 100644 index 0000000..6babb3e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html @@ -0,0 +1,331 @@ + + + + + + + + + session — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

session

+

Hypervisor session manager.

+
+
+class compute.session.Capabilities(arch: str, virt_type: str, emulator: str, machine: str, max_vcpus: int, cpu_vendor: str, cpu_model: str, cpu_features: dict, usable_cpus: list[dict])
+

Store domain capabilities info.

+
+
+arch: str
+

Alias for field number 0

+
+ +
+
+cpu_features: dict
+

Alias for field number 7

+
+ +
+
+cpu_model: str
+

Alias for field number 6

+
+ +
+
+cpu_vendor: str
+

Alias for field number 5

+
+ +
+
+emulator: str
+

Alias for field number 2

+
+ +
+
+machine: str
+

Alias for field number 3

+
+ +
+
+max_vcpus: int
+

Alias for field number 4

+
+ +
+
+usable_cpus: list[dict]
+

Alias for field number 8

+
+ +
+
+virt_type: str
+

Alias for field number 1

+
+ +
+ +
+
+class compute.session.NodeInfo(arch: str, memory: int, cpus: int, mhz: int, nodes: int, sockets: int, cores: int, threads: int)
+

Store compute node info.

+

See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo +NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB.

+
+
+arch: str
+

Alias for field number 0

+
+ +
+
+cores: int
+

Alias for field number 6

+
+ +
+
+cpus: int
+

Alias for field number 2

+
+ +
+
+memory: int
+

Alias for field number 1

+
+ +
+
+mhz: int
+

Alias for field number 3

+
+ +
+
+nodes: int
+

Alias for field number 4

+
+ +
+
+sockets: int
+

Alias for field number 5

+
+ +
+
+threads: int
+

Alias for field number 7

+
+ +
+ +
+
+class compute.session.Session(uri: str | None = None)
+

Hypervisor session context manager.

+
+
Variables:
+
    +
  • IMAGES_POOL – images storage pool name taken from env

  • +
  • VOLUMES_POOL – volumes storage pool name taken from env

  • +
+
+
+
+
+__init__(uri: str | None = None)
+

Initialise session with hypervisor.

+
+
Variables:
+
    +
  • uri (str) – libvirt connection URI.

  • +
  • connection (libvirt.virConnect) – libvirt connection object.

  • +
+
+
Parameters:
+

uri – libvirt connection URI.

+
+
+
+ +
+
+close() None
+

Close connection to libvirt daemon.

+
+ +
+
+create_instance(**kwargs: Any) Instance
+

Create and return new compute instance.

+
+
Parameters:
+
    +
  • name (str) – Instance name.

  • +
  • title (str) – Instance title for humans.

  • +
  • description (str) – Some information about instance.

  • +
  • memory (int) – Memory in MiB.

  • +
  • max_memory (int) – Maximum memory in MiB.

  • +
  • vcpus (int) – Number of vCPUs.

  • +
  • max_vcpus (int) – Maximum vCPUs.

  • +
  • cpu (dict) – CPU configuration. See CPUSchema for info.

  • +
  • machine (str) – QEMU emulated machine.

  • +
  • emulator (str) – Path to emulator.

  • +
  • arch (str) – CPU architecture to virtualization.

  • +
  • boot (dict) – Boot settings. See BootOptionsSchema.

  • +
  • image (str) – Source disk image name for system disk.

  • +
  • volumes (list[dict]) – List of storage volume configs. For more info +see VolumeSchema.

  • +
  • network_interfaces (list[dict]) – List of virtual network interfaces +configs. See NetworkInterfaceSchema for more info.

  • +
+
+
+
+ +
+
+get_capabilities() Capabilities
+

Return capabilities e.g. arch, virt, emulator, etc.

+
+ +
+
+get_instance(name: str) Instance
+

Get compute instance by name.

+
+ +
+
+get_node_info() NodeInfo
+

Return information about compute node.

+
+ +
+
+get_storage_pool(name: str) StoragePool
+

Get storage pool by name.

+
+ +
+
+list_instances() list[compute.instance.instance.Instance]
+

List all instances.

+
+ +
+
+list_storage_pools() list[compute.storage.pool.StoragePool]
+

List all strage pools.

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html new file mode 100644 index 0000000..0a6ca1b --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html @@ -0,0 +1,119 @@ + + + + + + + + + storage — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

storage

+
+

Contents:

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html new file mode 100644 index 0000000..43fa341 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html @@ -0,0 +1,201 @@ + + + + + + + + + pool — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

pool

+

Manage storage pools.

+
+
+class compute.storage.pool.StoragePool(pool: virStoragePool)
+

Storage pool manipulating class.

+
+
+__init__(pool: virStoragePool)
+

Initislise StoragePool.

+
+ +
+
+clone_volume(src: Volume, dst: VolumeConfig) Volume
+

Make storage volume copy.

+
+
Parameters:
+
    +
  • src – Input volume

  • +
  • dst – Output volume config

  • +
+
+
+
+ +
+
+create_volume(vol_conf: VolumeConfig) Volume
+

Create storage volume and return Volume instance.

+
+ +
+
+dump_xml() str
+

Return storage pool XML description as string.

+
+ +
+
+get_usage_info() StoragePoolUsageInfo
+

Return info about storage pool usage.

+
+ +
+
+get_volume(name: str) compute.storage.volume.Volume | None
+

Lookup and return Volume instance or None.

+
+ +
+
+list_volumes() list[compute.storage.volume.Volume]
+

Return list of volumes in storage pool.

+
+ +
+
+refresh() None
+

Refresh storage pool.

+
+ +
+ +
+
+class compute.storage.pool.StoragePoolUsageInfo(capacity: int, allocation: int, available: int)
+

Storage pool usage info.

+
+
+allocation: int
+

Alias for field number 1

+
+ +
+
+available: int
+

Alias for field number 2

+
+ +
+
+capacity: int
+

Alias for field number 0

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html new file mode 100644 index 0000000..9e35b8f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html @@ -0,0 +1,210 @@ + + + + + + + + + volume — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

volume

+

Manage storage volumes.

+
+
+class compute.storage.volume.DiskConfig(disk_type: str, source: str | pathlib.Path, target: str, readonly: bool = False)
+

Disk XML config builder.

+

Generate XML config for attaching or detaching storage volumes +to compute instances.

+
+
+__init__(disk_type: str, source: str | pathlib.Path, target: str, readonly: bool = False) None
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+
+class compute.storage.volume.Volume(pool: virStoragePool, vol: virStorageVol)
+

Storage volume manipulating class.

+
+
+__init__(pool: virStoragePool, vol: virStorageVol)
+

Initialise Volume.

+
+
Parameters:
+
    +
  • pool – libvirt virStoragePool object

  • +
  • vol – libvirt virStorageVol object

  • +
+
+
+
+ +
+
+clone(vol_conf: VolumeConfig) None
+

Make a copy of volume to the same storage pool.

+
+
Parameters:
+

VolumeInfo (vol_info) – New storage volume dataclass object

+
+
+
+ +
+
+delete() None
+

Delete volume from storage pool.

+
+ +
+
+dump_xml() str
+

Return volume XML description as string.

+
+ +
+
+resize(capacity: int, unit: DataUnit) None
+

Resize volume.

+
+
Parameters:
+
    +
  • int (capacity) – Volume new capacity.

  • +
  • DataUnit (unit) – Data unit. Internally converts into bytes.

  • +
+
+
+
+ +
+ +
+
+class compute.storage.volume.VolumeConfig(name: str, path: str, capacity: int)
+

Storage volume XML config builder.

+

Generate XML config for creating a volume in a libvirt +storage pool.

+
+
+__init__(name: str, path: str, capacity: int) None
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html new file mode 100644 index 0000000..b09e008 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html @@ -0,0 +1,144 @@ + + + + + + + + + utils — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

utils

+
+

utils.units

+

Tools for data units convertion.

+
+
+class compute.utils.units.DataUnit(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

Data units enumerated.

+
+ +
+
+exception compute.utils.units.InvalidDataUnitError(msg: str)
+

Data unit is not valid.

+
+ +
+
+compute.utils.units.to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) int
+

Convert value to bytes. See DataUnit.

+
+ +
+
+

utils.ids

+

Random identificators.

+
+
+compute.utils.ids.random_mac() str
+

Retrun random MAC address.

+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html new file mode 100644 index 0000000..60189cf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html @@ -0,0 +1,124 @@ + + + + + + + + Search — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + + +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js new file mode 100644 index 0000000..d20a6ca --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"docnames": ["index", "pyapi/exceptions", "pyapi/index", "pyapi/instance/guest_agent", "pyapi/instance/index", "pyapi/instance/instance", "pyapi/instance/schemas", "pyapi/session", "pyapi/storage/index", "pyapi/storage/pool", "pyapi/storage/volume", "pyapi/utils"], "filenames": ["index.rst", "pyapi/exceptions.rst", "pyapi/index.rst", "pyapi/instance/guest_agent.rst", "pyapi/instance/index.rst", "pyapi/instance/instance.rst", "pyapi/instance/schemas.rst", "pyapi/session.rst", "pyapi/storage/index.rst", "pyapi/storage/pool.rst", "pyapi/storage/volume.rst", "pyapi/utils.rst"], "titles": ["Compute", "exceptions", "Python API", "guest_agent", "instance", "instance", "schemas", "session", "storage", "pool", "volume", "utils"], "terms": {"instanc": [0, 2, 3, 6, 7, 9, 10], "manag": [0, 2, 5, 7, 9, 10], "librari": [0, 2], "python": 0, "api": 0, "index": 0, "modul": [0, 6, 11], "search": 0, "page": 0, "comput": [1, 2, 3, 5, 6, 7, 9, 10, 11], "computeerror": [1, 2], "basic": [1, 6], "class": [1, 2, 3, 5, 6, 7, 9, 10, 11], "configloadererror": [1, 2], "someth": 1, "went": 1, "wrong": [1, 7], "when": [1, 5], "load": 1, "configur": [1, 2, 5, 7], "guestagentcommandnotsupportederror": [1, 2, 3], "guest": [1, 3, 5], "agent": [1, 3, 5], "command": [1, 3], "i": [1, 2, 3, 5, 7, 11], "support": [1, 3], "blacklist": 1, "guestagenterror": [1, 2], "wring": 1, "qemu": [1, 3, 7], "call": [1, 2, 5], "guestagenttimeoutexceedederror": [1, 2], "msg": [1, 11], "int": [1, 3, 5, 6, 7, 9, 10, 11], "timeout": [1, 3], "exceed": 1, "guestagentunavailableerror": [1, 2], "connect": [1, 5, 7], "unavail": [1, 5], "instanceerror": [1, 2], "while": 1, "interact": [1, 3], "domain": [1, 3, 5, 7], "instancenotfounderror": [1, 2], "str": [1, 3, 5, 6, 7, 9, 10, 11], "virtual": [1, 7], "machin": [1, 2, 5, 6, 7], "contain": 1, "found": 1, "node": [1, 2, 7], "sessionerror": [1, 2], "libvirtd": 1, "storagepoolerror": [1, 2], "oper": [1, 2], "storag": [1, 2, 5, 6, 7, 9, 10], "pool": [1, 2, 7, 8, 10], "storagepoolnotfounderror": [1, 2], "volumenotfounderror": [1, 2], "volum": [1, 2, 5, 6, 7, 8, 9], "The": 2, "allow": [2, 6], "you": [2, 5], "perform": [2, 5], "action": [2, 5], "programmat": 2, "below": 2, "an": 2, "exampl": [2, 3], "chang": [2, 5], "paramet": [2, 3, 5, 7, 9, 10], "launch": 2, "myinstanc": 2, "import": 2, "log": 2, "from": [2, 5, 7, 10], "session": 2, "basicconfig": 2, "level": 2, "debug": 2, "get_inst": [2, 7], "set_vcpu": [2, 5], "4": [2, 5, 7], "start": [2, 5, 6, 11], "set_autostart": [2, 5], "enabl": [2, 5], "true": [2, 3, 5], "context": [2, 7], "provid": 2, "abstract": 2, "over": 2, "libvirt": [2, 3, 5, 7, 10], "virconnect": [2, 5, 7], "return": [2, 3, 5, 7, 9, 10], "object": [2, 3, 5, 6, 7, 10], "other": 2, "present": 2, "ar": 2, "repres": 2, "These": 2, "directli": 2, "method": [2, 5], "hypervisor": [2, 5, 7], "file": 2, "variou": 2, "describ": 2, "special": 2, "dataclass": [2, 10], "store": [2, 5, 7], "its": 2, "properti": 2, "can": 2, "xml": [2, 5, 9, 10], "config": [2, 5, 6, 7, 9, 10], "us": [2, 3, 5], "to_xml": [2, 5, 10], "For": [2, 3, 7], "volumeconfig": [2, 9, 10], "pydant": 2, "model": [2, 6], "valid": [2, 11], "input": [2, 9], "data": [2, 3, 5, 10, 11], "volumeschema": [2, 6, 7], "capabl": [2, 7], "arch": [2, 6, 7], "cpu_featur": [2, 7], "cpu_model": [2, 7], "cpu_vendor": [2, 7], "emul": [2, 5, 6, 7], "max_vcpu": [2, 6, 7], "usable_cpu": [2, 7], "virt_typ": [2, 7], "nodeinfo": [2, 7], "core": [2, 6, 7], "cpu": [2, 6, 7], "memori": [2, 5, 6, 7], "mhz": [2, 7], "socket": [2, 6, 7], "thread": [2, 6, 7], "__init__": [2, 3, 5, 7, 9, 10], "close": [2, 7], "create_inst": [2, 7], "get_cap": [2, 7], "get_node_info": [2, 7], "get_storage_pool": [2, 7], "list_inst": [2, 7], "list_storage_pool": [2, 7], "attach_devic": [2, 5], "delet": [2, 5, 10], "delete_ssh_kei": [2, 5], "detach_devic": [2, 5], "detach_disk": [2, 5], "dump_xml": [2, 5, 9, 10], "get_disk": [2, 5], "get_info": [2, 5], "get_max_memori": [2, 5], "get_max_vcpu": [2, 5], "get_ssh_kei": [2, 5], "get_statu": [2, 5], "is_autostart": [2, 5], "is_run": [2, 5], "paus": [2, 5], "power_reset": [2, 5], "reboot": [2, 5], "reset": [2, 5], "resize_disk": [2, 5], "resum": [2, 5], "set_memori": [2, 5], "set_ssh_kei": [2, 5], "set_user_password": [2, 5], "shutdown": [2, 5], "instanceconfig": [2, 5], "instanceinfo": [2, 5], "cputim": [2, 5], "max_memori": [2, 5, 6, 7], "nproc": [2, 5], "state": [2, 5], "guest_ag": [2, 4, 5], "guestag": [2, 3, 5], "execut": [2, 3, 5], "get_supported_command": [2, 3], "guest_exec": [2, 3], "guest_exec_statu": [2, 3], "is_avail": [2, 3], "raise_for_command": [2, 3], "guestexecoutput": [2, 3], "exitcod": [2, 3], "exit": [2, 3], "stderr": [2, 3], "stdout": [2, 3], "schema": [2, 4, 5], "bootoptionsschema": [2, 6, 7], "cpuemulationmod": [2, 6], "cpufeaturesschema": [2, 6], "cpuschema": [2, 6, 7], "cputopologyschema": [2, 6], "entitymodel": [2, 6], "instanceschema": [2, 5, 6], "networkinterfaceschema": [2, 6, 7], "volumecapacityschema": [2, 6], "volumetyp": [2, 6], "storagepool": [2, 7, 9], "clone_volum": [2, 9], "create_volum": [2, 9], "get_usage_info": [2, 9], "get_volum": [2, 9], "list_volum": [2, 9], "refresh": [2, 9], "storagepoolusageinfo": [2, 9], "alloc": [2, 9], "avail": [2, 3, 9], "capac": [2, 5, 6, 9, 10], "diskconfig": [2, 5, 10], "clone": [2, 10], "resiz": [2, 5, 10], "util": 2, "unit": [2, 5, 6, 7, 10], "dataunit": [2, 5, 6, 10, 11], "invaliddatauniterror": [2, 11], "to_byt": [2, 11], "id": 2, "random_mac": [2, 11], "except": [2, 3, 11], "virdomain": [3, 5], "60": 3, "initialis": [3, 5, 7, 10], "dict": [3, 7], "see": [3, 5, 7, 11], "http": [3, 5, 7], "project": 3, "gitlab": 3, "io": 3, "interop": 3, "ga": 3, "ref": 3, "html": [3, 5, 7], "output": [3, 9], "type": [3, 6, 11], "set": [3, 5, 6, 7], "path": [3, 6, 7, 10], "arg": 3, "list": [3, 5, 6, 7, 9], "none": [3, 5, 6, 7, 9, 10, 11], "env": [3, 7], "stdin": 3, "capture_output": 3, "bool": [3, 5, 6, 10], "fals": [3, 5, 6, 10], "decode_output": 3, "poll": 3, "exec": 3, "ot": 3, "argument": [3, 5], "pass": 3, "environ": 3, "variabl": [3, 5, 7], "lang": 3, "c": 3, "term": 3, "xterm": 3, "captur": 3, "base64_decod": 3, "decod": 3, "affect": [3, 5], "onli": 3, "self": 3, "poll_interv": 3, "constant": 3, "pid": 3, "float": 3, "0": [3, 5, 7, 9], "3": [3, 5, 7], "statu": 3, "If": [3, 5], "time": 3, "between": 3, "attempt": 3, "obtain": 3, "ping": 3, "unreach": 3, "rais": 3, "requir": [3, 6], "alia": [3, 5, 7, 9], "field": [3, 5, 6, 7, 9], "number": [3, 5, 7, 9], "1": [3, 5, 6, 7, 9, 11], "2": [3, 5, 7, 9], "name": [5, 6, 7, 9, 10, 11], "devic": 5, "entityconfig": 5, "live": 5, "attach": [5, 10], "descript": [5, 6, 7, 9, 10], "e": [5, 7], "g": [5, 7], "run": 5, "undefin": 5, "user": 5, "ssh_kei": 5, "remov": 5, "ssh": 5, "kei": 5, "specif": 5, "usernam": 5, "public": 5, "dettach": 5, "detach": [5, 10], "disk": [5, 7, 10], "target": [5, 6, 10], "There": 5, "attach_disk": 5, "vda": 5, "sda": 5, "etc": [5, 7], "thi": 5, "mai": 5, "match": 5, "insid": 5, "o": 5, "inact": 5, "info": [5, 7, 9], "maximum": [5, 7], "valu": [5, 6, 11], "kib": 5, "vcpu": [5, 6, 7], "shutoff": 5, "refer": 5, "org": [5, 7], "virdomainst": 5, "autostart": 5, "els": 5, "By": 5, "analogi": 5, "real": 5, "hardwar": 5, "normal": 5, "server": 5, "turn": 5, "off": 5, "power": 5, "suppli": 5, "again": 5, "applic": 5, "case": 5, "where": 5, "ha": 5, "been": 5, "need": 5, "restart": 5, "appli": 5, "new": [5, 7, 10], "send": 5, "acpi": 5, "signal": 5, "ignor": 5, "copypast": 5, "doc": [5, 7], "immedi": 5, "without": 5, "ani": [5, 7], "button": 5, "all": [5, 7], "rst": 5, "line": 5, "reiniti": 5, "intern": [5, 10], "note": [5, 7], "risk": 5, "loss": 5, "caus": 5, "block": 5, "flag": 5, "unset": 5, "current": 5, "boot": [5, 6, 7], "mebibyt": 5, "add": 5, "password": 5, "encrypt": 5, "alreadi": 5, "right": 5, "depend": 5, "nvcpu": 5, "nb": 5, "befor": 5, "finish": 5, "fail": 5, "process": 5, "soft": 5, "choosen": 5, "usual": 5, "hang": 5, "hard": 5, "simular": 5, "unplug": 5, "sigterm": 5, "destroi": 5, "gracefulli": 5, "unsaf": 5, "forc": 5, "sigkil": 5, "high": 5, "corrupt": 5, "defin": 5, "builder": [5, 10], "virdomaininfo": 5, "relat": 6, "order": 6, "tupl": 6, "qualnam": [6, 11], "boundari": [6, 11], "mode": 6, "enumer": [6, 11], "disabl": 6, "featur": 6, "emulation_mod": 6, "vendor": 6, "topologi": 6, "di": 6, "entiti": 6, "do": 6, "extra": 6, "titl": [6, 7], "network_interfac": [6, 7], "imag": [6, 7], "sourc": [6, 7, 10], "mac": [6, 11], "network": [6, 7], "inerfac": 6, "is_readonli": 6, "is_system": 6, "7": 7, "6": 7, "5": 7, "8": 7, "host": 7, "virnodeinfo": 7, "actual": 7, "mib": 7, "uri": 7, "images_pool": 7, "taken": 7, "volumes_pool": 7, "daemon": 7, "kwarg": 7, "creat": [7, 9, 10], "human": 7, "some": 7, "inform": 7, "about": [7, 9], "architectur": 7, "system": 7, "more": 7, "interfac": 7, "virt": 7, "get": 7, "strage": 7, "virstoragepool": [9, 10], "manipul": [9, 10], "initislis": 9, "src": 9, "dst": 9, "make": [9, 10], "copi": [9, 10], "vol_conf": [9, 10], "string": [9, 10], "usag": 9, "lookup": 9, "disk_typ": 10, "pathlib": 10, "readonli": 10, "gener": 10, "vol": 10, "virstoragevol": 10, "same": 10, "volumeinfo": 10, "vol_info": 10, "convert": [10, 11], "byte": [10, 11], "tool": 11, "random": 11, "identif": 11, "retrun": 11, "address": 11}, "objects": {"compute": [[1, 0, 0, "-", "exceptions"], [7, 0, 0, "-", "session"]], "compute.exceptions": [[1, 1, 1, "", "ComputeError"], [1, 1, 1, "", "ConfigLoaderError"], [1, 1, 1, "", "GuestAgentCommandNotSupportedError"], [1, 1, 1, "", "GuestAgentError"], [1, 1, 1, "", "GuestAgentTimeoutExceededError"], [1, 1, 1, "", "GuestAgentUnavailableError"], [1, 1, 1, "", "InstanceError"], [1, 1, 1, "", "InstanceNotFoundError"], [1, 1, 1, "", "SessionError"], [1, 1, 1, "", "StoragePoolError"], [1, 1, 1, "", "StoragePoolNotFoundError"], [1, 1, 1, "", "VolumeNotFoundError"]], "compute.instance": [[3, 0, 0, "-", "guest_agent"], [5, 0, 0, "-", "instance"], [6, 0, 0, "-", "schemas"]], "compute.instance.guest_agent": [[3, 2, 1, "", "GuestAgent"], [3, 2, 1, "", "GuestExecOutput"]], "compute.instance.guest_agent.GuestAgent": [[3, 3, 1, "", "__init__"], [3, 3, 1, "", "execute"], [3, 3, 1, "", "get_supported_commands"], [3, 3, 1, "", "guest_exec"], [3, 3, 1, "", "guest_exec_status"], [3, 3, 1, "", "is_available"], [3, 3, 1, "", "raise_for_commands"]], "compute.instance.guest_agent.GuestExecOutput": [[3, 4, 1, "", "exitcode"], [3, 4, 1, "", "exited"], [3, 4, 1, "", "stderr"], [3, 4, 1, "", "stdout"]], "compute.instance.instance": [[5, 2, 1, "", "Instance"], [5, 2, 1, "", "InstanceConfig"], [5, 2, 1, "", "InstanceInfo"]], "compute.instance.instance.Instance": [[5, 3, 1, "", "__init__"], [5, 3, 1, "", "attach_device"], [5, 3, 1, "", "delete"], [5, 3, 1, "", "delete_ssh_keys"], [5, 3, 1, "", "detach_device"], [5, 3, 1, "", "detach_disk"], [5, 3, 1, "", "dump_xml"], [5, 3, 1, "", "get_disks"], [5, 3, 1, "", "get_info"], [5, 3, 1, "", "get_max_memory"], [5, 3, 1, "", "get_max_vcpus"], [5, 3, 1, "", "get_ssh_keys"], [5, 3, 1, "", "get_status"], [5, 3, 1, "", "is_autostart"], [5, 3, 1, "", "is_running"], [5, 3, 1, "", "pause"], [5, 3, 1, "", "power_reset"], [5, 3, 1, "", "reboot"], [5, 3, 1, "", "reset"], [5, 3, 1, "", "resize_disk"], [5, 3, 1, "", "resume"], [5, 3, 1, "", "set_autostart"], [5, 3, 1, "", "set_memory"], [5, 3, 1, "", "set_ssh_keys"], [5, 3, 1, "", "set_user_password"], [5, 3, 1, "", "set_vcpus"], [5, 3, 1, "", "shutdown"], [5, 3, 1, "", "start"]], "compute.instance.instance.InstanceConfig": [[5, 3, 1, "", "__init__"], [5, 3, 1, "", "to_xml"]], "compute.instance.instance.InstanceInfo": [[5, 4, 1, "", "cputime"], [5, 4, 1, "", "max_memory"], [5, 4, 1, "", "memory"], [5, 4, 1, "", "nproc"], [5, 4, 1, "", "state"]], "compute.instance.schemas": [[6, 2, 1, "", "BootOptionsSchema"], [6, 2, 1, "", "CPUEmulationMode"], [6, 2, 1, "", "CPUFeaturesSchema"], [6, 2, 1, "", "CPUSchema"], [6, 2, 1, "", "CPUTopologySchema"], [6, 2, 1, "", "EntityModel"], [6, 2, 1, "", "InstanceSchema"], [6, 2, 1, "", "NetworkInterfaceSchema"], [6, 2, 1, "", "VolumeCapacitySchema"], [6, 2, 1, "", "VolumeSchema"], [6, 2, 1, "", "VolumeType"]], "compute.instance.schemas.EntityModel": [[6, 2, 1, "", "Config"]], "compute.session": [[7, 2, 1, "", "Capabilities"], [7, 2, 1, "", "NodeInfo"], [7, 2, 1, "", "Session"]], "compute.session.Capabilities": [[7, 4, 1, "", "arch"], [7, 4, 1, "", "cpu_features"], [7, 4, 1, "", "cpu_model"], [7, 4, 1, "", "cpu_vendor"], [7, 4, 1, "", "emulator"], [7, 4, 1, "", "machine"], [7, 4, 1, "", "max_vcpus"], [7, 4, 1, "", "usable_cpus"], [7, 4, 1, "", "virt_type"]], "compute.session.NodeInfo": [[7, 4, 1, "", "arch"], [7, 4, 1, "", "cores"], [7, 4, 1, "", "cpus"], [7, 4, 1, "", "memory"], [7, 4, 1, "", "mhz"], [7, 4, 1, "", "nodes"], [7, 4, 1, "", "sockets"], [7, 4, 1, "", "threads"]], "compute.session.Session": [[7, 3, 1, "", "__init__"], [7, 3, 1, "", "close"], [7, 3, 1, "", "create_instance"], [7, 3, 1, "", "get_capabilities"], [7, 3, 1, "", "get_instance"], [7, 3, 1, "", "get_node_info"], [7, 3, 1, "", "get_storage_pool"], [7, 3, 1, "", "list_instances"], [7, 3, 1, "", "list_storage_pools"]], "compute.storage": [[9, 0, 0, "-", "pool"], [10, 0, 0, "-", "volume"]], "compute.storage.pool": [[9, 2, 1, "", "StoragePool"], [9, 2, 1, "", "StoragePoolUsageInfo"]], "compute.storage.pool.StoragePool": [[9, 3, 1, "", "__init__"], [9, 3, 1, "", "clone_volume"], [9, 3, 1, "", "create_volume"], [9, 3, 1, "", "dump_xml"], [9, 3, 1, "", "get_usage_info"], [9, 3, 1, "", "get_volume"], [9, 3, 1, "", "list_volumes"], [9, 3, 1, "", "refresh"]], "compute.storage.pool.StoragePoolUsageInfo": [[9, 4, 1, "", "allocation"], [9, 4, 1, "", "available"], [9, 4, 1, "", "capacity"]], "compute.storage.volume": [[10, 2, 1, "", "DiskConfig"], [10, 2, 1, "", "Volume"], [10, 2, 1, "", "VolumeConfig"]], "compute.storage.volume.DiskConfig": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "to_xml"]], "compute.storage.volume.Volume": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "clone"], [10, 3, 1, "", "delete"], [10, 3, 1, "", "dump_xml"], [10, 3, 1, "", "resize"]], "compute.storage.volume.VolumeConfig": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "to_xml"]], "compute.utils": [[11, 0, 0, "-", "ids"], [11, 0, 0, "-", "units"]], "compute.utils.ids": [[11, 5, 1, "", "random_mac"]], "compute.utils.units": [[11, 2, 1, "", "DataUnit"], [11, 1, 1, "", "InvalidDataUnitError"], [11, 5, 1, "", "to_bytes"]]}, "objtypes": {"0": "py:module", "1": "py:exception", "2": "py:class", "3": "py:method", "4": "py:attribute", "5": "py:function"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "exception", "Python exception"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"], "4": ["py", "attribute", "Python attribute"], "5": ["py", "function", "Python function"]}, "titleterms": {"comput": 0, "indic": 0, "tabl": 0, "except": 1, "python": 2, "api": 2, "entiti": 2, "represent": 2, "modul": 2, "document": 2, "guest_ag": 3, "instanc": [4, 5], "content": [4, 8], "schema": 6, "session": 7, "storag": 8, "pool": 9, "volum": 10, "util": 11, "unit": 11, "id": 11}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Compute": [[0, "compute"]], "Indices and tables": [[0, "indices-and-tables"]], "exceptions": [[1, "module-compute.exceptions"]], "Python API": [[2, "python-api"]], "Entity representation": [[2, "entity-representation"]], "Modules documentation": [[2, "modules-documentation"]], "guest_agent": [[3, "module-compute.instance.guest_agent"]], "instance": [[4, "instance"], [5, "module-compute.instance.instance"]], "Contents:": [[4, null], [8, null]], "schemas": [[6, "module-compute.instance.schemas"]], "session": [[7, "module-compute.session"]], "storage": [[8, "storage"]], "pool": [[9, "module-compute.storage.pool"]], "volume": [[10, "module-compute.storage.volume"]], "utils": [[11, "utils"]], "utils.units": [[11, "module-compute.utils.units"]], "utils.ids": [[11, "module-compute.utils.ids"]]}, "indexentries": {"computeerror": [[1, "compute.exceptions.ComputeError"]], "configloadererror": [[1, "compute.exceptions.ConfigLoaderError"]], "guestagentcommandnotsupportederror": [[1, "compute.exceptions.GuestAgentCommandNotSupportedError"]], "guestagenterror": [[1, "compute.exceptions.GuestAgentError"]], "guestagenttimeoutexceedederror": [[1, "compute.exceptions.GuestAgentTimeoutExceededError"]], "guestagentunavailableerror": [[1, "compute.exceptions.GuestAgentUnavailableError"]], "instanceerror": [[1, "compute.exceptions.InstanceError"]], "instancenotfounderror": [[1, "compute.exceptions.InstanceNotFoundError"]], "sessionerror": [[1, "compute.exceptions.SessionError"]], "storagepoolerror": [[1, "compute.exceptions.StoragePoolError"]], "storagepoolnotfounderror": [[1, "compute.exceptions.StoragePoolNotFoundError"]], "volumenotfounderror": [[1, "compute.exceptions.VolumeNotFoundError"]], "compute.exceptions": [[1, "module-compute.exceptions"]], "module": [[1, "module-compute.exceptions"], [3, "module-compute.instance.guest_agent"], [5, "module-compute.instance.instance"], [6, "module-compute.instance.schemas"], [7, "module-compute.session"], [9, "module-compute.storage.pool"], [10, "module-compute.storage.volume"], [11, "module-compute.utils.ids"], [11, "module-compute.utils.units"]], "guestagent (class in compute.instance.guest_agent)": [[3, "compute.instance.guest_agent.GuestAgent"]], "guestexecoutput (class in compute.instance.guest_agent)": [[3, "compute.instance.guest_agent.GuestExecOutput"]], "__init__() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.__init__"]], "compute.instance.guest_agent": [[3, "module-compute.instance.guest_agent"]], "execute() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.execute"]], "exitcode (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.exitcode"]], "exited (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.exited"]], "get_supported_commands() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.get_supported_commands"]], "guest_exec() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.guest_exec"]], "guest_exec_status() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.guest_exec_status"]], "is_available() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.is_available"]], "raise_for_commands() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.raise_for_commands"]], "stderr (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.stderr"]], "stdout (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.stdout"]], "instance (class in compute.instance.instance)": [[5, "compute.instance.instance.Instance"]], "instanceconfig (class in compute.instance.instance)": [[5, "compute.instance.instance.InstanceConfig"]], "instanceinfo (class in compute.instance.instance)": [[5, "compute.instance.instance.InstanceInfo"]], "__init__() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.__init__"]], "__init__() (compute.instance.instance.instanceconfig method)": [[5, "compute.instance.instance.InstanceConfig.__init__"]], "attach_device() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.attach_device"]], "compute.instance.instance": [[5, "module-compute.instance.instance"]], "cputime (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.cputime"]], "delete() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.delete"]], "delete_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.delete_ssh_keys"]], "detach_device() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.detach_device"]], "detach_disk() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.detach_disk"]], "dump_xml() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.dump_xml"]], "get_disks() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_disks"]], "get_info() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_info"]], "get_max_memory() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_max_memory"]], "get_max_vcpus() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_max_vcpus"]], "get_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_ssh_keys"]], "get_status() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_status"]], "is_autostart() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.is_autostart"]], "is_running() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.is_running"]], "max_memory (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.max_memory"]], "memory (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.memory"]], "nproc (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.nproc"]], "pause() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.pause"]], "power_reset() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.power_reset"]], "reboot() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.reboot"]], "reset() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.reset"]], "resize_disk() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.resize_disk"]], "resume() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.resume"]], "set_autostart() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_autostart"]], "set_memory() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_memory"]], "set_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_ssh_keys"]], "set_user_password() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_user_password"]], "set_vcpus() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_vcpus"]], "shutdown() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.shutdown"]], "start() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.start"]], "state (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.state"]], "to_xml() (compute.instance.instance.instanceconfig method)": [[5, "compute.instance.instance.InstanceConfig.to_xml"]], "bootoptionsschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.BootOptionsSchema"]], "cpuemulationmode (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUEmulationMode"]], "cpufeaturesschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUFeaturesSchema"]], "cpuschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUSchema"]], "cputopologyschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUTopologySchema"]], "entitymodel (class in compute.instance.schemas)": [[6, "compute.instance.schemas.EntityModel"]], "entitymodel.config (class in compute.instance.schemas)": [[6, "compute.instance.schemas.EntityModel.Config"]], "instanceschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.InstanceSchema"]], "networkinterfaceschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.NetworkInterfaceSchema"]], "volumecapacityschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeCapacitySchema"]], "volumeschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeSchema"]], "volumetype (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeType"]], "compute.instance.schemas": [[6, "module-compute.instance.schemas"]], "capabilities (class in compute.session)": [[7, "compute.session.Capabilities"]], "nodeinfo (class in compute.session)": [[7, "compute.session.NodeInfo"]], "session (class in compute.session)": [[7, "compute.session.Session"]], "__init__() (compute.session.session method)": [[7, "compute.session.Session.__init__"]], "arch (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.arch"]], "arch (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.arch"]], "close() (compute.session.session method)": [[7, "compute.session.Session.close"]], "compute.session": [[7, "module-compute.session"]], "cores (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.cores"]], "cpu_features (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_features"]], "cpu_model (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_model"]], "cpu_vendor (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_vendor"]], "cpus (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.cpus"]], "create_instance() (compute.session.session method)": [[7, "compute.session.Session.create_instance"]], "emulator (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.emulator"]], "get_capabilities() (compute.session.session method)": [[7, "compute.session.Session.get_capabilities"]], "get_instance() (compute.session.session method)": [[7, "compute.session.Session.get_instance"]], "get_node_info() (compute.session.session method)": [[7, "compute.session.Session.get_node_info"]], "get_storage_pool() (compute.session.session method)": [[7, "compute.session.Session.get_storage_pool"]], "list_instances() (compute.session.session method)": [[7, "compute.session.Session.list_instances"]], "list_storage_pools() (compute.session.session method)": [[7, "compute.session.Session.list_storage_pools"]], "machine (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.machine"]], "max_vcpus (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.max_vcpus"]], "memory (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.memory"]], "mhz (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.mhz"]], "nodes (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.nodes"]], "sockets (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.sockets"]], "threads (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.threads"]], "usable_cpus (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.usable_cpus"]], "virt_type (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.virt_type"]], "storagepool (class in compute.storage.pool)": [[9, "compute.storage.pool.StoragePool"]], "storagepoolusageinfo (class in compute.storage.pool)": [[9, "compute.storage.pool.StoragePoolUsageInfo"]], "__init__() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.__init__"]], "allocation (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.allocation"]], "available (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.available"]], "capacity (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.capacity"]], "clone_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.clone_volume"]], "compute.storage.pool": [[9, "module-compute.storage.pool"]], "create_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.create_volume"]], "dump_xml() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.dump_xml"]], "get_usage_info() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.get_usage_info"]], "get_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.get_volume"]], "list_volumes() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.list_volumes"]], "refresh() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.refresh"]], "diskconfig (class in compute.storage.volume)": [[10, "compute.storage.volume.DiskConfig"]], "volume (class in compute.storage.volume)": [[10, "compute.storage.volume.Volume"]], "volumeconfig (class in compute.storage.volume)": [[10, "compute.storage.volume.VolumeConfig"]], "__init__() (compute.storage.volume.diskconfig method)": [[10, "compute.storage.volume.DiskConfig.__init__"]], "__init__() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.__init__"]], "__init__() (compute.storage.volume.volumeconfig method)": [[10, "compute.storage.volume.VolumeConfig.__init__"]], "clone() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.clone"]], "compute.storage.volume": [[10, "module-compute.storage.volume"]], "delete() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.delete"]], "dump_xml() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.dump_xml"]], "resize() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.resize"]], "to_xml() (compute.storage.volume.diskconfig method)": [[10, "compute.storage.volume.DiskConfig.to_xml"]], "to_xml() (compute.storage.volume.volumeconfig method)": [[10, "compute.storage.volume.VolumeConfig.to_xml"]], "dataunit (class in compute.utils.units)": [[11, "compute.utils.units.DataUnit"]], "invaliddatauniterror": [[11, "compute.utils.units.InvalidDataUnitError"]], "compute.utils.ids": [[11, "module-compute.utils.ids"]], "compute.utils.units": [[11, "module-compute.utils.units"]], "random_mac() (in module compute.utils.ids)": [[11, "compute.utils.ids.random_mac"]], "to_bytes() (in module compute.utils.units)": [[11, "compute.utils.units.to_bytes"]]}}) \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion b/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log b/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log new file mode 100644 index 0000000..8dc2028 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log @@ -0,0 +1 @@ +dh_sphinxdoc diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper b/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper new file mode 100644 index 0000000..2545e7a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p compute +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p compute || true +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper b/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper new file mode 100644 index 0000000..062ac2f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p compute +else + dpkg -L compute | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.substvars b/packaging/build/compute-0.1.0.dev1/debian/compute.substvars new file mode 100644 index 0000000..6561153 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-libvirt, python3-lxml, python3-pydantic, python3-yaml, python3:any +misc:Depends= +misc:Pre-Depends= diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control new file mode 100644 index 0000000..906243f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control @@ -0,0 +1,12 @@ +Package: compute +Version: 0.1.0.dev1-1 +Architecture: all +Maintainer: ge +Installed-Size: 118 +Depends: python3-libvirt, python3-lxml, python3-pydantic, python3-yaml, python3:any, qemu-system, qemu-utils, libvirt-daemon-system, libvirt-clients +Recommends: dnsmasq +Suggests: compute-doc +Section: admin +Priority: optional +Homepage: https://git.lulzette.ru/hstack/compute +Description: Compute instances management library and tools (Python 3) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums new file mode 100644 index 0000000..28d1b77 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums @@ -0,0 +1,27 @@ +e21aa7b0b8fd557e047296cdf5ced826 usr/bin/compute +f9bc2efd4317ac0a92b8c7d283b947b8 usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA +db790365fd79ce4e960409f8cfc71dae usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD +b65598d0aa0cfe0f390246499e741adb usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL +d6561300b96471e4e471ea1615006527 usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt +9c54095f8462231dc4be8f87fadee594 usr/lib/python3/dist-packages/compute/__init__.py +a1b4018266bd8295c5e829c45948f642 usr/lib/python3/dist-packages/compute/__main__.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/compute/cli/__init__.py +8c13534e878816096e129b15462d0840 usr/lib/python3/dist-packages/compute/cli/control.py +1c4b0023246c9cd9d37e2addc255d7f9 usr/lib/python3/dist-packages/compute/common.py +665c006c01d16e64323037b0089cacef usr/lib/python3/dist-packages/compute/exceptions.py +1ff1400c5f71bd3a55ce2521258b5bd2 usr/lib/python3/dist-packages/compute/instance/__init__.py +82ec67ce83d65b991a8aba5e70f30e76 usr/lib/python3/dist-packages/compute/instance/guest_agent.py +135f6785552229c6fac04ab1d7c3113b usr/lib/python3/dist-packages/compute/instance/instance.py +00df3cb0195a2b97f1972f020bdbb243 usr/lib/python3/dist-packages/compute/instance/schemas.py +1d557cf313b52726a3591bd2e59c3c9b usr/lib/python3/dist-packages/compute/session.py +0a98a65c1a665afb4e4ed9cb3aef38f5 usr/lib/python3/dist-packages/compute/storage/__init__.py +ecf7a8e68c733d8e5b241ca33ae7cae0 usr/lib/python3/dist-packages/compute/storage/pool.py +c4a6cb9dbccfaa9217c2dbc4a833e8c9 usr/lib/python3/dist-packages/compute/storage/volume.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/compute/utils/__init__.py +e7797202c176137f38a6652cf45170a2 usr/lib/python3/dist-packages/compute/utils/config_loader.py +6c36830706d7d714d9b3c1d23dcccf14 usr/lib/python3/dist-packages/compute/utils/ids.py +964156c54ebe27ba2b14313f8f9f9754 usr/lib/python3/dist-packages/compute/utils/units.py +1fd80db613384b8d5782cf8c5843eb94 usr/share/bash-completion/completions/compute +672a4b3f13e2a14e4040c7a513ed60ba usr/share/doc/compute/README.md +6845278a102bd147f30f770ed1134ce5 usr/share/doc/compute/changelog.Debian.gz +fb1a6c11d7a8fa5f238617c20b13b6a1 usr/share/doc/compute/copyright diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst new file mode 100755 index 0000000..cebdb00 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p compute +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p compute || true +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm new file mode 100755 index 0000000..d867122 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p compute +else + dpkg -L compute | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute new file mode 100755 index 0000000..56e33f2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from compute.cli.control import cli +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(cli()) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD new file mode 100644 index 0000000..5f97163 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD @@ -0,0 +1,23 @@ +../scripts/compute,sha256=b-Gj6H6ssfbGalpouUMSX5pmsjqDnN9xMdTwnU-UfZY,216 +compute/__init__.py,sha256=x4zp_CoVPKgDT6AqhometspAyinGxJUXO48duJ5aHUM,873 +compute/__main__.py,sha256=zJyKJul6pCbguFPtVLZBoAuZl9RXibn4CCMn46jIgUQ,745 +compute/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/cli/control.py,sha256=83wnR21pHOPyyk1i1n_YBIDz6dCFB6hmuIFguIk68rs,14634 +compute/common.py,sha256=G1qwC1EybG5LEJtyoux9ymiqB2ZOsgKXlCpbuhHv55Y,948 +compute/exceptions.py,sha256=Ga59L55qSAPeyDfjANPuMh4yVSRWHDYi9xqq5o4_7-0,2452 +compute/instance/__init__.py,sha256=kHN8jVamyrBZYZgi62tPtJ7rS73gUPhfswLalmPA5Zs,772 +compute/instance/guest_agent.py,sha256=fq89kQbcV5X5eFCsMmujRuwTOSghWO4ZhAjvxyUu84M,7018 +compute/instance/instance.py,sha256=WP6oTJfdAf6QlefwVLqdC8J6XoKHum6nZhwwHOEtjNk,23297 +compute/instance/schemas.py,sha256=B51ytPlxhnx0MrkR2WYhd49RaRT7Is7NsIM9OrMUpvI,4288 +compute/session.py,sha256=znYOIzoiCbSG62k-ViaXti_lOnw88wD8Syp3nCXAJ28,10050 +compute/storage/__init__.py,sha256=zNaVjZ2925DxrVUFWwVRsGU6bSYbF46sb4L6NsaiKbw,736 +compute/storage/pool.py,sha256=9z99bBDbb4ATGpfMkEWpxAO4fEQHNVOxxf0iUln9cN0,4197 +compute/storage/volume.py,sha256=_TbK9Y4d3NAeknPUiuhldAT3ZaN1sZgjy4QzC-Sw4Io,4110 +compute/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/utils/config_loader.py,sha256=ul1J3sZg0D9R0HbOz5Pg9JmL4nFaMahAzQEdGaWFABU,1989 +compute/utils/ids.py,sha256=fg6Xsg4OMM-BIaU3DPu0L91ICwx-L3qNoELEwQZz2s0,1007 +compute/utils/units.py,sha256=UkwD0zQ-rlpSpkbfezCcvJx4D8iZlI9M-oXXvdVEvy0,1549 +compute-0.1.0.dev1.dist-info/METADATA,sha256=tbX8xp92Jwqf44sOwPB-HqKHLezab5dU9DrQDYFitDQ,1944 +compute-0.1.0.dev1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88 +compute-0.1.0.dev1.dist-info/entry_points.txt,sha256=xHhg-Fo9Z5gJnIahbG8pVIGNDqlH5Eordn8hnXUwscw,51 +compute-0.1.0.dev1.dist-info/RECORD,, diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL new file mode 100644 index 0000000..4ba7671 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.4.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt new file mode 100644 index 0000000..4130f9f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +compute=compute.cli.control:cli + diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md new file mode 100644 index 0000000..0131e8e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md @@ -0,0 +1,65 @@ +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz new file mode 100644 index 0000000000000000000000000000000000000000..40eae6fedeef605a8bc33ff14a672c4eec8e46c4 GIT binary patch literal 176 zcmV;h08jrPiwFP!000020}YJJ4uUWgME88fEPc_GVqD-On#j($@DbNoF3?C@LR+GL zZzr>woH^3!A$Y=!vy5?8)0Cyz9M9{myp*SVdEO$7EgAXSYpPYyNdheJ=#)dO?+Ecj zy&W_ek9Sagy@Dfxv|1}4DT6RLKT@SJ(qPfpF^-L8QI)1>3A>h#Mt!?VejGF855S9} eMhIN(1i?iPkr#YZtaB`RO!5cgr!y+J0000<1Wum- literal 0 HcmV?d00001 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/control b/packaging/build/compute-0.1.0.dev1/debian/control new file mode 100644 index 0000000..6b99835 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/control @@ -0,0 +1,48 @@ +Source: compute +Section: admin +Priority: optional +Maintainer: ge +Rules-Requires-Root: no +Build-Depends: + debhelper-compat (= 13), + dh-sequence-python3, + bash-completion, + pybuild-plugin-pyproject, + python3-poetry-core, + python3-setuptools, + python3-all, + python3-sphinx, + python3-sphinx-multiversion, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Standards-Version: 4.6.2 +Homepage: https://git.lulzette.ru/hstack/compute + +Package: compute +Architecture: all +Depends: + ${python3:Depends}, + ${misc:Depends}, + qemu-system, + qemu-utils, + libvirt-daemon-system, + libvirt-clients, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Recommends: + dnsmasq +Suggests: + compute-doc +Description: Compute instances management library and tools (Python 3) + +Package: compute-doc +Section: doc +Architecture: all +Depends: + ${sphinxdoc:Depends}, + ${misc:Depends}, +Description: Compute instances management library and tools (documentation) diff --git a/packaging/build/compute-0.1.0.dev1/debian/copyright b/packaging/build/compute-0.1.0.dev1/debian/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp b/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp new file mode 100644 index 0000000..3445b01 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp @@ -0,0 +1,2 @@ +compute +compute-doc diff --git a/packaging/build/compute-0.1.0.dev1/debian/docs b/packaging/build/compute-0.1.0.dev1/debian/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/packaging/build/compute-0.1.0.dev1/debian/files b/packaging/build/compute-0.1.0.dev1/debian/files new file mode 100644 index 0000000..e63edf9 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/files @@ -0,0 +1,3 @@ +compute-doc_0.1.0.dev1-1_all.deb doc optional +compute_0.1.0.dev1-1_all.deb admin optional +compute_0.1.0.dev1-1_amd64.buildinfo admin optional diff --git a/packaging/build/compute-0.1.0.dev1/debian/rules b/packaging/build/compute-0.1.0.dev1/debian/rules new file mode 100755 index 0000000..f99ef32 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/rules @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 +export PYBUILD_DESTDIR_python3=debian/compute + +%: + dh $@ --with python3,sphinxdoc,bash-completion --buildsystem=pybuild + +override_dh_auto_test: + @echo No tests there + +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) + http_proxy=127.0.0.1:9 https_proxy=127.0.0.1:9 \ + HTTP_PROXY=127.0.0.1:9 HTTPS_PROXY=127.0.0.1:9 \ + PYTHONPATH=. PYTHON=python3 python3 -m sphinx $(SPHINXOPTS) -b html \ + ../docs/source \ + $(CURDIR)/debian/compute-doc/usr/share/doc/compute-doc/html + dh_sphinxdoc +endif diff --git a/packaging/build/compute-0.1.0.dev1/debian/source/format b/packaging/build/compute-0.1.0.dev1/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packaging/build/compute-0.1.0.dev1/debian/source/options b/packaging/build/compute-0.1.0.dev1/debian/source/options new file mode 100644 index 0000000..cb61fa5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff --git a/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex b/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex new file mode 100644 index 0000000..3fc47cc --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex @@ -0,0 +1,10 @@ +# Example file for upstream/metadata. +# See https://wiki.debian.org/UpstreamMetadata for more info/fields. +# Below an example based on a github project. + +# Bug-Database: https://github.com//compute/issues +# Bug-Submit: https://github.com//compute/issues/new +# Changelog: https://github.com//compute/blob/master/CHANGES +# Documentation: https://github.com//compute/wiki +# Repository-Browse: https://github.com//compute +# Repository: https://github.com//compute.git diff --git a/packaging/build/compute-0.1.0.dev1/pyproject.toml b/packaging/build/compute-0.1.0.dev1/pyproject.toml new file mode 100644 index 0000000..f7aab25 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = 'compute' +version = '0.1.0-dev1' +description = 'Compute instances management library and tools' +authors = ['ge '] +readme = 'README.md' + +[tool.poetry.dependencies] +python = '^3.11' +libvirt-python = '9.0.0' +lxml = '^4.9.2' +pydantic = '1.10.4' +pyyaml = "^6.0.1" + +[tool.poetry.scripts] +compute = 'compute.cli.control:cli' + +[tool.poetry.group.dev.dependencies] +ruff = '^0.1.3' +isort = '^5.12.0' + +[tool.poetry.group.docs.dependencies] +sphinx = '^7.2.6' +sphinx-autobuild = '^2021.3.14' +sphinx-multiversion = '^0.2.4' + +[build-system] +requires = ['poetry-core'] +build-backend = 'poetry.core.masonry.api' + +[tool.isort] +skip = ['.gitignore'] +lines_after_imports = 2 +include_trailing_comma = true +split_on_trailing_comma = true + +[tool.ruff] +line-length = 79 +indent-width = 4 +target-version = 'py311' + +[tool.ruff.lint] +select = ['ALL'] +ignore = [ + 'Q000', 'Q003', 'D211', 'D212', + 'ANN101', 'ISC001', 'COM812', + 'D203', 'ANN204', 'T201', + 'EM102', 'TRY003', 'EM101', + 'TD003', 'TD006', 'FIX002', # 'todo' strings linting +] +exclude = ['__init__.py'] + +[tool.ruff.lint.flake8-annotations] +mypy-init-return = true +allow-star-arg-any = true + +[tool.ruff.format] +quote-style = 'single' + +[tool.ruff.isort] +lines-after-imports = 2 diff --git a/packaging/build/compute-doc_0.1.0.dev1-1_all.deb b/packaging/build/compute-doc_0.1.0.dev1-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..c855890142ef68ebf4b458346f82e88e33503a60 GIT binary patch literal 40424 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 So|BNV#Ao{g000001X)_6^9At$ literal 0 HcmV?d00001 diff --git a/packaging/build/compute_0.1.0.dev1-1.dsc b/packaging/build/compute_0.1.0.dev1-1.dsc new file mode 100644 index 0000000..91f1fdb --- /dev/null +++ b/packaging/build/compute_0.1.0.dev1-1.dsc @@ -0,0 +1,21 @@ +Format: 3.0 (quilt) +Source: compute +Binary: compute, compute-doc +Architecture: all +Version: 0.1.0.dev1-1 +Maintainer: ge +Homepage: https://git.lulzette.ru/hstack/compute +Standards-Version: 4.6.2 +Build-Depends: debhelper-compat (= 13), dh-sequence-python3, bash-completion, pybuild-plugin-pyproject, python3-poetry-core, python3-setuptools, python3-all, python3-sphinx, python3-sphinx-multiversion, python3-libvirt, python3-lxml, python3-yaml, python3-pydantic +Package-List: + compute deb admin optional arch=all + compute-doc deb doc optional arch=all +Checksums-Sha1: + 94be605a5a0ca8b0ea93a46dd5029a2513486190 20824 compute_0.1.0.dev1.orig.tar.gz + 57790e9df9659f913fa1da65b77c84a3aba4976c 2660 compute_0.1.0.dev1-1.debian.tar.xz +Checksums-Sha256: + e310d2ddbdb334737efc7adcc98eac2db2f158e5e989ddfade2ddfae07a6174d 20824 compute_0.1.0.dev1.orig.tar.gz + c9e267c79fa5a9e06625ac0502af528f4b526c74035fa23e6933c2c8e6429ab2 2660 compute_0.1.0.dev1-1.debian.tar.xz +Files: + de78bd5eecc56034a990dd9395a089c4 20824 compute_0.1.0.dev1.orig.tar.gz + cb9d6978a83d7f1842063315333d6278 2660 compute_0.1.0.dev1-1.debian.tar.xz diff --git a/packaging/build/compute_0.1.0.dev1-1_all.deb b/packaging/build/compute_0.1.0.dev1-1_all.deb new file mode 100644 index 0000000000000000000000000000000000000000..d448f2231fa0b5a10947ae24224cb64299845e2f GIT binary patch literal 21644 zcmbr^L$EMB)F9|<+qP}nwr$(C?fYKawr$(CZO=FLPtT$^z35Y^R3%k8XZci4Ldaw2 zWNg6)WnyY%X=q1lWNByUFaZjLfM@{! z6Q}!O)cfRrVC1?0=+NCbzJRIyxKezcsDsmFMS7Uz%zLp2t=;(anp;!$FP|@RqtRss zskT6n8?0c%q=x&LbR;T?Q^xpxA;}*73N3RYZmM{I@+)GM#==d#LLK2++9BvT~!~4cn3rpbQ2n{HexD@!Z@z6PGXZJNt?9NqHO~4bu-?2RbNWWG`nA z$;F4Q9)_v>O^@fC85S0jg%*ac9On(UOw%#{{3;94iY{)+;`1k=BmNw{t1n(Q2%ro3 z?YG;A8Q+u{WM}JT(t~h@hp}s1zpSOmVKc78fzi(Z$6YE$XwVi$<7u1}7J zNj1f@G$cFcu_OESB-OrpmWn$hZ5an~7G7J|fX(9`qw3OLiNmY391??&$UbSm<_dg&uiixW>Bb4Q*k8OX z;sy>8wWIh<40v>@Npnh#=qTq}1#u)&6-F#fY2WrNXu=gJg-AAW%brzZ>8*%Lz4?AkY35kJ zDrJg|0PqQuO!o!|rlJ$Ug<*SQc`p)`nDp;u6-vyy*|3E}X%H81< z+|q?c3)+>Tmor8v*PW%Ee(^*wQxlpVWgGN6MJxZ1T4z^54DaTj5=vE)yan5ha6AEu zk7E2HYxWr^>7aVzjK&fNaDKE(7E}yq)*Y_4OQwOdWAu#PK8&`ldAOYO1I}+YcA=mI zYE1mL*j8Yi0HoL@9bBHRrhjIAuwmL$Z*HB%*KW` zak&;c8e^J0K+H8%1nO`GL*wkQ-h8j}%l!qLrV_{(QT%I%Yuo~B)1iHp6Eko12F*!- zSD2}OqVXjxjPx3Uu8h>GHHW^SD{wf9lZ;OiVmo3f5Z!ZFp45ETVQ2?Nu6f9nDIjJA zud)&75j6S1ZOkxV--tNT;%tNjI>!9g+SV2>_F82}d%DwZnhk7x0T#&W_Z4ON32|S! zUD@O9zO|?qgmhnZfKhIlPuEBjBYl5DN~ovPrF#iEVM|oaa5yCzo#`U93P;+J2?Z8% zrK9pML>;(KXx@>)zG1mamr_V86BpZE1}eW1w+h#w61Q+t?{-_z|Mw<3PZ;5j%a$;<9tmd@r4?jh3Lg+IjDA({$}Bz2JS90G7nRQmw*dq>&uPVWJ%z~bqXzk@p2yaLpq#qBOulPO zpQ!RsJkVJ?`f0{TyK|npSGodIad1#H%Z59!psC=|69dOOaCG>ImL{~FHczhuqrsuY zLyr2#*l@n3IeL66;sbG@9xXXNu|2NK2;t4O6wE6Wah#>9sM_BTA!ja{&pGcm_E(3= zbVhNPig|k_zjr(C!`u5gX8A(KHmzW4Q=9M~8!QwYTNCIukpO^G|5*Y6Kp=1b-7swa z1@!-t0-~v9V(4P{|3md3LjNc07&$o@{>Quj-KvaCz<_@6|D~1k+Kv6e-k&Vu|BY2x z@UfCfud}Hy8VizMU&Bkm#9CeS(ix5C~w8d)Af+KdB!8(5~tH&X~Ll-@xR( z_4`Hxnu@wu1IU>AX9?8><=3-(!8JTg#%@vre5k3}(2uW}@!am5l}|O&T-@)P0F^kP z$#ed^yK1#>(F_n1<@Q5wM{F7XXozPQB+=$_fk}~jE^kcAE9jKk-61BgRqajzd!#@I zm6rz4n9Qs|SGUAV5|7J%u5Z!HKV$oqDd^*^3@=jIXh4L%^1cWFdLTpq&M4(Wclk|J z_p2bQB|H4HPujFS64jtIXnS2ra1YqZx(m1gUG5@G%4Q^x2d@T=4m zHbawlRxp|WI5Ntns^#le(u%MaTZ2F2aG&q)chE^~ zXKhgkvqiwG&K}?zA@<#SEJRUo$+;Zw%!UiEiv@(znf6;}Y$fg^To4%4{yP3aniZwQ zt3Vm%mUB_!P2FG9DNi>-*a58$Spp$g`r``&S|lcmUWi}&0igha2ulVZ&w3Wx5b?j# zcaQnah({!M8rw%I+>ww$9Iw^}K?}3>UUIv>_snU@Js{kApLx9AxA_)3;C@$JKRomt zjRWv?>c6Q`zGTJ@ua$3vhI*vw=4vKKz~yov+ci%FAX^AFpnu_e>a zHIndUA}8l_D}M^D2S)ojTjtPrN!np+;BolN66*zbkf(^ z$vPMq8O`GY3<(HZS%NyPpbf4=Ia{c|mqHvNhJ6bW_p3e5HFmivm?0S@ZVlt#l1O21 zFec1KI#VU%2r;Njn(~xP$eNd;N=`VSgTd+jV?6P@8%EDEq|>(XC0BQ@+DzwbFG$7A zm+$54DW|c%P2d9D;9!oqDK%jE8bgERX|Mq{wwfo5^AhB=L%!iTKQQ444tSedg426I zw+{^}^Nf_fe=d$kXWx6?`zIr|9@aXiyrcj|a?Dl!MnDMPqXDZ7!HT9BnyDGp+hyc``!lIJkRjOzRy|?B!BNdyi5HUv`N5CqbP48|B46`j8 zT5zA->M5hjV(V{6Bl6R=yxH;6FysWAk-y3@Wjx_z2ZzPTC%IYym`xPMvlUNqU((z! zp+|+u`VeOM$f1p)4B4}aA^MY$E8^N|I0Nn@>8|zQ2Zr7nDy;IDRE)@;0ZxHs3-ydF zVzG&~dm$1T*V?Bf;OvT6G8upKS;P-{mNEO%CcVH22va?XiQ72vTi@f}rkz zwd`4z92_T!0~T}RfR=l%xZj;HN2FtLT~8%#F~vZn?jmQr@So7VrDC-=&J7q&*t&byQpoyCS#sa~Z$gn~!olk$-Mp1U#uWY6d#w_;t9EBwuv zy4oK79-zI_wV^DzTl#jfyMYYn%C_{9A>(8#2hQGra+s(JhtYHKu8A;*x_{jR3npwq zIr|F_1_&{gBL(FDaO|bAv-<>Pc1_`r?71ME5fBjGQ7W)ILVz(7$gAwr^~fz)M-r>f zlvRDN2*`!B1J3a%hu-3!0IzEEN6#eHCA#^Nu_5l7WFXzxh)>#a_`l}qj&Zi5gwyF) zJnRJ{)}ozoMiK!3wT<0PrbZOsec%u5wtUODYtZSwE6vSTIXRbU(r4+u_B=@^*8giKGn-m{NxtP?)co_2f>F_tJ=-nNrPb2Jo8`gf_bZLvXC9YWiy zuY5wgTbw^OK&s-SbB1;h;Dyj4QfNc+iJ*OltDh;Y1E(kb8@SMUZ!^3*Dzge&EMEif zFJ!2QO%x!6u3m0;3p#Mwh>-nQQXU?PX;sBl=MUf(k=pPBH6HZEEx~AQ7OWLH zmQnUk!7nv0RbDzzg-dz0|L8pP`@jZ!paBMny)!>`BY$v4>YK<)Jz@whf2jm^IjOih zLVm&7#!}sPPj$b1{pu3P%JlV(y+>bbf=jn=h@D*o*l^on_$I4eUGo|o^v3rzvTM^G zNAznWr0?4VY!#rufx&reTC$=L(SfVX@hXy zX|%t%>iQO*kr?AVH8Z{RC2MqS+D=<|c$6fo9`a$+a)-9%C+#-5gg<9qg=?wtH!i~v zg&5yHUr5(1m|aOaTYU(owG zLO0Q;4NRS;tmJ72pA}F|;qY5l>Gnv6?eFK%$$GAdOF7k1Ii~*S|6xR95Gyc0J_^Oc zJ4GT9)85LbPBqj%p{Xj`Hsa_$bAn1O6dYgflhDh8KynEpv|NHNXeqNOqIOU3>V>lg zZoJt{Qo&C0{g)q-=)>y*@QA_0Z4z)RPyt)cEdVlMx@#MV;i+kZW4EGrH}tYPmx2%l zt6y(4A0swRno{;_U$Ab-KEy#k9evu#IF@#8Wj^vp)=$aE*R9*3Z*vdG_C{OKUCE#3 z)MV{Fy5*Uo$nCP_Dk?D@O(1&nFTaBMO|QV(HTZF5v>-{aGjqALlW9&qdb06D`B5ZQ ztZp2U59h8vM;&{CDt&r@tS3CDk(j;N_KuO}dL{J`?wOXyW{Ag^!&Ju1loWD|hFe&% zj11#yKLMJ?bXA-XUH(zPjBpNWC6ADA*x&)8snMC1vDEDe(hb;87W&M#co4&m6@aH_ zh9pB`Qe&hof-#!53VZSx4K>h7R84DgES@ND4b1O((waIS%xJ03k2`+dB92wmvp8o8 z4C8jgqk$$KCHYP{fGea}J*OYb>K7{mRXrzq$O%mAgGp>PMT%Ob&M)VV+W?WeP< z3)Se^)D(s7gX%E=(BAs(T7f|Yp`uDoHcPvA_~LF$XEjw+@fY!&|8SKpMDr0CF9brf zrX|pYZdb|Vs>luD?5KDFu&(;Ymk{+dQ9Rl$EwD4Z`MgsJ7!V>TfD``vNz)CJIh>9` z8{{y`38-LbA_jbf;j}7P=mcxNdSGQUIYp2|{u6Q0gh+BXdWZ7#vt=pM&rHGlQ~O<~ zWb@7jY$C2(6u$Z=@4A#*N%2Ad$58V#9b@mlg#10!$BKeVMA=@@w*OyaMeID)eG67j-F{PTvQ9P02pc2u;&a7PbsB6=5u7cmC=l7QZ5unC+n%R(1pHF}F%LVur&qNz(edD9Y^IG`#|u+?Ut1~_I_5yR zo_Hwl`a}2L5QQe-1r~{L>Ml6vU98PO8rkYHyb0pkNIv93)%PWniy)cHUxdLTzLY|u z`-a$6+BZR*M6*GhBI#HM=Ne+VBr#Fi=(=noBUx|A2UsJg{l#4%^4CE2n}iaWwKn<^ zd#MI-g;3_-`RNxJC5l%}d~_W5Qz!nI*ghhZMKgBjx1lkZ`sV1^gSH>Ocrw;6SDkra z1FJ6wT@(?_FZMaEQaDqP{V!Fvy$i&_-UX}WmrzE#L!=o?JEG3I+Intz5ywNuVN5+O*(MTNl8+k+^Z0}$4+^(qvU0O z?*|I`%kFNFZl2&P_O~Q#Js-1gtp26AN4m4`&Tv)Xj3tK&-TbAbUC$%QwJ6eEmPc{# z1)ui(sFxVC&Ag@TlviBC1#jS?j%9mS_|RV77w7&lAyM)Li&}zO9gP%1w;KW(iz%oj~j^{8jST zsL<*1vg4r$LycMSVrc9MFW8w9bxrf#W+vb~?C`nqu=D7@n!I#!Mzr=k;8bM8+~Vyw zhH3rgiDy-;EksGyR2fvUeNgBR1H)}Png<43k+vJcwk@j0V(kp*7s_EqGRphoWgz0xhowM5 zUpyjjzQqIQID)+CdlZ}xoUv0)ju5ym4uX9TN&A%_oLreK(^qeglLg0kOb0o}px9%( z3bGH}oH#J7!vndwL})doI(3gxp7hOOal2i>{X%Q|%KlAnRVV60ME2gqs&&Mt=8PB{ zA5Ky2xy~oaLv;pU=!tBxX0i)5(gC+uruK7~GTvyGOhol951?+PX#vNlZv26&B7CbS zl_%&2q{m~Ce)zGd*x#E571GlW6A`9WF{DH#(E8HE)*6Oo7$~`8fMZyZnwy+S>}PvD z`L4yJCOzW7xM1|Eyn}iXGR6v`PiDhYv-N1-m!4BHfK(T%X?c2f2<|KAik3BJ&mN4C zf7N?po^?uYcIzyJX1O0WOhd<@+t~)*JiiJg#_c0}vlF7$Js!phHsEnZ8)wD5ui9pC z@RX4tEZfz^5-DX4ww2y!m$*y|$4C@evXxsu8B>9ixhyseEyPt>XZ_7ECz6MWFAw#tD46r56~>o5Q_~_*Ros3sF^^IGz+HLLCm% zwotlDt~w+W*OE+6@`!?%&s1*S0G_pY&1S{+HxupZWbX8&vRtE)@%CvO+Y-qJ#rqkYW? z!7do7Nu9tNd7-nVaa%I&3AM7$H$2A6L3FEVBQJ+-ZKLJ@ZQl5 zHOpb_!QaPEpd`YtnwV&$PfkfQDQmaWT~(Fh=04e6Kd&DEi@B^yB|F@KRxGC*+!O_8 z?WjV)Fx0M`{5q1>l@fuvpOp?O2D368I1<|NgIGbjB|QRK4k-rel@na(VCA zYpBJA!En37s;erqAF17jUesJPs_D%rjM#VGBjSWFedTD@5mE$o6gdq?F43a+p-|kHadd zAo~8qn}fig&a##?ZD!V7Cqy4;3Wxt!VDH;-COLRWzrfcfK0H%ix?Ick><$w2&gi%OhC7oQXz&DpxStTrZQ3iImkoT@t8>|w zWR}6TfdxC?#3@n!N9rAfsgeVSdz0k|BLhXLD_{pbzjvER?YJ+9%?&USA<@$6AhNv9*saxRU-5B8zyFobbiy|AVh^f_^hEdI7^`Z%$sBv0pEC!mM3Mn8 z^W`e)RwH<|GTkG)3O9}OzT=sRjcf?*-{#sI!Y9KFtT1)BY)5_krcY(nAM{ICwKc%Kj6G6dS$4RQI>2}MhBK4(;4=mHgdXl<#3UmHh| zYmsYkO5{kas6AKLLcqzH`$u1mmt-uh(zWGRshL81xwq6@T zKGSJuSn=6$kPxbXWJkRiP4#bVKa_^lPKGOA581wYo88qi@== zI7@Ym<7nIq*w7L5DvcT-IBfFgvKTa>w4kBgyZn@SL;&HPg^NOdrW(0V=MPh?(XaR} z1tthip)GgMR!6iVvyf&;?h{yX%00;^f0C2s&4sfN?JvC)LkhRCDPIRX`I%ha8ll{% zs>Xsf!Tkarc0HkCCDl|TNCBZxdXS>|YOp`c@PduZ|4M)~b|x8m-4+zo3%~`N;U73g z5EVV>{2IeLn;^OoqE-q#!V9bZ4(d{M1bX^TN*mI0n~JPxAt3vOyv-YM^~R8r_1<-K ztUal4xj06!;8N#lefv2Rnt%00@&Y-b`gw{>FS}zf=jr=haYQEVUmt`JetvI^&T1mp z`UViv-^F4NTyNXMN!0{R`r^F$DR5SkGE%9sA2s`!?gyP2W@V|!SdP))f_tvmA0Poo zk1XALm>OHXhZ#8Uij2w%ic>C>u2?8zQpuJ^A}m#W3plx`PcauNM$PY-p5xQw)YKAl(5Ev&?yLG zVAKzd`{h>#!@4dnRdjpy2unYs7#YT;(~NWro!>e>jwIL^CvnUv~JWR=sKu?y%B-9hK$8NbRkm*3~{uQtlElKt3ueePA#4IL|qZ58Q*I67VfyN|v5SMFLc7L&@UGVI; zk^)`9DlisvPY*M=&1j91m2yu{gO9^Z#mrktuH}#9lz&zX^_~fDVKQJ)A`F-W;x&$bx7(qk zR9~8<{*W$B`U#8%X!|^Zw+73q9RaNN&U1?A+p-{A@*K4#@%HHJx1xZw+{K@&F=O$q zniyU+%>cEDBUYJ{Cuc(cRI;W-@f++6>BTb0M(EuHsWzQJDijQ+v~z-U3<9j*HOt1s zxzTApFa6=Tnf8~Xxk1#tuK+Yx$-CJ6x-->xyG7;LfrFJ zJIsG^UA%zrz2AI39$Zm3X5OdmCdlLP@?oZ+HmZox#LIR@hsBqDHICAFd(?B<=7umhJ^ebQy*< z?TQyT93cs-86j90yn_IiNvfv#GR=k6zgVory9zV#x+r8-fepxpkUJ@+zFW~HSmIG! zCy*iKLROVv7%V2}#tSlbFfJgoBu&WZcm2W?3=Dz?)HGeXhK0AiE6@+Zr9tO!$dSND z-ms71n#*}qV-i(4g^v?3@Q#Ixhp>pB@#<{&UQ7)Z$T;d8_ z7{^wGW=~&k4^<`41z3@amv~<-y_#!d=Mv*5H~qbuY(y+BIkU;7Gy}M91Z&ry+b;R# z9?~F>4N-Zi)<;$+b3lLMQ|;k;C@--J<~NZ%SiA5JG|K|qn-!0xUW@ENkJpfC(TT>a zLNMaxqG3f9cIB(`r%T1QoFuN6a$xr)FzOeEN8|;%-Nf#E2}P5XxI@W^l<1wqr{|zs z^IJ}CD$4x?SCo!G>3!E+L~FnLC&sY)Pt}U%Z25CJWt@C+l$4cPjIb+P)96-7;)D_D zdul`CnqgDjW<$YqId8h&r?h)W3Muo40kJNK#Vf;FL10RNwd?pzNPg^~Mb&9hrRLm& zEws8H_t}nmBUr7`p8^mSs_GpnzBAv;C$EHWF2{d;F|Fl$6IBb(7-9Cu>Mh!l2TYCd zl;>x&^-QUnIRFSzv5PW<}aB4%VV#TN6r`7@b z6g+R)N=w%6*}TpXB#uu)F;v`Jn{7l8dbtP9NzgyeNB;AweMU(|RN#zuzgRVwFL*m^ z!LLwZgoA)9kAEejcd+HcMU9}x$K~wA5LM86iN$9w(bXT0iGjThQYo>@_hGACK5@4G z9A8)z$f3L|u~)HV?t}+s%nuh~>6*C!^plq$po4i6nG9>qmAjjLDYt0;f*kg#69t>y zz&|>q`<124bLiwD=*$l5$!;*9=4KWEPS3FTD$vDm?oBpypr(dM$x{ZsACoWlfyS8N zv5?kUc)$VR+rMPSn)F?v;N9+(6P9+bF_vCN*o^Hx)uI&^Z z-Kfdz5u7MHkq^O1!m*nMxvA7ouka_0QeXOiW34+*@FGwQ=jJY^9&ka%RxK|~)Iv_k zShYPE(e(HlR6kRLC%`cAS1O8)t_s?K;o`n?5j!)H7!I*|q?*{MRP#9TfW$i?$<&|6 zU)|OaH>33-zUZ!NDe_O#on)gW)M8fMIo`4>C-F-QUZLhgLnuR?Q;0E)%1<#5U_fgI zOUiSBD_=l|Vbn;CD9NU0&K7ZvOcrpNmC{g4&kPCz#@B64tnCvK z;HM8LYBj#phIMeO9h&qc8u+a)2@oJ{@82p=)KOu{p6N4;-S+?ni}j>Ov-bBXBpo&d z3lCBC(VgR2zZ-xLs!|517KYWc9rp|I)96s_;+vw(%P1O!bJqfX(~B}Ph9k#Ph9kQD zE5Ji+O1`t@Et%D0bOHl5mgv|)&>aD<2?H-9_fRM47v0757m$x|U^=fd=$(VP4uC5? zO*1J+F7d95&S)k^U7aOgC+{OVRkWiXB+h)6){xgq2urOL0L%(A zttBOG!qg~Zq<42<93r(6;mH6~{{^(CLGL9oKa5$=oj(je^x{^4X1GqcnsFC>t>u7PGG|L)l^(Lp z5=zeZMsWJ5evEUFi(r=8X4#n`xFA~PWRS6cp$|a9xxy*4Xqu5-@qtDGE(vPY#a#9^ z_Nxf}IM0YN&(+kyJtY74T&)816QD5P?cS$MX`H8n3D>+$oAy+kasz?@bTp5v$|3hK zv-I-DC5bZ6J}(C99XsO~ha?tQ-(U>^{aNO1%f+oS^U0;-_U(nq*y>d zz^V0C)i!3$&4oz8MI*0BxLIgC=7!E)YeOrPd>rEVNu|8OifY>g$dNSTdjFVs_{_W)gm zzjSeJrNL@}joHZBz`|CD@A(BWZ{vr_k?hh^^2^Id!c+H?Iya9pTWPY@?Z z>M|Si19E#m4+?J>7K&%Hqlx%*uwq|S7MzRTck95cWc7n`^-phAe-8urwM{iIKsB$r zun0J<*q4JAOT-V}qpJ&<(qku}Bk+%(6+#SCXn|HZPOvL)2cGuNto@-T`Ho)-7sD1X zFi>;xqHrf#VOyg**zhA1v#Ql{jl@o;C!Pc)#`|XZRm3=#aDh<&aah5tfwZt~ZEK^q1}FyY4c*StA4_KF1MVS3mWy(+-=C&HKdbe`G=r=1LTEK|`?hPlUH2rJNf z3C-^?AySM`nK*_YbuWhZdrBBs6C?}<1Km8Z<<5nmTQ2MJe9`y7OkJLwNb(+UNp`K_ zT^soyBuIv?hBgpojX88ZtbCb|D+{>{9E4$KVzz!W7pgA0CjcX(XZ4GULCfF5;_O0A zn&2@NY|oF}k5q2sfg&FIwL`kXy9oLSZrq?8|4L2m)!{8sANop38;Pze zC5I$c(jTl;Q4*(dsFuz7Za9S-S8x<;&`j{o4!Sa>Iyc~Nz*=X#o| zh~Y}Sb05vtIdl(|_$M1GDUx=OvCav|_kFi74XCxeNq6dm?|}bbLU!>*T6GJ;A)zvqn6$P)jBgcS8Y5A zQIPu6Ce*+h+*hf^h=M%mOZOwZYrHYXJPfov%JPUjIiO2O+QqFD?RZb`AA`~fpJL1H zz$-hB#yXiVw@&YJBinnKp+6Lj@a|M4+TV&e( zeP&RUd?jPL0f_)#xinE(7m|42u*su&k7TJbQDi-!PnEK>nFeJYydd-DJD-nD*5)U} zY9`}41t3I{Y~aKDfD^N|Am(6N+R*1u%%_pg1TevSxW^1ESKfM3Mlu03wfqG=OdI@e2j#Cl)vg%TAu2&#Tile zZjLxIYuzd|nN(SYNfvEsm%R)h5)nx|4SEh(_Aro5Cz1s)+AO4r9m-&AJF1IovA3Cz z8V{3pW%YtJ%mMnt27Nfn01v7bRPE{%g;t=6U6$`Ex9xU(5+}zcLLP=L4A+ic2varV zF&HbO=0x_qa_}vyfzG@rsfRJ(uHUm&igBzsHpz(GWmGtRM|4v==HbAAn1*Kdc3Jmb z9_5_lfxU84=zqsP zk9|OmW0>8`bUOG%4`PrYw#84~;BrEJgXxB3xpFkfjJpJs?-D#h$$g`6py2GFN`5lB zhEJ%|AW1O*Pz0nzz4&GG;MneTln(LY{l$E|0(;Y*F$3gxOx;1~iJWu=;`+>HyA=2? z>fh_ffxf^PcQLPZW-YJZZLOM&{*{9fuUNRwZG)_Y?3dJuS-C$)STrN65oVrwfZG0+ zb;V^qNkq9_kIbUZ)XyRF!HRqu*a~VrO*nix51ClFE`f!VX6jNyTDXuxbFExGq5taD z!b?k>ZyYb*Tt6{$0>d$h%iorUU zoL7{-0gEu~byZ*Dlv_GHeJkI|T9%2P*qz2_yk&t@X`y;^wT{M`|5&(?d!WQF4@HBw znLBGI1z+y?GYvL28D*P(`=&j}*?h_hRnBK;C|t52+uZ@9^@)4Aa-lH_5)~D}5=zwX z9wD`IFxY+Bhpf!Oi*QQ0zXcs*{p_494ke#u4nJx3%S}1lJZ81!q%@k<;F70rVvfBc zTp@9Q?Y3v+>nMTu zmywgo?&m0kN!cBM=8j)Ft#+;9a4=v2PXcxhbsW{&(y$}PqMu^k`O8oG#R||*kI#)w z?LkS3_;|dQ$%;*&vg*)=ebZzyKby_O%~22?D06^xF(oI#rt<=CJR_apN^RQiZ6+M@ zB(eA!xZ1!sjDDR+E8#@69%#(=L6z?6LMm*xV;ZJTb$}tLx#+JYforDo(jD$NLyk@> zx&#V~L2;MI`vG|NrrMC49PzhT;sQ-jDE>&R`BaMi)t}$z7MRkG#*fv!YUNT zbbuiS@VQ$L%F#skpH&xZ%ti(59pzrn;@=gsph@A5DKLBcD*1ty8XIktcbjuG?pd*| z2if~hOmQ<39#w`UjY&hsGI^b$1bjqi*RLY(v}Hn_)pQlU(q2!o1MN{p$k1D&g?bnH z&G_>^hKC)pEgYCW%|HTmlFO%C7out;5n=c4^vL$8@S|OuVsSF0yaI1s!bI)cw}!&8 zf7I~MYACS4^OqK9#z3G?0f#0O5JZU$WvD))RSTW8W7nKePA665TGDBQik6}5{D5Xl z_}Xf@zobx-2uE4WvFtZ!h)e#9{)c_k!43DlWdX|r0LWl{gtpVCO%~P6B90n&PUWL$ z-EO$i+b$CrGg~*$>q>@G1-j~#%fF(Bgko{ly*WP;Op{2x69G;3ZAlq*0!1y4Vef2S-J)t_7?f7RWBHDtB-w^+ zmlD0E#{emSqK2q&Vy(G~fjxM=;<0|e37kGv+6Y`c+THKJX;adZAhehQ`5^&q@^yn{@?MMCCMHBmxzI5`h>B!KQaQ4 zg7aE?u>m!4BtdRV{EpHZWoYvTIBq#)7WP-Lb80#+>_6f{20m$$#jWz|Q59~AyFH?F zB9HiOQ1TIN6u5kBApd)dL#?Yx86O(=ew}8q=`s#je@18nu`LN78FDshMS~4~zZjp_ zPMjQlxFJ6-iTeI7H3eWCFVAN8eHNz&C=zls@PcsSyD%v?^Ja_PjmHr+sLqI@Bq=N! z0!0lh6`}iDm+my*b3Q!R{Vzf|9d2ZGCh3VKTL2#>rv7c6*Nw2qg_o9mbnlYF3F-cT z*k54&Mc=(7`n9Wz>uneaJOw^V>5+{*H1rK_KS)YT((!$Tkgx?F1GDRaMM@=7k zEm#SU<4<8?)E};R*epdOQJ|X4sHrtVNSj39lp~#hcdi9cNw*>pw9#w;2%By3mDyp# zW(OOzwAh8|C8P%_8!lHQ0|e0^>ivFw8A?W=BN|<{ygw25n|hRrr^H_iVJxyga3yxE zFe@*^EXYFp|NaB%nDQd+3M3tSxV~9mH8_P4^aR$FotADy+iKPu^0mPzowT#eWAZhN zoDmJOK0agH_}7v1D)8M;*Ywmi1L<8gAhafXWBQlgaVY&(p8h<1ZxQpBxGo$z7;!82 zFtGX~opnN%l0a0iIdW)S?E13=4=g|9P)CL;T-d9UZ|RQj-)F}Qm)gjDktI~oH%#h3d-dMV(UT7A|P%;~1M zgxjk&_E9}8WW2~aHz3>oo?vT^tW|$oF?!%SQxR;0{nqS#^f8mImyq|{yb=nEQ_QKR zG!Nt6uVqpWEG(Y0?7dh_`8>OIpb`qpF6{2fH5IK}j`-vHr>p46Mhbu<4=>1+o{uXDH{&h|3)|ym z$0g*{B|`svjL*n9<%8Jux2PR``pt-DnVew7Q1tX2A#)^MP}q)^%{}coLvAJ zd>aF4&!X#e??HFp_ORQs{PM|AhMHOR!eo;BjZ>w8+EI zzRhAvJ6gi*?_OdxMG&!CWRl+|%D_;#)FJWv{$=wI;VcGYhG=AzC%js^jEHTAtlxU= zOwA0hoU{n8lUm+3L*Tw#MCw9f_0K|bN$}Y;0MY^~nFX~B(2;}c@)xkb*{<>`qTA&e zXGWbUs%}ELlkSh{!0_^3`R`dTYJAIJ%K3aIL2>iIDZ(`4)LEcDPb_tADl|}szlBE%@w;lqvV2&=6Ug7dE4yVUeW3wI>FBb~%_3NuqN}R-^dcM7UiEm$o@Io- z6tcO9i|p8E%%Dmi0Vz=*=gmcWF~#k%2Vc@kwIDI{Id+c>w9vBn**e`y$LiD>Zy{g# zz?&bEhj-qGZk(L278X7Ahu7y@F%KN#yArv|B?W-sYJd|2OMqQxQ~jE3+sO4OxZ$^W zno^8t(ENc4QmaAD1XAl8jl6Oz6H>b+Atv;K&wB$!NRkENjafgkpoX5^S0m20OkO1e z%lHa{V-2q~UCI{p0b_V|kHYzm&=gkdRgKVg%A9(j_--4n#IrR=;pVL2)h{HC7)_Vb zb{d;NicH5{2RU+JQ+TBXj2*WS`9322AO1z&S>6A3ZUErv1Wb2Z!|1bFv9=%Xu&w>G z)DrMzoiy~y$j2q0{?)Ro*4`d3KVG6gw`6c`u|4?X;eQWd?D%I_ zNoF%x3N~oxcwz4ic3&!pwCJBQM)gM6Na$utPp?fcX>mxLW6VzNWil%(S$&aPQS4tG zekA#5ZxRen(w5)4&4vrHr9+6-R-M&Ei0cGA@2%IphdMXZTiGW>Nh*%<$HAub#$?0^)$46~h zk9K5NeIwlX@S^-$QwxvN$U~%*7wHj2=KnQ+vzASUI}dy>vM-d(3Qp%FVw3S%2k`i zP|=1|w;OCYvckE>aSNV8zL{18x)H~rTiM8FinnAPfH%-gT)h$EX(rT9%9RzCv~=Kx z|9K6l){u(SK^-G87e+3ybJ0chVcaAe-Ystxa7AGbQfh%$t*|o@>f&DgUFwHpiJ4@< zP~-^P31skpoDnjCriRJi`E8wNtyGVE)7S(Y%#rr_u{hxZeR>d)8?e5uAe$|(cUr4n zD2h%(Zg?JSX@p<< zh|e8O+_lgx;`pGyfVy0WvLmbT`*;c;8IniPjm+}ZDvU6_jy!Xya_e{LV~!?>Y5acz zxf4e0fMY@J(__v#-p>qBg_C|#;6w`faJkDhutKK^V(IEK+miY4teb+0sAueAZ{dNP z&e904cN9`v^=4!xRvM9>Es@?Cyxhn42PknQ-nTJrcx?)jHN3zvIuKz4hsy{+rYr)G zOhdt!U0fP8`1S~X>_$42aFJOUeNpAVf^69Nq3*dMVn0^^JCsK-P5^m6?y_*fakyw9 zD;&-HJ_%|da@f?8fetSD!_qtWTg)tKzcN#ao-$mkWB1JIsUeUYdQHVk<*EV!aNqR* z3_BZbgtjzKQ~Ybb&;B`4@o0waPJ&7sa}&?%vG32@AtDRb#980&n_@WaD-zOT)6HH4; zPuHEE?gO<6h94}h^1|e4%%ctjQtBs~F=?uk&Vo_zivVlq;#@d%G0RMFk6K;ffW`B(_*D z9cq7VY}&C|*|u{08`P?5?(4aq+%gu{Y)e^=0*b~Fku-uJOdb9?I#}GB64frVjHUWc zAsqEsw^d4t7@_AuoT`z#)T~iuLd|l>6G(E@>+3n3X${TWbBHxXYw8TnYOf1Is+E_; z8~9pKVflvul569F(w&2k!jH-rg3FVT@Fd(*WRVe@iw;Tj7N}HcLzuj0EX>jqW?dW} zeiqu-KCm_nT}H8bxmPJv*~5SS(ArPCptmxWSp$&on%ZzIm6?7VE0!lEz#TTt>zgt; zXDYqfH$9U++Ip0xnU*?M%OPMfpm0#KpdOq>M%f!cxzELp&lUf#fVCSs}H|ESjja11Lj#@grDqZf}ff}L8 zITz^z{po+)=_LU^DDW6X@SU^7N4mO5P-To_BaLU@xP)IiVF9ksekM*Xoh7MsrZp$y z_BY(GZ1Y9&mzk1Zf26@=R7ov;c>-uT+6^ZsHVS66SCJGlmM#f0XtqT>4O!K6Vo;@SSROU-$yALsKJWpqj0#TZYhj)Z+2IIdnpSQ|WYEeP9?@ z3+Y$#B^hi4TCqdw(10P}I@#7~ZfEhYNC6LXuyUKQR zb{1>;)QVO=7*zP^Er2{t1AE{FwT!#taesg3U97vEGi0H7T9U-O=u1ZN{UG#0 zD+p4VXcQFiYbo;$aJDWAtvhE@^kOwQzoE)@bJ6jflc=QwXK?T}A>-IcT1EDVv;+?H z0=tdFA})ll%}2!=HE($542YG;V_5RZZv6qo3x55cNN4b=@G8t${~p?Pxd4<=oufGG zXVixF>L=DUN!qWrw^73N4rz-LG4kmoJ!FmjF)M!AanY|h(6G9;q)G{x%Pebt=*9Z_ zACgJ6uwl(0ldR+P-&#Bw2eJ!y2e6=QQ0{mos`^Vr?disZYOo) zmEk0FSR=Kq0&~M*jA=dkC!)*m6YO)`+#L8vi(C^8*(t}=RT@Nebe)YaU5Fj_?*Wf2 z@QNe|N8gTfKHDfC(0PWi>OO zrC+^98gEBHYdRcIczVH;jg)uhW~hsWbOO_(XNR)SJ!{M!K_Lq)z7||6TgH8tZ z1Iw}fuL5_W2;_xtIsvOC9Pu5dX|;K{8rd$zmh?3sfxz2Y155$8Hd^=Rc(HaxoXKa( zhMdm-NAjUa8=_r0fP#5Juyn{&h|+X*c<*RKnn8gf6}4NQbCH_^3OBQIAX@1qOM?>b zKs@!-2DFt2?$GUjqtJH!*X-e^BP5;;5ELoVA2Z&a8~O3X`V;@z0i|4tZ6s0BCWGeS zmpG}`8QOBvt@Q_Got}n~oC!va)_a)DQB71gmS(s>d%SY>LKU}YVC}d%lrcGRL zFhCm59IwO*$3G(ElPLjq!c6s=9f(G#1WQs=!4A0a1Yw`}=x}kp6=MUIW)Tn2$#qjb z?CFsIEa;l(fC-95EC*6crAQ9}n9tGv>fhF>e2>s}od1em?R1cWOnM|V)IG&z>_Xdi zDUKJ;)9&Rs>S-Yg33$AbWIwa$mn#Q|F1%(c77rStKC`3=UKJRuh7?z5#EmXeZmiH+>^2FDlxy6el ztcZAxewve4(u)jX8f5luuh)YS}-VFa_4HaGse z>dKc`Y@@u+fmE#5bw&v#m1GeHPIXqKC1t`!LoIR}w$Z?cjC zNk|LQ;9suLl>rE>aw~shP%bXod)^jzJ_1-ouW0%kdpEEI5>07Ycb4B8ga*D*lF+G4 z!tZQ)Ut|^yj`gVpMHjeWplW3W=bKGGxeb(9czsU;;!^JX%5O^DXrkf)!txC0e##0& z-sGMHT@g_Bvs5Jt>`vL7cPts_NS{{*b`?=X)EaJ3_EMZP639>e9_($WW9GZa!|=4? zfAD)PcGP78oN)j_(ZOosbHcfoC&u)gK9}&jAOST{CDr#}*i(>}9W(B9t63UWz8f@) zG%#=HTfQ}BrI#J6Gzcmj## zZz4|&JnyPs%(2(sq9BfrJ1bIBI_Q%CU_N6_ah%932ui2re7}S%vHC?l**P2OLcV*GY}Sv=?;cVK`#w|?ke1KN$z)-lTgDk@Py5Il zEAhB)<6GbcG@Q8772t4(R#X?p?(71m&!}~a{CubH<|Q@dXwwPebLN_Nbuj};KOW6= z`$(CG)j9$M@eVqtE&*pFOv)89rEp=>;=e{5#jc@>`+(D+<85l7LreeoX?b5BoYOWE zhswvwG#I1&yu>nsvbJ0rm%G3(Y%2^u!jLA1Z~@E6Ko3>kI>{2&j`g2TETW(8kORwb zbdsG^h76QNa{3nS;@`eY`%`Q%F`S z5DjI-nA4hGiG&vdPVlGq8T?-l)QSbN@irDy`n^%#Ym|G9)F|^akAu1$p5+ScNPm*l zRhwEv$lVw5@4kd^>I~mBwW5EP!}%xfWhlp$Y9#3D(mm^w7`)s(A?18~HNB4L zj?j#S4xFl8%-5X?*Ez;2>q#qfH4Sc(0j_nI5Vx617qTQ5>m|s71hyv<JEg$la_qEWnp$ZiO36zsu>-}mW$p6qJwWHV`Mf(I}{ z{T+8cO{I_9!2bWX$AW!0?^j!-kW`0dp7hTvUId{$Y1on=tt=Y3L2D@!G03IDlU}TN z>YBzy_g1qyy(nJZc8`t(@+OI%&8jttqRT<}oHR^@qZaji?}YzA{f1JVJ1hua@wBTZ z>$=k91?u;B%5L2ov+`O3dv;_a&u7rUm=oqGwkN_HZyf>JfXUmo9T4c#Ofv>^Uq#d> zbREkhRhV!9b9(mk;PLs4N;H2LMa>ke;tY%)OkI6>))we?2Wo97))C8WUx8;K#nRe- zU^4|I=m0mnY*^T@XJj`h78JJSw1cR|dfao#v22(V9X|u!8 +Changed-By: ge +Description: + compute - Compute instances management library and tools (Python 3) + compute-doc - Compute instances management library and tools (documentation) +Changes: + compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + . + * This is the development build, see commits in upstream repo for info. +Checksums-Sha1: + 455d0b203d96d97d4272d30be72c27cdde50fdc5 1123 compute_0.1.0.dev1-1.dsc + 94be605a5a0ca8b0ea93a46dd5029a2513486190 20824 compute_0.1.0.dev1.orig.tar.gz + 57790e9df9659f913fa1da65b77c84a3aba4976c 2660 compute_0.1.0.dev1-1.debian.tar.xz + ef2a0d6dd481adc0cf4bed1a2bc98536f1795adf 40424 compute-doc_0.1.0.dev1-1_all.deb + 20ecb0342e494a426634a5124462e49c6f7fd2c9 21644 compute_0.1.0.dev1-1_all.deb + 78f050568f3f50c415f23caf851482663c78513e 8126 compute_0.1.0.dev1-1_amd64.buildinfo +Checksums-Sha256: + c92ba4e3db43b496e01aa912f6e59240f7cd647b8b4950005182d91d071e31ac 1123 compute_0.1.0.dev1-1.dsc + e310d2ddbdb334737efc7adcc98eac2db2f158e5e989ddfade2ddfae07a6174d 20824 compute_0.1.0.dev1.orig.tar.gz + c9e267c79fa5a9e06625ac0502af528f4b526c74035fa23e6933c2c8e6429ab2 2660 compute_0.1.0.dev1-1.debian.tar.xz + 1c0d14fc87885f5dafe8bcbe1b6a07d9a57ce2dc0943a9837f751e3142ee8a42 40424 compute-doc_0.1.0.dev1-1_all.deb + a1f5a032f653276be3e4dc43818d663850463167a2b4b39138e184be1dabb44f 21644 compute_0.1.0.dev1-1_all.deb + d4ae62c2a518e36ba1acd6d56a94d1b218ffd766f5f558d7c73ac7a2ffe8102d 8126 compute_0.1.0.dev1-1_amd64.buildinfo +Files: + 635eae482cdff5bbe99a3911ed9e915c 1123 admin optional compute_0.1.0.dev1-1.dsc + de78bd5eecc56034a990dd9395a089c4 20824 admin optional compute_0.1.0.dev1.orig.tar.gz + cb9d6978a83d7f1842063315333d6278 2660 admin optional compute_0.1.0.dev1-1.debian.tar.xz + 8a8c6490cb363870735ec2572cf65cdf 40424 doc optional compute-doc_0.1.0.dev1-1_all.deb + bf5fb2ffd00e5373a54461f34b2d7033 21644 admin optional compute_0.1.0.dev1-1_all.deb + c319ab6fc548baccadb65dee3b0869af 8126 admin optional compute_0.1.0.dev1-1_amd64.buildinfo diff --git a/packaging/build/compute_0.1.0.dev1.orig.tar.gz b/packaging/build/compute_0.1.0.dev1.orig.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..fcb6882313d06126099819d8417de54da00684f0 GIT binary patch literal 20824 zcmV)7K*zryiwFn+00002|6^}$aCLNLEif)IE-)@+Wp*(xbYXG;?Y(Pz+c=UaJfHO| zxaoHuN9W{I|_wDp-`9LW$^N^$Kk{KC>%!VAAXZhi$7I=TdlSA;yb3dTdVEWKX?y+_#Hm8 zEDuxY{trLqXZ4jg&f{^^ZEv+&o3GbbH`_t$)#lb#d#m<`<>&wT&mb93XL+>J3SixX zVRYAidD7e7J?I7F;rHDCo9pXU`+s$FYx587^^Mi7jn?|w<_7Hl)zyuSKX|R>{{PKC zKY2Th0kxX=;>8mH$z%{^-Z-3uH_(I}Z>BNz3bhQ+UjcLiVXxt)_F9FD_jt+wKw zKU{b_X%y-s!DJ7TcTbKE#P`7{nGILsNt}DvN$O40*^Nj#h{g$k!%Tdi>ZuH{Lp*52dk0i01pWUSc(`X8RmI7xP^2xc^GNQ(9Am+$TZBd{5DNy zH@DjOZ)RZ{PVzV+FeS4TAs4~FfG62(I!#gu*njUGeDL1Q00Q3j4Q895_cYMUmNIXa zK@ks7!we7r1_`~&VeB0JeU(-w(+3DG|HsFp zeJklO%At`@&f7L~*;2+; zIQTT1%H;ij?C|a`c5AJ6{FvXuJlBGD`%gH#H2T*pPNN}gU}|PG^3JD8l&6mu_1iq3 zW}TNWr&MQ!lY=C^dD&><&YeVFeoJjklPt~?L0Hn+q*JS1US8I?25)3&cxB~2&Tj!l z@6=Z$JXo^!llNv8kA^VVt2hK8!At<8#h`#heFEgTj?*k}LbuUG>xYQe71m!FMpxY6 zr8pBWX-x7L617X&dgk57&@@)ena*(rpyEqv4X`JT1~{?6JC=0@FubcsP6`m>E)KoR z;q=qZr3WPJGMwfsH&K2m#wLC$AOtVPP<{^NSihbA2TT5c$^UEo|9WdRXs^G1)mq3IZ$R-S|Np!2|7Um*U&kY2S*Br{ zd¶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 literal 0 HcmV?d00001 diff --git a/packaging/build/docs/Makefile b/packaging/build/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/packaging/build/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/packaging/build/docs/make.bat b/packaging/build/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/packaging/build/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/packaging/build/docs/source/_templates/versioning.html b/packaging/build/docs/source/_templates/versioning.html new file mode 100644 index 0000000..318bd87 --- /dev/null +++ b/packaging/build/docs/source/_templates/versioning.html @@ -0,0 +1,8 @@ +{% if versions %} +

{{ _('Версии') }}

+
+{% endif %} diff --git a/packaging/build/docs/source/conf.py b/packaging/build/docs/source/conf.py new file mode 100644 index 0000000..d8738f3 --- /dev/null +++ b/packaging/build/docs/source/conf.py @@ -0,0 +1,33 @@ +# Add /mnt/build/compute-0.1.0.dev1 to path for autodoc Sphinx extension +import os +import sys +sys.path.insert(0, os.path.abspath('/mnt/build/compute-0.1.0.dev1')) + +# Project information +project = 'Compute' +copyright = '2023, Compute Authors' +author = 'Compute Authors' +release = '0.1.0' + +# Sphinx general settings +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_multiversion', +] +templates_path = ['_templates'] +exclude_patterns = [] +language = 'en' + +# HTML output settings +html_theme = 'alabaster' +html_static_path = ['_static'] +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', + 'searchbox.html', + 'donate.html', + 'versioning.html', + ] +} diff --git a/packaging/build/docs/source/index.rst b/packaging/build/docs/source/index.rst new file mode 100644 index 0000000..81222c2 --- /dev/null +++ b/packaging/build/docs/source/index.rst @@ -0,0 +1,16 @@ +Compute +======= + +Compute instances management library. + +.. toctree:: + :maxdepth: 1 + + pyapi/index + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/packaging/build/docs/source/pyapi/exceptions.rst b/packaging/build/docs/source/pyapi/exceptions.rst new file mode 100644 index 0000000..3912721 --- /dev/null +++ b/packaging/build/docs/source/pyapi/exceptions.rst @@ -0,0 +1,5 @@ +``exceptions`` +============== + +.. automodule:: compute.exceptions + :members: diff --git a/packaging/build/docs/source/pyapi/index.rst b/packaging/build/docs/source/pyapi/index.rst new file mode 100644 index 0000000..e0cebb8 --- /dev/null +++ b/packaging/build/docs/source/pyapi/index.rst @@ -0,0 +1,49 @@ +Python API +========== + +The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the `myinstance` instance. + +.. code-block:: python + + import logging + + from compute import Session + + logging.basicConfig(level=logging.DEBUG) + + with Session() as session: + instance = session.get_instance('myinstance') + instance.set_vcpus(4) + instance.start() + instance.set_autostart(enabled=True) + + +:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect` +and returns objects of other classes of the present library. + +Entity representation +--------------------- + +Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +:class:`Volume`. + +The configuration files of various libvirt objects in `compute` are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the ``to_xml()`` method. For example :class:`VolumeConfig`. + +`Pydantic `_ models are used to validate input data. +For example :class:`VolumeSchema`. + +Modules documentation +--------------------- + +.. toctree:: + :maxdepth: 4 + + session + instance/index + storage/index + utils + exceptions diff --git a/packaging/build/docs/source/pyapi/instance/guest_agent.rst b/packaging/build/docs/source/pyapi/instance/guest_agent.rst new file mode 100644 index 0000000..1305140 --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/guest_agent.rst @@ -0,0 +1,6 @@ +``guest_agent`` +=============== + +.. automodule:: compute.instance.guest_agent + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/instance/index.rst b/packaging/build/docs/source/pyapi/instance/index.rst new file mode 100644 index 0000000..659ffc2 --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/index.rst @@ -0,0 +1,10 @@ +``instance`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + instance + guest_agent + schemas diff --git a/packaging/build/docs/source/pyapi/instance/instance.rst b/packaging/build/docs/source/pyapi/instance/instance.rst new file mode 100644 index 0000000..3c58f1f --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/instance.rst @@ -0,0 +1,6 @@ +``instance`` +============ + +.. automodule:: compute.instance.instance + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/instance/schemas.rst b/packaging/build/docs/source/pyapi/instance/schemas.rst new file mode 100644 index 0000000..7dacabf --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/schemas.rst @@ -0,0 +1,5 @@ +``schemas`` +=========== + +.. automodule:: compute.instance.schemas + :members: diff --git a/packaging/build/docs/source/pyapi/session.rst b/packaging/build/docs/source/pyapi/session.rst new file mode 100644 index 0000000..2dec16e --- /dev/null +++ b/packaging/build/docs/source/pyapi/session.rst @@ -0,0 +1,6 @@ +``session`` +=========== + +.. automodule:: compute.session + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/storage/index.rst b/packaging/build/docs/source/pyapi/storage/index.rst new file mode 100644 index 0000000..e9ea734 --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/index.rst @@ -0,0 +1,9 @@ +``storage`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + pool + volume diff --git a/packaging/build/docs/source/pyapi/storage/pool.rst b/packaging/build/docs/source/pyapi/storage/pool.rst new file mode 100644 index 0000000..398124e --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/pool.rst @@ -0,0 +1,6 @@ +``pool`` +======== + +.. automodule:: compute.storage.pool + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/storage/volume.rst b/packaging/build/docs/source/pyapi/storage/volume.rst new file mode 100644 index 0000000..e1ba8d0 --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/volume.rst @@ -0,0 +1,6 @@ +``volume`` +========== + +.. automodule:: compute.storage.volume + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/utils.rst b/packaging/build/docs/source/pyapi/utils.rst new file mode 100644 index 0000000..b5ab60a --- /dev/null +++ b/packaging/build/docs/source/pyapi/utils.rst @@ -0,0 +1,14 @@ +``utils`` +========= + +``utils.units`` +--------------- + +.. automodule:: compute.utils.units + :members: + +``utils.ids`` +------------- + +.. automodule:: compute.utils.ids + :members: diff --git a/packaging/files/compute.bash-completion b/packaging/files/compute.bash-completion new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/files/compute.bash-completion @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/files/control b/packaging/files/control new file mode 100644 index 0000000..6b99835 --- /dev/null +++ b/packaging/files/control @@ -0,0 +1,48 @@ +Source: compute +Section: admin +Priority: optional +Maintainer: ge +Rules-Requires-Root: no +Build-Depends: + debhelper-compat (= 13), + dh-sequence-python3, + bash-completion, + pybuild-plugin-pyproject, + python3-poetry-core, + python3-setuptools, + python3-all, + python3-sphinx, + python3-sphinx-multiversion, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Standards-Version: 4.6.2 +Homepage: https://git.lulzette.ru/hstack/compute + +Package: compute +Architecture: all +Depends: + ${python3:Depends}, + ${misc:Depends}, + qemu-system, + qemu-utils, + libvirt-daemon-system, + libvirt-clients, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Recommends: + dnsmasq +Suggests: + compute-doc +Description: Compute instances management library and tools (Python 3) + +Package: compute-doc +Section: doc +Architecture: all +Depends: + ${sphinxdoc:Depends}, + ${misc:Depends}, +Description: Compute instances management library and tools (documentation) diff --git a/packaging/files/copyright b/packaging/files/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/files/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + . + This package is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + . + You should have received a copy of the GNU General Public License + along with this program. If not, see . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/files/docs b/packaging/files/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/packaging/files/docs @@ -0,0 +1 @@ +README.md diff --git a/packaging/files/rules b/packaging/files/rules new file mode 100755 index 0000000..f99ef32 --- /dev/null +++ b/packaging/files/rules @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 +export PYBUILD_DESTDIR_python3=debian/compute + +%: + dh $@ --with python3,sphinxdoc,bash-completion --buildsystem=pybuild + +override_dh_auto_test: + @echo No tests there + +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) + http_proxy=127.0.0.1:9 https_proxy=127.0.0.1:9 \ + HTTP_PROXY=127.0.0.1:9 HTTPS_PROXY=127.0.0.1:9 \ + PYTHONPATH=. PYTHON=python3 python3 -m sphinx $(SPHINXOPTS) -b html \ + ../docs/source \ + $(CURDIR)/debian/compute-doc/usr/share/doc/compute-doc/html + dh_sphinxdoc +endif diff --git a/pyproject.toml b/pyproject.toml index aa5e8e4..f7aab25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = 'compute' -version = '0.1.0' -description = 'Compute instances management library' +version = '0.1.0-dev1' +description = 'Compute instances management library and tools' authors = ['ge '] readme = 'README.md' @@ -42,10 +42,11 @@ target-version = 'py311' [tool.ruff.lint] select = ['ALL'] ignore = [ - 'Q000', 'Q003', 'D211', 'D212', 'ANN101', 'ISC001', 'COM812', + 'Q000', 'Q003', 'D211', 'D212', + 'ANN101', 'ISC001', 'COM812', 'D203', 'ANN204', 'T201', - 'EM102', 'TRY003', 'EM101', # maybe not ignore? - 'TD003', 'TD006', 'FIX002', # todo strings linting + 'EM102', 'TRY003', 'EM101', + 'TD003', 'TD006', 'FIX002', # 'todo' strings linting ] exclude = ['__init__.py'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d31aae1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,186 @@ +libvirt-python==9.0.0 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:49702d33fa8cbcae19fa727467a69f7ae2241b3091324085ca1cc752b2b414ce +lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3 \ + --hash=sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d \ + --hash=sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a \ + --hash=sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120 \ + --hash=sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305 \ + --hash=sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287 \ + --hash=sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23 \ + --hash=sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52 \ + --hash=sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f \ + --hash=sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4 \ + --hash=sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584 \ + --hash=sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f \ + --hash=sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693 \ + --hash=sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef \ + --hash=sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5 \ + --hash=sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02 \ + --hash=sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc \ + --hash=sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7 \ + --hash=sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da \ + --hash=sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a \ + --hash=sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40 \ + --hash=sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8 \ + --hash=sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd \ + --hash=sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601 \ + --hash=sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c \ + --hash=sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be \ + --hash=sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2 \ + --hash=sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c \ + --hash=sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129 \ + --hash=sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc \ + --hash=sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2 \ + --hash=sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1 \ + --hash=sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7 \ + --hash=sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d \ + --hash=sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477 \ + --hash=sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d \ + --hash=sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e \ + --hash=sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7 \ + --hash=sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2 \ + --hash=sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574 \ + --hash=sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf \ + --hash=sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b \ + --hash=sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98 \ + --hash=sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12 \ + --hash=sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42 \ + --hash=sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35 \ + --hash=sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d \ + --hash=sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce \ + --hash=sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d \ + --hash=sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f \ + --hash=sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db \ + --hash=sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4 \ + --hash=sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694 \ + --hash=sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac \ + --hash=sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2 \ + --hash=sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7 \ + --hash=sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96 \ + --hash=sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d \ + --hash=sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b \ + --hash=sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a \ + --hash=sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13 \ + --hash=sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340 \ + --hash=sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6 \ + --hash=sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458 \ + --hash=sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c \ + --hash=sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c \ + --hash=sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9 \ + --hash=sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432 \ + --hash=sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991 \ + --hash=sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69 \ + --hash=sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf \ + --hash=sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb \ + --hash=sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b \ + --hash=sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833 \ + --hash=sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76 \ + --hash=sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85 \ + --hash=sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e \ + --hash=sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50 \ + --hash=sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8 \ + --hash=sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4 \ + --hash=sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b \ + --hash=sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5 \ + --hash=sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190 \ + --hash=sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7 \ + --hash=sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa \ + --hash=sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0 \ + --hash=sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9 \ + --hash=sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0 \ + --hash=sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b \ + --hash=sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5 \ + --hash=sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7 \ + --hash=sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4 +pydantic==1.10.4 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d +pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef