We have created a Proof of Concept (POC) how to use the Python SDK to manage access to VM serial consoles in Oracle Cloud Infrastructure (OCI). This POC currently is implemented by creating an Oracle Secure Global Desktop (SGD) application that launches a python script on the SGD server.
In order for a user to access the Serial Console of a VM, valid credentials for the OCI API are required. Either the user goes to the Web UI and creates the SC and then uses the provided ssh command to connect, or does the same via oci-cli
Both ways will require the user to either specify the used ssh key as the default key in ${HOME}/.ssh/id_rsa or modify the command to include the required key, twice.
All a user needs is access to an SGD server configured with this POC. After authentication to the SGD server, the user is offered the typical workspace in the web browser with SGD applications the user is authorized to access
After launching the OCI Console SGD application (name is arbitrary) the user will be presented with a menu to choose the VM to connect to. Either a Serial Console Connection is being created on the fly, or an existing SC is being used and the appropriate ssh command is being launched
All the user needs to know are the credentials for SGD, no OCI API setup needs to be performed on the users system.
The easiest way to setup console access with the Python SDK is to install the OCI CLI by installing the rpm and some other packages to be able to more easily upgrade the SDK
In the POC I opted to store the configuration in /opt/tarantella/oci and changed the ownership that directory to ttasys:ttaserv after successful installation. Once the configuration file has been created in /opt/tarantella/oci/.oci we can test access the API with
Now we need to create our generic RSA key to be used for console connections and a directory where our python script stores some information. The generic RSA key is being used to create console connections in OCI. Rather than have the user upload his own public key, we have the script use the same key for any console connection, since we can assume that all connections to consoles will be managed via SGD and this script
Now, when the user gmelo authenticates to SGD, the workspace will present itself like in the screenshot above
The console-connection.py script
#!/bin/env python
#
# https://oracle-cloud-infrastructure-python-sdk.readthedocs.io/en/latest/api/index.html
#
import oci
import sys
import os
import getpass
import argparse
from time import sleep
import subprocess
PK = None
SLEEP_TIME = 2.0
arguments = None
compute = None
identity = None
OCI_CONFIG_ROOT = "/opt/tarantella/oci"
OCI_CONFIG_FILE = "%s/.oci/config" % OCI_CONFIG_ROOT
OCI_CONSOLE_KEY = "%s/.oci/oci_console_key" % OCI_CONFIG_ROOT
#
# read in our public key generated with ssh-keygen -N "" -q -f oci_console_key
#
def readPK():
pub = open("%s.pub" % OCI_CONSOLE_KEY,"r")
PK = pub.read()
if not PK:
print "no public key in %s" % OCI_CONSOLE_KEY
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
else:
return PK
def paginate(operation, *args, **kwargs):
while True:
try:
response = operation(*args, **kwargs)
except oci.exceptions.ServiceError as err:
print err.message
break
else:
for value in response.data:
yield value
kwargs["page"] = response.next_page
if not response.has_next_page:
break
# else:
# print "next page"
def list_compartments(config):
#
# create identity object
#
identity = oci.identity.IdentityClient(config)
#
# list compartments in root
#
i=1
compartment = []
for c in paginate(identity.list_compartments, compartment_id=config["tenancy"],limit=4):
if arguments.show_ocid:
print("%-3d %-8s %s [%s]" % (i, c.lifecycle_state,c.name, c.id))
else:
print("%-3d %-8s %s" % (i, c.lifecycle_state,c.name))
compartment.append(c)
i+=1
if arguments.list_compartments:
sys.exit(0)
else:
while True:
i = input("\nWhich compartment (pick number, 0 to exit): ")
if i <= len(compartment):
break
if i>0:
return compartment[i-1].id
else:
return False
def get_image_os(image_id):
result = compute.get_image(image_id)
os = "None"
if result.status and result.data and result.data.operating_system:
os = result.data.operating_system
return os
def get_instances():
instances=[]
for ins in paginate(compute.list_instances,compartment_id=arguments.compartment_id):
os = get_image_os(ins.image_id)
# we need to filter out Windows images
if ins.shape.startswith('VM.') and os != "Windows":
instances.append(ins)
if len(instances)>0:
print "\nPick one of the instances:\n"
i=1
for ins in instances:
if arguments.show_ocid:
print "\t%2i) %s (%s) [%s]" % (i,ins.display_name,ins.availability_domain,ins.id)
else:
print "\t%2i) %s (%s)" % (i,ins.display_name,ins.availability_domain)
i+=1
while True:
i = input("\nWhich instance (pick number, 0 to exit): ")
if i <= len(instances):
break
if i>0:
return instances[i-1].id
else:
return False
else:
return False
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Create Console Connection for OCI VM instance. Commandline options can be specified via environment variables where indicated')
parser.add_argument("--verbose","-v",action="store_true",help="add verbosity to the output, env VERBOSE ")
parser.add_argument("--nottasys","-n",action="store_true",default=False,help="do not check for ttasys [FALSE]")
parser.add_argument("--compartment_id","-c",help="specify compartment ID, env COMPARTMENT_ID")
parser.add_argument("--instance_id","-i",help="specify instance ID, env INSTANCE_ID")
parser.add_argument("--profile","-p",default="DEFAULT",help="specify config profile to use, env OCI_PROFILE")
parser.add_argument("--show_ocid","-s",action="store_true",help="show the OCID IDs, env SHOW_OCID")
parser.add_argument("--list_compartments","-l",action="store_true",help="list all compartments and exit")
arguments = parser.parse_args()
#
# for the next to work the following needs to be added to /etc/sudoers
#
# Cmnd_Alias OCI_CONSOLE=/opt/tarantella/oci/console-connection.py
# ALL ALL=(ttasys) NOPASSWD:SETENV: OCI_CONSOLE
#
if not arguments.nottasys and not getpass.getuser() == "ttasys":
# build arguments for execv
args = ["sudo","-Eu","ttasys" ]
args.extend(sys.argv)
os.execv("/bin/sudo",args)
#
# check for environment variables
#
if "COMPARTMENT_ID" in os.environ: arguments.compartment_id = os.environ["COMPARTMENT_ID"]
if "INSTANCE_ID" in os.environ: arguments.instance_id = os.environ["INSTANCE_ID"]
if "VERBOSE" in os.environ: arguments.verbose = os.environ["VERBOSE"]
if "SHOW_OCID" in os.environ: arguments.show_ocid = True
if "OCI_PROFILE" in os.environ: arguments.profile = os.environ["OCI_PROFILE"]
if "OCI_CONFIG_ROOT" in os.environ:
OCI_CONFIG_ROOT = os.environ["OCI_CONFIG_ROOT"]
OCI_CONFIG_FILE = "%s/.oci/config" % OCI_CONFIG_ROOT
OCI_CONSOLE_KEY = "%s/.oci/oci_console_key" % OCI_CONFIG_ROOT
if arguments.verbose: print "Running as %s\n" % getpass.getuser()
#
# get the required profile configuration
#
try:
config = oci.config.from_file(OCI_CONFIG_FILE, arguments.profile)
except oci.exceptions.ProfileNotFound as err:
print err
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
if arguments.list_compartments: list_compartments(config)
PK = readPK()
if not arguments.compartment_id:
arguments.compartment_id = list_compartments(config)
if not arguments.compartment_id:
print "\nERROR: you must supply a compartment ID"
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
#
# get compute object used in subsequent API calls
#
compute = oci.core.compute_client.ComputeClient(config)
if not arguments.instance_id: arguments.instance_id = get_instances()
if not arguments.instance_id:
print "\nERROR: you must supply an instance ID"
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
if arguments.verbose: print "get_instance(%s)" % arguments.instance_id
instance = compute.get_instance(arguments.instance_id)
if(instance.status):
instance = instance.data
else:
instance = None
print "The instance with ID %s does not exist" % arguments.instance_id
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
if not instance.shape.startswith("VM."):
print "Only VM shapes can have consoles, this instance is of shape %s" % instance.shape
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
console = None
#
# find active console
#
if arguments.verbose: print "list_instance_console_connections"
for c in paginate(compute.list_instance_console_connections,compartment_id=arguments.compartment_id,instance_id=arguments.instance_id):
if c.lifecycle_state == "ACTIVE" or c.lifecycle_state == "CREATING":
if arguments.verbose: print "%-10s %s" % (c.lifecycle_state, c.id)
console = c
#
# now check if the console we found already has a connect script. If it does continue
# otherwise we need to remove the console and create a new one, cause it most likely
# was one created manually and doesn't have our Public Key
#
if console and console.lifecycle_state == "ACTIVE":
if not os.path.exists("%s/info/connect.%s.sh" % (OCI_CONFIG_ROOT, console.id)):
#
# delete this console
#
if arguments.verbose: print "delete_instance_console_connection(%s)" % console.id
repsonse = compute.delete_instance_console_connection(console.id)
#
# now wait for this console to be deleted
#
while True:
response = compute.get_instance_console_connection(console.id)
if response and response.data and response.data.lifecycle_state == "DELETED":
break
print " ... waiting for console to be DELETED (%s)" % response.data.lifecycle_state
sleep(SLEEP_TIME)
console = None
if not console:
#
# we have no console, so create one
#
if arguments.verbose: print "no console for %s" % arguments.instance_id
cc = oci.core.models.CreateInstanceConsoleConnectionDetails()
cc.instance_id = arguments.instance_id
cc.public_key = PK
response = compute.create_instance_console_connection(cc)
if response.status:
console = response.data
#
# wait for console to become active
#
while console.lifecycle_state != "ACTIVE":
response = compute.get_instance_console_connection(console.id)
print " ... waiting for console connection to become active (%s)" % console.lifecycle_state
if response.status:
console = response.data
else:
#print dir(response)
break
sleep(SLEEP_TIME)
ssh_fn = "%s/info/connect.%s.sh" % (OCI_CONFIG_ROOT, console.id)
ssh_err = "%s/info/connect.%s.log" % (OCI_CONFIG_ROOT, console.id)
ProxyCommand="ProxyCommand='/usr/bin/ssh -e @ -i %s -o LogLevel=QUIET -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -W %%h:%%p -p 443 %s@instance-console.us-phoenix-1.oraclecloud.com'" % (OCI_CONSOLE_KEY,console.id)
if not os.path.exists(ssh_fn):
try:
ssh = open(ssh_fn, "w")
ssh.write("/usr/bin/ssh -e @ -i %s -o LogLevel=QUIET -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o %s %s 2>%s || cat %s\n" % (OCI_CONSOLE_KEY, ProxyCommand, arguments.instance_id, ssh_err, ssh_err))
ssh.write("echo '';echo 'Press Enter';read ans\n")
ssh.close()
#os.chmod(ssh_fn, 0666)
except IOError:
print "could not write %s" % ssh_fn
enter = raw_input("Hit Enter to continue")
sys.exit(-1)
print "\n---------------------------------------------------"
print "| launching ssh connection. terminate by typing @. |"
print "| hit return to see console login prompt. |"
print "----------------------------------------------------"
try:
os.execv("/bin/sh",["sh",ssh_fn])
except:
print "Failure to launch %s" % ssh_fn
enter = raw_input("Hit Enter to continue")
sys.exit(-1)