1
0
mirror of https://github.com/gryf/boxpy.git synced 2026-03-27 08:03:31 +01:00

6 Commits

Author SHA1 Message Date
62b86d5f81 Added poweroff option for stopping machine 2024-05-14 19:30:56 +02:00
14cd805e00 Bump the version.
Also, clean up raising exceptions.
2024-04-30 15:14:39 +02:00
86b02fca1b Keep image information right in the appropriate image subclass. 2024-04-30 15:14:13 +02:00
3d840de3ee Bump Debian, Fedora and Ubuntu versions.
Also, set Ubuntu latest LTS as default.
2024-04-30 15:10:03 +02:00
2a1a4cf40a Use pyproject.toml for installing box module. 2024-03-30 20:34:01 +01:00
71bf5b6d99 Bump Fedora version, remove archived versions prior to 37 2023-12-05 11:54:05 +01:00
4 changed files with 148 additions and 49 deletions

View File

@@ -29,25 +29,55 @@ Requirements
formats formats
Tested distros
--------------
- Ubuntu
- 18.04
- 20.04
- 22.04
- 24.04
- Fedora
- 37
- 38
- 39
- 40
- Centos Stream
- 8
- 9
- Debian
- 10 (buster)
- 11 (bullseye)
- 12 (bookworm)
- 13 (trixie) - prerelease
There is possibility to use whatever OS image which supports cloud-init. Use
the ``--image`` param for ``create`` command to pass image filename, although
it's wise to at least discover (or not, but it may be easier in certain
distributions) what username is supposed to be used as a default user and pass
it with ``--username`` param.
How to run it How to run it
------------- -------------
First, make sure you fulfill the requirements; either by using packages from First, make sure you fulfill the requirements; either by using packages from
your operating system, or by using virtualenv for Python requirements, i.e.: your operating system, or by using virtualenv, i.e.:
.. code:: shell-session .. code:: shell-session
$ python -m virtualenv .venv $ python -m virtualenv .venv
$ . .venv/bin/activate $ . .venv/bin/activate
(.venv) $ pip install -r requirements.txt (.venv) $ pip install .
then you can issue: You'll have ``boxpy`` command created for you as well.
.. code:: shell-session .. code:: shell-session
$ alias boxpy='python /path/to/box.py' $ boxpy -V
boxpy 1.9.2
or simply link it somewhere in the path: Other option is simply link it somewhere in the path:
.. code:: shell-session .. code:: shell-session
@@ -55,25 +85,25 @@ or simply link it somewhere in the path:
$ chmod +x ~/bin/boxpy $ chmod +x ~/bin/boxpy
and now you can issue some command. For example, to spin up a VM with Ubuntu and now you can issue some command. For example, to spin up a VM with Ubuntu
18.04 with one CPU, 1GB of memory and 6GB of disk: 20.04 with one CPU, 1GB of memory and 6GB of disk:
.. code:: shell-session .. code:: shell-session
$ boxpy create --version 18.04 myvm $ boxpy create --version 20.04 myvm
note, that Ubuntu is default distribution you don't need to specify note, that Ubuntu is default distribution you don't need to specify
``--distro`` nor ``--version`` it will pick up latest LTS version. Now, let's ``--distro`` nor ``--version`` it will pick up latest LTS version. Now, let's
recreate it with 20.04: recreate it with 22.04:
.. code:: shell-session .. code:: shell-session
$ boxpy rebuild --version 20.04 myvm $ boxpy rebuild --version 22.04 myvm
or recreate it with Fedora and add additional CPU: or recreate it with Fedora and add additional CPU:
.. code:: shell-session .. code:: shell-session
$ boxpy rebuild --distro fedora --version 34 --cpu 2 myvm $ boxpy rebuild --distro fedora --version 39 --cpu 2 myvm
now, let's connect to the VM using either ssh command, which is printed out at now, let's connect to the VM using either ssh command, which is printed out at
as last ``boxpy`` output line, or simply by using ssh boxpy command: as last ``boxpy`` output line, or simply by using ssh boxpy command:

96
box.py
View File

@@ -18,13 +18,10 @@ import requests
import yaml import yaml
__version__ = "1.9.2" __version__ = "1.10.1"
CACHE_DIR = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) CACHE_DIR = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
CLOUD_IMAGE = "ci.iso" CLOUD_IMAGE = "ci.iso"
FEDORA_RELEASE_MAP = {'32': '1.6', '33': '1.2', '34': '1.2', '35': '1.2',
'36': '1.5', '37': '1.7', '38': '1.6'}
DEBIAN_CODENAME_MAP = {'12': 'bookworm', '11': 'bullseye', '10': 'buster'}
TYPE_MAP = {'HardDisk': 'disk', 'DVD': 'dvd', 'Floppy': 'floppy'} TYPE_MAP = {'HardDisk': 'disk', 'DVD': 'dvd', 'Floppy': 'floppy'}
DISTRO_MAP = {'ubuntu': 'Ubuntu', 'fedora': 'Fedora', DISTRO_MAP = {'ubuntu': 'Ubuntu', 'fedora': 'Fedora',
'centos': 'Centos Stream', 'debian': 'Debian'} 'centos': 'Centos Stream', 'debian': 'Debian'}
@@ -221,8 +218,22 @@ _boxpy() {
fi fi
;; ;;
stop) stop)
items=(--poweroff)
if [[ ${prev} == ${cmd} ]]; then if [[ ${prev} == ${cmd} ]]; then
if [[ ${cmd} = "stop" ]]; then
_vms_comp runningvms _vms_comp runningvms
else
COMPREPLY=( $(compgen -W "${items[*]}" -- ${cur}) )
fi
else
_get_excluded_items "${items[@]}"
COMPREPLY=( $(compgen -W "$result" -- ${cur}) )
case "${prev}" in
--*)
COMPREPLY=( )
;;
esac
fi fi
;; ;;
esac esac
@@ -676,7 +687,7 @@ class VBoxManage:
dom = xml.dom.minidom.parse(path) dom = xml.dom.minidom.parse(path)
if len(dom.getElementsByTagName('HardDisk')) != 1: if len(dom.getElementsByTagName('HardDisk')) != 1:
# don't know what to do with multiple discs # don't know what to do with multiple discs
raise BoxError() raise BoxError
disk = dom.getElementsByTagName('HardDisk')[0] disk = dom.getElementsByTagName('HardDisk')[0]
location = disk.getAttribute('location') location = disk.getAttribute('location')
@@ -819,7 +830,8 @@ class VBoxManage:
self.uuid = line.split('UUID:')[1].strip() self.uuid = line.split('UUID:')[1].strip()
if not self.uuid: if not self.uuid:
raise BoxVBoxFailure(f'Cannot create VM "{self.name_or_uuid}".') msg = f'Cannot create VM "{self.name_or_uuid}".'
raise BoxVBoxFailure(msg)
port = conf.port if conf.port else self._find_unused_port() port = conf.port if conf.port else self._find_unused_port()
@@ -842,14 +854,14 @@ class VBoxManage:
if Run(cmd).returncode != 0: if Run(cmd).returncode != 0:
LOG.fatal(f'Cannot modify VM "{self.name_or_uuid}"') LOG.fatal(f'Cannot modify VM "{self.name_or_uuid}"')
raise BoxVBoxFailure() raise BoxVBoxFailure
if conf.disable_nested == 'False': if conf.disable_nested == 'False':
if Run(['vboxmanage', 'modifyvm', self.name_or_uuid, if Run(['vboxmanage', 'modifyvm', self.name_or_uuid,
'--nested-hw-virt', 'on']).returncode != 0: '--nested-hw-virt', 'on']).returncode != 0:
LOG.fatal(f'Cannot set nested virtualization for VM ' LOG.fatal(f'Cannot set nested virtualization for VM '
f'"{self.name_or_uuid}"') f'"{self.name_or_uuid}"')
raise BoxVBoxFailure() raise BoxVBoxFailure
return self.uuid return self.uuid
@@ -885,7 +897,7 @@ class VBoxManage:
if Run(['vboxmanage', 'modifymedium', 'disk', src, '--resize', if Run(['vboxmanage', 'modifymedium', 'disk', src, '--resize',
str(size), '--move', fullpath]).returncode != 0: str(size), '--move', fullpath]).returncode != 0:
LOG.fatal('Resizing and moving image %s has failed', dst) LOG.fatal('Resizing and moving image %s has failed', dst)
raise BoxVBoxFailure() raise BoxVBoxFailure
return fullpath return fullpath
def storageattach(self, controller_name, port, type_, image): def storageattach(self, controller_name, port, type_, image):
@@ -909,7 +921,7 @@ class VBoxManage:
if Run(['vboxmanage', 'startvm', self.name_or_uuid, '--type', if Run(['vboxmanage', 'startvm', self.name_or_uuid, '--type',
type_]).returncode != 0: type_]).returncode != 0:
LOG.fatal('Failed to start: %s', self.name_or_uuid) LOG.fatal('Failed to start: %s', self.name_or_uuid)
raise BoxVBoxFailure() raise BoxVBoxFailure
def setextradata(self, key, val): def setextradata(self, key, val):
res = Run(['vboxmanage', 'setextradata', self.name_or_uuid, key, val]) res = Run(['vboxmanage', 'setextradata', self.name_or_uuid, key, val])
@@ -923,7 +935,7 @@ class VBoxManage:
if Run(['vboxmanage', 'modifyvm', self.name_or_uuid, f'--{nic}', if Run(['vboxmanage', 'modifyvm', self.name_or_uuid, f'--{nic}',
kind]).returncode != 0: kind]).returncode != 0:
LOG.fatal('Cannot modify VM "%s"', self.name_or_uuid) LOG.fatal('Cannot modify VM "%s"', self.name_or_uuid)
raise BoxVBoxFailure() raise BoxVBoxFailure
def is_port_in_use(self, port): def is_port_in_use(self, port):
used_ports = self._get_defined_ports() used_ports = self._get_defined_ports()
@@ -985,7 +997,7 @@ class Image:
IMG = "" IMG = ""
CHECKSUMTOOL = 'sha256sum' CHECKSUMTOOL = 'sha256sum'
def __init__(self, vbox, version, arch, release, fname=None): def __init__(self, vbox, version, arch, fname=None):
self.vbox = vbox self.vbox = vbox
self._tmp = tempfile.mkdtemp(prefix='boxpy_') self._tmp = tempfile.mkdtemp(prefix='boxpy_')
self._img_fname = fname self._img_fname = fname
@@ -1048,8 +1060,7 @@ class Image:
return True return True
fname = os.path.join(CACHE_DIR, self._img_fname) fname = os.path.join(CACHE_DIR, self._img_fname)
LOG.header('Downloading image %s from %s', self._img_fname, LOG.header('Downloading image %s', self._img_url)
self._img_url)
Run(['wget', '-q', self._img_url, '-O', fname]) Run(['wget', '-q', self._img_url, '-O', fname])
if not self._checksum(): if not self._checksum():
@@ -1061,15 +1072,15 @@ class Image:
return True return True
def _get_checksum(self, fname): def _get_checksum(self, fname):
raise NotImplementedError() raise NotImplementedError
class Ubuntu(Image): class Ubuntu(Image):
URL = "https://cloud-images.ubuntu.com/releases/%s/release/%s" URL = "https://cloud-images.ubuntu.com/releases/%s/release/%s"
IMG = "ubuntu-%s-server-cloudimg-%s.img" IMG = "ubuntu-%s-server-cloudimg-%s.img"
def __init__(self, vbox, version, arch, release, fname=None): def __init__(self, vbox, version, arch, fname=None):
super().__init__(vbox, version, arch, release) super().__init__(vbox, version, arch)
self._img_fname = self.IMG % (version, arch) self._img_fname = self.IMG % (version, arch)
self._img_url = self.URL % (version, self._img_fname) self._img_url = self.URL % (version, self._img_fname)
self._checksum_file = 'SHA256SUMS' self._checksum_file = 'SHA256SUMS'
@@ -1091,13 +1102,18 @@ class Debian(Image):
URL = "https://cloud.debian.org/images/cloud/%s/daily/latest/%s" URL = "https://cloud.debian.org/images/cloud/%s/daily/latest/%s"
IMG = "debian-%s-generic-%s-daily.qcow2" IMG = "debian-%s-generic-%s-daily.qcow2"
CHECKSUMTOOL = 'sha512sum' CHECKSUMTOOL = 'sha512sum'
CODENAME_MAP = {'13': 'trixie',
'12': 'bookworm',
'11': 'bullseye',
'10': 'buster'}
def __init__(self, vbox, version, arch, release, fname=None): def __init__(self, vbox, version, arch, fname=None):
super().__init__(vbox, version, arch, release) super().__init__(vbox, version, arch)
codename = self.CODENAME_MAP[version]
self._img_fname = self.IMG % (version, arch) self._img_fname = self.IMG % (version, arch)
self._img_url = self.URL % (release, self._img_fname) self._img_url = self.URL % (codename, self._img_fname)
self._checksum_file = 'SHA512SUMS' self._checksum_file = 'SHA512SUMS'
self._checksum_url = self.URL % (release, self._checksum_file) self._checksum_url = self.URL % (codename, self._checksum_file)
def _get_checksum(self, fname): def _get_checksum(self, fname):
expected_sum = None expected_sum = None
@@ -1116,12 +1132,21 @@ class Fedora(Image):
"Cloud/%s/images/%s") "Cloud/%s/images/%s")
IMG = "Fedora-Cloud-Base-%s-%s.%s.qcow2" IMG = "Fedora-Cloud-Base-%s-%s.%s.qcow2"
CHKS = "Fedora-Cloud-%s-%s-%s-CHECKSUM" CHKS = "Fedora-Cloud-%s-%s-%s-CHECKSUM"
REVISION = {'37': '1.7',
'38': '1.6',
'39': '1.5',
'40': '1.14'}
def __init__(self, vbox, version, arch, release, fname=None): def __init__(self, vbox, version, arch, fname=None):
super().__init__(vbox, version, arch, release) super().__init__(vbox, version, arch)
self._img_fname = self.IMG % (version, release, arch) revision = self.REVISION[version]
if int(version) > 39:
self.IMG = "Fedora-Cloud-Base-Generic.%s-%s-%s.qcow2"
self._img_fname = self.IMG % (arch, version, revision)
else:
self._img_fname = self.IMG % (version, revision, arch)
self._img_url = self.URL % (version, arch, self._img_fname) self._img_url = self.URL % (version, arch, self._img_fname)
self._checksum_file = self.CHKS % (version, release, arch) self._checksum_file = self.CHKS % (version, revision, arch)
self._checksum_url = self.URL % (version, arch, self._checksum_file) self._checksum_url = self.URL % (version, arch, self._checksum_file)
def _get_checksum(self, fname): def _get_checksum(self, fname):
@@ -1199,12 +1224,12 @@ DISTROS = {'ubuntu': {'username': 'ubuntu',
'realname': 'ubuntu', 'realname': 'ubuntu',
'img_class': Ubuntu, 'img_class': Ubuntu,
'amd64': 'amd64', 'amd64': 'amd64',
'default_version': '22.04'}, 'default_version': '24.04'},
'fedora': {'username': 'fedora', 'fedora': {'username': 'fedora',
'realname': 'fedora', 'realname': 'fedora',
'img_class': Fedora, 'img_class': Fedora,
'amd64': 'x86_64', 'amd64': 'x86_64',
'default_version': '38'}, 'default_version': '40'},
'centos': {'username': 'centos', 'centos': {'username': 'centos',
'realname': 'centos', 'realname': 'centos',
'img_class': CentosStream, 'img_class': CentosStream,
@@ -1218,13 +1243,8 @@ DISTROS = {'ubuntu': {'username': 'ubuntu',
def get_image_object(vbox, version, image='ubuntu', arch='amd64'): def get_image_object(vbox, version, image='ubuntu', arch='amd64'):
release = None
if image == 'fedora':
release = FEDORA_RELEASE_MAP[version]
if image == 'debian':
release = DEBIAN_CODENAME_MAP[version]
return DISTROS[image]['img_class'](vbox, version, DISTROS[image]['amd64'], return DISTROS[image]['img_class'](vbox, version, DISTROS[image]['amd64'],
release, DISTROS[image].get('image')) DISTROS[image].get('image'))
class IsoImage: class IsoImage:
@@ -1605,7 +1625,7 @@ def connect(args):
return Run(cmd, False).returncode return Run(cmd, False).returncode
def _set_vmstate(name, state, guitype=None): def _set_vmstate(name, state, guitype=None, poweroff=False):
vbox = VBoxManage(name) vbox = VBoxManage(name)
if not vbox.get_vm_info(): if not vbox.get_vm_info():
@@ -1622,16 +1642,18 @@ def _set_vmstate(name, state, guitype=None):
if state == "start": if state == "start":
vbox.poweron(guitype) vbox.poweron(guitype)
elif poweroff:
vbox.poweroff()
else: else:
vbox.acpipowerbutton() vbox.acpipowerbutton()
def vmstart(args): def vmstart(args):
_set_vmstate(args.name, 'start', args.type) _set_vmstate(args.name, 'start', guitype=args.type)
def vmstop(args): def vmstop(args):
_set_vmstate(args.name, 'stop') _set_vmstate(args.name, 'stop', poweroff=args.poweroff)
def main(): def main():
@@ -1749,6 +1771,8 @@ def main():
stop = subparsers.add_parser('stop', help='stop VM') stop = subparsers.add_parser('stop', help='stop VM')
stop.add_argument('name', help='name or UUID of the VM') stop.add_argument('name', help='name or UUID of the VM')
stop.add_argument('-p', '--poweroff', action='store_true', help='poweroff '
'machine instead of using ACPI power signal')
stop.set_defaults(func=vmstop) stop.set_defaults(func=vmstop)
completion = subparsers.add_parser('completion', help='generate shell ' completion = subparsers.add_parser('completion', help='generate shell '

47
pyproject.toml Normal file
View File

@@ -0,0 +1,47 @@
[build-system]
requires = ["setuptools >= 61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "boxpy"
dynamic = ["version"]
authors = [
{name = "Roman Dobosz", email = "gryf73@gmail.com"}
]
license = {text = "GPLv3"}
description = "Run Linux cloud image on top of VirtualBox using commandline tool"
readme = "README.rst"
requires-python = ">=3.8"
keywords = ["vboxmanage", "virtualbox", "vm", "virtual machine", "automation"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: End Users/Desktop",
"Topic :: Terminals",
"Topic :: Utilities",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3 :: Only"
]
dependencies = [
"pyyaml>=5.4.1",
"requests>=2.26.0"
]
[project.urls]
Homepage = "https://github.com/gryf/boxpy"
[project.scripts]
boxpy = "box:main"
[tool.setuptools]
py-modules = ["box"]
[tool.setuptools.dynamic]
version = {attr = "box.__version__"}
[tool.distutils.bdist_wheel]
universal = true

View File

@@ -1,2 +0,0 @@
pyyaml>=5.4.1
requests>=2.26.0