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
import json
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
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 ) :
2015-09-08 11:29:30 +03:00
""" Docker container implementation.
: 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-05-11 20:35:36 +03:00
def __init__ ( self , name , node_id , project , manager , image ,
2016-02-29 22:08:25 +02:00
console = None , aux = None , start_command = None ,
2016-04-07 14:29:11 +03:00
adapters = None , environment = None , console_type = " telnet " ,
2016-05-03 17:49:33 +03:00
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
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 . _ubridge_hypervisor = None
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
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
log . debug (
" {module} : {name} [ {image} ] initialized. " . format (
module = self . manager . module_name ,
name = self . name ,
image = self . _image ) )
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-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-02-19 18:01:28 +02:00
command = command . strip ( )
if len ( command ) == 0 :
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
"""
2015-10-14 19:10:05 +03:00
result = yield from self . manager . query ( " GET " , " containers/ {} /json " . format ( self . _cid ) )
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-05-16 19:32:29 +03:00
binds = [ " {} :/gns3:ro " . format ( get_resource ( " compute/docker/resources " ) ) ]
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 ( )
binds . append ( " {} :/etc/network:rw " . format ( network_config ) )
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 )
binds . append ( " {} : {} " . format ( source , 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-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-03-01 19:38:03 +02:00
" Env " : [ ] ,
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 " ]
params [ " Entrypoint " ] . insert ( 0 , " /gns3/init.sh " )
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 ) )
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 ( )
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-02-29 22:08:25 +02:00
yield from self . close ( )
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 ( )
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-02-11 16:49:28 +02:00
namespace = yield from self . _get_namespace ( )
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 :
yield from self . _add_ubridge_connection ( nio , adapter_number , namespace )
except UbridgeNamespaceError :
yield from self . stop ( )
# The container can crash soon after the start this mean we can not move the interface to the container namespace
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-03-01 15:53:43 +02: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 ) )
@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
# https://github.com/GNS3/gns3-gui/issues/1039
process = yield from asyncio . subprocess . create_subprocess_exec (
2016-05-03 12:33:43 +03:00
" docker " , " exec " , " -i " , self . _cid , " /gns3/bin/busybox " , " script " , " -qfc " , " /gns3/bin/busybox sh " , " /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-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-04-07 15:24:53 +03:00
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 ) , " -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 ) :
"""
Start an HTTP tunnel to container localhost
"""
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-05-14 04:28:53 +03:00
# We replace the port in the server answer otherwise some link could be broken
2016-05-03 19:01:23 +03:00
server = AsyncioRawCommandServer ( command , replaces = [
(
2016-05-03 22:07:01 +03:00
' {} ' . 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
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
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-02-19 18:53:17 +02:00
2015-12-29 13:40:22 +02:00
if self . _ubridge_hypervisor and self . _ubridge_hypervisor . is_running ( ) :
yield from self . _ubridge_hypervisor . stop ( )
state = yield from self . _get_container_state ( )
if state == " paused " :
yield from self . unpause ( )
# t=5 number of seconds to wait before killing the container
2016-02-11 16:49:28 +02:00
try :
yield from self . manager . query ( " POST " , " containers/ {} /stop " . format ( self . _cid ) , params = { " t " : 5 } )
2016-05-14 05:41:58 +03:00
log . info ( " Docker container ' {name} ' [ {image} ] stopped " . format ( name = self . _name , image = self . _image ) )
2016-02-11 16:49:28 +02:00
except DockerHttp304Error :
# Container is already stopped
pass
2016-05-14 05:41:58 +03:00
finally :
self . status = " stopped "
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
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
2015-06-17 11:36:55 +03:00
2015-12-29 13:40:22 +02:00
try :
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 :
self . _x11vnc_process . terminate ( )
self . _xvfb_process . terminate ( )
yield from self . _x11vnc_process . wait ( )
yield from self . _xvfb_process . wait ( )
2016-02-29 22:08:25 +02:00
2015-12-29 13:40:22 +02:00
state = yield from self . _get_container_state ( )
2016-02-29 22:08:25 +02:00
if state == " paused " or state == " running " :
2015-12-29 13:40:22 +02:00
yield from self . stop ( )
yield from self . manager . query ( " DELETE " , " containers/ {} " . format ( self . _cid ) , params = { " force " : 1 } )
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-02-11 16:49:28 +02:00
def _add_ubridge_connection ( self , nio , adapter_number , namespace ) :
2015-06-17 11:36:55 +03:00
"""
Creates a connection in uBridge .
2016-02-09 15:22:37 +02:00
: param nio : NIO instance or None if it ' s a dummu 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-02-11 16:49:28 +02:00
: param namespace : Container namespace ( pid )
2015-06-17 11:36:55 +03:00
"""
try :
adapter = self . _ethernet_adapters [ adapter_number ]
except IndexError :
raise DockerError (
2015-10-14 19:10:05 +03:00
" 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-02-09 15:22:37 +02:00
for index in range ( 128 ) :
2016-03-01 16:33:30 +02:00
if " veth-gns3-ext {} " . format ( index ) not in psutil . net_if_addrs ( ) :
2016-02-09 15:22:37 +02:00
adapter . ifc = " eth {} " . format ( str ( index ) )
2016-03-01 16:33:30 +02:00
adapter . host_ifc = " veth-gns3-ext {} " . format ( str ( index ) )
adapter . guest_ifc = " veth-gns3-int {} " . format ( str ( index ) )
2016-02-09 15:22:37 +02:00
break
if not hasattr ( adapter , " ifc " ) :
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
yield from self . _ubridge_hypervisor . send (
' docker create_veth {hostif} {guestif} ' . format (
guestif = adapter . guest_ifc , hostif = adapter . host_ifc ) )
2015-10-14 19:10:05 +03:00
log . debug ( " Move container %s adapter %s to namespace %s " , self . name , adapter . guest_ifc , namespace )
2016-02-11 16:49:28 +02:00
try :
yield from self . _ubridge_hypervisor . send (
' docker move_to_ns {ifc} {ns} eth {adapter} ' . format (
ifc = adapter . guest_ifc , ns = namespace , adapter = adapter_number ) )
except UbridgeError as e :
raise UbridgeNamespaceError ( e )
2015-06-17 11:36:55 +03:00
if isinstance ( nio , NIOUDP ) :
2016-02-09 15:22:37 +02:00
yield from self . _ubridge_hypervisor . send (
' bridge create bridge {} ' . format ( adapter_number ) )
yield from self . _ubridge_hypervisor . send (
' bridge add_nio_linux_raw bridge {adapter} {ifc} ' . format (
ifc = adapter . host_ifc , adapter = adapter_number ) )
2015-06-17 11:36:55 +03:00
yield from self . _ubridge_hypervisor . send (
' bridge add_nio_udp bridge {adapter} {lport} {rhost} {rport} ' . format (
adapter = adapter_number , lport = nio . lport , rhost = nio . rhost ,
rport = nio . rport ) )
2016-02-09 15:22:37 +02:00
if nio . capturing :
yield from self . _ubridge_hypervisor . send (
' bridge start_capture bridge {adapter} " {pcap_file} " ' . format (
adapter = adapter_number , pcap_file = nio . pcap_output_file ) )
2015-06-17 11:36:55 +03:00
2016-02-09 15:22:37 +02:00
yield from self . _ubridge_hypervisor . send (
' bridge start bridge {adapter} ' . format ( adapter = adapter_number ) )
2015-06-17 11:36:55 +03:00
def _delete_ubridge_connection ( self , adapter_number ) :
""" Deletes a connection in uBridge.
: param adapter_number : adapter number
"""
2016-03-01 21:25:50 +02:00
if not self . _ubridge_hypervisor or not self . _ubridge_hypervisor . is_running ( ) :
2016-02-24 16:04:45 +02:00
return
2015-06-17 11:36:55 +03:00
adapter = self . _ethernet_adapters [ adapter_number ]
2016-03-25 17:27:31 +03:00
2016-04-07 15:33:21 +03:00
try :
yield from self . _ubridge_hypervisor . send ( " bridge delete bridge {name} " . format (
name = adapter_number ) )
except UbridgeError as e :
log . debug ( str ( e ) )
try :
yield from self . _ubridge_hypervisor . send ( ' docker delete_veth {hostif} ' . format ( hostif = adapter . host_ifc ) )
except UbridgeError as e :
log . debug ( str ( e ) )
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
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 :
raise DockerError (
" Adapter {adapter_number} doesn ' t exist on Docker container ' {name} ' " . format (
name = self . name , adapter_number = adapter_number ) )
adapter . add_nio ( 0 , nio )
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-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 :
raise DockerError (
" Adapter {adapter_number} doesn ' t exist on Docker VM ' {name} ' " . format (
name = self . name , adapter_number = adapter_number ) )
adapter . remove_nio ( 0 )
2016-02-24 16:04:45 +02:00
yield from self . _delete_ubridge_connection ( adapter_number )
2015-06-17 11:36:55 +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 ) )
@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 ( ) )
log . info (
' Docker container " {name} " [ {id} ]: number of Ethernet adapters changed to {adapters} ' . format (
name = self . _name ,
id = self . _id ,
adapters = adapters ) )
2015-10-14 19:10:05 +03:00
@asyncio.coroutine
def pull_image ( self , image ) :
"""
Pull image from docker repository
"""
log . info ( " Pull %s from docker hub " , image )
response = yield from self . manager . http_query ( " POST " , " images/create " , params = { " fromImage " : image } )
# The pull api will stream status via an HTTP JSON stream
content = " "
while True :
chunk = yield from response . content . read ( 1024 )
if not chunk :
break
content + = chunk . decode ( " utf-8 " )
try :
while True :
content = content . lstrip ( " \r \n \t " )
answer , index = json . JSONDecoder ( ) . raw_decode ( content )
if " progress " in answer :
self . project . emit ( " log.info " , { " message " : " Pulling image {} : {} : {} " . format ( self . _image , answer [ " id " ] , answer [ " progress " ] ) } )
content = content [ index : ]
except ValueError : # Partial JSON
pass
self . project . emit ( " log.info " , { " message " : " Success pulling image {} " . format ( self . _image ) } )
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-03-01 21:25:50 +02:00
if not self . _ubridge_hypervisor or not self . _ubridge_hypervisor . is_running ( ) :
2016-05-11 20:35:36 +03:00
raise DockerError ( " Cannot start the packet capture: uBridge is not running " )
2016-02-09 17:07:33 +02:00
yield from self . _ubridge_hypervisor . send ( ' bridge start_capture {name} " {output_file} " ' . format ( name = adapter , output_file = output_file ) )
@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-03-01 21:25:50 +02:00
if not self . _ubridge_hypervisor or not self . _ubridge_hypervisor . is_running ( ) :
2016-05-11 20:35:36 +03:00
raise DockerError ( " Cannot stop the packet capture: uBridge is not running " )
2016-02-09 17:07:33 +02:00
yield from self . _ubridge_hypervisor . send ( " bridge stop_capture {name} " . format ( name = adapter ) )
@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 )
if self . status == " started " :
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 ( )
if self . status == " started " :
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 ( )