diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0d1cdab3..bc923a26 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -25,9 +25,21 @@ workflows:
branches:
ignore: master
+waitforapt: &waitforapt
+ name: Remove cloud init lock
+ command: |
+ sudo rm -rf /var/lib/apt/lists/lock
+ while [ ! -f /var/lib/cloud/instance/boot-finished ]; do echo 'Waiting for cloud-init...'; sleep 10; done
+ while sudo fuser /var/lib/dpkg/lock >/dev/null 2>&1; do echo 'Waiting for autoupdates to complete...'; sleep 10; done
+ echo 'Waiting for instance to really be ready...'
+ sleep 30
+ sudo rm /var/lib/dpkg/lock && sudo dpkg --configure -a
+
+
install_spython: &install_spython
name: install spython
command: |
+ $HOME/conda/bin/pip uninstall spython --yes || echo "Not installed"
$HOME/conda/bin/python setup.py install
install_python_3: &install_python_3
@@ -57,13 +69,18 @@ install_python_2: &install_python_2
fi
test_spython: &test_spython
- name: Test Singularity Python
+ name: Test Singularity Python (Singularity Version 2 and 3)
command: |
cd ~/repo/spython
- ls
- export PATH=$PATH:/opt/circleci/.pyenv/shims
$HOME/conda/bin/python -m unittest tests.test_client
$HOME/conda/bin/python -m unittest tests.test_utils
+ $HOME/conda/bin/python -m unittest tests.test_instances
+
+test_spython_3: &test_spython_3
+ name: Test Singularity Python (Singularity Version 3 Only)
+ command: |
+ cd ~/repo/spython
+ $HOME/conda/bin/python -m unittest tests.test_oci
jobs:
@@ -76,6 +93,7 @@ jobs:
keys:
- v1-dependencies
- run: *install_python_3
+ - run: *waitforapt
- singularity/install-go:
go-version: 1.11.5
- singularity/debian-install-3:
@@ -86,6 +104,7 @@ jobs:
- /home/circleci/conda
key: v1-dependencies
- run: *test_spython
+ - run: *test_spython_3
test-singularity-3-python-2:
machine: true
@@ -96,6 +115,7 @@ jobs:
keys:
- v1-dependencies
- run: *install_python_2
+ - run: *waitforapt
- singularity/install-go:
go-version: 1.11.5
- singularity/debian-install-3:
@@ -106,6 +126,7 @@ jobs:
- /home/circleci/conda
key: v1-dependencies
- run: *test_spython
+ - run: *test_spython_3
test-singularity-2-python-3:
machine: true
@@ -116,6 +137,7 @@ jobs:
keys:
- v1-dependencies
- run: *install_python_3
+ - run: *waitforapt
- singularity/debian-install-2:
singularity-version: 2.6.1
- run: *install_spython
@@ -134,6 +156,7 @@ jobs:
keys:
- v1-dependencies
- run: *install_python_2
+ - run: *waitforapt
- singularity/debian-install-2:
singularity-version: 2.6.1
- run: *install_spython
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 151ae178..e6a317d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,8 +17,11 @@ The client here will eventually be released as "spython" (and eventually to
singularity on pypi), and the versions here will coincide with these releases.
## [master](https://github.com/singularityhub/singularity-cli/tree/master)
+ - Added support and tests for OCI image command group (0.0.54)
+ - client now has version() function to call get_singularity_version
+ - added return_result (boolean) to client run_command function.
- adding testing for 3.1.0 with Singularity Orbs (0.0.53)
- - inspect returns parsed json on success, or full message / return code otherwise
+ - inspect returns parsed json on success, or full message / return code otherwise
- instance stop all missing check for Singularity V3.+ (0.0.52)
- fixing bug with instances list, name not taken into account (0.0.51)
- additional of args to instance start commands (0.0.50)
@@ -27,7 +30,7 @@ singularity on pypi), and the versions here will coincide with these releases.
- adding support for instance list (0.0.47)
- ENV variables in Dockerfile can be empty (like unsetting) (0.0.46)
- COPY can handle multiple sources to one destination for Dockerfile parser (0.0.45)
- - Adding DockerRecipe, SingularityRecipe "load" action to load file
+ - Adding DockerRecipe, SingularityRecipe "load" action to load file
- issue #64 bug with hanging instances (0.0.44)
- flexible error printing given command to terminal fails (0.0.43)
- adding name_by_commit and name_by_hash to pull (0.0.42)
@@ -45,7 +48,7 @@ singularity on pypi), and the versions here will coincide with these releases.
- adding tests for client (0.0.30)
- bug in Dockerfile fromHeader variable fix (0.0.29)
- Dockerfile from "as level" removed (0.0.28)
- - fixed ENV parser to handle statements like A=B C=D
+ - fixed ENV parser to handle statements like A=B C=D
- adding ability to stream command executed to console (0.0.25)
- fixing import bug with recipe parsers in python 2.7 (0.0.24)
- addition of docker and singularity recipe parsers (0.0.22)
diff --git a/README.md b/README.md
index d06324cc..356d2f9f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Singularity Python
-Singularity Python (spython) is the Python API for working with Singularity containers. See
+Singularity Python (spython) is the Python API for working with Singularity containers. See
the [documentation](https://singularityhub.github.io/singularity-cli) for installation and usage.
We provide a [Singularity](Singularity) recipe for you to use if more convenient, along with the [full modules docstring](https://singularityhub.github.io/singularity-cli/api/source/spython.main.base.html#module-spython.main.base).
diff --git a/docs/_data/toc.yml b/docs/_data/toc.yml
index 0d4ee8b6..092029df 100644
--- a/docs/_data/toc.yml
+++ b/docs/_data/toc.yml
@@ -13,6 +13,9 @@
- title: "Instance Commands"
url: "/singularity-cli/commands-instances"
slug: instances
+ - title: "OCI Commands"
+ url: "/singularity-cli/commands-oci"
+ slug: oci
- title: "Python API Docstring"
url: "https://singularityhub.github.io/singularity-cli/api/source/spython.main.html"
slug: docstring
diff --git a/docs/api/_sources/changelog.md.txt b/docs/api/_sources/changelog.md.txt
index 0faa56f0..e6a317d2 100644
--- a/docs/api/_sources/changelog.md.txt
+++ b/docs/api/_sources/changelog.md.txt
@@ -17,12 +17,20 @@ The client here will eventually be released as "spython" (and eventually to
singularity on pypi), and the versions here will coincide with these releases.
## [master](https://github.com/singularityhub/singularity-cli/tree/master)
+ - Added support and tests for OCI image command group (0.0.54)
+ - client now has version() function to call get_singularity_version
+ - added return_result (boolean) to client run_command function.
+ - adding testing for 3.1.0 with Singularity Orbs (0.0.53)
+ - inspect returns parsed json on success, or full message / return code otherwise
+ - instance stop all missing check for Singularity V3.+ (0.0.52)
+ - fixing bug with instances list, name not taken into account (0.0.51)
+ - additional of args to instance start commands (0.0.50)
- continued lines should not be split in docker.py recipe parser (_setup) (0.0.49)
- COPY command should honor src src dest (and not reverse) (0.0.48)
- adding support for instance list (0.0.47)
- ENV variables in Dockerfile can be empty (like unsetting) (0.0.46)
- COPY can handle multiple sources to one destination for Dockerfile parser (0.0.45)
- - Adding DockerRecipe, SingularityRecipe "load" action to load file
+ - Adding DockerRecipe, SingularityRecipe "load" action to load file
- issue #64 bug with hanging instances (0.0.44)
- flexible error printing given command to terminal fails (0.0.43)
- adding name_by_commit and name_by_hash to pull (0.0.42)
@@ -40,7 +48,7 @@ singularity on pypi), and the versions here will coincide with these releases.
- adding tests for client (0.0.30)
- bug in Dockerfile fromHeader variable fix (0.0.29)
- Dockerfile from "as level" removed (0.0.28)
- - fixed ENV parser to handle statements like A=B C=D
+ - fixed ENV parser to handle statements like A=B C=D
- adding ability to stream command executed to console (0.0.25)
- fixing import bug with recipe parsers in python 2.7 (0.0.24)
- addition of docker and singularity recipe parsers (0.0.22)
diff --git a/docs/api/changelog.html b/docs/api/changelog.html
index 12a1ddd7..9bd1c25e 100644
--- a/docs/api/changelog.html
+++ b/docs/api/changelog.html
@@ -171,12 +171,26 @@
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.# This Source Code Form is subject to the terms of the# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -189,12 +189,13 @@
Source code for spython.instance.cmd.iutils
'''get is a list for a single instance. It is assumed to be running, and we need to look up the PID, etc. '''
- fromspython.utilsimport(check_install,get_singularity_version)
+ fromspython.utilsimportcheck_installcheck_install()# Ensure compatible for singularity prior to 3.0, and after 3.0subgroup="instance.list"
- ifget_singularity_version().find("version 3"):
+
+ if'version 3'inself.version():subgroup=["instance","list"]cmd=self._init_command(subgroup)
diff --git a/docs/api/modules/spython/instance/cmd/start.html b/docs/api/modules/spython/instance/cmd/start.html
index a4abeb07..c8c4a2a5 100644
--- a/docs/api/modules/spython/instance/cmd/start.html
+++ b/docs/api/modules/spython/instance/cmd/start.html
@@ -151,7 +151,7 @@
Source code for spython.instance.cmd.start
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.# This Source Code Form is subject to the terms of the# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -159,9 +159,8 @@
[docs]defstart(self,image=None,name=None,args=None,sudo=False,options=[],capture=False):'''start an instance. This is done by default when an instance is created. Parameters
@@ -170,6 +169,7 @@
Source code for spython.instance.cmd.start
name: a name for the instance sudo: if the user wants to run the command with sudo capture: capture output, default is False. With True likely to hang.
+ args: arguments to provide to the instance (supported Singularity 3.1+) options: a list of tuples, each an option to give to the start command [("--bind", "/tmp"),...]
@@ -178,8 +178,7 @@
Source code for spython.instance.cmd.start
'''fromspython.utilsimport(run_command,
- check_install,
- get_singularity_version)
+ check_install)check_install()# If no name provided, give it an excellent one!
@@ -192,14 +191,13 @@
Source code for spython.instance.cmd.start
# Not having this means it was called as a command, without an imageifnothasattr(self,"_image"):
- bot.error('Please provide an image, or create an Instance first.')
- sys.exit(1)
+ bot.exit('Please provide an image, or create an Instance first.')image=self._image# Derive subgroup command based on singularity versionsubgroup='instance.start'
- ifget_singularity_version().find("version 3"):
+ if'version 3'inself.version():subgroup=["instance","start"]cmd=self._init_command(subgroup)
@@ -211,8 +209,15 @@
Source code for spython.instance.cmd.start
# Assemble the command!cmd=cmd+options+[image,self.name]
+ # If arguments are provided
+ ifargs!=None:
+ ifnotisinstance(args,list):
+ args=[args]
+ cmd=cmd+args
+
# Save the options and cmd, if the user wants to see them laterself.options=options
+ self.args=argsself.cmd=cmdoutput=run_command(cmd,sudo=sudo,quiet=True,capture=capture)
diff --git a/docs/api/modules/spython/instance/cmd/stop.html b/docs/api/modules/spython/instance/cmd/stop.html
index 8f0db0a3..47a0f0f0 100644
--- a/docs/api/modules/spython/instance/cmd/stop.html
+++ b/docs/api/modules/spython/instance/cmd/stop.html
@@ -151,7 +151,7 @@
Source code for spython.instance.cmd.stop
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.# This Source Code Form is subject to the terms of the# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -159,29 +159,26 @@
Source code for spython.instance.cmd.stop
fromspython.loggerimportbot
-importsys
-
[docs]defstop(self,name=None,sudo=False):
- '''start an instance. This is done by default when an instance is created.
+ '''stop an instance. This is done by default when an instance is created. Parameters ==========
- image: optionally, an image uri (if called as a command from Client) name: a name for the instance sudo: if the user wants to run the command with sudo USAGE:
- singularity [...] instance.start [...] <container path> <instance name>
+ singularity [...] instance.stop [...] <instance name> '''fromspython.utilsimport(check_install,
- run_command,
- get_singularity_version)
+ run_command)check_install()subgroup='instance.stop'
- ifget_singularity_version().find("version 3"):
+
+ if'version 3'inself.version():subgroup=["instance","stop"]cmd=self._init_command(subgroup)
diff --git a/docs/api/modules/spython/logger/message.html b/docs/api/modules/spython/logger/message.html
index d56ea058..72306b6d 100644
--- a/docs/api/modules/spython/logger/message.html
+++ b/docs/api/modules/spython/logger/message.html
@@ -203,8 +203,9 @@
Source code for spython.logger.message
[docs]defuseColor(self):'''useColor will determine if color should be added
- to a print. Will check if being run in a terminal, and
- if has support for asci'''
+ to a print. Will check if being run in a terminal, and
+ if has support for ascii
+ '''COLORIZE=get_user_color_preference()ifCOLORIZEisnotNone:returnCOLORIZE
@@ -218,7 +219,8 @@
Source code for spython.logger.message
[docs]defaddColor(self,level,text):'''addColor to the prompt (usually prefix) if terminal
- supports, and specified to do so'''
+ supports, and specified to do so
+ '''ifself.colorize:iflevelinself.colors:text="%s%s%s"%(self.colors[level],
@@ -228,7 +230,8 @@
Source code for spython.logger.message
[docs]defemitError(self,level):'''determine if a level should print to
- stderr, includes all levels but INFO and QUIET'''
+ stderr, includes all levels but INFO and QUIET
+ '''iflevelin[ABORT,ERROR,WARNING,
@@ -390,6 +393,9 @@
def__repr__(self):returnself.__str__()
-
def__init__(self):'''the base client for singularity, will have commands added to it. upon init, store verbosity requested in environment MESSAGELEVEL. '''self._init_level()
-
[docs]defversion(self):
- '''return the version of singularity
+ '''a wrapped to get_singularity_version, takes no arguments. '''
-
- ifnotcheck_install():
- bot.warning("Singularity version not found, so it's likely not installed.")
- else:
- cmd=['singularity','--version']
- version=self._run_command(cmd).strip('\n')
- bot.debug("Singularity %s being used."%version)
- returnversion
-
+ returnget_singularity_version()
def_check_install(self):'''ensure that singularity is installed, and exit if not. '''ifcheck_install()isnotTrue:
- bot.error("Cannot find Singularity! Is it installed?")
- sys.exit(1)
+ bot.exit("Cannot find Singularity! Is it installed?")
[docs]defsend_command(self,cmd,sudo=False,stderr=None,stdout=None):
+ '''send command is a non interactive version of run_command, meaning
+ that we execute the command and return the return value, but don't
+ attempt to stream any content (text from the screen) back to the
+ user. This is useful for commands interacting with OCI bundles.
+
+ Parameters
+ ==========
+ cmd: the list of commands to send to the terminal
+ sudo: use sudo (or not)
+ '''
+
+ ifsudoisTrue:
+ cmd=['sudo']+cmd
+
+ process=subprocess.Popen(cmd,stderr=stderr,stdout=stdout)
+ result=process.communicate()
+ returnresult
[docs]defrun_command(self,cmd,sudo=False,capture=True):'''run_command is a wrapper for the global run_command, checking first for sudo and exiting on error if needed. The message is returned as a list of lines for the calling function to parse, and stdout uses
@@ -251,21 +275,27 @@
Source code for spython.main.base.command
========== cmd: the command to run sudo: does the command require sudo?
- On success, returns result. Otherwise, exists on error
+ quiet: if quiet set by function, overrides client setting.
+ return_result: return the result, if not successful (default False).
+ On success, returns result. '''
- result=run_cmd(cmd,sudo=sudo,capture=capture,quiet=self.quiet)
+ # First preference to function, then to client setting
+ ifquiet==None:
+ quiet=self.quiet
+
+ result=run_cmd(cmd,sudo=sudo,capture=capture,quiet=quiet)message=result['message']
- return_code=result['return_code']
+ # On success, return resultifresult['return_code']==0:
- iflen(message)==1:
- message=message[0]
- returnmessage
+ iflen(result['message'])==1:
+ result['message']=result['message'][0]
+ returnresult['message']
- ifself.quietisFalse:
- bot.error("Return Code %s: %s"%(return_code,
- message))
+ # For client (internal) calls, we want the return code
+ ifreturn_resultisTrue:
+ returnresult
cmd=self._init_command('build')
+ if'version 3'inself.version():
+ ext='sif'
+
# No image provided, default to use the client's loaded imageifrecipeisNone:recipe=self._get_uri()
@@ -221,8 +224,7 @@
Source code for spython.main.build
ifbuild_folderisnotNone:ifnotos.path.exists(build_folder):bot.exit('%s does not exist!'%build_folder)
-
- image="%s/%s"%(build_folder,image)
+ image=os.path.join(build_folder,image)# The user wants to run an isolated build
diff --git a/docs/api/modules/spython/main/execute.html b/docs/api/modules/spython/main/execute.html
index 12d6fc19..4021448d 100644
--- a/docs/api/modules/spython/main/execute.html
+++ b/docs/api/modules/spython/main/execute.html
@@ -234,7 +234,6 @@
Source code for spython.main.execute
returnself._run_command(cmd,sudo=sudo)returnstream_command(cmd,sudo=sudo)
-
bot.error('Please include a command (list) to execute.')
# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
-fromspython.loggerimportbot
-
-importrequests
-importshutil
-importsys
-importos
-
-
[docs]defhelp(self,command=None):'''help prints the general function help, or help for a specific command
diff --git a/docs/api/modules/spython/main/inspect.html b/docs/api/modules/spython/main/inspect.html
index 4efe2949..df316881 100644
--- a/docs/api/modules/spython/main/inspect.html
+++ b/docs/api/modules/spython/main/inspect.html
@@ -155,21 +155,25 @@
Source code for spython.main.inspect
# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+importjsonasjsonpfromspython.loggerimportbot
+fromspython.utilsimport(
+ check_install,
+ run_command
+)
-
[docs]definspect(self,image=None,json=True,app=None,quiet=True):'''inspect will show labels, defile, runscript, and tests for an image
-
Parameters ==========
- image_path: path of image to inspect
+ image: path of image to inspect json: print json instead of raw text (default True)
+ quiet: Don't print result to the screen (default True) app: if defined, return help in context of an app '''
- fromspython.utilsimportcheck_installcheck_install()# No image provided, default to use the client's loaded image
@@ -181,15 +185,60 @@
Source code for spython.main.inspect
cmd=cmd+['--app',app]options=['e','d','l','r','hf','t']
+
+ # After Singularity 3.0, helpfile was changed to H from
+
+ if"version 3"inself.version():
+ options=['e','d','l','r','H','t']
+
[cmd.append('-%s'%x)forxinoptions]ifjsonisTrue:cmd.append('--json')cmd.append(image)
- output=self._run_command(cmd)
- #self.println(output,quiet=self.quiet)
- returnoutput
[docs]definstances(self,name=None,return_json=False,quiet=False):'''list instances. For Singularity, this is provided as a command sub
@@ -184,7 +184,8 @@
cmd.append(name)output=run_command(cmd,quiet=True)
- instances=None
+ instances=[]# Success, we have instances
@@ -214,6 +215,12 @@
Source code for spython.main.instances
ifreturn_jsonisFalse:foriininstances:
+ # If the user has provided a name, only add instance matches
+ ifname!=None:
+ ifname!=i['daemon_name']:
+ continue
+
+ # Otherwise, add instances to the listingnew_instance=self.instance(pid=i['pid'],name=i['daemon_name'],image=i['container_image'],
@@ -232,7 +239,7 @@
Source code for spython.main.instances
bot.info('No instances found.')# If we are given a name, return just one
- ifnameisnotNoneandinstancesisnotNone:
+ ifname!=Noneandinstancesnotin[None,[]]:iflen(instances)==1:instances=instances[0]
@@ -252,7 +259,12 @@
cmd=self._init_command('pull')
+ # If Singularity version > 3.0, we have sif format
+ if'version 3'inself.version():
+ ext='sif'
+
# No image provided, default to use the client's loaded imageifimageisNone:image=self._get_uri()
@@ -224,7 +228,14 @@
+
+# Copyright (C) 2017-2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+fromspython.imageimportImageBase
+fromspython.loggerimportbot
+importos
+
+classOciImage(ImageBase):
+
+ # Default functions of client don't use sudo
+ sudo=False
+
+ def__init__(self,
+ container_id=None,
+ bundle=None,
+ create=True,
+ sudo=True,**kwargs):
+ ''' An Oci Image is an Image Base with OCI functions appended
+
+ Parameters
+ ==========
+ container_id: image uri to parse (required)
+ bundle: a bundle directory to create a container from.
+ the bundle should have a config.json at the root
+ create: if the bundle is provided, create a container (default True)
+ sudo: if init is called with or without sudo, keep a record and use
+ for following commands unless sudo is provided to function.
+ '''
+ super(ImageBase,self).__init__()
+
+ # Will typically be None, unless used outside of Client
+ self.container_id=container_id
+ self.uri='oci://'
+ self.sudo=sudo
+
+ # If bundle is provided, create it
+ ifbundle!=Noneandcontainer_id!=Noneandcreate:
+ self.bundle=bundle
+ self.create(bundle,container_id,**kwargs)
+
+# Unique resource identifier
+
+ defget_container_id(self,container_id=None):
+ ''' a helper function shared between functions that will return a
+ container_id. First preference goes to a container_id provided by
+ the user at runtime. Second preference goes to the container_id
+ instantiated with the client.
+
+ Parameters
+ ==========
+ container_id: image uri to parse (required)
+ '''
+
+ # The user must provide a container_id, or have one with the client
+ ifcontainer_id==Noneandself.container_id==None:
+ bot.exit('You must provide a container_id.')
+
+ # Choose whichever is not None, with preference for function provided
+ container_id=container_idorself.container_id
+ returncontainer_id
+
+
+ defget_uri(self):
+ '''return the image uri (oci://) along with it's name
+ '''
+ returnself.__str__()
+
+# Naming
+
+ def__str__(self):
+ ifself.container_id!=None:
+ return"[singularity-python-oci:%s]"%self.container_id
+ return"[singularity-python-oci]"
+
+ def__repr__(self):
+ returnself.__str__()
+
+
+# Commands
+
+ def_get_sudo(self,sudo=None):
+ '''if the client was initialized with sudo, remember this choice for
+ later communication with the Oci Images. However, if the user provides
+ a sudo argument (True or False) and not the default None, take
+ preference to this argument.
+
+ Parameters
+ ==========
+ sudo: if None, use self.sudo. Otherwise return sudo.
+ '''
+ ifsudo==None:
+ sudo=self.sudo
+ returnsudo
+
+
+ def_run_and_return(self,cmd,sudo=None):
+ ''' Run a command, show the message to the user if quiet isn't set,
+ and return the return code. This is a wrapper for the OCI client
+ to run a command and easily return the return code value (what
+ the user is ultimately interested in).
+
+ Parameters
+ ==========
+ cmd: the command (list) to run.
+ sudo: whether to add sudo or not.
+
+ '''
+ sudo=self._get_sudo(sudo)
+ result=self._run_command(cmd,
+ sudo=sudo,
+ quiet=True,
+ return_result=True)
+
+ # Successful return with no output
+ iflen(result)==0:
+ return
+
+ # Show the response to the user, only if not quiet.
+ elifnotself.quiet:
+ bot.print(result['message'][0])
+
+ # Return the state object to the user
+ returnresult['return_code']
+
+
+ def_init_command(self,action,flags=None):
+ ''' a wrapper to the base init_command, ensuring that "oci" is added
+ to each command
+
+ Parameters
+ ==========
+ action: the main action to perform (e.g., build)
+ flags: one or more additional flags (e.g, volumes)
+ not implemented yet.
+
+ '''
+ fromspython.main.base.commandimportinit_command
+ ifnotisinstance(action,list):
+ action=[action]
+ cmd=['oci']+action
+ returninit_command(self,cmd,flags)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/api/modules/spython/tests/test_client.html b/docs/api/modules/spython/tests/test_client.html
index a1322849..45394f16 100644
--- a/docs/api/modules/spython/tests/test_client.html
+++ b/docs/api/modules/spython/tests/test_client.html
@@ -179,10 +179,10 @@
[docs]defget_installdir():'''get_installdir returns the installation directory of the application '''
- returnos.path.abspath(os.path.join('..',os.path.dirname(__file__)))
run_command is a wrapper for the global run_command, checking first
for sudo and exiting on error if needed. The message is returned as
a list of lines for the calling function to parse, and stdout uses
@@ -238,7 +238,30 @@
Submodules
Parameters:
cmd (the command to run)
sudo (does the command require sudo?)
-
On success, returns result. Otherwise, exists on error
+
quiet (if quiet set by function, overrides client setting.)
+
return_result (return the result, if not successful (default False).)
send command is a non interactive version of run_command, meaning
+that we execute the command and return the return value, but don’t
+attempt to stream any content (text from the screen) back to the
+user. This is useful for commands interacting with OCI bundles.
+
+
+
+
+
Parameters:
+
cmd (the list of commands to send to the terminal)
diff --git a/docs/pages/commands-instances.md b/docs/pages/commands-instances.md
index d35ee921..24f53fa3 100644
--- a/docs/pages/commands-instances.md
+++ b/docs/pages/commands-instances.md
@@ -316,5 +316,5 @@ No instances found.
-
+
diff --git a/docs/pages/commands-oci.md b/docs/pages/commands-oci.md
new file mode 100644
index 00000000..c60296b8
--- /dev/null
+++ b/docs/pages/commands-oci.md
@@ -0,0 +1,301 @@
+---
+layout: default
+title: Oci Image Group Commands
+pdf: true
+permalink: /commands-oci
+toc: false
+---
+
+This section will discussion interaction with the OCI command group, new to
+Singularity 3.1.0. If you are using versions of Singularity before this,
+you won't be able to use these functions. If you want to learn more about OCI,
+see [opencontainers.org](https://opencontainers.org).
+
+ - [background](#background): some quick background on the OCI runtime spec
+ - [setup](#setup): create a bundle folder with a config.json to drive it
+ - [quick start](#quick-start): Quick start to create an OCI image object
+ - [create](#create): create an OCI image using the client
+
+The commands for state, kill, pause, resume, etc. will be discussed in context
+of the above.
+
+
+
+# Background
+
+To give you some quick background, the [runtime-spec](https://github.com/opencontainers/runtime-spec) of the open container initiative (OCI), you'll see that it defines a way
+to create a container from something called a "bundle"
+
+## What is a bundle?
+
+A bundle is just a folder on your computer with the contents of an operating system,
+libraries, and software, along with a configuration file (config.json). The
+configuration file conforms to this "runtime specification," and generally
+ describes permissions, binds, environment, and other runtime variables for the
+container.
+
+## Why do we have this specification?
+
+It might seem silly, but what this runtime specification is a standard set of
+terms for generating and interacting with containers. What does this mean in
+practice? We can have tools and infrastructure (for example, Kubernetes, a container
+orchestration system) that know how to interact with many different kinds of containers.
+How? Because they implement the OCI specification, and the communication is standardized.
+
+# Usage
+
+## Install
+
+If you haven't yet installed Singularity Python:
+
+```bash
+$ pip install spython
+```
+
+or see [the install docs](https://singularityhub.github.io/singularity-cli/install) for different variants of that.
+
+
+## Setup
+
+Now that you are familiar with the idea of a bundle, let's:
+
+ - create a folder with an operating system
+ - add a config.json template there
+ - create an OCI Image with the Singularity Python client
+
+We will do these steps before all of the "Create" examples below.
+First, open up an ipython shell. You can use `spython shell` to get one
+loaded with a client.
+
+```bash
+spython shell
+```
+
+Once in ipython, let's use Singularity build with sandbox to dump a busybox
+base operating system into a temporary folder. Singularity Python also
+will provide you with a dummy (limited) configuration file) to use to test.
+
+```python
+from spython.utils import get_installdir
+
+# Here is the dummy config.json for you!
+config = "%s/oci/config.json" % get_installdir()
+
+# Let's now build a bundle into /tmp/bundle
+image = client.build("docker://busybox:1.30.1",
+ image='/tmp/bundle',
+ sandbox=True,
+ sudo=False)
+```
+
+If you need to import the client on your own (if you don't use spython shell)
+
+```bash
+from spython.main import Client as client
+```
+
+Next, copy the config to your bundle folder.
+
+```python
+import shutil
+shutil.copyfile(config, '%s/config.json' % image)
+```
+
+Now that you have your bundle and config.json file, you can create an Oci Image
+from it.
+
+
+## Quick Start
+
+The quickest way to create a running Oci Image from a bundle is to
+instantiate an OciImage instance. If you didn't import the client, do
+that now:
+
+```python
+from spython.main import Client as client
+```
+
+Here is the OciImage object to instantiate! Notice that we are providing
+the full path to the bundle folder, along with an id for our container.
+
+```python
+$ image = client.oci.OciImage(bundle='/tmp/bundle',
+ container_id='figbars')
+[singularity-python-oci:figbars]
+```
+
+The variable "image" now holds a complete OciImage class (not attached to the client)
+and with "mycontainer" set as the container_id. If you are in ipython and press TAB,
+you will see all the expected functions.
+
+```bash
+attach() execute() mount() resume()
+ container_id get_container_id() OciImage RobotNamer
+ create() get_uri() parse_image_name() run()
+ delete() kill() remove_uri() run_command()
+```
+
+It's created off the bat! And further, the container id you defined is stored
+with the object, so you don't need to ask for it again.
+
+```python
+image.state()
+
+{'attachSocket': '/var/run/singularity/instances/root/figbars/attach.sock',
+ 'bundle': '/tmp/bundle',
+ 'controlSocket': '/var/run/singularity/instances/root/figbars/control.sock',
+ 'createdAt': 1551744400481853703,
+ 'id': 'figbars',
+ 'ociVersion': '1.0.1-dev',
+ 'pid': 20854,
+ 'status': 'created'}
+```
+
+Here is the stored container id:
+
+```python
+image.container_id
+'figbars'
+```
+
+And we can see that the default is that sudo is used to create it:
+
+```python
+image.sudo
+True
+```
+
+By default, if you don't provide a bundle directory it won't be created.
+
+```python
+$ image = client.oci.OciImage(container_id='figbars')
+```
+
+This would be a good way to get a handle for an (already created) image, or
+something you are otherwise not ready to create. You can also explicitly tell the function not to create the image.
+
+```python
+$ image = client.oci.OciImage(bundle='/tmp/bundle',
+ container_id='figbars',
+ create=False)
+```
+
+
+From this point on, you can interact with your image via this object. The
+commands that will be discussed below are available to you. Press TAB
+after typing "image." to see the options:
+
+```python
+In [39]: image.
+ attach() debug get_uri() parse_image_name()
+ bundle delete() kill() quiet
+ container_id execute() mount() remove_uri() >
+ create() get_container_id() OciImage resume()
+```
+
+Give everything a test! For example, try executing a command to your image:
+
+```python
+result = image.execute(command=["ls","/"])
+print(result)
+bin
+boot
+cdrom
+dev
+etc
+home
+initrd.img
+initrd.img.old
+lib
+lib32
+lib64
+lost+found
+media
+mnt
+opt
+proc
+root
+run
+sbin
+srv
+sys
+test
+tmp
+usr
+var
+vmlinuz
+vmlinuz.old
+```
+
+You can then clean up.
+
+```python
+image.pause()
+# or
+image.kill()
+image.delete()
+```
+
+
+## Create
+
+Let's discuss another option for create - one that also starts from our
+bundle folder, but instead just uses the client to interact with it (without directly creating an instance). Here is how to create it:
+
+```python
+metadata = client.oci.create(bundle=image, container_id='robot-man')
+```
+
+As of 3.1.0, the command above does require sudo, so it will prompt you for it
+unless you already have effective user id as 0. The return of the above will be a json object that describes metadata for the container, the same that we saw above
+when we asked for the `image.state()`.
+
+```python
+{'attachSocket': '/var/run/singularity/instances/root/robot-man/attach.sock',
+ 'bundle': '/tmp/bundle',
+ 'controlSocket': '/var/run/singularity/instances/root/robot-man/control.sock',
+ 'createdAt': 1551744049459537512,
+ 'id': 'robot-man',
+ 'ociVersion': '1.0.1-dev',
+ 'pid': 20602,
+ 'status': 'created'}
+```
+
+Notice that the status is "created." If you wanted to get this result again with
+the client:
+
+
+```bash
+$ client.oci.state('robot-man', sudo=True)
+
+{'attachSocket': '/var/run/singularity/instances/root/robot-man/attach.sock',
+ 'bundle': '/tmp/bundle',
+ 'controlSocket': '/var/run/singularity/instances/root/robot-man/control.sock',
+ 'createdAt': 1551744049459537512,
+ 'id': 'robot-man',
+ 'ociVersion': '1.0.1-dev',
+ 'pid': 20602,
+ 'status': 'created'}
+```
+
+*important* notice how we included sudo=True with the above - since we created
+the Oci Image with sudo, it lives in root's space. If we call without sudo, or
+ask for the state of a non existing container, we will get None for a result.
+
+```
+$ client.oci.state('doesntexist')
+
+# No sudo
+$ client.oci.state('mycontainer')
+```
+
+Both of the above return no result (None).
+
+We haven't gone through all of the examples, but if you would like a specific example
+please [let us know](https://www.github.com/singularityhub/singularity-cli/issues).
+
+
+
+
+
+
diff --git a/docs/pages/commands.md b/docs/pages/commands.md
index 7a8bfa78..55859a1f 100644
--- a/docs/pages/commands.md
+++ b/docs/pages/commands.md
@@ -9,9 +9,9 @@ toc: false
# Python API
This commands section primarily focuses on using Singularity Python from within Python and then in an interactive shell (for testing). We will first discuss the Python API, meaning functions that you can use in python to work with Singularity images or instances. Python is strong in the world of scientific programming, and so if you are reading these notes it's likely that you want to integrate Singularity containers into your Python applications. We wrote you a client to do that! Please select the type of command you are interested in below to continue!
-
- [Images](/singularity-cli/commands-images): base API to interact with containers:
- [Instances](/singularity-cli/commands-instances): interact with container instances.
+ - [OCI](/singularity-cli/commands-oci): interact with the OCI command group (3.1.0 and up)
diff --git a/spython/instance/cmd/iutils.py b/spython/instance/cmd/iutils.py
index f129f868..738acfb3 100644
--- a/spython/instance/cmd/iutils.py
+++ b/spython/instance/cmd/iutils.py
@@ -1,5 +1,5 @@
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.
# This Source Code Form is subject to the terms of the
# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -37,13 +37,13 @@ def get(self, name, return_json=False, quiet=False):
'''get is a list for a single instance. It is assumed to be running,
and we need to look up the PID, etc.
'''
- from spython.utils import ( check_install, get_singularity_version )
+ from spython.utils import check_install
check_install()
# Ensure compatible for singularity prior to 3.0, and after 3.0
subgroup = "instance.list"
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
subgroup = ["instance", "list"]
cmd = self._init_command(subgroup)
diff --git a/spython/instance/cmd/start.py b/spython/instance/cmd/start.py
index 975e935e..2c0e4260 100644
--- a/spython/instance/cmd/start.py
+++ b/spython/instance/cmd/start.py
@@ -1,5 +1,5 @@
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.
# This Source Code Form is subject to the terms of the
# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -7,7 +7,6 @@
from spython.logger import bot
-import sys
def start(self, image=None, name=None, args=None, sudo=False, options=[], capture=False):
'''start an instance. This is done by default when an instance is created.
@@ -27,8 +26,7 @@ def start(self, image=None, name=None, args=None, sudo=False, options=[], captur
'''
from spython.utils import ( run_command,
- check_install,
- get_singularity_version )
+ check_install )
check_install()
# If no name provided, give it an excellent one!
@@ -41,14 +39,13 @@ def start(self, image=None, name=None, args=None, sudo=False, options=[], captur
# Not having this means it was called as a command, without an image
if not hasattr(self, "_image"):
- bot.error('Please provide an image, or create an Instance first.')
- sys.exit(1)
+ bot.exit('Please provide an image, or create an Instance first.')
image = self._image
# Derive subgroup command based on singularity version
subgroup = 'instance.start'
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
subgroup = ["instance", "start"]
cmd = self._init_command(subgroup)
diff --git a/spython/instance/cmd/stop.py b/spython/instance/cmd/stop.py
index b06e34c7..cd3c8f3e 100644
--- a/spython/instance/cmd/stop.py
+++ b/spython/instance/cmd/stop.py
@@ -1,5 +1,5 @@
-# Copyright (C) 2017-2018 Vanessa Sochat.
+# Copyright (C) 2017-2019 Vanessa Sochat.
# This Source Code Form is subject to the terms of the
# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
@@ -7,7 +7,6 @@
from spython.logger import bot
-import sys
def stop(self, name=None, sudo=False):
'''stop an instance. This is done by default when an instance is created.
@@ -22,13 +21,12 @@ def stop(self, name=None, sudo=False):
'''
from spython.utils import ( check_install,
- run_command,
- get_singularity_version )
+ run_command )
check_install()
subgroup = 'instance.stop'
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
subgroup = ["instance", "stop"]
cmd = self._init_command(subgroup)
diff --git a/spython/logger/message.py b/spython/logger/message.py
index 3fe039fb..fbd8357a 100644
--- a/spython/logger/message.py
+++ b/spython/logger/message.py
@@ -55,8 +55,9 @@ def __init__(self, MESSAGELEVEL=None):
def useColor(self):
'''useColor will determine if color should be added
- to a print. Will check if being run in a terminal, and
- if has support for asci'''
+ to a print. Will check if being run in a terminal, and
+ if has support for ascii
+ '''
COLORIZE = get_user_color_preference()
if COLORIZE is not None:
return COLORIZE
@@ -70,7 +71,8 @@ def useColor(self):
def addColor(self, level, text):
'''addColor to the prompt (usually prefix) if terminal
- supports, and specified to do so'''
+ supports, and specified to do so
+ '''
if self.colorize:
if level in self.colors:
text = "%s%s%s" % (self.colors[level],
@@ -80,7 +82,8 @@ def addColor(self, level, text):
def emitError(self, level):
'''determine if a level should print to
- stderr, includes all levels but INFO and QUIET'''
+ stderr, includes all levels but INFO and QUIET
+ '''
if level in [ABORT,
ERROR,
WARNING,
@@ -242,6 +245,9 @@ def newline(self):
def verbose(self, message):
self.emit(VERBOSE, message, "VERBOSE")
+ def print(self, message):
+ print(message)
+
def verbose1(self, message):
self.emit(VERBOSE, message, "VERBOSE1")
diff --git a/spython/main/__init__.py b/spython/main/__init__.py
index 960d3359..25cd14cf 100644
--- a/spython/main/__init__.py
+++ b/spython/main/__init__.py
@@ -17,6 +17,7 @@ def get_client(quiet=False, debug=False):
debug: turn on debugging mode
'''
+ from spython.utils import get_singularity_version
from .base import Client
Client.quiet = quiet
@@ -50,6 +51,18 @@ def get_client(quiet=False, debug=False):
from spython.instance.cmd import generate_instance_commands # instance level commands
Client.instance = generate_instance_commands()
Client.instance_stopall = stopall
+ Client.instance.version = Client.version
+
+ # Commands Groups, OCI (Singularity version 3 and up)
+ if "version 3" in get_singularity_version():
+ from spython.oci.cmd import generate_oci_commands
+ Client.oci = generate_oci_commands()() # first () runs function, second
+ # initializes OciImage class
+ Client.oci.debug = Client.debug
+ Client.oci.quiet = Client.quiet
+ Client.oci.OciImage.quiet = Client.quiet
+ Client.oci.OciImage.debug = Client.debug
+
# Initialize
cli = Client()
@@ -58,7 +71,7 @@ def get_client(quiet=False, debug=False):
cli.image.debug = cli.debug
cli.image.quiet = cli.quiet
cli.instance.debug = cli.debug
- cli.instance.quiet = cli.quiet
+ cli.instance.quiet = cli.quiet
return cli
diff --git a/spython/main/base/__init__.py b/spython/main/base/__init__.py
index 7df29767..978d8d6c 100644
--- a/spython/main/base/__init__.py
+++ b/spython/main/base/__init__.py
@@ -7,7 +7,10 @@
from spython.logger import bot
-from spython.utils import check_install
+from spython.utils import (
+ check_install,
+ get_singularity_version
+)
import json
import sys
@@ -33,33 +36,22 @@ def __str__(self):
def __repr__(self):
return self.__str__()
-
def __init__(self):
'''the base client for singularity, will have commands added to it.
upon init, store verbosity requested in environment MESSAGELEVEL.
'''
self._init_level()
-
def version(self):
- '''return the version of singularity
+ '''a wrapped to get_singularity_version, takes no arguments.
'''
-
- if not check_install():
- bot.warning("Singularity version not found, so it's likely not installed.")
- else:
- cmd = ['singularity','--version']
- version = self._run_command(cmd).strip('\n')
- bot.debug("Singularity %s being used." % version)
- return version
-
+ return get_singularity_version()
def _check_install(self):
'''ensure that singularity is installed, and exit if not.
'''
if check_install() is not True:
- bot.error("Cannot find Singularity! Is it installed?")
- sys.exit(1)
+ bot.exit("Cannot find Singularity! Is it installed?")
diff --git a/spython/main/base/command.py b/spython/main/base/command.py
index 4a13e414..fb11a172 100644
--- a/spython/main/base/command.py
+++ b/spython/main/base/command.py
@@ -88,8 +88,32 @@ def generate_bind_list(self, bindlist=None):
return binds
+def send_command(self, cmd, sudo=False, stderr=None, stdout=None):
+ '''send command is a non interactive version of run_command, meaning
+ that we execute the command and return the return value, but don't
+ attempt to stream any content (text from the screen) back to the
+ user. This is useful for commands interacting with OCI bundles.
+
+ Parameters
+ ==========
+ cmd: the list of commands to send to the terminal
+ sudo: use sudo (or not)
+ '''
+
+ if sudo is True:
+ cmd = ['sudo'] + cmd
+
+ process = subprocess.Popen(cmd, stderr=stderr, stdout=stdout)
+ result = process.communicate()
+ return result
+
+
+def run_command(self, cmd,
+ sudo=False,
+ capture=True,
+ quiet=None,
+ return_result=False):
-def run_command(self, cmd, sudo=False, capture=True):
'''run_command is a wrapper for the global run_command, checking first
for sudo and exiting on error if needed. The message is returned as
a list of lines for the calling function to parse, and stdout uses
@@ -99,18 +123,24 @@ def run_command(self, cmd, sudo=False, capture=True):
==========
cmd: the command to run
sudo: does the command require sudo?
- On success, returns result. Otherwise, exists on error
+ quiet: if quiet set by function, overrides client setting.
+ return_result: return the result, if not successful (default False).
+ On success, returns result.
'''
- result = run_cmd(cmd, sudo=sudo, capture=capture, quiet=self.quiet)
+ # First preference to function, then to client setting
+ if quiet == None:
+ quiet = self.quiet
+
+ result = run_cmd(cmd, sudo=sudo, capture=capture, quiet=quiet)
message = result['message']
- return_code = result['return_code']
+ # On success, return result
if result['return_code'] == 0:
- if len(message) == 1:
- message = message[0]
- return message
+ if len(result['message']) == 1:
+ result['message'] = result['message'][0]
+ return result['message']
- if self.quiet is False:
- bot.error("Return Code %s: %s" %(return_code,
- message))
+ # For client (internal) calls, we want the return code
+ if return_result is True:
+ return result
diff --git a/spython/main/build.py b/spython/main/build.py
index e7ee5693..87a4f725 100644
--- a/spython/main/build.py
+++ b/spython/main/build.py
@@ -50,6 +50,9 @@ def build(self, recipe=None,
cmd = self._init_command('build')
+ if 'version 3' in self.version():
+ ext = 'sif'
+
# No image provided, default to use the client's loaded image
if recipe is None:
recipe = self._get_uri()
@@ -71,8 +74,7 @@ def build(self, recipe=None,
if build_folder is not None:
if not os.path.exists(build_folder):
bot.exit('%s does not exist!' % build_folder)
-
- image = "%s/%s" %(build_folder, image)
+ image = os.path.join(build_folder, image)
# The user wants to run an isolated build
diff --git a/spython/main/execute.py b/spython/main/execute.py
index 6ca7745a..80c61083 100644
--- a/spython/main/execute.py
+++ b/spython/main/execute.py
@@ -84,5 +84,4 @@ def execute(self,
return self._run_command(cmd,sudo=sudo)
return stream_command(cmd, sudo=sudo)
-
bot.error('Please include a command (list) to execute.')
diff --git a/spython/main/help.py b/spython/main/help.py
index e49cc1b1..20a676fd 100644
--- a/spython/main/help.py
+++ b/spython/main/help.py
@@ -6,14 +6,6 @@
# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
-from spython.logger import bot
-
-import requests
-import shutil
-import sys
-import os
-
-
def help(self, command=None):
'''help prints the general function help, or help for a specific command
diff --git a/spython/main/inspect.py b/spython/main/inspect.py
index 2a7c499b..796df504 100644
--- a/spython/main/inspect.py
+++ b/spython/main/inspect.py
@@ -10,7 +10,6 @@
from spython.logger import bot
from spython.utils import (
check_install,
- get_singularity_version,
run_command
)
@@ -39,7 +38,7 @@ def inspect(self, image=None, json=True, app=None, quiet=True):
# After Singularity 3.0, helpfile was changed to H from
- if "version 3" in get_singularity_version():
+ if "version 3" in self.version():
options = ['e','d','l','r','H','t']
[cmd.append('-%s' % x) for x in options]
diff --git a/spython/main/instances.py b/spython/main/instances.py
index 957a2f39..987a3d54 100644
--- a/spython/main/instances.py
+++ b/spython/main/instances.py
@@ -7,7 +7,7 @@
from spython.logger import bot
-from spython.utils import ( run_command, check_install, get_singularity_version )
+from spython.utils import ( run_command, check_install )
def instances(self, name=None, return_json=False, quiet=False):
'''list instances. For Singularity, this is provided as a command sub
@@ -35,7 +35,7 @@ def instances(self, name=None, return_json=False, quiet=False):
subgroup = 'instance.list'
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
subgroup = ["instance", "list"]
cmd = self._init_command(subgroup)
@@ -45,7 +45,7 @@ def instances(self, name=None, return_json=False, quiet=False):
cmd.append(name)
output = run_command(cmd, quiet=True)
- instances = None
+ instances = []
# Success, we have instances
@@ -111,7 +111,7 @@ def stopall(self, sudo=False, quiet=True):
subgroup = 'instance.stop'
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
subgroup = ["instance", "stop"]
cmd = self._init_command(subgroup)
diff --git a/spython/main/pull.py b/spython/main/pull.py
index af16ec9c..ce7a54a8 100644
--- a/spython/main/pull.py
+++ b/spython/main/pull.py
@@ -7,7 +7,7 @@
from spython.logger import bot
-from spython.utils import ( stream_command, get_singularity_version )
+from spython.utils import stream_command
import os
import re
import shutil
@@ -45,7 +45,7 @@ def pull(self,
cmd = self._init_command('pull')
# If Singularity version > 3.0, we have sif format
- if 'version 3' in get_singularity_version():
+ if 'version 3' in self.version():
ext = 'sif'
# No image provided, default to use the client's loaded image
@@ -81,7 +81,7 @@ def pull(self,
# Regression Singularity 3.* onward, PULLFOLDER not honored
# https://github.com/sylabs/singularity/issues/2788
- if pull_folder and 'version 3' in get_singularity_version():
+ if pull_folder and 'version 3' in self.version():
pull_folder_name = os.path.join(pull_folder, os.path.basename(name))
cmd = cmd + ["--name", pull_folder_name]
else:
diff --git a/spython/oci/README.md b/spython/oci/README.md
new file mode 100644
index 00000000..046efedb
--- /dev/null
+++ b/spython/oci/README.md
@@ -0,0 +1,31 @@
+# OCI Development
+
+Here I'll write how I created an OCI bundle using Singularity to help with
+development of the client. First, notice the [config.json](config.json)
+in the present working directory - it's a general configuration for OCI
+runtime specification (version 1.0) that we can put into a bundle (a folder
+that will serve as the root of a container). Here is how I did that.
+
+First, we are developing with the first release of Singularity that supports
+OCI:
+
+```bash
+$ singularity --version
+singularity version 3.1.0-rc2.28.ga72e427
+```
+
+Next, we are going to create a bundle. A bundle is a folder that we will
+treat as the root of our container filesystem. The easiest way to do
+this is to dump a filesystem there from another container.
+
+```bash
+$ singularity build --sandbox /tmp/bundle docker://ubuntu:18.04
+$ cp config.json /tmp/bundle
+```
+
+The purpose of the build is only to dump a complete operating system into the
+bundle folder. The configuration file then is to conform to the Oci
+Runtime specification.
+
+We can then test interaction with the OCI client of Singularity python
+by providing the bundle directory at /tmp/bundle.
diff --git a/spython/oci/__init__.py b/spython/oci/__init__.py
new file mode 100644
index 00000000..66939da1
--- /dev/null
+++ b/spython/oci/__init__.py
@@ -0,0 +1,145 @@
+
+# Copyright (C) 2017-2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from spython.image import ImageBase
+from spython.logger import bot
+import os
+
+class OciImage(ImageBase):
+
+ # Default functions of client don't use sudo
+ sudo = False
+
+ def __init__(self,
+ container_id=None,
+ bundle=None,
+ create=True,
+ sudo=True, **kwargs):
+ ''' An Oci Image is an Image Base with OCI functions appended
+
+ Parameters
+ ==========
+ container_id: image uri to parse (required)
+ bundle: a bundle directory to create a container from.
+ the bundle should have a config.json at the root
+ create: if the bundle is provided, create a container (default True)
+ sudo: if init is called with or without sudo, keep a record and use
+ for following commands unless sudo is provided to function.
+ '''
+ super(ImageBase, self).__init__()
+
+ # Will typically be None, unless used outside of Client
+ self.container_id = container_id
+ self.uri = 'oci://'
+ self.sudo = sudo
+
+ # If bundle is provided, create it
+ if bundle != None and container_id != None and create:
+ self.bundle = bundle
+ self.create(bundle, container_id, **kwargs)
+
+# Unique resource identifier
+
+ def get_container_id(self, container_id=None):
+ ''' a helper function shared between functions that will return a
+ container_id. First preference goes to a container_id provided by
+ the user at runtime. Second preference goes to the container_id
+ instantiated with the client.
+
+ Parameters
+ ==========
+ container_id: image uri to parse (required)
+ '''
+
+ # The user must provide a container_id, or have one with the client
+ if container_id == None and self.container_id == None:
+ bot.exit('You must provide a container_id.')
+
+ # Choose whichever is not None, with preference for function provided
+ container_id = container_id or self.container_id
+ return container_id
+
+
+ def get_uri(self):
+ '''return the image uri (oci://) along with it's name
+ '''
+ return self.__str__()
+
+# Naming
+
+ def __str__(self):
+ if self.container_id != None:
+ return "[singularity-python-oci:%s]" % self.container_id
+ return "[singularity-python-oci]"
+
+ def __repr__(self):
+ return self.__str__()
+
+
+# Commands
+
+ def _get_sudo(self, sudo=None):
+ '''if the client was initialized with sudo, remember this choice for
+ later communication with the Oci Images. However, if the user provides
+ a sudo argument (True or False) and not the default None, take
+ preference to this argument.
+
+ Parameters
+ ==========
+ sudo: if None, use self.sudo. Otherwise return sudo.
+ '''
+ if sudo == None:
+ sudo = self.sudo
+ return sudo
+
+
+ def _run_and_return(self, cmd, sudo=None):
+ ''' Run a command, show the message to the user if quiet isn't set,
+ and return the return code. This is a wrapper for the OCI client
+ to run a command and easily return the return code value (what
+ the user is ultimately interested in).
+
+ Parameters
+ ==========
+ cmd: the command (list) to run.
+ sudo: whether to add sudo or not.
+
+ '''
+ sudo = self._get_sudo(sudo)
+ result = self._run_command(cmd,
+ sudo=sudo,
+ quiet=True,
+ return_result=True)
+
+ # Successful return with no output
+ if len(result) == 0:
+ return
+
+ # Show the response to the user, only if not quiet.
+ elif not self.quiet:
+ bot.print(result['message'][0])
+
+ # Return the state object to the user
+ return result['return_code']
+
+
+ def _init_command(self, action, flags=None):
+ ''' a wrapper to the base init_command, ensuring that "oci" is added
+ to each command
+
+ Parameters
+ ==========
+ action: the main action to perform (e.g., build)
+ flags: one or more additional flags (e.g, volumes)
+ not implemented yet.
+
+ '''
+ from spython.main.base.command import init_command
+ if not isinstance(action, list):
+ action = [action]
+ cmd = ['oci'] + action
+ return init_command(self, cmd, flags)
diff --git a/spython/oci/cmd/__init__.py b/spython/oci/cmd/__init__.py
new file mode 100644
index 00000000..749c6061
--- /dev/null
+++ b/spython/oci/cmd/__init__.py
@@ -0,0 +1,49 @@
+
+# Copyright (C) 2017-2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+def generate_oci_commands():
+ ''' The oci command group will allow interaction with an image using
+ OCI commands.
+ '''
+ from spython.oci import OciImage
+
+ from spython.main.base.logger import println
+
+ # run_command uses run_cmd, but wraps to catch error
+ from spython.main.base.command import ( run_command, send_command )
+ from spython.main.base.generate import RobotNamer
+
+ # Oci Command Groups
+ from .mounts import ( mount, umount )
+ from .states import ( kill, state, start, pause, resume, _state_command )
+ from .actions import ( attach, create, delete, execute, run, _run, update )
+
+ # Oci Commands
+ OciImage.start = start
+ OciImage.mount = mount
+ OciImage.umount = umount
+ OciImage.state = state
+ OciImage.resume = resume
+ OciImage.pause = pause
+ OciImage.attach = attach
+ OciImage.create = create
+ OciImage.delete = delete
+ OciImage.execute = execute
+ OciImage.update = update
+ OciImage.kill = kill
+ OciImage.run = run
+ OciImage._run = _run
+ OciImage._state_command = _state_command
+
+ OciImage.RobotNamer = RobotNamer()
+ OciImage._send_command = send_command # send and disregard stderr, stdout
+ OciImage._run_command = run_command
+ OciImage._println = println
+ OciImage.OciImage = OciImage
+
+ return OciImage
diff --git a/spython/oci/cmd/actions.py b/spython/oci/cmd/actions.py
new file mode 100644
index 00000000..09f2a0f4
--- /dev/null
+++ b/spython/oci/cmd/actions.py
@@ -0,0 +1,265 @@
+
+# Copyright (C) 2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from spython.logger import bot
+from spython.utils import stream_command
+import os
+
+def run(self, bundle,
+ container_id=None,
+ log_path=None,
+ pid_file=None,
+ log_format="kubernetes"):
+
+ ''' run is a wrapper to create, start, attach, and delete a container.
+
+ Equivalent command line example:
+ singularity oci run -b ~/bundle mycontainer
+
+ Parameters
+ ==========
+ bundle: the full path to the bundle folder
+ container_id: an optional container_id. If not provided, use same
+ container_id used to generate OciImage instance
+ log_path: the path to store the log.
+ pid_file: specify the pid file path to use
+ log_format: defaults to kubernetes. Can also be "basic" or "json"
+ '''
+ return self._run(bundle,
+ container_id=container_id,
+ log_path=log_path,
+ pid_file=pid_file,
+ command="run",
+ log_format=log_format)
+
+
+def create(self, bundle,
+ container_id=None,
+ empty_process=False,
+ log_path=None,
+ pid_file=None,
+ sync_socket=None,
+ log_format="kubernetes"):
+
+ ''' use the client to create a container from a bundle directory. The bundle
+ directory should have a config.json. You must be the root user to
+ create a runtime.
+
+ Equivalent command line example:
+ singularity oci create [create options...]
+
+ Parameters
+ ==========
+ bundle: the full path to the bundle folder
+ container_id: an optional container_id. If not provided, use same
+ container_id used to generate OciImage instance
+ empty_process: run container without executing container process (for
+ example, for a pod container waiting for signals). This
+ is a specific use case for tools like Kubernetes
+ log_path: the path to store the log.
+ pid_file: specify the pid file path to use
+ sync_socket: the path to the unix socket for state synchronization.
+ log_format: defaults to kubernetes. Can also be "basic" or "json"
+ '''
+ return self._run(bundle,
+ container_id=container_id,
+ empty_process=empty_process,
+ log_path=log_path,
+ pid_file=pid_file,
+ sync_socket=sync_socket,
+ command="create",
+ log_format=log_format)
+
+
+def _run(self, bundle,
+ container_id=None,
+ empty_process=False,
+ log_path=None,
+ pid_file=None,
+ sync_socket=None,
+ command="run",
+ log_format="kubernetes"):
+
+ ''' _run is the base function for run and create, the only difference
+ between the two being that run does not have an option for sync_socket.
+
+ Equivalent command line example:
+ singularity oci create [create options...]
+
+ Parameters
+ ==========
+ bundle: the full path to the bundle folder
+ container_id: an optional container_id. If not provided, use same
+ container_id used to generate OciImage instance
+ empty_process: run container without executing container process (for
+ example, for a pod container waiting for signals). This
+ is a specific use case for tools like Kubernetes
+ log_path: the path to store the log.
+ pid_file: specify the pid file path to use
+ sync_socket: the path to the unix socket for state synchronization.
+ command: the command (run or create) to use (default is run)
+ log_format: defaults to kubernetes. Can also be "basic" or "json"
+ '''
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci create
+ cmd = self._init_command(command)
+
+ # Check that the bundle exists
+ if not os.path.exists(bundle):
+ bot.exit('Bundle not found at %s' % bundle)
+
+ # Add the bundle
+ cmd = cmd + ['--bundle', bundle]
+
+ # Additional Logging Files
+ cmd = cmd + ['--log-format', log_format]
+
+ if log_path != None:
+ cmd = cmd + ['--log-path', log_path]
+ if pid_file != None:
+ cmd = cmd + ['--pid-file', pid_file]
+ if sync_socket != None:
+ cmd = cmd + ['--sync-socket', sync_socket]
+ if empty_process:
+ cmd.append('--empty-process')
+
+ # Finally, add the container_id
+ cmd.append(container_id)
+
+ # Generate the instance
+ result = self._send_command(cmd, sudo=True)
+
+ # Get the status to report to the user!
+ # TODO: Singularity seems to create even with error, can we check and
+ # delete for the user if this happens?
+ return self.state(container_id, sudo=True, sync_socket=sync_socket)
+
+
+def delete(self, container_id=None, sudo=None):
+ '''delete an instance based on container_id.
+
+ Parameters
+ ==========
+ container_id: the container_id to delete
+ sudo: whether to issue the command with sudo (or not)
+ a container started with sudo will belong to the root user
+ If started by a user, the user needs to control deleting it
+ if the user doesn't set to True/False, we use client self.sudo
+
+ Returns
+ =======
+ return_code: the return code from the delete command. 0 indicates a
+ successful delete, 255 indicates not.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci delete
+ cmd = self._init_command('delete')
+
+ # Add the container_id
+ cmd.append(container_id)
+
+ # Delete the container, return code goes to user (message to screen)
+ return self._run_and_return(cmd, sudo=sudo)
+
+
+
+def attach(self, container_id=None, sudo=False):
+ '''attach to a container instance based on container_id
+
+ Parameters
+ ==========
+ container_id: the container_id to delete
+ sudo: whether to issue the command with sudo (or not)
+ a container started with sudo will belong to the root user
+ If started by a user, the user needs to control deleting it
+
+ Returns
+ =======
+ return_code: the return code from the delete command. 0 indicates a
+ successful delete, 255 indicates not.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci delete
+ cmd = self._init_command('attach')
+
+ # Add the container_id
+ cmd.append(container_id)
+
+ # Delete the container, return code goes to user (message to screen)
+ return self._run_and_return(cmd, sudo)
+
+
+def execute(self, command=None, container_id=None, sudo=False, stream=False):
+ '''execute a command to a container instance based on container_id
+
+ Parameters
+ ==========
+ container_id: the container_id to delete
+ command: the command to execute to the container
+ sudo: whether to issue the command with sudo (or not)
+ a container started with sudo will belong to the root user
+ If started by a user, the user needs to control deleting it
+ stream: if True, return an iterate to iterate over results of exec.
+ default is False, will return full output as string.
+
+ Returns
+ =======
+ return_code: the return code from the delete command. 0 indicates a
+ successful delete, 255 indicates not.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci delete
+ cmd = self._init_command('exec')
+
+ # Add the container_id
+ cmd.append(container_id)
+
+ if command != None:
+ if not isinstance(command, list):
+ command = [command]
+
+ cmd = cmd + command
+
+ # Execute the command, return response to user
+ if stream:
+ return stream_command(cmd, sudo=sudo)
+ return self._run_command(cmd, sudo=sudo, quiet=True)
+
+def update(self, container_id, from_file=None):
+ '''update container cgroup resources for a specific container_id,
+ The container must have state "running" or "created."
+
+ Singularity Example:
+ singularity oci update [update options...]
+ singularity oci update --from-file cgroups-update.json mycontainer
+
+ Parameters
+ ==========
+ container_id: the container_id to update cgroups for
+ from_file: a path to an OCI JSON resource file to update from.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci delete
+ cmd = self._init_command('update')
+
+ if from_file != None:
+ cmd = cmd + ['--from-file', from_file]
+
+ # Add the container_id
+ cmd.append(container_id)
+
+ # Delete the container, return code goes to user (message to screen)
+ return self._run_and_return(cmd, sudo)
diff --git a/spython/oci/cmd/mounts.py b/spython/oci/cmd/mounts.py
new file mode 100644
index 00000000..61fcd73c
--- /dev/null
+++ b/spython/oci/cmd/mounts.py
@@ -0,0 +1,31 @@
+
+# Copyright (C) 2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+from spython.logger import bot
+from .states import _state_command
+import sys
+
+
+def mount(self, image, sudo=None):
+ '''create an OCI bundle from SIF image
+
+ Parameters
+ ==========
+ image: the container (sif) to mount
+ '''
+ return self._state_command(image, command="mount", sudo=sudo)
+
+
+def umount(self, image):
+ '''delete an OCI bundle createdfrom SIF image
+
+ Parameters
+ ==========
+ image: the container (sif) to mount
+ '''
+ return self._state_command(image, command="umount", sudo=sudo)
diff --git a/spython/oci/cmd/states.py b/spython/oci/cmd/states.py
new file mode 100644
index 00000000..378ee264
--- /dev/null
+++ b/spython/oci/cmd/states.py
@@ -0,0 +1,177 @@
+
+# Copyright (C) 2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+from spython.logger import bot
+import json
+
+
+def state(self, container_id=None, sudo=None, sync_socket=None):
+
+ ''' get the state of an OciImage, if it exists. The optional states that
+ can be returned are created, running, stopped or (not existing).
+
+ Equivalent command line example:
+ singularity oci state
+
+ Parameters
+ ==========
+ container_id: the id to get the state of.
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+ sync_socket: the path to the unix socket for state synchronization
+
+ Returns
+ =======
+ state: a parsed json of the container state, if exists. If the
+ container is not found, None is returned.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci state
+ cmd = self._init_command('state')
+
+ if sync_socket != None:
+ cmd = cmd + ['--sync-socket', sync_socket]
+
+ # Finally, add the container_id
+ cmd.append(container_id)
+
+ # Get the instance state
+ result = self._run_command(cmd, sudo=sudo, quiet=True, return_result=True)
+
+ # If successful, a string is returned to parse
+ if isinstance(result, str):
+ return json.loads(result)
+
+
+def _state_command(self, container_id=None, command='start', sudo=None):
+
+ ''' A generic state command to wrap pause, resume, kill, etc., where the
+ only difference is the command. This function will be unwrapped if the
+ child functions get more complicated (with additional arguments).
+
+ Equivalent command line example:
+ singularity oci
+
+ Parameters
+ ==========
+ container_id: the id to start.
+ command: one of start, resume, pause, kill, defaults to start.
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+
+ Returns
+ =======
+ return_code: the return code to indicate if the container was started.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci state
+ cmd = self._init_command(command)
+
+ # Finally, add the container_id
+ cmd.append(container_id)
+
+ # Run the command, return return code
+ return self._run_and_return(cmd, sudo)
+
+
+
+def start(self, container_id=None, sudo=None):
+
+ ''' start a previously invoked OciImage, if it exists.
+
+ Equivalent command line example:
+ singularity oci start
+
+ Parameters
+ ==========
+ container_id: the id to start.
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+
+ Returns
+ =======
+ return_code: the return code to indicate if the container was started.
+ '''
+ return self._state_command(container_id, sudo=sudo)
+
+
+def kill(self, container_id=None, sudo=None, signal=None):
+
+ ''' stop (kill) a started OciImage container, if it exists
+
+ Equivalent command line example:
+ singularity oci kill
+
+ Parameters
+ ==========
+ container_id: the id to stop.
+ signal: signal sent to the container (default SIGTERM)
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+
+ Returns
+ =======
+ return_code: the return code to indicate if the container was killed.
+ '''
+ sudo = self._get_sudo(sudo)
+ container_id = self.get_container_id(container_id)
+
+ # singularity oci state
+ cmd = self._init_command('kill')
+
+ # Finally, add the container_id
+ cmd.append(container_id)
+
+ # Add the signal, if defined
+ if signal != None:
+ cmd = cmd + ['--signal', signal]
+
+ # Run the command, return return code
+ return self._run_and_return(cmd, sudo)
+
+
+def resume(self, container_id=None, sudo=None):
+ ''' resume a stopped OciImage container, if it exists
+
+ Equivalent command line example:
+ singularity oci resume
+
+ Parameters
+ ==========
+ container_id: the id to stop.
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+
+ Returns
+ =======
+ return_code: the return code to indicate if the container was resumed.
+ '''
+ return self._state_command(container_id, command='resume', sudo=sudo)
+
+
+def pause(self, container_id=None, sudo=None):
+ ''' pause a running OciImage container, if it exists
+
+ Equivalent command line example:
+ singularity oci pause
+
+ Parameters
+ ==========
+ container_id: the id to stop.
+ sudo: Add sudo to the command. If the container was created by root,
+ you need sudo to interact and get its state.
+
+ Returns
+ =======
+ return_code: the return code to indicate if the container was paused.
+ '''
+ return self._state_command(container_id, command='pause', sudo=sudo)
diff --git a/spython/oci/config.json b/spython/oci/config.json
new file mode 100644
index 00000000..486b2944
--- /dev/null
+++ b/spython/oci/config.json
@@ -0,0 +1,39 @@
+{
+ "ociVersion": "1.0.0",
+ "process": {
+ "terminal": true,
+ "user": {
+ "uid": 0,
+ "gid": 0
+ },
+ "args": [
+ "/bin/sh"
+ ],
+ "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
+ "TERM=xterm"
+ ],
+ "cwd": "/",
+ "noNewPrivileges": true
+ },
+ "root": {
+ "path": ".",
+ "readonly": true
+ },
+ "hostname": "runc",
+ "linux": {
+ "uidMappings": [
+ {
+ "hostID": 1000,
+ "containerID": 0,
+ "size": 1
+ }
+ ],
+ "gidMappings": [
+ {
+ "hostID": 1000,
+ "containerID": 0,
+ "size": 1
+ }
+ ]
+ }
+}
diff --git a/spython/tests/test_instances.py b/spython/tests/test_instances.py
index ba9a0991..d36c246c 100644
--- a/spython/tests/test_instances.py
+++ b/spython/tests/test_instances.py
@@ -21,7 +21,6 @@
class TestInstances(unittest.TestCase):
def setUp(self):
- self.pwd = get_installdir()
self.cli = Client
self.tmpdir = tempfile.mkdtemp()
@@ -31,15 +30,15 @@ def tearDown(self):
def test_instances(self):
print('Pulling testing container')
- image = self.cli.pull("shub://vsoch/singularity-images",
+ image = self.cli.pull("docker://busybox:1.30.1",
pull_folder=self.tmpdir)
self.assertTrue(os.path.exists(image))
- self.assertTrue('vsoch-singularity-images' in image)
+ self.assertTrue('busybox:1.30.1' in image)
print(image)
print("...Case 0: No instances: objects")
instances = self.cli.instances()
- self.assertEqual(instances, None)
+ self.assertEqual(instances, [])
print("...Case 1: Create instance")
myinstance = self.cli.instance(image)
@@ -55,19 +54,18 @@ def test_instances(self):
print("...Case 3: Commands to instances")
result = self.cli.execute(myinstance, ['echo', 'hello'])
self.assertTrue('hello\n' == result)
- result = self.cli.run(myinstance)
print("...Case 4: Stop instances")
myinstance.stop()
instances = self.cli.instances()
- self.assertEqual(instances, None)
+ self.assertEqual(instances, [])
myinstance1 = self.cli.instance(image)
myinstance2 = self.cli.instance(image)
instances = self.cli.instances()
self.assertEqual(len(instances), 2)
self.cli.instance_stopall()
instances = self.cli.instances()
- self.assertEqual(instances, None)
+ self.assertEqual(instances, [])
if __name__ == '__main__':
diff --git a/spython/tests/test_oci.py b/spython/tests/test_oci.py
new file mode 100644
index 00000000..3f06527e
--- /dev/null
+++ b/spython/tests/test_oci.py
@@ -0,0 +1,96 @@
+#!/usr/bin/python
+
+# Copyright (C) 2019 Vanessa Sochat.
+
+# This Source Code Form is subject to the terms of the
+# Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed
+# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from spython.utils import get_installdir
+from spython.main.base.generate import RobotNamer
+from spython.logger import bot
+from spython.main import Client
+import unittest
+import tempfile
+import shutil
+import json
+import os
+
+print("############################################################## test_oci")
+
+class TestOci(unittest.TestCase):
+
+ def setUp(self):
+ self.pwd = get_installdir()
+ self.cli = Client
+ self.tmpdir = tempfile.mkdtemp()
+ shutil.rmtree(self.tmpdir) # bundle will be created here
+ self.config = os.path.join(self.pwd, 'oci', 'config.json')
+ self.name = RobotNamer().generate()
+
+ def _build_sandbox(self):
+
+ print('Building testing sandbox')
+ image = self.cli.build("docker://busybox:1.30.1",
+ image=self.tmpdir,
+ sandbox=True,
+ sudo=False)
+
+ self.assertTrue(os.path.exists(image))
+
+ print('Copying OCI config.json to sandbox...')
+ shutil.copyfile(self.config, '%s/config.json' %image)
+ return image
+
+ def test_oci(self):
+
+ image = self._build_sandbox()
+
+ # A non existing process should not have a state
+ print('...Case 1. Check status of non-existing bundle.')
+ state = self.cli.oci.state('mycontainer')
+ self.assertEqual(state, None)
+
+ # This will use sudo
+ print("...Case 2: Create OCI image from bundle")
+ result = self.cli.oci.create(bundle=image,
+ container_id=self.name)
+
+ print(result)
+ self.assertEqual(result['status'], 'created')
+
+ print('...Case 3. Execute command to running bundle.')
+ result = self.cli.oci.execute(container_id=self.name,
+ sudo=True,
+ command=['ls','/'])
+
+ print(result)
+ self.assertTrue('bin' in result)
+
+ print('...Case 4. Check status of existing bundle.')
+ state = self.cli.oci.state(self.name, sudo=True)
+ self.assertEqual(state['status'], 'created')
+
+ print('...Case 5. Start container.')
+ state = self.cli.oci.start(self.name, sudo=True)
+ self.assertEqual(state, None)
+
+ print('...Case 6. Pause running container.')
+ state = self.cli.oci.pause(self.name, sudo=True)
+ self.assertEqual(state, None)
+
+ print('...Case 7. Resume paused container.')
+ state = self.cli.oci.resume(self.name, sudo=True)
+ self.assertEqual(state, None)
+
+ print('...Case 8. Kill should work with running container.')
+ state = self.cli.oci.kill(self.name, sudo=True)
+ self.assertTrue(state in [None, 255])
+
+ # Clean up the image (should still use sudo)
+ result = self.cli.oci.delete(self.name, sudo=True)
+ self.assertTrue(result in [None, 255])
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/spython/tests/test_utils.py b/spython/tests/test_utils.py
index 45a61692..7294e033 100644
--- a/spython/tests/test_utils.py
+++ b/spython/tests/test_utils.py
@@ -18,7 +18,6 @@
class TestUtils(unittest.TestCase):
def setUp(self):
- self.pwd = get_installdir()
self.tmpdir = tempfile.mkdtemp()
def tearDown(self):
@@ -91,7 +90,7 @@ def test_get_installdir(self):
from spython.utils import get_installdir
whereami = get_installdir()
print(whereami)
- self.assertTrue('spython' in whereami)
+ self.assertTrue(whereami.endswith('spython'))
def test_remove_uri(self):
diff --git a/spython/utils/terminal.py b/spython/utils/terminal.py
index d460f3a2..af88747c 100644
--- a/spython/utils/terminal.py
+++ b/spython/utils/terminal.py
@@ -69,7 +69,7 @@ def get_singularity_version():
def get_installdir():
'''get_installdir returns the installation directory of the application
'''
- return os.path.abspath(os.path.join('..', os.path.dirname(__file__)))
+ return os.path.abspath(os.path.dirname(os.path.dirname(__file__)))
def stream_command(cmd, no_newline_regexp="Progess", sudo=False):
diff --git a/spython/version.py b/spython/version.py
index 280ce035..ea618a61 100644
--- a/spython/version.py
+++ b/spython/version.py
@@ -6,7 +6,7 @@
# with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
-__version__ = "0.0.53"
+__version__ = "0.0.54"
AUTHOR = 'Vanessa Sochat'
AUTHOR_EMAIL = 'vsochat@stanford.edu'
NAME = 'spython'