Openstack 开发人员安装脚本解读 [stack.sh]
这是转载的ju136的专栏的文章。http://devstack.org/ 有一份英文版的源代码解读,不过如果对于Openstack没有过接触的人来说,可能看起来会有一些困难。http://devstack.org/stack.sh.html
stack.sh 简介
stack.sh是一个openstack开发人员可以选择的一种安装脚本。
这个脚本的主要作用是安装并配置nova, glance, horizon, 以及keystone。
使用这个脚本,你可以设置一些配置选项,比如使用哪个git源,开启哪些服务,网络如何配置,以及各种密码的设置与管理。通过这个脚本,也可以灵活地在多台机器上运行,通过一个类似的资源进行一种共享式的配置。比如mysql, rabbitmq,从而可以组建一个多机环境下的云。
为了保证这个脚本足够简单,我们假设你是在一个Ubuntu 11.10 Oneiric的机器上执行。当然,这个脚本也可以在VM上运行,或是在物理机上运行。此外,我们把一些apt, pip安装包放到别的文件中去了,那么开始了解这个脚本,以及这些依赖的包。
如果你想进一步的学习或了解,可以去网站
http://devstack.org
Sanity Check
这个脚本首先来说是针对于Oneiric来写的,如果要在Ubuntu的其他版本上执行,当然也可以通过执行命令
viewplaincopy
[*]$FORCE=yes ./stack.sh
,从而达到要求。但是我个人而言,不建议采用这种方式进来安装部署。在其他版本上的安装部署,后面再补上。
版本检查
下面这段代码就是主要是在检查一下OS的版本。
# DISTRO变量就是拿到OS的版本。在Ubuntu上执行,会得到发行版的名字。oneiric。
# 接下来的几句只是在进行检查是否是oneiric版本的Ubuntu
viewplaincopy
[*]DISTRO=$(lsb_release -c -s)
[*]
[*]if [[ ! ${DISTRO} =~ (oneiric) ]]; then
[*] echo "WARNING: this script has only been tested on oneiric"
[*] if [[ "$FORCE" != "yes" ]]; then
[*] echo "If you wish to run this script anyway run with FORCE=yes"
[*] exit 1
[*] fi
[*]fi
目录设定
stack.sh把很多需要安装的包与其他一些依赖都放到别的文件里面的,你可以在/devstack/files中找到这些文件。但是在脚本中,我们是通过FILES变量来引用到这些配置文件的位置。
#首先是拿到顶级目录所在位置。比如/root/devstack,那么$TOP_DIR="/root/devstack"
#生成FILES变量的值。再去检查一下这个目录是否存在。
viewplaincopy
[*]TOP_DIR=$(cd $(dirname "$0") && pwd)
[*]
[*]FILES=$TOP_DIR/files
[*]if [ ! -d $FILES ]; then
[*] echo "ERROR: missing devstack/files - did you grab more than just stack.sh?"
[*] exit 1
[*]fi
设定
这个脚本的可定制性体现在,一些环境变量可以放到别的文件中,或者是通过设定环境变量来进行处理。比如
viewplaincopy
[*]$export MYSQL_PASSWORD=anothersecret
[*]$./stack.sh
可以达到效果。当然,你也可以把这些语句放在同一行,比如
viewplaincopy
[*]$MYSQL_PASSWORD=simple ./stack.sh
也可以把这些设定放到$TOP_DIR/localrc文件中。
比如:
viewplaincopy
[*]$ cat $TOP_DIR/localrc
[*].........
[*]MYSQL_PASSWORD=anothersecret
[*]MYSQL_USER=hellaroot
一般来说,我们都使用的是一些很敏感的设定,所以为了省事,你也可以运行
viewplaincopy
[*]$./stack.sh
(一般而言,如果这样运行,脚本会在发现没有密码的时候,叫你输出密码,如果输入密码为空,那么会随机生成一个密码)。
环境变量
对于环境变量而言,一般我们都是放到stackrc文件中。这个文件是随着devstack一起发布的,并且主要是包含了需要使用的git源。如果你想要使用别的源,或者是别的分枝,那么你可以加入你自己的设定,并把这些设定写到localrc文件中。
一般而言localrc里面的设置会把stackrc文件中的变量给覆盖掉。这个是很有用的,特别是当你想使用别的源或者分枝的时候。你也可以更改掉一些已有设定,比如
viewplaincopy
[*]MYSQL_PASSWORD
[*]ADMIN_PASSWORD
否则devstack脚本会给你随机生成一个(这个随机生成的密码还有点长)。
#首先是从stackrc中引入环境变量
#$DEST变量是指的是安装目录,一般默认安装目录是/opt/stack
#下面是定义了一个函数,其实也就是使用apt-get命令。
viewplaincopy
[*]source ./stackrc
[*]
[*]DEST=${DEST:-/opt/stack}
[*]
[*]function apt_get() {
[*] [[ "$OFFLINE" = "True" ]] && return
[*] local sudo="sudo"
[*] [ "$(id -u)" = "0" ] && sudo="env"
[*] $sudo DEBIAN_FRONTEND=noninteractive apt-get \
[*] --option "Dpkg::Options::=--force-confold" --assume-yes "$@"
[*]}
这里检查是否已经有stack.sh程序在运行。
viewplaincopy
[*]if screen -ls | egrep -q ".stack"; then
[*] echo "You are already running a stack.sh session."
[*] echo "To rejoin this session type 'screen -x stack'."
[*] echo "To destroy this session, kill the running screen."
[*] exit 1
[*]fi
因为Openstack在设计的时候,是用的一般用户来运行的,主要原因是因为dashboard在运行的时候底层使用的是apache2的服务器,而apache2不可以在root权限下执行。
如果在运行脚本的时候是使用的是root权限,那么会自动新建一个stack用户来执行操作。并且这个用户会给足够的权限来进行很多操作
viewplaincopy
[*]if [[ $EUID -eq 0 ]]; then
[*] ROOTSLEEP=${ROOTSLEEP:-10}
[*] echo "You are running this script as root."
[*] echo "In $ROOTSLEEP seconds, we will create a user 'stack' and run as that user"
[*] sleep $ROOTSLEEP
[*]
[*] # 因为这个脚本接下来是按照一个普通用户来执行的,那么需要给这个普通用户以sudo的权限。
[*] #
[*] dpkg -l sudo || apt_get update && apt_get install sudo
[*]
[*] if ! getent passwd stack >/dev/null; then
[*] echo "Creating a user called stack"
[*] useradd -U -G sudo -s /bin/bash -d $DEST -m stack
[*] fi
[*]
[*] echo "Giving stack user passwordless sudo priviledges"
[*] # some uec images sudoers does not have a '#includedir'. add one.
[*] grep -q "^#includedir.*/etc/sudoers.d" /etc/sudoers ||
[*] echo "#includedir /etc/sudoers.d" >> /etc/sudoers
[*] ( umask 226 && echo "stack ALL=(ALL) NOPASSWD:ALL" \
[*] > /etc/sudoers.d/50_stack_sh )
[*]
[*] echo "Copying files to stack user"
[*] STACK_DIR="$DEST/${PWD##*/}"
[*] cp -r -f -T "$PWD" "$STACK_DIR"#这里在拷文件,额外的拷文件的动作也可以加在这里。
[*] chown -R stack "$STACK_DIR"
[*] if [[ "$SHELL_AFTER_RUN" != "no" ]]; then
[*] exec su -c "set -e; cd $STACK_DIR; bash stack.sh; bash" stack
[*] else
[*] exec su -c "set -e; cd $STACK_DIR; bash stack.sh" stack
[*] fi
[*] exit 1
[*]else
[*] # Our user needs passwordless priviledges for certain commands which nova
[*] # uses internally.
[*] # Natty uec images sudoers does not have a '#includedir'. add one.
[*] sudo grep -q "^#includedir.*/etc/sudoers.d" /etc/sudoers ||
[*] echo "#includedir /etc/sudoers.d" | sudo tee -a /etc/sudoers
[*] TEMPFILE=`mktemp`
[*] cat $FILES/sudo/nova > $TEMPFILE
[*] sed -e "s,%USER%,$USER,g" -i $TEMPFILE
[*] chmod 0440 $TEMPFILE
[*] sudo chown root:root $TEMPFILE
[*] sudo mv $TEMPFILE /etc/sudoers.d/stack_sh_nova
[*]fi
接下来这个就是设置是否是用离线的方式来进行安装,或者是用上网的方式来进行安装。
viewplaincopy
[*]function trueorfalse() {
[*] local default=$1
[*] local testval=$2
[*]
[*] [[ -z "$testval" ]] && { echo "$default"; return; }
[*] [[ "0 no false False FALSE" =~ "$testval" ]] && { echo "False"; return; }
[*] [[ "1 yes true True TRUE" =~ "$testval" ]] && { echo "True"; return; }
[*] echo "$default"
[*]}
[*]OFFLINE=`trueorfalse False $OFFLINE`
一般来说,安装方式都是在线的方式来安装的。如果设置的值是True,那么就是使用离线的方式来进行安装。
配置服务
接下来是设置一些环境变量:这些环境变量主要都是为了安装某个部件而设置的。
viewplaincopy
[*]NOVA_DIR=$DEST/nova
[*]HORIZON_DIR=$DEST/horizon
[*]GLANCE_DIR=$DEST/glance
[*]KEYSTONE_DIR=$DEST/keystone
[*]NOVACLIENT_DIR=$DEST/python-novaclient
[*]OPENSTACKX_DIR=$DEST/openstackx
[*]NOVNC_DIR=$DEST/noVNC
[*]SWIFT_DIR=$DEST/swift
[*]SWIFT_KEYSTONE_DIR=$DEST/swift-keystone2
[*]QUANTUM_DIR=$DEST/quantum
下面的设置是为了Quantum而进行设置。
viewplaincopy
[*]# Default Quantum Plugin#看样子这个应该是一个网络服务
[*]Q_PLUGIN=${Q_PLUGIN:-openvswitch}
[*]# Default Quantum Port#这里设置端口
[*]Q_PORT=${Q_PORT:-9696}
[*]# Default Quantum Host#这里设置主机
[*]Q_HOST=${Q_HOST:-localhost}
接下来设置哪些服务要开启。注意这里用到的都是缩写。
viewplaincopy
[*]ENABLED_SERVICES=${ENABLED_SERVICES:-g-api,g-reg,key,n-api,n-cpu,n-net,n-sch,n-vnc,horizon,mysql,rabbit,openstackx}
下面是设置volume服务
viewplaincopy
[*]# Name of the lvm volume group to use/create for iscsi volumes
[*]VOLUME_GROUP=${VOLUME_GROUP:-nova-volumes}
[*]VOLUME_NAME_PREFIX=${VOLUME_NAME_PREFIX:-volume-}
[*]INSTANCE_NAME_PREFIX=${INSTANCE_NAME_PREFIX:-instance-}
接下来是设置需要使用到的底层的服务
viewplaincopy
[*]VIRT_DRIVER=${VIRT_DRIVER:-libvirt}
[*]LIBVIRT_TYPE=${LIBVIRT_TYPE:-kvm}
一般而言我们都是使用kvm+libvirt。如果kvm没有驱动起来,我们会使用qemu。
设置要使用到的Scheduler
viewplaincopy
[*]SCHEDULER=${SCHEDULER:-nova.scheduler.simple.SimpleScheduler}
接下来是拿到主机IP地址。
viewplaincopy
[*]if [ ! -n "$HOST_IP" ]; then
[*] HOST_IP=`LC_ALL=C /sbin/ifconfig eth0 | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'`
[*] if [ "$HOST_IP" = "" ]; then
[*] echo "Could not determine host ip address."
[*] echo "If this is not your first run of stack.sh, it is "
[*] echo "possible that nova moved your eth0 ip address to the FLAT_NETWORK_BRIDGE."
[*] echo "Please specify your HOST_IP in your localrc."
[*] exit 1
[*] fi
[*]fi
这里是设置一下是否使用系统日志:
viewplaincopy
[*]SYSLOG=`trueorfalse False $SYSLOG`
[*]SYSLOG_HOST=${SYSLOG_HOST:-$HOST_IP}
[*]SYSLOG_PORT=${SYSLOG_PORT:-516}
服务超时设定
viewplaincopy
[*]SERVICE_TIMEOUT=${SERVICE_TIMEOUT:-60}
接下这个函数就是为了读取密码而写的。
viewplaincopy
[*]function read_password {
[*] set +o xtrace
[*] var=$1; msg=$2
[*] pw=${!var}
[*]
[*] localrc=$TOP_DIR/localrc
[*]
[*] # If the password is not defined yet, proceed to prompt user for a password.
[*] if [ ! $pw ]; then
[*] # If there is no localrc file, create one
[*] if [ ! -e $localrc ]; then
[*] touch $localrc
[*] fi
[*]
[*] # Presumably if we got this far it can only be that our localrc is missing
[*] # the required password.Prompt user for a password and write to localrc.
[*] echo ''
[*] echo '################################################################################'
[*] echo $msg
[*] echo '################################################################################'
[*] echo "This value will be written to your localrc file so you don't have to enter it again."
[*] echo "It is probably best to avoid spaces and weird characters."
[*] echo "If you leave this blank, a random default value will be used."
[*] echo "Enter a password now:"
[*] read -e $var
[*] pw=${!var}
[*] if [ ! $pw ]; then
[*] pw=`openssl rand -hex 10`
[*] fi
[*] eval "$var=$pw"
[*] echo "$var=$pw" >> $localrc
[*] fi
[*] set -o xtrace
[*]}
你如果想设置成为一个固定的密码,那么这里可以改为如下:
viewplaincopy
[*]# Presumably if we got this far it can only be that our localrc is missing
[*]# the required password.Prompt user for a password and write to localrc.
[*]echo "Set password to nova:"
[*]pw="nova"
[*]eval "$var=$pw"
[*]echo "$var=$pw" >> $localrc
为了简单起见,后面会一直使用nova做为密码。(自己安装着玩的时候这么干比较好,如果正式用的话,还是用一个靠谱的密码吧。)
设置网络:这些设定应该最后都会写到/etc/nova/nova.conf文件中的。不过这里好像是放在安装目录中。
viewplaincopy
[*]PUBLIC_INTERFACE=${PUBLIC_INTERFACE:-eth0}
[*]FIXED_RANGE=${FIXED_RANGE:-10.0.0.0/24}
[*]FIXED_NETWORK_SIZE=${FIXED_NETWORK_SIZE:-256}
[*]FLOATING_RANGE=${FLOATING_RANGE:-172.24.4.224/28}
[*]NET_MAN=${NET_MAN:-FlatDHCPManager}
[*]EC2_DMZ_HOST=${EC2_DMZ_HOST:-$HOST_IP}
[*]FLAT_NETWORK_BRIDGE=${FLAT_NETWORK_BRIDGE:-br100}
[*]VLAN_INTERFACE=${VLAN_INTERFACE:-$PUBLIC_INTERFACE}
下面是设置是否使用多节点模式,如果使用多节点模式,那么每个计算节点都会与网络节点都是一体的,访问相应的VM的时候,直接访问相应的物理机则可。
这种模式可以解除SPOF和带宽的制约。
viewplaincopy
[*]MULTI_HOST=${MULTI_HOST:-False}
下面一段话比较长,不过也可以看一下:
如果你是在多台物理机上使用FlatDHCP模式,设置"FLAT_INTERFACE"变量,并且要保证这个interface并没有一个IP。否则你可能会破坏一些东西。
"*DHCP Warning" 这种情况,如果你使用flat interface设备使用DHCP,发生下面这种情况的时候,可能会让你呆住,
网络从flat interface上移到flat network bridge。这种情况完全有可能发生。结果是当你创建第一台虚拟机的时候。这种情况会导致你的虚拟机出问题,比如连接不上,或者登陆失败。
如果你只是装着单机版的来玩玩,那么你可以使用FLAT_NETWORK_BRIDGE。
viewplaincopy
[*]FLAT_INTERFACE=${FLAT_INTERFACE:-eth0}
quantum网络
你需要确保quantum是在Enabled_services中是开启的。假如network manager设置成为QuantumManager。
假如你想在这台机器上运行Quantum,那么最好在ENABLED_SERVICES中要找到q-svc字段。
如果你想在quantum中使用openswitch插件,那么把 Q_PLUGIN 的值设置为openvswitch。并且需要保证q-agt服务也是在开启的服务之列。
如果你设置了Quantum网络,那么NET_MAN变量的值就被忽略了。
MYSQL 和 RabbitMQ
在这里,我们配置的目标是让Nova, Horizon, Glance, 与Keystone都使用MySQL做为其数据库。这里他们主要是使用一个单一的服务,但是每个服务都是有他们自己的表数据库与表单。
一般而言,这里的脚本会安装和配置MySQL。如果你想用一个已经有的服务,那么你需要传递/user/password/host参数。也同时需要传递MYSQL_PASSWORD参数到每个节点(当你使用的单台机器的时候,这一步就省了吧)。一般而言我都是采用一种最简单的方式,也就是一台服务器,也都使用这个服务器上的MySQL数据库。
下面的三行代码分别是设置MySQL的机器地址与用户名与密码。在这里是需要手动输入密码。
viewplaincopy
[*]MYSQL_HOST=${MYSQL_HOST:-localhost}
[*]MYSQL_USER=${MYSQL_USER:-root}
[*]read_password MYSQL_PASSWORD "ENTER A PASSWORD TO USE FOR MYSQL."
下面再给出一个链接,这个链接主要是用于连接MYSQL数据库。一般不需要去加上所需要连接的DataBase,因为其他几个服务都是需要通过这个链接去连MySQL。但是每个服务连接的数据库是不一样的。所以如果是用同一样的链接+数据库名,肯定会出错。一句话,保持下面这个不变就可以了。
viewplaincopy
[*]BASE_SQL_CONN=${BASE_SQL_CONN:-mysql://$MYSQL_USER:$MYSQL_PASSWORD@$MYSQL_HOST}
设置RabbitMQ的密码
viewplaincopy
[*]RABBIT_HOST=${RABBIT_HOST:-localhost}
[*]read_password RABBIT_PASSWORD "ENTER A PASSWORD TO USE FOR RABBIT."
接下来指出Glance的主机与端口,值得注意的是这里需要给出端口。这个端口按照原文好像是需要指定的,特定的这么个端口(总之保持不变就是了).。
viewplaincopy
[*]GLANCE_HOSTPORT=${GLANCE_HOSTPORT:-$HOST_IP:9292}
Swift设置
需要完善的功能:
TODO:实现对于glance的支持。
TODO: 加入对于不同存储位置的日志功能。
一般来说,swift驱动的位置与存储对象是放在swift源码的内部文件夹中。当然也可以自已指定一个目录。
viewplaincopy
[*]SWIFT_DATA_LOCATION=${SWIFT_DATA_LOCATION:-${SWIFT_DIR}/data}
同样的道里,swift的配置文件也是放在源码内部的。当然也可以根据你自己的情况做出调整。
viewplaincopy
[*]SWIFT_CONFIG_LOCATION=${SWIFT_CONFIG_LOCATION:-${SWIFT_DIR}/config}
接下来,devstack需要建立一个loop-back的磁盘,这个磁盘会被格式化为XFS格式,并且用来存储swift数据。一般来说,这个磁盘的大小是1G。变量SWIFT_LOOPBACK_DISK_SIZE指出了这个磁盘的大小。当然你也可以根据自己的情况来调整大小。
viewplaincopy
[*]SWIFT_LOOPBACK_DISK_SIZE=${SWIFT_LOOPBACK_DISK_SIZE:-1000000}
接下来是swift需要读入一个密码。这一段比较难以理解(主查涉及到swift内部的一些原理与实现)。
viewplaincopy
[*]# The ring uses a configurable number of bits from a path’s MD5 hash as
[*]# a partition index that designates a device. The number of bits kept
[*]# from the hash is known as the partition power, and 2 to the partition
[*]# power indicates the partition count. Partitioning the full MD5 hash
[*]# ring allows other parts of the cluster to work in batches of items at
[*]# once which ends up either more efficient or at least less complex than
[*]# working with each item separately or the entire cluster all at once.
[*]# By default we define 9 for the partition count (which mean 512).
[*]SWIFT_PARTITION_POWER_SIZE=${SWIFT_PARTITION_POWER_SIZE:-9}
[*]
[*]# We only ask for Swift Hash if we have enabled swift service.
[*]if [[ "$ENABLED_SERVICES" =~ "swift" ]]; then
[*] # SWIFT_HASH is a random unique string for a swift cluster that
[*] # can never change.
[*] read_password SWIFT_HASH "ENTER A RANDOM SWIFT HASH."
[*]fi
Keystone
服务口令:Openstack的组件需要有一个管理员口令来认证用户的口令。
下面则是读入一个密码:
viewplaincopy
[*]read_password SERVICE_TOKEN "ENTER A SERVICE_TOKEN TO USE FOR THE SERVICE ADMIN TOKEN."
同样也需要为Horizon输入一个密码。
viewplaincopy
[*]read_password ADMIN_PASSWORD "ENTER A PASSWORD TO USE FOR HORIZON AND KEYSTONE (20 CHARS OR LESS)."
接下来是设置日志
viewplaincopy
[*]LOGFILE=${LOGFILE:-"$PWD/stack.sh.$.log"}
[*](
[*]# So that errors don't compound we exit on any errors so you see only the
[*]# first error that occurred.
[*]trap failed ERR
[*]failed() {
[*] local r=$?
[*] set +o xtrace
[*] [ -n "$LOGFILE" ] && echo "${0##*/} failed: full log in $LOGFILE"
[*] exit $r
[*]}
接下来的这个命令比较有用,就是打出每个命令,以及这个命令的输出,主要是用于查看在哪里出错了。以便及时查错。
viewplaincopy
[*]set -o xtrace
创建目标目录,并且要保证stack用户对于这个目录是可写的。
viewplaincopy
[*]sudo mkdir -p $DEST
[*]if [ ! -w $DEST ]; then
[*] sudo chown `whoami` $DEST
[*]fi
前面讲了这么多,其实都是在讲怎么配置。接下来才是要进行人工操作阶段。
安装依赖包
Openstack用了很多其他的工程。所以需要安装其他工程。
注意的是:
我们只安装对于我们的服务来说是必须的包。
脚本学习---得到操作系统发行版的方法
方法一
viewplaincopy
[*]DISTRO=$(lsb_release -c -s)
可以用来得到操作系统的发行版。
缺点,
虽然这种方法可以在ubuntu上得到类似于oneiric的字样(如果你的发行版不同,那么得到的也肯定不一样)。
至于其他版本,则得到的结果就有点奇怪了。比如我在suse上运行,是得不到相应的结果的。
viewplaincopy
[*]$ lsb_release -c -s
[*]n/a
方法二
其实操作系统发行版的信息是写在/etc/issue 和/etc/issue.net文件中。为了方便我们用如下命令去查看吧。
viewplaincopy
[*]$ cat /etc/issue*
在ubuntu下的运行结果如下: viewplaincopy
[*]$ cat /etc/issue*
[*]Ubuntu 11.10 \n \l
[*]
[*]Ubuntu 11.10
在SUSE下面运行有如下结果:
viewplaincopy
[*]$ cat /etc/issue*
[*]Welcome to SUSE Linux Enterprise Server 11 SP1(x86_64) - Kernel \r (\l).
[*]
[*]
[*]Welcome to SUSE Linux Enterprise Server 11 SP1(x86_64) - Kernel %r (%t).
为了简单起见,我们需要把这些字符串转化成为小写,再进行判断。因为有时候有的发行版在说明的时候会写SUSE, 而Ubuntu的写的是Ubuntu,并不是全部都是大写。
我们用如下脚本来进行测试是否是suse系统
viewplaincopy
[*]<span style="color:#3333FF;">if [[ `cat $DISTRO | grep suse | wc -l` -eq 0 ]]; then
[*]echo "Not suse"
[*]exit 1
[*]fi</span>
脚本学习----如何得到脚本运行时所在目录。
当在问这个问题的时候,很有可能会回答说是pwd,在C语言里用的是getcwd()函数。实际上,pwd这个命令返回值是运行命令时所在路径,而不是脚本所在目录。
如果对于这个问题不是很了解。那么换个问题就是,一个脚本,如何知道自己所在的目录?这个脚本可以被放在很多位置。
方法一
第一种方法就是这个stack.sh脚本中所提到的,
viewplaincopy
[*]TOP_DIR=$(cd $(dirname "$0") && pwd)
一开始看时,可能不太明白其含义,现解释如下: viewplaincopy
[*]$0 这个只能是在脚本里面使用,在命令行里面只会输出-bash。$0表示的意思是运行脚本的全路径。
[*]
[*]dirname这个命令就是截取全路径中的目录部分比如
[*]$ dirname /root/stack/nova.sh
[*]/root/stack
[*]
[*]实际上,在脚本中,使用dirname $0可能就已经够用了。之所以再用cd && pwd我觉得主要的原因是怕$0给出的不是全路径。
方法二 来个简洁版的,
viewplaincopy
[*]TOP_DIR=`dirname $0`
从而可以确定脚本所在目录。
脚本学习---如何取得全路径中的路径
方法一
dirname $par
方法二
pkg_dir=${par%/*}
页:
[1]