#!/bin/bash -e # Common utility functions used across all OpenStack charms. error_out() { juju-log "$CHARM ERROR: $@" exit 1 } function service_ctl_status { # Return 0 if a service is running, 1 otherwise. local svc="$1" local status=$(service $svc status | cut -d/ -f1 | awk '{ print $2 }') case $status in "start") return 0 ;; "stop") return 1 ;; *) error_out "Unexpected status of service $svc: $status" ;; esac } function service_ctl { # control a specific service, or all (as defined by $SERVICES) # service restarts will only occur depending on global $CONFIG_CHANGED, # which should be updated in charm's set_or_update(). local config_changed=${CONFIG_CHANGED:-True} if [[ $1 == "all" ]] ; then ctl="$SERVICES" else ctl="$1" fi action="$2" if [[ -z "$ctl" ]] || [[ -z "$action" ]] ; then error_out "ERROR service_ctl: Not enough arguments" fi for i in $ctl ; do case $action in "start") service_ctl_status $i || service $i start ;; "stop") service_ctl_status $i && service $i stop || return 0 ;; "restart") if [[ "$config_changed" == "True" ]] ; then service_ctl_status $i && service $i restart || service $i start fi ;; esac if [[ $? != 0 ]] ; then juju-log "$CHARM: service_ctl ERROR - Service $i failed to $action" fi done # all configs should have been reloaded on restart of all services, reset # flag if its being used. if [[ "$action" == "restart" ]] && [[ -n "$CONFIG_CHANGED" ]] && [[ "$ctl" == "all" ]]; then CONFIG_CHANGED="False" fi } function configure_install_source { # Setup and configure installation source based on a config flag. local src="$1" # Default to installing from the main Ubuntu archive. [[ $src == "distro" ]] || [[ -z "$src" ]] && return 0 . /etc/lsb-release # standard 'ppa:someppa/name' format. if [[ "${src:0:4}" == "ppa:" ]] ; then juju-log "$CHARM: Configuring installation from custom src ($src)" add-apt-repository -y "$src" || error_out "Could not configure PPA access." return 0 fi # standard 'deb http://url/ubuntu main' entries. gpg key ids must # be appended to the end of url after a |, ie: # 'deb http://url/ubuntu main|$GPGKEYID' if [[ "${src:0:3}" == "deb" ]] ; then juju-log "$CHARM: Configuring installation from custom src URL ($src)" if echo "$src" | grep -q "|" ; then # gpg key id tagged to end of url folloed by a | url=$(echo $src | cut -d'|' -f1) key=$(echo $src | cut -d'|' -f2) juju-log "$CHARM: Importing repository key: $key" apt-key adv --keyserver keyserver.ubuntu.com --recv-keys "$key" || \ juju-log "$CHARM WARN: Could not import key from keyserver: $key" else juju-log "$CHARM No repository key specified." url="$src" fi echo "$url" > /etc/apt/sources.list.d/juju_deb.list return 0 fi # Cloud Archive if [[ "${src:0:6}" == "cloud:" ]] ; then # current os releases supported by the UCA. local cloud_archive_versions="folsom grizzly" local ca_rel=$(echo $src | cut -d: -f2) local u_rel=$(echo $ca_rel | cut -d- -f1) local os_rel=$(echo $ca_rel | cut -d- -f2 | cut -d/ -f1) [[ "$u_rel" != "$DISTRIB_CODENAME" ]] && error_out "Cannot install from Cloud Archive pocket $src " \ "on this Ubuntu version ($DISTRIB_CODENAME)!" valid_release="" for rel in $cloud_archive_versions ; do if [[ "$os_rel" == "$rel" ]] ; then valid_release=1 juju-log "Installing OpenStack ($os_rel) from the Ubuntu Cloud Archive." fi done if [[ -z "$valid_release" ]] ; then error_out "OpenStack release ($os_rel) not supported by "\ "the Ubuntu Cloud Archive." fi # CA staging repos are standard PPAs. if echo $ca_rel | grep -q "staging" ; then add-apt-repository -y ppa:ubuntu-cloud-archive/${os_rel}-staging return 0 fi # the others are LP-external deb repos. case "$ca_rel" in "$u_rel-$os_rel"|"$u_rel-$os_rel/updates") pocket="$u_rel-updates/$os_rel" ;; "$u_rel-$os_rel/proposed") pocket="$u_rel-proposed/$os_rel" ;; "$u_rel-$os_rel"|"$os_rel/updates") pocket="$u_rel-updates/$os_rel" ;; "$u_rel-$os_rel/proposed") pocket="$u_rel-proposed/$os_rel" ;; *) error_out "Invalid Cloud Archive repo specified: $src" esac apt-get -y install ubuntu-cloud-keyring entry="deb http://ubuntu-cloud.archive.canonical.com/ubuntu $pocket main" echo "$entry" \ >/etc/apt/sources.list.d/ubuntu-cloud-archive-$DISTRIB_CODENAME.list return 0 fi error_out "Invalid installation source specified in config: $src" } get_os_codename_install_source() { # derive the openstack release provided by a supported installation source. local rel="$1" local codename="unknown" . /etc/lsb-release # map ubuntu releases to the openstack version shipped with it. if [[ "$rel" == "distro" ]] ; then case "$DISTRIB_CODENAME" in "oneiric") codename="diablo" ;; "precise") codename="essex" ;; "quantal") codename="folsom" ;; "raring") codename="grizzly" ;; esac fi # derive version from cloud archive strings. if [[ "${rel:0:6}" == "cloud:" ]] ; then rel=$(echo $rel | cut -d: -f2) local u_rel=$(echo $rel | cut -d- -f1) local ca_rel=$(echo $rel | cut -d- -f2) if [[ "$u_rel" == "$DISTRIB_CODENAME" ]] ; then case "$ca_rel" in "folsom"|"folsom/updates"|"folsom/proposed"|"folsom/staging") codename="folsom" ;; "grizzly"|"grizzly/updates"|"grizzly/proposed"|"grizzly/staging") codename="grizzly" ;; esac fi fi # have a guess based on the deb string provided if [[ "${rel:0:3}" == "deb" ]] || \ [[ "${rel:0:3}" == "ppa" ]] ; then CODENAMES="diablo essex folsom grizzly havana" for cname in $CODENAMES; do if echo $rel | grep -q $cname; then codename=$cname fi done fi echo $codename } get_os_codename_package() { local pkg_vers=$(dpkg -l | grep "$1" | awk '{ print $3 }') || echo "none" pkg_vers=$(echo $pkg_vers | cut -d: -f2) # epochs case "${pkg_vers:0:6}" in "2011.2") echo "diablo" ;; "2012.1") echo "essex" ;; "2012.2") echo "folsom" ;; "2013.1") echo "grizzly" ;; "2013.2") echo "havana" ;; esac } get_os_version_codename() { case "$1" in "diablo") echo "2011.2" ;; "essex") echo "2012.1" ;; "folsom") echo "2012.2" ;; "grizzly") echo "2013.1" ;; "havana") echo "2013.2" ;; esac } get_ip() { dpkg -l | grep -q python-dnspython || { apt-get -y install python-dnspython 2>&1 > /dev/null } hostname=$1 python -c " import dns.resolver import socket try: # Test to see if already an IPv4 address socket.inet_aton('$hostname') print '$hostname' except socket.error: try: answers = dns.resolver.query('$hostname', 'A') if answers: print answers[0].address except dns.resolver.NXDOMAIN: pass " } # Common storage routines used by cinder, nova-volume and swift-storage. clean_storage() { # if configured to overwrite existing storage, we unmount the block-dev # if mounted and clear any previous pv signatures local block_dev="$1" juju-log "Cleaining storage '$block_dev'" if grep -q "^$block_dev" /proc/mounts ; then mp=$(grep "^$block_dev" /proc/mounts | awk '{ print $2 }') juju-log "Unmounting $block_dev from $mp" umount "$mp" || error_out "ERROR: Could not unmount storage from $mp" fi if pvdisplay "$block_dev" >/dev/null 2>&1 ; then juju-log "Removing existing LVM PV signatures from $block_dev" # deactivate any volgroups that may be built on this dev vg=$(pvdisplay $block_dev | grep "VG Name" | awk '{ print $3 }') if [[ -n "$vg" ]] ; then juju-log "Deactivating existing volume group: $vg" vgchange -an "$vg" || error_out "ERROR: Could not deactivate volgroup $vg. Is it in use?" fi echo "yes" | pvremove -ff "$block_dev" || error_out "Could not pvremove $block_dev" else juju-log "Zapping disk of all GPT and MBR structures" sgdisk --zap-all $block_dev || error_out "Unable to zap $block_dev" fi } function get_block_device() { # given a string, return full path to the block device for that # if input is not a block device, find a loopback device local input="$1" case "$input" in /dev/*) [[ ! -b "$input" ]] && error_out "$input does not exist." echo "$input"; return 0;; /*) :;; *) [[ ! -b "/dev/$input" ]] && error_out "/dev/$input does not exist." echo "/dev/$input"; return 0;; esac # this represents a file # support "/path/to/file|5G" local fpath size oifs="$IFS" if [ "${input#*|}" != "${input}" ]; then size=${input##*|} fpath=${input%|*} else fpath=${input} size=5G fi ## loop devices are not namespaced. This is bad for containers. ## it means that the output of 'losetup' may have the given $fpath ## in it, but that may not represent this containers $fpath, but ## another containers. To address that, we really need to ## allow some uniq container-id to be expanded within path. ## TODO: find a unique container-id that will be consistent for ## this container throughout its lifetime and expand it ## in the fpath. # fpath=${fpath//%{id}/$THAT_ID} local found="" # parse through 'losetup -a' output, looking for this file # output is expected to look like: # /dev/loop0: [0807]:961814 (/tmp/my.img) found=$(losetup -a | awk 'BEGIN { found=0; } $3 == f { sub(/:$/,"",$1); print $1; found=found+1; } END { if( found == 0 || found == 1 ) { exit(0); }; exit(1); }' \ f="($fpath)") if [ $? -ne 0 ]; then echo "multiple devices found for $fpath: $found" 1>&2 return 1; fi [ -n "$found" -a -b "$found" ] && { echo "$found"; return 1; } if [ -n "$found" ]; then echo "confused, $found is not a block device for $fpath"; return 1; fi # no existing device was found, create one mkdir -p "${fpath%/*}" truncate --size "$size" "$fpath" || { echo "failed to create $fpath of size $size"; return 1; } found=$(losetup --find --show "$fpath") || { echo "failed to setup loop device for $fpath" 1>&2; return 1; } echo "$found" return 0 } HAPROXY_CFG=/etc/haproxy/haproxy.cfg HAPROXY_DEFAULT=/etc/default/haproxy ########################################################################## # Description: Configures HAProxy services for Openstack API's # Parameters: # Space delimited list of service:port:mode combinations for which # haproxy service configuration should be generated for. The function # assumes the name of the peer relation is 'cluster' and that every # service unit in the peer relation is running the same services. # # Services that do not specify :mode in parameter will default to http. # # Example # configure_haproxy cinder_api:8776:8756:tcp nova_api:8774:8764:http ########################################################################## configure_haproxy() { local address=`unit-get private-address` local name=${JUJU_UNIT_NAME////-} cat > $HAPROXY_CFG << EOF global log 127.0.0.1 local0 log 127.0.0.1 local1 notice maxconn 20000 user haproxy group haproxy spread-checks 0 defaults log global mode http option httplog option dontlognull retries 3 timeout queue 1000 timeout connect 1000 timeout client 30000 timeout server 30000 listen stats :8888 mode http stats enable stats hide-version stats realm Haproxy\ Statistics stats uri / stats auth admin:password EOF for service in $@; do local service_name=$(echo $service | cut -d : -f 1) local haproxy_listen_port=$(echo $service | cut -d : -f 2) local api_listen_port=$(echo $service | cut -d : -f 3) local mode=$(echo $service | cut -d : -f 4) [[ -z "$mode" ]] && mode="http" juju-log "Adding haproxy configuration entry for $service "\ "($haproxy_listen_port -> $api_listen_port)" cat >> $HAPROXY_CFG << EOF listen $service_name 0.0.0.0:$haproxy_listen_port balance roundrobin mode $mode option ${mode}log server $name $address:$api_listen_port check EOF local r_id="" local unit="" for r_id in `relation-ids cluster`; do for unit in `relation-list -r $r_id`; do local unit_name=${unit////-} local unit_address=`relation-get -r $r_id private-address $unit` if [ -n "$unit_address" ]; then echo " server $unit_name $unit_address:$api_listen_port check" \ >> $HAPROXY_CFG fi done done done echo "ENABLED=1" > $HAPROXY_DEFAULT service haproxy restart } ########################################################################## # Description: Query HA interface to determine is cluster is configured # Returns: 0 if configured, 1 if not configured ########################################################################## is_clustered() { local r_id="" local unit="" for r_id in $(relation-ids ha); do if [ -n "$r_id" ]; then for unit in $(relation-list -r $r_id); do clustered=$(relation-get -r $r_id clustered $unit) if [ -n "$clustered" ]; then juju-log "Unit is haclustered" return 0 fi done fi done juju-log "Unit is not haclustered" return 1 } ########################################################################## # Description: Return a list of all peers in cluster relations ########################################################################## peer_units() { local peers="" local r_id="" for r_id in $(relation-ids cluster); do peers="$peers $(relation-list -r $r_id)" done echo $peers } ########################################################################## # Description: Determines whether the current unit is the oldest of all # its peers - supports partial leader election # Returns: 0 if oldest, 1 if not ########################################################################## oldest_peer() { peers=$1 local l_unit_no=$(echo $JUJU_UNIT_NAME | cut -d / -f 2) for peer in $peers; do echo "Comparing $JUJU_UNIT_NAME with peers: $peers" local r_unit_no=$(echo $peer | cut -d / -f 2) if (($r_unit_no<$l_unit_no)); then juju-log "Not oldest peer; deferring" return 1 fi done juju-log "Oldest peer; might take charge?" return 0 } ########################################################################## # Description: Determines whether the current service units is the # leader within a) a cluster of its peers or b) across a # set of unclustered peers. # Parameters: CRM resource to check ownership of if clustered # Returns: 0 if leader, 1 if not ########################################################################## eligible_leader() { if is_clustered; then if ! is_leader $1; then juju-log 'Deferring action to CRM leader' return 1 fi else peers=$(peer_units) if [ -n "$peers" ] && ! oldest_peer "$peers"; then juju-log 'Deferring action to oldest service unit.' return 1 fi fi return 0 } ########################################################################## # Description: Query Cluster peer interface to see if peered # Returns: 0 if peered, 1 if not peered ########################################################################## is_peered() { local r_id=$(relation-ids cluster) if [ -n "$r_id" ]; then if [ -n "$(relation-list -r $r_id)" ]; then juju-log "Unit peered" return 0 fi fi juju-log "Unit not peered" return 1 } ########################################################################## # Description: Determines whether host is owner of clustered services # Parameters: Name of CRM resource to check ownership of # Returns: 0 if leader, 1 if not leader ########################################################################## is_leader() { hostname=`hostname` if [ -x /usr/sbin/crm ]; then if crm resource show $1 | grep -q $hostname; then juju-log "$hostname is cluster leader." return 0 fi fi juju-log "$hostname is not cluster leader." return 1 } ########################################################################## # Description: Determines whether enough data has been provided in # configuration or relation data to configure HTTPS. # Parameters: None # Returns: 0 if HTTPS can be configured, 1 if not. ########################################################################## https() { local r_id="" if [[ -n "$(config-get ssl_cert)" ]] && [[ -n "$(config-get ssl_key)" ]] ; then return 0 fi for r_id in $(relation-ids identity-service) ; do for unit in $(relation-list -r $r_id) ; do if [[ "$(relation-get -r $r_id https_keystone $unit)" == "True" ]] && [[ -n "$(relation-get -r $r_id ssl_cert $unit)" ]] && [[ -n "$(relation-get -r $r_id ssl_key $unit)" ]] && [[ -n "$(relation-get -r $r_id ca_cert $unit)" ]] ; then return 0 fi done done return 1 } ########################################################################## # Description: For a given number of port mappings, configures apache2 # HTTPs local reverse proxying using certficates and keys provided in # either configuration data (preferred) or relation data. Assumes ports # are not in use (calling charm should ensure that). # Parameters: Variable number of proxy port mappings as # $internal:$external. # Returns: 0 if reverse proxy(s) have been configured, 0 if not. ########################################################################## enable_https() { local port_maps="$@" local http_restart="" juju-log "Enabling HTTPS for port mappings: $port_maps." # allow overriding of keystone provided certs with those set manually # in config. local cert=$(config-get ssl_cert) local key=$(config-get ssl_key) local ca_cert="" if [[ -z "$cert" ]] || [[ -z "$key" ]] ; then juju-log "Inspecting identity-service relations for SSL certificate." local r_id="" cert="" key="" ca_cert="" for r_id in $(relation-ids identity-service) ; do for unit in $(relation-list -r $r_id) ; do [[ -z "$cert" ]] && cert="$(relation-get -r $r_id ssl_cert $unit)" [[ -z "$key" ]] && key="$(relation-get -r $r_id ssl_key $unit)" [[ -z "$ca_cert" ]] && ca_cert="$(relation-get -r $r_id ca_cert $unit)" done done [[ -n "$cert" ]] && cert=$(echo $cert | base64 -di) [[ -n "$key" ]] && key=$(echo $key | base64 -di) [[ -n "$ca_cert" ]] && ca_cert=$(echo $ca_cert | base64 -di) else juju-log "Using SSL certificate provided in service config." fi [[ -z "$cert" ]] || [[ -z "$key" ]] && juju-log "Expected but could not find SSL certificate data, not "\ "configuring HTTPS!" && return 1 apt-get -y install apache2 a2enmod ssl proxy proxy_http | grep -v "To activate the new configuration" && http_restart=1 mkdir -p /etc/apache2/ssl/$CHARM echo "$cert" >/etc/apache2/ssl/$CHARM/cert echo "$key" >/etc/apache2/ssl/$CHARM/key if [[ -n "$ca_cert" ]] ; then juju-log "Installing Keystone supplied CA cert." echo "$ca_cert" >/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt update-ca-certificates --fresh # XXX TODO: Find a better way of exporting this? if [[ "$CHARM" == "nova-cloud-controller" ]] ; then [[ -e /var/www/keystone_juju_ca_cert.crt ]] && rm -rf /var/www/keystone_juju_ca_cert.crt ln -s /usr/local/share/ca-certificates/keystone_juju_ca_cert.crt \ /var/www/keystone_juju_ca_cert.crt fi fi for port_map in $port_maps ; do local ext_port=$(echo $port_map | cut -d: -f1) local int_port=$(echo $port_map | cut -d: -f2) juju-log "Creating apache2 reverse proxy vhost for $port_map." cat >/etc/apache2/sites-available/${CHARM}_${ext_port} < ServerName $(unit-get private-address) SSLEngine on SSLCertificateFile /etc/apache2/ssl/$CHARM/cert SSLCertificateKeyFile /etc/apache2/ssl/$CHARM/key ProxyPass / http://localhost:$int_port/ ProxyPassReverse / http://localhost:$int_port/ ProxyPreserveHost on Order deny,allow Allow from all Order allow,deny Allow from all END a2ensite ${CHARM}_${ext_port} | grep -v "To activate the new configuration" && http_restart=1 done if [[ -n "$http_restart" ]] ; then service apache2 restart fi } ########################################################################## # Description: Ensure HTTPS reverse proxying is disabled for given port # mappings. # Parameters: Variable number of proxy port mappings as # $internal:$external. # Returns: 0 if reverse proxy is not active for all portmaps, 1 on error. ########################################################################## disable_https() { local port_maps="$@" local http_restart="" juju-log "Ensuring HTTPS disabled for $port_maps." ( [[ ! -d /etc/apache2 ]] || [[ ! -d /etc/apache2/ssl/$CHARM ]] ) && return 0 for port_map in $port_maps ; do local ext_port=$(echo $port_map | cut -d: -f1) local int_port=$(echo $port_map | cut -d: -f2) if [[ -e /etc/apache2/sites-available/${CHARM}_${ext_port} ]] ; then juju-log "Disabling HTTPS reverse proxy for $CHARM $port_map." a2dissite ${CHARM}_${ext_port} | grep -v "To activate the new configuration" && http_restart=1 fi done if [[ -n "$http_restart" ]] ; then service apache2 restart fi } ########################################################################## # Description: Ensures HTTPS is either enabled or disabled for given port # mapping. # Parameters: Variable number of proxy port mappings as # $internal:$external. # Returns: 0 if HTTPS reverse proxy is in place, 1 if it is not. ########################################################################## setup_https() { # configure https via apache reverse proxying either # using certs provided by config or keystone. [[ -z "$CHARM" ]] && error_out "setup_https(): CHARM not set." if ! https ; then disable_https $@ else enable_https $@ fi } ########################################################################## # Description: Determine correct API server listening port based on # existence of HTTPS reverse proxy and/or haproxy. # Paremeters: The standard public port for given service. # Returns: The correct listening port for API service. ########################################################################## determine_api_port() { local public_port="$1" local i=0 ( [[ -n "$(peer_units)" ]] || is_clustered >/dev/null 2>&1 ) && i=$[$i + 1] https >/dev/null 2>&1 && i=$[$i + 1] echo $[$public_port - $[$i * 10]] } ########################################################################## # Description: Determine correct proxy listening port based on public IP + # existence of HTTPS reverse proxy. # Paremeters: The standard public port for given service. # Returns: The correct listening port for haproxy service public address. ########################################################################## determine_haproxy_port() { local public_port="$1" local i=0 https >/dev/null 2>&1 && i=$[$i + 1] echo $[$public_port - $[$i * 10]] } ########################################################################## # Description: Print the value for a given config option in an OpenStack # .ini style configuration file. # Parameters: File path, option to retrieve, optional # section name (default=DEFAULT) # Returns: Prints value if set, prints nothing otherwise. ########################################################################## local_config_get() { # return config values set in openstack .ini config files. # default placeholders starting (eg, %AUTH_HOST%) treated as # unset values. local file="$1" local option="$2" local section="$3" [[ -z "$section" ]] && section="DEFAULT" python -c " import ConfigParser config = ConfigParser.RawConfigParser() config.read('$file') try: value = config.get('$section', '$option') except: print '' exit(0) if value.startswith('%'): exit(0) print value " } ########################################################################## # Description: Creates an rc file exporting environment variables to a # script_path local to the charm's installed directory. # Any charm scripts run outside the juju hook environment can source this # scriptrc to obtain updated config information necessary to perform health # checks or service changes # # Parameters: # An array of '=' delimited ENV_VAR:value combinations to export. # If optional script_path key is not provided in the array, script_path # defaults to scripts/scriptrc ########################################################################## function save_script_rc { if [ ! -n "$JUJU_UNIT_NAME" ]; then echo "Error: Missing JUJU_UNIT_NAME environment variable" exit 1 fi # our default unit_path unit_path="$CHARM_DIR/scripts/scriptrc" echo $unit_path tmp_rc="/tmp/${JUJU_UNIT_NAME/\//-}rc" echo "#!/bin/bash" > $tmp_rc for env_var in "${@}" do if `echo $env_var | grep -q script_path`; then # well then we need to reset the new unit-local script path unit_path="$CHARM_DIR/${env_var/script_path=/}" else echo "export $env_var" >> $tmp_rc fi done chmod 755 $tmp_rc mv $tmp_rc $unit_path }