1
0
mirror of https://github.com/gryf/boxpy.git synced 2026-02-02 06:05:47 +01:00

46 Commits
1.0 ... 1.8

Author SHA1 Message Date
86a5655025 Improve detecting VM Operating System. 2023-04-20 19:21:24 +02:00
259d11e409 Don't reset Debian Bookworm on first run.
Issue/glitch with kernel panic on first run seems to be fixed on Debian
Bookworm (12). Do not reset VM for that version (and hopefully
upcoming).
2023-04-20 17:01:57 +02:00
b2457d497e Add experimental Debian support.
Note, there is something weird with those images, as the will segfault
on the first run (I've checked that with latest Debian 11 images, maybe
it will change in the future), so there is forced reboot after certain
amount of time as a workaround.
2023-04-18 21:48:19 +02:00
15a6ecb540 Use predefined os types for vbox VMs.
It might have impact on how operating system inside VM is treated by
VirtualBox, so let's stick with proper distro for every OS. As an
outcome, it turns out, that for graphical side of the vm, for Linux OS
the vmsvga driver is recommended, as a little more amount of vram.
2023-04-18 21:39:56 +02:00
10543cb506 Don't create vm till it's still there during rebuild 2023-04-02 10:05:06 +02:00
1f1084f294 Readme update. 2023-02-26 19:20:11 +01:00
f5ed3d37ac readme update 2023-02-22 21:10:59 +01:00
1483f49461 Added debug info about ssh connection to the vm 2023-02-22 21:08:43 +01:00
f0282874f8 Added information regarding url for the dl image 2023-02-12 11:45:50 +01:00
4db0b422b8 Added missing fname from Centos class constructor 2023-02-12 11:45:19 +01:00
c3ee529d95 Make 22.04 default version for ubuntu. 2022-12-24 12:29:49 +01:00
cdcb7ffdce Added implementation for custom image. 2022-11-17 20:34:13 +01:00
9658a9ef36 Add commandline options for providing custom image.
Two new commandline options are added: image and default-user. When
image parameter has been add there are implications that:

- default-user is also provided by commandline - regardless it is
  already present in yaml config
- distro parameter is ignored
- custom username, which might be provided by yaml file will become
  default-user if absent.

All of that is the consequence, that by providing custom qcow2 image
there is no easy way to determine what operating system is passed by,
therefore it is purely declarative way of creating VM with such image.
2022-11-17 20:27:58 +01:00
706dfe8688 Decrease memory/disk size of default vm 2022-11-17 19:22:16 +01:00
8252e189cc Fix issue with condition for extra data 2022-11-16 09:11:27 +01:00
e6d4d8ab7a Fix minor issue with pattern for centos image 2022-11-16 09:10:49 +01:00
b7b4ba5cbc Readme update 2022-11-15 20:33:10 +01:00
47766b6cd9 Added ability to point to local qcow2 image.
Instead of downloading image from the network, there is a way to point
out cloud user and image within yaml configuration.
2022-11-15 20:27:14 +01:00
55cb8d5e30 Better messagingn in conf/modules vbox issues 2022-10-16 08:52:55 +02:00
276ddd8681 Added bash completion for start/stop commands. 2022-05-23 12:02:58 +02:00
0fb0d64db6 Added two new commands for starting and stopping VMs. 2022-05-23 12:02:28 +02:00
847279a990 Align case for command help messages. 2022-05-23 12:01:59 +02:00
38ed618b5b Added info if vm is running.
Also, added command for acpi shutdown.
2022-05-23 12:00:55 +02:00
1c39cd1985 Add ability to set different default username.
During cloud init user might want to change default user from the
distribution to something else. It will not be possible to ssh to such
machine using boxpy ssh command - only using ssh directly with the
correct user name would be possible. In this change there is added
possibility for setting username in boxpy_data section with desired
value to be used in ssh command.
2022-04-22 07:53:50 +02:00
ed25a0d208 Fix typo in message. 2022-04-22 07:53:04 +02:00
20120d898a Add error message in case of wrong config 2022-04-22 07:52:22 +02:00
e63d83fc7f Don't clutter current directory with checksum files.
In case of Centos, there is a need to get the checksum file first to
figure out the correct image filename, during that process checksum file
was left alone in the current directory. Place it in the temp dir in the
first place and than remove after we know the right image filename.
2022-02-08 20:39:03 +01:00
0093e32b74 Fix for rebuild command and calling vmdestroy function. 2021-11-04 09:26:38 +01:00
353d848072 Pass more than one machine to destroy command.
From now on, there is a possibility to pass more than one machine to be
destroyed.
2021-11-04 08:41:13 +01:00
4581ab0ed0 Specify from which Python version boxpy is supported. 2021-10-19 19:33:15 +02:00
deba0aa621 Narrow down proposed VMs to running only for ssh command. 2021-10-19 19:32:30 +02:00
6528813d6a Update examples for multinode setup 2021-10-19 19:31:18 +02:00
9699e61b35 Exit, if image checksum is wrong. 2021-10-03 19:08:15 +02:00
f46432546e Added ability to add url for write_files section. 2021-10-01 19:15:25 +02:00
fe422576cd Fixed long param for listing VMs. 2021-10-01 19:09:14 +02:00
a7b0984f77 Handle nonexisted VMs for commands. 2021-09-30 21:11:18 +02:00
085785af46 Added sudo to cloud init status command.
Some systems have to have a strict control, even in system state
information. Prefixed with "sudo" to command to gather information how
cloud init is doing.
2021-09-30 21:00:18 +02:00
9288179474 Added support for Centos Stream.
Currently default and only version supported is 8, since 9 behaves
differently with similar config drive.
2021-09-29 17:16:09 +02:00
a5702254ca Removed owner field on examples, since it's in a way. 2021-09-29 17:13:35 +02:00
74053995c8 Refactored Image classes.
There were very similar methods for downloading/proceeding with
checksum for both Ubuntu and Fedora classes. Extracted those two into
methods in base class.
2021-09-29 14:47:03 +02:00
1999f1dc7e Fixed checking existence of func in args. 2021-09-26 18:53:08 +02:00
7f99f91933 Fail on yaml parse errors 2021-09-02 18:36:57 +02:00
db8a42518e Rephrase help for VM type option. 2021-09-02 09:27:34 +02:00
c19f4f1a61 Update devstack deployment examples. 2021-08-20 08:24:50 +02:00
d7544f52f6 Added run type option to create and rebuild subcommand.
For now, the only mode VirtualBox VM has launched was headless. For
debugging purposes, there were other types allowed using --type switch
for command create and rebuild, while headless will remain the default
one.
2021-08-19 21:09:06 +02:00
3c43263bb9 Fix clashing version from boxpy and subcommands.
Currently, you could pass long version of '--version' option to either
boxpy itself and subcommands create and rebuild. With this patch proper
context is now detected.
2021-08-19 21:08:34 +02:00
8 changed files with 641 additions and 207 deletions

View File

@@ -2,8 +2,8 @@
box.py box.py
====== ======
Box.py is a simple automation tool meant to run Ubuntu or Fedora cloud images Box.py is a simple automation tool meant to run Ubuntu, Fedora, Centos Stream
on top of VirtualBox. or Debian cloud images on top of VirtualBox.
What it does is simply download official cloud image, set up VM, tweak it up What it does is simply download official cloud image, set up VM, tweak it up
and do the initial pre-configuration using generated config drive. and do the initial pre-configuration using generated config drive.
@@ -16,14 +16,15 @@ weird named options for ``vboxmanage`` ;P)
Requirements Requirements
------------ ------------
- Python 3.x - Python >=3.8
- `pyyaml`_ - `pyyaml`_
- `requests`_
- Virtualbox (obviously) - Virtualbox (obviously)
- ``mkisofs`` or ``genisoimage`` command for generating iso image - ``mkisofs`` or ``genisoimage`` command for generating ISO image
- ``wget`` command for fetching images - ``wget`` command for fetching images
- ``sha256sum`` command for checksum check - ``sha256sum`` and ``sha512sum`` commands for checksum check
- ``qemu-img`` from *qemu-utils* package command for converting between images - ``qemu-img`` from *qemu-utils* package command for converting between images
formats formats
@@ -38,7 +39,7 @@ your operating system, or by using virtualenv for Python requirements, i.e.:
$ python -m virtualenv .venv $ python -m virtualenv .venv
$ . .venv/bin/activate $ . .venv/bin/activate
(.venv) $ pip install requirements.txt (.venv) $ pip install -r requirements.txt
then you can issue: then you can issue:
@@ -54,7 +55,7 @@ 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, 2GB of memory and 10GB of disk: 18.04 with one CPU, 1GB of memory and 6GB of disk:
.. code:: shell-session .. code:: shell-session
@@ -90,17 +91,23 @@ use it ad-hoc, or place on your ``.bashrc`` or whatever:
Currently, following commands are available: Currently, following commands are available:
- ``list`` - for quickly listing all/running VMs - ``completion`` - as described above
- ``info`` - to get summary about VM
- ``destroy`` - that is probably obvious one
- ``create`` - create new VM - ``create`` - create new VM
- ``destroy`` - that is probably obvious one
- ``info`` - to get summary about VM
- ``list`` - for quickly listing all/running VMs
- ``rebuild`` - recreate specified VM - ``rebuild`` - recreate specified VM
- ``ssh`` - connect to the VM using ssh - ``ssh`` - connect to the VM using ssh
- ``completion`` - as described above - ``start`` - stop the running VM
- ``stop`` - start stopped VM
All of the commands have a range of options, and can be examined by using All of the commands have a range of options, and can be examined by using
``--help`` option. ``--help`` option.
YAML Configuration
------------------
What is more interesting though, is the fact, that you can pass your own What is more interesting though, is the fact, that you can pass your own
`cloud-init`_ yaml file, so that VM can be provisioned in easy way. `cloud-init`_ yaml file, so that VM can be provisioned in easy way.
@@ -140,11 +147,20 @@ pass filenames to the custom config, instead of filling up
permissions: '0644' permissions: '0644'
filename: /path/to/local/file.txt filename: /path/to/local/file.txt
during processing this file, boxpy will look for ``filename`` key in the yaml or
file for the ``write_files`` sections, and it will remove that key, read the
file and put its contents under ``content`` key. What is more important, that .. code:: yaml
will be done after template processing, so there will be no interference for
possible ``$`` characters. write_files:
- path: /opt/somefile.txt
permissions: '0644'
url: https://some.url/content
during processing this file, boxpy will look for ``filename`` or ``url`` keys
in the yaml file for the ``write_files`` sections, and it will remove that key,
read the file and put its contents under ``content`` key. What is more
important, that will be done after template processing, so there will be no
interference for possible ``$`` characters.
What is more interesting is the fact, that you could use whatever cloud-init What is more interesting is the fact, that you could use whatever cloud-init
accepts, and a special section, for keeping configuration, so that you don't accepts, and a special section, for keeping configuration, so that you don't
@@ -193,6 +209,19 @@ configuration additional NIC for virtual machine, i.e:
advanced: advanced:
nic2: intnet nic2: intnet
To select image from local file system, it is possible to set one by providing
it under ``boxpy_data.image`` key:
.. code:: yaml
…
boxpy_data:
image: /path/to/the/qcow2/image
default_user: cloud-user
Note, that default_user is also needed to be provided, as there is no guess,
what is the default username for cloud-init configured within provided image.
License License
------- -------
@@ -202,3 +231,4 @@ This work is licensed under GPL-3.
.. _pyyaml: https://github.com/yaml/pyyaml .. _pyyaml: https://github.com/yaml/pyyaml
.. _cloud-init: https://cloudinit.readthedocs.io .. _cloud-init: https://cloudinit.readthedocs.io
.. _requests: https://docs.python-requests.org

572
box.py
View File

@@ -4,6 +4,7 @@ import argparse
import collections.abc import collections.abc
import os import os
import random import random
import re
import shutil import shutil
import string import string
import subprocess import subprocess
@@ -13,16 +14,19 @@ import time
import uuid import uuid
import xml.dom.minidom import xml.dom.minidom
import requests
import yaml import yaml
__version__ = "1.0" __version__ = "1.8"
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'} FEDORA_RELEASE_MAP = {'32': '1.6', '33': '1.2', '34': '1.2'}
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'}
META_DATA_TPL = string.Template('''\ META_DATA_TPL = string.Template('''\
instance-id: $instance_id instance-id: $instance_id
local-hostname: $vmhostname local-hostname: $vmhostname
@@ -43,9 +47,9 @@ ssh:
emit_keys_to_console: false emit_keys_to_console: false
boxpy_data: boxpy_data:
cpus: 1 cpus: 1
disk_size: 10240 disk_size: 6144
key: ~/.ssh/id_rsa key: ~/.ssh/id_rsa
memory: 2048 memory: 1024
''' '''
COMPLETIONS = {'bash': '''\ COMPLETIONS = {'bash': '''\
_boxpy() { _boxpy() {
@@ -122,7 +126,7 @@ _boxpy() {
fi fi
fi fi
opts="create destroy rebuild info list completion ssh" opts="create destroy rebuild info list completion ssh start stop"
if [[ ${cur} == "-q" || ${cur} == "-v" || ${COMP_CWORD} -eq 1 ]] ; then if [[ ${cur} == "-q" || ${cur} == "-v" || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
return 0 return 0
@@ -135,8 +139,9 @@ _boxpy() {
fi fi
;; ;;
create|rebuild) create|rebuild)
items=(--cpus --disable-nested --disk-size --distro --forwarding items=(--cpus --disable-nested --disk-size --default-user --distro
--key --memory --hostname --port --config --version) --forwarding --image --key --memory --hostname --port --config
--version --type)
if [[ ${prev} == ${cmd} ]]; then if [[ ${prev} == ${cmd} ]]; then
if [[ ${cmd} = "rebuild" ]]; then if [[ ${cmd} = "rebuild" ]]; then
_vms_comp vms _vms_comp vms
@@ -156,7 +161,12 @@ _boxpy() {
_ssh_identityfile _ssh_identityfile
;; ;;
--distro) --distro)
COMPREPLY=( $(compgen -W "ubuntu fedora" -- ${cur}) ) COMPREPLY=( $(compgen -W "ubuntu fedora centos
debian" -- ${cur}) )
;;
--type)
COMPREPLY=( $(compgen -W "gui headless sdl separate" \
-- ${cur}) )
;; ;;
--*) --*)
COMPREPLY=( ) COMPREPLY=( )
@@ -165,21 +175,36 @@ _boxpy() {
fi fi
;; ;;
destroy|info) info)
if [[ ${prev} == ${cmd} ]]; then if [[ ${prev} == ${cmd} ]]; then
_vms_comp vms _vms_comp vms
fi fi
;; ;;
destroy)
_vms_comp vms
_get_excluded_items "${COMPREPLY[@]}"
COMPREPLY=( $(compgen -W "$result" -- ${cur}) )
;;
list) list)
items=(--long --running --run-by-boxpy) items=(--long --running --run-by-boxpy)
_get_excluded_items "${items[@]}" _get_excluded_items "${items[@]}"
COMPREPLY=( $(compgen -W "$result" -- ${cur}) ) COMPREPLY=( $(compgen -W "$result" -- ${cur}) )
;; ;;
ssh) ssh)
if [[ ${prev} == ${cmd} ]]; then
_vms_comp runningvms
fi
;;
start)
if [[ ${prev} == ${cmd} ]]; then if [[ ${prev} == ${cmd} ]]; then
_vms_comp vms _vms_comp vms
fi fi
;; ;;
stop)
if [[ ${prev} == ${cmd} ]]; then
_vms_comp runningvms
fi
;;
esac esac
} }
@@ -244,6 +269,10 @@ class BoxVBoxFailure(BoxError):
pass pass
class BoxConfError(BoxError):
pass
class FakeLogger: class FakeLogger:
""" """
print based "logger" class. I like to use 'end' parameter of print print based "logger" class. I like to use 'end' parameter of print
@@ -335,23 +364,26 @@ class FakeLogger:
class Config: class Config:
ATTRS = ('cpus', 'config', 'creator', 'disable_nested', 'disk_size', ATTRS = ('cpus', 'config', 'creator', 'disable_nested', 'disk_size',
'distro', 'forwarding', 'hostname', 'key', 'memory', 'name', 'distro', 'default_user', 'forwarding', 'hostname', 'image',
'port', 'version') 'key', 'memory', 'name', 'port', 'version', 'username')
def __init__(self, args, vbox=None): def __init__(self, args, vbox=None):
self.advanced = None self.advanced = None
self.distro = None self.distro = None
self.default_user = None
self.cpus = None self.cpus = None
self.creator = None self.creator = None
self.disable_nested = 'False' self.disable_nested = 'False'
self.disk_size = None self.disk_size = None
self.forwarding = {} self.forwarding = {}
self.hostname = None self.hostname = None
self.image = None
self.key = None self.key = None
self.memory = None self.memory = None
self.name = args.name # this one is not stored anywhere self.name = args.name # this one is not stored anywhere
self.port = None # at least is not even tried to be retrieved self.port = None # at least is not even tried to be retrieved
self.version = None self.version = None
self.username = None
self._conf = {} self._conf = {}
# set defaults stored in hard coded yaml # set defaults stored in hard coded yaml
@@ -388,6 +420,10 @@ class Config:
continue continue
setattr(self, attr, str(val)) setattr(self, attr, str(val))
# sort out case, where there is image/default-user provided
if self.image:
self._update_distros_with_custom_image()
# set distribution and version if not specified by user # set distribution and version if not specified by user
if not self.distro: if not self.distro:
self.distro = 'ubuntu' self.distro = 'ubuntu'
@@ -416,20 +452,31 @@ class Config:
if conf.get('write_files'): if conf.get('write_files'):
new_list = [] new_list = []
for file_data in conf['write_files']: for file_data in conf['write_files']:
content = None
fname = file_data.get('filename') fname = file_data.get('filename')
if not fname: url = file_data.get('url')
if not any((fname, url)):
new_list.append(file_data) new_list.append(file_data)
continue continue
fname = os.path.expanduser(os.path.expandvars(fname)) if fname:
if not os.path.exists(fname): key = 'filename'
LOG.warning("File '%s' doesn't exists", content = self._read_filename(fname)
file_data['filename']) if content is None:
LOG.warning("File '%s' doesn't exists", fname)
continue continue
with open(fname) as fobj: if url:
file_data['content'] = fobj.read() key = 'url'
del file_data['filename'] code, content = self._get_url(url)
if content is None:
LOG.warning("Getting url '%s' returns %s code",
url, code)
continue
if content:
file_data['content'] = content
del file_data[key]
new_list.append(file_data) new_list.append(file_data)
conf['write_files'] = new_list conf['write_files'] = new_list
@@ -437,6 +484,20 @@ class Config:
# 3. finally dump it again. # 3. finally dump it again.
return "#cloud-config\n" + yaml.safe_dump(conf) return "#cloud-config\n" + yaml.safe_dump(conf)
def _get_url(self, url):
response = requests.get(url)
if response.status_code != 200:
return response.status_code, None
return response.status_code, response.text
def _read_filename(self, fname):
fullpath = os.path.expanduser(os.path.expandvars(fname))
if not os.path.exists(fullpath):
return
with open(fname) as fobj:
return fobj.read()
def _set_ssh_key_path(self): def _set_ssh_key_path(self):
self.ssh_key_path = self.key self.ssh_key_path = self.key
@@ -449,7 +510,7 @@ class Config:
self.ssh_key_path = os.path.join(os.path.expanduser("~/.ssh"), self.ssh_key_path = os.path.join(os.path.expanduser("~/.ssh"),
self.ssh_key_path) self.ssh_key_path)
if not os.path.exists(self.ssh_key_path): if not os.path.exists(self.ssh_key_path):
raise BoxNotFound(f'Cannot find ssh public key: {self.key}') raise BoxConfError(f'Cannot find ssh public key: {self.key}')
def _set_defaults(self): def _set_defaults(self):
conf = yaml.safe_load(USER_DATA) conf = yaml.safe_load(USER_DATA)
@@ -504,6 +565,18 @@ class Config:
self._conf = conf self._conf = conf
def _update_distros_with_custom_image(self):
self.image = os.path.abspath(self.image)
self.distro = 'custom'
if not self.username:
self.username = self.default_user
DISTROS['custom'] = {'username': self.default_user,
'realname': 'custom os',
'img_class': CustomImage,
'amd64': 'x86_64',
'image': self.image,
'default_version': '0'}
def _update(self, source, update): def _update(self, source, update):
for key, val in update.items(): for key, val in update.items():
if isinstance(val, collections.abc.Mapping): if isinstance(val, collections.abc.Mapping):
@@ -513,6 +586,48 @@ class Config:
return source return source
class OsTypes:
def __init__(self, conf):
self._conf = conf
self._ostypes = []
self._gather_os_types()
def _gather_os_types(self):
out = Run(['vboxmanage', 'list', 'ostypes']).stdout
for line in out.split('\n'):
if not line.startswith('ID:'):
continue
self._ostypes.append(line.split(':')[1].strip())
def ubuntu(self):
lts = ''
major, minor = [int(x) for x in self._conf.version.split('.')]
if major % 2 == 0 and minor == 4:
lts = '_LTS'
name = "Ubuntu%s%s_64" % (major, lts)
if name not in self._ostypes:
return 'Ubuntu_64'
return name
def fedora(self):
return "Fedora_64"
def debian(self):
name = "Debian%s_64" % self._conf.version
if name not in self._ostypes:
return 'Debian_64'
def get(self):
if not hasattr(self, self._conf.distro):
return "Linux_64"
return getattr(self, self._conf.distro)()
class VBoxManage: class VBoxManage:
""" """
Class for dealing with vboxmanage commands Class for dealing with vboxmanage commands
@@ -521,6 +636,7 @@ class VBoxManage:
self.name_or_uuid = name_or_uuid self.name_or_uuid = name_or_uuid
self.vm_info = {} self.vm_info = {}
self.uuid = None self.uuid = None
self.running = False
def get_vm_base_path(self): def get_vm_base_path(self):
path = self._get_vm_config() path = self._get_vm_config()
@@ -572,6 +688,9 @@ class VBoxManage:
if line.startswith('Config file:'): if line.startswith('Config file:'):
self.vm_info['config_file'] = line.split('Config ' self.vm_info['config_file'] = line.split('Config '
'file:')[1].strip() 'file:')[1].strip()
if line.startswith('State:'):
self.running = line.split(':')[1].strip().startswith('running')
break break
dom = xml.dom.minidom.parse(self.vm_info['config_file']) dom = xml.dom.minidom.parse(self.vm_info['config_file'])
@@ -615,6 +734,9 @@ class VBoxManage:
def poweroff(self): def poweroff(self):
Run(['vboxmanage', 'controlvm', self.name_or_uuid, 'poweroff']) Run(['vboxmanage', 'controlvm', self.name_or_uuid, 'poweroff'])
def acpipowerbutton(self):
Run(['vboxmanage', 'controlvm', self.name_or_uuid, 'acpipowerbutton'])
def vmlist(self, only_running=False, long_list=False, only_boxpy=False): def vmlist(self, only_running=False, long_list=False, only_boxpy=False):
subcommand = 'runningvms' if only_running else 'vms' subcommand = 'runningvms' if only_running else 'vms'
machines = {} machines = {}
@@ -630,7 +752,7 @@ class VBoxManage:
continue continue
if long_list: if long_list:
info = "\n".join(Run(['vboxmanage', 'showvminfo', info = "\n".join(Run(['vboxmanage', 'showvminfo',
info]).stdout.split('\n')) name]).stdout.split('\n'))
machines[name] = info machines[name] = info
return machines return machines
@@ -664,6 +786,12 @@ class VBoxManage:
LOG.fatal('Failed to create VM:\n%s', out.stderr) LOG.fatal('Failed to create VM:\n%s', out.stderr)
return None return None
if out.stdout.startswith('WARNING:'):
LOG.fatal('Created crippled VM:\n%s\nFix the issue with '
'VirtualBox, remove the dead VM and start over.',
out.stdout)
return None
for line in out.stdout.split('\n'): for line in out.stdout.split('\n'):
if line.startswith('UUID:'): if line.startswith('UUID:'):
self.uuid = line.split('UUID:')[1].strip() self.uuid = line.split('UUID:')[1].strip()
@@ -680,7 +808,11 @@ class VBoxManage:
'--acpi', 'on', '--acpi', 'on',
'--audio', 'none', '--audio', 'none',
'--nic1', 'nat', '--nic1', 'nat',
'--natpf1', f'boxpyssh,tcp,,{port},,22'] '--natpf1', f'boxpyssh,tcp,,{port},,22',
'--graphicscontroller', 'vmsvga',
'--vram', '16',
'--ostype', OsTypes(conf).get()]
for count, (hostport, vmport) in enumerate(conf.forwarding.items(), for count, (hostport, vmport) in enumerate(conf.forwarding.items(),
start=1): start=1):
cmd.extend(['--natpf1', f'custom-pf-{count},tcp,,{hostport},' cmd.extend(['--natpf1', f'custom-pf-{count},tcp,,{hostport},'
@@ -751,9 +883,9 @@ class VBoxManage:
return False return False
return True return True
def poweron(self): def poweron(self, type_='headless'):
if Run(['vboxmanage', 'startvm', self.name_or_uuid, '--type', if Run(['vboxmanage', 'startvm', self.name_or_uuid, '--type',
'headless']).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()
@@ -829,11 +961,12 @@ class VBoxManage:
class Image: class Image:
URL = "" URL = ""
IMG = "" IMG = ""
CHECKSUMTOOL = 'sha256sum'
def __init__(self, vbox, version, arch, release): def __init__(self, vbox, version, arch, release, fname=None):
self.vbox = vbox self.vbox = vbox
self._tmp = tempfile.mkdtemp(prefix='boxpy_') self._tmp = tempfile.mkdtemp(prefix='boxpy_')
self._img_fname = None self._img_fname = fname
def convert_to_vdi(self, disk_img, size): def convert_to_vdi(self, disk_img, size):
LOG.info('Converting and resizing "%s", new size: %s', disk_img, size) LOG.info('Converting and resizing "%s", new size: %s', disk_img, size)
@@ -861,21 +994,6 @@ class Image:
return False return False
return True return True
def _download_image(self):
raise NotImplementedError()
class Ubuntu(Image):
URL = "https://cloud-images.ubuntu.com/releases/%s/release/%s"
IMG = "ubuntu-%s-server-cloudimg-%s.img"
def __init__(self, vbox, version, arch, release):
super().__init__(vbox, version, arch, release)
self._img_fname = self.IMG % (version, arch)
self._img_url = self.URL % (version, self._img_fname)
self._checksum_file = 'SHA256SUMS'
self._checksum_url = self.URL % (version, self._checksum_file)
def _checksum(self): def _checksum(self):
""" """
Get and check checkusm for downloaded image. Return True if the Get and check checkusm for downloaded image. Return True if the
@@ -886,22 +1004,15 @@ class Ubuntu(Image):
return False return False
LOG.info('Calculating checksum for "%s"', self._img_fname) LOG.info('Calculating checksum for "%s"', self._img_fname)
expected_sum = None
fname = os.path.join(self._tmp, self._checksum_file) fname = os.path.join(self._tmp, self._checksum_file)
Run(['wget', self._checksum_url, '-q', '-O', fname]) expected_sum = self._get_checksum(fname)
with open(fname) as fobj:
for line in fobj.readlines():
if self._img_fname in line:
expected_sum = line.split(' ')[0]
break
if not expected_sum: if not expected_sum:
LOG.fatal('Cannot find checksum for provided cloud image') LOG.fatal('Cannot find checksum for provided cloud image')
return False return False
if os.path.exists(os.path.join(CACHE_DIR, self._img_fname)): if os.path.exists(os.path.join(CACHE_DIR, self._img_fname)):
cmd = ['sha256sum', os.path.join(CACHE_DIR, self._img_fname)] cmd = [self.CHECKSUMTOOL, os.path.join(CACHE_DIR, self._img_fname)]
calulated_sum = Run(cmd).stdout.split(' ')[0] calulated_sum = Run(cmd).stdout.split(' ')[0]
LOG.details('Checksum for image: %s, expected: %s', calulated_sum, LOG.details('Checksum for image: %s, expected: %s', calulated_sum,
expected_sum) expected_sum)
@@ -915,7 +1026,8 @@ class Ubuntu(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', self._img_fname) LOG.header('Downloading image %s from %s', self._img_fname,
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():
@@ -926,6 +1038,56 @@ class Ubuntu(Image):
LOG.header('Downloaded image %s', self._img_fname) LOG.header('Downloaded image %s', self._img_fname)
return True return True
def _get_checksum(self, fname):
raise NotImplementedError()
class Ubuntu(Image):
URL = "https://cloud-images.ubuntu.com/releases/%s/release/%s"
IMG = "ubuntu-%s-server-cloudimg-%s.img"
def __init__(self, vbox, version, arch, release, fname=None):
super().__init__(vbox, version, arch, release)
self._img_fname = self.IMG % (version, arch)
self._img_url = self.URL % (version, self._img_fname)
self._checksum_file = 'SHA256SUMS'
self._checksum_url = self.URL % (version, self._checksum_file)
def _get_checksum(self, fname):
expected_sum = None
Run(['wget', self._checksum_url, '-q', '-O', fname])
with open(fname) as fobj:
for line in fobj.readlines():
if self._img_fname in line:
expected_sum = line.split(' ')[0]
break
return expected_sum
class Debian(Image):
URL = "https://cloud.debian.org/images/cloud/%s/daily/latest/%s"
IMG = "debian-%s-generic-%s-daily.qcow2"
CHECKSUMTOOL = 'sha512sum'
def __init__(self, vbox, version, arch, release, fname=None):
super().__init__(vbox, version, arch, release)
self._img_fname = self.IMG % (version, arch)
self._img_url = self.URL % (release, self._img_fname)
self._checksum_file = 'SHA512SUMS'
self._checksum_url = self.URL % (release, self._checksum_file)
def _get_checksum(self, fname):
expected_sum = None
Run(['wget', self._checksum_url, '-q', '-O', fname])
with open(fname) as fobj:
for line in fobj.readlines():
if self._img_fname in line:
expected_sum = line.split(' ')[0]
break
return expected_sum
class Fedora(Image): class Fedora(Image):
URL = ("https://download.fedoraproject.org/pub/fedora/linux/releases/%s/" URL = ("https://download.fedoraproject.org/pub/fedora/linux/releases/%s/"
@@ -933,25 +1095,15 @@ class Fedora(Image):
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"
def __init__(self, vbox, version, arch, release): def __init__(self, vbox, version, arch, release, fname=None):
super().__init__(vbox, version, arch, release) super().__init__(vbox, version, arch, release)
self._img_fname = self.IMG % (version, release, arch) self._img_fname = self.IMG % (version, release, 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, release, arch)
self._checksum_url = self.URL % (version, arch, self._checksum_file) self._checksum_url = self.URL % (version, arch, self._checksum_file)
def _checksum(self): def _get_checksum(self, fname):
"""
Get and check checkusm for downloaded image. Return True if the
checksum is correct, False otherwise.
"""
if not os.path.exists(os.path.join(CACHE_DIR, self._img_fname)):
LOG.debug('Image %s not downloaded yet', self._img_fname)
return False
LOG.info('Calculating checksum for "%s"', self._img_fname)
expected_sum = None expected_sum = None
fname = os.path.join(self._tmp, self._checksum_file)
Run(['wget', self._checksum_url, '-q', '-O', fname]) Run(['wget', self._checksum_url, '-q', '-O', fname])
with open(fname) as fobj: with open(fname) as fobj:
@@ -961,34 +1113,63 @@ class Fedora(Image):
if self._img_fname in line: if self._img_fname in line:
expected_sum = line.split('=')[1].strip() expected_sum = line.split('=')[1].strip()
break break
return expected_sum
if not expected_sum:
LOG.fatal('Cannot find checksum for provided cloud image')
return False
if os.path.exists(os.path.join(CACHE_DIR, self._img_fname)): class CentosStream(Image):
cmd = ['sha256sum', os.path.join(CACHE_DIR, self._img_fname)] URL = "https://cloud.centos.org/centos/%s-stream/%s/images/%s"
calulated_sum = Run(cmd).stdout.split(' ')[0] IMG = '.*(CentOS-Stream-GenericCloud-%s-[0-9]+.[0-9].%s.qcow2).*'
LOG.details('Checksum for image: %s, expected: %s', calulated_sum, CHKS = "CHECKSUM"
expected_sum)
return calulated_sum == expected_sum
return False def __init__(self, vbox, version, arch, release, fname=None):
super().__init__(vbox, version, arch, release)
self._checksum_file = '%s-centos-stream-%s-%s' % (self.CHKS, version,
arch)
self._checksum_url = self.URL % (version, arch, self.CHKS)
# there is assumption, that we always need latest relese for specific
# version and architecture.
self._img_fname = self._get_image_name(version, arch)
self._img_url = self.URL % (version, arch, self._img_fname)
def _get_image_name(self, version, arch):
fname = os.path.join(self._tmp, self._checksum_file)
Run(['wget', self._checksum_url, '-q', '-O', fname])
pat = re.compile(self.IMG % (version, arch))
images = []
with open(fname) as fobj:
for line in fobj.read().strip().split('\n'):
line = line.strip()
if line.startswith('#'):
continue
match = pat.match(line)
if match and match.groups():
images.append(match.groups()[0])
Run(['rm', fname])
images.reverse()
if images:
return images[0]
def _get_checksum(self, fname):
expected_sum = None
Run(['wget', self._checksum_url, '-q', '-O', fname])
with open(fname) as fobj:
for line in fobj.readlines():
if line.startswith('#'):
continue
if self._img_fname in line:
expected_sum = line.split('=')[1].strip()
break
return expected_sum
class CustomImage(Image):
def _download_image(self): def _download_image(self):
if self._checksum(): # just use provided image
LOG.details('Image already downloaded: %s', self._img_fname)
return True
fname = os.path.join(CACHE_DIR, self._img_fname)
Run(['wget', '-q', self._img_url, '-O', fname])
if not self._checksum():
# TODO: make some retry mechanism?
LOG.fatal('Checksum for downloaded image differ from expected')
return False
LOG.header('Downloaded image %s', self._img_fname)
return True return True
@@ -996,20 +1177,32 @@ DISTROS = {'ubuntu': {'username': 'ubuntu',
'realname': 'ubuntu', 'realname': 'ubuntu',
'img_class': Ubuntu, 'img_class': Ubuntu,
'amd64': 'amd64', 'amd64': 'amd64',
'default_version': '20.04'}, 'default_version': '22.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': '34'}} 'default_version': '34'},
'centos': {'username': 'centos',
'realname': 'centos',
'img_class': CentosStream,
'amd64': 'x86_64',
'default_version': '8'},
'debian': {'username': 'debian',
'realname': 'debian',
'img_class': Debian,
'amd64': 'amd64',
'default_version': '11'}}
def get_image_object(vbox, version, image='ubuntu', arch='amd64'): def get_image_object(vbox, version, image='ubuntu', arch='amd64'):
release = None release = None
if image == 'fedora': if image == 'fedora':
release = FEDORA_RELEASE_MAP[version] 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) release, DISTROS[image].get('image'))
class IsoImage: class IsoImage:
@@ -1060,8 +1253,13 @@ def vmcreate(args, conf=None):
if not conf: if not conf:
try: try:
conf = Config(args) conf = Config(args)
except BoxNotFound: except BoxConfError as err:
LOG.fatal(f'Configuration error: {err.args[0]}.')
return 7 return 7
except yaml.YAMLError:
LOG.fatal(f'Cannot read or parse file `{args.config}` as YAML '
f'file')
return 14
LOG.header('Creating VM: %s', conf.name) LOG.header('Creating VM: %s', conf.name)
vbox = VBoxManage(conf.name) vbox = VBoxManage(conf.name)
@@ -1080,7 +1278,9 @@ def vmcreate(args, conf=None):
if not vbox.create_controller('SATA', 'sata'): if not vbox.create_controller('SATA', 'sata'):
return 4 return 4
for key in ('distro', 'hostname', 'key', 'version'): for key in ('distro', 'hostname', 'key', 'version', 'image', 'username'):
if getattr(conf, key) is None:
continue
if not vbox.setextradata(key, getattr(conf, key)): if not vbox.setextradata(key, getattr(conf, key)):
return 5 return 5
@@ -1094,6 +1294,9 @@ def vmcreate(args, conf=None):
image = get_image_object(vbox, conf.version, image=conf.distro) image = get_image_object(vbox, conf.version, image=conf.distro)
path_to_disk = image.convert_to_vdi(conf.name + '.vdi', conf.disk_size) path_to_disk = image.convert_to_vdi(conf.name + '.vdi', conf.disk_size)
if not path_to_disk:
return 21
iso = IsoImage(conf) iso = IsoImage(conf)
path_to_iso = iso.get_generated_image() path_to_iso = iso.get_generated_image()
if not path_to_iso: if not path_to_iso:
@@ -1109,40 +1312,75 @@ def vmcreate(args, conf=None):
vbox.add_nic(key, val) vbox.add_nic(key, val)
# start the VM and wait for cloud-init to finish # start the VM and wait for cloud-init to finish
vbox.poweron() vbox.poweron(args.type)
# give VBox some time to actually change the state of the VM before query # give VBox some time to actually change the state of the VM before query
time.sleep(3) time.sleep(3)
# than, let's try to see if boostraping process has finished # than, let's try to see if boostraping process has finished
LOG.info('Waiting for cloud init to finish ', end='') LOG.info('Waiting for cloud init to finish ', end='')
username = DISTROS[conf.distro]["username"]
cmd = ['ssh', '-o', 'StrictHostKeyChecking=no', cmd = ['ssh', '-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'UserKnownHostsFile=/dev/null',
'-o', 'ConnectTimeout=2', '-o', 'ConnectTimeout=2',
'-i', conf.ssh_key_path[:-4], '-i', conf.ssh_key_path[:-4],
f'ssh://{DISTROS[conf.distro]["username"]}' f'ssh://{username}@localhost:{vbox.vm_info["port"]}',
f'@localhost:{vbox.vm_info["port"]}', 'cloud-init status'] 'sudo cloud-init status']
try: try:
counter = 0
while True: while True:
out = Run(cmd).stdout out = Run(cmd)
LOG.debug('Out: %s', out) LOG.debug('Out: %s', out.stdout)
if (not out) or ('status' in out and 'running' in out): if (not out.stdout) or ('status' in out.stdout and
'running' in out.stdout):
LOG.info('.', end='') LOG.info('.', end='')
sys.stdout.flush() sys.stdout.flush()
if 'Permission denied (publickey)' in out.stderr:
if conf.username and conf.username != username:
username = conf.username
vbox.setextradata('username', username)
cmd[9] = (f'ssh://{username}'
f'@localhost:{vbox.vm_info["port"]}')
continue
raise PermissionError(f'There is an issue with accessing '
f'VM with ssh for user {username}. '
f'Check output in debug mode.')
time.sleep(3) time.sleep(3)
counter += 1
# TODO: there is something odd with debian cloud images prior
# to 12 (bookworm), as on first run system crashes. In that
# case after ~20 seconds there should already be panic, reset
# machine as a workaround. Remove this after debian 12
# stabilization later this year.
if (counter == 8 and conf.distro == 'debian'
and conf.version != '12'):
LOG.debug('Resetting `%s`, due to the issue with kernel '
'panic on Debian %s the first run', conf.name,
conf.version)
counter += 1
vbox.poweroff()
time.sleep(3)
vbox.poweron(args.type)
continue continue
LOG.info(' done') LOG.info(' done')
break break
out = out.split(':')[1].strip() out = out.stdout.split(':')[1].strip()
if out != 'done': if out != 'done':
cmd = cmd[:-1] cmd = cmd[:-1]
cmd.append('cloud-init status -l') cmd.append('cloud-init status -l')
LOG.warning('Cloud init finished with "%s" status:\n%s', out, LOG.warning('Cloud init finished with "%s" status:\n%s', out,
Run(cmd).stdout) Run(cmd).stdout)
except PermissionError:
LOG.info('\n')
iso.cleanup()
image.cleanup()
vbox.destroy()
raise
except KeyboardInterrupt: except KeyboardInterrupt:
LOG.warning('\nIterrupted, cleaning up') LOG.warning('\nInterrupted, cleaning up')
iso.cleanup() iso.cleanup()
image.cleanup() image.cleanup()
vbox.destroy() vbox.destroy()
@@ -1154,17 +1392,35 @@ def vmcreate(args, conf=None):
# reread config to update fields # reread config to update fields
conf = Config(args, vbox) conf = Config(args, vbox)
username = DISTROS[conf.distro]["username"]
LOG.info('You can access your VM by issuing:') LOG.info('You can access your VM by issuing:')
if conf.username and conf.username != username:
LOG.info(f'ssh -p {conf.port} -i {conf.ssh_key_path[:-4]} ' LOG.info(f'ssh -p {conf.port} -i {conf.ssh_key_path[:-4]} '
f'{DISTROS[conf.distro]["username"]}@localhost') f'{conf.username}@localhost')
else:
LOG.info(f'ssh -p {conf.port} -i {conf.ssh_key_path[:-4]} '
f'{username}@localhost')
LOG.info('or simply:') LOG.info('or simply:')
LOG.info(f'boxpy ssh {conf.name}') LOG.info(f'boxpy ssh {conf.name}')
return 0 return 0
def vmdestroy(args): def vmdestroy(args):
LOG.header('Removing VM: %s', args.name) if isinstance(args.name, list):
return VBoxManage(args.name).destroy() vm_names = args.name
else:
vm_names = [args.name]
for name in vm_names:
vbox = VBoxManage(name)
if not vbox.get_vm_info():
LOG.fatal(f'Cannot remove VM "{name}" - it doesn\'t exists.')
return 18
LOG.header('Removing VM: %s', name)
res = VBoxManage(name).destroy()
if res:
return res
return 0
def vmlist(args): def vmlist(args):
@@ -1182,6 +1438,8 @@ def vmlist(args):
LOG.header('All VMs:') LOG.header('All VMs:')
for key in sorted(vms): for key in sorted(vms):
if args.long:
LOG.header(f"\n{key}")
LOG.info(vms[key]) LOG.info(vms[key])
return 0 return 0
@@ -1190,6 +1448,10 @@ def vmlist(args):
def vminfo(args): def vminfo(args):
vbox = VBoxManage(args.name) vbox = VBoxManage(args.name)
info = vbox.get_vm_info() info = vbox.get_vm_info()
if not info:
LOG.fatal(f'Cannot show details of VM "{args.name}" - '
f'it doesn\'t exists.')
return 19
LOG.header('Details for VM: %s', args.name) LOG.header('Details for VM: %s', args.name)
LOG.info('Creator:\t\t%s', info.get('creator', 'unknown/manual')) LOG.info('Creator:\t\t%s', info.get('creator', 'unknown/manual'))
@@ -1245,12 +1507,22 @@ def vminfo(args):
def vmrebuild(args): def vmrebuild(args):
LOG.header('Rebuilding VM: %s', args.name)
vbox = VBoxManage(args.name) vbox = VBoxManage(args.name)
if not vbox.get_vm_info():
LOG.fatal(f'Cannot rebuild VM "{args.name}" - it doesn\'t exists.')
return 20
else:
LOG.header('Rebuilding VM: %s', args.name)
try: try:
conf = Config(args, vbox) conf = Config(args, vbox)
except BoxNotFound: except BoxNotFound as ex:
LOG.fatal(f'Error with parsing config: {ex}')
return 8 return 8
except yaml.YAMLError:
LOG.fatal(f'Cannot read or parse file `{args.config}` as YAML '
f'file')
return 15
vbox.poweroff() vbox.poweroff()
@@ -1269,6 +1541,13 @@ def vmrebuild(args):
conf.disk_size = vbox.get_media_size(disk_path) conf.disk_size = vbox.get_media_size(disk_path)
vmdestroy(args) vmdestroy(args)
# Wait till VM is gone
while True:
vbox = VBoxManage(args.name)
if not vbox.get_vm_info():
break
vmcreate(args, conf) vmcreate(args, conf)
return 0 return 0
@@ -1280,16 +1559,57 @@ def shell_completion(args):
def connect(args): def connect(args):
vbox = VBoxManage(args.name) vbox = VBoxManage(args.name)
if not vbox.get_vm_info():
LOG.fatal(f'No machine has been found with a name `{args.name}`.')
return 17
try: try:
conf = Config(args, vbox) conf = Config(args, vbox)
except BoxNotFound: except BoxNotFound:
return 11 return 11
except yaml.YAMLError:
LOG.fatal(f'Cannot read or parse file `{args.config}` as YAML '
f'file.')
return 16
return Run(['ssh', '-o', 'StrictHostKeyChecking=no', username = conf.username or DISTROS[conf.distro]["username"]
cmd = ['ssh', '-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null', '-o', 'UserKnownHostsFile=/dev/null',
'-i', conf.ssh_key_path[:-4], '-i', conf.ssh_key_path[:-4],
f'ssh://{DISTROS[conf.distro]["username"]}' f'ssh://{username}'
f'@localhost:{conf.port}'], False).returncode f'@localhost:{conf.port}']
LOG.debug('Connecting to vm `%s` using command:\n%s', args.name,
' '.join(cmd))
return Run(cmd, False).returncode
def _set_vmstate(name, state):
vbox = VBoxManage(name)
if not vbox.get_vm_info():
LOG.fatal(f'No machine has been found with a name `{name}`.')
return 20
if vbox.running and state == "start":
LOG.info(f'VM "{name}" is already running.')
return
if not vbox.running and state == "stop":
LOG.info(f'VM "{name}" is already stopped.')
return
if state == "start":
vbox.poweron()
else:
vbox.acpipowerbutton()
def vmstart(args):
_set_vmstate(args.name, 'start')
def vmstop(args):
_set_vmstate(args.name, 'stop')
def main(): def main():
@@ -1319,10 +1639,16 @@ def main():
help="Alternative user-data template filepath") help="Alternative user-data template filepath")
create.add_argument('-d', '--distro', help="Image name. 'ubuntu' is " create.add_argument('-d', '--distro', help="Image name. 'ubuntu' is "
"default") "default")
create.add_argument('-e', '--default-user', help="Default cloud-init user "
"to be used with custom image (--image param). "
"Without image it will make no effect.")
create.add_argument('-f', '--forwarding', action='append', help="expose " create.add_argument('-f', '--forwarding', action='append', help="expose "
"port from VM to the host. It should be in format " "port from VM to the host. It should be in format "
"'hostport:vmport'. this option can be used multiple " "'hostport:vmport'. this option can be used multiple "
"times for multiple ports.") "times for multiple ports.")
create.add_argument('-i', '--image', help="custom qcow2 image filepath. "
"Note, that it requires to provide --default-user as "
"well.")
create.add_argument('-k', '--key', help="SSH key to be add to the config " create.add_argument('-k', '--key', help="SSH key to be add to the config "
"drive. Default ~/.ssh/id_rsa") "drive. Default ~/.ssh/id_rsa")
create.add_argument('-m', '--memory', help="amount of memory in " create.add_argument('-m', '--memory', help="amount of memory in "
@@ -1335,13 +1661,16 @@ def main():
help="disable nested virtualization") help="disable nested virtualization")
create.add_argument('-s', '--disk-size', help="disk size to be expanded " create.add_argument('-s', '--disk-size', help="disk size to be expanded "
"to. By default to 10GB") "to. By default to 10GB")
create.add_argument('-t', '--type', default='headless',
help="VM run type, headless by default.",
choices=['gui', 'headless', 'sdl', 'separate'])
create.add_argument('-u', '--cpus', type=int, help="amount of CPUs to be " create.add_argument('-u', '--cpus', type=int, help="amount of CPUs to be "
"configured. Default 1.") "configured. Default 1.")
create.add_argument('-v', '--version', help=f"distribution version. " create.add_argument('-v', '--version', help=f"distribution version. "
f"Default {DISTROS['ubuntu']['default_version']}") f"Default {DISTROS['ubuntu']['default_version']}")
destroy = subparsers.add_parser('destroy', help='destroy VM') destroy = subparsers.add_parser('destroy', help='destroy VM')
destroy.add_argument('name', help='name or UUID of the VM') destroy.add_argument('name', nargs='+', help='name or UUID of the VM')
destroy.set_defaults(func=vmdestroy) destroy.set_defaults(func=vmdestroy)
list_vms = subparsers.add_parser('list', help='list VMs') list_vms = subparsers.add_parser('list', help='list VMs')
@@ -1354,17 +1683,23 @@ def main():
help='show only running VMs') help='show only running VMs')
list_vms.set_defaults(func=vmlist) list_vms.set_defaults(func=vmlist)
rebuild = subparsers.add_parser('rebuild', help='Rebuild VM, all options ' rebuild = subparsers.add_parser('rebuild', help='rebuild VM, all options '
'besides vm name are optional, and their ' 'besides vm name are optional, and their '
'values will be taken from vm definition.') 'values will be taken from vm definition.')
rebuild.add_argument('name', help='name or UUID of the VM') rebuild.add_argument('name', help='name or UUID of the VM')
rebuild.add_argument('-c', '--config', rebuild.add_argument('-c', '--config',
help="Alternative user-data template filepath") help="Alternative user-data template filepath")
rebuild.add_argument('-d', '--distro', help="Image name.") rebuild.add_argument('-d', '--distro', help="Image name.")
rebuild.add_argument('-e', '--default-user', help="Default cloud-init "
"user to be used with custom image (--image param). "
"Without image it will make no effect.")
rebuild.add_argument('-f', '--forwarding', action='append', help="expose " rebuild.add_argument('-f', '--forwarding', action='append', help="expose "
"port from VM to the host. It should be in format " "port from VM to the host. It should be in format "
"'hostport:vmport'. this option can be used multiple " "'hostport:vmport'. this option can be used multiple "
"times for multiple ports.") "times for multiple ports.")
rebuild.add_argument('-i', '--image', help="custom qcow2 image filepath. "
"Note, that it requires to provide --default-user as "
"well.")
rebuild.add_argument('-k', '--key', rebuild.add_argument('-k', '--key',
help='SSH key to be add to the config drive') help='SSH key to be add to the config drive')
rebuild.add_argument('-m', '--memory', help='amount of memory in ' rebuild.add_argument('-m', '--memory', help='amount of memory in '
@@ -1375,18 +1710,29 @@ def main():
help="disable nested virtualization") help="disable nested virtualization")
rebuild.add_argument('-s', '--disk-size', rebuild.add_argument('-s', '--disk-size',
help='disk size to be expanded to') help='disk size to be expanded to')
rebuild.add_argument('-t', '--type', default='headless',
help="VM run type, headless by default.",
choices=['gui', 'headless', 'sdl', 'separate'])
rebuild.add_argument('-u', '--cpus', type=int, rebuild.add_argument('-u', '--cpus', type=int,
help='amount of CPUs to be configured') help='amount of CPUs to be configured')
rebuild.add_argument('-v', '--version', help='distribution version') rebuild.add_argument('-v', '--version', help='distribution version')
rebuild.set_defaults(func=vmrebuild) rebuild.set_defaults(func=vmrebuild)
start = subparsers.add_parser('start', help='start VM')
start.add_argument('name', help='name or UUID of the VM')
start.set_defaults(func=vmstart)
stop = subparsers.add_parser('stop', help='stop VM')
stop.add_argument('name', help='name or UUID of the VM')
stop.set_defaults(func=vmstop)
completion = subparsers.add_parser('completion', help='generate shell ' completion = subparsers.add_parser('completion', help='generate shell '
'completion') 'completion')
completion.add_argument('shell', choices=['bash'], completion.add_argument('shell', choices=['bash'],
help="pick shell to generate completions for") help="pick shell to generate completions for")
completion.set_defaults(func=shell_completion) completion.set_defaults(func=shell_completion)
ssh = subparsers.add_parser('ssh', help='Connect to the machine via SSH') ssh = subparsers.add_parser('ssh', help='connect to the machine via SSH')
ssh.add_argument('name', help='name or UUID of the VM') ssh.add_argument('name', help='name or UUID of the VM')
ssh.set_defaults(func=connect) ssh.set_defaults(func=connect)
@@ -1396,9 +1742,13 @@ def main():
args = parser.parse_args() args = parser.parse_args()
if 'image' in args and 'default_user' not in args:
parser.error('Parameter --image requires --default-user')
return 22
LOG.set_verbose(args.verbose, args.quiet) LOG.set_verbose(args.verbose, args.quiet)
if args.version: if 'func' not in args and args.version:
LOG.info(f'boxpy {__version__}') LOG.info(f'boxpy {__version__}')
parser.exit() parser.exit()

View File

@@ -1,3 +1,5 @@
# Note, that cloud init will fail, due to old cloudinit package, which module
# cc_keys_to_console doesn't recognize skipping option. Just ignore this error.
package_update: true package_update: true
packages: packages:
- bash-completion - bash-completion
@@ -12,7 +14,6 @@ packages:
write_files: write_files:
- path: /tmp/local.conf - path: /tmp/local.conf
permissions: '0644' permissions: '0644'
owner: fedora:fedora
content: | content: |
[[local|localrc]] [[local|localrc]]
ADMIN_PASSWORD=pass ADMIN_PASSWORD=pass
@@ -33,4 +34,4 @@ boxpy_data:
memory: 4GB memory: 4GB
disk_size: 10GB disk_size: 10GB
distro: fedora distro: fedora
version: 32 version: 34

View File

@@ -19,7 +19,6 @@ packages:
write_files: write_files:
- path: /tmp/local.conf - path: /tmp/local.conf
permissions: '0644' permissions: '0644'
owner: ubuntu:ubuntu
content: | content: |
[[local|localrc]] [[local|localrc]]
ADMIN_PASSWORD=pass ADMIN_PASSWORD=pass
@@ -27,8 +26,10 @@ write_files:
RABBIT_PASSWORD=$$ADMIN_PASSWORD RABBIT_PASSWORD=$$ADMIN_PASSWORD
SERVICE_PASSWORD=$$ADMIN_PASSWORD SERVICE_PASSWORD=$$ADMIN_PASSWORD
runcmd: runcmd:
- [apt, purge, '-y', python3-pyasn1-modules]
- [apt, purge, '-y', python3-simplejson]
- [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"] - [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"]
- [su, -, ubuntu, -c, "vmstrap/bootstrap.sh"] - [su, -, ubuntu, -c, "vmstrap/bootstrap.sh -c"]
- [rm, -fr, /home/ubuntu/vmstrap] - [rm, -fr, /home/ubuntu/vmstrap]
- [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"] - [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"]
- [su, -, ubuntu, -c, "echo 'export HOST_IP=10.0.2.15' >> .bashrc"] - [su, -, ubuntu, -c, "echo 'export HOST_IP=10.0.2.15' >> .bashrc"]

View File

@@ -31,13 +31,10 @@ write_files:
- 192.168.10.10/24 - 192.168.10.10/24
- path: /tmp/local.conf - path: /tmp/local.conf
permissions: '0644' permissions: '0644'
owner: ubuntu:ubuntu
content: | content: |
[[local|localrc]] [[local|localrc]]
disable_all_services disable_all_services
disable_service tls-proxy
# Cinder # Cinder
disable_service c-api disable_service c-api
disable_service c-bak disable_service c-bak
@@ -81,15 +78,20 @@ write_files:
enable_service n-cond enable_service n-cond
enable_service n-cpu enable_service n-cpu
enable_service n-sch enable_service n-sch
# Neutron
enable_service neutron enable_service neutron
enable_service neutron-tag-ports-during-bulk-creation enable_service neutron-tag-ports-during-bulk-creation
# Octavia
enable_service o-api enable_service o-api
enable_service o-cw enable_service o-cw
enable_service o-da
enable_service o-hk enable_service o-hk
enable_service o-hm enable_service o-hm
enable_service octavia enable_service octavia
# OVN # Neutron ovn services
enable_service ovn-controller enable_service ovn-controller
enable_service ovn-northd enable_service ovn-northd
enable_service ovs-vswitchd enable_service ovs-vswitchd
@@ -100,60 +102,83 @@ write_files:
enable_service placement-client enable_service placement-client
# Neutron services # Neutron services
enable_service q-agt
enable_service q-dhcp
enable_service q-l3
enable_service q-meta
enable_service q-ovn-metadata-agent enable_service q-ovn-metadata-agent
enable_service q-qos
enable_service q-svc enable_service q-svc
enable_service q-trunk
enable_service rabbit enable_service rabbit
# Swift
disable_service s-account disable_service s-account
disable_service s-container disable_service s-container
disable_service s-object disable_service s-object
disable_service s-proxy disable_service s-proxy
# Tempest
enable_service tempest enable_service tempest
enable_service tls-proxy
# TLS
disable_service tls-proxy
# Vars # Vars
ADMIN_PASSWORD="secretadmin" ADMIN_PASSWORD="secretadmin"
DATABASE_PASSWORD="secretdatabase" DATABASE_PASSWORD="secretdatabase"
ETCD_USE_RAMDISK="True" ETCD_USE_RAMDISK="True"
KURYR_ENABLED_HANDLERS="vif,endpoints,service,namespace,pod_label,policy,kuryrnetworkpolicy,kuryrnetwork,kuryrport,kuryrloadbalancer" KURYR_ENABLED_HANDLERS="vif,endpoints,service,namespace,pod_label,policy,kuryrnetworkpolicy,kuryrnetwork,kuryrport,kuryrloadbalancer"
KURYR_EP_DRIVER_OCTAVIA_PROVIDER="amphora" KURYR_ENFORCE_SG_RULES="False"
KURYR_K8S_API_PORT="6443" KURYR_EP_DRIVER_OCTAVIA_PROVIDER="ovn"
KURYR_K8S_CLOUD_PROVIDER="False"
KURYR_K8S_CONTAINERIZED_DEPLOYMENT="True"
KURYR_K8S_MULTI_WORKER_TESTS="True" KURYR_K8S_MULTI_WORKER_TESTS="True"
KURYR_K8S_OCTAVIA_MEMBER_MODE="L2"
KURYR_LB_ALGORITHM="SOURCE_IP_PORT"
KURYR_NEUTRON_DEFAULT_ROUTER="router1"
KURYR_SG_DRIVER="policy"
KURYR_SUBNET_DRIVER="namespace"
LOGFILE="/opt/stack/logs/devstacklog.txt" LOGFILE="/opt/stack/logs/devstacklog.txt"
LOG_COLOR="False" LOG_COLOR="False"
ML2_L3_PLUGIN="router" ML2_L3_PLUGIN="ovn-router,trunk,qos"
OCTAVIA_AMP_IMAGE_FILE="/tmp/test-only-amphora-x64-haproxy-ubuntu-bionic.qcow2" OCTAVIA_AMP_IMAGE_FILE="/tmp/test-only-amphora-x64-haproxy-ubuntu-bionic.qcow2"
OCTAVIA_AMP_IMAGE_NAME="test-only-amphora-x64-haproxy-ubuntu-bionic" OCTAVIA_AMP_IMAGE_NAME="test-only-amphora-x64-haproxy-ubuntu-bionic"
OCTAVIA_AMP_IMAGE_SIZE="3" OCTAVIA_AMP_IMAGE_SIZE="3"
Q_AGENT="openvswitch"
Q_ML2_TENANT_NETWORK_TYPE="vxlan" OVN_BRANCH="v20.06.2"
Q_ML2_PLUGIN_MECHANISM_DRIVERS="openvswitch,linuxbridge" OVN_BUILD_FROM_SOURCE="True"
OVN_DBS_LOG_LEVEL="dbg"
OVN_L3_CREATE_PUBLIC_NETWORK="True"
VAR_RUN_PATH="/usr/local/var/run"
RABBIT_PASSWORD="secretrabbit" RABBIT_PASSWORD="secretrabbit"
RECLONE="no" RECLONE="no"
SERVICE_PASSWORD="secretservice" SERVICE_PASSWORD="secretservice"
SERVICE_TOKEN="password" SERVICE_TOKEN="password"
TEMPEST_PLUGINS="/opt/stack/kuryr-tempest-plugin"
USE_PYTHON3="True"
LIBS_FROM_GIT=cinder,devstack,devstack-gate,devstack-plugin-container,glance,keystone,kuryr-kubernetes,kuryr-tempest-plugin,neutron,nova,octavia,placement,python-octaviaclient,requirements,swift,tempest LIBS_FROM_GIT=cinder,devstack,devstack-gate,devstack-plugin-container,glance,keystone,kuryr-kubernetes,kuryr-tempest-plugin,neutron,nova,octavia,octavia-tempest-plugin,ovn-octavia-provider,placement,python-octaviaclient,requirements,swift,tempest
TEMPEST_PLUGINS="/opt/stack/kuryr-tempest-plugin /opt/stack/octavia-tempest-plugin"
# enabled plugins # enabled plugins
enable_plugin devstack-plugin-container https://opendev.org/openstack/devstack-plugin-container enable_plugin devstack-plugin-container https://opendev.org/openstack/devstack-plugin-container
enable_plugin kuryr-kubernetes https://github.com/gryf/kuryr-kubernetes enable_plugin kuryr-kubernetes https://opendev.org/openstack/kuryr-kubernetes
enable_plugin kuryr-tempest-plugin https://opendev.org/openstack/kuryr-tempest-plugin enable_plugin kuryr-tempest-plugin https://opendev.org/openstack/kuryr-tempest-plugin
enable_plugin neutron https://opendev.org/openstack/neutron enable_plugin neutron https://opendev.org/openstack/neutron
enable_plugin octavia https://opendev.org/openstack/octavia enable_plugin octavia https://opendev.org/openstack/octavia
enable_plugin octavia-tempest-plugin https://opendev.org/openstack/octavia-tempest-plugin
enable_plugin ovn-octavia-provider https://opendev.org/openstack/ovn-octavia-provider
[[post-config|$OCTAVIA_CONF]]
[api_settings]
enabled_provider_drivers = amphora:'Octavia Amphora driver',ovn:'Octavia OVN driver'
runcmd: runcmd:
- [apt, purge, '-y', python3-pyasn1-modules]
- [apt, purge, '-y', python3-simplejson]
- [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"] - [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"]
- [su, -, ubuntu, -c, "vmstrap/bootstrap.sh"] - [su, -, ubuntu, -c, "vmstrap/bootstrap.sh -c"]
- [rm, -fr, /home/ubuntu/vmstrap] - [rm, -fr, /home/ubuntu/vmstrap]
- [su, -, ubuntu, -c, "echo 'export HOST_IP=192.168.10.10' >> .bashrc"] - [su, -, ubuntu, -c, "echo 'export HOST_IP=192.168.10.10' >> .bashrc"]
- [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"] - [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"]
- [systemctl, restart, systemd-networkd]
boxpy_data: boxpy_data:
key: vm key: vm
cpus: 4 cpus: 4
@@ -161,3 +186,4 @@ boxpy_data:
disk_size: 50GB disk_size: 50GB
advanced: advanced:
nic2: intnet nic2: intnet
version: 20.04

View File

@@ -31,13 +31,10 @@ write_files:
- 192.168.10.11/24 - 192.168.10.11/24
- path: /tmp/local.conf - path: /tmp/local.conf
permissions: '0644' permissions: '0644'
owner: ubuntu:ubuntu
content: | content: |
[[local|localrc]] [[local|localrc]]
disable_all_services disable_all_services
disable_service tls-proxy
# Cinder # Cinder
disable_service c-bak disable_service c-bak
disable_service c-vol disable_service c-vol
@@ -56,46 +53,66 @@ write_files:
enable_service kuryr-daemon enable_service kuryr-daemon
disable_service kuryr-kubernetes disable_service kuryr-kubernetes
# Nova
enable_service n-cpu enable_service n-cpu
# OVN # Neutron
enable_service neutron
enable_service ovn-controller enable_service ovn-controller
disable_service ovn-northd
enable_service ovn-octavia-provider
enable_service ovs-vswitchd enable_service ovs-vswitchd
enable_service ovsdb-server enable_service ovsdb-server
# Placement API # Placement API
enable_service placement-client enable_service placement-client
# Neutron services # Neutron services cd
enable_service q-ovn-metadata-agent enable_service q-ovn-metadata-agent
disable_service q-svc
# tempest
disable_service tempest disable_service tempest
# tls
disable_service tls-proxy disable_service tls-proxy
# Vars # Vars
ADMIN_PASSWORD="secretadmin" ADMIN_PASSWORD="secretadmin"
DATABASE_HOST="192.168.10.10" DATABASE_HOST="192.168.10.10"
DATABASE_PASSWORD="secretdatabase" DATABASE_PASSWORD="secretdatabase"
ENABLE_CHASSIS_AS_GW="False"
GLANCE_HOSTPORT="192.168.10.10:9292" GLANCE_HOSTPORT="192.168.10.10:9292"
ML2_L3_PLUGIN="router"
# turn on ovn-provider
KURYR_ENFORCE_SG_RULES="False"
KURYR_EP_DRIVER_OCTAVIA_PROVIDER="ovn"
KURYR_K8S_OCTAVIA_MEMBER_MODE="L2"
KURYR_LB_ALGORITHM="SOURCE_IP_PORT"
KURYR_NEUTRON_DEFAULT_ROUTER="router1"
VAR_RUN_PATH="/usr/local/var/run"
KURYR_ENABLED_HANDLERS="vif,endpoints,service,namespace,pod_label,policy,kuryrnetworkpolicy,kuryrnetwork,kuryrport,kuryrloadbalancer" KURYR_ENABLED_HANDLERS="vif,endpoints,service,namespace,pod_label,policy,kuryrnetworkpolicy,kuryrnetwork,kuryrport,kuryrloadbalancer"
KURYR_FORCE_IMAGE_BUILD="True" KURYR_SG_DRIVER="policy"
KURYR_EP_DRIVER_OCTAVIA_PROVIDER="amphora" KURYR_SUBNET_DRIVER="namespace"
KURYR_K8S_API_PORT="6443"
KURYR_K8S_CLOUD_PROVIDER="False"
KURYR_K8S_CONTAINERIZED_DEPLOYMENT="True" OVN_BRANCH="v20.06.2"
OVN_BUILD_FROM_SOURCE="True"
OVN_DBS_LOG_LEVEL="dbg"
OVN_L3_CREATE_PUBLIC_NETWORK="True"
LIBVIRT_TYPE="qemu"
LOGFILE="/opt/stack/logs/devstacklog.txt" LOGFILE="/opt/stack/logs/devstacklog.txt"
LOG_COLOR="False" LOG_COLOR="False"
Q_AGENT="openvswitch" Q_HOST="192.168.10.10"
Q_ML2_TENANT_NETWORK_TYPE="vxlan"
Q_ML2_PLUGIN_MECHANISM_DRIVERS="openvswitch,linuxbridge"
RABBIT_HOST="192.168.10.10" RABBIT_HOST="192.168.10.10"
RABBIT_PASSWORD="secretrabbit" RABBIT_PASSWORD="secretrabbit"
RECLONE="no" RECLONE="no"
SERVICE_HOST="192.168.10.10" SERVICE_HOST="192.168.10.10"
SERVICE_PASSWORD="secretservice" SERVICE_PASSWORD="secretservice"
SERVICE_TOKEN="password" SERVICE_TOKEN="password"
TEMPEST_PLUGINS="/opt/stack/kuryr-tempest-plugin"
USE_PYTHON3="True"
LIBS_FROM_GIT=cinder,devstack,devstack-gate,devstack-plugin-container,glance,keystone,kuryr-kubernetes,kuryr-tempest-plugin,neutron,nova,octavia,placement,python-octaviaclient,requirements,swift,tempest LIBS_FROM_GIT=cinder,devstack,devstack-gate,devstack-plugin-container,glance,keystone,kuryr-kubernetes,kuryr-tempest-plugin,neutron,nova,octavia,placement,python-octaviaclient,requirements,swift,tempest
@@ -103,11 +120,14 @@ write_files:
enable_plugin devstack-plugin-container https://opendev.org/openstack/devstack-plugin-container enable_plugin devstack-plugin-container https://opendev.org/openstack/devstack-plugin-container
enable_plugin kuryr-kubernetes https://opendev.org/openstack/kuryr enable_plugin kuryr-kubernetes https://opendev.org/openstack/kuryr
runcmd: runcmd:
- [apt, purge, '-y', python3-pyasn1-modules]
- [apt, purge, '-y', python3-simplejson]
- [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"] - [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"]
- [su, -, ubuntu, -c, "vmstrap/bootstrap.sh"] - [su, -, ubuntu, -c, "vmstrap/bootstrap.sh -c"]
- [rm, -fr, /home/ubuntu/vmstrap] - [rm, -fr, /home/ubuntu/vmstrap]
- [su, -, ubuntu, -c, "echo 'export HOST_IP=192.168.10.11' >> .bashrc"] - [su, -, ubuntu, -c, "echo 'export HOST_IP=192.168.10.11' >> .bashrc"]
- [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"] - [su, -, ubuntu, -c, "cp /tmp/local.conf /home/ubuntu/devstack/"]
- [systemctl, restart, systemd-networkd]
boxpy_data: boxpy_data:
key: vm key: vm
cpus: 4 cpus: 4
@@ -115,4 +135,4 @@ boxpy_data:
disk_size: 50GB disk_size: 50GB
advanced: advanced:
nic2: intnet nic2: intnet
port: 2223 version: 20.04

View File

@@ -1,5 +1,6 @@
packages: packages:
- build-essential - build-essential
- exuberant-ctags
- gettext - gettext
- libfontconfig1-dev - libfontconfig1-dev
- libgif-dev - libgif-dev
@@ -21,12 +22,16 @@ packages:
- libxrender-dev - libxrender-dev
- libxt-dev - libxt-dev
- make - make
- mc
- sharutils - sharutils
- silversearcher-ag
- tmux
- vim-nox
- xinit - xinit
runcmd: runcmd:
- [su, -, ubuntu, -c, "git clone https://github.com/gryf/wmaker -b experimental"] - [su, -, ubuntu, -c, "git clone https://github.com/gryf/wmaker -b experimental"]
- [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"] - [su, -, ubuntu, -c, "git clone https://github.com/gryf/vmstrap"]
- [su, -, ubuntu, -c, "vmstrap/bootstrap.sh"] - [su, -, ubuntu, -c, "vmstrap/bootstrap.sh -c"]
- [rm, -fr, /home/ubuntu/vmstrap] - [rm, -fr, /home/ubuntu/vmstrap]
boxpy_data: boxpy_data:
key: vm key: vm

View File

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