Skip to content

Commit c5552db

Browse files
committed
2.0.0
1 parent c03c8d4 commit c5552db

File tree

1 file changed

+66
-96
lines changed

1 file changed

+66
-96
lines changed

ssh-oci-bastion.sh

Lines changed: 66 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ NAME
1313
1414
SYNOPSIS
1515
16-
$SCRIPT_NAME_WITH_EXT [-n] host_user
17-
$SCRIPT_NAME_WITH_EXT -p forwarded_host_port
16+
$SCRIPT_NAME_WITH_EXT [-o OCI_profile] [-n] host_user
17+
$SCRIPT_NAME_WITH_EXT [-o OCI_profile] -p forwarded_host_port
1818
$SCRIPT_NAME_WITH_EXT -h: display this help
1919
2020
DESCRIPTION
@@ -23,7 +23,7 @@ DESCRIPTION
2323
host_user
2424
* Create a session on the bastion for the OCI host with the maximum possible duration (3 h.)
2525
26-
* Configure (add or update) host-specific \`ProxyJump\` directive parameter in the SSH config, which enables
26+
* Configure (append or update) host-specific \`ProxyJump\` directive parameter in the SSH config, which enables
2727
SSH/SFTP in all clients for the session duration.
2828
2929
* ssh as the specified user (unless -n is specified).
@@ -34,11 +34,15 @@ DESCRIPTION
3434
then the OCI host. The tunnel process would run until the session expires or the process is
3535
terminated by the user.
3636
37-
ENVIRONMENT
37+
-o OCI_profile use a specified profile section from the \`~/.oci/config\` file [default: \`DEFAULT\`]
3838
39-
* OCI CLI is required to be installed.
39+
ENVIRONMENT
4040
41-
* \`jq\` is required to be installed.
41+
* Required commands:
42+
* OCI CLI
43+
* \`jq\`
44+
* \`pcregrep\`
45+
* \`perl\`
4246
4347
* Required environment variables:
4448
* \`OCI_INSTANCE\`, Internal FQDN or Private IP e.g., \`kharkiv.subxxx.main.oraclevcn.com\`
@@ -48,106 +52,41 @@ ENVIRONMENT
4852
* One of the following SSH public keys in \`~/.ssh/\`: \`id_rsa.pub\`, \`id_dsa.pub\`, \`id_ecdsa.pub\`,
4953
\`id_ed25519.pub\`, or \`id_xmss.pub\`. If there are multiple keys the first one found in this order will be used.
5054
51-
Limitations for the \`host_user\` mode:
52-
1. There is only one OCI bastion session proxy jump host that is being configured in the SSH config.
53-
2. The OCI host is not yet configured in the SSH config before the first run of this script.
55+
* If the SSH config has global (\`Host *\`) \`ProxyJump\` parameter it would take precedence.
56+
Since the first obtained value for each parameter is used, more host-specific declarations should be given near the
57+
beginning of the file, and general defaults at the end. Prepend the following configuration manually in this case
58+
before using this script:
59+
\`\`\`
60+
Host {target}
61+
ProxyJump
62+
\`\`\`
5463
55-
v1.2.0 April 2023 Created by Dima Korobskiy
64+
v2.0.0 May 2023 Created by Dima Korobskiy
5665
Credits: George Chacko, Oracle
5766
HEREDOC
5867
exit 1
5968
}
6069

61-
########################################################################################################################
62-
# Update or insert a property value in a file. The inserted line could be appended or prepended.
63-
#
64-
# Arguments:
65-
# $1 file
66-
# $2 key: an ERE expression. It is matched as a line substring.
67-
# When key = `^` no matching is done, and the replacement line is *prepended*.
68-
# $3 (optional) property value line. All matching lines get replaced with this. Defaults to key.
69-
#
70-
# Returns:
71-
# None
72-
#
73-
# Examples:
74-
# upsert /etc/ssh/sshd_config 'IgnoreRhosts ' 'IgnoreRhosts yes'
75-
# upsert /etc/ssh/sshd_config '#*Banner ' 'Banner /etc/issue.net'
76-
# upsert /etc/logrotate.d/syslog ^ /var/log/cron
77-
# upsert ~/.ssh/config "Host ${OCI_INSTANCE}"
78-
#
79-
# Author: Dima Korobskiy
80-
# Credits: https://superuser.com/questions/590630/sed-how-to-replace-line-if-found-or-append-to-end-of-file-if-not-found
81-
########################################################################################################################
82-
upsert() {
83-
local file="$1"
84-
# Escape all `/` as `\/`
85-
local -r key="${2//\//\/}"
86-
local value="${3:-$2}"
87-
if [[ $3 ]]; then
88-
echo "\`${file}\` / \`${key}\` := \`${value}\`"
89-
else
90-
echo "\`${file}\` << \`${key}\`"
91-
fi
92-
93-
if [[ -s "$file" ]]; then
94-
# Escape all `/` as `\/`
95-
value="${value//\//\/}"
96-
97-
case $OSTYPE in
98-
darwin*) local sed_options=(-I '' -E) ;;
99-
linux*) local sed_options=(--in-place --regexp-extended) ;;
100-
esac
101-
102-
if [[ "$key" == "^" ]]; then
103-
# no matching is done and the replacement line is *prepended*
104-
105-
sed "${sed_options[@]}" "1i${value}" "$file"
106-
else
107-
# For each matching line (`/.../`), copy it to the hold space (`h`) then substitute the whole line with the
108-
# `value` (`s`). If the `key` is anchored (`^prefix`), the key line is still matched via `^.*${key}.*`: the second
109-
# anchor gets ignored.
110-
#
111-
# On the last line (`$`): exchange (`x`) hold space and pattern space then check if the latter is empty. If it's
112-
# empty, that means no match was found so replace the pattern space with the desired value (`s`) then append
113-
# (`H`) to the current line in the hold buffer. If it's not empty, it means the substitution was already made.
114-
#
115-
# Finally, exchange again (`x`).
116-
sed "${sed_options[@]}" "/${key}/{
117-
h
118-
s/^.*${key}.*/${value}/
119-
}
120-
\${
121-
x
122-
/^\$/{
123-
s//${value}/
124-
H
125-
}
126-
x
127-
}" "$file"
128-
fi
129-
else
130-
# No file or empty file
131-
echo -e "$value" >"$file"
132-
fi
133-
}
134-
13570
#declare -a ports
13671
# If a character is followed by a colon, the option is expected to have an argument
137-
while getopts p:nh OPT; do
72+
while getopts np:o:h OPT; do
13873
case "$OPT" in
74+
n)
75+
readonly SKIP_SSH=true
76+
;;
13977
p)
14078
port="$OPTARG"
14179
#ports+=("$OPTARG")
14280
;;
143-
n)
144-
readonly SKIP_SSH=true
81+
o)
82+
readonly PROFILE_OPT="--profile $OPTARG"
14583
;;
14684
*) # -h or `?`: an unknown option
14785
usage
14886
;;
14987
esac
15088
done
89+
echo -e "\n# \`$0 $*\`: run by \`${USER:-${USERNAME:-${LOGNAME:-UID #$UID}}}@${HOSTNAME}\`, in \`${PWD}\` #\n"
15190
shift $((OPTIND - 1))
15291

15392
# Process positional parameters
@@ -165,16 +104,24 @@ if ! command -v jq >/dev/null; then
165104
exit 1
166105
fi
167106

107+
if ! command -v pcregrep >/dev/null; then
108+
# shellcheck disable=SC2016
109+
echo >&2 'Please install PCRE'
110+
exit 1
111+
fi
112+
113+
if ! command -v perl > /dev/null; then
114+
echo "Please install Perl"
115+
exit 1
116+
fi
117+
168118
for required_env_var in 'OCI_INSTANCE' 'OCI_INSTANCE_OCID' 'OCI_BASTION_OCID'; do
169119
if [[ ! ${!required_env_var} ]]; then
170120
echo >&2 "Please define $required_env_var"
171121
exit 1
172122
fi
173123
done
174124

175-
# `${USER:-${USERNAME:-${LOGNAME}}}` might not be available inside Docker containers
176-
echo -e "\n# oci-bastion.sh: running under $(whoami)@${HOSTNAME} in ${PWD} #"
177-
178125
readonly MAX_TTL=$((3 * 60 * 60))
179126
readonly CHECK_INTERVAL_SEC=5
180127
# Intermittent `Permission denied (publickey)` errors might occur when trying to ssh immediately after session creation
@@ -201,12 +148,16 @@ if [[ $port ]]; then
201148
# `--session-ttl`: session duration in seconds (defaults to 30 minutes, maximum is 3 hours).
202149
# `--wait-interval-seconds`: state check interval (defaults to 30 seconds).
203150
# `--ssh-public-key-file` is required
204-
session_ocid=$(time oci bastion session create-port-forwarding --bastion-id "$OCI_BASTION_OCID" \
151+
# shellcheck disable=SC2086 # $PROFILE_OPT is a two-word CLI option
152+
session_ocid=$(time oci bastion session create-port-forwarding $PROFILE_OPT --bastion-id "$OCI_BASTION_OCID" \
205153
--target-resource-id "$OCI_INSTANCE_OCID" --target-private-ip "${OCI_INSTANCE}" --target-port "$port" \
206154
--session-ttl $MAX_TTL --ssh-public-key-file $SSH_PUB_KEY --wait-for-state SUCCEEDED --wait-for-state FAILED \
207155
--wait-interval-seconds $CHECK_INTERVAL_SEC | jq --raw-output '.data.resources[0].identifier')
208156
echo "Bastion Port Forwarding Session OCID=$session_ocid"
209-
ssh_command=$(oci bastion session get --session-id "$session_ocid" | jq --raw-output '.data["ssh-metadata"].command')
157+
158+
# shellcheck disable=SC2086 # $PROFILE_OPT is a two-word CLI option
159+
ssh_command=$(oci bastion session get $PROFILE_OPT --session-id "$session_ocid" \
160+
| jq --raw-output '.data["ssh-metadata"].command')
210161
# Result: `ssh -i <privateKey> -N -L <localPort>:{HOST_IP}:5432 -p 22 [email protected]`
211162
# Remove the placeholder
212163
ssh_command="${ssh_command/-i <privateKey>/}"
@@ -227,12 +178,16 @@ if [[ $HOST_USER ]]; then
227178
# `--session-ttl`: session duration in seconds (defaults to 30 minutes, maximum is 3 hours).
228179
# `--wait-interval-seconds`: state check interval (defaults to 30 seconds).
229180
# `--ssh-public-key-file` is required
230-
session_ocid=$(time oci bastion session create-managed-ssh --bastion-id "$OCI_BASTION_OCID" \
181+
# shellcheck disable=SC2086 # $PROFILE_OPT is a two-word CLI option
182+
session_ocid=$(time oci bastion session create-managed-ssh $PROFILE_OPT --bastion-id "$OCI_BASTION_OCID" \
231183
--target-resource-id "$OCI_INSTANCE_OCID" --target-os-username "$HOST_USER" --session-ttl $MAX_TTL \
232184
--ssh-public-key-file $SSH_PUB_KEY --wait-for-state SUCCEEDED --wait-for-state FAILED \
233185
--wait-interval-seconds $CHECK_INTERVAL_SEC | jq --raw-output '.data.resources[0].identifier')
234186
echo "Bastion Session OCID=$session_ocid"
235-
ssh_command=$(oci bastion session get --session-id "$session_ocid" | jq --raw-output '.data["ssh-metadata"].command')
187+
188+
# shellcheck disable=SC2086 # $PROFILE_OPT is a two-word CLI option
189+
ssh_command=$(oci bastion session get $PROFILE_OPT --session-id "$session_ocid" \
190+
| jq --raw-output '.data["ssh-metadata"].command')
236191
# Result: `ssh -i <privateKey> -o ProxyCommand=\"ssh -i <privateKey> -W %h:%p -p 22
237192
# [email protected]\" -p 22 {HOST_USER}@{HOST_IP}`
238193
# Extract the bastion session SSH destination: the `[email protected]` part
@@ -241,8 +196,23 @@ if [[ $HOST_USER ]]; then
241196
# Remove the string tail and reconstruct `[email protected]`
242197
bastion_session_dest="ocid1.bastionsession.${bastion_session_dest%%oraclecloud.com*}oraclecloud.com"
243198

244-
upsert ~/.ssh/config "Host ${OCI_INSTANCE}"
245-
upsert ~/.ssh/config ' ProxyJump ocid1.bastionsession.' " ProxyJump ${bastion_session_dest}"
199+
# Multi-line upsert
200+
if pcregrep -M -q "(?s)Host ${OCI_INSTANCE}.*?ProxyJump" ~/.ssh/config; then
201+
# Update
202+
203+
# -i input edited in-place
204+
# -p iterate over filename arguments
205+
# -0 use null as record separator
206+
# `@host` in the bastion session has to be escaped
207+
perl -i -p -0 -e "s/(Host ${OCI_INSTANCE}.*?)ProxyJump.*/\1ProxyJump ${bastion_session_dest//@/\\@}/s" ~/.ssh/config
208+
else
209+
# Append
210+
cat >>~/.ssh/config <<HEREDOC
211+
212+
Host ${OCI_INSTANCE}
213+
ProxyJump ${bastion_session_dest}
214+
HEREDOC
215+
fi
246216

247217
if [[ $SKIP_SSH ]]; then
248218
exit 0

0 commit comments

Comments
 (0)