Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate an init file that can be used with exported images #477

Merged
merged 1 commit into from
Mar 16, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions lago/cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -313,10 +313,20 @@ def do_stop(prefix, vm_names, **kwargs):
default='.',
help='Dir to place the exported images in',
)
@lago.plugins.cli.cli_plugin_add_argument(
'--init-file-name',
default='LagoInitFile',
help='The name of the exported init file',
)
@in_lago_prefix
@with_logging
def do_export(prefix, vm_names, standalone, dst_dir, compress, **kwargs):
prefix.export_vms(vm_names, standalone, dst_dir, compress)
def do_export(
prefix, vm_names, standalone, dst_dir, compress, init_file_name,
out_format, **kwargs
):
prefix.export_vms(
vm_names, standalone, dst_dir, compress, init_file_name, out_format
)


@lago.plugins.cli.cli_plugin(
Expand Down
5 changes: 5 additions & 0 deletions lago/plugins/vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
create an alternative implementation of the provisioning details for the VM,
for example, using a remote libvirt instance or similar.
"""
from copy import deepcopy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have our "specific" implementation of deepcopy in 'utils.py', I guess here its safe? (it was added to avoid the cross-references issue iirc)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean by "cross-references" ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the discussion here: #346 (review)

Not 100% if its relevant, just checking.

import contextlib
import functools
import logging
Expand Down Expand Up @@ -364,6 +365,10 @@ def metadata(self):
def disks(self):
return self._spec['disks'][:]

@property
def spec(self):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the new spec somewhat "read only", no? was that the intention?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes.
Any change to the spec will effect the structure of the env. IMO changes to the env should be done with specific object's methods (in this case the 'vm' object), and not directly to its data.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

return deepcopy(self._spec)

def name(self):
return str(self._spec['name'])

Expand Down
14 changes: 10 additions & 4 deletions lago/prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,7 +961,8 @@ def _create_disks(
'dev': disk['dev'],
'format': disk['format'],
'metadata': metadata,
'type': disk['type']
'type': disk['type'],
'name': disk['name']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

},
)

Expand Down Expand Up @@ -1009,9 +1010,14 @@ def virt_conf(
self.save()
rollback.clear()

def export_vms(self, vms_names, standalone, export_dir, compress):

self.virt_env.export_vms(vms_names, standalone, export_dir, compress)
def export_vms(
self, vms_names, standalone, export_dir, compress, init_file_name,
out_format
):
self.virt_env.export_vms(
vms_names, standalone, export_dir, compress, init_file_name,
out_format
)

def start(self, vm_names=None):
"""
Expand Down
77 changes: 77 additions & 0 deletions lago/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,83 @@ def get_hash(file_path, checksum='sha1'):
return sha.hexdigest()


def filter_spec(spec, paths, wildcard='*', separator='/'):
"""
Remove keys from a spec file.
For example, with the following path: domains/*/disks/*/metadata
all the metadata dicts from all domains disks will be removed.

Args:
spec (dict): spec to remove keys from
paths (list): list of paths to the keys that should be removed
wildcard (str): wildcard character
separator (str): path separator

Returns:
None

Raises:
utils.LagoUserException: If a malformed path was detected
"""

def remove_key(path, spec):
if len(path) == 0:
return
elif len(path) == 1:
key = path.pop()
if not isinstance(spec, collections.Mapping):
raise LagoUserException(
'You have tried to remove the following key - "{key}".\n'
'Keys can not be removed from type {spec_type}\n'
'Please verify that path - "{{path}}" is valid'.format(
key=key, spec_type=type(spec)
)
)
if key == wildcard:
spec.clear()
else:
spec.pop(key, None)
else:
current = path[0]
if current == wildcard:
if isinstance(spec, list):
iterator = iter(spec)
elif isinstance(spec, collections.Mapping):
iterator = spec.itervalues()
else:
raise LagoUserException(
'Glob char {char} should refer only to dict or list, '
'not to {spec_type}\n'
'Please fix path - "{{path}}"'.format(
char=wildcard, spec_type=type(spec)
)
)

for i in iterator:
remove_key(path[1:], i)
else:
try:
remove_key(path[1:], spec[current])
except KeyError:
raise LagoUserException(
'Malformed path "{{path}}", key "{key}" '
'does not exist'.format(key=current)
)
except TypeError:
raise LagoUserException(
'Malformed path "{{path}}", can not get '
'by key from type {spec_type}'.
format(spec_type=type(spec))
)

for path in paths:
try:
remove_key(path.split(separator), spec)
except LagoUserException as e:
e.message = e.message.format(path=path)
raise


class LagoException(Exception):
pass

Expand Down
93 changes: 92 additions & 1 deletion lago/virt.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#
# Refer to the README and COPYING files for full details of the license
#
from copy import deepcopy
import functools
import hashlib
import json
Expand All @@ -25,6 +26,7 @@
import uuid
import time
import lxml.etree
import yaml

from . import (
brctl,
Expand Down Expand Up @@ -191,7 +193,10 @@ def virt_path(self, *args):
def bootstrap(self):
utils.invoke_in_parallel(lambda vm: vm.bootstrap(), self._vms.values())

def export_vms(self, vms_names, standalone, dst_dir, compress):
def export_vms(
self, vms_names, standalone, dst_dir, compress, init_file_name,
out_format
):
if not vms_names:
vms_names = self._vms.keys()

Expand Down Expand Up @@ -219,6 +224,88 @@ def export_vms(self, vms_names, standalone, dst_dir, compress):
for _vm in vms:
_vm.export_disks(standalone, dst_dir, compress)

self.generate_init(os.path.join(dst_dir, init_file_name), out_format)

def generate_init(self, dst, out_format, filters=None):
"""
Generate an init file which represents this env and can
be used with the images created by self.export_vms

Args:
dst (str): path and name of the new init file
out_format (plugins.output.OutFormatPlugin):
formatter for the output (the default is yaml)
filters (list): list of paths to keys that should be removed from
the init file
Returns:
None
"""
with LogTask('Exporting init file to: {}'.format(dst)):
# Set the default formatter to yaml. The default formatter
# doesn't generate a valid init file, so it's not reasonable
# to use it
if isinstance(out_format, plugins.output.DefaultOutFormatPlugin):
out_format = plugins.output.YAMLOutFormatPlugin()

if not filters:
filters = [
'domains/*/disks/*/metadata',
'domains/*/metadata/deploy-scripts', 'domains/*/snapshots',
'domains/*/name', 'nets/*/mapping'
]
spec = self.get_env_spec(filters)

for _, domain in spec['domains'].viewitems():
for disk in domain['disks']:
if disk['type'] == 'template':
disk['template_type'] = 'qcow2'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this is the right place for this logic. Should probably be a different method.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method is for generating an init file that can be used with the exported images 'out of the box'.
For general spec you should use get_env_spec

elif disk['type'] == 'empty':
disk['type'] = 'file'
disk['make_a_copy'] = 'True'

# Insert the relative path to the exported images
disk['path'] = os.path.join(
'$LAGO_INITFILE_PATH', os.path.basename(disk['path'])
)

with open(dst, 'wt') as f:
if isinstance(out_format, plugins.output.YAMLOutFormatPlugin):
# Dump the yaml file without type tags
# TODO: Allow passing parameters to output plugins
f.write(yaml.safe_dump(spec))
else:
f.write(out_format.format(spec))

def get_env_spec(self, filters=None):
"""
Get the spec of the current env.
The spec will hold the info about all the domains and
networks associated with this env.

Args:
filters (list): list of paths to keys that should be removed from
the init file
Returns:
dict: the spec of the current env
"""
spec = {
'domains':
{
vm_name: vm_object.spec
for vm_name, vm_object in self._vms.viewitems()
},
'nets':
{
net_name: net_object.spec
for net_name, net_object in self._nets.viewitems()
}
}

if filters:
utils.filter_spec(spec, filters)

return spec

def start(self, vm_names=None):
if not vm_names:
log_msg = 'Start Prefix'
Expand Down Expand Up @@ -459,6 +546,10 @@ def save(self):
with open(self._env.virt_path('net-%s' % self.name()), 'w') as f:
utils.json_dump(self._spec, f)

@property
def spec(self):
return deepcopy(self._spec)


class NATNetwork(Network):
def _libvirt_xml(self):
Expand Down
1 change: 1 addition & 0 deletions tests/functional/fixtures/snapshot/1host_1disk_list
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"size": 1780088832,
"version": "v1"
},
"name": "root",
"path": "@@PREFIX_PATH@@/images/lago_functional_tests_vm01_root.qcow2",
"type": "template"
}
Expand Down