From: Greg Blomquist <gblomqui(a)redhat.com>
* add new XML formats for tooling, service, parameters
* add new matching logic to test whether an instance requires a config server
* add new matching logic to find matches for a set of instances
* add new launch routine that launches all instances in a deployable into the
same provider account
* test matching logic
Change onkeyup to onblur event when entering deployment name to cut down on network
chatter
Implementing multi-instance launching with parameter sharing
---
src/app/controllers/config_servers_controller.rb | 74 +++++
src/app/models/config_server.rb | 161 +++++++++++
src/app/models/deployment.rb | 79 ++++++-
src/app/models/instance.rb | 44 +++
src/app/models/provider_account.rb | 2 +
src/app/models/quota.rb | 24 ++-
src/app/util/assembly_xml.rb | 50 ++++
src/app/util/config_server_util.rb | 286 ++++++++++++++++++++
src/app/util/config_tooling_xml.rb | 93 +++++++
src/app/util/deployable_xml.rb | 12 +-
src/app/util/instance_config_xml.rb | 45 +++
src/app/util/parameter_xml.rb | 117 ++++++++
src/app/util/service_xml.rb | 81 ++++++
.../views/config_servers/_config_server_form.haml | 22 ++
src/app/views/config_servers/_section_header.haml | 3 +
src/app/views/config_servers/edit.haml | 21 ++
src/app/views/config_servers/new.haml | 16 ++
src/app/views/deployments/_launch_new.haml | 2 +-
src/app/views/provider_accounts/_properties.haml | 22 ++
src/config/locales/en.yml | 14 +
src/config/routes.rb | 6 +
.../migrate/20110606141425_create_config_server.rb | 18 ++
...1_add_instance_config_user_data_to_instances.rb | 11 +
src/features/config_server.feature | 109 ++++++++
.../step_definitions/config_server_steps.rb | 37 +++
src/features/support/env.rb | 3 +
src/features/support/paths.rb | 8 +-
.../controllers/config_servers_controller_spec.rb | 104 +++++++
.../provider_accounts_controller_spec.rb | 2 +-
src/spec/factories/config_server.rb | 42 +++
src/spec/models/config_server_spec.rb | 42 +++
src/spec/models/instance_spec.rb | 50 ++++
src/spec/models/provider_account_spec.rb | 7 +
33 files changed, 1598 insertions(+), 9 deletions(-)
create mode 100644 src/app/controllers/config_servers_controller.rb
create mode 100644 src/app/models/config_server.rb
create mode 100644 src/app/util/config_server_util.rb
create mode 100644 src/app/util/config_tooling_xml.rb
create mode 100644 src/app/util/instance_config_xml.rb
create mode 100644 src/app/util/parameter_xml.rb
create mode 100644 src/app/util/service_xml.rb
create mode 100644 src/app/views/config_servers/_config_server_form.haml
create mode 100644 src/app/views/config_servers/_section_header.haml
create mode 100644 src/app/views/config_servers/edit.haml
create mode 100644 src/app/views/config_servers/new.haml
create mode 100644 src/db/migrate/20110606141425_create_config_server.rb
create mode 100644
src/db/migrate/20110729104521_add_instance_config_user_data_to_instances.rb
create mode 100644 src/features/config_server.feature
create mode 100644 src/features/step_definitions/config_server_steps.rb
create mode 100644 src/spec/controllers/config_servers_controller_spec.rb
create mode 100644 src/spec/factories/config_server.rb
create mode 100644 src/spec/models/config_server_spec.rb
diff --git a/src/app/controllers/config_servers_controller.rb
b/src/app/controllers/config_servers_controller.rb
new file mode 100644
index 0000000..dd53be1
--- /dev/null
+++ b/src/app/controllers/config_servers_controller.rb
@@ -0,0 +1,74 @@
+require 'rest_client'
+
+class ConfigServersController < ApplicationController
+ before_filter :require_user
+ layout 'application'
+
+ def top_section
+ :administer
+ end
+
+ def edit
+ @config_server = ConfigServer.find(params[:id])
+ @provider_account = @config_server.provider_account
+ require_privilege(Privilege::MODIFY, @provider_account)
+ end
+
+ def new
+ @provider_account = ProviderAccount.find(params[:provider_account_id])
+ require_privilege(Privilege::MODIFY, @provider_account)
+ @config_server = ConfigServer.new()
+ end
+
+ def test
+ config_server = ConfigServer.find(params[:id])
+ if not config_server.connection_valid?
+ flash[:error] = config_server.connection_error_msg
+ else
+ flash[:notice] = "Test successful"
+ end
+ redirect_to provider_account_path(config_server.provider_account.id)
+ end
+
+ def create
+ @provider_account = ProviderAccount.find(params[:provider_account_id])
+ # for now the privileges required to create, modify, or delete a config
+ # server are tied to modifying the particular associated provider account
+ require_privilege(Privilege::MODIFY, @provider_account)
+
+ @config_server = ConfigServer.new(params[:config_server])
+ @config_server.provider_account = @provider_account
+ if @config_server.invalid?
+ flash[:error] = "The config server information is invalid."
+ render :action => 'new' and return
+ end
+ @config_server.save!
+ flash[:notice] = "Config server added."
+ redirect_to provider_account_path(@provider_account)
+ end
+
+ def update
+ @config_server = ConfigServer.find(params[:id])
+ @provider_account = @config_server.provider_account
+ require_privilege(Privilege::MODIFY, @provider_account)
+
+ if @config_server.update_attributes(params[:config_server])
+ flash[:notice] = "Config server updated."
+ redirect_to provider_account_path(@provider_account)
+ else
+ flash[:error] = "Config server was not updated"
+ render :action => :edit
+ end
+ end
+
+ def destroy
+ @config_server = ConfigServer.find(params[:id])
+ require_privilege(Privilege::MODIFY, @config_server.provider_account)
+ if ConfigServer.destroy(params[:id])
+ flash[:notice] = "Config server was deleted."
+ else
+ flash[:error] = "Config server was not deleted"
+ end
+ redirect_to provider_account_path((a)config_server.provider_account)
+ end
+end
diff --git a/src/app/models/config_server.rb b/src/app/models/config_server.rb
new file mode 100644
index 0000000..540483e
--- /dev/null
+++ b/src/app/models/config_server.rb
@@ -0,0 +1,161 @@
+# == Schema Information
+# Schema version: 20110517095823
+#
+# Table name: config_servers
+#
+# id :integer not null, primary key
+# host :string(255) not null
+# port :string(255) not null
+# username :string(255) null
+# password :string(255) null
+# certificate :string(255) null
+# provider_id :integer not null, fk_provider
+#
+
+## Copyright (C) 2011 Red Hat, Inc.
+## Written by Greg Blomquist <gblomqui(a)redhat.com>
+##
+## 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; version 2 of the License.
+##
+## 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, write to the Free Software
+## Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+## MA 02110-1301, USA. A copy of the GNU General Public License is
+## also available at
http://www.gnu.org/copyleft/gpl.html.
+#
+
+require 'cgi'
+
+class ConfigServer < ActiveRecord::Base
+
+ class ConnectionStatus
+ attr_reader :state, :message
+ def initialize
+ @state = :UNTESTED
+ end
+
+ def untested?
+ return :UNTESTED == @state
+ end
+
+ def fail!(message)
+ @state = :FAILURE
+ @message = message
+ end
+ def fail?
+ return :FAILURE == @state
+ end
+
+ def success!(message=nil)
+ @state = :SUCCESS
+ @message = message
+ end
+ def success?
+ return :SUCCESS == @state
+ end
+ end
+ @@status_fields = [:host, :port, :username, :password, :certificate]
+
+ belongs_to :provider_account
+
+ validates_presence_of :host
+ validates_presence_of :port
+ validate :validate_connection
+
+ # Utility method to piece together the connection information for this config
+ # server.
+ def base_url
+ scheme = (certificate.blank?) ? "http" : "https"
+ domain = host
+ if not port.blank?
+ domain += ":#{port}"
+ end
+ "#{scheme}://#{domain}"
+ end
+
+ # Reports the error message (if any) produced by testing the connection to
+ # this config server.
+ def connection_error_msg
+ if not connection_valid?
+ return status.message
+ end
+ end
+
+ # Determines if the connection represented by #host, #port, #username,
+ # #password, and #certificate is a valid connection. If the connection is
+ # invalid, calling #connection_error_msg will return the error that was
+ # generated when testing the connection.
+ def connection_valid?
+ if status.untested? or (changed? and (changes.keys & @(a)status_fields).empty?)
+ begin
+ test_connection
+ status.success!
+ rescue => e
+ error_str = map_connection_exception_to_error(e)
+ status.fail!(error_str)
+ end
+ end
+ status.success?
+ end
+
+ def send_config(instance_config)
+ url = "#{base_url}/configs/0/#{instance_config.uuid}"
+ args = get_connection_args(url, "post")
+ data = CGI::escape(instance_config.to_s)
+ args[:payload] = "data=#{data}"
+ RestClient::Request.execute(args)
+ end
+
+ private
+ def status
+ # smells like a factory in here
+ @status ||= ConnectionStatus.new
+ end
+
+ def validate_connection
+ # for validation hook
+ if not connection_valid?
+ errors.add(:base, status.message)
+ end
+ end
+
+ def get_connection_args(url=nil, method="get")
+ args = {:method => method.to_sym}
+ args[:url] = url if url
+ if not username.blank?
+ args[:user] = username
+ args[:password] = password
+ end
+ if not certificate.blank?
+ args[:ssl_client_cert] = OpenSSL::X509::Certificate.new(certificate)
+ end
+ args
+ end
+
+ # Test the connection to this config server. Return nil on success, or throw
+ # an exception on errors. See
+ #
http://rubydoc.info/gems/rest-client/1.6.3/RestClient#STATUSES-constant
+ # for more information on the types of exceptions.
+ def test_connection
+ args = get_connection_args("#{base_url}/version")
+ args[:raw_response] = true
+ RestClient::Request.execute(args)
+ end
+
+ def map_connection_exception_to_error(ex)
+ if ex.class == OpenSSL::X509::CertificateError
+ error_string =
I18n.translate("config_servers.errors.connection.certificate", :url =>
base_url, :msg => ex.message)
+ elsif ex.kind_of?(RestClient::ExceptionWithResponse) or
ex.kind_of?(RestClient::Exception) or ex.class == Errno::ETIMEDOUT
+ error_string =
I18n.translate("config_servers.errors.connection.generic_with_message", :url
=> base_url, :msg => ex.message)
+ elsif not ex.nil?
+ error_string = I18n.translate("config_servers.errors.connection.generic",
:url => base_url)
+ end
+ end
+end
diff --git a/src/app/models/deployment.rb b/src/app/models/deployment.rb
index 74b3900..db7923f 100644
--- a/src/app/models/deployment.rb
+++ b/src/app/models/deployment.rb
@@ -34,6 +34,7 @@
#
require 'util/deployable_xml'
+require 'util/config_server_util'
class Deployment < ActiveRecord::Base
include PermissionedObject
@@ -146,7 +147,11 @@ class Deployment < ActiveRecord::Base
end
end
- def launch(user)
+ def launch(user, config_values = nil)
+ if deployable_xml.requires_config_server?
+ return launch_in_single_account(user, config_values)
+ end
+
status = { :errors => {}, :successes => {} }
deployable_xml.assemblies.each do |assembly|
# TODO: for now we try to start all instances even if some of them fails
@@ -303,4 +308,76 @@ class Deployment < ActiveRecord::Base
json
end
+
+ private
+ def launch_in_single_account(user, config_values = nil)
+ # first, create all the instance objects,
+ # then find if there is at least a single account where they can all launch,
+ # then generate the instance configs for the instances,
+ # then, for each instance send the instance configs to the config server,
+ # and launch the instance
+ #
+ # TODO: need to be able to handle deployable-level errors
+ #
+ status = { :errors => {}, :successes => {} }
+ assembly_instances = {}
+ deployable_xml.assemblies.each do |assembly|
+ begin
+ Instance.transaction do
+ hw_profile = HardwareProfile.frontend.find_by_name(assembly.hwp)
+ raise "Hardware Profile #{assembly.hwp} not found." unless
hw_profile
+ instance = Instance.create!(
+ :deployment => self,
+ :name => "#{name}/#{assembly.name}",
+ :frontend_realm => frontend_realm,
+ :pool => pool,
+ :image_uuid => assembly.image_id,
+ :image_build_uuid => assembly.image_build,
+ :assembly_xml => assembly.to_s,
+ :state => Instance::STATE_NEW,
+ :owner => user,
+ :hardware_profile => hw_profile
+ )
+ assembly_instances[assembly.name] = instance
+ end
+ rescue
+ logger.error $!.message
+ logger.error $!.backtrace.join("\n ")
+ status[:errors][assembly.name] = $!.message
+ end
+ end
+ matches, errors = Instance.matches(assembly_instances.values)
+ if matches.empty?
+ #TODO:need to have a way to show the errors in a meaningful way
+ status[:errors][name] = errors
+ return status
+ end
+ found = matches.first
+ config_server = found.account.config_server
+ instance_configs = ConfigServerUtil.instance_configs(deployable_xml, config_values,
config_server)
+ assembly_instances.each do |assembly_name, instance|
+ config = instance_configs[assembly_name]
+ begin
+ instance.user_data = config.user_data
+ instance.instance_config_xml = config.to_s
+ instance.save!
+ task = InstanceTask.create!({:user => user,
+ :task_target => instance,
+ :action => InstanceTask::ACTION_CREATE})
+ config_server.send_config(config)
+ condormatic_instance_create(task, found)
+ if task.state == Task::STATE_FAILED
+ status[:errors][assembly_name] = 'failed'
+ else
+ status[:successes][assembly_name] = 'launched'
+ end
+ rescue
+ logger.error $!.message
+ logger.error $!.backtrace.join("\n ")
+ status[:errors][assembly_name] = $!.message
+ end
+ end
+ status
+ end
+
end
diff --git a/src/app/models/instance.rb b/src/app/models/instance.rb
index 5f0d49a..9772e8c 100644
--- a/src/app/models/instance.rb
+++ b/src/app/models/instance.rb
@@ -46,6 +46,7 @@
# updated_at :datetime
# deployment_id :integer
# assembly_xml :text
+# instance_config_xml :text
# image_uuid :string(255)
# image_build_uuid :string(255)
# provider_image_uuid :string(255)
@@ -56,6 +57,8 @@
# Likewise, all the methods added will be available for all controllers.
require 'util/assembly_xml'
+require 'util/instance_config_xml'
+
class Instance < ActiveRecord::Base
include PermissionedObject
@@ -185,6 +188,12 @@ class Instance < ActiveRecord::Base
@assembly_xml ||= AssemblyXML.new(self[:assembly_xml].to_s)
end
+ def instance_config_xml
+ if not self[:instance_config_xml].nil?
+ @instance_config_xml ||= InstanceConfigXML.new(self[:instance_config_xml].to_s)
+ end
+ end
+
# Provide method to check if requested action exists, so caller can decide
# if they want to throw an error of some sort before continuing
# (ie in service api)
@@ -284,6 +293,10 @@ class Instance < ActiveRecord::Base
(state == STATE_CREATE_FAILED) or (state == STATE_STOPPED and not restartable?)
end
+ def requires_config_server?
+ not instance_config_xml.nil? or assembly_xml.requires_config_server?
+ end
+
def self.list(order_field, order_dir)
Instance.all(:include => [ :owner ],
:order => (order_field || 'name') +' '+ (order_dir ||
'asc'))
@@ -332,6 +345,10 @@ class Instance < ActiveRecord::Base
errors << "#{account.name}: provider account quota reached"
next
end
+ if requires_config_server? and account.config_server.nil?
+ errors << "#{account.name}: no config server available for provider
account"
+ next
+ end
account_images.each do |pi|
if not frontend_realm.nil?
brealms = frontend_realm.realm_backend_targets.select {|brealm_target|
brealm_target.target_provider == account.provider}
@@ -383,6 +400,33 @@ class Instance < ActiveRecord::Base
not deployment.instances.deployed.any? {|i| i != self}
end
+ # find the list of possibles that will accommodate all of the instances
+ def self.matches(instances)
+ matches = nil
+ errors = []
+ instances.each do |instance|
+ m, e = instance.matches
+ if matches.nil?
+ matches = m.dup
+ else
+ matches.delete_if {|match| not m.include?(match) }
+ end
+ errors << e
+ end
+ # For now, this only checks the account's quota to see whether all the
+ # instances can launch there
+ # TODO: Determine if there's more to check here
+ matches.reject! do |match|
+ rejected = false
+ if !match.account.quota.can_start? instances
+ errors << "#{match.account} quota limit too low to launch
deployable"
+ rejected = true
+ end
+ rejected
+ end
+ [matches, errors]
+ end
+
private
def key_name
diff --git a/src/app/models/provider_account.rb b/src/app/models/provider_account.rb
index a780186..4e54c41 100644
--- a/src/app/models/provider_account.rb
+++ b/src/app/models/provider_account.rb
@@ -50,6 +50,8 @@ class ProviderAccount < ActiveRecord::Base
# validation of credentials is done in provider_account validation, :validate =>
false prevents nested_attributes from validation
has_many :credentials, :dependent => :destroy, :validate => false
accepts_nested_attributes_for :credentials
+ # eventually, this might be "has_many", but first pass is one-to-one
+ has_one :config_server, :dependent => :destroy
# Helpers
attr_accessor :x509_cert_priv_file, :x509_cert_pub_file
diff --git a/src/app/models/quota.rb b/src/app/models/quota.rb
index df309ca..883eb64 100644
--- a/src/app/models/quota.rb
+++ b/src/app/models/quota.rb
@@ -80,8 +80,7 @@ class Quota < ActiveRecord::Base
[instance.owner, instance.pool, cloud_account].each do |parent|
if parent
quota = Quota.find(parent.quota_id)
- potential_total_instances = quota.total_instances + 1
- if !Quota.no_limit(quota.maximum_total_instances) &&
(quota.maximum_total_instances < potential_total_instances)
+ if !quota.can_create? instance
return false
end
end
@@ -93,8 +92,7 @@ class Quota < ActiveRecord::Base
[instance.owner, instance.pool, cloud_account].each do |parent|
if parent
quota = Quota.find(parent.quota_id)
- potential_running_instances = quota.running_instances + 1
- if !Quota.no_limit(quota.maximum_running_instances) &&
quota.maximum_running_instances < potential_running_instances
+ if !quota.can_start? instance
return false
end
end
@@ -102,6 +100,24 @@ class Quota < ActiveRecord::Base
return true
end
+ def can_start?(instances)
+ size = (instances.kind_of? Array) ? instances.size : 1
+ potential_running_instances = running_instances + size
+ if !Quota.no_limit(maximum_running_instances) && maximum_running_instances
< potential_running_instances
+ return false
+ end
+ return true
+ end
+
+ def can_create?(instances)
+ size = (instances.kind_of? Array) ? instances.size : 1
+ potential_total_instances = total_instances + size
+ if !Quota.no_limit(maximum_total_instances) && maximum_total_instances <
potential_total_instances
+ return false
+ end
+ return true
+ end
+
def reached?
!Quota.no_limit(maximum_running_instances) && running_instances >=
maximum_running_instances
end
diff --git a/src/app/util/assembly_xml.rb b/src/app/util/assembly_xml.rb
index fa40e50..a61fecb 100644
--- a/src/app/util/assembly_xml.rb
+++ b/src/app/util/assembly_xml.rb
@@ -17,8 +17,18 @@
# MA 02110-1301, USA. A copy of the GNU General Public License is
# also available at
http://www.gnu.org/copyleft/gpl.html.
+require 'util/config_tooling_xml'
+require 'util/service_xml'
+
class AssemblyXML
class ValidationError < RuntimeError; end
+=begin Assembly XML Format
+ <assembly name="assembly_name" hwp="hardware_profile">
+ <image id="image_id"/>
+ <tooling> ... (see config_tooling_xml.rb) </tooling>
+ <services> ... (see service_xml.rb) </services>
+ </assembly>
+=end
def initialize(xmlstr = "")
@doc = Nokogiri::XML(xmlstr)
@@ -31,6 +41,7 @@ class AssemblyXML
end
def validate!
+ # hmm...seems like all this validation should be replaced by relaxNG
raise ValidationError, "Assembly XML root element not found" unless
@doc.root
raise ValidationError, "<assembly> element not found" unless @root
errors = []
@@ -47,6 +58,22 @@ class AssemblyXML
else
errors << "<image> element not found"
end
+ if config_tooling
+ begin
+ config_tooling.validate!
+ rescue ConfigToolingXML::ValidationError => e
+ errors << e.message
+ end
+ end
+ unless services.empty?
+ services.each do |service|
+ begin
+ service.validate!
+ rescue ServiceXML::ValidationError => e
+ errors << e.message
+ end
+ end
+ end
raise ValidationError, errors.join(", ") unless errors.empty?
end
@@ -66,4 +93,27 @@ class AssemblyXML
@image["build"] if @image
end
+ def requires_config_server?
+ not (config_tooling.nil? and services.empty? and output_parameters.empty?)
+ end
+
+ def config_tooling
+ @config_tooling ||= if @root and @root.at_xpath('tooling')
+ ConfigToolingXML.new((a)root.at_xpath('tooling').to_s)
+ end
+ end
+
+ def services
+ @services ||=
+ @root.xpath('services/service').collect do |service_node|
+ ServiceXML.new(service_node.to_s)
+ end
+ end
+
+ def output_parameters
+ @output_parameters ||=
+ @root.xpath('returns/parameter').collect do |parameter_node|
+ parameter_node['name']
+ end
+ end
end
diff --git a/src/app/util/config_server_util.rb b/src/app/util/config_server_util.rb
new file mode 100644
index 0000000..728126c
--- /dev/null
+++ b/src/app/util/config_server_util.rb
@@ -0,0 +1,286 @@
+#
+# Copyright (C) 2011 Red Hat, Inc.
+# Written by Greg Blomquist <gblomqui(a)redhat.com>
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA. A copy of the GNU General Public License is
+# also available at
http://www.gnu.org/copyleft/gpl.html.
+
+require 'uuidtools'
+require 'nokogiri'
+require 'rest_client'
+require 'cgi'
+
+module ConfigServerUtil
+ class InstanceConfigError < RuntimeError; end
+
+=begin
+ Subclasses should use xml() to append XML text the XML string
+=end
+ class ToXML
+ @num_spaces = 0
+
+ def initialize
+ @xml = ""
+ end
+
+ def to_xml(opts={})
+ @num_spaces = opts[:indent] || 0
+ _xml
+ @xml
+ end
+
+ protected
+ def pad
+ @pad ||= @num_spaces.times.map {|x| " "}.join
+ end
+
+ def xml(str)
+ @xml << "#{pad}#{str}"
+ end
+
+ def _xml
+ end
+ end
+
+ class ParameterConfig < ToXML
+ attr_accessor :name
+ def initialize(name)
+ super()
+ @name = name
+ end
+
+ protected
+ def _xml
+ xml "<parameter name='#{@name}'>\n"
+ if block_given?
+ xml " " + yield
+ end
+ xml "</parameter>\n"
+ end
+ end
+
+ class ReferenceParameterConfig < ParameterConfig
+ attr_accessor :assembly_name, :parameter_name
+ def initialize(name, assembly_name, parameter_name)
+ super(name)
+ @assembly_name = assembly_name
+ @parameter_name = parameter_name
+ end
+
+ protected
+ def _xml
+ super do
+ "<reference assembly='#{@assembly_name}'" +
+ " provided-parameter='#{@parameter_name}'/>\n"
+ end
+ end
+ end
+
+ class ValueParameterConfig < ParameterConfig
+ attr_accessor :value
+ def initialize(name, value)
+ super(name)
+ @value = value
+ end
+
+ protected
+ def _xml
+ super do
+ "<value><![CDATA[#{@value}]]></value>\n"
+ end
+ end
+ end
+
+ class ServiceConfig < ToXML
+ attr_accessor :parameters
+ def initialize(svc)
+ super()
+ @svc = svc
+ @parameters = []
+ end
+
+ def name
+ @name ||= @svc.name
+ end
+
+ def executable
+ @executable ||= if @svc.config_tooling and @svc.config_tooling.executable
+ @svc.config_tooling.executable
+ end
+ end
+
+ def files
+ @files ||= if @svc.config_tooling and not @svc.config_tooling.files.empty?
+ @svc.config_tooling.files.map {|f| f.url}
+ else []; end
+ end
+
+ protected
+ def _xml
+ xml "<service name='#{@name}'>\n"
+ if executable
+ xml " <executable url='#{executable}'/>\n"
+ end
+ unless files.empty?
+ xml " <files>\n"
+ files.each do |file|
+ xml " <file url='#{file.url}'/>\n"
+ end
+ xml " </files>\n"
+ end
+ unless @parameters.empty?
+ xml " <parameters>\n"
+ xml((a)parameters.map {|p| p.to_xml(:indent => @num_spaces + 2)}.join)
+ xml " </parameters>\n"
+ end
+ xml "</service>\n"
+ end
+ end
+
+ class InstanceConfigXML < ToXML
+ attr_reader :deployable_name, :deployable_id
+ attr_reader :assembly_name, :assembly_type, :hwp
+ attr_reader :hardware_profile, :realm, :uuid
+ attr_reader :services, :provided_params
+
+ def initialize(assembly, assembly_uuids, config_values, deployable_id,
deployable_name, config_server)
+ super()
+ @assembly = assembly
+ @assembly_uuids = assembly_uuids
+ @config_values = config_values || {}
+ @deployable_id = deployable_id
+ @deployable_name = deployable_name
+
+ @config_server = config_server
+
+ @uuid = @assembly_uuids[(a)assembly.name]
+ end
+
+ def hardware_profile
+ @assembly.hwd if @assembly
+ end
+
+ def user_data(opts={})
+ opts[:base_64] ||= true
+ user_data = "#{@config_server.host}:#{@config_server.port}:#{@uuid}"
+ if @config_server.password
+ # if the config server requires a password, use "password" for this
+ # guest
+ user_data << ":password"
+ end
+ return (opts[:base_64]) ? [user_data].pack("m0").delete("\n") :
user_data
+ end
+
+ def to_s
+ to_xml
+ end
+
+ protected
+ def _xml
+ password = nil
+ if @config_server and @config_server.password
+ password = "password".crypt("NaCl")
+ end
+ xml "<instance-config id='#{@uuid}'
name='#{(a)assembly.name}'"
+ xml " password='#{password}'" if password
+ xml ">\n <deployable name='#{@deployable_name}'
id='#{@deployable_id}'/>\n"
+ if @assembly.config_tooling
+ config_tooling = @assembly.config_tooling
+ if config_tooling.executable
+ xml " <executable
url='#{config_tooling.executable}'/>\n"
+ end
+ unless config_tooling.files.empty?
+ xml " <files>\n"
+ config_tooling.files.map do |f|
+ xml " <file url='#{f.url}'/>\n"
+ end.join
+ xml " </files>\n"
+ end
+ end
+ xml " <provided-parameters>\n"
+ provided_parameters.map do |p|
+ xml " <provided-parameter name='#{p}'/>\n"
+ end.join
+ xml " </provided-parameters>\n"
+ xml " <services>\n"
+ xml(services.map {|s| s.to_xml(:indent => 4)}.join)
+ xml " </services>\n"
+ xml "</instance-config>"
+ end
+
+ private
+ def executable
+ @executable ||= if @assembly.config_tooling and
@assembly.config_tooling.executable
+ @assembly.config_tooling.executable
+ end
+ end
+
+ def files
+ @files ||= unless @assembly.config_tooling.nil? or
@assembly.config_tooling.files.empty?
+ @assembly.config_tooling.files.map {|f| f.url }
+ end
+ end
+
+ def provided_parameters
+ @provided_parameters ||= unless @assembly.output_parameters.empty?
+ @assembly.output_parameters.dup
+ else
+ []
+ end
+ end
+
+ def services
+ @services ||= @assembly.services.map do |svc|
+ service = ServiceConfig.new(svc)
+ vals = @config_values[service.name] || {}
+ svc.parameters.each do |param|
+ val = nil
+ if vals[param.name]
+ val = ValueParameterConfig.new(param.name, vals[param.name])
+ elsif param.default
+ val = ValueParameterConfig.new(param.name, param.default)
+ elsif param.reference
+ assembly_uuid = @assembly_uuids[param.reference.assembly]
+ val = ReferenceParameterConfig.new(param.name,
+ assembly_uuid, param.reference.parameter)
+ else
+ raise InstanceConfigError, "No value provided for parameter. " +
+ "Assembly: #{(a)assembly.name}, Service: #{service.name}, " +
+ "Parameter: #{param.name}"
+ end
+ service.parameters << val
+ end
+ service
+ end
+ end
+ end
+
+ def self.instance_configs(deployable, config_values, config_server)
+ deployable_id = UUIDTools::UUID.timestamp_create.to_s
+ # we need the list of assembly UUIDs before we start processing the
+ # assemblies to help resolve cross assembly dependencies
+ assembly_uuids = deployable.assemblies.map do |assembly|
+ {assembly.name => UUIDTools::UUID.timestamp_create.to_s}
+ end.inject :merge
+ deployable.assemblies.map do |assembly|
+ if assembly.requires_config_server?
+ values = config_values.nil? ? nil : config_values[assembly.name]
+ {assembly.name => InstanceConfigXML.new(assembly, assembly_uuids,
+ values, deployable_id, deployable.name,
+ config_server)}
+ end
+ end.compact.inject :merge
+ end
+end
diff --git a/src/app/util/config_tooling_xml.rb b/src/app/util/config_tooling_xml.rb
new file mode 100644
index 0000000..1ed8278
--- /dev/null
+++ b/src/app/util/config_tooling_xml.rb
@@ -0,0 +1,93 @@
+#
+# Copyright (C) 2011 Red Hat, Inc.
+# Written by Greg Blomquist <gblomqui(a)redhat.com>
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA. A copy of the GNU General Public License is
+# also available at
http://www.gnu.org/copyleft/gpl.html.
+
+class ConfigToolingXML
+ class ValidationError < RuntimeError; end
+
+ class FileXML
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/file') if @doc.root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ raise ValidationError, "File XML root element not found" unless
@doc.root
+ raise ValidationError, "<file> element not found" unless @root
+ errors = []
+ errors << "file element does not have a URL" unless url
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def url
+ @root['url'] if @root
+ end
+ end
+
+=begin Config Tooling XML Format
+ <config-tooling>
+ <executable
url="http://example.com/config.sh"/>
+ <files>
+ <file
url="http://example.com/file.conf"/>
+ <file
url="http://example.com/file.tar.gz"/>
+ </files>
+ </config-tooling>
+=end
+
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/tooling') if @doc.root
+ @executable = @root.at_xpath('executable') if @root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ raise ValidationError, "Config Tooling XML root element not found" unless
@doc.root
+ raise ValidationError, "<tooling> element not found" unless @root
+ errors = []
+ errors << "executable does not have a URL" if @executable and not
@executable['url']
+ unless files.empty?
+ files.each do |file|
+ begin
+ file.validate!
+ rescue ValidationError => e
+ errors << e.message
+ end
+ end
+ end
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def executable
+ @executable['url'] if @executable
+ end
+
+ def files
+ @files ||=
+ @root.xpath('files/file').collect do |file_node|
+ FileXML.new(file_node.to_s)
+ end
+ end
+end
diff --git a/src/app/util/deployable_xml.rb b/src/app/util/deployable_xml.rb
index 220ae3d..f72ff12 100644
--- a/src/app/util/deployable_xml.rb
+++ b/src/app/util/deployable_xml.rb
@@ -21,6 +21,12 @@ require 'util/assembly_xml'
class DeployableXML
class ValidationError < RuntimeError; end
+=begin
+ <deployable name="deployable_name">
+ <description>Description</description>
+ <assemblies> ... (see assembly_xml.rb) </assemblies>
+ </deployable>
+=end
def initialize(xmlstr = "")
@doc = Nokogiri::XML(xmlstr)
@@ -41,7 +47,7 @@ class DeployableXML
raise ValidationError, "<deployable> element not found" unless @root
errors = []
errors << "deployable name not found" unless name
- if assemblies.size > 0
+ unless assemblies.empty?
assemblies.each do |assembly|
begin
assembly.validate!
@@ -66,6 +72,10 @@ class DeployableXML
@image_uuids ||=
@root.xpath('/deployable/assemblies/assembly/image').collect{|x| x['id']}
end
+ def requires_config_server?
+ assemblies.any? {|assembly| assembly.requires_config_server? }
+ end
+
def self.import_xml_from_url(url)
# Right now we allow this to raise exceptions on timeout / errors
resource = RestClient::Resource.new(url, :open_timeout => 10, :timeout => 45)
diff --git a/src/app/util/instance_config_xml.rb b/src/app/util/instance_config_xml.rb
new file mode 100644
index 0000000..f897ed5
--- /dev/null
+++ b/src/app/util/instance_config_xml.rb
@@ -0,0 +1,45 @@
+#
+# Copyright (C) 2011 Red Hat, Inc.
+# Written by Greg Blomquist <gblomqui(a)redhat.com>
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA. A copy of the GNU General Public License is
+# also available at
http://www.gnu.org/copyleft/gpl.html.
+
+class InstanceConfigXML
+ class ValidationError < RuntimeError; end
+
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/instance-config') if @doc.root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ errors = []
+ errors << "No instance uuid found" unless uuid
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def uuid
+ @root['id'] if @root
+ end
+
+ def to_s
+ @root.to_s
+ end
+end
diff --git a/src/app/util/parameter_xml.rb b/src/app/util/parameter_xml.rb
new file mode 100644
index 0000000..493a11c
--- /dev/null
+++ b/src/app/util/parameter_xml.rb
@@ -0,0 +1,117 @@
+#
+# Copyright (C) 2011 Red Hat, Inc.
+# Written by Greg Blomquist <gblomqui(a)redhat.com>
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA. A copy of the GNU General Public License is
+# also available at
http://www.gnu.org/copyleft/gpl.html.
+
+class ParameterXML
+ class ValidationError < RuntimeError; end
+=begin Parameter XML Format
+ <parameter name="service_configuration_parameter_name"
type="scalar" default="default_value"/>
+
+ <parameter name="service_configuration_parameter_name"
type="scalar">
+ <reference assembly="other_assembly_name"
service="other_assembly_service_name"
+ parameter="other_assembly_parameter_name"/>
+ </parameter>
+
+ <parameter name="service_configuration_parameter_name"
type="scalar">
+ <reference assembly="other_assembly_name"
parameter="other_assembly_parameter_name"/>
+ </parameter>
+=end
+
+ class ParameterReferenceXML
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/reference') if @doc.root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ raise ValidationError, "Reference XML root element not found" unless
@doc.root
+ raise ValidationError, "<reference> element not found" unless
@root
+ errors = []
+ errors << "reference assembly name not found" unless assembly
+ errors << "reference parameter name not found" unless parameter
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def assembly
+ @root['assembly'] if @root
+ end
+
+ def service
+ @root['service'] if @root
+ end
+
+ def parameter
+ @root['parameter'] if @root
+ end
+ end
+
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/parameter') if @doc.root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ raise ValidationError, "Parameter XML root element not found" unless
@doc.root
+ raise ValidationError, "<parameter> element not found" unless @root
+ errors = []
+ errors << "parameter name not found" unless name
+ errors << "parameter type not found" unless type
+ if reference
+ begin
+ reference.validate!
+ rescue ValidationError => e
+ errors << e.message
+ end
+ end
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def name
+ @root['name'] if @root
+ end
+
+ def type
+ @root['type'] if @root
+ end
+
+ def default?
+ not default.nil?
+ end
+
+ def reference?
+ not reference.nil?
+ end
+
+ def default
+ @root['default'] if @root
+ end
+
+ def reference
+ @reference ||= if @root and @root.at_xpath('reference')
+ ParameterReferenceXML.new((a)root.at_xpath('reference').to_s)
+ end
+ end
+end
diff --git a/src/app/util/service_xml.rb b/src/app/util/service_xml.rb
new file mode 100644
index 0000000..0e9adb2
--- /dev/null
+++ b/src/app/util/service_xml.rb
@@ -0,0 +1,81 @@
+#
+# Copyright (C) 2011 Red Hat, Inc.
+# Written by Greg Blomquist <gblomqui(a)redhat.com>
+#
+# 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; version 2 of the License.
+#
+# 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, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA 02110-1301, USA. A copy of the GNU General Public License is
+# also available at
http://www.gnu.org/copyleft/gpl.html.
+
+require 'util/config_tooling_xml'
+require 'util/parameter_xml'
+
+class ServiceXML
+ class ValidationError < RuntimeError; end
+=begin Service XML Format
+ <service name="service_name">
+ <parameters> ... (see parameter_xml.rb) </parameters>
+ <tooling> ... (see config_tooling_xml.rb) </tooling>
+ </service>
+=end
+
+ def initialize(xmlstr = "")
+ @doc = Nokogiri::XML(xmlstr)
+ @root = @doc.root.at_xpath('/service') if @doc.root
+ end
+
+ def to_s
+ @root.to_s
+ end
+
+ def validate!
+ raise ValidationError, "Service XML root element not found" unless
@doc.root
+ raise ValidationError, "<service> element not found" unless @root
+ errors = []
+ errors << "service name not found" unless name
+ if config_tooling
+ begin
+ config_tooling.validate!
+ rescue ConfigToolingXML::ValidationError => e
+ errors << e.message
+ end
+ end
+ unless parameters.empty?
+ parameters.each do |parameter|
+ begin
+ parameter.validate!
+ rescue ParameterXML::ValidationError => e
+ errors << e.message
+ end
+ end
+ end
+ raise ValidationError, errors.join(", ") unless errors.empty?
+ end
+
+ def name
+ @root['name'] if @root
+ end
+
+ def parameters
+ @parameters ||=
+ @root.xpath('parameters/parameter').collect do |parameter_node|
+ ParameterXML.new(parameter_node.to_s)
+ end
+ end
+
+ def config_tooling
+ @config_tooling ||= if @root and @root.at_xpath('tooling')
+ ConfigToolingXML.new((a)root.at_xpath('tooling').to_s)
+ end
+ end
+end
diff --git a/src/app/views/config_servers/_config_server_form.haml
b/src/app/views/config_servers/_config_server_form.haml
new file mode 100644
index 0000000..5e60896
--- /dev/null
+++ b/src/app/views/config_servers/_config_server_form.haml
@@ -0,0 +1,22 @@
+%fieldset.clearfix
+ - if not @config_server.id.nil?
+ = hidden_field_tag "config_server[id]", @config_server.id
+ = hidden_field_tag "provider_account_id", @provider_account.id
+ = label_tag "config_server[host]", "Host: "
+ = text_field_tag "config_server[host]", @config_server.host
+%fieldset.clearfix
+ = label_tag "config_server[port]", "Port: "
+ = text_field_tag "config_server[port]", @config_server.port
+%fieldset.clearfix
+ = label_tag "config_server[username]", "Username: "
+ = text_field_tag "config_server[username]", @config_server.username
+%fieldset.clearfix
+ = label_tag "config_server[password]", "Password: "
+ = password_field_tag "config_server[password]", @config_server.password
+%fieldset.clearfix
+ = label_tag "config_server[certificate]", "Certificate: "
+ = text_area_tag "config_server[certificate]", @config_server.certificate,
:rows => 20, :cols => 64
+ = t('config_servers.certificate_help')
+%fieldset.clearfix
+ .grid_13.alpha.omega
+ = submit_tag t(:save), :class => "ra nomargin dialogbutton"
diff --git a/src/app/views/config_servers/_section_header.haml
b/src/app/views/config_servers/_section_header.haml
new file mode 100644
index 0000000..d8e75fe
--- /dev/null
+++ b/src/app/views/config_servers/_section_header.haml
@@ -0,0 +1,3 @@
+%header.page-header
+ %h1{:class => controller.controller_name} Config Server
+ .corner
diff --git a/src/app/views/config_servers/edit.haml
b/src/app/views/config_servers/edit.haml
new file mode 100644
index 0000000..42e40f8
--- /dev/null
+++ b/src/app/views/config_servers/edit.haml
@@ -0,0 +1,21 @@
+= render :partial => 'layouts/admin_header'
+%header.page-header
+ %h1{:class => controller.controller_name}= "#{(a)provider_account.name} Config
Server"
+ #obj_actions.button-container
+ %div.button-group
+ = link_to 'Cancel Editing', provider_account_path(@provider_account),
:class => 'button pill danger', :id => 'cancel_config_server_edit'
+ .corner
+
+%section.content-section.config_server
+ %header
+ %h2 Edit Config Server
+
+ .content
+ = error_messages_for :config_server
+ %h2
+ = t('config_servers.edit.edit_config_server')
+ - form_tag(config_server_path, { :method => :put }) do
+ = render :partial => 'config_server_form', :locals => {:from =>
"edit"}
+ -#%fieldset.clearfix
+ .grid_13.alpha.omega
+ = submit_tag t(:save), :class => "ra nomargin dialogbutton"
diff --git a/src/app/views/config_servers/new.haml
b/src/app/views/config_servers/new.haml
new file mode 100644
index 0000000..c21aee4
--- /dev/null
+++ b/src/app/views/config_servers/new.haml
@@ -0,0 +1,16 @@
+= render :partial => 'layouts/admin_header'
+= render :partial => 'section_header'
+
+%section.content-section.config_server
+ %header
+ %h2 New Config Server
+
+ .content
+ = error_messages_for :config_server
+ %h2
+ = t('config_servers.new.new_config_server')
+ - form_for(@config_server, :url => config_servers_path) do |f|
+ = render :partial => 'config_server_form', :locals => {:from =>
"new"}
+ -#%fieldset
+ .grid_13.alpha.omega
+ = f.submit t(:add), :class => "ra nomargin dialogbutton"
diff --git a/src/app/views/deployments/_launch_new.haml
b/src/app/views/deployments/_launch_new.haml
index d4c915e..50a9daa 100644
--- a/src/app/views/deployments/_launch_new.haml
+++ b/src/app/views/deployments/_launch_new.haml
@@ -62,7 +62,7 @@
$('#deployable-url-section').show();
}
});
- $('#deployment_name').keyup(function(e) {
+ $('#deployment_name').blur(function(e) {
e.preventDefault();
$.get('#{check_name_deployments_path}', {name:
$('#deployment_name').val() }, function(data) {
$('#name_avail_indicator').html(data == "false" ? "That
name is already in use" : "Name available");
diff --git a/src/app/views/provider_accounts/_properties.haml
b/src/app/views/provider_accounts/_properties.haml
index 60aef58..f8b60f1 100644
--- a/src/app/views/provider_accounts/_properties.haml
+++ b/src/app/views/provider_accounts/_properties.haml
@@ -17,3 +17,25 @@
= label_tag "Account number:"
%td
= @account_id
+ %tr
+ %td
+ %label Config Server:
+ %td
+ - missing_config_server = @account.config_server.nil?
+ %span#config_server
+ = missing_config_server ? "None" : @account.config_server.base_url
+ %span#config_server_control
+ - if missing_config_server
+ [
+ = link_to 'Add', new_config_server_url +
"?provider_account_id=#{(a)account.id}"
+ ]
+ - else
+ [
+ = link_to 'Edit', edit_config_server_path((a)account.config_server)
+ ]
+ [
+ = link_to 'Test', test_config_server_path((a)account.config_server)
+ ]
+ [
+ = link_to 'Delete', config_server_path((a)account.config_server),
:method => 'delete', :confirm => "Are you sure you want to delete this
config server?"
+ ]
diff --git a/src/config/locales/en.yml b/src/config/locales/en.yml
index 6c87532..acc04c7 100644
--- a/src/config/locales/en.yml
+++ b/src/config/locales/en.yml
@@ -260,6 +260,8 @@ en:
account_not_added:
one: "Account %{list} could not be added"
other: "Accounts %{list} could not be added"
+ config_server_saved: "Config Server data was successfully saved"
+ config_server_not_saved: "An error occurred while saving the Config Server
data."
new:
new_provider_account: New Account
required_field: Required field.
@@ -277,4 +279,16 @@ en:
account_number: AWS Account ID
account_private_cert: EC2 x509 private key
account_public_cert: EC2 x509 public key
+ config_servers:
+ certificate_help: Provide a certificate to enable https support
+ new:
+ new_config_server: New Config Server
+ edit:
+ edit_config_server: Edit Config Server
+ errors:
+ connection:
+ generic: "Could not validate config server connection (url: %{url}). Check
the connection parameters and try again."
+ generic_with_message: "Could not validate config server connection (url:
%{url}). %{msg}"
+ certificate: "Could not validate config server connection (url: %{url}).
Certificate error: %{msg}"
+ unhandled_response: "Could not validate config server connection (url:
%{url}). HTTP Status Code: %{code}"
uptime: Uptime
diff --git a/src/config/routes.rb b/src/config/routes.rb
index 78340bd..9e35890 100644
--- a/src/config/routes.rb
+++ b/src/config/routes.rb
@@ -145,6 +145,12 @@ Conductor::Application.routes.draw do
delete 'multi_destroy', :on => :collection
end
+ resources :config_servers do
+ member do
+ get 'test'
+ end
+ end
+
resources :roles do
delete 'multi_destroy', :on => :collection
end
diff --git a/src/db/migrate/20110606141425_create_config_server.rb
b/src/db/migrate/20110606141425_create_config_server.rb
new file mode 100644
index 0000000..e10ce78
--- /dev/null
+++ b/src/db/migrate/20110606141425_create_config_server.rb
@@ -0,0 +1,18 @@
+class CreateConfigServer < ActiveRecord::Migration
+ def self.up
+ create_table :config_servers do |t|
+ t.string :host, :null => false
+ t.string :port, :null => false
+ t.string :username, :null => true
+ t.string :password, :null => true
+ t.string :certificate, :null => true, :limit => 2048
+ t.integer :provider_account_id, :null => false
+
+ t.timestamps
+ end
+ end
+
+ def self.down
+ drop_table :config_servers
+ end
+end
diff --git a/src/db/migrate/20110729104521_add_instance_config_user_data_to_instances.rb
b/src/db/migrate/20110729104521_add_instance_config_user_data_to_instances.rb
new file mode 100644
index 0000000..77a3939
--- /dev/null
+++ b/src/db/migrate/20110729104521_add_instance_config_user_data_to_instances.rb
@@ -0,0 +1,11 @@
+class AddInstanceConfigUserDataToInstances < ActiveRecord::Migration
+ def self.up
+ add_column :instances, :instance_config_xml, :text, :null => true
+ add_column :instances, :user_data, :text, :null => true
+ end
+
+ def self.down
+ remove_column :instances, :instance_config_xml
+ remove_column :instances, :user_data
+ end
+end
diff --git a/src/features/config_server.feature b/src/features/config_server.feature
new file mode 100644
index 0000000..9beecf8
--- /dev/null
+++ b/src/features/config_server.feature
@@ -0,0 +1,109 @@
+Feature: Config Servers
+ In order to administer configuration management on systems
+ As a user
+ I want to manage config servers as part of provider accounts
+
+ Background:
+ Given I am an authorised user
+ And I am logged in
+
+ # This scenario relies on a stubbed version of ConfigServer
+ # that never fails on the connection test
+ Scenario: I am able to add a config server to a provider account
+ Given I am on the homepage
+ And there is mock provider account "mock_account"
+ And I want to add a new config server
+ When I go to mock_account's provider account page
+ Then I should see "None" within "#config_server"
+ And I should see "[ Add ]" within "#config_server_control"
+ When I follow "Add"
+ Then I should be on the new config server page
+ When I fill in "config_server[host]" with "valid_host"
+ When I fill in "config_server[port]" with "valid_port"
+ And I press "Save"
+ Then I should be on mock_account's provider account page
+ And I should see "Config server added"
+ And I should see "[ Edit ]" within "#config_server_control"
+
+ # This is essentially the same scenario as the first, but creates
+ # a different stubbed ConfigServer, so it fails
+ Scenario: I cannot add a config server with invalid host or port information
+ Given I am on the homepage
+ And there is mock provider account "mock_account"
+ And I am not sure about the config server host
+ When I go to mock_account's provider account page
+ Then I should see "None" within "#config_server"
+ And I should see "[ Add ]" within "#config_server_control"
+ When I follow "Add"
+ Then I should be on the new config server page
+ When I fill in "config_server[host]" with "invalid"
+ When I fill in "config_server[port]" with "invalid"
+ And I press "Save"
+ Then I should see "The config server information is invalid"
+ And I should see "Could not validate config server connection"
+
+ # This is essentially the same scenario as the first, but creates
+ # a different stubbed ConfigServer, so it fails
+ Scenario: I cannot add a config server with invalid credentials
+ Given I am on the homepage
+ And there is mock provider account "mock_account"
+ And I am not sure about the config server credentials
+ When I go to mock_account's provider account page
+ Then I should see "None" within "#config_server"
+ And I should see "[ Add ]" within "#config_server_control"
+ When I follow "Add"
+ Then I should be on the new config server page
+ When I fill in "config_server[host]" with "valid"
+ When I fill in "config_server[port]" with "valid"
+ When I fill in "config_server[username]" with "invalid"
+ When I fill in "config_server[password]" with "invalid"
+ And I press "Save"
+ Then I should see "The config server information is invalid"
+ And I should see "Could not validate config server connection"
+
+ Scenario: I should be able to edit a config server
+ Given I am on the homepage
+ And there is a mock config server "https://mock:443" for account
"mock_account"
+ When I go to mock_account's provider account page
+ Then I should see "[ Edit ]" within "#config_server_control"
+ When I follow "Edit" within "#config_server_control"
+ Then I should be on the edit config server page for account "mock_account"
+ And I press "Save"
+ Then I should be on mock_account's provider account page
+ And I should see "Config server updated"
+
+ Scenario: I should be able to delete an existing config server
+ Given I am on the homepage
+ And there is a mock config server "https://mock:443" for account
"mock_account"
+ When I go to mock_account's provider account page
+ Then I should see "[ Delete ]" within "#config_server_control"
+ When I follow "Delete" within "#config_server_control"
+ Then I should see "Config server was deleted"
+ And I should be on mock_account's provider account page
+
+ Scenario: I should be able to test a correctly configured and available config server
+ Given I am on the homepage
+ And there is a mock config server "https://mock:443" for account
"mock_account"
+ When I go to mock_account's provider account page
+ Then I should see "[ Test ]" within "#config_server_control"
+ When I follow "Test" within "#config_server_control"
+ Then I should see "Test successful"
+ And I should be on mock_account's provider account page
+
+ Scenario: I should see an error when I test a config server with invalid credentials
+ Given I am on the homepage
+ And there is a mock config server "https://bad_credentials:443" for account
"mock_account"
+ When I go to mock_account's provider account page
+ Then I should see "[ Test ]" within "#config_server_control"
+ When I follow "Test" within "#config_server_control"
+ Then I should see "Could not validate config server connection"
+ And I should see "Unauthorized"
+
+ Scenario: I should see an error when I test a config server with an invalid host
+ Given I am on the homepage
+ And there is a mock config server "https://bad_host:443" for account
"mock_account"
+ When I go to mock_account's provider account page
+ Then I should see "[ Test ]" within "#config_server_control"
+ When I follow "Test" within "#config_server_control"
+ Then I should see "Could not validate config server connection"
+ And I should see "Connection timed out"
diff --git a/src/features/step_definitions/config_server_steps.rb
b/src/features/step_definitions/config_server_steps.rb
new file mode 100644
index 0000000..438ad00
--- /dev/null
+++ b/src/features/step_definitions/config_server_steps.rb
@@ -0,0 +1,37 @@
+Given /^I want to add a new config server$/ do
+ c = Factory.build(:mock_config_server)
+ ConfigServer.stub!(:new).and_return(c)
+end
+
+Given /^I am not sure about the config server (host|port)$/ do |host_or_port|
+ c = Factory.build(:invalid_host_or_port_config_server)
+ ConfigServer.stub!(:new).and_return(c)
+end
+
+Given /^I am not sure about the config server credentials$/ do
+ c = Factory.build(:invalid_credentials_config_server)
+ ConfigServer.stub!(:new).and_return(c)
+end
+
+Given /^there is a mock config server "(http|https):\/\/(.*):(.*)" for account
"(.*)"$/ do |scheme,host,port,acc|
+ provider = Factory :mock_provider, :name => "mock_provider"
+ mock_account = Factory :mock_provider_account, :label => acc, :provider =>
provider
+ params = {:host => host, :port => port, :provider_account => mock_account}
+ if "https" == scheme
+ params[:certificate] = "cert"
+ end
+ @config_server = Factory :mock_config_server, params
+ # Don't particularly like this next bit, but the problem is the "create"
+ # default_strategy used when instantiating the Factory leaves the
+ # @config_server in a "SUCCESS" status. This bit effectively
"resets" the
+ # status to "UNTESTED" so the :test_connection method(stub) has to get
called
+ @config_server.stub!(:status).and_return(ConfigServer::ConnectionStatus.new())
+ case host
+ when "bad_credentials"
+ @config_server.stub!(:test_connection).and_raise(RestClient::Unauthorized)
+ when "bad_host"
+ @config_server.stub!(:test_connection).and_raise(Errno::ETIMEDOUT)
+ end
+ # ensure that the :mock_config_server (with stubbed methods) is returned
+ ConfigServer.stub!(:find).and_return(@config_server)
+end
diff --git a/src/features/support/env.rb b/src/features/support/env.rb
index 2d8b48e..52199eb 100644
--- a/src/features/support/env.rb
+++ b/src/features/support/env.rb
@@ -29,6 +29,9 @@ Capybara.default_selector = :css
#
ActionController::Base.allow_rescue = false
+# Pull in the ability to stub out methods
+require 'spec/stubs/cucumber'
+
# Remove/comment out the lines below if your app doesn't have a database.
# For some databases (like MongoDB and CouchDB) you may need to use :truncation instead.
begin
diff --git a/src/features/support/paths.rb b/src/features/support/paths.rb
index bbeea10..b0358f5 100644
--- a/src/features/support/paths.rb
+++ b/src/features/support/paths.rb
@@ -110,6 +110,13 @@ module NavigationHelpers
when /^(.*)'s new provider account page$/
new_provider_provider_account_path(Provider.find_by_name($1))
+ when /the new config server page/
+ url_for new_config_server_path
+
+ when /^the edit config server page/
+ @config_server.stub!(:connection_valid?).and_return(true)
+ edit_config_server_path(@config_server)
+
when /the operational status of deployment page/
deployment_path(@deployment, :details_tab => 'operation')
@@ -135,7 +142,6 @@ module NavigationHelpers
when /^the (.*)'s edit user page$/
edit_user_path(User.find_by_login($1))
-
# Add more mappings here.
# Here is an example that pulls values out of the Regexp:
#
diff --git a/src/spec/controllers/config_servers_controller_spec.rb
b/src/spec/controllers/config_servers_controller_spec.rb
new file mode 100644
index 0000000..8b02cd5
--- /dev/null
+++ b/src/spec/controllers/config_servers_controller_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe ConfigServersController do
+ fixtures :all
+
+ before(:each) do
+ @admin_permission = Factory :admin_permission
+ @admin = @admin_permission.user
+ activate_authlogic
+ @session = UserSession.create(@admin)
+ end
+
+ context "editing config servers" do
+ before(:each) do
+ @config_server = Factory :mock_config_server
+ @provider_account = @config_server.provider_account
+ end
+
+ it "should provide UI to edit an existing Config Server" do
+ get :edit, :id => @config_server.id
+ response.should be_success
+ response.should render_template("edit")
+ end
+
+ it "should allow users with account modify permissions to update a Config
Server" do
+ post :update, :id => @config_server.id, :config_server => {:host =>
"host", :port => "port"}
+ response.should be_success
+ end
+ end
+
+ context "creating config servers" do
+ before(:each) do
+ @provider_account = Factory :mock_provider_account
+ end
+
+ it "should provide UI to create a new Config Server" do
+ get :new, :provider_account_id => @provider_account.id
+ response.should be_success
+ response.should render_template("new")
+ end
+
+ it "should allow users with account modify permissions to create a Config
Server" do
+ config_server = Factory :mock_config_server, :host => "host", :port
=> "port"
+ ConfigServer.stub!(:new).and_return(config_server)
+ post :create, :provider_account_id => @provider_account.id,
+ :config_server => {
+ :host => "host",
+ :port => "port"
+ }
+ response.should redirect_to(provider_account_path((a)provider_account.id))
+ request.flash[:error].should be_nil
+ end
+
+ it "should fail creating a config server when the username or password is
invalid" do
+ config_server = Factory :invalid_credentials_config_server, :host =>
"host", :port => "port", :username => "invalid",
:password => "invalid"
+ ConfigServer.stub!(:new).and_return(config_server)
+ post :create, :provider_account_id => @provider_account.id,
+ :config_server => {
+ :host => "host",
+ :port => "port",
+ :username => "invalid",
+ :password => "invalid"
+ }
+ response.should be_success
+ response.should render_template("new")
+ request.flash[:error].should == "The config server information is
invalid."
+ end
+
+ it "should fail creating a config server when the host and port are
invalid" do
+ config_server = Factory :invalid_host_or_port_config_server, :host =>
"invalid", :port => "invalid"
+ ConfigServer.stub!(:new).and_return(config_server)
+ post :create, :provider_account_id => @provider_account.id,
+ :config_server => {
+ :host => "invalid",
+ :port => "invalid"
+ }
+ response.should be_success
+ response.should render_template("new")
+ request.flash[:error].should == "The config server information is
invalid."
+ end
+
+ it "should require that port is provided" do
+ post :create, :provider_account_id => @provider_account.id,
+ :config_server => {
+ :host => "host",
+ :port => ""
+ }
+ response.should be_success
+ response.should render_template("new")
+ request.flash[:error].should == "The config server information is
invalid."
+ end
+
+ it "should require that host is provided" do
+ post :create, :provider_account_id => @provider_account.id,
+ :config_server => {
+ :host => "",
+ :port => "port"
+ }
+ response.should be_success
+ response.should render_template("new")
+ request.flash[:error].should == "The config server information is
invalid."
+ end
+ end
+end
diff --git a/src/spec/controllers/provider_accounts_controller_spec.rb
b/src/spec/controllers/provider_accounts_controller_spec.rb
index 4406a83..e71e34c 100644
--- a/src/spec/controllers/provider_accounts_controller_spec.rb
+++ b/src/spec/controllers/provider_accounts_controller_spec.rb
@@ -43,7 +43,7 @@ describe ProviderAccountsController do
post :create, :provider_account => {:provider_id => @provider.id}
response.should be_success
response.should render_template("new")
- response.flash[:error].should == "Cannot add the provider account."
+ request.flash[:error].should == "Cannot add the provider account."
end
it "should permit users with account modify permission to access edit cloud
account interface" do
diff --git a/src/spec/factories/config_server.rb b/src/spec/factories/config_server.rb
new file mode 100644
index 0000000..044be4e
--- /dev/null
+++ b/src/spec/factories/config_server.rb
@@ -0,0 +1,42 @@
+Factory.define :config_server do |f|
+ f.sequence(:host) {|n| "config_server#{n}" }
+end
+
+Factory.define :base_config_server, :parent => :config_server do |f|
+ f.host "localhost"
+ f.port "80"
+ f.username "username"
+ f.password "password"
+end
+
+Factory.define :mock_config_server, :parent => :base_config_server do |f|
+ f.association :provider_account, :factory => :mock_provider_account
+ f.after_build do |cs|
+ cs.stub!(:test_connection).and_return(nil) if cs.respond_to? :stub!
+ end
+end
+
+Factory.define :invalid_credentials_config_server, :parent => :base_config_server do
|f|
+ f.port "443"
+ f.username "bad_username"
+ f.password "bad_password"
+ f.certificate "cert"
+ f.to_create do |cs|
+ # the default_strategy initialization parameter seemed much better
+ end
+ f.after_build do |cs|
+ cs.stub!(:test_connection).and_raise(RestClient::Unauthorized) if cs.respond_to?
:stub!
+ end
+end
+
+Factory.define :invalid_host_or_port_config_server, :parent => :base_config_server do
|f|
+ f.host "bad_host"
+ f.port "443"
+ f.certificate "cert"
+ f.to_create do |cs|
+ # the default_strategy initialization parameter seemed much better
+ end
+ f.after_build do |cs|
+ cs.stub!(:test_connection).and_raise(Errno::ETIMEDOUT) if cs.respond_to? :stub!
+ end
+end
diff --git a/src/spec/models/config_server_spec.rb
b/src/spec/models/config_server_spec.rb
new file mode 100644
index 0000000..cb9acfa
--- /dev/null
+++ b/src/spec/models/config_server_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe ConfigServer do
+ describe "standard behavior" do
+ before(:each) do
+ @config_server = Factory.build :mock_config_server
+ end
+
+ it "should require a host" do
+ @config_server.should be_valid
+ @config_server.host = nil
+ @config_server.should_not be_valid
+ end
+
+ it "should require a port" do
+ @config_server.should be_valid
+ @config_server.port = nil
+ @config_server.should_not be_valid
+ end
+
+ it "should suggest https when a cert is present" do
+ @config_server.certificate = "abc"
+ @config_server.base_url.should =~ /https:\/\/.*/
+ end
+
+ it "should suggest http when a cert is not present" do
+ @config_server.certificate = nil
+ @config_server.base_url.should =~ /http:\/\/.*/
+ end
+ end
+
+ describe "error behavior: invalid credentials" do
+ before(:each) do
+ @config_server = Factory.build :invalid_credentials_config_server
+ end
+
+ it "should report an error when unauthorized" do
+ @config_server.should_not be_valid
+ @config_server.errors.full_messages.join(" ").should include("Could
not validate config server connection")
+ end
+ end
+end
diff --git a/src/spec/models/instance_spec.rb b/src/spec/models/instance_spec.rb
index 10a15ea..fdc073c 100644
--- a/src/spec/models/instance_spec.rb
+++ b/src/spec/models/instance_spec.rb
@@ -263,4 +263,54 @@ describe Instance do
deployment.all_instances_running?.should be_true
end
end
+ it "should match if the account has a config server and the instance has
configs" do
+ build = @instance.image_build || @instance.image.latest_build
+ provider = FactoryGirl.create(:mock_provider, :name =>
build.provider_images.first.provider_name)
+ account = FactoryGirl.create(:mock_provider_account, :label =>
'testaccount_config_server', :provider => provider)
+ config_server = FactoryGirl.create(:mock_config_server, :provider_account =>
account)
+ @pool.pool_family.provider_accounts = [account]
+
+ @instance.stub!(:requires_config_server?).and_return(true)
+
+ matches, errors = @instance.matches
+ matches.should_not be_empty
+ matches.first.account.should eql(account)
+ end
+
+ it "should not match if the account does not have a config server and the instance
has configs" do
+ build = @instance.image_build || @instance.image.latest_build
+ provider = FactoryGirl.create(:mock_provider, :name =>
build.provider_images.first.provider_name)
+ account = FactoryGirl.create(:mock_provider_account, :label =>
'testaccount_no_config_server', :provider => provider)
+ @pool.pool_family.provider_accounts = [account]
+
+ @instance.stub!(:requires_config_server?).and_return(true)
+
+ matches, errors = @instance.matches
+ matches.should be_empty
+ errors.should_not be_empty
+ errors.select {|e| e.include?("no config server available") }.should_not
be_empty
+ end
+
+ it "should match only the intersecting provider accounts for all instances"
do
+ account1 = FactoryGirl.create(:mock_provider_account, :label =>
"test_account1")
+ possible1 = Possible.new(nil,account1,nil,nil,nil)
+ account2 = FactoryGirl.create(:mock_provider_account, :label =>
"test_account2")
+ possible2 = Possible.new(nil,account2,nil,nil,nil)
+ account3 = FactoryGirl.create(:mock_provider_account, :label =>
"test_account3")
+ possible3 = Possible.new(nil,account3,nil,nil,nil)
+
+ # not gonna test the individual instance "machtes" logic again
+ # just stub out the behavior
+ instance1 = Factory.build(:instance)
+ instance1.stub!(:matches).and_return([[possible1, possible2], []])
+ instance2 = Factory.build(:instance)
+ instance2.stub!(:matches).and_return([[possible2, possible3], []])
+ instance3 = Factory.build(:instance)
+ instance3.stub!(:matches).and_return([[possible2], []])
+
+ instances = [instance1, instance2, instance3]
+ matches, errors = Instance.matches(instances)
+ matches.should_not be_empty
+ matches.first.account.should eql(account2)
+ end
end
diff --git a/src/spec/models/provider_account_spec.rb
b/src/spec/models/provider_account_spec.rb
index d335689..b0b8702 100644
--- a/src/spec/models/provider_account_spec.rb
+++ b/src/spec/models/provider_account_spec.rb
@@ -36,6 +36,13 @@ describe ProviderAccount do
@provider_account.should be_frozen
end
+ it "should be destroyable if it has a config server" do
+ @provider_account.config_server = ConfigServer.new
+ @provider_account.destroyable?.should be_true
+ @provider_account.destroy.equal?((a)provider_account).should be_true
+ @provider_account.should be_frozen
+ end
+
it "should check the validitiy of the cloud account login credentials" do
mock_provider = FactoryGirl.create :mock_provider
--
1.7.4.4