You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@deltacloud.apache.org by lu...@apache.org on 2010/07/09 01:33:41 UTC

svn commit: r962174 - in /incubator/deltacloud/trunk/server/libexec: config/ lib/deltacloud/drivers/opennebula/

Author: lutter
Date: Thu Jul  8 23:33:40 2010
New Revision: 962174

URL: http://svn.apache.org/viewvc?rev=962174&view=rev
Log:
Add a driver for OpenNebula

Added:
    incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/
    incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/cloud_client.rb
    incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/occi_client.rb
    incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/opennebula_driver.rb
Modified:
    incubator/deltacloud/trunk/server/libexec/config/environment.rb

Modified: incubator/deltacloud/trunk/server/libexec/config/environment.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/config/environment.rb?rev=962174&r1=962173&r2=962174&view=diff
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/config/environment.rb (original)
+++ incubator/deltacloud/trunk/server/libexec/config/environment.rb Thu Jul  8 23:33:40 2010
@@ -71,6 +71,8 @@ case DRIVER
     DRIVER_CLASS_NAME = "Deltacloud::Drivers::RHEVM::RHEVMDriver"
   when :rimu
     DRIVER_CLASS_NAME = "Deltacloud::Drivers::Rimu::RimuHostingDriver"
+  when :opennebula
+    DRIVER_CLASS_NAME = "Deltacloud::Drivers::Opennebula::OpennebulaDriver"
   else
     raise Exception.new( "Unknown driver #{DRIVER}" )
 end

Added: incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/cloud_client.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/cloud_client.rb?rev=962174&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/cloud_client.rb (added)
+++ incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/cloud_client.rb Thu Jul  8 23:33:40 2010
@@ -0,0 +1,123 @@
+#--------------------------------------------------------------------------- #
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#--------------------------------------------------------------------------- #
+
+require 'rubygems'
+require 'uri'
+
+require 'net/https'
+
+begin
+    require 'curb'
+    CURL_LOADED=true
+rescue LoadError
+    CURL_LOADED=false
+end
+
+begin
+    require 'net/http/post/multipart'
+rescue LoadError
+end
+
+###############################################################################
+# The CloudClient module contains general functionality to implement a 
+# Cloud Client
+###############################################################################
+module CloudClient
+    # #########################################################################
+    # Default location for the authentication file
+    # #########################################################################
+    DEFAULT_AUTH_FILE = ENV["HOME"]+"/.one/one_auth"
+    
+    # #########################################################################
+    # Gets authorization credentials from ONE_AUTH or default
+    # auth file.
+    #
+    # Raises an error if authorization is not found
+    # #########################################################################
+    def self.get_one_auth
+        if ENV["ONE_AUTH"] and !ENV["ONE_AUTH"].empty? and 
+           File.file?(ENV["ONE_AUTH"])
+            one_auth=File.read(ENV["ONE_AUTH"]).strip.split(':')
+        elsif File.file?(DEFAULT_AUTH_FILE)
+            one_auth=File.read(DEFAULT_AUTH_FILE).strip.split(':')
+        else
+            raise "No authorization data present"
+        end
+        
+        raise "Authorization data malformed" if one_auth.length < 2
+        
+        one_auth
+    end
+        
+    # #########################################################################
+    # Starts an http connection and calls the block provided. SSL flag
+    # is set if needed.
+    # #########################################################################
+    def self.http_start(url, &block)
+        http = Net::HTTP.new(url.host, url.port)
+        if url.scheme=='https'
+            http.use_ssl = true
+            http.verify_mode=OpenSSL::SSL::VERIFY_NONE
+        end
+        
+        begin
+            http.start do |connection|
+                block.call(connection)
+            end
+        rescue Errno::ECONNREFUSED => e
+            str =  "Error connecting to server (#{e.to_s})."
+            str << "Server: #{url.host}:#{url.port}"
+        
+            return CloudClient::Error.new(str)
+        end
+    end
+
+    # #########################################################################
+    # The Error Class represents a generic error in the Cloud Client
+    # library. It contains a readable representation of the error.
+    # #########################################################################
+    class Error
+        attr_reader :message
+        
+        # +message+ a description of the error
+        def initialize(message=nil)
+            @message=message
+        end
+
+        def to_s()
+            @message
+        end
+    end
+
+    # ######################################################################### 
+    # Returns true if the object returned by a method of the OpenNebula
+    # library is an Error
+    # #########################################################################
+    def self.is_error?(value)
+        value.class==CloudClient::Error
+    end
+end
+        
+# Command line help functions
+module CloudCLI
+    # Returns the command name
+    def cmd_name
+        File.basename($0)
+    end
+end

Added: incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/occi_client.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/occi_client.rb?rev=962174&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/occi_client.rb (added)
+++ incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/occi_client.rb Thu Jul  8 23:33:40 2010
@@ -0,0 +1,373 @@
+#--------------------------------------------------------------------------- #
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#--------------------------------------------------------------------------- #
+
+require 'rubygems'
+require 'crack'
+require 'uri'
+
+require 'deltacloud/drivers/opennebula/cloud_client'
+
+
+module OCCIClient
+
+    #####################################################################
+    #  Client Library to interface with the OpenNebula OCCI Service
+    #####################################################################
+    class Client
+
+        ######################################################################
+        # Initialize client library
+        ######################################################################
+        def initialize(endpoint_str=nil, user=nil, pass=nil, debug_flag=true)
+            @debug = debug_flag
+
+            # Server location
+            if endpoint_str
+                @endpoint =  endpoint_str
+            elsif ENV["OCCI_URL"]
+                @endpoint = ENV["OCCI_URL"]
+            else
+                @endpoint = "http://localhost:4567"
+            end
+
+            # Autentication
+            if user && pass
+                @occiauth = [user, pass]
+            else
+                @occiauth = CloudClient::get_one_auth
+            end
+
+            if !@occiauth
+                raise "No authorization data present"
+            end
+
+            @occiauth[1] = Digest::SHA1.hexdigest(@occiauth[1])
+        end
+
+        #################################
+        # Pool Resource Request Methods #
+        #################################
+
+        ######################################################################
+        # Post a new VM to the VM Pool
+        # :instance_type
+        # :xmlfile
+        ######################################################################
+        def post_vms(xmlfile)
+            xml=File.read(xmlfile)
+
+            url = URI.parse(@endpoint+"/compute")
+
+            req = Net::HTTP::Post.new(url.path)
+            req.body=xml
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) do |http|
+                http.request(req)
+            end
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Retieves the pool of Virtual Machines
+        ######################################################################
+        def get_vms
+            url = URI.parse(@endpoint+"/compute")
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Post a new Network to the VN Pool
+        # :xmlfile xml description of the Virtual Network
+        ######################################################################
+        def post_network(xmlfile)
+            xml=File.read(xmlfile)
+
+            url = URI.parse(@endpoint+"/network")
+
+            req = Net::HTTP::Post.new(url.path)
+            req.body=xml
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) do |http|
+                http.request(req)
+            end
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Retieves the pool of Virtual Networks
+        ######################################################################
+        def get_networks
+            url = URI.parse(@endpoint+"/network")
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Post a new Image to the Image Pool
+        # :xmlfile
+        ######################################################################
+        def post_image(xmlfile, curb=true)
+            xml=File.read(xmlfile)
+            image_info=Crack::XML.parse(xml)
+
+            file_path = image_info['DISK']['URL']
+
+            m=file_path.match(/^\w+:\/\/(.*)$/)
+
+            if m
+                file_path="/"+m[1]
+            end
+
+            if curb and CURL_LOADED
+                curl=Curl::Easy.new(@endpoint+"/storage")
+
+                curl.http_auth_types     = Curl::CURLAUTH_BASIC
+                curl.userpwd             = "#{@occiauth[0]}:#{@occiauth[1]}"
+                curl.verbose             = true if @debug
+                curl.multipart_form_post = true
+
+                begin
+                    curl.http_post(
+                      Curl::PostField.content('occixml', xml),
+                      Curl::PostField.file('file', file_path)
+                    )
+                rescue Exception => e
+                    return CloudClient::Error.new(e.message)
+                end
+
+                return curl.body_str
+            else
+                file=File.open(file_path)
+
+                params=Hash.new
+                params["file"]=UploadIO.new(file,
+                    'application/octet-stream', file_path)
+
+                params['occixml'] = xml
+
+                url = URI.parse(@endpoint+"/storage")
+
+                req = Net::HTTP::Post::Multipart.new(url.path, params)
+
+                req.basic_auth @occiauth[0], @occiauth[1]
+
+                res = CloudClient::http_start(url) do |http|
+                    http.request(req)
+                end
+
+                file.close
+
+                if CloudClient::is_error?(res)
+                    return res
+                else
+                    return res.body
+                end
+            end
+        end
+
+        ######################################################################
+        # Retieves the pool of Images owned by the user
+        ######################################################################
+        def get_images
+            url = URI.parse(@endpoint+"/storage")
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ####################################
+        # Entity Resource Request Methods  #
+        ####################################
+
+        ######################################################################
+        # :id VM identifier
+        ######################################################################
+        def get_vm(id)
+            url = URI.parse(@endpoint+"/compute/" + id.to_s)
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Puts a new Compute representation in order to change its state
+        # :xmlfile Compute OCCI xml representation
+        ######################################################################
+        def put_vm(xmlfile)
+            xml=File.read(xmlfile)
+            vm_info=Crack::XML.parse(xml)
+
+            url = URI.parse(@endpoint+'/compute/' + vm_info['COMPUTE']['ID'])
+
+            req = Net::HTTP::Put.new(url.path)
+            req.body = xml
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) do |http|
+                http.request(req)
+            end
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ####################################################################
+        # :id Compute identifier
+        ####################################################################
+        def delete_vm(id)
+            url = URI.parse(@endpoint+"/compute/" + id.to_s)
+            req = Net::HTTP::Delete.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # Retrieves a Virtual Network
+        # :id Virtual Network identifier
+        ######################################################################
+        def get_network(id)
+            url = URI.parse(@endpoint+"/network/" + id.to_s)
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+        ######################################################################
+        # :id VM identifier
+        ######################################################################
+        def delete_network(id)
+            url = URI.parse(@endpoint+"/network/" + id.to_s)
+            req = Net::HTTP::Delete.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+
+       #######################################################################
+        # Retieves an Image
+        # :image_uuid Image identifier
+        ######################################################################
+        def get_image(image_uuid)
+            url = URI.parse(@endpoint+"/storage/"+image_uuid)
+            req = Net::HTTP::Get.new(url.path)
+
+            req.basic_auth @occiauth[0], @occiauth[1]
+
+            res = CloudClient::http_start(url) {|http|
+                http.request(req)
+            }
+
+            if CloudClient::is_error?(res)
+                return res
+            else
+                return res.body
+            end
+        end
+    end
+end

Added: incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/opennebula_driver.rb
URL: http://svn.apache.org/viewvc/incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/opennebula_driver.rb?rev=962174&view=auto
==============================================================================
--- incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/opennebula_driver.rb (added)
+++ incubator/deltacloud/trunk/server/libexec/lib/deltacloud/drivers/opennebula/opennebula_driver.rb Thu Jul  8 23:33:40 2010
@@ -0,0 +1,266 @@
+#--------------------------------------------------------------------------- #
+# Copyright 2002-2009, Distributed Systems Architecture Group, Universidad
+# Complutense de Madrid (dsa-research.org)
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the Free Software
+# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
+#--------------------------------------------------------------------------- #
+
+require 'deltacloud/base_driver'
+
+require 'erb'
+require 'rexml/document'
+
+path = File.dirname(__FILE__)
+$: << path
+
+require 'occi_client'
+
+module Deltacloud
+  module Drivers
+    module Opennebula
+
+class OpennebulaDriver < Deltacloud::BaseDriver
+
+  ######################################################################
+  # Flavors
+  ######################################################################
+
+  (FLAVORS = [
+	Flavor.new( {
+		:id=>'small',
+		:memory=>'small-memory',
+		:storage=>'small-storage',
+		:architecture=>'Any architecture',
+	} ),
+	Flavor.new( {
+		:id=>'medium',
+		:memory=>'medium-memory',
+		:storage=>'medium-storage',
+		:architecture=>'Any architecture',
+	} ),
+	Flavor.new( {
+		:id=>'large',
+		:memory=>'large-memory',
+		:storage=>'large-storage',
+		:architecture=>'Any architecture',
+	} ),
+  ] ) unless defined?( FLAVORS )
+
+
+  def flavors(credentials, opts=nil)
+	return FLAVORS if ( opts.nil? )
+	results = FLAVORS
+	results = filter_on( results, :id, opts )
+	results
+  end
+
+
+  ######################################################################
+  # Realms
+  ######################################################################
+
+  (REALMS = [
+	Realm.new( {
+		:id=>'Any id',
+		:name=>'Any name',
+		:limit=>:unlimited,
+		:state=>'Any state',
+	} ),
+  ] ) unless defined?( REALMS )
+
+
+  def realms(credentials, opts=nil)
+	return REALMS if ( opts.nil? )
+	results = REALMS
+	results = filter_on( results, :id, opts )
+	results
+  end
+
+
+  ######################################################################
+  # Images
+  ######################################################################
+
+  def images(credentials, opts=nil)
+	occi_client = new_client(credentials)
+
+	images = []
+	imagesxml = occi_client.get_images
+
+	storage = REXML::Document.new(imagesxml)
+	storage.root.elements.each do |d|
+		id = d.attributes['href'].split("/").last
+
+		diskxml = occi_client.get_image(id)
+
+		images << convert_image(diskxml.to_s(), credentials)
+	end
+	images
+  end
+
+
+  ######################################################################
+  # Instances
+  ######################################################################
+
+  define_instance_states do
+	start.to( :pending )			.on( :create )
+
+	pending.to( :running )			.automatically
+
+	running.to( :stopped )			.on( :stop )
+
+	stopped.to( :running )			.on( :start )
+	stopped.to( :finish )			.on( :destroy )
+  end
+
+
+  def instances(credentials, opts=nil)
+	occi_client = new_client(credentials)
+
+	instances = []
+	instancesxml = occi_client.get_vms
+
+	computes = REXML::Document.new(instancesxml)
+	computes.root.elements.each do |d|
+		vm_id = d.attributes['href'].split("/").last
+
+		computexml = occi_client.get_vm(vm_id)
+
+		instances << convert_instance(computexml.to_s(), credentials)
+	end
+	instances
+  end
+
+
+  def create_instance(credentials, image_id, opts=nil)
+	occi_client = new_client(credentials)
+
+	flavor_id = opts[:flavor_id].nil? ? 'small' : opts[:flavor_id]
+
+	instancexml = ERB.new(OCCI_VM).result(binding)
+	instancefile = "|echo '#{instancexml}'"
+
+	xmlvm = occi_client.post_vms(instancefile)
+
+	convert_instance(xmlvm.to_s(), credentials)
+  end
+
+
+  def start_instance(credentials, id)
+	occi_action(credentials, id, 'RESUME')
+  end
+
+
+  def stop_instance(credentials, id)
+	occi_action(credentials, id, 'STOPPED')
+  end
+
+
+  def destroy_instance(credentials, id)
+	occi_action(credentials, id, 'DONE')
+  end
+
+
+
+  private
+
+  def new_client(credentials)
+	if ( credentials[:name].nil? || credentials[:password].nil? || credentials[:name] == '' || credentials[:password] == '' )
+		raise Deltacloud::AuthException.new
+	end
+	OCCIClient::Client.new(nil,	credentials[:name], credentials[:password], false)
+  end
+
+
+  def convert_image(diskxml, credentials)
+	disk = REXML::Document.new(diskxml)
+	diskhash = disk.root.elements
+
+	Image.new( {
+		:id=>diskhash['ID'].text,
+		:name=>diskhash['NAME'].text,
+		:description=>diskhash['NAME'].text,
+		:owner_id=>credentials[:name],
+		:architecture=>'Any architecture',
+	} )
+  end
+
+
+  def convert_instance(computexml, credentials)
+	compute = REXML::Document.new(computexml)
+	computehash = compute.root.elements
+
+	imageid = computehash['STORAGE/DISK[@type="disk"]'].attributes['href'].split("/").last
+
+	state = (computehash['STATE'].text == 'ACTIVE') ? 'RUNNING' : computehash['STATE'].text
+
+	flavor = computehash['INSTANCE_TYPE'].nil? ? 'small' : computehash['INSTANCE_TYPE'].text
+
+	networks = []
+	(computehash['NETWORK'].each do |n|
+		networks << n.attributes['ip']
+	end) unless computehash['NETWORK'].nil?
+
+	Instance.new( {
+		:id=>computehash['ID'].text,
+		:owner_id=>credentials[:name],
+		:name=>computehash['NAME'].text,
+		:image_id=>imageid,
+		:flavor_id=>flavor,
+		:realm_id=>'Any realm',
+		:state=>state,
+		:public_addreses=>networks,
+		:private_addreses=>networks,
+		:actions=> instance_actions_for( state )
+	} )
+  end
+
+
+  def occi_action(credentials, id, strstate)
+	occi_client = new_client(credentials)
+
+	actionxml = ERB.new(OCCI_ACTION).result(binding)
+	actionfile = "|echo '#{actionxml}'"
+
+	xmlvm = occi_client.put_vm(actionfile)
+
+	convert_instance(xmlvm.to_s(), credentials)
+  end
+
+
+  (OCCI_VM = %q{
+	<COMPUTE>
+		<NAME><%=opts[:name]%></NAME>
+		<INSTANCE_TYPE><%= flavor_id %></INSTANCE_TYPE>
+		<STORAGE>
+			<DISK image="<%= image_id %>" dev="sda1" />
+		</STORAGE>
+	</COMPUTE>
+  }.gsub(/^        /, '') ) unless defined?( DELTA_VM )
+
+
+  (OCCI_ACTION = %q{
+	<COMPUTE>
+		<ID><%= id %></ID>
+		<STATE><%= strstate %></STATE>
+	</COMPUTE>
+  }.gsub(/^        /, '') ) unless defined?( DELTA_ACTION )
+
+ end
+
+    end
+  end
+end