1
0
mirror of https://github.com/gryf/boxpy.git synced 2025-12-19 21:47:59 +01:00

Added ability for merging options using all possible ways.

Currently specifying attributes of VM was done by using command options
during creation, or only required VM name for rebuilding (due to smart
way of storing information within VM definition).

Now, there is possibility for providing all the information using
special key "boxpy_data" in user-script, so that there is no need to
provide that information from command line.

All three ways are respected with following order:

- default, which are hard coded in defaults
- custom user script passed by --cloud-config (or stored in vm in case
  of rebuild)
- information which is stored in VM definition (if exists - this only
  affects "rebuild" command)
- and finally highest priority have parameters passed by command line.
This commit is contained in:
2021-05-05 20:15:37 +02:00
parent 359d1cf440
commit da1ae93fa2

183
box.py
View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import argparse import argparse
import collections.abc
import os import os
import shutil import shutil
import string import string
@@ -11,6 +12,8 @@ import time
import uuid import uuid
import xml.dom.minidom import xml.dom.minidom
import yaml
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"
@@ -19,7 +22,7 @@ instance-id: $instance_id
local-hostname: $vmhostname local-hostname: $vmhostname
''') ''')
UBUNTU_VERSION = '20.04' UBUNTU_VERSION = '20.04'
USER_DATA_TPL = string.Template('''\ USER_DATA = '''\
#cloud-config #cloud-config
users: users:
- default - default
@@ -34,7 +37,14 @@ power_state:
mode: poweroff mode: poweroff
timeout: 10 timeout: 10
condition: True condition: True
''') boxpy_data:
cpus: 1
disk_size: 10240
key: ~/.ssh/id_rsa
memory: 2048
port: 2222
version: 20.04
'''
COMPLETIONS = {'bash': '''\ COMPLETIONS = {'bash': '''\
_boxpy() { _boxpy() {
local cur prev words cword _GNUSED local cur prev words cword _GNUSED
@@ -205,6 +215,87 @@ class BoxSysCommandError(BoxError):
pass pass
class Config:
ATTRS = ('cpus', 'cloud_config', 'disk_size', 'hostname', 'key',
'memory', 'name', 'port', 'version')
def __init__(self, args, vbox=None):
self.cloud_config = None
self.cpus = None
self.disk_size = None
self.hostname = None
self.key = None
self.memory = None
self.name = None
self.port = None
self.version = None
# first, grab the cloud config file
self._custom_file = args.cloud_config
# initialize default from yaml file(s) first
self._combine_cc(vbox)
# than override all of the attrs with provided args from commandline.
# If the value of rhe
# in case we have vbox object provided.
# this means that we need to read params stored on the VM attributes.
vm_info = vbox.get_vm_info() if vbox else {}
for attr in self.ATTRS:
val = getattr(args, attr, None) or vm_info.get(attr)
if not val:
continue
setattr(self, attr, str(val))
self.hostname = self.hostname or self._normalize_name()
def _normalize_name(self):
name = self.name.replace(' ', '-')
name = name.encode('ascii', errors='ignore')
name = name.decode('utf-8')
return ''.join(x for x in name if x.isalnum() or x == '-')
def _combine_cc(self, vbox):
# that's default config
conf = yaml.safe_load(USER_DATA)
if vbox and not self._custom_file:
# in case of not provided (new) custom cloud config, and vbox
# object is present, read information out of potentially stored
# file in VM attributes.
vm_info = vbox.get_vm_info()
if os.path.exists(vm_info.get('cloud_config')):
self._custom_file = vm_info['cloud_config']
# read user custom cloud config (if present) and update config dict
if self._custom_file and os.path.exists(self._custom_file):
with open(self._custom_file) as fobj:
custom_conf = yaml.safe_load(fobj)
conf = self._update(conf, custom_conf)
# set the attributes.
for key, val in conf.get('boxpy_data', {}).items():
if not val:
continue
setattr(self, key, str(val))
if conf.get('boxpy_data'):
del conf['boxpy_data']
self._conf = "#cloud-config\n" + yaml.safe_dump(conf)
def get_cloud_config_tpl(self):
return string.Template(self._conf)
def _update(self, source, update):
for key, val in update.items():
if isinstance(val, collections.abc.Mapping):
source[key] = self._update(source.get(key, {}), val)
else:
source[key] = val
return source
class VBoxManage: class VBoxManage:
""" """
Class for dealing with vboxmanage commands Class for dealing with vboxmanage commands
@@ -491,10 +582,10 @@ class Image:
class IsoImage: class IsoImage:
def __init__(self, hostname, ssh_key, user_data=None): def __init__(self, conf):
self._tmp = tempfile.mkdtemp() self._tmp = tempfile.mkdtemp()
self.hostname = hostname self.hostname = conf.hostname
self.ssh_key_path = ssh_key self.ssh_key_path = conf.key
if not self.ssh_key_path.endswith('.pub'): if not self.ssh_key_path.endswith('.pub'):
self.ssh_key_path += '.pub' self.ssh_key_path += '.pub'
@@ -502,13 +593,9 @@ class IsoImage:
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 default ssh public key: ' raise BoxNotFound(f'Cannot find ssh public key: {conf.key}')
f'{self.ssh_key_path}')
self.ud_tpl = None self.ud_tpl = conf.get_cloud_config_tpl()
if user_data and os.path.exists(user_data):
with open(user_data) as fobj:
self.ud_tpl = string.Template(fobj.read())
def get_generated_image(self): def get_generated_image(self):
self._create_cloud_image() self._create_cloud_image()
@@ -529,8 +616,7 @@ class IsoImage:
ssh_pub_key = fobj.read().strip() ssh_pub_key = fobj.read().strip()
with open(os.path.join(self._tmp, 'user-data'), 'w') as fobj: with open(os.path.join(self._tmp, 'user-data'), 'w') as fobj:
template = self.ud_tpl or USER_DATA_TPL fobj.write(self.ud_tpl.substitute({'ssh_key': ssh_pub_key}))
fobj.write(template.substitute({'ssh_key': ssh_pub_key}))
mkiso = 'mkisofs' if shutil.which('mkisofs') else 'genisoimage' mkiso = 'mkisofs' if shutil.which('mkisofs') else 'genisoimage'
@@ -544,30 +630,23 @@ class IsoImage:
def vmcreate(args): def vmcreate(args):
vbox = VBoxManage(args.name) conf = Config(args)
if not vbox.create(args.cpus, args.memory, args.port): vbox = VBoxManage(conf.name)
if not vbox.create(conf.cpus, conf.memory, conf.port):
return 10 return 10
vbox.create_controller('IDE', 'ide') vbox.create_controller('IDE', 'ide')
vbox.create_controller('SATA', 'sata') vbox.create_controller('SATA', 'sata')
def normalize_name(name): vbox.setextradata('key', conf.key)
name = name.replace(' ', '-') vbox.setextradata('hostname', conf.hostname)
name = name.encode('ascii', errors='ignore') vbox.setextradata('version', conf.version)
name = name.decode('utf-8') if conf.cloud_config:
return ''.join(x for x in name if x.isalnum() or x == '-') vbox.setextradata('cloud_config', conf.cloud_config)
hostname = args.hostname or normalize_name(args.name) image = Image(vbox, conf.version)
path_to_disk = image.convert_to_vdi(conf.name + '.vdi', conf.disk_size)
vbox.setextradata('key', args.key) iso = IsoImage(conf)
vbox.setextradata('hostname', hostname)
vbox.setextradata('version', args.version)
if args.cloud_config:
vbox.setextradata('cloud_config', args.cloud_config)
image = Image(vbox, args.version)
path_to_disk = image.convert_to_vdi(args.name + '.vdi', args.disk_size)
iso = IsoImage(hostname, args.key, args.cloud_config)
path_to_iso = iso.get_generated_image() path_to_iso = iso.get_generated_image()
vbox.storageattach('SATA', 0, 'hdd', path_to_disk) vbox.storageattach('SATA', 0, 'hdd', path_to_disk)
vbox.storageattach('IDE', 1, 'dvddrive', path_to_iso) vbox.storageattach('IDE', 1, 'dvddrive', path_to_iso)
@@ -615,6 +694,7 @@ def vmlist(args):
def vmrebuild(args): def vmrebuild(args):
vbox = VBoxManage(args.name) vbox = VBoxManage(args.name)
conf = Config(args, vbox)
vbox.poweroff(silent=True) vbox.poweroff(silent=True)
@@ -624,18 +704,8 @@ def vmrebuild(args):
# no disks, return # no disks, return
return 1 return 1
vm_info = vbox.get_vm_info() if not conf.disk_size:
conf.disk_size = vbox.get_media_size(disk_path)
args.cpus = args.cpus or vm_info['cpus']
args.hostname = args.hostname or vm_info['hostname']
args.key = args.key or vm_info['key']
args.memory = args.memory or vm_info['memory']
args.port = args.port or vm_info.get('port')
args.cloud_config = args.cloud_config or vm_info.get('cloud_config')
args.version = args.version or vm_info['version']
if not args.disk_size:
args.disk_size = vbox.get_media_size(disk_path)
vmdestroy(args) vmdestroy(args)
vmcreate(args) vmcreate(args)
@@ -659,25 +729,22 @@ def main():
'drive and run') 'drive and run')
create.set_defaults(func=vmcreate) create.set_defaults(func=vmcreate)
create.add_argument('name', help='name of the VM') create.add_argument('name', help='name of the VM')
create.add_argument('-c', '--cpus', default=1, type=int, create.add_argument('-c', '--cpus', type=int, help="amount of CPUs to be "
help="amount of CPUs to be configured. Default 1.") "configured. Default 1.")
create.add_argument('-d', '--disk-size', default='10240', create.add_argument('-d', '--disk-size', help="disk size to be expanded "
help="disk size to be expanded to. By default to 10GB") "to. By default to 10GB")
create.add_argument('-k', '--key', create.add_argument('-k', '--key', help="SSH key to be add to the config "
default=os.path.expanduser("~/.ssh/id_rsa"), "drive. Default ~/.ssh/id_rsa")
help="SSH key to be add to the config drive. Default " create.add_argument('-m', '--memory', help="amount of memory in "
"~/.ssh/id_rsa") "Megabytes, default 2GB")
create.add_argument('-m', '--memory', default='2048',
help="amount of memory in Megabytes, default 2GB")
create.add_argument('-n', '--hostname', create.add_argument('-n', '--hostname',
help="VM hostname. Default same as vm name") help="VM hostname. Default same as vm name")
create.add_argument('-p', '--port', default='2222', create.add_argument('-p', '--port', help="set ssh port for VM, default "
help="set ssh port for VM, default 2222") "2222")
create.add_argument('-u', '--cloud-config', create.add_argument('-u', '--cloud-config',
help="Alternative user-data template filepath") help="Alternative user-data template filepath")
create.add_argument('-v', '--version', default=UBUNTU_VERSION, create.add_argument('-v', '--version', help=f"Ubuntu server version. "
help=f"Ubuntu server version. Default " f"Default {UBUNTU_VERSION}")
f"{UBUNTU_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', help='name or UUID of the VM')