2015-09-08 11:29:30 +03: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 .
"""
import asyncio
2015-06-17 11:36:55 +03:00
import shutil
2015-11-08 22:34:27 +02:00
import psutil
2015-10-14 19:10:05 +03:00
import shlex
import aiohttp
2016-02-12 12:57:56 +02:00
import os
2015-09-08 11:29:30 +03:00
2016-05-14 04:28:53 +03: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
from gns3server . utils . get_resource import get_resource
from gns3server . ubridge . ubridge_error import UbridgeError , UbridgeNamespaceError
2016-05-11 20:35:36 +03:00
from . . base_node import BaseNode
2016-05-24 11:13:53 +03:00
2015-06-17 11:36:55 +03:00
from . . adapters . ethernet_adapter import EthernetAdapter
from . . nios . nio_udp import NIOUDP
2016-05-14 04:28:53 +03:00
from . docker_error import (
DockerError ,
DockerHttp304Error ,
DockerHttp404Error
)
2016-02-11 16:49:28 +02:00
2015-09-08 11:29:30 +03:00
import logging
log = logging . getLogger ( __name__ )
2016-05-11 20:35:36 +03:00
class DockerVM ( BaseNode ) :
2016-06-24 01:56:06 +03:00
"""
Docker container implementation .
2015-09-08 11:29:30 +03:00
: param name : Docker container name
2016-05-11 20:35:36 +03:00
: param node_id : Node identifier
2015-09-08 11:29:30 +03:00
: param project : Project instance
: param manager : Manager instance
: param image : Docker image
2016-02-29 11:38:30 +02:00
: param console : TCP console port
2016-02-29 22:08:25 +02:00
: param console_type : Console type
2016-02-29 11:38:30 +02:00
: param aux : TCP aux console port
2016-04-07 14:29:11 +03:00
: param console_resolution : Resolution of the VNC display
2016-05-03 17:49:33 +03:00
: param console_http_port : Port to redirect HTTP queries
: param console_http_path : Url part with the path of the web interface
2015-09-08 11:29:30 +03:00
"""
2015-11-09 13:28:00 +02:00
2016-06-24 01:56:06 +03:00
def __init__ ( self , name , node_id , project , manager , image , console = None , aux = None , start_command = None ,
adapters = None , environment = None , console_type = " telnet " , console_resolution = " 1024x768 " ,
console_http_port = 80 , console_http_path = " / " ) :
2016-05-11 20:35:36 +03:00
super ( ) . __init__ ( name , node_id , project , manager , console = console , aux = aux , allocate_aux = True , console_type = console_type )
2015-10-14 19:10:05 +03:00
2016-06-24 01:56:06 +03:00
# force the latest image if no version is specified
2016-05-19 14:09:07 +03:00
if " : " not in image :
image = " {} :latest " . format ( image )
2015-09-08 11:29:30 +03:00
self . _image = image
2015-10-14 19:10:05 +03:00
self . _start_command = start_command
self . _environment = environment
self . _cid = None
2015-06-17 11:36:55 +03:00
self . _ethernet_adapters = [ ]
self . _temporary_directory = None
2016-03-01 15:53:43 +02:00
self . _telnet_servers = [ ]
2016-04-06 15:57:52 +03:00
self . _x11vnc_process = None
2016-04-07 14:29:11 +03:00
self . _console_resolution = console_resolution
2016-05-03 17:49:33 +03:00
self . _console_http_path = console_http_path
self . _console_http_port = console_http_port
2016-05-10 13:14:48 +03:00
self . _console_websocket = None
2016-05-31 22:08:41 +03:00
self . _volumes = [ ]
2015-10-14 19:10:05 +03:00
if adapters is None :
self . adapters = 1
else :
self . adapters = adapters
2015-09-08 11:29:30 +03:00
2016-06-24 01:56:06 +03:00
log . debug ( " {module} : {name} [ {image} ] initialized. " . format ( module = self . manager . module_name ,
name = self . name ,
image = self . _image ) )
2015-09-08 11:29:30 +03:00
def __json__ ( self ) :
return {
" name " : self . _name ,
2016-05-11 20:35:36 +03:00
" node_id " : self . _id ,
2015-10-14 19:10:05 +03:00
" container_id " : self . _cid ,
2015-09-08 11:29:30 +03:00
" project_id " : self . _project . id ,
" image " : self . _image ,
2015-10-14 19:10:05 +03:00
" adapters " : self . adapters ,
" console " : self . console ,
2016-02-29 22:08:25 +02:00
" console_type " : self . console_type ,
2016-04-07 14:29:11 +03:00
" console_resolution " : self . console_resolution ,
2016-05-03 17:49:33 +03:00
" console_http_port " : self . console_http_port ,
" console_http_path " : self . console_http_path ,
2016-02-29 11:38:30 +02:00
" aux " : self . aux ,
2015-10-14 19:10:05 +03:00
" start_command " : self . start_command ,
2016-05-17 20:51:06 +03:00
" status " : self . status ,
2016-02-12 12:57:56 +02:00
" environment " : self . environment ,
2016-05-12 11:39:50 +03:00
" node_directory " : self . working_dir
2015-09-08 11:29:30 +03:00
}
2016-02-29 22:08:25 +02: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
2015-06-17 11:36:55 +03:00
@property
2015-10-14 19:10:05 +03:00
def start_command ( self ) :
return self . _start_command
@start_command.setter
def start_command ( self , command ) :
2016-05-31 17:32:12 +03:00
if command :
command = command . strip ( )
if command is None or len ( command ) == 0 :
2016-02-19 18:01:28 +02:00
self . _start_command = None
else :
self . _start_command = command
2015-10-14 19:10:05 +03:00
2016-04-07 14:29:11 +03: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 17:49:33 +03: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 19:10:05 +03:00
@property
def environment ( self ) :
return self . _environment
@environment.setter
def environment ( self , command ) :
self . _environment = command
2015-06-17 11:36:55 +03:00
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def _get_container_state ( self ) :
""" Returns the container state (e.g. running, paused etc.)
: returns : state
: rtype : str
"""
2016-10-14 20:00:25 +03:00
try :
result = yield from self . manager . query ( " GET " , " containers/ {} /json " . format ( self . _cid ) )
except DockerError :
return " exited "
2015-10-14 19:10:05 +03:00
if result [ " State " ] [ " Paused " ] :
return " paused "
if result [ " State " ] [ " Running " ] :
return " running "
return " exited "
2015-09-08 11:29:30 +03:00
2016-02-12 12:57:56 +02:00
@asyncio.coroutine
2016-05-14 04:28:53 +03:00
def _get_image_information ( self ) :
2016-02-12 12:57:56 +02:00
"""
2016-05-14 04:28:53 +03:00
: returns : Dictionary information about the container image
2016-02-12 12:57:56 +02:00
"""
result = yield from self . manager . query ( " GET " , " images/ {} /json " . format ( self . _image ) )
return result
def _mount_binds ( self , image_infos ) :
"""
: returns : Return the path that we need to map to local folders
"""
2016-10-04 22:14:19 +03:00
ressources = get_resource ( " compute/docker/resources " )
if not os . path . exists ( ressources ) :
raise DockerError ( " {} is missing can ' t start Docker containers " . format ( ressources ) )
binds = [ " {} :/gns3:ro " . format ( ressources ) ]
2016-03-01 19:38:03 +02:00
2016-03-24 18:08:16 +02:00
# We mount our own etc/network
network_config = self . _create_network_config ( )
2016-06-06 13:39:29 +03:00
binds . append ( " {} :/gns3volumes/etc/network:rw " . format ( network_config ) )
2016-03-24 18:08:16 +02:00
2016-05-31 22:08:41 +03:00
self . _volumes = [ " /etc/network " ]
2016-02-12 17:25:43 +02:00
volumes = image_infos . get ( " ContainerConfig " , { } ) . get ( " Volumes " )
if volumes is None :
return binds
for volume in volumes . keys ( ) :
2016-02-12 12:57:56 +02:00
source = os . path . join ( self . working_dir , os . path . relpath ( volume , " / " ) )
os . makedirs ( source , exist_ok = True )
2016-06-06 13:39:29 +03:00
binds . append ( " {} :/gns3volumes {} " . format ( source , volume ) )
2016-05-31 22:08:41 +03:00
self . _volumes . append ( volume )
2016-03-01 19:38:03 +02:00
2016-02-12 12:57:56 +02:00
return binds
2016-03-24 18:08:16 +02: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 )
2016-03-24 19:35:15 +02: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 11:29:10 +03: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 )
2016-03-24 18:08:16 +02:00
if not os . path . exists ( os . path . join ( path , " interfaces " ) ) :
with open ( os . path . join ( path , " interfaces " ) , " w+ " ) as f :
f . write ( """ #
# This is a sample network config uncomment lines to configure the network
#
""" )
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}
# auto eth{adapter}
# iface eth{adapter} inet dhcp""".format(adapter=adapter))
return path
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def create ( self ) :
""" Creates the Docker container. """
2016-02-12 12:57:56 +02:00
2016-02-24 18:08:28 +02:00
try :
2016-05-14 04:28:53 +03:00
image_infos = yield from self . _get_image_information ( )
2016-02-24 18:08:28 +02:00
except DockerHttp404Error :
log . info ( " Image %s is missing pulling it from docker hub " , self . _image )
yield from self . pull_image ( self . _image )
2016-05-14 04:28:53 +03:00
image_infos = yield from self . _get_image_information ( )
2016-12-20 10:15:28 +02:00
if image_infos is None :
raise DockerError ( " Can ' t get image informations, please try again. " )
2016-02-12 12:57:56 +02:00
2015-06-17 11:36:55 +03:00
params = {
2016-02-23 20:22:35 +02:00
" Hostname " : self . _name ,
2015-10-14 19:10:05 +03:00
" Name " : self . _name ,
" Image " : self . _image ,
" NetworkDisabled " : True ,
" Tty " : True ,
" OpenStdin " : True ,
" StdinOnce " : False ,
" HostConfig " : {
" CapAdd " : [ " ALL " ] ,
2016-02-12 12:57:56 +02:00
" Privileged " : True ,
" Binds " : self . _mount_binds ( image_infos )
} ,
2016-02-29 22:08:25 +02:00
" Volumes " : { } ,
2016-06-15 11:12:32 +03:00
" Env " : [ " container=docker " ] , # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
2016-03-03 10:12:36 +02:00
" Cmd " : [ ] ,
" Entrypoint " : image_infos . get ( " Config " , { " Entrypoint " : [ ] } ) [ " Entrypoint " ]
2015-06-17 11:36:55 +03:00
}
2016-03-01 19:38:03 +02:00
2016-03-03 10:12:36 +02:00
if params [ " Entrypoint " ] is None :
params [ " Entrypoint " ] = [ ]
2015-10-14 19:10:05 +03:00
if self . _start_command :
2016-03-03 10:12:36 +02:00
params [ " Cmd " ] = shlex . split ( self . _start_command )
if len ( params [ " Cmd " ] ) == 0 :
params [ " Cmd " ] = image_infos . get ( " Config " , { " Cmd " : [ ] } ) [ " Cmd " ]
if params [ " Cmd " ] is None :
params [ " Cmd " ] = [ ]
if len ( params [ " Cmd " ] ) == 0 and len ( params [ " Entrypoint " ] ) == 0 :
params [ " Cmd " ] = [ " /bin/sh " ]
2016-06-25 02:26:40 +03:00
params [ " Entrypoint " ] . insert ( 0 , " /gns3/init.sh " ) # FIXME /gns3/init.sh is not found?
2015-10-14 19:10:05 +03:00
2016-05-06 20:07:18 +03: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 12:39:42 +03: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 20:07:18 +03:00
2015-10-14 19:10:05 +03:00
if self . _environment :
2016-02-29 22:08:25 +02:00
params [ " Env " ] + = [ e . strip ( ) for e in self . _environment . split ( " \n " ) ]
if self . _console_type == " vnc " :
yield from self . _start_vnc ( )
2016-06-15 12:01:03 +03:00
params [ " Env " ] . append ( " QT_GRAPHICSSYSTEM=native " ) # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556
2016-02-29 22:08:25 +02:00
params [ " Env " ] . append ( " DISPLAY=: {} " . format ( self . _display ) )
params [ " HostConfig " ] [ " Binds " ] . append ( " /tmp/.X11-unix/:/tmp/.X11-unix/ " )
2015-06-17 11:36:55 +03:00
2015-10-14 19:10:05 +03:00
result = yield from self . manager . query ( " POST " , " containers/create " , data = params )
2015-06-17 11:36:55 +03:00
self . _cid = result [ ' Id ' ]
2015-09-08 11:29:30 +03:00
log . info ( " Docker container ' {name} ' [ {id} ] created " . format (
name = self . _name , id = self . _id ) )
return True
2015-06-17 11:36:55 +03:00
@asyncio.coroutine
2015-10-14 19:10:05 +03:00
def update ( self ) :
"""
Destroy an recreate the container with the new settings
"""
2016-02-24 16:47:53 +02:00
# We need to save the console and state and restore it
2016-02-12 17:38:16 +02:00
console = self . console
2016-04-05 17:33:40 +03:00
aux = self . aux
2016-02-24 16:47:53 +02:00
state = yield from self . _get_container_state ( )
2016-06-13 22:20:29 +03:00
yield from self . reset ( )
2015-10-14 19:10:05 +03:00
yield from self . create ( )
2016-02-12 17:38:16 +02:00
self . console = console
2016-04-05 17:33:40 +03:00
self . aux = aux
2016-02-24 16:47:53 +02:00
if state == " running " :
yield from self . start ( )
2015-06-17 11:36:55 +03:00
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def start ( self ) :
""" Starts this Docker container. """
2015-06-17 11:36:55 +03:00
2015-09-08 11:29:30 +03:00
state = yield from self . _get_container_state ( )
if state == " paused " :
2015-06-17 11:36:55 +03:00
yield from self . unpause ( )
2016-12-16 17:19:23 +02:00
elif state == " running " :
return
2015-09-08 11:29:30 +03:00
else :
2016-05-10 12:38:50 +03:00
yield from self . _clean_servers ( )
2016-05-14 04:28:53 +03:00
yield from self . manager . query ( " POST " , " containers/ {} /start " . format ( self . _cid ) )
2016-12-14 17:53:20 +02:00
self . _namespace = yield from self . _get_namespace ( )
2016-02-11 16:49:28 +02:00
2015-10-14 19:10:05 +03:00
yield from self . _start_ubridge ( )
2016-02-11 16:49:28 +02:00
2015-10-14 19:10:05 +03:00
for adapter_number in range ( 0 , self . adapters ) :
nio = self . _ethernet_adapters [ adapter_number ] . get_nio ( 0 )
2016-02-09 15:22:37 +02:00
with ( yield from self . manager . ubridge_lock ) :
2016-02-11 16:49:28 +02:00
try :
2016-12-14 17:53:20 +02:00
yield from self . _add_ubridge_connection ( nio , adapter_number )
2016-02-11 16:49:28 +02:00
except UbridgeNamespaceError :
2017-04-10 17:58:00 +03:00
log . error ( " Container {} failed to start " , self . name )
2016-02-11 16:49:28 +02:00
yield from self . stop ( )
2016-05-31 07:07:37 +03:00
# The container can crash soon after the start, this means we can not move the interface to the container namespace
2016-02-11 16:49:28 +02:00
logdata = yield from self . _get_log ( )
for line in logdata . split ( ' \n ' ) :
log . error ( line )
raise DockerError ( logdata )
2015-06-17 11:36:55 +03:00
2016-02-29 22:08:25 +02:00
if self . console_type == " telnet " :
yield from self . _start_console ( )
2016-05-03 17:49:33 +03:00
elif self . console_type == " http " or self . console_type == " https " :
yield from self . _start_http ( )
2015-10-14 19:10:05 +03:00
2016-03-01 15:53:43 +02:00
if self . allocate_aux :
yield from self . _start_aux ( )
2015-10-14 19:10:05 +03:00
self . status = " started "
2016-05-31 07:07:37 +03: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 15:53:43 +02:00
@asyncio.coroutine
def _start_aux ( self ) :
"""
Start an auxilary console
"""
# We can not use the API because docker doesn't expose a websocket api for exec
2016-05-18 12:23:45 +03:00
# https://github.com/GNS3/gns3-gui/issues/1039
2016-03-01 15:53:43 +02:00
process = yield from asyncio . subprocess . create_subprocess_exec (
2016-06-15 11:21:30 +03:00
" docker " , " exec " , " -i " , self . _cid , " /gns3/bin/busybox " , " script " , " -qfc " , " while true; do /gns3/bin/busybox sh; done " , " /dev/null " ,
2016-03-01 15:53:43 +02:00
stdout = asyncio . subprocess . PIPE ,
stderr = asyncio . subprocess . STDOUT ,
stdin = asyncio . subprocess . PIPE )
2016-05-03 12:33:43 +03:00
server = AsyncioTelnetServer ( reader = process . stdout , writer = process . stdin , binary = True , echo = True )
2016-03-01 15:53:43 +02:00
self . _telnet_servers . append ( ( yield from asyncio . start_server ( server . run , self . _manager . port_manager . console_host , self . aux ) ) )
log . debug ( " Docker container ' %s ' started listen for auxilary telnet on %d " , self . name , self . aux )
2015-10-14 19:10:05 +03:00
2016-05-31 22:08:41 +03:00
@asyncio.coroutine
def _fix_permissions ( self ) :
"""
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
"""
for volume in self . _volumes :
log . debug ( " Docker container ' {name} ' [ {image} ] fix ownership on {path} " . format (
name = self . _name , image = self . _image , path = volume ) )
2016-06-24 01:56:06 +03:00
process = yield from asyncio . subprocess . create_subprocess_exec ( " 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 ) )
2016-05-31 22:08:41 +03:00
yield from process . wait ( )
2016-02-29 22:08:25 +02:00
@asyncio.coroutine
def _start_vnc ( self ) :
"""
Start a VNC server for this container
"""
self . _display = self . _get_free_display_port ( )
if shutil . which ( " Xvfb " ) is None or shutil . which ( " x11vnc " ) is None :
raise DockerError ( " Please install Xvfb and x11vnc before using the VNC support " )
2016-04-07 14:29:11 +03:00
self . _xvfb_process = yield from asyncio . create_subprocess_exec ( " Xvfb " , " -nolisten " , " tcp " , " : {} " . format ( self . _display ) , " -screen " , " 0 " , self . _console_resolution + " x16 " )
2016-06-13 21:12:42 +03:00
# We pass a port for TCPV6 due to a crash in X11VNC if not here: https://github.com/GNS3/gns3-server/issues/569
self . _x11vnc_process = yield from asyncio . create_subprocess_exec ( " x11vnc " , " -forever " , " -nopw " , " -shared " , " -geometry " , self . _console_resolution , " -display " , " WAIT: {} " . format ( self . _display ) , " -rfbport " , str ( self . console ) , " -rfbportv6 " , str ( self . console ) , " -noncache " , " -listen " , self . _manager . port_manager . console_host )
2016-02-29 22:08:25 +02:00
x11_socket = os . path . join ( " /tmp/.X11-unix/ " , " X {} " . format ( self . _display ) )
yield from wait_for_file_creation ( x11_socket )
2016-05-03 17:49:33 +03:00
@asyncio.coroutine
def _start_http ( self ) :
"""
2016-12-09 17:41:15 +02:00
Start an HTTP tunnel to container localhost . It ' s not perfect
but the only way we have to inject network packet is using nc .
2016-05-03 17:49:33 +03: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 17:41:15 +02:00
# We replace host and port in the server answer otherwise some link could be broken
2016-05-03 19:01:23 +03:00
server = AsyncioRawCommandServer ( command , replaces = [
(
2016-12-09 17:41:15 +02: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 19:01:23 +03:00
)
] )
2016-05-03 17:49:33 +03:00
self . _telnet_servers . append ( ( yield from asyncio . start_server ( server . run , self . _manager . port_manager . console_host , self . console ) ) )
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
def _start_console ( self ) :
"""
Start streaming the console via telnet
"""
2016-05-03 17:49:33 +03:00
2015-10-14 19:10:05 +03:00
class InputStream :
def __init__ ( self ) :
self . _data = b " "
def write ( self , data ) :
self . _data + = data
@asyncio.coroutine
def drain ( self ) :
if not self . ws . closed :
self . ws . send_bytes ( self . _data )
self . _data = b " "
output_stream = asyncio . StreamReader ( )
input_stream = InputStream ( )
2016-03-01 15:53:43 +02:00
telnet = AsyncioTelnetServer ( reader = output_stream , writer = input_stream , echo = True )
self . _telnet_servers . append ( ( yield from asyncio . start_server ( telnet . run , self . _manager . port_manager . console_host , self . console ) ) )
2015-10-14 19:10:05 +03:00
2016-05-10 13:14:48 +03:00
self . _console_websocket = yield from self . manager . websocket_query ( " containers/ {} /attach/ws?stream=1&stdin=1&stdout=1&stderr=1 " . format ( self . _cid ) )
input_stream . ws = self . _console_websocket
2015-10-14 19:10:05 +03:00
output_stream . feed_data ( self . name . encode ( ) + b " console is now available... Press RETURN to get started. \r \n " )
2016-05-10 13:14:48 +03:00
asyncio . async ( self . _read_console_output ( self . _console_websocket , output_stream ) )
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
def _read_console_output ( self , ws , out ) :
"""
2016-05-14 04:28:53 +03:00
Read Websocket and forward it to the telnet
: param ws : Websocket connection
2015-10-14 19:10:05 +03:00
: param out : Output stream
"""
while True :
msg = yield from ws . receive ( )
if msg . tp == aiohttp . MsgType . text :
out . feed_data ( msg . data . encode ( ) )
else :
out . feed_eof ( )
ws . close ( )
break
2016-06-15 11:32:38 +03:00
yield from self . stop ( )
2015-09-08 11:29:30 +03:00
2016-06-15 11:32:38 +03:00
@asyncio.coroutine
2015-09-08 11:29:30 +03:00
def is_running ( self ) :
""" Checks if the container is running.
: returns : True or False
: rtype : bool
"""
2015-06-17 11:36:55 +03:00
state = yield from self . _get_container_state ( )
2015-09-08 11:29:30 +03:00
if state == " running " :
return True
2017-04-10 17:58:00 +03:00
if self . status == " started " : # The container crashed we need to clean
yield from self . stop ( )
2015-09-08 11:29:30 +03:00
return False
@asyncio.coroutine
def restart ( self ) :
2015-10-14 19:10:05 +03:00
""" Restart this Docker container. """
yield from self . manager . query ( " POST " , " containers/ {} /restart " . format ( self . _cid ) )
2015-09-08 11:29:30 +03:00
log . info ( " Docker container ' {name} ' [ {image} ] restarted " . format (
name = self . _name , image = self . _image ) )
2016-05-10 12:38:50 +03:00
@asyncio.coroutine
def _clean_servers ( self ) :
"""
Clean the list of running console servers
"""
if len ( self . _telnet_servers ) > 0 :
for telnet_server in self . _telnet_servers :
telnet_server . close ( )
yield from telnet_server . wait_closed ( )
self . _telnet_servers = [ ]
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def stop ( self ) :
""" Stops this Docker container. """
2015-06-17 11:36:55 +03:00
2015-12-29 13:40:22 +02:00
try :
2016-05-10 12:38:50 +03:00
yield from self . _clean_servers ( )
2016-05-31 07:07:37 +03:00
yield from self . _stop_ubridge ( )
2015-12-29 13:40:22 +02:00
2016-09-07 15:24:56 +03:00
try :
state = yield from self . _get_container_state ( )
except DockerHttp404Error :
state = " stopped "
2015-12-29 13:40:22 +02:00
if state == " paused " :
yield from self . unpause ( )
2016-06-15 11:32:38 +03:00
if state != " stopped " :
yield from self . _fix_permissions ( )
# t=5 number of seconds to wait before killing the container
try :
yield from self . manager . query ( " POST " , " containers/ {} /stop " . format ( self . _cid ) , params = { " t " : 5 } )
log . info ( " Docker container ' {name} ' [ {image} ] stopped " . format (
name = self . _name , image = self . _image ) )
except DockerHttp304Error :
# Container is already stopped
pass
2015-12-29 13:40:22 +02: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 11:32:38 +03:00
self . status = " stopped "
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def pause ( self ) :
""" Pauses this Docker container. """
2015-10-14 19:10:05 +03:00
yield from self . manager . query ( " POST " , " containers/ {} /pause " . format ( self . _cid ) )
2016-05-14 05:41:58 +03:00
self . status = " suspended "
log . info ( " Docker container ' {name} ' [ {image} ] paused " . format ( name = self . _name , image = self . _image ) )
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
def unpause ( self ) :
""" Unpauses this Docker container. """
2015-10-14 19:10:05 +03:00
yield from self . manager . query ( " POST " , " containers/ {} /unpause " . format ( self . _cid ) )
self . status = " started "
2016-05-14 05:41:58 +03:00
log . info ( " Docker container ' {name} ' [ {image} ] unpaused " . format ( name = self . _name , image = self . _image ) )
2015-09-08 11:29:30 +03:00
@asyncio.coroutine
2016-02-29 22:08:25 +02:00
def close ( self ) :
""" Closes this Docker container. """
if not ( yield from super ( ) . close ( ) ) :
return False
2016-06-13 22:20:29 +03:00
yield from self . reset ( )
2015-06-17 11:36:55 +03:00
2016-06-13 22:20:29 +03:00
@asyncio.coroutine
def reset ( self ) :
2015-12-29 13:40:22 +02:00
try :
2016-09-01 10:28:22 +03:00
state = yield from self . _get_container_state ( )
if state == " paused " or state == " running " :
yield from self . stop ( )
2016-02-29 22:08:25 +02:00
if self . console_type == " vnc " :
2016-04-06 15:57:52 +03:00
if self . _x11vnc_process :
2016-05-19 14:23:53 +03:00
try :
self . _x11vnc_process . terminate ( )
yield from self . _x11vnc_process . wait ( )
except ProcessLookupError :
pass
try :
self . _xvfb_process . terminate ( )
yield from self . _xvfb_process . wait ( )
except ProcessLookupError :
pass
2016-06-13 18:39:04 +03: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 20:06:12 +03:00
try :
yield from self . manager . query ( " DELETE " , " containers/ {} " . format ( self . _cid ) , params = { " force " : 1 , " v " : 1 } )
except DockerError :
pass
2015-12-29 13:40:22 +02:00
log . info ( " Docker container ' {name} ' [ {image} ] removed " . format (
name = self . _name , image = self . _image ) )
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 )
# Ignore runtime error because when closing the server
2016-02-12 17:48:19 +02:00
except ( DockerHttp404Error , RuntimeError ) as e :
log . debug ( " Docker error when closing: {} " . format ( str ( e ) ) )
2015-12-29 13:40:22 +02:00
return
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
2016-12-14 17:53:20 +02:00
def _add_ubridge_connection ( self , nio , adapter_number ) :
2015-06-17 11:36:55 +03:00
"""
Creates a connection in uBridge .
2016-06-24 01:56:06 +03: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 11:36:55 +03:00
: param adapter_number : adapter number
"""
2016-06-24 01:56:06 +03:00
2015-06-17 11:36:55 +03:00
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 07:07:37 +03:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker container ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 11:36:55 +03:00
2016-06-27 06:50:08 +03:00
for index in range ( 4096 ) :
2016-12-13 17:05:38 +02:00
if " tap-gns3-e {} " . format ( index ) not in psutil . net_if_addrs ( ) :
2016-10-24 13:35:50 +03:00
adapter . host_ifc = " tap-gns3-e {} " . format ( str ( index ) )
2016-02-09 15:22:37 +02:00
break
2016-12-13 17:05:38 +02:00
if adapter . host_ifc is None :
2016-05-31 07:07:37 +03: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 ) )
2015-06-17 11:36:55 +03:00
2016-10-24 13:35:50 +03:00
yield from self . _ubridge_send ( ' bridge create bridge {} ' . format ( adapter_number ) )
yield from self . _ubridge_send ( ' bridge add_nio_tap bridge {adapter_number} {hostif} ' . format ( adapter_number = adapter_number ,
hostif = adapter . host_ifc ) )
2016-12-14 17:53:20 +02:00
log . debug ( " Move container %s adapter %s to namespace %s " , self . name , adapter . host_ifc , self . _namespace )
2016-02-11 16:49:28 +02:00
try :
2016-10-24 13:35:50 +03:00
yield from self . _ubridge_send ( ' docker move_to_ns {ifc} {ns} eth {adapter} ' . format ( ifc = adapter . host_ifc ,
2016-12-14 17:53:20 +02:00
ns = self . _namespace ,
2016-05-31 07:07:37 +03:00
adapter = adapter_number ) )
2016-02-11 16:49:28 +02:00
except UbridgeError as e :
raise UbridgeNamespaceError ( e )
2015-06-17 11:36:55 +03:00
2016-10-24 13:35:50 +03:00
if nio :
2016-12-14 17:53:20 +02:00
yield from self . _connect_nio ( adapter_number , nio )
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
def _get_namespace ( self ) :
result = yield from self . manager . query ( " GET " , " containers/ {} /json " . format ( self . _cid ) )
return int ( result [ ' State ' ] [ ' Pid ' ] )
2015-06-17 11:36:55 +03:00
2016-12-14 17:53:20 +02:00
@asyncio.coroutine
def _connect_nio ( self , adapter_number , nio ) :
yield from self . _ubridge_send ( ' bridge add_nio_udp bridge {adapter} {lport} {rhost} {rport} ' . format ( adapter = adapter_number ,
lport = nio . lport ,
rhost = nio . rhost ,
rport = nio . rport ) )
if nio . capturing :
yield from self . _ubridge_send ( ' bridge start_capture bridge {adapter} " {pcap_file} " ' . format ( adapter = adapter_number ,
pcap_file = nio . pcap_output_file ) )
yield from self . _ubridge_send ( ' bridge start bridge {adapter} ' . format ( adapter = adapter_number ) )
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
2015-06-17 11:36:55 +03:00
def adapter_add_nio_binding ( self , adapter_number , nio ) :
""" Adds an adapter NIO binding.
: param adapter_number : adapter number
: param nio : NIO instance to add to the slot / port
"""
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 07:07:37 +03:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker container ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 11:36:55 +03:00
2016-12-14 13:01:34 +02:00
if self . status == " started " and self . ubridge :
2016-12-14 17:53:20 +02:00
yield from self . _connect_nio ( adapter_number , nio )
2016-06-25 02:26:40 +03:00
2015-06-17 11:36:55 +03:00
adapter . add_nio ( 0 , nio )
2016-05-31 07:07:37 +03: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 11:36:55 +03:00
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
2015-06-17 11:36:55 +03:00
def adapter_remove_nio_binding ( self , adapter_number ) :
"""
Removes an adapter NIO binding .
: param adapter_number : adapter number
: returns : NIO instance
"""
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
2016-05-31 07:07:37 +03:00
raise DockerError ( " Adapter {adapter_number} doesn ' t exist on Docker VM ' {name} ' " . format ( name = self . name ,
adapter_number = adapter_number ) )
2015-06-17 11:36:55 +03:00
2016-12-14 17:53:20 +02:00
if self . ubridge :
nio = adapter . get_nio ( 0 )
yield from self . _ubridge_send ( " bridge stop bridge {name} " . format ( name = adapter_number ) )
yield from self . _ubridge_send ( ' bridge remove_nio_udp bridge {adapter} {lport} {rhost} {rport} ' . format ( adapter = adapter_number ,
lport = nio . lport ,
rhost = nio . rhost ,
rport = nio . rport ) )
2015-06-17 11:36:55 +03:00
adapter . remove_nio ( 0 )
2016-05-31 07:07:37 +03: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 11:36:55 +03:00
@property
def adapters ( self ) :
""" Returns the number of Ethernet adapters for this Docker VM.
: returns : number of adapters
: rtype : int
"""
return len ( self . _ethernet_adapters )
@adapters.setter
def adapters ( self , adapters ) :
""" Sets the number of Ethernet adapters for this Docker container.
: param adapters : number of adapters
"""
2016-04-25 17:09:04 +03:00
if len ( self . _ethernet_adapters ) == adapters :
return
2015-06-17 11:36:55 +03:00
self . _ethernet_adapters . clear ( )
for adapter_number in range ( 0 , adapters ) :
self . _ethernet_adapters . append ( EthernetAdapter ( ) )
2016-05-31 07:07:37 +03: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 11:36:55 +03:00
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
def pull_image ( self , image ) :
"""
Pull image from docker repository
"""
2017-03-27 21:46:25 +03:00
def callback ( msg ) :
self . project . emit ( " log.info " , { " message " : msg } )
yield from self . manager . pull_image ( image , progress_callback = callback )
2016-02-09 17:07:33 +02:00
@asyncio.coroutine
def _start_ubridge_capture ( self , adapter_number , output_file ) :
"""
Start a packet capture in uBridge .
: param adapter_number : adapter number
: param output_file : PCAP destination file for the capture
"""
adapter = " bridge {} " . format ( adapter_number )
2016-12-14 13:01:34 +02:00
if not self . ubridge :
2016-05-11 20:35:36 +03:00
raise DockerError ( " Cannot start the packet capture: uBridge is not running " )
2016-05-31 07:07:37 +03:00
yield from self . _ubridge_send ( ' bridge start_capture {name} " {output_file} " ' . format ( name = adapter , output_file = output_file ) )
2016-02-09 17:07:33 +02:00
@asyncio.coroutine
def _stop_ubridge_capture ( self , adapter_number ) :
"""
Stop a packet capture in uBridge .
: param adapter_number : adapter number
"""
adapter = " bridge {} " . format ( adapter_number )
2016-12-14 13:01:34 +02:00
if not self . ubridge :
2016-05-11 20:35:36 +03:00
raise DockerError ( " Cannot stop the packet capture: uBridge is not running " )
2016-05-31 07:07:37 +03:00
yield from self . _ubridge_send ( " bridge stop_capture {name} " . format ( name = adapter ) )
2016-02-09 17:07:33 +02:00
@asyncio.coroutine
def start_capture ( self , adapter_number , output_file ) :
"""
Starts a packet capture .
: param adapter_number : adapter number
: param output_file : PCAP destination file for the capture
"""
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 ) )
if nio . capturing :
raise DockerError ( " Packet capture is already activated on adapter {adapter_number} " . format ( adapter_number = adapter_number ) )
nio . startPacketCapture ( output_file )
2016-12-14 13:01:34 +02:00
if self . status == " started " and self . ubridge :
2016-02-09 17:07:33 +02:00
yield from self . _start_ubridge_capture ( adapter_number , output_file )
log . info ( " Docker VM ' {name} ' [ {id} ]: starting packet capture on adapter {adapter_number} " . format ( name = self . name ,
id = self . id ,
adapter_number = adapter_number ) )
def stop_capture ( self , adapter_number ) :
"""
Stops a packet capture .
: param adapter_number : adapter number
"""
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 ) )
nio . stopPacketCapture ( )
2016-12-14 13:01:34 +02:00
if self . status == " started " and self . ubridge :
2016-02-09 17:07:33 +02:00
yield from self . _stop_ubridge_capture ( adapter_number )
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 16:49:28 +02:00
@asyncio.coroutine
def _get_log ( self ) :
"""
Return the log from the container
: returns : string
"""
result = yield from self . manager . query ( " GET " , " containers/ {} /logs " . format ( self . _cid ) , params = { " stderr " : 1 , " stdout " : 1 } )
return result
2016-02-29 22:08:25 +02:00
@asyncio.coroutine
def delete ( self ) :
"""
Delete the VM ( including all its files ) .
"""
yield from self . close ( )
yield from super ( ) . delete ( )