<commit_before># -*- 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.
"""Common utilities."""

import binascii
import datetime
import re
import socket
import time
from typing import TYPE_CHECKING, Dict, List, Optional, Any
import netaddr

from google.auth import default
from google.auth.exceptions import DefaultCredentialsError
from google.auth.exceptions import RefreshError
from googleapiclient.discovery import build
from libcloudforensics import logging_utils  # pylint: disable=ungrouped-imports
from libcloudforensics import errors  # pylint: disable=ungrouped-imports

if TYPE_CHECKING:
  import googleapiclient
  # 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.gcp.internal import compute  # pylint: disable=cyclic-import

RETRY_MAX = 10
COMPUTE_RFC1035_REGEX = re.compile('^(?=.{1,63}$)[a-z]([-a-z0-9]*[a-z0-9])?$')
REGEX_DISK_NAME = COMPUTE_RFC1035_REGEX
COMPUTE_NAME_LIMIT = 63
STORAGE_LINK_URL = 'https://storage.cloud.google.com'
DEFAULT_COMPUTE_INSTANCE_SCOPE = [
    'https://www.googleapis.com/auth/devstorage.read_only',
    'https://www.googleapis.com/auth/logging.write',
    'https://www.googleapis.com/auth/monitoring.write',
    'https://www.googleapis.com/auth/service.management.readonly',
    'https://www.googleapis.com/auth/servicecontrol',
    'https://www.googleapis.com/auth/trace.append'
]
DEFAULT_SA_EMAIL = 'default'
logging_utils.SetUpLogger(__name__)
logger = logging_utils.GetLogger(__name__)


def GenerateDiskName(
    snapshot: 'compute.GoogleComputeSnapshot',
    disk_name_prefix: Optional[str] = None) -> str:
  """Generate a new disk name for the disk to be created from the Snapshot.

  The disk name must comply with the following RegEx:
      - ^(?=.{1,63}$)[a-z]([-a-z0-9]*[a-z0-9])?$

  i.e., it must be between 1 and 63 chars, the first character must be a
  lowercase letter, and all following characters must be a dash, lowercase
  letter, or digit, except the last character, which cannot be a dash.

  Args:
    snapshot (GoogleComputeSnapshot): A disk's Snapshot.
    disk_name_prefix (str): Optional. A prefix for the disk name.

  Returns:
    str: A name for the disk.

  Raises:
    InvalidNameError: If the disk name does not comply with the RegEx.
  """

  # Max length of disk names in GCP is 63 characters
  project_id = snapshot.project_id
  disk_id = project_id + snapshot.name
  disk_id_crc32 = '{0:08x}'.format(
      binascii.crc32(disk_id.encode()) & 0xffffffff)
  truncate_at = 63 - len(disk_id_crc32) - len('-copy') - 1
  if disk_name_prefix:
    disk_name_prefix += '-'
    if len(disk_name_prefix) > truncate_at:
      # The disk name prefix is too long
      disk_name_prefix = disk_name_prefix[:truncate_at]
    truncate_at -= len(disk_name_prefix)
    disk_name = '{0:s}{1:s}-{2:s}-copy'.format(
        disk_name_prefix, snapshot.name[:truncate_at], disk_id_crc32)
  else:
    disk_name = '{0:s}-{1:s}-copy'.format(
        snapshot.name[:truncate_at], disk_id_crc32)

  if not REGEX_DISK_NAME.match(disk_name):
    raise errors.InvalidNameError(
        'Disk name {0:s} does not comply with {1:s}'.format(
            disk_name, REGEX_DISK_NAME.pattern),
        __name__)

  return disk_name


def GenerateSourceRange(
    exempted_src_ips: Optional[List[str]] = None) -> List[str]:
  """Generate a list of denied source IP ranges.

  The final list is a list of all IPs except the exempted ones.

  Args:
    exempted_src_ips (List[str]): List of IPs exempted from the deny-all
        ingress firewall rules, ex: analyst IPs.

  Returns:
      List[str]: Denied source IP ranges specified in CIDR notation.
  """
  source_range = []
  if exempted_src_ips:
    ip_set = netaddr.IPSet(netaddr.IPRange('0.0.0.0', '255.255.255.255'))
    for ip in exempted_src_ips:
      ip_set.remove(ip)
    ip_cidrs = ip_set.iter_cidrs()
    for ip_cidr in ip_cidrs:
      source_range.append(
          '{0:s}/{1:d}'.format(ip_cidr.ip.format(), ip_cidr.prefixlen))
  else:
    source_range.append('0.0.0.0/0')
  return source_range


def GenerateUniqueInstanceName(
    prefix: str, truncate_at: Optional[int] = None) -> str:
  """Add a timestamp as a suffix to provided name and truncate at max limit.

  Args:
    prefix (str): The name prefix to add the timestamp to and truncate.
    truncate_at (int): Optional. The maximum length of the generated name.
        Default no limit.

  Returns:
    str: The name after adding a timestamp.
        Ex: [prefix]-[TIMESTAMP('%Y%m%d%H%M%S')]
  """
  timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
  if truncate_at:
    truncate_at = truncate_at - len(timestamp) - 1
  name = '{0:s}-{1:s}'.format(prefix[:truncate_at], timestamp)
  return name


def CreateService(
    service_name: str,
    api_version: str) -> 'googleapiclient.discovery.Resource':
  """Creates an GCP API service.
<commit_msg>Add randomness to snapshot names (#434)<commit_after># -*- 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.
"""Common utilities."""

import binascii
import datetime
import random
import re
import socket
import string
import time
from typing import TYPE_CHECKING, Dict, List, Optional, Any
import netaddr

from google.auth import default
from google.auth.exceptions import DefaultCredentialsError
from google.auth.exceptions import RefreshError
from googleapiclient.discovery import build
from libcloudforensics import logging_utils  # pylint: disable=ungrouped-imports
from libcloudforensics import errors  # pylint: disable=ungrouped-imports

if TYPE_CHECKING:
  import googleapiclient
  # 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.gcp.internal import compute  # pylint: disable=cyclic-import

RETRY_MAX = 10
COMPUTE_RFC1035_REGEX = re.compile('^(?=.{1,63}$)[a-z]([-a-z0-9]*[a-z0-9])?$')
REGEX_DISK_NAME = COMPUTE_RFC1035_REGEX
COMPUTE_NAME_LIMIT = 63
STORAGE_LINK_URL = 'https://storage.cloud.google.com'
DEFAULT_COMPUTE_INSTANCE_SCOPE = [
    'https://www.googleapis.com/auth/devstorage.read_only',
    'https://www.googleapis.com/auth/logging.write',
    'https://www.googleapis.com/auth/monitoring.write',
    'https://www.googleapis.com/auth/service.management.readonly',
    'https://www.googleapis.com/auth/servicecontrol',
    'https://www.googleapis.com/auth/trace.append'
]
DEFAULT_SA_EMAIL = 'default'
logging_utils.SetUpLogger(__name__)
logger = logging_utils.GetLogger(__name__)


def GenerateDiskName(
    snapshot: 'compute.GoogleComputeSnapshot',
    disk_name_prefix: Optional[str] = None) -> str:
  """Generate a new disk name for the disk to be created from the Snapshot.

  The disk name must comply with the following RegEx:
      - ^(?=.{1,63}$)[a-z]([-a-z0-9]*[a-z0-9])?$

  i.e., it must be between 1 and 63 chars, the first character must be a
  lowercase letter, and all following characters must be a dash, lowercase
  letter, or digit, except the last character, which cannot be a dash.

  Args:
    snapshot (GoogleComputeSnapshot): A disk's Snapshot.
    disk_name_prefix (str): Optional. A prefix for the disk name.

  Returns:
    str: A name for the disk.

  Raises:
    InvalidNameError: If the disk name does not comply with the RegEx.
  """

  # Max length of disk names in GCP is 63 characters
  project_id = snapshot.project_id
  disk_id = project_id + snapshot.name
  disk_id_crc32 = '{0:08x}'.format(
      binascii.crc32(disk_id.encode()) & 0xffffffff)
  truncate_at = 63 - len(disk_id_crc32) - len('-copy') - 1
  if disk_name_prefix:
    disk_name_prefix += '-'
    if len(disk_name_prefix) > truncate_at:
      # The disk name prefix is too long
      disk_name_prefix = disk_name_prefix[:truncate_at]
    truncate_at -= len(disk_name_prefix)
    disk_name = '{0:s}{1:s}-{2:s}-copy'.format(
        disk_name_prefix, snapshot.name[:truncate_at], disk_id_crc32)
  else:
    disk_name = '{0:s}-{1:s}-copy'.format(
        snapshot.name[:truncate_at], disk_id_crc32)

  if not REGEX_DISK_NAME.match(disk_name):
    raise errors.InvalidNameError(
        'Disk name {0:s} does not comply with {1:s}'.format(
            disk_name, REGEX_DISK_NAME.pattern),
        __name__)

  return disk_name


def GenerateSourceRange(
    exempted_src_ips: Optional[List[str]] = None) -> List[str]:
  """Generate a list of denied source IP ranges.

  The final list is a list of all IPs except the exempted ones.

  Args:
    exempted_src_ips (List[str]): List of IPs exempted from the deny-all
        ingress firewall rules, ex: analyst IPs.

  Returns:
      List[str]: Denied source IP ranges specified in CIDR notation.
  """
  source_range = []
  if exempted_src_ips:
    ip_set = netaddr.IPSet(netaddr.IPRange('0.0.0.0', '255.255.255.255'))
    for ip in exempted_src_ips:
      ip_set.remove(ip)
    ip_cidrs = ip_set.iter_cidrs()
    for ip_cidr in ip_cidrs:
      source_range.append(
          '{0:s}/{1:d}'.format(ip_cidr.ip.format(), ip_cidr.prefixlen))
  else:
    source_range.append('0.0.0.0/0')
  return source_range


def GenerateUniqueInstanceName(
    prefix: str, truncate_at: Optional[int] = None) -> str:
  """Add a timestamp as a suffix to provided name and truncate at max limit.

  Args:
    prefix (str): The name prefix to add the timestamp to and truncate.
    truncate_at (int): Optional. The maximum length of the generated name.
        Default no limit.

  Returns:
    str: The name after adding a timestamp.
        Ex: [prefix]-[rand]-[TIMESTAMP('%Y%m%d%H%M%S')]
  """
  rand = ''.join(random.choices(string.ascii_lowercase, k=5))
  timestamp = datetime.datetime.now().strftime('%Y%m%d%H%M%S')
  if truncate_at:
    prefix = prefix[:(truncate_at - len(timestamp) - 7)]
  name = f'{prefix}-{rand}-{timestamp}'
  return name


def CreateService(
    service_name: str,
    api_version: str) -> 'googleapiclient.discovery.Resource':
  """Creates an GCP API service.

{'fraction_non_alphanumeric': 0.11034047919293821, 'fraction_numerical': 0.01639344262295082, 'mean_word_length': 3.876152832674572, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 13, 'https://': 14, 'lorem ipsum': 0, 'www.': 14, 'xml': 0}, 'pii_count': 6, 'substrings_counts': 2, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6433545', 'n_tokens_mistral': 3773, 'n_tokens_neox': 3620, 'n_words': 1061}