blob: 25a018a9529231594364fe27e8bec82a5626de40 [file] [log] [blame]
# Copyright 2021 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from . import customization
from . import helper
from . import mount_wim
from . import unmount_wim
from . import regedit
from . import add_windows_package
from . import add_windows_driver
from PB.recipes.infra.windows_image_builder import (offline_winpe_customization
as winpe)
from PB.recipes.infra.windows_image_builder import windows_image_builder as wib
from PB.recipes.infra.windows_image_builder import sources as src_pb
from PB.recipes.infra.windows_image_builder import dest as dest_pb
COPYPE = 'Copy-PE.ps1'
ADDFILE = 'robocopy'
# map arch to microsofts version of arch
# https://docs.microsoft.com/en-us/windows-hardware/manufacture/desktop/copype-command-line-options?#copype-command-line-options-1
COPYPE_ARCH = {'amd64': 'amd64',
'x86': 'x86',
'aarch64': 'arm64'}
class OfflineWinPECustomization(customization.Customization):
""" WinPE based customization support """
def __init__(self, **kwargs):
""" __init__ generates a ref for the given customization
"""
super(OfflineWinPECustomization, self).__init__(**kwargs)
# ensure that the customization is of the correct type
assert self.customization().WhichOneof(
'customization') == 'offline_winpe_customization'
# use a custom work dir
self._name = self.customization().offline_winpe_customization.name
self._workdir = self.m.path.cleanup_dir.joinpath(self._name, 'workdir')
self._scratchpad = self.m.path.cleanup_dir.joinpath(self._name, 'sp')
self._canon_cust = None
helper.ensure_dirs(self.m.file, [self._workdir])
def pin_sources(self, ctx):
""" pins the given config by replacing the sources in customization
Args:
* ctx: dict containing the context for the customization
"""
wpec = self._customization.offline_winpe_customization
if wpec.image_src.WhichOneof('src'):
wpec.image_src.CopyFrom(self._source.pin(wpec.image_src, ctx))
for off_action in wpec.offline_customization:
for action in off_action.actions:
helper.pin_src_from_action(action, self._source, ctx)
def download_sources(self):
""" download_sources downloads the sources in the given config to disk"""
wpec = self._customization.offline_winpe_customization
self._source.download(wpec.image_src)
for off_action in wpec.offline_customization:
for action in off_action.actions:
srcs = helper.get_src_from_action(action)
for src in srcs:
self._source.download(src)
def get_canonical_cfg(self):
""" get_canonical_cfg returns canonical config after removing name and dest
Example:
Given a config
Customization{
offline_winpe_customization: OfflineWinPECustomization{
name: "winpe_gce_vanilla"
image_src: Src{...}
image_dests: [...]
offline_customization: [...]
}
}
returns config
Customization{
offline_winpe_customization: OfflineWinPECustomization{
name: ""
image_src: Src{...}
offline_customization: [...]
}
}
"""
if not self._canon_cust:
wpec = self._customization.offline_winpe_customization
# Generate customization without any names or dest refs. This will make
# customization deterministic to the generated image
cust = wib.Customization(
offline_winpe_customization=winpe.OfflineWinPECustomization(
image_src=wpec.image_src,
offline_customization=[
helper.get_build_offline_customization(c)
for c in wpec.offline_customization
],
),)
self._canon_cust = cust
return self._canon_cust
def remove_upload_dests(self):
""" remove_upload_dests removes the upload dests specified in a config.
This is meant to be used on a try job to avoid uploading to user specified
locations on a try job
"""
self._customization.offline_winpe_customization.image_dests.clear()
@property
def outputs(self):
""" return the output(s) of executing this config. Doesn't guarantee that
the output(s) exists."""
dests = []
if self.get_key():
location = 'WIB-WIM/{}.zip'
if self.tryrun:
location = 'WIB-WIM-TRY/{}.zip' # pragma: nocover
output = src_pb.GCSSrc(
bucket='chrome-gce-images', source=location.format(self.get_key()))
dests.append(
dest_pb.Dest(
gcs_src=output,
tags={'orig': self._source.get_url(src_pb.Src(gcs_src=output))},
))
if self._customization.offline_winpe_customization.image_dests:
dests.extend(self._customization.offline_winpe_customization.image_dests)
return dests # pragma: no cover
@property
def inputs(self): # pragma: no cover
""" inputs returns the input(s) required for this customization.
inputs here refer to any external refs that might be required for this
customization
"""
inputs = []
wpec = self._customization.offline_winpe_customization
if wpec.image_src.WhichOneof('src'):
# Add the image src required to the list
inputs.append(wpec.image_src)
for off_action in wpec.offline_customization:
for action in off_action.actions:
# Add all the srcs from actions to the list
inputs.extend(helper.get_src_from_action(action))
return inputs
@property
def context(self):
""" context returns a dict containing the local_src id mapping to output
src.
"""
return {
'{}-output'.format(self.id): self._source.dest_to_src(self.outputs[0])
}
def execute_customization(self):
""" execute_customization initializes the winpe image, runs the given
actions and repackages the image and uploads the result to GCS"""
wpec = self._customization.offline_winpe_customization
if wpec and len(wpec.offline_customization) > 0:
with self.m.step.nest('offline winpe customization ' + wpec.name):
self.init_win_pe_image(self._arch, wpec.image_src)
try:
for action in wpec.offline_customization:
self.perform_winpe_actions(action)
except Exception:
# Unmount the image and discard changes on failure
self.deinit_win_pe_image(save=False)
raise
else:
self.deinit_win_pe_image()
def init_win_pe_image(self, arch, image, index=1):
""" init_win_pe_image initializes the source image (if given) by mounting
it to dest
Args:
* arch: string representing architecture of the image
* image: sources.Src object ref an image to be modified
* dest: destination to upload artifacts to
* index: index of the image to be mounted
"""
with self.m.step.nest('Init WinPE image modification ' + arch + ' in ' +
str(self._workdir)):
# Path to boot.wim. This is where COPY-PE generates the image
wim_path = self._workdir.joinpath('media', 'sources', 'boot.wim')
# Use WhichOneOf to test for emptiness
# https://developers.google.com/protocol-buffers/docs/reference/python-generated#oneof
if not image.WhichOneof('src'):
# gen a winpe arch dir for the given arch
self.m.powershell(
'Gen WinPE media for {}'.format(arch),
self._scripts('WindowsPowerShell\Scripts') / 'Copy-PE.ps1',
args=[
'-WinPeArch', COPYPE_ARCH[arch], '-Destination',
str(self._workdir)
])
else:
image_path = self._source.get_local_src(image)
if str(image_path).endswith('.zip'):
# unzip the given image
self.m.archive.extract(
'Unpack {}'.format(self._source.get_url(image)),
self._source.get_local_src(image), self._workdir)
else:
# Path to copy the image to
wim_path = self._workdir.joinpath(self.m.path.basename(image_path))
# image was from remote source. Copy to workdir
self.m.file.copy(
'Copy {} to workdir'.format(self._source.get_url(image)),
image_path, wim_path)
# I can't figure out a way to get this to work with cipd. Somehow the
# wim gets set to ReadOnly. I tried changing the permissions before
# uploading to cipd. But, It was still set ReadOnly.
self.m.powershell(
'Set RW access',
'Set-ItemProperty',
args=[
'-Path', wim_path, '-Name', 'IsReadOnly', '-Value', '$false'
])
# ensure that the destination exists
dest = self._workdir / 'mount'
self.m.file.ensure_directory('Ensure mount point', dest)
# Mount the boot.wim to mount dir for modification
mount_wim.mount_win_wim(self.m.powershell, dest, wim_path, index,
self.m.path.cleanup_dir)
def deinit_win_pe_image(self, save=True):
""" deinit_win_pe_image unmounts the winpe image and saves/discards changes
to it
Args:
* save: bool to determine if we need to save the changes to this image.
"""
with self.m.step.nest('Deinit WinPE image modification'):
if save:
# copy the config used for building the image
source = self._configs / '{}.cfg'.format(self.get_key())
self.execute_script(
'Add cfg {}'.format(source),
ADDFILE,
self._configs,
self._workdir / 'mount',
'{}.cfg'.format(self.get_key()),
logs=None,
ret_codes=[0, 1])
unmount_wim.unmount_win_wim(
self.m.powershell,
self._workdir / 'mount',
self._scratchpad,
save=save)
if save:
with self.m.step.nest('Upload the output of {}'.format(self.name())):
# There is only one output for offine winpe build
def_dest = self.outputs[0]
# upload the output to default bucket for offline_winpe_customization
self._source.upload_package(def_dest, self._workdir)
# upload to any custom destinations that might be given
cust = self._customization.offline_winpe_customization
for image_dest in cust.image_dests:
# update the link to the original upload.
image_dest.tags['orig'] = def_dest.tags['orig']
self._source.upload_package(image_dest, self._workdir)
def perform_winpe_action(self, action):
""" perform_winpe_action Performs the given action
Args:
* action: actions.Action proto object that specifies an action to be
performed
"""
a = action.WhichOneof('action')
if a == 'add_file':
return self.add_file(action.add_file)
if a == 'add_windows_package':
src = self._source.get_local_src(action.add_windows_package.src)
return self.add_windows_package(action.add_windows_package, src)
if a == 'add_windows_driver':
src = self._source.get_local_src(action.add_windows_driver.src)
return self.add_windows_driver(action.add_windows_driver, src)
if a == 'edit_offline_registry':
return regedit.edit_offline_registry(
self.m.powershell, self._scripts('WindowsPowerShell\Scripts'),
action.edit_offline_registry, self._workdir / 'mount')
def perform_winpe_actions(self, offline_action):
""" perform_winpe_actions Performs the given offline_action
Args:
* offline_action: actions.OfflineAction proto object that needs to be
executed
"""
for a in offline_action.actions:
self.perform_winpe_action(a)
def add_windows_package(self, awp, src):
""" add_windows_package runs Add-WindowsPackage command in powershell.
https://docs.microsoft.com/en-us/powershell/module/dism/add-windowspackage?view=windowsserver2019-ps
Args:
* awp: actions.AddWindowsPackage proto object
* src: Path to the package on bot disk
"""
add_windows_package.install_package(
self.m.powershell, self._scripts('WindowsPowerShell\Scripts'), awp, src,
self._workdir / 'mount', self._scratchpad)
def add_file(self, af):
""" add_file runs Copy-Item in Powershell to copy the given file to image.
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/copy-item?view=powershell-5.1
Args:
* af: actions.AddFile proto object
"""
# src contains the path for the src dir
src = self._source.get_local_src(af.src)
# src_file contains the file/dir name to be copied
src_file = '*'
if not self.m.path.isdir(src):
# if the src is a file then src is the dir name and src_file is filename
src_file = self.m.path.basename(src)
src = self.m.path.dirname(src)
# destination to copy the file to
dest = '"{}"'.format(self._workdir.joinpath('mount', af.dst))
self.execute_script(
'Add file {}'.format(self._source.get_url(af.src)),
ADDFILE,
src,
dest,
src_file,
'/e',
logs=None,
ret_codes=[0, 1, 2, 3])
def add_windows_driver(self, awd, src):
""" add_windows_driver runs Add-WindowsDriver command in powershell.
https://docs.microsoft.com/en-us/powershell/module/dism/add-windowsdriver?view=windowsserver2019-ps
Args:
* awd: actions.AddWindowsDriver proto object
* src: Path to the driver on bot disk
"""
add_windows_driver.install_driver(
self.m.powershell, self._scripts('WindowsPowerShell\Scripts'), awd, src,
self._workdir / 'mount', self._scratchpad)