2016-07-26 08:32:43 +00:00
#!/usr/bin/env python
#
# Copyright (C) 2016 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import uuid
import shutil
2018-04-28 09:01:43 +00:00
import tempfile
2019-02-28 10:25:05 +00:00
import aiofiles
import zipfile
import time
2017-02-13 14:30:02 +00:00
import aiohttp . web
2024-05-09 10:23:32 +00:00
from datetime import datetime , timezone
2016-07-26 08:32:43 +00:00
2018-04-28 09:01:43 +00:00
from . . utils . asyncio import wait_run_in_executor
2019-02-28 10:25:05 +00:00
from . . utils . asyncio import aiozipstream
2018-04-28 09:01:43 +00:00
from . export_project import export_project
2016-07-26 08:32:43 +00:00
from . import_project import import_project
2019-02-28 10:25:05 +00:00
import logging
log = logging . getLogger ( __name__ )
2016-07-26 08:32:43 +00:00
# The string use to extract the date from the filename
FILENAME_TIME_FORMAT = " %d % m % y_ % H % M % S "
class Snapshot :
"""
A snapshot object
"""
def __init__ ( self , project , name = None , filename = None ) :
assert filename or name , " You need to pass a name or a filename "
self . _id = str ( uuid . uuid4 ( ) ) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk
self . _project = project
if name :
self . _name = name
2024-05-09 10:23:32 +00:00
self . _created_at = datetime . now ( timezone . utc ) . timestamp ( )
filename = self . _name + " _ " + datetime . fromtimestamp ( self . _created_at , tz = timezone . utc ) . replace ( tzinfo = None ) . strftime ( FILENAME_TIME_FORMAT ) + " .gns3project "
2016-07-26 08:32:43 +00:00
else :
2024-07-05 10:04:53 +00:00
self . _name = filename . rsplit ( " _ " , 2 ) [ 0 ]
2016-07-26 08:32:43 +00:00
datestring = filename . replace ( self . _name + " _ " , " " ) . split ( " . " ) [ 0 ]
2024-07-05 10:04:53 +00:00
self . _created_at = datetime . strptime ( datestring , FILENAME_TIME_FORMAT ) . replace ( tzinfo = timezone . utc ) . timestamp ( )
2016-07-26 08:32:43 +00:00
self . _path = os . path . join ( project . path , " snapshots " , filename )
@property
def id ( self ) :
return self . _id
@property
def name ( self ) :
return self . _name
@property
def path ( self ) :
return self . _path
@property
def created_at ( self ) :
return int ( self . _created_at )
2018-10-15 10:05:49 +00:00
async def create ( self ) :
2018-04-28 09:01:43 +00:00
"""
Create the snapshot
"""
if os . path . exists ( self . path ) :
raise aiohttp . web . HTTPConflict ( text = " The snapshot file ' {} ' already exists " . format ( self . name ) )
snapshot_directory = os . path . join ( self . _project . path , " snapshots " )
try :
os . makedirs ( snapshot_directory , exist_ok = True )
except OSError as e :
raise aiohttp . web . HTTPInternalServerError ( text = " Could not create the snapshot directory ' {} ' : {} " . format ( snapshot_directory , e ) )
try :
2019-02-28 10:25:05 +00:00
begin = time . time ( )
2020-07-17 05:39:43 +00:00
with tempfile . TemporaryDirectory ( dir = snapshot_directory ) as tmpdir :
2019-03-07 10:05:32 +00:00
# Do not compress the snapshots
2019-02-28 10:25:05 +00:00
with aiozipstream . ZipFile ( compression = zipfile . ZIP_STORED ) as zstream :
2024-07-06 15:08:16 +00:00
await export_project ( zstream , self . _project , tmpdir , keep_compute_ids = True , allow_all_nodes = True )
2019-02-28 10:25:05 +00:00
async with aiofiles . open ( self . path , ' wb ' ) as f :
async for chunk in zstream :
await f . write ( chunk )
2019-03-06 16:00:01 +00:00
log . info ( " Snapshot ' {} ' created in {:.4f} seconds " . format ( self . name , time . time ( ) - begin ) )
2018-08-25 11:10:40 +00:00
except ( ValueError , OSError , RuntimeError ) as e :
2018-04-28 09:01:43 +00:00
raise aiohttp . web . HTTPConflict ( text = " Could not create snapshot file ' {} ' : {} " . format ( self . path , e ) )
2018-10-15 10:05:49 +00:00
async def restore ( self ) :
2016-07-26 08:32:43 +00:00
"""
Restore the snapshot
"""
2018-10-15 10:05:49 +00:00
await self . _project . delete_on_computes ( )
2018-04-28 09:01:43 +00:00
# We don't send close notification to clients because the close / open dance is purely internal
2018-10-15 10:05:49 +00:00
await self . _project . close ( ignore_notification = True )
2018-04-28 09:01:43 +00:00
2017-02-13 14:30:02 +00:00
try :
2018-04-28 09:01:43 +00:00
# delete the current project files
project_files_path = os . path . join ( self . _project . path , " project-files " )
if os . path . exists ( project_files_path ) :
2018-10-15 10:05:49 +00:00
await wait_run_in_executor ( shutil . rmtree , project_files_path )
2017-02-13 14:30:02 +00:00
with open ( self . _path , " rb " ) as f :
2021-05-24 16:31:04 +00:00
project = await import_project ( self . _project . controller , self . _project . id , f , location = self . _project . path ,
auto_start = self . _project . auto_start , auto_open = self . _project . auto_open ,
auto_close = self . _project . auto_close )
2017-02-13 14:30:02 +00:00
except ( OSError , PermissionError ) as e :
raise aiohttp . web . HTTPConflict ( text = str ( e ) )
2018-10-15 10:05:49 +00:00
await project . open ( )
2019-02-23 14:08:52 +00:00
self . _project . emit_notification ( " snapshot.restored " , self . __json__ ( ) )
2018-04-28 09:01:43 +00:00
return self . _project
2016-07-26 08:32:43 +00:00
def __json__ ( self ) :
return {
" snapshot_id " : self . _id ,
" name " : self . _name ,
" created_at " : int ( self . _created_at ) ,
" project_id " : self . _project . id
}