diff --git a/install.sh b/install.sh index ae4cf825b8..3b44c1cf32 100755 --- a/install.sh +++ b/install.sh @@ -43,6 +43,7 @@ usage() { echo "-s: install OpenNebula Sunstone" echo "-G: install OpenNebula Gate" echo "-o: install OpenNebula Zones (OZones)" + echo "-f: install OpenNebula Flow" echo "-r: remove Opennebula, only useful if -d was not specified, otherwise" echo " rm -rf \$ONE_LOCATION would do the job" echo "-l: creates symlinks instead of copying files, useful for development" @@ -72,6 +73,7 @@ CLIENT="no" ONEGATE="no" SUNSTONE="no" OZONES="no" +ONEFLOW="no" ONEADMIN_USER=`id -u` ONEADMIN_GROUP=`id -g` SRC_DIR=$PWD @@ -86,6 +88,7 @@ while true ; do -G) ONEGATE="yes"; shift ;; -s) SUNSTONE="yes"; shift ;; -o) OZONES="yes"; shift ;; + -f) ONEFLOW="yes"; shift ;; -u) ONEADMIN_USER="$2" ; shift 2;; -g) ONEADMIN_GROUP="$2"; shift 2;; -d) ROOT="$2" ; shift 2 ;; @@ -109,6 +112,7 @@ if [ -z "$ROOT" ] ; then ONEGATE_LOCATION="$LIB_LOCATION/onegate" SUNSTONE_LOCATION="$LIB_LOCATION/sunstone" OZONES_LOCATION="$LIB_LOCATION/ozones" + ONEFLOW_LOCATION="$LIB_LOCATION/oneflow" SYSTEM_DS_LOCATION="$VAR_LOCATION/datastores/0" DEFAULT_DS_LOCATION="$VAR_LOCATION/datastores/1" RUN_LOCATION="/var/run/one" @@ -144,13 +148,20 @@ if [ -z "$ROOT" ] ; then DELETE_DIRS="$MAKE_DIRS" + CHOWN_DIRS="" + elif [ "$ONEFLOW" = "yes" ]; then + MAKE_DIRS="$BIN_LOCATION $LIB_LOCATION $VAR_LOCATION $ONEFLOW_LOCATION \ + $ETC_LOCATION" + + DELETE_DIRS="$MAKE_DIRS" + CHOWN_DIRS="" else MAKE_DIRS="$BIN_LOCATION $LIB_LOCATION $ETC_LOCATION $VAR_LOCATION \ $INCLUDE_LOCATION $SHARE_LOCATION \ $LOG_LOCATION $RUN_LOCATION $LOCK_LOCATION \ $SYSTEM_DS_LOCATION $DEFAULT_DS_LOCATION $MAN_LOCATION \ - $VM_LOCATION $ONEGATE_LOCATION" + $VM_LOCATION $ONEGATE_LOCATION $ONEFLOW_LOCATION" DELETE_DIRS="$LIB_LOCATION $ETC_LOCATION $LOG_LOCATION $VAR_LOCATION \ $RUN_LOCATION $SHARE_DIRS" @@ -166,6 +177,7 @@ else ONEGATE_LOCATION="$LIB_LOCATION/onegate" SUNSTONE_LOCATION="$LIB_LOCATION/sunstone" OZONES_LOCATION="$LIB_LOCATION/ozones" + ONEFLOW_LOCATION="$LIB_LOCATION/oneflow" SYSTEM_DS_LOCATION="$VAR_LOCATION/datastores/0" DEFAULT_DS_LOCATION="$VAR_LOCATION/datastores/1" INCLUDE_LOCATION="$ROOT/include" @@ -191,12 +203,17 @@ else MAKE_DIRS="$BIN_LOCATION $LIB_LOCATION $VAR_LOCATION $OZONES_LOCATION \ $ETC_LOCATION" + DELETE_DIRS="$MAKE_DIRS" + elif [ "$ONEFLOW" = "yes" ]; then + MAKE_DIRS="$BIN_LOCATION $LIB_LOCATION $VAR_LOCATION $ONEFLOW_LOCATION \ + $ETC_LOCATION" + DELETE_DIRS="$MAKE_DIRS" else MAKE_DIRS="$BIN_LOCATION $LIB_LOCATION $ETC_LOCATION $VAR_LOCATION \ $INCLUDE_LOCATION $SHARE_LOCATION $SYSTEM_DS_LOCATION \ $DEFAULT_DS_LOCATION $MAN_LOCATION $OZONES_LOCATION \ - $VM_LOCATION $ONEGATE_LOCATION" + $VM_LOCATION $ONEGATE_LOCATION $ONEFLOW_LOCATION" DELETE_DIRS="$MAKE_DIRS" @@ -366,6 +383,10 @@ OZONES_CLIENT_DIRS="$LIB_LOCATION/ruby \ $LIB_LOCATION/ruby/cli/ozones_helper \ $LIB_LOCATION/ruby/zona" +ONEFLOW_DIRS="$ONEFLOW_LOCATION/lib \ + $ONEFLOW_LOCATION/lib/strategy \ + $ONEFLOW_LOCATION/lib/models" + LIB_ECO_CLIENT_DIRS="$LIB_LOCATION/ruby \ $LIB_LOCATION/ruby/opennebula \ $LIB_LOCATION/ruby/cloud/ \ @@ -397,9 +418,11 @@ elif [ "$SUNSTONE" = "yes" ]; then MAKE_DIRS="$MAKE_DIRS $SUNSTONE_DIRS $LIB_OCA_CLIENT_DIRS" elif [ "$OZONES" = "yes" ]; then MAKE_DIRS="$MAKE_DIRS $OZONES_DIRS $OZONES_CLIENT_DIRS $LIB_OCA_CLIENT_DIRS" +elif [ "$ONEFLOW" = "yes" ]; then + MAKE_DIRS="$MAKE_DIRS $ONEFLOW_DIRS $LIB_OCA_CLIENT_DIRS" else MAKE_DIRS="$MAKE_DIRS $SHARE_DIRS $ETC_DIRS $LIB_DIRS $VAR_DIRS \ - $OZONES_DIRS $OZONES_CLIENT_DIRS $SUNSTONE_DIRS" + $OZONES_DIRS $OZONES_CLIENT_DIRS $SUNSTONE_DIRS $ONEFLOW_DIRS" fi #------------------------------------------------------------------------------- @@ -522,6 +545,7 @@ INSTALL_SUNSTONE_FILES=( SUNSTONE_VIEWS_FILES:$SUNSTONE_LOCATION/views SUNSTONE_PUBLIC_JS_FILES:$SUNSTONE_LOCATION/public/js SUNSTONE_PUBLIC_JS_PLUGINS_FILES:$SUNSTONE_LOCATION/public/js/plugins + SUNSTONE_ROUTES_FILES:$SUNSTONE_LOCATION/routes SUNSTONE_PUBLIC_CSS_FILES:$SUNSTONE_LOCATION/public/css SUNSTONE_PUBLIC_VENDOR_CRYPTOJS:$SUNSTONE_LOCATION/public/vendor/crypto-js SUNSTONE_PUBLIC_VENDOR_EXPLORERCANVAS:$SUNSTONE_LOCATION/public/vendor/explorercanvas @@ -614,6 +638,17 @@ INSTALL_OZONES_ETC_FILES=( OZONES_ETC_FILES:$ETC_LOCATION ) +INSTALL_ONEFLOW_FILES=( + ONEFLOW_FILES:$ONEFLOW_LOCATION + ONEFLOW_BIN_FILES:$BIN_LOCATION + ONEFLOW_LIB_FILES:$ONEFLOW_LOCATION/lib + ONEFLOW_LIB_STRATEGY_FILES:$ONEFLOW_LOCATION/lib/strategy + ONEFLOW_LIB_MODELS_FILES:$ONEFLOW_LOCATION/lib/models +) + +INSTALL_ONEFLOW_ETC_FILES=( + ONEFLOW_ETC_FILES:$ETC_LOCATION +) INSTALL_ETC_FILES=( ETC_FILES:$ETC_LOCATION @@ -647,6 +682,8 @@ BIN_FILES="src/nebula/oned \ src/cli/oneacl \ src/cli/onedatastore \ src/cli/onecluster \ + src/cli/oneflow \ + src/cli/oneflow-template \ src/onedb/onedb \ src/onedb/onezonedb/onezonedb \ src/mad/utils/tty_expect \ @@ -1182,38 +1219,39 @@ INSTALL_GEMS_SHARE_FILE="share/install_gems/install_gems" #------------------------------------------------------------------------------- OCA_LIB_FILES="src/oca/ruby/opennebula.rb" -RUBY_OPENNEBULA_LIB_FILES="src/oca/ruby/opennebula/host.rb \ - src/oca/ruby/opennebula/host_pool.rb \ - src/oca/ruby/opennebula/pool.rb \ - src/oca/ruby/opennebula/user.rb \ - src/oca/ruby/opennebula/user_pool.rb \ - src/oca/ruby/opennebula/virtual_machine.rb \ - src/oca/ruby/opennebula/virtual_machine_pool.rb \ - src/oca/ruby/opennebula/virtual_network.rb \ - src/oca/ruby/opennebula/virtual_network_pool.rb \ - src/oca/ruby/opennebula/image.rb \ - src/oca/ruby/opennebula/image_pool.rb \ - src/oca/ruby/opennebula/template.rb \ - src/oca/ruby/opennebula/template_pool.rb \ - src/oca/ruby/opennebula/document.rb \ - src/oca/ruby/opennebula/document_pool.rb \ - src/oca/ruby/opennebula/document_json.rb \ - src/oca/ruby/opennebula/document_pool_json.rb \ - src/oca/ruby/opennebula/group.rb \ - src/oca/ruby/opennebula/group_pool.rb \ - src/oca/ruby/opennebula/acl.rb \ - src/oca/ruby/opennebula/acl_pool.rb \ - src/oca/ruby/opennebula/datastore.rb \ - src/oca/ruby/opennebula/datastore_pool.rb \ - src/oca/ruby/opennebula/cluster.rb \ - src/oca/ruby/opennebula/cluster_pool.rb \ - src/oca/ruby/opennebula/xml_utils.rb \ - src/oca/ruby/opennebula/client.rb \ - src/oca/ruby/opennebula/error.rb \ - src/oca/ruby/opennebula/pool_element.rb \ - src/oca/ruby/opennebula/xml_element.rb \ - src/oca/ruby/opennebula/xml_pool.rb \ - src/oca/ruby/opennebula/system.rb" +RUBY_OPENNEBULA_LIB_FILES="src/oca/ruby/opennebula/acl_pool.rb \ + src/oca/ruby/opennebula/acl.rb \ + src/oca/ruby/opennebula/client.rb \ + src/oca/ruby/opennebula/cluster_pool.rb \ + src/oca/ruby/opennebula/cluster.rb \ + src/oca/ruby/opennebula/datastore_pool.rb \ + src/oca/ruby/opennebula/datastore.rb \ + src/oca/ruby/opennebula/document_json.rb \ + src/oca/ruby/opennebula/document_pool_json.rb \ + src/oca/ruby/opennebula/document_pool.rb \ + src/oca/ruby/opennebula/document.rb \ + src/oca/ruby/opennebula/error.rb \ + src/oca/ruby/opennebula/group_pool.rb \ + src/oca/ruby/opennebula/group.rb \ + src/oca/ruby/opennebula/host_pool.rb \ + src/oca/ruby/opennebula/host.rb \ + src/oca/ruby/opennebula/image_pool.rb \ + src/oca/ruby/opennebula/image.rb \ + src/oca/ruby/opennebula/pool_element.rb \ + src/oca/ruby/opennebula/pool.rb \ + src/oca/ruby/opennebula/system.rb \ + src/oca/ruby/opennebula/template_pool.rb \ + src/oca/ruby/opennebula/template.rb \ + src/oca/ruby/opennebula/user_pool.rb \ + src/oca/ruby/opennebula/user.rb \ + src/oca/ruby/opennebula/virtual_machine_pool.rb \ + src/oca/ruby/opennebula/virtual_machine.rb \ + src/oca/ruby/opennebula/virtual_network_pool.rb \ + src/oca/ruby/opennebula/virtual_network.rb \ + src/oca/ruby/opennebula/xml_element.rb \ + src/oca/ruby/opennebula/xml_pool.rb \ + src/oca/ruby/opennebula/xml_utils.rb \ + src/oca/ruby/opennebula/oneflow_client.rb" #------------------------------------------------------------------------------- # Common Cloud Files @@ -1405,6 +1443,8 @@ CLI_BIN_FILES="src/cli/onevm \ src/cli/oneacl \ src/cli/onedatastore \ src/cli/onecluster \ + src/cli/oneflow \ + src/cli/oneflow-template \ src/cli/oneacct" CLI_CONF_FILES="src/cli/etc/onegroup.yaml \ @@ -1432,7 +1472,8 @@ SUNSTONE_BIN_FILES="src/sunstone/bin/sunstone-server \ src/sunstone/bin/novnc-server" SUNSTONE_ETC_FILES="src/sunstone/etc/sunstone-server.conf \ - src/sunstone/etc/sunstone-views.yaml" + src/sunstone/etc/sunstone-views.yaml \ + src/sunstone/etc/sunstone-oneflow.conf" SUNSTONE_ETC_VIEW_FILES="src/sunstone/etc/sunstone-views/admin.yaml \ src/sunstone/etc/sunstone-views/user.yaml" @@ -1485,7 +1526,12 @@ SUNSTONE_PUBLIC_JS_PLUGINS_FILES="\ src/sunstone/public/js/plugins/acls-tab.js \ src/sunstone/public/js/plugins/vnets-tab.js \ src/sunstone/public/js/plugins/marketplace-tab.js \ - src/sunstone/public/js/plugins/config-tab.js" + src/sunstone/public/js/plugins/config-tab.js \ + src/sunstone/public/js/plugins/oneflow-dashboard.js \ + src/sunstone/public/js/plugins/oneflow-services.js \ + src/sunstone/public/js/plugins/oneflow-templates.js" + +SUNSTONE_ROUTES_FILES="src/sunstone/routes/oneflow.rb" SUNSTONE_PUBLIC_CSS_FILES="src/sunstone/public/css/app.css \ src/sunstone/public/css/login.css" @@ -1892,6 +1938,35 @@ OZONES_BIN_CLIENT_FILES="src/ozones/Client/bin/onevdc \ OZONES_RUBY_LIB_FILES="src/oca/ruby/OpenNebula.rb" +#----------------------------------------------------------------------------- +# OneFlow files +#----------------------------------------------------------------------------- + + +ONEFLOW_FILES="src/flow/oneflow-server.rb \ + src/flow/config.ru" + +ONEFLOW_BIN_FILES="src/flow/bin/oneflow-server" + +ONEFLOW_ETC_FILES="src/flow/etc/oneflow-server.conf" + +ONEFLOW_LIB_FILES="src/flow/lib/grammar.rb \ + src/flow/lib/grammar.treetop \ + src/flow/lib/LifeCycleManager.rb \ + src/flow/lib/log.rb \ + src/flow/lib/models.rb \ + src/flow/lib/strategy.rb \ + src/flow/lib/validator.rb" + +ONEFLOW_LIB_STRATEGY_FILES="src/flow/lib/strategy/straight.rb" + +ONEFLOW_LIB_MODELS_FILES="src/flow/lib/models/role.rb \ + src/flow/lib/models/service_pool.rb \ + src/flow/lib/models/service.rb \ + src/flow/lib/models/service_template_pool.rb \ + src/flow/lib/models/service_template.rb" + + #----------------------------------------------------------------------------- # MAN files #----------------------------------------------------------------------------- @@ -1909,6 +1984,8 @@ MAN_FILES="share/man/oneauth.1.gz \ share/man/onedb.1.gz \ share/man/onedatastore.1.gz \ share/man/onecluster.1.gz \ + share/man/oneflow.1.gz \ + share/man/oneflow-template.1.gz \ share/man/econe-allocate-address.1.gz \ share/man/econe-associate-address.1.gz \ share/man/econe-attach-volume.1.gz \ @@ -2022,9 +2099,12 @@ elif [ "$SUNSTONE" = "yes" ]; then INSTALL_SET="${INSTALL_SUNSTONE_RUBY_FILES[@]} ${INSTALL_SUNSTONE_FILES[@]}" elif [ "$OZONES" = "yes" ]; then INSTALL_SET="${INSTALL_OZONES_RUBY_FILES[@]} ${INSTALL_OZONES_FILES[@]}" +elif [ "$ONEFLOW" = "yes" ]; then + INSTALL_SET="${INSTALL_ONEFLOW_FILES[@]}" else INSTALL_SET="${INSTALL_FILES[@]} ${INSTALL_OZONES_FILES[@]} \ - ${INSTALL_SUNSTONE_FILES[@]} ${INSTALL_ONEGATE_FILES[@]}" + ${INSTALL_SUNSTONE_FILES[@]} ${INSTALL_ONEGATE_FILES[@]} \ + ${INSTALL_ONEFLOW_FILES[@]}" fi for i in ${INSTALL_SET[@]}; do @@ -2045,11 +2125,14 @@ if [ "$INSTALL_ETC" = "yes" ] ; then INSTALL_ETC_SET="${INSTALL_ONEGATE_ETC_FILES[@]}" elif [ "$OZONES" = "yes" ]; then INSTALL_ETC_SET="${INSTALL_OZONES_ETC_FILES[@]}" + elif [ "$ONEFLOW" = "yes" ]; then + INSTALL_ETC_SET="${INSTALL_ONEFLOW_ETC_FILES[@]}" else INSTALL_ETC_SET="${INSTALL_ETC_FILES[@]} \ ${INSTALL_SUNSTONE_ETC_FILES[@]} \ ${INSTALL_OZONES_ETC_FILES[@]} \ - ${INSTALL_ONEGATE_ETC_FILES[@]}" + ${INSTALL_ONEGATE_ETC_FILES[@]} \ + ${INSTALL_ONEFLOW_ETC_FILES[@]}" fi for i in ${INSTALL_ETC_SET[@]}; do diff --git a/share/doc/states/service.dot b/share/doc/states/service.dot new file mode 100644 index 0000000000..9d46735cfb --- /dev/null +++ b/share/doc/states/service.dot @@ -0,0 +1,63 @@ + + +digraph AppFlow { + graph [ + nodesep= 0.5, + ratio=1, + pad=0.5, + ]; + + node [fontname="Helvetica"]; + edge [fontname="Menlo"]; + + user [ + fillcolor="black", + style="filled", shape="box", + fontcolor="white", fontsize= "24.0"]; + + ANY [ shape="box" ]; + + end [ width=0.15, height=0.15, shape=point ]; + + running [ label="running /\nwarning"]; + + subgraph { rank = min; user; pending} + subgraph { rank = same; deploying; failed_deploying; color="white"} + + subgraph { rank = same; running; scaling; failed_scaling; color="white"} + + subgraph { rank = same; undeploying; failed_undeploying; color="white"} +// subgraph { rank = same; done; end; color="white"} + + + user -> pending [label="instantiate"]; + + pending -> deploying [style="dashed"]; + deploying -> running [style="dashed"]; + running -> undeploying [label="shutdown"]; + undeploying -> done [style="dashed"]; + done -> end [label="delete"]; + + +// running -> warning [style="dotted"]; +// warning -> running [style="dotted"]; + + + running -> scaling [style="dashed"]; + scaling -> cooldown [style="dashed"]; + cooldown -> running [style="dashed"]; + + cooldown -> failed_undeploying [style="invis"]; + + failed_deploying -> deploying [label="recover"]; + deploying -> failed_deploying [style="dotted"]; + + failed_undeploying -> undeploying [label="recover"]; + undeploying -> failed_undeploying [style="dotted"]; + + failed_scaling -> scaling [label="recover"]; + scaling -> failed_scaling [style="dotted"]; + + + ANY -> end [label="delete"]; +} diff --git a/share/examples/oneflow/none.json b/share/examples/oneflow/none.json new file mode 100644 index 0000000000..e51fa6d057 --- /dev/null +++ b/share/examples/oneflow/none.json @@ -0,0 +1,34 @@ +{ + "name": "my_first_service", + "roles": [ + { + "name": "frontend", + "cardinality": 1, + "parents": [ + "mysql_server", + "kvm_host", + "nfs_server" + ], + "vm_template": 3 + }, + { + "name": "nfs_server", + "cardinality": 1, + "vm_template": 2 + }, + { + "name": "mysql_server", + "cardinality": 1, + "vm_template": 5 + }, + { + "name": "kvm_host", + "parents": [ + "nfs_server" + ], + "cardinality": 3, + "vm_template": 7 + } + ], + "deployment": "none" +} diff --git a/share/examples/oneflow/scal.json b/share/examples/oneflow/scal.json new file mode 100644 index 0000000000..aedf357bdb --- /dev/null +++ b/share/examples/oneflow/scal.json @@ -0,0 +1,57 @@ +{ + "name": "scalability", + "deployment": "straight", + "roles": [ + { + "name": "frontend", + "cardinality": 1, + "vm_template": 0, + + "min_vms" : 1, + "max_vms" : 5, + + "elasticity_policies" : [ + { + // +2 VMs when the exp. is true for 3 times in a row, + // separated by 10 seconds + "type" : "CHANGE", + "expression" : "ATT > 50", + "adjust" : 2, + + "period_number" : 3, + "period" : 10 + }, + { + // -10 percent VMs when the exp. is true. + // If 10 percent is less than 2, -2 VMs. + "type" : "PERCENTAGE_CHANGE", + "expression" : "ATT < 10", + "adjust" : -10, + "min_adjust_step" : 2 + }, + { + // Set cardinality to 4 when the exp. is true + "type" : "CARDINALITY", + "expression" : "SOME_ATT <= 50 && OTHER_ATT > 90", + "adjust" : 4 + } + ], + + "scheduled_policies" : [ + { + // Set cardinality to 2 each 10 minutes + "type" : "CARDINALITY", + "recurrence" : "*/10 * * * *", + "adjust" : 2 + }, + { + // +10 percent at the given date and time + "type" : "PERCENTAGE_CHANGE", + "start_time" : "2nd oct 2013 15:45", + "adjust" : 10 + } + ] + } + ] +} + diff --git a/share/examples/oneflow/straight.json b/share/examples/oneflow/straight.json new file mode 100644 index 0000000000..2ed67bd5d0 --- /dev/null +++ b/share/examples/oneflow/straight.json @@ -0,0 +1,35 @@ +{ + "name": "my_first_service", + "deployment": "straight", + "roles": [ + { + "name": "frontend", + "cardinality": 1, + "parents": [ + "mysql_server", + "kvm_host", + "nfs_server" + ], + "vm_template": 3 + }, + { + "name": "nfs_server", + "cardinality": 1, + "vm_template": 2 + }, + { + "name": "mysql_server", + "cardinality": 1, + "vm_template": 5 + }, + { + "name": "kvm_host", + "parents": [ + "nfs_server" + ], + "cardinality": 3, + "vm_template": 7 + } + ] +} + diff --git a/share/install_gems/install_gems b/share/install_gems/install_gems index ec588d2459..a0dc6c1b73 100755 --- a/share/install_gems/install_gems +++ b/share/install_gems/install_gems @@ -4,7 +4,7 @@ require 'pp' $nokogiri='nokogiri' -DEFAULT=%w{sunstone quota cloud ozones_server auth_ldap vmware} +DEFAULT=%w{sunstone quota cloud ozones_server auth_ldap vmware oneflow} if defined?(RUBY_VERSION) && RUBY_VERSION>="1.8.7" SQLITE='sqlite3' @@ -23,7 +23,8 @@ GROUPS={ :ozones_server_sqlite => %w{json sequel}< %w{json sequel mysql}, :auth_ldap => 'net-ldap', - :vmware => 'builder' + :vmware => 'builder', + :oneflow => %w{sinatra json treetop parse-cron} } PACKAGES=GROUPS.keys diff --git a/src/cli/oneflow b/src/cli/oneflow new file mode 100755 index 0000000000..030d1d6c51 --- /dev/null +++ b/src/cli/oneflow @@ -0,0 +1,673 @@ +#!/usr/bin/env ruby + +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +ONE_LOCATION=ENV["ONE_LOCATION"] + +if !ONE_LOCATION + RUBY_LIB_LOCATION="/usr/lib/one/ruby" +else + RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby" +end + +$: << RUBY_LIB_LOCATION +$: << RUBY_LIB_LOCATION+'/cli' + +require 'command_parser' +require 'opennebula/oneflow_client' + +require 'cli_helper' +require 'one_helper/onevm_helper' + +require 'json' + +USER_AGENT = "CLI" + +# Base Path representing the resource to be used in the requests +RESOURCE_PATH = "/service" + +# +# Table +# + +SERVICE_TABLE = CLIHelper::ShowTable.new(nil, self) do + column :ID, "ID", :size=>10 do |d| + d["ID"] + end + + column :USER, "Username", :left, :size=>15 do |d| + d["UNAME"] + end + + column :GROUP, "Group", :left, :size=>15 do |d| + d["GNAME"] + end + + column :NAME, "Name", :size=>25, :left=>true do |d| + d["NAME"] + end + + column :STATE, "State", :size=>11, :left=>true do |d| + Service.state_str(d["TEMPLATE"]["BODY"]['state']) + end + + default :ID, :USER, :GROUP, :NAME, :STATE +end + +NODE_TABLE = CLIHelper::ShowTable.new(nil, self) do + column :VM_ID, "ONE identifier for Virtual Machine", :size=>6 do |d| + st = "" + if d['scale_up'] + st << "\u2191 " + elsif d['disposed'] + st << "\u2193 " + end + + if d['vm_info'].nil? + st << d['deploy_id'].to_s + else + st << d['vm_info']['VM']["ID"] + end + + st + end + + column :NAME, "Name of the Virtual Machine", :left, + :size=>23 do |d| + if !d['vm_info'].nil? + if d['vm_info']['VM']["RESCHED"] == "1" + "*#{d["NAME"]}" + else + d['vm_info']['VM']["NAME"] + end + else + "" + end + end + + column :USER, "Username of the Virtual Machine owner", :left, + :size=>8 do |d| + if !d['vm_info'].nil? + d['vm_info']['VM']["UNAME"] + else + "" + end + end + + column :GROUP, "Group of the Virtual Machine", :left, + :size=>8 do |d| + if !d['vm_info'].nil? + d['vm_info']['VM']["GNAME"] + else + "" + end + end + + column :STAT, "Actual status", :size=>4 do |d,e| + if !d['vm_info'].nil? + OneVMHelper.state_to_str(d['vm_info']['VM']["STATE"], d['vm_info']['VM']["LCM_STATE"]) + else + "" + end + end + + column :UCPU, "CPU percentage used by the VM", :size=>4 do |d| + if !d['vm_info'].nil? + d['vm_info']['VM']["CPU"] + else + "" + end + end + + column :UMEM, "Memory used by the VM", :size=>7 do |d| + if !d['vm_info'].nil? + OpenNebulaHelper.unit_to_str(d['vm_info']['VM']["MEMORY"].to_i, {}) + else + "" + end + end + + column :HOST, "Host where the VM is running", :left, :size=>20 do |d| + if !d['vm_info'].nil? + if d['vm_info']['VM']['HISTORY_RECORDS'] && d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'] + state_str = VirtualMachine::VM_STATE[d['vm_info']['VM']['STATE'].to_i] + if %w{ACTIVE SUSPENDED}.include? state_str + history = if d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'].instance_of?(Array) + d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'].last + else + d['vm_info']['VM']['HISTORY_RECORDS']['HISTORY'] + end + + history['HOSTNAME'] + end + end + else + "" + end + end + + column :TIME, "Time since the VM was submitted", :size=>10 do |d| + if !d['vm_info'].nil? + stime = d['vm_info']['VM']["STIME"].to_i + etime = d['vm_info']['VM']["ETIME"]=="0" ? Time.now.to_i : d['vm_info']['VM']["ETIME"].to_i + dtime = etime-stime + OpenNebulaHelper.period_to_str(dtime, false) + else + "" + end + end + + default :VM_ID, :NAME, :STAT, :UCPU, :UMEM, :HOST, :TIME +end + +# List the services. This method is used in top and list commands +# @param [Service::Client] client +# @param [Array] args +# @param [Hash] options +# @return [[Integer, String], Integer] Returns the exit_code and optionally +# a String to be printed +def list_services(client, args, options) + response = client.get(RESOURCE_PATH) + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + #[0,response.body] + if options[:json] + [0,response.body] + else + array_list = JSON.parse(response.body) + SERVICE_TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT']) + 0 + end + end +end + +# Show the service information. This method is used in top and show commands +# @param [Service::Client] client +# @param [Array] args +# @param [Hash] options +# @return [[Integer, String], Integer] Returns the exit_code and optionally +# a String to be printed +def show_service(client, args, options) + response = client.get("#{RESOURCE_PATH}/#{args[0]}") + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + #[0,response.body] + if options[:json] + [0,response.body] + else + str="%-20s: %-20s" + str_h1="%-80s" + + document_hash = JSON.parse(response.body) + template = document_hash['DOCUMENT']['TEMPLATE']['BODY'] + + CLIHelper.print_header(str_h1 % "SERVICE #{document_hash['DOCUMENT']['ID']} INFORMATION") + + puts str % ["ID", document_hash['DOCUMENT']['ID']] + puts str % ["NAME", document_hash['DOCUMENT']['NAME']] + puts str % ["USER", document_hash['DOCUMENT']['UNAME']] + puts str % ["GROUP",document_hash['DOCUMENT']['GNAME']] + + puts str % ["STRATEGY", template['deployment']] + puts str % ["SERVICE STATE", Service.state_str(template['state'])] + puts str % ["SHUTDOWN", template['shutdown_action']] if template['shutdown_action'] + + puts + + CLIHelper.print_header(str_h1 % "PERMISSIONS",false) + + ["OWNER", "GROUP", "OTHER"].each { |e| + mask = "---" + mask[0] = "u" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_U"] == "1" + mask[1] = "m" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_M"] == "1" + mask[2] = "a" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_A"] == "1" + + puts str % [e, mask] + } + + puts + + template['roles'].each {|role| + CLIHelper.print_header("ROLE #{role['name']}", false) + + puts str % ["ROLE STATE", Role.state_str(role['state'])] + puts str % ["PARENTS", role['parents'].join(', ')] if role['parents'] + puts str % ["VM TEMPLATE", role['vm_template']] + puts str % ["CARNIDALITY", role['cardinality']] + puts str % ["MIN VMS", role['min_vms']] if role['min_vms'] + puts str % ["MAX VMS", role['max_vms']] if role['max_vms'] + puts str % ["SHUTDOWN", role['shutdown_action']] if role['shutdown_action'] + + puts "NODES INFORMATION" + NODE_TABLE.show(role['nodes']) + + if !role['elasticity_policies'].nil? || !role['scheduled_policies'].nil? + puts + puts "ELASTICITY RULES" + puts str % ["COOLDOWN", "#{role['cooldown']}s"] if role['cooldown'] + + if role['elasticity_policies'] + puts +# puts "ELASTICITY POLICIES" + CLIHelper::ShowTable.new(nil, self) do + column :ADJUST, "", :left, :size=>12 do |d| + adjust_str(d) + end + + column :EXPRESSION, "", :left, :size=>48 do |d| + if !d['expression_evaluated'].nil? + d['expression_evaluated'] + else + d['expression'] + end + end + + column :'EVALS', "", :left, :size=>5 do |d| + "#{d['true_evals'].to_i} / #{d['period_number']}" + end + + column :PERIOD, "", :size=>6 do |d| + "#{d['period']}s" + end + + column :COOL, "", :size=>5 do |d| + d['cooldown'] ? "#{d['cooldown']}s" : '-' + end + + default :ADJUST, :EXPRESSION, :EVALS, :PERIOD, :COOL + end.show([role['elasticity_policies']].flatten, {}) + end + + if role['scheduled_policies'] + puts +# puts "SCHEDULED POLICIES" + CLIHelper::ShowTable.new(nil, self) do + column :ADJUST, "", :left, :size=>12 do |d| + adjust_str(d) + end + + column :TIME, "", :left, :size=>67 do |d| + if d['start_time'] + Time.parse(d['start_time']).to_s + else + d['recurrence'] + end + end + + default :ADJUST, :TIME + end.show([role['scheduled_policies']].flatten, {}) + end + end + + puts + } + + puts + + CLIHelper.print_header(str_h1 % "LOG MESSAGES",false) + + if template['log'] + template['log'].each { |log| + t = Time.at(log['timestamp']).strftime("%m/%d/%y %H:%M") + puts "#{t} [#{log['severity']}] #{log['message']}" + } + end + + 0 + end + end +end + +def adjust_str(policy) + sign = policy['adjust'].to_i >= 0 ? "+" : "-" + adjust = policy['adjust'].to_i.abs + + case policy['type'] + when 'CARDINALITY' + "= #{adjust}" + when 'PERCENTAGE_CHANGE' + st = "#{sign} #{adjust} %" + if policy['min_adjust_step'] + st << " (#{policy['min_adjust_step']})" + end + + st + else + "#{sign} #{adjust}" + end +end + +# +# Commands +# + +cmd=CommandParser::CmdParser.new(ARGV) do + usage "`oneflow` [] []" + version OpenNebulaHelper::ONE_VERSION + + set :option, Service::DEFAULT_OPTIONS + set :option, CommandParser::VERSION + set :option, CommandParser::HELP + + # + # Formatters for arguments + # + set :format, :groupid, OpenNebulaHelper.rname_to_id_desc("GROUP") do |arg| + OpenNebulaHelper.rname_to_id(arg, "GROUP") + end + + set :format, :userid, OpenNebulaHelper.rname_to_id_desc("USER") do |arg| + OpenNebulaHelper.rname_to_id(arg, "USER") + end + + set :format, :service_id, Service.rname_to_id_desc("SERVICE") do |arg| + Service.rname_to_id(arg, "SERVICE") + end + + set :format, :service_id_list, Service.list_to_id_desc("SERVICE") do |arg| + Service.list_to_id(arg, "SERVICE") + end + + set :format, :vm_action, "Actions supported: #{Role::SCHEDULE_ACTIONS.join(', ')}" do |arg| + if Role::SCHEDULE_ACTIONS.include?(arg) + [0, arg] + else + [-1, "Action #{arg} is not supported. Actions supported: #{Role::SCHEDULE_ACTIONS.join(', ')}"] + end + end + + # + # List + # + + list_desc = <<-EOT.unindent + List the available services + EOT + + command :list, list_desc, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + list_services(client, args, options) + end + + # + # Show + # + + show_desc = <<-EOT.unindent + Show detailed information of a given service + EOT + + command :show, show_desc, :service_id, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + show_service(client, args, options) + end + + # + # Top + # + + top_desc = <<-EOT.unindent + Top the services or the extended information of the target service if a + id is specified + EOT + + command :top, top_desc, [:service_id, nil], + :options => [Service::JSON_FORMAT, Service::TOP, CLIHelper::DELAY] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + delay=options[:delay] ? options[:delay] : 3 + + begin + while true + CLIHelper.scr_cls + CLIHelper.scr_move(0,0) + + if args[0] + rc, message = show_service(client, args, options) + + if rc != 0 + raise message + end + else + rc, message = list_services(client, args, options) + + if rc != 0 + raise message + end + end + + sleep delay + end + rescue Exception => e + puts e.message + -1 + end + end + + # + # Delete + # + + delete_desc = <<-EOT.unindent + Delete a given service + EOT + + command :delete, delete_desc, [:range, :service_id_list] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + client.delete("#{RESOURCE_PATH}/#{service_id}") + } + end + + # + # Shutdown + # + + shutdown_desc = <<-EOT.unindent + Shutdown a service. + From RUNNING or WARNING shuts down the Service + EOT + + command :shutdown, shutdown_desc, [:range, :service_id_list] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + json_action = Service.build_json_action('shutdown') + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + # + # Recover + # + + recover_desc = <<-EOT.unindent + Recover a failed service, cleaning the failed VMs. + From FAILED_DEPLOYING continues deploying the Service + From FAILED_SCALING continues scaling the Service + From FAILED_UNDEPLOYING continues shutting down the Service + From COOLDOWN the Service is set to running ignoring the cooldown duration + From WARNING failed VMs are deleted, and new VMs are instantiated + EOT + + command :recover, recover_desc, [:range, :service_id_list] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + json_action = Service.build_json_action('recover') + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + # + # Scale + # + + scale_desc = <<-EOT.unindent + Scale a role to the given cardinality + EOT + + command :'scale', scale_desc, :service_id, :role_name, + :cardinality, :options => [Service::FORCE] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + if !(args[2] =~ /^\d+$/) + puts "Cardinality must be an integer number" + exit -1 + end + + exit_code = 0 + + json = "{ \"cardinality\" : #{args[2]},\n"<< + " \"force\" : #{options[:force] == true} }" + + response = client.put("#{RESOURCE_PATH}/#{args[0]}/role/#{args[1]}", json) + + if CloudClient::is_error?(response) + puts response.to_s + exit_code = response.code.to_i + end + + exit_code + end + + chgrp_desc = <<-EOT.unindent + Changes the service group + EOT + + command :chgrp, chgrp_desc, [:range, :service_id_list], :groupid do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['group_id'] = args[1].to_i + + json_action = Service.build_json_action('chgrp', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + chown_desc = <<-EOT.unindent + Changes the service owner and group + EOT + + command :chown, chown_desc, [:range, :service_id_list], :userid, [:groupid, nil] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['owner_id'] = args[1] + params['group_id'] = args[2] if args[2] + + json_action = Service.build_json_action('chown', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + chmod_desc = <<-EOT.unindent + Changes the service permissions + EOT + + command :chmod, chmod_desc, [:range, :service_id_list], :octet do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['octet'] = args[1] + + json_action = Service.build_json_action('chmod', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + action_desc = <<-EOT.unindent + Perform an action on all the Virtual Machines of a given role. + Actions supported: #{Role::SCHEDULE_ACTIONS.join(",")} + EOT + + command :"action", action_desc, :service_id, :role_name, :vm_action, + :options => [Service::PERIOD, Service::NUMBER] do + + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions([args[0]]) { |service_id| + params = Hash.new + params[:period] = options[:period].to_i if options[:period] + params[:number] = options[:number].to_i if options[:number] + + json_action = Service.build_json_action(args[2], params) + + client.post("#{RESOURCE_PATH}/#{service_id}/role/#{args[1]}/action", json_action) + } + end +end diff --git a/src/cli/oneflow-template b/src/cli/oneflow-template new file mode 100755 index 0000000000..81ef339794 --- /dev/null +++ b/src/cli/oneflow-template @@ -0,0 +1,451 @@ +#!/usr/bin/env ruby + +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +ONE_LOCATION=ENV["ONE_LOCATION"] + +if !ONE_LOCATION + RUBY_LIB_LOCATION="/usr/lib/one/ruby" +else + RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby" +end + +$: << RUBY_LIB_LOCATION +$: << RUBY_LIB_LOCATION+'/cli' + +require 'command_parser' +require 'opennebula/oneflow_client' + +require 'cli_helper' +require 'one_helper' + +require 'json' + +USER_AGENT = "CLI" + +# Base Path representing the resource to be used in the requests +RESOURCE_PATH = '/service_template' + +# +# Table +# + +TABLE = CLIHelper::ShowTable.new(nil, self) do + column :ID, "ID", :size=>10 do |d| + d["ID"] + end + + column :USER, "Username", :left, :size=>15 do |d| + d["UNAME"] + end + + column :GROUP, "Group", :left, :size=>15 do |d| + d["GNAME"] + end + + column :NAME, "Name", :left, :size=>37 do |d| + d["NAME"] + end + + default :ID, :USER, :GROUP, :NAME +end + + +# Show the service template information. This method is used in top and +# show commands +# @param [Service::Client] client +# @param [Array] args +# @param [Hash] options +# @return [[Integer, String], Integer] Returns the exit_code and optionally +# a String to be printed +def show_service_template(client, args, options) + response = client.get("#{RESOURCE_PATH}/#{args[0]}") + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + #[0,response.body] + if options[:json] + [0,response.body] + else + str="%-20s: %-20s" + str_h1="%-80s" + + document_hash = JSON.parse(response.body) + template = document_hash['DOCUMENT']['TEMPLATE']['BODY'] + + CLIHelper.print_header(str_h1 % + "SERVICE TEMPLATE #{document_hash['DOCUMENT']['ID']} INFORMATION") + + puts str % ["ID", document_hash['DOCUMENT']['ID']] + puts str % ["NAME", document_hash['DOCUMENT']['NAME']] + puts str % ["USER", document_hash['DOCUMENT']['UNAME']] + puts str % ["GROUP",document_hash['DOCUMENT']['GNAME']] + + puts + + CLIHelper.print_header(str_h1 % "PERMISSIONS",false) + + ["OWNER", "GROUP", "OTHER"].each { |e| + mask = "---" + mask[0] = "u" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_U"] == "1" + mask[1] = "m" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_M"] == "1" + mask[2] = "a" if document_hash['DOCUMENT']['PERMISSIONS']["#{e}_A"] == "1" + + puts str % [e, mask] + } + + puts + + CLIHelper.print_header(str_h1 % "TEMPLATE CONTENTS",false) + puts JSON.pretty_generate(template) + + 0 + end + end +end + +# List the services. This method is used in top and list commands +# @param [Service::Client] client +# @param [Array] args +# @param [Hash] options +# @return [[Integer, String], Integer] Returns the exit_code and optionally +# a String to be printed +def list_service_templates(client, args, options) + response = client.get(RESOURCE_PATH) + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + #[0,response.body] + if options[:json] + [0,response.body] + else + array_list = JSON.parse(response.body) + TABLE.show(array_list['DOCUMENT_POOL']['DOCUMENT']) + 0 + end + end +end + +# +# Commands +# + +cmd=CommandParser::CmdParser.new(ARGV) do + usage "`oneflow-template` [] []" + version OpenNebulaHelper::ONE_VERSION + + set :option, Service::DEFAULT_OPTIONS + set :option, CommandParser::VERSION + set :option, CommandParser::HELP + + # + # Formatters for arguments + # + set :format, :groupid, OpenNebulaHelper.rname_to_id_desc("GROUP") do |arg| + OpenNebulaHelper.rname_to_id(arg, "GROUP") + end + + set :format, :userid, OpenNebulaHelper.rname_to_id_desc("USER") do |arg| + OpenNebulaHelper.rname_to_id(arg, "USER") + end + + set :format, :template_id, Service.rname_to_id_desc("SERVICE TEMPLATE") do |arg| + Service.rname_to_id(arg, "SERVICE TEMPLATE") + end + + set :format, :templateid_list, Service.list_to_id_desc("SERVICE TEMPLATE") do |arg| + Service.list_to_id(arg, "SERVICE TEMPLATE") + end + + # + # List + # + + list_desc = <<-EOT.unindent + List the available Service Templates + EOT + + command :list, list_desc, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + list_service_templates(client, args, options) + end + + # + # Top + # + + top_desc = <<-EOT.unindent + List the available Service Templates continuously + EOT + + command :top, top_desc, + :options => [Service::JSON_FORMAT, Service::TOP, CLIHelper::DELAY] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + delay=options[:delay] ? options[:delay] : 3 + + begin + while true + CLIHelper.scr_cls + CLIHelper.scr_move(0,0) + + rc, message = list_service_templates(client, args, options) + + if rc != 0 + raise message + end + + sleep delay + end + rescue Exception => e + puts e.message + -1 + end + end + + # + # Create + # + + create_desc = <<-EOT.unindent + Create a new Service Template + EOT + + command :create, create_desc, :file, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + response = client.post(RESOURCE_PATH, File.read(args[0])) + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + if options[:json] + [0,response.body] + else + template = JSON.parse(response.body) + puts "ID: #{template['DOCUMENT']['ID']}" + 0 + end + end + end + + # + # Show + # + + show_desc = <<-EOT.unindent + Show detailed information of a given Service Template + EOT + + command :show, show_desc, :template_id, :options => Service::JSON_FORMAT do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + show_service_template(client, args, options) + end + + # + # Delete + # + + delete_desc = <<-EOT.unindent + Delete a given Service Template + EOT + + command :delete, delete_desc, [:range, :templateid_list] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |template_id| + client.delete("#{RESOURCE_PATH}/#{template_id}") + } + end + + # + # Instantiate + # + + instantiate_desc = <<-EOT.unindent + Instantiate a Service Template + EOT + + command :instantiate, instantiate_desc, :template_id, + :options => [Service::JSON_FORMAT, Service::TOP] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + json_str = Service.build_json_action('instantiate') + + response = client.post("#{RESOURCE_PATH}/#{args[0]}/action", json_str) + + if CloudClient::is_error?(response) + [response.code.to_i, response.to_s] + else + if options[:json] + [0,response.body] + else + template = JSON.parse(response.body) + puts "ID: #{template['DOCUMENT']['ID']}" + 0 + end + end + end + + chgrp_desc = <<-EOT.unindent + Changes the service template group + EOT + + command :chgrp, chgrp_desc, [:range, :templateid_list], :groupid do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['group_id'] = args[1].to_i + + json_action = Service.build_json_action('chgrp', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + chown_desc = <<-EOT.unindent + Changes the service template owner and group + EOT + + command :chown, chown_desc, [:range, :templateid_list], :userid, [:groupid, nil] do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['owner_id'] = args[1] + params['group_id'] = args[2] if args[2] + + json_action = Service.build_json_action('chown', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + chmod_desc = <<-EOT.unindent + Changes the service template permissions + EOT + + command :chmod, chmod_desc, [:range, :templateid_list], :octet do + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + Service.perform_actions(args[0]) { |service_id| + params = Hash.new + params['octet'] = args[1] + + json_action = Service.build_json_action('chmod', params) + + client.post("#{RESOURCE_PATH}/#{service_id}/action", json_action) + } + end + + update_desc = <<-EOT.unindent + Update the template contents. If a path is not provided the editor will + be launched to modify the current content. + EOT + + command :update, update_desc, :templateid, [:file, nil] do + template_id = args[0] + client = Service::Client.new( + :username => options[:username], + :password => options[:password], + :url => options[:server], + :user_agent => USER_AGENT) + + if args[1] + path = args[1] + else + require 'tempfile' + + tmp = Tempfile.new(template_id.to_s) + path = tmp.path + + response = client.get("#{RESOURCE_PATH}/#{template_id}") + + if CloudClient::is_error?(response) + exit_with_code response.code.to_i, response.to_s + else + document_hash = JSON.parse(response.body) + template = document_hash['DOCUMENT']['TEMPLATE']['BODY'] + + tmp << JSON.pretty_generate(template) + tmp.flush + + editor_path = ENV["EDITOR"] ? ENV["EDITOR"] : OpenNebulaHelper::EDITOR_PATH + system("#{editor_path} #{path}") + + unless $?.exitstatus == 0 + puts "Editor not defined" + exit -1 + end + + tmp.close + end + end + + exit_code = 0 + + t_str = File.read(path) + response = client.put("#{RESOURCE_PATH}/#{template_id}", t_str) + if CloudClient::is_error?(response) + puts response.to_s + exit_code = response.code.to_i + end + + exit_code + end +end \ No newline at end of file diff --git a/src/flow/Gemfile b/src/flow/Gemfile new file mode 100644 index 0000000000..0335249ddc --- /dev/null +++ b/src/flow/Gemfile @@ -0,0 +1,7 @@ +source "http://rubygems.org" + +gem 'sinatra' +gem 'json' +gem 'xml-simple' +gem 'treetop' +gem 'parse-cron' diff --git a/src/flow/Gemfile.lock b/src/flow/Gemfile.lock new file mode 100644 index 0000000000..886395dcfe --- /dev/null +++ b/src/flow/Gemfile.lock @@ -0,0 +1,28 @@ +GEM + remote: http://rubygems.org/ + specs: + json (1.7.3) + parse-cron (0.1.2) + polyglot (0.3.3) + rack (1.4.1) + rack-protection (1.2.0) + rack + sinatra (1.3.2) + rack (~> 1.3, >= 1.3.6) + rack-protection (~> 1.2) + tilt (~> 1.3, >= 1.3.3) + tilt (1.3.3) + treetop (1.4.12) + polyglot + polyglot (>= 0.3.1) + xml-simple (1.1.1) + +PLATFORMS + ruby + +DEPENDENCIES + json + parse-cron + sinatra + treetop + xml-simple diff --git a/src/flow/bin/oneflow-server b/src/flow/bin/oneflow-server new file mode 100755 index 0000000000..4ffda4763d --- /dev/null +++ b/src/flow/bin/oneflow-server @@ -0,0 +1,123 @@ +#! /bin/sh + +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +if [ -z "$ONE_LOCATION" ]; then + FLOW_PID=/var/run/one/oneflow.pid + FLOW_SERVER=/usr/lib/one/oneflow/oneflow-server.rb + FLOW_LOCK_FILE=/var/lock/one/.oneflow.lock + FLOW_LOG=/var/log/one/oneflow.log + FLOW_LOG_ERROR=/var/log/one/oneflow.error + FLOW_CONF=/etc/one/oneflow-server.conf +else + FLOW_PID=$ONE_LOCATION/var/oneflow.pid + FLOW_SERVER=$ONE_LOCATION/lib/oneflow/oneflow-server.rb + FLOW_LOCK_FILE=$ONE_LOCATION/var/.oneflow.lock + FLOW_LOG=$ONE_LOCATION/var/oneflow.log + FLOW_LOG_ERROR=$ONE_LOCATION/var/oneflow.error + FLOW_CONF=$ONE_LOCATION/etc/oneflow-server.conf +fi + +setup() +{ + if [ -f $FLOW_LOCK_FILE ]; then + if [ -f $FLOW_PID ]; then + FLOWPID=`cat $FLOW_PID` + ps $FLOWPID > /dev/null 2>&1 + if [ $? -eq 0 ]; then + echo "OneFlow Server is still running (PID:$FLOWPID)." + echo "Please try 'oneflow-server stop' first." + exit 1 + fi + fi + echo "Stale .lock detected. Erasing it." + rm $FLOW_LOCK_FILE + fi +} + +start() +{ + if [ ! -f "$FLOW_SERVER" ]; then + echo "Cannot find $FLOW_SERVER." + exit 1 + fi + + touch $FLOW_LOCK_FILE + + # Start the oneflow-server daemon + ruby $FLOW_SERVER >$FLOW_LOG 2>$FLOW_LOG_ERROR & + + LASTRC=$? + LASTPID=$! + + if [ $LASTRC -ne 0 ]; then + echo "Error executing oneflow-server." + echo "Check $FLOW_LOG_ERROR and $FLOW_LOG for more information" + exit 1 + else + echo $LASTPID > $FLOW_PID + fi + + sleep 2 + ps $LASTPID > /dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo "Error executing oneflow-server." + echo "Check $FLOW_LOG_ERROR and $FLOW_LOG for more information" + exit 1 + fi + + echo "oneflow-server started" +} + +# +# Function that stops the daemon/service +# +stop() +{ + if [ ! -f $FLOW_PID ]; then + echo "Couldn't find oneflow-server process pid." + exit 1 + fi + + # Kill the oneflow-server daemon + + kill -INT `cat $FLOW_PID` > /dev/null 2>&1 + + # Remove pid files + rm -f $FLOW_PID > /dev/null 2>&1 + rm -f $FLOW_LOCK_FILE &> /dev/null + + echo "oneflow-server stopped" +} + + +case "$1" in + start) + setup + start + ;; + stop) + stop + ;; + *) + echo "Usage: oneflow-server {start|stop}" >&2 + exit 3 + ;; +esac + + diff --git a/src/flow/config.ru b/src/flow/config.ru new file mode 100644 index 0000000000..19ee3e578f --- /dev/null +++ b/src/flow/config.ru @@ -0,0 +1,20 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +$: << '.' +require 'oneflow-server' + +run Sinatra::Application \ No newline at end of file diff --git a/src/flow/etc/oneflow-server.conf b/src/flow/etc/oneflow-server.conf new file mode 100644 index 0000000000..74370295df --- /dev/null +++ b/src/flow/etc/oneflow-server.conf @@ -0,0 +1,63 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +################################################################################ +# Server Configuration +################################################################################ + +# OpenNebula daemon contact information +# +:one_xmlrpc: http://localhost:2633/RPC2 + +# Time in seconds between Life Cycle Manager steps +# +:lcm_interval: 30 + +# Host and port where OneFlow server will run +:host: 127.0.0.1 +:port: 2474 + +################################################################################ +# Defaults +################################################################################ + +# Default cooldown period after a scale operation, in seconds +:default_cooldown: 300 + +# Default shutdown action. Values: 'shutdown', 'shutdown-hard' +:shutdown_action: 'shutdown' + +# Default oneflow action options when only one is supplied +:action_number: 1 +:action_period: 60 + +############################################################# +# Auth +############################################################# + +# Authentication driver to communicate with OpenNebula core +# - cipher, for symmetric cipher encryption of tokens +# - x509, for x509 certificate encryption of tokens +:core_auth: cipher + +################################################################################ +# Log +################################################################################ + +# Log debug level +# 0 = ERROR, 1 = WARNING, 2 = INFO, 3 = DEBUG +# +:debug_level: 2 \ No newline at end of file diff --git a/src/flow/lib/LifeCycleManager.rb b/src/flow/lib/LifeCycleManager.rb new file mode 100644 index 0000000000..212cc4af91 --- /dev/null +++ b/src/flow/lib/LifeCycleManager.rb @@ -0,0 +1,174 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'strategy' + +class ServiceLCM + + LOG_COMP = "LCM" + + def initialize(sleep_time, cloud_auth) + @sleep_time = sleep_time + @cloud_auth = cloud_auth + end + + def loop() + Log.info LOG_COMP, "Starting Life Cycle Manager" + + while true + srv_pool = ServicePool.new(@cloud_auth.client) + + rc = srv_pool.info_all() + + if OpenNebula.is_error?(rc) + Log.error LOG_COMP, "Error retrieving the Service Pool: #{rc.message}" + else + srv_pool.each_xpath('DOCUMENT/ID') { |id| + + service = srv_pool.get(id.to_i) { |service| + + owner_client = @cloud_auth.client(service.owner_name) + service.replace_client(owner_client) + + Log.debug LOG_COMP, "Loop for service #{service.id()} #{service.name()}" \ + " #{service.state_str()} #{service.strategy()}" + + strategy = get_deploy_strategy(service) + + case service.state() + when Service::STATE['PENDING'] + service.set_state(Service::STATE['DEPLOYING']) + + rc = strategy.boot_step(service) + if !rc[0] + service.set_state(Service::STATE['FAILED_DEPLOYING']) + end + when Service::STATE['DEPLOYING'] + strategy.monitor_step(service) + + if service.all_roles_running? + service.set_state(Service::STATE['RUNNING']) + elsif service.any_role_failed? + service.set_state(Service::STATE['FAILED_DEPLOYING']) + else + rc = strategy.boot_step(service) + if !rc[0] + service.set_state(Service::STATE['FAILED_DEPLOYING']) + end + end + when Service::STATE['RUNNING'], Service::STATE['WARNING'] + strategy.monitor_step(service) + + if service.all_roles_running? + if service.state() == Service::STATE['WARNING'] + service.set_state(Service::STATE['RUNNING']) + end + else + if service.state() == Service::STATE['RUNNING'] + service.set_state(Service::STATE['WARNING']) + end + end + + if strategy.apply_scaling_policies(service) + service.set_state(Service::STATE['SCALING']) + + rc = strategy.scale_step(service) + if !rc[0] + service.set_state(Service::STATE['FAILED_SCALING']) + end + end + when Service::STATE['SCALING'] + strategy.monitor_step(service) + + if service.any_role_failed_scaling? + service.set_state(Service::STATE['FAILED_SCALING']) + elsif service.any_role_cooldown? + service.set_state(Service::STATE['COOLDOWN']) + elsif !service.any_role_scaling? + service.set_state(Service::STATE['RUNNING']) + else + rc = strategy.scale_step(service) + if !rc[0] + service.set_state(Service::STATE['FAILED_SCALING']) + end + end + when Service::STATE['COOLDOWN'] + strategy.monitor_step(service) + + if !service.any_role_cooldown? + service.set_state(Service::STATE['RUNNING']) + end + when Service::STATE['FAILED_SCALING'] + strategy.monitor_step(service) + + if !service.any_role_failed_scaling? + service.set_state(Service::STATE['SCALING']) + end + when Service::STATE['UNDEPLOYING'] + strategy.monitor_step(service) + + if service.all_roles_done? + service.set_state(Service::STATE['DONE']) + elsif service.any_role_failed? + service.set_state(Service::STATE['FAILED_UNDEPLOYING']) + else + rc = strategy.shutdown_step(service) + if !rc[0] + service.set_state(Service::STATE['FAILED_UNDEPLOYING']) + end + end + when Service::STATE['FAILED_DEPLOYING'] + strategy.monitor_step(service) + + if !service.any_role_failed? + service.set_state(Service::STATE['DEPLOYING']) + end + when Service::STATE['FAILED_UNDEPLOYING'] + strategy.monitor_step(service) + + if !service.any_role_failed? + service.set_state(Service::STATE['UNDEPLOYING']) + end + end + + rc = service.update() + if OpenNebula.is_error?(rc) + Log.error LOG_COMP, "Error trying to update " << + "Service #{service.id()} : #{rc.message}" + end + } + } + end + + sleep @sleep_time + end + end + +private + # Returns the deployment strategy for the given Service + # @param [Service] service the service + # @return [Strategy] the deployment Strategy + def get_deploy_strategy(service) + strategy = Strategy.new + + case service.strategy + when 'straight' + strategy.extend(Straight) + end + + return strategy + end +end diff --git a/src/flow/lib/grammar.rb b/src/flow/lib/grammar.rb new file mode 100644 index 0000000000..dfa3facbb8 --- /dev/null +++ b/src/flow/lib/grammar.rb @@ -0,0 +1,1153 @@ +# Autogenerated from a Treetop grammar. Edits may be lost. + + +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +module ElasticityGrammar + include Treetop::Runtime + + def root + @root ||= :expression + end + + module Expression0 + def space1 + elements[0] + end + + def exp + elements[1] + end + + def space2 + elements[2] + end + end + + module Expression1 + def result(role) + return exp.result(role) + end + end + + def _nt_expression + start_index = index + if node_cache[:expression].has_key?(index) + cached = node_cache[:expression][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0, s0 = index, [] + r1 = _nt_space + s0 << r1 + if r1 + i2 = index + r3 = _nt_boolean_exp + if r3 + r2 = r3 + else + r4 = _nt_logic_cond + if r4 + r2 = r4 + else + @index = i2 + r2 = nil + end + end + s0 << r2 + if r2 + r5 = _nt_space + s0 << r5 + end + end + if s0.last + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + r0.extend(Expression0) + r0.extend(Expression1) + else + @index = i0 + r0 = nil + end + + node_cache[:expression][start_index] = r0 + + r0 + end + + module BooleanExp0 + def left + elements[0] + end + + def space1 + elements[1] + end + + def op + elements[2] + end + + def space2 + elements[3] + end + + def right + elements[4] + end + end + + module BooleanExp1 + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{op.text_value} #{r_st}" + val = l_val && r_val + + return [val, st] + end + end + + module BooleanExp2 + def left + elements[0] + end + + def space1 + elements[1] + end + + def op + elements[2] + end + + def space2 + elements[3] + end + + def right + elements[4] + end + end + + module BooleanExp3 + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{op.text_value} #{r_st}" + val = l_val || r_val + + return [val, st] + end + end + + def _nt_boolean_exp + start_index = index + if node_cache[:boolean_exp].has_key?(index) + cached = node_cache[:boolean_exp][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + r2 = _nt_logic_cond + s1 << r2 + if r2 + r3 = _nt_space + s1 << r3 + if r3 + i4 = index + if has_terminal?('&&', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('&&') + r5 = nil + end + if r5 + r4 = r5 + else + if has_terminal?('&', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('&') + r6 = nil + end + if r6 + r4 = r6 + else + @index = i4 + r4 = nil + end + end + s1 << r4 + if r4 + r7 = _nt_space + s1 << r7 + if r7 + r8 = _nt_expression + s1 << r8 + end + end + end + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(BooleanExp0) + r1.extend(BooleanExp1) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + else + i9, s9 = index, [] + r10 = _nt_logic_cond + s9 << r10 + if r10 + r11 = _nt_space + s9 << r11 + if r11 + i12 = index + if has_terminal?('||', false, index) + r13 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('||') + r13 = nil + end + if r13 + r12 = r13 + else + if has_terminal?('|', false, index) + r14 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('|') + r14 = nil + end + if r14 + r12 = r14 + else + @index = i12 + r12 = nil + end + end + s9 << r12 + if r12 + r15 = _nt_space + s9 << r15 + if r15 + r16 = _nt_expression + s9 << r16 + end + end + end + end + if s9.last + r9 = instantiate_node(SyntaxNode,input, i9...index, s9) + r9.extend(BooleanExp2) + r9.extend(BooleanExp3) + else + @index = i9 + r9 = nil + end + if r9 + r0 = r9 + else + @index = i0 + r0 = nil + end + end + + node_cache[:boolean_exp][start_index] = r0 + + r0 + end + + module LogicCond0 + def left + elements[0] + end + + def space1 + elements[1] + end + + def comp_op + elements[2] + end + + def space2 + elements[3] + end + + def right + elements[4] + end + end + + module LogicCond1 + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{comp_op.text_value} #{r_st}" + + if l_val.nil? || r_val.nil? + # An attribute was not found, we return false instead + # of assuming a value of 0 + + val = false + else + val = comp_op.apply(l_val, r_val) + end + + return [val, st] + end + end + + module LogicCond2 + def space + elements[1] + end + + def expression + elements[2] + end + end + + module LogicCond3 + def result(role) + e_val, e_st = expression.result(role) + + val = !e_val + st = "!#{e_st}" + + return [val, st] + end + end + + module LogicCond4 + def space1 + elements[1] + end + + def expression + elements[2] + end + + def space2 + elements[3] + end + + end + + module LogicCond5 + def result(role) + e_val, e_st = expression.result(role) + + st = "(#{e_st})" + + return [e_val, st] + end + end + + def _nt_logic_cond + start_index = index + if node_cache[:logic_cond].has_key?(index) + cached = node_cache[:logic_cond][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + r2 = _nt_operand + s1 << r2 + if r2 + r3 = _nt_space + s1 << r3 + if r3 + r4 = _nt_comp_op + s1 << r4 + if r4 + r5 = _nt_space + s1 << r5 + if r5 + r6 = _nt_operand + s1 << r6 + end + end + end + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(LogicCond0) + r1.extend(LogicCond1) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + else + i7, s7 = index, [] + if has_terminal?('!', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('!') + r8 = nil + end + s7 << r8 + if r8 + r9 = _nt_space + s7 << r9 + if r9 + r10 = _nt_expression + s7 << r10 + end + end + if s7.last + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + r7.extend(LogicCond2) + r7.extend(LogicCond3) + else + @index = i7 + r7 = nil + end + if r7 + r0 = r7 + else + i11, s11 = index, [] + if has_terminal?('(', false, index) + r12 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('(') + r12 = nil + end + s11 << r12 + if r12 + r13 = _nt_space + s11 << r13 + if r13 + r14 = _nt_expression + s11 << r14 + if r14 + r15 = _nt_space + s11 << r15 + if r15 + if has_terminal?(')', false, index) + r16 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(')') + r16 = nil + end + s11 << r16 + end + end + end + end + if s11.last + r11 = instantiate_node(SyntaxNode,input, i11...index, s11) + r11.extend(LogicCond4) + r11.extend(LogicCond5) + else + @index = i11 + r11 = nil + end + if r11 + r0 = r11 + else + @index = i0 + r0 = nil + end + end + end + + node_cache[:logic_cond][start_index] = r0 + + r0 + end + + module CompOp0 + def apply(a,b) + a == b + end + end + + module CompOp1 + def apply(a,b) + a != b + end + end + + module CompOp2 + def apply(a,b) + a >= b + end + end + + module CompOp3 + def apply(a,b) + a > b + end + end + + module CompOp4 + def apply(a,b) + a <= b + end + end + + module CompOp5 + def apply(a,b) + a < b + end + end + + def _nt_comp_op + start_index = index + if node_cache[:comp_op].has_key?(index) + cached = node_cache[:comp_op][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1 = index + if has_terminal?('==', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('==') + r2 = nil + end + if r2 + r1 = r2 + r1.extend(CompOp0) + else + if has_terminal?('=', false, index) + r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('=') + r3 = nil + end + if r3 + r1 = r3 + r1.extend(CompOp0) + else + @index = i1 + r1 = nil + end + end + if r1 + r0 = r1 + else + i4 = index + if has_terminal?('!=', false, index) + r5 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('!=') + r5 = nil + end + if r5 + r4 = r5 + r4.extend(CompOp1) + else + if has_terminal?('<>', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 2)) + @index += 2 + else + terminal_parse_failure('<>') + r6 = nil + end + if r6 + r4 = r6 + r4.extend(CompOp1) + else + @index = i4 + r4 = nil + end + end + if r4 + r0 = r4 + else + if has_terminal?('>=', false, index) + r7 = instantiate_node(SyntaxNode,input, index...(index + 2)) + r7.extend(CompOp2) + @index += 2 + else + terminal_parse_failure('>=') + r7 = nil + end + if r7 + r0 = r7 + else + if has_terminal?('>', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) + r8.extend(CompOp3) + @index += 1 + else + terminal_parse_failure('>') + r8 = nil + end + if r8 + r0 = r8 + else + if has_terminal?('<=', false, index) + r9 = instantiate_node(SyntaxNode,input, index...(index + 2)) + r9.extend(CompOp4) + @index += 2 + else + terminal_parse_failure('<=') + r9 = nil + end + if r9 + r0 = r9 + else + if has_terminal?('<', false, index) + r10 = instantiate_node(SyntaxNode,input, index...(index + 1)) + r10.extend(CompOp5) + @index += 1 + else + terminal_parse_failure('<') + r10 = nil + end + if r10 + r0 = r10 + else + @index = i0 + r0 = nil + end + end + end + end + end + end + + node_cache[:comp_op][start_index] = r0 + + r0 + end + + module Operand0 + def result + number.result(role) + end + end + + module Operand1 + def result + variable.result(role) + end + end + + def _nt_operand + start_index = index + if node_cache[:operand].has_key?(index) + cached = node_cache[:operand][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + r1 = _nt_number + if r1 + r0 = r1 + else + r2 = _nt_variable + if r2 + r0 = r2 + else + @index = i0 + r0 = nil + end + end + + node_cache[:operand][start_index] = r0 + + r0 + end + + module Number0 + end + + module Number1 + def result(role) + val = text_value.to_f + st = val.to_s + + return [val, st] + end + end + + module Number2 + end + + module Number3 + def result(role) + val = text_value.to_i + st = val.to_s + + return [val, st] + end + end + + def _nt_number + start_index = index + if node_cache[:number].has_key?(index) + cached = node_cache[:number][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + if has_terminal?('-', false, index) + r3 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('-') + r3 = nil + end + if r3 + r2 = r3 + else + r2 = instantiate_node(SyntaxNode,input, index...index) + end + s1 << r2 + if r2 + s4, i4 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r5 = true + @index += 1 + else + r5 = nil + end + if r5 + s4 << r5 + else + break + end + end + if s4.empty? + @index = i4 + r4 = nil + else + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + end + s1 << r4 + if r4 + if has_terminal?('.', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('.') + r6 = nil + end + s1 << r6 + if r6 + s7, i7 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r8 = true + @index += 1 + else + r8 = nil + end + if r8 + s7 << r8 + else + break + end + end + if s7.empty? + @index = i7 + r7 = nil + else + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + end + s1 << r7 + end + end + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(Number0) + r1.extend(Number1) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + else + i9, s9 = index, [] + if has_terminal?('-', false, index) + r11 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('-') + r11 = nil + end + if r11 + r10 = r11 + else + r10 = instantiate_node(SyntaxNode,input, index...index) + end + s9 << r10 + if r10 + s12, i12 = [], index + loop do + if has_terminal?('\G[0-9]', true, index) + r13 = true + @index += 1 + else + r13 = nil + end + if r13 + s12 << r13 + else + break + end + end + if s12.empty? + @index = i12 + r12 = nil + else + r12 = instantiate_node(SyntaxNode,input, i12...index, s12) + end + s9 << r12 + end + if s9.last + r9 = instantiate_node(SyntaxNode,input, i9...index, s9) + r9.extend(Number2) + r9.extend(Number3) + else + @index = i9 + r9 = nil + end + if r9 + r0 = r9 + else + @index = i0 + r0 = nil + end + end + + node_cache[:number][start_index] = r0 + + r0 + end + + module Variable0 + end + + module Variable1 + end + + module Variable2 + end + + module Variable3 + + def result(role) + nodes = role.get_nodes + total = 0 + n_nodes = 0 + att = text_value.upcase + + nodes.each { |node| + if node && node['vm_info'] + + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + # Use values from VMs in RUNNING only + + if vm_state != '3' || lcm_state != '3' + next + end + + if node['vm_info']['VM']['USER_TEMPLATE'][att] + total += (node['vm_info']['VM']['USER_TEMPLATE'][att]).to_f + n_nodes += 1 + elsif node['vm_info']['VM'][att] + total += (node['vm_info']['VM'][att]).to_f + n_nodes += 1 + elsif node['vm_info']['VM']['TEMPLATE'][att] + total += (node['vm_info']['VM']['TEMPLATE'][att]).to_f + n_nodes += 1 + end + end + } + + # The attribute wasn't found for any of the nodes + if n_nodes == 0 + val = nil + st = "#{att}[--]" + else + val = total / n_nodes + st = "#{att}[#{val.to_s}]" + end + + return [val, st] + end + end + + def _nt_variable + start_index = index + if node_cache[:variable].has_key?(index) + cached = node_cache[:variable][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + i0 = index + i1, s1 = index, [] + if has_terminal?('"', false, index) + r2 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('"') + r2 = nil + end + s1 << r2 + if r2 + if has_terminal?('\G[a-zA-Z]', true, index) + r3 = true + @index += 1 + else + r3 = nil + end + s1 << r3 + if r3 + s4, i4 = [], index + loop do + if has_terminal?('\G[0-9a-zA-Z_]', true, index) + r5 = true + @index += 1 + else + r5 = nil + end + if r5 + s4 << r5 + else + break + end + end + r4 = instantiate_node(SyntaxNode,input, i4...index, s4) + s1 << r4 + if r4 + if has_terminal?('"', false, index) + r6 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('"') + r6 = nil + end + s1 << r6 + end + end + end + if s1.last + r1 = instantiate_node(SyntaxNode,input, i1...index, s1) + r1.extend(Variable0) + else + @index = i1 + r1 = nil + end + if r1 + r0 = r1 + r0.extend(Variable3) + else + i7, s7 = index, [] + if has_terminal?('\'', false, index) + r8 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('\'') + r8 = nil + end + s7 << r8 + if r8 + if has_terminal?('\G[a-zA-Z]', true, index) + r9 = true + @index += 1 + else + r9 = nil + end + s7 << r9 + if r9 + s10, i10 = [], index + loop do + if has_terminal?('\G[0-9a-zA-Z_]', true, index) + r11 = true + @index += 1 + else + r11 = nil + end + if r11 + s10 << r11 + else + break + end + end + r10 = instantiate_node(SyntaxNode,input, i10...index, s10) + s7 << r10 + if r10 + if has_terminal?('\'', false, index) + r12 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure('\'') + r12 = nil + end + s7 << r12 + end + end + end + if s7.last + r7 = instantiate_node(SyntaxNode,input, i7...index, s7) + r7.extend(Variable1) + else + @index = i7 + r7 = nil + end + if r7 + r0 = r7 + r0.extend(Variable3) + else + i13, s13 = index, [] + if has_terminal?('\G[a-zA-Z]', true, index) + r14 = true + @index += 1 + else + r14 = nil + end + s13 << r14 + if r14 + s15, i15 = [], index + loop do + if has_terminal?('\G[0-9a-zA-Z_]', true, index) + r16 = true + @index += 1 + else + r16 = nil + end + if r16 + s15 << r16 + else + break + end + end + r15 = instantiate_node(SyntaxNode,input, i15...index, s15) + s13 << r15 + end + if s13.last + r13 = instantiate_node(SyntaxNode,input, i13...index, s13) + r13.extend(Variable2) + else + @index = i13 + r13 = nil + end + if r13 + r0 = r13 + r0.extend(Variable3) + else + @index = i0 + r0 = nil + end + end + end + + node_cache[:variable][start_index] = r0 + + r0 + end + + def _nt_space + start_index = index + if node_cache[:space].has_key?(index) + cached = node_cache[:space][index] + if cached + cached = SyntaxNode.new(input, index...(index + 1)) if cached == true + @index = cached.interval.end + end + return cached + end + + s0, i0 = [], index + loop do + if has_terminal?(' ', false, index) + r1 = instantiate_node(SyntaxNode,input, index...(index + 1)) + @index += 1 + else + terminal_parse_failure(' ') + r1 = nil + end + if r1 + s0 << r1 + else + break + end + end + r0 = instantiate_node(SyntaxNode,input, i0...index, s0) + + node_cache[:space][start_index] = r0 + + r0 + end + +end + +class ElasticityGrammarParser < Treetop::Runtime::CompiledParser + include ElasticityGrammar +end diff --git a/src/flow/lib/grammar.treetop b/src/flow/lib/grammar.treetop new file mode 100644 index 0000000000..c8f2434697 --- /dev/null +++ b/src/flow/lib/grammar.treetop @@ -0,0 +1,234 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +grammar ElasticityGrammar + + rule expression + space exp:(boolean_exp / logic_cond) space { + def result(role) + return exp.result(role) + end + } + end + + rule boolean_exp + left:logic_cond space op:('&&' / '&') space right:expression { + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{op.text_value} #{r_st}" + val = l_val && r_val + + return [val, st] + end + } + / + left:logic_cond space op:('||' / '|') space right:expression { + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{op.text_value} #{r_st}" + val = l_val || r_val + + return [val, st] + end + } + end + + rule logic_cond +# 'true' { +# def result(role) +# return true +# end +# } +# / +# 'false' { +# def result(role) +# return false +# end +# } +# / + left:operand space comp_op space right:operand { + def result(role) + l_val, l_st = left.result(role) + r_val, r_st = right.result(role) + + st = "#{l_st} #{comp_op.text_value} #{r_st}" + + if l_val.nil? || r_val.nil? + # An attribute was not found, we return false instead + # of assuming a value of 0 + + val = false + else + val = comp_op.apply(l_val, r_val) + end + + return [val, st] + end + } + / + '!' space expression { + def result(role) + e_val, e_st = expression.result(role) + + val = !e_val + st = "!#{e_st}" + + return [val, st] + end + } + / + '(' space expression space ')' { + def result(role) + e_val, e_st = expression.result(role) + + st = "(#{e_st})" + + return [e_val, st] + end + } + end + + rule comp_op + ('==' / '=') { + def apply(a,b) + a == b + end + } + / + ('!=' / '<>') { + def apply(a,b) + a != b + end + } + / + '>=' { + def apply(a,b) + a >= b + end + } + / + '>' { + def apply(a,b) + a > b + end + } + / + '<=' { + def apply(a,b) + a <= b + end + } + / + '<' { + def apply(a,b) + a < b + end + } + end + + rule operand + ( number ) { + def result + number.result(role) + end + } + / + ( variable ) { + def result + variable.result(role) + end + } + end + + rule number + '-'? [0-9]+ '.' [0-9]+ { + def result(role) + val = text_value.to_f + st = val.to_s + + return [val, st] + end + } + / + '-'? [0-9]+ { + def result(role) + val = text_value.to_i + st = val.to_s + + return [val, st] + end + } + end + + rule variable + ( '"' [a-zA-Z] [0-9a-zA-Z_]* '"' + / '\'' [a-zA-Z] [0-9a-zA-Z_]* '\'' + / [a-zA-Z] [0-9a-zA-Z_]* + ) { + + def result(role) + nodes = role.get_nodes + total = 0 + n_nodes = 0 + att = text_value.upcase + + nodes.each { |node| + if node && node['vm_info'] + + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + # Use values from VMs in RUNNING only + + if vm_state != '3' || lcm_state != '3' + next + end + + if node['vm_info']['VM']['USER_TEMPLATE'][att] + total += (node['vm_info']['VM']['USER_TEMPLATE'][att]).to_f + n_nodes += 1 + elsif node['vm_info']['VM'][att] + total += (node['vm_info']['VM'][att]).to_f + n_nodes += 1 + elsif node['vm_info']['VM']['TEMPLATE'][att] + total += (node['vm_info']['VM']['TEMPLATE'][att]).to_f + n_nodes += 1 + end + end + } + + # The attribute wasn't found for any of the nodes + if n_nodes == 0 + val = nil + st = "#{att}[--]" + else + val = total / n_nodes + st = "#{att}[#{val.to_s}]" + end + + return [val, st] + end + } + end + + rule space + ' '* + end +end \ No newline at end of file diff --git a/src/flow/lib/log.rb b/src/flow/lib/log.rb new file mode 100644 index 0000000000..f316c63726 --- /dev/null +++ b/src/flow/lib/log.rb @@ -0,0 +1,157 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +class Log + + class << self + # This class handles logging for each service and for the service server + # Log.info("SERVER", "It works"), it will be written in the main log + # Log.info("LCM", "Service 3 started", 3), it will be written in a + # specific log for service number 3 + + LOG_COMP = "LOG" + + DEBUG_LEVEL = [ + Logger::ERROR, # 0 + Logger::WARN, # 1 + Logger::INFO, # 2 + Logger::DEBUG # 3 + ] + + #LOG_LOCATION = '/var/log/one' + + @log_level = Logger::DEBUG + + # Mon Feb 27 06:02:30 2012 [Clo] [E]: Error message example + MSG_FORMAT = %{%s [%s]: [%s] %s\n} + + # Mon Feb 27 06:02:30 2012 + DATE_FORMAT = "%a %b %d %H:%M:%S %Y" + + # Message to be used in CloudLogger + CLOUD_LOGGER_MSG = %{[%s] %s} + + # Sets the server logger + # @param [CloudLogger::CloudLogger, Logger] logger + def logger=(logger) + @@logger = logger + end + + # Sets the log level + # @param [Integer] log_level (0:ERROR, 1:WARN, 2:INFO, 3:DEBUG) + def level=(log_level) + @@log_level = DEBUG_LEVEL[log_level] + end + + # Writes a info message to the log. If a service_id is specified the + # message will be written to the service log, otherwise the server log + # will be used. + # @param [String] component + # @param [String] message + # @param [String] service_id + # + # @example + # Mon Feb 27 06:02:30 2012 [] [I]: + def info(component, message, service_id=nil) + if service_id + add(Logger::INFO, component, message, service_id) + else + @@logger.info(CLOUD_LOGGER_MSG % [component, message]) + end + end + + # Writes a debug message to the log. If a service_id is specified the + # message will be written to the service log, otherwise the server log + # will be used. + # @param [String] component + # @param [String] message + # @param [String] service_id + # + # @example + # Mon Feb 27 06:02:30 2012 [] [D]: + def debug(component, message, service_id=nil) + if service_id + add(Logger::DEBUG, component, message, service_id) + else + @@logger.debug(CLOUD_LOGGER_MSG % [component, message]) + end + end + + # Writes a error message to the log. If a service_id is specified the + # message will be written to the service log, otherwise the server log + # will be used. + # @param [String] component + # @param [String] message + # @param [String] service_id + # + # @example + # Mon Feb 27 06:02:30 2012 [] [E]: + def error(component, message, service_id=nil) + if service_id + add(Logger::ERROR, component, message, service_id) + else + @@logger.error(CLOUD_LOGGER_MSG % [component, message]) + end + end + + # Writes a warn message to the log. If a service_id is specified the + # message will be written to the service log, otherwise the server log + # will be used. + # @param [String] component + # @param [String] message + # @param [String] service_id + # + # @example + # Mon Feb 27 06:02:30 2012 [] [W]: + def warn(component, message, service_id=nil) + if service_id + add(Logger::WARN, component, message, service_id) + else + @@logger.warn(CLOUD_LOGGER_MSG % [component, message]) + end + end + + private + + def add(severity, component, message, service_id) + if severity < @@log_level + return true + end + + begin + msg = MSG_FORMAT % [ + Time.now.strftime(DATE_FORMAT), + Logger::SEV_LABEL[severity][0..0], + component, + message ] + + open("#{LOG_LOCATION}/oneflow/#{service_id}.log", 'a') do |f| + f << msg + end + rescue Errno::ENOENT => e + FileUtils.mkdir("#{LOG_LOCATION}/oneflow/") + + open("#{LOG_LOCATION}/oneflow/#{service_id}.log", 'a') do |f| + f << msg + end + rescue => e + message = "Could not log into #{LOG_LOCATION}/oneflow/#{service_id}.log: #{e.message}" + error LOG_COMP, message + end + end + + end +end \ No newline at end of file diff --git a/src/flow/lib/models.rb b/src/flow/lib/models.rb new file mode 100644 index 0000000000..d9fbf5ec4c --- /dev/null +++ b/src/flow/lib/models.rb @@ -0,0 +1,30 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'opennebula' + +include OpenNebula + +require 'opennebula/document_json' +require 'opennebula/document_pool_json' + +require 'validator' + +require 'models/role.rb' +require 'models/service_pool.rb' +require 'models/service.rb' +require 'models/service_template_pool.rb' +require 'models/service_template.rb' diff --git a/src/flow/lib/models/role.rb b/src/flow/lib/models/role.rb new file mode 100644 index 0000000000..dfaa861519 --- /dev/null +++ b/src/flow/lib/models/role.rb @@ -0,0 +1,899 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'treetop' +require 'grammar' +require 'parse-cron' + +module OpenNebula + class Role + + # Actions that can be performed on the VMs of a given Role + SCHEDULE_ACTIONS = [ + 'shutdown', + 'delete', + 'hold', + 'release', + 'stop', + 'shutdown-hard', + 'suspend', + 'resume', + 'boot', + 'delete-recreate', + 'reboot', + 'reboot-hard', + 'poweroff', + 'snapshot-create' + ] + + STATE = { + 'PENDING' => 0, + 'DEPLOYING' => 1, + 'RUNNING' => 2, + 'UNDEPLOYING' => 3, + 'WARNING' => 4, + 'DONE' => 5, + 'FAILED_UNDEPLOYING' => 6, + 'FAILED_DEPLOYING' => 7, + 'SCALING' => 8, + 'FAILED_SCALING' => 9, + 'COOLDOWN' => 10 + } + + STATE_STR = [ + 'PENDING', + 'DEPLOYING', + 'RUNNING', + 'UNDEPLOYING', + 'WARNING', + 'DONE', + 'FAILED_UNDEPLOYING', + 'FAILED_DEPLOYING', + 'SCALING', + 'FAILED_SCALING', + 'COOLDOWN' + ] + + LOG_COMP = "ROL" + + def initialize(body, service) + @body = body + @service = service + + @body['nodes'] ||= [] + @body['disposed_nodes'] ||= [] + end + + def name + return @body['name'] + end + + # Returns the role state + # @return [Integer] the role state + def state + return @body['state'].to_i + end + + # Returns the role parents + # @return [Array] the role parents + def parents + return @body['parents'] || [] + end + + # Returns the role cardinality + # @return [Integer] the role cardinality + def cardinality + return @body['cardinality'].to_i + end + + # Sets a new cardinality for this role + # @param [Integer] the new cardinality + def set_cardinality(target_cardinality) + dir = target_cardinality > cardinality ? "up" : "down" + msg = "Role #{name} scaling #{dir} from #{cardinality} to #{target_cardinality} nodes" + Log.info LOG_COMP, msg, @service.id() + @service.log_info(msg) + + @body['cardinality'] = target_cardinality.to_i + end + + # Updates the cardinality with the current number of nodes + def update_cardinality() + @body['cardinality'] = @body['nodes'].size() + end + + # Returns the role max cardinality + # @return [Integer,nil] the role cardinality, or nil if it was not defined + def max_cardinality + max = @body['max_vms'] + + return nil if max.nil? + + return max.to_i + end + + # Returns the role min cardinality + # @return [Integer,nil] the role cardinality, or nil if it was not defined + def min_cardinality + min = @body['min_vms'] + + return nil if min.nil? + + return min.to_i + end + + # Returns the string representation of the service state + # @return [String] the state string + def state_str + return STATE_STR[state] + end + + # Returns the nodes of the role + # @return [Array] the nodes + def get_nodes + @body['nodes'] + end + + # Sets a new state + # @param [Integer] the new state + # @return [true, false] true if the value was changed + def set_state(state) + if state < 0 || state > STATE_STR.size + return false + end + + @body['state'] = state.to_s + + if state == STATE['SCALING'] + + elasticity_pol = @body['elasticity_policies'] + + if !elasticity_pol.nil? + elasticity_pol.each do |policy| + policy.delete('true_evals') + end + end + end + + Log.info LOG_COMP, "Role #{name} new state: #{STATE_STR[state]}", @service.id() + + return true + end + + # Retrieves the VM information for each Node in this Role. If a Node + # is to be disposed and it is found in DONE, it will be cleaned + # + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def info + success = true + + nodes = @body['nodes'] + new_nodes = [] + disposed_nodes = @body['disposed_nodes'] + + nodes.each do |node| + vm_id = node['deploy_id'] + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.info + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : VM #{vm_id} monitorization failed; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + success = false + node['vm_info'] = nil + + new_nodes << node + else + node['vm_info'] = vm.to_hash + + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + if (vm_state == '6') + # Store the VM id in the array of disposed nodes + disposed_nodes << vm_id + else + if (node['scale_up'] == "1" && vm_state == '3' && lcm_state == '3') + # If the VM was a scale-up and it reaches RUNNING, + # clear the flag + node.delete('scale_up') + end + + new_nodes << node + end + end + end + + @body['nodes'] = new_nodes + + if !success + return OpenNebula::Error.new() + end + + return nil + end + + # Deploys all the nodes in this role + # @return [Array, Array] true if all the VMs + # were created, false and the error reason if there was a problem + # creating the VMs + def deploy(scale_up=false) + n_nodes = cardinality() - get_nodes.size + + n_nodes.times { |i| + vm_name = "#{@body['name']}_#{i}_(service_#{@service.id()})" + + template_id = @body['vm_template'] + + Log.debug LOG_COMP, "Role #{name} : Trying to instantiate template "\ + "#{template_id}, with name #{vm_name}", @service.id() + + template = OpenNebula::Template.new_with_id(template_id, @service.client) + + extra_template = "SERVICE_ID = #{@service.id()}\n"\ + "ROLE_NAME = #{@body['name']}" + + vm_id = template.instantiate(vm_name, false, extra_template) + + if OpenNebula.is_error?(vm_id) + msg = "Role #{name} : Instantiate failed for template #{template_id}; #{vm_id.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + return [false, "Error trying to instantiate the VM Template" \ + " #{template_id} in Role #{self.name}: #{vm_id.message}"] + end + + Log.debug LOG_COMP, "Role #{name} : Instantiate success, VM ID #{vm_id}", @service.id() + node = { + 'deploy_id' => vm_id, + } + + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.info + if OpenNebula.is_error?(rc) + node['vm_info'] = nil + else + node['vm_info'] = vm.to_hash + end + + if scale_up + node['scale_up'] = '1' + end + + @body['nodes'] << node + } + + return [true, nil] + end + + # Shutdown all the nodes in this role + # + # @param scale_down [true, false] true to shutdown and dispose the + # number of VMs needed to get down to cardinality nodes + # @return [Array, Array] true if all the VMs + # were shutdown, false and the error reason if there was a problem + # shutting down the VMs + def shutdown(scale_down=false) + success = true + + nodes = get_nodes + + if scale_down + n_nodes = nodes.size - cardinality() + else + n_nodes = nodes.size + end + + shutdown_nodes(nodes[0..n_nodes-1], scale_down) + + return [success, nil] + end + + # Delete all the nodes in this role + # @return [Array] All the VMs are deleted, and the return + # ignored + def delete + get_nodes.each { |node| + vm_id = node['deploy_id'] + + Log.debug LOG_COMP, "Role #{name} : Deleting VM #{vm_id}", @service.id() + + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.finalize + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Delete failed for VM #{vm_id}; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + else + Log.debug LOG_COMP, "Role #{name} : Delete success for VM #{vm_id}", @service.id() + end + } + + return [true, nil] + end + + # Changes the owner/group of all the nodes in this role + # + # @param [Integer] uid the new owner id. Set to -1 to leave the current one + # @param [Integer] gid the new group id. Set to -1 to leave the current one + # + # @return [Array, Array] true if all the VMs + # were updated, false and the error reason if there was a problem + # updating the VMs + def chown(uid, gid) + get_nodes.each { |node| + vm_id = node['deploy_id'] + + Log.debug LOG_COMP, "Role #{name} : Chown for VM #{vm_id}", @service.id() + + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.chown(uid, gid) + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Chown failed for VM #{vm_id}; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + return [false, rc.message] + else + Log.debug LOG_COMP, "Role #{name} : Chown success for VM #{vm_id}", @service.id() + end + } + + return [true, nil] + end + + # Schedule the given action on all the VMs that belong to the Role + # @param [String] action one of the available actions defined in SCHEDULE_ACTIONS + # @param [Integer] period + # @param [Integer] vm_per_period + def batch_action(action, period, vms_per_period) + vms_id = [] + + # TODO: check action is a valid string, period vm_per_period integer + + error_msgs = [] + nodes = @body['nodes'] + now = Time.now.to_i + + do_offset = ( !period.nil? && period.to_i > 0 && + !vms_per_period.nil? && vms_per_period.to_i > 0 ) + + time_offset = 0 + + nodes.each_with_index do |node, index| + vm_id = node['deploy_id'] + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.info + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : VM #{vm_id} monitorization failed; #{rc.message}" + error_msgs << msg + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + else + ids = vm.retrieve_elements('USER_TEMPLATE/SCHED_ACTION/ID') + + id = 0 + if (!ids.nil? && !ids.empty?) + ids.map! {|e| e.to_i } + id = ids.max + 1 + end + + tmp_str = vm.user_template_str + + if do_offset + time_offset = (index / vms_per_period.to_i).floor * period.to_i + end + + tmp_str << "\nSCHED_ACTION = "<< + "[ID = #{id}, ACTION = #{action}, TIME = #{now + time_offset}]" + + rc = vm.update(tmp_str) + if OpenNebula.is_error?(rc) + msg = "Role #{name} : VM #{vm_id} error scheduling action; #{rc.message}" + error_msgs << msg + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + else + vms_id << vm.id + end + end + end + + log_msg = "Action:#{action} scheduled on Role:#{self.name} VMs:#{vms_id.join(',')}" + Log.info LOG_COMP, log_msg, @service.id() + + if error_msgs.empty? + return [true, log_msg] + else + error_msgs << log_msg + return [false, error_msgs.join('\n')] + end + end + + ######################################################################## + # Scalability + ######################################################################## + + # Returns a positive, 0, or negative number of nodes to adjust, + # according to the elasticity and scheduled policies + # @return [Array] positive, 0, or negative number of nodes to + # adjust, plus the cooldown period duration + def scale?() + elasticity_pol = @body['elasticity_policies'] + scheduled_pol = @body['scheduled_policies'] + + elasticity_pol ||= [] + scheduled_pol ||= [] + + scheduled_pol.each do |policy| + diff = scale_time?(policy) + return [diff, 0] if diff != 0 + end + + elasticity_pol.each do |policy| + diff, cooldown_duration = scale_attributes?(policy) + if diff != 0 + cooldown_duration = @body['cooldown'] if cooldown_duration.nil? + cooldown_duration = @@default_cooldown if cooldown_duration.nil? + + return [diff, cooldown_duration] + end + end + + # Implicit rule that scales up to maintain the min_cardinality, with + # no cooldown period + if cardinality < min_cardinality.to_i + return [min_cardinality.to_i - cardinality, 0] + end + + return [0, 0] + end + + # Scales up or down the number of nodes needed to match the current + # cardinality + # + # @return [Array, Array] true if all the VMs + # were created/shut down, false and the error reason if there + # was a problem + def scale() + n_nodes = 0 + + get_nodes.each do |node| + n_nodes += 1 if node['disposed'] != "1" + end + + diff = cardinality - n_nodes + + if diff > 0 + return deploy(true) + elsif diff < 0 + return shutdown(true) + end + + return [true, nil] + end + + # Updates the duration for the next cooldown + # @param cooldown_duration [Integer] duration for the next cooldown + def set_cooldown_duration(cooldown_duration) + @body['cooldown_duration'] = cooldown_duration.to_i + end + + # Updates the duration for the next cooldown with the default value + def set_default_cooldown_duration() + cooldown_duration = @body['cooldown'] + cooldown_duration = @@default_cooldown if cooldown_duration.nil? + + set_cooldown_duration(cooldown_duration) + end + + # Sets the cooldown end time from now + the duration set in set_cooldown_duration + # @return [true, false] true if the cooldown duration is bigger than 0 + def apply_cooldown_duration() + cooldown_duration = @body['cooldown_duration'].to_i + + if cooldown_duration != 0 + @body['cooldown_end'] = Time.now.to_i + cooldown_duration + @body.delete('cooldown_duration') + + return true + end + + return false + end + + # Returns true if the cooldown period ended + # @return [true, false] true if the cooldown period ended + def cooldown_over?() + return Time.now.to_i >= @body['cooldown_end'].to_i + end + + def self.init_default_cooldown(default_cooldown) + @@default_cooldown = default_cooldown + end + + def self.init_default_shutdown(shutdown_action) + @@default_shutdown = shutdown_action + end + + # Updates the role + # @param [Hash] template + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def update(template) + + force = template['force'] == true + new_cardinality = template["cardinality"] + + if new_cardinality.nil? + return nil + end + + new_cardinality = new_cardinality.to_i + + if !force + if new_cardinality < min_cardinality().to_i + return OpenNebula::Error.new( + "Minimum cardinality is #{min_cardinality()}") + + elsif !max_cardinality().nil? && new_cardinality > max_cardinality().to_i + return OpenNebula::Error.new( + "Maximum cardinality is #{max_cardinality()}") + + end + end + + set_cardinality(new_cardinality) + + return nil + end + + ######################################################################## + # Recover + ######################################################################## + + def recover_deployment() + recover() + end + + def recover_warning() + recover() + deploy() + end + + def recover_scale() + recover() + retry_scale() + end + + + ######################################################################## + ######################################################################## + + + private + + # Returns a positive, 0, or negative number of nodes to adjust, + # according to a SCHEDULED type policy + # @param [Hash] A SCHEDULED type policy + # @return [Integer] positive, 0, or negative number of nodes to adjust + def scale_time?(elasticity_pol) + now = Time.now.to_i + + last_eval = elasticity_pol['last_eval'].to_i + + elasticity_pol['last_eval'] = now + + # If this is the first time this is evaluated, ignore it. + # We don't want to execute actions planned in the past when the + # server starts. + + if last_eval == 0 + return 0 + end + + start_time = elasticity_pol['start_time'] + target_vms = elasticity_pol['adjust'] + + if target_vms.nil? + # TODO error msg + return 0 + end + + if !(start_time.nil? || start_time.empty?) + begin + start_time = Time.parse(start_time).to_i + rescue ArgumentError + # TODO error msg + return 0 + end + else + recurrence = elasticity_pol['recurrence'] + + if recurrence.nil? || recurrence.empty? + # TODO error msg + return 0 + end + + begin + cron_parser = CronParser.new(recurrence) + + # This returns the next planned time, starting from the last + # step + start_time = cron_parser.next(Time.at(last_eval)).to_i + rescue + # TODO error msg bad format + return 0 + end + end + + # Only actions planned between last step and this one are triggered + if start_time > last_eval && start_time <= now + Log.debug LOG_COMP, "Role #{name} : scheduled scalability for "\ + "#{Time.at(start_time)} triggered", @service.id() + + new_cardinality = calculate_new_cardinality(elasticity_pol) + + return new_cardinality - cardinality() + end + + return 0 + end + + # Returns a positive, 0, or negative number of nodes to adjust, + # according to a policy based on attributes + # @param [Hash] A policy based on attributes + # @return [Array] positive, 0, or negative number of nodes to + # adjust, plus the cooldown period duration + def scale_attributes?(elasticity_pol) + + now = Time.now.to_i + + # TODO: enforce true_up_evals type in ServiceTemplate::ROLE_SCHEMA ? + + period_duration = elasticity_pol['period'].to_i + period_number = elasticity_pol['period_number'].to_i + last_eval = elasticity_pol['last_eval'].to_i + true_evals = elasticity_pol['true_evals'].to_i + expression = elasticity_pol['expression'] + + if !last_eval.nil? + if now < (last_eval + period_duration) + return [0, 0] + end + end + + elasticity_pol['last_eval'] = now + + new_cardinality = cardinality() + new_evals = 0 + + exp_value, exp_st = scale_rule(expression) + + if exp_value + new_evals = true_evals + 1 + new_evals = period_number if new_evals > period_number + + if new_evals >= period_number + Log.debug LOG_COMP, "Role #{name} : elasticy policy #{exp_st} "\ + "triggered", @service.id() + new_cardinality = calculate_new_cardinality(elasticity_pol) + end + end + + elasticity_pol['true_evals'] = new_evals + elasticity_pol['expression_evaluated'] = exp_st + + return [new_cardinality - cardinality(), elasticity_pol['cooldown']] + end + + # Returns true if the scalability rule is triggered + # @return true if the scalability rule is triggered + def scale_rule(elas_expr) + parser = ElasticityGrammarParser.new + + if elas_expr.nil? || elas_expr.empty? + return false + end + + treetop = parser.parse(elas_expr) + if treetop.nil? + return [false, "Parse error. '#{elas_expr}': #{parser.failure_reason}"] + end + + val, st = treetop.result(self) + + return [val, st] + end + + def calculate_new_cardinality(elasticity_pol) + type = elasticity_pol['type'] + adjust = elasticity_pol['adjust'].to_i + + # Min is a hard limit, if the current cardinality + adjustment does + # not reach it, the difference is added + + max = [cardinality(), max_cardinality.to_i].max() +# min = [cardinality(), min_cardinality.to_i].min() + min = min_cardinality.to_i + + case type.upcase + when 'CHANGE' + new_cardinality = cardinality() + adjust + when 'PERCENTAGE_CHANGE' + min_adjust_step = elasticity_pol['min_adjust_step'].to_i + + change = cardinality() * adjust / 100.0 + + sign = change > 0 ? 1 : -1 + change = change.abs + + if change < 1 + change = 1 + else + change = change.to_i + end + + change = sign * [change, min_adjust_step].max + + new_cardinality = cardinality() + change + + when 'CARDINALITY' + new_cardinality = adjust + else + # TODO: error message + return cardinality() + end + + # The cardinality can be forced to be outside the min,max + # range. If that is the case, the scale up/down will not + # move further outside the range. It will move towards the + # range with the adjustement set, instead of jumping the + # difference + if (adjust > 0) + new_cardinality = max if new_cardinality > max + elsif (adjust < 0) + new_cardinality = min if new_cardinality < min + end + + return new_cardinality + end + + # For a failed scale up, the cardinality is updated to the actual value + # For a failed scale down, the shutdown actions are retried + def retry_scale() + nodes_dispose = get_nodes.select { |node| + node['disposed'] == "1" + } + + shutdown_nodes(nodes_dispose, true) + + set_cardinality( get_nodes.size() - n_dispose.size() ) + end + + # Deletes VMs in DONE or FAILED, and sends a boot action to VMs in UNKNOWN + def recover() + + nodes = @body['nodes'] + new_nodes = [] + disposed_nodes = @body['disposed_nodes'] + + nodes.each do |node| + vm_state = nil + vm_id = node['deploy_id'] + + if node['vm_info'] && node['vm_info']['VM'] && node['vm_info']['VM']['STATE'] + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + end + + if vm_state == '6' # DONE + # Store the VM id in the array of disposed nodes + disposed_nodes << vm_id + + elsif vm_state == '7' # FAILED + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + rc = vm.finalize + + if !OpenNebula.is_error?(rc) + # Store the VM id in the array of disposed nodes + disposed_nodes << vm_id + + Log.debug LOG_COMP, "Role #{name} : Delete success for VM #{vm_id}", @service.id() + else + msg = "Role #{name} : Delete failed for VM #{vm_id}; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + success = false + + new_nodes << node + end + elsif vm_state == '3' && lcm_state == '16' # UNKNOWN + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + vm.boot + else + new_nodes << node + end + end + + @body['nodes'] = new_nodes + end + + + # Shuts down all the given roles + # @param scale_down [true,false] True to set the 'disposed' node flag + def shutdown_nodes(nodes, scale_down) + + action = @body['shutdown_action'] + + if action.nil? + action = @service.get_shutdown_action() + end + + if action.nil? + action = @@default_shutdown + end + + nodes.each { |node| + vm_id = node['deploy_id'] + + Log.debug LOG_COMP, "Role #{name} : Shutting down VM #{vm_id}", @service.id() + + vm = OpenNebula::VirtualMachine.new_with_id(vm_id, @service.client) + + if action == 'shutdown-hard' + rc = vm.shutdown(true) + else + rc = vm.shutdown + end + + if scale_down + node['disposed'] = '1' + end + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Shutdown failed for VM #{vm_id}, will perform a Delete; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + rc = vm.finalize + + if OpenNebula.is_error?(rc) + msg = "Role #{name} : Delete failed for VM #{vm_id}; #{rc.message}" + Log.error LOG_COMP, msg, @service.id() + @service.log_error(msg) + + success = false + #return [false, rc.message] + else + Log.debug LOG_COMP, "Role #{name} : Delete success for VM #{vm_id}", @service.id() + end + else + Log.debug LOG_COMP, "Role #{name} : Shutdown success for VM #{vm_id}", @service.id() + end + } + end + + end +end diff --git a/src/flow/lib/models/service.rb b/src/flow/lib/models/service.rb new file mode 100644 index 0000000000..d85d4c29d7 --- /dev/null +++ b/src/flow/lib/models/service.rb @@ -0,0 +1,409 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +module OpenNebula + class Service < DocumentJSON + + DOCUMENT_TYPE = 100 + + STATE = { + 'PENDING' => 0, + 'DEPLOYING' => 1, + 'RUNNING' => 2, + 'UNDEPLOYING' => 3, + 'WARNING' => 4, + 'DONE' => 5, + 'FAILED_UNDEPLOYING' => 6, + 'FAILED_DEPLOYING' => 7, + 'SCALING' => 8, + 'FAILED_SCALING' => 9, + 'COOLDOWN' => 10 + } + + STATE_STR = [ + 'PENDING', + 'DEPLOYING', + 'RUNNING', + 'UNDEPLOYING', + 'WARNING', + 'DONE', + 'FAILED_UNDEPLOYING', + 'FAILED_DEPLOYING', + 'SCALING', + 'FAILED_SCALING', + 'COOLDOWN' + ] + + LOG_COMP = "SER" + + # Returns the service state + # @return [Integer] the service state + def state + return @body['state'].to_i + end + + # Returns the service strategy + # @return [String] the service strategy + def strategy + return @body['deployment'] + end + + # Returns the string representation of the service state + # @return the state string + def state_str + return STATE_STR[state] + end + + # Sets a new state + # @param [Integer] the new state + # @return [true, false] true if the value was changed + def set_state(state) + if state < 0 || state > STATE_STR.size + return false + end + + @body['state'] = state + + msg = "New state: #{STATE_STR[state]}" + Log.info LOG_COMP, msg, self.id() + self.log_info(msg) + + return true + end + + # Returns the owner username + # @return [String] the service's owner username + def owner_name() + return self['UNAME'] + end + + # Replaces this object's client with a new one + # @param [OpenNebula::Client] owner_client the new client + def replace_client(owner_client) + @client = owner_client + end + + # Returns all the node Roles + # @return [Hash] all the node Roles + def get_roles + return @roles + end + + # Returns true if all the nodes are correctly deployed + # @return [true, false] true if all the nodes are correctly deployed + def all_roles_running?() + @roles.each { |name, role| + if role.state != Role::STATE['RUNNING'] + return false + end + } + + return true + end + + # Returns true if all the nodes are in done state + # @return [true, false] true if all the nodes are correctly deployed + def all_roles_done?() + @roles.each { |name, role| + if role.state != Role::STATE['DONE'] + return false + end + } + + return true + end + + # Returns true if any of the roles is in failed state + # @return [true, false] true if any of the roles is in failed state + def any_role_failed?() + failed_states = [ + Role::STATE['FAILED_DEPLOYING'], + Role::STATE['FAILED_UNDEPLOYING']] + + @roles.each { |name, role| + if failed_states.include?(role.state) + return true + end + } + + return false + end + + def any_role_scaling?() + @roles.each do |name, role| + if role.state == Role::STATE['SCALING'] + return true + end + end + + return false + end + + def any_role_failed_scaling?() + @roles.each do |name, role| + if role.state == Role::STATE['FAILED_SCALING'] + return true + end + end + + return false + end + + def any_role_cooldown?() + @roles.each do |name, role| + if role.state == Role::STATE['COOLDOWN'] + return true + end + end + + return false + end + + # Create a new service based on the template provided + # @param [String] template_json + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def allocate(template_json) + template = JSON.parse(template_json) + template['state'] = STATE['PENDING'] + + super(template.to_json, template['name']) + end + + # Shutdown the service. This action is called when user wants to shutdwon + # the Service + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def shutdown + if ![Service::STATE['FAILED_SCALING'], + Service::STATE['DONE']].include?(self.state) + self.set_state(Service::STATE['UNDEPLOYING']) + return self.update + else + return OpenNebula::Error.new("Action shutdown: Wrong state" \ + " #{self.state_str()}") + end + end + + # Recover a failed service. + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def recover + if [Service::STATE['FAILED_DEPLOYING']].include?(self.state) + @roles.each do |name, role| + if role.state == Role::STATE['FAILED_DEPLOYING'] + role.set_state(Role::STATE['PENDING']) + role.recover_deployment() + end + end + + self.set_state(Service::STATE['DEPLOYING']) + + elsif self.state == Service::STATE['FAILED_SCALING'] + @roles.each do |name, role| + if role.state == Role::STATE['FAILED_SCALING'] + role.recover_scale() + role.set_state(Role::STATE['SCALING']) + end + end + + self.set_state(Service::STATE['SCALING']) + + elsif self.state == Service::STATE['FAILED_UNDEPLOYING'] + @roles.each do |name, role| + if role.state == Role::STATE['FAILED_UNDEPLOYING'] + role.set_state(Role::STATE['RUNNING']) + end + end + + self.set_state(Service::STATE['UNDEPLOYING']) + + elsif self.state == Service::STATE['COOLDOWN'] + @roles.each do |name, role| + if role.state == Role::STATE['COOLDOWN'] + role.set_state(Role::STATE['RUNNING']) + end + end + + self.set_state(Service::STATE['RUNNING']) + + elsif self.state == Service::STATE['WARNING'] + @roles.each do |name, role| + if role.state == Role::STATE['WARNING'] + role.recover_warning() + end + end + + else + return OpenNebula::Error.new("Action recover: Wrong state" \ + " #{self.state_str()}") + end + + return self.update + end + + # Delete the service. All the VMs are also deleted from OpenNebula. + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def delete + @roles.each { |name, role| + role.delete() + } + + return super() + end + + # Retrieves the information of the Service and all its Nodes. + # + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def info + rc = super + if OpenNebula.is_error?(rc) + return rc + end + + @roles = {} + + if @body['roles'] + @body['roles'].each { |elem| + elem['state'] ||= Role::STATE['PENDING'] + role = Role.new(elem, self) + @roles[role.name] = role + } + end + + return nil + end + + # Add an info message in the service information that will be stored + # in OpenNebula + # @param [String] message + def log_info(message) + add_log(Logger::INFO, message) + end + + # Add an error message in the service information that will be stored + # in OpenNebula + # @param [String] message + def log_error(message) + add_log(Logger::ERROR, message) + end + + # Retrieve the service client + def client + @client + end + + # Changes the owner/group + # + # @param [Integer] uid the new owner id. Set to -1 to leave the current one + # @param [Integer] gid the new group id. Set to -1 to leave the current one + # + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def chown(uid, gid) + old_uid = self['UID'].to_i + old_gid = self['GID'].to_i + + rc = super(uid, gid) + + if OpenNebula.is_error?(rc) + return rc + end + + @roles.each { |name, role| + rc = role.chown(uid, gid) + + break if rc[0] == false + } + + if rc[0] == false + self.log_error("Chown operation failed, will try to rollback all VMs to the old user and group") + update() + + super(old_uid, old_gid) + + @roles.each { |name, role| + role.chown(old_uid, old_gid) + } + + return OpenNebula::Error.new(rc[1]) + end + + return nil + end + + # Updates a role + # @param [String] role_name + # @param [String] template_json + # @return [nil, OpenNebula::Error] nil in case of success, Error + # otherwise + def update_role(role_name, template_json) + + if ![Service::STATE['RUNNING'], Service::STATE['WARNING']].include?(self.state) + return OpenNebula::Error.new("Update role: Wrong state" \ + " #{self.state_str()}") + end + + template = JSON.parse(template_json) + + # TODO: Validate template? + + role = @roles[role_name] + + if role.nil? + return OpenNebula::Error.new("ROLE \"#{role_name}\" does not exist") + end + + rc = role.update(template) + + if OpenNebula.is_error?(rc) + return rc + end + + # TODO: The update may not change the cardinality, only + # the max and min vms... + + role.set_state(Role::STATE['SCALING']) + + role.set_default_cooldown_duration() + + self.set_state(Service::STATE['SCALING']) + + return self.update + end + + def get_shutdown_action() + return @body['shutdown_action'] + end + + private + + # @param [Logger::Severity] severity + # @param [String] message + def add_log(severity, message) + severity_str = Logger::SEV_LABEL[severity][0..0] + + @body['log'] ||= Array.new + @body['log'] << { + :timestamp => Time.now.to_i, + :severity => severity_str, + :message => message + } + end + end +end diff --git a/src/flow/lib/models/service_pool.rb b/src/flow/lib/models/service_pool.rb new file mode 100644 index 0000000000..d7ebf9cb31 --- /dev/null +++ b/src/flow/lib/models/service_pool.rb @@ -0,0 +1,97 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +module OpenNebula + class ServicePool < DocumentPoolJSON + + DOCUMENT_TYPE = 100 + + @@mutex = Mutex.new + @@mutex_hash = Hash.new + + # Class constructor + # + # @param [OpenNebula::Client] client the xml-rpc client + # @param [Integer] user_id the filter flag, see + # http://opennebula.org/documentation:rel3.6:api + # + # @return [DocumentPool] the new object + def initialize(client, user_id=-1) + super(client, user_id) + end + + def factory(element_xml) + service = OpenNebula::Service.new(element_xml, @client) + service.load_body + service + end + + # Retrieves a Service element from OpenNebula. The Service::info() + # method is called + # + # @param [Integer] service_id Numerical Id of the service to retrieve + # @yieldparam [Service] this block will have the service's mutex locked. + # The mutex will be unlocked after the block execution. + # + # @return [Service, OpenNebula::Error] The Service in case of success + def get(service_id, &block) + service_id = service_id.to_i if service_id + service = Service.new_with_id(service_id, @client) + + rc = service.info + + if OpenNebula.is_error?(rc) + return rc + else + if block_given? + obj_mutex = nil + entry = nil + + @@mutex.synchronize { + # entry is an array of [Mutex, waiting] + # waiting is the number of threads waiting on this mutex + entry = @@mutex_hash[service_id] + + if entry.nil? + entry = [Mutex.new, 0] + @@mutex_hash[service_id] = entry + end + + obj_mutex = entry[0] + entry[1] = entry[1] + 1 + + if @@mutex_hash.size > 10000 + @@mutex_hash.delete_if { |s_id, entry| + entry[1] == 0 + } + end + } + + obj_mutex.synchronize { + block.call(service) + } + + @@mutex.synchronize { + entry[1] = entry[1] - 1 + } + end + + return service + end + end + + end +end diff --git a/src/flow/lib/models/service_template.rb b/src/flow/lib/models/service_template.rb new file mode 100644 index 0000000000..b67da73715 --- /dev/null +++ b/src/flow/lib/models/service_template.rb @@ -0,0 +1,311 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'parse-cron' + +module OpenNebula + + class ServiceTemplate < DocumentJSON + ROLE_SCHEMA = { + :type => :object, + :properties => { + 'name' => { + :type => :string, + :required => true + }, + 'cardinality' => { + :type => :integer, + :default => 1, + :minimum => 0 + }, + 'vm_template' => { + :type => :integer, + :required => true + }, + 'parents' => { + :type => :array, + :items => { + :type => :string + } + }, + 'shutdown_action' => { + :type => :string, + :enum => %w{shutdown shutdown-hard}, + :required => false + }, + 'min_vms' => { + :type => :integer, + :required => false, + :minimum => 0 + }, + 'max_vms' => { + :type => :integer, + :required => false, + :minimum => 0 + }, + 'cooldown' => { + :type => :integer, + :required => false, + :minimum => 0 + }, + 'elasticity_policies' => { + :type => :array, + :items => { + :type => :object, + :properties => { + 'type' => { + :type => :string, + :enum => %w{CHANGE CARDINALITY PERCENTAGE_CHANGE}, + :required => true + }, + 'adjust' => { + :type => :integer, + :required => true + }, + 'min_adjust_step' => { + :type => :integer, + :required => false, + :minimum => 1 + }, + 'period_number' => { + :type => :integer, + :required => false, + :minimum => 0 + }, + 'period' => { + :type => :integer, + :required => false, + :minimum => 0 + }, + 'expression' => { + :type => :string, + :required => true + }, + 'cooldown' => { + :type => :integer, + :required => false, + :minimum => 0 + } + #'statistic' => { + # # SampleCount | Average | Sum | Minimum | Maximum + # :type => :string + #} + } + } + }, + 'scheduled_policies' => { + :type => :array, + :items => { + :type => :object, + :properties => { + 'type' => { + :type => :string, + :enum => %w{CHANGE CARDINALITY PERCENTAGE_CHANGE}, + :required => true + }, + 'adjust' => { + :type => :integer, + :required => true + }, + 'min_adjust_step' => { + :type => :integer, + :required => false, + :minimum => 1 + }, + 'start_time' => { + :type => :string, + :required => false + }, + 'recurrence' => { + :type => :string, + :required => false + } + } + } + } + } + } + + SCHEMA = { + :type => :object, + :properties => { + 'name' => { + :type => :string, + :required => true + }, + 'deployment' => { + :type => :string, + :enum => %w{none straight}, + :default => 'none' + }, + 'shutdown_action' => { + :type => :string, + :enum => %w{shutdown shutdown-hard}, + :required => false + }, + 'roles' => { + :type => :array, + :items => ROLE_SCHEMA, + :required => true + } + } + } + + + + DOCUMENT_TYPE = 101 + + def allocate(template_json) + template = JSON.parse(template_json) + + validator = Validator::Validator.new( + :default_values => true, + :delete_extra_properties => false + ) + + validator.validate!(template, SCHEMA) + + validate_values(template) + + super(template.to_json, template['name']) + end + + # Retrieves the template + # + # @return [String] json template + def template + @body.to_json + end + + def update(template_json) + template = JSON.parse(template_json) + + validator = Validator::Validator.new( + :default_values => true, + :delete_extra_properties => false + ) + + validator.validate!(template, SCHEMA) + + validate_values(template) + + super(template.to_json) + end + + private + + def validate_values(template) + parser = ElasticityGrammarParser.new + + roles = template['roles'] + + roles.each_with_index do |role, role_index| + + roles[role_index+1..-1].each do |other_role| + if role['name'] == other_role['name'] + raise Validator::ParseException, + "Role name '#{role['name']}' is repeated" + end + end + + if (!role['min_vms'].nil? && role['min_vms'].to_i > role['cardinality'].to_i) + + raise Validator::ParseException, + "Role '#{role['name']}' 'cardinality' must be greater than "\ + "or equal to 'min_vms'" + end + + if !role['max_vms'].nil? && + role['max_vms'].to_i < role['cardinality'].to_i + + raise Validator::ParseException, + "Role '#{role['name']}' 'cardinality' must be lower than "\ + "or equal to 'max_vms'" + end + + if ((role['elasticity_policies'] && role['elasticity_policies'].size > 0) || + (role['scheduled_policies'] && role['scheduled_policies'].size > 0)) + + if role['min_vms'].nil? || role['max_vms'].nil? + raise Validator::ParseException, + "Role '#{role['name']}' with 'elasticity_policies' or "<< + "'scheduled_policies' must define both 'min_vms'"<< + "and 'max_vms'" + end + end + + if role['elasticity_policies'] + role['elasticity_policies'].each_with_index do |policy, index| + exp = policy['expression'] + + if exp.empty? + raise Validator::ParseException, + "Role '#{role['name']}', elasticity policy "\ + "##{index} 'expression' cannot be empty" + end + + treetop = parser.parse(exp) + if treetop.nil? + raise Validator::ParseException, + "Role '#{role['name']}', elasticity policy "\ + "##{index} 'expression' parse error: #{parser.failure_reason}" + end + end + end + + if role['scheduled_policies'] + role['scheduled_policies'].each_with_index do |policy, index| + + start_time = policy['start_time'] + recurrence = policy['recurrence'] + + if !start_time.nil? + if !policy['recurrence'].nil? + raise Validator::ParseException, + "Role '#{role['name']}', scheduled policy "\ + "##{index} must define "\ + "'start_time' or 'recurrence', but not both" + end + + begin + Time.parse(start_time) + rescue ArgumentError + raise Validator::ParseException, + "Role '#{role['name']}', scheduled policy "\ + "##{index} 'start_time' is not a valid Time. "\ + "Try with YYYY-MM-DD hh:mm:ss or YYYY-MM-DDThh:mm:ssZ" + end + elsif !recurrence.nil? + begin + cron_parser = CronParser.new(recurrence) + start_time = cron_parser.next() + rescue Exception => e + raise Validator::ParseException, + "Role '#{role['name']}', scheduled policy "\ + "##{index} 'recurrence' is not a valid "\ + "cron expression" + end + else + raise Validator::ParseException, + "Role '#{role['name']}', scheduled policy ##{index} needs to define either "<< + "'start_time' or 'recurrence'" + end + end + end + end + end + end +end diff --git a/src/flow/lib/models/service_template_pool.rb b/src/flow/lib/models/service_template_pool.rb new file mode 100644 index 0000000000..4eeb2fcf78 --- /dev/null +++ b/src/flow/lib/models/service_template_pool.rb @@ -0,0 +1,28 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +module OpenNebula + class ServiceTemplatePool < DocumentPoolJSON + + DOCUMENT_TYPE = 101 + + def factory(element_xml) + s_template = OpenNebula::ServiceTemplate.new(element_xml, @client) + s_template.load_body + s_template + end + end +end diff --git a/src/flow/lib/strategy.rb b/src/flow/lib/strategy.rb new file mode 100644 index 0000000000..5147fab886 --- /dev/null +++ b/src/flow/lib/strategy.rb @@ -0,0 +1,382 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'strategy/straight' + +class Strategy + + LOG_COMP = "STR" + + # Performs a boot step, deploying all nodes that meet the requirements + # @param [Service] service service to boot + # @return [Array, Array] true if all the nodes + # were created, false and the error reason if there was a problem + # creating the VMs + def boot_step(service) + Log.debug LOG_COMP, "Boot step", service.id() + + roles_deploy = get_roles_deploy(service) + + roles_deploy.each { |name, role| + Log.debug LOG_COMP, "Deploying role #{name}", service.id() + + rc = role.deploy + + if !rc[0] + role.set_state(Role::STATE['FAILED_DEPLOYING']) + + return rc + else + role.set_state(Role::STATE['DEPLOYING']) + end + } + + return [true, nil] + end + + # Performs a shutdown step, shutting down all nodes that meet the requirements + # @param [Service] service service to boot + # @return [Array, Array] true if all the nodes + # were created, false and the error reason if there was a problem + # creating the VMs + def shutdown_step(service) + Log.debug LOG_COMP, "Shutdown step", service.id() + + roles_shutdown = get_roles_shutdown(service) + + roles_shutdown.each { |name, role| + Log.debug LOG_COMP, "Shutting down role #{name}", service.id() + + rc = role.shutdown + + if !rc[0] + role.set_state(Role::STATE['FAILED_UNDEPLOYING']) + + return rc + else + role.set_state(Role::STATE['UNDEPLOYING']) + end + } + + return [true, nil] + end + + # If a role needs to scale, its cardinality is updated, and its state is set + # to SCALING. Only one role is set to scale. + # @param [Service] service + # @return [true|false] true if any role needs to scale + def apply_scaling_policies(service) + Log.debug LOG_COMP, "Apply scaling policies", service.id() + + service.get_roles.each do |name, role| + diff, cooldown_duration = role.scale? + + if diff != 0 + Log.debug LOG_COMP, "Role #{name} needs to scale #{diff} nodes", service.id() + + role.set_cardinality(role.cardinality() + diff) + + role.set_state(Role::STATE['SCALING']) + role.set_cooldown_duration(cooldown_duration) + + return true + end + end + + return false + end + + # If a role is scaling, the nodes are created/destroyed to match the current + # cardinality + # @return [Array, Array] true if the action was + # performed, false and the error reason if there was a problem + def scale_step(service) + Log.debug LOG_COMP, "Scale step", service.id() + + service.get_roles.each do |name, role| + + if role.state == Role::STATE['SCALING'] + rc = role.scale() + + if !rc[0] + role.set_state(Role::STATE['FAILED_SCALING']) + + return rc + end + end + end + + return [true, nil] + end + + # Performs a monitor step, check if the roles already deployed are running + # @param [Service] service service to monitor + # @return [nil] + def monitor_step(service) + Log.debug LOG_COMP, "Monitor step", service.id() + + roles_monitor = get_roles_monitor(service) + + roles_monitor.each { |name, role| + Log.debug LOG_COMP, "Monitoring role #{name}", service.id() + + rc = role.info + + case role.state() + when Role::STATE['RUNNING'] + if OpenNebula.is_error?(rc) || role_nodes_warning?(role) + role.set_state(Role::STATE['WARNING']) + end + + role.update_cardinality() + when Role::STATE['WARNING'] + if !OpenNebula.is_error?(rc) && !role_nodes_warning?(role) + role.set_state(Role::STATE['RUNNING']) + end + + role.update_cardinality() + when Role::STATE['DEPLOYING'] + if OpenNebula.is_error?(rc) + role.set_state(Role::STATE['FAILED_DEPLOYING']) + elsif role_nodes_running?(role) + role.set_state(Role::STATE['RUNNING']) + elsif any_node_failed?(role) + role.set_state(Role::STATE['FAILED_DEPLOYING']) + end + when Role::STATE['SCALING'] + if OpenNebula.is_error?(rc) + role.set_state(Role::STATE['FAILED_SCALING']) + elsif role_finished_scaling?(role) + if role.apply_cooldown_duration() + role.set_state(Role::STATE['COOLDOWN']) + else + role.set_state(Role::STATE['RUNNING']) + end + elsif any_node_failed_scaling?(role) + role.set_state(Role::STATE['FAILED_SCALING']) + end + when Role::STATE['COOLDOWN'] + if role.cooldown_over? + role.set_state(Role::STATE['RUNNING']) + end + + role.update_cardinality() + when Role::STATE['UNDEPLOYING'] + if OpenNebula.is_error?(rc) + role.set_state(Role::STATE['FAILED_UNDEPLOYING']) + elsif role_nodes_done?(role) + role.set_state(Role::STATE['DONE']) + elsif any_node_failed?(role) + role.set_state(Role::STATE['FAILED_UNDEPLOYING']) + end + when Role::STATE['FAILED_DEPLOYING'] + if !OpenNebula.is_error?(rc) && role_nodes_running?(role) + role.set_state(Role::STATE['RUNNING']) + end + when Role::STATE['FAILED_UNDEPLOYING'] + if !OpenNebula.is_error?(rc) && role_nodes_done?(role) + role.set_state(Role::STATE['DONE']) + end + when Role::STATE['FAILED_SCALING'] + if !OpenNebula.is_error?(rc) && role_finished_scaling?(role) + role.set_state(Role::STATE['SCALING']) + end + end + } + end + +protected + # All subclasses must define these methods + + # Returns all node Roles ready to be deployed + # @param [Service] service + # @return [Hash] Roles + def get_roles_deploy(service) + result = service.get_roles.select {|name, role| + role.state == Role::STATE['PENDING'] || + role.state == Role::STATE['DEPLOYING'] + } + + # Ruby 1.8 compatibility + if result.instance_of?(Array) + result = Hash[result] + end + + result + end + + # Returns all node Roles be monitored + # @param [Service] service + # @return [Hash] Roles + def get_roles_monitor(service) + result = service.get_roles.select {|name, role| + ![Role::STATE['PENDING'], Role::STATE['DONE']].include?(role.state) + } + + # Ruby 1.8 compatibility + if result.instance_of?(Array) + result = Hash[result] + end + + result + end + + # Returns all node Roles ready to be shutdown + # @param [Service] service + # @return [Hash] Roles + def get_roles_shutdown(service) + result = service.get_roles.select {|name, role| + ![Role::STATE['UNDEPLOYING'], + Role::STATE['DONE'], + Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) + } + + # Ruby 1.8 compatibility + if result.instance_of?(Array) + result = Hash[result] + end + + result + end + + # Determine if the role nodes are running + # @param [Role] role + # @return [true|false] + def role_nodes_running?(role) + if role.get_nodes.size() != role.cardinality() + return false + end + + role.get_nodes.each { |node| + if node && node['vm_info'] + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + # !(ACTIVE && RUNNING) + if (vm_state != '3') || (lcm_state != '3') + return false + end + else + return false + end + } + + return true + end + + # Returns true if any VM is in UNKNOWN or FAILED + # @param [Role] role + # @return [true|false] + def role_nodes_warning?(role) + role.get_nodes.each do |node| + if node && node['vm_info'] + vm_state = node['vm_info']['VM']['STATE'] + lcm_state = node['vm_info']['VM']['LCM_STATE'] + + # UNKNOWN or FAILED + if (vm_state == '3' && lcm_state == '16') || vm_state == '7' + return true + end + end + end + + return false + end + + # Determine if any of the role nodes failed + # @param [Role] role + # @return [true|false] + def any_node_failed?(role) + role.get_nodes.each { |node| + if node && node['vm_info'] + vm_state = node['vm_info']['VM']['STATE'] + + if vm_state == '7' # FAILED + return true + end + end + } + + return false + end + + # Determine if the role nodes are in done state + # @param [Role] role + # @return [true|false] + def role_nodes_done?(role) + role.get_nodes.each { |node| + if node && node['vm_info'] + vm_state = node['vm_info']['VM']['STATE'] + + if vm_state != '6' # DONE + return false + end + else + return false + end + } + + return true + end + + # Determine if any of the role nodes failed to scale + # @param [Role] role + # @return [true|false] + def any_node_failed_scaling?(role) + role.get_nodes.each { |node| + if node && node['vm_info'] && + (node['disposed'] == '1' || node['scale_up'] == '1') && + node['vm_info']['VM']['STATE'] == '7' # FAILED + + return true + end + } + + return false + end + + def role_finished_scaling?(role) + role.get_nodes.each { |node| + if node && node['vm_info'] + # For scale up, check new nodes are running, or past running + if node['scale_up'] == '1' + vm_state = node['vm_info']['VM']['STATE'].to_i + lcm_state = node['vm_info']['VM']['LCM_STATE'].to_i + + # If any node didn't make it through the initial deployment, + # return false + + # INIT, PENDING, HOLD || + # ACTIVE && (LCM_INIT, PROLOG, BOOT) + + if vm_state == 7 || + ((vm_state < 3) || (vm_state == 3 && lcm_state < 3 )) + + return false + end + end + else + return false + end + } + + # TODO: If a shutdown ends in running again (VM doesn't have acpi), + # the role/service will stay in SCALING + + # For scale down, it will finish when scaling nodes are deleted + return role.get_nodes.size() == role.cardinality() + end +end diff --git a/src/flow/lib/strategy/straight.rb b/src/flow/lib/strategy/straight.rb new file mode 100644 index 0000000000..e7d8994db4 --- /dev/null +++ b/src/flow/lib/strategy/straight.rb @@ -0,0 +1,151 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +module Straight + # Using this strategy the service is deployed based on a directed + # acyclic graph where each node defines its parents. + # + # For example: + # + # mysql nfs + # | | \ + # | | kvm + # \ | / + # front-end + # + # The above graph represents the following service: + # + # "name": "my_first_service", + # "roles": [ + # { + # "name": "front-end", + # "parents": [ + # "mysql", + # "kvm", + # "nfs" + # ], + # ... + # }, + # { + # "name": "nfs", + # ... + # }, + # { + # "name": "mysql", + # ... + # }, + # { + # "name": "kvm", + # "parents": "nfs", + # ... + # } + # ], + # "deployment": "straight" + # } + # + # The roles will be deployed in the following order: + # 1. myslq & nfs + # 2. kvm + # 3. front-end + # + # And the service will be shutdown in the reverse order + # 1. front-end + # 2. kvm & myslq + # 3. nfs + + + + # Returns all node Roles ready to be deployed + # @param [Service] service + # @return [Hash] Roles + def get_roles_deploy(service) + roles = service.get_roles + + running_roles = roles.select {|name, role| + role.state == Role::STATE['RUNNING'] + } + + # Ruby 1.8 compatibility + if running_roles.instance_of?(Array) + running_roles = Hash[running_roles] + end + + result = roles.select {|name, role| + check = true + + if role.state == Role::STATE['PENDING'] + role.parents.each { |parent| + if !running_roles.include?(parent) + check = false + break + end + } + elsif role.state == Role::STATE['DEPLOYING'] + check = true + else + check = false + end + + check + } + + # Ruby 1.8 compatibility + if result.instance_of?(Array) + result = Hash[result] + end + + result + end + + # Returns all node Roles ready to be shutdown + # @param [Service] service + # @return [Hash] Roles + def get_roles_shutdown(service) + roles = service.get_roles + + # Get all the parents from running roles + parents = [] + running_roles = {} + + roles.each { |name, role| + # All roles can be shutdown, except the ones in these states + if (![Role::STATE['UNDEPLOYING'], + Role::STATE['DONE'], + Role::STATE['FAILED_UNDEPLOYING']].include?(role.state) ) + + running_roles[name]= role + end + + # Only the parents of DONE roles can be shutdown + if (role.state != Role::STATE['DONE'] ) + parents += role.parents + end + } + + # Select the nodes that are not parent from any node + result = running_roles.select {|name, role| + !parents.include?(name) + } + + # Ruby 1.8 compatibility + if result.instance_of?(Array) + result = Hash[result] + end + + result + end + +end \ No newline at end of file diff --git a/src/flow/lib/validator.rb b/src/flow/lib/validator.rb new file mode 100644 index 0000000000..f696aa9347 --- /dev/null +++ b/src/flow/lib/validator.rb @@ -0,0 +1,435 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'uri' + +class Hash + # Returns a new hash containing the contents of other_hash and the + # contents of self. If the value for entries with duplicate keys + # is a Hash, it will be merged recursively, otherwise it will be that + # of other_hash. + # + # @param [Hash] other_hash + # + # @return [Hash] Containing the merged values + # + # @example Merging two hashes + # h1 = {:a => 3, {:b => 3, :c => 7}} + # h2 = {:a => 22, c => 4, {:b => 5}} + # + # h1.deep_merge(h2) #=> {:a => 22, c => 4, {:b => 5, :c => 7}} + def deep_merge(other_hash) + target = dup + + other_hash.each do |hash_key, hash_value| + if hash_value.is_a?(Hash) and self[hash_key].is_a?(Hash) + target[hash_key] = self[hash_key].deep_merge(hash_value) + elsif hash_value.is_a?(Array) and self[hash_key].is_a?(Array) + hash_value.each_with_index { |elem, i| + if self[hash_key][i].is_a?(Hash) and elem.is_a?(Hash) + target[hash_key][i] = self[hash_key][i].deep_merge(elem) + else + target[hash_key] = hash_value + end + } + else + target[hash_key] = hash_value + end + end + + target + end +end + +module Validator + +class ParseException < StandardError; end +class SchemaException < StandardError; end + +class Validator + + # @param [Hash] opts the options to validate a body + # @option opts [Boolean] :default_values Set default values if the schema + # specifies it (if true) + # @option opts [Booblean] :delete_extra_properties If the body contains properties + # not specified in the schema delete them from the body (if true) + # or raise an exception (if false) + def initialize(opts={}) + @opts = { + :default_values => true, + :delete_extra_properties => false + }.merge(opts) + end + + # Recursively validate and modify a JSON body based on a schema. + # + # @see http://tools.ietf.org/html/draft-zyp-json-schema-03 + # + # @param [Hash, Array, String, nil] body JSON represented as Ruby objects + # @param [Hash] schema that will be used to validate + # @param [String] key of the body that will be validated in this step + # + # @return [Hash, Array, String, nil] The modified body + # + # @raise [SchemaException] If the schema is not correctly defined + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate a User + # schema = { + # :type => :object, + # :properties => { + # 'username' => { + # :type => :string + # } + # } + # } + # + # hash = { + # 'username' => 'pepe' + # } + # + # Validator.validate!(hash, schema) + # #=> {'username' => 'pepe'} + # + # @note The parameter body will be modified + # @note Schema options supported + # :extends + # :type => [:object, :array, :string, :null] + # + def validate!(body, schema, key="") + if schema[:extends] + base_schema = schema.delete(:extends) + schema = base_schema.deep_merge(schema) + end + + case schema[:type] + when :object then validate_object(body, schema, key) + when :array then validate_array(body, schema, key) + when :string then validate_string(body, schema, key) + when :integer then validate_integer(body, schema, key) + when :null then validate_null(body, schema, key) + else raise SchemaException, "type #{schema[:type]} is not a valid type" + end + end + + private + + # Validate an object type + # + # @param [Hash] body to be validated + # @param [Hash] schema_object of the objectto validate the body + # @param [String] key of the body that will be validated in this step + # + # @return [Hash] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate with default values + # schema_body = { + # :type => :object, + # :properties => { + # 'username' => { + # :type => :string, + # :default => 'def' + # } + # } + # + # body = {} + # + # Validator.validate_object(body, schema_body) + # #=> {'username' => 'def'} + # + # @note The parameter body will be modified + # @note Schema options supported + # :properties + # :required + # :default + # + def validate_object(body, schema_object, key) + unless body.is_a?(Hash) + raise ParseException, "KEY: #{key} must be a Hash; SCHEMA:"\ + "#{schema_object}" + end + + new_body = body.dup + + schema_object[:properties].each{ |schema_key, schema_value| + body_value = new_body.delete(schema_key) + + if body_value + body[schema_key] = validate!(body_value, schema_value, + schema_key) + else + if schema_value[:required] + raise ParseException, "KEY: '#{schema_key}' is required;"\ + " SCHEMA: #{schema_value}" + end + + if @opts[:default_values] && schema_value[:default] + body[schema_key] = schema_value[:default] + end + end + } + + # raise error if body.keys is not empty + unless new_body.keys.empty? + if @opts[:delete_extra_properties] + new_body.keys.each{ |key| + body.delete(key) + } + else + raise ParseException, "KEY: #{new_body.keys.join(', ')} not"\ + " allowed; SCHEMA: #{schema_object}" + end + end + + body + end + + # Validate an array type + # + # @param [Array] body to be validated + # @param [Hash] schema_array of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [Hash] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate array + # schema = { + # :type => :array, + # :items => { + # :type => :string + # } + # } + # + # body = ['pepe', 'luis', 'juan'] + # + # Validator.validate_array(body, schema) + # #=> 'username' => ['pepe', 'luis', 'juan'] + # + # @note The parameter body will be modified + # @note Schema options supported + # :items + # + def validate_array(body, schema_array, schema_key) + if body.instance_of?(Array) + body.collect { |body_item| + validate!(body_item, schema_array[:items], schema_key) + } + else + raise ParseException, "KEY: '#{schema_key}' must be an Array;"\ + " SCHEMA: #{schema_array}" + end + end + + # Validate an integer type + # + # @param [Array] body to be validated + # @param [Hash] schema_array of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [Hash] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate array + # schema = { + # :type => :integer + # } + # + # body = 5 + # + # Validator.validate_integer(body, schema) + # #=> 5 + # + # + def validate_integer(body, schema_array, schema_key) + value = Integer(body) + + if schema_array[:maximum] + excl = schema_array[:exclusiveMaximum] + max = schema_array[:maximum] + if !(excl ? value < max : value <= max) + raise ParseException, "KEY: '#{schema_key}' must be "\ + "lower than #{excl ? '' : 'or equal to'} #{max};"\ + " SCHEMA: #{schema_array}" + end + end + + if schema_array[:minimum] + excl = schema_array[:exclusiveMinimum] + min = schema_array[:minimum] + if !(excl ? value > min : value >= min) + raise ParseException, "KEY: '#{schema_key}' must be "\ + "greater than #{excl ? '' : 'or equal to'} #{min};"\ + " SCHEMA: #{schema_array}" + end + end + + value + rescue ArgumentError + raise ParseException, "KEY: '#{schema_key}' must be an Integer;"\ + " SCHEMA: #{schema_array}" + end + + # Validate an null type + # + # @param [nil] body to be validated + # @param [Hash] schema_null of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [nil] + # + # @raise [ParseException] if the body is not nil + # + # @example Validate array + # schema = { + # :type => :null + # } + # + # body = nil + # + # Validator.validate_null(body, schema) + # #=> nil + # + # + def validate_null(body, schema_null, schema_key) + if body != nil + raise ParseException, "KEY: '#{schema_key}' is not allowed;"\ + " SCHEMA: #{schema_null}" + end + end + + # Validate an string type + # + # @param [String] body to be validated + # @param [Hash] schema_string of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [String] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate array + # schema = { + # :type => :string + # } + # + # body = "pepe" + # + # Validator.validate_string(body, schema) + # #=> "pepe" + # + # @note The parameter body will be modified + # @note Schema options supported + # :format + # :enum + # + def validate_string(body, schema_string, schema_key) + if body.instance_of?(String) + if schema_string[:format] + check_format(body, schema_string, schema_key) + elsif schema_string[:enum] + check_enum(body, schema_string, schema_key) + else + body + end + else + raise ParseException, "KEY: '#{schema_key}' must be a String;"\ + " SCHEMA: #{schema_string}" + end + end + + # Validate an string format + # + # @param [String] body_value to be validated + # @param [Hash] schema_string of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [String] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate array + # schema = { + # :type => :string, + # :format => :url + # } + # + # body = "http://localhost:4567" + # + # Validator.check_format(body, schema) + # #=> "http://localhost:4567" + # + # @note The parameter body will be modified + # @note Schema options supported + # :url + # + def check_format(body_value, schema_string, schema_key) + case schema_string[:format] + when :uri + begin + require 'uri' + uri = URI.parse(body_value) + rescue + raise ParseException, "KEY: '#{schema_key}' must be a valid URL;"\ + " SCHEMA: #{schema_string} #{$!.message}" + end + + body_value + end + + body_value + end + + # Validate an string enum + # + # @param [String] body_value to be validated + # @param [Hash] schema_string of the object to validate the body + # @param [String] schema_key of the body that will be validated in this step + # + # @return [String] The modified body + # + # @raise [ParseException] if the body does not meet the schema definition + # + # @example Validate array + # schema = { + # :type => :string, + # :enum => ['juan', 'luis'] + # } + # + # body = "juan" + # + # Validator.check_enum(body, schema) + # #=> "juan" + # + # @note The parameter body will be modified + # @note Schema options supported + # :enum + # + def check_enum(body_value, schema_string, schema_key) + if schema_string[:enum].include?(body_value) + body_value + else + raise ParseException, "KEY: '#{schema_key}' must be one of"\ + " #{schema_string[:enum].join(', ')}; SCHEMA: #{schema_string}" + end + end +end + +end \ No newline at end of file diff --git a/src/flow/oneflow-server.rb b/src/flow/oneflow-server.rb new file mode 100644 index 0000000000..5309f18f45 --- /dev/null +++ b/src/flow/oneflow-server.rb @@ -0,0 +1,468 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'rubygems' +require 'sinatra' +require 'yaml' + +ONE_LOCATION = ENV["ONE_LOCATION"] + +if !ONE_LOCATION + LOG_LOCATION = "/var/log/one" + VAR_LOCATION = "/var/lib/one" + ETC_LOCATION = "/etc/one" + LIB_LOCATION = "/usr/lib/one" + RUBY_LIB_LOCATION = "/usr/lib/one/ruby" +else + VAR_LOCATION = ONE_LOCATION + "/var" + LOG_LOCATION = ONE_LOCATION + "/var" + ETC_LOCATION = ONE_LOCATION + "/etc" + LIB_LOCATION = ONE_LOCATION+"/lib" + RUBY_LIB_LOCATION = ONE_LOCATION+"/lib/ruby" +end + +ONEFLOW_AUTH = VAR_LOCATION + "/.one/oneflow_auth" + +ONEFLOW_LOG = LOG_LOCATION + "/oneflow.log" +CONFIGURATION_FILE = ETC_LOCATION + "/oneflow-server.conf" + +$: << RUBY_LIB_LOCATION +$: << RUBY_LIB_LOCATION+'/cloud' +$: << LIB_LOCATION+'/oneflow/lib' + +require 'CloudAuth' +require 'CloudServer' + +require 'models' +require 'log' + +############################################################################## +# Configuration +############################################################################## + +begin + conf = YAML.load_file(CONFIGURATION_FILE) +rescue Exception => e + STDERR.puts "Error parsing config file #{CONFIGURATION_FILE}: #{e.message}" + exit 1 +end + +conf[:debug_level] ||= 2 +conf[:lcm_interval] ||= 30 +conf[:default_cooldown] ||= 300 +conf[:shutdown_action] ||= 'shutdown' +conf[:action_number] ||= 1 +conf[:action_period] ||= 60 + +conf[:auth] = 'opennebula' + +set :bind, conf[:host] +set :port, conf[:port] + +set :config, conf + +include CloudLogger +logger = enable_logging ONEFLOW_LOG, conf[:debug_level].to_i + +use Rack::Session::Pool, :key => 'oneflow' + +Log.logger = logger +Log.level = conf[:debug_level].to_i + + +LOG_COMP = "ONEFLOW" + +Log.info LOG_COMP, "Starting server" + +begin + ENV["ONE_CIPHER_AUTH"] = ONEFLOW_AUTH + cloud_auth = CloudAuth.new(conf) +rescue => e + message = "Error initializing authentication system : #{e.message}" + Log.error LOG_COMP, message + STDERR.puts message + exit -1 +end + +set :cloud_auth, cloud_auth + +############################################################################## +# Helpers +############################################################################## + + +before do + auth = Rack::Auth::Basic::Request.new(request.env) + + if auth.provided? && auth.basic? + username, password = auth.credentials + + @client = OpenNebula::Client.new("#{username}:#{password}") + else + error 401, "A username and password must be provided" + end +end + +############################################################################## +# Defaults +############################################################################## + +Role.init_default_cooldown(conf[:default_cooldown]) +Role.init_default_shutdown(conf[:shutdown_action]) + +############################################################################## +# LCM thread +############################################################################## + +t = Thread.new { + require 'LifeCycleManager' + + ServiceLCM.new(conf[:lcm_interval], settings.cloud_auth).loop +} +t.abort_on_exception = true + + +############################################################################## +# Service +############################################################################## + +get '/service' do + service_pool = OpenNebula::ServicePool.new(@client, OpenNebula::Pool::INFO_ALL) + + rc = service_pool.info + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 200 + + body service_pool.to_json +end + +get '/service/:id' do + service_pool = OpenNebula::ServicePool.new(@client) + + service = service_pool.get(params[:id]) + + if OpenNebula.is_error?(service) + error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + end + + status 200 + + body service.to_json +end + +delete '/service/:id' do + service_pool = OpenNebula::ServicePool.new(@client) + + rc = nil + service = service_pool.get(params[:id]) { |service| + rc = service.delete + } + + if OpenNebula.is_error?(service) + error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 201 +end + +post '/service/:id/action' do + service_pool = OpenNebula::ServicePool.new(@client) + action = JSON.parse(request.body.read)['action'] + opts = action['params'] + + rc = nil + service = service_pool.get(params[:id]) { |service| + rc = case action['perform'] + when 'shutdown' + service.shutdown + when 'recover', 'deploy' + service.recover + when 'chown' + if opts && opts['owner_id'] + args = Array.new + args << opts['owner_id'].to_i + args << (opts['group_id'] || -1).to_i + + ret = service.chown(*args) + + if !OpenNebula.is_error?(ret) + Log.info(LOG_COMP, "Service owner changed to #{args[0]}:#{args[1]}", params[:id]) + end + + ret + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify a UID") + end + when 'chgrp' + if opts && opts['group_id'] + ret = service.chown(-1, opts['group_id'].to_i) + + if !OpenNebula.is_error?(ret) + Log.info(LOG_COMP, "Service group changed to #{opts['group_id']}", params[:id]) + end + + ret + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify a GID") + end + when 'chmod' + if opts && opts['octet'] + ret = service.chmod_octet(opts['octet']) + + if !OpenNebula.is_error?(ret) + Log.info(LOG_COMP, "Service permissions changed to #{opts['octet']}", params[:id]) + end + + ret + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify an OCTET") + end + else + OpenNebula::Error.new("Action #{action['perform']} not supported") + end + } + + if OpenNebula.is_error?(service) + error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 201 +end + +put '/service/:id/role/:name' do + + service_pool = OpenNebula::ServicePool.new(@client) + + rc = nil + service = service_pool.get(params[:id]) do |service| + begin + rc = service.update_role(params[:name], request.body.read) + rescue Validator::ParseException, JSON::ParserError + return error 400, $!.message + end + end + + if OpenNebula.is_error?(service) + error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 200 +end + +post '/service/:id/role/:role_name/action' do + service_pool = OpenNebula::ServicePool.new(@client) + action = JSON.parse(request.body.read)['action'] + opts = action['params'] + + rc = nil + service = service_pool.get(params[:id]) { |service| + roles = service.get_roles + + role = roles[params[:role_name]] + if role.nil? + rc = OpenNebula::Error.new("Role '#{params[:role_name]}' not found") + else + # Use defaults only if one of the options is supplied + if opts['period'].nil? ^ opts['number'].nil? + opts['period'] = conf[:action_period] if opts['period'].nil? + opts['number'] = conf[:action_number] if opts['number'].nil? + end + + rc = role.batch_action(action['perform'], opts['period'], opts['number']) + end + } + + if OpenNebula.is_error?(service) + error CloudServer::HTTP_ERROR_CODE[service.errno], service.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 201 + body rc.to_json +end + +############################################################################## +# Service Template +############################################################################## + +get '/service_template' do + s_template_pool = OpenNebula::ServiceTemplatePool.new(@client, OpenNebula::Pool::INFO_ALL) + + rc = s_template_pool.info + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 200 + + body s_template_pool.to_json +end + +get '/service_template/:id' do + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + + rc = service_template.info + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 200 + + body service_template.to_json +end + +delete '/service_template/:id' do + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + + rc = service_template.delete + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + status 201 +end + +put '/service_template/:id' do + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + + begin + rc = service_template.update(request.body.read) + rescue Validator::ParseException, JSON::ParserError + error 400, $!.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + service_template.info + + status 200 + body service_template.to_json +end + +post '/service_template' do + s_template = OpenNebula::ServiceTemplate.new( + OpenNebula::ServiceTemplate.build_xml, + @client) + + begin + rc = s_template.allocate(request.body.read) + rescue Validator::ParseException, JSON::ParserError + error 400, $!.message + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + s_template.info + + status 201 + #body Parser.render(rc) + body s_template.to_json +end + +post '/service_template/:id/action' do + service_template = OpenNebula::ServiceTemplate.new_with_id(params[:id], @client) + + action = JSON.parse(request.body.read)['action'] + + opts = action['params'] + + rc = case action['perform'] + when 'instantiate' + rc = service_template.info + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + service = OpenNebula::Service.new(OpenNebula::Service.build_xml, @client) + rc = service.allocate(service_template.template) + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + end + + service.info + + body service.to_json + when 'chown' + if opts && opts['owner_id'] + args = Array.new + args << opts['owner_id'].to_i + args << (opts['group_id'].to_i || -1) + + service_template.chown(*args) + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify a UID") + end + when 'chgrp' + if opts && opts['group_id'] + service_template.chown(-1, opts['group_id'].to_i) + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify a GID") + end + when 'chmod' + if opts && opts['octet'] + service_template.chmod_octet(opts['octet']) + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to specify an OCTET") + end + when 'update' + if opts && opts['template_json'] + begin + rc = service_template.update(opts['template_json']) + rescue Validator::ParseException, JSON::ParserError + OpenNebula::Error.new($!.message) + end + else + OpenNebula::Error.new("Action #{action['perform']}: " << + "You have to provide a template") + end + else + OpenNebula::Error.new("Action #{action['perform']} not supported") + end + + if OpenNebula.is_error?(rc) + error CloudServer::HTTP_ERROR_CODE[rc.errno], rc.message + else + status 201 + end +end \ No newline at end of file diff --git a/src/oca/ruby/opennebula/oneflow_client.rb b/src/oca/ruby/opennebula/oneflow_client.rb new file mode 100644 index 0000000000..d0a8d7f443 --- /dev/null +++ b/src/oca/ruby/opennebula/oneflow_client.rb @@ -0,0 +1,387 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +require 'uri' +require 'cloud/CloudClient' + +include CloudCLI + +module Role + # Actions that can be performed on the VMs of a given Role + SCHEDULE_ACTIONS = [ + 'shutdown', + 'shutdown-hard', + 'undeploy', + 'undeploy-hard', + 'hold', + 'release', + 'stop', + 'suspend', + 'resume', + 'boot', + 'delete', + 'delete-recreate', + 'reboot', + 'reboot-hard', + 'poweroff', + 'poweroff-hard', + 'snapshot-create' + ] + + STATE = { + 'PENDING' => 0, + 'DEPLOYING' => 1, + 'RUNNING' => 2, + 'UNDEPLOYING' => 3, + 'WARNING' => 4, + 'DONE' => 5, + 'FAILED_UNDEPLOYING' => 6, + 'FAILED_DEPLOYING' => 7, + 'SCALING' => 8, + 'FAILED_SCALING' => 9, + 'COOLDOWN' => 10 + } + + STATE_STR = [ + 'PENDING', + 'DEPLOYING', + 'RUNNING', + 'UNDEPLOYING', + 'WARNING', + 'DONE', + 'FAILED_UNDEPLOYING', + 'FAILED_DEPLOYING', + 'SCALING', + 'FAILED_SCALING', + 'COOLDOWN' + ] + + # Returns the string representation of the role state + # @param [String] state String number representing the state + # @return the state string + def self.state_str(state_number) + return STATE_STR[state_number.to_i] + end +end + +module Service + + STATE = { + 'PENDING' => 0, + 'DEPLOYING' => 1, + 'RUNNING' => 2, + 'UNDEPLOYING' => 3, + 'WARNING' => 4, + 'DONE' => 5, + 'FAILED_UNDEPLOYING' => 6, + 'FAILED_DEPLOYING' => 7, + 'SCALING' => 8, + 'FAILED_SCALING' => 9, + 'COOLDOWN' => 10 + } + + STATE_STR = [ + 'PENDING', + 'DEPLOYING', + 'RUNNING', + 'UNDEPLOYING', + 'WARNING', + 'DONE', + 'FAILED_UNDEPLOYING', + 'FAILED_DEPLOYING', + 'SCALING', + 'FAILED_SCALING', + 'COOLDOWN' + ] + + # Returns the string representation of the service state + # @param [String] state String number representing the state + # @return the state string + def self.state_str(state_number) + return STATE_STR[state_number.to_i] + end + + # Build a json specifying an action + # @param [String] perform action to be performed (i.e: shutdowm) + # @param [Hash, nil] params contains the params for the action + # @return [String] json representing the action + def self.build_json_action(perform, params=nil) + body = Hash.new + body['perform'] = perform + body['params'] = params if params + + action = Hash.new + action['action'] = body + + JSON.pretty_generate action + end + + # CLI options + + DEFAULT_OPTIONS = [ + ENDPOINT = { + :name => "server", + :short => "-s url", + :large => "--server url", + :format => String, + :description => "Service endpoint" + }, + USERNAME={ + :name => "username", + :short => "-u name", + :large => "--username name", + :format => String, + :description => "User name" + }, + PASSWORD={ + :name => "password", + :short => "-p pass", + :large => "--password pass", + :format => String, + :description => "User password" + } + ] + + JSON_FORMAT = { + :name => "json", + :short => "-j", + :large => "--json", + :description => "Print the resource in JSON" + } + + TOP = { + :name => "top", + :short => "-t", + :large => "--top", + :description => "Top for the command" + } + + PERIOD = { + :name => "period", + :short => "-p x", + :large => "--period x", + :format => Integer, + :description => "Seconds between each group of actions" + } + + NUMBER = { + :name => "number", + :short => "-n x", + :large => "--number x", + :format => Integer, + :description => "Number of VMs to apply the action to each period" + } + + FORCE = { + :name => "force", + :short => "-f", + :large => "--force", + :description => "Force the new cardinality even if it is outside the limits" + } + + # Format helpers + +# def self.rname_to_id(name, poolname, options) + def self.rname_to_id(name, poolname) + return 0, name.to_i if name.match(/^[0123456789]+$/) + + client = Service::Client.new() + + resource_path = case poolname + when "SERVICE" then "/service" + when "SERVICE TEMPLATE" then "/service_template" + end + + response = client.get(resource_path) + + if CloudClient::is_error?(response) + return -1, "OpenNebula #{poolname} name not found," << + " use the ID instead" + end + + pool = JSON.parse(response.body) + name_to_id(name, pool, poolname) + end + + def self.rname_to_id_desc(poolname) + "OpenNebula #{poolname} name or id" + end + + def self.name_to_id(name, pool, ename) + if pool['DOCUMENT_POOL']['DOCUMENT'].nil? + return -1, "#{ename} named #{name} not found." + end + + objects = pool['DOCUMENT_POOL']['DOCUMENT'].select {|object| object['NAME'] == name } + + if objects.length>0 + if objects.length>1 + return -1, "There are multiple #{ename}s with name #{name}." + else + result = objects.first['ID'] + end + else + return -1, "#{ename} named #{name} not found." + end + + return 0, result + end + + def self.list_to_id(names, poolname) + + client = Service::Client.new() + + resource_path = case poolname + when "SERVICE" then "/service" + when "SERVICE TEMPLATE" then "/service_template" + end + + response = client.get(resource_path) + + if CloudClient::is_error?(response) + return -1, "OpenNebula #{poolname} name not found," << + " use the ID instead" + end + + pool = JSON.parse(response.body) + + result = names.split(',').collect { |name| + if name.match(/^[0123456789]+$/) + name.to_i + else + rc = name_to_id(name, pool, poolname) + + if rc.first == -1 + return rc[0], rc[1] + end + + rc[1] + end + } + + return 0, result + end + + def self.list_to_id_desc(poolname) + "Comma-separated list of OpenNebula #{poolname} names or ids" + end + + # Perform an action on several resources + # @param [Array] ids resources ids + # @param [Block] block action to be performed + # @return [Integer] exit_code + def self.perform_actions(ids, &block) + exit_code = 0 + + ids.each do |id| + response = block.call(id) + + if CloudClient::is_error?(response) + puts response.to_s + exit_code = response.code.to_i + end + end + + exit_code + end + + class Client + def initialize(opts={}) + @username = opts[:username] || ENV['ONEFLOW_USER'] + @password = opts[:password] || ENV['ONEFLOW_PASSWORD'] + + url = opts[:url] || ENV['ONEFLOW_URL'] || 'http://localhost:2474' + + if @username.nil? && @password.nil? + if ENV["ONE_AUTH"] and !ENV["ONE_AUTH"].empty? and File.file?(ENV["ONE_AUTH"]) + one_auth = File.read(ENV["ONE_AUTH"]) + elsif File.file?(ENV["HOME"]+"/.one/one_auth") + one_auth = File.read(ENV["HOME"]+"/.one/one_auth") + end + + one_auth.rstrip! + + @username, @password = one_auth.split(':') + end + + @uri = URI.parse(url) + + @user_agent = "OpenNebula #{CloudClient::VERSION} " << + "(#{opts[:user_agent]||"Ruby"})" + + @host = nil + @port = nil + + if ENV['http_proxy'] + uri_proxy = URI.parse(ENV['http_proxy']) + @host = uri_proxy.host + @port = uri_proxy.port + end + end + + def get(path) + req = Net::HTTP::Proxy(@host, @port)::Get.new(path) + + do_request(req) + end + + def delete(path) + req =Net::HTTP::Proxy(@host, @port)::Delete.new(path) + + do_request(req) + end + + def post(path, body) + req = Net::HTTP::Proxy(@host, @port)::Post.new(path) + req.body = body + + do_request(req) + end + + def put(path, body) + req = Net::HTTP::Proxy(@host, @port)::Put.new(path) + req.body = body + + do_request(req) + end + + def login + req = Net::HTTP::Proxy(@host, @port)::Post.new('/login') + + do_request(req) + end + + def logout + req = Net::HTTP::Proxy(@host, @port)::Post.new('/logout') + + do_request(req) + end + + private + + def do_request(req) + req.basic_auth @username, @password + + req['User-Agent'] = @user_agent + + res = CloudClient::http_start(@uri, @timeout) do |http| + http.request(req) + end + + res + end + end +end \ No newline at end of file diff --git a/src/onedb/4.0.1_to_4.1.80.rb b/src/onedb/4.0.1_to_4.1.80.rb index 8a016e58d3..f3920f2b30 100644 --- a/src/onedb/4.0.1_to_4.1.80.rb +++ b/src/onedb/4.0.1_to_4.1.80.rb @@ -30,10 +30,13 @@ module Migrator begin FileUtils.cp("#{VAR_LOCATION}/.one/sunstone_auth", "#{VAR_LOCATION}/.one/onegate_auth", :preserve => true) + + FileUtils.cp("#{VAR_LOCATION}/.one/sunstone_auth", + "#{VAR_LOCATION}/.one/oneflow_auth", :preserve => true) rescue puts "Error trying to copy #{VAR_LOCATION}/.one/sunstone_auth "<< - "to #{VAR_LOCATION}/.one/onegate_auth." - puts "Please copy the file manually." + "to #{VAR_LOCATION}/.one/onegate_auth and #{VAR_LOCATION}/.one/oneflow_auth." + puts "Please copy the files manually." end @db.run "ALTER TABLE user_pool RENAME TO old_user_pool;" diff --git a/src/sunstone/etc/sunstone-oneflow.conf b/src/sunstone/etc/sunstone-oneflow.conf new file mode 100644 index 0000000000..9898b67015 --- /dev/null +++ b/src/sunstone/etc/sunstone-oneflow.conf @@ -0,0 +1,23 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +################################################################################ +# OneFlow +################################################################################ + +# OneFlow endpoint +# +:oneflow_server: http://localhost:2474/ \ No newline at end of file diff --git a/src/sunstone/etc/sunstone-server.conf b/src/sunstone/etc/sunstone-server.conf index f0a86503fc..a0d3b00289 100644 --- a/src/sunstone/etc/sunstone-server.conf +++ b/src/sunstone/etc/sunstone-server.conf @@ -124,3 +124,6 @@ #:routes: # - custom # - other + +:routes: + - oneflow \ No newline at end of file diff --git a/src/sunstone/etc/sunstone-views/admin.yaml b/src/sunstone/etc/sunstone-views/admin.yaml index e5d149054a..a7d99e82fd 100644 --- a/src/sunstone/etc/sunstone-views/admin.yaml +++ b/src/sunstone/etc/sunstone-views/admin.yaml @@ -16,6 +16,9 @@ enabled_tabs: - datastores-tab - vnets-tab - marketplace-tab + #- oneflow-dashboard + #- oneflow-services + #- oneflow-templates tabs: dashboard-tab: panel_tabs: @@ -341,3 +344,86 @@ tabs: actions: Marketplace.refresh: true Marketplace.import: true + oneflow-dashboard: + panel_tabs: + table_columns: + actions: + oneflow-services: + panel_tabs: + service_info_tab: true + service_roles_tab: true + service_log_tab: true + panel_tabs_actions: + service_roles_tab: + Role.scale: true + Role.hold: true + Role.release: true + Role.suspend: true + Role.resume: true + Role.stop: true + Role.boot: true + Role.reboot: true + Role.reboot_hard: true + Role.poweroff: true + Role.poweroff_hard: true + Role.shutdown: true + Role.shutdown_hard: true + Role.delete: true + Role.delete_recreate: true + RoleVM.chown: true + RoleVM.chgrp: true + RoleVM.deploy: true + RoleVM.migrate: true + RoleVM.migrate_live: true + RoleVM.hold: true + RoleVM.release: true + RoleVM.suspend: true + RoleVM.resume: true + RoleVM.stop: true + RoleVM.boot: true + RoleVM.reboot: true + RoleVM.reboot_hard: true + RoleVM.poweroff: true + RoleVM.poweroff_hard: true + RoleVM.undeploy: true + RoleVM.undeploy_hard: true + RoleVM.shutdown: true + RoleVM.shutdown_hard: true + RoleVM.delete: true + RoleVM.delete_recreate: true + RoleVM.resched: true + RoleVM.unresched: true + RoleVM.recover: true + table_columns: + - 0 # Checkbox + - 1 # ID + - 2 # Owner + - 3 # Group + - 4 # Name + - 5 # State + actions: + Service.refresh: true + Service.chown: true + Service.chgrp: true + Service.chmod: true + Service.shutdown: true + Service.recover: true + Service.delete: true + oneflow-templates: + panel_tabs: + service_template_info_panel: true + service_template_roles_tab: true + table_columns: + - 0 # Checkbox + - 1 # ID + - 2 # Owner + - 3 # Group + - 4 # Name + actions: + ServiceTemplate.refresh: true + ServiceTemplate.create_dialog: true + ServiceTemplate.instantiate: true + ServiceTemplate.chown: true + ServiceTemplate.chgrp: true + ServiceTemplate.chmod: true + ServiceTemplate.delete: true diff --git a/src/sunstone/etc/sunstone-views/user.yaml b/src/sunstone/etc/sunstone-views/user.yaml index 8dea51c044..066feb7dfb 100644 --- a/src/sunstone/etc/sunstone-views/user.yaml +++ b/src/sunstone/etc/sunstone-views/user.yaml @@ -16,6 +16,9 @@ enabled_tabs: - datastores-tab - vnets-tab - marketplace-tab + #- oneflow-dashboard + #- oneflow-services + #- oneflow-templates tabs: dashboard-tab: panel_tabs: @@ -342,3 +345,86 @@ tabs: actions: Marketplace.refresh: true Marketplace.import: true + oneflow-dashboard: + panel_tabs: + table_columns: + actions: + oneflow-services: + panel_tabs: + service_info_tab: true + service_roles_tab: true + service_log_tab: true + panel_tabs_actions: + service_roles_tab: + Role.scale: true + Role.hold: true + Role.release: true + Role.suspend: true + Role.resume: true + Role.stop: true + Role.boot: true + Role.reboot: true + Role.reboot_hard: true + Role.poweroff: true + Role.poweroff_hard: true + Role.shutdown: true + Role.shutdown_hard: true + Role.delete: true + Role.delete_recreate: true + RoleVM.chown: false + RoleVM.chgrp: false + RoleVM.deploy: false + RoleVM.migrate: false + RoleVM.migrate_live: false + RoleVM.hold: true + RoleVM.release: true + RoleVM.suspend: true + RoleVM.resume: true + RoleVM.stop: true + RoleVM.boot: true + RoleVM.reboot: true + RoleVM.reboot_hard: true + RoleVM.poweroff: true + RoleVM.poweroff_hard: true + RoleVM.undeploy: true + RoleVM.undeploy_hard: true + RoleVM.shutdown: true + RoleVM.shutdown_hard: true + RoleVM.delete: true + RoleVM.delete_recreate: true + RoleVM.resched: false + RoleVM.unresched: false + RoleVM.recover: false + table_columns: + - 0 # Checkbox + - 1 # ID + - 2 # Owner + - 3 # Group + - 4 # Name + - 5 # State + actions: + Service.refresh: true + Service.chown: false + Service.chgrp: false + Service.chmod: true + Service.shutdown: true + Service.recover: true + Service.delete: true + oneflow-templates: + panel_tabs: + service_template_info_panel: true + service_template_roles_tab: true + table_columns: + - 0 # Checkbox + - 1 # ID + - 2 # Owner + - 3 # Group + - 4 # Name + actions: + ServiceTemplate.refresh: true + ServiceTemplate.create_dialog: true + ServiceTemplate.instantiate: true + ServiceTemplate.chown: false + ServiceTemplate.chgrp: false + ServiceTemplate.chmod: true + ServiceTemplate.delete: true diff --git a/src/sunstone/public/js/plugins/oneflow-dashboard.js b/src/sunstone/public/js/plugins/oneflow-dashboard.js new file mode 100644 index 0000000000..d63e32d671 --- /dev/null +++ b/src/sunstone/public/js/plugins/oneflow-dashboard.js @@ -0,0 +1,22 @@ +// ------------------------------------------------------------------------ // +// Copyright 2010-2013, C12G Labs S.L. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); you may // +// not use this file except in compliance with the License. You may obtain // +// a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +//------------------------------------------------------------------------- // + +var oneflow_dashboard_tab = { + title: 'OneFlow' +} + + +Sunstone.addMainTab('oneflow_dashboard_tab',oneflow_dashboard_tab); diff --git a/src/sunstone/public/js/plugins/oneflow-services.js b/src/sunstone/public/js/plugins/oneflow-services.js new file mode 100644 index 0000000000..9e6570dbc8 --- /dev/null +++ b/src/sunstone/public/js/plugins/oneflow-services.js @@ -0,0 +1,1833 @@ +// ------------------------------------------------------------------------ // +// Copyright 2010-2013, C12G Labs S.L. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); you may // +// not use this file except in compliance with the License. You may obtain // +// a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +//------------------------------------------------------------------------- // +var selected_row_role_id; +var last_selected_row_role; +var last_selected_row_rolevm; +var checked_row_rolevm_ids = []; + +var Service = { + "resource" : 'DOCUMENT', + "path" : 'service', + "shutdown": function(params){ + OpenNebula.Action.simple_action(params, + Service.resource, + "shutdown", + action_obj, + Service.path); + }, + "del": function(params){ + OpenNebula.Action.del(params,Service.resource, Service.path); + }, + "list" : function(params){ + OpenNebula.Action.list(params, Service.resource, Service.path) + }, + "show" : function(params){ + OpenNebula.Action.show(params, Service.resource, false, Service.path) + }, + "chown" : function(params){ + OpenNebula.Action.chown(params,Service.resource, Service.path); + }, + "chgrp" : function(params){ + OpenNebula.Action.chgrp(params,Service.resource, Service.path); + }, + "chmod" : function(params){ + var action_obj = params.data.extra_param; + OpenNebula.Action.simple_action(params, + Service.resource, + "chmod", + action_obj, + Service.path); + }, + "shutdown" : function(params){ + OpenNebula.Action.simple_action(params, + Service.resource, + "shutdown", + null, + Service.path); + }, + "recover" : function(params){ + OpenNebula.Action.simple_action(params, + Service.resource, + "recover", + null, + Service.path); + }, + "state" : function(state_int){ + var state = [ + tr("PENDING"), + tr("DEPLOYING"), + tr("RUNNING"), + tr("UNDEPLOYING"), + tr("WARNING"), + tr("DONE"), + tr("FAILED_UNDEPLOYING"), + tr("FAILED_DEPLOYING"), + tr("SCALING"), + tr("FAILED_SCALING"), + tr("COOLDOWN") + ][state_int] + return state ? state : state_int; + } +} + +var Role = { + "resource" : 'DOCUMENT', + "path" : 'service', + "state" : function(state_int){ + state_int = state_int ? state_int : 0; + var state = [ + tr("PENDING"), + tr("DEPLOYING"), + tr("RUNNING"), + tr("UNDEPLOYING"), + tr("WARNING"), + tr("DONE"), + tr("FAILED_UNDEPLOYING"), + tr("FAILED_DEPLOYING"), + tr("SCALING"), + tr("FAILED_SCALING"), + tr("COOLDOWN") + ][state_int] + return state ? state : state_int; + }, + "hold" : function(params){ + OpenNebula.Action.simple_action(params, + Service.resource, + "hold", + generate_batch_action_params(), + Role.path); + }, + "release" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "release", + generate_batch_action_params(), + Role.path); + }, + "suspend" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "suspend", + generate_batch_action_params(), + Role.path); + }, + "resume" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "resume", + generate_batch_action_params(), + Role.path); + }, + "stop" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "stop", + generate_batch_action_params(), + Role.path); + }, + "boot" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "boot", + generate_batch_action_params(), + Role.path); + }, + "delete_recreate" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "delete-recreate", + generate_batch_action_params(), + Role.path); + }, + "reboot" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "reboot", + generate_batch_action_params(), + Role.path); + }, + "reboot_hard" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "reboot-hard", + generate_batch_action_params(), + Role.path); + }, + "poweroff" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "poweroff", + generate_batch_action_params(), + Role.path); + }, + "poweroff_hard" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "poweroff-hard", + generate_batch_action_params(), + Role.path); + }, + "undeploy" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "undeploy", + generate_batch_action_params(), + Role.path); + }, + "undeploy_hard" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "undeploy-hard", + generate_batch_action_params(), + Role.path); + }, + "snapshot_create" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "snapshot-create", + generate_batch_action_params(), + Role.path); + }, + "shutdown" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "shutdown", + generate_batch_action_params(), + Role.path); + }, + "cancel" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "shutdown-hard", + generate_batch_action_params(), + Role.path); + }, + "del" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "delete", + generate_batch_action_params(), + Role.path); + }, + "recover" : function(params){ + OpenNebula.Action.simple_action(params, + Role.resource, + "recover", + null, + Role.path); + }, + "update" : function(params){ + request = OpenNebula.Helper.request(Role.resource, "update", params.data.id); + + $.ajax({ + url: Role.path + "/" + params.data.id, + type: "PUT", + dataType: "json", + data: JSON.stringify(params.data.extra_param), + success: function(response){ + return roleCallback(request, response); + }, + error: function(response){ + return onError(request, OpenNebula.Error(response)); + } + }); + } +} + +var generate_batch_action_params = function(){ + var action_obj = { + "period" : $("#batch_action_period").val(), + "number" : $("#batch_action_number").val()}; + + return action_obj; +} + +var role_actions = { + "Role.update_dialog" : { + type: "custom", + call: popUpScaleDialog + }, + + "Role.update" : { + type: "multiple", + call: Role.update, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.hold" : { + type: "multiple", + call: Role.hold, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.release" : { + type: "multiple", + call: Role.release, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.suspend" : { + type: "multiple", + call: Role.suspend, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.resume" : { + type: "multiple", + call: Role.resume, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.stop" : { + type: "multiple", + call: Role.stop, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.boot" : { + type: "multiple", + call: Role.boot, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.reboot_hard" : { + type: "multiple", + call: Role.reboot_hard, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.delete_recreate" : { + type: "multiple", + call: Role.delete_recreate, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.reboot" : { + type: "multiple", + call: Role.reboot, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.poweroff" : { + type: "multiple", + call: Role.poweroff, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.poweroff_hard" : { + type: "multiple", + call: Role.poweroff_hard, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.undeploy" : { + type: "multiple", + call: Role.undeploy, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.undeploy_hard" : { + type: "multiple", + call: Role.undeploy_hard, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.snapshot_create" : { + type: "single", + call: Role.snapshot_create, + callback: roleCallback, + error:onError, + notify: true + }, + + "Role.shutdown" : { + type: "multiple", + call: Role.shutdown, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.shutdown_hard" : { + type: "multiple", + call: Role.cancel, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.delete" : { + type: "multiple", + call: Role.del, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "Role.recover" : { + type: "multiple", + call: Role.recover, + callback: roleCallback, + elements: roleElements, + error: onError, + notify: true + }, + + "RoleVM.deploy" : { + type: "multiple", + call: OpenNebula.VM.deploy, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.migrate" : { + type: "multiple", + call: OpenNebula.VM.migrate, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.migrate_live" : { + type: "multiple", + call: OpenNebula.VM.livemigrate, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.hold" : { + type: "multiple", + call: OpenNebula.VM.hold, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.release" : { + type: "multiple", + call: OpenNebula.VM.release, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.suspend" : { + type: "multiple", + call: OpenNebula.VM.suspend, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.resume" : { + type: "multiple", + call: OpenNebula.VM.resume, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.stop" : { + type: "multiple", + call: OpenNebula.VM.stop, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.boot" : { + type: "multiple", + call: OpenNebula.VM.restart, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.reboot_hard" : { + type: "multiple", + call: OpenNebula.VM.reset, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.delete_recreate" : { + type: "multiple", + call: OpenNebula.VM.resubmit, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.reboot" : { + type: "multiple", + call: OpenNebula.VM.reboot, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.poweroff" : { + type: "multiple", + call: OpenNebula.VM.poweroff, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.poweroff_hard" : { + type: "multiple", + call: OpenNebula.VM.poweroff_hard, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.undeploy" : { + type: "multiple", + call: OpenNebula.VM.undeploy, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.undeploy_hard" : { + type: "multiple", + call: OpenNebula.VM.undeploy_hard, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.shutdown" : { + type: "multiple", + call: OpenNebula.VM.shutdown, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.shutdown_hard" : { + type: "multiple", + call: OpenNebula.VM.cancel, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.delete" : { + type: "multiple", + call: OpenNebula.VM.del, + callback: deleteVMachineElement, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.recover" : { + type: "multiple", + call: OpenNebula.VM.recover, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.resched" : { + type: "multiple", + call: OpenNebula.VM.resched, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.unresched" : { + type: "multiple", + call: OpenNebula.VM.unresched, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + + "RoleVM.chown" : { + type: "multiple", + call: OpenNebula.VM.chown, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + }, + "RoleVM.chgrp" : { + type: "multiple", + call: OpenNebula.VM.chgrp, + callback: roleCallback, + elements: roleVMElements, + error: onError, + notify: true + } +}; + +Sunstone.addActions(role_actions); + +function roleElements() { + return getSelectedNodes(servicerolesDataTable); +}; + +function roleVMElements() { + return getSelectedNodes(serviceroleVMsDataTable); +}; + +function roleCallback() { + return $("#service_info_panel_refresh", $("#service_info_panel")).click(); +} + +var role_buttons = { + "Role.update_dialog" : { + type: "action", + text: tr("Scale"), + tip: tr("This will hold selected pending VMs from being deployed"), + layout: "create" + }, + "Role.hold" : { + type: "action", + text: tr("Hold"), + tip: tr("This will hold selected pending VMs from being deployed"), + layout: "vmsplanification_buttons" + }, + "Role.release" : { + type: "action", + text: tr("Release"), + layout: "vmsplanification_buttons", + tip: tr("This will release held machines") + }, + "Role.suspend" : { + type: "action", + text: tr("Suspend"), + layout: "vmspause_buttons", + tip: tr("This will suspend selected machines") + }, + "Role.resume" : { + type: "action", + text: '', + layout: "vmsplay_buttons", + tip: tr("This will resume selected VMs") + }, + "Role.stop" : { + type: "action", + text: tr("Stop"), + layout: "vmsstop_buttons", + tip: tr("This will stop selected VMs") + }, + "Role.boot" : { + type: "action", + text: tr("Boot"), + layout: "vmsplanification_buttons", + tip: tr("This will force the hypervisor boot action of VMs stuck in UNKNOWN or BOOT state") + }, + "Role.reboot" : { + type: "action", + text: tr("Reboot"), + layout: "vmsrepeat_buttons", + tip: tr("This will send a reboot action to running VMs") + }, + "Role.reboot_hard" : { + type: "action", + text: tr("Reboot") + ' hard', + layout: "vmsrepeat_buttons", + tip: tr("This will perform a hard reboot on selected VMs") + }, + "Role.poweroff" : { + type: "action", + text: tr("Power Off"), + layout: "vmspause_buttons", + tip: tr("This will send a power off signal to running VMs. They can be resumed later.") + }, + "Role.poweroff_hard" : { + type: "action", + text: tr("Power Off") + ' hard', + layout: "vmspause_buttons", + tip: tr("This will send a forced power off signal to running VMs. They can be resumed later.") + }, + "Role.undeploy" : { + type: "action", + text: tr("Undeploy"), + layout: "vmsstop_buttons", + tip: tr("Shuts down the given VM. The VM is saved in the system Datastore.") + }, + "Role.undeploy_hard" : { + type: "action", + text: tr("Undeploy") + ' hard', + layout: "vmsstop_buttons", + tip: tr("Shuts down the given VM. The VM is saved in the system Datastore.") + }, + "Role.shutdown" : { + type: "action", + text: tr("Shutdown"), + layout: "vmsdelete_buttons", + tip: tr("This will initiate the shutdown process in the selected VMs") + }, + "Role.shutdown_hard" : { + type: "action", + text: tr("Shutdown") + ' hard', + layout: "vmsdelete_buttons", + tip: tr("This will initiate the shutdown-hard (forced) process in the selected VMs") + }, + "Role.delete" : { + type: "action", + text: tr("Delete"), + layout: "vmsdelete_buttons", + tip: tr("This will delete the selected VMs from the database") + }, + "Role.delete_recreate" : { + type: "action", + text: tr("Delete") + ' recreate', + layout: "vmsrepeat_buttons", + tip: tr("This will delete and recreate VMs to PENDING state") + } +} + + +var role_vm_buttons = { + "RoleVM.chown" : { + type: "confirm_with_select", + text: tr("Change owner"), + select: users_sel, + layout: "user_select", + tip: tr("Select the new owner")+":", + condition: mustBeAdmin + }, + + "RoleVM.chgrp" : { + type: "confirm_with_select", + text: tr("Change group"), + select: groups_sel, + layout: "user_select", + tip: tr("Select the new group")+":", + condition: mustBeAdmin + }, + "RoleVM.deploy" : { + type: "confirm_with_select", + text: tr("Deploy"), + tip: tr("This will deploy the selected VMs on the chosen host"), + layout: "vmsplanification_buttons", + select: hosts_sel, + condition: mustBeAdmin + }, + "RoleVM.migrate" : { + type: "confirm_with_select", + text: tr("Migrate"), + tip: tr("This will migrate the selected VMs to the chosen host"), + layout: "vmsplanification_buttons", + select: hosts_sel, + condition: mustBeAdmin + + }, + "RoleVM.migrate_live" : { + type: "confirm_with_select", + text: tr("Migrate") + ' live', + tip: tr("This will live-migrate the selected VMs to the chosen host"), + layout: "vmsplanification_buttons", + select: hosts_sel, + condition: mustBeAdmin + }, + "RoleVM.hold" : { + type: "action", + text: tr("Hold"), + tip: tr("This will hold selected pending VMs from being deployed"), + layout: "vmsplanification_buttons", + }, + "RoleVM.release" : { + type: "action", + text: tr("Release"), + layout: "vmsplanification_buttons", + tip: tr("This will release held machines") + }, + "RoleVM.suspend" : { + type: "action", + text: tr("Suspend"), + layout: "vmspause_buttons", + tip: tr("This will suspend selected machines") + }, + "RoleVM.resume" : { + type: "action", + text: '', + layout: "vmsplay_buttons", + tip: tr("This will resume selected VMs") + }, + "RoleVM.stop" : { + type: "action", + text: tr("Stop"), + layout: "vmsstop_buttons", + tip: tr("This will stop selected VMs") + }, + "RoleVM.boot" : { + type: "action", + text: tr("Boot"), + layout: "vmsplanification_buttons", + tip: tr("This will force the hypervisor boot action of VMs stuck in UNKNOWN or BOOT state") + }, + "RoleVM.reboot" : { + type: "action", + text: tr("Reboot"), + layout: "vmsrepeat_buttons", + tip: tr("This will send a reboot action to running VMs") + }, + "RoleVM.reboot_hard" : { + type: "action", + text: tr("Reboot") + ' hard', + layout: "vmsrepeat_buttons", + tip: tr("This will perform a hard reboot on selected VMs") + }, + "RoleVM.poweroff" : { + type: "action", + text: tr("Power Off"), + layout: "vmspause_buttons", + tip: tr("This will send a power off signal to running VMs. They can be resumed later.") + }, + "RoleVM.poweroff_hard" : { + type: "action", + text: tr("Power Off") + ' hard', + layout: "vmspause_buttons", + tip: tr("This will send a forced power off signal to running VMs. They can be resumed later.") + }, + "RoleVM.undeploy" : { + type: "action", + text: tr("Undeploy"), + layout: "vmsstop_buttons", + tip: tr("Shuts down the given VM. The VM is saved in the system Datastore.") + }, + "RoleVM.undeploy_hard" : { + type: "action", + text: tr("Undeploy") + ' hard', + layout: "vmsstop_buttons", + tip: tr("Shuts down the given VM. The VM is saved in the system Datastore.") + }, + "RoleVM.shutdown" : { + type: "action", + text: tr("Shutdown"), + layout: "vmsdelete_buttons", + tip: tr("This will initiate the shutdown process in the selected VMs") + }, + "RoleVM.shutdown_hard" : { + type: "action", + text: tr("Shutdown") + ' hard', + layout: "vmsdelete_buttons", + tip: tr("This will initiate the shutdown-hard (forced) process in the selected VMs") + }, + + "RoleVM.delete" : { + type: "action", + text: tr("Delete"), + layout: "vmsdelete_buttons", + tip: tr("This will delete the selected VMs from the database") + }, + "RoleVM.delete_recreate" : { + type: "action", + text: tr("Delete") + ' recreate', + layout: "vmsrepeat_buttons", + tip: tr("This will delete and recreate VMs to PENDING state") + }, + "RoleVM.resched" : { + type: "action", + text: tr("Reschedule"), + layout: "vmsplanification_buttons", + tip: tr("This will reschedule selected VMs") + }, + "RoleVM.unresched" : { + type: "action", + text: tr("Un-Reschedule"), + layout: "vmsplanification_buttons", + tip: tr("This will cancel the rescheduling for the selected VMs") + }, +// "RoleVM.recover" : { +// type: "confirm_with_select", +// text: tr("Recover"), +// layout: "vmsplanification_buttons", +// select: function(){ return '\ +// '}, +// tip: tr("Recovers a stuck VM that is waiting for a driver operation. \ +// The recovery may be done by failing or succeeding the pending operation. \ +// YOU NEED TO MANUALLY CHECK THE VM STATUS ON THE HOST, to decide if the operation \ +// was successful or not.") +// } +} + +var service_tab_content = '\ +
\ +
\ +
\ +
\ +

\ + \ + '+tr("OneFlow - Services")+'\ + \ + \ +  \ + \ + \ +

\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("ID")+''+tr("Owner")+''+tr("Group")+''+tr("Name")+''+tr("State")+'
\ +
\ +
\ +
'; + +var dataTable_services; + +var service_actions = { + "Service.list" : { + type: "list", + call: Service.list, + callback: updateServicesView, + error: onError + }, + + "Service.show" : { + type : "single", + call: Service.show, + callback: updateServiceElement, + error: onError + }, + + "Service.showinfo" : { + type: "single", + call: Service.show, + callback: updateServiceInfo, + error: onError + }, + + "Service.refresh" : { + type: "custom", + call: function () { + waitingNodes(dataTable_services); + Sunstone.runAction("Service.list"); + } + }, + + "Service.autorefresh" : { + type: "custom", + call: function() { + Service.list({timeout: true, success: updateServicesView, error: onError}); + } + }, + + "Service.delete" : { + type: "multiple", + call: Service.del, + callback: deleteServiceElement, + elements: serviceElements, + error: onError, + notify: true + }, + + "Service.chown" : { + type: "multiple", + call: Service.chown, + callback: function (req) { + Sunstone.runAction("Service.show",req.request.data[0][0]); + }, + elements: serviceElements, + error: onError, + notify: true + }, + + "Service.chgrp" : { + type: "multiple", + call: Service.chgrp, + callback: function (req) { + Sunstone.runAction("Service.show",req.request.data[0][0]); + }, + elements: serviceElements, + error: onError, + notify: true + }, + + "Service.chmod" : { + type: "single", + call: Service.chmod, + error: onError, + notify: true + }, + + "Service.shutdown" : { + type: "multiple", + call: Service.shutdown, + elements: serviceElements, + error: onError, + notify: true + }, + + "Service.recover" : { + type: "multiple", + call: Service.recover, + elements: serviceElements, + error: onError, + notify: true + } +}; + + +var service_buttons = { + "Service.refresh" : { + type: "action", + layout: "refresh", + alwaysActive: true + }, + + "Service.chown" : { + type: "confirm_with_select", + text: tr("Change owner"), + select: users_sel, + tip: tr("Select the new owner")+":", + layout: "user_select" + }, + "Service.chgrp" : { + type: "confirm_with_select", + text: tr("Change group"), + select: groups_sel, + tip: tr("Select the new group")+":", + layout: "user_select" + }, + "Service.shutdown" : { + type: "action", + layout: "main", + text: tr("Shutdown") + }, + "Service.recover" : { + type: "action", + layout: "main", + text: tr("Recover") + }, + "Service.delete" : { + type: "confirm", + text: tr("Delete"), + layout: "del", + tip: tr("This will delete the selected services") + } +} + +var service_info_panel = { + "service_info_tab" : { + title: tr("Service information"), + content: "" + } +} + +var services_tab = { + title: "Services", + content: service_tab_content, + buttons: service_buttons, + tabClass: 'subTab', + parentTab: 'oneflow_dashboard_tab' +} + +Sunstone.addActions(service_actions); +Sunstone.addMainTab('oneflow-services',services_tab); +Sunstone.addInfoPanel('service_info_panel',service_info_panel); + + +function serviceElements() { + return getSelectedNodes(dataTable_services); +} + +// Returns an array containing the values of the service_json and ready +// to be inserted in the dataTable +function serviceElementArray(service_json){ + var service = service_json.DOCUMENT; + + return [ + '', + service.ID, + service.UNAME, + service.GNAME, + service.NAME, + Service.state(service.TEMPLATE.BODY.state) + ]; +} + +// Callback to update an element in the dataTable +function updateServiceElement(request, service_json){ + var id = service_json.DOCUMENT.ID; + var element = serviceElementArray(service_json); + updateSingleElement(element,dataTable_services,'#service_'+id); +} + +// Callback to remove an element from the dataTable +function deleteServiceElement(req){ + deleteElement(dataTable_services,'#service_'+req.request.data); +} + +// Callback to add an service element +function addServiceElement(request, service_json){ + var element = serviceElementArray(service_json); + addElement(element,dataTable_services); +} + +// Callback to refresh the list +function updateServicesView(request, services_list){ + var service_list_array = []; + + $.each(services_list,function(){ + service_list_array.push(serviceElementArray(this)); + }); + + updateView(service_list_array,dataTable_services); +} + +// Callback to refresh the list of Virtual Machines +function updateServiceVMInfo(vmachine_list){ + var vmachine_list_array = []; + + $.each(vmachine_list,function(){ + vmachine_list_array.push( vMachineElementArray(this)); + }); + + updateView(vmachine_list_array, serviceroleVMsDataTable); +}; + +// Callback to update the information panel tabs and pop it up +function updateServiceInfo(request,elem){ + var elem_info = elem.DOCUMENT; + + var info_tab = { + title: tr("Information"), + content: + '
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("Service")+' \"'+elem_info.NAME+'\"'+'
'+tr("ID")+''+elem_info.ID+'
'+tr("Name")+''+elem_info.NAME+'
'+tr("Strategy")+''+elem_info.TEMPLATE.BODY.deployment+'
'+tr("Shutdown action")+''+(elem_info.TEMPLATE.BODY.shutdown_action || "")+'
'+tr("State")+''+ Service.state(elem_info.TEMPLATE.BODY.state) +'
' + + '
\ +
' + insert_permissions_table('oneflow-services', + "Service", + elem_info.ID, + elem_info.UNAME, + elem_info.GNAME, + elem_info.UID, + elem_info.GID) + + '
\ +
' + } + + Sunstone.updateInfoPanelTab("service_info_panel", "service_info_tab",info_tab); + + var roles_tab = { + title : "Roles", + content : '
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Period") +'
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("VMs per period") +'
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("Name")+''+tr("State")+''+tr("Cardinality")+''+tr("VM Template")+''+tr("Parents")+'
\ +
\ +
\ + '+tr("Select a role in the table for more information")+'\ +
\ +
' + }; + + Sunstone.updateInfoPanelTab("service_info_panel", "service_roles_tab",roles_tab); + + var logs = elem_info.TEMPLATE.BODY.log + var log_info = '' + if (logs) { + log_info += '
' + + for (var i = 0; i < logs.length; i++) { + var line = pretty_time(logs[i].timestamp)+' ['+logs[i].severity + '] ' + logs[i].message+ '
'; + if (logs[i].severity == 'E'){ + line = ''+line+''; + } + + log_info += line; + } + + log_info += '
' + } + + var logs_tab = { + title: "Logs", + content: log_info + } + + + Sunstone.updateInfoPanelTab("service_info_panel", "service_log_tab",logs_tab); + + + // Popup panel + Sunstone.popUpInfoPanel("service_info_panel", "oneflow-services"); + setPermissionsTable(elem_info,''); + + $("#service_info_panel_refresh", $("#service_info_panel")).click(function(){ + $(this).html(spinner); + selected_row_role_id = $($('td.markrowselected',servicerolesDataTable.fnGetNodes())[1]).html(); + checked_row_rolevm_ids = new Array(); + + if (typeof(serviceroleVMsDataTable) !== 'undefined') { + $.each($(serviceroleVMsDataTable.fnGetNodes()), function(){ + if($('td.markrowchecked',this).length!=0) + { + checked_row_rolevm_ids.push($($('td',$(this))[1]).html()); + } + }); + } + Sunstone.runAction('Service.showinfo', elem_info.ID); + }) + + var roles = elem_info.TEMPLATE.BODY.roles + if (roles && roles.length) { + servicerolesDataTable = $('#datatable_service_roles').dataTable({ + "bSortClasses": false, + "bAutoWidth":false, + "aoColumnDefs": [ + { "bSortable": false, "aTargets": ["check"] } + ], + "sDom" : '<"H">t<"F"p>' + }); + + var context = $("#roles_extended_info", $("#service_info_panel")); + var role_elements = []; + $.each(roles, function(){ + role_elements.push([ + '', + this.name, + Role.state(this.state), + this.cardinality, + this.vm_template, + this.parents ? this.parents.join(', ') : '-' + ]) + }); + + updateView(role_elements ,servicerolesDataTable); + + insertButtonsInTab("oneflow-services", "service_roles_tab", role_buttons, $('#role_actions', $("#dialog"))) + $('#role_actions', $("#dialog")).foundationButtons(); + $('#role_actions', $("#dialog")).foundationButtons(); + + setupScaleDialog(); + + $('tbody input.check_item',servicerolesDataTable).die(); + $('tbody tr',servicerolesDataTable).die(); + + initCheckAllBoxes(servicerolesDataTable, $('#role_actions', $("#dialog"))); + tableCheckboxesListener(servicerolesDataTable, $('#role_actions', $("#dialog"))); + + $('tbody tr',servicerolesDataTable).die() + $('tbody tr',servicerolesDataTable).live("click",function(e){ + if ($(e.target).is('input') || + $(e.target).is('select') || + $(e.target).is('option')) return true; + + if (e.ctrlKey || e.metaKey || $(e.target).is('input')) + { + $('.check_item',this).trigger('click'); + } + else + { + var aData = servicerolesDataTable.fnGetData(this); + var role_name = $(aData[0]).val(); + + var role_index = servicerolesDataTable.fnGetPosition(this); + + generate_role_div(role_index); + + $('tbody input.check_item',$(this).parents('table')).removeAttr('checked'); + $('.check_item',this).click(); + $('td',$(this).parents('table')).removeClass('markrowchecked'); + + if(last_selected_row_role) { + last_selected_row_role.children().each(function(){ + $(this).removeClass('markrowselected'); + }); + } + + last_selected_row_role = $(this); + $(this).children().each(function(){ + $(this).addClass('markrowselected'); + }); + } + + //if($(this).is(":checked")) + //{ + // $(this).parents('tr').children().each(function(){$(this).addClass('markrowchecked');}); + //} + //else + //{ + // $(this).parents('tr').children().removeClass('markrowchecked'); + // $(this).parents('tr').children().removeClass('markrowselected'); + //} +// + //recountCheckboxes(datatable); + }); + + var generate_role_div = function(role_index) { + var role = roles[role_index] + var info_str = "
\ +
\ + "+tr("Role")+" - "+role.name+"\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
"+tr("Information")+"
"+tr("Shutdown action")+""+(role.shutdown_action || "-")+""+tr("Cooldown")+""+(role.cooldown || "-")+""+tr("Min VMs")+""+(role.min_vms || "-")+""+tr("Max VMs")+""+(role.max_vms || "-")+"
\ +
"; + + info_str += '
\ + '+tr("Virtual Machines")+'\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("ID")+''+tr("Owner")+''+tr("Group")+''+tr("Name")+''+tr("Status")+''+tr("Used CPU")+''+tr("Used Memory")+''+tr("Host")+''+tr("IPs")+''+tr("Start Time")+''+tr("VNC")+'
\ +
\ +
'; + + info_str += "

"; + + if (role.elasticity_policies && role.elasticity_policies.length > 0) { + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + $.each(role.elasticity_policies, function(){ + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + ' + }); + + info_str += '\ +
'+tr("Elasticity policies")+'
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Expression")+''+tr("# Periods")+''+tr("Period")+''+tr("Cooldown")+'
'+this.type+''+this.adjust+''+(this.min_adjust_step || "-")+''+(this.expression_evaluated || this.expression)+''+(this.true_evals || 0 )+'/'+ this.period+''+(this.period_number || "-")+''+(this.cooldown || "-")+'
'; + } + + if (role.scheduled_policies && role.scheduled_policies.length > 0) { + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + $.each(role.scheduled_policies, function(){ + info_str += '\ + \ + \ + '; + + if (this['start_time']) { + info_str += ''; + info_str += ''; + } else if (this['recurrence']) { + info_str += ''; + info_str += ''; + } + }); + + info_str += '\ +
'+tr("Scheduled policies")+'
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Time format")+''+tr("Time expression")+'
'+this.type+''+this.adjust+''+(this.min_adjust_step || "")+'start_time'+this.start_time+'recurrence'+this.recurrence+'
'; + } + + info_str += '
\ +
\ +
' + + context.html(info_str); + setupTips(context, "tip-top"); + + var vms = []; + serviceroleVMsDataTable = $('#datatable_service_vms_'+role.name, context).dataTable({ + "aoColumnDefs": [ + { "bSortable": false, "aTargets": [0,1,7,8,10,12] }, + { "sWidth": "35px", "aTargets": [0,1] }, + { "bVisible": false, "aTargets": [7,8,11]} + ] + }); + + if (role.nodes) { + $.each(role.nodes, function(){ + var vm_info = this.vm_info; + + var info = []; + if (this.scale_up) { + info.push(""); + } else if (this.disposed) { + info.push(""); + } else { + info.push(""); + } + + if (vm_info) { + vms.push(info.concat(vMachineElementArray(vm_info))); + } else { + empty_arr = [ + '', + this.deploy_id, + '', '', '', '', '', '', '', '', '', '' ]; + + vms.push(info.concat(empty_arr)); + } + }); + + updateView(vms, serviceroleVMsDataTable); + } + + + insertButtonsInTab("oneflow-services", "service_roles_tab", role_vm_buttons, $('div#role_vms_actions', $("#dialog"))) + $('div#role_vms_actions', $("#dialog")).foundationButtons(); + $('div#role_vms_actions', $("#dialog")).foundationButtons(); + + $('tbody input.check_item',serviceroleVMsDataTable).die(); + $('tbody tr',serviceroleVMsDataTable).die(); + + initCheckAllBoxes(serviceroleVMsDataTable, $('div#role_vms_actions', $("#dialog"))); + tableCheckboxesListener(serviceroleVMsDataTable, $('div#role_vms_actions', $("#dialog"))); + + $('tbody tr',serviceroleVMsDataTable).live("click",function(e){ + if ($(e.target).is('input') || + $(e.target).is('select') || + $(e.target).is('option')) return true; + + if (e.ctrlKey || e.metaKey || $(e.target).is('input')) + { + $('.check_item',this).trigger('click'); + } + else + { + $('tbody input.check_item',$(this).parents('table')).removeAttr('checked'); + $('.check_item',this).click(); + $('td',$(this).parents('table')).removeClass('markrowchecked'); + + if(last_selected_row_rolevm) { + last_selected_row_rolevm.children().each(function(){ + $(this).removeClass('markrowchecked'); + }); + } + + last_selected_row_rolevm = $(this); + $(this).children().each(function(){ + $(this).addClass('markrowchecked'); + }); + } + }); + + + //insertButtonsInTab("oneflow-services", "service_roles_tab", role_buttons) + //$('li#service_roles_tabTab', $("#dialog")).foundationButtons(); + //$('li#service_roles_tabTab', $("#dialog")).foundationButtons(); + } + + if(selected_row_role_id) { + $.each($(servicerolesDataTable.fnGetNodes()),function(){ + if($($('td',this)[1]).html()==selected_row_role_id) { + $('td',this)[2].click(); + } + }); + } + + if(checked_row_rolevm_ids.length!=0) { + $.each($(serviceroleVMsDataTable.fnGetNodes()),function(){ + var current_id = $($('td',this)[1]).html(); + if (current_id) { + if(jQuery.inArray(current_id, checked_row_rolevm_ids)!=-1) { + $('input.check_item',this).first().click(); + $('td',this).addClass('markrowchecked'); + } + } + }); + } + //setupActionButtons($('li#service_roles_tabTab', $("#dialog"))); + } + + + //$('tbody tr',serviceroleVMsDataTable).click(function(e){ + // var aData = serviceroleVMsDataTable.fnGetData(this); + // var id = aData[1]; + // if (!id) return true; + // if ($(e.target).is('img')) return true; +// + // //open the Vresources submenu in case it was closed + // var vres_menu = $('div#menu li#li_vres_tab') + // $('li.vres_tab', vres_menu.parent()).fadeIn('fast'); + // $('span', vres_menu).removeClass('ui-icon-circle-plus'); + // $('span', vres_menu).addClass('ui-icon-circle-minus'); +// + // showTab('vms_tab'); +// + // popDialogLoading(); + // Sunstone.runAction("VM.showinfo", id) + // return false; + //}); + + + setupTips($("#roles_form")); +} + +function setupScaleDialog(){ + dialogs_context.append('
'); + $scale_dialog = $('#scale_dialog', dialogs_context); + var dialog = $scale_dialog; + + dialog.html('
\ +

\ + '+tr("Scale")+'\ +

\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Force the new cardinality even if it is outside the limits") +'
\ +
\ +
\ +
\ +
\ + \ + \ +
\ + ×\ +
') + + dialog.addClass("reveal-modal"); + setupTips(dialog); + + $('#scale_form',dialog).submit(function(){ + var force = false; + if ($("#force", this).is(":checked")) { + force = true; + } + + var obj = { + "force": force, + "cardinality": $("#cardinality", this).val(), + } + + Sunstone.runAction('Role.update', roleElements(), obj); + + $scale_dialog.trigger("reveal:close") + return false; + }); +}; + + +function popUpScaleDialog(){ + $scale_dialog.reveal(); + return false; +} + +// Set the autorefresh interval for the datatable +function setServiceAutorefresh() { + setInterval(function(){ + var checked = $('input.check_item:checked',dataTable_services); + var filter = $("#services_search").attr('value'); + if ((checked.length==0) && !filter){ + Sunstone.runAction("Service.autorefresh"); + } + },INTERVAL+someTime()); +}; + +//The DOM is ready at this point +$(document).ready(function(){ + var tab_name = "oneflow-services"; + + dataTable_services = $("#datatable_services",main_tabs_context).dataTable({ + "aoColumnDefs": [ + { "bSortable": false, "aTargets": ["check"] }, + { "sWidth": "35px", "aTargets": [0] }, + { "bVisible": true, "aTargets": Config.tabTableColumns(tab_name)}, + { "bVisible": false, "aTargets": ['_all']} + ] + }); + + $('#services_search').keyup(function(){ + dataTable_services.fnFilter( $(this).val() ); + }) + + dataTable_services.on('draw', function(){ + recountCheckboxes(dataTable_services); + }) + + Sunstone.runAction("Service.list"); + + setServiceAutorefresh(); + + initCheckAllBoxes(dataTable_services); + tableCheckboxesListener(dataTable_services); + infoListener(dataTable_services,'Service.showinfo'); +}); diff --git a/src/sunstone/public/js/plugins/oneflow-templates.js b/src/sunstone/public/js/plugins/oneflow-templates.js new file mode 100644 index 0000000000..3c3ea7d6cc --- /dev/null +++ b/src/sunstone/public/js/plugins/oneflow-templates.js @@ -0,0 +1,1474 @@ +// ------------------------------------------------------------------------ // +// Copyright 2010-2013, C12G Labs S.L. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); you may // +// not use this file except in compliance with the License. You may obtain // +// a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +//------------------------------------------------------------------------- // +var selected_row_template_role_id; +var last_selected_row_template_role; + +var ServiceTemplate = { + "resource" : 'DOCUMENT', + "path" : 'service_template', + "create": function(params){ + OpenNebula.Action.create(params, ServiceTemplate.resource, ServiceTemplate.path); + }, + + "instantiate": function(params){ + var action_obj = params.data.extra_param; + OpenNebula.Action.simple_action(params, + ServiceTemplate.resource, + "instantiate", + action_obj, + ServiceTemplate.path); + }, + "update": function(params){ + var action_obj = {"template_json" : params.data.extra_param }; + OpenNebula.Action.simple_action(params, + ServiceTemplate.resource, + "update", + action_obj, + ServiceTemplate.path); + }, + "del": function(params){ + OpenNebula.Action.del(params,ServiceTemplate.resource, ServiceTemplate.path); + }, + "list" : function(params){ + OpenNebula.Action.list(params, ServiceTemplate.resource, ServiceTemplate.path) + }, + "show" : function(params){ + OpenNebula.Action.show(params, ServiceTemplate.resource, false, ServiceTemplate.path) + }, + "chown" : function(params){ + OpenNebula.Action.chown(params,ServiceTemplate.resource, ServiceTemplate.path); + }, + "chgrp" : function(params){ + OpenNebula.Action.chgrp(params,ServiceTemplate.resource, ServiceTemplate.path); + }, + "chmod" : function(params){ + var action_obj = params.data.extra_param; + OpenNebula.Action.simple_action(params, + ServiceTemplate.resource, + "chmod", + action_obj, + ServiceTemplate.path); + } +} + +var service_template_tab_content = '\ +
\ +
\ +
\ +
\ +

\ + \ + '+tr("OneFlow - Templates")+'\ + \ + \ +  \ + \ + \ +

\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("ID")+''+tr("Owner")+''+tr("Group")+''+tr("Name")+'
\ +
\ +
\ +
'; + +var create_service_template_tmpl = '\ +
\ +

\ + '+tr("Create Service Template")+'\ + \ +

\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Name for this template") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Straight strategy will instantiate each role in order: parents role will be deployed before their children. None strategy will instantiate the roles regardless of their relationships.") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Shutdown action") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ +
'+tr("Add another role")+'
\ +
\ +
    \ +
\ +
\ + \ + ×\ +
\ +
'; + +var role_tab_content = '\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Name of the role") +'
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Number of VMs to instantiate with this role") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Template associated to this role") +'
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Shutdown action") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ +
'+tr("Parent roles")+'
\ +
\ +
\ +
\ +
\ +
\ +
\ +
\ + '+tr("Elasticity")+' - \ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Minimum number of VMs for elasticity adjustments") +'
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Maximum number of VMs for elasticity adjustments") +'
\ +
\ +
\ +
\ +
\ + \ +
\ +
\ + \ +
\ +
\ +
'+ tr("Cooldown time after an elasticity operation (secs)") +'
\ +
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
\ + '+tr("Elasticty policies")+'\ +
'+tr("Add")+'
\ +
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Expression")+''+tr("# Periods")+''+tr("Period")+''+tr("Cooldown")+'
\ +
\ +
\ +
\ +
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
\ + '+tr("Scheduled policies")+'\ +
'+tr("Add")+'
\ +
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Time format")+''+tr("Time expression")+'
\ +
\ +
\ +
\ +
\ +
'; + +var dataTable_service_templates; +var $create_service_template_dialog; + +var service_template_actions = { + + "ServiceTemplate.create" : { + type: "create", + call: ServiceTemplate.create, + callback: function(req, res){ + //empty creation dialog roles after successful creation + var dialog = $create_service_template_dialog; + $('table#current_roles tbody', dialog).empty(); + $('select[name="parents"]', dialog).empty(); + addServiceTemplateElement(req, res); + }, + error: onError, + notify:true + }, + + "ServiceTemplate.create_dialog" : { + type : "custom", + call: popUpCreateServiceTemplateDialog + }, + + "ServiceTemplate.update_dialog" : { + type : "single", + call: popUpUpdateServiceTemplateDialog, + error: onError + }, + + "ServiceTemplate.show_to_update" : { + type : "single", + call: ServiceTemplate.show, + callback: fillUpUpdateServiceTemplateDialog, + error: onError + }, + + "ServiceTemplate.update" : { // Update template + type: "single", + call: ServiceTemplate.update, + callback: function(request,response){ + notifyMessage(tr("ServiceTemplate updated correctly")); + Sunstone.runAction('ServiceTemplate.show',response.DOCUMENT.ID); + }, + error: onError + }, + + "ServiceTemplate.list" : { + type: "list", + call: ServiceTemplate.list, + callback: updateServiceTemplatesView, + error: onError + }, + + "ServiceTemplate.show" : { + type : "single", + call: ServiceTemplate.show, + callback: updateServiceTemplateElement, + error: onError + }, + + "ServiceTemplate.showinfo" : { + type: "single", + call: ServiceTemplate.show, + callback: updateServiceTemplateInfo, + error: onError + }, + + "ServiceTemplate.instantiate" : { + type: "multiple", + call: ServiceTemplate.instantiate, + elements: serviceTemplateElements, + error: onError, + notify: true + }, + + "ServiceTemplate.refresh" : { + type: "custom", + call: function () { + waitingNodes(dataTable_service_templates); + Sunstone.runAction("ServiceTemplate.list"); + } + }, + + "ServiceTemplate.autorefresh" : { + type: "custom", + call: function() { + ServiceTemplate.list({timeout: true, success: updateServiceTemplatesView, error: onError}); + } + }, + + "ServiceTemplate.delete" : { + type: "multiple", + call: ServiceTemplate.del, + callback: deleteServiceTemplateElement, + elements: serviceTemplateElements, + error: onError, + notify: true + }, + + "ServiceTemplate.chown" : { + type: "multiple", + call: ServiceTemplate.chown, + callback: function (req) { + Sunstone.runAction("ServiceTemplate.show",req.request.data[0][0]); + }, + elements: serviceTemplateElements, + error: onError, + notify: true + }, + + "ServiceTemplate.chgrp" : { + type: "multiple", + call: ServiceTemplate.chgrp, + callback: function (req) { + Sunstone.runAction("ServiceTemplate.show",req.request.data[0][0]); + }, + elements: serviceTemplateElements, + error: onError, + notify: true + }, + + "ServiceTemplate.chmod" : { + type: "single", + call: ServiceTemplate.chmod, + error: onError, + notify: true + }, + + "ServiceTemplate.help" : { + type: "custom", + call: function() { + hideDialog(); + $('div#service_templates_tab div.legend_div').slideToggle(); + } + } +}; + + +var service_template_buttons = { + "ServiceTemplate.refresh" : { + type: "action", + layout: "refresh", + alwaysActive: true + }, + "ServiceTemplate.create_dialog" : { + type: "create_dialog", + layout: "create" + }, + "ServiceTemplate.instantiate" : { + type: "action", + layout: "main", + text: tr("Instantiate") + }, + "ServiceTemplate.update_dialog" : { + type: "action", + layout: "main", + text: tr("Update") + }, + "ServiceTemplate.chown" : { + type: "confirm_with_select", + text: tr("Change owner"), + select: users_sel, + layout: "user_select", + tip: tr("Select the new owner")+":", + condition: mustBeAdmin + }, + "ServiceTemplate.chgrp" : { + type: "confirm_with_select", + text: tr("Change group"), + select: groups_sel, + layout: "user_select", + tip: tr("Select the new group")+":", + condition: mustBeAdmin + }, + + "ServiceTemplate.delete" : { + type: "confirm", + text: tr("Delete"), + layout: "del", + tip: tr("This will delete the selected templates") + } +} + +var service_template_info_panel = { + "service_template_info_tab" : { + title: tr("Service information"), + content: "" + } +} + +var service_templates_tab = { + title: "Templates", + content: service_template_tab_content, + buttons: service_template_buttons, + tabClass: 'subTab', + parentTab: 'oneflow_dashboard_tab' +} + +Sunstone.addActions(service_template_actions); +Sunstone.addMainTab('oneflow-templates',service_templates_tab); +Sunstone.addInfoPanel('service_template_info_panel',service_template_info_panel); + + +function serviceTemplateElements() { + return getSelectedNodes(dataTable_service_templates); +} + +// Returns an array containing the values of the service_template_json and ready +// to be inserted in the dataTable +function serviceTemplateElementArray(service_template_json){ + var service_template = service_template_json.DOCUMENT; + + return [ + '', + service_template.ID, + service_template.UNAME, + service_template.GNAME, + service_template.NAME + ]; +} + +// Callback to update an element in the dataTable +function updateServiceTemplateElement(request, service_template_json){ + var id = service_template_json.DOCUMENT.ID; + var element = serviceTemplateElementArray(service_template_json); + updateSingleElement(element,dataTable_service_templates,'#service_template_'+id); +} + +// Callback to remove an element from the dataTable +function deleteServiceTemplateElement(req){ + deleteElement(dataTable_service_templates,'#service_template_'+req.request.data); +} + +// Callback to add an service_template element +function addServiceTemplateElement(request, service_template_json){ + var element = serviceTemplateElementArray(service_template_json); + addElement(element,dataTable_service_templates); +} + +// Callback to refresh the list +function updateServiceTemplatesView(request, service_templates_list){ + var service_template_list_array = []; + + $.each(service_templates_list,function(){ + service_template_list_array.push(serviceTemplateElementArray(this)); + }); + + updateView(service_template_list_array,dataTable_service_templates); +} + +// Callback to update the information panel tabs and pop it up +function updateServiceTemplateInfo(request,elem){ + var elem_info = elem.DOCUMENT; + + var info_tab = { + title: tr("Information"), + content: + '
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("Service Template")+' \"'+elem_info.NAME+'\"'+'
'+tr("ID")+''+elem_info.ID+'
'+tr("Name")+''+elem_info.NAME+'
'+tr("Strategy")+''+elem_info.TEMPLATE.BODY.deployment+'
'+tr("Shutdown action")+''+elem_info.TEMPLATE.BODY.shutdown_action+'
' + + '
\ +
' + insert_permissions_table('oneflow-templates', + "ServiceTemplate", + elem_info.ID, + elem_info.UNAME, + elem_info.GNAME, + elem_info.UID, + elem_info.GID) + + '
\ +
' + }; + + Sunstone.updateInfoPanelTab("service_template_info_panel","service_template_info_tab",info_tab); + + var roles_tab = { + title : "Roles", + content : '
\ +
\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ +
'+tr("Name")+''+tr("Cardinality")+''+tr("VM Template")+''+tr("Parents")+'
\ +
\ +
\ + '+tr("Select a role in the table for more information")+'\ +
\ +
' + }; + + Sunstone.updateInfoPanelTab("service_template_info_panel", "service_template_roles_tab",roles_tab); + + Sunstone.popUpInfoPanel("service_template_info_panel", "oneflow-templates"); + + setPermissionsTable(elem_info,''); + + $("#service_template_info_panel_refresh", $("#service_template_info_panel")).click(function(){ + $(this).html(spinner); + selected_row_template_role_id = $($('td.markrowselected',serviceTemplaterolesDataTable.fnGetNodes())[1]).html(); + Sunstone.runAction('ServiceTemplate.showinfo', elem_info.ID); + }) + + var roles = elem_info.TEMPLATE.BODY.roles + if (roles && roles.length) { + serviceTemplaterolesDataTable = $('#datatable_service_template_roles').dataTable({ + "bSortClasses": false, + "bAutoWidth":false, + "aoColumnDefs": [ + { "bSortable": false, "aTargets": ["check"] } + ], + "sDom" : '<"H">t<"F"p>' + }); + + var context = $("#roles_extended_info", $("#service_template_info_panel")); + var role_elements = []; + $.each(roles, function(){ + role_elements.push([ + this.name, + this.cardinality, + this.vm_template, + this.parents ? this.parents.join(', ') : '-' + ]) + + updateView(role_elements ,serviceTemplaterolesDataTable); + + $('tbody tr',serviceTemplaterolesDataTable).die(); + $('tbody tr',serviceTemplaterolesDataTable).live("click",function(e){ + var aData = serviceTemplaterolesDataTable.fnGetData(this); + var role_name = $(aData[0]).val(); + + var role_index = serviceTemplaterolesDataTable.fnGetPosition(this); + + generate_template_role_div(role_index); + + if(last_selected_row_template_role) { + last_selected_row_template_role.children().each(function(){ + $(this).removeClass('markrowselected'); + }); + } + + last_selected_row_template_role = $(this); + $(this).children().each(function(){ + $(this).addClass('markrowselected'); + }); + }); + }); + + var generate_template_role_div = function(role_index) { + var role = roles[role_index] + var info_str = '
\ +
\ + '+tr("Role")+' - '+role.name+''; + + info_str += "
\ + \ + \ + \ + \ + "; + + info_str += "\ + \ + \ + \ + \ + \ + \ + \ + \ + "; + + info_str += "\ +
"+tr("Information")+"
"+tr("Shutdown action")+""+(role.shutdown_action || "-")+""+tr("Cooldown")+""+(role.cooldown || "-")+""+tr("Min VMs")+""+(role.min_vms || "-")+""+tr("Max VMs")+""+(role.max_vms || "-")+"
"; + + + info_str += "
\ +
"; + + if (role.elasticity_policies && role.elasticity_policies.length > 0) { + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + $.each(role.elasticity_policies, function(){ + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + ' + }); + + info_str += '\ +
'+tr("Elasticity policies")+'
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Expression")+''+tr("# Periods")+''+tr("Period")+''+tr("Cooldown")+'
'+this.type+''+this.adjust+''+(this.min_adjust_step || "-")+''+this.expression+''+(this.period || "-")+''+(this.period_number || "-")+''+(this.cooldown || "-")+'
'; + } + + if (role.scheduled_policies && role.scheduled_policies.length > 0) { + info_str += '\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '; + + $.each(role.scheduled_policies, function(){ + info_str += '\ + \ + \ + '; + + if (this['start_time']) { + info_str += ''; + info_str += ''; + } else if (this['recurrence']) { + info_str += ''; + info_str += ''; + } + }); + + info_str += '\ +
'+tr("Scheduled policies")+'
'+tr("Type")+'\ + '+tr("Type of adjustment.")+'

\ + '+tr("CHANGE: Add/substract the given number of VMs.")+'
\ + '+tr("CARDINALITY: Set the cardinality to the given number.")+'
\ + '+tr("PERCENTAGE_CHANGE: Add/substract the given percentage to the current cardinality.")+'\ +
\ +
'+tr("Adjust")+'\ + '+tr("Positive or negative adjustment. Its meaning depends on 'type'")+'

\ + '+tr("CHANGE: -2, will substract 2 VMs from the role")+'
\ + '+tr("CARDINALITY: 8, will set carditanilty to 8")+'
\ + '+tr("PERCENTAGE_CHANGE: 20, will increment cardinality by 20%")+'\ +
\ +
'+tr("Min")+'\ + '+tr("Optional parameter for PERCENTAGE_CHANGE adjustment type. If present, the policy will change the cardinality by at least the number of VMs set in this attribute.")+'\ + \ + '+tr("Time format")+''+tr("Time expression")+'
'+this.type+''+this.adjust+''+(this.min_adjust_step || "-")+'start_time'+this.start_time+'recurrence'+this.recurrence+'
'; + } + + info_str += '
\ +
\ +
' + + context.html(info_str); + setupTips(context, "tip-top"); + } + + if(selected_row_template_role_id) { + $.each($(serviceTemplaterolesDataTable.fnGetNodes()),function(){ + if($($('td',this)[1]).html()==selected_row_template_role_id) { + $('td',this)[2].click(); + } + }); + } + } + + setupTips($("#roles_form")); +} + +function setup_policy_tab_content(policy_section, html_policy_id) { + setupTips(policy_section); + + return false; +} + +function setup_role_tab_content(role_section, html_role_id) { + setupTips(role_section); + + var tpl_select = makeSelectOptions(dataTable_templates, 1, 4, [], [], true); + $('select[name="vm_template"]', role_section).html(tpl_select); + + $("#role_name", role_section).change(function(){ + $("#" + html_role_id +" #role_name_text").html($(this).val()); + $("#role_name_text", role_section).html($(this).val()); + }); + + $("select#type").live("change", function(){ + var new_tr = $(this).closest('tr'); + if ($(this).val() == "PERCENTAGE_CHANGE") { + $("#min_adjust_step_td", new_tr).html(''); + } else { + $("#min_adjust_step_td", new_tr).empty(); + } + }); + + var add_elas_policy_tab = function() { + var new_tr = $('\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + '); + new_tr.appendTo($("#elasticity_policies_tbody", role_section)); + //setup_policy_tab_content(policy_section, html_policy_id); + } + + var add_sche_policy_tab = function() { + var new_tr = $('\ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + \ + ') + new_tr.appendTo($("#scheduled_policies_tbody", role_section)) + //setup_policy_tab_content(policy_section, html_policy_id); + } + + // close icon: removing the tab on click + $( "#scheduled_policies_table i.remove-tab").live( "click", function() { + var tr = $(this).closest('tr'); + tr.remove(); + }); + + $( "#elasticity_policies_table i.remove-tab").live( "click", function() { + var tr = $(this).closest('tr'); + tr.remove(); + }); + + $("#tf_btn_elas_policies", role_section).bind("click", function(){ + add_elas_policy_tab(); + }); + + $("#tf_btn_sche_policies", role_section).bind("click", function(){ + add_sche_policy_tab(); + }); + + policies_index = 0; + + return false; +} + + +// Prepare the creation dialog +function setupCreateServiceTemplateDialog(){ + dialogs_context.append('
'); + $create_service_template_dialog = $('#create_service_template_dialog',dialogs_context); + + var dialog = $create_service_template_dialog; + dialog.html(create_service_template_tmpl); + dialog.addClass("reveal-modal xlarge max-height"); + + setupTips(dialog); + + var add_role_tab = function(role_id) { + var html_role_id = 'role' + role_id; + + // Append the new div containing the tab and add the tab to the list + var role_section = $('
  • '+ + role_tab_content + + '
  • ').appendTo($("ul#roles_tabs_content")); + + var a = $("
    \ + "+tr("Role ")+role_id+" \ + \ + \ +
    ").appendTo($("dl#roles_tabs")); + + $("#"+html_role_id, a).click(); + $(document).foundationTabs("set_tab", a); + + setup_role_tab_content(role_section, html_role_id); + + roles_index++; + } + + // close icon: removing the tab on click + $( "#roles_tabs i.remove-tab" ).live( "click", function() { + var target = $(this).parent().attr("href"); + var dd = $(this).closest('dd'); + var dl = $(this).closest('dl'); + var content = $(target + 'Tab'); + + dd.remove(); + content.remove(); + + if (dd.attr("class") == 'active') { + $('a', dl.children('dd').last()).click(); + dl.foundationTabs("set_tab", dl.children('dd').last()); + } + + roles_index--; + }); + + // Fill parents table + // Each time a tab is clicked the table is filled with existing tabs (roles) + // Selected roles are kept + // TODO If the name of a role is changed and is selected, selection will be lost + $("#roles_tabs a").live('click', function(){ + var tab_id = "#"+this.id+"Tab"; + var str = ""; + + + $(tab_id+" #parent_roles").hide(); + var parent_role_available = false; + + $("#roles_tabs_content #role_name").each(function(){ + if ($(this).val() && ($(this).val() != $(tab_id+" #role_name").val())) { + parent_role_available = true; + str += "\ + \ + "+$(this).val()+"\ + "; + } + }); + + if (parent_role_available) { + $(tab_id+" #parent_roles").show(); + } + + var selected_parents = []; + $(tab_id+" #parent_roles_body input:checked").each(function(){ + selected_parents.push($(this).val()); + }); + + $(tab_id+" #parent_roles_body").html(str); + + $.each(selected_parents, function(){ + $(tab_id+" #parent_roles_body #"+this).attr('checked', true); + }); + }) + + $("#tf_btn_roles", dialog).bind("click", function(){ + add_role_tab(roles_index); + }); + + + $('#create_service_template_submit',dialog).click(function(){ + var json_template = generate_json_service_template_from_form(); + Sunstone.runAction("ServiceTemplate.create", json_template ); + dialog.trigger("reveal:close"); + return false; + }); + + $('#update_service_template_submit',dialog).click(function(){ + var json_template = generate_json_service_template_from_form(); + Sunstone.runAction("ServiceTemplate.update",service_template_to_update_id, JSON.stringify(json_template)); + dialog.trigger("reveal:close"); + return false; + }); + + $('#create_service_template_reset', dialog).click(function(){ + $create_service_template_dialog.trigger('reveal:close'); + $create_service_template_dialog.remove(); + setupCreateServiceTemplateDialog(); + + popUpCreateServiceTemplateDialog(); + }); + + roles_index = 0; + add_role_tab(roles_index); +} + +var removeEmptyObjects = function(obj){ + for (elem in obj){ + var remove = false; + var value = obj[elem]; + if (value instanceof Array) + { + if (value.length == 0) + remove = true; + else if (value.length > 0) + { + value = jQuery.grep(value, function (n) { + var obj_length = 0; + for (e in n) + obj_length += 1; + + if (obj_length == 0) + return false; + + return true; + }); + + if (value.length == 0) + remove = true; + } + } + else if (value instanceof Object) + { + var obj_length = 0; + for (e in value) + obj_length += 1; + if (obj_length == 0) + remove = true; + } + else + { + value = String(value); + if (value.length == 0) + remove = true; + } + + if (remove) + delete obj[elem]; + } + return obj; +} + +function generate_json_service_template_from_form() { + var name = $('input[name="service_name"]', $create_service_template_dialog).val(); + var deployment = $('select[name="deployment"]', $create_service_template_dialog).val(); + var shutdown_action_service = $('select[name="shutdown_action_service"]', $create_service_template_dialog).val(); + + var roles = []; + + $('#roles_tabs_content li', $create_service_template_dialog).each(function(){ + var role = {}; + role['name'] = $('input[name="name"]', this).val(); + role['cardinality'] = $('input[name="cardinality"]', this).val(); + role['vm_template'] = $('select[name="vm_template"]', this).val(); + role['shutdown_action'] = $('select[name="shutdown_action_role"]', this).val(); + role['parents'] = []; + + if (!name || !cardinality || !template){ + notifyError(tr("Please specify name, cardinality and template for this role")); + return false; + } else { + $('#parent_roles_body input.check_item:checked', this).each(function(){ + role['parents'].push($(this).val()) + }); + + var shutdown_action = $('select[name="shutdown_action_role"]', this).val(); + if (shutdown_action) { + role['shutdown_action'] = shutdown_action + } + var min_vms = $('input[name="min_vms"]', this).val(); + if (min_vms) { + role['min_vms'] = min_vms + } + var max_vms = $('input[name="max_vms"]', this).val(); + if (max_vms) { + role['max_vms'] = max_vms + } + var cooldown = $('input[name="cooldown"]', this).val(); + if (cooldown) { + role['cooldown'] = cooldown + } + + role = removeEmptyObjects(role); + role['elasticity_policies'] = []; + $("#elasticity_policies_tbody tr", this).each(function(){ + var policy = {}; + policy['type'] = $("#type" ,this).val(); + policy['adjust'] = $("#adjust" ,this).val(); + policy['min_adjust_step'] = $("#min_adjust_step" ,this).val(); + policy['expression'] = $("#expression" ,this).val(); + policy['period'] = $("#period" ,this).val(); + policy['period_number'] = $("#period_number" ,this).val(); + policy['cooldown'] = $("#cooldown" ,this).val(); + + // TODO remove empty policies + role['elasticity_policies'].push(removeEmptyObjects(policy)); + }); + + role['scheduled_policies'] = []; + $("#scheduled_policies_tbody tr", this).each(function(){ + var policy = {}; + policy['type'] = $("#type" ,this).val(); + policy['adjust'] = $("#adjust" ,this).val(); + policy['min_adjust_step'] = $("#min_adjust_step" ,this).val(); + + var time_format = $("#time_format" ,this).val(); + policy[time_format] = $("#time" ,this).val(); + + // TODO remove empty policies + role['scheduled_policies'].push(removeEmptyObjects(policy)); + }); + + roles.push(role); + } + }); + + var obj = { + name: name, + deployment: deployment, + roles: roles + } + + if (shutdown_action_service){ + obj['shutdown_action'] = shutdown_action_service + } + + return obj; +} + +function popUpCreateServiceTemplateDialog(){ + if (!$create_service_template_dialog) { + setupCreateServiceTemplateDialog(); + } + + var dialog = $create_service_template_dialog; + + $("#create_service_template_header", dialog).show(); + $("#update_service_template_header", dialog).hide(); + $("#create_service_template_submit", dialog).show(); + $("#update_service_template_submit", dialog).hide(); + + $("#service_name", dialog).removeAttr("disabled"); + + dialog.reveal(); +} + +function popUpUpdateServiceTemplateDialog() { + var selected_nodes = getSelectedNodes(dataTable_service_templates); + + if ( selected_nodes.length != 1 ) + { + notifyMessage("Please select one (and just one) template to update."); + return false; + } + + // Get proper cluster_id + var template_id = ""+selected_nodes[0]; + + Sunstone.runAction("ServiceTemplate.show_to_update", template_id); +} + + +function fillUpUpdateServiceTemplateDialog(request, response){ + if (!$create_service_template_dialog) { + setupCreateServiceTemplateDialog(); + } else { + $("#create_service_template_reset", $create_service_template_dialog).click(); + } + + var dialog = $create_service_template_dialog; + + $("#create_service_template_header", dialog).hide(); + $("#update_service_template_header", dialog).show(); + $("#create_service_template_submit", dialog).hide(); + $("#update_service_template_submit", dialog).show(); + + var service_template = response[ServiceTemplate.resource] + $("#service_name", dialog).attr("disabled", "disabled"); + $("#service_name", dialog).val(service_template.NAME); + + // TODO Check if the template still exists + $('select[name="deployment"]', dialog).val(service_template.TEMPLATE.BODY.deployment); + $("select[name='shutdown_action_service']", dialog).val(service_template.TEMPLATE.BODY.shutdown_action); + + var more_than_one = false; + var roles_names = []; + $.each(service_template.TEMPLATE.BODY.roles, function(index, value){ + more_than_one ? $("#tf_btn_roles", dialog).click() : (more_than_one = true); + + var context = $('#roles_tabs_content li', $create_service_template_dialog).last(); + + $("#role_name", context).val(value.name); + $("#role_name", context).change(); + roles_names.push(value.name); + + $("#cardinality", context).val(value.cardinality); + $('select[name="vm_template"]', context).val(value.vm_template); + + $("select[name='shutdown_action_role']", context).val(value.shutdown_action); + $("#min_vms", context).val(value.min_vms); + $("#max_vms", context).val(value.max_vms); + $("#cooldown", context).val(value.cooldown); + + if (value['elasticity_policies']) { + $.each(value['elasticity_policies'], function(){ + $("#tf_btn_elas_policies", context).click(); + var td = $("#elasticity_policies_tbody tr", context).last(); + $("#type" ,td).val(this['type']) + $("#type" ,td).change(); + $("#adjust" ,td).val(this['adjust'] ) + $("#min_adjust_step" ,td).val(this['min_adjust_step'] ) + $("#expression" ,td).val(this['expression'] ) + $("#period" ,td).val(this['period'] ) + $("#period_number" ,td).val(this['period_number'] ) + $("#cooldown" ,td).val(this['cooldown'] ) + }) + } + + if (value['scheduled_policies']) { + $.each(value['scheduled_policies'], function(){ + $("#tf_btn_sche_policies", context).click(); + var td = $("#scheduled_policies_tbody tr", context).last(); + $("#type", td).val(this['type']) + $("#type" ,td).change(); + $("#adjust", td).val(this['adjust'] ) + $("#min_adjust_step", td).val(this['min_adjust_step'] ) + + if (this['start_time']) { + $("#time_format", td).val('start_time'); + $("#time", td).val(this['start_time']); + } else if (this['recurrence']) { + $("#time_format", td).val('recurrence'); + $("#time", td).val(this['recurrence']); + } + }) + } + }) + + $.each(service_template.TEMPLATE.BODY.roles, function(index, value){ + var tab_id = "#role"+index+"Tab" + var str = ""; + + $.each(roles_names, function(){ + if (this != $(tab_id+" #role_name").val()) { + str += "\ + \ + "+this+"\ + "; + } + }); + + $(tab_id+" #parent_roles_body").html(str); + + var context = $('#roles_tabs_content li#role'+index+'Tab', $create_service_template_dialog); + + if (value.parents) { + $.each(value.parents, function(index, value){ + $("#parent_roles_body #"+this, context).attr('checked', true); + }); + } + }); + + service_template_to_update_id = service_template.ID; + + dialog.reveal(); +} + +// Set the autorefresh interval for the datatable +function setServiceTemplateAutorefresh() { + setInterval(function(){ + var checked = $('input.check_item:checked',dataTable_service_templates); + var filter = $("#service_template_search").attr('value'); + if ((checked.length==0) && !filter){ + Sunstone.runAction("ServiceTemplate.autorefresh"); + } + },INTERVAL+someTime()); +}; + + +//The DOM is ready at this point +$(document).ready(function(){ + var tab_name = "oneflow-templates"; + + dataTable_service_templates = $("#datatable_service_templates",main_tabs_context).dataTable({ + "aoColumnDefs": [ + { "bSortable": false, "aTargets": ["check"] }, + { "sWidth": "35px", "aTargets": [0] }, + { "bVisible": true, "aTargets": Config.tabTableColumns(tab_name)}, + { "bVisible": false, "aTargets": ['_all']} + ] + }); + + $('#service_templates_search').keyup(function(){ + dataTable_service_templates.fnFilter( $(this).val() ); + }) + + dataTable_service_templates.on('draw', function(){ + recountCheckboxes(dataTable_service_templates); + }) + + Sunstone.runAction("ServiceTemplate.list"); + + setServiceTemplateAutorefresh(); + + initCheckAllBoxes(dataTable_service_templates); + tableCheckboxesListener(dataTable_service_templates); + infoListener(dataTable_service_templates,'ServiceTemplate.showinfo'); + + $('div#service_templates_tab div.legend_div').hide(); +}); diff --git a/src/sunstone/routes/oneflow.rb b/src/sunstone/routes/oneflow.rb new file mode 100644 index 0000000000..4e4975e0b8 --- /dev/null +++ b/src/sunstone/routes/oneflow.rb @@ -0,0 +1,151 @@ +# -------------------------------------------------------------------------- # +# Copyright 2010-2013, C12G Labs S.L. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); you may # +# not use this file except in compliance with the License. You may obtain # +# a copy of the License at # +# # +# http://www.apache.org/licenses/LICENSE-2.0 # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # +# See the License for the specific language governing permissions and # +# limitations under the License. # +#--------------------------------------------------------------------------- # + +ONEFLOW_CONF_FILE = ETC_LOCATION + "/sunstone-oneflow.conf" + +$: << RUBY_LIB_LOCATION+"/oneflow" + +require 'opennebula/oneflow_client' + +begin + oneflow_conf = YAML.load_file(ONEFLOW_CONF_FILE) +rescue Exception => e + STDERR.puts "Error parsing config file #{ONEFLOW_CONF_FILE}: #{e.message}" + exit 1 +end + +set :oneflow_config, oneflow_conf + +helpers do + def af_build_client + flow_client = settings.cloud_auth.client(session[:user]) + split_array = flow_client.one_auth.split(':') + + Service::Client.new( + :url => settings.oneflow_config[:oneflow_server], + :user_agent => "Sunstone", + :username => split_array.shift, + :password => split_array.join(':')) + end + + def af_format_response(resp) + if CloudClient::is_error?(resp) + logger.error("[OneFlow] " + resp.to_s) + + error = Error.new(resp.to_s) + error resp.code.to_i, error.to_json + else + body resp.body.to_s + end + end +end + +############################################################################## +# Service +############################################################################## + +get '/service' do + client = af_build_client + + resp = client.get('/service') + + af_format_response(resp) +end + +get '/service/:id' do + client = af_build_client + + resp = client.get('/service/' + params[:id]) + + af_format_response(resp) +end + +delete '/service/:id' do + client = af_build_client + + resp = client.delete('/service/' + params[:id]) + + af_format_response(resp) +end + +post '/service/:id/action' do + client = af_build_client + + resp = client.post('/service/' + params[:id] + '/action', request.body.read) + + af_format_response(resp) +end + +post '/service/:id/role/:role_name/action' do + client = af_build_client + + resp = client.post('/service/' + params[:id] + '/role/' + params[:role_name] + '/action', request.body.read) + + af_format_response(resp) +end + + +put '/service/:id/role/:role_name' do + client = af_build_client + + resp = client.put('/service/' + params[:id] + '/role/' + params[:role_name], request.body.read) + + af_format_response(resp) +end + +############################################################################## +# Service Template +############################################################################## + +get '/service_template' do + client = af_build_client + + resp = client.get('/service_template') + + af_format_response(resp) +end + +get '/service_template/:id' do + client = af_build_client + + resp = client.get('/service_template/' + params[:id]) + + af_format_response(resp) +end + +delete '/service_template/:id' do + client = af_build_client + + resp = client.delete('/service_template/' + params[:id]) + + af_format_response(resp) +end + +post '/service_template/:id/action' do + client = af_build_client + + resp = client.post('/service_template/' + params[:id] + '/action', request.body.read) + + af_format_response(resp) +end + +post '/service_template' do + client = af_build_client + + resp = client.post('/service_template', request.body.read) + + af_format_response(resp) +end diff --git a/src/um/UserPool.cc b/src/um/UserPool.cc index e7429ed26b..7e05d4de5a 100644 --- a/src/um/UserPool.cc +++ b/src/um/UserPool.cc @@ -72,7 +72,7 @@ UserPool::UserPool(SqlDB * db, const char * one_auth; ifstream file; - string filenames[4]; + string filenames[5]; string error_str; Nebula& nd = Nebula::instance(); @@ -147,10 +147,11 @@ UserPool::UserPool(SqlDB * db, filenames[1] = nd.get_var_location() + "/.one/occi_auth"; filenames[2] = nd.get_var_location() + "/.one/ec2_auth"; filenames[3] = nd.get_var_location() + "/.one/onegate_auth"; + filenames[4] = nd.get_var_location() + "/.one/oneflow_auth"; mkdir(string(nd.get_var_location() + "/.one").c_str(), S_IRWXU); - for (i=0 ; i < 4; i++) + for (i=0 ; i < 5; i++) { int cfile = creat(filenames[i].c_str(), S_IRUSR | S_IWUSR); close(cfile);