From 29a5749aa9b2970a5e45a46ce83d198671550deb Mon Sep 17 00:00:00 2001 From: Neil Hanlon Date: Fri, 30 May 2025 20:06:27 -0400 Subject: [PATCH] vagrant/base: add support for info.json inside boxes WIP: vagrantconfig-info tests --- kiwi/schema/kiwi.rnc | 23 +++- kiwi/schema/kiwi.rng | 17 +++ kiwi/storage/subformat/vagrant_base.py | 24 +++- kiwi/xml_parse.py | 118 +++++++++++++++++- .../storage/subformat/vagrant_base_test.py | 21 +++- 5 files changed, 193 insertions(+), 10 deletions(-) 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' % (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' % (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