diff --git a/items/openstack-backups/README.md b/items/openstack-backups/README.md index 1e9bc1c..5f4624e 100644 --- a/items/openstack-backups/README.md +++ b/items/openstack-backups/README.md @@ -1,6 +1,6 @@ # Automation of OpenStack backups -The OpenStack Horizon UI allows the manual creation of backups and snapshots from individual instances and volumnes, but does not provide automation features for backing up or restoring multiple resources nor it offers options for the scheduling of backups. The OpenStack Command Line Interface (CLI) provides the same capabilities, without any automation features. The scheduling of backups using the OpenStack CLI must be done using `cron` jobs. Documentation on how to use the OpenStack CLI to create and schedule backups can be found [here](https://confluence.ecmwf.int/display/EWCLOUDKB/EWC+OpenStack+API+access+-+How+to+create+backups+from+VMs) and to restore backups [here](https://confluence.ecmwf.int/display/EWCLOUDKB/EWC+OpenStack+API+access+-+How+to+restore+backups+from+VMs). With this script, users can create, schedule or restore multiple backups automatically. +The OpenStack Horizon UI allows the manual creation of backups and snapshots from individual instances and volumes, but does not provide automation features for backing up or restoring multiple resources nor it offers options for the scheduling of backups. The OpenStack Command Line Interface (CLI) provides the same capabilities, without any automation features. The scheduling of backups using the OpenStack CLI must be done using `cron` jobs. Documentation on how to use the OpenStack CLI to create and schedule backups can be found [here](https://confluence.ecmwf.int/display/EWCLOUDKB/EWC+OpenStack+API+access+-+How+to+create+backups+from+VMs) and to restore backups [here](https://confluence.ecmwf.int/display/EWCLOUDKB/EWC+OpenStack+API+access+-+How+to+restore+backups+from+VMs). With this script, users can create, schedule or restore multiple backups automatically. ## Functionality This script automates the creation, restoration and scheduling of backups using the OpenStack SDK. This allows users with OpenStack credentials to create backups or snapshots of multiple instances and their attached volumes, scheduling backups for a future time with and without repetition and with and without a retention count. It also allows the restoration of multiple instances or volumes, in-place or to a new instance or volume. @@ -17,39 +17,18 @@ OR * `docker>=29.1.3` ## Usage -The script can be run either directly using the current python environment, satisfying the prerequisites above, or inside a docker container. In either case, it is necessary to first set up the authentication credentials file and the configuration file. - -### 1. Authentication credentials - -It is necessary to have the required OpenStack credentials to access the project/domain/cloud specified in the configuration file. The program expects a credentials file in the root directory called `clouds.yaml`, which contains the necessary information for authentication into the cloud. An example authentication file to acess in to `my_cloud` with `my_username` is -``` -clouds: - my_cloud: - auth_type: v3oidcpassword - auth: - auth_url: https://keystone.cloudferro.com:5000/v3 - username: my_username - password: my_password - project_id: my_project_id - project_name: my_project_name - project_domain_name: my_domain_name - project_domain_id: my_project_domain_id - client_id: openstack - client_secret: my_client_secret - protocol: openid - identity_provider: eumetsat_provider - discovery_endpoint: https://identity.cloudferro.com/auth/realms/Eumetsat-elasticity/.well-known/openid-configuration - region_name: WAW3-1 - interface: public - identity_api_version: 3 -``` -The `clouds.yaml` file can be obtained from the cloud server provider or filled manually with the information in an OpenStack RC file. A template `clouds.yaml` file can be found in the `templates` directory. +The script can be run either directly using the current python environment, satisfying the prerequisites above, or inside a docker container. In either case, it is necessary to first set up the authentication credentials file and the configuration file. It is highly recommended not to run this script within the VM you wish to back up or restore. + +### 1. Application credentials + +To run this script it is necessary to have the required OpenStack application credentials to access the project/domain/cloud specified in the configuration file. You can find information on how to create application credentials and obtain the RC file or clouds.yaml file in [here](https://confluence.ecmwf.int/display/EWCLOUDKB/EWC+Cloud+Management+UI+-+Identity+-+Create+Application+Credentials) ### 2. Configuration file A configuration YAML file that contains the requested information to create, schedule or restore backups is required to run the script. A template YAML file can be found in the `templates` directory and some examples in the `tests` directory. The structure of this configuration file is as follows ``` cloud: +authentication: backup: - name: @@ -63,7 +42,7 @@ restore: mode: - ... ``` -where the `` corresponds to the name of cloud (domain name) to which the various resources belong. The optional `backup` node contains instructions to create and schedule backups, as explained below, and the optional `restore` node contains instructions to restore backups. +where the `` corresponds to the name of cloud (domain name) to which the various resources belong. The `authentication` node indicates the mode in which the credentials will be provided, which should be either `openrc` if the application credentials have been sourced from an OpenRC file, or `clouds.yaml` if the application credentials are stored in an eponymous file in the current directory. The optional `backup` node contains instructions to create and schedule backups, as explained below, and the optional `restore` node contains instructions to restore backups. #### 2.1 Creating backups @@ -83,7 +62,7 @@ backup: - ... ``` -where `` is the name of the instance or volume to back up, `` is either `instance` or `volume` and `` is either `snapshot` or `backup`. Any number of entries, corresponding to the resources to backup, can be provided to the `backup` node. By default, attachments are not backed up along with the resource. In order to back these up, one must provide the `attachment` field to the resource, as seen above. It is possible to select to backup all attachments of the resource, with `attachments: all`, or a specific set, by providing a list of the resources to backup. It is recommended to stop instances and detaching volumes before backing them up. This is the default behaviour. The options `stop` and `detach` can be supplied to instances and volumes, respectively, to change this default behaviour. +where `` is the name of the instance or volume to back up, `` is either `instance` or `volume` and `` is either `snapshot` or `backup`. Any number of entries, corresponding to the resources to backup, can be provided to the `backup` node. By default, attachments are not backed up along with the resource (with the exception of root volume of volume-backed instances). In order to back these up, one must provide the `attachment` field to the resource, as seen above. It is possible to select to backup all attachments of the resource, with `attachments: all`, or a specific set, by providing a list of the resources to backup. It is recommended to stop instances and detaching volumes before backing them up. This is the default behaviour. The options `stop` and `detach` can be supplied to instances and volumes, respectively, to change this default behaviour. #### 2.2 Scheduling backups diff --git a/items/openstack-backups/openstack_backups.py b/items/openstack-backups/openstack_backups.py index 491936f..8c251d7 100644 --- a/items/openstack-backups/openstack_backups.py +++ b/items/openstack-backups/openstack_backups.py @@ -54,13 +54,14 @@ def parse_config_file(config_file_path: str) -> dict: else: raise RuntimeError(f'{yaml_error}: Cloud name missing.') - # Validate the authentication method (default clouds.yaml) - # FIXME: Currently the openrc method does not work, so force it to be clouds.yaml - #if "auth" in yaml_contents: - # config['auth'] = yaml_contents['auth'] - #else: - # config['auth'] = 'openrc' - config['auth'] = 'clouds.yaml' + + # Authentication method can be with Application Credentials from an OpenRC file or a clouds.yaml file + if 'authentication' not in yaml_contents: + raise RuntimeError(f'{yaml_error}: Missing application credentials. The entry \'authentication\' is required.') + elif yaml_contents['authentication'] not in ['openrc','clouds.yaml']: + raise RuntimeError(f'{yaml_error}: Unknown application credentials: The entry \'authentication\' should be either \'openrc\' or \'clouds.yaml\'.') + else: + config['authentication'] = yaml_contents['authentication'] # Validate the backup entry if 'backup' in yaml_contents: @@ -152,7 +153,6 @@ def parse_config_file(config_file_path: str) -> dict: restore['in_place'] = restore.get('in_place', False) # If the restoration is for an instance and not in place, one needs to provide flavor, network and (optionally) security groups - # TODO: Might restore all networks later if restore['type'] == 'instance' and not restore['in_place']: if 'flavor' not in restore.keys(): raise RuntimeError(f'{yaml_error}: Restore entry missing flavor, required for non-in-place restorations.') @@ -170,26 +170,74 @@ def parse_config_file(config_file_path: str) -> dict: else: raise FileNotFoundError('Backups config file not found.') -def authenticate(auth: str) -> None: +def authenticate(authentication: str) -> None: """ - Ensure authentication credential are ready. + Ensure application credentials are ready. Two methods of authentication, with openrc file (pre-source) or clouds.yaml file """ - if auth=="openrc": - # Check the appropriate environment variables have been set - #required_envs = ['OS_IDENTITY_API_VERSION', 'OS_AUTH_URL', 'OS_AUTH_TYPE', 'OS_USERNAME', 'OS_PASSWORD', 'OS_PROJECT_NAME', 'OS_PROJECT_DOMAIN_ID'] - required_envs = ['OS_AUTH_URL', 'OS_INTERFACE', 'OS_IDENTITY_API_VERSION', 'OS_USERNAME', 'OS_REGION_NAME', 'OS_USER_DOMAIN_NAME', 'OS_PROJECT_DOMAIN_ID', 'OS_AUTH_TYPE', 'OS_APPLICATION_CREDENTIAL_ID', 'OS_APPLICATION_CREDENTIAL_SECRET'] + if authentication=="openrc": + + # Check if authentication method uses application credentials of password + + if "OS_AUTH_TYPE" in os.environ: + + required_envs = ['OS_AUTH_TYPE', 'OS_AUTH_URL', 'OS_IDENTITY_API_VERSION', 'OS_REGION_NAME', 'OS_INTERFACE'] - for env in required_envs: - if env not in os.environ: - raise RuntimeError(f'Authentication not possible. Environment variable {env} not set. Source the openrc file in order to set all required environment varibles') + # Application credentials + if os.getenv("OS_AUTH_TYPE") == "v3applicationcredential": + required_envs += ['OS_APPLICATION_CREDENTIAL_ID', 'OS_APPLICATION_CREDENTIAL_SECRET'] + elif os.getenv("OS_AUTH_TYPE") == "v3oidcpassword": + required_envs += ['OS_USERNAME', 'OS_PASSWORD', 'OS_CLIENT_ID', 'OS_CLIENT_SECRET', 'OS_PROTOCOL', 'OS_IDENTITY_PROVIDER', 'OS_DISCOVERY_ENDPOINT', 'OS_USER_DOMAIN_NAME', 'OS_PROJECT_DOMAIN_ID'] + else: + raise RuntimeError(f'Authentication not possible. Unrecognised authentication type {os.getenv("OS_AUTH_TYPE")}') + + for env in required_envs: + if env not in os.environ: + raise RuntimeError(f'Authentication not possible. Environment variable {env} not set. Source the openrc file in order to set all required environment varibles') + + else: + raise RuntimeError(f'Authentication not possible. Environment variable {env} not set. Source the openrc file in order to set all required environment varibles') + + elif authentication=='clouds.yaml': - elif auth=='clouds.yaml': # Check the the appropriate clouds.yaml file exists in the current directory + if not os.path.exists('clouds.yaml'): raise RuntimeError(f'Authentication not possible. Required file `clouds.yaml` not present in the current directory.') + # Check contents of clouds.yaml + with open("clouds.yaml", 'r') as f: + + clouds_yaml = yaml.safe_load(f) + + if 'clouds' not in clouds_yaml: + raise RuntimeError(f'Authentication not possible, missing `clouds` entry in `clouds.yaml` file.') + + clouds = clouds_yaml['clouds'] + if 'openstack' not in clouds: + raise RuntimeError(f'Authentication not possible, missing `openstack` entry in `clouds.yaml` file.') + + openstack = clouds['openstack'] + + if 'auth_type' not in openstack: + raise RuntimeError(f'Authentication not possible, missing `auth_type` in `clouds.yaml` file.') + + if 'auth' not in openstack or \ + 'auth_url' not in openstack['auth'] or \ + 'application_credential_id' not in openstack['auth'] or \ + 'application_credential_secret' not in openstack['auth'] : + raise RuntimeError(f'Authenciation not possible, missing application credentials in `clouds.yaml` file.') + + if 'regions' not in openstack: + raise RuntimeError(f'Authentication not possible, missing `region` in `clouds.yaml` file.') + + if 'interface' not in openstack: + raise RuntimeError(f'Authentication not possible, missing `interface` in `clouds.yaml` file.') + + if 'identity_api_version' not in openstack: + raise RuntimeError(f'Authentication not possible, missing `identity_api_version` in `clouds.yaml` file.') + else: raise RuntimeError(f'Authentication not possible. Unrecognised authentication metod.') @@ -209,7 +257,7 @@ def main(): config = parse_config_file(args.config_file_path) # Ensure authentication credentials are ready - authenticate(config['auth']) + authenticate(config['authentication']) # Connect to the cloud cloud = openstack_connect(config['cloud']) diff --git a/items/openstack-backups/openstack_backups/create_backups.py b/items/openstack-backups/openstack_backups/create_backups.py index 6663fbf..347fc87 100644 --- a/items/openstack-backups/openstack_backups/create_backups.py +++ b/items/openstack-backups/openstack_backups/create_backups.py @@ -17,48 +17,148 @@ def create_instance_snapshot(cloud: Connection, backup: dict) -> dict: name_or_id = backup['name'] if 'name' in backup.keys() else backup['id'] server = get_server(cloud, name_or_id) - logger.info(f'Backing up instance {name_or_id}.') - # TODO: Store networks and security groups somehow + # Only image-backed instances can be backed up this way + if server.image.id is not None: - # It is recommended to stop the server before backing it up - if backup['stop']: - ensure_server_stopped(cloud, server) + logger.info(f'Backing up instance {name_or_id}.') - # Now create an image from the server - image_name = stable_name(server.name, server.id, "snapshot") - logger.info(f'Creating snapshot image of server {server.name}.') - image = cloud.compute.create_server_image(server, image_name) + # It is recommended to stop the server before backing it up + if backup['stop']: + ensure_server_stopped(cloud, server) - image_id = None - if isinstance(image, str): - image_id = image - elif isinstance(image, dict): - image_id = image.get("image_id") or image.get("id") + # Now create an image from the server + image_name = stable_name(server.name, server.id, "snapshot") + logger.info(f'Creating snapshot image of server {server.name}.') + image = cloud.compute.create_server_image(server, image_name) + + image_id = None + if isinstance(image, str): + image_id = image + elif isinstance(image, dict): + image_id = image.get("image_id") or image.get("id") + else: + image_id = getattr(image, "id", None) + + if not image_id: + raise RuntimeError(f"Could not determine snapshot image id for {server.name}.") + + # Wait for image to be ready + wait_for_status(lambda rid: cloud.compute.find_image(rid, ignore_missing=True), + image_id, wanted="ACTIVE", fail_states=["KILLED", "DELETED", "DEACTIVATED"], + timeout=7200, interval=10, desc=f"create image {image_id}") + logger.info(f"Snapshot created with id {image_id}") + + # If instance was shut down by us, start it again + ensure_server_started(cloud, server) + + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'snapshot_name': image_name, + 'snapshot_id': image_id + } + + + return result + + # Volume-backed instances require backing up their root volume else: - image_id = getattr(image, "id", None) - - if not image_id: - raise RuntimeError(f"Could not determine snapshot image id for {server.name}.") - # Wait for image to be ready - wait_for_status(lambda rid: cloud.compute.find_image(rid, ignore_missing=True), - image_id, wanted="ACTIVE", fail_states=["KILLED", "DELETED", "DEACTIVATED"], - timeout=7200, interval=10, desc=f"create image {image_id}") - logger.info(f"Snapshot created with id {image_id}") + # First find the root volume of the instance + attachments = get_attachments(cloud, backup) + for attachment in attachments: + # The root volume is bootable + if attachment['bootable']: - # If instance was shut down by us, start it again - ensure_server_started(cloud, server) + logger.info(f'Backing up root volume {attachment["id"]} of instance {name_or_id}.') - result = { - 'instance_name': server.name, - 'instance_id': server.id, - 'snapshot_name': image_name, - 'snapshot_id': image_id - } + # Root volumes cannot be detached + attachment['detach'] = False + result = create_volume_snapshot(cloud, attachment) + result['instance_name'] = server.name + result['instance_id'] = server.id - return result + return result + + +def create_instance_backup(cloud: Connection, backup: dict) -> dict: + """ + Create a backup of an instance / server + """ + + name_or_id = backup['name'] if 'name' in backup.keys() else backup['id'] + + server = get_server(cloud, name_or_id) + + # Only image-backed instances can be backed up this way + if server.image.id is not None: + + logger.info(f'Backing up instance {name_or_id}.') + + # It is recommended to stop the server before backing it up + if backup['stop']: + ensure_server_stopped(cloud, server) + + # Now create an image from the server + image_name = stable_name(server.name, server.id, "snapshot") + logger.info(f'Creating backup of server {server.name}.') + image = cloud.compute.backup_server(server, image_name, backup_type='daily', rotation=1) + + image_id = None + if image == None: + image = get_image(cloud, image_name) + + if isinstance(image, str): + image_id = image + elif isinstance(image, dict): + image_id = image.get("image_id") or image.get("id") + else: + image_id = getattr(image, "id", None) + + if not image_id: + raise RuntimeError(f"Could not determine backup id for {server.name}.") + + # Wait for image to be ready + wait_for_status(lambda rid: cloud.compute.find_image(rid, ignore_missing=True), + image_id, wanted="ACTIVE", fail_states=["KILLED", "DELETED", "DEACTIVATED"], + timeout=7200, interval=10, desc=f"create image {image_id}") + logger.info(f"Backup created with id {image_id}") + + # If instance was shut down by us, start it again + ensure_server_started(cloud, server) + + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'backup_name': image_name, + 'backup_id': image_id + } + + + return result + + # Volume-backed instances require backing up their root volume + else: + + # First find the root volume of the instance + attachments = get_attachments(cloud, backup) + for attachment in attachments: + # The root volume is bootable + if attachment['bootable']: + + logger.info(f'Backing up root volume {attachment["id"]} of instance {name_or_id}.') + + # Root volumes cannot be detached + attachment['detach'] = False + + result = create_volume_backup(cloud, attachment) + result['instance_name'] = server.name + result['instance_id'] = server.id + + return result + def create_volume_snapshot(cloud: Connection, backup: dict) -> dict: @@ -66,7 +166,7 @@ def create_volume_snapshot(cloud: Connection, backup: dict) -> dict: Create a snapshot of a volume """ - name_or_id = backup['name'] if 'name' in backup.keys() else backup['id'] + name_or_id = backup['name'] if 'name' in backup.keys() and backup['name'] != '' else backup['id'] volume = get_volume(cloud, name_or_id) logger.info(f'Backing up volume {name_or_id}.') @@ -82,7 +182,7 @@ def create_volume_snapshot(cloud: Connection, backup: dict) -> dict: # Now create an snapshot from the volume snapshot_name = stable_name(volume.name, volume.id, "snapshot") - logger.info(f'Creating snapshot of volume {volume.name}.') + logger.info(f'Creating snapshot of volume {name_or_id}.') snapshot = cloud.create_volume_snapshot(volume.id, name=snapshot_name, force=not backup['detach']) snapshot_id = None @@ -105,7 +205,7 @@ def create_volume_snapshot(cloud: Connection, backup: dict) -> dict: # If volume was detached, attach it back for attachment in attachments: if 'server_id' in attachment.keys(): - ensure_volume_attached(cloud, volume, attachment['server_id'], attachment['device']) + ensure_volume_attached(cloud, volume.id, attachment['server_id'], attachment['device']) result = { 'volume_name': volume.name, @@ -122,7 +222,7 @@ def create_volume_backup(cloud: Connection, backup: dict) -> dict: Create a backup of a volume """ - name_or_id = backup['name'] if 'name' in backup.keys() else backup['id'] + name_or_id = backup['name'] if 'name' in backup.keys() and backup['name'] != '' else backup['id'] volume = get_volume(cloud, name_or_id) logger.info(f'Backing up volume {name_or_id}.') @@ -139,7 +239,7 @@ def create_volume_backup(cloud: Connection, backup: dict) -> dict: # Now create an backup from the volume backup_name = stable_name(volume.name, volume.id, "backup") - logger.info(f'Creating backup of volume {volume.name}.') + logger.info(f'Creating backup of volume {name_or_id}.') backup = cloud.create_volume_backup(volume.id, name=backup_name, force=not backup['detach']) backup_id = None @@ -162,7 +262,7 @@ def create_volume_backup(cloud: Connection, backup: dict) -> dict: # If volume was detached, attach it back for attachment in attachments: if 'server_id' in attachment.keys(): - ensure_volume_attached(cloud, volume, attachment['server_id'], attachment['device']) + ensure_volume_attached(cloud, volume.id, attachment['server_id'], attachment['device']) result = { 'volume_name': volume.name, @@ -187,12 +287,14 @@ def create_backup(cloud: Connection, backup: dict) -> None: if type == 'instance' and mode == 'snapshot': result = [create_instance_snapshot(cloud, backup)] + elif type == 'instance' and mode == 'backup': + result = [create_instance_backup(cloud, backup)] elif type == 'volume' and mode == 'snapshot': result = [create_volume_snapshot(cloud, backup)] elif type == 'volume' and mode == 'backup': result = [create_volume_backup(cloud, backup)] else: - raise RuntimeError(f'Backup configuration for type: {type} and mode: {mode} not valid.') + raise RuntimeError(f'Backup configuration for type: `{type}` and mode: `{mode}` not valid.') # If the resource has attachments, back those up too if 'attachments' in backup.keys(): @@ -203,7 +305,9 @@ def create_backup(cloud: Connection, backup: dict) -> None: elif not isinstance(attachments, list): raise RuntimeError(f'Attachments not recognised') for attachment in attachments: - result.append(create_backup(cloud, attachment)) + # Any bootable volume is a root volume and was already backed up with the instance + if 'bootable' not in attachment or not attachment['bootable']: + result.append(create_backup(cloud, attachment)) return result diff --git a/items/openstack-backups/openstack_backups/openstack_utils.py b/items/openstack-backups/openstack_backups/openstack_utils.py index 0249d53..e0a0096 100644 --- a/items/openstack-backups/openstack_backups/openstack_utils.py +++ b/items/openstack-backups/openstack_backups/openstack_utils.py @@ -6,6 +6,7 @@ Notes: many functions adapted from the openstack-migration repository by Ahmed Naga """ +import os import hashlib import time from datetime import datetime @@ -23,10 +24,28 @@ def openstack_connect(cloud_name: str) -> Connection: Connect to the OS cloud """ - connection = openstack.connect(cloud=cloud_name) + connection = openstack.connect( + auth_type = os.getenv('OS_AUTH_TYPE'), + auth_url = os.getenv('OS_AUTH_URL'), + identity_api_version = os.getenv('OS_IDENTITY_API_VERSION'), + region_name = os.getenv('OS_REGION_NAME'), + interface = os.getenv('OS_INTERFACE'), + username = os.getenv('OS_USERNAME'), + user_domain_name = os.getenv('OS_USER_DOMAIN_NAME'), + project_domain_id = os.getenv('OS_PROJECT_DOMAIN_ID'), + application_credential_id = os.getenv('OS_APPLICATION_CREDENTIAL_ID'), + application_crendetial_secret = os.getenv('OS_APPLICATION_CREDENTIAL_SECRET') + ) connection.authorize() return connection +def find_server(cloud: Connection, name_or_id: str) -> Server: + """ + Find a server + """ + + return cloud.compute.find_server(name_or_id, ignore_missing=True) + def get_server(cloud: Connection, name_or_id: str) -> Server: """ Find and get server object @@ -51,6 +70,13 @@ def get_volume(cloud: Connection, name_or_id: str) -> Volume: return cloud.block_storage.get_volume(volume.id) +def find_image(cloud: Connection, name_or_id: str) -> Image: + """ + Find an image + """ + + return cloud.compute.find_image(name_or_id, ignore_missing=True) + def get_image(cloud: Connection, name_or_id: str) -> Image: """ Find and get an image object @@ -63,6 +89,13 @@ def get_image(cloud: Connection, name_or_id: str) -> Image: return cloud.compute.get_image(image.id) +def find_volume_snapshot(cloud: Connection, name_or_id: str): + """ + Find volume snapshot + """ + + return cloud.block_storage.find_snapshot(name_or_id, ignore_missing=True) + def get_volume_snapshot(cloud: Connection, name_or_id: str): """ Find and get a volume snapshot @@ -75,6 +108,13 @@ def get_volume_snapshot(cloud: Connection, name_or_id: str): return cloud.block_storage.get_snapshot(snapshot.id) +def find_volume_backup(cloud: Connection, name_or_id: str): + """ + Find volume backup + """ + + return cloud.block_storage.find_backup(name_or_id, ignore_missing=True) + def get_volume_backup(cloud: Connection, name_or_id: str): """ Find and get a volume backup @@ -87,6 +127,7 @@ def get_volume_backup(cloud: Connection, name_or_id: str): return cloud.block_storage.get_backup(backup.id) + def get_attachments(cloud: Connection, backup: dict): """ Get list of attachments of resource @@ -97,6 +138,7 @@ def get_attachments(cloud: Connection, backup: dict): raise RuntimeError(f'Resource {backup["name"]} is not an instance and therefore cannot have attachments') server = get_server(cloud, backup['name']) + logger.info(f"Getting attachments for server {server.name}") raw_attachments = cloud.compute.volume_attachments(server) @@ -105,14 +147,103 @@ def get_attachments(cloud: Connection, backup: dict): attachments = [] for attachment in raw_attachments: volume = get_volume(cloud, attachment.volume_id) - attachments.append({'name': volume.name, - 'id': volume.id, + attachments.append({'id': volume.id, 'type': 'volume', - 'mode': backup['mode'], - 'detach': True}) + 'mode': backup["mode"], + 'bootable': volume['bootable'], + 'device': attachment.device, + 'detach': False if volume['bootable'] else True}) return attachments +def get_fixed_ips(cloud: Connection, server_id: str) -> list: + """ + Get the fixed ips from a server + """ + + logger.info(f'Getting fixed ips for server {server_id}') + + ports = cloud.network.ports(device_id=server_id) + fixed_ips = [] + + # Record fixed IPs + for port in ports: + for fixed_ip in port.fixed_ips: + fixed_ips.append(fixed_ip) + + return fixed_ips + +def get_floating_ips(cloud: Connection, server_id: str) -> list: + """ + Get the floating ips from a server + """ + + logger.info(f'Getting floating ips for server {server_id}') + + server = get_server(cloud, server_id) + floating_ips = [] + + # Record floating IPs + for ip in cloud.list_floating_ips(filters={"port_details": {"device_id": server_id}}): + floating_ips.append({ + 'port_id': ip['port_id'], + 'ip_id': ip['id'], + 'ip_address': ip['floating_ip_address'], + 'fixed_ip_address': ip['fixed_ip_address'], + 'network_id': ip['port_details']['network_id'], + }) + + return floating_ips + +def recreate_ports(cloud: Connection, fixed_ips: list) -> list: + """ + Recreate the ports with the original fixed IPs + """ + + new_port_ids = [] + for fixed_ip in fixed_ips: + ports = cloud.network.ports() + port_exists = False + for port in ports: + for ip in port.fixed_ips: + if ip['subnet_id'] == fixed_ip['subnet_id'] and ip['ip_address'] == fixed_ip['ip_address']: + port_exists = True + new_port_ids.append(port.id) + + if not port_exists: + port = cloud.network.create_port( + network_id=cloud.network.get_subnet(fixed_ip["subnet_id"]).network_id, + fixed_ips=[{"subnet_id": fixed_ip["subnet_id"], "ip_address": fixed_ip["ip_address"]}], + ) + new_port_ids.append(port.id) + + return new_port_ids + +def add_floating_ips_to_server(cloud: Connection, floating_ips: list, server_id: str) -> None: + """ + Add floating ips to server + """ + + server = get_server(cloud, server_id) + + for ip in floating_ips: + floating_ip = cloud.get_floating_ip(ip['ip_id']) + + if floating_ip and floating_ip.status != "ACTIVE": + logger.info(f"Adding floating ip {ip['ip_address']} to server {server_id}") + cloud.compute.add_floating_ip_to_server(server, floating_ip, fixed_address=ip['fixed_ip_address']) + + ports = list(cloud.network.ports(device_id=server.id)) + for port in ports: + if port.network_id == ip['network_id']: + # Attach the floating IP to the port + floating_ip.port_id = port.id + cloud.dns.update_floating_ip(floating_ip.id, port_id=port.id) + logger.warning(f"Attachment of floating ip {ip['ip_address']} may have failed, if so you must add it manually using the OpenStack CLI") + + elif not floating_ip: + raise RuntimeError(f"Error restoring backup: floating IP {ip} not found") + def stable_name(*parts: str) -> str: """ Create a stable name for images/snapshots/backups @@ -226,12 +357,12 @@ def ensure_volume_detached(cloud: Connection, volume: Volume, server_id: str) -> ) return volume -def ensure_volume_attached(cloud: Connection, volume: Volume, server_id: str, device: str) -> Volume: +def ensure_volume_attached(cloud: Connection, volume_id: str, server_id: str, device: str) -> Volume: """ Make sure the volume is attached """ - volume = cloud.block_storage.get_volume(volume.id) + volume = cloud.block_storage.get_volume(volume_id) status = volume.status if status != "in-use": logger.info(f"Attaching volume {volume.name}.") @@ -248,3 +379,29 @@ def ensure_volume_attached(cloud: Connection, volume: Volume, server_id: str, de ) return volume +def ensure_server_deleted(cloud: Connection, server_id: str): + """ + Make sure the server is deleted + """ + + wait_for_status(lambda rid: find_server(cloud,rid), server_id, wanted=None, fail_states=["ACTIVE"], timeout=7200, interval=10, desc=f"delete server {server_id}") + + + +def delete_server(cloud: Connection, server_id: str) -> None: + """ + Delete server + """ + + server = get_server(cloud, server_id) + ensure_server_stopped(cloud, server) + + cloud.compute.delete_server(server_id) + + +def delete_volume(cloud: Connection, volume_id: str) -> None: + """ + Delete volume + """ + + cloud.block_storage.delete_volume(volume_id, wait=True) diff --git a/items/openstack-backups/openstack_backups/restore_backups.py b/items/openstack-backups/openstack_backups/restore_backups.py index ccc3d83..d456cce 100644 --- a/items/openstack-backups/openstack_backups/restore_backups.py +++ b/items/openstack-backups/openstack_backups/restore_backups.py @@ -16,81 +16,383 @@ def restore_instance_snapshot(cloud: Connection, restore: dict) -> dict: name_or_id = restore['name'] if 'name' in restore.keys() else restore['id'] - snapshot = get_image(cloud, name_or_id) - logger.info(f'Restoring instance snapshot {name_or_id}.') + # First check if the name or id is an image or a volume snapshot + if find_image(cloud, name_or_id): - # If the restoration is in place, find the original server id and check if it still exists - if restore['in_place']: + # There is an image with the given name or id, then it's a image-backed instance - metadata = getattr(snapshot, "metadata", None) - if not metadata: - raise RuntimeError(f'Could not retrieve information from snapshot {name_or_id}.') + snapshot = get_image(cloud, name_or_id) - instance_id = metadata.get("instance_uuid",None) + logger.info(f'Restoring image snapshot {name_or_id}.') - # Try to find server - server = cloud.compute.find_server(instance_id, ignore_missing=True) - if not server: - raise RuntimeError(f'Original instace not found, in-place restoration is impossible.') - - # For in-place restorations, ensure server is shutoff before - ensure_server_stopped(cloud, server) + # If the restoration is in place, find the original server id and check if it still exists + if restore['in_place']: - # Now rebuild the server - logger.info(f'In-place restoration of instance {server.name} with snapshot {name_or_id}.') - server = cloud.compute.rebuild_server(server.id, snapshot.id) - if not server: - raise RuntimeError(f'Error rebuilding instance.') + metadata = getattr(snapshot, "metadata", None) + if not metadata: + raise RuntimeError(f'Could not retrieve information from snapshot {name_or_id}.') + + instance_id = metadata.get("instance_uuid",None) + + # Try to find server + server = cloud.compute.find_server(instance_id, ignore_missing=True) + if not server: + raise RuntimeError(f'Original instance not found, in-place restoration is impossible.') + + # For in-place restorations, ensure server is shutoff before + ensure_server_stopped(cloud, server) + + # Now rebuild the server + logger.info(f'In-place restoration of instance {server.name} with snapshot {name_or_id}.') + server = cloud.compute.rebuild_server(server.id, snapshot.id) + if not server: + raise RuntimeError(f'Error rebuilding instance.') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud, rid), server.id, wanted="SHUTOFF", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"rebuild server {server.id}") + + # Restart server + ensure_server_started(cloud, server) + logger.info(f'Instance {server.name} successfully restored from snapshot.') + + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'snapshot_name': snapshot.name, + 'snapshot_id': snapshot.id + } + + # If the restoration is not in place, create a new instance + else: - # Wait until the server is ready - wait_for_status(lambda rid: get_server(cloud, rid), server.id, wanted="SHUTOFF", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"rebuild server {server.id}") + # Get the new instance properties + if 'new_name' in restore: + server_name = restore['new_name'] + else: + server_name = stable_name(snapshot.name, snapshot.id, 'restore') + flavor = cloud.get_flavor(restore['flavor']) + network = cloud.get_network(restore['network']) - # Restart server - ensure_server_started(cloud, server) - logger.info(f'Instance {server.name} successfully restored from snapshot.') + # Create the new instance + logger.info(f'New copy restoration of snapshot {name_or_id} to instance {server_name}.') + server = cloud.compute.create_server(name=server_name, image_id=snapshot.id, flavor_id=flavor.id, networks=[{'uuid':network.id}], security_groups=restore['security_groups']) + if not server: + raise RuntimeError(f'Error creating instance from snapshot') - # TODO: Maybe restore networks? + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") - result = { - 'instance_name': server.name, - 'instance_id': server.id, - 'snapshot_name': snapshot.name, - 'snapshot_id': snapshot.id - } + logger.info(f'Instance {server.name} successfully restored from snapshot.') + + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'snapshot_name': snapshot.name, + 'snapshot_id': snapshot.id + } + + return result + + elif find_volume_snapshot(cloud, name_or_id): + + # There is a volume snapshot with the given name or id, then it's a volume-backed instance + + logger.info(f'Restoring root volume snapshot {name_or_id}.') + + if restore['in_place']: + + # If the restoration is in place, the volume can be restored in-place once the server is deleted, and then the server must be rebuilt + # Our version of OpenstackSDK does not support rebuilding servers with new root volumes, so we need to delete it and rebuild it + + # First, if the original server does not exist anymore, restore in-place is not possible + snapshot = get_volume_snapshot(cloud, name_or_id) + volume = get_volume(cloud, snapshot['volume_id']) + if not volume['attachments']: + raise RuntimeError(f"The root volume of the original instance has no attachments. The original server does not exist and it cannot be restored in-place.") + server = get_server(cloud, volume['attachments'][0]['server_id']) + if snapshot is None or volume is None or server is None: + raise RuntimeError(f"Problem restoring snapshot, snapshot, volume or server can't be found") + + logger.info(f'In-place restoration of instance {server.name} with volume snapshot {name_or_id}.') + logger.warning("In-place restoration of a volume-backed instance requires deleting the old VM and creating a new one with the restored root volume.") + + # First get all the info from the server + server_name = getattr(server,"name") + server_id = getattr(server, "id") + flavor = getattr(server,"flavor") + networks = getattr(server, "addresses") + security_groups = getattr(server, "security_groups") + volumes = get_attachments(cloud, {"name": server.name, "type": "instance", "mode": "snapshot"}) + + # Get the fixed and floating ips for the new server + fixed_ips = get_fixed_ips(cloud, server_id) + floating_ips = get_floating_ips(cloud, server_id) + + # Delete the server + logger.info(f"Deleting old server {server_name}") + delete_server(cloud, server_id) + + # Wait until the server is deleted + ensure_server_deleted(cloud, server_id) + + # Now restore the volume snapshot + result = restore_volume_snapshot(cloud, restore) + + # Recreate fixed ips + ports = recreate_ports(cloud, fixed_ips) + port_ids = [{"port": port_id} for port_id in ports] + + # Rebuild the server + logger.info(f'Creating new server {server_name}.') + server = cloud.compute.create_server(name=server_name, boot_volume=volume.id, block_device_mapping=[{"boot_index": 0,"uuid": volume.id,"source_type": "volume","destination_type": "volume","delete_on_termination": True}], flavor_id=flavor.id, networks=port_ids, security_groups=security_groups) + + if not server: + raise RuntimeError(f'Error rebuilding instance.') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") + + # Recreate floating ips + add_floating_ips_to_server(cloud, floating_ips, server.id) + + # Reattach volumes + for attachment in volumes: + if not attachment["bootable"]: + ensure_volume_attached(cloud, attachment["id"], server.id, attachment["device"]) + + logger.info(f'Instance {server.name} successfully restored from snapshot.') + + result['instance_name'] = server.name + result['instance_id'] = server.id + + return result + else: + + # If the restoration is not in place, restore to a new volume and create a new instance + + result = restore_volume_snapshot(cloud, restore) + + # Get the new instance properties + if 'new_name' in restore: + server_name = restore['new_name'] + else: + server_name = stable_name(snapshot.name, snapshot.id, 'restore') + flavor = cloud.get_flavor(restore['flavor']) + network = cloud.get_network(restore['network']) + + volume = get_volume(cloud, result['volume_id']) + + # Create the new instance + logger.info(f'New copy restoration of volume snapshot {name_or_id} to instance {server_name}.') + server = cloud.compute.create_server(name=server_name, boot_volume=volume.id, block_device_mapping=[{"boot_index": 0,"uuid": volume.id,"source_type": "volume","destination_type": "volume","delete_on_termination": True}], flavor_id=flavor.id, networks=[{'uuid':network.id}], security_groups=restore['security_groups']) + if not server: + raise RuntimeError(f'Error creating instance from snapshot') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") + + logger.info(f'Instance {server.name} successfully restored from snapshot.') + + result['instance_name'] = server.name + result['instance_id'] = server.id + + return result - # If the restoration is not in place, create a new instance else: + raise RuntimeError(f'Resource to restore {name_or_id} is neither an image nor a volume snapshot') - # Get the new instance properties - if 'new_name' in restore: - server_name = restore['new_name'] + +def restore_instance_backup(cloud: Connection, restore: dict) -> dict: + """ + Restore a backup of an instance / server + """ + + name_or_id = restore['name'] if 'name' in restore.keys() else restore['id'] + + # First check if the name or id is an image or a volume backup + if find_image(cloud, name_or_id): + + # There is an image with the given name or id, then it's a image-backed instance + + backup = get_image(cloud, name_or_id) + + logger.info(f'Restoring image backup {name_or_id}.') + + # If the restoration is in place, find the original server id and check if it still exists + if restore['in_place']: + + metadata = getattr(snapshot, "metadata", None) + if not metadata: + raise RuntimeError(f'Could not retrieve information from backup {name_or_id}.') + + instance_id = metadata.get("instance_uuid",None) + + # Try to find server + server = cloud.compute.find_server(instance_id, ignore_missing=True) + if not server: + raise RuntimeError(f'Original instance not found, in-place restoration is impossible.') + + # For in-place restorations, ensure server is shutoff before + ensure_server_stopped(cloud, server) + + # Now rebuild the server + logger.info(f'In-place restoration of instance {server.name} with backup {name_or_id}.') + server = cloud.compute.rebuild_server(server.id, backup.id) + if not server: + raise RuntimeError(f'Error rebuilding instance.') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud, rid), server.id, wanted="SHUTOFF", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"rebuild server {server.id}") + + # Restart server + ensure_server_started(cloud, server) + logger.info(f'Instance {server.name} successfully restored from backup.') + + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'backup_name': backup.name, + 'backup_id': backup.id + } + + # If the restoration is not in place, create a new instance else: - server_name = stable_name(snapshot.name, snapshot.id, 'restore') - flavor = cloud.get_flavor(restore['flavor']) - network = cloud.get_network(restore['network']) - # Create the new instance - logger.info(f'New copy restoration of snapshot {name_or_id} to instance {server_name}.') - server = cloud.compute.create_server(name=server_name, image_id=snapshot.id, flavor_id=flavor.id, networks=[{'uuid':network.id}], security_groups=restore['security_groups']) - if not server: - raise RuntimeError(f'Error creating instance from snapshot') + # Get the new instance properties + if 'new_name' in restore: + server_name = restore['new_name'] + else: + server_name = stable_name(backup.name, backup.id, 'restore') + flavor = cloud.get_flavor(restore['flavor']) + network = cloud.get_network(restore['network']) - # Wait until the server is ready - wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") + # Create the new instance + logger.info(f'New copy restoration of backup {name_or_id} to instance {server_name}.') + server = cloud.compute.create_server(name=server_name, image_id=backup.id, flavor_id=flavor.id, networks=[{'uuid':network.id}], security_groups=restore['security_groups']) + if not server: + raise RuntimeError(f'Error creating instance from bcakup') - logger.info(f'Instance {server.name} successfully restored from snapshot.') + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") - # TODO: Maybe restore networks? + logger.info(f'Instance {server.name} successfully restored from backup.') - result = { - 'instance_name': server.name, - 'instance_id': server.id, - 'snapshot_name': snapshot.name, - 'snapshot_id': snapshot.id - } + result = { + 'instance_name': server.name, + 'instance_id': server.id, + 'backup_name': backup.name, + 'backup_id': backup.id + } - return result + return result + + elif find_volume_backup(cloud, name_or_id): + + # There is a volume backup with the given name or id, then it's a volume-backed instance + + logger.info(f'Restoring root volume backup {name_or_id}.') + + if restore['in_place']: + + # If the restoration is in place, the volume can be restored in-place once the server is deleted, and then the server must be rebuilt + # Our version of OpenstackSDK does not support rebuilding servers with new root volumes, so we need to delete it and rebuild it + + # First, if the original server does not exist anymore, restore in-place is not possible + backup = get_volume_backup(cloud, name_or_id) + volume = get_volume(cloud, backup['volume_id']) + if not volume['attachments']: + raise RuntimeError(f"The root volume of the original instance has no attachments. The original server does not exist and it cannot be restored in-place.") + server = get_server(cloud, volume['attachments'][0]['server_id']) + if backup is None or volume is None or server is None: + raise RuntimeError(f"Problem restoring backup. Backup, volume or server can't be found") + + logger.info(f'In-place restoration of instance {server.name} with volume backup {name_or_id}.') + logger.warning("In-place restoration of a volume-backed instance requires deleting the old VM and creating a new one with the restored root volume.") + + # First get all the info from the server + server_name = getattr(server,"name") + server_id = getattr(server, "id") + flavor = getattr(server,"flavor") + networks = getattr(server, "addresses") + security_groups = getattr(server, "security_groups") + volumes = get_attachments(cloud, {"name": server.name, "type": "instance", "mode": "backup"}) + + # Get the fixed and floating ips for the new server + fixed_ips = get_fixed_ips(cloud, server_id) + floating_ips = get_floating_ips(cloud, server_id) + + # Delete the server + logger.info(f"Deleting old server {server_name}") + delete_server(cloud, server_id) + + # Wait until the server is deleted + ensure_server_deleted(cloud, server_id) + # Now restore the volume backup + result = restore_volume_backup(cloud, restore) + + # Recreate fixed ips + ports = recreate_ports(cloud, fixed_ips) + port_ids = [{"port": port_id} for port_id in ports] + + # Rebuild the server + logger.info(f'Creating new server {server_name}.') + server = cloud.compute.create_server(name=server_name, boot_volume=volume.id, block_device_mapping=[{"boot_index": 0,"uuid": volume.id,"source_type": "volume","destination_type": "volume","delete_on_termination": True}], flavor_id=flavor.id, networks=port_ids, security_groups=security_groups) + + if not server: + raise RuntimeError(f'Error rebuilding instance.') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") + + # Recreate floating ips + add_floating_ips_to_server(cloud, floating_ips, server.id) + + # Reattach volumes + for attachment in volumes: + if not attachment["bootable"]: + ensure_volume_attached(cloud, attachment["id"], server.id, attachment["device"]) + + logger.info(f'Instance {server.name} successfully restored from backup.') + + result['instance_name'] = server.name + result['instance_id'] = server.id + + return result + else: + + # If the restoration is not in place, restore to a new volume and create a new instance + + result = restore_volume_backup(cloud, restore) + + # Get the new instance properties + if 'new_name' in restore: + server_name = restore['new_name'] + else: + server_name = stable_name(backup.name, backup.id, 'restore') + flavor = cloud.get_flavor(restore['flavor']) + network = cloud.get_network(restore['network']) + + volume = get_volume(cloud, result['volume_id']) + + # Create the new instance + logger.info(f'New copy restoration of volume backup {name_or_id} to instance {server_name}.') + server = cloud.compute.create_server(name=server_name, boot_volume=volume.id, block_device_mapping=[{"boot_index": 0,"uuid": volume.id,"source_type": "volume","destination_type": "volume","delete_on_termination": True}], flavor_id=flavor.id, networks=[{'uuid':network.id}], security_groups=restore['security_groups']) + if not server: + raise RuntimeError(f'Error creating instance from backup') + + # Wait until the server is ready + wait_for_status(lambda rid: get_server(cloud,rid), server.id, wanted="ACTIVE", fail_states=["ERROR"], timeout=7200, interval=10, desc=f"create server {server.id}") + + logger.info(f'Instance {server.name} successfully restored from backup.') + + result['instance_name'] = server.name + result['instance_id'] = server.id + + return result + + else: + raise RuntimeError(f'Resource to restore {name_or_id} is neither an image nor a volume backup') def restore_volume_snapshot(cloud: Connection, restore: dict) -> dict: """ @@ -106,13 +408,12 @@ def restore_volume_snapshot(cloud: Connection, restore: dict) -> dict: if restore['in_place']: volume_id = getattr(snapshot, 'volume_id', None) - print(volume_id) if not volume_id or not cloud.block_storage.find_volume(volume_id, ignore_missing=True): raise RuntimeError(f'Original volume not found, in-place restoration impossible.') volume = get_volume(cloud, volume_id) - # For in-place restorations, ensure volume is detached beforehand + # For in-place restorations of non-root volumes, ensure volume is detached beforehand attachments = getattr(volume, "attachments", []) for attachment in attachments: if "server_id" in attachment.keys(): @@ -128,7 +429,7 @@ def restore_volume_snapshot(cloud: Connection, restore: dict) -> dict: # Reattach volume if it was attached for attachment in attachments: if 'server_id' in attachment.keys(): - ensure_volume_attached(cloud, volume, attachment['server_id'], attachment['device']) + ensure_volume_attached(cloud, volume.id, attachment['server_id'], attachment['device']) logger.info(f'Volume {volume.name} successfully restored from snapshot.') result = { @@ -203,7 +504,7 @@ def restore_volume_backup(cloud: Connection, restore: dict) -> dict: # Reattach volume if it was attached for attachment in attachments: if 'server_id' in attachment.keys(): - ensure_volume_attached(cloud, volume, attachment['server_id'], attachment['device']) + ensure_volume_attached(cloud, volume.id, attachment['server_id'], attachment['device']) logger.info(f'Volume {volume.name} successfully restored from backup.') result = { @@ -268,6 +569,8 @@ def restore_backup(cloud: Connection, restore: dict) -> None: if type == 'instance' and mode == 'snapshot': result = [restore_instance_snapshot(cloud, restore)] + elif type == 'instance' and mode == 'backup': + result = [restore_instance_backup(cloud, restore)] elif type == 'volume' and mode == 'snapshot': result = [restore_volume_snapshot(cloud, restore)] elif type == 'volume' and mode == 'backup': diff --git a/items/openstack-backups/tests/test_backup.yaml b/items/openstack-backups/tests/test_backup.yaml index 2a54081..f4dcca0 100644 --- a/items/openstack-backups/tests/test_backup.yaml +++ b/items/openstack-backups/tests/test_backup.yaml @@ -1,19 +1,16 @@ -cloud: cloud_00215 +cloud: cloud_081720 +authentication: clouds.yaml backup: - name: TestVM type: instance - mode: snapshot - stop: false + mode: snapshot scheduler: start: "2026-04-01, 00:00" end: "2026-04-05, 00:00" repeat_every: day retention_count: 5 - attachments: - - name: TestVM_Volume_1 + attachments: all + - name: TestVM_Volume type: volume mode: backup - - name: TestVM_Volume_2 - type: volume - mode: snapshot diff --git a/items/openstack-backups/tests/test_restore.yaml b/items/openstack-backups/tests/test_restore.yaml index c14ad33..3da3f33 100644 --- a/items/openstack-backups/tests/test_restore.yaml +++ b/items/openstack-backups/tests/test_restore.yaml @@ -1,10 +1,12 @@ -cloud: cloud_00215 +cloud: cloud_081720 +authentication: clouds.yaml + restore: -- name: TestVM_Volume_1-d452ff49-6c16-4797-99b8-f3778d398ee3-backup-8e9157ca4b36 - type: volume +- name: 9b94f633-04e3-417e-9ad3-03f127008980 + type: instance mode: backup in_place: false - new_name: TestVM_Volume_1_restored - #flavor: eo1.xsmall - #network: private + new_name: TestVM_restored + flavor: 1cpu-1gbmem + network: private