1313
1414SYNOPSIS
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
2020DESCRIPTION
@@ -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
5665Credits: George Chacko, Oracle
5766HEREDOC
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
15088done
89+ echo -e " \n# \` $0 $* \` : run by \` ${USER:- ${USERNAME:- ${LOGNAME:- UID # $UID } } } @${HOSTNAME} \` , in \` ${PWD} \` #\n"
15190shift $(( OPTIND - 1 ))
15291
15392# Process positional parameters
@@ -165,16 +104,24 @@ if ! command -v jq >/dev/null; then
165104 exit 1
166105fi
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+
168118for 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
173123done
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-
178125readonly MAX_TTL=$(( 3 * 60 * 60 ))
179126readonly 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