├── .gitignore ├── .pre-commit-config.yaml ├── CHANGELOG.rst ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── THIRD_PARTY_LICENSES.txt ├── buildrpm ├── .gitignore └── yo.spec ├── contrib └── yo.zsh ├── doc ├── Makefile ├── changelog.rst ├── cmds │ ├── cmd00.rst │ ├── cmd01.rst │ ├── cmd02.rst │ ├── cmd03.rst │ ├── cmd04.rst │ ├── cmd05.rst │ ├── cmd06.rst │ └── index.rst ├── conf.py ├── development.rst ├── guide │ ├── block.rst │ ├── concepts.rst │ ├── configuration.rst │ ├── index.rst │ ├── region.rst │ ├── tasks.rst │ ├── troubleshooting.rst │ └── vnc.rst ├── index.rst ├── install.rst ├── optional_setup.rst ├── yo.png └── yo.svg ├── requirements-dev.txt ├── sbom_generation.yaml ├── scripts ├── publish-docs.sh └── rebuild_docs.py ├── setup.cfg ├── setup.py ├── test-tasks ├── test-deps ├── test-existing-task ├── test-prereq ├── test-run-many └── test-task ├── tests ├── __init__.py ├── test_api.py ├── test_cmds.py ├── test_tasks.py └── testing │ ├── __init__.py │ ├── factories.py │ ├── fake_oci.py │ └── rich.py ├── tox.ini └── yo ├── __init__.py ├── __main__.py ├── api.py ├── data ├── pkgman ├── sample.yo.ini ├── yo-tasks │ ├── drgn │ └── ocid └── yo_tasklib.sh ├── main.py ├── oci.py ├── ssh.py ├── subc.py ├── tasks.py └── util.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | __pycache__ 3 | yo.egg-info 4 | build 5 | dist 6 | .tox 7 | .coverage 8 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.4.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | - repo: https://github.com/psf/black 12 | rev: "23.7.0" 13 | hooks: 14 | - id: black 15 | args: [--line-length=80, --exclude=setup.py] 16 | - repo: https://github.com/pycqa/flake8 17 | rev: "6.1.0" 18 | hooks: 19 | - id: flake8 20 | - repo: https://github.com/pre-commit/mirrors-mypy 21 | rev: "v1.4.1" 22 | hooks: 23 | - id: mypy 24 | args: [--strict, --disallow-untyped-calls] 25 | exclude: "(tests|doc|scripts)/.*" 26 | additional_dependencies: 27 | - types-setuptools 28 | - rich 29 | - subc>=0.8.0 30 | - repo: https://github.com/asottile/reorder_python_imports 31 | rev: v3.10.0 32 | hooks: 33 | - id: reorder-python-imports 34 | - repo: https://github.com/netromdk/vermin 35 | rev: v1.6.0 36 | hooks: 37 | - id: vermin 38 | args: ['-t=3.6-', '--violations', '--backport', 'dataclasses', '--eval-annotations'] 39 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to this repository 2 | 3 | We welcome your contributions! There are multiple ways to contribute. 4 | 5 | ## Opening issues 6 | 7 | For bugs or enhancement requests, please file a GitHub issue unless it's 8 | security related. When filing a bug remember that the better written the bug is, 9 | the more likely it is to be fixed. If you think you've found a security 10 | vulnerability, do not raise a GitHub issue and follow the instructions in our 11 | [security policy](./SECURITY.md). 12 | 13 | ## Contributing code 14 | 15 | We welcome your code contributions. Before submitting code via a pull request, 16 | you will need to have signed the [Oracle Contributor Agreement][OCA] (OCA) and 17 | your commits need to include the following line using the name and e-mail 18 | address you used to sign the OCA: 19 | 20 | ```text 21 | Signed-off-by: Your Name 22 | ``` 23 | 24 | This can be automatically added to pull requests by committing with `--sign-off` 25 | or `-s`, e.g. 26 | 27 | ```text 28 | git commit --signoff 29 | ``` 30 | 31 | Only pull requests from committers that can be verified as having signed the OCA 32 | can be accepted. 33 | 34 | ## Pull request process 35 | 36 | 1. Ensure there is an issue created to track and discuss the fix or enhancement 37 | you intend to submit. 38 | 1. Fork this repository. 39 | 1. Create a branch in your fork to implement the changes. We recommend using 40 | the issue number as part of your branch name, e.g. `1234-fixes`. 41 | 1. Ensure that any documentation is updated with the changes that are required 42 | by your change. 43 | 1. Ensure that any samples are updated if the base image has been changed. 44 | 1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly 45 | what your changes are meant to do and provide simple steps on how to validate. 46 | your changes. Ensure that you reference the issue you created as well. 47 | 1. We will assign the pull request to 2-3 people for review before it is merged. 48 | 49 | ## Code of conduct 50 | 51 | Follow the [Golden Rule](https://en.wikipedia.org/wiki/Golden_Rule). If you'd 52 | like more specific guidelines, see the [Contributor Covenant Code of Conduct][COC]. 53 | 54 | [OCA]: https://oca.opensource.oracle.com 55 | [COC]: https://www.contributor-covenant.org/version/1/4/code-of-conduct/ 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023, Oracle and/or its affiliates. 2 | 3 | The Universal Permissive License (UPL), Version 1.0 4 | 5 | Subject to the condition set forth below, permission is hereby granted to any 6 | person obtaining a copy of this software, associated documentation and/or data 7 | (collectively the "Software"), free of charge and under any and all copyright 8 | rights in the Software, and any and all patent rights owned or freely 9 | licensable by each licensor hereunder covering either (i) the unmodified 10 | Software as contributed to or provided by such licensor, or (ii) the Larger 11 | Works (as defined below), to deal in both 12 | 13 | (a) the Software, and 14 | (b) any piece of software and/or hardware listed in the 15 | lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | Work" to which the Software is contributed by such licensors), 17 | 18 | without restriction, including without limitation the rights to copy, create 19 | derivative works of, display, perform, and distribute the Software and make, 20 | use, sell, offer for sale, import, export, have made, and have sold the 21 | Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | either these or other terms. 23 | 24 | This license is subject to the following condition: 25 | The above copyright notice and either this complete permission notice or at 26 | a minimum a reference to the UPL must be included in all copies or 27 | substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | SOFTWARE. 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | VERSION=$(shell grep 'VERSION =' setup.py | sed s/\"//g | awk '{print($$3)}') 37 | 38 | PYTHON ?= python3 39 | pyver_maj = $(shell $(PYTHON) --version | cut -d. -f1 | sed 's/Python //') 40 | pyver_min = $(shell $(PYTHON) --version | cut -d. -f2) 41 | 42 | .PHONY: development 43 | development: 44 | @if [ $(pyver_maj) -ne 3 ] || [ $(pyver_min) -lt 6 ]; then \ 45 | echo error: Your Python $(pyver_maj).$(pyver_min), from command \"$(PYTHON)\", is not supported; \ 46 | echo Yo requires Python 3.6 or newer.; \ 47 | echo If you have another installed, try using make PYTHON=/path/to/python; \ 48 | exit 1; \ 49 | fi 50 | $(PYTHON) -m pip install --user --upgrade tox pre-commit 51 | $(PYTHON) -m pre_commit install --install-hooks 52 | 53 | .PHONY: test 54 | test: 55 | @$(PYTHON) -m tox 56 | 57 | .PHONY: docs 58 | docs: 59 | @$(PYTHON) scripts/rebuild_docs.py 60 | @$(PYTHON) -m tox -e docs 61 | @if [ ! -z "$$(git status docs --porcelain)" ]; then \ 62 | echo "note: The docs tree is unclean"; \ 63 | fi 64 | 65 | .PHONY: docs-publish 66 | docs-publish: docs 67 | @scripts/publish-docs.sh 68 | 69 | .PHONY: _release_sanity_check 70 | _release_sanity_check: 71 | @if [ "$$(git log --pretty='%s' -1)" != "Release v$(VERSION)" ]; then \ 72 | echo error: tip commit must be \"Release v$(VERSION)\"; \ 73 | exit 1; \ 74 | fi 75 | @if [ ! -z "$$(git status --porcelain)" ]; then \ 76 | echo error: Your git tree is unclean, please commit or stash it.; \ 77 | exit 1; \ 78 | fi 79 | @if [ "$$(git describe --tags --abbrev=0)" = "$(VERSION)" ]; then \ 80 | echo error: It looks like you have not bumped the version since last release.; \ 81 | exit 1; \ 82 | fi 83 | @if [ -z "$$(grep -P "^Version:\s+$(shell echo $(VERSION) | sed 's/\./\\./g')" buildrpm/yo.spec)" ]; then \ 84 | echo error: It looks like you have not updated buildrpm/yo.spec; \ 85 | exit 1; \ 86 | fi 2>/dev/null 87 | @if [ -z "$$(grep -Pzo $(shell echo $(VERSION) | sed 's/\./\\./g')"[^\n]+\n-+\n" CHANGELOG.rst)" ]; then \ 88 | echo error: It looks like you have not documented this release in CHANGELOG.rst; \ 89 | exit 1; \ 90 | fi 2>/dev/null 91 | @if [ -f dist/yo-$(VERSION).tar.gz ]; then \ 92 | echo error: There is already a built tarball: dist/yo-$(VERSION).tar.gz; \ 93 | echo Either verify you have bumped the version, or delete the; \ 94 | echo distributions you have built for $(VERSION); \ 95 | exit 1; \ 96 | fi 97 | 98 | .PHONY: prerelease 99 | prerelease: _release_sanity_check test 100 | @echo "Safe to release $(VERSION)" 101 | 102 | .PHONY: rpm 103 | rpm: 104 | git archive HEAD --format=tar.gz --prefix=yo-$(VERSION)/ -o buildrpm/v$(VERSION).tar.gz 105 | rpmbuild --define "_sourcedir $$(pwd)/buildrpm" \ 106 | --define "_topdir $$(pwd)/buildrpm/tmp" \ 107 | -ba buildrpm/yo.spec 108 | 109 | .PHONY: release 110 | release: _release_sanity_check test rpm 111 | @if [ ! $$(git symbolic-ref -q HEAD) = "refs/heads/main" ]; then \ 112 | echo error: You must be on main to release a new version.; \ 113 | exit 1; \ 114 | fi 115 | $(PYTHON) setup.py sdist 116 | $(PYTHON) setup.py bdist_wheel 117 | @echo "Built the following artifacts for yo $(VERSION):" 118 | @ls -l dist/yo-$(VERSION)* 119 | @echo "Point of no return: time to tag and upload this release" 120 | @echo -n "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ] 121 | @echo Confirmed 122 | twine upload -r yo dist/yo-$(VERSION)* 123 | twine upload -r oracle dist/yo-$(VERSION)* 124 | git push origin main 125 | git tag v$(VERSION) 126 | git push origin v$(VERSION) 127 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yo - fast and simple OCI client 2 | 3 | yo is a command-line client for managing OCI instances. It makes launching OCI 4 | instances as simple as rudely telling your computer "yo, launch an instance". 5 | Its goals are speed, ease of use, and simplicity. It was originally designed to 6 | help developers in the Oracle Linux team quickly launch disposable VMs for 7 | testing, but is slowly gaining users outside of OL. Here are some examples of 8 | how yo tries to improve on the OCI command line and browser tools: 9 | 10 | - yo hides other people's VMs from you, so you can just manage your own 11 | instances. 12 | - yo doesn't make you type any more than you need. Compartment IDs, common shape 13 | configurations, etc can all be stored in your config. It really can be as 14 | simple as `yo launch` (or, for the lazier, `yo la`). 15 | - yo lets you refer to your instances by name. You should never need to memorize 16 | an IP address again. 17 | - yo aggressively caches data to make operations as quick as possible. 18 | 19 | ## Installation 20 | 21 | A minimum of Python 3.6 is required in order to use Yo. 22 | 23 | **Via Pip:** 24 | 25 | pip install yo oci-cli 26 | 27 | This will install the standard OCI CLI alongside Yo, which can be useful as 28 | well. After installation, you'll need to configure Yo to work with your OCI 29 | tenancy. Please see the [documentation][] for detailed instructions. 30 | 31 | ## Documentation 32 | 33 | The [documentation][] contains information on the configuration file, as well as 34 | a listing of sub-commands and features offered. 35 | 36 | ## Examples 37 | 38 | ```bash 39 | # Launch an instance based on your default settings, and SSH into it 40 | yo launch -s 41 | 42 | # Launch a flexible instance with given shape, size, and name 43 | yo launch -S VM.Standard.E4.Flex --cpu 3 --mem 12 -n my-vm 44 | 45 | # SSH into my-vm 46 | yo ssh my-vm 47 | 48 | # Copy files to my-vm 49 | yo scp ./files my-vm: 50 | 51 | # Terminate my-vm 52 | yo terminate my-vm 53 | ``` 54 | 55 | ## Help 56 | 57 | We hope you can find all the answers to your questions in our documentation. But 58 | if you're still having trouble, feel free to open a Github issue and we'll try 59 | our best to help! 60 | 61 | ## Contributing 62 | 63 | We welcome contributions from the community. Before submitting a pull request, 64 | please [review our contribution guide][contributing]. 65 | 66 | ## Security 67 | 68 | Please consult the [security guide][security] for our responsible security 69 | vulnerability disclosure process. 70 | 71 | ## License 72 | 73 | Copyright (c) 2023 Oracle and/or its affiliates. 74 | 75 | Released under the Universal Permissive License v1.0 as shown at 76 | https://oss.oracle.com/licenses/upl/. 77 | 78 | [documentation]: https://oracle.github.io/yo/ 79 | [contributing]: ./CONTRIBUTING.md 80 | [security]: ./SECURITY.md 81 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting security vulnerabilities 2 | 3 | Oracle values the independent security research community and believes that 4 | responsible disclosure of security vulnerabilities helps us ensure the security 5 | and privacy of all our users. 6 | 7 | Please do NOT raise a GitHub Issue to report a security vulnerability. If you 8 | believe you have found a security vulnerability, please submit a report to 9 | [secalert_us@oracle.com][1] preferably with a proof of concept. Please review 10 | some additional information on [how to report security vulnerabilities to Oracle][2]. 11 | We encourage people who contact Oracle Security to use email encryption using 12 | [our encryption key][3]. 13 | 14 | We ask that you do not use other channels or contact the project maintainers 15 | directly. 16 | 17 | Non-vulnerability related security issues including ideas for new or improved 18 | security features are welcome on GitHub Issues. 19 | 20 | ## Security updates, alerts and bulletins 21 | 22 | Security updates will be released on a regular cadence. Many of our projects 23 | will typically release security fixes in conjunction with the 24 | Oracle Critical Patch Update program. Additional 25 | information, including past advisories, is available on our [security alerts][4] 26 | page. 27 | 28 | ## Security-related information 29 | 30 | We will provide security related information such as a threat model, considerations 31 | for secure use, or any known security issues in our documentation. Please note 32 | that labs and sample code are intended to demonstrate a concept and may not be 33 | sufficiently hardened for production use. 34 | 35 | [1]: mailto:secalert_us@oracle.com 36 | [2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html 37 | [3]: https://www.oracle.com/security-alerts/encryptionkey.html 38 | [4]: https://www.oracle.com/security-alerts/ 39 | -------------------------------------------------------------------------------- /buildrpm/.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | v*.tar.gz 3 | -------------------------------------------------------------------------------- /buildrpm/yo.spec: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 3 | Name: yo 4 | Version: 1.10.0 5 | Release: 0%{?dist} 6 | Summary: A fast and simple CLI client for managing OCI instances 7 | 8 | License: UPL 9 | URL: https://github.com/oracle/yo 10 | Source: https://github.com/oracle/yo/archive/refs/tags/v%{version}.tar.gz 11 | 12 | BuildArch: noarch 13 | BuildRequires: python3-devel 14 | BuildRequires: python3dist(sphinx) 15 | BuildRequires: python3dist(sphinx-argparse) 16 | BuildRequires: python3dist(pytest) 17 | 18 | %global _description %{expand: 19 | yo is a command-line client for managing OCI instances. It makes launching OCI 20 | instances as simple as rudely telling your computer "yo, launch an instance". 21 | Its goals are speed, ease of use, and simplicity. 22 | ...} 23 | 24 | %description %_description 25 | 26 | %prep 27 | %autosetup -p1 -n yo-%{version} 28 | echo -n "dnf" >yo/data/pkgman 29 | 30 | 31 | %build 32 | %pyproject_wheel 33 | 34 | 35 | %install 36 | %pyproject_install 37 | %pyproject_save_files yo 38 | sphinx-build --color -W -bhtml doc %{buildroot}/%{_docdir}/yo 39 | 40 | 41 | %check 42 | %pytest tests/ 43 | 44 | 45 | %files -n yo -f %{pyproject_files} 46 | %doc %{_docdir}/yo 47 | %doc README.* 48 | %{_bindir}/yo 49 | 50 | 51 | %changelog 52 | * Wed Apr 9 2025 Stephen Brennan - 1.10.0-0 53 | - Update to 1.10.0, see documentation for details 54 | 55 | * Wed Apr 9 2025 Stephen Brennan - 1.9.0-0 56 | - Initial packaging of 1.9.0 57 | -------------------------------------------------------------------------------- /doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /doc/changelog.rst: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.rst -------------------------------------------------------------------------------- /doc/cmds/cmd00.rst: -------------------------------------------------------------------------------- 1 | Basic Commands 2 | ============== 3 | 4 | .. _yo_launch: 5 | 6 | yo launch 7 | --------- 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_launch_args 12 | :prog: yo launch 13 | 14 | 15 | .. _yo_list: 16 | 17 | yo list 18 | ------- 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_list_args 23 | :prog: yo list 24 | 25 | 26 | .. _yo_ssh: 27 | 28 | yo ssh 29 | ------ 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_ssh_args 34 | :prog: yo ssh 35 | -------------------------------------------------------------------------------- /doc/cmds/cmd01.rst: -------------------------------------------------------------------------------- 1 | Instance Management 2 | =================== 3 | 4 | .. _yo_nmi: 5 | 6 | yo nmi 7 | ------ 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_nmi_args 12 | :prog: yo nmi 13 | 14 | 15 | .. _yo_protect: 16 | 17 | yo protect 18 | ---------- 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_protect_args 23 | :prog: yo protect 24 | 25 | 26 | .. _yo_reboot: 27 | 28 | yo reboot 29 | --------- 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_reboot_args 34 | :prog: yo reboot 35 | 36 | 37 | .. _yo_rebuild: 38 | 39 | yo rebuild 40 | ---------- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_rebuild_args 45 | :prog: yo rebuild 46 | 47 | 48 | .. _yo_rename: 49 | 50 | yo rename 51 | --------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_rename_args 56 | :prog: yo rename 57 | 58 | 59 | .. _yo_resize: 60 | 61 | yo resize 62 | --------- 63 | 64 | .. argparse:: 65 | :module: yo.main 66 | :func: cmd_resize_args 67 | :prog: yo resize 68 | 69 | 70 | .. _yo_start: 71 | 72 | yo start 73 | -------- 74 | 75 | .. argparse:: 76 | :module: yo.main 77 | :func: cmd_start_args 78 | :prog: yo start 79 | 80 | 81 | .. _yo_stop: 82 | 83 | yo stop 84 | ------- 85 | 86 | .. argparse:: 87 | :module: yo.main 88 | :func: cmd_stop_args 89 | :prog: yo stop 90 | 91 | 92 | .. _yo_teardown: 93 | 94 | yo teardown 95 | ----------- 96 | 97 | .. argparse:: 98 | :module: yo.main 99 | :func: cmd_teardown_args 100 | :prog: yo teardown 101 | 102 | 103 | .. _yo_terminate: 104 | 105 | yo terminate 106 | ------------ 107 | 108 | .. argparse:: 109 | :module: yo.main 110 | :func: cmd_terminate_args 111 | :prog: yo terminate 112 | 113 | 114 | .. _yo_wait: 115 | 116 | yo wait 117 | ------- 118 | 119 | .. argparse:: 120 | :module: yo.main 121 | :func: cmd_wait_args 122 | :prog: yo wait 123 | -------------------------------------------------------------------------------- /doc/cmds/cmd02.rst: -------------------------------------------------------------------------------- 1 | Instance Communication & Interaction 2 | ==================================== 3 | 4 | .. _yo_console: 5 | 6 | yo console 7 | ---------- 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_console_args 12 | :prog: yo console 13 | 14 | 15 | .. _yo_console_history: 16 | 17 | yo console-history 18 | ------------------ 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_console_history_args 23 | :prog: yo console-history 24 | 25 | 26 | .. _yo_copy_id: 27 | 28 | yo copy-id 29 | ---------- 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_copy_id_args 34 | :prog: yo copy-id 35 | 36 | 37 | .. _yo_ip: 38 | 39 | yo ip 40 | ----- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_ip_args 45 | :prog: yo ip 46 | 47 | 48 | .. _yo_mosh: 49 | 50 | yo mosh 51 | ------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_mosh_args 56 | :prog: yo mosh 57 | 58 | 59 | .. _yo_rdp: 60 | 61 | yo rdp 62 | ------ 63 | 64 | .. argparse:: 65 | :module: yo.main 66 | :func: cmd_rdp_args 67 | :prog: yo rdp 68 | 69 | 70 | .. _yo_rsync: 71 | 72 | yo rsync 73 | -------- 74 | 75 | .. argparse:: 76 | :module: yo.main 77 | :func: cmd_rsync_args 78 | :prog: yo rsync 79 | 80 | 81 | .. _yo_scp: 82 | 83 | yo scp 84 | ------ 85 | 86 | .. argparse:: 87 | :module: yo.main 88 | :func: cmd_scp_args 89 | :prog: yo scp 90 | 91 | 92 | .. _yo_vnc: 93 | 94 | yo vnc 95 | ------ 96 | 97 | .. argparse:: 98 | :module: yo.main 99 | :func: cmd_vnc_args 100 | :prog: yo vnc 101 | -------------------------------------------------------------------------------- /doc/cmds/cmd03.rst: -------------------------------------------------------------------------------- 1 | Task Management Commands 2 | ======================== 3 | 4 | .. _yo_task_info: 5 | 6 | yo task info 7 | ------------ 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_task_info_args 12 | :prog: yo task info 13 | 14 | 15 | .. _yo_task_join: 16 | 17 | yo task join 18 | ------------ 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_task_join_args 23 | :prog: yo task join 24 | 25 | 26 | .. _yo_task_list: 27 | 28 | yo task list 29 | ------------ 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_task_list_args 34 | :prog: yo task list 35 | 36 | 37 | .. _yo_task_run: 38 | 39 | yo task run 40 | ----------- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_task_run_args 45 | :prog: yo task run 46 | 47 | 48 | .. _yo_task_status: 49 | 50 | yo task status 51 | -------------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_task_status_args 56 | :prog: yo task status 57 | 58 | 59 | .. _yo_task_wait: 60 | 61 | yo task wait 62 | ------------ 63 | 64 | .. argparse:: 65 | :module: yo.main 66 | :func: cmd_task_wait_args 67 | :prog: yo task wait 68 | -------------------------------------------------------------------------------- /doc/cmds/cmd04.rst: -------------------------------------------------------------------------------- 1 | Volume Management Commands 2 | ========================== 3 | 4 | .. _yo_volume_attach: 5 | 6 | yo volume attach 7 | ---------------- 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_volume_attach_args 12 | :prog: yo volume attach 13 | 14 | 15 | .. _yo_volume_attached: 16 | 17 | yo volume attached 18 | ------------------ 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_volume_attached_args 23 | :prog: yo volume attached 24 | 25 | 26 | .. _yo_volume_create: 27 | 28 | yo volume create 29 | ---------------- 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_volume_create_args 34 | :prog: yo volume create 35 | 36 | 37 | .. _yo_volume_delete: 38 | 39 | yo volume delete 40 | ---------------- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_volume_delete_args 45 | :prog: yo volume delete 46 | 47 | 48 | .. _yo_volume_detach: 49 | 50 | yo volume detach 51 | ---------------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_volume_detach_args 56 | :prog: yo volume detach 57 | 58 | 59 | .. _yo_volume_list: 60 | 61 | yo volume list 62 | -------------- 63 | 64 | .. argparse:: 65 | :module: yo.main 66 | :func: cmd_volume_list_args 67 | :prog: yo volume list 68 | 69 | 70 | .. _yo_volume_rename: 71 | 72 | yo volume rename 73 | ---------------- 74 | 75 | .. argparse:: 76 | :module: yo.main 77 | :func: cmd_volume_rename_args 78 | :prog: yo volume rename 79 | -------------------------------------------------------------------------------- /doc/cmds/cmd05.rst: -------------------------------------------------------------------------------- 1 | Informative Commands 2 | ==================== 3 | 4 | .. _yo_compat: 5 | 6 | yo compat 7 | --------- 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_compat_args 12 | :prog: yo compat 13 | 14 | 15 | .. _yo_images: 16 | 17 | yo images 18 | --------- 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_images_args 23 | :prog: yo images 24 | 25 | 26 | .. _yo_limits: 27 | 28 | yo limits 29 | --------- 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_limits_args 34 | :prog: yo limits 35 | 36 | 37 | .. _yo_os: 38 | 39 | yo os 40 | ----- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_os_args 45 | :prog: yo os 46 | 47 | 48 | .. _yo_shape: 49 | 50 | yo shape 51 | -------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_shape_args 56 | :prog: yo shape 57 | 58 | 59 | .. _yo_shapes: 60 | 61 | yo shapes 62 | --------- 63 | 64 | .. argparse:: 65 | :module: yo.main 66 | :func: cmd_shapes_args 67 | :prog: yo shapes 68 | -------------------------------------------------------------------------------- /doc/cmds/cmd06.rst: -------------------------------------------------------------------------------- 1 | Diagnostic Commands 2 | =================== 3 | 4 | .. _yo_cache_clean: 5 | 6 | yo cache-clean 7 | -------------- 8 | 9 | .. argparse:: 10 | :module: yo.main 11 | :func: cmd_cache_clean_args 12 | :prog: yo cache-clean 13 | 14 | 15 | .. _yo_debug: 16 | 17 | yo debug 18 | -------- 19 | 20 | .. argparse:: 21 | :module: yo.main 22 | :func: cmd_debug_args 23 | :prog: yo debug 24 | 25 | 26 | .. _yo_help: 27 | 28 | yo help 29 | ------- 30 | 31 | .. argparse:: 32 | :module: yo.main 33 | :func: cmd_help_args 34 | :prog: yo help 35 | 36 | 37 | .. _yo_script: 38 | 39 | yo script 40 | --------- 41 | 42 | .. argparse:: 43 | :module: yo.main 44 | :func: cmd_script_args 45 | :prog: yo script 46 | 47 | 48 | .. _yo_version: 49 | 50 | yo version 51 | ---------- 52 | 53 | .. argparse:: 54 | :module: yo.main 55 | :func: cmd_version_args 56 | :prog: yo version 57 | -------------------------------------------------------------------------------- /doc/cmds/index.rst: -------------------------------------------------------------------------------- 1 | Commands 2 | ======== 3 | 4 | Yo has an extensive list of commands. This section organizes and presents the 5 | full listing and arguments for each command. 6 | 7 | When invoking a command, the normal way is ``yo [sub-command]``. However, Yo 8 | also allows you to use the shortest unambiguous prefix as a shortcut for a 9 | particular command-name. For example, ``yo la`` could be used as shorthand for 10 | ``yo launch``. However, as commands are added, these aliases may stop working. 11 | For example, once upon a time, ``yo li`` could be used as a shorthand for ``yo 12 | list``. But in version 0.23.0 of Yo, the ``yo limits`` command was added, and 13 | the shorthand no longer worked. 14 | 15 | So, you may instead create an ``[aliases]`` section in your ``~/.oci/yo.ini`` 16 | file. This will disable the shortest-prefix aliasing, and allow you full control 17 | over the aliases. See the configuration documentation for more information. 18 | 19 | Overview 20 | -------- 21 | 22 | Basic Commands: 23 | 24 | - :ref:`yo_launch` - Launch an OCI instance. 25 | - :ref:`yo_list` - List your OCI instances. 26 | - :ref:`yo_ssh` - SSH into an instance. 27 | 28 | Instance Management: 29 | 30 | - :ref:`yo_nmi` - Send diagnostic interrupt (NMI) to one or more instance (dangerous) 31 | - :ref:`yo_protect` - Enable or disable Yo's termination protection. 32 | - :ref:`yo_reboot` - Reboot one or more OCI instances. 33 | - :ref:`yo_rebuild` - Rebuild a saved & torn down instance. 34 | - :ref:`yo_rename` - Give an instance a new name. 35 | - :ref:`yo_resize` - Resize (change shape) and reboot an OCI instance. 36 | - :ref:`yo_start` - Start (boot up) one or more OCI instances. 37 | - :ref:`yo_stop` - Stop (shut down) one or more OCI instances 38 | - :ref:`yo_teardown` - Save block volume and instance metadata, then terminate. 39 | - :ref:`yo_terminate` - Terminate one or more instances. 40 | - :ref:`yo_wait` - Wait for an instance to enter a state. 41 | 42 | Instance Communication & Interaction: 43 | 44 | - :ref:`yo_console` - View an instance's serial console using an SSH connection 45 | - :ref:`yo_console_history` - Fetch and print serial console history for an instance. 46 | - :ref:`yo_copy_id` - Copy an SSH public key onto an instance using ssh-copy-id. 47 | - :ref:`yo_ip` - Print the IP address for one or more instances. 48 | - :ref:`yo_mosh` - Connect to the instance via mosh. 49 | - :ref:`yo_rdp` - Connect to instance remote desktop using RDP. 50 | - :ref:`yo_rsync` - Synchronize files using the rsync command. 51 | - :ref:`yo_scp` - Copy files to/from an instance using the scp command 52 | - :ref:`yo_vnc` - Connect to instance remote desktop using VNC. 53 | 54 | Task Management Commands: 55 | 56 | - :ref:`yo_task_info` - Show the basic information and script contents for a task. 57 | - :ref:`yo_task_join` - Wait for all tasks on a given instance to complete. 58 | - :ref:`yo_task_list` - List every task and its basic metadata 59 | - :ref:`yo_task_run` - Run a long-running task script on an instance. 60 | - :ref:`yo_task_status` - Report the status of all tasks on an instance. 61 | - :ref:`yo_task_wait` - Wait for a task to complete on an instance. 62 | 63 | Volume Management Commands: 64 | 65 | - :ref:`yo_volume_attach` - Attach a block or boot volume to an instance. 66 | - :ref:`yo_volume_attached` - List volumes by their current instance attachment. 67 | - :ref:`yo_volume_create` - Create a block volume. 68 | - :ref:`yo_volume_delete` - Delete a block or boot volume. 69 | - :ref:`yo_volume_detach` - Detach a block or boot volume from an instance. 70 | - :ref:`yo_volume_list` - List block & boot volumes. 71 | - :ref:`yo_volume_rename` - Rename a block or boot volume. 72 | 73 | Informative Commands: 74 | 75 | - :ref:`yo_compat` - Show a compatibility matrix of images and shapes. 76 | - :ref:`yo_images` - List images available to use for launching an instance. 77 | - :ref:`yo_limits` - Display your tenancy & region's service limits. 78 | - :ref:`yo_os` - List official OS and version combinations. 79 | - :ref:`yo_shape` - Get info about a single shape. 80 | - :ref:`yo_shapes` - List instance shape options. 81 | 82 | Diagnostic Commands: 83 | 84 | - :ref:`yo_cache_clean` - Clear Yo's caches -- a good first troubleshooting step. 85 | - :ref:`yo_debug` - Open up a python prompt in the context of a command. 86 | - :ref:`yo_help` - Show help for yo. 87 | - :ref:`yo_script` - Run a script file with Yo context available 88 | - :ref:`yo_version` - Show the version of yo and check for updates. 89 | 90 | Command Group Index 91 | ------------------- 92 | 93 | .. toctree:: 94 | :maxdepth: 1 95 | 96 | cmd00 97 | cmd01 98 | cmd02 99 | cmd03 100 | cmd04 101 | cmd05 102 | cmd06 103 | -------------------------------------------------------------------------------- /doc/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | # -- Path setup -------------------------------------------------------------- 7 | import os 8 | import re 9 | 10 | # -- Project information ----------------------------------------------------- 11 | 12 | project = "yo" 13 | copyright = "2020-2025, Oracle and/or its affiliates" 14 | author = "Oracle" 15 | 16 | # The full version, including alpha/beta/rc tags 17 | with open(os.path.join(os.path.dirname(__file__), "..", "setup.py")) as f: 18 | match = re.search(r"VERSION = \"(.+)\"", f.read()) 19 | assert match, "VERSION variable not found in setup.py" 20 | release = "v" + match.group(1) 21 | 22 | 23 | # -- General configuration --------------------------------------------------- 24 | 25 | # Add any Sphinx extension module names here, as strings. They can be 26 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 27 | # ones. 28 | extensions = [ 29 | "sphinxarg.ext", 30 | ] 31 | autodoc_typehints = "description" 32 | 33 | nitpicky = True 34 | 35 | # Add any paths that contain templates here, relative to this directory. 36 | templates_path = ["_templates"] 37 | 38 | # List of patterns, relative to source directory, that match files and 39 | # directories to ignore when looking for source files. 40 | # This pattern also affects html_static_path and html_extra_path. 41 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] 42 | 43 | # Set an environment variable to signal if we're imported by sphinx 44 | os.environ["SPHINX_BUILD"] = "1" 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = "alabaster" 52 | 53 | # Free, no attribution required, quick logo. 54 | # https://logodust.com 55 | html_logo = "yo.png" 56 | html_favicon = "yo.png" 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = [] 62 | 63 | # -- Options for Intersphinx linking (linking to external docs) 64 | intersphinx_mapping = { 65 | "python": ("https://docs.python.org/3", None), 66 | "pandas": ("https://pandas.pydata.org/pandas-docs/stable", None), 67 | "matplotlib": ("https://matplotlib.org", None), 68 | } 69 | -------------------------------------------------------------------------------- /doc/development.rst: -------------------------------------------------------------------------------- 1 | Development 2 | =========== 3 | 4 | You can find the source code for yo 5 | `here `_. You are welcomed to participate 6 | in its development in many ways: 7 | 8 | - Report bugs via issues 9 | - Submit merge requests with your new features or bug fixes, if you'd like. 10 | 11 | This document contains information about how to get a development environment 12 | set up, and how to do the main development processes. 13 | 14 | Environment Setup 15 | ----------------- 16 | 17 | There are some prerequisite dependencies you should have available on your 18 | system. I develop on my Oracle Linux 9 laptop, but development should be 19 | possible on any suitably recent Linux system (or even macOS) with an appropriate 20 | Python version (3.6 or higher). You'll need to ensure that ``~/.local/bin`` is 21 | in your ``$PATH`` environment variable, and you'll also want to double check 22 | that standard unix tools such as make, git, cut, and sed are installed. 23 | 24 | First, clone the repository (or your fork, if applicable) and install 25 | development tools: 26 | 27 | .. code:: bash 28 | 29 | git clone git@github.com:oracle/yo 30 | cd yo 31 | make development 32 | 33 | This last command will use your system Python 3 (defaults to the command 34 | ``python3``, but configurable via ``make PYTHON=python_cmd``) to install some 35 | tools to your home directory: 36 | 37 | - tox: a Python test runner framework 38 | - pre-commit: a tool for executing code checks and formatters before committing 39 | code. 40 | 41 | Assuming you already had installed yo on this computer, then you should be all 42 | set to begin running it directly from the git checkout, rather than the 43 | installed copy. To run from the git checkout: 44 | 45 | 1. Ensure that your current directory is the root of the git repository. 46 | 2. Rather than using the command ``yo``, use the command ``python3 -m yo``. 47 | Python will see the yo package in your current working directory, and execute 48 | that instead. 49 | 50 | Editable Install 51 | ~~~~~~~~~~~~~~~~ 52 | 53 | **If you enjoy living on the edge,** then you can set up your system so that 54 | whenever you run ``yo``, it always comes from whatever is in your git checkout. 55 | Simply run the following command from the root of the git repo: 56 | 57 | .. code:: bash 58 | 59 | # only for those who want to live on the edge: 60 | python3 -m pip install --user --editable . 61 | 62 | .. warning:: 63 | 64 | Note that now, if your development copy has a bug, then yo will be unusable 65 | on your whole system. If you'd prefer not to have this risk, then don't use 66 | this method! 67 | 68 | Building an RPM 69 | --------------- 70 | 71 | Yo has an RPM spec file at ``buildrpm/yo.spec``, which can be used to build an 72 | RPM on Oracle Linux 9+, and likely Fedora as well. The RPMs themselves are not 73 | currently officially built or distributed, but they can be built easily. These 74 | instructions are for Oracle Linux 9. Similar instructions will likely work for 75 | Oracle Linux 10, but Oracle Linux 8 does not contain necessary dependencies. 76 | 77 | First, install the build requirements: 78 | 79 | .. code:: bash 80 | 81 | sudo dnf install -y oracle-epel-release-el9 \ 82 | oraclelinux-developer-release-el9 \ 83 | pyproject-rpm-macros 84 | sudo dnf builddep -y buildrpm/yo.spec 85 | 86 | 87 | There are two ways to build the RPM. First is by using the current git tree to 88 | build, and the second is to fetch the latest release from Github and build from 89 | that source. 90 | 91 | To use the current git tree, first ensure that all your changes are committed. 92 | The source distribution is built using ``git archive``, so only committed 93 | changes are included. You may also want to update the spec file to tweak the 94 | "Release" value, if you do not have a release tag checked out. Then: 95 | 96 | .. code:: bash 97 | 98 | make rpm 99 | 100 | To download the source tarball from Github and then build: 101 | 102 | .. code:: bash 103 | 104 | cd buildrpm 105 | spectool -gS yo.spec 106 | rpmbuild --define "_sourcedir `pwd`" --define "_topdir `pwd`/tmp" -ba yo.spec 107 | 108 | 109 | Creating and Testing Changes 110 | ---------------------------- 111 | 112 | If you want to contribute a change, then make sure you create a fork, and set 113 | that as your git origin. Then, make sure your branch is up to date with 114 | upstream/master, and create a branch to work on your changes. 115 | 116 | When you've verified your changes work and you're happy with them, be sure to 117 | run the tests. You should be able to run them simply with ``make test``. There 118 | are currently only a few tests written, and it would be excellent if you add 119 | tests for your change (see the ``tests/`` directory). The test framework will 120 | attempt to run the tests on Python versions 3.6-3.10, but it's ok if you only 121 | have one suitable version installed. 122 | 123 | If the tests pass, then you can go ahead and commit your changes. The pre-commit 124 | hooks will verify a few things: 125 | 126 | 1. Your Python code files should have type annotations for functions and 127 | classes. 128 | 2. The mypy type checker should verify that there are no invalid operations 129 | based on the declared types. Note that Python type checking is a bit finicky 130 | at this point. If you have any issues with this (the "mypy" pre-commit hook), 131 | please reach out via Github Issue and we'll try to help you out. 132 | 3. The black code formatter will automatically reformat your changes to ensure 133 | they meet the existing code style. 134 | 4. The flake8 static checker will run static checks for low-hanging fruit bugs. 135 | 136 | Some of these hooks will automatically edit your code (leaving unstaged 137 | changes). Review these changes and ``git add`` them when you're satisfied. Other 138 | hooks will simply output error line numbers for you to fix. After one or two 139 | tries, you should satisfy the static checks. If you have too much trouble, you 140 | can use ``git commit --no-verify``, but please note that in your merge request 141 | and note what the issue was. 142 | 143 | At this point, you can now push your branch up to your Github fork and make a 144 | review request via the UI. 145 | 146 | Here's a checklist for things you may want to include in your changes. 147 | 148 | - If you've added a command or CLI flag, be sure to use 149 | ``scripts/rebuild_docs.py`` to regenerate the command documentation. 150 | - If you've added a configuration option, be sure to document it in 151 | ``doc/guide/configuration.rst``. 152 | - If your change is user-facing at all (fixing a bug, adding a feature), then 153 | document it in the "Unreleased" section of the ``CHANGELOG.rst``. 154 | -------------------------------------------------------------------------------- /doc/guide/block.rst: -------------------------------------------------------------------------------- 1 | Block Volume Management 2 | ======================= 3 | 4 | Yo now allows you to manage block volumes. These are useful for a 5 | variety of reasons, such as keeping long-term data around after you’ve 6 | terminated an instance, or for having a larger storage area without 7 | expanding a root device, or possibly for filesystem testing. 8 | 9 | OCI has two kinds of volumes: boot volumes, and block volumes. Boot 10 | volumes are created from the image you selected when you launch the 11 | instance. Block volumes are blank when created, and must be manually 12 | attached to the instance. 13 | 14 | Once created, volumes can be “attached” in a few ways, but the most 15 | common are: 16 | 17 | - “Paravirtualized” - meaning that the hypervisor will create a 18 | virtualized disk and notify your instance’s OS that a new disk is 19 | available. This is the simplest way to attach block devices, and it’s 20 | the default. It shouldn’t require any setup on the instance. However, 21 | paravirtualized disks are only available for VM instances, because BM 22 | instances don’t have hypervisors. 23 | - “iSCSI” - meaning that the disk is made available over the iSCSI 24 | protocol, but first your instance must be configured to connect to 25 | it. 26 | 27 | A volume can be attached to more than one instance (if attached in 28 | shared mode) and instances may of course be attached to more than one 29 | volume. 30 | 31 | Yo provides commands to create, list, attach, detach, and delete block 32 | devices. For iSCSI disks, these commands can also automatically run 33 | commands which will connect the disk. 34 | 35 | A note about volume names 36 | ------------------------- 37 | 38 | If you use yo frequently, you know that it has strong opinions about how 39 | to name instances. Yo is the same about block volumes as well: you 40 | should prefix them with your global username. 41 | 42 | Yo will automatically add that prefix if it isn’t found, and there is 43 | currently no ``--exact-name`` argument to disable this behavior (but it 44 | can be added if absolutely necessary). 45 | 46 | However, yo **will not** attempt to enforce unique naming for your 47 | volumes. So, if you create an instance named “stepbren-volume1” and then 48 | create a second one named “stepbren-volume1”, yo will not prevent this. 49 | The reason is mainly for efficiency: refreshing the volume list is quite 50 | slow, even with multiple threads. Thus, you should be careful to avoid 51 | duplicate names. If you do create a duplicate, you may need to visit the 52 | OCI Web Console to remedy the situation. 53 | 54 | yo volume-create 55 | ---------------- 56 | 57 | Creates a new, blank block volume. You need to provide at least three 58 | arguments: 59 | 60 | - ``name`` - we will use this to refer to your volume. This will get 61 | automatically prefixed by your username (if not already done), but yo 62 | *will not* check for duplicate names, nor automatically increment 63 | trailing numbers. 64 | - ``size_gbs`` - the size, in gigabytes, of your volume. 65 | - OCI places volumes into a particular availability domain, and they 66 | can be accessed only by instances in the same AD. By default, yo uses 67 | your default instance profile’s AD. However, you can customize it 68 | with the ``--ad AVAILABILITY_DOMAIN`` flag. More usefully, you can 69 | instead provide ``--for INSTANCE_NAME`` to tell yo to use the same 70 | availability domain as that instance. 71 | 72 | Unlike some of the other commands, yo will always wait for your volume 73 | to become ready. 74 | 75 | Creating a volume is frequently followed by attaching a volume. So, you 76 | can also use the ``--attach`` option to attach the volume to an instance 77 | after it is created. If you do so, you *must* use ``--for`` to tell yo 78 | which instance to attach it to. You can also provide the attachment 79 | arguments accepted by ``yo attach``. 80 | 81 | yo attach 82 | --------- 83 | 84 | Given a volume, attach it to a running instance. You need to provide at 85 | least two things: 86 | 87 | - ``volume_name`` - the name of the volume 88 | - ``instance_name`` - which instance to attach to 89 | 90 | The operation won’t succeed if the instances are in different ADs. This 91 | command will wait until the attachment succeeds. You can provide the 92 | following arguments to customize the block volume attachment (which can 93 | also be provided to ``yo volume-create --attach``): 94 | 95 | - ``--ro`` - attach read-only 96 | - ``--shared`` - use shared mode, to support attaching to multiple 97 | instances 98 | - ``--no-setup`` - do not run setup commands for iSCSI 99 | - Select the attachment method with one of ``--iscsi``, ``--pv`` 100 | (default), ``--emulated``, ``--service-determined``. The last two 101 | options are provided by the API, but they are not recommended. Please 102 | use either ``--iscsi`` or ``--pv`` for best results. 103 | 104 | yo detach 105 | --------- 106 | 107 | Given a volume, attach it from a specific instance, or all. You should 108 | provide at least the volume name, as the first argument. You can specify 109 | what to detach by providing one of the two options: 110 | 111 | - ``--from INSTANCE`` - detach from a particular instance 112 | - ``--all`` - detach from all instances 113 | 114 | By default, yo will run detachment commands from iSCSI instances. To 115 | avoid this, use ``--no-teardown``. 116 | 117 | yo volume-delete 118 | ---------------- 119 | 120 | Delete a volume. Since you typically want to ensure that the volume is 121 | detached from all instances prior to this, yo will automatically detach 122 | each attachment, that is still connected, including the iSCSI commands 123 | as necessary. These can be controlled by ``--no-detach`` and 124 | ``--no-teardown`` respectively. 125 | 126 | yo volume-list 127 | -------------- 128 | 129 | List all volumes. This command gives an overview of all volumes, 130 | regardless of whether they are attached to anything. The view includes 131 | both boot and block volumes. 132 | 133 | yo attached 134 | ----------- 135 | 136 | This is the slightly more useful volume listing command, though it is 137 | also a bit slower. This lists each instance which has attached boot or 138 | block devices (which, of course, is all instances), and then shows their 139 | attached devices. Here is an example of the output. 140 | 141 | :: 142 | 143 | ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━┳━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓ 144 | ┃ Instance/Volume ┃ Kind ┃ GiB ┃ Volume State ┃ Att. State ┃ Att. Kind ┃ 145 | ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━╇━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩ 146 | │ stepbren-ol8-1: │ │ │ │ │ │ 147 | │ - stepbren-drgnutils-images-2 │ block │ 512 │ AVAILABLE │ ATTACHED │ paravirtualized │ 148 | │ - stepbren-ol8-1 (Boot Volume) │ boot │ 47 │ AVAILABLE │ ATTACHED │ boot │ 149 | ├─────────────────────────────────────┼───────┼─────┼──────────────┼────────────┼─────────────────┤ 150 | │ stepbren-flamescope: │ │ │ │ │ │ 151 | │ - stepbren-flamescope (Boot Volume) │ boot │ 47 │ AVAILABLE │ ATTACHED │ boot │ 152 | ├─────────────────────────────────────┼───────┼─────┼──────────────┼────────────┼─────────────────┤ 153 | │ stepbren-ol8-3: │ │ │ │ │ │ 154 | │ - stepbren-ol8-3 (Boot Volume) │ boot │ 47 │ AVAILABLE │ ATTACHED │ boot │ 155 | ├─────────────────────────────────────┼───────┼─────┼──────────────┼────────────┼─────────────────┤ 156 | │ stepbren-focal-1: │ │ │ │ │ │ 157 | │ - stepbren-focal-1 (Boot Volume) │ boot │ 47 │ AVAILABLE │ ATTACHED │ boot │ 158 | └─────────────────────────────────────┴───────┴─────┴──────────────┴────────────┴─────────────────┘ 159 | -------------------------------------------------------------------------------- /doc/guide/concepts.rst: -------------------------------------------------------------------------------- 1 | Concepts 2 | ======== 3 | 4 | Limitations of yo 5 | ----------------- 6 | 7 | yo is **not** a general-purpose OCI client. It's all about managing instances, 8 | and to a lesser extent, block devices. It was built in an organization where 9 | many developers share a tenancy, and just want to manage their own instances 10 | without stepping on others' toes. 11 | 12 | yo is also pretty limited in functionality. It doesn't know much about VCNs or 13 | subnets, and it can't really do anything beyond managing instances and block 14 | devices. If you need access to those features, then you may be better off using 15 | the Web UI or the standard command line. 16 | 17 | These limitations are here to make yo simple and easy. The idea is to make it 18 | trivial for a Linux developer to go from thinking "I need to setup a VM in the 19 | cloud to test this", to being SSH'd into that very VM. 20 | 21 | Of course, some of these limitations are due to laziness on the part of the 22 | developer. Feel free to make the case for an omission via Github issues. Or, 23 | even better, feel free to submit an improvement via Github pull request! 24 | 25 | .. _instance_naming: 26 | 27 | Instance Naming 28 | --------------- 29 | 30 | yo manages instance names in an opinionated way. Suppose you have global 31 | username in your organization which is is "stepbren". yo believes that: 32 | 33 | 1. All instance names should be prefixed by "stepbren-". 34 | 2. Name collisions should be avoided by appending a "-N" suffix, where N 35 | increments. 36 | 37 | To enforce this, yo will automatically apply these rules on instance creation, 38 | and when looking up an instance name, if your name doesn't already fit the 39 | criteria. Some examples: 40 | 41 | .. code:: 42 | 43 | yo launch -n bug # new instance stepbren-bug 44 | yo launch -n stepbren-bug # same as above 45 | yo launch -n bug # if stepbren-bug already exists, creates 46 | # stepbren-bug-1 47 | yo launch -n bug-1 # if stepbren-bug-1 already exists, creates 48 | # stepbren-bug-2 49 | yo ssh bug # connect to stepbren-bug 50 | 51 | This behavior is designed with the idea of shared compartments in mind. It's 52 | nice include your username in the name of the instance, so that other users can 53 | easily determine who created it without needing to investigate it further. 54 | 55 | Of course, if you don't share your compartment with anybody, or you have 56 | specific naming requirements, then this approach can quickly get in your way. 57 | You can avoid this behavior in two ways: 58 | 59 | * You can pass ``--exact-name`` to various subcommands, avoiding the behavior on 60 | a case-by-case basis. 61 | 62 | * You can set the configuration value :ref:`exact_name` to true in your 63 | configuration file. This operates globally, completely disabling the behavior. 64 | If necessary, you can re-enable it on a case-by-case basis with 65 | ``--no-exact-name``. 66 | 67 | .. _resource visibility: 68 | 69 | Resource Visibility 70 | ------------------- 71 | 72 | Yo was designed to be used by teams that share compartments, and thus want to 73 | avoid stepping on each others' toes. So in Yo's default configuration, 74 | instances, block volumes, etc, are only shown if Yo knows that you created them. 75 | Similarly, Yo will not allow you to manage those resources which you did not 76 | create. 77 | 78 | However, not all people use OCI this way. Some people have their own compartment 79 | which is not shared with others. In other cases, the automatic tag rules 80 | necessary for Yo to determine the creator are not available (see :ref:`this 81 | explanation ` for more details). 82 | 83 | Whatever the reason, you can disable this visibility restriction by setting 84 | ``resource_filtering = false`` in your configuration file. See 85 | :ref:`resource_filtering` for more details. 86 | 87 | Instance Profile 88 | ---------------- 89 | 90 | yo allows you to create "Instance Profiles" in the configuration file. These 91 | specify details such as the operating system, shape, name, availability domain, 92 | and SSH keys. You can refer to these by name and simply launch an instance from 93 | a profile via the following command: 94 | 95 | .. code:: 96 | 97 | yo launch -p PROFILE 98 | 99 | If you don't specify ``-p``, you'll use the "DEFAULT" profile instead. See the 100 | :ref:`Instance Profiles` configuration section for more information. 101 | 102 | Instance States & Saved Instances 103 | --------------------------------- 104 | 105 | Instances in OCI are always in a state. Common instance states include: 106 | 107 | - PROVISIONING 108 | - STARTING 109 | - RUNNING 110 | - STOPPING 111 | - STOPPED 112 | - TERMINATING 113 | - TERMINATED 114 | 115 | Most states are self-explanatory. A newly launched instance starts in 116 | PROVISIONING, then moves to STARTING, and then enters RUNNING. If an instance is 117 | stopped, it moves through STOPPING to STOPPED, and starting it will return it to 118 | STARTING, followed by RUNNING. Finally, terminating an instance causes it to 119 | enter TERMINATING, followed by TERMINATED. 120 | 121 | Instances in the STOPPED state have special rules regarding `billing`_. Thus, 122 | stopping an instance may be advantageous, as it may pause billing for the 123 | instance. However, STOPPED instances still count toward service limits. Thus, Yo 124 | implements an additional option, which is expressed to the user as an additional 125 | instance state: "SAVED". 126 | 127 | Instances in the SAVED state are really just saved boot volumes -- they do not 128 | appear in the OCI console as instances. Yo attaches a small amount of metadata 129 | to the instances, which allows it to recreate the instance with the same name 130 | and shape. The ``yo teardown`` command is used to make an instance SAVED, and 131 | ``yo rebuild`` is used to recreate the instance (returning it to RUNNING). 132 | 133 | .. _billing: https://docs.oracle.com/en-us/iaas/Content/Compute/Tasks/resource-billing-stopped-instances.htm 134 | -------------------------------------------------------------------------------- /doc/guide/index.rst: -------------------------------------------------------------------------------- 1 | .. _user guide: 2 | 3 | User Guide 4 | ========== 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents 9 | 10 | concepts 11 | configuration 12 | tasks 13 | vnc 14 | block 15 | troubleshooting 16 | region 17 | -------------------------------------------------------------------------------- /doc/guide/region.rst: -------------------------------------------------------------------------------- 1 | Multiple OCI Region Support in Yo 2 | ================================= 3 | 4 | OCI’s services are spread across datacenters in many regions. Most OCI 5 | resources are specific to one OCI region, such as instances and block 6 | devices. Further, the OCI API is region-specific, and the OCI web 7 | console only shows resources from a single region at a time. 8 | 9 | Yo’s functionality is similar. Any Yo command can only run against a 10 | single region (e.g. ``yo list`` will only show instances of a specific 11 | region). By default, Yo runs in the region configured by the ``region`` 12 | key in the ``[yo]`` section of ``yo.ini``. However, with proper 13 | configuration, you can override the region that Yo runs in with 14 | ``yo -r REGION ...`` for any sub-command. 15 | 16 | Configuring multiple regions 17 | ---------------------------- 18 | 19 | Each region you would like to use must have its own VCN and subnet 20 | information configured, within a region-specific configuration section. 21 | For example: 22 | 23 | :: 24 | 25 | [regions.us-ashburn-1] 26 | vcn_id = ocid.vcn... 27 | subnet_id = ocid.subnet... 28 | 29 | [regions.us-phoenix-1] 30 | vcn_id = ocid.vcn... 31 | subnet_id = ocid.subnet... 32 | 33 | Using multiple regions 34 | ---------------------- 35 | 36 | The default region is selected via the ``region`` configuration key in 37 | the ``[yo]`` section. This may be overridden on the command line using 38 | ``yo -r REGION``. The ``-r`` flag *must* be immediately after the ``yo`` 39 | command and before the sub-command. For example: 40 | 41 | .. code:: bash 42 | 43 | # CORRECT: 44 | yo -r us-ashburn-1 list 45 | 46 | # INCORRECT 47 | yo list -r us-ashburn-1 48 | 49 | If you’d like to run an entire shell session using a specific Yo region, 50 | you may set the ``YO_REGION`` environment variable, which Yo will detect 51 | and use. The environment variable may be overridden by the command line 52 | option. 53 | 54 | Migrating yo.ini to multi-region support 55 | ---------------------------------------- 56 | 57 | Upon upgrading Yo to version 1.7.0 or later from any prior version, you 58 | may see the following warning: 59 | 60 | :: 61 | 62 | warning: region-specific configurations in [yo] section are deprecated, please update your config to use [regions.*] sections 63 | 64 | To resolve this warning, make the following changes to your 65 | ``~/.oci/yo.ini`` configuration file: 66 | 67 | 1. You should already have a line ``region = something`` in your 68 | configuration. We’ll refer to this value as ``$REGION`` here. First, 69 | create a new section beneath the ``[yo]`` section named 70 | ``[regions.$REGION]`` 71 | 2. Move the configuration keys ``vcn_id`` and ``subnet_id`` (or 72 | ``subnet_compartment_id``, if you use that instead) into the 73 | ``[regions.$REGION]`` section. 74 | 3. Optionally, update your availability domain configurations in each 75 | instance profile to refer to the AD by number, rather than name. 76 | 77 | For example, consider this (incomplete) configuration snippet: 78 | 79 | .. code:: ini 80 | 81 | [yo] 82 | instance_compartment_id = ocid1.compartment... 83 | region = us-ashburn-1 84 | vcn_id = ocid1.vcn... 85 | subnet_id = ocid1.subnet... 86 | my_email = example@example.com 87 | my_username = example 88 | 89 | [instances.DEFAULT] 90 | availability_domain = VkEH:US-ASHBURN-AD-1 91 | shape = VM.Standard.x86.Generic 92 | cpu = 1 93 | mem = 8 94 | os = Oracle Linux:9 95 | name = ol9-1 96 | 97 | The updated configuration would look like this: 98 | 99 | .. code:: ini 100 | 101 | [yo] 102 | instance_compartment_id = ocid1.compartment... 103 | region = us-ashburn-1 104 | my_email = example@example.com 105 | my_username = example 106 | 107 | [regions.us-ashburn-1] 108 | vcn_id = ocid1.vcn... 109 | subnet_id = ocid1.subnet... 110 | 111 | [instances.DEFAULT] 112 | availability_domain = 1 113 | shape = VM.Standard.x86.Generic 114 | cpu = 1 115 | mem = 8 116 | os = Oracle Linux:9 117 | name = ol9-1 118 | 119 | Now, you may add more region configurations (each with its own section), 120 | and you can easily switch between these regions on the command line. 121 | -------------------------------------------------------------------------------- /doc/guide/tasks.rst: -------------------------------------------------------------------------------- 1 | Tasks 2 | ===== 3 | 4 | .. _tasks_overview: 5 | 6 | An Overview Of Yo Tasks 7 | ----------------------- 8 | 9 | Tasks are a yo feature which enable you to run Bash scripts on your instance 10 | remotely, without being connected to it. The command output and exit status are 11 | recorded for your viewing later. 12 | 13 | On its own, a feature like this isn't very useful, because you can just run 14 | those scripts manually within a program like screen or tmux. What makes tasks 15 | interesting is that yo maintains an internal library of them (and you can add 16 | your own to it), and the tasks can be automatically started as an instance is 17 | launched, without any human interaction. 18 | 19 | What this means is that you can think of a task as a way to configure your 20 | instance without having to do it manually at the beginning of every session. If 21 | the main functionality of yo liberates you from needing to manually use the OCI 22 | console and juggle IP addresses and SSH keys, then the task feature liberates 23 | you from installing dependencies and setting up your instance once you've 24 | connected to it. 25 | 26 | Here are a few use cases for tasks, which you may want to consider: 27 | 28 | - Automatically installing and activating a proxy so your instance has Internet 29 | access. 30 | - Setting up software such as ``drgn`` or ``mosh`` without needing to understand 31 | the details of installation. 32 | - Placing personal configuration files and user scripts onto the system (e.g. 33 | bashrc customizations and personal tools). 34 | - Once you've figured out a way to reproduce a bug, place the configuration and 35 | setup steps into a task so you can easily return to it in the future. 36 | 37 | Tasks aren't always a perfect solution. They have some limitations that you may 38 | want to consider: 39 | 40 | - Yo doesn't have a way to trigger a reboot from within a task, and then 41 | continue running more commands. So you can't easily install a custom kernel 42 | and reboot into it. 43 | - Tasks that take a long time (e.g. long package installations) may not be well 44 | suited to re-running them each time you start an instance. You may want to 45 | look into creating an image from your instance after you've configured it. The 46 | downside of this approach is that your image will be static and based on a 47 | single OS version, whereas a well-written task is just a set of instructions, 48 | and can be run on any compatible image as newer versions are released. Of 49 | course, creating a custom image is difficult, and tasks are much easier to 50 | write! 51 | 52 | Creating Tasks 53 | -------------- 54 | 55 | Tasks are identified by their filename. Yo searches for them in a list of 56 | directories, using the first one it finds: 57 | 58 | - ``~/.oci/yo-tasks`` (referred to as the "user task directory") 59 | - Yo's installation directory (don't modify these!) 60 | 61 | Since the user task directory is first in the list, you're able to override any 62 | task you'd like with a newer version. The tasks are always run with ``bash`` and 63 | don't need to be marked executable. 64 | 65 | Once you've created your script, there's no more bookkeeping necessary. Yo will 66 | find the script when you ask to use it. 67 | 68 | Special Task Syntax 69 | ------------------- 70 | 71 | While tasks are generally normal bash scripts, there are a few special syntactic 72 | elements which are available to you. These are implemented in a rather unique 73 | way: as Yo loads your script, it reads each line and detects the use of the 74 | following keywords, somewhat like a macro processor. Most of these keywords also 75 | have a corresponding bash function that Yo provides to implement the necessary 76 | functionality. 77 | 78 | - ``DEPENDS_ON `` - this declares that your task depends on 79 | another one. Yo will search for these lines in your script and automatically 80 | find and load that script too. The bash function will wait for the successful 81 | completion of the dependency, or else it will exit with a failure message. 82 | 83 | - ``MAYBE_DEPENDS_ON `` - this is similar to ``DEPENDS_ON``, but 84 | it is optional. If Yo finds a task with that name, then it will replace this 85 | with ``DEPENDS_ON`` and include the dependency . Otherwise, it will comment 86 | out this line and continue without error. This allows you to specify 87 | dependencies that only get run if they exist. A common use case for this is 88 | for networking configuration. Some tenancies or VCNs have no direct Internet 89 | access except via a proxy. Scripts should use ``MAYBE_DEPENDS_ON networking`` 90 | if they access the Internet, which allows users who require a proxy to 91 | implement a ``networking`` task to configure it. 92 | 93 | - ``CONFLICTS_WITH `` - this could be helpful for declaring that 94 | your task won't work with other ones. 95 | 96 | - ``RUN_ONCE`` - this function indicates that after a successful run, your 97 | script should not run again. It will detect a previous success, and exit with 98 | code 0. Note that this will still allow you to re-run a failed task. 99 | 100 | - ``PREREQ_FOR `` - this declares that your task is a 101 | prerequisite of another. It can be thought of as the inverse of 102 | ``DEPENDS_ON``, but with one important caveat. This relationship only applies 103 | if the other task is actually loaded and run by Yo at the same time as this 104 | one. For example, suppose task ``A`` contains ``PREREQ_FOR B``. Then: 105 | 106 | - Specifying task ``A`` will not automatically run task ``B`` 107 | - Similarly, specifying task ``B`` will not automatically run ``A`` 108 | - However, if task ``A`` and ``B`` are both specified to run, then Yo will 109 | ensure that A runs to completion before task ``B`` begins. 110 | 111 | - ``INCLUDE_FILE [destination]`` - this declares that the given path from 112 | the client system should be copied to the instance. If the path is a 113 | directory, it will be included recursively. Paths must be either absolute 114 | (i.e. starting with ``/``) or relative to the home directory (i.e. starting 115 | with ``~/``). By default, files are copied into the corresponding location on 116 | the instance, but a different ``destination`` may be specified if necessary. 117 | The path may also be a glob -- in which case, the destination argument must be 118 | provided, and it will be interpreted as a directory into which each matching 119 | file or directory is placed. 120 | 121 | This command works by building a tarball of all required files for a task, 122 | copying it to the instance, and extracting it into place. For more details on 123 | file management, see the section below. 124 | 125 | - The variant ``MAYBE_INCLUDE_FILE [destination]`` can be used to 126 | include a file if it exists on the host system. No error will be raised if 127 | the file does not exist. 128 | 129 | - ``SENDFILE `` - this declares that the given filename should 130 | be directly copied into ``$TASK_DATA_DIR/$FILENAME``. This is a somewhat 131 | low-level command -- no tarball is involved. See the section below for more 132 | details on file management. 133 | 134 | These functions can be used anywhere in your script, however bash variable 135 | expansion is not respected when Yo reads and pre-processes the script. So, while 136 | the following is valid bash, it won't work with Yo: 137 | 138 | .. code:: bash 139 | 140 | TASK=drgn 141 | DEPENDS_ON $TASK 142 | 143 | 144 | Other Task Variables and Functions 145 | ---------------------------------- 146 | 147 | Additionally, Yo's bash task library makes a few conveniences available to you. 148 | It sources the contents of ``/etc/os-release`` so that you can use common 149 | variables from this file, such as ``$NAME``, ``$VERSION_ID``, etc. In addition, 150 | Yo provides the following variables: 151 | 152 | - ``$ORAVER`` - an integer representing the current Oracle Linux release. The 153 | variable is undefined if not running on Oracle Linux. You can detect when 154 | Oracle Linux is running by matching the ``NAME`` variable against ``Oracle*`` 155 | 156 | - ``$UBUVER`` - an integer representing the Ubuntu version (e.g. for 24.10, this 157 | would be "24"). If you would like the full version (e.g. to distinguish 24.04 158 | and 24.10), use the ``$VERSION_ID`` field directly. 159 | 160 | - ``$FEDVER`` - an integer representing the Fedora version 161 | 162 | - ``$DEBVER`` - an integer representing the Debian version 163 | 164 | - ``$PKGMGR`` - the name of the system package manager (only detected for the 165 | above operating systems) 166 | 167 | And below are the simple bash functions (not interpreted by Yo) provided in the 168 | task library: 169 | 170 | - ``PKG_INSTALL [package [...]]`` install one or more packages. This relies on 171 | the detected ``$PKGMGR`` from above, and ensures that the correct options are 172 | used for the specific package manager, especially to avoid interactive 173 | confirmation. 174 | 175 | Managing Tasks 176 | -------------- 177 | 178 | At any time, you can view all available tasks with ``yo task-list``. You can get 179 | details about a particular task using ``yo task-info``. This will essentially 180 | dump the script contents to stdout, along with a header giving its file 181 | location and other info. 182 | 183 | You can manually start a task on an instance with ``yo task-run``. The first 184 | argument to this command is the instance name, which can be omitted if you only 185 | have one running. The second argument is the name of the task. For example: 186 | 187 | .. code:: 188 | 189 | # Only one instance is running, start "test-task" on that 190 | yo task-run test-task 191 | 192 | # Start "test-task" on instance "vm3". Wait for completion. 193 | yo task-run vm3 test-task --wait 194 | 195 | You can also use ``yo task-join [inst]`` to wait for all currently running 196 | tasks. Finally, you can get a bird's eye view of all tasks running on an 197 | instance with ``yo task-status [inst]``. If a task fails, or if you just want 198 | more information, you can go into the ``/tmp/tasks`` directory on your instance. 199 | Each task gets a directory, with the following files: 200 | 201 | - ``output`` - stdout and stderr of the task (which is executed with ``bash -x`` 202 | so you can see each command executed). 203 | - ``pid`` - process ID of the parent for this task 204 | - ``status`` - exit status of the task 205 | - ``wait`` - while a task waits for a dependency, it writes the name of the 206 | dependency into this file, and deletes it once the wait completes 207 | 208 | The task directory can be configured from its default (``/tmp/tasks``) using the 209 | :ref:`task_dir` configuration option. 210 | 211 | Running Tasks at Launch Time 212 | ---------------------------- 213 | 214 | With the ``--task`` argument to ``yo launch``, you can request that a task be 215 | executed at startup. This will result in your command automatically waiting for 216 | the instance to start, and then waiting for SSH access, so that yo can then run 217 | the task. 218 | 219 | You can specify the ``--task`` option multiple times, so it's valid to do 220 | something like this: 221 | 222 | .. code:: bash 223 | 224 | yo launch -p ol8 -t ocid -t drgn -s 225 | 226 | What's more, you can even specify tasks inside an instance profile. This makes 227 | it quite easy to automatically get an instance with particular tools installed 228 | without thinking of it. See the configuration option :ref:`config_tasks`. 229 | 230 | By using the ``--ssh`` or ``--wait`` arguments to ``yo launch``, along with 231 | specifying tasks to run, you will automatically get SSH'd into your instance 232 | once all the tasks are completed and your environment is ready. For bonus 233 | points, consider setting up the :ref:`notify_prog` configuration, which will 234 | allow you to receive a desktop notification when your instance is ready. This is 235 | quite convenient to allow you to focus on another task while your instance boots 236 | and self-configures. 237 | 238 | Please note that tasks specified in an instance profile cannot be removed from 239 | the profile on the command line. You can only specify _additional_ tasks to run. 240 | 241 | Specifying Files for Tasks 242 | -------------------------- 243 | 244 | Tasks can be included onto an instance with two mechanisms, ``INCLUDE_FILE`` and 245 | ``SENDFILE``, described above. The implementation of these commands is described 246 | here in a bit more detail, so you can understand what's happening under the hood 247 | and make use of them well. 248 | 249 | Files copied by ``INCLUDE_FILE`` are split into two groups: user files (those 250 | whose destination is prefixed by ``~/``, and thus are destined for the home 251 | directory), and system files (those whose destination starts with ``/``). The 252 | user files are placed into a tarball called ``user.tar.gz``, and the system 253 | files go into ``system.tar.gz``. Yo caches these files in 254 | ``~/.cache/yo-tasks/$TASK/`` for each task. When a task is launched, Yo 255 | enumerates all the files that will be included, and if any are more recent than 256 | the cached tarball, it rebuilds the tarball. 257 | 258 | The idea behind ``INCLUDE_FILE`` is that it allows you to automatically include 259 | useful files from your client system directly onto the instance. As an example, 260 | you might want to include your ``~/.bashrc``, ``~/.vimrc`` and a collection of 261 | useful scripts. You can create a custom task which does so quite easily: 262 | 263 | .. code:: 264 | 265 | INCLUDE_FILE ~/.bashrc 266 | INCLUDE_FILE ~/.vimrc 267 | INCLUDE_FILE ~/oci-scripts ~/bin 268 | 269 | So the hope is that this mechanism will suit most use cases. However, there may 270 | be other cases that are more complex. For that, we have ``SENDFILE``. 271 | 272 | Files copied by ``SENDFILE`` have none of the above logic applied to them. They 273 | are copied directly to the instance into the ``$TASK_DATA_DIR``. Then your 274 | script can process it however you would like. For instance, you may want to 275 | distribute a tarball containing software that your script will install manually. 276 | That could be achieved like so: 277 | 278 | .. code:: 279 | 280 | SENDFILE ~/Software/mypkg-1.2.3.tar.gz 281 | 282 | PKG_INSTALL dependency1-devel dependency2-devel 283 | mkdir build 284 | cd build 285 | tar xf $TASK_DATA_DIR/mypkg-1.2.3.tar.gz 286 | cd mypkg-1.2.3 287 | ./configure --prefix=$HOME/.local 288 | make -j $(nproc) 289 | make install 290 | cd ../.. 291 | rm -rf build $TASK_DATA_DIR/mypkg-1.2.3.tar.gz 292 | 293 | Finally, there's one implementation detail worth noting: after Yo creates the 294 | ``user.tar.gz`` and ``system.tar.gz`` tarballs, it treats them like any other 295 | file specified with ``SENDFILE``, except for one crucial difference. Files with 296 | those names get automatically extracted into the correct places by Yo's task 297 | launcher stub program. This means that you can elect to build your own archives 298 | that contain exactly what you want (rather than using ``INCLUDE_FILE`` to build 299 | them at runtime), and then specify them using ``SENDFILE``. Yo will extract 300 | those just the same as it does for the archives it creates. 301 | 302 | Builtin Tasks 303 | ------------- 304 | 305 | - ```drgn`` - install drgn and (if possible) kernel debuginfo, supported on 306 | Oracle Linux 8 and later. 307 | - ``ocid`` - enable and run the ``ocid`` service, which can automatically 308 | respond to OCI actions like resizing block volumes in sensible ways. 309 | 310 | The following task names are used as optional dependencies by ``yo``, but no 311 | task is included by that name, to allow users to customize their setup: 312 | 313 | - ``networking`` - used as an optional dependency by tasks requiring Internet 314 | access. The implementation should configure networking so that dependent tasks 315 | can automatically begin using it. 316 | 317 | Tasks - Future Work 318 | ------------------- 319 | 320 | Most of the work planned for tasks is now completed. However, one additional 321 | feature which could be nice is the ability to pass variables or file data to a 322 | task script. I'm currently waiting for a use case before building this feature. 323 | 324 | I'd also like to be able to support rebooting an instance with a custom kernel, 325 | this could save some preparation time in bug reproduction. However, I'm not 326 | currently clear how to implement it, which is why it's future work. 327 | 328 | Finally, tasks all have a timeout around 10 minutes. This timeout value is 329 | hardcoded around the code base and not particularly customizable. If you write a 330 | particularly long task, you risk timing out, without a clear way to resolve it. 331 | So one final piece of work is to resolve that and allow longer task timeouts. 332 | -------------------------------------------------------------------------------- /doc/guide/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | Issues With Bash Completion 5 | --------------------------- 6 | 7 | Assuming you've setup :ref:`Bash Completion`, you can temporarily set the 8 | following environment variable: 9 | 10 | .. code:: bash 11 | 12 | export _ARC_DEBUG=1 13 | 14 | Then try doing tab completion, for example: 15 | 16 | .. code:: bash 17 | 18 | yo ssh 19 | 20 | You should get a flood of output. If there's no output, then the completion is 21 | not properly configured with Bash. If there is output, check for a Python 22 | exception traceback. If you see one, please file a Github issue and we can 23 | investigate. 24 | 25 | Caching 26 | ------- 27 | 28 | Yo has a somewhat aggressive approach to caching. It turns out that loading the 29 | OCI SDK is quite slow. Part of the reason for this is McAfee on my laptop 30 | (scanning each individual Python file...), and part of the reason is that the 31 | SDK library is massive and includes a lot of small Python files. In order to 32 | load any information from OCI, you need to first load this SDK, and then make 33 | HTTP requests, both of which are bound to be slow. To get a sense for this, run 34 | ``time yo list`` -- it's probably 3-4 seconds. Without caching, ``yo`` would ru 35 | quite slowly for common operations. Imagine if each time you run ``yo ssh``, 36 | we'd need to lookup the IP address, inserting a delay of several seconds. 37 | 38 | To make this all better, we do two things: 39 | 40 | 1. Lazy load the OCI SDK -- if we don't need it, don't bother importing it. 41 | 2. Cache data from OCI 42 | 43 | If you like reading the code ``yo/api.py`` is the internal "barrier" between yo 44 | and the OCI library, it handles both of those strategies in a way that is mostly 45 | transparent. We currently cache 4 types of data: 46 | 47 | 1. The list of instances 48 | 2. The list of images 49 | 3. The list of VNICs 50 | 4. The list of instance console connections 51 | 52 | With a fully populated cache, this means that ``yo ssh``, ``yo console``, and 53 | others can run very snappy. The downside is that you could encounter some 54 | weirdness if you make changes to your instances outside of ``yo``. The cache 55 | maintenance operations are a bit boring to go through, but the basic 56 | troubleshooting steps for determining if there is a caching issue impacting you 57 | are: 58 | 59 | 1. Run ``yo list --cached; yo list`` - this will show you the cached view of the 60 | world, and then refresh the cache (the default behavior of ``yo list`` is to 61 | refresh the cached instance list). If you see differences, then your issue 62 | might be resolved now. 63 | 2. Run ``rm ~/.cache/yo.json`` - this will remove the entire cache. This is 64 | harmless; yo can fetch everything and repopulate it. 65 | 66 | Note that if you make changes to important configuration items, such as the OCI 67 | region you are using, or your configured email address, then you should run ``rm 68 | ~/.cache/yo.json`` as well. The cache is not equipped to detect these changes. 69 | -------------------------------------------------------------------------------- /doc/guide/vnc.rst: -------------------------------------------------------------------------------- 1 | VNC and Remote desktop 2 | ====================== 3 | 4 | In addition to providing access to your OCI instances via SSH, it’s 5 | possible to configure them to run a full graphical environment, which 6 | you can access over the VNC or RDP protocols. Yo has helper commands, 7 | ``yo vnc`` and ``yo rdp``, which allow you to specify a client program 8 | and automatically launch a connection to your OCI instance. 9 | 10 | In order to use VNC or RDP, there are three steps 11 | 12 | 1. Setup the server on your OCI instance 13 | 2. Install and configure the client on your local computer 14 | 3. Use yo to connect 15 | 16 | See below for details on each step. 17 | 18 | Server Support 19 | -------------- 20 | 21 | Setting up VNC and RDP Servers on Oracle Linux 22 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 23 | 24 | For Oracle Linux 8 and above, there is an `official 25 | guide `__ 26 | that demonstrates how to install and start a VNC server. Follow this 27 | guide, but you do not need to configure X509 encryption, nor do you need 28 | to configure firewall rules: Yo is able to automatically create an SSH 29 | tunnel, which allows a secure connection without these steps. 30 | 31 | It is also possible to setup an RDP server on Oracle Linux. The 32 | following commands can install and enable the XRDP server 33 | implementation: 34 | 35 | :: 36 | 37 | sudo dnf config-manager --enable ol8_developer_EPEL 38 | sudo dnf -y install xrdp 39 | sudo dnf -y groupinstall "server with gui" 40 | sudo systemctl enable xrdp 41 | sudo systemctl start xrdp 42 | sudo passwd opc # make sure to set the password for your user 43 | 44 | Similar to the above VNC guide, this configuration doesn’t create a 45 | firewall rule allowing inbound connections from the network. You can use 46 | yo to automatically create an SSH tunnel, which is a more secure 47 | configuration. 48 | 49 | You may find it quite convenient to write a `Task `__ which 50 | scripts the installation of your preferred server, so that there are no 51 | manual steps between launching your instance and connecting to it. 52 | 53 | Using RDP on Windows Server 54 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 55 | 56 | Windows Server images come with RDP configured and ready to use. OCI 57 | provides you with a default username and password, which must be changed 58 | upon your first connection. 59 | 60 | Yo is able to detect this, and displays your initial credentials 61 | whenever you connect with ``yo rdp``. 62 | 63 | Client Support 64 | -------------- 65 | 66 | Yo supports launching VNC and RDP clients via the command line. You will 67 | need to configure {ref}\ ``vnc_prog`` or {ref}\ ``rdp_prog`` in order to 68 | launch your chosen client. Most clients should accept a host and port on 69 | the command line. 70 | 71 | One tested client for Linux is KRDC, the KDE Remote Desktop Client. It 72 | is known to work well with Yo. The package is available in the Oracle 73 | Linux EPEL package repository for OL8 and OL9: 74 | 75 | :: 76 | 77 | sudo dnf config-manager --enable ol8_developer_EPEL # or ol9 78 | sudo dnf -y install krdc 79 | 80 | Other clients are also likely to work. If you test a client and have any 81 | issues, or would like to share your configuration steps, feel free to 82 | file a Github issue. 83 | 84 | Connecting with Yo 85 | ------------------ 86 | 87 | Once you have configured your client and server, you can use Yo to 88 | connect: 89 | 90 | :: 91 | 92 | yo rdp $instance 93 | yo vnc $instance 94 | 95 | Please note that by default, Yo tunnels the connection over SSH. This is 96 | to encourage secure setups which do not expose remote desktop ports to 97 | public network. However, if you have configured your instance to be 98 | public facing (or if you are using a Windows Server instance, which 99 | cannot do SSH tunneling), then you can disable the tunneling with 100 | ``-T``: 101 | 102 | :: 103 | 104 | yo rdp -T $instance 105 | yo vnc -T $instance 106 | -------------------------------------------------------------------------------- /doc/index.rst: -------------------------------------------------------------------------------- 1 | yo 2 | == 3 | 4 | yo is a command-line client for managing OCI instances. It makes launching OCI 5 | instances as simple as rudely telling your computer "yo, launch an instance". 6 | Its goals are speed, ease of use, and simplicity. It was originally designed to 7 | help developers in the Oracle Linux team quickly launch disposable VMs for 8 | testing, but is slowly gaining users outside of OL. Here are some examples of 9 | how yo tries to improve on the OCI command line and browser tools: 10 | 11 | - yo hides other people's VMs from you, so you can just manage your own 12 | instances. 13 | - yo doesn't make you type any more than you need. Compartment IDs, common shape 14 | configurations, etc can all be stored in your config. It really can be as 15 | simple as ``yo launch`` (or, for the lazier, ``yo la``). 16 | - yo lets you refer to your instances by name. You should never need to memorize 17 | an IP address again. 18 | - yo aggressively caches data to make operations as quick as possible. 19 | 20 | This documentation site contains instructions on how to install and configure 21 | yo, as well as some frequently asked questions and an illustrated list of yo 22 | sub-commands. 23 | 24 | Contact 25 | ------- 26 | 27 | yo is created and maintained by Oracle and contributors. Its code is hosted 28 | `here `_ -- feel free to file bugs or issues 29 | there, or fork it and submit a pull request. 30 | 31 | Contents 32 | -------- 33 | 34 | .. toctree:: 35 | :maxdepth: 2 36 | 37 | install 38 | guide/index 39 | cmds/index 40 | optional_setup 41 | development 42 | 43 | .. toctree:: 44 | :maxdepth: 1 45 | 46 | changelog 47 | -------------------------------------------------------------------------------- /doc/install.rst: -------------------------------------------------------------------------------- 1 | Installation Guide 2 | ================== 3 | 4 | yo is known to work on Linux and Mac OS. More guinea pigs are necessary for 5 | Windows. yo requires at least Python 3.6, and it also requires that you have 6 | OpenSSH installed. Yo can make use of the following optionally, if you have them 7 | installed: 8 | 9 | - ``rsync`` 10 | - ``mosh`` 11 | - Any RDP or VNC client that can connect via the command line arguments 12 | 13 | This document expects that you've already created an OCI tenancy, or that you 14 | have an account on an existing tenancy. Further, we expect that you've already 15 | launched an instance once in the Web UI, which should create some default 16 | resources for you. 17 | 18 | Installation 19 | ------------ 20 | 21 | First, ensure you have at least Python 3.6 installed, along with pip. For Oracle 22 | Linux users, ``yum install python3-pip`` should do the trick. 23 | 24 | .. code:: bash 25 | 26 | pip install yo oci-cli 27 | 28 | It's recommended that you use the above command to install yo. If you run it as 29 | a non-root user (recommended), it will install to your home directory. The above 30 | will also install the ``oci-cli`` package, which will help you configure yo. 31 | 32 | Initial Configuration 33 | --------------------- 34 | 35 | If you run ``yo`` at this point, you should be greeted by a message asking you 36 | to configure OCI and/or yo. 37 | 38 | OCI Configuration 39 | ~~~~~~~~~~~~~~~~~ 40 | 41 | yo depends on the OCI SDK in order to manage your compute instances. If ``yo`` 42 | is installed, then the ``oci`` command line utility should also be alongside it. 43 | The OCI SDK requires some keys to be created and associated with your account, 44 | but thankfully, this can be easily achieved with the command: 45 | 46 | .. code:: bash 47 | 48 | oci setup bootstrap 49 | 50 | Follow the prompts to authenticate with your tenancy. If you don't know your 51 | region, open the OCI Web UI and see what it defaults to. 52 | 53 | Please note: if you encounter an error message in the above command which 54 | contains the code ``NotAuthorizedOrNotFound``, then there's an alternative way 55 | to setup your OCI credentials: 56 | 57 | 1. Run ``oci setup keys``. At the prompt, you probably want to type "N/A" to 58 | avoid setting a passphrase for the key. However, if you really feel it is 59 | necessary, you can use a passphrase. Yo will detect that it is necessary and 60 | prompt you for it. 61 | 2. Open the OCI web console. Navigate to your profile using the icon at the top 62 | right (or search your email address in the search bar). On your profile page, 63 | select the "API keys" link, and then choose "Add API key". 64 | 3. Use "choose public key file" to upload the **public key** generated by step 65 | 1, and click submit. The resulting page should contain a fragment of 66 | configuration file. 67 | 4. Paste the configuration fragment into ``~/.oci/config``. Be sure to set 68 | ``key_file`` to the name of your **private** key. 69 | 70 | yo Configuration 71 | ~~~~~~~~~~~~~~~~ 72 | 73 | If you run ``yo`` without having a configuration file setup, ``yo`` will copy 74 | the sample configuration file to ``~/.oci/yo.ini``. 75 | 76 | **Edit this configuration file.** You'll need to set ``my_username`` to a unique 77 | username identifying you. Set ``my_email`` to your full email address (the one 78 | associated with your OCI account -- see the OCI console if you're not sure). 79 | 80 | There are a few OCI-related configurations that yo needs you to set, so it knows 81 | how to organize your instance and which subnets to use. 82 | 83 | 1. ``region`` - Choose this to match what you configured before! 84 | 85 | 2. ``instance_compartment_id`` - Compartments are like containers for resources 86 | in OCI. They are useful for larger organizations. You can use the default OCI 87 | command line tool ``oci iam compartment list`` to explore and select the 88 | correct ID. If you have a personal account, you may not use compartments, and 89 | instead you can use `your tenancy ID `_. 90 | 91 | 3. ``vcn_id`` - You can explore the VCNs in your account via ``oci network vcn 92 | list``. Use the ``id`` field of the desired VCN. 93 | 94 | 4. ``subnet_id`` - This selects a particular subnet, you can select from the 95 | list in ``oci network subnet list``. 96 | 97 | 5. Under the ``[instances.DEFAULT]`` section, you'll see an 98 | ``availability_domain`` configuration. Choose one of the availability domains 99 | in your region (try ``oci iam availability-domain list`` to get their names). 100 | 101 | .. _get_tenancy_id: https://docs.oracle.com/en-us/iaas/Content/GSG/Tasks/contactingsupport_topic-Finding_Your_Tenancy_OCID_Oracle_Cloud_Identifier.htm 102 | 103 | SSH Key 104 | ~~~~~~~ 105 | 106 | A major part of managing your OCI instances is being able to access them over 107 | SSH. You'll probably want to take a minute to ensure your SSH key is configured 108 | correctly. As a default, Yo assumes your SSH public key is 109 | ``~/.ssh/id_rsa.pub``, but you can change that with the :ref:`ssh_public_key` 110 | configuration value in ``~/.oci/yo.ini``. Please keep the following in mind: 111 | 112 | 1. Yo sometimes connects via SSH to execute single commands. If your SSH key is 113 | password protected, you may find yourself being prompted for the password 114 | more than you'd like. It's recommended to use an SSH agent to avoid frequent 115 | password prompts, or if it is appropriate and secure for your system, to 116 | store your SSH key without password protection. 117 | 118 | 2. ED25519 keys are supported by some operating systems, but not all. What's 119 | more, the OCI Instance Console Connection service (that is, ``yo console``) 120 | does not support ED25519. If you intend to use the serial console, or older 121 | OS versions, you should configure a 4096-bit RSA key. 122 | 123 | You may find it simplest to just create a SSH key especially for Yo and your OCI 124 | instances, then configure that in your :ref:`ssh_public_key` setting: 125 | 126 | .. code-block:: 127 | 128 | ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_yo 129 | # enter blank to disable passphrase 130 | 131 | Done! 132 | ~~~~~ 133 | 134 | At this point, you should be able to run yo commands, such as ``yo list``. 135 | Please continue to the :ref:`User Guide` to understand the basics for how to use 136 | Yo. 137 | 138 | There are some optional features which yo also supports. If you want to set them 139 | up, visit :ref:`Optional Features` to learn more. 140 | -------------------------------------------------------------------------------- /doc/optional_setup.rst: -------------------------------------------------------------------------------- 1 | .. _optional features: 2 | 3 | Optional Features 4 | ================= 5 | 6 | Once yo is configured, you're able to use its base feature set, which is 7 | (hopefully) described well in the Guide. However, there are a few additional 8 | features which are optional and require some additional setup or configuration. 9 | 10 | .. _bash completion: 11 | 12 | Bash Completion 13 | --------------- 14 | 15 | ``yo`` can provide bash tab completion. Add the following lines to your 16 | ``~/.bashrc`` file to enable it: 17 | 18 | .. code:: 19 | 20 | eval "$(register-python-argcomplete yo)" 21 | 22 | The completer will provide context-sensitive suggestions. For example, if you 23 | type ``yo terminate `` then command-line arguments for terminate will 24 | be suggested, as well as the names of instances which could be terminated. 25 | 26 | Completions may not be 100% accurrate: yo is simply reading cached data about 27 | your OCI resources, which it previously stored. If the cache is out of date (or 28 | you used the OCI console directly) then this could be misleading. But for normal 29 | use, it's fine. 30 | 31 | Zsh Completion 32 | -------------- 33 | 34 | The python argcomplete option should also work for zsh, and includes argument 35 | listings and explanations comparable to the bash completions. However, it lacks 36 | features native to the zsh completion system like tags, descriptions, and 37 | argument groups. 38 | 39 | An alternative zsh completion definition is available in contrib/yo.zsh that 40 | implements a more native zsh experience. Add this file to a directory in your 41 | $fpath and run ``compinit`` again. You may need to delete ~/.zcompdump to pick 42 | up the new completion definition depending on your distribution and 43 | configuration. 44 | 45 | For these completions, ``jq`` is used to parse the yo cache files if it is 46 | installed. Otherwise, argument listings for some words like instances and 47 | shapes are not available. 48 | 49 | Typical user zstyle configurations should now work, E.g.: 50 | 51 | .. code:: 52 | 53 | zstyle ':completion:*' verbose yes 54 | zstyle ':completion:*:yo:*' group-name '' 55 | zstyle ':completion:*:yo:*:descriptions' format '%F{magenta}completing%f: %d' 56 | 57 | Notifications 58 | ------------- 59 | 60 | Sometimes OCI operations (like launching) take a few moments to complete. These 61 | operations can trigger desktop notifications, if you configure them correctly. 62 | See :ref:`Global Configuration Options` for details on configuring the 63 | ``notify_prog``. 64 | -------------------------------------------------------------------------------- /doc/yo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle/yo/97d9378f25208e2a6e4caf698f90159423170474/doc/yo.png -------------------------------------------------------------------------------- /doc/yo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | wheel 4 | -------------------------------------------------------------------------------- /sbom_generation.yaml: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. 2 | 3 | # This OCI DevOps build specification file [1] generates a Software Bill of Materials (SBOM) of the repository. 4 | # The file is needed to run checks for third-party vulnerabilities and business approval according to Oracle's GitHub policies. 5 | # [1] https://docs.oracle.com/en-us/iaas/Content/devops/using/build_specs.htm 6 | 7 | version: 0.1 8 | component: build 9 | timeoutInSeconds: 1000 10 | shell: bash 11 | env: 12 | variables: 13 | PYTHON_CMD: "python3" 14 | CDXGEN_DEBUG_MODE: "debug" 15 | 16 | steps: 17 | - type: Command 18 | name: "Download the version 10.10.0 of cdxgen globally" 19 | command: | 20 | npm install -g @cyclonedx/cdxgen@10.10.0 21 | - type: Command 22 | name: "Workaround to let cdxgen run on nodejs 16" 23 | command: | 24 | # cdxgen relies on a fourth-party dependency that cannot be executed in a Node.js environment running version 16 25 | # (as installed on the build runner instance) 26 | # This is a workaround to ensure cdxgen functions correctly, even in an older Node.js environment. 27 | cd /node/node-v16.14.2-linux-x64/lib/node_modules/@cyclonedx/cdxgen && \ 28 | npm install cheerio@v1.0.0-rc.12 29 | - type: Command 30 | name: "Generate SBOM for Python " 31 | command: | 32 | cdxgen -t python -o artifactSBOM.json --spec-version 1.4 \ 33 | --exclude "*{requirements,dev,test}*{requirements,dev,test}*.txt" --project-name "$(basename $OCI_PRIMARY_SOURCE_URL)" --no-recurse --filter setuptools 34 | outputArtifacts: 35 | - name: artifactSBOM 36 | type: BINARY 37 | location: ${OCI_PRIMARY_SOURCE_DIR}/artifactSBOM.json 38 | -------------------------------------------------------------------------------- /scripts/publish-docs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Publish documentation. Assumes documentation was already generated via the 3 | # "make docs" target. 4 | # 5 | # NOTE: this is only for use by Yo maintainers. It will not work if you don't 6 | # have push access. 7 | 8 | YO_DIR=$(pwd) 9 | TMPDIR=$(mktemp -d) 10 | cleanup() { 11 | cd "$YO_DIR" 12 | rm -rf "$TMPDIR" 13 | } 14 | trap cleanup EXIT 15 | 16 | echo "Preparing gh-pages commit at $TMPDIR" 17 | git clone git@github.com:oracle/yo --depth 1 --branch gh-pages "$TMPDIR" 18 | cd "$TMPDIR" 19 | cp -rT "$YO_DIR/.tox/docs_out/" . 20 | git add . 21 | git commit -m "Automatic documentation update" 22 | git push origin HEAD 23 | -------------------------------------------------------------------------------- /scripts/rebuild_docs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import yo.main 3 | 4 | DIRECTIVE_FMT = """.. argparse:: 5 | :module: yo.main 6 | :func: cmd_{stdname}_args 7 | :prog: yo {name}""" 8 | 9 | 10 | def main(): 11 | cmds = yo.main.YoCmd.iter_commands() 12 | group_to_cmd = {} 13 | for cmd in cmds: 14 | group_to_cmd.setdefault(cmd.group, []).append(cmd) 15 | 16 | for group, cmds in group_to_cmd.items(): 17 | cmds.sort(key=lambda c: c.name) 18 | 19 | trans = str.maketrans(" -", "__") 20 | 21 | for i, group in enumerate(yo.main.COMMAND_GROUP_ORDER): 22 | with open(f"doc/cmds/cmd{i:02d}.rst", "w") as f: 23 | # f.write(f".. _{group}::\n\n") 24 | f.write(group + "\n") 25 | f.write("=" * len(group) + "\n\n") 26 | for j, cmd in enumerate(group_to_cmd[group]): 27 | if j > 0: 28 | f.write("\n\n") 29 | stdname = cmd.name.translate(trans) 30 | directive = DIRECTIVE_FMT.format( 31 | name=cmd.name, 32 | stdname=stdname, 33 | ) 34 | f.write(f".. _yo_{stdname}:\n\n") 35 | f.write(f"yo {cmd.name}\n") 36 | f.write("-" * (len(cmd.name) + 3) + "\n\n") 37 | f.write(directive + "\n") 38 | 39 | with open("doc/cmds/index.rst", "r+") as f: 40 | contents = f.read() 41 | ix = contents.index("Overview\n") 42 | f.seek(ix) 43 | f.truncate(ix) 44 | f.write("Overview\n--------\n\n") 45 | 46 | for group in yo.main.COMMAND_GROUP_ORDER: 47 | f.write(group + ":\n\n") 48 | for cmd in group_to_cmd[group]: 49 | stdname = cmd.name.translate(trans) 50 | help = getattr(cmd, "help", cmd.description.strip()) 51 | f.write(f" - :ref:`yo_{stdname}` - {help}\n") 52 | f.write("\n") 53 | 54 | f.write("Command Group Index\n-------------------\n\n") 55 | f.write(".. toctree::\n :maxdepth: 1\n\n") 56 | for group_idx in range(len(yo.main.COMMAND_GROUP_ORDER)): 57 | f.write(f" cmd{group_idx:02d}\n") 58 | 59 | 60 | if __name__ == "__main__": 61 | main() 62 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_files = LICENSE.txt,THIRD_PARTY_LICENSES.txt 3 | 4 | [flake8] 5 | max-line-length = 80 6 | select = C,E,F,W,B,B950 7 | ignore = E203, E501, W503 8 | 9 | [mypy] 10 | 11 | [mypy-oci.*] 12 | ignore_missing_imports = True 13 | 14 | [mypy-yo.*] 15 | disallow_any_generics = True 16 | disallow_untyped_defs = True 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, 2025, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | import sys 36 | 37 | from setuptools import find_packages 38 | from setuptools import setup 39 | 40 | long_description = open("README.md").read() 41 | 42 | VERSION = "1.10.0" 43 | 44 | additional_requires = [] # type: ignore 45 | 46 | # Add backport for dataclasses, only if we need it 47 | if sys.version_info < (3, 7): 48 | additional_requires.append("dataclasses") 49 | 50 | setup( 51 | name="yo", 52 | version=VERSION, 53 | description="A fast and simple CLI client for managing OCI instances", 54 | long_description=long_description, 55 | long_description_content_type="text/markdown", 56 | install_requires=[ 57 | # TaskProgressColumn 58 | "rich>=12.3.0", 59 | # Older versions of OCI SDK don't have support for things like 60 | # quota_names for Images. While that particular issue was resolved by an 61 | # version prior to 2.85.0, I think there's no reason for me not to 62 | # request a relatively recent version, i.e. the latest as of this 63 | # writing. 64 | "oci>=2.85.0", 65 | "setuptools", 66 | "argcomplete", 67 | ] 68 | + additional_requires, 69 | url="https://github.com/oracle/yo", 70 | author="Oracle", 71 | author_email="stephen.s.brennan@oracle.com", 72 | license="UPL", 73 | packages=find_packages(include=["yo", "yo.*"]), 74 | classifiers=[ 75 | "Programming Language :: Python :: 3", 76 | "License :: OSI Approved :: Universal Permissive License (UPL)", 77 | "Development Status :: 5 - Production/Stable", 78 | "Natural Language :: English", 79 | ], 80 | keywords="oci client", 81 | entry_points={ 82 | "console_scripts": [ 83 | "yo=yo.main:main", 84 | ], 85 | }, 86 | package_data={ 87 | "yo": [ 88 | "data/yo-tasks/*", 89 | "data/sample.yo.ini", 90 | "data/yo_tasklib.sh", 91 | "data/pkgman", 92 | ], 93 | }, 94 | ) 95 | -------------------------------------------------------------------------------- /test-tasks/test-deps: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | MAYBE_DEPENDS_ON test-non-existing-task 37 | MAYBE_DEPENDS_ON test-existing-task 38 | DEPENDS_ON test-task 39 | sleep 10 40 | echo completed 41 | -------------------------------------------------------------------------------- /test-tasks/test-existing-task: -------------------------------------------------------------------------------- 1 | sleep 5 2 | echo completed 3 | -------------------------------------------------------------------------------- /test-tasks/test-prereq: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | PREREQ_FOR test-existing-task 37 | sleep 10 38 | echo completed 39 | -------------------------------------------------------------------------------- /test-tasks/test-run-many: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | #RUN_ONCE 37 | echo test-run-many ran >>~/test.log 38 | -------------------------------------------------------------------------------- /test-tasks/test-task: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | RUN_ONCE 37 | echo hello world 38 | sleep 10 39 | echo hello world 40 | sleep 10 41 | echo hello world 42 | sleep 10 43 | echo done 44 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle/yo/97d9378f25208e2a6e4caf698f90159423170474/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import contextlib 37 | import dataclasses 38 | import datetime 39 | import typing as t 40 | from unittest import mock 41 | 42 | import pytest 43 | 44 | from tests.testing.factories import config_factory 45 | from tests.testing.factories import instance_factory 46 | from tests.testing.factories import NOT_MY_EMAIL 47 | from tests.testing.factories import oci_instance_factory 48 | from tests.testing.factories import oci_instance_fromyo 49 | from tests.testing.factories import short_name 50 | from tests.testing.fake_oci import FakeOCI 51 | from tests.testing.rich import FakeTable 52 | from yo.api import now 53 | from yo.api import YoCtx 54 | from yo.util import YoExc 55 | 56 | 57 | @pytest.fixture 58 | def fake_ctx(tmpdir): 59 | es = contextlib.ExitStack() 60 | with es: 61 | es.enter_context(mock.patch("rich.table.Table", new=FakeTable)) 62 | cache = tmpdir.join("cache.json") 63 | ctx = YoCtx(config_factory(), {}, cache_file=cache) 64 | fake = FakeOCI(ctx) 65 | yield fake, ctx 66 | 67 | 68 | @pytest.fixture 69 | def ctx(fake_ctx): 70 | return fake_ctx[1] 71 | 72 | 73 | @pytest.fixture 74 | def fake(fake_ctx): 75 | return fake_ctx[0] 76 | 77 | 78 | def set_cache( 79 | ctx: YoCtx, kind: str, values: t.List[t.Any], update_age=0, refresh_age=0 80 | ): 81 | under = "_" + kind 82 | assert under in ctx._caches 83 | cache = getattr(ctx, under) 84 | cache.set(values) 85 | if update_age: 86 | cache.last_update = now() - datetime.timedelta(seconds=update_age) 87 | if refresh_age: 88 | cache.last_refresh = now() - datetime.timedelta(seconds=refresh_age) 89 | 90 | 91 | def test_list(ctx: YoCtx, fake: FakeOCI): 92 | fake.compute._instances = [ 93 | oci_instance_factory(created_by=NOT_MY_EMAIL), 94 | oci_instance_factory(display_name="myinstance1", id="myinstance1"), 95 | oci_instance_factory(display_name="myinstance2", id="myinstance2"), 96 | ] 97 | result = ctx.list_instances() 98 | fake.compute.list_instances.assert_called_once() 99 | assert len(result) == 2 100 | assert result[0].name == "myinstance1" 101 | assert result[0].id == "myinstance1" 102 | assert result[1].name == "myinstance2" 103 | assert result[1].id == "myinstance2" 104 | 105 | 106 | @pytest.mark.parametrize("short", [True, False]) 107 | def test_get_instance_by_name_uncached(ctx, fake, short): 108 | inst = instance_factory() 109 | oci_inst = oci_instance_fromyo(inst) 110 | set_cache(ctx, "instances", []) 111 | fake.compute._instances = [oci_inst] 112 | name = short_name(inst.name) if short else inst.name 113 | assert inst == ctx.get_instance_by_name(name, (), ()) 114 | fake.compute.list_instances.assert_called() 115 | assert ctx._instances.get_all() == [inst] 116 | 117 | 118 | @pytest.mark.parametrize("short", [True, False]) 119 | def test_get_instance_by_name_cached(ctx, fake, short): 120 | inst = instance_factory() 121 | set_cache(ctx, "instances", [inst]) 122 | name = short_name(inst.name) if short else inst.name 123 | assert inst == ctx.get_instance_by_name(name, (), ()) 124 | fake.compute.list_instances.assert_not_called() 125 | 126 | 127 | def test_get_instance_by_name_exact_nomatch(ctx, fake): 128 | # We use a name that's the short name for an existing instance, but use 129 | # --exact-name. We expect that we'll get an exception and that it will check 130 | # --the API once the cache lookup fails. 131 | inst = instance_factory() 132 | oci_inst = oci_instance_fromyo(inst) 133 | set_cache(ctx, "instances", [inst]) 134 | sn = short_name(inst.name) 135 | fake.compute._instances = [oci_inst] 136 | with pytest.raises(YoExc, match=f"No instance named {sn}"): 137 | ctx.get_instance_by_name(sn, (), (), exact_name=True) 138 | fake.compute.list_instances.assert_called_once() 139 | 140 | 141 | def test_get_instance_by_name_wrong_state_api_correct(ctx, fake): 142 | # Lookup an instance by shortname, but filter to only RUNNING instances. The 143 | # cached instance is in state STOPPED, but the true API state is RUNNING. We 144 | # expect that yo refreshes the instance list, discovers it is running, and 145 | # returns the updated instance. 146 | inst = instance_factory(state="STOPPED") 147 | inst_run = dataclasses.replace(inst, state="RUNNING") 148 | inst_run_oci = oci_instance_fromyo(inst_run) 149 | set_cache(ctx, "instances", [inst]) 150 | fake.compute._instances = [inst_run_oci] 151 | assert inst_run == ctx.get_instance_by_name( 152 | short_name(inst.name), ("RUNNING"), () 153 | ) 154 | fake.compute.list_instances.assert_called_once() 155 | 156 | 157 | def test_get_instance_by_name_wrong_state(ctx, fake): 158 | # Lookup an instance by shortname, but filter to only RUNNING instances. The 159 | # cached instance is in state STOPPED, but the true API state is RUNNING. We 160 | # expect that yo refreshes the instance list, discovers it is running, and 161 | # returns the updated instance. 162 | inst = instance_factory(state="STOPPED") 163 | inst_run_oci = oci_instance_fromyo(inst) 164 | set_cache(ctx, "instances", [inst]) 165 | fake.compute._instances = [inst_run_oci] 166 | # roughly match the error message 167 | with pytest.raises( 168 | YoExc, 169 | match=rf'Instance named "{inst.name}" found, but its state \(STOPPED\)', 170 | ): 171 | ctx.get_instance_by_name(short_name(inst.name), ("RUNNING"), ()) 172 | fake.compute.list_instances.assert_called_once() 173 | fake.compute.get_instance.assert_not_called() 174 | 175 | 176 | def test_get_instance_by_name_two(ctx, fake): 177 | # Lookup an instance by shortname, but filter to only RUNNING instances. The 178 | # cached instance is in state PROVISIONING (but the true API state is 179 | # RUNNING). Another instance with the same name exists and is in state 180 | # TERMINATED. We expect that yo refreshes the instance list, discovers it 181 | # is running, ignores the old terminated instance, and returns the updated 182 | # correct instance. 183 | inst = instance_factory(state="PROVISIONING") 184 | inst2 = instance_factory(name=inst.name, state="TERMINATED") 185 | inst_run = dataclasses.replace(inst, state="RUNNING") 186 | inst_run_oci = oci_instance_fromyo(inst_run) 187 | inst2_oci = oci_instance_fromyo(inst2) 188 | set_cache(ctx, "instances", [inst, inst2]) 189 | fake.compute._instances = [inst_run_oci, inst2_oci] 190 | assert inst_run == ctx.get_instance_by_name( 191 | short_name(inst.name), ("RUNNING"), () 192 | ) 193 | fake.compute.list_instances.assert_called_once() 194 | fake.compute.get_instance.assert_not_called() 195 | -------------------------------------------------------------------------------- /tests/test_cmds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import contextlib 37 | import dataclasses 38 | from unittest import mock 39 | 40 | import pytest 41 | 42 | from tests.testing.factories import config_factory 43 | from tests.testing.factories import image_factory 44 | from tests.testing.factories import instance_factory 45 | from tests.testing.rich import FakeTable 46 | from yo.main import YoCmd 47 | from yo.util import strftime 48 | 49 | 50 | @pytest.fixture 51 | def mock_ctx(): 52 | es = contextlib.ExitStack() 53 | with es: 54 | mock_ctx = es.enter_context( 55 | mock.patch("yo.main.YoCtx"), 56 | ).return_value 57 | mock_ctx.config = config_factory() 58 | mock_con = es.enter_context( 59 | mock.patch( 60 | "rich.console.Console", 61 | autospec=True, 62 | ) 63 | ).return_value 64 | mock_ctx.con = mock_con 65 | es.enter_context( 66 | mock.patch( 67 | "rich.table.Table", 68 | new=FakeTable, 69 | ) 70 | ) 71 | YoCmd.c = mock_ctx 72 | yield mock_ctx 73 | 74 | 75 | @dataclasses.dataclass 76 | class SshMocks: 77 | ssh_into: mock.Mock 78 | wait: mock.Mock 79 | 80 | 81 | @pytest.fixture(autouse=True) # always prevent SSH 82 | def mock_ssh(): 83 | with contextlib.ExitStack() as es: 84 | ssh_into = es.enter_context( 85 | mock.patch("yo.main.ssh_into", autospec=True) 86 | ) 87 | wait = es.enter_context( 88 | mock.patch("yo.main.wait_for_ssh_access", autospec=True) 89 | ) 90 | yield SshMocks(ssh_into, wait) 91 | 92 | 93 | @pytest.fixture(autouse=True) # always prevent system notifications 94 | def mock_notify(): 95 | with mock.patch("yo.main.send_notification", autospec=True) as notify: 96 | yield notify 97 | 98 | 99 | def test_list_empty(mock_ctx): 100 | mock_ctx.list_instances.return_value = [] 101 | YoCmd.main("blah", args=["list"]) 102 | mock_ctx.list_instances.assert_called_once_with( 103 | verbose=False, show_all=False 104 | ) 105 | mock_ctx.con.print.assert_called_once() 106 | table = mock_ctx.con.print.call_args[0][0] 107 | assert table._columns == ["Name", "Shape", "Mem", "CPU", "State", "Created"] 108 | 109 | 110 | def test_list_results(mock_ctx): 111 | insts = [ 112 | instance_factory(), 113 | instance_factory(), 114 | ] 115 | mock_ctx.list_instances.return_value = insts 116 | YoCmd.main("", args=["list"]) 117 | mock_ctx.list_instances.assert_called_once_with( 118 | verbose=False, show_all=False 119 | ) 120 | mock_ctx.con.print.assert_called_once() 121 | table = mock_ctx.con.print.call_args[0][0] 122 | assert table._columns == ["Name", "Shape", "Mem", "CPU", "State", "Created"] 123 | assert table._rows == [ 124 | ( 125 | insts[0].name, 126 | insts[0].shape, 127 | str(insts[0].memory_gb), 128 | str(insts[0].ocpu), 129 | insts[0].state, 130 | strftime(insts[0].time_created), 131 | ), 132 | ( 133 | insts[1].name, 134 | insts[1].shape, 135 | str(insts[1].memory_gb), 136 | str(insts[1].ocpu), 137 | insts[1].state, 138 | strftime(insts[1].time_created), 139 | ), 140 | ] 141 | 142 | 143 | def test_ssh_one_instance(mock_ctx, mock_ssh): 144 | mock_ctx.get_only_instance.return_value = instance_factory() 145 | mock_ctx.get_image.return_value = image_factory() 146 | mock_ctx.get_instance_ip.return_value = "1.2.3.4" 147 | mock_ctx.get_ssh_user.return_value = "opc" 148 | YoCmd.main("", args=["ssh"]) 149 | mock_ssh.ssh_into.assert_called_once_with( 150 | "1.2.3.4", 151 | "opc", 152 | ctx=mock_ctx, 153 | extra_args=[], 154 | cmds=[], 155 | quiet=False, 156 | ) 157 | # need to ensure that we used get_only_instance() 158 | mock_ctx.get_only_instance.assert_called_once_with( 159 | ("RUNNING", "PROVISIONING", "STARTING"), 160 | (), 161 | ) 162 | mock_ctx.wait_instance_state.assert_not_called() 163 | mock_ssh.wait.assert_not_called() 164 | 165 | 166 | @pytest.mark.parametrize("exact_name", [True, None]) 167 | @pytest.mark.parametrize("dash_n", [True, False]) 168 | def test_ssh_specify(mock_ctx, mock_ssh, exact_name, dash_n): 169 | mock_ctx.get_instance_by_name.return_value = instance_factory() 170 | mock_ctx.get_image.return_value = image_factory() 171 | mock_ctx.get_instance_ip.return_value = "1.2.3.4" 172 | mock_ctx.get_ssh_user.return_value = "opc" 173 | args = ["ssh"] 174 | if dash_n: 175 | args.extend(["-n", "myinst"]) 176 | else: 177 | args.append("myinst") 178 | if exact_name: 179 | args.append("--exact-name") 180 | YoCmd.main("", args=args) 181 | mock_ssh.ssh_into.assert_called_once_with( 182 | "1.2.3.4", 183 | "opc", 184 | ctx=mock_ctx, 185 | extra_args=[], 186 | cmds=[], 187 | quiet=False, 188 | ) 189 | mock_ctx.get_instance_by_name.assert_called_once_with( 190 | "myinst", 191 | ("RUNNING", "PROVISIONING", "STARTING"), 192 | (), 193 | exact_name=exact_name, 194 | ) 195 | mock_ctx.wait_instance_state.assert_not_called() 196 | mock_ssh.wait.assert_not_called() 197 | 198 | 199 | @pytest.mark.parametrize("state", ["RUNNING", "STARTING", "PROVISIONING"]) 200 | def test_ssh_wait(mock_ctx, mock_ssh, mock_notify, state): 201 | inst = instance_factory(state=state, name="myinstance") 202 | mock_ctx.get_only_instance.return_value = inst 203 | mock_ctx.get_image.return_value = image_factory() 204 | mock_ctx.get_instance_ip.return_value = "1.2.3.4" 205 | mock_ctx.get_ssh_user.return_value = "opc" 206 | YoCmd.main("", args=["ssh", "-Aw"]) 207 | if state != "RUNNING": 208 | mock_ctx.wait_instance_state.assert_called_once_with( 209 | inst.id, 210 | "RUNNING", 211 | max_interval_seconds=1, 212 | max_wait_seconds=600, 213 | ) 214 | else: 215 | mock_ctx.wait_instance_state.assert_not_called() 216 | mock_ssh.wait.assert_called_once_with("1.2.3.4", "opc", mock_ctx) 217 | mock_notify.assert_called_once_with( 218 | mock_ctx, "Instance myinstance is connected via SSH!" 219 | ) 220 | mock_ssh.ssh_into.assert_called_once_with( 221 | "1.2.3.4", 222 | "opc", 223 | ctx=mock_ctx, 224 | extra_args=["-A"], 225 | cmds=[], 226 | quiet=False, 227 | ) 228 | -------------------------------------------------------------------------------- /tests/test_tasks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import inspect 3 | import os 4 | import tarfile 5 | import time 6 | from pathlib import Path 7 | from unittest import mock 8 | 9 | import pytest 10 | 11 | from yo.tasks import build_tarball 12 | from yo.tasks import standardize_globs 13 | from yo.tasks import YoTask 14 | from yo.util import YoExc 15 | 16 | 17 | @pytest.fixture(autouse=True) 18 | def mock_all_tasks(): 19 | with mock.patch("yo.tasks.list_tasks") as m: 20 | m.return_value = [ 21 | "task_one", 22 | "task_two", 23 | "task_three", 24 | "task_four", 25 | "task_five", 26 | "task_six", 27 | ] 28 | yield 29 | 30 | 31 | def test_depends_conflicts(): 32 | contents = inspect.cleandoc( 33 | """ 34 | DEPENDS_ON task_one 35 | DEPENDS_ON task_two 36 | MAYBE_DEPENDS_ON task_three 37 | MAYBE_DEPENDS_ON dne_one 38 | CONFLICTS_WITH task_four 39 | CONFLICTS_WITH dne_two 40 | PREREQ_FOR task_five 41 | PREREQ_FOR dne_three 42 | """ 43 | ) 44 | 45 | task = YoTask.create_from_string("test_task", contents) 46 | 47 | # the maybe_depends_on dne_one is dropped since it doesn't exist 48 | assert task.dependencies == ["task_one", "task_two", "task_three"] 49 | # all conflicts are kept 50 | assert task.conflicts == ["task_four", "dne_two"] 51 | # all prereqs are kept 52 | assert task.prereq_for == ["task_five", "dne_three"] 53 | 54 | assert task.script == contents.replace( 55 | "MAYBE_DEPENDS_ON dne_one", 56 | "# MAYBE_DEPENDS_ON dne_one", 57 | ).replace("MAYBE_DEPENDS_ON task_three", "DEPENDS_ON task_three") 58 | 59 | 60 | def test_include_files(): 61 | contents = inspect.cleandoc( 62 | """ 63 | INCLUDE_FILE ~/.profile 64 | INCLUDE_FILE ~/Documents/"Important File.docx" /usr/share/docs/"Important File".docx 65 | INCLUDE_FILE ~/dotfiles/bashrc_oci.sh ~/.bashrc 66 | MAYBE_INCLUDE_FILE ~/.cache/yo.*.json ~/.cache/ 67 | SENDFILE ~/data.tar.gz 68 | SENDFILE ~/"Special File" 69 | """ 70 | ) 71 | 72 | task = YoTask.create_from_string("test_task", contents) 73 | assert task.include_files == [ 74 | ("~/.profile", "~/.profile", False), 75 | ( 76 | "~/Documents/Important File.docx", 77 | "/usr/share/docs/Important File.docx", 78 | False, 79 | ), 80 | ("~/dotfiles/bashrc_oci.sh", "~/.bashrc", False), 81 | ("~/.cache/yo.*.json", "~/.cache/", True), 82 | ] 83 | assert task.sendfiles == [ 84 | Path.home() / "data.tar.gz", 85 | Path.home() / "Special File", 86 | ] 87 | 88 | expected_contents = "\n".join("# " + line for line in contents.split("\n")) 89 | assert task.script == expected_contents 90 | assert task.script == expected_contents 91 | 92 | 93 | def test_wrong_args(): 94 | cases = [ 95 | "INCLUDE_FILE", 96 | "MAYBE_INCLUDE_FILE", 97 | "INCLUDE_FILE one two three", 98 | "MAYBE_INCLUDE_FILE one two three", 99 | "SENDFILE", 100 | "SENDFILE one two", 101 | ] 102 | for case in cases: 103 | with pytest.raises(YoExc): 104 | YoTask.create_from_string("test_task", case) 105 | 106 | 107 | def test_standardize_globs(): 108 | assert standardize_globs( 109 | [ 110 | # The standard: copy to the same path 111 | ("~/.bashrc", "~/.bashrc", False), 112 | # An unusual: copy from homedir to a system path 113 | ("~/.bashrc", "/etc/bashrc", False), 114 | # Also unusual: copy from system path to homedir 115 | ("/etc/bashrc", "~/.bashrc", True), 116 | ] 117 | ) == ( 118 | [ 119 | (str(Path.home() / ".bashrc"), ".bashrc", False), 120 | ("/etc/bashrc", ".bashrc", True), 121 | ], 122 | [ 123 | (str(Path.home() / ".bashrc"), "etc/bashrc", False), 124 | ], 125 | ) 126 | 127 | # neither side can be relative 128 | failing = [ 129 | [("relative path", "~/.bashrc", False)], 130 | [("/foobar", ".bashrc", False)], 131 | ] 132 | for case in failing: 133 | with pytest.raises(YoExc): 134 | standardize_globs(case) 135 | 136 | 137 | def create_test_dir(tmp_path): 138 | # The important cases to cover are: 139 | # 1. A regular file 140 | # 2. A directory being included recursively 141 | # 3. A glob matching several files or directories 142 | tarball = tmp_path / "tarball.tar.gz" 143 | 144 | bashrc = tmp_path / ".bashrc" 145 | bashrc.write_text("export PS2='$ '") 146 | 147 | bash_history = tmp_path / ".bash_history" 148 | bash_history.write_text(":(){ :|:& };:") 149 | 150 | note_dir = tmp_path / "my-notes" 151 | note_dir.mkdir() 152 | note_one = note_dir / "one.txt" 153 | note_one.write_text("some data") 154 | note_two = note_dir / "two.txt" 155 | note_two.write_text("some other data") 156 | unmatched_note = note_dir / "not included.md" 157 | unmatched_note.write_text("I won't be in the tarball") 158 | 159 | doc_dir = tmp_path / "my-docs" 160 | doc_dir.mkdir() 161 | doc_one = doc_dir / ".hidden_document" 162 | doc_one.write_text("test data") 163 | doc_two = doc_dir / "an important, space-filled document title" 164 | doc_two.write_text("more test data!") 165 | doc_subdir = doc_dir / "Project" 166 | doc_subdir.mkdir() 167 | project_doc = doc_subdir / "Rollout plan.md" 168 | project_doc.write_text( 169 | "steps 1: write a plan, step 2: ???, step 3: profit!" 170 | ) 171 | 172 | included = [ 173 | bashrc, 174 | note_one, 175 | note_two, 176 | doc_dir, 177 | doc_one, 178 | doc_two, 179 | doc_subdir, 180 | project_doc, 181 | ] 182 | excluded = [unmatched_note, bash_history] 183 | return tarball, included, excluded 184 | 185 | 186 | def test_build_tarball_skip(tmp_path): 187 | ctx = mock.Mock() 188 | name = "test_task" 189 | 190 | tarball, included, excluded = create_test_dir(tmp_path) 191 | base = str(tmp_path) 192 | include_files = [ 193 | (f"{base}/.bashrc", ".bashrc", False), 194 | (f"{base}/my-notes/*.txt", "notes/", False), 195 | (f"{base}/my-docs/", "docs/", False), 196 | ] 197 | 198 | TEST_TIME = int(time.time()) 199 | 200 | # Set the mtimes to be older than the tarball 201 | for path in included: 202 | os.utime(path, times=(TEST_TIME, TEST_TIME - 5)) 203 | # Set the excluded paths to be newer than the tarball, 204 | # demonstrating that they don't impact the generation. 205 | for path in excluded: 206 | os.utime(path, times=(TEST_TIME, TEST_TIME + 5)) 207 | 208 | tarball.write_text("foobar") 209 | os.utime(tarball, times=(TEST_TIME, TEST_TIME)) 210 | 211 | with mock.patch("yo.tasks.subprocess") as m: 212 | build_tarball(ctx, include_files, tmp_path, tarball, name) 213 | # Subprocess should not have been called at all 214 | assert not m.mock_calls 215 | # Ctx should not have been called at all 216 | assert not ctx.mock_calls 217 | 218 | # ensure that making any file newer results in a rebuilt tarball 219 | for path in included: 220 | os.utime(path, times=(TEST_TIME, TEST_TIME + 5)) 221 | with mock.patch("yo.tasks.subprocess") as m: 222 | build_tarball(ctx, include_files, tmp_path, tarball, name) 223 | assert m.mock_calls 224 | assert ctx.mock_calls 225 | ctx.reset_mock() 226 | os.utime(path, times=(TEST_TIME, TEST_TIME - 5)) 227 | 228 | 229 | def test_build_tarball(tmp_path): 230 | ctx = mock.Mock() 231 | name = "test_task" 232 | 233 | tarball, included, excluded = create_test_dir(tmp_path) 234 | base = str(tmp_path) 235 | include_files = [ 236 | (f"{base}/.bashrc", ".bashrc", False), 237 | (f"{base}/my-notes/*.txt", "notes/", False), 238 | (f"{base}/my-docs/", "docs/", False), 239 | ] 240 | 241 | with mock.patch("yo.tasks.subprocess") as m: 242 | build_tarball(ctx, include_files, tmp_path, tarball, name) 243 | assert len(m.run.mock_calls) == 1 244 | assert m.run.mock_calls[0].args[0][:3] == ["tar", "-czhf", tarball] 245 | assert sorted(m.run.mock_calls[0].args[0][3:]) == sorted( 246 | [".bashrc", "notes/one.txt", "notes/two.txt", "docs"] 247 | ) 248 | ctx.con.log.assert_called() 249 | 250 | 251 | def test_real_tarball(tmp_path): 252 | ctx = mock.Mock() 253 | name = "test_task" 254 | 255 | tarball, included, excluded = create_test_dir(tmp_path) 256 | base = str(tmp_path) 257 | include_files = [ 258 | (f"{base}/.bashrc", ".bashrc", False), 259 | (f"{base}/my-notes/*.txt", "notes/", False), 260 | (f"{base}/my-docs/", "docs/", False), 261 | ] 262 | 263 | build_tarball(ctx, include_files, tmp_path, tarball, name) 264 | ctx.con.log.assert_called() 265 | 266 | tf = tarfile.open(tarball) 267 | expected_members = [ 268 | ".bashrc", 269 | "notes/one.txt", 270 | "notes/two.txt", 271 | "docs", 272 | "docs/.hidden_document", 273 | "docs/an important, space-filled document title", 274 | "docs/Project", 275 | "docs/Project/Rollout plan.md", 276 | ] 277 | assert sorted(tf.getnames()) == sorted(expected_members) 278 | -------------------------------------------------------------------------------- /tests/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle/yo/97d9378f25208e2a6e4caf698f90159423170474/tests/testing/__init__.py -------------------------------------------------------------------------------- /tests/testing/factories.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import datetime 37 | import typing as t 38 | import uuid 39 | from types import SimpleNamespace 40 | 41 | from yo.api import now 42 | from yo.api import TERMPROTECT 43 | from yo.api import YoImage 44 | from yo.api import YoInstance 45 | from yo.util import YoConfig 46 | from yo.util import YoRegion 47 | 48 | 49 | AVAILABILITY_DOMAIN = "VkEH:US-ASHBURN-AD-1" 50 | 51 | SHAPE = "VM.Standard.E2.2" 52 | 53 | MY_EMAIL = "test@example.com" 54 | NOT_MY_EMAIL = "test2@example.com" 55 | 56 | MY_USERNAME = "test" 57 | 58 | _NAME_SEQNO = 1 59 | 60 | 61 | class FakeResponse: 62 | data: t.Any 63 | 64 | def __init__(self, data: t.Any) -> None: 65 | self.data = data 66 | 67 | 68 | def _random_id() -> str: 69 | return str(uuid.uuid4()) 70 | 71 | 72 | def _unique_name() -> str: 73 | global _NAME_SEQNO 74 | name = "{}-vm-{}".format(MY_USERNAME, _NAME_SEQNO) 75 | _NAME_SEQNO += 1 76 | return name 77 | 78 | 79 | def short_name(name: str, username: str = MY_USERNAME) -> str: 80 | assert name.startswith(username + "-") 81 | return name[len(username) + 1 :] 82 | 83 | 84 | def instance_factory(**kwargs) -> YoInstance: 85 | defaults = { 86 | "name": _unique_name(), 87 | "id": _random_id(), 88 | "ad": AVAILABILITY_DOMAIN, 89 | "state": "RUNNING", 90 | "shape": SHAPE, 91 | "memory_gb": 16, 92 | "ocpu": 2, 93 | "time_created": now() - datetime.timedelta(seconds=60), 94 | "image_id": _random_id(), 95 | "termination_protected": False, 96 | "created_by": MY_EMAIL, 97 | "freeform_tags": {TERMPROTECT: "false"}, 98 | "username": None, 99 | } 100 | defaults.update(kwargs) 101 | defaults["defined_tags"] = { 102 | "Oracle-Tags": {"CreatedBy": defaults.pop("created_by")}, 103 | } 104 | return YoInstance(**defaults) # type: ignore 105 | 106 | 107 | def image_factory(**kwargs) -> YoImage: 108 | defaults = { # type: ignore 109 | "name": _unique_name(), 110 | "id": _random_id(), 111 | "ad": AVAILABILITY_DOMAIN, 112 | "state": "AVAILABLE", 113 | "base_image_id": None, 114 | "compartment_id": None, 115 | "display_name": "Dummy-Oracle-Linux-8", 116 | "launch_mode": "foo", 117 | "os": "Oracle Linux", 118 | "os_version": "8", 119 | "size_in_mbs": 123, 120 | "time_created": datetime.datetime.now() 121 | - datetime.timedelta(seconds=60), 122 | "compatibility": {}, 123 | "created_by": None, 124 | } 125 | defaults.update(kwargs) 126 | return YoImage(**defaults) # type: ignore 127 | 128 | 129 | def config_factory(**kwargs) -> YoConfig: 130 | defaults = { 131 | "mtime": 123, 132 | "instance_compartment_id": _random_id(), 133 | "my_email": MY_EMAIL, 134 | "my_username": "test", 135 | "ssh_public_key": "/fake/path/to/id_rsa.pub", 136 | "region": "fake-testing-region", 137 | "list_columns": "Name,Shape,Mem,CPU,State,Created", 138 | "silence_automatic_tag_warning": True, 139 | "regions": { 140 | "fake-testing-region": YoRegion( 141 | name="fake-testing-region", 142 | vcn_id=_random_id(), 143 | subnet_compartment_id=_random_id(), 144 | ), 145 | }, 146 | } 147 | defaults.update(kwargs) 148 | return YoConfig(**defaults) # type: ignore 149 | 150 | 151 | def oci_instance_factory(**kwargs) -> t.Any: 152 | defaults = { 153 | "id": _random_id(), 154 | "display_name": _unique_name(), 155 | "availability_domain": AVAILABILITY_DOMAIN, 156 | "lifecycle_state": "RUNNING", 157 | "time_created": now() - datetime.timedelta(seconds=60), 158 | "image_id": _random_id(), 159 | "shape": SHAPE, 160 | # shape_config 161 | "memory_in_gbs": 16, 162 | "ocpus": 2, 163 | # tags 164 | "created_by": MY_EMAIL, 165 | "freeform_tags": {TERMPROTECT: "false"}, 166 | } 167 | defaults.update(kwargs) 168 | defaults["shape_config"] = SimpleNamespace( 169 | memory_in_gbs=defaults.pop("memory_in_gbs"), 170 | ocpus=defaults.pop("ocpus"), 171 | ) 172 | defaults["defined_tags"] = { 173 | "Oracle-Tags": {"CreatedBy": defaults.pop("created_by")}, 174 | } 175 | return SimpleNamespace(**defaults) 176 | 177 | 178 | def oci_instance_fromyo(i: YoInstance) -> t.Any: 179 | return oci_instance_factory( 180 | id=i.id, 181 | display_name=i.name, 182 | availability_domain=i.ad, 183 | lifecycle_state=i.state, 184 | time_created=i.time_created, 185 | image_id=i.image_id, 186 | shape=i.shape, 187 | memory_in_gbs=i.memory_gb, 188 | ocpus=i.ocpu, 189 | freeform_tags={TERMPROTECT: str(i.termination_protected).lower()}, 190 | ) 191 | -------------------------------------------------------------------------------- /tests/testing/fake_oci.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | """ 37 | Allows us to fake the OCI SDK without getting too tied up mocking functions. 38 | """ 39 | import typing as t 40 | from unittest import mock 41 | 42 | from oci.core.models import Instance 43 | from oci.core.models import VnicAttachment 44 | 45 | from tests.testing.factories import FakeResponse 46 | 47 | 48 | class FakeOCICompute: 49 | """ 50 | Fakes the OCI Compute Client, but with a bit of smarts. 51 | 52 | Essentially, you can load up the compute client with objects that you'd like 53 | it to know about (e.g. to be able to list or get, or verify the existence). 54 | Each function of the compute client is a mock, and they generally have 55 | side effects which perform the validation or return the known objects. 56 | This means that you can still make assertions about arguments, call counts, 57 | etc, but you probably don't need as many assertions and you definitely don't 58 | need to program a fragile set of return values with a specific order. 59 | """ 60 | 61 | def __init__(self): 62 | self._instances: t.List[Instance] = [] 63 | self._vnic_attachments: t.List[VnicAttachment] = [] 64 | 65 | # Create mocks for each f_ method. 66 | for key in dir(self): 67 | if key.startswith("f_"): 68 | setattr( 69 | self, key[2:], mock.Mock(side_effect=getattr(self, key)) 70 | ) 71 | print(f"Create mock {key}") 72 | 73 | # OCI API Calls: 74 | # * Instances: list_instances terminate_instance get_instance 75 | # launch_instance instance_action update_instance 76 | # * Images: get_image list_images 77 | # * Shapes: list_shapes 78 | # * Image/Shape Compat: list_image_shape_compatibility_entries 79 | # * Vnic: list_vnic_attachments 80 | # * Console Conn: create_instance_console_connection 81 | # list_instance_console_connections 82 | # * Boot Vol: list_boot_volume_attachments attach_boot_volume 83 | # detach_boot_volume get_boot_volume_attachment 84 | # * Block Vol: list_volume_attachments attach_volume detach_volume 85 | # get_volume_attachment 86 | # * Console Hist: capture_console_history get_console_history 87 | # get_console_history_content delete_console_history 88 | # * Windows: get_windows_instance_initial_credentials 89 | 90 | def _get_instance(self, inst_id: str) -> Instance: 91 | for instance in self._instances: 92 | if instance.id == inst_id: 93 | return instance 94 | assert False, "Instance ID not present in fake OCI" 95 | 96 | def f_list_instances( 97 | self, compartment_id: str, limit: int = 1000 98 | ) -> FakeResponse: 99 | return FakeResponse(self._instances) 100 | 101 | def f_terminate_instance( 102 | self, inst_id: str, **kwargs: t.Any 103 | ) -> FakeResponse: 104 | instance = self._get_instance(inst_id) 105 | instance.lifecycle_state = "TERMINATING" 106 | return FakeResponse(None) 107 | 108 | def f_get_instance(self, inst_id: str) -> FakeResponse: 109 | return FakeResponse(self._get_instance(inst_id)) 110 | 111 | def f_launch_instance(self, *args: t.Any, **kwargs: t.Any): 112 | pass 113 | 114 | def f_instance_action(self, inst_id: str, action: str) -> FakeResponse: 115 | return FakeResponse(self._get_instance(inst_id)) 116 | 117 | def f_update_instance(self, inst_id: str, details: t.Any) -> FakeResponse: 118 | return FakeResponse(self._get_instance(inst_id)) 119 | 120 | 121 | class FakeOCI: 122 | def __init__(self, ctx): 123 | self.compute = FakeOCICompute() 124 | self.ctx = ctx 125 | 126 | ctx.con = mock.Mock() 127 | ctx._oci = mock.Mock() 128 | ctx._oci.list_call_get_all_results.side_effect = ( 129 | self.list_call_get_all_results 130 | ) 131 | ctx._oci.list_call_get_all_results_generator.side_effect = ( 132 | self.list_call_get_all_results_generator 133 | ) 134 | ctx._vnet = mock.Mock() 135 | ctx._compute = self.compute 136 | 137 | def list_call_get_all_results_generator(self, fn, scope, *args, **kwargs): 138 | assert scope == "record" 139 | return fn(*args, **kwargs).data 140 | 141 | def list_call_get_all_results(self, fn, *args, **kwargs): 142 | return fn(*args, **kwargs) 143 | -------------------------------------------------------------------------------- /tests/testing/rich.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import typing as t 37 | 38 | 39 | class FakeTable: 40 | _columns: t.List[str] 41 | _rows: t.List[t.Collection[str]] 42 | 43 | def __init__(self): 44 | self._columns = [] 45 | self._rows = [] 46 | 47 | def add_column(self, name: str) -> None: 48 | assert not self._rows 49 | self._columns.append(name) 50 | 51 | def add_row(self, *args: str) -> None: 52 | assert len(args) == len(self._columns) 53 | self._rows.append(args) 54 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, Oracle and/or its affiliates. 2 | # 3 | # The Universal Permissive License (UPL), Version 1.0 4 | # 5 | # Subject to the condition set forth below, permission is hereby granted to any 6 | # person obtaining a copy of this software, associated documentation and/or data 7 | # (collectively the "Software"), free of charge and under any and all copyright 8 | # rights in the Software, and any and all patent rights owned or freely 9 | # licensable by each licensor hereunder covering either (i) the unmodified 10 | # Software as contributed to or provided by such licensor, or (ii) the Larger 11 | # Works (as defined below), to deal in both 12 | # 13 | # (a) the Software, and 14 | # (b) any piece of software and/or hardware listed in the 15 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 16 | # Work" to which the Software is contributed by such licensors), 17 | # 18 | # without restriction, including without limitation the rights to copy, create 19 | # derivative works of, display, perform, and distribute the Software and make, 20 | # use, sell, offer for sale, import, export, have made, and have sold the 21 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 22 | # either these or other terms. 23 | # 24 | # This license is subject to the following condition: The above copyright notice 25 | # and either this complete permission notice or at a minimum a reference to the 26 | # UPL must be included in all copies or substantial portions of the Software. 27 | # 28 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 29 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 30 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 31 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 32 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 33 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 34 | # SOFTWARE. 35 | 36 | [tox] 37 | envlist = py37,py38,py39,py310,py311 38 | skip_missing_interpreters = true 39 | 40 | [testenv] 41 | deps = -rrequirements-dev.txt 42 | commands = 43 | pytest tests --cov=yo --cov=tests {posargs} 44 | 45 | [testenv:docs] 46 | deps = 47 | sphinx 48 | sphinx-argparse 49 | commands = sphinx-build -d "{toxworkdir}/docs_doctree" doc "{toxworkdir}/docs_out" --color -W -bhtml {posargs} 50 | python -c 'import pathlib; print("documentation available under file://\{0\}".format(pathlib.Path(r"{toxworkdir}") / "docs_out" / "index.html"))' 51 | -------------------------------------------------------------------------------- /yo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oracle/yo/97d9378f25208e2a6e4caf698f90159423170474/yo/__init__.py -------------------------------------------------------------------------------- /yo/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | from yo.main import main 37 | 38 | 39 | if __name__ == "__main__": 40 | main() 41 | -------------------------------------------------------------------------------- /yo/data/pkgman: -------------------------------------------------------------------------------- 1 | pip 2 | -------------------------------------------------------------------------------- /yo/data/sample.yo.ini: -------------------------------------------------------------------------------- 1 | # These settings are global. 2 | [yo] 3 | 4 | # REQUIRED: Compartment ID in which instances should be created. 5 | # You should normally see the compartment name when you use the OCI console and 6 | # visit the Compute -> Overview page. It will show "Instances in the XXXX 7 | # Compartment". Below are some common instance compartment IDs in the Linux org. 8 | # If yours is not listed, check out the output of "oci iam compartment list" and 9 | # find the ID there. 10 | instance_compartment_id = INSTANCE_COMPARTMENT_ID 11 | 12 | # REQUIRED: Set the OCI region. This takes precedence over anything in 13 | # ~/.oci/config. 14 | # 15 | # Whichever region you choose should be a valid OCI region. There must be a 16 | # corresponding section below, [regions.NAME], which contains region-specific 17 | # configuration information (vcn and subnet). 18 | # 19 | # The region may be overridden on the command line with `yo -r REGION`, in which 20 | # case, a different region (with corresponding config section) will be used. Yo 21 | # maintains separate cache information for each region. Yo commands can only 22 | # operate in one region at a time. 23 | region = us-ashburn-1 24 | 25 | # REQUIRED: The email address below MUST be the one you use to log into OCI. 26 | my_email = EXAMPLE@EXAMPLE.COM 27 | 28 | # REQUIRED: Choose a username to identify you. It will be used in your instance 29 | # names. 30 | my_username = EXAMPLE 31 | 32 | # OPTIONAL: Set your SSH public key (used for console connections) 33 | # ssh_public_key = ~/.ssh/id_rsa.pub 34 | 35 | # OPTIONAL: Set a desired program to launch for VNC (remote desktop). This 36 | # string will be interpreted by your shell, but first Python's format method 37 | # will replace the following strings: 38 | # {host} --> host for VNC connection 39 | # {port} --> port for VNC connection 40 | # The default is given below (the KDE remote desktop client), feel free to 41 | # modify it. 42 | vnc_prog = krdc vnc://{host}:{port} 43 | 44 | # OPTIONAL: Set a desired program to launch to send a notification for various 45 | # events (wait complete, launch complete, etc). If unset, no notifications are 46 | # sent. This string will be split according to shell lexer rules, and any 47 | # argument which contains {message} will be replaced with the notification 48 | # message. 49 | # 50 | # A Linux example: notify-send "yo!" {message} 51 | # A Mac OS X example: osascript -e 'display notification "{message}" with title "yo!"' 52 | # 53 | # notify_prog = COMMAND HERE 54 | 55 | # OPTIONAL: Configures yo's default behavior on termination. When False (the 56 | # default when not specified), the root block device will be deleted when the 57 | # instance is terminated. Your data is not preserved in any way, and the 58 | # instance cannot be recreated later. When true, the root block device will be 59 | # preserved, so that an instance could later be created using the same root 60 | # device. This configuration option simply sets the default behavior. You may 61 | # override this behavior on the command line of yo terminate with either the 62 | # --preserve-volume -p argument, or the --no-preserve-volume -P argument, 63 | # depending on your configuration value. 64 | # 65 | # Note that these block devices are not free, and disk space in a given tenancy 66 | # is limited. You may be asked to reduce your disk usage if you leave lots of 67 | # block volumes around. Further, yo does not (yet) have commands for managing 68 | # block devices. Once you've terminated an instance, preserving its volume, you 69 | # will need to use other tools to delete it. Finally, yo does not (yet) have 70 | # support for launching an instance with a given root device. So you'll need to 71 | # use some other tool to use your preserved volumes in this way. 72 | # 73 | # preserve_volume_on_terminate = false 74 | 75 | # OPTIONAL: oci_profile 76 | # 77 | # Use this configuration to specify the OCI SDK profile name that yo should use. 78 | # 79 | # oci_profile = DEFAULT 80 | 81 | # OPTIONAL: ssh_args 82 | # OPTIONAL: ssh_interactive_args 83 | # 84 | # Use these to customize the SSH commands used. 85 | # 86 | # ssh_interactive_args = -A 87 | 88 | # OPTIONAL: task_dir 89 | # 90 | # Specify the directory on OCI instances where yo tasks are copied and in which 91 | # they execute. The default is /tmp/tasks, but in some cases, this may not be 92 | # suitable (e.g. if /tmp is ephemeral). The setting can't be changed on a 93 | # per-instance basis, but it should be good enough. 94 | # The path should be absolute, and your user should have permission to create 95 | # the directory (and parents) if it does not exist. 96 | # Note: shell home directory syntax cannot be used (~/), but you can use 97 | # $HOME instead. 98 | # 99 | # task_dir = /tmp/tasks 100 | 101 | # OPTIONAL: exact_name 102 | # 103 | # Globally disables Yo's behavior regarding instance and volume naming. 104 | # Normally, yo prefixes names with your system username, and uses an automatic 105 | # incrementing number as a suffix to avoid name collisions. If this config is 106 | # set to "true", then the behavior is disabled: Yo will never modify the name 107 | # you give. 108 | # 109 | # exact_name = false 110 | 111 | # OPTIONAL: check_for_update_every 112 | # 113 | # (Integer, default=6). The minimum number of hours between automatic checks for 114 | # a newer version of Yo. 115 | # 116 | # During each "yo list" operation, if it has been more than this number of hours 117 | # since the previous check, Yo will check in the background for the latest 118 | # version. If there is a newer version, then Yo will print a message about it at 119 | # the end of the operation. Since this occurs in a separate thread during an 120 | # operation which typically takes a few seconds, there's virtually no 121 | # performance impact. 122 | # 123 | # However, you can set this to 0 in order to disable these checks. 124 | # 125 | # check_for_update_every = 6 126 | 127 | # OPTIONAL: list_columns 128 | # 129 | # (String, default: Name,Shape,Mem,CPU,State,Created) 130 | # 131 | # A comma-separated list of column names to include in the table for the `yo 132 | # list` command. You can override this on the command line with the `-C` 133 | # argument. You can add to this on the command line with the `-x` argument. 134 | # 135 | # list_columns = Name,Shape,Mem,CPU,State,Created 136 | 137 | # OPTIONAL: allow_hash_in_config_value 138 | # 139 | # (Boolean, Optional, Default: false) 140 | # 141 | # By default, Yo detects the '#' character within a configuration value, and 142 | # raises an error, becaues this is a common mistake for users. The '#' character 143 | # can only introduce a comment at the beginning of a line. If you use it after 144 | # a config value, it is included in the resulting value, which is usually not 145 | # what you want. However, if there is some case where you actually want to 146 | # include a hash in the config, set this to true to bypass the error. 147 | # 148 | # allow_hash_in_config_value = false 149 | 150 | # OPTIONAL: allow_legacy_imds_endpoints 151 | # 152 | # (Boolean, Optional, Default: false) 153 | # 154 | # The Instance Metadata Service v1 is a less-secure mechanism for OCI to 155 | # retrieve metadata from your instance. If your images support IMDS v2, then v1 156 | # should be disabled. Since virtually all platform images support v2, it is best 157 | # practice at this point to disable v1. 158 | # 159 | # If this causes issues, then you can either set this configuration to "true" to 160 | # use the less-secure option globally, or you can use the 161 | # ``--allow-legacy-imds-endpoints`` flag for ``yo launch`` to use the 162 | # less-secure option for just one instance. 163 | # 164 | # allow_legacy_imds_endpoints = false 165 | 166 | ################################## 167 | # OCI Region Configuration 168 | # 169 | # Any section prefixed with "regions." declares a region configuration. 170 | # These sections specify region-specific information: namely, the VCN and 171 | # subnet. The region you are interacting with can be set by the "region" 172 | # configuration in the [yo] section, or on the command line with: 173 | # 174 | # yo -r REGION ... 175 | # 176 | # You can only use regions which have a corresponding configuration section 177 | # here. 178 | ################################## 179 | [regions.us-ashburn-1] 180 | 181 | # REQUIRED: VCN ID which contains the subnets your instances should be created 182 | # in. 183 | vcn_id = VCN_ID 184 | 185 | ## There are two ways to specify a subnet: either specify the subnet_id (most 186 | ## common), OR specify a compartment which contains subnets. You must configure 187 | ## either subnet_id or subnet_compartment_id. 188 | # 189 | # Option 1: Specify a single subnet ID to use for your instances. 190 | # subnet_id = SUBNET_ID 191 | # 192 | # Option 2: Subnets may change based on which availability domain you're 193 | # operating in. As a result, yo's logic is to list all subnets in the following 194 | # subnet, which belong to the above VCN. If there are multiple, it takes the 195 | # first one. For sustaining engineers, this is the "Networks" compartment 196 | # below. 197 | # subnet_compartment_id = COMPARTMENT_ID 198 | subnet_id = SUBNET_ID 199 | 200 | 201 | ################################## 202 | # Instance Profile Configuration 203 | # 204 | # Any section prefixed with "instances." declares an instance profile. The 205 | # instance profile DEFAULT is required, but you can make additional ones as 206 | # well. 207 | ################################## 208 | [instances.DEFAULT] 209 | 210 | # REQUIRED: set the availability domain for this instance. 211 | # 212 | # This should be set as a simple integer. For instance, instead of 213 | # "VkEH:US-ASHBURN-AD-3", simply specify "3". This ensures that your profile 214 | # will work in any region. 215 | # 216 | # Note that for compatibility, Yo does allow you to specify a full string AD 217 | # name. However, if that AD is not found in the region, then Yo will take the 218 | # trailing integer at the end of the AD name, and use that to select a valid AD 219 | # within the region. 220 | # 221 | # If the value specified is less than or equal to zero, then Yo will randomly 222 | # choose an availability domain each time you launch an instance. If the value 223 | # is larger than the number of ADs present in a given region, then Yo will allow 224 | # the index to silently wrap around (e.g. specifying AD 4 in a region with only 225 | # 3 ADs will result in AD 1 being selected). 226 | availability_domain = VkEH:US-ASHBURN-AD-3 227 | 228 | # REQUIRED: set the shape for this instance 229 | shape = VM.Standard.x86.Generic 230 | 231 | # For flex shapes, this is required: set memory and CPU: 232 | mem = 16 233 | cpu = 1 234 | 235 | # REQUIRED: set the OS for this instance (OS:version) 236 | # Want to see what OS options are available? Run `yo os`. 237 | os = Oracle Linux:9 238 | 239 | # OPTIONAL: set the disk size in GiB. 240 | # boot_volume_size_gbs = 100 241 | # 242 | # Valid values are >=50 and less than or equal to 16384. If unset, then we just 243 | # accept whatever OCI gives us. NOTE: if you use this, your instance may boot 244 | # with mismatched partition table for the disk size. You may want to use 245 | # `service ocid start` to launch the OCI daemon to tune these things. 246 | 247 | # OPTIONAL: set the default name for your instance 248 | # name = ol8-1 249 | # -> Names get automatically prefixed by your username, if they aren't already. 250 | # -> Trailing "-" gets incremented automatically, if an instance already 251 | # exists with a given name. 252 | # -> So this would result in instances named "USERNAME-ol8-1", "USERNAME-ol8-2", 253 | # etc, as you create them. 254 | name = ol9-1 255 | 256 | # OPTIONAL: add tasks which are automatically started on instance launch 257 | # tasks = task_a,task_b 258 | # 259 | # Multiple tasks can be joined by a comma, but do not use a space between them. 260 | # Please note that you cannot remove the tasks specified here via the command 261 | # line. For example, running "yo launch -t task_c" with the above tasks 262 | # configuration will result in ALL THREE tasks running. So only place tasks here 263 | # which you will always want running. 264 | # 265 | # In order to start tasks at launch time, yo must wait for an instance to become 266 | # RUNNING, and for SSH to come up. This means that when you configure the tasks 267 | # field, yo will always block waiting for SSH, regardless of whether you pass 268 | # --wait or --ssh at launch time. 269 | # 270 | # The ocid service is provided by all Oracle Linux images, and it will 271 | # automatically do things like resizing partitions and filesystems when you 272 | # expand a block device, or adding newly attached volumes. You should consider 273 | # keeping this configuration so that your experience using OCI is a bit 274 | # smoother! 275 | tasks = ocid 276 | 277 | # Instances can inherit from other profiles. Even the DEFAULT profile may 278 | # inherit from something else. They need not set any configuration which a 279 | # parent sets, but anything they do set will override the parent. There may be 280 | # any loops in the inheritance tree, but the depth can be arbitrarily large. 281 | [instances.ol6] 282 | inherit = DEFAULT 283 | os = Oracle Linux:6.10 284 | name = ol6-1 285 | 286 | [instances.ol7] 287 | inherit = DEFAULT 288 | os = Oracle Linux:7.9 289 | name = ol7-1 290 | 291 | [instances.ol8] 292 | inherit = DEFAULT 293 | os = Oracle Linux:8 294 | name = ol8-1 295 | 296 | [instances.ol9] 297 | inherit = DEFAULT 298 | 299 | [aliases] 300 | # The aliases section is optional. When provided, it specifies string shortcuts 301 | # for commands. Be careful, as you could overwrite an existing command here. If 302 | # you do not specify an aliases section at all (or if it is empty), then yo will 303 | # simply allow you to use any non-ambiguous prefix as an alias/shortcut for a 304 | # command name. But beware, as new sub-commands are added to yo, the 305 | # non-ambiguous prefixes may change and disrupt your muscle memory. 306 | # 307 | # la = launch 308 | # li = list 309 | # etc... 310 | -------------------------------------------------------------------------------- /yo/data/yo-tasks/drgn: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2025, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | 37 | RUN_ONCE 38 | MAYBE_DEPENDS_ON networking 39 | 40 | # Install kernel-uek-debuginfo package, which allows drgn to debug the running 41 | # kernel. 42 | REPO="https://oss.oracle.com/ol${ORAVER}/debuginfo/" 43 | sudo yum -y install $REPO/kernel-uek-debuginfo{,-common}-$(uname -r).rpm 44 | 45 | if [ "$ORAVER" -le 7 ]; then 46 | echo "Not supported on OL7 or earlier" 47 | exit 1 48 | else 49 | sudo yum -y install drgn 50 | fi 51 | -------------------------------------------------------------------------------- /yo/data/yo-tasks/ocid: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | 37 | sudo systemctl enable --now ocid 38 | -------------------------------------------------------------------------------- /yo/data/yo_tasklib.sh: -------------------------------------------------------------------------------- 1 | ## BEGIN: yo_tasklib.sh - a few helper functions for tasks 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 4 | 5 | TASK_DIR=$$TASK_DIR$$ 6 | 7 | DEPENDS_ON() { 8 | echo "Waiting on task $1" 9 | echo "$1" > ./wait 10 | while ! [ -f "$TASK_DIR/$1/status" ]; do 11 | sleep 1 12 | done 13 | rm -f ./wait 14 | if [ "$(cat "$TASK_DIR/$1/status")" != "0" ]; then 15 | echo "error: dependency task $1 failed" 16 | exit 1 17 | fi 18 | echo "Wait on task $1 completed" 19 | } 20 | 21 | CONFLICTS_WITH() { 22 | true 23 | } 24 | 25 | RUN_ONCE() { 26 | if ! [ -f "./status.old" ]; then 27 | return 0 28 | fi 29 | if [ "$(cat ./status.old)" = "0" ]; then 30 | echo "Task already completed" 31 | exit 0 32 | else 33 | echo "Task has already run unsuccessfully, trying again." 34 | return 0 35 | fi 36 | } 37 | 38 | PREREQ_FOR() { 39 | true 40 | } 41 | 42 | # Operating system information 43 | source /etc/os-release 44 | case "${NAME}" in 45 | Oracle*) 46 | # Setup the "$ORAVER" variable for Oracle Linux. It is a single integer 47 | # number describing the current Oracle Linux distribution release. EG: 48 | # "8", "9", "10", etc. 49 | ORAVER="${VERSION//\.*}" 50 | case "${ORAVER}" in 51 | 6|7) 52 | PKGMGR=yum 53 | ;; 54 | *) 55 | PKGMGR=dnf 56 | ;; 57 | esac 58 | ;; 59 | Ubuntu*) 60 | UBUVER="${VERSION_ID//\.*}" 61 | PKGMGR=apt-get 62 | ;; 63 | Debian*) 64 | DEBVER="$VERSION_ID" 65 | PKGMGR=apt-get 66 | ;; 67 | Fedora*) 68 | FEDVER="$VERSION_ID" 69 | PKGMGR=dnf 70 | ;; 71 | Arch*) 72 | PKGMGR=pacman 73 | ;; 74 | esac 75 | 76 | PKG_INSTALL() { 77 | if [ "$PKGMGR" = "pacman" ]; then 78 | sudo pacman -Sy --noconfirm "$@" 79 | elif [ -n "$PKGMGR" ]; then 80 | sudo $PKGMGR install -y "$@" 81 | else 82 | echo "error: package manager is unknown" 83 | exit 1 84 | fi 85 | } 86 | 87 | ## END: yo_tasklib.sh 88 | -------------------------------------------------------------------------------- /yo/oci.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | """ 37 | Things requiring OCI import 38 | 39 | TODO: this module was created as a hack around how slooooowly the OCI module 40 | loads. It turns out that this is because OCI imports every single service when 41 | you run "import oci", unless you set an environment variable to disable this 42 | (silly) behavior: 43 | 44 | https://docs.oracle.com/en-us/iaas/tools/python/2.79.0/sdk_behaviors/enable_selective_service_imports.html 45 | 46 | We now set this environment variable in yo.main.main, and since the yo.oci 47 | module is lazily loaded (very much on purpose), that will take effect before any 48 | of the imports here. 49 | 50 | However, the 100 milliseconds it takes to load the relevant OCI modules probably 51 | aren't important, so the design decision to stuff all the OCI imports into a 52 | separate module is no longer a great one. In the future, I may get rid of this 53 | monstrosity and need to set the environment variable at the top of the script. 54 | """ 55 | import time 56 | import typing as t 57 | 58 | import oci.core # noqa 59 | import oci.identity # noqa 60 | import oci.limits # noqa 61 | import rich.progress 62 | from oci import wait_until 63 | from oci.base_client import Response 64 | from oci.core.models import AttachBootVolumeDetails # noqa 65 | from oci.core.models import AttachEmulatedVolumeDetails # noqa 66 | from oci.core.models import AttachIScsiVolumeDetails # noqa 67 | from oci.core.models import AttachParavirtualizedVolumeDetails # noqa 68 | from oci.core.models import AttachServiceDeterminedVolumeDetails # noqa 69 | from oci.core.models import CaptureConsoleHistoryDetails # noqa 70 | from oci.core.models import CreateInstanceConsoleConnectionDetails # noqa 71 | from oci.core.models import CreateVolumeDetails # noqa 72 | from oci.core.models import InstanceOptions # noqa 73 | from oci.core.models import InstanceSourceViaBootVolumeDetails # noqa 74 | from oci.core.models import InstanceSourceViaImageDetails # noqa 75 | from oci.core.models import LaunchInstanceDetails # noqa 76 | from oci.core.models import LaunchInstanceShapeConfigDetails # noqa 77 | from oci.core.models import UpdateBootVolumeDetails # noqa 78 | from oci.core.models import UpdateInstanceDetails # noqa 79 | from oci.core.models import UpdateVolumeDetails # noqa 80 | from oci.exceptions import ServiceError # noqa 81 | from oci.exceptions import TransientServiceError # noqa 82 | from oci.pagination import list_call_get_all_results # noqa 83 | from oci.pagination import list_call_get_all_results_generator # noqa 84 | from rich.progress import Progress 85 | 86 | from yo.api import YoCtx 87 | 88 | 89 | __all__ = ["oci"] 90 | 91 | 92 | def wait_until_progress( 93 | ctx: YoCtx, 94 | client: t.Any, 95 | item: Response, 96 | attr: str, 97 | state: str, 98 | max_interval_seconds: int = 1, 99 | max_wait_seconds: int = 600, 100 | wait_callback: t.Optional[t.Callable[[int, Response], None]] = None, 101 | display_name: t.Optional[str] = None, 102 | ) -> Response: 103 | progress = Progress( 104 | rich.progress.TextColumn("{task.description}"), 105 | rich.progress.SpinnerColumn(), 106 | rich.progress.TimeElapsedColumn(), 107 | rich.progress.TextColumn("Timeout in:"), 108 | rich.progress.TimeRemainingColumn(), 109 | console=ctx.con, 110 | ) 111 | with progress: 112 | task = progress.add_task( 113 | "WAIT", start=True, total=max_wait_seconds, finished_time=1 114 | ) 115 | start = time.time() 116 | last_update = start 117 | last_state = getattr(item.data, attr) 118 | item_kind = type(item.data).__name__ 119 | if display_name: 120 | item_str = f"{item_kind} [blue]{display_name}[/blue]" 121 | else: 122 | item_str = f"{item_kind}" 123 | ctx.con.log(f"Wait for {item_str} to enter state [purple]{state}") 124 | ctx.con.log(f"{item_str} starts in state [purple]{last_state}") 125 | 126 | def update(check_count: int, last_response: Response) -> None: 127 | nonlocal last_update, last_state 128 | current = time.time() 129 | progress.advance(task, current - last_update) 130 | last_update = current 131 | current_state = getattr(last_response.data, attr) 132 | if current_state != last_state: 133 | progress.print( 134 | f"{item_str} entered state [purple]{current_state}" 135 | ) 136 | last_state = current_state 137 | if wait_callback: 138 | wait_callback(check_count, last_response) 139 | 140 | resp: Response = wait_until( 141 | client, 142 | item, 143 | attr, 144 | state, 145 | max_interval_seconds=1, 146 | max_wait_seconds=max_wait_seconds, 147 | wait_callback=update, 148 | ) 149 | progress.advance(task, max_wait_seconds) 150 | ctx.con.log(f"{item_str} has reached state [purple]{state}!") 151 | return resp 152 | -------------------------------------------------------------------------------- /yo/ssh.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2025, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import shlex 37 | import subprocess 38 | import sys 39 | import time 40 | import typing as t 41 | 42 | import rich.progress 43 | from rich.progress import Progress 44 | 45 | from yo.api import YoCtx 46 | from yo.util import YoExc 47 | 48 | 49 | PYVER = sys.version_info[:2] 50 | 51 | SSH_OPTIONS = [ 52 | # OCI instances reuse the same pool of IPs. If you use OCI a lot, you'll 53 | # start getting IP collisions and key verification failures. While I'm never 54 | # one to disable security features, this is a case where it just doesn't 55 | # make sense to have it enabled. First, it annoys users who expect yo to 56 | # "just connect", but instead it prompts yes/no, or fails due to 57 | # verification errors. Second, it clutters up the ~/.ssh/known_hosts and 58 | # encourages users to chuck out their existing host keys, because of the OCI 59 | # verification failures. SO, we completely neuter the feature in our config. 60 | "-oCheckHostIP=no", 61 | "-oStrictHostKeyChecking=no", 62 | "-oUpdateHostKeys=no", 63 | "-oUserKnownHostsFile=/dev/null", 64 | # This will suppress warnings regarding "permanently added host key", which 65 | # are pretty pointless given that our known hosts file is /dev/null, and so 66 | # it's not very permanent, is it? 67 | "-oLogLevel=ERROR", 68 | # The following two options are mostly useful for long-running serial 69 | # console connections. But they certainly don't harm things when you're 70 | # keeping SSH open for a while. 71 | "-oServerAliveInterval=60", 72 | "-oTCPKeepAlive=yes", 73 | ] 74 | SSH_CONSOLE_OPTIONS = [ 75 | "-oHostKeyAlgorithms=+ssh-rsa", 76 | # From the OpenSSH 8.5 Changelog: 77 | # 78 | # ssh(1), sshd(8): rename the PubkeyAcceptedKeyTypes keyword to 79 | # PubkeyAcceptedAlgorithms. The previous name incorrectly suggested 80 | # that it control allowed key algorithms, when this option actually 81 | # specifies the signature algorithms that are accepted. The previous 82 | # name remains available as an alias. bz#3253 83 | # 84 | # Thankfully, the alias seems to be available for a while. Unfortunately, 85 | # it means we're using an "undocumented" option in order to retain maximal 86 | # compatibility with OpenSSH versions. 87 | "-oPubkeyAcceptedKeyTypes=+ssh-rsa", 88 | ] 89 | SSH_MINIMUM_TIME = 4 90 | 91 | warned_about_SSH_timeout = False 92 | 93 | 94 | def ssh_args( 95 | ctx: YoCtx, 96 | interactive: bool, 97 | ) -> t.List[str]: 98 | cmd = SSH_OPTIONS.copy() 99 | cmd += shlex.split(ctx.config.ssh_args or "") 100 | if "-i" in cmd: 101 | raise YoExc( 102 | "you have -i configured in ssh_args, but yo now " 103 | "automatically selects the -i value corresponding to" 104 | "your configured SSH key. Please remove it from your" 105 | "configuration." 106 | ) 107 | if ctx.config.ssh_private_key is not None: 108 | cmd.extend(["-i", str(ctx.config.ssh_private_key)]) 109 | if interactive: 110 | cmd += shlex.split(ctx.config.ssh_interactive_args or "") 111 | return cmd 112 | 113 | 114 | def ssh_cmd( 115 | ctx: YoCtx, 116 | target: str, 117 | extra_args: t.Iterable[str] = (), 118 | cmds: t.Iterable[str] = (), 119 | ) -> t.List[str]: 120 | cmds = list(cmds) 121 | cmd = ["ssh"] + ssh_args(ctx, bool(cmds)) 122 | cmd.extend(extra_args) 123 | cmd.append(target) 124 | cmd.extend(cmds) 125 | return cmd 126 | 127 | 128 | def ssh_into( 129 | ip: str, 130 | user: str, 131 | ctx: YoCtx, 132 | extra_args: t.Iterable[str] = (), 133 | cmds: t.Iterable[str] = (), 134 | quiet: bool = False, 135 | **kwargs: t.Any, 136 | ) -> "subprocess.CompletedProcess[bytes]": 137 | """ 138 | Run an SSH command with a user@ip target. Use extra_args before the 139 | hostname, and then add the strings in "cmds" to the command. If cmds is 140 | empty, this will result in SSH to the machine. Otherwise, SSH will interpret 141 | them as a command to execute on the remote system, and then exit. 142 | """ 143 | if not quiet: 144 | ctx.con.print(f"ssh [green]{user}[/green]@[blue]{ip}[/blue]...") 145 | 146 | extra_args = list(extra_args) 147 | cmd = ssh_cmd(ctx, f"{user}@{ip}", extra_args, cmds) 148 | 149 | if extra_args and not quiet: 150 | print("Exact SSH command: {}".format(repr(cmd))) 151 | 152 | # Python 3.6 has no capture_output= kwarg. But we can just implement it 153 | # here. The run() method *will* properly handle reading from the pipe, 154 | # so we don't risk a deadlock. 155 | capture_output = kwargs.get("capture_output") 156 | if capture_output is not None and PYVER == (3, 6): 157 | del kwargs["capture_output"] 158 | if capture_output: 159 | kwargs["stdout"] = subprocess.PIPE 160 | kwargs["stderr"] = subprocess.STDOUT 161 | 162 | return subprocess.run(cmd, **kwargs) 163 | 164 | 165 | def wait_for_ssh_access( 166 | ip: str, 167 | user: str, 168 | ctx: YoCtx, 169 | timeout_sec: int = 600, 170 | ssh_warn_grace: int = 60, 171 | ) -> bool: 172 | global warned_about_SSH_timeout 173 | start_time = last_time = time.time() 174 | progress = Progress( 175 | rich.progress.TextColumn("{task.description}"), 176 | rich.progress.SpinnerColumn(), 177 | rich.progress.TimeElapsedColumn(), 178 | rich.progress.TextColumn("Timeout in:"), 179 | rich.progress.TimeRemainingColumn(), 180 | console=ctx.con, 181 | ) 182 | with progress: 183 | t = progress.add_task( 184 | "Wait for SSH", total=timeout_sec, finished_time=1, start=True 185 | ) 186 | while not progress.finished: 187 | cmd = ssh_cmd(ctx, f"{user}@{ip}", ["-q"], ["true"]) 188 | proc = subprocess.Popen(cmd) 189 | rv = 1 190 | try: 191 | rv = proc.wait(timeout=5) 192 | except subprocess.TimeoutExpired: 193 | proc.terminate() 194 | proc.wait() 195 | if ( 196 | not warned_about_SSH_timeout 197 | and time.time() - start_time >= ssh_warn_grace 198 | ): 199 | ctx.con.log( 200 | "[magenta]Warning:[/magenta] SSH command timed out. " 201 | "This is normal: it may happen early in the boot. " 202 | "But, it can also happen if you're disconnected from " 203 | "a VPN between you and your instance. Double check your " 204 | "connection if this hangs for more than a few minutes." 205 | ) 206 | warned_about_SSH_timeout = True 207 | new_time = time.time() 208 | progress.advance(t, new_time - last_time) 209 | last_time = new_time 210 | if rv == 0: 211 | ctx.con.log("SSH is up!") 212 | return True 213 | return False 214 | -------------------------------------------------------------------------------- /yo/util.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (c) 2023, Oracle and/or its affiliates. 3 | # 4 | # The Universal Permissive License (UPL), Version 1.0 5 | # 6 | # Subject to the condition set forth below, permission is hereby granted to any 7 | # person obtaining a copy of this software, associated documentation and/or data 8 | # (collectively the "Software"), free of charge and under any and all copyright 9 | # rights in the Software, and any and all patent rights owned or freely 10 | # licensable by each licensor hereunder covering either (i) the unmodified 11 | # Software as contributed to or provided by such licensor, or (ii) the Larger 12 | # Works (as defined below), to deal in both 13 | # 14 | # (a) the Software, and 15 | # (b) any piece of software and/or hardware listed in the 16 | # lrgrwrks.txt file if one is included with the Software (each a "Larger 17 | # Work" to which the Software is contributed by such licensors), 18 | # 19 | # without restriction, including without limitation the rights to copy, create 20 | # derivative works of, display, perform, and distribute the Software and make, 21 | # use, sell, offer for sale, import, export, have made, and have sold the 22 | # Software and the Larger Work(s), and to sublicense the foregoing rights on 23 | # either these or other terms. 24 | # 25 | # This license is subject to the following condition: The above copyright notice 26 | # and either this complete permission notice or at a minimum a reference to the 27 | # UPL must be included in all copies or substantial portions of the Software. 28 | # 29 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 30 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 31 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 32 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 33 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 34 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 35 | # SOFTWARE. 36 | import configparser 37 | import dataclasses 38 | import datetime 39 | import os.path 40 | import re 41 | import shlex 42 | import typing as t 43 | import urllib.request 44 | import warnings 45 | from pathlib import Path 46 | 47 | T = t.TypeVar("T") 48 | 49 | _PKGMAN_FILE = os.path.join( 50 | os.path.abspath(os.path.dirname(__file__)), "data/pkgman" 51 | ) 52 | PKGMAN = ( 53 | open(_PKGMAN_FILE).read().strip() if os.path.exists(_PKGMAN_FILE) else "pip" 54 | ) 55 | 56 | 57 | class YoExc(Exception): 58 | pass 59 | 60 | 61 | def opt_strlist(opts: t.Dict[str, t.Any], field: str) -> None: 62 | """ 63 | Helper for parsing string lists in the yo.ini config file. 64 | 65 | Allows strings to be delimited by commas and/or whitespace, including 66 | newlines. If the field exists, split it into a list and set that on the 67 | dictionary. Otherwise, do nothing at all. 68 | 69 | :param opts: option dict 70 | :param field: name of field 71 | """ 72 | val = t.cast(t.Optional[str], opts.get(field)) 73 | if val: 74 | opts[field] = re.split(r"[,\s]+", val.strip(), flags=re.M) 75 | 76 | 77 | def filter_keys( 78 | d: t.Dict[str, t.Any], fields: t.Sequence[str] 79 | ) -> t.Dict[str, t.Any]: 80 | output = {} 81 | for k in fields: 82 | if k in d: 83 | output[k] = d.pop(k) 84 | return output 85 | 86 | 87 | def hasherr(k: str, v: str, sec: str) -> None: 88 | raise YoExc( 89 | f"There is a '#' character in the config entry within section \\[{sec}]:\n" 90 | f" {k} = {v}\n" 91 | "Ini-style configs do not allow comments except on their own line, you\n" 92 | "cannot include a comment after a config value. If this was intended,\n" 93 | "you can bypass this error by setting 'allow_hash_in_config_value = true'\n" 94 | "in the \\[yo] section." 95 | ) 96 | 97 | 98 | @dataclasses.dataclass 99 | class YoRegion: 100 | name: str 101 | vcn_id: str 102 | subnet_id: t.Optional[str] = None 103 | subnet_compartment_id: t.Optional[str] = None 104 | 105 | @classmethod 106 | def from_config_section( 107 | cls, name: str, conf: t.Mapping[str, str] 108 | ) -> "YoRegion": 109 | d = dict(**conf) 110 | d["name"] = name 111 | check_args_dataclass( 112 | cls, d.keys(), f"~/.oci/yo.ini \\[region {name}] section" 113 | ) 114 | return YoRegion(**d) 115 | 116 | def check_hasherr(self) -> None: 117 | for k in ("vcn_id", "subnet_id", "subnet_compartment_id"): 118 | v = getattr(self, k) 119 | if isinstance(v, str) and "#" in v: 120 | hasherr(k, v, "regions." + self.name) 121 | 122 | 123 | @dataclasses.dataclass 124 | class YoConfig: 125 | mtime: float 126 | instance_compartment_id: str 127 | region: str 128 | my_email: str 129 | my_username: str 130 | regions: t.Dict[str, YoRegion] 131 | ssh_public_key: str = "~/.ssh/id_rsa.pub" 132 | rsync_args: t.Optional[str] = None 133 | vnc_prog: str = "krdc vnc://{host}:{port}" 134 | notify_prog: t.Optional[str] = None 135 | oci_profile: str = "DEFAULT" 136 | preserve_volume_on_terminate: t.Optional[bool] = None 137 | ssh_args: t.Optional[str] = None 138 | ssh_interactive_args: t.Optional[str] = None 139 | rdp_prog: t.Optional[str] = None 140 | extension_modules: t.List[str] = dataclasses.field(default_factory=list) 141 | task_dir: str = "/tmp/tasks" 142 | image_compartment_ids: t.List[str] = dataclasses.field(default_factory=list) 143 | silence_automatic_tag_warning: t.Optional[bool] = None 144 | exact_name: t.Optional[bool] = None 145 | resource_filtering: bool = True 146 | check_for_update_every: t.Optional[int] = 6 147 | creator_tags: t.List[str] = dataclasses.field(default_factory=list) 148 | list_columns: str = "Name,Shape,Mem,CPU,State,Created" 149 | allow_hash_in_config_value: bool = False 150 | allow_legacy_imds_endpoints: bool = False 151 | 152 | @property 153 | def vcn_id(self) -> str: 154 | return self.regions[self.region].vcn_id 155 | 156 | @property 157 | def subnet_id(self) -> t.Optional[str]: 158 | return self.regions[self.region].subnet_id 159 | 160 | @property 161 | def subnet_compartment_id(self) -> t.Optional[str]: 162 | return self.regions[self.region].subnet_compartment_id 163 | 164 | @property 165 | def ssh_public_key_full(self) -> str: 166 | path = os.path.expanduser(self.ssh_public_key) 167 | return open(path).read() 168 | 169 | @property 170 | def task_dir_safe(self) -> str: 171 | """ 172 | Return the task_dir processed for insertion into a shell script. 173 | 174 | The task_dir may start with the string "$HOME" or "~", in which case the 175 | path will be prefixed by the home directory. However, this is a special case 176 | handled by yo, and there is no further shell processing allowed on the 177 | task_dir: its value will be escaped for inclusion in the shell script. For 178 | insertion into the script, this value should be used as-is, without any 179 | quotes around it. The best way to do so is to insert a bash variable, and 180 | then use the best practices of quoting bash variables for the remainder of 181 | the script. 182 | 183 | dir={value_returned_from_this_function} 184 | echo "$dir" 185 | 186 | :param task_dir: the configured task_dir 187 | :returns: a final escaped shell token that represents the task_dir 188 | """ 189 | task_dir = self.task_dir.rstrip("/") 190 | use_home = False 191 | if task_dir.startswith("~"): 192 | use_home = True 193 | task_dir = task_dir[1:] 194 | elif task_dir.startswith("$HOME"): 195 | use_home = True 196 | task_dir = task_dir[5:] 197 | task_dir = shlex.quote(task_dir) 198 | if use_home: 199 | task_dir = '"$HOME"' + task_dir 200 | return task_dir 201 | 202 | @property 203 | def ssh_private_key(self) -> t.Union[Path, None]: 204 | private_key = Path(removesuffix(self.ssh_public_key, ".pub")) 205 | private_key = private_key.expanduser() 206 | if private_key.exists(): 207 | return private_key 208 | else: 209 | return None 210 | 211 | @classmethod 212 | def from_config_section( 213 | cls, 214 | conf: configparser.SectionProxy, 215 | regions: t.Dict[str, YoRegion], 216 | mtime: float, 217 | ) -> "YoConfig": 218 | d = dict(**conf) 219 | d["mtime"] = mtime 220 | 221 | d["regions"] = regions 222 | region_conf = filter_keys( 223 | d, ("vcn_id", "subnet_id", "subnet_compartment_id") 224 | ) 225 | check_args_dataclass(cls, d.keys(), "~/.oci/yo.ini \\[yo] section") 226 | if region_conf: 227 | warnings.warn( 228 | "region-specific configurations in [yo] section are deprecated, please update your config to use [regions.*] sections\n" 229 | "See https://oracle.github.io/yo/guide/region.html#migrating-yo-ini-to-multi-region-support" 230 | ) 231 | regions[d["region"]] = YoRegion.from_config_section( 232 | d["region"], region_conf 233 | ) 234 | bools = [ 235 | "preserve_volume_on_terminate", 236 | "silence_automatic_tag_warning", 237 | "exact_name", 238 | "resource_filtering", 239 | "allow_hash_in_config_value", 240 | "allow_legacy_imdc_endpoints", 241 | ] 242 | for b in bools: 243 | if b in d: 244 | d[b] = conf.getboolean(b) 245 | 246 | allow_hash = d.get("allow_hash_in_config_value", False) 247 | if not allow_hash: 248 | for k, v in d.items(): 249 | if isinstance(v, str) and "#" in v: 250 | hasherr(k, v, "yo") 251 | # Region configs are loaded before [yo], so check them once 252 | # we know whether we should be raising an error. 253 | for v in regions.values(): 254 | v.check_hasherr() 255 | if "check_for_update_every" in d: 256 | d["check_for_update_every"] = int(d["check_for_update_every"]) 257 | # OCI stores email addresses as lower case. While most people write 258 | # their email address in lower case, it's not a guarantee. Since we use 259 | # email address to filter OCI resources, it's imperative that the casing 260 | # match. To be safe, lower-case the email. 261 | d["my_email"] = d["my_email"].lower() 262 | opt_strlist(d, "extension_modules") 263 | opt_strlist(d, "image_compartment_ids") 264 | opt_strlist(d, "creator_tags") 265 | return YoConfig(**d) 266 | 267 | @property 268 | def all_creator_tags(self) -> t.Set[str]: 269 | if not hasattr(self, "_all_creator_tags"): 270 | self._all_creator_tags = set( 271 | [self.my_email, f"oracle/{self.my_email}", self.my_username] 272 | + self.creator_tags 273 | ) 274 | return self._all_creator_tags 275 | 276 | 277 | def check_args_dataclass( 278 | klass: t.Any, args: t.Iterable[str], name: str 279 | ) -> None: 280 | """ 281 | Check whether required args are present, and raise error for unknown. 282 | 283 | Dataclasses are pretty nice, but just passing user configuration dicts 284 | directly into their constructors will result in bad error messages for 285 | users. This function can check for missing required arguments or unknown 286 | arguments, and raise a pretty error. 287 | """ 288 | optional = set() 289 | required = set() 290 | for field in dataclasses.fields(klass): 291 | if ( 292 | field.default == dataclasses.MISSING 293 | and field.default_factory == dataclasses.MISSING 294 | ): 295 | required.add(field.name) 296 | else: 297 | optional.add(field.name) 298 | 299 | for arg in args: 300 | if arg in required: 301 | required.remove(arg) 302 | elif arg in optional: 303 | continue 304 | else: 305 | raise YoExc(f'In {name}: unknown configuration "{arg}"') 306 | if required: 307 | missing = ", ".join(required) 308 | raise YoExc(f"In {name}: missing required configurations: {missing}") 309 | 310 | 311 | def one(items: t.List[T], nonemsg: str, multiplemsg: str) -> T: 312 | if len(items) == 0: 313 | raise YoExc(nonemsg) 314 | elif len(items) > 1: 315 | raise YoExc(multiplemsg) 316 | return items[0] 317 | 318 | 319 | def standardize_name( 320 | name: str, exact_name: t.Optional[bool], config: YoConfig 321 | ) -> str: 322 | # When --exact-name is given on the CLI, return name unchanged. 323 | if exact_name: 324 | return name 325 | # When neither --exact-name nor --no-exact-name are given, but the config contains 326 | # an exact_name configuration that's true, return the name unchanged. 327 | # This means that an explicit --no-exact-name (exact_name == False) will fail this 328 | # test and continue with the logic. 329 | if exact_name is None and config.exact_name: 330 | return name 331 | pfx = f"{config.my_username}-" 332 | if not name.startswith(pfx): 333 | name = pfx + name 334 | return name 335 | 336 | 337 | def fmt_allow_deny(allow: t.Collection[str], deny: t.Collection[str]) -> str: 338 | if not allow: 339 | fmt = "any" 340 | else: 341 | fmt = "[{}]".format(", ".join(allow)) 342 | if deny: 343 | fmt += " except [{}]".format(", ".join(deny)) 344 | return fmt 345 | 346 | 347 | def strftime(dt: datetime.datetime) -> str: 348 | return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S") 349 | 350 | 351 | def removesuffix(string: str, suffix: str) -> str: 352 | """ 353 | If string ends with suffix, return it without the suffix. 354 | Replaces str.removesuffix() which is added in Python 3.9. 355 | """ 356 | if string.endswith(suffix): 357 | return string[: len(string) - len(suffix)] 358 | return string 359 | 360 | 361 | def shlex_join(args: t.Iterable[str]) -> str: 362 | return " ".join(shlex.quote(s) for s in args) 363 | 364 | 365 | PYPI_URL = "https://pypi.org/simple/yo/" 366 | UPGRADE_COMMAND = "pip install --upgrade yo" 367 | 368 | 369 | def latest_yo_version() -> t.Optional[t.Tuple[int, int, int]]: 370 | try: 371 | with urllib.request.urlopen(PYPI_URL, timeout=5) as response: 372 | html = response.read().decode("utf-8") 373 | expr = re.compile(r"yo-(\d+)\.(\d+)\.(\d+)") 374 | return max( 375 | [ 376 | (int(m.group(1)), int(m.group(2)), int(m.group(3))) 377 | for m in expr.finditer(html) 378 | ] 379 | ) 380 | except Exception: 381 | return None 382 | 383 | 384 | def current_yo_version() -> t.Tuple[int, int, int]: 385 | import pkg_resources 386 | 387 | ver_str = pkg_resources.get_distribution("yo").version 388 | g1, g2, g3 = ver_str.split(".") 389 | return (int(g1), int(g2), int(g3)) 390 | 391 | 392 | _NATURAL_SORT_RE = re.compile(r"([0-9]+)") 393 | 394 | 395 | def natural_sort(s: str) -> t.List[t.Union[str, int]]: 396 | return [ 397 | int(f) if f and f[0].isdigit() else f for f in _NATURAL_SORT_RE.split(s) 398 | ] 399 | --------------------------------------------------------------------------------