diff --git a/kiwi/schema/kiwi.rnc b/kiwi/schema/kiwi.rnc
index e045d2a01e7..db6fed4960d 100644
--- a/kiwi/schema/kiwi.rnc
+++ b/kiwi/schema/kiwi.rnc
@@ -3563,6 +3563,26 @@ div {
## The boxname as it's written into the json file
## If not specified the image name is used
attribute boxname { text }
+ k.vagrantconfig.info.element =
+ ## The info.json is a freeform document with info to display
+ ## with the box when running `vagrant box list -i`
+ element info {
+ attribute name { text },
+ text
+ }
+ >> sch:pattern [ id = "vagrant_info_structure"
+ sch:rule [
+ context = "info"
+ sch:assert [
+ test = "@name"
+ "vagrantconfig info element must have a name attribute"
+ ]
+ sch:assert [
+ test = "normalize-space(.) != ''"
+ "vagrantconfig info element must contain a non-empty value"
+ ]
+ ]
+ ]
k.vagrantconfig.attlist =
k.vagrantconfig.provider.attribute &
k.vagrantconfig.virtualsize.attribute &
@@ -3573,7 +3593,8 @@ div {
## The vagrantconfig element specifies the Vagrant meta
## configuration options which are used inside a vagrant box
element vagrantconfig {
- k.vagrantconfig.attlist
+ k.vagrantconfig.attlist,
+ k.vagrantconfig.info.element*
}
}
diff --git a/kiwi/schema/kiwi.rng b/kiwi/schema/kiwi.rng
index 654d70bc294..cba2519386c 100644
--- a/kiwi/schema/kiwi.rng
+++ b/kiwi/schema/kiwi.rng
@@ -5352,6 +5352,20 @@ virtual disk when it creates the VM.
If not specified the image name is used
+
+
+ The info.json is a freeform document with info to display
+with the box when running `vagrant box list -i`
+
+
+
+
+
+ vagrantconfig info element must have a name attribute
+ vagrantconfig info element must contain a non-empty value
+
+
+
@@ -5372,6 +5386,9 @@ If not specified the image name is used
The vagrantconfig element specifies the Vagrant meta
configuration options which are used inside a vagrant box
+
+
+
diff --git a/kiwi/storage/subformat/vagrant_base.py b/kiwi/storage/subformat/vagrant_base.py
index 3f15996ec57..48106d48c4a 100644
--- a/kiwi/storage/subformat/vagrant_base.py
+++ b/kiwi/storage/subformat/vagrant_base.py
@@ -124,6 +124,7 @@ def create_image_format(self) -> None:
* creation of box metadata.json
* creation of box Vagrantfile (either from scratch or by using the user
provided Vagrantfile)
+ * creation of box info.json
* creation of result format tarball from the files created above
"""
if not self.image_format or not self.provider:
@@ -155,6 +156,11 @@ def create_image_format(self) -> None:
vagrant.write(embedded_vagrantfile)
+ info_json = os.path.join(temp_image_dir.name, 'info.json')
+ if box_info := self._create_box_info():
+ with open(info_json, 'w') as info:
+ info.write(box_info)
+
Command.run(
[
'tar', '-C', temp_image_dir.name,
@@ -162,7 +168,8 @@ def create_image_format(self) -> None:
self.image_format
),
os.path.basename(metadata_json),
- os.path.basename(vagrantfile)
+ os.path.basename(vagrantfile),
+ os.path.basename(info_json)
] + [
os.path.basename(box_img_file)
for box_img_file in box_img_files
@@ -221,6 +228,21 @@ def get_additional_vagrant_config_settings(self) -> str:
"""
return ''
+ def _create_box_info(self) -> Optional[str]:
+ """Serialize Vagrant box info as a JSON string, if present"""
+ vagrant_info = self.vagrantconfig.get_info()
+ if not vagrant_info:
+ return None
+ info: Dict[str, str] = {}
+ for item in vagrant_info:
+ name = item.get_name()
+ value = item.get_valueOf_()
+
+ # just use the last one if someone sends two of the same...
+ info[name] = value
+ return json.dumps(
+ info, sort_keys=True, indent=2, separators=(',', ': '))
+
def _create_box_metadata(self):
metadata = self.get_additional_metadata() or {}
metadata['provider'] = self.provider
diff --git a/kiwi/xml_parse.py b/kiwi/xml_parse.py
index 822e5860c4e..c95b0a5f451 100644
--- a/kiwi/xml_parse.py
+++ b/kiwi/xml_parse.py
@@ -8714,18 +8714,112 @@ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False):
# end class oemconfig
+class info(GeneratedsSuper):
+ """The info.json is a freeform document with info to display with the
+ box when running `vagrant box list -i`"""
+ subclass = None
+ superclass = None
+ def __init__(self, name=None, valueOf_=None, mixedclass_=None, content_=None):
+ self.original_tagname_ = None
+ self.name = _cast(None, name)
+ self.valueOf_ = valueOf_
+ if mixedclass_ is None:
+ self.mixedclass_ = MixedContainer
+ else:
+ self.mixedclass_ = mixedclass_
+ if content_ is None:
+ self.content_ = []
+ else:
+ self.content_ = content_
+ self.valueOf_ = valueOf_
+ def factory(*args_, **kwargs_):
+ if CurrentSubclassModule_ is not None:
+ subclass = getSubclassFromModule_(
+ CurrentSubclassModule_, info)
+ if subclass is not None:
+ return subclass(*args_, **kwargs_)
+ if info.subclass:
+ return info.subclass(*args_, **kwargs_)
+ else:
+ return info(*args_, **kwargs_)
+ factory = staticmethod(factory)
+ def get_name(self): return self.name
+ def set_name(self, name): self.name = name
+ def get_valueOf_(self): return self.valueOf_
+ def set_valueOf_(self, valueOf_): self.valueOf_ = valueOf_
+ def hasContent_(self):
+ if (
+ (1 if type(self.valueOf_) in [int,float] else self.valueOf_)
+ ):
+ return True
+ else:
+ return False
+ def export(self, outfile, level, namespaceprefix_='', name_='info', namespacedef_='', pretty_print=True):
+ imported_ns_def_ = GenerateDSNamespaceDefs_.get('info')
+ if imported_ns_def_ is not None:
+ namespacedef_ = imported_ns_def_
+ if pretty_print:
+ eol_ = '\n'
+ else:
+ eol_ = ''
+ if self.original_tagname_ is not None:
+ name_ = self.original_tagname_
+ showIndent(outfile, level, pretty_print)
+ outfile.write('<%s%s%s' % (namespaceprefix_, name_, namespacedef_ and ' ' + namespacedef_ or '', ))
+ already_processed = set()
+ self.exportAttributes(outfile, level, already_processed, namespaceprefix_, name_='info')
+ outfile.write('>')
+ self.exportChildren(outfile, level + 1, namespaceprefix_, name_, pretty_print=pretty_print)
+ outfile.write(self.convert_unicode(self.valueOf_))
+ outfile.write('%s%s>%s' % (namespaceprefix_, name_, eol_))
+ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='', name_='info'):
+ if self.name is not None and 'name' not in already_processed:
+ already_processed.add('name')
+ outfile.write(' name=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.name), input_name='name')), ))
+ def exportChildren(self, outfile, level, namespaceprefix_='', name_='info', fromsubclass_=False, pretty_print=True):
+ pass
+ def build(self, node):
+ already_processed = set()
+ self.buildAttributes(node, node.attrib, already_processed)
+ self.valueOf_ = get_all_text_(node)
+ if node.text is not None:
+ obj_ = self.mixedclass_(MixedContainer.CategoryText,
+ MixedContainer.TypeNone, '', node.text)
+ self.content_.append(obj_)
+ for child in node:
+ nodeName_ = Tag_pattern_.match(child.tag).groups()[-1]
+ self.buildChildren(child, node, nodeName_)
+ return self
+ def buildAttributes(self, node, attrs, already_processed):
+ value = find_attr_value_('name', node)
+ if value is not None and 'name' not in already_processed:
+ already_processed.add('name')
+ self.name = value
+ def buildChildren(self, child_, node, nodeName_, fromsubclass_=False):
+ if not fromsubclass_ and child_.tail is not None:
+ obj_ = self.mixedclass_(MixedContainer.CategoryText,
+ MixedContainer.TypeNone, '', child_.tail)
+ self.content_.append(obj_)
+ pass
+# end class info
+
+
class vagrantconfig(GeneratedsSuper):
"""The vagrantconfig element specifies the Vagrant meta configuration
options which are used inside a vagrant box"""
subclass = None
superclass = None
- def __init__(self, provider=None, virtualsize=None, boxname=None, virtualbox_guest_additions_present=None, embedded_vagrantfile=None):
+ def __init__(self, provider=None, virtualsize=None, boxname=None, virtualbox_guest_additions_present=None, embedded_vagrantfile=None, info=None):
self.original_tagname_ = None
self.provider = _cast(None, provider)
self.virtualsize = _cast(int, virtualsize)
self.boxname = _cast(None, boxname)
self.virtualbox_guest_additions_present = _cast(bool, virtualbox_guest_additions_present)
self.embedded_vagrantfile = _cast(None, embedded_vagrantfile)
+ if info is None:
+ self.info = []
+ else:
+ self.info = info
def factory(*args_, **kwargs_):
if CurrentSubclassModule_ is not None:
subclass = getSubclassFromModule_(
@@ -8737,6 +8831,11 @@ def factory(*args_, **kwargs_):
else:
return vagrantconfig(*args_, **kwargs_)
factory = staticmethod(factory)
+ def get_info(self): return self.info
+ def set_info(self, info): self.info = info
+ def add_info(self, value): self.info.append(value)
+ def insert_info_at(self, index, value): self.info.insert(index, value)
+ def replace_info_at(self, index, value): self.info[index] = value
def get_provider(self): return self.provider
def set_provider(self, provider): self.provider = provider
def get_virtualsize(self): return self.virtualsize
@@ -8749,7 +8848,7 @@ def get_embedded_vagrantfile(self): return self.embedded_vagrantfile
def set_embedded_vagrantfile(self, embedded_vagrantfile): self.embedded_vagrantfile = embedded_vagrantfile
def hasContent_(self):
if (
-
+ self.info
):
return True
else:
@@ -8771,6 +8870,7 @@ def export(self, outfile, level, namespaceprefix_='', name_='vagrantconfig', nam
if self.hasContent_():
outfile.write('>%s' % (eol_, ))
self.exportChildren(outfile, level + 1, namespaceprefix_='', name_='vagrantconfig', pretty_print=pretty_print)
+ showIndent(outfile, level, pretty_print)
outfile.write('%s%s>%s' % (namespaceprefix_, name_, eol_))
else:
outfile.write('/>%s' % (eol_, ))
@@ -8791,7 +8891,12 @@ def exportAttributes(self, outfile, level, already_processed, namespaceprefix_='
already_processed.add('embedded_vagrantfile')
outfile.write(' embedded_vagrantfile=%s' % (self.gds_encode(self.gds_format_string(quote_attrib(self.embedded_vagrantfile), input_name='embedded_vagrantfile')), ))
def exportChildren(self, outfile, level, namespaceprefix_='', name_='vagrantconfig', fromsubclass_=False, pretty_print=True):
- pass
+ if pretty_print:
+ eol_ = '\n'
+ else:
+ eol_ = ''
+ for info_ in self.info:
+ info_.export(outfile, level, namespaceprefix_, name_='info', pretty_print=pretty_print)
def build(self, node):
already_processed = set()
self.buildAttributes(node, node.attrib, already_processed)
@@ -8832,7 +8937,11 @@ def buildAttributes(self, node, attrs, already_processed):
already_processed.add('embedded_vagrantfile')
self.embedded_vagrantfile = value
def buildChildren(self, child_, node, nodeName_, fromsubclass_=False):
- pass
+ if nodeName_ == 'info':
+ obj_ = info.factory()
+ obj_.build(child_)
+ self.info.append(obj_)
+ obj_.original_tagname_ = 'info'
# end class vagrantconfig
@@ -10276,6 +10385,7 @@ def main():
"ignore",
"image",
"include",
+ "info",
"initrd",
"installmedia",
"installoption",
diff --git a/test/unit/storage/subformat/vagrant_base_test.py b/test/unit/storage/subformat/vagrant_base_test.py
index 86924c054e1..2515aed5ca0 100644
--- a/test/unit/storage/subformat/vagrant_base_test.py
+++ b/test/unit/storage/subformat/vagrant_base_test.py
@@ -36,6 +36,7 @@ def setup(self):
self.vagrantconfig.get_virtualsize = Mock(
return_value=42
)
+ self.vagrantconfig.get_info = Mock(return_value=[])
self.runtime_config = Mock()
self.runtime_config.get_bundle_compression.return_value = False
kiwi.storage.subformat.base.RuntimeConfig = Mock(
@@ -87,6 +88,17 @@ def test_create_image_format(
}
''').strip()
+ info_json = dedent('''
+ {
+ "repo": "my-kiwi-descriptions"
+ }
+ ''').strip()
+
+ vagrantconfig_info = [Mock()]
+ vagrantconfig_info[0].get_name = Mock(return_value='repo')
+ vagrantconfig_info[0].get_valueOf_ = Mock(return_value='my-kiwi-descriptions')
+ self.vagrantconfig.get_info.return_value = vagrantconfig_info
+
expected_vagrantfile = dedent('''
Vagrant.configure("2") do |config|
end
@@ -102,17 +114,18 @@ def test_create_image_format(
assert mock_file.call_args_list == [
call('tmpdir/metadata.json', 'w'),
- call('tmpdir/Vagrantfile', 'w')
+ call('tmpdir/Vagrantfile', 'w'),
+ call('tmpdir/info.json', 'w')
]
assert file_handle.write.call_args_list == [
- call(metadata_json), call(expected_vagrantfile)
+ call(metadata_json), call(expected_vagrantfile), call(info_json)
]
mock_command.assert_called_once_with(
[
'tar', '-C', 'tmpdir', '-czf',
'target_dir/some-disk-image.x86_64-1.2.3.vagrant.libvirt.box',
- 'metadata.json', 'Vagrantfile'
+ 'metadata.json', 'Vagrantfile', 'info.json'
]
)
@@ -144,7 +157,7 @@ def test_user_provided_vagrantfile(
assert mock_file.call_args_list == [
call('tmpdir/metadata.json', 'w'),
call('tmpdir/Vagrantfile', 'w'),
- call('./example_Vagrantfile', 'r')
+ call('./example_Vagrantfile', 'r'),
]
assert file_handle.write.call_args_list[1] == call(
expected_vagrantfile