2015-09-08 08:29:30 +00:00
#
# Copyright (C) 2015 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/>.
"""
Docker server module .
"""
2023-08-10 12:44:37 +00:00
import os
2016-06-23 22:56:06 +00:00
import sys
2017-04-24 15:37:41 +00:00
import json
2015-09-08 08:29:30 +00:00
import asyncio
import logging
2015-06-15 17:30:09 +00:00
import aiohttp
2023-08-10 12:44:37 +00:00
import shutil
2023-11-13 01:23:26 +00:00
import platformdirs
2023-08-10 12:44:37 +00:00
2016-05-02 15:13:23 +00:00
from gns3server . utils import parse_version
2018-08-25 07:10:47 +00:00
from gns3server . utils . asyncio import locking
2017-03-27 18:46:25 +00:00
from gns3server . compute . base_manager import BaseManager
from gns3server . compute . docker . docker_vm import DockerVM
from gns3server . compute . docker . docker_error import DockerError , DockerHttp304Error , DockerHttp404Error
2015-09-08 08:29:30 +00:00
log = logging . getLogger ( __name__ )
2018-03-15 07:17:39 +00:00
# Be careful to keep it consistent
2017-04-24 16:43:12 +00:00
DOCKER_MINIMUM_API_VERSION = " 1.25 "
2017-05-23 09:00:15 +00:00
DOCKER_MINIMUM_VERSION = " 1.13 "
2017-07-06 09:24:55 +00:00
DOCKER_PREFERRED_API_VERSION = " 1.30 "
2019-03-06 16:00:01 +00:00
CHUNK_SIZE = 1024 * 8 # 8KB
2016-02-11 15:01:47 +00:00
2015-09-08 08:29:30 +00:00
class Docker ( BaseManager ) :
2016-05-11 17:35:36 +00:00
_NODE_CLASS = DockerVM
2015-09-08 08:29:30 +00:00
def __init__ ( self ) :
2018-03-15 07:17:39 +00:00
2015-09-08 08:29:30 +00:00
super ( ) . __init__ ( )
2021-04-13 09:16:50 +00:00
self . _server_url = " /var/run/docker.sock "
2016-01-06 13:46:45 +00:00
self . _connected = False
2015-10-14 16:10:05 +00:00
# Allow locking during ubridge operations
self . ubridge_lock = asyncio . Lock ( )
2017-03-20 22:50:31 +00:00
self . _connector = None
2017-03-27 18:46:25 +00:00
self . _session = None
2017-07-06 09:24:55 +00:00
self . _api_version = DOCKER_MINIMUM_API_VERSION
2015-09-08 08:29:30 +00:00
2023-08-10 12:44:37 +00:00
@staticmethod
2023-11-13 01:23:26 +00:00
async def install_busybox ( dst_dir ) :
2023-08-10 12:44:37 +00:00
2023-11-13 01:23:26 +00:00
dst_busybox = os . path . join ( dst_dir , " bin " , " busybox " )
2023-08-10 12:44:37 +00:00
if os . path . isfile ( dst_busybox ) :
return
for busybox_exec in ( " busybox-static " , " busybox.static " , " busybox " ) :
busybox_path = shutil . which ( busybox_exec )
if busybox_path :
try :
2023-08-11 04:10:25 +00:00
# check that busybox is statically linked
# (dynamically linked busybox will fail to run in a container)
proc = await asyncio . create_subprocess_exec (
" ldd " ,
busybox_path ,
stdout = asyncio . subprocess . PIPE ,
stderr = asyncio . subprocess . DEVNULL
)
stdout , _ = await proc . communicate ( )
if proc . returncode == 1 :
# ldd returns 1 if the file is not a dynamic executable
log . info ( f " Installing busybox from ' { busybox_path } ' to ' { dst_busybox } ' " )
shutil . copy2 ( busybox_path , dst_busybox , follow_symlinks = True )
return
else :
log . warning ( f " Busybox ' { busybox_path } ' is dynamically linked \n "
f " { stdout . decode ( ' utf-8 ' , errors = ' ignore ' ) . strip ( ) } " )
2023-08-10 12:44:37 +00:00
except OSError as e :
raise DockerError ( f " Could not install busybox: { e } " )
2024-04-23 10:54:06 +00:00
raise DockerError ( " No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH " )
2023-08-10 12:44:37 +00:00
2023-11-13 01:23:26 +00:00
@staticmethod
def resources_path ( ) :
"""
Get the Docker resources storage directory
"""
appname = vendor = " GNS3 "
docker_resources_dir = os . path . join ( platformdirs . user_data_dir ( appname , vendor , roaming = True ) , " docker " , " resources " )
os . makedirs ( docker_resources_dir , exist_ok = True )
return docker_resources_dir
async def install_resources ( self ) :
"""
Copy the necessary resources to a writable location and install busybox
"""
try :
dst_path = self . resources_path ( )
log . info ( f " Installing Docker resources in ' { dst_path } ' " )
from gns3server . controller import Controller
Controller . instance ( ) . install_resource_files ( dst_path , " compute/docker/resources " )
await self . install_busybox ( dst_path )
except OSError as e :
raise DockerError ( f " Could not install Docker resources to { dst_path } : { e } " )
2018-10-15 10:05:49 +00:00
async def _check_connection ( self ) :
2018-03-15 07:17:39 +00:00
2017-03-27 18:46:25 +00:00
if not self . _connected :
2016-01-06 13:46:45 +00:00
try :
2016-01-11 18:11:25 +00:00
self . _connected = True
2018-10-15 10:05:49 +00:00
version = await self . query ( " GET " , " version " )
2020-10-22 05:49:44 +00:00
except ( aiohttp . ClientError , FileNotFoundError ) :
2016-01-06 13:46:45 +00:00
self . _connected = False
raise DockerError ( " Can ' t connect to docker daemon " )
2017-07-06 09:24:55 +00:00
2021-04-13 09:16:50 +00:00
docker_version = parse_version ( version [ " ApiVersion " ] )
2017-07-06 09:24:55 +00:00
if docker_version < parse_version ( DOCKER_MINIMUM_API_VERSION ) :
2021-04-13 09:16:50 +00:00
raise DockerError (
f " Docker version is { version [ ' Version ' ] } . "
f " GNS3 requires a minimum version of { DOCKER_MINIMUM_VERSION } "
)
2017-07-06 09:24:55 +00:00
preferred_api_version = parse_version ( DOCKER_PREFERRED_API_VERSION )
if docker_version > = preferred_api_version :
self . _api_version = DOCKER_PREFERRED_API_VERSION
2017-03-20 22:50:31 +00:00
def connector ( self ) :
2018-03-15 07:17:39 +00:00
2017-03-20 22:50:31 +00:00
if self . _connector is None or self . _connector . closed :
if not sys . platform . startswith ( " linux " ) :
raise DockerError ( " Docker is supported only on Linux " )
try :
2017-05-26 13:42:46 +00:00
self . _connector = aiohttp . connector . UnixConnector ( self . _server_url , limit = None )
2020-10-22 05:49:44 +00:00
except ( aiohttp . ClientError , FileNotFoundError ) :
2017-03-20 22:50:31 +00:00
raise DockerError ( " Can ' t connect to docker daemon " )
2016-01-06 13:46:45 +00:00
return self . _connector
2018-10-15 10:05:49 +00:00
async def unload ( self ) :
2018-03-15 07:17:39 +00:00
2018-10-15 10:05:49 +00:00
await super ( ) . unload ( )
2016-01-06 13:46:45 +00:00
if self . _connected :
2017-03-20 22:50:31 +00:00
if self . _connector and not self . _connector . closed :
2019-01-17 10:43:09 +00:00
await self . _connector . close ( )
2018-10-16 08:56:06 +00:00
if self . _session and not self . _session . closed :
await self . _session . close ( )
2015-12-29 11:40:22 +00:00
2018-10-15 10:05:49 +00:00
async def query ( self , method , path , data = { } , params = { } ) :
2015-09-08 08:29:30 +00:00
"""
2018-03-15 07:17:39 +00:00
Makes a query to the Docker daemon and decode the request
2015-09-08 08:29:30 +00:00
2015-10-14 16:10:05 +00:00
: param method : HTTP method
: param path : Endpoint in API
2018-03-12 06:38:50 +00:00
: param data : Dictionary with the body . Will be transformed to a JSON
2015-10-14 16:10:05 +00:00
: param params : Parameters added as a query arg
"""
2016-02-11 14:49:28 +00:00
2018-10-15 10:05:49 +00:00
response = await self . http_query ( method , path , data = data , params = params )
body = await response . read ( )
2018-10-16 08:56:06 +00:00
response . close ( )
2017-03-27 18:46:25 +00:00
if body and len ( body ) :
2021-11-04 06:29:35 +00:00
if response . headers . get ( ' CONTENT-TYPE ' ) == ' application/json ' :
2016-02-11 14:49:28 +00:00
body = json . loads ( body . decode ( " utf-8 " ) )
else :
body = body . decode ( " utf-8 " )
2015-10-14 16:10:05 +00:00
log . debug ( " Query Docker %s %s params= %s data= %s Response: %s " , method , path , params , data , body )
return body
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def http_query ( self , method , path , data = { } , params = { } , timeout = 300 ) :
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
Makes a query to the docker daemon
2015-09-08 08:29:30 +00:00
2015-10-14 16:10:05 +00:00
: param method : HTTP method
: param path : Endpoint in API
: param data : Dictionnary with the body . Will be transformed to a JSON
: param params : Parameters added as a query arg
2017-03-20 16:06:00 +00:00
: param timeout : Timeout
2015-10-14 16:10:05 +00:00
: returns : HTTP response
2015-09-08 08:29:30 +00:00
"""
2018-03-15 07:17:39 +00:00
2015-10-14 16:10:05 +00:00
data = json . dumps ( data )
2017-03-27 18:46:25 +00:00
if timeout is None :
timeout = 60 * 60 * 24 * 31 # One month timeout
2017-05-02 08:37:29 +00:00
if path == ' version ' :
2024-02-14 07:40:19 +00:00
url = " http://docker/v1.24/ " + path
2017-05-02 08:37:29 +00:00
else :
url = " http://docker/v " + DOCKER_MINIMUM_API_VERSION + " / " + path
2016-06-20 09:46:10 +00:00
try :
2017-03-27 18:46:25 +00:00
if path != " version " : # version is use by check connection
2018-10-15 10:05:49 +00:00
await self . _check_connection ( )
2017-03-27 18:46:25 +00:00
if self . _session is None or self . _session . closed :
connector = self . connector ( )
self . _session = aiohttp . ClientSession ( connector = connector )
2021-04-13 09:16:50 +00:00
response = await self . _session . request (
method ,
url ,
params = params ,
data = data ,
headers = {
" content-type " : " application/json " ,
} ,
timeout = timeout ,
)
2020-10-22 05:49:44 +00:00
except aiohttp . ClientError as e :
2021-04-13 09:07:58 +00:00
raise DockerError ( f " Docker has returned an error: { e } " )
2023-05-30 06:47:12 +00:00
except asyncio . TimeoutError :
2017-04-18 09:44:20 +00:00
raise DockerError ( " Docker timeout " + method + " " + path )
2015-10-14 16:10:05 +00:00
if response . status > = 300 :
2018-10-15 10:05:49 +00:00
body = await response . read ( )
2015-10-14 16:10:05 +00:00
try :
body = json . loads ( body . decode ( " utf-8 " ) ) [ " message " ]
except ValueError :
pass
2021-04-13 09:07:58 +00:00
log . debug ( f " Query Docker { method } { path } params= { params } data= { data } Response: { body } " )
2016-02-11 14:49:28 +00:00
if response . status == 304 :
2021-04-13 09:07:58 +00:00
raise DockerHttp304Error ( f " Docker has returned an error: { response . status } { body } " )
2016-02-11 14:49:28 +00:00
elif response . status == 404 :
2021-04-13 09:07:58 +00:00
raise DockerHttp404Error ( f " Docker has returned an error: { response . status } { body } " )
2016-02-11 14:49:28 +00:00
else :
2021-04-13 09:07:58 +00:00
raise DockerError ( f " Docker has returned an error: { response . status } { body } " )
2015-10-14 16:10:05 +00:00
return response
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def websocket_query ( self , path , params = { } ) :
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
Opens a websocket connection
2015-09-08 08:29:30 +00:00
2015-10-14 16:10:05 +00:00
: param path : Endpoint in API
: param params : Parameters added as a query arg
: returns : Websocket
2015-09-08 08:29:30 +00:00
"""
2017-07-06 09:24:55 +00:00
url = " http://docker/v " + self . _api_version + " / " + path
2018-10-16 08:56:06 +00:00
connection = await self . _session . ws_connect ( url , origin = " http://docker " , autoping = True )
2015-10-14 16:10:05 +00:00
return connection
2015-09-08 08:29:30 +00:00
2018-08-25 07:10:47 +00:00
@locking
2018-10-15 10:05:49 +00:00
async def pull_image ( self , image , progress_callback = None ) :
2017-03-27 18:46:25 +00:00
"""
2018-03-15 07:17:39 +00:00
Pulls an image from the Docker repository
2017-03-27 18:46:25 +00:00
: params image : Image name
: params progress_callback : A function that receive a log message about image download progress
"""
try :
2021-04-13 09:07:58 +00:00
await self . query ( " GET " , f " images/ { image } /json " )
2017-03-27 18:46:25 +00:00
return # We already have the image skip the download
except DockerHttp404Error :
pass
if progress_callback :
2021-04-13 09:07:58 +00:00
progress_callback ( f " Pulling ' { image } ' from docker hub " )
2019-02-17 11:21:21 +00:00
try :
2019-02-22 11:04:49 +00:00
response = await self . http_query ( " POST " , " images/create " , params = { " fromImage " : image } , timeout = None )
2019-02-17 11:21:21 +00:00
except DockerError as e :
2021-04-13 09:16:50 +00:00
raise DockerError (
f " Could not pull the ' { image } ' image from Docker Hub, "
f " please check your Internet connection (original error: { e } ) "
)
2017-03-27 18:46:25 +00:00
# The pull api will stream status via an HTTP JSON stream
content = " "
while True :
2017-04-24 15:37:41 +00:00
try :
2019-03-06 16:00:01 +00:00
chunk = await response . content . read ( CHUNK_SIZE )
2017-05-16 17:28:47 +00:00
except aiohttp . ServerDisconnectedError :
2021-04-13 09:07:58 +00:00
log . error ( f " Disconnected from server while pulling Docker image ' { image } ' from docker hub " )
2018-04-28 10:42:02 +00:00
break
except asyncio . TimeoutError :
2021-04-13 09:07:58 +00:00
log . error ( f " Timeout while pulling Docker image ' { image } ' from docker hub " )
2017-04-24 15:37:41 +00:00
break
2017-03-27 18:46:25 +00:00
if not chunk :
break
content + = chunk . decode ( " utf-8 " )
try :
while True :
content = content . lstrip ( " \r \n \t " )
answer , index = json . JSONDecoder ( ) . raw_decode ( content )
if " progress " in answer and progress_callback :
progress_callback ( " Pulling image {} : {} : {} " . format ( image , answer [ " id " ] , answer [ " progress " ] ) )
content = content [ index : ]
except ValueError : # Partial JSON
pass
response . close ( )
if progress_callback :
2021-04-13 09:07:58 +00:00
progress_callback ( f " Success pulling image { image } " )
2017-03-27 18:46:25 +00:00
2018-10-15 10:05:49 +00:00
async def list_images ( self ) :
2018-03-15 07:17:39 +00:00
"""
Gets Docker image list .
2015-09-08 08:29:30 +00:00
2015-10-14 16:10:05 +00:00
: returns : list of dicts
: rtype : list
2015-09-08 08:29:30 +00:00
"""
2018-03-15 07:17:39 +00:00
2015-10-14 16:10:05 +00:00
images = [ ]
2021-04-13 09:16:50 +00:00
for image in await self . query ( " GET " , " images/json " , params = { " all " : 0 } ) :
if image [ " RepoTags " ] :
for tag in image [ " RepoTags " ] :
2017-01-10 09:09:34 +00:00
if tag != " <none>:<none> " :
2021-04-13 09:16:50 +00:00
images . append ( { " image " : tag } )
return sorted ( images , key = lambda i : i [ " image " ] )