gce_rescue/gce.py (160 lines of code) (raw):

# Copyright 2021 Google LLC # # 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. """ Initilization Instance() with VM information. """ import sys from googleapiclient.discovery import Resource from googleapiclient.errors import HttpError from dataclasses import dataclass, field from typing import Dict, List, Union from time import time from gce_rescue.tasks.backup import backup_metadata_items from gce_rescue.tasks.disks import list_disk, list_snapshot from gce_rescue.tasks.pre_validations import Validations from gce_rescue.config import get_config def get_instance_info( compute: Resource, name: str, project_data: Dict[str, str] ) -> Dict: """Set Dictionary with complete data from instances().get() from the instance. https://cloud.google.com/compute/docs/reference/rest/v1/instances/get Attributes: compute: obj, API Object instance: str, Instace name project_data: dict, Dictionary containing project and zone keys to be unpacked when calling the API. """ return compute.instances().get( **project_data, instance = name).execute() def guess_guest(data: Dict) -> str: """Determined which Guest OS Family is being used and select a different OS for recovery disk. Default: projects/debian-cloud/global/images/family/debian-11""" guests = get_config('source_guests') for disk in data['disks']: if disk['boot']: if 'architecture' in disk: arch = disk['architecture'].lower() else: arch = 'x86_64' guest_default = guests[arch][0] guest_name = guest_default.split('/')[-1] for lic in disk['licenses']: if guest_name in lic: guest_default = guests[arch][1] return guest_default def validate_instance_mode(data: Dict) -> Dict: """Validate if the instance is already configured as rescue mode.""" result = { 'rescue-mode': False, 'ts': generate_ts() } if 'metadata' in data and 'items' in data['metadata']: metadata = data['metadata'] for item in metadata['items']: if item['key'] == 'rescue-mode': result = { 'rescue-mode': True, 'ts': item['value'] } return result def generate_ts() -> int: """Get the current timestamp to be used as unique ID during this execution.""" return int(time()) @dataclass class Instance(Resource): """Initialize instance.""" zone: str name: str project: str = None test_mode: bool = field(default_factory=False) compute: Resource = field(init=False) data: Dict[str, Union[str, int]] = field(init=False) ts: int = field(init=False) _status: str = '' _rescue_source_disk: str = '' _rescue_mode_status: Dict[str, Union[str, int]] = field( default_factory=lambda: ({}) ) _disks: Dict[str, str] = field(default_factory=lambda: ({})) _backup_items: Dict[str, Union[str, int]] = field( default_factory=lambda: ([]) ) def __post_init__(self): check = Validations( name=self.name, test_mode=self.test_mode, **self.project_data ) try: self.compute = check.compute self.project = check.adc_project self.data = get_instance_info( compute=self.compute, name=self.name, project_data=self.project_data) except HttpError as e: print(e.reason) sys.exit(1) self._rescue_mode_status = validate_instance_mode(self.data) self.ts = self._rescue_mode_status['ts'] self._status = self.data['status'] self._rescue_source_disk = guess_guest(self.data) self._disks = self._define_disks() # Backup metadata items self._backup_items = backup_metadata_items( data=self.data ) def refresh_fingerprint(self) -> None: """Refresh the current metadata fingerprint value.""" project_data = get_instance_info( compute=self.compute, name=self.name, project_data=self.project_data) new_fingerprint = project_data['metadata']['fingerprint'] self.data['metadata']['fingerprint'] = new_fingerprint def _define_disks(self) -> Dict[str, str]: """Define the values of disk_name and device_name.""" rescue_on = self._rescue_mode_status['rescue-mode'] if not rescue_on: for disk in self.data['disks']: if disk['boot']: device_name = disk['deviceName'] source = disk['source'] disk_name = source.split('/')[-1] else: ts = self._rescue_mode_status['ts'] disk_filter = f'labels.rescue={ts}' disk = list_disk( vm=self, project_data=self.project_data, label_filter=disk_filter ) disk_name = disk[0]['name'] disks = self.data['disks'] for disk in disks: full_source = disk['source'] source = full_source.split('/')[-1] if disk_name == source: device_name = disk['deviceName'] result = { 'device_name': device_name, 'disk_name': disk_name } return result @property def rescue_mode_status(self) -> Dict[str, Union[str, int]]: return self._rescue_mode_status @property def project_data(self) -> str: return {'project': self.project, 'zone': self.zone} @property def rescue_disk(self) -> str: return f'linux-rescue-disk-{self.ts}' @property def status(self) -> str: return self._status @status.setter def status(self, v: str) -> None: self._status = v @property def rescue_source_disk(self) -> str: return self._rescue_source_disk @rescue_source_disk.setter def rescue_source_disk(self, v: str) -> None: self._rescue_source_disk = v @property def backup_items(self) -> List[str]: return self._backup_items @backup_items.setter def backup_items(self, v: List[str]) -> None: self._backup_items = v @property def disks(self) -> List[str]: return self._disks @property def snapshot(self) -> str: if not self.rescue_mode_status['rescue-mode']: return f"{self.disks['disk_name']}-{self.ts}" return list_snapshot(self)