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:
183
box.py
183
box.py
@@ -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')
|
||||||
|
|||||||
Reference in New Issue
Block a user