2015-09-08 08:29:30 +00:00
# -*- coding: utf-8 -*-
#
# 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 .
"""
2024-02-14 08:13:45 +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
2024-02-14 08:13:45 +00:00
import shutil
import platformdirs
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__ ( )
2015-10-14 16:10:05 +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
2024-02-14 08:13:45 +00:00
@staticmethod
async def install_busybox ( dst_dir ) :
dst_busybox = os . path . join ( dst_dir , " bin " , " busybox " )
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 :
# 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 ( ) } " )
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 " )
2024-02-14 08:13:45 +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
2017-03-20 22:50:31 +00:00
connector = self . connector ( )
2018-10-15 10:05:49 +00:00
version = await self . query ( " GET " , " version " )
2017-05-16 17:28:47 +00:00
except ( aiohttp . ClientOSError , 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
docker_version = parse_version ( version [ ' ApiVersion ' ] )
if docker_version < parse_version ( DOCKER_MINIMUM_API_VERSION ) :
raise DockerError (
" Docker version is {} . GNS3 requires a minimum version of {} " . format (
version [ " Version " ] , DOCKER_MINIMUM_VERSION ) )
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 )
2017-05-16 17:28:47 +00:00
except ( aiohttp . ClientOSError , 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 )
2018-10-16 08:56:06 +00:00
response = await self . _session . request ( method ,
url ,
params = params ,
data = data ,
headers = { " content-type " : " application/json " , } ,
timeout = timeout )
2023-05-30 06:47:12 +00:00
except aiohttp . ClientError as e :
2016-06-20 09:46:10 +00:00
raise DockerError ( " Docker has returned an error: {} " . format ( str ( 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
log . debug ( " Query Docker %s %s params= %s data= %s Response: %s " , method , path , params , data , body )
2016-02-11 14:49:28 +00:00
if response . status == 304 :
2016-05-18 09:23:45 +00:00
raise DockerHttp304Error ( " Docker has returned an error: {} {} " . format ( response . status , body ) )
2016-02-11 14:49:28 +00:00
elif response . status == 404 :
2016-05-18 09:23:45 +00:00
raise DockerHttp404Error ( " Docker has returned an error: {} {} " . format ( response . status , body ) )
2016-02-11 14:49:28 +00:00
else :
2016-05-18 09:23:45 +00:00
raise DockerError ( " Docker has returned an error: {} {} " . format ( 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 :
2018-10-15 10:05:49 +00:00
await self . query ( " GET " , " images/ {} /json " . format ( image ) )
2017-03-27 18:46:25 +00:00
return # We already have the image skip the download
except DockerHttp404Error :
pass
if progress_callback :
2018-04-28 10:42:02 +00:00
progress_callback ( " Pulling ' {} ' from docker hub " . format ( image ) )
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 :
raise DockerError ( " Could not pull the ' {} ' image from Docker Hub, please check your Internet connection (original error: {} ) " . format ( image , 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 :
2018-04-28 10:42:02 +00:00
log . error ( " Disconnected from server while pulling Docker image ' {} ' from docker hub " . format ( image ) )
break
except asyncio . TimeoutError :
log . error ( " Timeout while pulling Docker image ' {} ' from docker hub " . format ( image ) )
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 :
progress_callback ( " Success pulling image {} " . format ( image ) )
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 = [ ]
2018-10-15 10:05:49 +00:00
for image in ( await self . query ( " GET " , " images/json " , params = { " all " : 0 } ) ) :
2017-01-10 09:09:34 +00:00
if image [ ' RepoTags ' ] :
for tag in image [ ' RepoTags ' ] :
if tag != " <none>:<none> " :
images . append ( { ' image ' : tag } )
2015-10-14 16:10:05 +00:00
return sorted ( images , key = lambda i : i [ ' image ' ] )