Skip to content

Commit d4281eb

Browse files
committed
Add support for image formats when exporting to GCS
Also, command line / reference example to invoke the exporting of a disk image to GCS.
1 parent 975a6a5 commit d4281eb

4 files changed

Lines changed: 97 additions & 15 deletions

File tree

libcloudforensics/providers/gcp/forensics.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,50 @@ def CreateDiskFromGCSImage(
252252
return result
253253

254254

255+
def CopyDisksToGCS(source_project: str,
256+
source_disk: str,
257+
destination_bucket: str,
258+
destination_directory: str,
259+
image_format: str) -> str:
260+
"""Given a VM, copy the disks to a GCS bucket.
261+
262+
Args:
263+
source_project: The project containing the disk to copy
264+
source_disk: The name of the disk to copy
265+
destination_bucket: The destination bucket to store the disk copy
266+
destination_directory: The directory in the bucket in which to store the
267+
disk image
268+
image_format: The image format to use. Supported formats documented at
269+
https://github.com/GoogleCloudPlatform/compute-image-import/blob/edee48bddbe159100da9ad961131a4beb0f12158/cli_tools/gce_vm_image_export/README.md?plain=1#L3
270+
"""
271+
try:
272+
src_project = gcp_project.GoogleCloudProject(source_project)
273+
disk_to_copy = src_project.compute.GetDisk(source_disk)
274+
copied_image = src_project.compute.CreateImageFromDisk(disk_to_copy)
275+
return copied_image.ExportImage(
276+
gcs_output_folder=f'gs://{destination_bucket}/{destination_directory}',
277+
image_format=image_format,
278+
output_name=disk_to_copy.name)
279+
except (RefreshError, DefaultCredentialsError) as exception:
280+
raise errors.CredentialsConfigurationError(
281+
'Something is wrong with your Application Default Credentials. Try '
282+
'running: $ gcloud auth application-default login: {0!s}'.format(
283+
exception),
284+
__name__) from exception
285+
except HttpError as exception:
286+
if exception.resp.status == 403:
287+
raise errors.CredentialsConfigurationError(
288+
'Make sure you have the appropriate permissions on the project: '
289+
'{0!s}'.format(exception),
290+
__name__) from exception
291+
if exception.resp.status == 404:
292+
raise errors.ResourceNotFoundError(
293+
'GCP resource not found. Maybe a typo in the project / instance / '
294+
'disk name?',
295+
__name__) from exception
296+
raise RuntimeError(exception) from exception
297+
298+
255299
def AddDenyAllFirewallRules(
256300
project_id: str,
257301
network: str,

libcloudforensics/providers/gcp/internal/compute.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2253,39 +2253,49 @@ def GetOperation(self) -> Dict[str, Any]:
22532253
return response
22542254

22552255
def ExportImage(
2256-
self, gcs_output_folder: str, output_name: Optional[str] = None) -> None:
2257-
"""Export compute image to Google Cloud storage.
2258-
2256+
self,
2257+
gcs_output_folder: str,
2258+
image_format: str,
2259+
output_name: Optional[str]) -> str:
2260+
"""Export compute image to Google Cloud Storage.
2261+
22592262
Exported image is compressed and stored in .tar.gz format.
22602263
22612264
Args:
22622265
gcs_output_folder (str): Folder path of the exported image.
2266+
image_format (str): The image format to use for the export.
22632267
output_name (str): Optional. Name of the output file. Name will be
22642268
appended with .tar.gz. Default is [image_name].tar.gz.
2265-
2269+
Returns:
2270+
str: The full path of the exported image.
22662271
Raises:
22672272
InvalidNameError: If exported image name is invalid.
22682273
"""
2269-
22702274
if output_name:
22712275
if not common.REGEX_DISK_NAME.match(output_name):
22722276
raise errors.InvalidNameError(
22732277
'Exported image name {0:s} does not comply with {1:s}'.format(
22742278
output_name, common.REGEX_DISK_NAME.pattern),
22752279
__name__)
2276-
full_path = '{0:s}.tar.gz'.format(
2277-
os.path.join(gcs_output_folder, output_name))
2280+
full_path = '{0:s}'.format(os.path.join(gcs_output_folder, output_name))
22782281
else:
2279-
full_path = '{0:s}.tar.gz'.format(
2280-
os.path.join(gcs_output_folder, self.name))
2282+
full_path = '{0:s}'.format(os.path.join(gcs_output_folder, self.name))
2283+
if not image_format:
2284+
full_path = '{0:s}.tar.gz'.format(full_path)
2285+
else:
2286+
full_path = '{0:s}.{1:s}'.format(full_path, image_format)
2287+
2288+
build_args = [
2289+
'-source_image={0:s}'.format(self.name),
2290+
'-destination_uri={0:s}'.format(full_path),
2291+
'-client_id=api',
2292+
]
2293+
if image_format:
2294+
build_args.append('-format={0:s}'.format(image_format))
22812295
build_body = {
22822296
'timeout': '86400s',
2283-
'steps': [{
2284-
'args': [
2285-
'-source_image={0:s}'.format(self.name),
2286-
'-destination_uri={0:s}'.format(full_path),
2287-
'-client_id=api',
2288-
],
2297+
'steps': [{
2298+
'args': build_args,
22892299
'name': 'gcr.io/compute-image-tools/gce_vm_image_export:release',
22902300
'env': []
22912301
}],
@@ -2295,6 +2305,7 @@ def ExportImage(
22952305
response = cloud_build.CreateBuild(build_body)
22962306
cloud_build.BlockOperation(response)
22972307
logger.info('Image {0:s} exported to {1:s}.'.format(self.name, full_path))
2308+
return full_path
22982309

22992310
def Delete(self) -> None:
23002311
"""Delete Compute Disk Image from a project."""

tools/cli.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
'bucketacls': gcp_cli.GetBucketACLs,
5353
'bucketsize': gcp_cli.GetBucketSize,
5454
'copydisk': gcp_cli.CreateDiskCopy,
55+
'copydisktogcs': gcp_cli.CopyDiskToGCS,
5556
'creatediskgcs': gcp_cli.CreateDiskFromGCSImage,
5657
'deleteinstance': gcp_cli.DeleteInstance,
5758
'deleteobject': gcp_cli.DeleteObject,
@@ -387,6 +388,16 @@ def Main() -> None:
387388
'The default behavior is to use the same disk '
388389
'type as the source disk.', None)
389390
])
391+
AddParser('gcp', gcp_subparsers, 'copydisktogcs',
392+
'Copy a disk content into GCS.',
393+
args=[
394+
('project', 'Source GCP project containing the disk to copy',
395+
''),
396+
('disk_name', 'Name of the disk to copy.', ''),
397+
('bucket', 'Name of the destination bucket.', ''),
398+
('directory', 'Destination directory path in the GCS bucket.',
399+
''),
400+
('image_format', 'Image format.', '')])
390401
AddParser('gcp', gcp_subparsers, 'startvm', 'Start a forensic analysis VM.',
391402
args=[
392403
('instance_name', 'Name of the GCE instance to create.',

tools/gcp_cli.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@ def CreateDiskCopy(args: 'argparse.Namespace') -> None:
113113
logger.info('Name: {0:s}'.format(disk.name))
114114

115115

116+
def CopyDiskToGCS(args):
117+
"""Make a copy of a GCE disk into GCS storage.
118+
119+
Args:
120+
args (argparse.Namespace): Arguments from ArgumentParser.
121+
"""
122+
gcs_path = forensics.CopyDisksToGCS(source_project=args.project,
123+
source_disk=args.disk_name,
124+
destination_bucket=args.bucket,
125+
destination_directory=args.directory,
126+
image_format=args.image_format)
127+
logger.info('Disk copy completed.')
128+
logger.info('Location: {0:s}'.format(gcs_path))
129+
130+
116131
def DeleteInstance(args: 'argparse.Namespace') -> None:
117132
"""Deletes a GCE instance.
118133
@@ -541,6 +556,7 @@ def GKEWorkloadQuarantine(args: 'argparse.Namespace') -> None:
541556
args.namespace, args.workload,
542557
exempted_src_ips=exempted_src_ips)
543558

559+
544560
def GKEEnumerate(args: 'argparse.Namespace') -> None:
545561
"""Enumerate GKE cluster objects.
546562

0 commit comments

Comments
 (0)