Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
itminedu
snf-image
Commits
09ac08db
Commit
09ac08db
authored
Nov 11, 2011
by
Nikos Skalkotos
Browse files
Merge branch 'v0.2-dev'
parents
d0362bb3
de7269cd
Changes
26
Hide whitespace changes
Inline
Side-by-side
snf-image-helper/Makefile.am
View file @
09ac08db
...
...
@@ -7,7 +7,7 @@ SUBDIRS = tasks
dist_doc_DATA
=
COPYING AUTHORS ChangeLog
dist_bin_SCRIPTS
=
snf-image-helper
dist_scripts_SCRIPTS
=
snf-passtohash.py
dist_scripts_SCRIPTS
=
snf-passtohash.py
inject-files.py decode-properties.py
dist_common_DATA
=
common.sh unattend.xml
edit
=
sed
\
...
...
snf-image-helper/common.sh
View file @
09ac08db
...
...
@@ -29,12 +29,14 @@
RESULT
=
/dev/ttyS1
FLOPPY_DEV
=
/dev/fd0
PROGNAME
=
$(
basename
$0
)
PATH
=
/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
# Programs
XMLSTARLET
=
xmlstarlet
RESIZE2FS
=
resize2fs
PARTED
=
parted
CLEANUP
=(
)
...
...
@@ -49,6 +51,10 @@ log_error() {
exit
1
}
warn
()
{
echo
"Warning:
$@
"
>
&2
}
get_base_distro
()
{
local
root_dir
=
$1
...
...
@@ -92,6 +98,35 @@ get_distro() {
fi
}
get_last_partition
()
{
local
dev
=
"
$1
"
"
$PARTED
"
-s
-m
"
$dev
"
unit s print |
tail
-1
}
get_partition
()
{
local
dev
=
"
$1
"
local id
=
"
$2
"
"
$PARTED
"
-s
-m
"
$dev
"
unit s print |
grep
"^
$id
"
}
get_partition_count
()
{
local
dev
=
"
$1
"
expr
$(
"
$PARTED
"
-s
-m
"
$dev
"
unit s print |
wc
-l
)
- 2
}
get_last_free_sector
()
{
local
dev
=
"
$1
"
local
last_line
=
$(
"
$PARTED
"
-s
-m
"
$dev
"
unit s print free |
tail
-1
)
local type
=
$(
echo
"
$last_line
"
|
cut
-d
:
-f
5
)
if
[
"
$type
"
=
"free;"
]
;
then
echo
"
$last_line
"
|
cut
-d
:
-f
3
fi
}
cleanup
()
{
# if something fails here, it shouldn't call cleanup again...
trap
- EXIT
...
...
@@ -124,6 +159,23 @@ cleanup() {
fi
}
check_if_excluded
()
{
test
"
$PROGNAME
"
=
"snf-image-helper"
&&
return
0
eval local
do_exclude
=
\$
SNF_IMAGE_EXCLUDE_
${
PROGNAME
:2
}
_TASK
if
[
-n
"
$do_exclude
"
]
;
then
warn
"Task
$PROGNAME
was excluded and will not run."
exit
0
fi
return
0
}
trap
cleanup EXIT
# Check if the execution of a task should be ommited
check_if_excluded
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/configure.ac
View file @
09ac08db
...
...
@@ -23,6 +23,11 @@ if test -z "$RESIZE2FS" ; then
AC_MSG_ERROR([resize2fs not found in $PATH])
fi
AC_PATH_PROG(PARTED, [parted], [], [$PATH:/usr/sbin:/sbin])
if test -z "$PARTED" ; then
AC_MSG_ERROR([parted not found in $PATH])
fi
AC_CONFIG_FILES([
Makefile
tasks/Makefile
...
...
snf-image-helper/decode-properties.py
0 → 100755
View file @
09ac08db
#!/usr/bin/env python
"""Decode a json encoded string with properties
This program decodes a json encoded properties string and outputs it in a
bash sourcable way. The properties are passed to the program through a JSON
string either read from a file or from standard input and are outputed to a
target file.
"""
import
sys
import
os
import
subprocess
import
json
from
StringIO
import
StringIO
from
optparse
import
OptionParser
def
parse_arguments
(
input_args
):
usage
=
"Usage: %prog [options] <output_file>"
parser
=
OptionParser
(
usage
=
usage
)
parser
.
add_option
(
"-i"
,
"--input"
,
action
=
"store"
,
type
=
'string'
,
dest
=
"input_file"
,
help
=
"get input from FILE instead of stdin"
,
metavar
=
"FILE"
)
opts
,
args
=
parser
.
parse_args
(
input_args
)
if
len
(
args
)
!=
1
:
parser
.
error
(
'output file is missing'
)
output_file
=
args
[
0
]
if
opts
.
input_file
is
not
None
:
if
not
os
.
path
.
isfile
(
opts
.
input_file
):
parser
.
error
(
'input file does not exist'
)
return
(
opts
.
input_file
,
output_file
)
def
main
():
(
input_file
,
output_file
)
=
parse_arguments
(
sys
.
argv
[
1
:])
infh
=
sys
.
stdin
if
input_file
is
None
else
open
(
input_file
,
'r'
)
outfh
=
open
(
output_file
,
'w'
)
properties
=
json
.
load
(
infh
)
for
key
,
value
in
properties
.
items
():
os
.
environ
[
'SNF_IMAGE_PROPERTY_'
+
key
]
=
value
p
=
subprocess
.
Popen
([
'bash'
,
'-c'
,
'set'
],
stdout
=
subprocess
.
PIPE
)
output
=
StringIO
(
p
.
communicate
()[
0
]);
for
line
in
iter
(
output
):
if
line
.
startswith
(
'SNF_IMAGE_PROPERTY_'
):
outfh
.
write
(
'export '
+
line
)
infh
.
close
()
outfh
.
close
()
return
0
if
__name__
==
"__main__"
:
sys
.
exit
(
main
())
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/inject-files.py
0 → 100755
View file @
09ac08db
#!/usr/bin/env python
"""Inject files into a directory
This program injects files into a target directory.
The files are passed to the program through a JSON string either read from a
file or from standard input.
"""
import
sys
import
os
import
json
import
datetime
import
base64
from
optparse
import
OptionParser
def
timestamp
():
now
=
datetime
.
datetime
.
now
()
current_time
=
now
.
strftime
(
"%Y%m%d.%H%M%S"
)
return
current_time
def
parse_arguments
(
input_args
):
usage
=
"Usage: %prog [options] <target>"
parser
=
OptionParser
(
usage
=
usage
)
parser
.
add_option
(
"-i"
,
"--input"
,
action
=
"store"
,
type
=
'string'
,
dest
=
"input_file"
,
help
=
"get input from FILE instead of stdin"
,
metavar
=
"FILE"
)
opts
,
args
=
parser
.
parse_args
(
input_args
)
if
len
(
args
)
!=
1
:
parser
.
error
(
'target is missing'
)
target
=
args
[
0
]
if
not
os
.
path
.
isdir
(
target
):
parser
.
error
(
'target is not a directory'
)
input_file
=
opts
.
input_file
if
input_file
is
None
:
input_file
=
sys
.
stdin
else
:
if
not
os
.
path
.
isfile
(
input_file
):
parser
.
error
(
'input file does not exist'
)
input_file
=
open
(
input_file
,
'r'
)
return
(
input_file
,
target
)
def
main
():
(
input_file
,
target
)
=
parse_arguments
(
sys
.
argv
[
1
:])
files
=
json
.
load
(
input_file
)
for
f
in
files
:
real_path
=
target
+
'/'
+
f
[
'path'
]
if
os
.
path
.
lexists
(
real_path
):
backup_file
=
real_path
+
'.bak.'
+
timestamp
()
os
.
rename
(
real_path
,
backup_file
)
parentdir
=
os
.
path
.
dirname
(
real_path
)
if
not
os
.
path
.
exists
(
parentdir
):
os
.
makedirs
(
parentdir
)
newfile
=
open
(
real_path
,
'w'
)
newfile
.
write
(
base64
.
b64decode
(
f
[
'contents'
]))
newfile
.
close
()
os
.
chmod
(
real_path
,
0440
)
sys
.
stderr
.
write
(
'Successful personalization of Image
\n
'
)
input_file
.
close
()
return
0
if
__name__
==
"__main__"
:
sys
.
exit
(
main
())
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/snf-image-helper.in
View file @
09ac08db
...
...
@@ -59,11 +59,22 @@ else
log_error
"Floppy does not contain
\`
rules
\'
file"
fi
if
[
-n
"
$SNF_IMAGE_PROPERTIES
"
]
;
then
properties
=
$(
mktemp
--tmpdir
properties.XXXXXX
)
add_cleanup
rm
"
$properties
"
echo
"
$SNF_IMAGE_PROPERTIES
"
|
"@scriptsdir@/decode-properties.py"
"
$properties
"
source
"
$properties
"
else
log_error
"SNF_IMAGE_PROPERTIES variable is missing"
fi
# Image mount point...
target
=
$(
mktemp
-d
--tmpdir
target.XXXXXX
)
add_cleanup
rmdir
"
$target
"
export
SNF_IMAGE_TARGET
=
"
$target
"
export
SNF_IMAGE_ROOTDEV
=
"
${
SNF_IMAGE_DEV
}${
SNF_IMAGE_ROOT
}
"
if
[
!
-d
"@tasksdir@"
]
;
then
log_error
"snf-image/tasks directory is missing"
...
...
@@ -78,13 +89,15 @@ fi
# in case it is left mounted...
trap
'{ umount "$target"; }'
ERR
# Redirect standard error to standard output,
# prepend a timestamp before each line of output.
echo
"Execute all snf-image tasks...."
$RUN_PARTS
-v
--exit-on-error
"@tasksdir@"
2>&1|
while
IFS
=
read
-r
line
;
do
echo
$(
date
+%Y:%m:%d-%H:%M:%S.%N
)
"
$line
"
done
if
[
-z
"
$SNF_IMAGE_EXCLUDE_ALL_TASKS
"
]
;
then
# Redirect standard error to standard output,
# prepend a timestamp before each line of output.
echo
"Execute all snf-image tasks...."
$RUN_PARTS
-v
--exit-on-error
"@tasksdir@"
2>&1|
while
IFS
=
read
-r
line
;
do
echo
$(
date
+%Y:%m:%d-%H:%M:%S.%N
)
"
$line
"
done
fi
# Disable the trap. If code reaches here, the filesystem is unmounted.
trap
- ERR
...
...
snf-image-helper/tasks/10FixPartitionTable.in
0 → 100644
View file @
09ac08db
#! /bin/bash
### BEGIN TASK INFO
# Provides: FixPartitionTable
# RunBefore: FilesystemResizeUnmounted
# Short-Description: Resize filesystem to use all the available space
### END TASK INFO
set
-e
.
"@commondir@/common.sh"
if
[
!
-b
"
$SNF_IMAGE_DEV
"
]
;
then
log_error
"Device file:
\`
${
SNF_IMAGE_DEV
}
' is not a block device"
fi
if
[
$(
get_partition_count
"
$SNF_IMAGE_DEV
"
)
-eq
0
]
;
then
log_error
"Device:
\`
${
SNF_IMAGE_DEV
}
' does not contain any partition"
fi
retval
=
$(
get_last_partition
"
$SNF_IMAGE_DEV
"
)
id
=
$(
echo
$retval
|
cut
-d
:
-f1
)
pstart
=
$(
echo
$retval
|
cut
-d
:
-f2
)
pend
=
$(
echo
$retval
|
cut
-d
:
-f3
)
ptype
=
$(
echo
$retval
|
cut
-d
:
-f5
)
if
[
$id
-gt
4
]
;
then
log_error
"We don't support logical volumes"
fi
if
[
x
"
$ptype
"
=
"x"
]
;
then
# Don't know how to handle this
warn
"Last partition with id:
\`
$id
' is empty or has unknown filesystem"
warn
"I won't resize the partition"
exit
0
fi
new_pend
=
$(
get_last_free_sector
"
$SNF_IMAGE_DEV
"
)
if
[
-z
"
$new_pend
"
]
;
then
# Nothing to do
exit
0
fi
# Extend the partition
$PARTED
-s
-m
"
$SNF_IMAGE_DEV
"
rm
"
$id
"
$PARTED
-s
-m
"
$SNF_IMAGE_DEV
"
mkpart primary
"
$ptype
"
"
$pstart
"
"
$new_pend
"
# Inform the kernel about the changes
partprobe
"
$SNF_IMAGE_DEV
"
exit
0
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/tasks/
10
ResizeUnmounted.in
→
snf-image-helper/tasks/
20Filesystem
ResizeUnmounted.in
View file @
09ac08db
#! /bin/bash
### BEGIN TASK INFO
# Provides: ResizeUnmounted
# Provides:
Filesystem
ResizeUnmounted
# RunBefore: MountImage
# RunAfter: FixPartitionTable
# Short-Description: Resize filesystem to use all the available space
### END TASK INFO
...
...
@@ -11,15 +12,16 @@ set -e
if
[
!
-b
"
$SNF_IMAGE_DEV
"
]
;
then
log_error
"Device file:
\`
${
SNF_IMAGE_DEV
}
' is not a block device"
fi
if
[
-z
"
$SNF_IMAGE_TYPE
"
]
;
then
log_error
"Image type does not exist"
fi
if
[
"
$SNF_IMAGE_TYPE
"
=
"extdump"
]
;
then
"
$RESIZE2FS
"
"
$SNF_IMAGE_DEV
"
fi
last_partition
=
$(
get_last_partition
"
$SNF_IMAGE_DEV
"
)
id
=
$(
echo
"
$last_partition
"
|
cut
-d
:
-f1
)
ptype
=
$(
echo
"
$last_partition
"
|
cut
-d
:
-f5
)
if
[[
"
$ptype
"
==
ext[234]
]]
;
then
device
=
"
${
SNF_IMAGE_DEV
}${
id
}
"
"
$RESIZE2FS
"
"
$device
"
fi
exit
0
...
...
snf-image-helper/tasks/30MountImage.in
View file @
09ac08db
...
...
@@ -13,11 +13,11 @@ if [ ! -d "$SNF_IMAGE_TARGET" ]; then
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing"
fi
if
[
!
-b
"
$SNF_IMAGE_DEV
"
]
;
then
log_error
"Device file:
\`
$SNF_IMAGE_DEV
' is not a block device"
if
[
!
-b
"
$SNF_IMAGE_
ROOT
DEV
"
]
;
then
log_error
"Device file:
\`
$SNF_IMAGE_
ROOT
DEV
' is not a block device"
fi
mount
"
$SNF_IMAGE_DEV
"
"
$SNF_IMAGE_TARGET
"
mount
"
$SNF_IMAGE_
ROOT
DEV
"
"
$SNF_IMAGE_TARGET
"
-o
rw
exit
0
...
...
snf-image-helper/tasks/40AddDeleteUnattendScript.in
View file @
09ac08db
...
...
@@ -14,7 +14,7 @@ if [ ! -d "$SNF_IMAGE_TARGET" ]; then
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing"
fi
if
[
"
$SNF_IMAGE_
TYPE
"
=
"ntfsdump
"
]
;
then
if
[
"
$SNF_IMAGE_
PROPERTY_OSFAMILY
"
=
"windows
"
]
;
then
# Make sure Unattend.xml is removed after setup has finished
mkdir
-p
"
$SNF_IMAGE_TARGET
/Windows/Setup/Scripts"
echo
"del /Q /F C:
\U
nattend.xml"
>
"
$SNF_IMAGE_TARGET
/Windows/Setup/Scripts/SetupComplete.cmd"
...
...
snf-image-helper/tasks/40DeleteSSHKeys.in
View file @
09ac08db
...
...
@@ -14,11 +14,7 @@ if [ ! -d "$SNF_IMAGE_TARGET" ]; then
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing."
fi
target
=
"
$SNF_IMAGE_TARGET
"
if
[
"
$SNF_IMAGE_TYPE
"
!=
"extdump"
]
;
then
cleanup
trap
- EXIT
if
[
"
$SNF_IMAGE_PROPERTY_OSFAMILY
"
!=
"linux"
]
;
then
exit
0
fi
...
...
@@ -29,6 +25,7 @@ RSA_KEY="/etc/ssh/ssh_host_rsa_key"
DSA_KEY
=
"/etc/ssh/ssh_host_dsa_key"
ECDSA_KEY
=
"/etc/ssh/ssh_host_ecdsa_key"
target
=
"
$SNF_IMAGE_TARGET
"
#Remove the default keys
for
pair
in
"
$HOST_KEY
@rsa1"
"
$RSA_KEY
@rsa"
"
$DSA_KEY
@dsa"
"
$ECDSA_KEY
@ecdsa"
;
do
...
...
@@ -48,8 +45,6 @@ config="$target/etc/ssh/sshd_config"
if
[
!
-e
"
$config
"
]
;
then
echo
"Warning: Config file:
\`
$config
' is missing."
echo
"Warning: Can't check for non-default keys."
cleanup
trap
- EXIT
exit
0
fi
...
...
@@ -93,9 +88,6 @@ grep ^HostKey "$config" | while read key_line; do
fi
done
cleanup
trap
- EXIT
exit
0
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/tasks/40EnforcePersonality
deleted
100644 → 0
View file @
d0362bb3
#!/usr/bin/env python
### BEGIN TASK INFO
# Provides: EnforcePersonality
# RunBefore: UmountImage
# RunAfter: MountImage
# Short-Description: Inject files to the instance
### END TASK INFO
"""Personalize an Image by injecting files
This hook injects files into the filesystem of an Image.
The files are passed to the hook through the Ganeti
OS interface and found in the variable OSP_IMG_PERSONALITY.
"""
import
sys
import
os
import
json
import
datetime
import
base64
def
timestamp
():
now
=
datetime
.
datetime
.
now
()
current_time
=
now
.
strftime
(
"%Y%m%d.%H%M%S"
)
return
current_time
def
main
():
if
not
os
.
environ
.
has_key
(
'SNF_IMAGE_TARGET'
):
sys
.
stderr
.
write
(
'Error: SNF_IMAGE_TARGET variable is missing
\n
'
)
return
1
target
=
os
.
environ
[
'SNF_IMAGE_TARGET'
]
if
not
os
.
path
.
isdir
(
target
):
sys
.
stderr
.
write
(
'Error: Target: `'
+
target
+
'
\'
is not a directory.
\n
'
)
return
2
if
os
.
environ
.
has_key
(
'SNF_IMAGE_PERSONALITY'
):
osp_img_personality
=
os
.
environ
[
'SNF_IMAGE_PERSONALITY'
]
files
=
json
.
loads
(
osp_img_personality
)
for
f
in
files
:
real_path
=
target
+
'/'
+
f
[
'path'
]
if
os
.
path
.
lexists
(
real_path
):
backup_file
=
real_path
+
'.bak.'
+
timestamp
()
os
.
rename
(
real_path
,
backup_file
)
newfile
=
open
(
real_path
,
'w'
)
newfile
.
write
(
base64
.
b64decode
(
f
[
'contents'
]))
newfile
.
close
()
os
.
chmod
(
real_path
,
0440
)
sys
.
stderr
.
write
(
'Successful personalization of Image
\n
'
)
else
:
sys
.
stderr
.
write
(
'This Image has no personality (0 files to inject)
\n
'
)
return
0
if
__name__
==
"__main__"
:
sys
.
exit
(
main
())
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/tasks/40ResizeMounted.in
→
snf-image-helper/tasks/40
Filesystem
ResizeMounted.in
View file @
09ac08db
#! /bin/bash
### BEGIN TASK INFO
# Provides: ResizeMounted
# Provides:
Filesystem
ResizeMounted
# RunBefore: UmountImage
# RunAfter: MountImage
# Short-Description: Resize filesystem to use all the available space
...
...
@@ -14,21 +14,26 @@ if [ ! -d "$SNF_IMAGE_TARGET" ]; then
log_error
"Target directory
\`
$SNF_IMAGE_TARGET
' is missing"
fi
if
[
"
$SNF_IMAGE_TYPE
"
=
"ntfsdump"
]
;
then
if
[
"
$SNF_IMAGE_PROPERTY_OSFAMILY
"
!=
"windows"
]
;
then
exit
0
fi
last_partition
=
$(
get_last_partition
"
$SNF_IMAGE_DEV
"
)
id
=
$(
echo
"
$last_partition
"
|
cut
-d
:
-f1
)
ptype
=
$(
echo
"
$last_partition
"
|
cut
-d
:
-f5
)
if
[
"
$ptype
"
=
"ntfs"
]
;
then
# Write a diskpart script to %SystemDrive%\Windows\SnfScripts. Sysprep will
# try to execute this script during the specialize pass.
mkdir
-p
"
$SNF_IMAGE_TARGET
/Windows/SnfScripts"
cat
>
"
$SNF_IMAGE_TARGET
/Windows/SnfScripts/ExtendFilesystem"
<<
EOF
select disk 0
select volume
1
select volume
$id
extend filesystem
exit
EOF
fi
cleanup
trap
- EXIT
exit
0
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
snf-image-helper/tasks/40InstallUnattend.in
View file @
09ac08db
...
...
@@ -11,10 +11,10 @@ set -e
.
"@commondir@/common.sh"
if
[
-z
"
$SNF_IMAGE_TARGET
"
]
;
then
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing"
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing"
fi
if
[
"
$SNF_IMAGE_
TYPE
"
!=
"ntfsdump
"
]
;
then
if
[
"
$SNF_IMAGE_
PROPERTY_OSFAMILY
"
!=
"windows
"
]
;
then
exit
0
fi
...
...
snf-image-helper/tasks/40SELinuxAutorelabel.in
View file @
09ac08db
...
...
@@ -14,7 +14,7 @@ if [ ! -d "$SNF_IMAGE_TARGET" ]; then
log_error
"Target dir:
\`
$SNF_IMAGE_TARGET
' is missing"
fi
if
[
"
$SNF_IMAGE_
TYPE
"
=
"extdump
"
]
;
then
if
[
"
$SNF_IMAGE_
PROPERTY_OSFAMILY
"
=
"linux
"
]
;
then
distro
=
$(
get_base_distro
$SNF_IMAGE_TARGET
)
if
[
"
$distro
"
=
"redhat"
]
;
then
...
...
snf-image-helper/tasks/50AssignHostname.in
View file @
09ac08db
...
...
@@ -62,15 +62,12 @@ if [ -z "$SNF_IMAGE_HOSTNAME" ]; then
log_error
"Hostname is missing"
fi
if
[
"
$SNF_IMAGE_
TYPE
"
=
"ntfsdump
"
]
;
then
if
[
"
$SNF_IMAGE_
PROPERTY_OSFAMILY
"
=
"windows
"
]
;
then
windows_hostname
"
$SNF_IMAGE_TARGET
"
"
$SNF_IMAGE_HOSTNAME
"
elif
[
"
$SNF_IMAGE_
TYPE
"
=
"extdump
"
]
;
then
elif
[
"
$SNF_IMAGE_
PROPERTY_OSFAMILY
"
=
"linux
"
]
;
then
linux_hostname
"
$SNF_IMAGE_TARGET
"
"
$SNF_IMAGE_HOSTNAME
"
fi
cleanup
trap
- EXIT
exit
0
# vim: set sta sts=4 shiftwidth=4 sw=4 et ai :
...
...
snf-image-helper/tasks/50ChangePassword.in
View file @
09ac08db
...
...
@@ -36,13 +36,21 @@ linux_password() {
log_error
"No /etc/shadow found!"
fi
declare
-a
users
=(
"root"
)
local
distro
=
$(
get_distro
$target
)
if
[
"x
$distro
"
=
"xubuntu"
-o
\
"x
$distro
"
=
"xfedora"
]
;
then
users
+
=(
"user"
)
declare
-a
users