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
2016-07-26 08:32:43 +00:00
from datetime import datetime , timezone
2020-10-02 06:37:50 +00:00
from . controller_error import ControllerError
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
self . _created_at = datetime . now ( ) . timestamp ( )
filename = self . _name + " _ " + datetime . utcfromtimestamp ( self . _created_at ) . replace ( tzinfo = None ) . strftime ( FILENAME_TIME_FORMAT ) + " .gns3project "
else :
self . _name = filename . split ( " _ " ) [ 0 ]
datestring = filename . replace ( self . _name + " _ " , " " ) . split ( " . " ) [ 0 ]
try :
self . _created_at = datetime . strptime ( datestring , FILENAME_TIME_FORMAT ) . replace ( tzinfo = timezone . utc ) . timestamp ( )
except ValueError :
self . _created_at = datetime . utcnow ( ) . timestamp ( )
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 ) :
2020-10-02 06:37:50 +00:00
raise ControllerError ( " The snapshot file ' {} ' already exists " . format ( self . name ) )
2018-04-28 09:01:43 +00:00
snapshot_directory = os . path . join ( self . _project . path , " snapshots " )
try :
os . makedirs ( snapshot_directory , exist_ok = True )
except OSError as e :
2020-10-02 06:37:50 +00:00
raise ControllerError ( " Could not create the snapshot directory ' {} ' : {} " . format ( snapshot_directory , e ) )
2018-04-28 09:01:43 +00:00
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 :
await export_project ( zstream , self . _project , tmpdir , keep_compute_id = True , allow_all_nodes = True )
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 :
2020-10-02 06:37:50 +00:00
raise ControllerError ( " Could not create snapshot file ' {} ' : {} " . format ( self . path , e ) )
2018-04-28 09:01:43 +00:00
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 :
2018-10-15 10:05:49 +00:00
project = await import_project ( self . _project . controller , self . _project . id , f , location = self . _project . path )
2017-02-13 14:30:02 +00:00
except ( OSError , PermissionError ) as e :
2020-10-02 06:37:50 +00:00
raise ControllerError ( 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
}