Create a boxcreator class
A boxcreator class defines how a Vagrant box for a specific OS, version, edition and so on will be created. Before you start with the creation a boxcreator class, it is really helpful to first read the chapter Functionality to better understand how the creation of a box works. Additionally it is recommended to have a basic knowledge of packer and the way how this tool performs the box creation process.
Creating a new boxcreator class can be done by extending the BoxCreator baseclass, which provides some functionality of a box creation. Your class therefore just needs to cover the following functionality:
specify os (version) the boxcreator class should be used for
download of the system image
create OS specific files for the automated unattended os installation
copy of provisioner scripts
configure builder configuration variables, provisioners and postprocessors
Nevertheless in most cases you do not need to implement all of those functionality by yourself, due to the fact that some already existing boxcreator classes cover this steps. In this cases it is much easier to create a class that extend an already existing boxcreator class than the BoxCreator baseclass. Sometimes you may even just want to replace or add a single provisioner or source_attribute, due to some minor change in the installation process from version to version.
The following sections first explain how to make small changes while extending an existing boxcreator class before a short overview on how to create a boxcreator class from scratch is provided.
But before we start we have a look at the boxcreator classes available so far, so that you know where you can start. The following diagram shows all so far available boxcreator classes. Orange marked are those, who just implement a part of the functionality needed to create a box. Those are used to share some attributes and methods between their childs. Blue marked are the boxcreator classes implementing the full functionality of creating a box.
small and easy doable adjustments
We start with showing in a simple example how to change, set and delete builder configuration variables (stored in the source_attributes attribute), provisioners and postprocessors. Therefore we create a boxcreator class, that should be used for all editions, languages and architectures for the windows version Windows 10 in version 21H1. To demonstrate the creation of such a class we will show some possible changes. Please be aware that these changes are just meant to demonstrate how changes in general are done - these changes are not needed to make the box creation for this specific windows version work.
To create a boxcreator class we start with the creation of a python file in the src/windupbox/boxcreator/custom/
directory.
In there we create our new boxcreator class.
This boxcreator class extends the Win10BoxCreator boxcreator class, which is provided by the tool (located in src/windupbox/boxcreator/provided/windows10.py
).
After the creation of the python file the content should look like the following:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14
15 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
16 super().__init__(boxdirectory, windows_info)
The first thing to do is always to specify the windows versions, versions, editions and so for which your boxcreator class should be used.
This can be done by setting the os_info_filter attribute to an instance of the WindowsInfoFilter class (located in src/windupbox/winconstants/windowsinfo.py
).
In our example we want that our boxcreator class will be used for all windows 10 systems in version 21H1.
Therefore we create the WindowsInfoFilter instance with the keyword arguments windows_version='Windows 10'
and version='21H1'
.
The result should look like the following:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14 # specify the Windows version the boxcreator class should be used for
15 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
16 windows_version='Windows 10',
17 version='21H2',
18 )
19
20 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
21 super().__init__(boxdirectory, windows_info)
Now we can start to implement some changes to the os installation process. We will therefore show code implementing the following changes:
change/add/remove a builder configuration variable
disable a provisioner
add/replace a provisioner
change/add/remove builder configuration variables
All builder configuration variables are stored within an attribute of a boxcreator class, called source_attributes. This attribute is a dictionary, where each key represents a builder configuration variable name and each value the builder configuration variable value. To change, add or remove a builder configuration variable it is best to create a method performing the necessary dictionary operation. An example with all three actions can be seen below.
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14 # specify the Windows version the changes should apply
15 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
16 windows_version='Windows 10',
17 version='21H2',
18 )
19
20 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
21 super().__init__(boxdirectory, windows_info)
22
23 # add builder configuration variable
24 def _add_usb_bus(self):
25 self.source_attributes['usb'] = True
26
27 # change builder configuration variable
28 def _change_cpus(self):
29 self.source_attributes['cpus'] = 6
30
31 # remove builder configuration variable
32 def _remove_headless(self):
33 del self.source_attributes['headless']
After creating the methods it is needed to call them. This can be done by calling the function at the end of the initialization method:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14 # specify the Windows version the changes should apply
15 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
16 windows_version='Windows 10',
17 version='21H2',
18 )
19
20 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
21 super().__init__(boxdirectory, windows_info)
22
23 # call own functions here
24 self._add_usb_bus()
25 self._change_cpus()
26 self._remove_headless()
27
28 def _add_usb_bus(self):
29 self.source_attributes['usb'] = True
30
31 def _change_cpus(self):
32 self.source_attributes['cpus'] = 6
33
34 def _remove_headless(self):
35 del self.source_attributes['headless']
add/change a provisioner
In our case we will add a PowerShellProvisioner, that executes a Powershell Script called donothing.ps1, which we have stored in the directory X:\donothing.ps1
.
In a first step we create a own method for adding the provisioner, that in our case is called _add_provisioner_donothing.
First we copy the donothing.ps1 into our projects scripts directory.
This can be easily done by using the provided _copy_script_to_packer method as shown below.
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14 # specify the Windows version the changes should apply
15 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
16 windows_version='Windows 10',
17 version='21H2',
18 )
19
20 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
21 super().__init__(boxdirectory, windows_info)
22
23 def _add_provisioner_donothing(self):
24 script_path_source = Path(r'X:\donothing.ps1')
25 script_path_dst_relative_to_script_dir = Path('X:\donothing.ps1')
26 script_path_for_packerfile = self._copy_script_to_packer(script_path_source, script_path_dst_relative_to_script_dir)
Now we need to add a Provisioner to the provisioners of our class (stored in the attribute provisioners).
This is done by creating an instance of a provisioner class (located in src/windupbox/packerAPI/provisioner.py
).
In our case the PowerShellProvisioner class, because we want to run a powershell script as a provisioner.
The constructor of the PowerShellProvisioner class requires a type first.
The three available types are inline, script and scripts.
If inline is chosen the second parameter needs to be a string with a powershell code line.
If instead script/scripts is chosen the script path/list of script paths needs to be provided in the second parameter.
Additionally there are many ways to customize the provisioner by setting different attributes.
All attributes can be found in the powershell provisioner section in the packer documentation.
In our example we create a PowerShellProvisioner instance with type script and set the attribute debug_mode to 1.
The resulting code can be seen below:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7from windupbox.packerAPI.provisioner import PowershellProvisioner
8
9# configure logging
10import logging
11log = logging.getLogger(__name__)
12
13
14class CustomWindows10BoxCreator(Win10BoxCreator):
15 # specify the Windows version the changes should apply
16 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
17 windows_version='Windows 10',
18 version='21H2',
19 )
20
21 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
22 super().__init__(boxdirectory, windows_info)
23
24 def _add_provisioner_donothing(self):
25 script_path_source = Path(r'X:\donothing.ps1')
26 script_path_dst_relative_to_script_dir = Path('X:\donothing.ps1')
27 script_path_for_packerfile = self._copy_script_to_packer(script_path_source, script_path_dst_relative_to_script_dir)
28 provisioner_donothing = PowershellProvisioner('script', script_path_for_packerfile.as_posix())
29 provisioner_donothing.add_custom_attribute('debug_mode', '1')
30 self.provisioners.append(provisioner_donothing)
As a last step we need to call our newly created function. Therefore we again call the method at the end of the initialization method as shown below:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7from windupbox.packerAPI.provisioner import PowershellProvisioner
8
9# configure logging
10import logging
11log = logging.getLogger(__name__)
12
13
14class CustomWindows10BoxCreator(Win10BoxCreator):
15 # specify the Windows version the changes should apply
16 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
17 windows_version='Windows 10',
18 version='21H2',
19 )
20
21 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
22 super().__init__(boxdirectory, windows_info)
23
24 # call own functions here
25 self._add_provisioner_donothing()
26
27 def _add_provisioner_donothing(self):
28 script_path_source = Path(r'X:\donothing.ps1')
29 script_path_dst_relative_to_script_dir = Path('X:\donothing.ps1')
30 script_path_for_packerfile = self._copy_script_to_packer(script_path_source, script_path_dst_relative_to_script_dir)
31 provisioner_donothing = PowershellProvisioner('script', script_path_for_packerfile.as_posix())
32 provisioner_donothing.add_custom_attribute('debug_mode', '1')
33 self.provisioners.append(provisioner_donothing)
If you want to replace an already existing provisioner, find out the method name by looking at the source code and overload the function, as shown below. In this case it is import to NOT call the function in the initialization method because its should be already called in the parent class.
disable a provisioner
In order to disable a provisioner (or another function of a parent class) just simply overload the regarding method with an empty one as shown below with the _add_provisioner_disable_hibernate method.
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.provided import Win10BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomWindows10BoxCreator(Win10BoxCreator):
14 # specify the Windows version the changes should apply
15 os_info_filter: WindowsInfoFilter = WindowsInfoFilter(
16 windows_version='Windows 10',
17 version='21H2',
18 )
19
20 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
21 super().__init__(boxdirectory, windows_info)
22
23 def _add_provisioner_disable_hibernate(self):
24 log.info(f'provisioner disable hibernate disabled')
create a boxcreator class from scratch
To create a boxcreator class from scratch it is needed to implement all functionality mentioned in the list above. These are:
define specific os the boxcreator class should be used for
download of the system image (if not provided)
create OS specific files for the automated unattended os installation
copy of provisioner scripts
configure builder configuration variables, provisioners and postprocessors
Steps 4 and 5 are already covered in the section above. Therefore in the following sections just cover how to define the os the boxcreator class should be used for , how to implement the os image download and how to create os specific files, that are needed for the box creation.
We first start with the basic structure of each boxcreator class, which looks like the following:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.boxcreator import BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomBoxCreator(BoxCreator):
14
15 def __init__(self, boxdirectory: Path):
16 super().__init__(boxdirectory)
17 # you can be place methods that add provisioners, add builder configuration variables or create additional files here
18
19 # custom methods
20
21 def create_box(self):
22 # if you have methods that should be done not in the initialization but right before the box creation, you can place them here
23 super().create_box()
In the following we will create such a boxcreator class for Windows 10.
Because it would be way to much to explain every function needed we will just cover some basics, that help you understand how everything works.
For details please look at the real Windows 10 boxcreator class and the Windows boxcreator class (located in src/windupbox/boxcreator/provided/
).
specify os
It is important to specify for which os and which version your boxcreator class should be used. To better understand how this is done properly we shortly explain how the tool chooses a boxcreator class for the input given in the command line interface.
If a user run the tool, it provide them a list of os, versions, editions and so on. These data will be stored in an instance of the so called OsInfo class and determines which boxcreator class will be used for the box creation. Each boxcreator class contains an instance of the so called OsInfoFilter class, which is used to specify which os given through their OsInfo match with the filter. Therefore the OsInfoFilter has a method called match, which returns whether the OsInfo matches the filter. Additionally it provides the information how precise it matches the filter.
For specific os, the characteristic data can vary.
Therefore we work with childs of the OsInfo and OsInfoFilter class for certain os.
So far the tool supports only windows, where the regarding classes are called WindowsInfo and WindowsInfoFilter.
All this classes are located in the src/windupbox/osinfo/
module.
As an example let us say we just have two boxcreator called Windows10_21H1BoxCreator and Windows10_BoxCreator.
In the Windows10_21H1BoxCreator class the os_info_filter has the value WindowsInfoFilter(windows_version=['Windows 10'], version=['21H1'])
while the Windows10_BoxCreator os_info_filter attribute is WindowsInfoFilter(windows_version=['Windows 10'])
.
If the user now chooses any os_info containing ‘Windows 10’ as the windows version and ‘21H1’ as the version the Windows10_21H1BoxCreator class will be used, due to the fact it matches more precise.
Accordingly if the user chooses any os_info containing ‘Windows 10’ and another windows version the Windows10_BoxCreator will be used.
This provides the possibility to build a flexible structure, which allows small adjustments in the installation process for certain versions.
If you create a new boxcreator class it may be needed for you to add options to the os selection in the command line interface. An tutorial on how to do that can be found … .
download of the system image
For the download of the system image overload the _download_image method, that will download the system image and add the necessary builder configuration variables. We therefore first need to find the location where to place the system image. This can be done by using the _get_image_path method provided by the BoxCreator baseclass. Then you can download the image and set the attribute image to the filepath of the downloaded file. Because packer wants the sha256 checksum of the image, we then calculate the sha256 checksum by calling the _get_image_sha256checksum method, which is also provided by the BoxCreator baseclass. In a next step we determine the path of the image relative to our project directory, because this is needed for the packerfile. As a last step we add this relative path as well as the checksum of the iso to the builder configuration variables. The resulting code then looks something like the following:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.boxcreator import BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo
7
8# configure logging
9import logging
10log = logging.getLogger(__name__)
11
12
13class CustomBoxCreator(BoxCreator):
14 def __init__(self, boxdirectory: Path):
15 super().__init__(boxdirectory)
16 # you can be place methods that add provisioners, add builder configuration variables or create additional files here
17
18
19 def _download_image(self):
20 filepath = self._get_image_path()
21 downloader = WindowsDownloader(self.windows_info)
22 downloader.download_iso(filepath)
23 self.image = filepath
24 self.image_sha256checksum = self._get_image_sha256checksum()
25 iso_url = self.image.relative_to(self.base_directory)
26 self.source_attributes['iso_urls'] = [iso_url.as_posix()]
27 self.source_attributes['iso_checksum'] = f'sha256:{self.image_sha256checksum}'
28
29
30 def create_box(self):
31 # if you have methods that should be done not in the initialization but right before the box creation, you can place them here
32 super().create_box()
As already mentioned before, there is no need to call the _download_image method here due to the fact that it is called by the BoxCreator baseclass.
creation of os specific files
For most os installations additional files are needed that configure the installation process. Newer windows versions, such as Windows 8, 10 and 11 for instance use a so called Autounattend.xml file. These can be distributed to the packer virtual machine by a floppy drive or a http directory. Dependent on the os it may be necessary to execute the file. Therefore in most cases the boot command is used. For windows systems no boot command is needed due to the fact that windows automatically finds the Autounattend.xml if provided through a drive, like the floppy drive.
In the following section it is shortly shown how to add a file to the floppy drive. We therefore create an Autounattend file using a minimal API for Autounattend files provided by this tool. We first setup the class as already done in the sections before and add a method where we create the Autounattend.xml and add it to the floppy files. Additionally we import the Autounattendfile class as well as the SynchronousCommand class from the winautounattendAPI module.
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.boxcreator import BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7from windupbox.winautounattendAPI.autounattendfile import Autounantendfile, SynchronousCommand
8
9# configure logging
10import logging
11log = logging.getLogger(__name__)
12
13
14class CustomBoxCreator(BoxCreator):
15 # specify the Windows version the changes should apply
16 box_creator_info: WindowsInfoFilter = WindowsInfoFilter(
17 windows_version='Windows 10',
18 version='21H2',
19 )
20
21 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
22 super().__init__(boxdirectory, windows_info)
23 self.setup_os_specific_files()
24
25 def setup_os_specific_files(self):
26 pass
In a next step we create an Autounattendfile instance. Additionally we add a command to the Autounattend file, that will change the execution policy for windows powershell scripts. As a last step we can save the Autounattend file to the floppy directory, by using the save method of the class, which will return the path were the file is saved. The result looks like the following:
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.boxcreator import BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7from windupbox.winautounattendAPI.autounattendfile import Autounantendfile, SynchronousCommand
8
9# configure logging
10import logging
11log = logging.getLogger(__name__)
12
13
14class CustomBoxCreator(BoxCreator):
15 # specify the Windows version the changes should apply
16 box_creator_info: WindowsInfoFilter = WindowsInfoFilter(
17 windows_version='Windows 10',
18 version='21H2',
19 )
20
21 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
22 super().__init__(boxdirectory, windows_info)
23 self.setup_os_specific_files()
24
25 def setup_os_specific_files(self):
26 autounantendfile = Autounantendfile(self.windows_info)
27 set_execution_policy = SynchronousCommand(
28 command=r'cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"',
29 description=r'Set Execution Policy',
30 )
31 autounantendfile.add_command(set_execution_policy)
32 filepath_abs = autounantendfile.save(self.floppy_directory)
No we need to add the relative path of our Autounattendfile to the floppy files. This can simply be done by appending the relative path to the floppy_files attribute of the boxcreator class.
1# external imports
2from pathlib import Path
3
4# internal imports
5from windupbox.boxcreator.boxcreator import BoxCreator
6from windupbox.winconstants.windowsinfo import WindowsInfo, WindowsInfoFilter
7from windupbox.winautounattendAPI.autounattendfile import Autounantendfile, SynchronousCommand
8
9# configure logging
10import logging
11log = logging.getLogger(__name__)
12
13
14class CustomBoxCreator(BoxCreator):
15 # specify the Windows version the changes should apply
16 box_creator_info: WindowsInfoFilter = WindowsInfoFilter(
17 windows_version='Windows 10',
18 version='21H2',
19 )
20
21 def __init__(self, boxdirectory: Path, windows_info: WindowsInfo):
22 super().__init__(boxdirectory, windows_info)
23 self.setup_os_specific_files()
24
25 def setup_os_specific_files(self):
26 autounantendfile = Autounantendfile(self.windows_info)
27 set_execution_policy = SynchronousCommand(
28 command=r'cmd.exe /c powershell -Command "Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Force"',
29 description=r'Set Execution Policy',
30 )
31 autounantendfile.add_command(set_execution_policy)
32 filepath_abs = autounantendfile.save(self.floppy_directory)
33 filepath_rel = filepath_abs.relative_to(self.base_directory)
34 self.floppy_files.append(filepath_rel)
As already mentioned this is just a small example, that should show you how its done in theory. The here created box will not work, because more scripts are needed. To have further insight have a look in the already existing boxcreator classes.