Repository: google/cloud-forensics-utils

Issue:
Write custom error classes

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=h1) Report
> Merging [#223](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=desc) into [master](https://codecov.io/gh/google/cloud-forensics-utils/commit/e4448c60002eee03c344ec2803276ff5d91936be&el=desc) will **increase** coverage by `3.13%`.
> The diff coverage is `71.68%`.

[![Impacted file tree graph](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/graphs/tree.svg?width=650&height=150&src=pr&token=zZkPno3ah0)](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #223      +/-   ##
==========================================
+ Coverage   65.39%   68.52%   +3.13%     
==========================================
  Files          14       23       +9     
  Lines         968     1700     +732     
==========================================
+ Hits          633     1165     +532     
- Misses        335      535     +200     
```

| Flag | Coverage Δ | |
|---|---|---|
| #nosetests | `68.52% <71.68%> (+3.13%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [libcloudforensics/providers/aws/forensics.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2F3cy9mb3JlbnNpY3MucHk=) | `55.71% <45.45%> (-14.06%)` | :arrow_down: |
| [libcloudforensics/providers/gcp/forensics.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9mb3JlbnNpY3MucHk=) | `52.30% <47.82%> (-12.70%)` | :arrow_down: |
| [...cs/providers/gcp/internal/compute\_base\_resource.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9pbnRlcm5hbC9jb21wdXRlX2Jhc2VfcmVzb3VyY2UucHk=) | `37.33% <52.94%> (+0.82%)` | :arrow_up: |
| [...ibcloudforensics/providers/gcp/internal/compute.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9pbnRlcm5hbC9jb21wdXRlLnB5) | `56.31% <53.73%> (-6.05%)` | :arrow_down: |
| [libcloudforensics/providers/aws/internal/log.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2F3cy9pbnRlcm5hbC9sb2cucHk=) | `73.07% <55.55%> (-4.20%)` | :arrow_down: |
| [...cloudforensics/providers/azure/internal/compute.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2F6dXJlL2ludGVybmFsL2NvbXB1dGUucHk=) | `55.81% <55.81%> (ø)` | |
| [...ibcloudforensics/providers/gcp/internal/project.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9pbnRlcm5hbC9wcm9qZWN0LnB5) | `61.90% <66.66%> (-3.81%)` | :arrow_down: |
| [libcloudforensics/providers/gcp/internal/common.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9pbnRlcm5hbC9jb21tb24ucHk=) | `61.16% <69.69%> (+4.84%)` | :arrow_up: |
| [...bcloudforensics/providers/azure/internal/common.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2F6dXJlL2ludGVybmFsL2NvbW1vbi5weQ==) | `72.46% <72.46%> (ø)` | |
| [libcloudforensics/providers/gcp/internal/gke.py](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree#diff-bGliY2xvdWRmb3JlbnNpY3MvcHJvdmlkZXJzL2djcC9pbnRlcm5hbC9na2UucHk=) | `73.68% <73.68%> (ø)` | |
| ... and [23 more](https://codecov.io/gh/google/cloud-forensics-utils/pull/223/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=footer). Last update [cc650e1...15d53e4](https://codecov.io/gh/google/cloud-forensics-utils/pull/223?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/libcloudforensics/errors.py b/libcloudforensics/errors.py
new file mode 100644
index 0000000..0cbdf4c
--- /dev/null
+++ b/libcloudforensics/errors.py
@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+# Copyright 2020 Google Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Generic error wrapper"""
+
+from libcloudforensics import logging_utils
+
+
+class LCFError(Exception):
+  """Class to represent a cloud-forensics-utils (CFU) error.
+
+  Attributes:
+    message (str): The error message.
+    name (str): Name of the module that generated the error.
+  """
+
+  def __init__(self,
+               message: str,
+               name: str) -> None:
+    """Initializes the CFUError with provided message.
+
+    Args:
+      message (str): The error message.
+      name (str): The name of the module that generated the error.
+    """
+    super(LCFError, self).__init__(message)
+    self.message = message
+    self.name = name
+    logging_utils.SetUpLogger(self.name)
+    logger = logging_utils.GetLogger(self.name)
+    logger.error(self.message)
+
+
+class CredentialsConfigurationError(LCFError):
+  """Error when an issue with the credentials configuration is encountered."""
+
+
+class InvalidFileFormatError(LCFError):
+  """Error when an issue with file format is encountered."""
+
+
+class InvalidNameError(LCFError):
+  """Error when an issue with resource name is encountered."""
+
+
+class ResourceNotFoundError(LCFError):
+  """Error when an issue with non-existent resource is encountered."""
+
+
+class ResourceCreationError(LCFError):
+  """Error when an issue with creating a new resource is encountered."""
+class ResourceDeletionError(LCFError):
+  """Error when an issue with deleting a resource is encountered."""
diff --git a/libcloudforensics/providers/aws/forensics.py b/libcloudforensics/providers/aws/forensics.py
index c2fa462..44d2566 100644
--- a/libcloudforensics/providers/aws/forensics.py
+++ b/libcloudforensics/providers/aws/forensics.py
@@ -17,7 +17,7 @@ from typing import TYPE_CHECKING, Tuple, List, Optional, Dict
 
 from libcloudforensics.providers.aws.internal.common import UBUNTU_1804_FILTER
 from libcloudforensics.providers.aws.internal import account
-from libcloudforensics import logging_utils
+from libcloudforensics import logging_utils, errors
 
 if TYPE_CHECKING:
   from libcloudforensics.providers.aws.internal import ebs, ec2
@@ -99,8 +99,8 @@ def CreateVolumeCopy(zone: str,
     AWSVolume: An AWS EBS Volume object.
 
   Raises:
-    RuntimeError: If there are errors copying the volume, or errors during
-        KMS key creation/sharing if the target volume is encrypted.
+    ResourceCreationError: If there are errors copying the volume, or errors
+        during KMS key creation/sharing if the target volume is encrypted.
     ValueError: If both instance_id and volume_id are missing, or if AWS
         account information could not be retrieved.
   """
@@ -185,10 +185,10 @@ def CreateVolumeCopy(zone: str,
     # Delete the one-time use KMS key, if one was generated
     source_account.kms.DeleteKMSKey(kms_key_id)
     logger.info('Done')
-  except RuntimeError as exception:
-    error_msg = 'Copying volume {0:s}: {1!s}'.format(
-        (volume_id or instance_id), exception)
-    raise RuntimeError(error_msg)
+  except (errors.LCFError, RuntimeError) as exception:
+    raise errors.ResourceCreationError(
+        'Copying volume {0:s}: {1!s}'.format(
+            (volume_id or instance_id), exception), __name__)
 
   return new_volume
 
diff --git a/libcloudforensics/providers/aws/internal/ebs.py b/libcloudforensics/providers/aws/internal/ebs.py
index 3519890..89de911 100644
--- a/libcloudforensics/providers/aws/internal/ebs.py
+++ b/libcloudforensics/providers/aws/internal/ebs.py
@@ -13,11 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Disk functionality."""
+
 import binascii
 from typing import TYPE_CHECKING, Dict, Optional, Union, List, Any
 
 import botocore
 
+from libcloudforensics import errors
 from libcloudforensics.providers.aws.internal import common
 
 if TYPE_CHECKING:
@@ -115,8 +117,8 @@ class AWSVolume(AWSElasticBlockStore):
       AWSSnapshot: A snapshot object.
 
     Raises:
-      ValueError: If the snapshot name does not comply with the RegEx.
-      RuntimeError: If the snapshot could not be created.
+      InvalidNameError: If the snapshot name does not comply with the RegEx.
+      ResourceCreationError: If the snapshot could not be created.
     """
 
     if not tags:
@@ -126,8 +128,9 @@ class AWSVolume(AWSElasticBlockStore):
     truncate_at = 255 - 1
     snapshot_name = snapshot_name[:truncate_at]
     if len(snapshot_name) > 255:
-      raise ValueError('Snapshot name {0:s} is too long (>255 chars)'.format(
-          snapshot_name))
+      raise errors.InvalidNameError(
+          'Snapshot name {0:s} is too long (>255 chars)'.format(snapshot_name),
+          __name__)
     tags['Name'] = snapshot_name
 
     client = self.aws_account.ClientApi(common.EC2_SERVICE)
@@ -143,8 +146,9 @@ class AWSVolume(AWSElasticBlockStore):
           WaiterConfig={'Delay': 30, 'MaxAttempts': 100})
     except (client.exceptions.ClientError,
             botocore.exceptions.WaiterError) as exception:
-      raise RuntimeError('Could not create snapshot for volume {0:s}: '
-                         '{1:s}'.format(self.volume_id, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create snapshot for volume {0:s}: {1:s}'.format(
+              self.volume_id, str(exception)), __name__)
 
     return AWSSnapshot(snapshot_id,
                        self.aws_account,
@@ -154,13 +158,18 @@ class AWSVolume(AWSElasticBlockStore):
                        name=snapshot_name)
 
   def Delete(self) -> None:
-    """Delete a volume."""
+    """Delete a volume.
+
+    Raises:
+      ResourceDeletionError: If the volume could not be deleted.
+    """
     client = self.aws_account.ClientApi(common.EC2_SERVICE)
     try:
       client.delete_volume(VolumeId=self.volume_id)
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not delete volume {0:s}: {1:s}'.format(
-          self.volume_id, str(exception)))
+      raise errors.ResourceDeletionError(
+          'Could not delete volume {0:s}: {1:s}'.format(
+              self.volume_id, str(exception)), __name__)
 
   def GetVolumeType(self) -> str:
     """Return the volume type.
@@ -238,7 +247,7 @@ class AWSSnapshot(AWSElasticBlockStore):
       AWSSnapshot: A copy of the snapshot.
 
     Raises:
-      RuntimeError: If the snapshot could not be copied.
+      ResourceCreationError: If the snapshot could not be copied.
     """
 
     client = self.aws_account.ClientApi(common.EC2_SERVICE)
@@ -252,8 +261,9 @@ class AWSSnapshot(AWSElasticBlockStore):
     try:
       response = client.copy_snapshot(**copy_args)
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not copy snapshot {0:s}: {1:s}'.format(
-          self.snapshot_id, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not copy snapshot {0:s}: {1:s}'.format(
+              self.snapshot_id, str(exception)), __name__)
 
     snapshot_copy = AWSSnapshot(
         # The response contains the new snapshot ID
@@ -278,14 +288,19 @@ class AWSSnapshot(AWSElasticBlockStore):
     return snapshot_copy
 
   def Delete(self) -> None:
-    """Delete a snapshot."""
+    """Delete a snapshot.
+
+    Raises:
+      ResourceDeletionError: If the snapshot could not be deleted.
+    """
 
     client = self.aws_account.ClientApi(common.EC2_SERVICE)
     try:
       client.delete_snapshot(SnapshotId=self.snapshot_id)
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not delete snapshot {0:s}: {1:s}'.format(
-          self.snapshot_id, str(exception)))
+      raise errors.ResourceDeletionError(
+          'Could not delete snapshot {0:s}: {1:s}'.format(
+              self.snapshot_id, str(exception)), __name__)
 
   def ShareWithAWSAccount(self, aws_account_id: str) -> None:
     """Share the snapshot with another AWS account ID.
@@ -448,15 +463,15 @@ class EBS:
       AWSVolume: An Amazon EC2 Volume object.
 
     Raises:
-      RuntimeError: If volume does not exist.
+      ResourceNotFoundError: If the volume does not exist.
     """
 
     volumes = self.ListVolumes(region=region)
     volume = volumes.get(volume_id)
     if not volume:
-      error_msg = 'Volume {0:s} was not found in AWS account'.format(
-          volume_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Volume {0:s} was not found in AWS account'.format(volume_id),
+          __name__)
     return volume
 
   def CreateVolumeFromSnapshot(
@@ -485,9 +500,9 @@ class EBS:
       AWSVolume: An AWS EBS Volume.
 
     Raises:
-      ValueError: If the volume name does not comply with the RegEx,
-          or if the volume type is invalid.
-      RuntimeError: If the volume could not be created.
+      InvalidNameError: If the volume name does not comply with the RegEx
+      ValueError: If the volume type is invalid.
+      ResourceCreationError: If the volume could not be created.
     """
 
     if volume_type not in ['standard', 'io1', 'gp2', 'sc1', 'st1']:
@@ -499,8 +514,9 @@ class EBS:
           snapshot, volume_name_prefix=volume_name_prefix)
 
     if len(volume_name) > 255:
-      raise ValueError(
-          'Volume name {0:s} is too long (>255 chars)'.format(volume_name))
+      raise errors.InvalidNameError(
+          'Volume name {0:s} is too long (>255 chars)'.format(volume_name),
+          __name__)
 
     if not tags:
       tags = {}
@@ -530,9 +546,9 @@ class EBS:
       client.get_waiter('volume_available').wait(VolumeIds=[volume_id])
     except (client.exceptions.ClientError,
             botocore.exceptions.WaiterError) as exception:
-      raise RuntimeError('Could not create volume {0:s} from snapshot '
-                         '{1:s}: {2:s}'.format(volume_name, snapshot.name,
-                                               str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create volume {0:s} from snapshot {1:s}: {2!s}'.format(
+              volume_name, snapshot.name, exception), __name__)
 
     zone = volume['AvailabilityZone']
     encrypted = volume['Encrypted']
diff --git a/libcloudforensics/providers/aws/internal/ec2.py b/libcloudforensics/providers/aws/internal/ec2.py
index 5209385..437dbe7 100644
--- a/libcloudforensics/providers/aws/internal/ec2.py
+++ b/libcloudforensics/providers/aws/internal/ec2.py
@@ -13,11 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """Instance functionality."""
+
 import binascii
 import os
 from typing import TYPE_CHECKING, Dict, Optional, List, Any, Tuple
 
 import botocore
+from libcloudforensics import errors
 from libcloudforensics.scripts import utils
 
 from libcloudforensics.providers.aws.internal import common
@@ -70,7 +72,7 @@ class AWSInstance:
       AWSVolume: Volume object if the volume is found.
 
     Raises:
-      RuntimeError: If no boot volume could be found.
+      ResourceNotFoundError: If no boot volume could be found.
     """
 
     boot_device = self.aws_account.ResourceApi(
@@ -81,9 +83,9 @@ class AWSInstance:
       if volumes[volume_id].device_name == boot_device:
         return volumes[volume_id]
 
-    error_msg = 'Boot volume not found for instance: {0:s}'.format(
-        self.instance_id)
-    raise RuntimeError(error_msg)
+    raise errors.ResourceNotFoundError(
+        'Boot volume not found for instance: {0:s}'.format(self.instance_id),
+        __name__)
 
   def GetVolume(self, volume_id: str) -> 'ebs.AWSVolume':
     """Get a volume attached to the instance by ID.
@@ -95,15 +97,15 @@ class AWSInstance:
       AWSVolume: The AWSVolume object.
 
     Raises:
-      RuntimeError: If volume_id is not found amongst the volumes attached
-          to the instance.
+      ResourceNotFoundError: If volume_id is not found amongst the volumes
+          attached to the instance.
     """
 
     volume = self.ListVolumes().get(volume_id)
     if not volume:
-      raise RuntimeError(
+      raise errors.ResourceNotFoundError(
           'Volume {0:s} is not attached to instance {1:s}'.format(
-              volume_id, self.instance_id))
+              volume_id, self.instance_id), __name__)
     return volume
 
   def ListVolumes(self) -> Dict[str, 'ebs.AWSVolume']:
@@ -289,15 +291,15 @@ class EC2:
       AWSInstance: An Amazon EC2 Instance object.
 
     Raises:
-      RuntimeError: If instance does not exist.
+      ResourceNotFoundError: If instance does not exist.
     """
 
     instances = self.ListInstances(region=region)
     instance = instances.get(instance_id)
     if not instance:
-      error_msg = 'Instance {0:s} was not found in AWS account'.format(
-          instance_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Instance {0:s} was not found in AWS account'.format(instance_id),
+          __name__)
     return instance
 
   def ListImages(
@@ -365,17 +367,14 @@ class EC2:
           reused (False).
 
     Raises:
-      RuntimeError: If the virtual machine cannot be found or created.
+      ResourceCreationError: If the virtual machine cannot be created.
     """
 
     # Re-use instance if it already exists, or create a new one.
-    try:
-      instances = self.GetInstancesByName(vm_name)
-      if instances:
-        created = False
-        return instances[0], created
-    except RuntimeError:
-      pass
+    instances = self.GetInstancesByName(vm_name)
+    if instances:
+      created = False
+      return instances[0], created
 
     instance_type = common.GetInstanceTypeByCPU(cpu_cores)
     startup_script = utils.ReadStartupScript()
@@ -420,8 +419,9 @@ class EC2:
       client.get_waiter('instance_status_ok').wait(InstanceIds=[instance_id])
     except (client.exceptions.ClientError,
             botocore.exceptions.WaiterError) as exception:
-      raise RuntimeError('Could not create instance {0:s}: {1:s}'.format(
-          vm_name, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create instance {0:s}: {1!s}'.format(vm_name, exception),
+          __name__)
 
     instance = AWSInstance(self.aws_account,
                            instance_id,
@@ -447,16 +447,16 @@ class EC2:
           specified AMI.
 
     Raises:
-      RuntimeError: If AMI details cannot be found.
+      ResourceNotFoundError: If AMI details cannot be found.
     """
 
     client = self.aws_account.ClientApi(common.EC2_SERVICE)
     try:
       image = client.describe_images(ImageIds=[ami])
     except client.exceptions.ClientError as exception:
-      raise RuntimeError(
-          'Could not find image information for AMI {0:s}: {1:s}'.format(
-              ami, str(exception)))
+      raise errors.ResourceNotFoundError(
+          'Could not find image information for AMI {0:s}: {1!s}'.format(
+              ami, exception), __name__)
 
     # If the call to describe_images was successful, then the API's response
     # is expected to contain at least one image and its corresponding block
@@ -486,7 +486,7 @@ class EC2:
 
     Raises:
       ValueError: If vm_name is None.
-      RuntimeError: If the key could not be created.
+      ResourceCreationError: If the key could not be created.
     """
 
     if not vm_name:
@@ -500,7 +500,7 @@ class EC2:
     try:
       key = client.create_key_pair(KeyName=key_name)
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not create SSH key pair: {0:s}'.format(
-          str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create SSH key pair: {0!s}'.format(exception), __name__)
     # If the call was successful, the response contains key information
     return key['KeyName'], key['KeyMaterial']
diff --git a/libcloudforensics/providers/aws/internal/kms.py b/libcloudforensics/providers/aws/internal/kms.py
index daf2949..0fd1dd8 100644
--- a/libcloudforensics/providers/aws/internal/kms.py
+++ b/libcloudforensics/providers/aws/internal/kms.py
@@ -13,9 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """KMS functionality."""
+
 import json
 from typing import Optional, TYPE_CHECKING
 
+from libcloudforensics import errors
 from libcloudforensics.providers.aws.internal import common
 
 if TYPE_CHECKING:
@@ -43,15 +45,15 @@ class KMS:
       str: The KMS key ID for the key that was created.
 
     Raises:
-      RuntimeError: If the key could not be created.
+      ResourceCreationError: If the key could not be created.
     """
 
     client = self.aws_account.ClientApi(common.KMS_SERVICE)
     try:
       kms_key = client.create_key()
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not create KMS key: {0:s}'.format(
-          str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create KMS key: {0!s}'.format(exception), __name__)
 
     # The response contains the key ID
     key_id = kms_key['KeyMetadata']['KeyId']  # type: str
@@ -110,7 +112,7 @@ class KMS:
       kms_key_id (str): The ID of the KMS key to delete.
 
     Raises:
-      RuntimeError: If the key could not be scheduled for deletion.
+      ResourceDeletionError: If the key could not be scheduled for deletion.
     """
 
     if not kms_key_id:
@@ -120,5 +122,6 @@ class KMS:
     try:
       client.schedule_key_deletion(KeyId=kms_key_id)
     except client.exceptions.ClientError as exception:
-      raise RuntimeError('Could not schedule the KMS key: {0:s} for '
-                         'deletion'.format(str(exception)))
+      raise errors.ResourceDeletionError(
+          'Could not schedule the KMS key {0:s} for deletion {1!s}'.format(
+              exception, kms_key_id), __name__)
diff --git a/libcloudforensics/providers/azure/forensics.py b/libcloudforensics/providers/azure/forensics.py
index 10cd19c..fde04ad 100644
--- a/libcloudforensics/providers/azure/forensics.py
+++ b/libcloudforensics/providers/azure/forensics.py
@@ -16,7 +16,7 @@
 
 from typing import TYPE_CHECKING, Optional, List, Dict, Tuple
 
-from libcloudforensics import logging_utils
+from libcloudforensics import errors, logging_utils
 from libcloudforensics.providers.azure.internal import account, common
 
 if TYPE_CHECKING:
@@ -66,7 +66,7 @@ def CreateDiskCopy(
     AZComputeDisk: An Azure Compute Disk object.
 
   Raises:
-    RuntimeError: If there are errors copying the disk.
+    ResourceCreationError: If there are errors copying the disk.
     ValueError: If both instance_name and disk_name are missing.
   """
 
@@ -123,10 +123,9 @@ def CreateDiskCopy(
     snapshot.Delete()
     logger.info('Disk {0:s} successfully copied to {1:s}'.format(
         disk_to_copy.name, new_disk.name))
-  except RuntimeError as exception:
-    error_msg = 'Cannot copy disk "{0:s}": {1!s}'.format(
-        str(disk_name), str(exception))
-    raise RuntimeError(error_msg)
+  except (errors.LCFError, RuntimeError) as exception:
+    raise errors.ResourceCreationError('Cannot copy disk "{0:s}": {1!s}'.format(
+        str(disk_name), exception), __name__)
 
   return new_disk
 
diff --git a/libcloudforensics/providers/azure/internal/common.py b/libcloudforensics/providers/azure/internal/common.py
index d5a6837..c015d76 100644
--- a/libcloudforensics/providers/azure/internal/common.py
+++ b/libcloudforensics/providers/azure/internal/common.py
@@ -24,6 +24,9 @@ from typing import Any, List, Dict, Optional, TYPE_CHECKING, Tuple
 # so we can ignore the warning.
 from azure.common.credentials import ServicePrincipalCredentials  # pylint: disable=import-error
 
+
+from libcloudforensics import errors
+
 if TYPE_CHECKING:
   # TYPE_CHECKING is always False at runtime, therefore it is safe to ignore
   # the following cyclic import, as it it only used for type hints
@@ -83,9 +86,10 @@ def GetCredentials(profile_name: Optional[str] = None
         corresponding Azure credentials.
 
   Raises:
-    RuntimeError: If the credential file is not found.
-    ValueError: If the requested profile name is not found in the credential
-        file or if there are missing entries in the profile name.
+    CredentialsConfigurationError: If there are environment variables that
+        are not set or if the credentials file has missing entries/profiles.
+    FileNotFoundError: If the credentials file is not found.
+    InvalidFileFormatError: If the credentials file couldn't be parsed.
   """
   # pylint: enable=line-too-long
   if not profile_name:
@@ -94,10 +98,10 @@ def GetCredentials(profile_name: Optional[str] = None
     secret = os.getenv("AZURE_CLIENT_SECRET")
     tenant = os.getenv("AZURE_TENANT_ID")
     if not (subscription_id and client_id and secret and tenant):
-      raise RuntimeError('Please make sure you defined the following '
-                         'environment variables: [AZURE_SUBSCRIPTION_ID,'
-                         'AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,'
-                         'AZURE_TENANT_ID].')
+      raise errors.CredentialsConfigurationError(
+          'Please make sure you defined the following environment variables: '
+          '[AZURE_SUBSCRIPTION_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, '
+          'AZURE_TENANT_ID].', __name__)
     return subscription_id, ServicePrincipalCredentials(client_id,
                                                         secret,
                                                         tenant=tenant)
@@ -107,28 +111,29 @@ def GetCredentials(profile_name: Optional[str] = None
     path = os.path.expanduser('~/.azure/credentials.json')
 
   if not os.path.exists(path):
-    raise RuntimeError('Credential files not found. Please place it in '
-                       '"~/.azure/credentials.json" or specify an absolute '
-                       'path to it in the AZURE_CREDENTIALS_PATH environment '
-                       'variable.')
+    raise FileNotFoundError(
+        'Credentials file not found. Please place it in '
+        '"~/.azure/credentials.json" or specify an absolute path to it in '
+        'the AZURE_CREDENTIALS_PATH environment variable.')
 
   with open(path) as profiles:
     try:
       account_info = json.load(profiles).get(profile_name)
     except ValueError as exception:
-      raise ValueError('Could not decode JSON file. Please verify the file '
-                       'format: {0:s}'.format(str(exception)))
+      raise errors.InvalidFileFormatError(
+          'Could not decode JSON file. Please verify the file format:'
+          ' {0!s}'.format(exception), __name__)
     if not account_info:
-      raise ValueError(
+      raise errors.CredentialsConfigurationError(
           'Profile name {0:s} not found in credentials file {1:s}'.format(
-              profile_name, path))
+              profile_name, path), __name__)
     required_entries = ['subscriptionId', 'clientId', 'clientSecret',
                         'tenantId']
     if not all(account_info.get(entry) for entry in required_entries):
-      raise ValueError(
+      raise errors.CredentialsConfigurationError(
           'Please make sure that your JSON file has the required entries. The '
           'file should contain at least the following: {0:s}'.format(
-              ', '.join(required_entries)))
+              ', '.join(required_entries)), __name__)
     return account_info['subscriptionId'], ServicePrincipalCredentials(
         account_info['clientId'],
         account_info['clientSecret'],
@@ -187,7 +192,7 @@ def GenerateDiskName(snapshot: 'compute.AZComputeSnapshot',
     str: A name for the disk.
 
   Raises:
-    ValueError: If the disk name does not comply with the RegEx.
+    InvalidNameError: If the disk name does not comply with the RegEx.
   """
 
   # Max length of disk names in Azure is 80 characters
@@ -211,8 +216,8 @@ def GenerateDiskName(snapshot: 'compute.AZComputeSnapshot',
   # name of the source snapshot contained dashes, we need to replace them.
   disk_name = disk_name.replace('-', '_')
   if not REGEX_DISK_NAME.match(disk_name):
-    raise ValueError(
+    raise errors.InvalidNameError(
         'Disk name {0:s} does not comply with '
-        '{1:s}'.format(disk_name, REGEX_DISK_NAME.pattern))
+        '{1:s}'.format(disk_name, REGEX_DISK_NAME.pattern), __name__)
 
   return disk_name
diff --git a/libcloudforensics/providers/azure/internal/compute.py b/libcloudforensics/providers/azure/internal/compute.py
index 8955ca1..09ad3aa 100644
--- a/libcloudforensics/providers/azure/internal/compute.py
+++ b/libcloudforensics/providers/azure/internal/compute.py
@@ -30,7 +30,7 @@ from azure.mgmt import compute as compute_sdk
 from azure.mgmt.compute.v2020_05_01 import models
 # pylint: enable=import-error
 
-from libcloudforensics import logging_utils
+from libcloudforensics import errors, logging_utils
 from libcloudforensics.providers.azure.internal import common, compute_base_resource  # pylint: disable=line-too-long, ungrouped-imports
 
 from libcloudforensics.scripts import utils
@@ -144,14 +144,14 @@ class AZCompute:
       AZComputeVirtualMachine: An Azure virtual machine object.
 
     Raises:
-      RuntimeError: If the instance was not found in the subscription / resource
-          group.
+      ResourceNotFoundError: If the instance was not found in the subscription/
+          resource group.
     """
     instances = self.ListInstances(resource_group_name=resource_group_name)
     if instance_name not in instances:
-      error_msg = 'Instance {0:s} was not found in subscription {1:s}'.format(
-          instance_name, self.az_account.subscription_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Instance {0:s} was not found in subscription {1:s}'.format(
+              instance_name, self.az_account.subscription_id), __name__)
     return instances[instance_name]
 
   def GetDisk(
@@ -170,14 +170,14 @@ class AZCompute:
       AZComputeDisk: An Azure Compute Disk object.
 
     Raises:
-      RuntimeError: If the disk was not found in the subscription / resource
-          group.
+      ResourceNotFoundError: If the disk was not found in the subscription/
+          resource group.
     """
     disks = self.ListDisks(resource_group_name=resource_group_name)
     if disk_name not in disks:
-      error_msg = 'Disk {0:s} was not found in subscription {1:s}'.format(
-          disk_name, self.az_account.subscription_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Disk {0:s} was not found in subscription {1:s}'.format(
+              disk_name, self.az_account.subscription_id), __name__)
     return disks[disk_name]
 
   def CreateDiskFromSnapshot(
@@ -204,7 +204,7 @@ class AZCompute:
       AZComputeDisk: Azure Compute Disk.
 
     Raises:
-      RuntimeError: If the disk could not be created.
+      ResourceCreationError: If the disk could not be created.
     """
 
     if not disk_name:
@@ -234,8 +234,9 @@ class AZCompute:
       disk = request.result()
       logger.info('Disk {0:s} successfully created'.format(disk_name))
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create disk from snapshot {0:s}: {1:s}'
-                         .format(snapshot.resource_id, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create disk from snapshot {0:s}: {1!s}'.format(
+              snapshot.resource_id, exception), __name__)
 
     return AZComputeDisk(self.az_account,
                          disk.id,
@@ -276,7 +277,7 @@ class AZCompute:
       AZComputeDisk: Azure Compute Disk.
 
     Raises:
-      RuntimeError: If the disk could not be created.
+      ResourceCreationError: If the disk could not be created.
     """
 
     if not region:
@@ -319,8 +320,9 @@ class AZCompute:
       sleep(5)  # Wait for the vhd to be imported in the Azure storage container
       copy_status = copied_blob.get_blob_properties().copy.status
       if copy_status in ('aborted', 'failed'):
-        raise RuntimeError('Could not import the snapshot from URI '
-                           '{0:s}'.format(snapshot_uri))
+        raise errors.ResourceCreationError(
+            'Could not import the snapshot from URI {0:s}'.format(
+                snapshot_uri), __name__)
       logger.debug('Importing snapshot from URI {0:s}'.format(snapshot_uri))
     logger.info('Snapshot successfully imported from URI {0:s}'.format(
         snapshot_uri))
@@ -351,8 +353,9 @@ class AZCompute:
       disk = request.result()
       logger.info('Disk {0:s} successfully created'.format(disk_name))
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create disk from URI {0:s}: {1:s}'
-                         .format(snapshot_uri, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create disk from URI {0:s}: {1!s}'.format(
+              snapshot_uri, exception), __name__)
 
     # Cleanup the temporary account storage
     self.az_account.storage.DeleteStorageAccount(storage_account_name)
@@ -398,7 +401,8 @@ class AZCompute:
           virtual machine was created (True) or reused (False).
 
     Raises:
-      RuntimeError: If the virtual machine cannot be found or created.
+      RuntimeError: If the provided SSH key is invalid.
+      ResourceCreationError: If the virtual machine cannot be found or created.
     """
 
     # Re-use instance if it already exists, or create a new one.
@@ -484,8 +488,9 @@ class AZCompute:
         sleep(5)  # Wait 5 seconds before checking disk status again
       vm = request.result()
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create instance {0:s}: {1:s}'.format(
-          vm_name, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create instance {0:s}: {1!s}'.format(vm_name, exception),
+          __name__)
 
     instance = AZComputeVirtualMachine(self.az_account,
                                        vm.id,
@@ -577,7 +582,7 @@ class AZComputeVirtualMachine(compute_base_resource.AZComputeResource):
       AZComputeDisk: Disk object if the disk is found.
 
     Raises:
-      RuntimeError: If no boot disk could be found.
+      ResourceNotFoundError: If no boot disk could be found.
     """
     # pylint: disable=line-too-long
     disks = self.az_account.compute.ListDisks(
@@ -586,9 +591,9 @@ class AZComputeVirtualMachine(compute_base_resource.AZComputeResource):
     boot_disk_name = self.compute_client.virtual_machines.get(
         self.resource_group_name, self.name).storage_profile.os_disk.name
     if boot_disk_name not in disks:
-      error_msg = 'Boot disk not found for instance: {0:s}'.format(
-          self.resource_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Boot disk not found for instance {0:s}'.format(self.resource_id),
+          __name__)
     return disks[boot_disk_name]
 
   def GetDisk(self, disk_name: str) -> 'AZComputeDisk':
@@ -601,14 +606,14 @@ class AZComputeVirtualMachine(compute_base_resource.AZComputeResource):
       AZComputeDisk: The disk object.
 
     Raises:
-      RuntimeError: If disk_name is not found amongst the disks attached
-          to the instance.
+      ResourceNotFoundError: If disk_name is not found amongst the disks
+          attached to the instance.
     """
     disks = self.ListDisks()
     if disk_name not in disks:
-      error_msg = 'Disk {0:s} not found in instance: {1:s}'.format(
-          disk_name, self.resource_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Disk {0:s} was not found in instance {1:s}'.format(
+              disk_name, self.resource_id), __name__)
     return disks[disk_name]
 
   def ListDisks(self) -> Dict[str, 'AZComputeDisk']:
@@ -700,8 +705,8 @@ class AZComputeDisk(compute_base_resource.AZComputeResource):
       AZComputeSnapshot: A snapshot object.
 
     Raises:
-      ValueError: If the snapshot name does not comply with the RegEx.
-      RuntimeError: If the snapshot could not be created.
+      InvalidNameError: If the snapshot name does not comply with the RegEx.
+      ResourceCreationError: If the snapshot could not be created.
     """
 
     if not snapshot_name:
@@ -709,9 +714,9 @@ class AZComputeDisk(compute_base_resource.AZComputeResource):
       truncate_at = 80 - 1
       snapshot_name = snapshot_name[:truncate_at]
       if not common.REGEX_SNAPSHOT_NAME.match(snapshot_name):
-        raise ValueError('Snapshot name {0:s} does not comply with '
-                         '{1:s}'.format(snapshot_name,
-                                        common.REGEX_SNAPSHOT_NAME.pattern))
+        raise errors.InvalidNameError(
+            'Snapshot name {0:s} does not comply with {1:s}'.format(
+                snapshot_name, common.REGEX_SNAPSHOT_NAME.pattern), __name__)
 
     creation_data = {
         'location': self.region,
@@ -735,8 +740,9 @@ class AZComputeDisk(compute_base_resource.AZComputeResource):
       snapshot = request.result()
       logger.info('Snapshot {0:s} successfully created'.format(snapshot_name))
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create snapshot for disk {0:s}: {1:s}'
-                         .format(self.resource_id, str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create snapshot for disk {0:s}: {1!s}'.format(
+              self.resource_id, exception), __name__)
 
     return AZComputeSnapshot(self.az_account,
                              snapshot.id,
@@ -786,7 +792,11 @@ class AZComputeSnapshot(compute_base_resource.AZComputeResource):
     self.disk = source_disk
 
   def Delete(self) -> None:
-    """Delete a snapshot."""
+    """Delete a snapshot.
+
+    Raises:
+      ResourceDeletionError: If the snapshot could not be deleted.
+    """
 
     try:
       logger.info('Deleting snapshot: {0:s}'.format(self.name))
@@ -796,8 +806,9 @@ class AZComputeSnapshot(compute_base_resource.AZComputeResource):
         sleep(5)  # Wait 5 seconds before checking snapshot status again
       logger.info('Snapshot {0:s} successfully deleted.'.format(self.name))
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not delete snapshot {0:s}: {1:s}'
-                         .format(self.resource_id, str(exception)))
+      raise errors.ResourceDeletionError(
+          'Could not delete snapshot {0:s}: {1!s}'.format(
+              self.resource_id, exception), __name__)
 
   def GrantAccessAndGetURI(self) -> str:
     """Grant access to a snapshot and return its access URI.
diff --git a/libcloudforensics/providers/azure/internal/compute_base_resource.py b/libcloudforensics/providers/azure/internal/compute_base_resource.py
index 7eb6d5a..ba84fbc 100644
--- a/libcloudforensics/providers/azure/internal/compute_base_resource.py
+++ b/libcloudforensics/providers/azure/internal/compute_base_resource.py
@@ -18,12 +18,13 @@ from typing import Optional, List, TYPE_CHECKING
 
 from azure.mgmt import compute as compute_sdk  # pylint: disable=import-error
 
-from libcloudforensics.providers.azure.internal import common  # pylint: disable=ungrouped-imports
+from libcloudforensics import errors
+from libcloudforensics.providers.azure.internal import common
 
 if TYPE_CHECKING:
   # TYPE_CHECKING is always False at runtime, therefore it is safe to ignore
   # the following cyclic import, as it it only used for type hints
-  from libcloudforensics.providers.azure.internal import account  # pylint: disable=cyclic-import
+  from libcloudforensics.providers.azure.internal import account  # pylint: disable=cyclic-import, ungrouped-imports
 
 
 class AZComputeResource:
@@ -56,13 +57,13 @@ class AZComputeResource:
           the resource is located.
 
     Raises:
-      ValueError: If the resource ID is malformed.
+      InvalidNameError: If the resource ID is malformed.
     """
 
     if not common.REGEX_COMPUTE_RESOURCE_ID.match(resource_id):
-      raise ValueError(
+      raise errors.InvalidNameError(
           'Malformed resource ID: expected {0:s}, got {1:s}'.format(
-              common.REGEX_COMPUTE_RESOURCE_ID.pattern, resource_id))
+              common.REGEX_COMPUTE_RESOURCE_ID.pattern, resource_id), __name__)
 
     self.az_account = az_account
     # Format of resource_id: /subscriptions/{id}/resourceGroups/{
diff --git a/libcloudforensics/providers/azure/internal/network.py b/libcloudforensics/providers/azure/internal/network.py
index 494bdee..906a7a9 100644
--- a/libcloudforensics/providers/azure/internal/network.py
+++ b/libcloudforensics/providers/azure/internal/network.py
@@ -21,6 +21,7 @@ from azure.mgmt import network
 from msrestazure import azure_exceptions
 # pylint: enable=import-error
 
+from libcloudforensics import errors
 from libcloudforensics.providers.azure.internal import common
 
 if TYPE_CHECKING:
@@ -63,7 +64,7 @@ class AZNetwork:
 
     Raises:
       ValueError: if name is not provided.
-      RuntimeError: If no network interface could be created.
+      ResourceCreationError: If no network interface could be created.
     """
     if not name:
       raise ValueError('name must be specified. Provided: {0!s}'.format(name))
@@ -82,8 +83,9 @@ class AZNetwork:
       return nic_id
     except azure_exceptions.CloudError as exception:
       if 'ResourceNotFound' not in exception.error.error:
-        raise RuntimeError('Could not create network interface: {0:s}'.format(
-            str(exception)))
+        raise errors.ResourceCreationError(
+            'Could not create network interface: {0!s}'.format(exception),
+            __name__)
       # NIC doesn't exist, ignore the error and create it
 
     # pylint: disable=unbalanced-tuple-unpacking
@@ -111,8 +113,9 @@ class AZNetwork:
           creation_data)
       request.wait()
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create network interface: {0:s}'.format(
-          str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create network interface: {0!s}'.format(exception),
+          __name__)
 
     network_interface_id = request.result().id  # type: str
     return network_interface_id
@@ -135,7 +138,7 @@ class AZNetwork:
           group object.
 
     Raises:
-      RuntimeError: If the elements could not be created.
+      ResourceCreationError: If the elements could not be created.
     """
 
     if not region:
@@ -202,6 +205,7 @@ class AZNetwork:
         request.wait()
         result.append(request.result())
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not create network interface elements: '
-                         '{0:s}'.format(str(exception)))
+      raise errors.ResourceCreationError(
+          'Could not create network interface elements: {0!s}'.format(
+              exception), __name__)
     return tuple(result)
diff --git a/libcloudforensics/providers/azure/internal/storage.py b/libcloudforensics/providers/azure/internal/storage.py
index 8335624..e077b77 100644
--- a/libcloudforensics/providers/azure/internal/storage.py
+++ b/libcloudforensics/providers/azure/internal/storage.py
@@ -21,7 +21,7 @@ from azure.mgmt import storage
 from msrestazure import azure_exceptions
 # pylint: enable=import-error
 
-from libcloudforensics import logging_utils
+from libcloudforensics import errors, logging_utils
 from libcloudforensics.providers.azure.internal import common
 
 if TYPE_CHECKING:
@@ -68,13 +68,14 @@ class AZStorage:
       Tuple[str, str]: The storage account ID and its access key.
 
     Raises:
-      ValueError: If the storage account name is invalid.
+      InvalidNameError: If the storage account name is invalid.
     """
 
     if not common.REGEX_ACCOUNT_STORAGE_NAME.match(storage_account_name):
-      raise ValueError(
+      raise errors.InvalidNameError(
           'Storage account name {0:s} does not comply with {1:s}'.format(
-              storage_account_name, common.REGEX_ACCOUNT_STORAGE_NAME.pattern))
+              storage_account_name, common.REGEX_ACCOUNT_STORAGE_NAME.pattern),
+          __name__)
 
     if not region:
       region = self.az_account.default_region
@@ -113,7 +114,7 @@ class AZStorage:
     """Delete an account storage.
 
     Raises:
-      RuntimeError: if the storage account could not be deleted.
+      ResourceDeletionError: if the storage account could not be deleted.
     """
     try:
       logger.info('Deleting storage account: {0:s}'.format(
@@ -123,5 +124,6 @@ class AZStorage:
       logger.info('Storage account {0:s} successfully deleted'.format(
           storage_account_name))
     except azure_exceptions.CloudError as exception:
-      raise RuntimeError('Could not delete account storage {0:s}: {1:s}'
-                         .format(storage_account_name, str(exception)))
+      raise errors.ResourceDeletionError(
+          'Could not delete account storage {0:s}: {1:s}'.format(
+              storage_account_name, str(exception)), __name__)
diff --git a/libcloudforensics/providers/gcp/forensics.py b/libcloudforensics/providers/gcp/forensics.py
index f308bb2..d2942d8 100644
--- a/libcloudforensics/providers/gcp/forensics.py
+++ b/libcloudforensics/providers/gcp/forensics.py
@@ -22,7 +22,7 @@ from googleapiclient.errors import HttpError
 
 from libcloudforensics.providers.gcp.internal import project as gcp_project
 from libcloudforensics.providers.gcp.internal import common
-from libcloudforensics import logging_utils
+from libcloudforensics import errors, logging_utils
 
 if TYPE_CHECKING:
   from libcloudforensics.providers.gcp.internal import compute
@@ -55,7 +55,10 @@ def CreateDiskCopy(
     GoogleComputeDisk: A Google Compute Disk object.
 
   Raises:
-    RuntimeError: If there are errors copying the disk.
+    ResourceNotFoundError: If the GCP resource is not found.
+    CredentialsConfigurationError: If the library could not authenticate to GCP.
+    RuntimeError: If an unknown HttpError is thrown.
+    ResourceCreationError: If there are errors copying the disk.
     ValueError: If both instance_name and disk_name are missing.
   """
 
@@ -88,28 +91,25 @@ def CreateDiskCopy(
     snapshot.Delete()
     logger.debug('Snapshot {0:s} deleted.'.format(snapshot.name))
 
-  except RefreshError as exception:
-    error_msg = ('Something is wrong with your gcloud access token: '
-                 '{0:s}.').format(exception)
-    raise RuntimeError(error_msg)
-  except DefaultCredentialsError as exception:
-    error_msg = (
-        'Something is wrong with your Application Default '
-        'Credentials. '
-        'Try running:\n  $ gcloud auth application-default login')
-    raise RuntimeError(error_msg)
+  except (RefreshError, DefaultCredentialsError) as exception:
+    raise errors.CredentialsConfigurationError(
+        'Something is wrong with your Application Default Credentials. Try '
+        'running: $ gcloud auth application-default login: {0!s}'.format(
+            exception), __name__)
   except HttpError as exception:
     if exception.resp.status == 403:
-      raise RuntimeError(
-          'Make sure you have the appropriate permissions on the project')
+      raise errors.CredentialsConfigurationError(
+          'Make sure you have the appropriate permissions on the project',
+          __name__)
     if exception.resp.status == 404:
-      raise RuntimeError(
+      raise errors.ResourceNotFoundError(
           'GCP resource not found. Maybe a typo in the project / instance / '
-          'disk name?')
+          'disk name?', __name__)
     raise RuntimeError(exception)
   except RuntimeError as exception:
-    error_msg = 'Cannot copy disk "{0:s}": {1!s}'.format(disk_name, exception)
-    raise RuntimeError(error_msg)
+    raise errors.ResourceCreationError(
+        'Cannot copy disk "{0:s}": {1!s}'.format(disk_name, exception),
+        __name__)
 
   return new_disk
 
@@ -200,14 +200,14 @@ def CreateDiskFromGCSImage(
         }
 
   Raises:
-    ValueError: If the GCE disk name is invalid.
+    InvalidNameError: If the GCE disk name is invalid.
   """
 
   if name:
     if not common.REGEX_DISK_NAME.match(name):
-      raise ValueError(
+      raise errors.InvalidNameError(
           'Disk name {0:s} does not comply with {1:s}'.format(
-              name, common.REGEX_DISK_NAME.pattern))
+              name, common.REGEX_DISK_NAME.pattern), __name__)
     name = name[:common.COMPUTE_NAME_LIMIT]
   else:
     name = common.GenerateUniqueInstanceName('imported-disk',
diff --git a/libcloudforensics/providers/gcp/internal/common.py b/libcloudforensics/providers/gcp/internal/common.py
index 58f27e1..6347427 100644
--- a/libcloudforensics/providers/gcp/internal/common.py
+++ b/libcloudforensics/providers/gcp/internal/common.py
@@ -24,7 +24,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Any
 from google.auth import default
 from google.auth.exceptions import DefaultCredentialsError, RefreshError
 from googleapiclient.discovery import build
-from libcloudforensics import logging_utils  # pylint: disable=ungrouped-imports
+from libcloudforensics import errors, logging_utils  # pylint: disable=ungrouped-imports
 
 if TYPE_CHECKING:
   import googleapiclient
@@ -59,7 +59,7 @@ def GenerateDiskName(snapshot: 'compute.GoogleComputeSnapshot',
     str: A name for the disk.
 
   Raises:
-    ValueError: If the disk name does not comply with the RegEx.
+    InvalidNameError: If the disk name does not comply with the RegEx.
   """
 
   # Max length of disk names in GCP is 63 characters
@@ -81,9 +81,9 @@ def GenerateDiskName(snapshot: 'compute.GoogleComputeSnapshot',
         snapshot.name[:truncate_at], disk_id_crc32)
 
   if not REGEX_DISK_NAME.match(disk_name):
-    raise ValueError(
-        'Disk name {0:s} does not comply with '
-        '{1:s}'.format(disk_name, REGEX_DISK_NAME.pattern))
+    raise errors.InvalidNameError(
+        'Disk name {0:s} does not comply with {1:s}'.format(
+            disk_name, REGEX_DISK_NAME.pattern), __name__)
 
   return disk_name
 
@@ -120,18 +120,18 @@ def CreateService(service_name: str,
     googleapiclient.discovery.Resource: API service resource.
 
   Raises:
-    RuntimeError: If Application Default Credentials could not be obtained or if
-        service build times out.
+    CredentialsConfigurationError: If Application Default Credentials could
+        not be obtained
+    RuntimeError: If service build times out.
   """
 
   try:
     credentials, _ = default()
-  except DefaultCredentialsError as error:
-    error_msg = (
-        'Could not get application default credentials: {0!s}\n'
-        'Have you run $ gcloud auth application-default '
-        'login?').format(error)
-    raise RuntimeError(error_msg)
+  except DefaultCredentialsError as exception:
+    raise errors.CredentialsConfigurationError(
+        'Could not get application default credentials. Have you run $ gcloud '
+        'auth application-default login?: {0!s}'.format_map(exception),
+        __name__)
 
   service_built = False
   for retry in range(RETRY_MAX):
@@ -249,7 +249,8 @@ def ExecuteRequest(client: 'googleapiclient.discovery.Resource',
     List[Dict]: A List of dictionaries (responses from the request).
 
   Raises:
-    RuntimeError: If the request to the GCP API could not complete.
+    CredentialsConfigurationError: If the request to the GCP API could not
+        complete.
   """
 
   responses = []
@@ -266,12 +267,10 @@ def ExecuteRequest(client: 'googleapiclient.discovery.Resource',
       request = getattr(client, func)
       response = request(**kwargs).execute()
     except (RefreshError, DefaultCredentialsError) as exception:
-      error_msg = (
-          '{0:s}\n'
-          'Something is wrong with your Application Default '
-          'Credentials. Try running: '
-          '$ gcloud auth application-default login'.format(str(exception)))
-      raise RuntimeError(error_msg)
+      raise errors.CredentialsConfigurationError(
+          ': {0!s}. Something is wrong with your Application Default '
+          'Credentials. Try running: $ gcloud auth application-default '
+          'login'.format(exception), __name__)
     responses.append(response)
     next_token = response.get('nextPageToken')
     if not next_token:
diff --git a/libcloudforensics/providers/gcp/internal/compute.py b/libcloudforensics/providers/gcp/internal/compute.py
index 6c1bc66..d3cd2d5 100644
--- a/libcloudforensics/providers/gcp/internal/compute.py
+++ b/libcloudforensics/providers/gcp/internal/compute.py
@@ -24,7 +24,7 @@ from googleapiclient.errors import HttpError
 from libcloudforensics.providers.gcp.internal import common, build
 from libcloudforensics.providers.gcp.internal import compute_base_resource
 from libcloudforensics.scripts import utils
-from libcloudforensics import logging_utils
+from libcloudforensics import errors, logging_utils
 
 if TYPE_CHECKING:
   import googleapiclient
@@ -153,15 +153,15 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeInstance: A Google Compute Instance object.
 
     Raises:
-      RuntimeError: If instance does not exist.
+      ResourceNotFoundError: If instance does not exist.
     """
 
     instances = self.Instances()
     instance = instances.get(instance_name)
     if not instance:
-      error_msg = 'Instance {0:s} was not found in project {1:s}'.format(
-          instance_name, self.project_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Instance {0:s} was not found in project {1:s}'.format(
+              instance_name, self.project_id), __name__)
     return instance
 
   def GetDisk(self, disk_name: str) -> 'GoogleComputeDisk':
@@ -174,15 +174,15 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeDisk: Disk object.
 
     Raises:
-      RuntimeError: When the specified disk cannot be found in project.
+      ResourceNotFoundError: When the specified disk cannot be found in project.
     """
 
     disks = self.Disks()
     disk = disks.get(disk_name)
     if not disk:
-      error_msg = 'Disk {0:s} was not found in project {1:s}'.format(
-          disk_name, self.project_id)
-      raise RuntimeError(error_msg)
+      raise errors.ResourceNotFoundError(
+          'Disk {0:s} was not found in project {1:s}'.format(
+              disk_name, self.project_id), __name__)
     return disk
 
   def CreateDiskFromSnapshot(
@@ -206,7 +206,7 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeDisk: Google Compute Disk.
 
     Raises:
-      RuntimeError: If the disk exists already.
+      ResourceCreationError: If the disk could not be created.
     """
 
     if not disk_name:
@@ -227,12 +227,12 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       response = request.execute()
     except HttpError as exception:
       if exception.resp.status == 409:
-        error_msg = 'Disk {0:s} already exists'.format(disk_name)
-        raise RuntimeError(error_msg)
-      error_msg = (
-          'Unknown error (status: {0:d}) occurred when creating disk '
-          'from Snapshot:\n{1!s}').format(exception.resp.status, exception)
-      raise RuntimeError(error_msg)
+        raise errors.ResourceCreationError(
+            'Disk {0:s} already exists: {1!s}'.format(disk_name, exception),
+            __name__)
+      raise errors.ResourceCreationError(
+          'Unknown error occurred when creating disk from Snapshot:'
+          ' {0!s}'.format(exception), __name__)
     self.BlockOperation(response, zone=self.default_zone)
     return GoogleComputeDisk(
         project_id=self.project_id,
@@ -276,20 +276,16 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       RuntimeError: If virtual machine cannot be created.
     """
 
-    if not self.default_zone:
-      raise RuntimeError('Cannot create VM, zone information is missing')
-
     # Re-use instance if it already exists, or create a new one.
     try:
       instance = self.GetInstance(vm_name)
       created = False
       return instance, created
-    except RuntimeError:
+    except errors.ResourceNotFoundError:
       pass
 
-    machine_type = \
-        'zones/{0:s}/machineTypes/n1-standard-{1:d}'.format(
-            self.default_zone, cpu_cores)
+    machine_type = 'zones/{0:s}/machineTypes/n1-standard-{1:d}'.format(
+        self.default_zone, cpu_cores)
     ubuntu_image = self.GceApi().images().getFromFamily(
         project=image_project, family=image_family).execute()
     source_disk_image = ubuntu_image['selfLink']
@@ -422,14 +418,13 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
           GoogleComputeDisk object.
 
     Raises:
+      TypeError: If filter_union is not of type bool
       RuntimeError: If the operation doesn't complete on GCP.
     """
 
     if not isinstance(filter_union, bool):
-      error_msg = (
-          'filter_union parameter must be of Type boolean {0:s} is an '
-          'invalid argument.').format(filter_union)
-      raise RuntimeError(error_msg)
+      raise TypeError('Filter_union parameter must be of Type boolean. {0:s} '
+                      'is an invalid argument.'.format(filter_union))
 
     # pylint: disable=line-too-long
     resource_dict = {}  # type: Dict[str, Union[GoogleComputeInstance, GoogleComputeDisk]]
@@ -481,14 +476,14 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeImage: A Google Compute Image object.
 
     Raises:
-      ValueError: If the GCE Image name is invalid.
+      InvalidNameError: If the GCE Image name is invalid.
     """
 
     if name:
       if not common.REGEX_DISK_NAME.match(name):
-        raise ValueError(
+        raise errors.InvalidNameError(
             'Image name {0:s} does not comply with {1:s}'.format(
-                name, common.REGEX_DISK_NAME.pattern))
+                name, common.REGEX_DISK_NAME.pattern), __name__)
       name = name[:common.COMPUTE_NAME_LIMIT]
     else:
       name = common.GenerateUniqueInstanceName(src_disk.name,
@@ -529,15 +524,15 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeImage: A Google Compute Image object.
 
     Raises:
-      ValueError: If the GCE Image name is invalid, or if the extension of
-          the archived image is not valid.
+      InvalidNameError: If the GCE Image name is invalid.
+      ValueError: If the extension of the archived image is invalid.
     """
 
     if name:
       if not common.REGEX_DISK_NAME.match(name):
-        raise ValueError(
+        raise errors.InvalidNameError(
             'Image name {0:s} does not comply with {1:s}'.format(
-                name, common.REGEX_DISK_NAME.pattern))
+                name, common.REGEX_DISK_NAME.pattern), __name__)
       name = name[:common.COMPUTE_NAME_LIMIT]
     else:
       name = common.GenerateUniqueInstanceName('imported-image',
@@ -579,14 +574,14 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeDisk: A Google Compute Disk object.
 
     Raises:
-      ValueError: If GCE disk name is invalid.
+      InvalidNameError: If GCE disk name is invalid.
     """
 
     if name:
       if not common.REGEX_DISK_NAME.match(name):
-        raise ValueError(
+        raise errors.InvalidNameError(
             'Disk name {0:s} does not comply with {1:s}'.format(
-                name, common.REGEX_DISK_NAME.pattern))
+                name, common.REGEX_DISK_NAME.pattern), __name__)
       name = name[:common.COMPUTE_NAME_LIMIT]
     else:
       name = common.GenerateUniqueInstanceName(src_image.name,
@@ -637,8 +632,8 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       GoogleComputeImage: A Google Compute Image object.
 
     Raises:
-      ValueError: If bootable is True and os_name not specified or
-          if imported image name is invalid.
+      ValueError: If bootable is True and os_name not specified.
+      InvalidNameError: If imported image name is invalid.
     """
 
     supported_os = [
@@ -669,9 +664,9 @@ class GoogleCloudCompute(common.GoogleCloudComputeClient):
       img_type = '-os={0:s}'.format(os_name)
     if image_name:
       if not common.REGEX_DISK_NAME.match(image_name):
-        raise ValueError(
+        raise errors.InvalidNameError(
             'Imported image name {0:s} does not comply with {1:s}'.format(
-                image_name, common.REGEX_DISK_NAME.pattern))
+                image_name, common.REGEX_DISK_NAME.pattern), __name__)
       image_name = image_name[:common.COMPUTE_NAME_LIMIT]
     else:
       image_name = common.GenerateUniqueInstanceName('imported-image',
@@ -728,15 +723,16 @@ class GoogleComputeInstance(compute_base_resource.GoogleComputeBaseResource):
       GoogleComputeDisk: Disk object.
 
     Raises:
-      RuntimeError: If no boot disk could be found.
+      ResourceNotFoundError: If no boot disk could be found.
     """
 
     for disk in self.GetValue('disks'):
       if disk['boot']:
         disk_name = disk['source'].split('/')[-1]
         return GoogleCloudCompute(self.project_id).GetDisk(disk_name=disk_name)
-    raise RuntimeError(
-        'Boot disk not found for instance: {0:s}'.format(self.name))
+    raise errors.ResourceNotFoundError(
+        'Boot disk not found for instance {0:s}'.format(self.name),
+        __name__)
 
   def GetDisk(self, disk_name: str) -> 'GoogleComputeDisk':
     """Gets a disk attached to this virtual machine disk by name.
@@ -748,15 +744,16 @@ class GoogleComputeInstance(compute_base_resource.GoogleComputeBaseResource):
       GoogleComputeDisk: Disk object.
 
     Raises:
-      RuntimeError: If disk name is not found among those attached to the
-          instance.
+      ResourceNotFoundError: If disk name is not found among those attached to
+          the instance.
     """
 
     for disk in self.GetValue('disks'):
       if disk['source'].split('/')[-1] == disk_name:
         return GoogleCloudCompute(self.project_id).GetDisk(disk_name=disk_name)
-    error_msg = 'Disk name "{0:s}" not attached to instance'.format(disk_name)
-    raise RuntimeError(error_msg)
+    raise errors.ResourceNotFoundError(
+        'Disk {0:s} was not found in instance {1:s}'.format(
+            disk_name, self.name), __name__)
 
   def ListDisks(self) -> Dict[str, 'GoogleComputeDisk']:
     """List all disks for the virtual machine.
@@ -893,7 +890,8 @@ class GoogleComputeDisk(compute_base_resource.GoogleComputeBaseResource):
       GoogleComputeSnapshot: A Snapshot object.
 
     Raises:
-      ValueError: If the name of the snapshot does not comply with the RegEx.
+      InvalidNameError: If the name of the snapshot does not comply with the
+          RegEx.
     """
 
     if not snapshot_name:
@@ -901,9 +899,9 @@ class GoogleComputeDisk(compute_base_resource.GoogleComputeBaseResource):
     snapshot_name = common.GenerateUniqueInstanceName(snapshot_name,
                                                       common.COMPUTE_NAME_LIMIT)
     if not common.REGEX_DISK_NAME.match(snapshot_name):
-      raise ValueError(
-          'Snapshot name {0:s} does not comply with '
-          '{1:s}'.format(snapshot_name, common.REGEX_DISK_NAME.pattern))
+      raise errors.InvalidNameError(
+          'Snapshot name {0:s} does not comply with {1:s}'.format(
+              snapshot_name, common.REGEX_DISK_NAME.pattern), __name__)
     logger.info(
         self.FormatLogMessage('New Snapshot: {0:s}'.format(snapshot_name)))
     operation_config = {'name': snapshot_name}
@@ -1003,14 +1001,14 @@ class GoogleComputeImage(compute_base_resource.GoogleComputeBaseResource):
           appended with .tar.gz. Default is [image_name].tar.gz.
 
     Raises:
-      RuntimeError: If exported image name is invalid.
+      InvalidNameError: If exported image name is invalid.
     """
 
     if output_name:
       if not common.REGEX_DISK_NAME.match(output_name):
-        raise RuntimeError(
+        raise errors.InvalidNameError(
             'Exported image name {0:s} does not comply with {1:s}'.format(
-                output_name, common.REGEX_DISK_NAME.pattern))
+                output_name, common.REGEX_DISK_NAME.pattern), __name__)
       full_path = '{0:s}.tar.gz'.format(
           os.path.join(gcs_output_folder, output_name))
     else:
diff --git a/libcloudforensics/providers/gcp/internal/gke.py b/libcloudforensics/providers/gcp/internal/gke.py
index c1db7c3..5518c88 100644
--- a/libcloudforensics/providers/gcp/internal/gke.py
+++ b/libcloudforensics/providers/gcp/internal/gke.py
@@ -20,6 +20,7 @@ from libcloudforensics.providers.gcp.internal import common
 if TYPE_CHECKING:
   import googleapiclient
 
+
 class GoogleKubernetesEngine:
   """Class to call Google Kubernetes Engine (GKE) APIs.
 


Test patch:
diff --git a/tests/providers/aws/aws_test.py b/tests/providers/aws/aws_test.py
index 2925f3c..3053ce7 100644
--- a/tests/providers/aws/aws_test.py
+++ b/tests/providers/aws/aws_test.py
@@ -19,6 +19,7 @@ import unittest
 
 import mock
 
+from libcloudforensics import errors
 from libcloudforensics.providers.aws.internal import account, common, ebs, ec2
 from libcloudforensics.providers.aws.internal import log as aws_log
 from libcloudforensics.providers.aws import forensics
@@ -237,7 +238,7 @@ class AWSAccountTest(unittest.TestCase):
     self.assertEqual('fake-instance-id', found_instance.instance_id)
     self.assertEqual('fake-zone-2', found_instance.region)
     self.assertEqual('fake-zone-2b', found_instance.availability_zone)
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceNotFoundError):
       FAKE_AWS_ACCOUNT.ec2.GetInstanceById('non-existent-instance-id')
 
   @typing.no_type_check
@@ -400,10 +401,10 @@ class AWSAccountTest(unittest.TestCase):
     self.assertFalse(created)
 
     # GetOrCreateAnalysisVm(non_existing_vm, boot_volume_size, AMI, cpu_cores).
-    # We mock the GetInstanceById() call to throw a RuntimeError to mimic
-    # an instance that wasn't found. This should trigger run_instances to be
-    # called.
-    mock_get_instance.side_effect = RuntimeError()
+    # We mock the GetInstancesByName() call to return an empty list to mimic
+    # an instance that wasn't found. This should trigger run_instances
+    # to be called.
+    mock_get_instance.return_value = []
     vm, created = FAKE_AWS_ACCOUNT.ec2.GetOrCreateAnalysisVm(
         'non-existent-instance-name', 1, 'ami-id', 2)
     mock_get_instance.assert_called_with('non-existent-instance-name')
@@ -657,7 +658,7 @@ class AWSTest(unittest.TestCase):
     # Should raise a RuntimeError in GetInstanceById as we are querying a
     # non-existent instance.
     mock_list_instances.return_value = {}
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceCreationError):
       forensics.CreateVolumeCopy(
           FAKE_INSTANCE.availability_zone,
           instance_id='non-existent-instance-id')
@@ -665,7 +666,7 @@ class AWSTest(unittest.TestCase):
     # Should raise a RuntimeError in GetVolumeById as we are querying a
     # non-existent volume.
     mock_list_volumes.return_value = {}
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceCreationError):
       forensics.CreateVolumeCopy(
           FAKE_INSTANCE.availability_zone,
           volume_id='non-existent-volume-id')
diff --git a/tests/providers/azure/azure_test.py b/tests/providers/azure/azure_test.py
index c199caa..86f77ca 100644
--- a/tests/providers/azure/azure_test.py
+++ b/tests/providers/azure/azure_test.py
@@ -20,6 +20,7 @@ import mock
 
 from azure.mgmt.compute.v2020_05_01 import models  # pylint: disable=import-error
 
+from libcloudforensics import errors
 from libcloudforensics.providers.azure.internal import account, compute, common, monitoring  # pylint:disable=line-too-long
 from libcloudforensics.providers.azure import forensics
 
@@ -437,7 +438,7 @@ class TestAccount(unittest.TestCase):
     self.assertEqual('fakestorageid', account_id)
     self.assertEqual('fake-key-value', account_key)
 
-    with self.assertRaises(ValueError) as error:
+    with self.assertRaises(errors.InvalidNameError) as error:
       _, _ = FAKE_ACCOUNT.storage.CreateStorageAccount(
           'fake-non-conform-name')
     # pylint: enable=protected-access
@@ -480,25 +481,27 @@ class TestCommon(unittest.TestCase):
     mock_azure_credentials.assert_called_with(
         'fake-client-id', 'fake-client-secret', tenant='fake-tenant-id')
 
-    # If an environment variable is missing, a RuntimeError should be raised
+    # If an environment variable is missing, a CredentialsConfigurationError
+    # \should be raised
     del os.environ['AZURE_SUBSCRIPTION_ID']
-    with self.assertRaises(RuntimeError) as error:
+    with self.assertRaises(errors.CredentialsConfigurationError) as error:
       _, _ = common.GetCredentials()
       mock_azure_credentials.assert_not_called()
     self.assertEqual(
         'Please make sure you defined the following environment variables: '
-        '[AZURE_SUBSCRIPTION_ID,AZURE_CLIENT_ID, AZURE_CLIENT_SECRET,'
+        '[AZURE_SUBSCRIPTION_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, '
         'AZURE_TENANT_ID].', str(error.exception))
 
     # If a profile name is passed to the method, then it will look for a
     # credential file (default path being ~/.azure/credentials.json). We can
     # set a particular path by setting the AZURE_CREDENTIALS_PATH variable.
 
-    # If the file is not a valid json file, should raise a ValueError
+    # If the file is not a valid json file, should raise an
+    # InvalidFileFormatError
     os.environ['AZURE_CREDENTIALS_PATH'] = os.path.join(
         os.path.dirname(os.path.dirname(os.path.dirname(
             os.path.realpath(__file__)))), STARTUP_SCRIPT)
-    with self.assertRaises(ValueError) as error:
+    with self.assertRaises(errors.InvalidFileFormatError) as error:
       _, _ = common.GetCredentials(profile_name='foo')
       mock_azure_credentials.assert_not_called()
     self.assertEqual(
@@ -518,8 +521,9 @@ class TestCommon(unittest.TestCase):
         'fake-client-secret-from-credential-file',
         tenant='fake-tenant-id-from-credential-file')
 
-    # If the profile name does not exist, should raise a ValueError
-    with self.assertRaises(ValueError) as error:
+    # If the profile name does not exist, should raise a
+    # CredentialsConfigurationError
+    with self.assertRaises(errors.CredentialsConfigurationError) as error:
       _, _ = common.GetCredentials(profile_name='foo')
       mock_azure_credentials.assert_not_called()
     self.assertEqual(
@@ -527,8 +531,8 @@ class TestCommon(unittest.TestCase):
             os.environ['AZURE_CREDENTIALS_PATH']), str(error.exception))
 
     # If the profile name exists but there are missing entries, should raise
-    # a ValueError
-    with self.assertRaises(ValueError) as error:
+    # a CredentialsConfigurationError
+    with self.assertRaises(errors.CredentialsConfigurationError) as error:
       _, _ = common.GetCredentials(profile_name='incomplete_profile_name')
       mock_azure_credentials.assert_not_called()
     self.assertEqual(
@@ -563,12 +567,12 @@ class TestAZVirtualMachine(unittest.TestCase):
     disk = FAKE_INSTANCE.GetDisk('fake-disk-name')
     self.assertEqual('fake-disk-name', disk.name)
 
-    with self.assertRaises(RuntimeError) as error:
+    with self.assertRaises(errors.ResourceNotFoundError) as error:
       FAKE_INSTANCE.GetDisk('non-existent-disk-name')
     self.assertEqual(
-        'Disk non-existent-disk-name not found in instance: '
-        '/subscriptions/sub/resourceGroups/fake-resource-group'
-        '/providers/Microsoft.Compute/type/fake-vm-name', str(error.exception))
+        'Disk non-existent-disk-name was not found in instance '
+        '/subscriptions/sub/resourceGroups/fake-resource-group/providers/'
+        'Microsoft.Compute/type/fake-vm-name', str(error.exception))
 
   @mock.patch('libcloudforensics.providers.azure.internal.compute.AZCompute.ListDisks')
   @mock.patch('azure.mgmt.compute.v2020_06_01.operations._virtual_machines_operations.VirtualMachinesOperations.get')
@@ -756,7 +760,7 @@ class TestForensics(unittest.TestCase):
     mock_resource_group.return_value = 'fake-resource-group'
     mock_disk_type.return_value = 'fake-disk-type'
 
-    with self.assertRaises(RuntimeError) as error:
+    with self.assertRaises(errors.ResourceCreationError) as error:
       forensics.CreateDiskCopy(
           FAKE_ACCOUNT.default_resource_group_name,
           instance_name='non-existent-vm-name',
@@ -765,7 +769,7 @@ class TestForensics(unittest.TestCase):
         'Cannot copy disk "None": Instance non-existent-vm-name was not found '
         'in subscription fake-subscription-id', str(error.exception))
 
-    with self.assertRaises(RuntimeError) as error:
+    with self.assertRaises(errors.ResourceCreationError) as error:
       forensics.CreateDiskCopy(
           FAKE_ACCOUNT.default_resource_group_name,
           disk_name='non-existent-disk-name',
diff --git a/tests/providers/gcp/gcp_test.py b/tests/providers/gcp/gcp_test.py
index 460d0fd..7ed07c5 100644
--- a/tests/providers/gcp/gcp_test.py
+++ b/tests/providers/gcp/gcp_test.py
@@ -27,6 +27,7 @@ import mock
 import six
 
 # pylint: disable=line-too-long
+from libcloudforensics import errors
 from libcloudforensics.providers.gcp import forensics
 from libcloudforensics.providers.gcp.internal import build as gcp_build
 from libcloudforensics.providers.gcp.internal import common, compute
@@ -406,7 +407,7 @@ class GoogleCloudProjectTest(unittest.TestCase):
     # pylint: disable=protected-access
     self.assertEqual(FAKE_INSTANCE._data, found_instance._data)
     # pylint: enable=protected-access
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceNotFoundError):
       FAKE_SOURCE_PROJECT.compute.GetInstance('non-existent-instance')
 
   @typing.no_type_check
@@ -419,7 +420,7 @@ class GoogleCloudProjectTest(unittest.TestCase):
     self.assertEqual(FAKE_SOURCE_PROJECT.project_id, found_disk.project_id)
     self.assertEqual('fake-disk', found_disk.name)
     self.assertEqual('fake-zone', found_disk.zone)
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceNotFoundError):
       FAKE_SOURCE_PROJECT.compute.GetDisk('non-existent-disk')
 
   @typing.no_type_check
@@ -468,19 +469,20 @@ class GoogleCloudProjectTest(unittest.TestCase):
     # disk_name='fake-disk') where 'fake-disk' exists already
     disks.return_value.insert.return_value.execute.side_effect = HttpError(
         resp=mock.Mock(status=409), content=b'Disk already exists')
-    with self.assertRaises(RuntimeError) as context:
+    with self.assertRaises(errors.ResourceCreationError) as context:
       _ = FAKE_ANALYSIS_PROJECT.compute.CreateDiskFromSnapshot(
           FAKE_SNAPSHOT, disk_name=FAKE_DISK.name)
-    self.assertEqual(
+    self.assertIn(
         'Disk {0:s} already exists'.format('fake-disk'), str(context.exception))
 
     # other network issue should fail the disk creation
     disks.return_value.insert.return_value.execute.side_effect = HttpError(
         resp=mock.Mock(status=418), content=b'I am a teapot')
-    with self.assertRaises(RuntimeError) as context:
+    with self.assertRaises(errors.ResourceCreationError) as context:
       _ = FAKE_ANALYSIS_PROJECT.compute.CreateDiskFromSnapshot(
           FAKE_SNAPSHOT, FAKE_DISK.name)
-    self.assertIn('status: 418', str(context.exception))
+    self.assertIn('Unknown error occurred when creating disk from Snapshot',
+                  str(context.exception))
 
   @typing.no_type_check
   @mock.patch('libcloudforensics.providers.gcp.internal.compute.GoogleCloudCompute.GetInstance')
@@ -511,7 +513,7 @@ class GoogleCloudProjectTest(unittest.TestCase):
     # GetOrCreateAnalysisVm(non_existing_vm, boot_disk_size) mocking the
     # GetInstance() call to throw a runtime error to mimic an instance that
     # wasn't found
-    mock_get_instance.side_effect = RuntimeError()
+    mock_get_instance.side_effect = errors.ResourceNotFoundError('', __name__)
     vm, created = FAKE_ANALYSIS_PROJECT.compute.GetOrCreateAnalysisVm(
         'non-existent-analysis-vm', boot_disk_size=1)
     mock_get_instance.assert_called_with('non-existent-analysis-vm')
@@ -681,7 +683,7 @@ class GoogleComputeInstanceTest(unittest.TestCase):
     self.assertEqual('fake-boot-disk', disk.name)
 
     # Disk that's not attached to the instance
-    with self.assertRaises(RuntimeError):
+    with self.assertRaises(errors.ResourceNotFoundError):
       FAKE_INSTANCE.GetDisk('non-existent-disk')
 
   @typing.no_type_check
@@ -721,8 +723,9 @@ class GoogleComputeDiskTest(unittest.TestCase):
     self.assertIsInstance(snapshot, compute.GoogleComputeSnapshot)
     self.assertTrue(snapshot.name.startswith('my-snapshot'))
 
-    # Snapshot(snapshot_name='Non-compliant-name'). Should raise a ValueError
-    with self.assertRaises(ValueError):
+    # Snapshot(snapshot_name='Non-compliant-name'). Should raise an
+    # InvalidNameError
+    with self.assertRaises(errors.InvalidNameError):
       FAKE_DISK.Snapshot('Non-compliant-name')
 
 
@@ -943,8 +946,9 @@ class GCPTest(unittest.TestCase):
     #     dst_proj,
     #     zone='fake-zone',
     #     instance_name=None,
-    #     disk_name='non-existent-disk') Should raise an exception
-    with self.assertRaises(RuntimeError):
+    #     disk_name='non-existent-disk') Should raise a ResourceNotFoundError
+    #     exception
+    with self.assertRaises(errors.ResourceNotFoundError):
       forensics.CreateDiskCopy(FAKE_SOURCE_PROJECT.project_id,
                                FAKE_ANALYSIS_PROJECT.project_id,
                                zone=FAKE_INSTANCE.zone,
@@ -955,8 +959,8 @@ class GCPTest(unittest.TestCase):
     #     dst_proj,
     #     instance_name='non-existent-instance',
     #     zone='fake-zone',
-    #     disk_name=None) Should raise an exception
-    with self.assertRaises(RuntimeError):
+    #     disk_name=None) Should raise a ResourceNotFoundError exception
+    with self.assertRaises(errors.ResourceNotFoundError):
       forensics.CreateDiskCopy(FAKE_SOURCE_PROJECT.project_id,
                                FAKE_ANALYSIS_PROJECT.project_id,
                                instance_name='non-existent-instance',
@@ -1018,7 +1022,7 @@ class GCPTest(unittest.TestCase):
     self.assertTrue(REGEX_DISK_NAME.match(disk_name))
 
     # Disk prefix cannot start with a capital letter
-    with self.assertRaises(ValueError):
+    with self.assertRaises(errors.InvalidNameError):
       common.GenerateDiskName(
           FAKE_SNAPSHOT, 'Some-prefix-that-starts-with-a-capital-letter')