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 container instance .
"""
2020-04-28 04:19:05 +00:00
import sys
2015-09-08 08:29:30 +00:00
import asyncio
2015-06-17 08:36:55 +00:00
import shutil
2015-11-08 20:34:27 +00:00
import psutil
2015-10-14 16:10:05 +00:00
import shlex
import aiohttp
2018-11-11 13:07:33 +00:00
import subprocess
2016-02-12 10:57:56 +00:00
import os
2019-04-22 11:46:28 +00:00
import re
2015-09-08 08:29:30 +00:00
2016-05-14 01:28:53 +00:00
from gns3server . utils . asyncio . telnet_server import AsyncioTelnetServer
from gns3server . utils . asyncio . raw_command_server import AsyncioRawCommandServer
from gns3server . utils . asyncio import wait_for_file_creation
2018-09-05 07:16:07 +00:00
from gns3server . utils . asyncio import monitor_process
2024-09-18 09:30:22 +00:00
from gns3server . utils import macaddress_to_int , int_to_macaddress
2016-05-14 01:28:53 +00:00
from gns3server . ubridge . ubridge_error import UbridgeError , UbridgeNamespaceError
2016-05-11 17:35:36 +00:00
from . . base_node import BaseNode
2016-05-24 08:13:53 +00:00
2015-06-17 08:36:55 +00:00
from . . adapters . ethernet_adapter import EthernetAdapter
from . . nios . nio_udp import NIOUDP
2016-05-14 01:28:53 +00:00
from . docker_error import (
DockerError ,
DockerHttp304Error ,
DockerHttp404Error
)
2016-02-11 14:49:28 +00:00
2015-09-08 08:29:30 +00:00
import logging
log = logging . getLogger ( __name__ )
2016-05-11 17:35:36 +00:00
class DockerVM ( BaseNode ) :
2016-06-23 22:56:06 +00:00
"""
Docker container implementation .
2015-09-08 08:29:30 +00:00
: param name : Docker container name
2016-05-11 17:35:36 +00:00
: param node_id : Node identifier
2015-09-08 08:29:30 +00:00
: param project : Project instance
: param manager : Manager instance
: param image : Docker image
2016-02-29 09:38:30 +00:00
: param console : TCP console port
2024-09-23 06:10:58 +00:00
: param console_type : Console type
2016-02-29 09:38:30 +00:00
: param aux : TCP aux console port
2016-04-07 11:29:11 +00:00
: param console_resolution : Resolution of the VNC display
2016-05-03 14:49:33 +00:00
: param console_http_port : Port to redirect HTTP queries
: param console_http_path : Url part with the path of the web interface
2018-04-25 14:03:01 +00:00
: param extra_hosts : Hosts which will be written into / etc / hosts into docker conainer
2019-04-22 08:53:38 +00:00
: param extra_volumes : Additional directories to make persistent
2015-09-08 08:29:30 +00:00
"""
2015-11-09 11:28:00 +00:00
2016-06-23 22:56:06 +00:00
def __init__ ( self , name , node_id , project , manager , image , console = None , aux = None , start_command = None ,
2024-09-23 06:10:58 +00:00
adapters = None , environment = None , console_type = " telnet " , console_resolution = " 1024x768 " ,
2019-04-22 08:53:38 +00:00
console_http_port = 80 , console_http_path = " / " , extra_hosts = None , extra_volumes = [ ] ) :
2016-06-23 22:56:06 +00:00
2024-09-23 06:10:58 +00:00
super ( ) . __init__ ( name , node_id , project , manager , console = console , aux = aux , allocate_aux = True , console_type = console_type )
2015-10-14 16:10:05 +00:00
2016-06-23 22:56:06 +00:00
# force the latest image if no version is specified
2016-05-19 11:09:07 +00:00
if " : " not in image :
image = " {} :latest " . format ( image )
2015-09-08 08:29:30 +00:00
self . _image = image
2015-10-14 16:10:05 +00:00
self . _start_command = start_command
self . _environment = environment
self . _cid = None
2015-06-17 08:36:55 +00:00
self . _ethernet_adapters = [ ]
2024-09-18 09:30:22 +00:00
self . _mac_address = " "
2015-06-17 08:36:55 +00:00
self . _temporary_directory = None
2016-03-01 13:53:43 +00:00
self . _telnet_servers = [ ]
2018-11-11 15:23:30 +00:00
self . _vnc_process = None
2020-04-15 14:14:20 +00:00
self . _vncconfig_process = None
2016-04-07 11:29:11 +00:00
self . _console_resolution = console_resolution
2016-05-03 14:49:33 +00:00
self . _console_http_path = console_http_path
self . _console_http_port = console_http_port
2016-05-10 10:14:48 +00:00
self . _console_websocket = None
2018-04-25 14:03:01 +00:00
self . _extra_hosts = extra_hosts
2019-04-22 08:53:38 +00:00
self . _extra_volumes = extra_volumes or [ ]
2021-11-04 06:29:35 +00:00
self . _permissions_fixed = True
2018-10-06 13:57:03 +00:00
self . _display = None
2018-10-06 13:30:39 +00:00
self . _closing = False
2018-04-25 14:03:01 +00:00
2016-05-31 19:08:41 +00:00
self . _volumes = [ ]
2017-07-12 15:39:19 +00:00
# Keep a list of created bridge
self . _bridges = set ( )
2015-10-14 16:10:05 +00:00
if adapters is None :
self . adapters = 1
else :
self . adapters = adapters
2015-09-08 08:29:30 +00:00
2024-09-18 09:30:22 +00:00
self . mac_address = " " # this will generate a MAC address
2016-06-23 22:56:06 +00:00
log . debug ( " {module} : {name} [ {image} ] initialized. " . format ( module = self . manager . module_name ,
name = self . name ,
image = self . _image ) )
2015-09-08 08:29:30 +00:00
def __json__ ( self ) :
return {
" name " : self . _name ,
2018-12-30 12:35:24 +00:00
" usage " : self . usage ,
2016-05-11 17:35:36 +00:00
" node_id " : self . _id ,
2015-10-14 16:10:05 +00:00
" container_id " : self . _cid ,
2015-09-08 08:29:30 +00:00
" project_id " : self . _project . id ,
" image " : self . _image ,
2015-10-14 16:10:05 +00:00
" adapters " : self . adapters ,
2024-09-18 09:30:22 +00:00
" mac_address " : self . mac_address ,
2015-10-14 16:10:05 +00:00
" console " : self . console ,
2016-02-29 20:08:25 +00:00
" console_type " : self . console_type ,
2016-04-07 11:29:11 +00:00
" console_resolution " : self . console_resolution ,
2016-05-03 14:49:33 +00:00
" console_http_port " : self . console_http_port ,
" console_http_path " : self . console_http_path ,
2016-02-29 09:38:30 +00:00
" aux " : self . aux ,
2015-10-14 16:10:05 +00:00
" start_command " : self . start_command ,
2016-05-17 17:51:06 +00:00
" status " : self . status ,
2016-02-12 10:57:56 +00:00
" environment " : self . environment ,
2018-04-25 14:03:01 +00:00
" node_directory " : self . working_path ,
2019-04-22 08:53:38 +00:00
" extra_hosts " : self . extra_hosts ,
" extra_volumes " : self . extra_volumes ,
2015-09-08 08:29:30 +00:00
}
2016-02-29 20:08:25 +00:00
def _get_free_display_port ( self ) :
"""
Search a free display port
"""
display = 100
if not os . path . exists ( " /tmp/.X11-unix/ " ) :
return display
while True :
if not os . path . exists ( " /tmp/.X11-unix/X {} " . format ( display ) ) :
return display
display + = 1
2017-07-12 15:39:19 +00:00
@property
def ethernet_adapters ( self ) :
return self . _ethernet_adapters
2024-09-18 09:30:22 +00:00
@property
def mac_address ( self ) :
"""
Returns the MAC address for this Docker container .
: returns : adapter type ( string )
"""
return self . _mac_address
@mac_address.setter
def mac_address ( self , mac_address ) :
"""
Sets the MAC address for this Docker container .
: param mac_address : MAC address
"""
if not mac_address :
# use the node UUID to generate a random MAC address
self . _mac_address = " 02:42: %s : %s : %s :00 " % ( self . id [ 2 : 4 ] , self . id [ 4 : 6 ] , self . id [ 6 : 8 ] )
else :
self . _mac_address = mac_address
log . info ( ' Docker container " {name} " [ {id} ]: MAC address changed to {mac_addr} ' . format (
name = self . _name ,
id = self . _id ,
mac_addr = self . _mac_address )
)
2015-06-17 08:36:55 +00:00
@property
2015-10-14 16:10:05 +00:00
def start_command ( self ) :
return self . _start_command
@start_command.setter
def start_command ( self , command ) :
2016-05-31 14:32:12 +00:00
if command :
command = command . strip ( )
if command is None or len ( command ) == 0 :
2016-02-19 16:01:28 +00:00
self . _start_command = None
else :
self . _start_command = command
2015-10-14 16:10:05 +00:00
2016-04-07 11:29:11 +00:00
@property
def console_resolution ( self ) :
return self . _console_resolution
@console_resolution.setter
def console_resolution ( self , resolution ) :
self . _console_resolution = resolution
2016-05-03 14:49:33 +00:00
@property
def console_http_path ( self ) :
return self . _console_http_path
@console_http_path.setter
def console_http_path ( self , path ) :
self . _console_http_path = path
@property
def console_http_port ( self ) :
return self . _console_http_port
@console_http_port.setter
def console_http_port ( self , port ) :
self . _console_http_port = port
2015-10-14 16:10:05 +00:00
@property
def environment ( self ) :
return self . _environment
@environment.setter
def environment ( self , command ) :
self . _environment = command
2015-06-17 08:36:55 +00:00
2018-04-25 14:03:01 +00:00
@property
def extra_hosts ( self ) :
return self . _extra_hosts
@extra_hosts.setter
def extra_hosts ( self , extra_hosts ) :
self . _extra_hosts = extra_hosts
2019-04-22 08:53:38 +00:00
@property
def extra_volumes ( self ) :
return self . _extra_volumes
@extra_volumes.setter
def extra_volumes ( self , extra_volumes ) :
self . _extra_volumes = extra_volumes
2018-10-15 10:05:49 +00:00
async def _get_container_state ( self ) :
2018-03-15 07:17:39 +00:00
"""
Returns the container state ( e . g . running , paused etc . )
2015-09-08 08:29:30 +00:00
: returns : state
: rtype : str
"""
2018-03-15 07:17:39 +00:00
2016-10-14 17:00:25 +00:00
try :
2018-10-15 10:05:49 +00:00
result = await self . manager . query ( " GET " , " containers/ {} /json " . format ( self . _cid ) )
2016-10-14 17:00:25 +00:00
except DockerError :
return " exited "
2015-10-14 16:10:05 +00:00
if result [ " State " ] [ " Paused " ] :
return " paused "
if result [ " State " ] [ " Running " ] :
return " running "
return " exited "
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def _get_image_information ( self ) :
2016-02-12 10:57:56 +00:00
"""
2016-05-14 01:28:53 +00:00
: returns : Dictionary information about the container image
2016-02-12 10:57:56 +00:00
"""
2018-03-15 07:17:39 +00:00
2018-10-15 10:05:49 +00:00
result = await self . manager . query ( " GET " , " images/ {} /json " . format ( self . _image ) )
2016-02-12 10:57:56 +00:00
return result
2018-03-15 07:17:39 +00:00
def _mount_binds ( self , image_info ) :
2016-02-12 10:57:56 +00:00
"""
: returns : Return the path that we need to map to local folders
"""
2018-03-15 07:17:39 +00:00
2024-02-14 08:13:45 +00:00
try :
resources_path = self . manager . resources_path ( )
except OSError as e :
raise DockerError ( f " Cannot access resources: { e } " )
log . info ( f ' Mount resources from " { resources_path } " ' )
binds = [ " {} :/gns3:ro " . format ( resources_path ) ]
2016-03-01 17:38:03 +00:00
2016-03-24 16:08:16 +00:00
# We mount our own etc/network
2018-09-06 07:49:12 +00:00
try :
2019-06-05 08:39:44 +00:00
self . _create_network_config ( )
2018-09-06 07:49:12 +00:00
except OSError as e :
raise DockerError ( " Could not create network config in the container: {} " . format ( e ) )
2019-06-05 08:39:44 +00:00
volumes = [ " /etc/network " ]
2016-03-24 16:08:16 +00:00
2019-06-05 08:39:44 +00:00
volumes . extend ( ( image_info . get ( " Config " , { } ) . get ( " Volumes " ) or { } ) . keys ( ) )
2019-04-22 08:53:38 +00:00
for volume in self . _extra_volumes :
2019-04-22 11:46:28 +00:00
if not volume . strip ( ) or volume [ 0 ] != " / " or volume . find ( " .. " ) > = 0 :
raise DockerError ( " Persistent volume ' {} ' has invalid format. It must start with a ' / ' and not contain ' .. ' . " . format ( volume ) )
2019-04-22 08:53:38 +00:00
volumes . extend ( self . _extra_volumes )
2019-06-05 08:39:44 +00:00
self . _volumes = [ ]
2019-04-22 11:46:28 +00:00
# define lambdas for validation checks
nf = lambda x : re . sub ( r " //+ " , " / " , ( x if x . endswith ( " / " ) else x + " / " ) )
2019-06-05 08:39:44 +00:00
generalises = lambda v1 , v2 : nf ( v2 ) . startswith ( nf ( v1 ) )
2019-04-22 08:53:38 +00:00
for volume in volumes :
2019-06-05 08:39:44 +00:00
# remove any mount that is equal or more specific, then append this one
self . _volumes = list ( filter ( lambda v : not generalises ( volume , v ) , self . _volumes ) )
# if there is nothing more general, append this mount
if not [ v for v in self . _volumes if generalises ( v , volume ) ] :
self . _volumes . append ( volume )
for volume in self . _volumes :
2016-02-12 10:57:56 +00:00
source = os . path . join ( self . working_dir , os . path . relpath ( volume , " / " ) )
os . makedirs ( source , exist_ok = True )
2016-06-06 10:39:29 +00:00
binds . append ( " {} :/gns3volumes {} " . format ( source , volume ) )
2016-03-01 17:38:03 +00:00
2016-02-12 10:57:56 +00:00
return binds
2016-03-24 16:08:16 +00:00
def _create_network_config ( self ) :
"""
If network config is empty we create a sample config
"""
path = os . path . join ( self . working_dir , " etc " , " network " )
os . makedirs ( path , exist_ok = True )
2017-12-17 21:35:26 +00:00
open ( os . path . join ( path , " .gns3_perms " ) , ' a ' ) . close ( )
2016-03-24 17:35:15 +00:00
os . makedirs ( os . path . join ( path , " if-up.d " ) , exist_ok = True )
os . makedirs ( os . path . join ( path , " if-down.d " ) , exist_ok = True )
2016-03-30 08:29:10 +00:00
os . makedirs ( os . path . join ( path , " if-pre-up.d " ) , exist_ok = True )
os . makedirs ( os . path . join ( path , " if-post-down.d " ) , exist_ok = True )
2022-04-20 15:59:51 +00:00
os . makedirs ( os . path . join ( path , " interfaces.d " ) , exist_ok = True )
2016-03-24 16:08:16 +00:00
if not os . path . exists ( os . path . join ( path , " interfaces " ) ) :
with open ( os . path . join ( path , " interfaces " ) , " w+ " ) as f :
f . write ( """ #
2022-04-20 12:29:56 +00:00
# This is a sample network config, please uncomment lines to configure the network
2016-03-24 16:08:16 +00:00
#
2022-04-27 11:37:09 +00:00
# Uncomment this line to load custom interface files
# source /etc/network/interfaces.d/*
2016-03-24 16:08:16 +00:00
""" )
for adapter in range ( 0 , self . adapters ) :
f . write ( """
# Static config for eth{adapter}
#auto eth{adapter}
#iface eth{adapter} inet static
#\taddress 192.168.{adapter}.2
#\tnetmask 255.255.255.0
#\tgateway 192.168.{adapter}.1
#\tup echo nameserver 192.168.{adapter}.1 > /etc/resolv.conf
# DHCP config for eth{adapter}
2022-04-20 12:29:56 +00:00
#auto eth{adapter}
#iface eth{adapter} inet dhcp
2022-06-12 11:32:34 +00:00
#\thostname {hostname}
2022-06-07 17:07:31 +00:00
""" .format(adapter=adapter, hostname=self._name))
2016-03-24 16:08:16 +00:00
return path
2018-10-15 10:05:49 +00:00
async def create ( self ) :
2018-03-15 07:17:39 +00:00
"""
Creates the Docker container .
"""
2016-02-12 10:57:56 +00:00
2023-03-24 08:17:29 +00:00
if " : " in os . path . splitdrive ( self . working_dir ) [ 1 ] :
raise DockerError ( " Cannot create a Docker container with a project directory containing a colon character ( ' : ' ) " )
2023-03-19 08:26:26 +00:00
2016-02-24 16:08:28 +00:00
try :
2018-10-15 10:05:49 +00:00
image_infos = await self . _get_image_information ( )
2016-02-24 16:08:28 +00:00
except DockerHttp404Error :
2018-09-06 07:49:12 +00:00
log . info ( " Image ' {} ' is missing, pulling it from Docker hub... " . format ( self . _image ) )
2018-10-15 10:05:49 +00:00
await self . pull_image ( self . _image )
image_infos = await self . _get_image_information ( )
2018-09-06 07:49:12 +00:00
if image_infos is None :
raise DockerError ( " Cannot get information for image ' {} ' , please try again. " . format ( self . _image ) )
2016-02-12 10:57:56 +00:00
2015-06-17 08:36:55 +00:00
params = {
2016-02-23 18:22:35 +00:00
" Hostname " : self . _name ,
2015-10-14 16:10:05 +00:00
" Image " : self . _image ,
" NetworkDisabled " : True ,
" Tty " : True ,
" OpenStdin " : True ,
" StdinOnce " : False ,
" HostConfig " : {
" CapAdd " : [ " ALL " ] ,
2016-02-12 10:57:56 +00:00
" Privileged " : True ,
2018-04-25 14:03:01 +00:00
" Binds " : self . _mount_binds ( image_infos ) ,
2024-10-26 09:03:05 +00:00
" UsernsMode " : " host " ,
2016-02-12 10:57:56 +00:00
} ,
2016-02-29 20:08:25 +00:00
" Volumes " : { } ,
2016-06-15 08:12:32 +00:00
" Env " : [ " container=docker " ] , # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
2016-03-03 08:12:36 +00:00
" Cmd " : [ ] ,
2019-04-05 10:06:35 +00:00
" Entrypoint " : image_infos . get ( " Config " , { " Entrypoint " : [ ] } ) . get ( " Entrypoint " )
2015-06-17 08:36:55 +00:00
}
2016-03-01 17:38:03 +00:00
2016-03-03 08:12:36 +00:00
if params [ " Entrypoint " ] is None :
params [ " Entrypoint " ] = [ ]
2015-10-14 16:10:05 +00:00
if self . _start_command :
2018-09-06 07:49:12 +00:00
try :
params [ " Cmd " ] = shlex . split ( self . _start_command )
except ValueError as e :
raise DockerError ( " Invalid start command ' {} ' : {} " . format ( self . _start_command , e ) )
2016-03-03 08:12:36 +00:00
if len ( params [ " Cmd " ] ) == 0 :
2019-04-05 10:06:35 +00:00
params [ " Cmd " ] = image_infos . get ( " Config " , { " Cmd " : [ ] } ) . get ( " Cmd " )
2016-03-03 08:12:36 +00:00
if params [ " Cmd " ] is None :
params [ " Cmd " ] = [ ]
if len ( params [ " Cmd " ] ) == 0 and len ( params [ " Entrypoint " ] ) == 0 :
params [ " Cmd " ] = [ " /bin/sh " ]
2016-06-24 23:26:40 +00:00
params [ " Entrypoint " ] . insert ( 0 , " /gns3/init.sh " ) # FIXME /gns3/init.sh is not found?
2015-10-14 16:10:05 +00:00
2016-05-06 17:07:18 +00:00
# Give the information to the container on how many interface should be inside
params [ " Env " ] . append ( " GNS3_MAX_ETHERNET=eth {} " . format ( self . adapters - 1 ) )
2016-06-01 09:39:42 +00:00
# Give the information to the container the list of volume path mounted
params [ " Env " ] . append ( " GNS3_VOLUMES= {} " . format ( " : " . join ( self . _volumes ) ) )
2016-05-06 17:07:18 +00:00
2019-04-05 07:51:57 +00:00
# Pass user configured for image to init script
2019-04-05 10:06:35 +00:00
if image_infos . get ( " Config " , { " User " : " " } ) . get ( " User " ) :
params [ " User " ] = " root "
params [ " Env " ] . append ( " GNS3_USER= {} " . format ( image_infos . get ( " Config " , { " User " : " " } ) [ " User " ] ) )
2019-04-05 07:51:57 +00:00
2018-05-09 09:25:55 +00:00
variables = self . project . variables
if not variables :
variables = [ ]
for var in variables :
formatted = self . _format_env ( variables , var . get ( ' value ' , ' ' ) )
params [ " Env " ] . append ( " {} = {} " . format ( var [ " name " ] , formatted ) )
2018-05-04 12:34:44 +00:00
2015-10-14 16:10:05 +00:00
if self . _environment :
2018-03-12 06:38:50 +00:00
for e in self . _environment . strip ( ) . split ( " \n " ) :
2017-07-20 13:37:56 +00:00
e = e . strip ( )
2019-02-19 10:34:10 +00:00
if e . split ( " = " ) [ 0 ] == " " :
self . project . emit ( " log.warning " , { " message " : " {} has invalid environment variable: {} " . format ( self . name , e ) } )
continue
2017-07-20 13:37:56 +00:00
if not e . startswith ( " GNS3_ " ) :
2018-05-09 09:25:55 +00:00
formatted = self . _format_env ( variables , e )
2022-02-23 04:03:30 +00:00
vm_name = self . _name . replace ( " , " , " ,, " )
project_path = self . project . path . replace ( " , " , " ,, " )
formatted = formatted . replace ( " % vm-name % " , ' " ' + vm_name . replace ( ' " ' , ' \\ " ' ) + ' " ' )
formatted = formatted . replace ( " % vm-id % " , self . _id )
formatted = formatted . replace ( " % project-id % " , self . project . id )
formatted = formatted . replace ( " % project-path % " , ' " ' + project_path . replace ( ' " ' , ' \\ " ' ) + ' " ' )
2018-05-09 09:25:55 +00:00
params [ " Env " ] . append ( formatted )
2016-02-29 20:08:25 +00:00
if self . _console_type == " vnc " :
2018-10-15 10:05:49 +00:00
await self . _start_vnc ( )
2016-06-15 09:01:03 +00:00
params [ " Env " ] . append ( " QT_GRAPHICSSYSTEM=native " ) # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556
2016-02-29 20:08:25 +00:00
params [ " Env " ] . append ( " DISPLAY=: {} " . format ( self . _display ) )
2023-08-27 08:30:37 +00:00
params [ " HostConfig " ] [ " Binds " ] . append ( " /tmp/.X11-unix/X {0} :/tmp/.X11-unix/X {0} :ro " . format ( self . _display ) )
2015-06-17 08:36:55 +00:00
2018-04-27 12:33:07 +00:00
if self . _extra_hosts :
extra_hosts = self . _format_extra_hosts ( self . _extra_hosts )
if extra_hosts :
params [ " Env " ] . append ( " GNS3_EXTRA_HOSTS= {} " . format ( extra_hosts ) )
2018-04-25 14:03:01 +00:00
2024-10-26 09:03:05 +00:00
print ( params )
2018-10-15 10:05:49 +00:00
result = await self . manager . query ( " POST " , " containers/create " , data = params )
2015-06-17 08:36:55 +00:00
self . _cid = result [ ' Id ' ]
2018-09-06 07:49:12 +00:00
log . info ( " Docker container ' {name} ' [ {id} ] created " . format ( name = self . _name , id = self . _id ) )
2015-09-08 08:29:30 +00:00
return True
2018-05-09 09:25:55 +00:00
def _format_env ( self , variables , env ) :
for variable in variables :
env = env . replace ( ' $ { ' + variable [ " name " ] + ' } ' , variable . get ( " value " , " " ) )
return env
2018-04-27 12:33:07 +00:00
def _format_extra_hosts ( self , extra_hosts ) :
lines = [ h . strip ( ) for h in self . _extra_hosts . split ( " \n " ) if h . strip ( ) != " " ]
hosts = [ ]
try :
for host in lines :
hostname , ip = host . split ( " : " )
hostname = hostname . strip ( )
ip = ip . strip ( )
if hostname and ip :
hosts . append ( ( hostname , ip ) )
except ValueError :
raise DockerError ( " Can ' t apply `ExtraHosts`, wrong format: {} " . format ( extra_hosts ) )
return " \n " . join ( [ " {} \t {} " . format ( h [ 1 ] , h [ 0 ] ) for h in hosts ] )
2018-10-15 10:05:49 +00:00
async def update ( self ) :
2015-10-14 16:10:05 +00:00
"""
Destroy an recreate the container with the new settings
"""
2018-03-15 07:17:39 +00:00
2016-02-24 14:47:53 +00:00
# We need to save the console and state and restore it
2016-02-12 15:38:16 +00:00
console = self . console
2016-04-05 14:33:40 +00:00
aux = self . aux
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2016-02-24 14:47:53 +00:00
2018-06-10 22:19:09 +00:00
# reset the docker container, but don't release the NIO UDP ports
2018-10-15 10:05:49 +00:00
await self . reset ( False )
await self . create ( )
2016-02-12 15:38:16 +00:00
self . console = console
2016-04-05 14:33:40 +00:00
self . aux = aux
2016-02-24 14:47:53 +00:00
if state == " running " :
2018-10-15 10:05:49 +00:00
await self . start ( )
2015-06-17 08:36:55 +00:00
2018-10-15 10:05:49 +00:00
async def start ( self ) :
2018-03-15 07:17:39 +00:00
"""
Starts this Docker container .
"""
2015-06-17 08:36:55 +00:00
2024-02-14 08:13:45 +00:00
await self . manager . install_resources ( )
2018-03-12 06:38:50 +00:00
try :
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2018-03-12 06:38:50 +00:00
except DockerHttp404Error :
raise DockerError ( " Docker container ' {name} ' with ID {cid} does not exist or is not ready yet. Please try again in a few seconds. " . format ( name = self . name ,
cid = self . _cid ) )
2015-09-08 08:29:30 +00:00
if state == " paused " :
2018-10-15 10:05:49 +00:00
await self . unpause ( )
2016-12-16 15:19:23 +00:00
elif state == " running " :
return
2015-09-08 08:29:30 +00:00
else :
2018-10-06 13:30:39 +00:00
2018-11-11 15:23:30 +00:00
if self . _console_type == " vnc " and not self . _vnc_process :
# restart the vnc process in case it had previously crashed
await self . _start_vnc_process ( restart = True )
monitor_process ( self . _vnc_process , self . _vnc_callback )
2018-10-06 13:30:39 +00:00
2024-04-04 15:17:21 +00:00
if self . _console_websocket :
await self . _console_websocket . close ( )
self . _console_websocket = None
2018-10-15 10:05:49 +00:00
await self . _clean_servers ( )
2016-05-10 09:38:50 +00:00
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /start " . format ( self . _cid ) )
self . _namespace = await self . _get_namespace ( )
2016-02-11 14:49:28 +00:00
2018-11-19 17:22:16 +00:00
await self . _start_ubridge ( require_privileged_access = True )
2016-02-11 14:49:28 +00:00
2015-10-14 16:10:05 +00:00
for adapter_number in range ( 0 , self . adapters ) :
nio = self . _ethernet_adapters [ adapter_number ] . get_nio ( 0 )
2018-10-15 10:05:49 +00:00
async with self . manager . ubridge_lock :
2016-02-11 14:49:28 +00:00
try :
2018-10-15 10:05:49 +00:00
await self . _add_ubridge_connection ( nio , adapter_number )
2016-02-11 14:49:28 +00:00
except UbridgeNamespaceError :
2017-05-23 11:31:04 +00:00
log . error ( " Container %s failed to start " , self . name )
2018-10-15 10:05:49 +00:00
await self . stop ( )
2016-02-11 14:49:28 +00:00
2016-05-31 04:07:37 +00:00
# The container can crash soon after the start, this means we can not move the interface to the container namespace
2018-10-15 10:05:49 +00:00
logdata = await self . _get_log ( )
2016-02-11 14:49:28 +00:00
for line in logdata . split ( ' \n ' ) :
log . error ( line )
raise DockerError ( logdata )
2015-06-17 08:36:55 +00:00
2016-02-29 20:08:25 +00:00
if self . console_type == " telnet " :
2018-10-15 10:05:49 +00:00
await self . _start_console ( )
2016-05-03 14:49:33 +00:00
elif self . console_type == " http " or self . console_type == " https " :
2018-10-15 10:05:49 +00:00
await self . _start_http ( )
2015-10-14 16:10:05 +00:00
2024-09-23 06:10:58 +00:00
if self . allocate_aux :
2018-10-15 10:05:49 +00:00
await self . _start_aux ( )
2016-03-01 13:53:43 +00:00
2018-11-26 08:53:24 +00:00
self . _permissions_fixed = False
2015-10-14 16:10:05 +00:00
self . status = " started "
2016-05-31 04:07:37 +00:00
log . info ( " Docker container ' {name} ' [ {image} ] started listen for {console_type} on {console} " . format ( name = self . _name ,
image = self . _image ,
console = self . console ,
console_type = self . console_type ) )
2016-03-01 13:53:43 +00:00
2018-10-15 10:05:49 +00:00
async def _start_aux ( self ) :
2016-03-01 13:53:43 +00:00
"""
2018-09-06 07:49:12 +00:00
Start an auxiliary console
2016-03-01 13:53:43 +00:00
"""
# We can not use the API because docker doesn't expose a websocket api for exec
2016-05-18 09:23:45 +00:00
# https://github.com/GNS3/gns3-gui/issues/1039
2018-09-06 07:49:12 +00:00
try :
2018-10-15 10:05:49 +00:00
process = await asyncio . subprocess . create_subprocess_exec (
2023-01-01 09:04:48 +00:00
" script " ,
" -qfc " ,
f " docker exec -i -t { self . _cid } /gns3/bin/busybox sh -c ' while true; do TERM=vt100 /gns3/bin/busybox sh; done ' " ,
" /dev/null " ,
2018-09-06 07:49:12 +00:00
stdout = asyncio . subprocess . PIPE ,
stderr = asyncio . subprocess . STDOUT ,
2023-01-01 09:04:48 +00:00
stdin = asyncio . subprocess . PIPE
)
2018-09-06 07:49:12 +00:00
except OSError as e :
raise DockerError ( " Could not start auxiliary console process: {} " . format ( e ) )
2016-05-03 09:33:43 +00:00
server = AsyncioTelnetServer ( reader = process . stdout , writer = process . stdin , binary = True , echo = True )
2018-04-16 07:30:06 +00:00
try :
2018-10-15 10:05:49 +00:00
self . _telnet_servers . append ( ( await asyncio . start_server ( server . run , self . _manager . port_manager . console_host , self . aux ) ) )
2018-04-16 07:30:06 +00:00
except OSError as e :
raise DockerError ( " Could not start Telnet server on socket {} : {} : {} " . format ( self . _manager . port_manager . console_host , self . aux , e ) )
2018-11-25 10:11:42 +00:00
log . debug ( " Docker container ' %s ' started listen for auxiliary telnet on %d " , self . name , self . aux )
2015-10-14 16:10:05 +00:00
2018-10-15 10:05:49 +00:00
async def _fix_permissions ( self ) :
2016-05-31 19:08:41 +00:00
"""
Because docker run as root we need to fix permission and ownership to allow user to interact
with it from their filesystem and do operation like file delete
"""
2017-07-26 10:41:06 +00:00
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2018-11-25 10:11:42 +00:00
log . info ( " Docker container ' {name} ' fix ownership, state = {state} " . format ( name = self . _name , state = state ) )
2017-07-26 10:41:06 +00:00
if state == " stopped " or state == " exited " :
# We need to restart it to fix permissions
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /start " . format ( self . _cid ) )
2017-07-26 10:41:06 +00:00
2016-05-31 19:08:41 +00:00
for volume in self . _volumes :
log . debug ( " Docker container ' {name} ' [ {image} ] fix ownership on {path} " . format (
name = self . _name , image = self . _image , path = volume ) )
2018-09-06 07:49:12 +00:00
try :
2018-10-15 10:05:49 +00:00
process = await asyncio . subprocess . create_subprocess_exec (
2018-09-06 07:49:12 +00:00
" docker " ,
" exec " ,
self . _cid ,
" /gns3/bin/busybox " ,
" sh " ,
" -c " ,
" ( "
" /gns3/bin/busybox find \" {path} \" -depth -print0 "
" | /gns3/bin/busybox xargs -0 /gns3/bin/busybox stat -c ' %a : %u : %g : % n ' > \" {path} /.gns3_perms \" "
" ) "
" && /gns3/bin/busybox chmod -R u+rX \" {path} \" "
" && /gns3/bin/busybox chown {uid} : {gid} -R \" {path} \" "
. format ( uid = os . getuid ( ) , gid = os . getgid ( ) , path = volume ) ,
)
except OSError as e :
raise DockerError ( " Could not fix permissions for {} : {} " . format ( volume , e ) )
2018-10-15 10:05:49 +00:00
await process . wait ( )
2018-11-26 08:53:24 +00:00
self . _permissions_fixed = True
2016-05-31 19:08:41 +00:00
2018-11-11 15:23:30 +00:00
async def _start_vnc_process ( self , restart = False ) :
2016-02-29 20:08:25 +00:00
"""
2018-11-11 15:23:30 +00:00
Starts the VNC process .
2016-02-29 20:08:25 +00:00
"""
self . _display = self . _get_free_display_port ( )
2020-03-19 08:36:51 +00:00
tigervnc_path = shutil . which ( " Xtigervnc " ) or shutil . which ( " Xvnc " )
2018-11-11 13:07:33 +00:00
2023-05-31 11:58:06 +00:00
if not tigervnc_path :
raise DockerError ( " Please install TigerVNC server before using VNC support " )
2020-03-19 08:36:51 +00:00
if tigervnc_path :
2018-11-11 13:07:33 +00:00
with open ( os . path . join ( self . working_dir , " vnc.log " ) , " w " ) as fd :
2020-03-19 08:36:51 +00:00
self . _vnc_process = await asyncio . create_subprocess_exec ( tigervnc_path ,
2022-06-07 16:05:24 +00:00
" -extension " , " MIT-SHM " ,
2018-11-11 14:31:29 +00:00
" -geometry " , self . _console_resolution ,
" -depth " , " 16 " ,
" -interface " , self . _manager . port_manager . console_host ,
" -rfbport " , str ( self . console ) ,
" -AlwaysShared " ,
" -SecurityTypes " , " None " ,
" : {} " . format ( self . _display ) ,
stdout = fd , stderr = subprocess . STDOUT )
2016-02-29 20:08:25 +00:00
2018-11-11 15:23:30 +00:00
async def _start_vnc ( self ) :
"""
Starts a VNC server for this container
"""
self . _display = self . _get_free_display_port ( )
2020-03-19 08:36:51 +00:00
tigervnc_path = shutil . which ( " Xtigervnc " ) or shutil . which ( " Xvnc " )
2023-05-31 11:58:06 +00:00
if not tigervnc_path :
raise DockerError ( " Please install TigerVNC server before using VNC support " )
2018-11-11 15:23:30 +00:00
await self . _start_vnc_process ( )
2016-02-29 20:08:25 +00:00
x11_socket = os . path . join ( " /tmp/.X11-unix/ " , " X {} " . format ( self . _display ) )
2020-06-10 11:07:13 +00:00
try :
await wait_for_file_creation ( x11_socket )
except asyncio . TimeoutError :
raise DockerError ( ' x11 socket file " {} " does not exist ' . format ( x11_socket ) )
2016-02-29 20:08:25 +00:00
2020-04-28 04:19:05 +00:00
if not hasattr ( sys , " _called_from_test " ) or not sys . _called_from_test :
# Start vncconfig for tigervnc clipboard support, connection available only after socket creation.
tigervncconfig_path = shutil . which ( " vncconfig " )
if tigervnc_path and tigervncconfig_path :
self . _vncconfig_process = await asyncio . create_subprocess_exec ( tigervncconfig_path , " -display " , " : {} " . format ( self . _display ) , " -nowin " )
2020-04-15 14:14:20 +00:00
2018-11-11 14:31:29 +00:00
# sometimes the VNC process can crash
monitor_process ( self . _vnc_process , self . _vnc_callback )
2018-09-05 07:16:07 +00:00
2018-11-11 13:07:33 +00:00
def _vnc_callback ( self , returncode ) :
2018-09-05 07:16:07 +00:00
"""
Called when the process has stopped .
: param returncode : Process returncode
"""
2018-10-06 13:30:39 +00:00
if returncode != 0 and self . _closing is False :
2018-11-11 14:31:29 +00:00
self . project . emit ( " log.error " , { " message " : " The vnc process has stopped with return code {} for node ' {} ' . Please restart this node. " . format ( returncode , self . name ) } )
self . _vnc_process = None
2018-09-05 07:16:07 +00:00
2018-10-15 10:05:49 +00:00
async def _start_http ( self ) :
2016-05-03 14:49:33 +00:00
"""
2018-03-15 07:17:39 +00:00
Starts an HTTP tunnel to container localhost . It ' s not perfect
2016-12-09 15:41:15 +00:00
but the only way we have to inject network packet is using nc .
2016-05-03 14:49:33 +00:00
"""
2018-03-15 07:17:39 +00:00
2016-05-03 14:49:33 +00:00
log . debug ( " Forward HTTP for %s to %d " , self . name , self . _console_http_port )
command = [ " docker " , " exec " , " -i " , self . _cid , " /gns3/bin/busybox " , " nc " , " 127.0.0.1 " , str ( self . _console_http_port ) ]
2016-12-09 15:41:15 +00:00
# We replace host and port in the server answer otherwise some link could be broken
2016-05-03 16:01:23 +00:00
server = AsyncioRawCommandServer ( command , replaces = [
(
2016-12-09 15:41:15 +00:00
' ://127.0.0.1 ' . encode ( ) , # {{HOST}} mean client host
' :// {{ HOST}} ' . encode ( ) ,
) ,
(
' : {} ' . format ( self . _console_http_port ) . encode ( ) ,
' : {} ' . format ( self . console ) . encode ( ) ,
2016-05-03 16:01:23 +00:00
)
] )
2018-10-15 10:05:49 +00:00
self . _telnet_servers . append ( ( await asyncio . start_server ( server . run , self . _manager . port_manager . console_host , self . console ) ) )
2016-05-03 14:49:33 +00:00
2018-11-30 04:38:02 +00:00
async def _window_size_changed_callback ( self , columns , rows ) :
2018-11-27 08:06:56 +00:00
"""
Called when the console window size has been changed .
( when naws is enabled in the Telnet server )
: param columns : number of columns
: param rows : number of rows
"""
# resize the container TTY.
2018-11-30 04:38:02 +00:00
await self . _manager . query ( " POST " , " containers/ {} /resize?h= {} &w= {} " . format ( self . _cid , rows , columns ) )
2018-11-27 08:06:56 +00:00
2018-10-15 10:05:49 +00:00
async def _start_console ( self ) :
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
Starts streaming the console via telnet
2015-10-14 16:10:05 +00:00
"""
2016-05-03 14:49:33 +00:00
2015-10-14 16:10:05 +00:00
class InputStream :
def __init__ ( self ) :
self . _data = b " "
def write ( self , data ) :
self . _data + = data
2018-10-15 10:05:49 +00:00
async def drain ( self ) :
2015-10-14 16:10:05 +00:00
if not self . ws . closed :
2018-11-30 09:06:21 +00:00
await self . ws . send_bytes ( self . _data )
2015-10-14 16:10:05 +00:00
self . _data = b " "
output_stream = asyncio . StreamReader ( )
input_stream = InputStream ( )
2018-11-27 08:06:56 +00:00
telnet = AsyncioTelnetServer ( reader = output_stream , writer = input_stream , echo = True , naws = True , window_size_changed_callback = self . _window_size_changed_callback )
2018-04-16 07:30:06 +00:00
try :
2018-10-15 10:05:49 +00:00
self . _telnet_servers . append ( ( await asyncio . start_server ( telnet . run , self . _manager . port_manager . console_host , self . console ) ) )
2018-04-16 07:30:06 +00:00
except OSError as e :
raise DockerError ( " Could not start Telnet server on socket {} : {} : {} " . format ( self . _manager . port_manager . console_host , self . console , e ) )
2015-10-14 16:10:05 +00:00
2018-10-15 10:05:49 +00:00
self . _console_websocket = await self . manager . websocket_query ( " containers/ {} /attach/ws?stream=1&stdin=1&stdout=1&stderr=1 " . format ( self . _cid ) )
2016-05-10 10:14:48 +00:00
input_stream . ws = self . _console_websocket
2015-10-14 16:10:05 +00:00
output_stream . feed_data ( self . name . encode ( ) + b " console is now available... Press RETURN to get started. \r \n " )
2018-10-15 10:05:49 +00:00
asyncio . ensure_future ( self . _read_console_output ( self . _console_websocket , output_stream ) )
2015-10-14 16:10:05 +00:00
2018-10-15 10:05:49 +00:00
async def _read_console_output ( self , ws , out ) :
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
Reads Websocket and forward it to the telnet
2016-05-14 01:28:53 +00:00
: param ws : Websocket connection
2015-10-14 16:10:05 +00:00
: param out : Output stream
"""
while True :
2018-10-15 10:05:49 +00:00
msg = await ws . receive ( )
2018-11-30 10:59:02 +00:00
if msg . type == aiohttp . WSMsgType . TEXT :
2015-10-14 16:10:05 +00:00
out . feed_data ( msg . data . encode ( ) )
2018-11-30 09:06:21 +00:00
elif msg . type == aiohttp . WSMsgType . BINARY :
2017-07-06 08:13:00 +00:00
out . feed_data ( msg . data )
2018-11-30 09:06:21 +00:00
elif msg . type == aiohttp . WSMsgType . ERROR :
log . critical ( " Docker WebSocket Error: {} " . format ( ws . exception ( ) ) )
2015-10-14 16:10:05 +00:00
else :
out . feed_eof ( )
2018-11-30 09:06:21 +00:00
await ws . close ( )
2015-10-14 16:10:05 +00:00
break
2015-09-08 08:29:30 +00:00
2020-07-26 08:57:18 +00:00
async def reset_console ( self ) :
"""
Reset the console .
"""
2024-04-04 15:17:21 +00:00
if self . _console_websocket :
await self . _console_websocket . close ( )
2020-07-26 08:57:18 +00:00
await self . _clean_servers ( )
await self . _start_console ( )
2018-10-15 10:05:49 +00:00
async def is_running ( self ) :
2018-03-15 07:17:39 +00:00
"""
Checks if the container is running .
2015-09-08 08:29:30 +00:00
: returns : True or False
: rtype : bool
"""
2018-03-15 07:17:39 +00:00
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2015-09-08 08:29:30 +00:00
if state == " running " :
return True
2017-04-10 14:58:00 +00:00
if self . status == " started " : # The container crashed we need to clean
2018-10-15 10:05:49 +00:00
await self . stop ( )
2015-09-08 08:29:30 +00:00
return False
2018-10-15 10:05:49 +00:00
async def restart ( self ) :
2018-03-15 07:17:39 +00:00
"""
Restart this Docker container .
"""
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /restart " . format ( self . _cid ) )
2015-09-08 08:29:30 +00:00
log . info ( " Docker container ' {name} ' [ {image} ] restarted " . format (
name = self . _name , image = self . _image ) )
2018-10-15 10:05:49 +00:00
async def _clean_servers ( self ) :
2016-05-10 09:38:50 +00:00
"""
Clean the list of running console servers
"""
2018-03-15 07:17:39 +00:00
2016-05-10 09:38:50 +00:00
if len ( self . _telnet_servers ) > 0 :
for telnet_server in self . _telnet_servers :
telnet_server . close ( )
2018-10-15 10:05:49 +00:00
await telnet_server . wait_closed ( )
2016-05-10 09:38:50 +00:00
self . _telnet_servers = [ ]
2018-10-15 10:05:49 +00:00
async def stop ( self ) :
2018-03-15 07:17:39 +00:00
"""
Stops this Docker container .
"""
2015-06-17 08:36:55 +00:00
2015-12-29 11:40:22 +00:00
try :
2024-04-04 15:17:21 +00:00
if self . _console_websocket :
await self . _console_websocket . close ( )
self . _console_websocket = None
2018-10-15 10:05:49 +00:00
await self . _clean_servers ( )
await self . _stop_ubridge ( )
2015-12-29 11:40:22 +00:00
2016-09-07 12:24:56 +00:00
try :
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2016-09-07 12:24:56 +00:00
except DockerHttp404Error :
2017-07-26 10:41:06 +00:00
self . status = " stopped "
return
2016-09-07 12:24:56 +00:00
2015-12-29 11:40:22 +00:00
if state == " paused " :
2018-10-15 10:05:49 +00:00
await self . unpause ( )
2015-12-29 11:40:22 +00:00
2018-11-26 08:53:24 +00:00
if not self . _permissions_fixed :
2018-11-30 04:38:02 +00:00
await self . _fix_permissions ( )
2018-11-25 10:11:42 +00:00
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2017-07-26 10:41:06 +00:00
if state != " stopped " or state != " exited " :
2016-06-15 08:32:38 +00:00
# t=5 number of seconds to wait before killing the container
try :
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /stop " . format ( self . _cid ) , params = { " t " : 5 } )
2018-11-25 10:11:42 +00:00
log . info ( " Docker container ' {name} ' [ {image} ] stopped " . format ( name = self . _name , image = self . _image ) )
2016-06-15 08:32:38 +00:00
except DockerHttp304Error :
# Container is already stopped
pass
2015-12-29 11:40:22 +00:00
# Ignore runtime error because when closing the server
except RuntimeError as e :
log . debug ( " Docker runtime error when closing: {} " . format ( str ( e ) ) )
return
2016-06-15 08:32:38 +00:00
self . status = " stopped "
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def pause ( self ) :
2018-03-15 07:17:39 +00:00
"""
Pauses this Docker container .
"""
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /pause " . format ( self . _cid ) )
2016-05-14 02:41:58 +00:00
self . status = " suspended "
log . info ( " Docker container ' {name} ' [ {image} ] paused " . format ( name = self . _name , image = self . _image ) )
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def unpause ( self ) :
2018-03-15 07:17:39 +00:00
"""
Unpauses this Docker container .
"""
2018-10-15 10:05:49 +00:00
await self . manager . query ( " POST " , " containers/ {} /unpause " . format ( self . _cid ) )
2015-10-14 16:10:05 +00:00
self . status = " started "
2016-05-14 02:41:58 +00:00
log . info ( " Docker container ' {name} ' [ {image} ] unpaused " . format ( name = self . _name , image = self . _image ) )
2015-09-08 08:29:30 +00:00
2018-10-15 10:05:49 +00:00
async def close ( self ) :
2018-03-15 07:17:39 +00:00
"""
Closes this Docker container .
"""
2016-02-29 20:08:25 +00:00
2018-10-06 13:30:39 +00:00
self . _closing = True
2018-10-15 10:05:49 +00:00
if not ( await super ( ) . close ( ) ) :
2016-02-29 20:08:25 +00:00
return False
2018-10-15 10:05:49 +00:00
await self . reset ( )
2015-06-17 08:36:55 +00:00
2018-10-15 10:05:49 +00:00
async def reset ( self , release_nio_udp_ports = True ) :
2018-03-15 07:17:39 +00:00
2015-12-29 11:40:22 +00:00
try :
2018-10-15 10:05:49 +00:00
state = await self . _get_container_state ( )
2016-09-01 07:28:22 +00:00
if state == " paused " or state == " running " :
2018-10-15 10:05:49 +00:00
await self . stop ( )
2018-10-06 13:30:39 +00:00
2016-02-29 20:08:25 +00:00
if self . console_type == " vnc " :
2020-04-15 14:14:20 +00:00
if self . _vncconfig_process :
try :
self . _vncconfig_process . terminate ( )
await self . _vncconfig_process . wait ( )
except ProcessLookupError :
pass
2018-11-11 13:07:33 +00:00
if self . _vnc_process :
2016-05-19 11:23:53 +00:00
try :
2018-11-11 13:07:33 +00:00
self . _vnc_process . terminate ( )
2018-11-11 14:31:29 +00:00
await self . _vnc_process . wait ( )
2016-05-19 11:23:53 +00:00
except ProcessLookupError :
pass
2018-10-06 13:30:39 +00:00
2018-10-06 13:57:03 +00:00
if self . _display :
display = " /tmp/.X11-unix/X {} " . format ( self . _display )
try :
if os . path . exists ( display ) :
os . remove ( display )
except OSError as e :
log . warning ( " Could not remove display {} : {} " . format ( display , e ) )
2018-10-06 13:30:39 +00:00
2016-06-13 15:39:04 +00:00
# v – 1/True/true or 0/False/false, Remove the volumes associated to the container. Default false.
# force - 1/True/true or 0/False/false, Kill then remove the container. Default false.
2016-10-14 17:06:12 +00:00
try :
2018-10-15 10:05:49 +00:00
await self . manager . query ( " DELETE " , " containers/ {} " . format ( self . _cid ) , params = { " force " : 1 , " v " : 1 } )
2016-10-14 17:06:12 +00:00
except DockerError :
pass
2015-12-29 11:40:22 +00:00
log . info ( " Docker container ' {name} ' [ {image} ] removed " . format (
name = self . _name , image = self . _image ) )
2018-06-10 22:19:09 +00:00
if release_nio_udp_ports :
for adapter in self . _ethernet_adapters :
if adapter is not None :
for nio in adapter . ports . values ( ) :
if nio and isinstance ( nio , NIOUDP ) :
self . manager . port_manager . release_udp_port ( nio . lport , self . _project )
2015-12-29 11:40:22 +00:00
# Ignore runtime error because when closing the server
2016-02-12 15:48:19 +00:00
except ( DockerHttp404Error , RuntimeError ) as e :
log . debug ( " Docker error when closing: {} " . format ( str ( e ) ) )
2015-12-29 11:40:22 +00:00
return
2015-10-14 16:10:05 +00:00
2018-10-15 10:05:49 +00:00
async def _add_ubridge_connection ( self , nio , adapter_number ) :
2015-06-17 08:36:55 +00:00
"""
Creates a connection in uBridge .
2016-06-23 22:56:06 +00:00
: param nio : NIO instance or None if it ' s a dummy interface (if an interface is missing in ubridge you can ' t see it via ifconfig in the container )
2015-06-17 08:36:55 +00:00
: param adapter_number : adapter number
"""
2016-06-23 22:56:06 +00:00
2015-06-17 08:36:55 +00:00
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 04:07:37 +00:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker container ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 08:36:55 +00:00
2016-06-27 03:50:08 +00:00
for index in range ( 4096 ) :
2016-12-13 15:05:38 +00:00
if " tap-gns3-e {} " . format ( index ) not in psutil . net_if_addrs ( ) :
2016-10-24 10:35:50 +00:00
adapter . host_ifc = " tap-gns3-e {} " . format ( str ( index ) )
2016-02-09 13:22:37 +00:00
break
2016-12-13 15:05:38 +00:00
if adapter . host_ifc is None :
2016-05-31 04:07:37 +00:00
raise DockerError ( " Adapter {adapter_number} couldn ' t allocate interface on Docker container ' {name} ' . Too many Docker interfaces already exists " . format ( name = self . name ,
adapter_number = adapter_number ) )
2017-07-12 15:39:19 +00:00
bridge_name = ' bridge {} ' . format ( adapter_number )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( ' bridge create {} ' . format ( bridge_name ) )
2017-07-12 15:39:19 +00:00
self . _bridges . add ( bridge_name )
2024-09-18 09:30:22 +00:00
await self . _ubridge_send ( ' bridge add_nio_tap bridge {adapter_number} {hostif} ' . format (
adapter_number = adapter_number ,
hostif = adapter . host_ifc )
)
mac_address = int_to_macaddress ( macaddress_to_int ( self . _mac_address ) + adapter_number )
custom_adapter = self . _get_custom_adapter_settings ( adapter_number )
custom_mac_address = custom_adapter . get ( " mac_address " )
if custom_mac_address :
mac_address = custom_mac_address
try :
await self . _ubridge_send ( ' docker set_mac_addr {ifc} {mac} ' . format ( ifc = adapter . host_ifc , mac = mac_address ) )
except UbridgeError :
log . warning ( " Could not set MAC address %s on interface %s " , mac_address , adapter . host_ifc )
2016-12-14 15:53:20 +00:00
log . debug ( " Move container %s adapter %s to namespace %s " , self . name , adapter . host_ifc , self . _namespace )
2016-02-11 14:49:28 +00:00
try :
2024-09-18 09:30:22 +00:00
await self . _ubridge_send ( ' docker move_to_ns {ifc} {ns} eth {adapter} ' . format (
ifc = adapter . host_ifc ,
ns = self . _namespace ,
adapter = adapter_number )
)
2016-02-11 14:49:28 +00:00
except UbridgeError as e :
raise UbridgeNamespaceError ( e )
2024-09-18 09:30:22 +00:00
else :
log . info ( " Created adapter %s with MAC address %s in namespace %s " , adapter_number , mac_address , self . _namespace )
2015-06-17 08:36:55 +00:00
2016-10-24 10:35:50 +00:00
if nio :
2018-10-15 10:05:49 +00:00
await self . _connect_nio ( adapter_number , nio )
2015-10-14 16:10:05 +00:00
2018-10-15 10:05:49 +00:00
async def _get_namespace ( self ) :
2018-03-15 07:17:39 +00:00
2018-10-15 10:05:49 +00:00
result = await self . manager . query ( " GET " , " containers/ {} /json " . format ( self . _cid ) )
2015-10-14 16:10:05 +00:00
return int ( result [ ' State ' ] [ ' Pid ' ] )
2015-06-17 08:36:55 +00:00
2018-10-15 10:05:49 +00:00
async def _connect_nio ( self , adapter_number , nio ) :
2018-03-15 07:17:39 +00:00
2017-07-12 15:39:19 +00:00
bridge_name = ' bridge {} ' . format ( adapter_number )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( ' bridge add_nio_udp {bridge_name} {lport} {rhost} {rport} ' . format ( bridge_name = bridge_name ,
2017-07-12 15:39:19 +00:00
lport = nio . lport ,
rhost = nio . rhost ,
rport = nio . rport ) )
2016-12-14 15:53:20 +00:00
if nio . capturing :
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( ' bridge start_capture {bridge_name} " {pcap_file} " ' . format ( bridge_name = bridge_name ,
2017-07-12 15:39:19 +00:00
pcap_file = nio . pcap_output_file ) )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( ' bridge start {bridge_name} ' . format ( bridge_name = bridge_name ) )
await self . _ubridge_apply_filters ( bridge_name , nio . filters )
2016-12-14 15:53:20 +00:00
2018-10-15 10:05:49 +00:00
async def adapter_add_nio_binding ( self , adapter_number , nio ) :
2018-03-15 07:17:39 +00:00
"""
Adds an adapter NIO binding .
2017-07-12 15:39:19 +00:00
2015-06-17 08:36:55 +00:00
: param adapter_number : adapter number
: param nio : NIO instance to add to the slot / port
"""
2018-03-15 07:17:39 +00:00
2015-06-17 08:36:55 +00:00
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 04:07:37 +00:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker container ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 08:36:55 +00:00
2016-12-14 11:01:34 +00:00
if self . status == " started " and self . ubridge :
2018-10-15 10:05:49 +00:00
await self . _connect_nio ( adapter_number , nio )
2016-06-24 23:26:40 +00:00
2015-06-17 08:36:55 +00:00
adapter . add_nio ( 0 , nio )
2016-05-31 04:07:37 +00:00
log . info ( " Docker container ' {name} ' [ {id} ]: {nio} added to adapter {adapter_number} " . format ( name = self . name ,
id = self . _id ,
nio = nio ,
adapter_number = adapter_number ) )
2015-06-17 08:36:55 +00:00
2018-10-15 10:05:49 +00:00
async def adapter_update_nio_binding ( self , adapter_number , nio ) :
2017-07-12 15:39:19 +00:00
"""
2018-10-27 07:47:17 +00:00
Update an adapter NIO binding .
2017-07-12 15:39:19 +00:00
: param adapter_number : adapter number
2018-10-27 07:47:17 +00:00
: param nio : NIO instance to update the adapter
2017-07-12 15:39:19 +00:00
"""
2017-11-23 03:04:32 +00:00
if self . ubridge :
bridge_name = ' bridge {} ' . format ( adapter_number )
if bridge_name in self . _bridges :
2018-10-15 10:05:49 +00:00
await self . _ubridge_apply_filters ( bridge_name , nio . filters )
2017-07-12 15:39:19 +00:00
2018-10-15 10:05:49 +00:00
async def adapter_remove_nio_binding ( self , adapter_number ) :
2015-06-17 08:36:55 +00:00
"""
Removes an adapter NIO binding .
: param adapter_number : adapter number
: returns : NIO instance
"""
2018-03-15 07:17:39 +00:00
2015-06-17 08:36:55 +00:00
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 04:07:37 +00:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker VM ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 08:36:55 +00:00
2019-04-01 12:47:31 +00:00
await self . stop_capture ( adapter_number )
2016-12-14 15:53:20 +00:00
if self . ubridge :
nio = adapter . get_nio ( 0 )
2017-07-12 15:39:19 +00:00
bridge_name = ' bridge {} ' . format ( adapter_number )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( " bridge stop {} " . format ( bridge_name ) )
await self . _ubridge_send ( ' bridge remove_nio_udp bridge {adapter} {lport} {rhost} {rport} ' . format ( adapter = adapter_number ,
2016-12-14 15:53:20 +00:00
lport = nio . lport ,
rhost = nio . rhost ,
rport = nio . rport ) )
2015-06-17 08:36:55 +00:00
adapter . remove_nio ( 0 )
2016-05-31 04:07:37 +00:00
log . info ( " Docker VM ' {name} ' [ {id} ]: {nio} removed from adapter {adapter_number} " . format ( name = self . name ,
id = self . id ,
nio = adapter . host_ifc ,
adapter_number = adapter_number ) )
2015-06-17 08:36:55 +00:00
2018-10-27 07:47:17 +00:00
def get_nio ( self , adapter_number ) :
"""
Gets an adapter NIO binding .
: param adapter_number : adapter number
: returns : NIO instance
"""
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except KeyError :
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker VM ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
nio = adapter . get_nio ( 0 )
if not nio :
raise DockerError ( " Adapter {} is not connected " . format ( adapter_number ) )
return nio
2015-06-17 08:36:55 +00:00
@property
def adapters ( self ) :
2018-03-15 07:17:39 +00:00
"""
Returns the number of Ethernet adapters for this Docker VM .
2015-06-17 08:36:55 +00:00
: returns : number of adapters
: rtype : int
"""
2018-03-15 07:17:39 +00:00
2015-06-17 08:36:55 +00:00
return len ( self . _ethernet_adapters )
@adapters.setter
def adapters ( self , adapters ) :
2018-03-15 07:17:39 +00:00
"""
Sets the number of Ethernet adapters for this Docker container .
2015-06-17 08:36:55 +00:00
: param adapters : number of adapters
"""
2016-04-25 14:09:04 +00:00
if len ( self . _ethernet_adapters ) == adapters :
return
2015-06-17 08:36:55 +00:00
self . _ethernet_adapters . clear ( )
for adapter_number in range ( 0 , adapters ) :
self . _ethernet_adapters . append ( EthernetAdapter ( ) )
2016-05-31 04:07:37 +00:00
log . info ( ' Docker container " {name} " [ {id} ]: number of Ethernet adapters changed to {adapters} ' . format ( name = self . _name ,
id = self . _id ,
adapters = adapters ) )
2015-06-17 08:36:55 +00:00
2018-10-15 10:05:49 +00:00
async def pull_image ( self , image ) :
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
Pulls an image from Docker repository
2015-10-14 16:10:05 +00:00
"""
2018-03-15 07:17:39 +00:00
2017-03-27 18:46:25 +00:00
def callback ( msg ) :
self . project . emit ( " log.info " , { " message " : msg } )
2018-10-15 10:05:49 +00:00
await self . manager . pull_image ( image , progress_callback = callback )
2016-02-09 15:07:33 +00:00
2018-10-15 10:05:49 +00:00
async def _start_ubridge_capture ( self , adapter_number , output_file ) :
2016-02-09 15:07:33 +00:00
"""
2018-03-15 07:17:39 +00:00
Starts a packet capture in uBridge .
2016-02-09 15:07:33 +00:00
: param adapter_number : adapter number
: param output_file : PCAP destination file for the capture
"""
adapter = " bridge {} " . format ( adapter_number )
2016-12-14 11:01:34 +00:00
if not self . ubridge :
2016-05-11 17:35:36 +00:00
raise DockerError ( " Cannot start the packet capture: uBridge is not running " )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( ' bridge start_capture {name} " {output_file} " ' . format ( name = adapter , output_file = output_file ) )
2016-02-09 15:07:33 +00:00
2018-10-15 10:05:49 +00:00
async def _stop_ubridge_capture ( self , adapter_number ) :
2016-02-09 15:07:33 +00:00
"""
2018-03-15 07:17:39 +00:00
Stops a packet capture in uBridge .
2016-02-09 15:07:33 +00:00
: param adapter_number : adapter number
"""
adapter = " bridge {} " . format ( adapter_number )
2016-12-14 11:01:34 +00:00
if not self . ubridge :
2016-05-11 17:35:36 +00:00
raise DockerError ( " Cannot stop the packet capture: uBridge is not running " )
2018-10-15 10:05:49 +00:00
await self . _ubridge_send ( " bridge stop_capture {name} " . format ( name = adapter ) )
2016-02-09 15:07:33 +00:00
2018-10-15 10:05:49 +00:00
async def start_capture ( self , adapter_number , output_file ) :
2016-02-09 15:07:33 +00:00
"""
Starts a packet capture .
: param adapter_number : adapter number
: param output_file : PCAP destination file for the capture
"""
2018-10-27 07:47:17 +00:00
nio = self . get_nio ( adapter_number )
2016-02-09 15:07:33 +00:00
if nio . capturing :
raise DockerError ( " Packet capture is already activated on adapter {adapter_number} " . format ( adapter_number = adapter_number ) )
2019-04-01 13:58:18 +00:00
nio . start_packet_capture ( output_file )
2016-12-14 11:01:34 +00:00
if self . status == " started " and self . ubridge :
2018-10-15 10:05:49 +00:00
await self . _start_ubridge_capture ( adapter_number , output_file )
2016-02-09 15:07:33 +00:00
log . info ( " Docker VM ' {name} ' [ {id} ]: starting packet capture on adapter {adapter_number} " . format ( name = self . name ,
id = self . id ,
adapter_number = adapter_number ) )
2018-10-15 10:05:49 +00:00
async def stop_capture ( self , adapter_number ) :
2016-02-09 15:07:33 +00:00
"""
Stops a packet capture .
: param adapter_number : adapter number
"""
2018-10-27 07:47:17 +00:00
nio = self . get_nio ( adapter_number )
2019-04-01 12:47:31 +00:00
if not nio . capturing :
return
2019-04-01 13:58:18 +00:00
nio . stop_packet_capture ( )
2016-12-14 11:01:34 +00:00
if self . status == " started " and self . ubridge :
2018-10-15 10:05:49 +00:00
await self . _stop_ubridge_capture ( adapter_number )
2016-02-09 15:07:33 +00:00
log . info ( " Docker VM ' {name} ' [ {id} ]: stopping packet capture on adapter {adapter_number} " . format ( name = self . name ,
id = self . id ,
adapter_number = adapter_number ) )
2016-02-11 14:49:28 +00:00
2018-10-15 10:05:49 +00:00
async def _get_log ( self ) :
2016-02-11 14:49:28 +00:00
"""
2018-03-15 07:17:39 +00:00
Returns the log from the container
2016-02-11 14:49:28 +00:00
: returns : string
"""
2018-10-15 10:05:49 +00:00
result = await self . manager . query ( " GET " , " containers/ {} /logs " . format ( self . _cid ) , params = { " stderr " : 1 , " stdout " : 1 } )
2016-02-11 14:49:28 +00:00
return result
2016-02-29 20:08:25 +00:00
2018-10-15 10:05:49 +00:00
async def delete ( self ) :
2016-02-29 20:08:25 +00:00
"""
2018-03-15 07:17:39 +00:00
Deletes the VM ( including all its files ) .
2016-02-29 20:08:25 +00:00
"""
2018-03-15 07:17:39 +00:00
2018-10-15 10:05:49 +00:00
await self . close ( )
await super ( ) . delete ( )