#!/usr/bin/perl -w
###############################################################################
# $Id: docker.pm 1798488 2017-06-12 16:38:02Z arkurth $
###############################################################################
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

=head1 NAME

VCL::Provisioning::docker - VCL module to support povisioning of docker

=head1 SYNOPSIS

 Needs to be written

=head1 DESCRIPTION

 This module provides...

=cut

##############################################################################
package VCL::Module::Provisioning::docker;

# Specify the lib path using FindBin
use FindBin;
use lib "$FindBin::Bin/../../..";

# Configure inheritance
use base qw(VCL::Module::Provisioning);

# Specify the version of this module
our $VERSION = '2.5';

# Specify the version of Perl to use
use 5.008000;

use strict;
use warnings;
use diagnostics;
use English qw(-no_match_vars);

use VCL::utils;

use LWP::UserAgent;
use JSON qw(from_json to_json encode_json decode_json);

##############################################################################

=head1 OBJECT METHODS

=cut

sub initialize {
	my $self = shift;
	notify($ERRORS{'DEBUG'}, 0, "Docker module initialized");
	return 1;
} ## end sub initialize


#/////////////////////////////////////////////////////////////////////////////

=head2 load

 Parameters  :
 Returns     :
 Description :

=cut

sub load {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	my $image_name = $self->data->get_image_name() || return;
	my $computer_id = $self->data->get_computer_id() || return;
	my $computer_name = $self->data->get_computer_short_name() || return;
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0) || return;
	my $image_os_type = $self->data->get_image_os_type() || return;
 
	# The docker daemon listens on unix:///var/run/docker.sock 
	# but you can Bind Docker to another host/port or a Unix socket
	# set Docker daemon to listen on a specific IP and port
	# set Docker host ip / port (/etc/init/docker.conf in Ubuntu 14.04 )
	# (/usr/lib/systemd/system/docker.service in CentOS 7)
	# e.g.,) docker -H tcp://0.0.0.0:4243 -H unix:///var/run/docker.sock -d &

	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";
	my $nathost_name = $self->data->get_nathost_hostname(0);
	my $vmhost_name = $self->data->get_vmhost_short_name() || return;
	my $using_nat_host = 0;
	if (defined($nathost_name)) {
		notify($ERRORS{'DEBUG'}, 0, "the VM hostname: $vmhost_name for computer: $computer_name is a nat host: $nathost_name");
		notify($ERRORS{'DEBUG'}, 0, "computer: $computer_name will use random ports for nat");
		$using_nat_host = 1;
	}
	# set ssh port of the vm_host (if not 22)
	my $remote_connection_target = determine_remote_connection_target($vmhost_name);
	# set the specific ssh port for vmhost in the variable table. e.g., 24
	notify($ERRORS{'OK'}, 0, "remote_connection_target: $remote_connection_target");
	my $target_ssh_port = get_variable("vmhost_ssh_port") || 22;
	$ENV{ssh_port}{$remote_connection_target} = $target_ssh_port;
	notify($ERRORS{'OK'}, 0, "vmhost_ssh_port: $target_ssh_port");

	# create a useragent
	my $ua = LWP::UserAgent->new();
	# Using docker inspect to check whether there is any container with the same computer name or not
	# if it exists, unload the previous one 
	my $resp = $ua->get(
		$docker_host_url . "/containers/$computer_name/json",
		content_type => 'application/json',
	);
	my $container_exist = 0;
	eval {
		from_json($resp->content);
		$container_exist = 1;
	} or do {
		notify($ERRORS{'DEBUG'}, 0, "the container id for $computer_name does not exist: " . join("\n", $resp->content));
	};

	if ($container_exist) {
		my $output = from_json($resp->content);
		my $container_id = $output->{'Id'};
		notify($ERRORS{'OK'}, 0, "container_id: [$container_id]");
		if (defined($container_id)) {
			notify($ERRORS{'WARNING'}, 0, "the container id for $computer_name already exists");
			if(!$self->unload()) {
				notify($ERRORS{'WARNING'}, 0, "failed to unload the container for $computer_name");
				return 0;
			}
		}
	}
	
	# set the number of CPUs and memory size for the container
	my $cpu_count = $self->data->get_image_minprocnumber() || 1;
	notify($ERRORS{'OK'}, 0, "cpu_count: [$cpu_count]");
	my $cpus = "0";
	if ($cpu_count == 1) {
		$cpus = "0";
	}
	else {
		$cpu_count = $cpu_count - 1;
		$cpus = "0-$cpu_count";	
	}
	my $memory_mb = $self->data->get_image_minram();
	my $memory_bytes = ($memory_mb * 1024 * 1024);
	$memory_bytes = 0 + $memory_bytes;	
	my $eth0_mac_address = $self->data->get_computer_eth0_mac_address();
	notify($ERRORS{'OK'}, 0, "eth0 mac: $eth0_mac_address, cpus: [$cpus], memory_bytes: [$memory_bytes]");
	if (!defined($eth0_mac_address) || !defined($memory_bytes)) {
		notify($ERRORS{'WARNINGS'}, 0, "eth0 mac address:[$eth0_mac_address] OR memory size: [$memory_bytes] is not defined for $computer_name");
		return;
	}
	
	my $docker_default_ssh_port = get_variable("docker_default_ssh_port") || 22; # set docker container default ssh port in variable table, e.g., 22	
	my $docker_default_public_port = get_default_public_port($image_os_type);
	if (!defined($docker_default_public_port)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get default public port for $computer_name");
		return 0;
	}

	my $container_data;
	# json docker create format of the container
	if ($using_nat_host) {
		# get a random ssh port for the container node for public access
		my $random_ssh_port = $self->get_dockerhost_random_port($docker_default_ssh_port);
		# get a random public port for public access of the applications (e.g., xrdp, lxde, rstudio)
		my $random_public_port = $self->get_dockerhost_random_port($docker_default_public_port);
		#my $random_vnc_port = $self->get_dockerhost_random_port($docker_default_vnc_port);
		notify($ERRORS{'DEBUG'}, 0, "default_ssh_port: $docker_default_ssh_port, random_public_port: $docker_default_public_port");
		notify($ERRORS{'DEBUG'}, 0, "random_ssh_port: $random_ssh_port, random_public_port: $random_public_port");
		if (!defined($random_ssh_port) || !defined($random_public_port)) {
			notify($ERRORS{'WARNING'}, 0, "failed to get ssh: $random_ssh_port, public: $random_public_port port for $computer_name");
			return 0;
		}
		
		$container_data = {
			Image => $image_name,
			# MacAddress => $eth0_mac_address, # It could be used for future DHCP configuration. 
			HostConfig => {
				Privileged => JSON::true, # 0/1 and true/false cause GO Marshall converting error 
				#Memory => 0 + $memory_bytes, # add 0 to avoid JSON format error (GO Marshall converting error)
				# CpusetCpus => "$cpus", # This fixed and biased distribution would reduce the CPU utilization. Need more fair distribution methods.
				# Docker uses all the available CPUs equally(Only set if you have better solutions)
				CapAdd => ["NET_ADMIN"], #// allow to use iptables inside a container
				#PublishAllPorts => JSON::true,
				#NetworkMode => 'none' # default NetworkMode is bridge
				PortBindings => {
					"$docker_default_ssh_port\/tcp" => [{HostPort => $random_ssh_port }],
					"$docker_default_public_port\/tcp" => [{HostPort => $random_public_port }],
					# Examples
					#"8080/tcp" =>  [{ HostPort => "" }],
					#"8088/tcp" =>  [{ HostPort => "" }],
					#"8443/tcp" =>  [{ HostPort => "" }],
					#"18080/tcp" =>  [{ HostPort => "" }],
					#"$docker_default_vnc_port\/tcp" => [{HostPort => $random_vnc_port }]
					#"$docker_default_ssh_port\/tcp" => [{ HostIp => $vmhost_public_ip_address, HostPort => $random_ssh_port }],
					#"$docker_default_public_port\/tcp" => [{ HostIp => $vmhost_public_ip_address, HostPort => $random_public_port }]
				},
			},
		};	
	}
	else {
		$container_data = {
			Image => $image_name,
			# MacAddress => $eth0_mac_address, # It could be used for future DHCP configuration. 
			HostConfig => {
				# Privileged => JSON::true, # 0/1 and true/false cause GO Marshall converting error 
				##Memory => 0 + $memory_bytes, # add 0 to avoid JSON format error (GO Marshall converting error)
				# CpusetCpus => "$cpus", # This fixed and biased distribution would reduce the CPU utilization. Need more fair distribution methods.
				# Docker uses all the available CPUs equally(Only set if you have better solutions)
				CapAdd => ["NET_ADMIN"], #// allow to use iptables inside a container
				NetworkMode => 'none' # default NetworkMode is bridge
			},
		};
	}
	
	# create the container
	$resp =  $ua->post(
		$docker_host_url . "/containers/create?name=$computer_name",
		content_type => 'application/json',
		content => to_json($container_data),
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to create a container for $computer_name: " . join("\n", $resp->content));
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "successfully create a container: ". join("\n", $resp->content));

	# start the container
	$resp =  $ua->post(
		$docker_host_url . "/containers/$computer_name/start",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to start the container for $computer_name: " . join("\n", $resp->content));
		return;
	}
	notify($ERRORS{'DEBUG'}, 0, "successfully start the container: ". join("\n", $resp->content));


	$resp =  $ua->get(
		$docker_host_url . "/containers/$computer_name/json",
		content_type => 'application/json',
	);

	eval {
		from_json($resp->content);
		1;
	} or do {
		notify($ERRORS{'DEBUG'}, 0, "the container id for $computer_name does not exist: " . join("\n", $resp->content));
		return;
	};

	my $output = from_json($resp->content);
	my $container_pid = $output->{State}{Pid};
	if (!defined($container_pid)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get Pid of the container on $computer_name");
		return;
	}
	
	# if docker uses dhcp server to assign IPs to computer
	if ($using_nat_host) {
		my $ua_output = from_json($resp->content);	
		my $private_ip = $ua_output->{NetworkSettings}{IPAddress};
		if (!defined($private_ip)) {
			notify($ERRORS{'WARNINGS'}, 0, "private IP address is not defined for $computer_name");
			return;
		}
		# YOUNG update /etc/hosts
		`sed -i "/.*\\b$computer_name\$/d" /etc/hosts`;
		`echo "$private_ip\t$computer_name" >> /etc/hosts`;
 
		# update the private ip address of the computer
		my $result = update_computer_private_ip_address($computer_id, $private_ip);
		if (!defined($result)) {
			notify($ERRORS{'WARNING'}, 0, "failed to update $private_ip on $computer_name");
			return 0;
		}
		notify($ERRORS{'DEBUG'}, 0, "private IP address is $private_ip for $computer_name");
	} else {
		# add eth0 to the container
		#my $private_nic = get_variable("docker_container_private_nic");
		#my $private_bridge = get_variable("docker_host_privae_bridge");
		my $container_private_nic = "eth0";
		my $private_bridge = 'br0';
		#my $private_bridge = $self->data->get_vmhost_profile_virtualswitch0(0) || 'br0';
		if(!$self->create_eth_inside_container($container_pid, $container_private_nic, $private_bridge)) {
			notify($ERRORS{'WARNING'}, 0, "failed to create eth0 inside the container on $computer_name");
			return;
		}
	
		# add eth1 to the container
		#my $public_nic = get_variable("docker_container_public_nic");
		#my $public_bridge = get_variable("docker_host_public_bridge");
		my $container_public_nic = "eth1";
		my $public_bridge = 'br1';
		#my $public_bridge = $self->data->get_vmhost_profile_virtualswitch1(0) || 'br1';
		if(!$self->create_eth_inside_container($container_pid, $container_public_nic, $public_bridge)) {
			notify($ERRORS{'WARNING'}, 0, "failed to create eth1 inside the container on $computer_name");
			return;
		}
		# sleep to get dhcp
		sleep(20);
	}

	# Call post_load
	if ($self->os->can("post_load")) {
		notify($ERRORS{'DEBUG'}, 0, "calling " . ref($self->os) . "->post_load()");
		if ($self->os->post_load()) {
			notify($ERRORS{'DEBUG'}, 0, "successfully ran OS post_load subroutine");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to run OS post_load subroutine");
			return;
		}
	}
	else {
		notify($ERRORS{'WARNING'}, 0, ref($self->os) . "::post_load() has not been implemented");
		return;
	}

	return 1;
} ## end sub load

#/////////////////////////////////////////////////////////////////////////////

=head2 create_eth_inside_container

 Parameters  : container process id
 Returns     : 1(success) or 0(failure)
 Description :

=cut

sub create_eth_inside_container {
	my $self = shift;
	my $container_pid = shift;
	my $eth_name = shift;
	my $bridge_name = shift;
	notify($ERRORS{'DEBUG'}, 0, "container_pid: $container_pid");
	notify($ERRORS{'DEBUG'}, 0, "ethernet_name: $eth_name");
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}

	my $computer_name = $self->data->get_computer_short_name() || return;


	my $veth_eth_name = $eth_name . $container_pid;
	my $veth_name = "v" . $eth_name . $container_pid;

	# PreStep 1
	my $netns_path = "/var/run/netns";
	my $command = "mkdir -p /var/run/netns";
	my ($exit_status, $output);
	if (!$self->vmhost_os->file_exists($netns_path)) {
		#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
		($exit_status, $output) = $self->vmhost_os->execute($command);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to create netns on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
			#return 0;
		}
	}

	# PreStep 2
	my $netns_veth_path = "/var/run/netns/$container_pid";
	if (!$self->vmhost_os->file_exists($netns_veth_path)) {
		$command = "ln -s /proc/$container_pid/ns/net /var/run/netns/$container_pid";
		#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
		($exit_status, $output) = $self->vmhost_os->execute($command);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to create netns  on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
			#return 0;
		}
	}

	# Step 1
	$command = "ip link add $veth_name type veth peer name $veth_eth_name";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to create veth pair on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	# Step 2
	$command = "brctl addif $bridge_name $veth_name";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to add interface to the veth on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}


	my $mac_address;
	if ($eth_name eq 'eth0') {
		$mac_address = $self->data->get_computer_eth0_mac_address();
		notify($ERRORS{'OK'}, 0, "mac address of the eth0: $mac_address");
	}
	elsif ($eth_name eq 'eth1') {
		$mac_address = $self->data->get_computer_eth1_mac_address();
		notify($ERRORS{'OK'}, 0, "mac address of the eth1: $mac_address");
	}
	if (!defined($mac_address)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get mac address: $mac_address");
		#return 0;
	}

	# Step 3
	$command = "ip link set $veth_name up";
	($exit_status, $output) = $self->vmhost_os->execute($command);
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	# Step 4
	$command = "ip link set $veth_eth_name netns $container_pid";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	# Step 5
	$command = "ip netns exec $container_pid ip link set dev $veth_eth_name name $eth_name";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	# Step 6
	$command = "ip netns exec $container_pid ip link set $eth_name address $mac_address";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	$command = "ip netns exec $container_pid ip link set $eth_name up";
	#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
	($exit_status, $output) = $self->vmhost_os->execute($command);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		#return 0;
	}

	if ($eth_name eq 'eth1') {
		$command = "docker exec $computer_name dhclient";
		#$command = "docker exec $computer_name dhclient eth0 eth1";
		#($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
		($exit_status, $output) = $self->vmhost_os->execute($command);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "failed to set the interface up on $container_pid\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
			#return 0;
		}
	}

	return 1;
}


#/////////////////////////////////////////////////////////////////////////////

=head2 unload

 Parameters  : hash
 Returns     : 1(success) or 0(failure)
 Description : loads virtual machine with requested image

=cut

sub unload {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	my $image_name = $self->data->get_image_name() || return;
	my $computer_name = $self->data->get_computer_short_name() || return;
	my $vmhost_name = $self->data->get_vmhost_short_name() || return;
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0) || return;
	my $vmhost_internal_ip_address = $self->vmhost_os->data->get_computer_private_ip_address(0) || return;

	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";
	# set ssh port of the vm_host (if not 22)
	my $remote_connection_target = determine_remote_connection_target($vmhost_name);
	# set the specific ssh port for vmhost in the variable table. e.g., 24
	my $target_ssh_port = get_variable("vmhost_ssh_port") || 22;
	$ENV{ssh_port}{$remote_connection_target} = $target_ssh_port;

	# create a useragent
	my $ua = LWP::UserAgent->new();
	# stop the container
	my $resp =  $ua->post(
		$docker_host_url . "/containers/$computer_name/stop",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to stop the container for $computer_name: " . join("\n", $resp->content));
		return;
	}
	sleep(1);
	$resp =  $ua->delete(
		$docker_host_url . "/containers/$computer_name",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to delete the container for $computer_name: " . join("\n", $resp->content));
		return;
	}

	# delete dangled network namespace ids
	my $nathost_name = $self->data->get_nathost_hostname(0);
	if (!defined($nathost_name)) {
		notify($ERRORS{'DEBUG'}, 0, "docker container $computer_name is not running in a NATHOST");
		my $command = "find -L /var/run/netns -type l -delete";
		#my ($exit_status, $output) = $self->vmhost_os->execute($command,1,10,3,24);
		my ($exit_status, $output) = $self->vmhost_os->execute($command);
		if (!defined($output)) {
			notify($ERRORS{'WARNING'}, 0, "delete dangled veth on $computer_name\ncommand: $command\nexit status: $exit_status\noutput:\n" . join("\n", @$output));
		}
	}

	notify($ERRORS{'DEBUG'}, 0, "docker container $computer_name is completely removed");
	sleep(10);

	return 1;
} ## end sub unload

#/////////////////////////////////////////////////////////////////////////////

=head2 capture

 Parameters  : None
 Returns     : 1 if sucessful, 0 if failed
 Description : capturing a new OpenStack image.

=cut

sub capture {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	notify($ERRORS{'OK'}, 0, "Docker Capturing....");

	my $old_image_name = $self->data->get_image_name();
	my $new_image_name = $self->get_new_image_name();
	$self->data->set_image_name($new_image_name);

	my $image_id = $self->data->get_image_id();
	my $imagerevision_id = $self->data->get_imagerevision_id();
	my $imagerevision_comments = $self->data->get_imagerevision_comments(0);
	my $computer_name = $self->data->get_computer_short_name();
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0);
	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";

	if ($self->os->can("pre_capture")) {
		notify($ERRORS{'OK'}, 0, "calling OS module's pre_capture() subroutine");
		# do not turn the container off to run "docker commit"
		if (!$self->os->pre_capture({end_state => 'on'})) {
			notify($ERRORS{'WARNING'}, 0, "OS module pre_capture() failed");
			return 0;
		}
	}

	my $ua = LWP::UserAgent->new();
	my $resp = $ua->get(
		$docker_host_url . "/containers/$computer_name/json",
		content_type => 'application/json',
	);
	eval {
		from_json($resp->content);
		1;
	} or do {
		notify($ERRORS{'DEBUG'}, 0, "the container id for $computer_name does not exist: " . join("\n", $resp->content));
		return 0;
	};

	my $output = from_json($resp->content);
	my $container_id = $output->{'Id'};
	notify($ERRORS{'OK'}, 0, "container_id: [$container_id]");
	if (!defined($container_id)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get the container id for $computer_name");
		return 0;
	}

	$resp =  $ua->post(
		$docker_host_url . "/commit?container=$container_id&comment=$imagerevision_comments&repo=$new_image_name",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to commit the container for $computer_name: " . join("\n", $resp->content));
		return;
	}

	# Update the image name in the database
	if ($old_image_name ne $new_image_name && !update_image_name($image_id, $imagerevision_id, $new_image_name)) {
		notify($ERRORS{'WARNING'}, 0, "failed to update image name in the database: $old_image_name --> $new_image_name");
		return;
	}

	sleep(10);
	notify($ERRORS{'DEBUG'}, 0, "successfully captured the container for $computer_name");

	if(!$self->unload()) {
		notify($ERRORS{'WARNING'}, 0, "failed to unload the container for $computer_name");
		return 0;
	}
	return 1;
} ## end sub capture

#/////////////////////////////////////////////////////////////////////////////

=head2  get_image_size

 Parameters  : imagename
 Returns     : 0 failure or size of image
 Description : in size of Megabytes

=cut

sub get_image_size {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}

	my $image_name = $self->data->get_image_name() || return;
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0);

	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";

	# create a useragent
	my $ua = LWP::UserAgent->new();
	my $resp = $ua->get(
		$docker_host_url . "/images/json?filter=$image_name",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to get image info: " . join("\n", $resp->content));
		return 0;
	}

	my $output = from_json($resp->content);
	if (!defined($output)) {
		notify($ERRORS{'WARNING'}, 0, "failed to parse json output");
		return 0;
	}
	my $image_size = $output->[0]{'VirtualSize'};
	if (!defined($image_size)) {
		notify($ERRORS{'WARNING'}, 0, "The docker image size for $image_name does not be defined");
		return 0;
	}
	else
	{
		notify($ERRORS{'OK'}, 0, "The docker image size for $image_name is $image_size");
		return round($image_size); # Mbytes
	}
} ## end sub get_image_size


#/////////////////////////////////////////////////////////////////////////////

=head2 power_reset

 Parameters  : $computer_node_name (optional)
 Returns     : boolean
 Description : Powers off and then powers on the computer.

=cut

sub power_reset() {
	my $self = shift;
	unless (ref($self) && $self->isa('VCL::Module')) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}
## To DO (YOUNG)
# restart the docker container remove the veth 
	notify($ERRORS{'OK'}, 0, "Power_reset does NOT support for Docker");
	return 1;

	my $computer_name = $self->data->get_computer_short_name() || return;
	notify($ERRORS{'OK'}, 0, "computer_name: $computer_name");
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0);
	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";

	# create a useragent
	my $ua = LWP::UserAgent->new();
	my $resp =  $ua->post(
		$docker_host_url . "/containers/$computer_name/restart",
		content_type => 'application/json',
	);
	if (!$resp->is_success) {
		notify($ERRORS{'WARNING'}, 0, "failed to restart the container for $computer_name: " . join("\n", $resp->content));
		return;
	}
	notify($ERRORS{'OK'}, 0, "The docker container is successfully restarted");
	return 1;

} ## end sub power_reset

#/////////////////////////////////////////////////////////////////////////////

=head2 node_status

 Parameters  : [0]: computer node name (optional)
	       [1]: log file path (optional)
 Returns     : Depends on the context which node_status was called:
	       default: string containing "READY" or "FAIL"
					boolean: true if ping, SSH, and VCL client checks are successful
						 false if any checks fail
	       list: array, values are 1 for SUCCESS, 0 for FAIL
						 [0]: Node status ("READY" or "FAIL")
							   [1]: Ping status (0 or 1)
							   [2]: SSH status (0 or 1)
							[3]: VCL client daemon status (0 ir 1)
					arrayref: reference to array described above
	       hashref: reference to hash with keys/values:
						 {status} => <"READY","FAIL">
							{ping} => <0,1>
							{ssh} => <0,1>
							   {vcl_client} => <0,1>
 Description : Checks the status of a lab machine.  Checks if the machine is
	       pingable, can be accessed via SSH, and the VCL client is running.

=cut

sub node_status{
	my $self;

	# Get the argument
	my $argument = shift;

	# Check if this subroutine was called an an object method or an argument was passed
	if (ref($argument) =~ /VCL::Module/i) {
		$self = $argument;
	}
	elsif (!ref($argument) || ref($argument) eq 'HASH') {
		# An argument was passed, check its type and determine the computer ID
		my $computer_id;
		if (ref($argument)) {
			# Hash reference was passed
			$computer_id = $argument->{id};
		}
		elsif ($argument =~ /^\d+$/) {
			# Computer ID was passed
			$computer_id = $argument;
		}
		else {
			# Computer name was passed
			($computer_id) = get_computer_ids($argument);
		}

		if ($computer_id) {
			notify($ERRORS{'DEBUG'}, 0, "computer ID: $computer_id");
		}

		else {
			notify($ERRORS{'WARNING'}, 0, "unable to determine computer ID from argument:\n" . format_data($argument));
			return;
		}

		# Create a DataStructure object containing data for the computer specified as the argument
		my $data;
		eval {
			$data= new VCL::DataStructure({computer_identifier => $computer_id});
		};
		if ($EVAL_ERROR) {
			notify($ERRORS{'WARNING'}, 0, "failed to create DataStructure object for computer ID: $computer_id, error: $EVAL_ERROR");
			return;
		}
		elsif (!$data) {
			notify($ERRORS{'WARNING'}, 0, "failed to create DataStructure object for computer ID: $computer_id, DataStructure object is not defined");
			return;
		}
		else {
			notify($ERRORS{'DEBUG'}, 0, "created DataStructure object  for computer ID: $computer_id");
		}

		# Create a VMware object
		my $object_type = 'VCL::Module::Provisioning::docker';
		if ($self = ($object_type)->new({data_structure => $data})) {
			notify($ERRORS{'DEBUG'}, 0, "created $object_type object to check the status of computer ID: $computer_id");
		}
		else {
			notify($ERRORS{'WARNING'}, 0, "failed to create $object_type object to check the status of computer ID: $computer_id");
			return;
		}

		# Create an OS object for the VMware object to access
		if (!$self->create_os_object()) {
			notify($ERRORS{'WARNING'}, 0, "failed to create OS object");
			return;
		}
	}

	# Create a hash reference and populate it with the default values
	my $status;
	$status->{currentimage} = '';
	$status->{ssh} = 0;
	$status->{image_match} = 0;
	$status->{status} = 'RELOAD';

	return $status;
} ## end sub node_status


#/////////////////////////////////////////////////////////////////////////

=head2 does_image_exist

 Parameters  :
 Returns     : 1 or 0
 Description : Checks the existence of an image.

=cut

sub does_image_exist {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return 0;
	}

	my $image_name = $self->data->get_image_name();
	my $vmhost_public_ip_address = $self->vmhost_os->data->get_computer_public_ip_address(0);
 
	my $docker_host_port = get_variable("docker_host_port"); # set docker host port in variable table, e.g., 4243
	my $docker_host_url = "http://$vmhost_public_ip_address:$docker_host_port";
	notify($ERRORS{'DEBUG'}, 0, "find image name: $image_name in docker host: $docker_host_url");

	my $ua = LWP::UserAgent->new();
	my $resp = $ua->get(
		$docker_host_url . "/images/$image_name/json",
		content_type => 'application/json',
	);
	eval {
		from_json($resp->content);
		1;
	} or do {
		notify($ERRORS{'DEBUG'}, 0, "the image id for $image_name does not exist: " . join("\n", $resp->content));
		return 0;
	};
	my $output = from_json($resp->content);
	my $image_id = $output->{'Id'};
	notify($ERRORS{'OK'}, 0, "image_id: [$image_id]");

	if (!defined($image_id)) {
		notify($ERRORS{'WARNING'}, 0, "The docker image for $image_name does not exist");
		return 0;
	}
	else
	{
		notify($ERRORS{'OK'}, 0, "The docker image for $image_name exists");
		return 1;
	}


} ## end sub does_image_exist

#/////////////////////////////////////////////////////////////////////////////
=head2 get_default_public_port

 Parameters  :
 Returns     : string
 Description : Find the default public port for the image based on the connect method 

=cut

sub get_default_public_port {
	my $image_os_type = shift;
	if (!defined($image_os_type)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get image os type");
		return;
	}

	my $sql_statement = <<EOF;
SELECT 
port  
FROM 
connectmethodport 
WHERE 
connectmethodid = (
SELECT id 
FROM 
connectmethod 
WHERE 
name = '$image_os_type')
EOF

	notify($ERRORS{'DEBUG'}, 0, "$sql_statement");
	my @selected_rows = database_select($sql_statement);
	if (scalar @selected_rows == 0 || scalar @selected_rows > 1) {
		notify($ERRORS{'WARNING'}, 0, "" . scalar @selected_rows . " rows were returned from database select");
		return;
	}

	my $default_public_port = $selected_rows[0]{port};
	if (!defined($default_public_port)) {
		notify($ERRORS{'WARNING'}, 0, "failed to get default public port");
		return;
	}

	notify($ERRORS{'DEBUG'}, 0, "default public port for $image_os_type is $default_public_port");
	return $default_public_port;
}


#/////////////////////////////////////////////////////////////////////////////

=head2 get_new_image_name

 Parameters  :
 Returns     : string
 Description : Constructs a new image name for images being captured. This is
					used instead of the name in the database in case an image is
					converted. The new image name shouldn't contain vmware.

=cut

sub get_new_image_name {
	my $self = shift;
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	my $image_id = $self->data->get_image_id();
	my $image_os_name = $self->data->get_image_os_name();
	my $image_prettyname = $self->data->get_image_prettyname();
	my $imagerevision_revision = $self->data->get_imagerevision_revision();

	# Clean up the OS name
	$image_os_name = "docker";

	# Remove non-word characters and underscores from the image prettyname
	$image_prettyname =~ s/[\W\_]+//ig;

	my $new_image_name = "$image_os_name\-$image_prettyname"."$image_id\-v$imagerevision_revision";
	notify($ERRORS{'DEBUG'}, 0, "new_image_name: [$new_image_name]");
	return $new_image_name;
} ## end sub get_new_image_name

#/////////////////////////////////////////////////////////////////////////////

=head2 get_dockerhost_random_port

 Parameters  : $remote_ip (optional), $overwrite
 Returns     : ssh_port or public_port
 Description : Processes the random port for the connect methods configured for the image revision.

=cut

sub get_dockerhost_random_port{
	my $self = shift;
	my $default_port = shift;
	notify($ERRORS{'DEBUG'}, 0, "default_port: $default_port");
	if (ref($self) !~ /docker/i) {
		notify($ERRORS{'CRITICAL'}, 0, "subroutine was called as a function, it must be called as a class method");
		return;
	}

	my $image_name = $self->data->get_image_name();
	my $computer_node_name = $self->data->get_computer_node_name();
	my $nathost_hostname = $self->data->get_nathost_hostname(0);
	my $nathost_public_ip_address = $self->data->get_nathost_public_ip_address(0);

	# Retrieve the connect method info hash
	my $connect_method_info = $self->data->get_connect_methods();
	if (!$connect_method_info) {
		notify($ERRORS{'WARNING'}, 0, "failed to retrieve connect method info");
		return;
	}

	CONNECT_METHOD: for my $connect_method_id (sort keys %{$connect_method_info} ) {
		my $connect_method = $connect_method_info->{$connect_method_id};

		for my $connect_method_port_id (keys %{$connect_method->{connectmethodport}}) {
			my $protocol = $connect_method->{connectmethodport}{$connect_method_port_id}{protocol};
			my $port = $connect_method->{connectmethodport}{$connect_method_port_id}{port};

			if ($port == $default_port) {
				my $random_port = $connect_method->{connectmethodport}{$connect_method_port_id}{natport}{publicport};
				if (!defined($random_port)) {
					notify($ERRORS{'WARNING'}, 0, "$computer_node_name is assigned to NAT host $nathost_hostname but connect method info does not contain NAT port information:\n" . format_data($connect_method));
					return;
				}
				else {
					return $random_port;
				}
			}
			# notify($ERRORS{'WARNING'}, 0, "$computer_node_name is assigned to NAT host $nathost_hostname but connect method info does not contain NAT port information:\n" . format_data($connect_method));
		}
	}
	return;
} ## end sub get_dockerhost_random_port

#/////////////////////////////////////////////////////////////////////////////

1;
__END__

=head1 SEE ALSO

L<http://cwiki.apache.org/VCL/>

=cut
