diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 24226de..8b2eafd 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,11 +1,11 @@ --- -name: Lint +name: Super-Linter on: push: - branches: [ master, devel ] + branches: [2.0-alpha-tests] pull_request: - branches: [ master ] + branches: [2.0-alpha-tests] workflow_dispatch: jobs: diff --git a/.github/workflows/publish_image.yml b/.github/workflows/publish_image.yml index f33ce98..fc7e278 100644 --- a/.github/workflows/publish_image.yml +++ b/.github/workflows/publish_image.yml @@ -1,3 +1,4 @@ +--- name: Build and push images to DockerHub on: diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 0b3605c..0de8b23 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [ 3.7, 3.8, 3.9 ] + python-version: [ 3.8, 3.9 ] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/sphinx-build.yml b/.github/workflows/sphinx-build.yml new file mode 100644 index 0000000..807b8d9 --- /dev/null +++ b/.github/workflows/sphinx-build.yml @@ -0,0 +1,35 @@ +name: Sphinx Documentation + +on: + push: + branches: + - main # Adjust this to your default branch if it's not 'main' + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.x + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install sphinx ghp-import + + - name: Build Sphinx documentation + run: | + cd docs + make html + + - name: Deploy to GitHub Pages + run: | + cd docs/_build/html + ghp-import -n -p -f . + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/sphinx-pages.yml b/.github/workflows/sphinx-pages.yml deleted file mode 100644 index 6b44649..0000000 --- a/.github/workflows/sphinx-pages.yml +++ /dev/null @@ -1,24 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Build Sphinx pages - -# Controls when the action will run. -on: - # Triggers the workflow on push or pull request events but only for the master branch - push: - branches: [ master ] - - # Allows you to run this workflow manually from the Actions tab - workflow_dispatch: - -# A workflow run is made up of one or more jobs that can run sequentially or in parallel -jobs: - build: - name: Push Sphinx Pages - runs-on: ubuntu-latest - steps: - - uses: tdviet/sphinx-pages@master - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - create_readme: true - diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..55d039f --- /dev/null +++ b/.pylintrc @@ -0,0 +1,569 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the ignore-list. The +# regex matches against paths and can be in Posix or Windows format. +ignore-paths= + +# Files or directories matching the regex patterns are skipped. The regex +# matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.10 + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the 'python-enchant' package. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear and the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# class is considered mixin if its name matches the mixin-class-rgx option. +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins ignore-mixin- +# members is set to 'yes' +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=no + +# Signatures are removed from the similarity computation +ignore-signatures=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=150 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + vo, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=10 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=20 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..c1049bf --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,22 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.11" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# We recommend specifying your dependencies to enable reproducible builds: +# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html +python: + install: + - requirements: docs/requirements.txt \ No newline at end of file diff --git a/AUTHORS b/AUTHORS index c6ecbe0..4cbeef6 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1,3 +1,4 @@ Viet Tran Enol Fernandez -Levente Farkas \ No newline at end of file +Levente Farkas +Jaromir Klarak \ No newline at end of file diff --git a/FEDCLOUD_CONFIG b/FEDCLOUD_CONFIG new file mode 100644 index 0000000..ff18f94 --- /dev/null +++ b/FEDCLOUD_CONFIG @@ -0,0 +1,25 @@ +_MIN_ACCESS_TOKEN_TIME: 30 +gocdb_public_url: https://goc.egi.eu/gocdbpi/public/ +gocdb_service_group: org.openstack.nova +jaro221: jaro221 +log_config_file: /home/jaro221/.config/fedcloud/logging.conf +log_file: /home/jaro221/.config/fedcloud/logs/fedcloud.log +log_level: DEBUG +min_access_token_time: 30 +mytoken: OOOOO +mytoken_server: https://mytoken.data.kit.edu +oidc_agent_account: egi +oidc_url: https://aai.egi.eu/auth/realms/egi +os_auth_type: v3oidcaccesstoken +os_identity_provider: egi.eu +os_protocol: openid +requests_cert_file: /home/jaro221/.config/fedcloud/cert/certs.pem +site: IISAS-FedCloud +site_dir: /home/jaro221/.config/fedcloud/site-config +site_list_url: https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml +vault_endpoint: https://vault.services.fedcloud.eu:8200 +vault_locker_mount_point: /v1/cubbyhole/ +vault_mount_point: /secrets/ +vault_role: '' +vault_salt: fedcloud_salt +vo: vo.access.egi.eu diff --git a/README.md b/README.md index de97571..3c5893d 100644 --- a/README.md +++ b/README.md @@ -175,23 +175,3 @@ fedcloudclient API is available at You can also execute `fedcloud endpoint projects --site SITE --oidc-access-token ACCESS_TOKEN` to find project IDs of the VOs on the site and add the VOs to local site configuration on your machine manually. - -1. I would like to add supports for additional sites/VOs/identity providers that are not parts of EGI Federated Cloud. - - Other identity providers may be specified via option `--oidc-url` or environment variable `CHECKIN_OIDC_URL`. - Additional sites and VOs may be added to local site configuration files. - -1. Why there are so many options for authentication: access token, refresh token, and oidc-agent? Which one should be - used? - - Cloud operations need only access tokens, not refresh tokens. Access tokens have short lifetime (one hour in EGI - Check-in), so they have lower security constraints. However, they have to be refreshed frequently, that may be - inconvenient for some users. - - If a refresh token is given as parameter to `fedcloud` client (together with client ID and client secret), an access - token will be generated on the fly from the refresh token and client ID/secret. However, using unencrypted refresh - tokens is considered as insecure and will be removed in future versions in favor of oidc-agent. - - [oidc-agent](https://indigo-dc.gitbook.io/oidc-agent/) stores the refresh token securely and will automatically - generate a new access token when the current one expires, so that is the recommended way to provide access token to - fedcloudclient diff --git a/config/sites.yaml b/config/sites.yaml index ae97c16..9dcbf7f 100644 --- a/config/sites.yaml +++ b/config/sites.yaml @@ -1,28 +1,29 @@ -# List of sites in https://github.com/EGI-Foundation/fedcloud-catchall-operations/tree/main/sites +# List of sites in https://github.com/EGI-Federation/fedcloud-catchall-operations/tree/main/sites # As GitHub API has limited access rates, the list of sites is stored here for simple access -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/100IT.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/BIFI.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/CESGA.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/BIFI.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CENI.yaml" - "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CESGA-CLOUD.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/CESNET-MCC.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/CETA-GRID.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/CLOUDIFIN.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/CYFRONET-CLOUD.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/DESY-CC.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CESNET-MCC.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CESNET-MCCG2.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CETA-GRID.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CLOUDIFIN.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CSTCLOUD-EGI.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/CYFRONET-CLOUD.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/DESY-CC.yaml" - "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/GRNET-OPENSTACK.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/GSI-LCG2.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/IFCA-LCG2.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/IISAS-FedCloud-cloud.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/IN2P3-IRES.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/INFN-CATANIA-STACK.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/INFN-CLOUD-BARI.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/INFN-CLOUD-CNAF.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/INFN-PADOVA-STACK.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/NCG-INGRID-PT.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/SCAI.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/TR-FC1-ULAKBIM.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/UA-BITP.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/UNIV-LILLE.yaml" -- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/UPV-GRyCAP.yaml" -- "https://raw.githubusercontent.com/EGI-Foundation/fedcloud-catchall-operations/main/sites/fedcloud.srce.hr.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/IFCA-LCG2.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/IISAS-FedCloud-cloud.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/ILIFU-UCT.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/IN2P3-IRES.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/INFN-CLOUD-BARI.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/INFN-CLOUD-CNAF.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/NCG-INGRID-PT.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/SCAI.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/TR-FC1-ULAKBIM.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/UA-BITP.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/WALTON-CLOUD.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/fedcloud.srce.hr.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/EODC.yaml" +- "https://raw.githubusercontent.com/EGI-Federation/fedcloud-catchall-operations/main/sites/ELKH-CLOUD.yaml" + diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/_build/doctrees/FAQ.doctree b/docs/_build/doctrees/FAQ.doctree new file mode 100644 index 0000000..42c5d77 Binary files /dev/null and b/docs/_build/doctrees/FAQ.doctree differ diff --git a/docs/_build/doctrees/cheat.doctree b/docs/_build/doctrees/cheat.doctree new file mode 100644 index 0000000..3a0611f Binary files /dev/null and b/docs/_build/doctrees/cheat.doctree differ diff --git a/docs/_build/doctrees/development.doctree b/docs/_build/doctrees/development.doctree new file mode 100644 index 0000000..357417a Binary files /dev/null and b/docs/_build/doctrees/development.doctree differ diff --git a/docs/_build/doctrees/environment.pickle b/docs/_build/doctrees/environment.pickle new file mode 100644 index 0000000..43aad7a Binary files /dev/null and b/docs/_build/doctrees/environment.pickle differ diff --git a/docs/_build/doctrees/fedcloudclient.doctree b/docs/_build/doctrees/fedcloudclient.doctree new file mode 100644 index 0000000..9625ed7 Binary files /dev/null and b/docs/_build/doctrees/fedcloudclient.doctree differ diff --git a/docs/_build/doctrees/index.doctree b/docs/_build/doctrees/index.doctree new file mode 100644 index 0000000..34b7829 Binary files /dev/null and b/docs/_build/doctrees/index.doctree differ diff --git a/docs/_build/doctrees/install.doctree b/docs/_build/doctrees/install.doctree new file mode 100644 index 0000000..4591112 Binary files /dev/null and b/docs/_build/doctrees/install.doctree differ diff --git a/docs/_build/doctrees/intro.doctree b/docs/_build/doctrees/intro.doctree new file mode 100644 index 0000000..43944e6 Binary files /dev/null and b/docs/_build/doctrees/intro.doctree differ diff --git a/docs/_build/doctrees/modules.doctree b/docs/_build/doctrees/modules.doctree new file mode 100644 index 0000000..695fb44 Binary files /dev/null and b/docs/_build/doctrees/modules.doctree differ diff --git a/docs/_build/doctrees/quickstart.doctree b/docs/_build/doctrees/quickstart.doctree new file mode 100644 index 0000000..c50f029 Binary files /dev/null and b/docs/_build/doctrees/quickstart.doctree differ diff --git a/docs/_build/doctrees/scripts.doctree b/docs/_build/doctrees/scripts.doctree new file mode 100644 index 0000000..e05014b Binary files /dev/null and b/docs/_build/doctrees/scripts.doctree differ diff --git a/docs/_build/doctrees/usage.doctree b/docs/_build/doctrees/usage.doctree new file mode 100644 index 0000000..bee7440 Binary files /dev/null and b/docs/_build/doctrees/usage.doctree differ diff --git a/docs/_build/html/.buildinfo b/docs/_build/html/.buildinfo new file mode 100644 index 0000000..7e797cd --- /dev/null +++ b/docs/_build/html/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file records the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 227b1f00a398b7ee04ba775a28434087 +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/_build/html/FAQ.html b/docs/_build/html/FAQ.html new file mode 100644 index 0000000..f224051 --- /dev/null +++ b/docs/_build/html/FAQ.html @@ -0,0 +1,140 @@ + + + + + + + + + FAQ and Troubleshooting — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

FAQ and Troubleshooting

+
    +
  1. FedCloud client gives error message “SSL exception connecting to https:// …” when connecting to some sites

  2. +
+

Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message “SSL exception connecting to https:// …”, +follow instructions +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +README.md for more details.

+
$ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh
+$ bash install_certs.sh
+
+
+
    +
  1. FedCloud client frozen during initialization (mainly on virtual machines at some sites)

  2. +
+

It is a known problem of libsodium which is used by oidc-agent Python library. The problem is described +here. Check the entropy on the VMs by executing command +“cat /proc/sys/kernel/random/entropy_avail”, and if the result is lower than 300, install haveged or rng-tools. +On VMs with Centos, you also have to start the daemon manually after installation (or reboot the VMs)

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/_sources/FAQ.rst.txt b/docs/_build/html/_sources/FAQ.rst.txt new file mode 100644 index 0000000..5a5a7f9 --- /dev/null +++ b/docs/_build/html/_sources/FAQ.rst.txt @@ -0,0 +1,29 @@ +FAQ and Troubleshooting +======================= + +1. *FedCloud client* gives error message *"SSL exception connecting to https:// ..."* when connecting to some sites + +Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message *"SSL exception connecting to https:// ..."*, +follow `instructions `_ +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +`README.md `_ for more details. + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + + +2. *FedCloud client* frozen during initialization (mainly on virtual machines at some sites) + +It is a known problem of *libsodium* which is used by *oidc-agent* Python library. The problem is described +`here `_. Check the entropy on the VMs by executing command +*"cat /proc/sys/kernel/random/entropy_avail"*, and if the result is lower than 300, install *haveged* or *rng-tools*. +On VMs with Centos, you also have to start the daemon manually after installation (or reboot the VMs) + + + + + diff --git a/docs/_build/html/_sources/cheat.rst.txt b/docs/_build/html/_sources/cheat.rst.txt new file mode 100644 index 0000000..155d40b --- /dev/null +++ b/docs/_build/html/_sources/cheat.rst.txt @@ -0,0 +1,265 @@ +Cheat sheet +=========== + +See `Tutorial `_ +for more details of commands. + +Local install via pip3 +********************** + +* Create a Python virtual environment: + +:: + + $ python3 -m venv env + +* Activate the virtual environment + +:: + + $ source env/bin/activate + +* Install fedcloudclient via pip: + +:: + + $ pip3 install fedcloudclient + +* Install IGTF certificates: + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + +Using Docker container +********************** + +* Pull the latest version of fedcloudclient container + +:: + + $ sudo docker pull tdviet/fedcloudclient + +* Start fedcloudclient container with oidc-agent account: + +:: + + $ sudo docker run -it -v ~/.config/oidc-agent/egi:/root/.config/oidc-agent/egi --name fedcloud tdviet/fedcloudclient bash + +* Restart previously terminated container: + +:: + + $ sudo docker start -i fedcloud + +Using oidc-agent +**************** + +* Create an oidc-agent account (if not done): + +:: + + $ oidc-gen --pub --issuer https://aai.egi.eu/auth/realms/egi --scope "eduperson_entitlement email" egi + +* Load oidc-agent account and set environment for fedcloudclient: + +:: + + $ eval `oidc-keychain --accounts egi` && export OIDC_AGENT_ACCOUNT=egi + +Basic usages +************ + +* List your VO memberships according to the access token: + +:: + + $ fedcloud token list-vos + +* List sites in the EGI Federated Cloud: + +:: + + $ fedcloud site list + +* List all sites supporting a Virtual Organization in the EGI Federated Cloud: + +:: + + $ fedcloud site list --vo vo.access.egi.eu + +* Execute an OpenStack command: + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + +* Execute an OpenStack command on all sites: + +:: + + $ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu + + +* Print only selected values (for scripting): + +:: + + $ export OS_TOKEN=$(fedcloud openstack --site CESGA --vo vo.access.egi.eu token issue -c id -f value) + +* All-sites commands with full JSON output: + +:: + + $ fedcloud openstack image list --site ALL_SITES --vo eosc-synergy.eu --json-output + + +Searching and selecting resources +********************************* + +* Show all available projects: + +:: + + $ fedcloud endpoint projects --site ALL_SITES + +* Show all Horizon dashboards: + +:: + + $ fedcloud endpoint list --service-type org.openstack.horizon --site ALL_SITES + +* Search images with appliance title in AppDB: + +:: + + $ fedcloud openstack image list --property "dc:title"="Image for EGI Docker [Ubuntu/18.04/VirtualBox]" --site CESNET-MCC --vo eosc-synergy.eu + + +* Select flavors with 2 CPUs and RAM >= 2048 on a site/VO: + +:: + + $ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --vcpus 2 --flavor-specs "RAM>=2048" --output-format list + + +* Select EGI Ubuntu 20.04 images on a site/VO: + +:: + + # Simpler but longer way + $ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ Ubuntu" --image-specs "Name =~ '20.04'" --image-specs "Name =~ EGI" --output-format list + +:: + + # Shorter but more complex regex + $ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ 'EGI.*Ubuntu.*20.04'" --output-format list + + +Mapping and filtering results from OpenStack commands +***************************************************** + +* Select flavors with 2 CPUs: + +:: + + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \ + jq -r '.[].Result[] | select(.VCPUs == 2) | .Name' + +* Select GPU flavors and show their GPU properties on a site: + +:: + + $ fedcloud openstack flavor list --long --site IISAS-FedCloud --vo acc-comp.egi.eu --json-output | \ + jq -r '.[].Result | map(select(.Properties."Accelerator:Type" == "GPU")) | .' + +* Select GPU flavors and show their GPU properties on all sites: + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) | + map(select(.Result | length > 0))' + + +* Construct JSON objects just with site names and flavor names, remove all other properties: + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map({Site:.Site, Flavors:[.Result[].Name]})' + + +Useful commands +*************** + +* Check expiration time of access token (not work for oidc-agent-account): + +:: + + $ fedcloud token check + + +* Set OpenStack environment variables: + +:: + + $ eval $(fedcloud site env --site IISAS-FedCloud --vo vo.access.egi.eu) + + +* List all my own VMs: + +:: + + $ list-all-my-own-vms.sh --vo fedcloud.egi.eu + + +* Activate shell completion + +:: + + # Quick and dirty way (may be resulted in unresponsive shell) + $ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)" + +:: + + # More systematic way + $ wget https://raw.githubusercontent.com/tdviet/fedcloudclient/master/examples/fedcloud_bash_completion.sh + $ source fedcloud_bash_completion.sh + + +* Pass a *mytoken* to Virtual Machines in the EGI Federated Cloud + +:: + + # Create the file "user.txt" with + $ cat user.txt + FEDCLOUD_MYTOKEN= # created on https://mytoken.data.kit.edu/ + + # Pass it to OpenStack + FEDCLOUD_SITE=IISAS-FedCloud + FEDCLOUD_VO=vo.access.egi.eu + fedcloud openstack server create --flavor --image --user-data user.txt --key-name testvm + + # Once you log into the VM you can retrieve the "mytoken" with + curl http://169.254.169.254/openstack/latest/user_data/ + + # and use it with + FEDCLOUD_MYTOKEN= # copied from the previous curl command + fedcloud token check + + +More information +**************** + +* Get help: + +:: + + $ fedcloud --help + $ fedcloud site --help + +* Tutorial `Tutorial `_ diff --git a/docs/_build/html/_sources/development.rst.txt b/docs/_build/html/_sources/development.rst.txt new file mode 100644 index 0000000..e7c43bd --- /dev/null +++ b/docs/_build/html/_sources/development.rst.txt @@ -0,0 +1,39 @@ +Using FedCloud client in Python +=============================== + +FedCloud client can be used as a library for developing other services and tools for EGI Federated Cloud. Most of +functionalities of FedCloud client can be called directly from other codes without side effects. An example of the code +using FedCloud client is available at `GitHub `_. +Just copy/download the code, add your access token and execute *"python demo.py"* to see how it works. + +:: + + # Import FedCloud client library + from fedcloudclient.openstack import fedcloud_openstack + import json + + # Setting values for input parameters: token, site, VO + token = "YOUR_ACCESS_TOKEN" + site = "CYFRONET-CLOUD" + vo = "fedcloud.egi.eu" + + # OpenStack command and options. Must be a tuple + command = ("image", "list", "--long") + + # Execute the OpenStack command on the site/VO with single line of code + # If command finishes correctly, the error_code is 0 and the result is stored + # in JSON object for easy processing + + error_code, result = fedcloud_openstack(token, site, vo, command) + + # Check error code and print the result if OK + if error_code == 0: + print(json.dumps(result, indent=4)) + else: + # If error, result is string containing error message + print("Error message is %s" % result) + + +Read the `FedCloud client API references `_ +for more details about each function in FedCloud client library. Check the output of the equivalent command of +FedCloud client and its source code to see how the function is used. \ No newline at end of file diff --git a/docs/_build/html/_sources/fedcloudclient.rst.txt b/docs/_build/html/_sources/fedcloudclient.rst.txt new file mode 100644 index 0000000..4429eea --- /dev/null +++ b/docs/_build/html/_sources/fedcloudclient.rst.txt @@ -0,0 +1,78 @@ +FedCloud client API references +============================== + + +fedcloudclient.checkin module +----------------------------- + +.. automodule:: fedcloudclient.checkin + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.auth module +-------------------------- + +.. automodule:: fedcloudclient.auth + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.conf module +-------------------------- + +.. automodule:: fedcloudclient.conf + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.endpoint module +------------------------------ + +.. automodule:: fedcloudclient.endpoint + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.sites module +--------------------------- + +.. automodule:: fedcloudclient.sites + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.openstack module +------------------------------- + +.. automodule:: fedcloudclient.openstack + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.logger module +---------------------------- + +.. automodule:: fedcloudclient.logger + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.locker_auth module +--------------------------------- + +.. automodule:: fedcloudclient.locker_auth + :members: + :undoc-members: + :show-inheritance: + + + +fedcloudclient.cli module +------------------------- + +.. automodule:: fedcloudclient.cli + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs/_build/html/_sources/index.rst.txt b/docs/_build/html/_sources/index.rst.txt new file mode 100644 index 0000000..44d4418 --- /dev/null +++ b/docs/_build/html/_sources/index.rst.txt @@ -0,0 +1,13 @@ +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + intro + quickstart + install + usage + development + scripts + modules + FAQ + cheat diff --git a/docs/_build/html/_sources/install.rst.txt b/docs/_build/html/_sources/install.rst.txt new file mode 100644 index 0000000..55033d2 --- /dev/null +++ b/docs/_build/html/_sources/install.rst.txt @@ -0,0 +1,86 @@ +Installation +============ + +Installing FedCloud client with pip +*********************************** + +Simply use the following **pip3** command (should be done without root privileges). + +:: + + $ pip3 install -U fedcloudclient + +Installation Notes +------------------ + +Installing the latest version of the **FedCloud client** package using ``pip3`` will also install all required dependencies (such as **openstackclient**). It will create the executable files **``fedcloud``** and **``openstack``**, placing them in the appropriate directory based on your Python environment: + +* ``$VIRTUAL_ENV/bin`` – when using ``pip3`` inside a Python virtual environment, +* ``~/.local/bin`` – when installing with ``pip3 --user``, +* ``/usr/local/bin`` – when installing system-wide with ``pip3`` as root. + +.. note:: + + If you install the package with the ``--user`` option, make sure that ``~/.local/bin`` is included in your ``$PATH``. + +Verifying the Installation +-------------------------- + +To check if the installation was successful, run the following command:: + +:: + + $ fedcloud --help + + +This will show output:: + +:: + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + CLI main function. Intentionally empty + + Options: + --version Show the version and exit. + --help Show this message and exit. + + Commands: + ec3 EC3 cluster provisioning + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token + + + +Installing EGI Core Trust Anchor certificates +********************************************* + +Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message *"SSL exception connecting to https:// ..."*, +follow `instructions `_ +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +`README.md `_ for more details. + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + +Using FedCloud client via Docker container +****************************************** + +You can use Docker container for testing **FedCloud client** without installation. EGI Core Trust Anchor certificates +and site configurations are preinstalled. + +:: + + $ sudo docker pull tdviet/fedcloudclient + $ sudo docker run -it tdviet/fedcloudclient bash + + + diff --git a/docs/_build/html/_sources/intro.rst.txt b/docs/_build/html/_sources/intro.rst.txt new file mode 100644 index 0000000..2b7301f --- /dev/null +++ b/docs/_build/html/_sources/intro.rst.txt @@ -0,0 +1,81 @@ +Introduction +============ + +.. image:: https://zenodo.org/badge/336671726.svg + :target: https://zenodo.org/badge/latestdoi/336671726 + +FedCloud Client +=============== + +The **FedCloud Client** (`https://fedcloudclient.fedcloud.eu/`) is a high-level Python package providing a unified command-line interface to the EGI Federated Cloud’s OpenStack services. It streamlines tasks such as managing access tokens, discovering services, and executing OpenStack commands across multiple sites. + +Key Features +------------ + +- **Comprehensive Command Set** + Access a broad range of operations, including: + + - Token management (retrieve, inspect, renew) + - Service discovery and endpoint lookup + - Listing and filtering sites, virtual organizations (VOs), and resources + - Direct interaction with OpenStack services (compute, image, network, etc.) + +- **Minimal Syntax, Maximum Power** + Execute any OpenStack action on any site with just three parameters: + + .. code-block:: console + + $ fedcloud openstack --vo --site + + For example, to list all available VM images for the VO ``vo.access.egi.eu`` on the ``IISAS-FedCloud`` site: + + .. code-block:: console + + $ fedcloud openstack image list \ + --vo vo.access.egi.eu \ + --site IISAS-FedCloud + +- **Federation-Wide Scope** + One client, all sites. Use the special ``ALL_SITES`` identifier to run a command across every configured site in the federation: + + .. code-block:: console + + $ fedcloud openstack server list --vo vo.access.egi.eu --site ALL_SITES + +- **Scriptable & Extensible** + Designed for automation and integration: + + - **Shell scripting**: embed ``fedcloud`` calls in CI/CD pipelines or custom scripts. See :doc:`scripts `. + - **Python library**: import and invoke client functionality programmatically. See :doc:`developer guide `. + +Core Modules +------------ + +.. list-table:: + :header-rows: 1 + + * - Module + - Purpose + * - ``fedcloudclient.conf`` + - Manage and validate client configuration (profiles, defaults). + * - ``fedcloudclient.checkin`` + - Interact with EGI Check-in: fetch and renew OpenID Connect tokens. + * - ``fedcloudclient.endpoint`` + - Discover service endpoints via GOCDB; obtain unscoped and scoped tokens from Keystone. + * - ``fedcloudclient.sites`` + - Configure and query site metadata (regions, URLs, capabilities). + * - ``fedcloudclient.openstack`` + - Execute OpenStack CLI commands (``nova``, ``glance``, ``neutron``) against specified sites. + * - ``fedcloudclient.secret`` + - Retrieve and manage secrets from the EGI Vault service. + * - ``fedcloudclient.auth`` + - Validate access tokens and credentials; utilities for checking token expiration and scopes. + +Getting Started +--------------- + +A concise introductory walkthrough is available in this `presentation `_. + +For detailed installation instructions, full usage examples, and API references, see the official documentation: + +`https://fedcloudclient.fedcloud.eu/ `_ diff --git a/docs/_build/html/_sources/modules.rst.txt b/docs/_build/html/_sources/modules.rst.txt new file mode 100644 index 0000000..ab786e8 --- /dev/null +++ b/docs/_build/html/_sources/modules.rst.txt @@ -0,0 +1,7 @@ +fedcloudclient API references +============================= + +.. toctree:: + :maxdepth: 1 + + fedcloudclient/ diff --git a/docs/_build/html/_sources/quickstart.rst.txt b/docs/_build/html/_sources/quickstart.rst.txt new file mode 100644 index 0000000..2f08113 --- /dev/null +++ b/docs/_build/html/_sources/quickstart.rst.txt @@ -0,0 +1,120 @@ +Quick start +=========== + +The `Tutorial `_ +presentation is designed for new users of **FedCloud client**. It starts with the quick setup and basic usages, +then step by step to more advanced scenarios. + +Setup +***** + +* Install FedCloud client via pip: + +:: + + $ pip3 install fedcloudclient + +or use Docker container: + +:: + + $ docker run -it tdviet/fedcloudclient bash + + +* Get a new access token from EGI Check-in according to instructions from + `EGI Check-in Token Portal `_ + or `oidc-agent `_ and set + environment variable. + +:: + + $ export FEDCLOUD_OIDC_ACCESS_TOKEN= + +Basic usages +************ + +* List your VO memberships according to the access token: + +:: + + $ fedcloud token list-vos + eosc-synergy.eu + fedcloud.egi.eu + training.egi.eu + ... + +* List sites in the EGI Federated Cloud + +:: + + $ fedcloud site list + 100IT + BIFI + CESGA + ... + +* List sites supporting a Virtual Organization in the EGI Federated Cloud + +:: + + $ fedcloud site list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + +* Execute an OpenStack command, e.g. list images in eosc-synergy.eu VO on IISAS-FedCloud site + (or other combination of site and VO you have access): + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + Site: IISAS-FedCloud, VO: eosc-synergy.eu + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + +* Execute an OpenStack command, e.g. list VMs in eosc-synergy.eu VO on all sites + and print output in JSON format for further machine processing: + +:: + + $ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu --json-output + [ + { + "Site": "IISAS-FedCloud", + "VO": "eosc-synergy.eu", + "command": "server list", + "Exception": null, + "Error code": 0, + "Result": [ + { + ... + }, + ... + ] + }, + ... + ] + +* Get helps from the client + +:: + + $ fedcloud --help + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + Options: + --help Show this message and exit. + + Commands: + endpoint Endpoint command group for interaction with GOCDB and endpoints + openstack Executing OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + site Site command group for manipulation with site configurations + token Token command group for manipulation with tokens + +* Read the `Tutorial `_ + presentation or next sections for more information. diff --git a/docs/_build/html/_sources/scripts.rst.txt b/docs/_build/html/_sources/scripts.rst.txt new file mode 100644 index 0000000..70cae88 --- /dev/null +++ b/docs/_build/html/_sources/scripts.rst.txt @@ -0,0 +1,95 @@ +Using FedCloud client in scripts +================================ + +FedCloud client can be used in scripts for simple automation, either for setting environment variables for other tools +or processing outputs from OpenStack commands. + +Setting environment variables for external tools +************************************************ + +Outputs from FedCloud client commands for setting environment variables are already in the forms *"export VAR=VALUE"*. +Simple *eval* command in scripts can be used for setting environment variables for external tools: + +:: + + $ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu + export OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + + # This command will set environment variables + $ eval $(fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu) + + # Check the value of the variable + $ echo $OS_AUTH_URL + https://cloud.ui.savba.sk:5000/v3/ + + +Processing JSON outputs from OpenStack commands via jq +****************************************************** + +The outputs from Openstack command can be printed in JSON formats with *--json-output* parameter for further machine +processing. The JSON outputs can be processed in scripts by `jq `_ command. +For examples, if users want to select flavors with 2 CPUs: + +:: + + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output + [ + { + "Site": "IISAS-FedCloud", + "VO": "eosc-synergy.eu", + "command": "flavor list", + "Exception": null, + "Error code": 0, + "Result": [ + { + "ID": "0", + "Name": "m1.nano", + "RAM": 64, + "Disk": 1, + "Ephemeral": 0, + "VCPUs": 1, + "Is Public": true + }, + { + "ID": "2e562a51-8861-40d5-8fc9-2638bab4662c", + "Name": "m1.xlarge", + "RAM": 16384, + "Disk": 40, + "Ephemeral": 0, + "VCPUs": 8, + "Is Public": true + }, + ... + ] + } + ] + + # The following jq command selects flavors with VCPUs=2 and print their names + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \ + jq -r '.[].Result[] | select(.VCPUs == 2) | .Name' + m1.medium + +The following example is more complex: + +* List all flavors in the VO vo.access.egi.eu on all sites and print them in JSON format + +* Filter out sites with error code > 0 + +* Select only GPU flavors + +* Filter out sites with empty list of GPU flavors + +* Print the result (list of all GPU flavors on all sites) in JSON format + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) | + map(select(.Result | length > 0))' + +Note that only OpenStack commands that have outputs can be used with *--json-output*. Using the parameter with +commands without outputs (e.g. setting properties) will generate errors of unsupported parameters. + + diff --git a/docs/_build/html/_sources/usage.rst.txt b/docs/_build/html/_sources/usage.rst.txt new file mode 100644 index 0000000..ac422bd --- /dev/null +++ b/docs/_build/html/_sources/usage.rst.txt @@ -0,0 +1,617 @@ +Usage +===== + +**FedCloud client** has the following main groups of commands: + +* **"fedcloud config"** for handling fedcloudclient configuration, + +* **"fedcloud token"** for interactions with EGI Check-in and access tokens, + +* **"fedcloud endpoint"** for interactions with GOCDB (and site endpoints according to GOCDB), + +* **"fedcloud site"** for manipulations with site configurations, + +* **"fedcloud openstack"** or **"fedcloud openstack-int"** for performing OpenStack commands on sites, + +* **"fedcloud secret"** for accessing secrets in + `Secret management service `_, + +* **"fedcloud auth"** is used to check the validity of access tokens and authentication credential, + + + +Authentication Options +********************** + +**FedCloud** commands require access tokens for authentication. Users have multiple options for providing these tokens: + +- **Direct access token**: Use the ``--oidc-access-token`` option to provide an access token directly. You can retrieve this token from the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``, or pass it explicitly, e.g.: + + ``fedcloud token check --oidc-access-token `` + +- **OIDC agent**: Use the ``--oidc-agent-account`` option to integrate with `oidc-agent `_. For example, check token validity with: + + ``fedcloud token check --oidc-agent-account `` + + To use this method, follow the instructions at `oidc-agent for EGI `_ to register a client, then pass the client (account) name to the FedCloud client. + +- **Mytoken**: Use the ``--mytoken`` option to authenticate with a token from the `Mytoken service `_. To check token validity: + + ``fedcloud token check --mytoken `` + + When creating a Mytoken, ensure you select **"Allows obtaining OpenID Connect Access Tokens"**. You may also use the ``--mytoken-server`` option to authenticate with a specific Mytoken server. + +Alternatively, you can obtain tokens using the `EGI Check-in Token Portal `_, which provides all necessary information for EGI Check-in users. + +In addition to command-line options, environment variables can be used for passing tokens, as summarized in the table below (not shown here). + +By default, the protocol used is ``openid``. This can be changed using the ``--os-protocol`` option. Note that some sites may have a fixed protocol defined in their site configuration (e.g., ``oidc`` for INFN-CLOUD-BARI). + + +Configuration +************* + +Display the current configuration of *fedcloud* with: + +:: + + $ fedcloud config show + +This will show a list of configuration parameters: + ++----------------------------+------------------------------------------------------------------------------------+ +| Parameter | Default value | ++============================+====================================================================================+ +| site | IISAS-FedCloud | ++----------------------------+------------------------------------------------------------------------------------+ +| vo | vo.access.egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ +| site_list_url | https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml | ++----------------------------+------------------------------------------------------------------------------------+ +| site_dir | ${HOME}/.config/fedcloud/site-config | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_url | https://aai.egi.eu/auth/realms/egi | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_public_url | https://goc.egi.eu/gocdbpi/public/ | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_service_group | org.openstack.nova | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_endpoint | https://vault.services.fedcloud.eu:8200 | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_role | | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_mount_point | /secrets/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_locker_mount_point | /v1/cubbyhole/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_salt | fedcloud_salt | ++----------------------------+------------------------------------------------------------------------------------+ +| log_file | ${HOME}/.config/fedcloud/logs/fedcloud.log | ++----------------------------+------------------------------------------------------------------------------------+ +| log_level | DEBUG | ++----------------------------+------------------------------------------------------------------------------------+ +| log_config_file | ${HOME}/.config/fedcloud/logging.conf | ++----------------------------+------------------------------------------------------------------------------------+ +| requests_cert_file | ${HOME}/.config/fedcloud/cert/certs.pem | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_agent_account | egi | ++----------------------------+------------------------------------------------------------------------------------+ +| min_access_token_time | 30 | ++----------------------------+------------------------------------------------------------------------------------+ +| mytoken_server | https://mytoken.data.kit.edu | ++----------------------------+------------------------------------------------------------------------------------+ +| os_protocol | openid | ++----------------------------+------------------------------------------------------------------------------------+ +| os_auth_type | v3oidcaccesstoken | ++----------------------------+------------------------------------------------------------------------------------+ +| os_identity_provider | egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ +| _MIN_ACCESS_TOKEN_TIME | 30 | ++----------------------------+------------------------------------------------------------------------------------+ + + +The **FedCloud client** supports multiple types of configuration: + +- **Default settings** – accessible using **``DEFAULT_SETTINGS``**, these are the built-in default values. +- **Local environment settings** – custom configuration values defined in the environment and loaded via **``env_config``**. +- **Saved configuration settings** – user-defined settings stored in a JSON file, accessible via **``saved_config``**. + +For example, to print the environment configuration, use the following command:: + + $ fedcloud config show --source env_config + + +This command shows, for instance, the following output: + ++-----------------------------+------------------------------------------------------------------+ +| Parameter | Default value | ++=============================+==================================================================+ +| oidc_agent_account | | ++-----------------------------+------------------------------------------------------------------+ +| ... | ... | ++-----------------------------+------------------------------------------------------------------+ + + + +The *fedcloud* configuration can be saved to a file using the following command + +:: + $ fedcloud config create + +By default, the configuration file is saved to **${HOME}/.config/fedcloud/config.yaml**, +but this location can be changed using the ``--config`` option. For example: + +:: + + $ fedcloud config create --config-file /path/to/file.yaml + + +Using Environment Variables and Configuration Priorities +-------------------------------------------------------- + +It is also possible to use the *FEDCLOUD_CONFIG_FILE* environment variable instead of the ``--config`` option in the command line. +This allows users to manage and switch between multiple configuration files—one per project—each with its own settings. + +The *fedcloud* client supports configuration from multiple sources, in the following order of priority (highest to lowest): + +#. **Command-line options** – override all other settings. + Example: ``--site IISAS-FedCloud`` + +#. **Environment variables** – must begin with the prefix ``FEDCLOUD_``. + Example: ``FEDCLOUD_SITE=IISAS-FedCloud`` + +#. **Configuration file** – typically stored as ``config.yaml``. + Example: the ``site`` setting in ``config.yaml`` + +#. **Default configuration** – hardcoded defaults (lowest priority). + See the `source code `_ for details. + +The priority order is important: +default values are overridden by the configuration file, +which is overridden by environment variables, +which are in turn overridden by command-line options. + +For example, the default configuration includes: + +- ``site = IISAS-FedCloud`` +- ``vo = vo.access.egi.eu`` + +These values can be changed using any of the higher-priority methods. For example: + +:: + + $ fedcloud openstack --vo training.egi.eu --site IFCA-LCG2 server list + +or + +:: + + $ export FEDCLOUD_VO=training.egi.eu + $ export FEDCLOUD_SITE=IFCA-LCG2 + $ fedcloud openstack server list + + +Consistent Parameter Naming +--------------------------- + +Note the consistent naming convention for configuration parameters across different sources. For example, the same parameter is represented as: + +* ``--access_token`` in the **command-line** +* ``FEDCLOUD_ACCESS_TOKEN`` as an **environment variable** + + +All configuration parameters follow this consistent mapping across command-line options, environment variables, and configuration files. + +Additional Configurable Parameters +---------------------------------- + +In addition to ``oidc_agent_account``, the following parameters can also be configured in the same way: + +* ``site`` – the OpenStack site to target +* ``vo`` – the Virtual Organisation (VO) +* ``check_in_url`` – the EGI Check-in OIDC endpoint +* ``client_id`` – the OIDC client ID +* ``scopes`` – requested OIDC scopes +* ``access_token`` – manually provided access token +* ``output_format`` – format of output, e.g., ``table``, ``json``, or ``yaml`` + +These parameters can be specified via: + +- command-line options (e.g., ``--site``, ``--vo``), +- environment variables (e.g., ``FEDCLOUD_SITE``, ``FEDCLOUD_VO``), or +- configuration files (e.g., ``site: IISAS-FedCloud`` in ``config.yaml``). + +This design allows flexible and convenient configuration for various usage scenarios. + ++-------------------------------+-------------------------+---------------------------+ +| Environment variable | Command-line option | Configuration parameters | ++===============================+=========================+===========================+ +| FEDCLOUD_OIDC_ACCESS_TOKEN | --oidc-access-token | Not implemented | ++-------------------------------+-------------------------+---------------------------+ +| Not implemented | --mytoken | mytoken | ++-------------------------------+-------------------------+---------------------------+ +| FEDCLOUD_OIDC_AGENT_ACCOUNT | --oidc-agent-account | oidc_agent_account | ++-------------------------------+-------------------------+---------------------------+ + +For convenience, it is recommended to set transient parameters—such as access tokens—via **environment variables**. +This simplifies the usage of *fedcloud* commands by avoiding the need to specify these parameters on the command line each time. + + +Shell completion +**************** + +Shell completion for *fedcloud* command in *bash* can be activated by executing the following command: + +:: + + $ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)" + +The command above may affect responsiveness of the shell. For long work, it is recommended to copy the +`fedcloud_bash_completion.sh script +`_ to a local file, and +source it from ~/.bashrc. Refer `Click documentation +`_ for a long explanation. + +After enabling shell completion, press twice for shell completion: + +:: + + $ fedcloud site + env list save-config show show-project-id + + +fedcloud --help command +*********************** + +* **"fedcloud --help"** command will print help message. When using it in combination with other + commands, e.g. **"fedcloud token --help"**, **"fedcloud token check --hep"**, it will print list of options for the + corresponding commands + +:: + + $ fedcloud --help + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + Options: + --help Show this message and exit. + + Commands: + config Managing fedcloud configurations + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token + + +fedcloud token commands +*********************** + +* **``fedcloud token check``** – Checks the expiration time of the configured access token, + allowing users to determine whether it needs to be refreshed. + +As mentioned earlier, the access token can be provided via the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``. +For this reason, the ``--oidc-access-token`` option is not shown in all examples below, even though it may be required if the token is not set via environment variables. + + +:: + + $ fedcloud token check + +Output is shown as: +:: + Access token is valid to 2021-01-02 01:25:39 UTC + Access token expires in 3571 seconds + + +* **"fedcloud token list-vos"** : Print the list of VO memberships according to EGI Check-in + +:: + + $ fedcloud token list-vos + +Sample output: +:: + eosc-synergy.eu + fedcloud.egi.eu + training.egi.eu + +* **"fedcloud token issue"** : Print the access_token + +:: + + $ fedcloud token issue + +Sample output: +:: + egwergwregrwegreg... + +fedcloud endpoint commands +************************** + +**"fedcloud endpoint"** commands are complementary part of the **"fedcloud site"** commands. Instead of using site +configurations defined in files saved in GitHub repository or local disk, the commands try to get site information +directly from GOCDB (Grid Operations Configuration Management Database) https://goc.egi.eu/ or make probe test on sites + +* **"fedcloud endpoint list"** : List of endpoints of sites defined in GOCDB. + +:: + + $ fedcloud endpoint list + +Sample output: + +:: + Site type URL + ------------------ ------------------ ------------------------------------------------ + IFCA-LCG2 org.openstack.nova https://api.cloud.ifca.es:5000/v3/ + IN2P3-IRES org.openstack.nova https://sbgcloud.in2p3.fr:5000/v3 + ... + + +* **"fedcloud endpoint projects --site --oidc-access-token "** : List of projects to which the owner + of the access token has access at the given site + +:: + + $ fedcloud endpoint projects --site IFCA-LCG2 + id Name enabled site + -------------------------------- -------------------------- --------- --------- + 2a7e2cd4b6dc4e609dd934964c1715c6 VO:demo.fedcloud.egi.eu True IFCA-LCG2 + 3b9754ad8c6046b4aec43ec21abe7d8c VO:eosc-synergy.eu True IFCA-LCG2 + ... + +If the site is set to *ALL_SITES*, or the argument *-a* is used, the command will show accessible projects from all sites of the EGI Federated Cloud. + + +* **"fedcloud endpoint vos --site --oidc-access-token "** : List of Virtual Organisations (VOs) + to which the owner of the access token has access at the given site + +:: + + $ fedcloud endpoint vos --site IFCA-LCG2 + VO id Project name enabled site + ---------------- -------------------------------- ------------------- --------- --------- + vo.access.egi.eu 233f045cb1ff46842a15ebb33af69460 VO:vo.access.egi.eu True IFCA-LCG2 + training.egi.eu d340308880134d04294097524eace710 VO:training.egi.eu True IFCA-LCG2 + ... + +If the site is set to *ALL_SITES*, or the argument *-a* is used, the command will show accessible VOs from all sites of the EGI Federated Cloud. + +:: + + $ fedcloud endpoint vos -a + VO id Project name enabled site + ------------------- -------------------------------- ------------------- --------- ----------------- + vo.access.egi.eu 233f045cb1ff46842a15ebb33af69460 VO:vo.access.egi.eu True IFCA-LCG2 + training.egi.eu d340308880134d04294097524eace710 VO:training.egi.eu True IFCA-LCG2 + vo.access.egi.eu 7101022b9ae74ed9ac1a574497279499 EGI_access True IN2P3-IRES + vo.access.egi.eu 5bbdb5c1e0b2bcbac29904f4ac22dcaa vo_access_egi_eu True UNIV-LILLE + vo.access.egi.eu 4cab325ca8c2495bf2d4e8f230bcd51a VO:vo.access.egi.eu True INFN-PADOVA-STACK + ... + + +* **"fedcloud endpoint token --site --project-id --oidc-access-token "** : Get + OpenStack keystone scoped token on the site for the project ID. + +:: + + $ fedcloud endpoint token --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c + export FEDCLOUD_OS_TOKEN="eayeghjtjtj..." + + +* **"fedcloud endpoint env --site --project-id --oidc-access-token "** : Print + environment variables for working with the project ID on the site. + +:: + + $ fedcloud endpoint env --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c + # environment for IFCA-LCG2 + export FEDCLOUD_OS_AUTH_URL="https://api.cloud.ifca.es:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_ACCESS_TOKEN="..." + + +fedcloud site commands +********************** + +**"fedcloud site"** commands will read site configurations and manipulate with them. If the local site configurations +exist at *~/.config/fedcloud/site-config/*, **fedcloud** will read them from there, otherwise the commands will read +from `GitHub repository `_. + +By default, **fedcloud** does not save anything on local disk, users have to save the site configuration to local disk +explicitly via **"fedcloud site save-config"** command. The advantage of having local +site configurations, beside faster loading, is to give users ability to make customizations, e.g. add additional VOs, +remove sites they do not have access, and so on. + +* **"fedcloud site save-config"** : Read the default site configurations from GitHub + and save them to *~/.config/fedcloud/site-config/* local directory. The command will overwrite existing site configurations + in the local directory. + +:: + + $ fedcloud site save-config + Saving site configs to directory /home/viet/.config/fedcloud/site-config/ + + +After saving site configurations, users can edit and customize them, e.g. remove inaccessible sites, add new +VOs and so on. + +* **"fedcloud site list"** : List of existing sites in the site configurations + +:: + + $ fedcloud site list + 100IT + BIFI + CESGA + ... + + +* **"fedcloud site list --vo "** : List all sites supporting a Virtual Organization + +:: + + $ fedcloud site vo-list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + + +* **"fedcloud site show --site "** : Show configuration of the corresponding site. + +:: + + $ fedcloud site show --site IISAS-FedCloud + endpoint: https://cloud.ui.savba.sk:5000/v3/ + gocdb: IISAS-FedCloud + vos: + - auth: + project_id: a22bbffb007745b2934bf308b0a4d186 + name: covid19.eosc-synergy.eu + - auth: + project_id: 51f736d36ce34b9ebdf196cfcabd24ee + name: eosc-synergy.eu + + +* **"fedcloud site show-project-id --site --vo "**: show the project ID of the VO on the site. + +:: + + $ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + + +* **"fedcloud site env --site --vo "**: set OpenStack environment variable for the VO on the site. + +:: + + $ fedcloud site env --site IISAS-FedCloud --vo eosc-synergy.eu + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + # Remember to set OS_ACCESS_TOKEN, e.g. : + # export FEDCLOUD_OS_ACCESS_TOKEN=`oidc-token egi` + + +The main differences between *"fedcloud endpoint env"* and *"fedcloud site env"* commands are that the second command +needs VO name as input parameter instead of project ID. The command may set also environment variable OS_ACCESS_TOKEN, +if access token is provided, otherwise it will print notification. + + +fedcloud select commands +*************************** + +* **"fedcloud select flavor --site --vo --oidc-access-token --flavor-specs "** : + Select flavor according to the specification in *flavor-specs*. The specifications may be repeated, + e.g. *--flavor-specs "VCPUs==2" --flavor-specs "RAM>=2048"*, or may be joined, e.g. + *--flavor-specs "VCPUs==2 & Disk>10"*. For frequently used specs, short-option alternatives are available, e.g. + *--vcpus 2* is equivalent to *--flavor-specs "VCPUs==2"*. The output is sorted, flavors using less resources + (in the order: GPUs, CPUs, RAM, Disk) are placed on the first places. Users can choose to print only the best-matched + flavor with *--output-format first* (suitable for scripting) or the full list of all matched flavors in list/YAML/JSON + format. + +:: + + $ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --flavor-specs "RAM>=2096" --flavor-specs "Disk > 10" --output-format list + m1.medium + m1.large + m1.xlarge + m1.huge + g1.c08r30-K20m + g1.c16r60-2xK20m + + +* **"fedcloud select image --site --vo --oidc-access-token --image-specs "** : + Select image according to the specification in *image-specs*. The specifications may be repeated, + e.g. *--image-specs "Name=~Ubuntu" --image-specs "Name=~'20.04'"*. The output is sorted, newest images + are placed on the first places. Users can choose to print only the best-matched + image with *--output-format first* (suitable for scripting) or the full list of all matched images in list/YAML/JSON + format. + +:: + + $ fedcloud select image --site INFN-CATANIA-STACK --vo training.egi.eu --image-specs "Name =~ Ubuntu" --output-format list + TRAINING.EGI.EU Image for EGI Docker [Ubuntu/18.04/VirtualBox] + TRAINING.EGI.EU Image for EGI Ubuntu 20.04 [Ubuntu/20.04/VirtualBox] + + +* **"fedcloud select network --site --vo --oidc-access-token --network-specs "** : + Select network according to the specification in *network-specs*. User can choose to select only public or private + network, or both (default). The output is sorted in the order: public, shared, + private. Users can choose to print only the best-matched network with *--output-format first* + (suitable for scripting) or the full list of all matched networks in list/YAML/JSON format. + +:: + + $ fedcloud select network --site IISAS-FedCloud --vo training.egi.eu --network-specs default --output-format list + public-network + private-network + + +fedcloud openstack commands +*************************** + +* **"fedcloud openstack --site --vo --oidc-access-token "** : Execute an + OpenStack command on the site and VO. Examples of OpenStack commands are *"image list"*, *"server list"* and can be used + with additional options for the commands, e.g. *"image list --long"*, *"server list --format json"*. The list of all + OpenStack commands, and their parameters/usages are available + `here `_. + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + Site: IISAS-FedCloud, VO: eosc-synergy.eu + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + + +If the site is *ALL_SITES*, the OpenStack command will be executed on all sites in EGI Federated Cloud. + +* **"fedcloud openstack-int --site --vo --oidc-access-token "** : Call OpenStack client without + command, so users can work with OpenStack site in interactive mode. This is useful when users need to perform multiple + commands successively. For example, users may need get list of images, list of flavors, list of networks before + creating a VM. OIDC authentication is done only once at the beginning, then the keystone token is cached and will + be used for successive commands without authentication via CheckIn again. + +:: + + $ fedcloud openstack-int --site IISAS-FedCloud --vo eosc-synergy.eu + (openstack) image list + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + (openstack) flavor list + +--------------------------------------+-----------+-------+------+-----------+-------+-----------+ + | ID | Name | RAM | Disk | Ephemeral | VCPUs | Is Public | + +--------------------------------------+-----------+-------+------+-----------+-------+-----------+ + | 5bd8397c-b97f-462d-9d2b-5b533844996c | m1.small | 2048 | 10 | 0 | 1 | True | + | df25f80f-ed19-4e0b-805e-d34620ba0334 | m1.medium | 4096 | 40 | 0 | 2 | True | + ... + (openstack) + + +fedcloud config commands +*************************** +* **"fedcloud config --config-file create"** : Create default configuration file in default location for configuration file + + + +fedcloud secret commands +*************************** + +The **"fedcloud secret"** commands are described in details in the documentation of the +`Secret management service `_. diff --git a/docs/_build/html/_static/_sphinx_javascript_frameworks_compat.js b/docs/_build/html/_static/_sphinx_javascript_frameworks_compat.js new file mode 100644 index 0000000..8141580 --- /dev/null +++ b/docs/_build/html/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1,123 @@ +/* Compatability shim for jQuery and underscores.js. + * + * Copyright Sphinx contributors + * Released under the two clause BSD licence + */ + +/** + * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL + */ +jQuery.urldecode = function(x) { + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); +}; + +/** + * small helper function to urlencode strings + */ +jQuery.urlencode = encodeURIComponent; + +/** + * This function returns the parsed url parameters of the + * current request. Multiple values per key are supported, + * it will always return arrays of strings for the value parts. + */ +jQuery.getQueryParameters = function(s) { + if (typeof s === 'undefined') + s = document.location.search; + var parts = s.substr(s.indexOf('?') + 1).split('&'); + var result = {}; + for (var i = 0; i < parts.length; i++) { + var tmp = parts[i].split('=', 2); + var key = jQuery.urldecode(tmp[0]); + var value = jQuery.urldecode(tmp[1]); + if (key in result) + result[key].push(value); + else + result[key] = [value]; + } + return result; +}; + +/** + * highlight a given string on a jquery object by wrapping it in + * span elements with the given class name. + */ +jQuery.fn.highlightText = function(text, className) { + function highlight(node, addItems) { + if (node.nodeType === 3) { + var val = node.nodeValue; + var pos = val.toLowerCase().indexOf(text); + if (pos >= 0 && + !jQuery(node.parentNode).hasClass(className) && + !jQuery(node.parentNode).hasClass("nohighlight")) { + var span; + var isInSVG = jQuery(node).closest("body, svg, foreignObject").is("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.className = className; + } + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + node.parentNode.insertBefore(span, node.parentNode.insertBefore( + document.createTextNode(val.substr(pos + text.length)), + node.nextSibling)); + node.nodeValue = val.substr(0, pos); + if (isInSVG) { + var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect"); + var bbox = node.parentElement.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute('class', className); + addItems.push({ + "parent": node.parentNode, + "target": rect}); + } + } + } + else if (!jQuery(node).is("button, select, textarea")) { + jQuery.each(node.childNodes, function() { + highlight(this, addItems); + }); + } + } + var addItems = []; + var result = this.each(function() { + highlight(this, addItems); + }); + for (var i = 0; i < addItems.length; ++i) { + jQuery(addItems[i].parent).before(addItems[i].target); + } + return result; +}; + +/* + * backward compatibility for jQuery.browser + * This will be supported until firefox bug is fixed. + */ +if (!jQuery.browser) { + jQuery.uaMatch = function(ua) { + ua = ua.toLowerCase(); + + var match = /(chrome)[ \/]([\w.]+)/.exec(ua) || + /(webkit)[ \/]([\w.]+)/.exec(ua) || + /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || + /(msie) ([\w.]+)/.exec(ua) || + ua.indexOf("compatible") < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + []; + + return { + browser: match[ 1 ] || "", + version: match[ 2 ] || "0" + }; + }; + jQuery.browser = {}; + jQuery.browser[jQuery.uaMatch(navigator.userAgent).browser] = true; +} diff --git a/docs/_build/html/_static/basic.css b/docs/_build/html/_static/basic.css new file mode 100644 index 0000000..7ebbd6d --- /dev/null +++ b/docs/_build/html/_static/basic.css @@ -0,0 +1,914 @@ +/* + * Sphinx stylesheet -- basic theme. + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin-top: 10px; +} + +ul.search li { + padding: 5px 0; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +a:visited { + color: #551A8B; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} + +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ + +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} + +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +.sig dd { + margin-top: 0px; + margin-bottom: 0px; +} + +.sig dl { + margin-top: 0px; + margin-bottom: 0px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +.translated { + background-color: rgba(207, 255, 207, 0.2) +} + +.untranslated { + background-color: rgba(255, 207, 207, 0.2) +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/docs/_build/html/_static/css/badge_only.css b/docs/_build/html/_static/css/badge_only.css new file mode 100644 index 0000000..88ba55b --- /dev/null +++ b/docs/_build/html/_static/css/badge_only.css @@ -0,0 +1 @@ +.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#FontAwesome) format("svg")}.fa:before{font-family:FontAwesome;font-style:normal;font-weight:400;line-height:1}.fa:before,a .fa{text-decoration:inherit}.fa:before,a .fa,li .fa{display:inline-block}li .fa-large:before{width:1.875em}ul.fas{list-style-type:none;margin-left:2em;text-indent:-.8em}ul.fas li .fa{width:.8em}ul.fas li .fa-large:before{vertical-align:baseline}.fa-book:before,.icon-book:before{content:"\f02d"}.fa-caret-down:before,.icon-caret-down:before{content:"\f0d7"}.fa-caret-up:before,.icon-caret-up:before{content:"\f0d8"}.fa-caret-left:before,.icon-caret-left:before{content:"\f0d9"}.fa-caret-right:before,.icon-caret-right:before{content:"\f0da"}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60}.rst-versions .rst-current-version:after{clear:both;content:"";display:block}.rst-versions .rst-current-version .fa{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px} \ No newline at end of file diff --git a/docs/_build/html/_static/css/custom.css b/docs/_build/html/_static/css/custom.css new file mode 100644 index 0000000..380f1f2 --- /dev/null +++ b/docs/_build/html/_static/css/custom.css @@ -0,0 +1,23 @@ +/* 1) Classes stay blue */ +dt.sig.sig-class { + background-color: #e8f0fe !important; +} +dt.sig.sig-class .sig-name { + color: #000 !important; +} + +/* 2) “Pure” functions (module-level) go gray */ +dl.py.function dt.sig { + background-color: #f5f5f5 !important; +} +dl.py.function dt.sig .sig-name { + color: #000 !important; +} + +/* 3) Methods (inside classes) also go gray */ +dl.py.method dt.sig { + background-color: #f5f5f5 !important; +} +dl.py.method dt.sig .sig-name { + color: #000 !important; +} \ No newline at end of file diff --git a/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff differ diff --git a/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/Roboto-Slab-Bold.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff differ diff --git a/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/Roboto-Slab-Regular.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot b/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000..e9f60ca Binary files /dev/null and b/docs/_build/html/_static/css/fonts/fontawesome-webfont.eot differ diff --git a/docs/_build/html/_static/css/fonts/fontawesome-webfont.svg b/docs/_build/html/_static/css/fonts/fontawesome-webfont.svg new file mode 100644 index 0000000..855c845 --- /dev/null +++ b/docs/_build/html/_static/css/fonts/fontawesome-webfont.svg @@ -0,0 +1,2671 @@ + + + + +Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 + By ,,, +Copyright Dave Gandy 2016. All rights reserved. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf b/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000..35acda2 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/fontawesome-webfont.ttf differ diff --git a/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000..400014a Binary files /dev/null and b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff differ diff --git a/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 new file mode 100644 index 0000000..4d13fc6 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/fontawesome-webfont.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/lato-bold-italic.woff b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff differ diff --git a/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-bold-italic.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/lato-bold.woff b/docs/_build/html/_static/css/fonts/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-bold.woff differ diff --git a/docs/_build/html/_static/css/fonts/lato-bold.woff2 b/docs/_build/html/_static/css/fonts/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-bold.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/lato-normal-italic.woff b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff differ diff --git a/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-normal-italic.woff2 differ diff --git a/docs/_build/html/_static/css/fonts/lato-normal.woff b/docs/_build/html/_static/css/fonts/lato-normal.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-normal.woff differ diff --git a/docs/_build/html/_static/css/fonts/lato-normal.woff2 b/docs/_build/html/_static/css/fonts/lato-normal.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/docs/_build/html/_static/css/fonts/lato-normal.woff2 differ diff --git a/docs/_build/html/_static/css/theme.css b/docs/_build/html/_static/css/theme.css new file mode 100644 index 0000000..0f14f10 --- /dev/null +++ b/docs/_build/html/_static/css/theme.css @@ -0,0 +1,4 @@ +html{box-sizing:border-box}*,:after,:before{box-sizing:inherit}article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio,canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden],audio:not([controls]){display:none}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}body{margin:0}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:700}blockquote{margin:0}dfn{font-style:italic}ins{background:#ff9;text-decoration:none}ins,mark{color:#000}mark{background:#ff0;font-style:italic;font-weight:700}.rst-content code,.rst-content tt,code,kbd,pre,samp{font-family:monospace,serif;_font-family:courier new,monospace;font-size:1em}pre{white-space:pre}q{quotes:none}q:after,q:before{content:"";content:none}small{font-size:85%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}dl,ol,ul{margin:0;padding:0;list-style:none;list-style-image:none}li{list-style:none}dd{margin:0}img{border:0;-ms-interpolation-mode:bicubic;vertical-align:middle;max-width:100%}svg:not(:root){overflow:hidden}figure,form{margin:0}label{cursor:pointer}button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}button,input{line-height:normal}button,input[type=button],input[type=reset],input[type=submit]{cursor:pointer;-webkit-appearance:button;*overflow:visible}button[disabled],input[disabled]{cursor:default}input[type=search]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}textarea{resize:vertical}table{border-collapse:collapse;border-spacing:0}td{vertical-align:top}.chromeframe{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.ir{display:block;border:0;text-indent:-999em;overflow:hidden;background-color:transparent;background-repeat:no-repeat;text-align:left;direction:ltr;*line-height:0}.ir br{display:none}.hidden{display:none!important;visibility:hidden}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.relative{position:relative}big,small{font-size:100%}@media print{body,html,section{background:none!important}*{box-shadow:none!important;text-shadow:none!important;filter:none!important;-ms-filter:none!important}a,a:visited{text-decoration:underline}.ir a:after,a[href^="#"]:after,a[href^="javascript:"]:after{content:""}blockquote,pre{page-break-inside:avoid}thead{display:table-header-group}img,tr{page-break-inside:avoid}img{max-width:100%!important}@page{margin:.5cm}.rst-content .toctree-wrapper>p.caption,h2,h3,p{orphans:3;widows:3}.rst-content .toctree-wrapper>p.caption,h2,h3{page-break-after:avoid}}.btn,.fa:before,.icon:before,.rst-content .admonition,.rst-content .admonition-title:before,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .code-block-caption .headerlink:before,.rst-content .danger,.rst-content .eqno .headerlink:before,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-alert,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before,input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week],select,textarea{-webkit-font-smoothing:antialiased}.clearfix{*zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713);src:url(fonts/fontawesome-webfont.eot?674f50d287a8c48dc19ba404d20fe713?#iefix&v=4.7.0) format("embedded-opentype"),url(fonts/fontawesome-webfont.woff2?af7ae505a9eed503f8b8e6982036873e) format("woff2"),url(fonts/fontawesome-webfont.woff?fee66e712a8a08eef5805a46892932ad) format("woff"),url(fonts/fontawesome-webfont.ttf?b06871f281fee6b241d60582ae9369b9) format("truetype"),url(fonts/fontawesome-webfont.svg?912ec66d7572ff821749319396470bde#fontawesomeregular) format("svg");font-weight:400;font-style:normal}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14286em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14286em;width:2.14286em;top:.14286em;text-align:center}.fa-li.fa-lg{left:-1.85714em}.fa-border{padding:.2em .25em .15em;border:.08em solid #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa-pull-left.icon,.fa.fa-pull-left,.rst-content .code-block-caption .fa-pull-left.headerlink,.rst-content .eqno .fa-pull-left.headerlink,.rst-content .fa-pull-left.admonition-title,.rst-content code.download span.fa-pull-left:first-child,.rst-content dl dt .fa-pull-left.headerlink,.rst-content h1 .fa-pull-left.headerlink,.rst-content h2 .fa-pull-left.headerlink,.rst-content h3 .fa-pull-left.headerlink,.rst-content h4 .fa-pull-left.headerlink,.rst-content h5 .fa-pull-left.headerlink,.rst-content h6 .fa-pull-left.headerlink,.rst-content p .fa-pull-left.headerlink,.rst-content table>caption .fa-pull-left.headerlink,.rst-content tt.download span.fa-pull-left:first-child,.wy-menu-vertical li.current>a button.fa-pull-left.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-left.toctree-expand,.wy-menu-vertical li button.fa-pull-left.toctree-expand{margin-right:.3em}.fa-pull-right.icon,.fa.fa-pull-right,.rst-content .code-block-caption .fa-pull-right.headerlink,.rst-content .eqno .fa-pull-right.headerlink,.rst-content .fa-pull-right.admonition-title,.rst-content code.download span.fa-pull-right:first-child,.rst-content dl dt .fa-pull-right.headerlink,.rst-content h1 .fa-pull-right.headerlink,.rst-content h2 .fa-pull-right.headerlink,.rst-content h3 .fa-pull-right.headerlink,.rst-content h4 .fa-pull-right.headerlink,.rst-content h5 .fa-pull-right.headerlink,.rst-content h6 .fa-pull-right.headerlink,.rst-content p .fa-pull-right.headerlink,.rst-content table>caption .fa-pull-right.headerlink,.rst-content tt.download span.fa-pull-right:first-child,.wy-menu-vertical li.current>a button.fa-pull-right.toctree-expand,.wy-menu-vertical li.on a button.fa-pull-right.toctree-expand,.wy-menu-vertical li button.fa-pull-right.toctree-expand{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left,.pull-left.icon,.rst-content .code-block-caption .pull-left.headerlink,.rst-content .eqno .pull-left.headerlink,.rst-content .pull-left.admonition-title,.rst-content code.download span.pull-left:first-child,.rst-content dl dt .pull-left.headerlink,.rst-content h1 .pull-left.headerlink,.rst-content h2 .pull-left.headerlink,.rst-content h3 .pull-left.headerlink,.rst-content h4 .pull-left.headerlink,.rst-content h5 .pull-left.headerlink,.rst-content h6 .pull-left.headerlink,.rst-content p .pull-left.headerlink,.rst-content table>caption .pull-left.headerlink,.rst-content tt.download span.pull-left:first-child,.wy-menu-vertical li.current>a button.pull-left.toctree-expand,.wy-menu-vertical li.on a button.pull-left.toctree-expand,.wy-menu-vertical li button.pull-left.toctree-expand{margin-right:.3em}.fa.pull-right,.pull-right.icon,.rst-content .code-block-caption .pull-right.headerlink,.rst-content .eqno .pull-right.headerlink,.rst-content .pull-right.admonition-title,.rst-content code.download span.pull-right:first-child,.rst-content dl dt .pull-right.headerlink,.rst-content h1 .pull-right.headerlink,.rst-content h2 .pull-right.headerlink,.rst-content h3 .pull-right.headerlink,.rst-content h4 .pull-right.headerlink,.rst-content h5 .pull-right.headerlink,.rst-content h6 .pull-right.headerlink,.rst-content p .pull-right.headerlink,.rst-content table>caption .pull-right.headerlink,.rst-content tt.download span.pull-right:first-child,.wy-menu-vertical li.current>a button.pull-right.toctree-expand,.wy-menu-vertical li.on a button.pull-right.toctree-expand,.wy-menu-vertical li button.pull-right.toctree-expand{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);-ms-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scaleY(-1);-ms-transform:scaleY(-1);transform:scaleY(-1)}:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:""}.fa-music:before{content:""}.fa-search:before,.icon-search:before{content:""}.fa-envelope-o:before{content:""}.fa-heart:before{content:""}.fa-star:before{content:""}.fa-star-o:before{content:""}.fa-user:before{content:""}.fa-film:before{content:""}.fa-th-large:before{content:""}.fa-th:before{content:""}.fa-th-list:before{content:""}.fa-check:before{content:""}.fa-close:before,.fa-remove:before,.fa-times:before{content:""}.fa-search-plus:before{content:""}.fa-search-minus:before{content:""}.fa-power-off:before{content:""}.fa-signal:before{content:""}.fa-cog:before,.fa-gear:before{content:""}.fa-trash-o:before{content:""}.fa-home:before,.icon-home:before{content:""}.fa-file-o:before{content:""}.fa-clock-o:before{content:""}.fa-road:before{content:""}.fa-download:before,.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{content:""}.fa-arrow-circle-o-down:before{content:""}.fa-arrow-circle-o-up:before{content:""}.fa-inbox:before{content:""}.fa-play-circle-o:before{content:""}.fa-repeat:before,.fa-rotate-right:before{content:""}.fa-refresh:before{content:""}.fa-list-alt:before{content:""}.fa-lock:before{content:""}.fa-flag:before{content:""}.fa-headphones:before{content:""}.fa-volume-off:before{content:""}.fa-volume-down:before{content:""}.fa-volume-up:before{content:""}.fa-qrcode:before{content:""}.fa-barcode:before{content:""}.fa-tag:before{content:""}.fa-tags:before{content:""}.fa-book:before,.icon-book:before{content:""}.fa-bookmark:before{content:""}.fa-print:before{content:""}.fa-camera:before{content:""}.fa-font:before{content:""}.fa-bold:before{content:""}.fa-italic:before{content:""}.fa-text-height:before{content:""}.fa-text-width:before{content:""}.fa-align-left:before{content:""}.fa-align-center:before{content:""}.fa-align-right:before{content:""}.fa-align-justify:before{content:""}.fa-list:before{content:""}.fa-dedent:before,.fa-outdent:before{content:""}.fa-indent:before{content:""}.fa-video-camera:before{content:""}.fa-image:before,.fa-photo:before,.fa-picture-o:before{content:""}.fa-pencil:before{content:""}.fa-map-marker:before{content:""}.fa-adjust:before{content:""}.fa-tint:before{content:""}.fa-edit:before,.fa-pencil-square-o:before{content:""}.fa-share-square-o:before{content:""}.fa-check-square-o:before{content:""}.fa-arrows:before{content:""}.fa-step-backward:before{content:""}.fa-fast-backward:before{content:""}.fa-backward:before{content:""}.fa-play:before{content:""}.fa-pause:before{content:""}.fa-stop:before{content:""}.fa-forward:before{content:""}.fa-fast-forward:before{content:""}.fa-step-forward:before{content:""}.fa-eject:before{content:""}.fa-chevron-left:before{content:""}.fa-chevron-right:before{content:""}.fa-plus-circle:before{content:""}.fa-minus-circle:before{content:""}.fa-times-circle:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before{content:""}.fa-check-circle:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before{content:""}.fa-question-circle:before{content:""}.fa-info-circle:before{content:""}.fa-crosshairs:before{content:""}.fa-times-circle-o:before{content:""}.fa-check-circle-o:before{content:""}.fa-ban:before{content:""}.fa-arrow-left:before{content:""}.fa-arrow-right:before{content:""}.fa-arrow-up:before{content:""}.fa-arrow-down:before{content:""}.fa-mail-forward:before,.fa-share:before{content:""}.fa-expand:before{content:""}.fa-compress:before{content:""}.fa-plus:before{content:""}.fa-minus:before{content:""}.fa-asterisk:before{content:""}.fa-exclamation-circle:before,.rst-content .admonition-title:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before{content:""}.fa-gift:before{content:""}.fa-leaf:before{content:""}.fa-fire:before,.icon-fire:before{content:""}.fa-eye:before{content:""}.fa-eye-slash:before{content:""}.fa-exclamation-triangle:before,.fa-warning:before{content:""}.fa-plane:before{content:""}.fa-calendar:before{content:""}.fa-random:before{content:""}.fa-comment:before{content:""}.fa-magnet:before{content:""}.fa-chevron-up:before{content:""}.fa-chevron-down:before{content:""}.fa-retweet:before{content:""}.fa-shopping-cart:before{content:""}.fa-folder:before{content:""}.fa-folder-open:before{content:""}.fa-arrows-v:before{content:""}.fa-arrows-h:before{content:""}.fa-bar-chart-o:before,.fa-bar-chart:before{content:""}.fa-twitter-square:before{content:""}.fa-facebook-square:before{content:""}.fa-camera-retro:before{content:""}.fa-key:before{content:""}.fa-cogs:before,.fa-gears:before{content:""}.fa-comments:before{content:""}.fa-thumbs-o-up:before{content:""}.fa-thumbs-o-down:before{content:""}.fa-star-half:before{content:""}.fa-heart-o:before{content:""}.fa-sign-out:before{content:""}.fa-linkedin-square:before{content:""}.fa-thumb-tack:before{content:""}.fa-external-link:before{content:""}.fa-sign-in:before{content:""}.fa-trophy:before{content:""}.fa-github-square:before{content:""}.fa-upload:before{content:""}.fa-lemon-o:before{content:""}.fa-phone:before{content:""}.fa-square-o:before{content:""}.fa-bookmark-o:before{content:""}.fa-phone-square:before{content:""}.fa-twitter:before{content:""}.fa-facebook-f:before,.fa-facebook:before{content:""}.fa-github:before,.icon-github:before{content:""}.fa-unlock:before{content:""}.fa-credit-card:before{content:""}.fa-feed:before,.fa-rss:before{content:""}.fa-hdd-o:before{content:""}.fa-bullhorn:before{content:""}.fa-bell:before{content:""}.fa-certificate:before{content:""}.fa-hand-o-right:before{content:""}.fa-hand-o-left:before{content:""}.fa-hand-o-up:before{content:""}.fa-hand-o-down:before{content:""}.fa-arrow-circle-left:before,.icon-circle-arrow-left:before{content:""}.fa-arrow-circle-right:before,.icon-circle-arrow-right:before{content:""}.fa-arrow-circle-up:before{content:""}.fa-arrow-circle-down:before{content:""}.fa-globe:before{content:""}.fa-wrench:before{content:""}.fa-tasks:before{content:""}.fa-filter:before{content:""}.fa-briefcase:before{content:""}.fa-arrows-alt:before{content:""}.fa-group:before,.fa-users:before{content:""}.fa-chain:before,.fa-link:before,.icon-link:before{content:""}.fa-cloud:before{content:""}.fa-flask:before{content:""}.fa-cut:before,.fa-scissors:before{content:""}.fa-copy:before,.fa-files-o:before{content:""}.fa-paperclip:before{content:""}.fa-floppy-o:before,.fa-save:before{content:""}.fa-square:before{content:""}.fa-bars:before,.fa-navicon:before,.fa-reorder:before{content:""}.fa-list-ul:before{content:""}.fa-list-ol:before{content:""}.fa-strikethrough:before{content:""}.fa-underline:before{content:""}.fa-table:before{content:""}.fa-magic:before{content:""}.fa-truck:before{content:""}.fa-pinterest:before{content:""}.fa-pinterest-square:before{content:""}.fa-google-plus-square:before{content:""}.fa-google-plus:before{content:""}.fa-money:before{content:""}.fa-caret-down:before,.icon-caret-down:before,.wy-dropdown .caret:before{content:""}.fa-caret-up:before{content:""}.fa-caret-left:before{content:""}.fa-caret-right:before{content:""}.fa-columns:before{content:""}.fa-sort:before,.fa-unsorted:before{content:""}.fa-sort-desc:before,.fa-sort-down:before{content:""}.fa-sort-asc:before,.fa-sort-up:before{content:""}.fa-envelope:before{content:""}.fa-linkedin:before{content:""}.fa-rotate-left:before,.fa-undo:before{content:""}.fa-gavel:before,.fa-legal:before{content:""}.fa-dashboard:before,.fa-tachometer:before{content:""}.fa-comment-o:before{content:""}.fa-comments-o:before{content:""}.fa-bolt:before,.fa-flash:before{content:""}.fa-sitemap:before{content:""}.fa-umbrella:before{content:""}.fa-clipboard:before,.fa-paste:before{content:""}.fa-lightbulb-o:before{content:""}.fa-exchange:before{content:""}.fa-cloud-download:before{content:""}.fa-cloud-upload:before{content:""}.fa-user-md:before{content:""}.fa-stethoscope:before{content:""}.fa-suitcase:before{content:""}.fa-bell-o:before{content:""}.fa-coffee:before{content:""}.fa-cutlery:before{content:""}.fa-file-text-o:before{content:""}.fa-building-o:before{content:""}.fa-hospital-o:before{content:""}.fa-ambulance:before{content:""}.fa-medkit:before{content:""}.fa-fighter-jet:before{content:""}.fa-beer:before{content:""}.fa-h-square:before{content:""}.fa-plus-square:before{content:""}.fa-angle-double-left:before{content:""}.fa-angle-double-right:before{content:""}.fa-angle-double-up:before{content:""}.fa-angle-double-down:before{content:""}.fa-angle-left:before{content:""}.fa-angle-right:before{content:""}.fa-angle-up:before{content:""}.fa-angle-down:before{content:""}.fa-desktop:before{content:""}.fa-laptop:before{content:""}.fa-tablet:before{content:""}.fa-mobile-phone:before,.fa-mobile:before{content:""}.fa-circle-o:before{content:""}.fa-quote-left:before{content:""}.fa-quote-right:before{content:""}.fa-spinner:before{content:""}.fa-circle:before{content:""}.fa-mail-reply:before,.fa-reply:before{content:""}.fa-github-alt:before{content:""}.fa-folder-o:before{content:""}.fa-folder-open-o:before{content:""}.fa-smile-o:before{content:""}.fa-frown-o:before{content:""}.fa-meh-o:before{content:""}.fa-gamepad:before{content:""}.fa-keyboard-o:before{content:""}.fa-flag-o:before{content:""}.fa-flag-checkered:before{content:""}.fa-terminal:before{content:""}.fa-code:before{content:""}.fa-mail-reply-all:before,.fa-reply-all:before{content:""}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:""}.fa-location-arrow:before{content:""}.fa-crop:before{content:""}.fa-code-fork:before{content:""}.fa-chain-broken:before,.fa-unlink:before{content:""}.fa-question:before{content:""}.fa-info:before{content:""}.fa-exclamation:before{content:""}.fa-superscript:before{content:""}.fa-subscript:before{content:""}.fa-eraser:before{content:""}.fa-puzzle-piece:before{content:""}.fa-microphone:before{content:""}.fa-microphone-slash:before{content:""}.fa-shield:before{content:""}.fa-calendar-o:before{content:""}.fa-fire-extinguisher:before{content:""}.fa-rocket:before{content:""}.fa-maxcdn:before{content:""}.fa-chevron-circle-left:before{content:""}.fa-chevron-circle-right:before{content:""}.fa-chevron-circle-up:before{content:""}.fa-chevron-circle-down:before{content:""}.fa-html5:before{content:""}.fa-css3:before{content:""}.fa-anchor:before{content:""}.fa-unlock-alt:before{content:""}.fa-bullseye:before{content:""}.fa-ellipsis-h:before{content:""}.fa-ellipsis-v:before{content:""}.fa-rss-square:before{content:""}.fa-play-circle:before{content:""}.fa-ticket:before{content:""}.fa-minus-square:before{content:""}.fa-minus-square-o:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before{content:""}.fa-level-up:before{content:""}.fa-level-down:before{content:""}.fa-check-square:before{content:""}.fa-pencil-square:before{content:""}.fa-external-link-square:before{content:""}.fa-share-square:before{content:""}.fa-compass:before{content:""}.fa-caret-square-o-down:before,.fa-toggle-down:before{content:""}.fa-caret-square-o-up:before,.fa-toggle-up:before{content:""}.fa-caret-square-o-right:before,.fa-toggle-right:before{content:""}.fa-eur:before,.fa-euro:before{content:""}.fa-gbp:before{content:""}.fa-dollar:before,.fa-usd:before{content:""}.fa-inr:before,.fa-rupee:before{content:""}.fa-cny:before,.fa-jpy:before,.fa-rmb:before,.fa-yen:before{content:""}.fa-rouble:before,.fa-rub:before,.fa-ruble:before{content:""}.fa-krw:before,.fa-won:before{content:""}.fa-bitcoin:before,.fa-btc:before{content:""}.fa-file:before{content:""}.fa-file-text:before{content:""}.fa-sort-alpha-asc:before{content:""}.fa-sort-alpha-desc:before{content:""}.fa-sort-amount-asc:before{content:""}.fa-sort-amount-desc:before{content:""}.fa-sort-numeric-asc:before{content:""}.fa-sort-numeric-desc:before{content:""}.fa-thumbs-up:before{content:""}.fa-thumbs-down:before{content:""}.fa-youtube-square:before{content:""}.fa-youtube:before{content:""}.fa-xing:before{content:""}.fa-xing-square:before{content:""}.fa-youtube-play:before{content:""}.fa-dropbox:before{content:""}.fa-stack-overflow:before{content:""}.fa-instagram:before{content:""}.fa-flickr:before{content:""}.fa-adn:before{content:""}.fa-bitbucket:before,.icon-bitbucket:before{content:""}.fa-bitbucket-square:before{content:""}.fa-tumblr:before{content:""}.fa-tumblr-square:before{content:""}.fa-long-arrow-down:before{content:""}.fa-long-arrow-up:before{content:""}.fa-long-arrow-left:before{content:""}.fa-long-arrow-right:before{content:""}.fa-apple:before{content:""}.fa-windows:before{content:""}.fa-android:before{content:""}.fa-linux:before{content:""}.fa-dribbble:before{content:""}.fa-skype:before{content:""}.fa-foursquare:before{content:""}.fa-trello:before{content:""}.fa-female:before{content:""}.fa-male:before{content:""}.fa-gittip:before,.fa-gratipay:before{content:""}.fa-sun-o:before{content:""}.fa-moon-o:before{content:""}.fa-archive:before{content:""}.fa-bug:before{content:""}.fa-vk:before{content:""}.fa-weibo:before{content:""}.fa-renren:before{content:""}.fa-pagelines:before{content:""}.fa-stack-exchange:before{content:""}.fa-arrow-circle-o-right:before{content:""}.fa-arrow-circle-o-left:before{content:""}.fa-caret-square-o-left:before,.fa-toggle-left:before{content:""}.fa-dot-circle-o:before{content:""}.fa-wheelchair:before{content:""}.fa-vimeo-square:before{content:""}.fa-try:before,.fa-turkish-lira:before{content:""}.fa-plus-square-o:before,.wy-menu-vertical li button.toctree-expand:before{content:""}.fa-space-shuttle:before{content:""}.fa-slack:before{content:""}.fa-envelope-square:before{content:""}.fa-wordpress:before{content:""}.fa-openid:before{content:""}.fa-bank:before,.fa-institution:before,.fa-university:before{content:""}.fa-graduation-cap:before,.fa-mortar-board:before{content:""}.fa-yahoo:before{content:""}.fa-google:before{content:""}.fa-reddit:before{content:""}.fa-reddit-square:before{content:""}.fa-stumbleupon-circle:before{content:""}.fa-stumbleupon:before{content:""}.fa-delicious:before{content:""}.fa-digg:before{content:""}.fa-pied-piper-pp:before{content:""}.fa-pied-piper-alt:before{content:""}.fa-drupal:before{content:""}.fa-joomla:before{content:""}.fa-language:before{content:""}.fa-fax:before{content:""}.fa-building:before{content:""}.fa-child:before{content:""}.fa-paw:before{content:""}.fa-spoon:before{content:""}.fa-cube:before{content:""}.fa-cubes:before{content:""}.fa-behance:before{content:""}.fa-behance-square:before{content:""}.fa-steam:before{content:""}.fa-steam-square:before{content:""}.fa-recycle:before{content:""}.fa-automobile:before,.fa-car:before{content:""}.fa-cab:before,.fa-taxi:before{content:""}.fa-tree:before{content:""}.fa-spotify:before{content:""}.fa-deviantart:before{content:""}.fa-soundcloud:before{content:""}.fa-database:before{content:""}.fa-file-pdf-o:before{content:""}.fa-file-word-o:before{content:""}.fa-file-excel-o:before{content:""}.fa-file-powerpoint-o:before{content:""}.fa-file-image-o:before,.fa-file-photo-o:before,.fa-file-picture-o:before{content:""}.fa-file-archive-o:before,.fa-file-zip-o:before{content:""}.fa-file-audio-o:before,.fa-file-sound-o:before{content:""}.fa-file-movie-o:before,.fa-file-video-o:before{content:""}.fa-file-code-o:before{content:""}.fa-vine:before{content:""}.fa-codepen:before{content:""}.fa-jsfiddle:before{content:""}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-ring:before,.fa-life-saver:before,.fa-support:before{content:""}.fa-circle-o-notch:before{content:""}.fa-ra:before,.fa-rebel:before,.fa-resistance:before{content:""}.fa-empire:before,.fa-ge:before{content:""}.fa-git-square:before{content:""}.fa-git:before{content:""}.fa-hacker-news:before,.fa-y-combinator-square:before,.fa-yc-square:before{content:""}.fa-tencent-weibo:before{content:""}.fa-qq:before{content:""}.fa-wechat:before,.fa-weixin:before{content:""}.fa-paper-plane:before,.fa-send:before{content:""}.fa-paper-plane-o:before,.fa-send-o:before{content:""}.fa-history:before{content:""}.fa-circle-thin:before{content:""}.fa-header:before{content:""}.fa-paragraph:before{content:""}.fa-sliders:before{content:""}.fa-share-alt:before{content:""}.fa-share-alt-square:before{content:""}.fa-bomb:before{content:""}.fa-futbol-o:before,.fa-soccer-ball-o:before{content:""}.fa-tty:before{content:""}.fa-binoculars:before{content:""}.fa-plug:before{content:""}.fa-slideshare:before{content:""}.fa-twitch:before{content:""}.fa-yelp:before{content:""}.fa-newspaper-o:before{content:""}.fa-wifi:before{content:""}.fa-calculator:before{content:""}.fa-paypal:before{content:""}.fa-google-wallet:before{content:""}.fa-cc-visa:before{content:""}.fa-cc-mastercard:before{content:""}.fa-cc-discover:before{content:""}.fa-cc-amex:before{content:""}.fa-cc-paypal:before{content:""}.fa-cc-stripe:before{content:""}.fa-bell-slash:before{content:""}.fa-bell-slash-o:before{content:""}.fa-trash:before{content:""}.fa-copyright:before{content:""}.fa-at:before{content:""}.fa-eyedropper:before{content:""}.fa-paint-brush:before{content:""}.fa-birthday-cake:before{content:""}.fa-area-chart:before{content:""}.fa-pie-chart:before{content:""}.fa-line-chart:before{content:""}.fa-lastfm:before{content:""}.fa-lastfm-square:before{content:""}.fa-toggle-off:before{content:""}.fa-toggle-on:before{content:""}.fa-bicycle:before{content:""}.fa-bus:before{content:""}.fa-ioxhost:before{content:""}.fa-angellist:before{content:""}.fa-cc:before{content:""}.fa-ils:before,.fa-shekel:before,.fa-sheqel:before{content:""}.fa-meanpath:before{content:""}.fa-buysellads:before{content:""}.fa-connectdevelop:before{content:""}.fa-dashcube:before{content:""}.fa-forumbee:before{content:""}.fa-leanpub:before{content:""}.fa-sellsy:before{content:""}.fa-shirtsinbulk:before{content:""}.fa-simplybuilt:before{content:""}.fa-skyatlas:before{content:""}.fa-cart-plus:before{content:""}.fa-cart-arrow-down:before{content:""}.fa-diamond:before{content:""}.fa-ship:before{content:""}.fa-user-secret:before{content:""}.fa-motorcycle:before{content:""}.fa-street-view:before{content:""}.fa-heartbeat:before{content:""}.fa-venus:before{content:""}.fa-mars:before{content:""}.fa-mercury:before{content:""}.fa-intersex:before,.fa-transgender:before{content:""}.fa-transgender-alt:before{content:""}.fa-venus-double:before{content:""}.fa-mars-double:before{content:""}.fa-venus-mars:before{content:""}.fa-mars-stroke:before{content:""}.fa-mars-stroke-v:before{content:""}.fa-mars-stroke-h:before{content:""}.fa-neuter:before{content:""}.fa-genderless:before{content:""}.fa-facebook-official:before{content:""}.fa-pinterest-p:before{content:""}.fa-whatsapp:before{content:""}.fa-server:before{content:""}.fa-user-plus:before{content:""}.fa-user-times:before{content:""}.fa-bed:before,.fa-hotel:before{content:""}.fa-viacoin:before{content:""}.fa-train:before{content:""}.fa-subway:before{content:""}.fa-medium:before{content:""}.fa-y-combinator:before,.fa-yc:before{content:""}.fa-optin-monster:before{content:""}.fa-opencart:before{content:""}.fa-expeditedssl:before{content:""}.fa-battery-4:before,.fa-battery-full:before,.fa-battery:before{content:""}.fa-battery-3:before,.fa-battery-three-quarters:before{content:""}.fa-battery-2:before,.fa-battery-half:before{content:""}.fa-battery-1:before,.fa-battery-quarter:before{content:""}.fa-battery-0:before,.fa-battery-empty:before{content:""}.fa-mouse-pointer:before{content:""}.fa-i-cursor:before{content:""}.fa-object-group:before{content:""}.fa-object-ungroup:before{content:""}.fa-sticky-note:before{content:""}.fa-sticky-note-o:before{content:""}.fa-cc-jcb:before{content:""}.fa-cc-diners-club:before{content:""}.fa-clone:before{content:""}.fa-balance-scale:before{content:""}.fa-hourglass-o:before{content:""}.fa-hourglass-1:before,.fa-hourglass-start:before{content:""}.fa-hourglass-2:before,.fa-hourglass-half:before{content:""}.fa-hourglass-3:before,.fa-hourglass-end:before{content:""}.fa-hourglass:before{content:""}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:""}.fa-hand-paper-o:before,.fa-hand-stop-o:before{content:""}.fa-hand-scissors-o:before{content:""}.fa-hand-lizard-o:before{content:""}.fa-hand-spock-o:before{content:""}.fa-hand-pointer-o:before{content:""}.fa-hand-peace-o:before{content:""}.fa-trademark:before{content:""}.fa-registered:before{content:""}.fa-creative-commons:before{content:""}.fa-gg:before{content:""}.fa-gg-circle:before{content:""}.fa-tripadvisor:before{content:""}.fa-odnoklassniki:before{content:""}.fa-odnoklassniki-square:before{content:""}.fa-get-pocket:before{content:""}.fa-wikipedia-w:before{content:""}.fa-safari:before{content:""}.fa-chrome:before{content:""}.fa-firefox:before{content:""}.fa-opera:before{content:""}.fa-internet-explorer:before{content:""}.fa-television:before,.fa-tv:before{content:""}.fa-contao:before{content:""}.fa-500px:before{content:""}.fa-amazon:before{content:""}.fa-calendar-plus-o:before{content:""}.fa-calendar-minus-o:before{content:""}.fa-calendar-times-o:before{content:""}.fa-calendar-check-o:before{content:""}.fa-industry:before{content:""}.fa-map-pin:before{content:""}.fa-map-signs:before{content:""}.fa-map-o:before{content:""}.fa-map:before{content:""}.fa-commenting:before{content:""}.fa-commenting-o:before{content:""}.fa-houzz:before{content:""}.fa-vimeo:before{content:""}.fa-black-tie:before{content:""}.fa-fonticons:before{content:""}.fa-reddit-alien:before{content:""}.fa-edge:before{content:""}.fa-credit-card-alt:before{content:""}.fa-codiepie:before{content:""}.fa-modx:before{content:""}.fa-fort-awesome:before{content:""}.fa-usb:before{content:""}.fa-product-hunt:before{content:""}.fa-mixcloud:before{content:""}.fa-scribd:before{content:""}.fa-pause-circle:before{content:""}.fa-pause-circle-o:before{content:""}.fa-stop-circle:before{content:""}.fa-stop-circle-o:before{content:""}.fa-shopping-bag:before{content:""}.fa-shopping-basket:before{content:""}.fa-hashtag:before{content:""}.fa-bluetooth:before{content:""}.fa-bluetooth-b:before{content:""}.fa-percent:before{content:""}.fa-gitlab:before,.icon-gitlab:before{content:""}.fa-wpbeginner:before{content:""}.fa-wpforms:before{content:""}.fa-envira:before{content:""}.fa-universal-access:before{content:""}.fa-wheelchair-alt:before{content:""}.fa-question-circle-o:before{content:""}.fa-blind:before{content:""}.fa-audio-description:before{content:""}.fa-volume-control-phone:before{content:""}.fa-braille:before{content:""}.fa-assistive-listening-systems:before{content:""}.fa-american-sign-language-interpreting:before,.fa-asl-interpreting:before{content:""}.fa-deaf:before,.fa-deafness:before,.fa-hard-of-hearing:before{content:""}.fa-glide:before{content:""}.fa-glide-g:before{content:""}.fa-sign-language:before,.fa-signing:before{content:""}.fa-low-vision:before{content:""}.fa-viadeo:before{content:""}.fa-viadeo-square:before{content:""}.fa-snapchat:before{content:""}.fa-snapchat-ghost:before{content:""}.fa-snapchat-square:before{content:""}.fa-pied-piper:before{content:""}.fa-first-order:before{content:""}.fa-yoast:before{content:""}.fa-themeisle:before{content:""}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:""}.fa-fa:before,.fa-font-awesome:before{content:""}.fa-handshake-o:before{content:""}.fa-envelope-open:before{content:""}.fa-envelope-open-o:before{content:""}.fa-linode:before{content:""}.fa-address-book:before{content:""}.fa-address-book-o:before{content:""}.fa-address-card:before,.fa-vcard:before{content:""}.fa-address-card-o:before,.fa-vcard-o:before{content:""}.fa-user-circle:before{content:""}.fa-user-circle-o:before{content:""}.fa-user-o:before{content:""}.fa-id-badge:before{content:""}.fa-drivers-license:before,.fa-id-card:before{content:""}.fa-drivers-license-o:before,.fa-id-card-o:before{content:""}.fa-quora:before{content:""}.fa-free-code-camp:before{content:""}.fa-telegram:before{content:""}.fa-thermometer-4:before,.fa-thermometer-full:before,.fa-thermometer:before{content:""}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:""}.fa-thermometer-2:before,.fa-thermometer-half:before{content:""}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:""}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:""}.fa-shower:before{content:""}.fa-bath:before,.fa-bathtub:before,.fa-s15:before{content:""}.fa-podcast:before{content:""}.fa-window-maximize:before{content:""}.fa-window-minimize:before{content:""}.fa-window-restore:before{content:""}.fa-times-rectangle:before,.fa-window-close:before{content:""}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:""}.fa-bandcamp:before{content:""}.fa-grav:before{content:""}.fa-etsy:before{content:""}.fa-imdb:before{content:""}.fa-ravelry:before{content:""}.fa-eercast:before{content:""}.fa-microchip:before{content:""}.fa-snowflake-o:before{content:""}.fa-superpowers:before{content:""}.fa-wpexplorer:before{content:""}.fa-meetup:before{content:""}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}.fa,.icon,.rst-content .admonition-title,.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content code.download span:first-child,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink,.rst-content tt.download span:first-child,.wy-dropdown .caret,.wy-inline-validate.wy-inline-validate-danger .wy-input-context,.wy-inline-validate.wy-inline-validate-info .wy-input-context,.wy-inline-validate.wy-inline-validate-success .wy-input-context,.wy-inline-validate.wy-inline-validate-warning .wy-input-context,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li button.toctree-expand{font-family:inherit}.fa:before,.icon:before,.rst-content .admonition-title:before,.rst-content .code-block-caption .headerlink:before,.rst-content .eqno .headerlink:before,.rst-content code.download span:first-child:before,.rst-content dl dt .headerlink:before,.rst-content h1 .headerlink:before,.rst-content h2 .headerlink:before,.rst-content h3 .headerlink:before,.rst-content h4 .headerlink:before,.rst-content h5 .headerlink:before,.rst-content h6 .headerlink:before,.rst-content p.caption .headerlink:before,.rst-content p .headerlink:before,.rst-content table>caption .headerlink:before,.rst-content tt.download span:first-child:before,.wy-dropdown .caret:before,.wy-inline-validate.wy-inline-validate-danger .wy-input-context:before,.wy-inline-validate.wy-inline-validate-info .wy-input-context:before,.wy-inline-validate.wy-inline-validate-success .wy-input-context:before,.wy-inline-validate.wy-inline-validate-warning .wy-input-context:before,.wy-menu-vertical li.current>a button.toctree-expand:before,.wy-menu-vertical li.on a button.toctree-expand:before,.wy-menu-vertical li button.toctree-expand:before{font-family:FontAwesome;display:inline-block;font-style:normal;font-weight:400;line-height:1;text-decoration:inherit}.rst-content .code-block-caption a .headerlink,.rst-content .eqno a .headerlink,.rst-content a .admonition-title,.rst-content code.download a span:first-child,.rst-content dl dt a .headerlink,.rst-content h1 a .headerlink,.rst-content h2 a .headerlink,.rst-content h3 a .headerlink,.rst-content h4 a .headerlink,.rst-content h5 a .headerlink,.rst-content h6 a .headerlink,.rst-content p.caption a .headerlink,.rst-content p a .headerlink,.rst-content table>caption a .headerlink,.rst-content tt.download a span:first-child,.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand,.wy-menu-vertical li a button.toctree-expand,a .fa,a .icon,a .rst-content .admonition-title,a .rst-content .code-block-caption .headerlink,a .rst-content .eqno .headerlink,a .rst-content code.download span:first-child,a .rst-content dl dt .headerlink,a .rst-content h1 .headerlink,a .rst-content h2 .headerlink,a .rst-content h3 .headerlink,a .rst-content h4 .headerlink,a .rst-content h5 .headerlink,a .rst-content h6 .headerlink,a .rst-content p.caption .headerlink,a .rst-content p .headerlink,a .rst-content table>caption .headerlink,a .rst-content tt.download span:first-child,a .wy-menu-vertical li button.toctree-expand{display:inline-block;text-decoration:inherit}.btn .fa,.btn .icon,.btn .rst-content .admonition-title,.btn .rst-content .code-block-caption .headerlink,.btn .rst-content .eqno .headerlink,.btn .rst-content code.download span:first-child,.btn .rst-content dl dt .headerlink,.btn .rst-content h1 .headerlink,.btn .rst-content h2 .headerlink,.btn .rst-content h3 .headerlink,.btn .rst-content h4 .headerlink,.btn .rst-content h5 .headerlink,.btn .rst-content h6 .headerlink,.btn .rst-content p .headerlink,.btn .rst-content table>caption .headerlink,.btn .rst-content tt.download span:first-child,.btn .wy-menu-vertical li.current>a button.toctree-expand,.btn .wy-menu-vertical li.on a button.toctree-expand,.btn .wy-menu-vertical li button.toctree-expand,.nav .fa,.nav .icon,.nav .rst-content .admonition-title,.nav .rst-content .code-block-caption .headerlink,.nav .rst-content .eqno .headerlink,.nav .rst-content code.download span:first-child,.nav .rst-content dl dt .headerlink,.nav .rst-content h1 .headerlink,.nav .rst-content h2 .headerlink,.nav .rst-content h3 .headerlink,.nav .rst-content h4 .headerlink,.nav .rst-content h5 .headerlink,.nav .rst-content h6 .headerlink,.nav .rst-content p .headerlink,.nav .rst-content table>caption .headerlink,.nav .rst-content tt.download span:first-child,.nav .wy-menu-vertical li.current>a button.toctree-expand,.nav .wy-menu-vertical li.on a button.toctree-expand,.nav .wy-menu-vertical li button.toctree-expand,.rst-content .btn .admonition-title,.rst-content .code-block-caption .btn .headerlink,.rst-content .code-block-caption .nav .headerlink,.rst-content .eqno .btn .headerlink,.rst-content .eqno .nav .headerlink,.rst-content .nav .admonition-title,.rst-content code.download .btn span:first-child,.rst-content code.download .nav span:first-child,.rst-content dl dt .btn .headerlink,.rst-content dl dt .nav .headerlink,.rst-content h1 .btn .headerlink,.rst-content h1 .nav .headerlink,.rst-content h2 .btn .headerlink,.rst-content h2 .nav .headerlink,.rst-content h3 .btn .headerlink,.rst-content h3 .nav .headerlink,.rst-content h4 .btn .headerlink,.rst-content h4 .nav .headerlink,.rst-content h5 .btn .headerlink,.rst-content h5 .nav .headerlink,.rst-content h6 .btn .headerlink,.rst-content h6 .nav .headerlink,.rst-content p .btn .headerlink,.rst-content p .nav .headerlink,.rst-content table>caption .btn .headerlink,.rst-content table>caption .nav .headerlink,.rst-content tt.download .btn span:first-child,.rst-content tt.download .nav span:first-child,.wy-menu-vertical li .btn button.toctree-expand,.wy-menu-vertical li.current>a .btn button.toctree-expand,.wy-menu-vertical li.current>a .nav button.toctree-expand,.wy-menu-vertical li .nav button.toctree-expand,.wy-menu-vertical li.on a .btn button.toctree-expand,.wy-menu-vertical li.on a .nav button.toctree-expand{display:inline}.btn .fa-large.icon,.btn .fa.fa-large,.btn .rst-content .code-block-caption .fa-large.headerlink,.btn .rst-content .eqno .fa-large.headerlink,.btn .rst-content .fa-large.admonition-title,.btn .rst-content code.download span.fa-large:first-child,.btn .rst-content dl dt .fa-large.headerlink,.btn .rst-content h1 .fa-large.headerlink,.btn .rst-content h2 .fa-large.headerlink,.btn .rst-content h3 .fa-large.headerlink,.btn .rst-content h4 .fa-large.headerlink,.btn .rst-content h5 .fa-large.headerlink,.btn .rst-content h6 .fa-large.headerlink,.btn .rst-content p .fa-large.headerlink,.btn .rst-content table>caption .fa-large.headerlink,.btn .rst-content tt.download span.fa-large:first-child,.btn .wy-menu-vertical li button.fa-large.toctree-expand,.nav .fa-large.icon,.nav .fa.fa-large,.nav .rst-content .code-block-caption .fa-large.headerlink,.nav .rst-content .eqno .fa-large.headerlink,.nav .rst-content .fa-large.admonition-title,.nav .rst-content code.download span.fa-large:first-child,.nav .rst-content dl dt .fa-large.headerlink,.nav .rst-content h1 .fa-large.headerlink,.nav .rst-content h2 .fa-large.headerlink,.nav .rst-content h3 .fa-large.headerlink,.nav .rst-content h4 .fa-large.headerlink,.nav .rst-content h5 .fa-large.headerlink,.nav .rst-content h6 .fa-large.headerlink,.nav .rst-content p .fa-large.headerlink,.nav .rst-content table>caption .fa-large.headerlink,.nav .rst-content tt.download span.fa-large:first-child,.nav .wy-menu-vertical li button.fa-large.toctree-expand,.rst-content .btn .fa-large.admonition-title,.rst-content .code-block-caption .btn .fa-large.headerlink,.rst-content .code-block-caption .nav .fa-large.headerlink,.rst-content .eqno .btn .fa-large.headerlink,.rst-content .eqno .nav .fa-large.headerlink,.rst-content .nav .fa-large.admonition-title,.rst-content code.download .btn span.fa-large:first-child,.rst-content code.download .nav span.fa-large:first-child,.rst-content dl dt .btn .fa-large.headerlink,.rst-content dl dt .nav .fa-large.headerlink,.rst-content h1 .btn .fa-large.headerlink,.rst-content h1 .nav .fa-large.headerlink,.rst-content h2 .btn .fa-large.headerlink,.rst-content h2 .nav .fa-large.headerlink,.rst-content h3 .btn .fa-large.headerlink,.rst-content h3 .nav .fa-large.headerlink,.rst-content h4 .btn .fa-large.headerlink,.rst-content h4 .nav .fa-large.headerlink,.rst-content h5 .btn .fa-large.headerlink,.rst-content h5 .nav .fa-large.headerlink,.rst-content h6 .btn .fa-large.headerlink,.rst-content h6 .nav .fa-large.headerlink,.rst-content p .btn .fa-large.headerlink,.rst-content p .nav .fa-large.headerlink,.rst-content table>caption .btn .fa-large.headerlink,.rst-content table>caption .nav .fa-large.headerlink,.rst-content tt.download .btn span.fa-large:first-child,.rst-content tt.download .nav span.fa-large:first-child,.wy-menu-vertical li .btn button.fa-large.toctree-expand,.wy-menu-vertical li .nav button.fa-large.toctree-expand{line-height:.9em}.btn .fa-spin.icon,.btn .fa.fa-spin,.btn .rst-content .code-block-caption .fa-spin.headerlink,.btn .rst-content .eqno .fa-spin.headerlink,.btn .rst-content .fa-spin.admonition-title,.btn .rst-content code.download span.fa-spin:first-child,.btn .rst-content dl dt .fa-spin.headerlink,.btn .rst-content h1 .fa-spin.headerlink,.btn .rst-content h2 .fa-spin.headerlink,.btn .rst-content h3 .fa-spin.headerlink,.btn .rst-content h4 .fa-spin.headerlink,.btn .rst-content h5 .fa-spin.headerlink,.btn .rst-content h6 .fa-spin.headerlink,.btn .rst-content p .fa-spin.headerlink,.btn .rst-content table>caption .fa-spin.headerlink,.btn .rst-content tt.download span.fa-spin:first-child,.btn .wy-menu-vertical li button.fa-spin.toctree-expand,.nav .fa-spin.icon,.nav .fa.fa-spin,.nav .rst-content .code-block-caption .fa-spin.headerlink,.nav .rst-content .eqno .fa-spin.headerlink,.nav .rst-content .fa-spin.admonition-title,.nav .rst-content code.download span.fa-spin:first-child,.nav .rst-content dl dt .fa-spin.headerlink,.nav .rst-content h1 .fa-spin.headerlink,.nav .rst-content h2 .fa-spin.headerlink,.nav .rst-content h3 .fa-spin.headerlink,.nav .rst-content h4 .fa-spin.headerlink,.nav .rst-content h5 .fa-spin.headerlink,.nav .rst-content h6 .fa-spin.headerlink,.nav .rst-content p .fa-spin.headerlink,.nav .rst-content table>caption .fa-spin.headerlink,.nav .rst-content tt.download span.fa-spin:first-child,.nav .wy-menu-vertical li button.fa-spin.toctree-expand,.rst-content .btn .fa-spin.admonition-title,.rst-content .code-block-caption .btn .fa-spin.headerlink,.rst-content .code-block-caption .nav .fa-spin.headerlink,.rst-content .eqno .btn .fa-spin.headerlink,.rst-content .eqno .nav .fa-spin.headerlink,.rst-content .nav .fa-spin.admonition-title,.rst-content code.download .btn span.fa-spin:first-child,.rst-content code.download .nav span.fa-spin:first-child,.rst-content dl dt .btn .fa-spin.headerlink,.rst-content dl dt .nav .fa-spin.headerlink,.rst-content h1 .btn .fa-spin.headerlink,.rst-content h1 .nav .fa-spin.headerlink,.rst-content h2 .btn .fa-spin.headerlink,.rst-content h2 .nav .fa-spin.headerlink,.rst-content h3 .btn .fa-spin.headerlink,.rst-content h3 .nav .fa-spin.headerlink,.rst-content h4 .btn .fa-spin.headerlink,.rst-content h4 .nav .fa-spin.headerlink,.rst-content h5 .btn .fa-spin.headerlink,.rst-content h5 .nav .fa-spin.headerlink,.rst-content h6 .btn .fa-spin.headerlink,.rst-content h6 .nav .fa-spin.headerlink,.rst-content p .btn .fa-spin.headerlink,.rst-content p .nav .fa-spin.headerlink,.rst-content table>caption .btn .fa-spin.headerlink,.rst-content table>caption .nav .fa-spin.headerlink,.rst-content tt.download .btn span.fa-spin:first-child,.rst-content tt.download .nav span.fa-spin:first-child,.wy-menu-vertical li .btn button.fa-spin.toctree-expand,.wy-menu-vertical li .nav button.fa-spin.toctree-expand{display:inline-block}.btn.fa:before,.btn.icon:before,.rst-content .btn.admonition-title:before,.rst-content .code-block-caption .btn.headerlink:before,.rst-content .eqno .btn.headerlink:before,.rst-content code.download span.btn:first-child:before,.rst-content dl dt .btn.headerlink:before,.rst-content h1 .btn.headerlink:before,.rst-content h2 .btn.headerlink:before,.rst-content h3 .btn.headerlink:before,.rst-content h4 .btn.headerlink:before,.rst-content h5 .btn.headerlink:before,.rst-content h6 .btn.headerlink:before,.rst-content p .btn.headerlink:before,.rst-content table>caption .btn.headerlink:before,.rst-content tt.download span.btn:first-child:before,.wy-menu-vertical li button.btn.toctree-expand:before{opacity:.5;-webkit-transition:opacity .05s ease-in;-moz-transition:opacity .05s ease-in;transition:opacity .05s ease-in}.btn.fa:hover:before,.btn.icon:hover:before,.rst-content .btn.admonition-title:hover:before,.rst-content .code-block-caption .btn.headerlink:hover:before,.rst-content .eqno .btn.headerlink:hover:before,.rst-content code.download span.btn:first-child:hover:before,.rst-content dl dt .btn.headerlink:hover:before,.rst-content h1 .btn.headerlink:hover:before,.rst-content h2 .btn.headerlink:hover:before,.rst-content h3 .btn.headerlink:hover:before,.rst-content h4 .btn.headerlink:hover:before,.rst-content h5 .btn.headerlink:hover:before,.rst-content h6 .btn.headerlink:hover:before,.rst-content p .btn.headerlink:hover:before,.rst-content table>caption .btn.headerlink:hover:before,.rst-content tt.download span.btn:first-child:hover:before,.wy-menu-vertical li button.btn.toctree-expand:hover:before{opacity:1}.btn-mini .fa:before,.btn-mini .icon:before,.btn-mini .rst-content .admonition-title:before,.btn-mini .rst-content .code-block-caption .headerlink:before,.btn-mini .rst-content .eqno .headerlink:before,.btn-mini .rst-content code.download span:first-child:before,.btn-mini .rst-content dl dt .headerlink:before,.btn-mini .rst-content h1 .headerlink:before,.btn-mini .rst-content h2 .headerlink:before,.btn-mini .rst-content h3 .headerlink:before,.btn-mini .rst-content h4 .headerlink:before,.btn-mini .rst-content h5 .headerlink:before,.btn-mini .rst-content h6 .headerlink:before,.btn-mini .rst-content p .headerlink:before,.btn-mini .rst-content table>caption .headerlink:before,.btn-mini .rst-content tt.download span:first-child:before,.btn-mini .wy-menu-vertical li button.toctree-expand:before,.rst-content .btn-mini .admonition-title:before,.rst-content .code-block-caption .btn-mini .headerlink:before,.rst-content .eqno .btn-mini .headerlink:before,.rst-content code.download .btn-mini span:first-child:before,.rst-content dl dt .btn-mini .headerlink:before,.rst-content h1 .btn-mini .headerlink:before,.rst-content h2 .btn-mini .headerlink:before,.rst-content h3 .btn-mini .headerlink:before,.rst-content h4 .btn-mini .headerlink:before,.rst-content h5 .btn-mini .headerlink:before,.rst-content h6 .btn-mini .headerlink:before,.rst-content p .btn-mini .headerlink:before,.rst-content table>caption .btn-mini .headerlink:before,.rst-content tt.download .btn-mini span:first-child:before,.wy-menu-vertical li .btn-mini button.toctree-expand:before{font-size:14px;vertical-align:-15%}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning,.wy-alert{padding:12px;line-height:24px;margin-bottom:24px;background:#e7f2fa}.rst-content .admonition-title,.wy-alert-title{font-weight:700;display:block;color:#fff;background:#6ab0de;padding:6px 12px;margin:-12px -12px 12px}.rst-content .danger,.rst-content .error,.rst-content .wy-alert-danger.admonition,.rst-content .wy-alert-danger.admonition-todo,.rst-content .wy-alert-danger.attention,.rst-content .wy-alert-danger.caution,.rst-content .wy-alert-danger.hint,.rst-content .wy-alert-danger.important,.rst-content .wy-alert-danger.note,.rst-content .wy-alert-danger.seealso,.rst-content .wy-alert-danger.tip,.rst-content .wy-alert-danger.warning,.wy-alert.wy-alert-danger{background:#fdf3f2}.rst-content .danger .admonition-title,.rst-content .danger .wy-alert-title,.rst-content .error .admonition-title,.rst-content .error .wy-alert-title,.rst-content .wy-alert-danger.admonition-todo .admonition-title,.rst-content .wy-alert-danger.admonition-todo .wy-alert-title,.rst-content .wy-alert-danger.admonition .admonition-title,.rst-content .wy-alert-danger.admonition .wy-alert-title,.rst-content .wy-alert-danger.attention .admonition-title,.rst-content .wy-alert-danger.attention .wy-alert-title,.rst-content .wy-alert-danger.caution .admonition-title,.rst-content .wy-alert-danger.caution .wy-alert-title,.rst-content .wy-alert-danger.hint .admonition-title,.rst-content .wy-alert-danger.hint .wy-alert-title,.rst-content .wy-alert-danger.important .admonition-title,.rst-content .wy-alert-danger.important .wy-alert-title,.rst-content .wy-alert-danger.note .admonition-title,.rst-content .wy-alert-danger.note .wy-alert-title,.rst-content .wy-alert-danger.seealso .admonition-title,.rst-content .wy-alert-danger.seealso .wy-alert-title,.rst-content .wy-alert-danger.tip .admonition-title,.rst-content .wy-alert-danger.tip .wy-alert-title,.rst-content .wy-alert-danger.warning .admonition-title,.rst-content .wy-alert-danger.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-danger .admonition-title,.wy-alert.wy-alert-danger .rst-content .admonition-title,.wy-alert.wy-alert-danger .wy-alert-title{background:#f29f97}.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .warning,.rst-content .wy-alert-warning.admonition,.rst-content .wy-alert-warning.danger,.rst-content .wy-alert-warning.error,.rst-content .wy-alert-warning.hint,.rst-content .wy-alert-warning.important,.rst-content .wy-alert-warning.note,.rst-content .wy-alert-warning.seealso,.rst-content .wy-alert-warning.tip,.wy-alert.wy-alert-warning{background:#ffedcc}.rst-content .admonition-todo .admonition-title,.rst-content .admonition-todo .wy-alert-title,.rst-content .attention .admonition-title,.rst-content .attention .wy-alert-title,.rst-content .caution .admonition-title,.rst-content .caution .wy-alert-title,.rst-content .warning .admonition-title,.rst-content .warning .wy-alert-title,.rst-content .wy-alert-warning.admonition .admonition-title,.rst-content .wy-alert-warning.admonition .wy-alert-title,.rst-content .wy-alert-warning.danger .admonition-title,.rst-content .wy-alert-warning.danger .wy-alert-title,.rst-content .wy-alert-warning.error .admonition-title,.rst-content .wy-alert-warning.error .wy-alert-title,.rst-content .wy-alert-warning.hint .admonition-title,.rst-content .wy-alert-warning.hint .wy-alert-title,.rst-content .wy-alert-warning.important .admonition-title,.rst-content .wy-alert-warning.important .wy-alert-title,.rst-content .wy-alert-warning.note .admonition-title,.rst-content .wy-alert-warning.note .wy-alert-title,.rst-content .wy-alert-warning.seealso .admonition-title,.rst-content .wy-alert-warning.seealso .wy-alert-title,.rst-content .wy-alert-warning.tip .admonition-title,.rst-content .wy-alert-warning.tip .wy-alert-title,.rst-content .wy-alert.wy-alert-warning .admonition-title,.wy-alert.wy-alert-warning .rst-content .admonition-title,.wy-alert.wy-alert-warning .wy-alert-title{background:#f0b37e}.rst-content .note,.rst-content .seealso,.rst-content .wy-alert-info.admonition,.rst-content .wy-alert-info.admonition-todo,.rst-content .wy-alert-info.attention,.rst-content .wy-alert-info.caution,.rst-content .wy-alert-info.danger,.rst-content .wy-alert-info.error,.rst-content .wy-alert-info.hint,.rst-content .wy-alert-info.important,.rst-content .wy-alert-info.tip,.rst-content .wy-alert-info.warning,.wy-alert.wy-alert-info{background:#e7f2fa}.rst-content .note .admonition-title,.rst-content .note .wy-alert-title,.rst-content .seealso .admonition-title,.rst-content .seealso .wy-alert-title,.rst-content .wy-alert-info.admonition-todo .admonition-title,.rst-content .wy-alert-info.admonition-todo .wy-alert-title,.rst-content .wy-alert-info.admonition .admonition-title,.rst-content .wy-alert-info.admonition .wy-alert-title,.rst-content .wy-alert-info.attention .admonition-title,.rst-content .wy-alert-info.attention .wy-alert-title,.rst-content .wy-alert-info.caution .admonition-title,.rst-content .wy-alert-info.caution .wy-alert-title,.rst-content .wy-alert-info.danger .admonition-title,.rst-content .wy-alert-info.danger .wy-alert-title,.rst-content .wy-alert-info.error .admonition-title,.rst-content .wy-alert-info.error .wy-alert-title,.rst-content .wy-alert-info.hint .admonition-title,.rst-content .wy-alert-info.hint .wy-alert-title,.rst-content .wy-alert-info.important .admonition-title,.rst-content .wy-alert-info.important .wy-alert-title,.rst-content .wy-alert-info.tip .admonition-title,.rst-content .wy-alert-info.tip .wy-alert-title,.rst-content .wy-alert-info.warning .admonition-title,.rst-content .wy-alert-info.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-info .admonition-title,.wy-alert.wy-alert-info .rst-content .admonition-title,.wy-alert.wy-alert-info .wy-alert-title{background:#6ab0de}.rst-content .hint,.rst-content .important,.rst-content .tip,.rst-content .wy-alert-success.admonition,.rst-content .wy-alert-success.admonition-todo,.rst-content .wy-alert-success.attention,.rst-content .wy-alert-success.caution,.rst-content .wy-alert-success.danger,.rst-content .wy-alert-success.error,.rst-content .wy-alert-success.note,.rst-content .wy-alert-success.seealso,.rst-content .wy-alert-success.warning,.wy-alert.wy-alert-success{background:#dbfaf4}.rst-content .hint .admonition-title,.rst-content .hint .wy-alert-title,.rst-content .important .admonition-title,.rst-content .important .wy-alert-title,.rst-content .tip .admonition-title,.rst-content .tip .wy-alert-title,.rst-content .wy-alert-success.admonition-todo .admonition-title,.rst-content .wy-alert-success.admonition-todo .wy-alert-title,.rst-content .wy-alert-success.admonition .admonition-title,.rst-content .wy-alert-success.admonition .wy-alert-title,.rst-content .wy-alert-success.attention .admonition-title,.rst-content .wy-alert-success.attention .wy-alert-title,.rst-content .wy-alert-success.caution .admonition-title,.rst-content .wy-alert-success.caution .wy-alert-title,.rst-content .wy-alert-success.danger .admonition-title,.rst-content .wy-alert-success.danger .wy-alert-title,.rst-content .wy-alert-success.error .admonition-title,.rst-content .wy-alert-success.error .wy-alert-title,.rst-content .wy-alert-success.note .admonition-title,.rst-content .wy-alert-success.note .wy-alert-title,.rst-content .wy-alert-success.seealso .admonition-title,.rst-content .wy-alert-success.seealso .wy-alert-title,.rst-content .wy-alert-success.warning .admonition-title,.rst-content .wy-alert-success.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-success .admonition-title,.wy-alert.wy-alert-success .rst-content .admonition-title,.wy-alert.wy-alert-success .wy-alert-title{background:#1abc9c}.rst-content .wy-alert-neutral.admonition,.rst-content .wy-alert-neutral.admonition-todo,.rst-content .wy-alert-neutral.attention,.rst-content .wy-alert-neutral.caution,.rst-content .wy-alert-neutral.danger,.rst-content .wy-alert-neutral.error,.rst-content .wy-alert-neutral.hint,.rst-content .wy-alert-neutral.important,.rst-content .wy-alert-neutral.note,.rst-content .wy-alert-neutral.seealso,.rst-content .wy-alert-neutral.tip,.rst-content .wy-alert-neutral.warning,.wy-alert.wy-alert-neutral{background:#f3f6f6}.rst-content .wy-alert-neutral.admonition-todo .admonition-title,.rst-content .wy-alert-neutral.admonition-todo .wy-alert-title,.rst-content .wy-alert-neutral.admonition .admonition-title,.rst-content .wy-alert-neutral.admonition .wy-alert-title,.rst-content .wy-alert-neutral.attention .admonition-title,.rst-content .wy-alert-neutral.attention .wy-alert-title,.rst-content .wy-alert-neutral.caution .admonition-title,.rst-content .wy-alert-neutral.caution .wy-alert-title,.rst-content .wy-alert-neutral.danger .admonition-title,.rst-content .wy-alert-neutral.danger .wy-alert-title,.rst-content .wy-alert-neutral.error .admonition-title,.rst-content .wy-alert-neutral.error .wy-alert-title,.rst-content .wy-alert-neutral.hint .admonition-title,.rst-content .wy-alert-neutral.hint .wy-alert-title,.rst-content .wy-alert-neutral.important .admonition-title,.rst-content .wy-alert-neutral.important .wy-alert-title,.rst-content .wy-alert-neutral.note .admonition-title,.rst-content .wy-alert-neutral.note .wy-alert-title,.rst-content .wy-alert-neutral.seealso .admonition-title,.rst-content .wy-alert-neutral.seealso .wy-alert-title,.rst-content .wy-alert-neutral.tip .admonition-title,.rst-content .wy-alert-neutral.tip .wy-alert-title,.rst-content .wy-alert-neutral.warning .admonition-title,.rst-content .wy-alert-neutral.warning .wy-alert-title,.rst-content .wy-alert.wy-alert-neutral .admonition-title,.wy-alert.wy-alert-neutral .rst-content .admonition-title,.wy-alert.wy-alert-neutral .wy-alert-title{color:#404040;background:#e1e4e5}.rst-content .wy-alert-neutral.admonition-todo a,.rst-content .wy-alert-neutral.admonition a,.rst-content .wy-alert-neutral.attention a,.rst-content .wy-alert-neutral.caution a,.rst-content .wy-alert-neutral.danger a,.rst-content .wy-alert-neutral.error a,.rst-content .wy-alert-neutral.hint a,.rst-content .wy-alert-neutral.important a,.rst-content .wy-alert-neutral.note a,.rst-content .wy-alert-neutral.seealso a,.rst-content .wy-alert-neutral.tip a,.rst-content .wy-alert-neutral.warning a,.wy-alert.wy-alert-neutral a{color:#2980b9}.rst-content .admonition-todo p:last-child,.rst-content .admonition p:last-child,.rst-content .attention p:last-child,.rst-content .caution p:last-child,.rst-content .danger p:last-child,.rst-content .error p:last-child,.rst-content .hint p:last-child,.rst-content .important p:last-child,.rst-content .note p:last-child,.rst-content .seealso p:last-child,.rst-content .tip p:last-child,.rst-content .warning p:last-child,.wy-alert p:last-child{margin-bottom:0}.wy-tray-container{position:fixed;bottom:0;left:0;z-index:600}.wy-tray-container li{display:block;width:300px;background:transparent;color:#fff;text-align:center;box-shadow:0 5px 5px 0 rgba(0,0,0,.1);padding:0 24px;min-width:20%;opacity:0;height:0;line-height:56px;overflow:hidden;-webkit-transition:all .3s ease-in;-moz-transition:all .3s ease-in;transition:all .3s ease-in}.wy-tray-container li.wy-tray-item-success{background:#27ae60}.wy-tray-container li.wy-tray-item-info{background:#2980b9}.wy-tray-container li.wy-tray-item-warning{background:#e67e22}.wy-tray-container li.wy-tray-item-danger{background:#e74c3c}.wy-tray-container li.on{opacity:1;height:56px}@media screen and (max-width:768px){.wy-tray-container{bottom:auto;top:0;width:100%}.wy-tray-container li{width:100%}}button{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;cursor:pointer;line-height:normal;-webkit-appearance:button;*overflow:visible}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}button[disabled]{cursor:default}.btn{display:inline-block;border-radius:2px;line-height:normal;white-space:nowrap;text-align:center;cursor:pointer;font-size:100%;padding:6px 12px 8px;color:#fff;border:1px solid rgba(0,0,0,.1);background-color:#27ae60;text-decoration:none;font-weight:400;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 2px -1px hsla(0,0%,100%,.5),inset 0 -2px 0 0 rgba(0,0,0,.1);outline-none:false;vertical-align:middle;*display:inline;zoom:1;-webkit-user-drag:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-transition:all .1s linear;-moz-transition:all .1s linear;transition:all .1s linear}.btn-hover{background:#2e8ece;color:#fff}.btn:hover{background:#2cc36b;color:#fff}.btn:focus{background:#2cc36b;outline:0}.btn:active{box-shadow:inset 0 -1px 0 0 rgba(0,0,0,.05),inset 0 2px 0 0 rgba(0,0,0,.1);padding:8px 12px 6px}.btn:visited{color:#fff}.btn-disabled,.btn-disabled:active,.btn-disabled:focus,.btn-disabled:hover,.btn:disabled{background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=40);opacity:.4;cursor:not-allowed;box-shadow:none}.btn::-moz-focus-inner{padding:0;border:0}.btn-small{font-size:80%}.btn-info{background-color:#2980b9!important}.btn-info:hover{background-color:#2e8ece!important}.btn-neutral{background-color:#f3f6f6!important;color:#404040!important}.btn-neutral:hover{background-color:#e5ebeb!important;color:#404040}.btn-neutral:visited{color:#404040!important}.btn-success{background-color:#27ae60!important}.btn-success:hover{background-color:#295!important}.btn-danger{background-color:#e74c3c!important}.btn-danger:hover{background-color:#ea6153!important}.btn-warning{background-color:#e67e22!important}.btn-warning:hover{background-color:#e98b39!important}.btn-invert{background-color:#222}.btn-invert:hover{background-color:#2f2f2f!important}.btn-link{background-color:transparent!important;color:#2980b9;box-shadow:none;border-color:transparent!important}.btn-link:active,.btn-link:hover{background-color:transparent!important;color:#409ad5!important;box-shadow:none}.btn-link:visited{color:#9b59b6}.wy-btn-group .btn,.wy-control .btn{vertical-align:middle}.wy-btn-group{margin-bottom:24px;*zoom:1}.wy-btn-group:after,.wy-btn-group:before{display:table;content:""}.wy-btn-group:after{clear:both}.wy-dropdown{position:relative;display:inline-block}.wy-dropdown-active .wy-dropdown-menu{display:block}.wy-dropdown-menu{position:absolute;left:0;display:none;float:left;top:100%;min-width:100%;background:#fcfcfc;z-index:100;border:1px solid #cfd7dd;box-shadow:0 2px 2px 0 rgba(0,0,0,.1);padding:12px}.wy-dropdown-menu>dd>a{display:block;clear:both;color:#404040;white-space:nowrap;font-size:90%;padding:0 12px;cursor:pointer}.wy-dropdown-menu>dd>a:hover{background:#2980b9;color:#fff}.wy-dropdown-menu>dd.divider{border-top:1px solid #cfd7dd;margin:6px 0}.wy-dropdown-menu>dd.search{padding-bottom:12px}.wy-dropdown-menu>dd.search input[type=search]{width:100%}.wy-dropdown-menu>dd.call-to-action{background:#e3e3e3;text-transform:uppercase;font-weight:500;font-size:80%}.wy-dropdown-menu>dd.call-to-action:hover{background:#e3e3e3}.wy-dropdown-menu>dd.call-to-action .btn{color:#fff}.wy-dropdown.wy-dropdown-up .wy-dropdown-menu{bottom:100%;top:auto;left:auto;right:0}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu{background:#fcfcfc;margin-top:2px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a{padding:6px 12px}.wy-dropdown.wy-dropdown-bubble .wy-dropdown-menu a:hover{background:#2980b9;color:#fff}.wy-dropdown.wy-dropdown-left .wy-dropdown-menu{right:0;left:auto;text-align:right}.wy-dropdown-arrow:before{content:" ";border-bottom:5px solid #f5f5f5;border-left:5px solid transparent;border-right:5px solid transparent;position:absolute;display:block;top:-4px;left:50%;margin-left:-3px}.wy-dropdown-arrow.wy-dropdown-arrow-left:before{left:11px}.wy-form-stacked select{display:block}.wy-form-aligned .wy-help-inline,.wy-form-aligned input,.wy-form-aligned label,.wy-form-aligned select,.wy-form-aligned textarea{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-form-aligned .wy-control-group>label{display:inline-block;vertical-align:middle;width:10em;margin:6px 12px 0 0;float:left}.wy-form-aligned .wy-control{float:left}.wy-form-aligned .wy-control label{display:block}.wy-form-aligned .wy-control select{margin-top:6px}fieldset{margin:0}fieldset,legend{border:0;padding:0}legend{width:100%;white-space:normal;margin-bottom:24px;font-size:150%;*margin-left:-7px}label,legend{display:block}label{margin:0 0 .3125em;color:#333;font-size:90%}input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle}.wy-control-group{margin-bottom:24px;max-width:1200px;margin-left:auto;margin-right:auto;*zoom:1}.wy-control-group:after,.wy-control-group:before{display:table;content:""}.wy-control-group:after{clear:both}.wy-control-group.wy-control-group-required>label:after{content:" *";color:#e74c3c}.wy-control-group .wy-form-full,.wy-control-group .wy-form-halves,.wy-control-group .wy-form-thirds{padding-bottom:12px}.wy-control-group .wy-form-full input[type=color],.wy-control-group .wy-form-full input[type=date],.wy-control-group .wy-form-full input[type=datetime-local],.wy-control-group .wy-form-full input[type=datetime],.wy-control-group .wy-form-full input[type=email],.wy-control-group .wy-form-full input[type=month],.wy-control-group .wy-form-full input[type=number],.wy-control-group .wy-form-full input[type=password],.wy-control-group .wy-form-full input[type=search],.wy-control-group .wy-form-full input[type=tel],.wy-control-group .wy-form-full input[type=text],.wy-control-group .wy-form-full input[type=time],.wy-control-group .wy-form-full input[type=url],.wy-control-group .wy-form-full input[type=week],.wy-control-group .wy-form-full select,.wy-control-group .wy-form-halves input[type=color],.wy-control-group .wy-form-halves input[type=date],.wy-control-group .wy-form-halves input[type=datetime-local],.wy-control-group .wy-form-halves input[type=datetime],.wy-control-group .wy-form-halves input[type=email],.wy-control-group .wy-form-halves input[type=month],.wy-control-group .wy-form-halves input[type=number],.wy-control-group .wy-form-halves input[type=password],.wy-control-group .wy-form-halves input[type=search],.wy-control-group .wy-form-halves input[type=tel],.wy-control-group .wy-form-halves input[type=text],.wy-control-group .wy-form-halves input[type=time],.wy-control-group .wy-form-halves input[type=url],.wy-control-group .wy-form-halves input[type=week],.wy-control-group .wy-form-halves select,.wy-control-group .wy-form-thirds input[type=color],.wy-control-group .wy-form-thirds input[type=date],.wy-control-group .wy-form-thirds input[type=datetime-local],.wy-control-group .wy-form-thirds input[type=datetime],.wy-control-group .wy-form-thirds input[type=email],.wy-control-group .wy-form-thirds input[type=month],.wy-control-group .wy-form-thirds input[type=number],.wy-control-group .wy-form-thirds input[type=password],.wy-control-group .wy-form-thirds input[type=search],.wy-control-group .wy-form-thirds input[type=tel],.wy-control-group .wy-form-thirds input[type=text],.wy-control-group .wy-form-thirds input[type=time],.wy-control-group .wy-form-thirds input[type=url],.wy-control-group .wy-form-thirds input[type=week],.wy-control-group .wy-form-thirds select{width:100%}.wy-control-group .wy-form-full{float:left;display:block;width:100%;margin-right:0}.wy-control-group .wy-form-full:last-child{margin-right:0}.wy-control-group .wy-form-halves{float:left;display:block;margin-right:2.35765%;width:48.82117%}.wy-control-group .wy-form-halves:last-child,.wy-control-group .wy-form-halves:nth-of-type(2n){margin-right:0}.wy-control-group .wy-form-halves:nth-of-type(odd){clear:left}.wy-control-group .wy-form-thirds{float:left;display:block;margin-right:2.35765%;width:31.76157%}.wy-control-group .wy-form-thirds:last-child,.wy-control-group .wy-form-thirds:nth-of-type(3n){margin-right:0}.wy-control-group .wy-form-thirds:nth-of-type(3n+1){clear:left}.wy-control-group.wy-control-group-no-input .wy-control,.wy-control-no-input{margin:6px 0 0;font-size:90%}.wy-control-no-input{display:inline-block}.wy-control-group.fluid-input input[type=color],.wy-control-group.fluid-input input[type=date],.wy-control-group.fluid-input input[type=datetime-local],.wy-control-group.fluid-input input[type=datetime],.wy-control-group.fluid-input input[type=email],.wy-control-group.fluid-input input[type=month],.wy-control-group.fluid-input input[type=number],.wy-control-group.fluid-input input[type=password],.wy-control-group.fluid-input input[type=search],.wy-control-group.fluid-input input[type=tel],.wy-control-group.fluid-input input[type=text],.wy-control-group.fluid-input input[type=time],.wy-control-group.fluid-input input[type=url],.wy-control-group.fluid-input input[type=week]{width:100%}.wy-form-message-inline{padding-left:.3em;color:#666;font-size:90%}.wy-form-message{display:block;color:#999;font-size:70%;margin-top:.3125em;font-style:italic}.wy-form-message p{font-size:inherit;font-style:italic;margin-bottom:6px}.wy-form-message p:last-child{margin-bottom:0}input{line-height:normal}input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;*overflow:visible}input[type=color],input[type=date],input[type=datetime-local],input[type=datetime],input[type=email],input[type=month],input[type=number],input[type=password],input[type=search],input[type=tel],input[type=text],input[type=time],input[type=url],input[type=week]{-webkit-appearance:none;padding:6px;display:inline-block;border:1px solid #ccc;font-size:80%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;box-shadow:inset 0 1px 3px #ddd;border-radius:0;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}input[type=datetime-local]{padding:.34375em .625em}input[disabled]{cursor:default}input[type=checkbox],input[type=radio]{padding:0;margin-right:.3125em;*height:13px;*width:13px}input[type=checkbox],input[type=radio],input[type=search]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}input[type=color]:focus,input[type=date]:focus,input[type=datetime-local]:focus,input[type=datetime]:focus,input[type=email]:focus,input[type=month]:focus,input[type=number]:focus,input[type=password]:focus,input[type=search]:focus,input[type=tel]:focus,input[type=text]:focus,input[type=time]:focus,input[type=url]:focus,input[type=week]:focus{outline:0;outline:thin dotted\9;border-color:#333}input.no-focus:focus{border-color:#ccc!important}input[type=checkbox]:focus,input[type=file]:focus,input[type=radio]:focus{outline:thin dotted #333;outline:1px auto #129fea}input[type=color][disabled],input[type=date][disabled],input[type=datetime-local][disabled],input[type=datetime][disabled],input[type=email][disabled],input[type=month][disabled],input[type=number][disabled],input[type=password][disabled],input[type=search][disabled],input[type=tel][disabled],input[type=text][disabled],input[type=time][disabled],input[type=url][disabled],input[type=week][disabled]{cursor:not-allowed;background-color:#fafafa}input:focus:invalid,select:focus:invalid,textarea:focus:invalid{color:#e74c3c;border:1px solid #e74c3c}input:focus:invalid:focus,select:focus:invalid:focus,textarea:focus:invalid:focus{border-color:#e74c3c}input[type=checkbox]:focus:invalid:focus,input[type=file]:focus:invalid:focus,input[type=radio]:focus:invalid:focus{outline-color:#e74c3c}input.wy-input-large{padding:12px;font-size:100%}textarea{overflow:auto;vertical-align:top;width:100%;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif}select,textarea{padding:.5em .625em;display:inline-block;border:1px solid #ccc;font-size:80%;box-shadow:inset 0 1px 3px #ddd;-webkit-transition:border .3s linear;-moz-transition:border .3s linear;transition:border .3s linear}select{border:1px solid #ccc;background-color:#fff}select[multiple]{height:auto}select:focus,textarea:focus{outline:0}input[readonly],select[disabled],select[readonly],textarea[disabled],textarea[readonly]{cursor:not-allowed;background-color:#fafafa}input[type=checkbox][disabled],input[type=radio][disabled]{cursor:not-allowed}.wy-checkbox,.wy-radio{margin:6px 0;color:#404040;display:block}.wy-checkbox input,.wy-radio input{vertical-align:baseline}.wy-form-message-inline{display:inline-block;*display:inline;*zoom:1;vertical-align:middle}.wy-input-prefix,.wy-input-suffix{white-space:nowrap;padding:6px}.wy-input-prefix .wy-input-context,.wy-input-suffix .wy-input-context{line-height:27px;padding:0 8px;display:inline-block;font-size:80%;background-color:#f3f6f6;border:1px solid #ccc;color:#999}.wy-input-suffix .wy-input-context{border-left:0}.wy-input-prefix .wy-input-context{border-right:0}.wy-switch{position:relative;display:block;height:24px;margin-top:12px;cursor:pointer}.wy-switch:before{left:0;top:0;width:36px;height:12px;background:#ccc}.wy-switch:after,.wy-switch:before{position:absolute;content:"";display:block;border-radius:4px;-webkit-transition:all .2s ease-in-out;-moz-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.wy-switch:after{width:18px;height:18px;background:#999;left:-3px;top:-3px}.wy-switch span{position:absolute;left:48px;display:block;font-size:12px;color:#ccc;line-height:1}.wy-switch.active:before{background:#1e8449}.wy-switch.active:after{left:24px;background:#27ae60}.wy-switch.disabled{cursor:not-allowed;opacity:.8}.wy-control-group.wy-control-group-error .wy-form-message,.wy-control-group.wy-control-group-error>label{color:#e74c3c}.wy-control-group.wy-control-group-error input[type=color],.wy-control-group.wy-control-group-error input[type=date],.wy-control-group.wy-control-group-error input[type=datetime-local],.wy-control-group.wy-control-group-error input[type=datetime],.wy-control-group.wy-control-group-error input[type=email],.wy-control-group.wy-control-group-error input[type=month],.wy-control-group.wy-control-group-error input[type=number],.wy-control-group.wy-control-group-error input[type=password],.wy-control-group.wy-control-group-error input[type=search],.wy-control-group.wy-control-group-error input[type=tel],.wy-control-group.wy-control-group-error input[type=text],.wy-control-group.wy-control-group-error input[type=time],.wy-control-group.wy-control-group-error input[type=url],.wy-control-group.wy-control-group-error input[type=week],.wy-control-group.wy-control-group-error textarea{border:1px solid #e74c3c}.wy-inline-validate{white-space:nowrap}.wy-inline-validate .wy-input-context{padding:.5em .625em;display:inline-block;font-size:80%}.wy-inline-validate.wy-inline-validate-success .wy-input-context{color:#27ae60}.wy-inline-validate.wy-inline-validate-danger .wy-input-context{color:#e74c3c}.wy-inline-validate.wy-inline-validate-warning .wy-input-context{color:#e67e22}.wy-inline-validate.wy-inline-validate-info .wy-input-context{color:#2980b9}.rotate-90{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-ms-transform:rotate(90deg);-o-transform:rotate(90deg);transform:rotate(90deg)}.rotate-180{-webkit-transform:rotate(180deg);-moz-transform:rotate(180deg);-ms-transform:rotate(180deg);-o-transform:rotate(180deg);transform:rotate(180deg)}.rotate-270{-webkit-transform:rotate(270deg);-moz-transform:rotate(270deg);-ms-transform:rotate(270deg);-o-transform:rotate(270deg);transform:rotate(270deg)}.mirror{-webkit-transform:scaleX(-1);-moz-transform:scaleX(-1);-ms-transform:scaleX(-1);-o-transform:scaleX(-1);transform:scaleX(-1)}.mirror.rotate-90{-webkit-transform:scaleX(-1) rotate(90deg);-moz-transform:scaleX(-1) rotate(90deg);-ms-transform:scaleX(-1) rotate(90deg);-o-transform:scaleX(-1) rotate(90deg);transform:scaleX(-1) rotate(90deg)}.mirror.rotate-180{-webkit-transform:scaleX(-1) rotate(180deg);-moz-transform:scaleX(-1) rotate(180deg);-ms-transform:scaleX(-1) rotate(180deg);-o-transform:scaleX(-1) rotate(180deg);transform:scaleX(-1) rotate(180deg)}.mirror.rotate-270{-webkit-transform:scaleX(-1) rotate(270deg);-moz-transform:scaleX(-1) rotate(270deg);-ms-transform:scaleX(-1) rotate(270deg);-o-transform:scaleX(-1) rotate(270deg);transform:scaleX(-1) rotate(270deg)}@media only screen and (max-width:480px){.wy-form button[type=submit]{margin:.7em 0 0}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=text],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week],.wy-form label{margin-bottom:.3em;display:block}.wy-form input[type=color],.wy-form input[type=date],.wy-form input[type=datetime-local],.wy-form input[type=datetime],.wy-form input[type=email],.wy-form input[type=month],.wy-form input[type=number],.wy-form input[type=password],.wy-form input[type=search],.wy-form input[type=tel],.wy-form input[type=time],.wy-form input[type=url],.wy-form input[type=week]{margin-bottom:0}.wy-form-aligned .wy-control-group label{margin-bottom:.3em;text-align:left;display:block;width:100%}.wy-form-aligned .wy-control{margin:1.5em 0 0}.wy-form-message,.wy-form-message-inline,.wy-form .wy-help-inline{display:block;font-size:80%;padding:6px 0}}@media screen and (max-width:768px){.tablet-hide{display:none}}@media screen and (max-width:480px){.mobile-hide{display:none}}.float-left{float:left}.float-right{float:right}.full-width{width:100%}.rst-content table.docutils,.rst-content table.field-list,.wy-table{border-collapse:collapse;border-spacing:0;empty-cells:show;margin-bottom:24px}.rst-content table.docutils caption,.rst-content table.field-list caption,.wy-table caption{color:#000;font:italic 85%/1 arial,sans-serif;padding:1em 0;text-align:center}.rst-content table.docutils td,.rst-content table.docutils th,.rst-content table.field-list td,.rst-content table.field-list th,.wy-table td,.wy-table th{font-size:90%;margin:0;overflow:visible;padding:8px 16px}.rst-content table.docutils td:first-child,.rst-content table.docutils th:first-child,.rst-content table.field-list td:first-child,.rst-content table.field-list th:first-child,.wy-table td:first-child,.wy-table th:first-child{border-left-width:0}.rst-content table.docutils thead,.rst-content table.field-list thead,.wy-table thead{color:#000;text-align:left;vertical-align:bottom;white-space:nowrap}.rst-content table.docutils thead th,.rst-content table.field-list thead th,.wy-table thead th{font-weight:700;border-bottom:2px solid #e1e4e5}.rst-content table.docutils td,.rst-content table.field-list td,.wy-table td{background-color:transparent;vertical-align:middle}.rst-content table.docutils td p,.rst-content table.field-list td p,.wy-table td p{line-height:18px}.rst-content table.docutils td p:last-child,.rst-content table.field-list td p:last-child,.wy-table td p:last-child{margin-bottom:0}.rst-content table.docutils .wy-table-cell-min,.rst-content table.field-list .wy-table-cell-min,.wy-table .wy-table-cell-min{width:1%;padding-right:0}.rst-content table.docutils .wy-table-cell-min input[type=checkbox],.rst-content table.field-list .wy-table-cell-min input[type=checkbox],.wy-table .wy-table-cell-min input[type=checkbox]{margin:0}.wy-table-secondary{color:grey;font-size:90%}.wy-table-tertiary{color:grey;font-size:80%}.rst-content table.docutils:not(.field-list) tr:nth-child(2n-1) td,.wy-table-backed,.wy-table-odd td,.wy-table-striped tr:nth-child(2n-1) td{background-color:#f3f6f6}.rst-content table.docutils,.wy-table-bordered-all{border:1px solid #e1e4e5}.rst-content table.docutils td,.wy-table-bordered-all td{border-bottom:1px solid #e1e4e5;border-left:1px solid #e1e4e5}.rst-content table.docutils tbody>tr:last-child td,.wy-table-bordered-all tbody>tr:last-child td{border-bottom-width:0}.wy-table-bordered{border:1px solid #e1e4e5}.wy-table-bordered-rows td{border-bottom:1px solid #e1e4e5}.wy-table-bordered-rows tbody>tr:last-child td{border-bottom-width:0}.wy-table-horizontal td,.wy-table-horizontal th{border-width:0 0 1px;border-bottom:1px solid #e1e4e5}.wy-table-horizontal tbody>tr:last-child td{border-bottom-width:0}.wy-table-responsive{margin-bottom:24px;max-width:100%;overflow:auto}.wy-table-responsive table{margin-bottom:0!important}.wy-table-responsive table td,.wy-table-responsive table th{white-space:nowrap}a{color:#2980b9;text-decoration:none;cursor:pointer}a:hover{color:#3091d1}a:visited{color:#9b59b6}html{height:100%}body,html{overflow-x:hidden}body{font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-weight:400;color:#404040;min-height:100%;background:#edf0f2}.wy-text-left{text-align:left}.wy-text-center{text-align:center}.wy-text-right{text-align:right}.wy-text-large{font-size:120%}.wy-text-normal{font-size:100%}.wy-text-small,small{font-size:80%}.wy-text-strike{text-decoration:line-through}.wy-text-warning{color:#e67e22!important}a.wy-text-warning:hover{color:#eb9950!important}.wy-text-info{color:#2980b9!important}a.wy-text-info:hover{color:#409ad5!important}.wy-text-success{color:#27ae60!important}a.wy-text-success:hover{color:#36d278!important}.wy-text-danger{color:#e74c3c!important}a.wy-text-danger:hover{color:#ed7669!important}.wy-text-neutral{color:#404040!important}a.wy-text-neutral:hover{color:#595959!important}.rst-content .toctree-wrapper>p.caption,h1,h2,h3,h4,h5,h6,legend{margin-top:0;font-weight:700;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif}p{line-height:24px;font-size:16px;margin:0 0 24px}h1{font-size:175%}.rst-content .toctree-wrapper>p.caption,h2{font-size:150%}h3{font-size:125%}h4{font-size:115%}h5{font-size:110%}h6{font-size:100%}hr{display:block;height:1px;border:0;border-top:1px solid #e1e4e5;margin:24px 0;padding:0}.rst-content code,.rst-content tt,code{white-space:nowrap;max-width:100%;background:#fff;border:1px solid #e1e4e5;font-size:75%;padding:0 5px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#e74c3c;overflow-x:auto}.rst-content tt.code-large,code.code-large{font-size:90%}.rst-content .section ul,.rst-content .toctree-wrapper ul,.rst-content section ul,.wy-plain-list-disc,article ul{list-style:disc;line-height:24px;margin-bottom:24px}.rst-content .section ul li,.rst-content .toctree-wrapper ul li,.rst-content section ul li,.wy-plain-list-disc li,article ul li{list-style:disc;margin-left:24px}.rst-content .section ul li p:last-child,.rst-content .section ul li ul,.rst-content .toctree-wrapper ul li p:last-child,.rst-content .toctree-wrapper ul li ul,.rst-content section ul li p:last-child,.rst-content section ul li ul,.wy-plain-list-disc li p:last-child,.wy-plain-list-disc li ul,article ul li p:last-child,article ul li ul{margin-bottom:0}.rst-content .section ul li li,.rst-content .toctree-wrapper ul li li,.rst-content section ul li li,.wy-plain-list-disc li li,article ul li li{list-style:circle}.rst-content .section ul li li li,.rst-content .toctree-wrapper ul li li li,.rst-content section ul li li li,.wy-plain-list-disc li li li,article ul li li li{list-style:square}.rst-content .section ul li ol li,.rst-content .toctree-wrapper ul li ol li,.rst-content section ul li ol li,.wy-plain-list-disc li ol li,article ul li ol li{list-style:decimal}.rst-content .section ol,.rst-content .section ol.arabic,.rst-content .toctree-wrapper ol,.rst-content .toctree-wrapper ol.arabic,.rst-content section ol,.rst-content section ol.arabic,.wy-plain-list-decimal,article ol{list-style:decimal;line-height:24px;margin-bottom:24px}.rst-content .section ol.arabic li,.rst-content .section ol li,.rst-content .toctree-wrapper ol.arabic li,.rst-content .toctree-wrapper ol li,.rst-content section ol.arabic li,.rst-content section ol li,.wy-plain-list-decimal li,article ol li{list-style:decimal;margin-left:24px}.rst-content .section ol.arabic li ul,.rst-content .section ol li p:last-child,.rst-content .section ol li ul,.rst-content .toctree-wrapper ol.arabic li ul,.rst-content .toctree-wrapper ol li p:last-child,.rst-content .toctree-wrapper ol li ul,.rst-content section ol.arabic li ul,.rst-content section ol li p:last-child,.rst-content section ol li ul,.wy-plain-list-decimal li p:last-child,.wy-plain-list-decimal li ul,article ol li p:last-child,article ol li ul{margin-bottom:0}.rst-content .section ol.arabic li ul li,.rst-content .section ol li ul li,.rst-content .toctree-wrapper ol.arabic li ul li,.rst-content .toctree-wrapper ol li ul li,.rst-content section ol.arabic li ul li,.rst-content section ol li ul li,.wy-plain-list-decimal li ul li,article ol li ul li{list-style:disc}.wy-breadcrumbs{*zoom:1}.wy-breadcrumbs:after,.wy-breadcrumbs:before{display:table;content:""}.wy-breadcrumbs:after{clear:both}.wy-breadcrumbs>li{display:inline-block;padding-top:5px}.wy-breadcrumbs>li.wy-breadcrumbs-aside{float:right}.rst-content .wy-breadcrumbs>li code,.rst-content .wy-breadcrumbs>li tt,.wy-breadcrumbs>li .rst-content tt,.wy-breadcrumbs>li code{all:inherit;color:inherit}.breadcrumb-item:before{content:"/";color:#bbb;font-size:13px;padding:0 6px 0 3px}.wy-breadcrumbs-extra{margin-bottom:0;color:#b3b3b3;font-size:80%;display:inline-block}@media screen and (max-width:480px){.wy-breadcrumbs-extra,.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}@media print{.wy-breadcrumbs li.wy-breadcrumbs-aside{display:none}}html{font-size:16px}.wy-affix{position:fixed;top:1.618em}.wy-menu a:hover{text-decoration:none}.wy-menu-horiz{*zoom:1}.wy-menu-horiz:after,.wy-menu-horiz:before{display:table;content:""}.wy-menu-horiz:after{clear:both}.wy-menu-horiz li,.wy-menu-horiz ul{display:inline-block}.wy-menu-horiz li:hover{background:hsla(0,0%,100%,.1)}.wy-menu-horiz li.divide-left{border-left:1px solid #404040}.wy-menu-horiz li.divide-right{border-right:1px solid #404040}.wy-menu-horiz a{height:32px;display:inline-block;line-height:32px;padding:0 16px}.wy-menu-vertical{width:300px}.wy-menu-vertical header,.wy-menu-vertical p.caption{color:#55a5d9;height:32px;line-height:32px;padding:0 1.618em;margin:12px 0 0;display:block;font-weight:700;text-transform:uppercase;font-size:85%;white-space:nowrap}.wy-menu-vertical ul{margin-bottom:0}.wy-menu-vertical li.divide-top{border-top:1px solid #404040}.wy-menu-vertical li.divide-bottom{border-bottom:1px solid #404040}.wy-menu-vertical li.current{background:#e3e3e3}.wy-menu-vertical li.current a{color:grey;border-right:1px solid #c9c9c9;padding:.4045em 2.427em}.wy-menu-vertical li.current a:hover{background:#d6d6d6}.rst-content .wy-menu-vertical li tt,.wy-menu-vertical li .rst-content tt,.wy-menu-vertical li code{border:none;background:inherit;color:inherit;padding-left:0;padding-right:0}.wy-menu-vertical li button.toctree-expand{display:block;float:left;margin-left:-1.2em;line-height:18px;color:#4d4d4d;border:none;background:none;padding:0}.wy-menu-vertical li.current>a,.wy-menu-vertical li.on a{color:#404040;font-weight:700;position:relative;background:#fcfcfc;border:none;padding:.4045em 1.618em}.wy-menu-vertical li.current>a:hover,.wy-menu-vertical li.on a:hover{background:#fcfcfc}.wy-menu-vertical li.current>a:hover button.toctree-expand,.wy-menu-vertical li.on a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.current>a button.toctree-expand,.wy-menu-vertical li.on a button.toctree-expand{display:block;line-height:18px;color:#333}.wy-menu-vertical li.toctree-l1.current>a{border-bottom:1px solid #c9c9c9;border-top:1px solid #c9c9c9}.wy-menu-vertical .toctree-l1.current .toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .toctree-l11>ul{display:none}.wy-menu-vertical .toctree-l1.current .current.toctree-l2>ul,.wy-menu-vertical .toctree-l2.current .current.toctree-l3>ul,.wy-menu-vertical .toctree-l3.current .current.toctree-l4>ul,.wy-menu-vertical .toctree-l4.current .current.toctree-l5>ul,.wy-menu-vertical .toctree-l5.current .current.toctree-l6>ul,.wy-menu-vertical .toctree-l6.current .current.toctree-l7>ul,.wy-menu-vertical .toctree-l7.current .current.toctree-l8>ul,.wy-menu-vertical .toctree-l8.current .current.toctree-l9>ul,.wy-menu-vertical .toctree-l9.current .current.toctree-l10>ul,.wy-menu-vertical .toctree-l10.current .current.toctree-l11>ul{display:block}.wy-menu-vertical li.toctree-l3,.wy-menu-vertical li.toctree-l4{font-size:.9em}.wy-menu-vertical li.toctree-l2 a,.wy-menu-vertical li.toctree-l3 a,.wy-menu-vertical li.toctree-l4 a,.wy-menu-vertical li.toctree-l5 a,.wy-menu-vertical li.toctree-l6 a,.wy-menu-vertical li.toctree-l7 a,.wy-menu-vertical li.toctree-l8 a,.wy-menu-vertical li.toctree-l9 a,.wy-menu-vertical li.toctree-l10 a{color:#404040}.wy-menu-vertical li.toctree-l2 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l3 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l4 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l5 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l6 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l7 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l8 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l9 a:hover button.toctree-expand,.wy-menu-vertical li.toctree-l10 a:hover button.toctree-expand{color:grey}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a,.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a,.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a,.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a,.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a,.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a,.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a,.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{display:block}.wy-menu-vertical li.toctree-l2.current>a{padding:.4045em 2.427em}.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{padding:.4045em 1.618em .4045em 4.045em}.wy-menu-vertical li.toctree-l3.current>a{padding:.4045em 4.045em}.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{padding:.4045em 1.618em .4045em 5.663em}.wy-menu-vertical li.toctree-l4.current>a{padding:.4045em 5.663em}.wy-menu-vertical li.toctree-l4.current li.toctree-l5>a{padding:.4045em 1.618em .4045em 7.281em}.wy-menu-vertical li.toctree-l5.current>a{padding:.4045em 7.281em}.wy-menu-vertical li.toctree-l5.current li.toctree-l6>a{padding:.4045em 1.618em .4045em 8.899em}.wy-menu-vertical li.toctree-l6.current>a{padding:.4045em 8.899em}.wy-menu-vertical li.toctree-l6.current li.toctree-l7>a{padding:.4045em 1.618em .4045em 10.517em}.wy-menu-vertical li.toctree-l7.current>a{padding:.4045em 10.517em}.wy-menu-vertical li.toctree-l7.current li.toctree-l8>a{padding:.4045em 1.618em .4045em 12.135em}.wy-menu-vertical li.toctree-l8.current>a{padding:.4045em 12.135em}.wy-menu-vertical li.toctree-l8.current li.toctree-l9>a{padding:.4045em 1.618em .4045em 13.753em}.wy-menu-vertical li.toctree-l9.current>a{padding:.4045em 13.753em}.wy-menu-vertical li.toctree-l9.current li.toctree-l10>a{padding:.4045em 1.618em .4045em 15.371em}.wy-menu-vertical li.toctree-l10.current>a{padding:.4045em 15.371em}.wy-menu-vertical li.toctree-l10.current li.toctree-l11>a{padding:.4045em 1.618em .4045em 16.989em}.wy-menu-vertical li.toctree-l2.current>a,.wy-menu-vertical li.toctree-l2.current li.toctree-l3>a{background:#c9c9c9}.wy-menu-vertical li.toctree-l2 button.toctree-expand{color:#a3a3a3}.wy-menu-vertical li.toctree-l3.current>a,.wy-menu-vertical li.toctree-l3.current li.toctree-l4>a{background:#bdbdbd}.wy-menu-vertical li.toctree-l3 button.toctree-expand{color:#969696}.wy-menu-vertical li.current ul{display:block}.wy-menu-vertical li ul{margin-bottom:0;display:none}.wy-menu-vertical li ul li a{margin-bottom:0;color:#d9d9d9;font-weight:400}.wy-menu-vertical a{line-height:18px;padding:.4045em 1.618em;display:block;position:relative;font-size:90%;color:#d9d9d9}.wy-menu-vertical a:hover{background-color:#4e4a4a;cursor:pointer}.wy-menu-vertical a:hover button.toctree-expand{color:#d9d9d9}.wy-menu-vertical a:active{background-color:#2980b9;cursor:pointer;color:#fff}.wy-menu-vertical a:active button.toctree-expand{color:#fff}.wy-side-nav-search{display:block;width:300px;padding:.809em;margin-bottom:.809em;z-index:200;background-color:#2980b9;text-align:center;color:#fcfcfc}.wy-side-nav-search input[type=text]{width:100%;border-radius:50px;padding:6px 12px;border-color:#2472a4}.wy-side-nav-search img{display:block;margin:auto auto .809em;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-side-nav-search .wy-dropdown>a,.wy-side-nav-search>a{color:#fcfcfc;font-size:100%;font-weight:700;display:inline-block;padding:4px 6px;margin-bottom:.809em;max-width:100%}.wy-side-nav-search .wy-dropdown>a:hover,.wy-side-nav-search .wy-dropdown>aactive,.wy-side-nav-search .wy-dropdown>afocus,.wy-side-nav-search>a:hover,.wy-side-nav-search>aactive,.wy-side-nav-search>afocus{background:hsla(0,0%,100%,.1)}.wy-side-nav-search .wy-dropdown>a img.logo,.wy-side-nav-search>a img.logo{display:block;margin:0 auto;height:auto;width:auto;border-radius:0;max-width:100%;background:transparent}.wy-side-nav-search .wy-dropdown>a.icon,.wy-side-nav-search>a.icon{display:block}.wy-side-nav-search .wy-dropdown>a.icon img.logo,.wy-side-nav-search>a.icon img.logo{margin-top:.85em}.wy-side-nav-search>div.switch-menus{position:relative;display:block;margin-top:-.4045em;margin-bottom:.809em;font-weight:400;color:hsla(0,0%,100%,.3)}.wy-side-nav-search>div.switch-menus>div.language-switch,.wy-side-nav-search>div.switch-menus>div.version-switch{display:inline-block;padding:.2em}.wy-side-nav-search>div.switch-menus>div.language-switch select,.wy-side-nav-search>div.switch-menus>div.version-switch select{display:inline-block;margin-right:-2rem;padding-right:2rem;max-width:240px;text-align-last:center;background:none;border:none;border-radius:0;box-shadow:none;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;font-size:1em;font-weight:400;color:hsla(0,0%,100%,.3);cursor:pointer;appearance:none;-webkit-appearance:none;-moz-appearance:none}.wy-side-nav-search>div.switch-menus>div.language-switch select:active,.wy-side-nav-search>div.switch-menus>div.language-switch select:focus,.wy-side-nav-search>div.switch-menus>div.language-switch select:hover,.wy-side-nav-search>div.switch-menus>div.version-switch select:active,.wy-side-nav-search>div.switch-menus>div.version-switch select:focus,.wy-side-nav-search>div.switch-menus>div.version-switch select:hover{background:hsla(0,0%,100%,.1);color:hsla(0,0%,100%,.5)}.wy-side-nav-search>div.switch-menus>div.language-switch select option,.wy-side-nav-search>div.switch-menus>div.version-switch select option{color:#000}.wy-side-nav-search>div.switch-menus>div.language-switch:has(>select):after,.wy-side-nav-search>div.switch-menus>div.version-switch:has(>select):after{display:inline-block;width:1.5em;height:100%;padding:.1em;content:"\f0d7";font-size:1em;line-height:1.2em;font-family:FontAwesome;text-align:center;pointer-events:none;box-sizing:border-box}.wy-nav .wy-menu-vertical header{color:#2980b9}.wy-nav .wy-menu-vertical a{color:#b3b3b3}.wy-nav .wy-menu-vertical a:hover{background-color:#2980b9;color:#fff}[data-menu-wrap]{-webkit-transition:all .2s ease-in;-moz-transition:all .2s ease-in;transition:all .2s ease-in;position:absolute;opacity:1;width:100%;opacity:0}[data-menu-wrap].move-center{left:0;right:auto;opacity:1}[data-menu-wrap].move-left{right:auto;left:-100%;opacity:0}[data-menu-wrap].move-right{right:-100%;left:auto;opacity:0}.wy-body-for-nav{background:#fcfcfc}.wy-grid-for-nav{position:absolute;width:100%;height:100%}.wy-nav-side{position:fixed;top:0;bottom:0;left:0;padding-bottom:2em;width:300px;overflow-x:hidden;overflow-y:hidden;min-height:100%;color:#9b9b9b;background:#343131;z-index:200}.wy-side-scroll{width:320px;position:relative;overflow-x:hidden;overflow-y:scroll;height:100%}.wy-nav-top{display:none;background:#2980b9;color:#fff;padding:.4045em .809em;position:relative;line-height:50px;text-align:center;font-size:100%;*zoom:1}.wy-nav-top:after,.wy-nav-top:before{display:table;content:""}.wy-nav-top:after{clear:both}.wy-nav-top a{color:#fff;font-weight:700}.wy-nav-top img{margin-right:12px;height:45px;width:45px;background-color:#2980b9;padding:5px;border-radius:100%}.wy-nav-top i{font-size:30px;float:left;cursor:pointer;padding-top:inherit}.wy-nav-content-wrap{margin-left:300px;background:#fcfcfc;min-height:100%}.wy-nav-content{padding:1.618em 3.236em;height:100%;max-width:800px;margin:auto}.wy-body-mask{position:fixed;width:100%;height:100%;background:rgba(0,0,0,.2);display:none;z-index:499}.wy-body-mask.on{display:block}footer{color:grey}footer p{margin-bottom:12px}.rst-content footer span.commit tt,footer span.commit .rst-content tt,footer span.commit code{padding:0;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:1em;background:none;border:none;color:grey}.rst-footer-buttons{*zoom:1}.rst-footer-buttons:after,.rst-footer-buttons:before{width:100%;display:table;content:""}.rst-footer-buttons:after{clear:both}.rst-breadcrumbs-buttons{margin-top:12px;*zoom:1}.rst-breadcrumbs-buttons:after,.rst-breadcrumbs-buttons:before{display:table;content:""}.rst-breadcrumbs-buttons:after{clear:both}#search-results .search li{margin-bottom:24px;border-bottom:1px solid #e1e4e5;padding-bottom:24px}#search-results .search li:first-child{border-top:1px solid #e1e4e5;padding-top:24px}#search-results .search li a{font-size:120%;margin-bottom:12px;display:inline-block}#search-results .context{color:grey;font-size:90%}.genindextable li>ul{margin-left:24px}@media screen and (max-width:768px){.wy-body-for-nav{background:#fcfcfc}.wy-nav-top{display:block}.wy-nav-side{left:-300px}.wy-nav-side.shift{width:85%;left:0}.wy-menu.wy-menu-vertical,.wy-side-nav-search,.wy-side-scroll{width:auto}.wy-nav-content-wrap{margin-left:0}.wy-nav-content-wrap .wy-nav-content{padding:1.618em}.wy-nav-content-wrap.shift{position:fixed;min-width:100%;left:85%;top:0;height:100%;overflow:hidden}}@media screen and (min-width:1100px){.wy-nav-content-wrap{background:rgba(0,0,0,.05)}.wy-nav-content{margin:0;background:#fcfcfc}}@media print{.rst-versions,.wy-nav-side,footer{display:none}.wy-nav-content-wrap{margin-left:0}}.rst-versions{position:fixed;bottom:0;left:0;width:300px;color:#fcfcfc;background:#1f1d1d;font-family:Lato,proxima-nova,Helvetica Neue,Arial,sans-serif;z-index:400}.rst-versions a{color:#2980b9;text-decoration:none}.rst-versions .rst-badge-small{display:none}.rst-versions .rst-current-version{padding:12px;background-color:#272525;display:block;text-align:right;font-size:90%;cursor:pointer;color:#27ae60;*zoom:1}.rst-versions .rst-current-version:after,.rst-versions .rst-current-version:before{display:table;content:""}.rst-versions .rst-current-version:after{clear:both}.rst-content .code-block-caption .rst-versions .rst-current-version .headerlink,.rst-content .eqno .rst-versions .rst-current-version .headerlink,.rst-content .rst-versions .rst-current-version .admonition-title,.rst-content code.download .rst-versions .rst-current-version span:first-child,.rst-content dl dt .rst-versions .rst-current-version .headerlink,.rst-content h1 .rst-versions .rst-current-version .headerlink,.rst-content h2 .rst-versions .rst-current-version .headerlink,.rst-content h3 .rst-versions .rst-current-version .headerlink,.rst-content h4 .rst-versions .rst-current-version .headerlink,.rst-content h5 .rst-versions .rst-current-version .headerlink,.rst-content h6 .rst-versions .rst-current-version .headerlink,.rst-content p .rst-versions .rst-current-version .headerlink,.rst-content table>caption .rst-versions .rst-current-version .headerlink,.rst-content tt.download .rst-versions .rst-current-version span:first-child,.rst-versions .rst-current-version .fa,.rst-versions .rst-current-version .icon,.rst-versions .rst-current-version .rst-content .admonition-title,.rst-versions .rst-current-version .rst-content .code-block-caption .headerlink,.rst-versions .rst-current-version .rst-content .eqno .headerlink,.rst-versions .rst-current-version .rst-content code.download span:first-child,.rst-versions .rst-current-version .rst-content dl dt .headerlink,.rst-versions .rst-current-version .rst-content h1 .headerlink,.rst-versions .rst-current-version .rst-content h2 .headerlink,.rst-versions .rst-current-version .rst-content h3 .headerlink,.rst-versions .rst-current-version .rst-content h4 .headerlink,.rst-versions .rst-current-version .rst-content h5 .headerlink,.rst-versions .rst-current-version .rst-content h6 .headerlink,.rst-versions .rst-current-version .rst-content p .headerlink,.rst-versions .rst-current-version .rst-content table>caption .headerlink,.rst-versions .rst-current-version .rst-content tt.download span:first-child,.rst-versions .rst-current-version .wy-menu-vertical li button.toctree-expand,.wy-menu-vertical li .rst-versions .rst-current-version button.toctree-expand{color:#fcfcfc}.rst-versions .rst-current-version .fa-book,.rst-versions .rst-current-version .icon-book{float:left}.rst-versions .rst-current-version.rst-out-of-date{background-color:#e74c3c;color:#fff}.rst-versions .rst-current-version.rst-active-old-version{background-color:#f1c40f;color:#000}.rst-versions.shift-up{height:auto;max-height:100%;overflow-y:scroll}.rst-versions.shift-up .rst-other-versions{display:block}.rst-versions .rst-other-versions{font-size:90%;padding:12px;color:grey;display:none}.rst-versions .rst-other-versions hr{display:block;height:1px;border:0;margin:20px 0;padding:0;border-top:1px solid #413d3d}.rst-versions .rst-other-versions dd{display:inline-block;margin:0}.rst-versions .rst-other-versions dd a{display:inline-block;padding:6px;color:#fcfcfc}.rst-versions .rst-other-versions .rtd-current-item{font-weight:700}.rst-versions.rst-badge{width:auto;bottom:20px;right:20px;left:auto;border:none;max-width:300px;max-height:90%}.rst-versions.rst-badge .fa-book,.rst-versions.rst-badge .icon-book{float:none;line-height:30px}.rst-versions.rst-badge.shift-up .rst-current-version{text-align:right}.rst-versions.rst-badge.shift-up .rst-current-version .fa-book,.rst-versions.rst-badge.shift-up .rst-current-version .icon-book{float:left}.rst-versions.rst-badge>.rst-current-version{width:auto;height:30px;line-height:30px;padding:0 6px;display:block;text-align:center}@media screen and (max-width:768px){.rst-versions{width:85%;display:none}.rst-versions.shift{display:block}}#flyout-search-form{padding:6px}.rst-content .toctree-wrapper>p.caption,.rst-content h1,.rst-content h2,.rst-content h3,.rst-content h4,.rst-content h5,.rst-content h6{margin-bottom:24px}.rst-content img{max-width:100%;height:auto}.rst-content div.figure,.rst-content figure{margin-bottom:24px}.rst-content div.figure .caption-text,.rst-content figure .caption-text{font-style:italic}.rst-content div.figure p:last-child.caption,.rst-content figure p:last-child.caption{margin-bottom:0}.rst-content div.figure.align-center,.rst-content figure.align-center{text-align:center}.rst-content .section>a>img,.rst-content .section>img,.rst-content section>a>img,.rst-content section>img{margin-bottom:24px}.rst-content abbr[title]{text-decoration:none}.rst-content.style-external-links a.reference.external:after{font-family:FontAwesome;content:"\f08e";color:#b3b3b3;vertical-align:super;font-size:60%;margin:0 .2em}.rst-content blockquote{margin-left:24px;line-height:24px;margin-bottom:24px}.rst-content pre.literal-block{white-space:pre;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;display:block;overflow:auto}.rst-content div[class^=highlight],.rst-content pre.literal-block{border:1px solid #e1e4e5;overflow-x:auto;margin:1px 0 24px}.rst-content div[class^=highlight] div[class^=highlight],.rst-content pre.literal-block div[class^=highlight]{padding:0;border:none;margin:0}.rst-content div[class^=highlight] td.code{width:100%}.rst-content .linenodiv pre{border-right:1px solid #e6e9ea;margin:0;padding:12px;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;user-select:none;pointer-events:none}.rst-content div[class^=highlight] pre{white-space:pre;margin:0;padding:12px;display:block;overflow:auto}.rst-content div[class^=highlight] pre .hll{display:block;margin:0 -12px;padding:0 12px}.rst-content .linenodiv pre,.rst-content div[class^=highlight] pre,.rst-content pre.literal-block{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;font-size:12px;line-height:1.4}.rst-content div.highlight .gp,.rst-content div.highlight span.linenos{user-select:none;pointer-events:none}.rst-content div.highlight span.linenos{display:inline-block;padding-left:0;padding-right:12px;margin-right:12px;border-right:1px solid #e6e9ea}.rst-content .code-block-caption{font-style:italic;font-size:85%;line-height:1;padding:1em 0;text-align:center}@media print{.rst-content .codeblock,.rst-content div[class^=highlight],.rst-content div[class^=highlight] pre{white-space:pre-wrap}}.rst-content .admonition,.rst-content .admonition-todo,.rst-content .attention,.rst-content .caution,.rst-content .danger,.rst-content .error,.rst-content .hint,.rst-content .important,.rst-content .note,.rst-content .seealso,.rst-content .tip,.rst-content .warning{clear:both}.rst-content .admonition-todo .last,.rst-content .admonition-todo>:last-child,.rst-content .admonition .last,.rst-content .admonition>:last-child,.rst-content .attention .last,.rst-content .attention>:last-child,.rst-content .caution .last,.rst-content .caution>:last-child,.rst-content .danger .last,.rst-content .danger>:last-child,.rst-content .error .last,.rst-content .error>:last-child,.rst-content .hint .last,.rst-content .hint>:last-child,.rst-content .important .last,.rst-content .important>:last-child,.rst-content .note .last,.rst-content .note>:last-child,.rst-content .seealso .last,.rst-content .seealso>:last-child,.rst-content .tip .last,.rst-content .tip>:last-child,.rst-content .warning .last,.rst-content .warning>:last-child{margin-bottom:0}.rst-content .admonition-title:before{margin-right:4px}.rst-content .admonition table{border-color:rgba(0,0,0,.1)}.rst-content .admonition table td,.rst-content .admonition table th{background:transparent!important;border-color:rgba(0,0,0,.1)!important}.rst-content .section ol.loweralpha,.rst-content .section ol.loweralpha>li,.rst-content .toctree-wrapper ol.loweralpha,.rst-content .toctree-wrapper ol.loweralpha>li,.rst-content section ol.loweralpha,.rst-content section ol.loweralpha>li{list-style:lower-alpha}.rst-content .section ol.upperalpha,.rst-content .section ol.upperalpha>li,.rst-content .toctree-wrapper ol.upperalpha,.rst-content .toctree-wrapper ol.upperalpha>li,.rst-content section ol.upperalpha,.rst-content section ol.upperalpha>li{list-style:upper-alpha}.rst-content .section ol li>*,.rst-content .section ul li>*,.rst-content .toctree-wrapper ol li>*,.rst-content .toctree-wrapper ul li>*,.rst-content section ol li>*,.rst-content section ul li>*{margin-top:12px;margin-bottom:12px}.rst-content .section ol li>:first-child,.rst-content .section ul li>:first-child,.rst-content .toctree-wrapper ol li>:first-child,.rst-content .toctree-wrapper ul li>:first-child,.rst-content section ol li>:first-child,.rst-content section ul li>:first-child{margin-top:0}.rst-content .section ol li>p,.rst-content .section ol li>p:last-child,.rst-content .section ul li>p,.rst-content .section ul li>p:last-child,.rst-content .toctree-wrapper ol li>p,.rst-content .toctree-wrapper ol li>p:last-child,.rst-content .toctree-wrapper ul li>p,.rst-content .toctree-wrapper ul li>p:last-child,.rst-content section ol li>p,.rst-content section ol li>p:last-child,.rst-content section ul li>p,.rst-content section ul li>p:last-child{margin-bottom:12px}.rst-content .section ol li>p:only-child,.rst-content .section ol li>p:only-child:last-child,.rst-content .section ul li>p:only-child,.rst-content .section ul li>p:only-child:last-child,.rst-content .toctree-wrapper ol li>p:only-child,.rst-content .toctree-wrapper ol li>p:only-child:last-child,.rst-content .toctree-wrapper ul li>p:only-child,.rst-content .toctree-wrapper ul li>p:only-child:last-child,.rst-content section ol li>p:only-child,.rst-content section ol li>p:only-child:last-child,.rst-content section ul li>p:only-child,.rst-content section ul li>p:only-child:last-child{margin-bottom:0}.rst-content .section ol li>ol,.rst-content .section ol li>ul,.rst-content .section ul li>ol,.rst-content .section ul li>ul,.rst-content .toctree-wrapper ol li>ol,.rst-content .toctree-wrapper ol li>ul,.rst-content .toctree-wrapper ul li>ol,.rst-content .toctree-wrapper ul li>ul,.rst-content section ol li>ol,.rst-content section ol li>ul,.rst-content section ul li>ol,.rst-content section ul li>ul{margin-bottom:12px}.rst-content .section ol.simple li>*,.rst-content .section ol.simple li ol,.rst-content .section ol.simple li ul,.rst-content .section ul.simple li>*,.rst-content .section ul.simple li ol,.rst-content .section ul.simple li ul,.rst-content .toctree-wrapper ol.simple li>*,.rst-content .toctree-wrapper ol.simple li ol,.rst-content .toctree-wrapper ol.simple li ul,.rst-content .toctree-wrapper ul.simple li>*,.rst-content .toctree-wrapper ul.simple li ol,.rst-content .toctree-wrapper ul.simple li ul,.rst-content section ol.simple li>*,.rst-content section ol.simple li ol,.rst-content section ol.simple li ul,.rst-content section ul.simple li>*,.rst-content section ul.simple li ol,.rst-content section ul.simple li ul{margin-top:0;margin-bottom:0}.rst-content .line-block{margin-left:0;margin-bottom:24px;line-height:24px}.rst-content .line-block .line-block{margin-left:24px;margin-bottom:0}.rst-content .topic-title{font-weight:700;margin-bottom:12px}.rst-content .toc-backref{color:#404040}.rst-content .align-right{float:right;margin:0 0 24px 24px}.rst-content .align-left{float:left;margin:0 24px 24px 0}.rst-content .align-center{margin:auto}.rst-content .align-center:not(table){display:block}.rst-content .code-block-caption .headerlink,.rst-content .eqno .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink,.rst-content dl dt .headerlink,.rst-content h1 .headerlink,.rst-content h2 .headerlink,.rst-content h3 .headerlink,.rst-content h4 .headerlink,.rst-content h5 .headerlink,.rst-content h6 .headerlink,.rst-content p.caption .headerlink,.rst-content p .headerlink,.rst-content table>caption .headerlink{opacity:0;font-size:14px;font-family:FontAwesome;margin-left:.5em}.rst-content .code-block-caption .headerlink:focus,.rst-content .code-block-caption:hover .headerlink,.rst-content .eqno .headerlink:focus,.rst-content .eqno:hover .headerlink,.rst-content .toctree-wrapper>p.caption .headerlink:focus,.rst-content .toctree-wrapper>p.caption:hover .headerlink,.rst-content dl dt .headerlink:focus,.rst-content dl dt:hover .headerlink,.rst-content h1 .headerlink:focus,.rst-content h1:hover .headerlink,.rst-content h2 .headerlink:focus,.rst-content h2:hover .headerlink,.rst-content h3 .headerlink:focus,.rst-content h3:hover .headerlink,.rst-content h4 .headerlink:focus,.rst-content h4:hover .headerlink,.rst-content h5 .headerlink:focus,.rst-content h5:hover .headerlink,.rst-content h6 .headerlink:focus,.rst-content h6:hover .headerlink,.rst-content p.caption .headerlink:focus,.rst-content p.caption:hover .headerlink,.rst-content p .headerlink:focus,.rst-content p:hover .headerlink,.rst-content table>caption .headerlink:focus,.rst-content table>caption:hover .headerlink{opacity:1}.rst-content p a{overflow-wrap:anywhere}.rst-content .wy-table td p,.rst-content .wy-table td ul,.rst-content .wy-table th p,.rst-content .wy-table th ul,.rst-content table.docutils td p,.rst-content table.docutils td ul,.rst-content table.docutils th p,.rst-content table.docutils th ul,.rst-content table.field-list td p,.rst-content table.field-list td ul,.rst-content table.field-list th p,.rst-content table.field-list th ul{font-size:inherit}.rst-content .btn:focus{outline:2px solid}.rst-content table>caption .headerlink:after{font-size:12px}.rst-content .centered{text-align:center}.rst-content .sidebar{float:right;width:40%;display:block;margin:0 0 24px 24px;padding:24px;background:#f3f6f6;border:1px solid #e1e4e5}.rst-content .sidebar dl,.rst-content .sidebar p,.rst-content .sidebar ul{font-size:90%}.rst-content .sidebar .last,.rst-content .sidebar>:last-child{margin-bottom:0}.rst-content .sidebar .sidebar-title{display:block;font-family:Roboto Slab,ff-tisa-web-pro,Georgia,Arial,sans-serif;font-weight:700;background:#e1e4e5;padding:6px 12px;margin:-24px -24px 24px;font-size:100%}.rst-content .highlighted{background:#f1c40f;box-shadow:0 0 0 2px #f1c40f;display:inline;font-weight:700}.rst-content .citation-reference,.rst-content .footnote-reference{vertical-align:baseline;position:relative;top:-.4em;line-height:0;font-size:90%}.rst-content .citation-reference>span.fn-bracket,.rst-content .footnote-reference>span.fn-bracket{display:none}.rst-content .hlist{width:100%}.rst-content dl dt span.classifier:before{content:" : "}.rst-content dl dt span.classifier-delimiter{display:none!important}html.writer-html4 .rst-content table.docutils.citation,html.writer-html4 .rst-content table.docutils.footnote{background:none;border:none}html.writer-html4 .rst-content table.docutils.citation td,html.writer-html4 .rst-content table.docutils.citation tr,html.writer-html4 .rst-content table.docutils.footnote td,html.writer-html4 .rst-content table.docutils.footnote tr{border:none;background-color:transparent!important;white-space:normal}html.writer-html4 .rst-content table.docutils.citation td.label,html.writer-html4 .rst-content table.docutils.footnote td.label{padding-left:0;padding-right:0;vertical-align:top}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{display:grid;grid-template-columns:auto minmax(80%,95%)}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{display:inline-grid;grid-template-columns:max-content auto}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{display:grid;grid-template-columns:auto auto minmax(.65rem,auto) minmax(40%,95%)}html.writer-html5 .rst-content aside.citation>span.label,html.writer-html5 .rst-content aside.footnote>span.label,html.writer-html5 .rst-content div.citation>span.label{grid-column-start:1;grid-column-end:2}html.writer-html5 .rst-content aside.citation>span.backrefs,html.writer-html5 .rst-content aside.footnote>span.backrefs,html.writer-html5 .rst-content div.citation>span.backrefs{grid-column-start:2;grid-column-end:3;grid-row-start:1;grid-row-end:3}html.writer-html5 .rst-content aside.citation>p,html.writer-html5 .rst-content aside.footnote>p,html.writer-html5 .rst-content div.citation>p{grid-column-start:4;grid-column-end:5}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.field-list,html.writer-html5 .rst-content dl.footnote{margin-bottom:24px}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dt{padding-left:1rem}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.field-list>dd,html.writer-html5 .rst-content dl.field-list>dt,html.writer-html5 .rst-content dl.footnote>dd,html.writer-html5 .rst-content dl.footnote>dt{margin-bottom:0}html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{font-size:.9rem}html.writer-html5 .rst-content dl.citation>dt,html.writer-html5 .rst-content dl.footnote>dt{margin:0 .5rem .5rem 0;line-height:1.2rem;word-break:break-all;font-weight:400}html.writer-html5 .rst-content dl.citation>dt>span.brackets:before,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:before{content:"["}html.writer-html5 .rst-content dl.citation>dt>span.brackets:after,html.writer-html5 .rst-content dl.footnote>dt>span.brackets:after{content:"]"}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a{word-break:keep-all}html.writer-html5 .rst-content dl.citation>dt>span.fn-backref>a:not(:first-child):before,html.writer-html5 .rst-content dl.footnote>dt>span.fn-backref>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content dl.citation>dd,html.writer-html5 .rst-content dl.footnote>dd{margin:0 0 .5rem;line-height:1.2rem}html.writer-html5 .rst-content dl.citation>dd p,html.writer-html5 .rst-content dl.footnote>dd p{font-size:.9rem}html.writer-html5 .rst-content aside.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content div.citation{padding-left:1rem;padding-right:1rem;font-size:.9rem;line-height:1.2rem}html.writer-html5 .rst-content aside.citation p,html.writer-html5 .rst-content aside.footnote p,html.writer-html5 .rst-content div.citation p{font-size:.9rem;line-height:1.2rem;margin-bottom:12px}html.writer-html5 .rst-content aside.citation span.backrefs,html.writer-html5 .rst-content aside.footnote span.backrefs,html.writer-html5 .rst-content div.citation span.backrefs{text-align:left;font-style:italic;margin-left:.65rem;word-break:break-word;word-spacing:-.1rem;max-width:5rem}html.writer-html5 .rst-content aside.citation span.backrefs>a,html.writer-html5 .rst-content aside.footnote span.backrefs>a,html.writer-html5 .rst-content div.citation span.backrefs>a{word-break:keep-all}html.writer-html5 .rst-content aside.citation span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content aside.footnote span.backrefs>a:not(:first-child):before,html.writer-html5 .rst-content div.citation span.backrefs>a:not(:first-child):before{content:" "}html.writer-html5 .rst-content aside.citation span.label,html.writer-html5 .rst-content aside.footnote span.label,html.writer-html5 .rst-content div.citation span.label{line-height:1.2rem}html.writer-html5 .rst-content aside.citation-list,html.writer-html5 .rst-content aside.footnote-list,html.writer-html5 .rst-content div.citation-list{margin-bottom:24px}html.writer-html5 .rst-content dl.option-list kbd{font-size:.9rem}.rst-content table.docutils.footnote,html.writer-html4 .rst-content table.docutils.citation,html.writer-html5 .rst-content aside.footnote,html.writer-html5 .rst-content aside.footnote-list aside.footnote,html.writer-html5 .rst-content div.citation-list>div.citation,html.writer-html5 .rst-content dl.citation,html.writer-html5 .rst-content dl.footnote{color:grey}.rst-content table.docutils.footnote code,.rst-content table.docutils.footnote tt,html.writer-html4 .rst-content table.docutils.citation code,html.writer-html4 .rst-content table.docutils.citation tt,html.writer-html5 .rst-content aside.footnote-list aside.footnote code,html.writer-html5 .rst-content aside.footnote-list aside.footnote tt,html.writer-html5 .rst-content aside.footnote code,html.writer-html5 .rst-content aside.footnote tt,html.writer-html5 .rst-content div.citation-list>div.citation code,html.writer-html5 .rst-content div.citation-list>div.citation tt,html.writer-html5 .rst-content dl.citation code,html.writer-html5 .rst-content dl.citation tt,html.writer-html5 .rst-content dl.footnote code,html.writer-html5 .rst-content dl.footnote tt{color:#555}.rst-content .wy-table-responsive.citation,.rst-content .wy-table-responsive.footnote{margin-bottom:0}.rst-content .wy-table-responsive.citation+:not(.citation),.rst-content .wy-table-responsive.footnote+:not(.footnote){margin-top:24px}.rst-content .wy-table-responsive.citation:last-child,.rst-content .wy-table-responsive.footnote:last-child{margin-bottom:24px}.rst-content table.docutils th{border-color:#e1e4e5}html.writer-html5 .rst-content table.docutils th{border:1px solid #e1e4e5}html.writer-html5 .rst-content table.docutils td>p,html.writer-html5 .rst-content table.docutils th>p{line-height:1rem;margin-bottom:0;font-size:.9rem}.rst-content table.docutils td .last,.rst-content table.docutils td .last>:last-child{margin-bottom:0}.rst-content table.field-list,.rst-content table.field-list td{border:none}.rst-content table.field-list td p{line-height:inherit}.rst-content table.field-list td>strong{display:inline-block}.rst-content table.field-list .field-name{padding-right:10px;text-align:left;white-space:nowrap}.rst-content table.field-list .field-body{text-align:left}.rst-content code,.rst-content tt{color:#000;font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;padding:2px 5px}.rst-content code big,.rst-content code em,.rst-content tt big,.rst-content tt em{font-size:100%!important;line-height:normal}.rst-content code.literal,.rst-content tt.literal{color:#e74c3c;white-space:normal}.rst-content code.xref,.rst-content tt.xref,a .rst-content code,a .rst-content tt{font-weight:700;color:#404040;overflow-wrap:normal}.rst-content kbd,.rst-content pre,.rst-content samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace}.rst-content a code,.rst-content a tt{color:#2980b9}.rst-content dl{margin-bottom:24px}.rst-content dl dt{font-weight:700;margin-bottom:12px}.rst-content dl ol,.rst-content dl p,.rst-content dl table,.rst-content dl ul{margin-bottom:12px}.rst-content dl dd{margin:0 0 12px 24px;line-height:24px}.rst-content dl dd>ol:last-child,.rst-content dl dd>p:last-child,.rst-content dl dd>table:last-child,.rst-content dl dd>ul:last-child{margin-bottom:0}html.writer-html4 .rst-content dl:not(.docutils),html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple){margin-bottom:24px}html.writer-html4 .rst-content dl:not(.docutils)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{display:table;margin:6px 0;font-size:90%;line-height:normal;background:#e7f2fa;color:#2980b9;border-top:3px solid #6ab0de;padding:6px;position:relative}html.writer-html4 .rst-content dl:not(.docutils)>dt:before,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:before{color:#6ab0de}html.writer-html4 .rst-content dl:not(.docutils)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt{margin-bottom:6px;border:none;border-left:3px solid #ccc;background:#f0f0f0;color:#555}html.writer-html4 .rst-content dl:not(.docutils) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) dl:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt .headerlink{color:#404040;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils)>dt:first-child,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple)>dt:first-child{margin-top:0}html.writer-html4 .rst-content dl:not(.docutils) code.descclassname,html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descclassname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{background-color:transparent;border:none;padding:0;font-size:100%!important}html.writer-html4 .rst-content dl:not(.docutils) code.descname,html.writer-html4 .rst-content dl:not(.docutils) tt.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) code.descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) tt.descname{font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .optional,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .optional{display:inline-block;padding:0 4px;color:#000;font-weight:700}html.writer-html4 .rst-content dl:not(.docutils) .property,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .property{display:inline-block;padding-right:8px;max-width:100%}html.writer-html4 .rst-content dl:not(.docutils) .k,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .k{font-style:italic}html.writer-html4 .rst-content dl:not(.docutils) .descclassname,html.writer-html4 .rst-content dl:not(.docutils) .descname,html.writer-html4 .rst-content dl:not(.docutils) .sig-name,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descclassname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .descname,html.writer-html5 .rst-content dl[class]:not(.option-list):not(.field-list):not(.footnote):not(.citation):not(.glossary):not(.simple) .sig-name{font-family:SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,Courier,monospace;color:#000}.rst-content .viewcode-back,.rst-content .viewcode-link{display:inline-block;color:#27ae60;font-size:80%;padding-left:24px}.rst-content .viewcode-back{display:block;float:right}.rst-content p.rubric{margin-bottom:12px;font-weight:700}.rst-content code.download,.rst-content tt.download{background:inherit;padding:inherit;font-weight:400;font-family:inherit;font-size:inherit;color:inherit;border:inherit;white-space:inherit}.rst-content code.download span:first-child,.rst-content tt.download span:first-child{-webkit-font-smoothing:subpixel-antialiased}.rst-content code.download span:first-child:before,.rst-content tt.download span:first-child:before{margin-right:4px}.rst-content .guilabel,.rst-content .menuselection{font-size:80%;font-weight:700;border-radius:4px;padding:2.4px 6px;margin:auto 2px}.rst-content .guilabel,.rst-content .menuselection{border:1px solid #7fbbe3;background:#e7f2fa}.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>.kbd,.rst-content :not(dl.option-list)>:not(dt):not(kbd):not(.kbd)>kbd{color:inherit;font-size:80%;background-color:#fff;border:1px solid #a6a6a6;border-radius:4px;box-shadow:0 2px grey;padding:2.4px 6px;margin:auto 0}.rst-content .versionmodified{font-style:italic}@media screen and (max-width:480px){.rst-content .sidebar{width:100%}}span[id*=MathJax-Span]{color:#404040}.math{text-align:center}@font-face{font-family:Lato;src:url(fonts/lato-normal.woff2?bd03a2cc277bbbc338d464e679fe9942) format("woff2"),url(fonts/lato-normal.woff?27bd77b9162d388cb8d4c4217c7c5e2a) format("woff");font-weight:400;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold.woff2?cccb897485813c7c256901dbca54ecf2) format("woff2"),url(fonts/lato-bold.woff?d878b6c29b10beca227e9eef4246111b) format("woff");font-weight:700;font-style:normal;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-bold-italic.woff2?0b6bb6725576b072c5d0b02ecdd1900d) format("woff2"),url(fonts/lato-bold-italic.woff?9c7e4e9eb485b4a121c760e61bc3707c) format("woff");font-weight:700;font-style:italic;font-display:block}@font-face{font-family:Lato;src:url(fonts/lato-normal-italic.woff2?4eb103b4d12be57cb1d040ed5e162e9d) format("woff2"),url(fonts/lato-normal-italic.woff?f28f2d6482446544ef1ea1ccc6dd5892) format("woff");font-weight:400;font-style:italic;font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:400;src:url(fonts/Roboto-Slab-Regular.woff2?7abf5b8d04d26a2cafea937019bca958) format("woff2"),url(fonts/Roboto-Slab-Regular.woff?c1be9284088d487c5e3ff0a10a92e58c) format("woff");font-display:block}@font-face{font-family:Roboto Slab;font-style:normal;font-weight:700;src:url(fonts/Roboto-Slab-Bold.woff2?9984f4a9bda09be08e83f2506954adbe) format("woff2"),url(fonts/Roboto-Slab-Bold.woff?bed5564a116b05148e3b3bea6fb1162a) format("woff");font-display:block} \ No newline at end of file diff --git a/docs/_build/html/_static/doctools.js b/docs/_build/html/_static/doctools.js new file mode 100644 index 0000000..0398ebb --- /dev/null +++ b/docs/_build/html/_static/doctools.js @@ -0,0 +1,149 @@ +/* + * Base JavaScript utilities for all Sphinx HTML documentation. + */ +"use strict"; + +const BLACKLISTED_KEY_CONTROL_ELEMENTS = new Set([ + "TEXTAREA", + "INPUT", + "SELECT", + "BUTTON", +]); + +const _ready = (callback) => { + if (document.readyState !== "loading") { + callback(); + } else { + document.addEventListener("DOMContentLoaded", callback); + } +}; + +/** + * Small JavaScript module for the documentation. + */ +const Documentation = { + init: () => { + Documentation.initDomainIndexTable(); + Documentation.initOnKeyListeners(); + }, + + /** + * i18n support + */ + TRANSLATIONS: {}, + PLURAL_EXPR: (n) => (n === 1 ? 0 : 1), + LOCALE: "unknown", + + // gettext and ngettext don't access this so that the functions + // can safely bound to a different name (_ = Documentation.gettext) + gettext: (string) => { + const translated = Documentation.TRANSLATIONS[string]; + switch (typeof translated) { + case "undefined": + return string; // no translation + case "string": + return translated; // translation exists + default: + return translated[0]; // (singular, plural) translation tuple exists + } + }, + + ngettext: (singular, plural, n) => { + const translated = Documentation.TRANSLATIONS[singular]; + if (typeof translated !== "undefined") + return translated[Documentation.PLURAL_EXPR(n)]; + return n === 1 ? singular : plural; + }, + + addTranslations: (catalog) => { + Object.assign(Documentation.TRANSLATIONS, catalog.messages); + Documentation.PLURAL_EXPR = new Function( + "n", + `return (${catalog.plural_expr})` + ); + Documentation.LOCALE = catalog.locale; + }, + + /** + * helper function to focus on search bar + */ + focusSearchBar: () => { + document.querySelectorAll("input[name=q]")[0]?.focus(); + }, + + /** + * Initialise the domain index toggle buttons + */ + initDomainIndexTable: () => { + const toggler = (el) => { + const idNumber = el.id.substr(7); + const toggledRows = document.querySelectorAll(`tr.cg-${idNumber}`); + if (el.src.substr(-9) === "minus.png") { + el.src = `${el.src.substr(0, el.src.length - 9)}plus.png`; + toggledRows.forEach((el) => (el.style.display = "none")); + } else { + el.src = `${el.src.substr(0, el.src.length - 8)}minus.png`; + toggledRows.forEach((el) => (el.style.display = "")); + } + }; + + const togglerElements = document.querySelectorAll("img.toggler"); + togglerElements.forEach((el) => + el.addEventListener("click", (event) => toggler(event.currentTarget)) + ); + togglerElements.forEach((el) => (el.style.display = "")); + if (DOCUMENTATION_OPTIONS.COLLAPSE_INDEX) togglerElements.forEach(toggler); + }, + + initOnKeyListeners: () => { + // only install a listener if it is really needed + if ( + !DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS && + !DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS + ) + return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.altKey || event.ctrlKey || event.metaKey) return; + + if (!event.shiftKey) { + switch (event.key) { + case "ArrowLeft": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const prevLink = document.querySelector('link[rel="prev"]'); + if (prevLink && prevLink.href) { + window.location.href = prevLink.href; + event.preventDefault(); + } + break; + case "ArrowRight": + if (!DOCUMENTATION_OPTIONS.NAVIGATION_WITH_KEYS) break; + + const nextLink = document.querySelector('link[rel="next"]'); + if (nextLink && nextLink.href) { + window.location.href = nextLink.href; + event.preventDefault(); + } + break; + } + } + + // some keyboard layouts may need Shift to get / + switch (event.key) { + case "/": + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) break; + Documentation.focusSearchBar(); + event.preventDefault(); + } + }); + }, +}; + +// quick alias for translations +const _ = Documentation.gettext; + +_ready(Documentation.init); diff --git a/docs/_build/html/_static/documentation_options.js b/docs/_build/html/_static/documentation_options.js new file mode 100644 index 0000000..7e4c114 --- /dev/null +++ b/docs/_build/html/_static/documentation_options.js @@ -0,0 +1,13 @@ +const DOCUMENTATION_OPTIONS = { + VERSION: '', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/docs/_build/html/_static/fedcloudclient-logo-non-transparent-small.png b/docs/_build/html/_static/fedcloudclient-logo-non-transparent-small.png new file mode 100644 index 0000000..74e50c3 Binary files /dev/null and b/docs/_build/html/_static/fedcloudclient-logo-non-transparent-small.png differ diff --git a/docs/_build/html/_static/file.png b/docs/_build/html/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/docs/_build/html/_static/file.png differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bold.eot b/docs/_build/html/_static/fonts/Lato/lato-bold.eot new file mode 100644 index 0000000..3361183 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bold.eot differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bold.ttf b/docs/_build/html/_static/fonts/Lato/lato-bold.ttf new file mode 100644 index 0000000..29f691d Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bold.ttf differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bold.woff b/docs/_build/html/_static/fonts/Lato/lato-bold.woff new file mode 100644 index 0000000..c6dff51 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bold.woff differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bold.woff2 b/docs/_build/html/_static/fonts/Lato/lato-bold.woff2 new file mode 100644 index 0000000..bb19504 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bold.woff2 differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bolditalic.eot b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.eot new file mode 100644 index 0000000..3d41549 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.eot differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bolditalic.ttf b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.ttf new file mode 100644 index 0000000..f402040 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.ttf differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff new file mode 100644 index 0000000..88ad05b Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 new file mode 100644 index 0000000..c4e3d80 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-bolditalic.woff2 differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-italic.eot b/docs/_build/html/_static/fonts/Lato/lato-italic.eot new file mode 100644 index 0000000..3f82642 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-italic.eot differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-italic.ttf b/docs/_build/html/_static/fonts/Lato/lato-italic.ttf new file mode 100644 index 0000000..b4bfc9b Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-italic.ttf differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-italic.woff b/docs/_build/html/_static/fonts/Lato/lato-italic.woff new file mode 100644 index 0000000..76114bc Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-italic.woff differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-italic.woff2 b/docs/_build/html/_static/fonts/Lato/lato-italic.woff2 new file mode 100644 index 0000000..3404f37 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-italic.woff2 differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-regular.eot b/docs/_build/html/_static/fonts/Lato/lato-regular.eot new file mode 100644 index 0000000..11e3f2a Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-regular.eot differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-regular.ttf b/docs/_build/html/_static/fonts/Lato/lato-regular.ttf new file mode 100644 index 0000000..74decd9 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-regular.ttf differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-regular.woff b/docs/_build/html/_static/fonts/Lato/lato-regular.woff new file mode 100644 index 0000000..ae1307f Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-regular.woff differ diff --git a/docs/_build/html/_static/fonts/Lato/lato-regular.woff2 b/docs/_build/html/_static/fonts/Lato/lato-regular.woff2 new file mode 100644 index 0000000..3bf9843 Binary files /dev/null and b/docs/_build/html/_static/fonts/Lato/lato-regular.woff2 differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot new file mode 100644 index 0000000..79dc8ef Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.eot differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf new file mode 100644 index 0000000..df5d1df Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.ttf differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff new file mode 100644 index 0000000..6cb6000 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 new file mode 100644 index 0000000..7059e23 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-bold.woff2 differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot new file mode 100644 index 0000000..2f7ca78 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.eot differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf new file mode 100644 index 0000000..eb52a79 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.ttf differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff new file mode 100644 index 0000000..f815f63 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff differ diff --git a/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 new file mode 100644 index 0000000..f2c76e5 Binary files /dev/null and b/docs/_build/html/_static/fonts/RobotoSlab/roboto-slab-v7-regular.woff2 differ diff --git a/docs/_build/html/_static/jquery.js b/docs/_build/html/_static/jquery.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/docs/_build/html/_static/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0"),n("table.docutils.footnote").wrap("
"),n("table.docutils.citation").wrap("
"),n(".wy-menu-vertical ul").not(".simple").siblings("a").each((function(){var t=n(this);expand=n(''),expand.on("click",(function(n){return e.toggleCurrent(t),n.stopPropagation(),!1})),t.prepend(expand)}))},reset:function(){var n=encodeURI(window.location.hash)||"#";try{var e=$(".wy-menu-vertical"),t=e.find('[href="'+n+'"]');if(0===t.length){var i=$('.document [id="'+n.substring(1)+'"]').closest("div.section");0===(t=e.find('[href="#'+i.attr("id")+'"]')).length&&(t=e.find('[href="#"]'))}if(t.length>0){$(".wy-menu-vertical .current").removeClass("current").attr("aria-expanded","false"),t.addClass("current").attr("aria-expanded","true"),t.closest("li.toctree-l1").parent().addClass("current").attr("aria-expanded","true");for(let n=1;n<=10;n++)t.closest("li.toctree-l"+n).addClass("current").attr("aria-expanded","true");t[0].scrollIntoView()}}catch(n){console.log("Error expanding nav for anchor",n)}},onScroll:function(){this.winScroll=!1;var n=this.win.scrollTop(),e=n+this.winHeight,t=this.navBar.scrollTop()+(n-this.winPosition);n<0||e>this.docHeight||(this.navBar.scrollTop(t),this.winPosition=n)},onResize:function(){this.winResize=!1,this.winHeight=this.win.height(),this.docHeight=$(document).height()},hashChange:function(){this.linkScroll=!0,this.win.one("hashchange",(function(){this.linkScroll=!1}))},toggleCurrent:function(n){var e=n.closest("li");e.siblings("li.current").removeClass("current").attr("aria-expanded","false"),e.siblings().find("li.current").removeClass("current").attr("aria-expanded","false");var t=e.find("> ul li");t.length&&(t.removeClass("current").attr("aria-expanded","false"),e.toggleClass("current").attr("aria-expanded",(function(n,e){return"true"==e?"false":"true"})))}},"undefined"!=typeof window&&(window.SphinxRtdTheme={Navigation:n.exports.ThemeNav,StickyNav:n.exports.ThemeNav}),function(){for(var n=0,e=["ms","moz","webkit","o"],t=0;t a.language.name.localeCompare(b.language.name)); + + const languagesHTML = ` +
+
Languages
+ ${languages + .map( + (translation) => ` +
+ ${translation.language.code} +
+ `, + ) + .join("\n")} +
+ `; + return languagesHTML; + } + + function renderVersions(config) { + if (!config.versions.active.length) { + return ""; + } + const versionsHTML = ` +
+
Versions
+ ${config.versions.active + .map( + (version) => ` +
+ ${version.slug} +
+ `, + ) + .join("\n")} +
+ `; + return versionsHTML; + } + + function renderDownloads(config) { + if (!Object.keys(config.versions.current.downloads).length) { + return ""; + } + const downloadsNameDisplay = { + pdf: "PDF", + epub: "Epub", + htmlzip: "HTML", + }; + + const downloadsHTML = ` +
+
Downloads
+ ${Object.entries(config.versions.current.downloads) + .map( + ([name, url]) => ` +
+ ${downloadsNameDisplay[name]} +
+ `, + ) + .join("\n")} +
+ `; + return downloadsHTML; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const flyout = ` +
+ + Read the Docs + v: ${config.versions.current.slug} + + +
+
+ ${renderLanguages(config)} + ${renderVersions(config)} + ${renderDownloads(config)} +
+
On Read the Docs
+
+ Project Home +
+
+ Builds +
+
+ Downloads +
+
+
+
Search
+
+
+ +
+
+
+
+ + Hosted by Read the Docs + +
+
+ `; + + // Inject the generated flyout into the body HTML element. + document.body.insertAdjacentHTML("beforeend", flyout); + + // Trigger the Read the Docs Addons Search modal when clicking on the "Search docs" input from inside the flyout. + document + .querySelector("#flyout-search-form") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); + }) +} + +if (themeLanguageSelector || themeVersionSelector) { + function onSelectorSwitch(event) { + const option = event.target.selectedIndex; + const item = event.target.options[option]; + window.location.href = item.dataset.url; + } + + document.addEventListener("readthedocs-addons-data-ready", function (event) { + const config = event.detail.data(); + + const versionSwitch = document.querySelector( + "div.switch-menus > div.version-switch", + ); + if (themeVersionSelector) { + let versions = config.versions.active; + if (config.versions.current.hidden || config.versions.current.type === "external") { + versions.unshift(config.versions.current); + } + const versionSelect = ` + + `; + + versionSwitch.innerHTML = versionSelect; + versionSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + + const languageSwitch = document.querySelector( + "div.switch-menus > div.language-switch", + ); + + if (themeLanguageSelector) { + if (config.projects.translations.length) { + // Add the current language to the options on the selector + let languages = config.projects.translations.concat( + config.projects.current, + ); + languages = languages.sort((a, b) => + a.language.name.localeCompare(b.language.name), + ); + + const languageSelect = ` + + `; + + languageSwitch.innerHTML = languageSelect; + languageSwitch.firstElementChild.addEventListener("change", onSelectorSwitch); + } + else { + languageSwitch.remove(); + } + } + }); +} + +document.addEventListener("readthedocs-addons-data-ready", function (event) { + // Trigger the Read the Docs Addons Search modal when clicking on "Search docs" input from the topnav. + document + .querySelector("[role='search'] input") + .addEventListener("focusin", () => { + const event = new CustomEvent("readthedocs-search-show"); + document.dispatchEvent(event); + }); +}); \ No newline at end of file diff --git a/docs/_build/html/_static/language_data.js b/docs/_build/html/_static/language_data.js new file mode 100644 index 0000000..c7fe6c6 --- /dev/null +++ b/docs/_build/html/_static/language_data.js @@ -0,0 +1,192 @@ +/* + * This script contains the language-specific data used by searchtools.js, + * namely the list of stopwords, stemmer, scorer and splitter. + */ + +var stopwords = ["a", "and", "are", "as", "at", "be", "but", "by", "for", "if", "in", "into", "is", "it", "near", "no", "not", "of", "on", "or", "such", "that", "the", "their", "then", "there", "these", "they", "this", "to", "was", "will", "with"]; + + +/* Non-minified version is copied as a separate JS file, if available */ + +/** + * Porter Stemmer + */ +var Stemmer = function() { + + var step2list = { + ational: 'ate', + tional: 'tion', + enci: 'ence', + anci: 'ance', + izer: 'ize', + bli: 'ble', + alli: 'al', + entli: 'ent', + eli: 'e', + ousli: 'ous', + ization: 'ize', + ation: 'ate', + ator: 'ate', + alism: 'al', + iveness: 'ive', + fulness: 'ful', + ousness: 'ous', + aliti: 'al', + iviti: 'ive', + biliti: 'ble', + logi: 'log' + }; + + var step3list = { + icate: 'ic', + ative: '', + alize: 'al', + iciti: 'ic', + ical: 'ic', + ful: '', + ness: '' + }; + + var c = "[^aeiou]"; // consonant + var v = "[aeiouy]"; // vowel + var C = c + "[^aeiouy]*"; // consonant sequence + var V = v + "[aeiou]*"; // vowel sequence + + var mgr0 = "^(" + C + ")?" + V + C; // [C]VC... is m>0 + var meq1 = "^(" + C + ")?" + V + C + "(" + V + ")?$"; // [C]VC[V] is m=1 + var mgr1 = "^(" + C + ")?" + V + C + V + C; // [C]VCVC... is m>1 + var s_v = "^(" + C + ")?" + v; // vowel in stem + + this.stemWord = function (w) { + var stem; + var suffix; + var firstch; + var origword = w; + + if (w.length < 3) + return w; + + var re; + var re2; + var re3; + var re4; + + firstch = w.substr(0,1); + if (firstch == "y") + w = firstch.toUpperCase() + w.substr(1); + + // Step 1a + re = /^(.+?)(ss|i)es$/; + re2 = /^(.+?)([^s])s$/; + + if (re.test(w)) + w = w.replace(re,"$1$2"); + else if (re2.test(w)) + w = w.replace(re2,"$1$2"); + + // Step 1b + re = /^(.+?)eed$/; + re2 = /^(.+?)(ed|ing)$/; + if (re.test(w)) { + var fp = re.exec(w); + re = new RegExp(mgr0); + if (re.test(fp[1])) { + re = /.$/; + w = w.replace(re,""); + } + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1]; + re2 = new RegExp(s_v); + if (re2.test(stem)) { + w = stem; + re2 = /(at|bl|iz)$/; + re3 = new RegExp("([^aeiouylsz])\\1$"); + re4 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re2.test(w)) + w = w + "e"; + else if (re3.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + else if (re4.test(w)) + w = w + "e"; + } + } + + // Step 1c + re = /^(.+?)y$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(s_v); + if (re.test(stem)) + w = stem + "i"; + } + + // Step 2 + re = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step2list[suffix]; + } + + // Step 3 + re = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + suffix = fp[2]; + re = new RegExp(mgr0); + if (re.test(stem)) + w = stem + step3list[suffix]; + } + + // Step 4 + re = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/; + re2 = /^(.+?)(s|t)(ion)$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + if (re.test(stem)) + w = stem; + } + else if (re2.test(w)) { + var fp = re2.exec(w); + stem = fp[1] + fp[2]; + re2 = new RegExp(mgr1); + if (re2.test(stem)) + w = stem; + } + + // Step 5 + re = /^(.+?)e$/; + if (re.test(w)) { + var fp = re.exec(w); + stem = fp[1]; + re = new RegExp(mgr1); + re2 = new RegExp(meq1); + re3 = new RegExp("^" + C + v + "[^aeiouwxy]$"); + if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) + w = stem; + } + re = /ll$/; + re2 = new RegExp(mgr1); + if (re.test(w) && re2.test(w)) { + re = /.$/; + w = w.replace(re,""); + } + + // and turn initial Y back to y + if (firstch == "y") + w = firstch.toLowerCase() + w.substr(1); + return w; + } +} + diff --git a/docs/_build/html/_static/minus.png b/docs/_build/html/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/docs/_build/html/_static/minus.png differ diff --git a/docs/_build/html/_static/plus.png b/docs/_build/html/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/docs/_build/html/_static/plus.png differ diff --git a/docs/_build/html/_static/pygments.css b/docs/_build/html/_static/pygments.css new file mode 100644 index 0000000..6f8b210 --- /dev/null +++ b/docs/_build/html/_static/pygments.css @@ -0,0 +1,75 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #3D7B7B; font-style: italic } /* Comment */ +.highlight .err { border: 1px solid #F00 } /* Error */ +.highlight .k { color: #008000; font-weight: bold } /* Keyword */ +.highlight .o { color: #666 } /* Operator */ +.highlight .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #9C6500 } /* Comment.Preproc */ +.highlight .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #A00000 } /* Generic.Deleted */ +.highlight .ge { font-style: italic } /* Generic.Emph */ +.highlight .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */ +.highlight .gr { color: #E40000 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #008400 } /* Generic.Inserted */ +.highlight .go { color: #717171 } /* Generic.Output */ +.highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ +.highlight .gs { font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #04D } /* Generic.Traceback */ +.highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #008000 } /* Keyword.Pseudo */ +.highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #B00040 } /* Keyword.Type */ +.highlight .m { color: #666 } /* Literal.Number */ +.highlight .s { color: #BA2121 } /* Literal.String */ +.highlight .na { color: #687822 } /* Name.Attribute */ +.highlight .nb { color: #008000 } /* Name.Builtin */ +.highlight .nc { color: #00F; font-weight: bold } /* Name.Class */ +.highlight .no { color: #800 } /* Name.Constant */ +.highlight .nd { color: #A2F } /* Name.Decorator */ +.highlight .ni { color: #717171; font-weight: bold } /* Name.Entity */ +.highlight .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #00F } /* Name.Function */ +.highlight .nl { color: #767600 } /* Name.Label */ +.highlight .nn { color: #00F; font-weight: bold } /* Name.Namespace */ +.highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #19177C } /* Name.Variable */ +.highlight .ow { color: #A2F; font-weight: bold } /* Operator.Word */ +.highlight .w { color: #BBB } /* Text.Whitespace */ +.highlight .mb { color: #666 } /* Literal.Number.Bin */ +.highlight .mf { color: #666 } /* Literal.Number.Float */ +.highlight .mh { color: #666 } /* Literal.Number.Hex */ +.highlight .mi { color: #666 } /* Literal.Number.Integer */ +.highlight .mo { color: #666 } /* Literal.Number.Oct */ +.highlight .sa { color: #BA2121 } /* Literal.String.Affix */ +.highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ +.highlight .sc { color: #BA2121 } /* Literal.String.Char */ +.highlight .dl { color: #BA2121 } /* Literal.String.Delimiter */ +.highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #BA2121 } /* Literal.String.Double */ +.highlight .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */ +.highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ +.highlight .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */ +.highlight .sx { color: #008000 } /* Literal.String.Other */ +.highlight .sr { color: #A45A77 } /* Literal.String.Regex */ +.highlight .s1 { color: #BA2121 } /* Literal.String.Single */ +.highlight .ss { color: #19177C } /* Literal.String.Symbol */ +.highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #00F } /* Name.Function.Magic */ +.highlight .vc { color: #19177C } /* Name.Variable.Class */ +.highlight .vg { color: #19177C } /* Name.Variable.Global */ +.highlight .vi { color: #19177C } /* Name.Variable.Instance */ +.highlight .vm { color: #19177C } /* Name.Variable.Magic */ +.highlight .il { color: #666 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/docs/_build/html/_static/searchtools.js b/docs/_build/html/_static/searchtools.js new file mode 100644 index 0000000..2c774d1 --- /dev/null +++ b/docs/_build/html/_static/searchtools.js @@ -0,0 +1,632 @@ +/* + * Sphinx JavaScript utilities for the full-text search. + */ +"use strict"; + +/** + * Simple result scoring code. + */ +if (typeof Scorer === "undefined") { + var Scorer = { + // Implement the following function to further tweak the score for each result + // The function takes a result array [docname, title, anchor, descr, score, filename] + // and returns the new score. + /* + score: result => { + const [docname, title, anchor, descr, score, filename, kind] = result + return score + }, + */ + + // query matches the full name of an object + objNameMatch: 11, + // or matches in the last dotted part of the object name + objPartialMatch: 6, + // Additive scores depending on the priority of the object + objPrio: { + 0: 15, // used to be importantResults + 1: 5, // used to be objectResults + 2: -5, // used to be unimportantResults + }, + // Used when the priority is not in the mapping. + objPrioDefault: 0, + + // query found in title + title: 15, + partialTitle: 7, + // query found in terms + term: 5, + partialTerm: 2, + }; +} + +// Global search result kind enum, used by themes to style search results. +class SearchResultKind { + static get index() { return "index"; } + static get object() { return "object"; } + static get text() { return "text"; } + static get title() { return "title"; } +} + +const _removeChildren = (element) => { + while (element && element.lastChild) element.removeChild(element.lastChild); +}; + +/** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping + */ +const _escapeRegExp = (string) => + string.replace(/[.*+\-?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string + +const _displayItem = (item, searchTerms, highlightTerms) => { + const docBuilder = DOCUMENTATION_OPTIONS.BUILDER; + const docFileSuffix = DOCUMENTATION_OPTIONS.FILE_SUFFIX; + const docLinkSuffix = DOCUMENTATION_OPTIONS.LINK_SUFFIX; + const showSearchSummary = DOCUMENTATION_OPTIONS.SHOW_SEARCH_SUMMARY; + const contentRoot = document.documentElement.dataset.content_root; + + const [docName, title, anchor, descr, score, _filename, kind] = item; + + let listItem = document.createElement("li"); + // Add a class representing the item's type: + // can be used by a theme's CSS selector for styling + // See SearchResultKind for the class names. + listItem.classList.add(`kind-${kind}`); + let requestUrl; + let linkUrl; + if (docBuilder === "dirhtml") { + // dirhtml builder + let dirname = docName + "/"; + if (dirname.match(/\/index\/$/)) + dirname = dirname.substring(0, dirname.length - 6); + else if (dirname === "index/") dirname = ""; + requestUrl = contentRoot + dirname; + linkUrl = requestUrl; + } else { + // normal html builders + requestUrl = contentRoot + docName + docFileSuffix; + linkUrl = docName + docLinkSuffix; + } + let linkEl = listItem.appendChild(document.createElement("a")); + linkEl.href = linkUrl + anchor; + linkEl.dataset.score = score; + linkEl.innerHTML = title; + if (descr) { + listItem.appendChild(document.createElement("span")).innerHTML = + " (" + descr + ")"; + // highlight search terms in the description + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + } + else if (showSearchSummary) + fetch(requestUrl) + .then((responseData) => responseData.text()) + .then((data) => { + if (data) + listItem.appendChild( + Search.makeSearchSummary(data, searchTerms, anchor) + ); + // highlight search terms in the summary + if (SPHINX_HIGHLIGHT_ENABLED) // set in sphinx_highlight.js + highlightTerms.forEach((term) => _highlightText(listItem, term, "highlighted")); + }); + Search.output.appendChild(listItem); +}; +const _finishSearch = (resultCount) => { + Search.stopPulse(); + Search.title.innerText = _("Search Results"); + if (!resultCount) + Search.status.innerText = Documentation.gettext( + "Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories." + ); + else + Search.status.innerText = Documentation.ngettext( + "Search finished, found one page matching the search query.", + "Search finished, found ${resultCount} pages matching the search query.", + resultCount, + ).replace('${resultCount}', resultCount); +}; +const _displayNextItem = ( + results, + resultCount, + searchTerms, + highlightTerms, +) => { + // results left, load the summary and display it + // this is intended to be dynamic (don't sub resultsCount) + if (results.length) { + _displayItem(results.pop(), searchTerms, highlightTerms); + setTimeout( + () => _displayNextItem(results, resultCount, searchTerms, highlightTerms), + 5 + ); + } + // search finished, update title and status message + else _finishSearch(resultCount); +}; +// Helper function used by query() to order search results. +// Each input is an array of [docname, title, anchor, descr, score, filename, kind]. +// Order the results by score (in opposite order of appearance, since the +// `_displayNextItem` function uses pop() to retrieve items) and then alphabetically. +const _orderResultsByScoreThenName = (a, b) => { + const leftScore = a[4]; + const rightScore = b[4]; + if (leftScore === rightScore) { + // same score: sort alphabetically + const leftTitle = a[1].toLowerCase(); + const rightTitle = b[1].toLowerCase(); + if (leftTitle === rightTitle) return 0; + return leftTitle > rightTitle ? -1 : 1; // inverted is intentional + } + return leftScore > rightScore ? 1 : -1; +}; + +/** + * Default splitQuery function. Can be overridden in ``sphinx.search`` with a + * custom function per language. + * + * The regular expression works by splitting the string on consecutive characters + * that are not Unicode letters, numbers, underscores, or emoji characters. + * This is the same as ``\W+`` in Python, preserving the surrogate pair area. + */ +if (typeof splitQuery === "undefined") { + var splitQuery = (query) => query + .split(/[^\p{Letter}\p{Number}_\p{Emoji_Presentation}]+/gu) + .filter(term => term) // remove remaining empty strings +} + +/** + * Search Module + */ +const Search = { + _index: null, + _queued_query: null, + _pulse_status: -1, + + htmlToText: (htmlString, anchor) => { + const htmlElement = new DOMParser().parseFromString(htmlString, 'text/html'); + for (const removalQuery of [".headerlink", "script", "style"]) { + htmlElement.querySelectorAll(removalQuery).forEach((el) => { el.remove() }); + } + if (anchor) { + const anchorContent = htmlElement.querySelector(`[role="main"] ${anchor}`); + if (anchorContent) return anchorContent.textContent; + + console.warn( + `Anchored content block not found. Sphinx search tries to obtain it via DOM query '[role=main] ${anchor}'. Check your theme or template.` + ); + } + + // if anchor not specified or not found, fall back to main content + const docContent = htmlElement.querySelector('[role="main"]'); + if (docContent) return docContent.textContent; + + console.warn( + "Content block not found. Sphinx search tries to obtain it via DOM query '[role=main]'. Check your theme or template." + ); + return ""; + }, + + init: () => { + const query = new URLSearchParams(window.location.search).get("q"); + document + .querySelectorAll('input[name="q"]') + .forEach((el) => (el.value = query)); + if (query) Search.performSearch(query); + }, + + loadIndex: (url) => + (document.body.appendChild(document.createElement("script")).src = url), + + setIndex: (index) => { + Search._index = index; + if (Search._queued_query !== null) { + const query = Search._queued_query; + Search._queued_query = null; + Search.query(query); + } + }, + + hasIndex: () => Search._index !== null, + + deferQuery: (query) => (Search._queued_query = query), + + stopPulse: () => (Search._pulse_status = -1), + + startPulse: () => { + if (Search._pulse_status >= 0) return; + + const pulse = () => { + Search._pulse_status = (Search._pulse_status + 1) % 4; + Search.dots.innerText = ".".repeat(Search._pulse_status); + if (Search._pulse_status >= 0) window.setTimeout(pulse, 500); + }; + pulse(); + }, + + /** + * perform a search for something (or wait until index is loaded) + */ + performSearch: (query) => { + // create the required interface elements + const searchText = document.createElement("h2"); + searchText.textContent = _("Searching"); + const searchSummary = document.createElement("p"); + searchSummary.classList.add("search-summary"); + searchSummary.innerText = ""; + const searchList = document.createElement("ul"); + searchList.setAttribute("role", "list"); + searchList.classList.add("search"); + + const out = document.getElementById("search-results"); + Search.title = out.appendChild(searchText); + Search.dots = Search.title.appendChild(document.createElement("span")); + Search.status = out.appendChild(searchSummary); + Search.output = out.appendChild(searchList); + + const searchProgress = document.getElementById("search-progress"); + // Some themes don't use the search progress node + if (searchProgress) { + searchProgress.innerText = _("Preparing search..."); + } + Search.startPulse(); + + // index already loaded, the browser was quick! + if (Search.hasIndex()) Search.query(query); + else Search.deferQuery(query); + }, + + _parseQuery: (query) => { + // stem the search terms and add them to the correct list + const stemmer = new Stemmer(); + const searchTerms = new Set(); + const excludedTerms = new Set(); + const highlightTerms = new Set(); + const objectTerms = new Set(splitQuery(query.toLowerCase().trim())); + splitQuery(query.trim()).forEach((queryTerm) => { + const queryTermLower = queryTerm.toLowerCase(); + + // maybe skip this "word" + // stopwords array is from language_data.js + if ( + stopwords.indexOf(queryTermLower) !== -1 || + queryTerm.match(/^\d+$/) + ) + return; + + // stem the word + let word = stemmer.stemWord(queryTermLower); + // select the correct list + if (word[0] === "-") excludedTerms.add(word.substr(1)); + else { + searchTerms.add(word); + highlightTerms.add(queryTermLower); + } + }); + + if (SPHINX_HIGHLIGHT_ENABLED) { // set in sphinx_highlight.js + localStorage.setItem("sphinx_highlight_terms", [...highlightTerms].join(" ")) + } + + // console.debug("SEARCH: searching for:"); + // console.info("required: ", [...searchTerms]); + // console.info("excluded: ", [...excludedTerms]); + + return [query, searchTerms, excludedTerms, highlightTerms, objectTerms]; + }, + + /** + * execute search (requires search index to be loaded) + */ + _performSearch: (query, searchTerms, excludedTerms, highlightTerms, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + const indexEntries = Search._index.indexentries; + + // Collect multiple result groups to be sorted separately and then ordered. + // Each is an array of [docname, title, anchor, descr, score, filename, kind]. + const normalResults = []; + const nonMainIndexResults = []; + + _removeChildren(document.getElementById("search-progress")); + + const queryLower = query.toLowerCase().trim(); + for (const [title, foundTitles] of Object.entries(allTitles)) { + if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { + for (const [file, id] of foundTitles) { + const score = Math.round(Scorer.title * queryLower.length / title.length); + const boost = titles[file] === title ? 1 : 0; // add a boost for document titles + normalResults.push([ + docNames[file], + titles[file] !== title ? `${titles[file]} > ${title}` : title, + id !== null ? "#" + id : "", + null, + score + boost, + filenames[file], + SearchResultKind.title, + ]); + } + } + } + + // search for explicit entries in index directives + for (const [entry, foundEntries] of Object.entries(indexEntries)) { + if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { + for (const [file, id, isMain] of foundEntries) { + const score = Math.round(100 * queryLower.length / entry.length); + const result = [ + docNames[file], + titles[file], + id ? "#" + id : "", + null, + score, + filenames[file], + SearchResultKind.index, + ]; + if (isMain) { + normalResults.push(result); + } else { + nonMainIndexResults.push(result); + } + } + } + } + + // lookup as object + objectTerms.forEach((term) => + normalResults.push(...Search.performObjectSearch(term, objectTerms)) + ); + + // lookup as search terms in fulltext + normalResults.push(...Search.performTermsSearch(searchTerms, excludedTerms)); + + // let the scorer override scores with a custom scoring function + if (Scorer.score) { + normalResults.forEach((item) => (item[4] = Scorer.score(item))); + nonMainIndexResults.forEach((item) => (item[4] = Scorer.score(item))); + } + + // Sort each group of results by score and then alphabetically by name. + normalResults.sort(_orderResultsByScoreThenName); + nonMainIndexResults.sort(_orderResultsByScoreThenName); + + // Combine the result groups in (reverse) order. + // Non-main index entries are typically arbitrary cross-references, + // so display them after other results. + let results = [...nonMainIndexResults, ...normalResults]; + + // remove duplicate search results + // note the reversing of results, so that in the case of duplicates, the highest-scoring entry is kept + let seen = new Set(); + results = results.reverse().reduce((acc, result) => { + let resultStr = result.slice(0, 4).concat([result[5]]).map(v => String(v)).join(','); + if (!seen.has(resultStr)) { + acc.push(result); + seen.add(resultStr); + } + return acc; + }, []); + + return results.reverse(); + }, + + query: (query) => { + const [searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms] = Search._parseQuery(query); + const results = Search._performSearch(searchQuery, searchTerms, excludedTerms, highlightTerms, objectTerms); + + // for debugging + //Search.lastresults = results.slice(); // a copy + // console.info("search results:", Search.lastresults); + + // print the results + _displayNextItem(results, results.length, searchTerms, highlightTerms); + }, + + /** + * search for object names + */ + performObjectSearch: (object, objectTerms) => { + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const objects = Search._index.objects; + const objNames = Search._index.objnames; + const titles = Search._index.titles; + + const results = []; + + const objectSearchCallback = (prefix, match) => { + const name = match[4] + const fullname = (prefix ? prefix + "." : "") + name; + const fullnameLower = fullname.toLowerCase(); + if (fullnameLower.indexOf(object) < 0) return; + + let score = 0; + const parts = fullnameLower.split("."); + + // check for different match types: exact matches of full name or + // "last name" (i.e. last dotted part) + if (fullnameLower === object || parts.slice(-1)[0] === object) + score += Scorer.objNameMatch; + else if (parts.slice(-1)[0].indexOf(object) > -1) + score += Scorer.objPartialMatch; // matches in last name + + const objName = objNames[match[1]][2]; + const title = titles[match[0]]; + + // If more than one term searched for, we require other words to be + // found in the name/title/description + const otherTerms = new Set(objectTerms); + otherTerms.delete(object); + if (otherTerms.size > 0) { + const haystack = `${prefix} ${name} ${objName} ${title}`.toLowerCase(); + if ( + [...otherTerms].some((otherTerm) => haystack.indexOf(otherTerm) < 0) + ) + return; + } + + let anchor = match[3]; + if (anchor === "") anchor = fullname; + else if (anchor === "-") anchor = objNames[match[1]][1] + "-" + fullname; + + const descr = objName + _(", in ") + title; + + // add custom score for some objects according to scorer + if (Scorer.objPrio.hasOwnProperty(match[2])) + score += Scorer.objPrio[match[2]]; + else score += Scorer.objPrioDefault; + + results.push([ + docNames[match[0]], + fullname, + "#" + anchor, + descr, + score, + filenames[match[0]], + SearchResultKind.object, + ]); + }; + Object.keys(objects).forEach((prefix) => + objects[prefix].forEach((array) => + objectSearchCallback(prefix, array) + ) + ); + return results; + }, + + /** + * search for full-text terms in the index + */ + performTermsSearch: (searchTerms, excludedTerms) => { + // prepare search + const terms = Search._index.terms; + const titleTerms = Search._index.titleterms; + const filenames = Search._index.filenames; + const docNames = Search._index.docnames; + const titles = Search._index.titles; + + const scoreMap = new Map(); + const fileMap = new Map(); + + // perform the search on the required terms + searchTerms.forEach((word) => { + const files = []; + const arr = [ + { files: terms[word], score: Scorer.term }, + { files: titleTerms[word], score: Scorer.title }, + ]; + // add support for partial matches + if (word.length > 2) { + const escapedWord = _escapeRegExp(word); + if (!terms.hasOwnProperty(word)) { + Object.keys(terms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: terms[term], score: Scorer.partialTerm }); + }); + } + if (!titleTerms.hasOwnProperty(word)) { + Object.keys(titleTerms).forEach((term) => { + if (term.match(escapedWord)) + arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); + }); + } + } + + // no match but word was a required one + if (arr.every((record) => record.files === undefined)) return; + + // found search word in contents + arr.forEach((record) => { + if (record.files === undefined) return; + + let recordFiles = record.files; + if (recordFiles.length === undefined) recordFiles = [recordFiles]; + files.push(...recordFiles); + + // set score for the word in each file + recordFiles.forEach((file) => { + if (!scoreMap.has(file)) scoreMap.set(file, {}); + scoreMap.get(file)[word] = record.score; + }); + }); + + // create the mapping + files.forEach((file) => { + if (!fileMap.has(file)) fileMap.set(file, [word]); + else if (fileMap.get(file).indexOf(word) === -1) fileMap.get(file).push(word); + }); + }); + + // now check if the files don't contain excluded terms + const results = []; + for (const [file, wordList] of fileMap) { + // check if all requirements are matched + + // as search terms with length < 3 are discarded + const filteredTermCount = [...searchTerms].filter( + (term) => term.length > 2 + ).length; + if ( + wordList.length !== searchTerms.size && + wordList.length !== filteredTermCount + ) + continue; + + // ensure that none of the excluded terms is in the search result + if ( + [...excludedTerms].some( + (term) => + terms[term] === file || + titleTerms[term] === file || + (terms[term] || []).includes(file) || + (titleTerms[term] || []).includes(file) + ) + ) + break; + + // select one (max) score for the file. + const score = Math.max(...wordList.map((w) => scoreMap.get(file)[w])); + // add result to the result list + results.push([ + docNames[file], + titles[file], + "", + null, + score, + filenames[file], + SearchResultKind.text, + ]); + } + return results; + }, + + /** + * helper function to return a node containing the + * search summary for a given text. keywords is a list + * of stemmed words. + */ + makeSearchSummary: (htmlText, keywords, anchor) => { + const text = Search.htmlToText(htmlText, anchor); + if (text === "") return null; + + const textLower = text.toLowerCase(); + const actualStartPosition = [...keywords] + .map((k) => textLower.indexOf(k.toLowerCase())) + .filter((i) => i > -1) + .slice(-1)[0]; + const startWithContext = Math.max(actualStartPosition - 120, 0); + + const top = startWithContext === 0 ? "" : "..."; + const tail = startWithContext + 240 < text.length ? "..." : ""; + + let summary = document.createElement("p"); + summary.classList.add("context"); + summary.textContent = top + text.substr(startWithContext, 240).trim() + tail; + + return summary; + }, +}; + +_ready(Search.init); diff --git a/docs/_build/html/_static/sphinx_highlight.js b/docs/_build/html/_static/sphinx_highlight.js new file mode 100644 index 0000000..8a96c69 --- /dev/null +++ b/docs/_build/html/_static/sphinx_highlight.js @@ -0,0 +1,154 @@ +/* Highlighting utilities for Sphinx HTML documentation. */ +"use strict"; + +const SPHINX_HIGHLIGHT_ENABLED = true + +/** + * highlight a given string on a node by wrapping it in + * span elements with the given class name. + */ +const _highlight = (node, addItems, text, className) => { + if (node.nodeType === Node.TEXT_NODE) { + const val = node.nodeValue; + const parent = node.parentNode; + const pos = val.toLowerCase().indexOf(text); + if ( + pos >= 0 && + !parent.classList.contains(className) && + !parent.classList.contains("nohighlight") + ) { + let span; + + const closestNode = parent.closest("body, svg, foreignObject"); + const isInSVG = closestNode && closestNode.matches("svg"); + if (isInSVG) { + span = document.createElementNS("http://www.w3.org/2000/svg", "tspan"); + } else { + span = document.createElement("span"); + span.classList.add(className); + } + + span.appendChild(document.createTextNode(val.substr(pos, text.length))); + const rest = document.createTextNode(val.substr(pos + text.length)); + parent.insertBefore( + span, + parent.insertBefore( + rest, + node.nextSibling + ) + ); + node.nodeValue = val.substr(0, pos); + /* There may be more occurrences of search term in this node. So call this + * function recursively on the remaining fragment. + */ + _highlight(rest, addItems, text, className); + + if (isInSVG) { + const rect = document.createElementNS( + "http://www.w3.org/2000/svg", + "rect" + ); + const bbox = parent.getBBox(); + rect.x.baseVal.value = bbox.x; + rect.y.baseVal.value = bbox.y; + rect.width.baseVal.value = bbox.width; + rect.height.baseVal.value = bbox.height; + rect.setAttribute("class", className); + addItems.push({ parent: parent, target: rect }); + } + } + } else if (node.matches && !node.matches("button, select, textarea")) { + node.childNodes.forEach((el) => _highlight(el, addItems, text, className)); + } +}; +const _highlightText = (thisNode, text, className) => { + let addItems = []; + _highlight(thisNode, addItems, text, className); + addItems.forEach((obj) => + obj.parent.insertAdjacentElement("beforebegin", obj.target) + ); +}; + +/** + * Small JavaScript module for the documentation. + */ +const SphinxHighlight = { + + /** + * highlight the search words provided in localstorage in the text + */ + highlightSearchWords: () => { + if (!SPHINX_HIGHLIGHT_ENABLED) return; // bail if no highlight + + // get and clear terms from localstorage + const url = new URL(window.location); + const highlight = + localStorage.getItem("sphinx_highlight_terms") + || url.searchParams.get("highlight") + || ""; + localStorage.removeItem("sphinx_highlight_terms") + url.searchParams.delete("highlight"); + window.history.replaceState({}, "", url); + + // get individual terms from highlight string + const terms = highlight.toLowerCase().split(/\s+/).filter(x => x); + if (terms.length === 0) return; // nothing to do + + // There should never be more than one element matching "div.body" + const divBody = document.querySelectorAll("div.body"); + const body = divBody.length ? divBody[0] : document.querySelector("body"); + window.setTimeout(() => { + terms.forEach((term) => _highlightText(body, term, "highlighted")); + }, 10); + + const searchBox = document.getElementById("searchbox"); + if (searchBox === null) return; + searchBox.appendChild( + document + .createRange() + .createContextualFragment( + '" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(() => { + /* Do not call highlightSearchWords() when we are on the search page. + * It will highlight words from the *previous* search query. + */ + if (typeof Search === "undefined") SphinxHighlight.highlightSearchWords(); + SphinxHighlight.initEscapeListener(); +}); diff --git a/docs/_build/html/cheat.html b/docs/_build/html/cheat.html new file mode 100644 index 0000000..921ae98 --- /dev/null +++ b/docs/_build/html/cheat.html @@ -0,0 +1,377 @@ + + + + + + + + + Cheat sheet — fedcloudclient documentation + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Cheat sheet

+

See Tutorial +for more details of commands.

+
+

Local install via pip3

+
    +
  • Create a Python virtual environment:

  • +
+
$ python3 -m venv env
+
+
+
    +
  • Activate the virtual environment

  • +
+
$ source env/bin/activate
+
+
+
    +
  • Install fedcloudclient via pip:

  • +
+
$ pip3 install fedcloudclient
+
+
+
    +
  • Install IGTF certificates:

  • +
+
$ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh
+$ bash install_certs.sh
+
+
+
+
+

Using Docker container

+
    +
  • Pull the latest version of fedcloudclient container

  • +
+
$ sudo docker pull tdviet/fedcloudclient
+
+
+
    +
  • Start fedcloudclient container with oidc-agent account:

  • +
+
$ sudo docker run -it -v ~/.config/oidc-agent/egi:/root/.config/oidc-agent/egi --name fedcloud tdviet/fedcloudclient bash
+
+
+
    +
  • Restart previously terminated container:

  • +
+
$ sudo docker start -i fedcloud
+
+
+
+
+

Using oidc-agent

+
    +
  • Create an oidc-agent account (if not done):

  • +
+
$ oidc-gen --pub --issuer https://aai.egi.eu/auth/realms/egi --scope "eduperson_entitlement email" egi
+
+
+
    +
  • Load oidc-agent account and set environment for fedcloudclient:

  • +
+
$ eval `oidc-keychain --accounts egi` && export OIDC_AGENT_ACCOUNT=egi
+
+
+
+
+

Basic usages

+
    +
  • List your VO memberships according to the access token:

  • +
+
$ fedcloud token list-vos
+
+
+
    +
  • List sites in the EGI Federated Cloud:

  • +
+
$ fedcloud site list
+
+
+
    +
  • List all sites supporting a Virtual Organization in the EGI Federated Cloud:

  • +
+
$ fedcloud site list --vo vo.access.egi.eu
+
+
+
    +
  • Execute an OpenStack command:

  • +
+
$ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu
+
+
+
    +
  • Execute an OpenStack command on all sites:

  • +
+
$ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu
+
+
+
    +
  • Print only selected values (for scripting):

  • +
+
$ export OS_TOKEN=$(fedcloud openstack --site CESGA --vo vo.access.egi.eu token issue -c id -f value)
+
+
+
    +
  • All-sites commands with full JSON output:

  • +
+
$ fedcloud openstack image list --site ALL_SITES --vo eosc-synergy.eu --json-output
+
+
+
+
+

Searching and selecting resources

+
    +
  • Show all available projects:

  • +
+
$ fedcloud endpoint projects --site ALL_SITES
+
+
+
    +
  • Show all Horizon dashboards:

  • +
+
$ fedcloud endpoint list --service-type org.openstack.horizon --site ALL_SITES
+
+
+
    +
  • Search images with appliance title in AppDB:

  • +
+
$ fedcloud openstack image list --property "dc:title"="Image for EGI Docker [Ubuntu/18.04/VirtualBox]" --site CESNET-MCC  --vo eosc-synergy.eu
+
+
+
    +
  • Select flavors with 2 CPUs and RAM >= 2048 on a site/VO:

  • +
+
$ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --vcpus 2 --flavor-specs "RAM>=2048" --output-format list
+
+
+
    +
  • Select EGI Ubuntu 20.04 images on a site/VO:

  • +
+
# Simpler but longer way
+$ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ Ubuntu" --image-specs "Name =~ '20.04'" --image-specs "Name =~ EGI" --output-format list
+
+
+
# Shorter but more complex regex
+$ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ 'EGI.*Ubuntu.*20.04'"  --output-format list
+
+
+
+
+

Mapping and filtering results from OpenStack commands

+
    +
  • Select flavors with 2 CPUs:

  • +
+
$ fedcloud openstack flavor list  --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \
+  jq -r  '.[].Result[] | select(.VCPUs == 2) | .Name'
+
+
+
    +
  • Select GPU flavors and show their GPU properties on a site:

  • +
+
$ fedcloud openstack flavor list --long --site IISAS-FedCloud --vo acc-comp.egi.eu --json-output | \
+  jq -r '.[].Result | map(select(.Properties."Accelerator:Type" == "GPU")) | .'
+
+
+
    +
  • Select GPU flavors and show their GPU properties on all sites:

  • +
+
$ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \
+  jq -r 'map(select(."Error code" ==  0)) |
+         map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) |
+         map(select(.Result | length >  0))'
+
+
+
    +
  • Construct JSON objects just with site names and flavor names, remove all other properties:

  • +
+
$ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \
+  jq -r 'map(select(."Error code" ==  0)) |
+         map({Site:.Site, Flavors:[.Result[].Name]})'
+
+
+
+
+

Useful commands

+
    +
  • Check expiration time of access token (not work for oidc-agent-account):

  • +
+
$ fedcloud token check
+
+
+
    +
  • Set OpenStack environment variables:

  • +
+
$ eval $(fedcloud site env --site IISAS-FedCloud --vo vo.access.egi.eu)
+
+
+
    +
  • List all my own VMs:

  • +
+
$  list-all-my-own-vms.sh --vo fedcloud.egi.eu
+
+
+
    +
  • Activate shell completion

  • +
+
# Quick and dirty way (may be resulted in unresponsive shell)
+$ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)"
+
+
+
# More systematic way
+$ wget https://raw.githubusercontent.com/tdviet/fedcloudclient/master/examples/fedcloud_bash_completion.sh
+$ source fedcloud_bash_completion.sh
+
+
+
    +
  • Pass a mytoken to Virtual Machines in the EGI Federated Cloud

  • +
+
# Create the file "user.txt" with
+$ cat user.txt
+FEDCLOUD_MYTOKEN=<mytoken> # created on https://mytoken.data.kit.edu/
+
+# Pass it to OpenStack
+FEDCLOUD_SITE=IISAS-FedCloud
+FEDCLOUD_VO=vo.access.egi.eu
+fedcloud openstack server create --flavor <flavor> --image <image> --user-data user.txt --key-name <keypair> testvm
+
+# Once you log into the VM you can retrieve the "mytoken" with
+curl http://169.254.169.254/openstack/latest/user_data/
+
+# and use it with
+FEDCLOUD_MYTOKEN=<mytoken> # copied from the previous curl command
+fedcloud token check
+
+
+
+
+

More information

+
    +
  • Get help:

  • +
+
$ fedcloud --help
+$ fedcloud site --help
+
+
+ +
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/development.html b/docs/_build/html/development.html new file mode 100644 index 0000000..1301d94 --- /dev/null +++ b/docs/_build/html/development.html @@ -0,0 +1,153 @@ + + + + + + + + + Using FedCloud client in Python — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Using FedCloud client in Python

+

FedCloud client can be used as a library for developing other services and tools for EGI Federated Cloud. Most of +functionalities of FedCloud client can be called directly from other codes without side effects. An example of the code +using FedCloud client is available at GitHub. +Just copy/download the code, add your access token and execute “python demo.py” to see how it works.

+
# Import FedCloud client library
+from fedcloudclient.openstack import fedcloud_openstack
+import json
+
+# Setting values for input parameters: token, site, VO
+token = "YOUR_ACCESS_TOKEN"
+site = "CYFRONET-CLOUD"
+vo = "fedcloud.egi.eu"
+
+# OpenStack command and options. Must be a tuple
+command = ("image", "list", "--long")
+
+# Execute the OpenStack command on the site/VO with single line of code
+# If command finishes correctly, the error_code is 0 and the result is stored
+# in JSON object for easy processing
+
+error_code, result = fedcloud_openstack(token, site, vo, command)
+
+# Check error code and print the result if OK
+if error_code == 0:
+    print(json.dumps(result, indent=4))
+else:
+    # If error, result is string containing error message
+    print("Error message is %s" % result)
+
+
+

Read the FedCloud client API references +for more details about each function in FedCloud client library. Check the output of the equivalent command of +FedCloud client and its source code to see how the function is used.

+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/fedcloudclient.html b/docs/_build/html/fedcloudclient.html new file mode 100644 index 0000000..a6bb813 --- /dev/null +++ b/docs/_build/html/fedcloudclient.html @@ -0,0 +1,900 @@ + + + + + + + + + FedCloud client API references — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

FedCloud client API references

+
+

fedcloudclient.checkin module

+

Implementation of “fedcloud token” commands for interactions with EGI Check-in and +access tokens

+
+
+

fedcloudclient.auth module

+

Class for managing tokens

+
+
+class fedcloudclient.auth.OIDCToken(access_token=None)
+

Bases: Token

+

OIDC token handler.

+

This class manages access tokens from various sources (OIDC agent, +Mytoken server), decodes them, and exposes user and VO membership +information.

+
+
Parameters:
+

access_token (str, optional) – Optional initial access token to decode and discover metadata from.

+
+
Variables:
+
    +
  • access_token (str) – The raw access token string.

  • +
  • payload (dict) – The decoded JWT payload.

  • +
  • oidc_agent_account (str) – The account name used with oidc-agent.

  • +
  • mytoken (str) – The Mytoken identifier used to fetch a token.

  • +
  • user_id (str) – The sub claim extracted from the token payload.

  • +
  • _vo_pattern (str) – Regex pattern for extracting VO names.

  • +
  • request_json (dict) – OIDC discovery metadata.

  • +
  • _min_access_token_time (int) – Minimum required lifetime for a token.

  • +
  • conf (dict) – Reference to global CONF dict for defaults.

  • +
+
+
+
+
+check_token(access_token, verbose=False)
+

Check validity of an access token.

+
+
Parameters:
+
    +
  • access_token (str) – The JWT or opaque token string to validate.

  • +
  • verbose (bool) – If True, print human‐readable expiration information. +Defaults to False.

  • +
+
+
Returns:
+

The same access_token if it’s still valid, otherwise None.

+
+
Return type:
+

str or None

+
+
Raises:
+

TokenError – If the token is expired (or fails other validation checks).

+
+
+
+ +
+
+decode_token() dict
+

Decode the stored access token into its JWT payload.

+

This method will decode self.access_token without verifying the +signature (i.e. verify_signature=False), extract the sub claim +into self.user_id, and cache the payload in self.payload.

+
+
Returns:
+

The decoded JWT payload as a dictionary.

+
+
Return type:
+

dict

+
+
Raises:
+

TokenError – If self.access_token is missing or invalid.

+
+
+
+ +
+
+get_checkin_id(access_token)
+

Retrieve the user ID from the decoded token payload.

+

If the payload hasn’t been decoded yet, this will trigger a decode +(via self.decode_token()) and may still return None if decoding fails.

+
+
Returns:
+

The user_id string extracted from the token payload, or None if +no payload is available.

+
+
Return type:
+

str or None

+
+
+
+ +
+
+get_token_from_mytoken(mytoken: str, mytoken_server: str | None = None) str
+

Obtain an access token by exchanging a Mytoken identifier.

+
+
Parameters:
+
    +
  • mytoken (str) – The Mytoken identifier (a one-time code or token name) to exchange for an access token.

  • +
  • mytoken_server (str, optional) – Optional base URL of the Mytoken server. +If not provided, defaults to CONF[“mytoken_server”].

  • +
+
+
Returns:
+

The access token string retrieved from the Mytoken server, or None if an error occurred.

+
+
Return type:
+

str or None

+
+
Raises:
+
    +
  • TokenError – If no mytoken is provided, or if the server returns an HTTP error.

  • +
  • requests.exceptions.Timeout – If the HTTP request to the Mytoken server times out.

  • +
+
+
+
+ +
+
+get_token_from_oidc_agent(oidc_agent_account: str) str
+

Obtain an access token from a local OIDC agent.

+
+
Parameters:
+

oidc_agent_account (str) – The name of the account registered with your local oidc-agent.

+
+
Returns:
+

The retrieved access token string.

+
+
Return type:
+

str

+
+
Raises:
+

TokenError – If oidc_agent_account is not provided or if the agent fails to return a token.

+
+
+
+ +
+
+get_user_id() str
+

Return use ID

+
+
Returns:
+

user_id

+
+
+
+ +
+
+multiple_token(access_token: str, oidc_agent_account: str, mytoken: str, mytoken_server=None) str
+

Select a valid token from multiple sources.

+
+
Parameters:
+
    +
  • access_token (str) – An existing access token to try first. If it’s valid, it will be returned.

  • +
  • oidc_agent_account (str) – The account name for your local OIDC agent. +If provided, we’ll ask the agent for a token.

  • +
  • mytoken (str) – A Mytoken identifier (e.g. a token name) to exchange for an access token.

  • +
  • mytoken_server (str, optional) – Optional URL of the Mytoken server to contact (defaults to your configured server).

  • +
+
+
Returns:
+

A valid access token string.

+
+
Return type:
+

str

+
+
Raises:
+

TokenError – If none of the methods yields a valid token.

+
+
+
+ +
+
+oidc_discover() dict
+

Perform OpenID Connect discovery.

+

Reads the issuer URL from self.payload[“iss”] and fetches the +provider metadata from <issuer>/.well-known/openid-configuration.

+
+
Returns:
+

OIDC provider metadata as a JSON-like dict.

+
+
Return type:
+

dict

+
+
Raises:
+

requests.HTTPError – If fetching the discovery document fails.

+
+
+
+ +
+
+token_list_vos(access_token)
+

List VO memberships in EGI Check-in.

+
+
Parameters:
+

access_token (str) – A valid access token (JWT or opaque string) to authenticate the request.

+
+
Returns:
+

A sorted list of VO names parsed from the eduperson_entitlement claims.

+
+
Return type:
+

list[str]

+
+
Raises:
+
    +
  • requests.exceptions.Timeout – If the HTTP request to the userinfo endpoint times out.

  • +
  • requests.HTTPError – If the HTTP response status code indicates an error.

  • +
+
+
+
+ +
+ +
+
+class fedcloudclient.auth.Token
+

Bases: object

+

Abstract base class for token management.

+
+ +
+
+

fedcloudclient.conf module

+

Read/write configuration files

+
+
+fedcloudclient.conf.init_config() dict
+

Initialize the FedCloud client configuration.

+

This function merges three sources of configuration, in order of precedence:

+
    +
  1. DEFAULT_SETTINGS (hard-coded defaults).

  2. +
  3. Environment variables prefixed with FEDCLOUD_ (stripped of the prefix and lower-cased).

  4. +
  5. Values loaded from a YAML config file (path can be overridden via FEDCLOUD_CONFIG_FILE).

  6. +
+
+
Returns:
+

A dict containing the merged configuration settings. +Later sources override earlier ones.

+
+
Return type:
+

dict[str, Any]

+
+
+
+ +
+
+fedcloudclient.conf.init_default_config()
+

Initialize the default configuration settings.

+
+
Returns:
+

A dictionary containing the default settings as defined by DEFAULT_SETTINGS.

+
+
Return type:
+

dict

+
+
+
+ +
+
+fedcloudclient.conf.load_config(filename: str) dict
+

Load configuration data from a YAML file.

+
+
Parameters:
+

filename (str) – Path to the configuration file to read.

+
+
Returns:
+

A dictionary of configuration values parsed from the file. +Returns an empty dict if the file does not exist.

+
+
Return type:
+

dict

+
+
Raises:
+

ConfigError – If the file exists but cannot be parsed as valid YAML.

+
+
+
+ +
+
+fedcloudclient.conf.load_env() dict
+

Load FedCloud client configuration from environment variables.

+

Scans the current process environment for variables prefixed with +FEDCLOUD_, strips that prefix, converts the remainder to lowercase, +and returns them as configuration entries.

+
+
Returns:
+

A dictionary mapping config keys (lowercased, prefix removed) to +their string values from the environment.

+
+
Return type:
+

dict[str, str]

+
+
+
+ +
+
+fedcloudclient.conf.save_config(filename: str, config_data: dict)
+

Save a configuration dictionary to a YAML file.

+
+
Parameters:
+
    +
  • filename (str) – Path to the configuration file to write.

  • +
  • config_data (dict) – A mapping of configuration keys to values to be serialized.

  • +
+
+
Raises:
+

ConfigError – If writing the YAML fails (wraps yaml.YAMLError).

+
+
+
+ +
+
+

fedcloudclient.endpoint module

+

“fedcloud endpoint” commands are complementary part of the “fedcloud site” commands.

+

Instead of using site configurations defined in files saved in GitHub repository or +local disk, the commands try to get site information directly from GOCDB +(Grid Operations Configuration Management Database) https://goc.egi.eu/ or make probe +test on sites

+
+
+exception fedcloudclient.endpoint.TokenException
+

Bases: Exception

+

Exception for Token related errors

+
+ +
+
+fedcloudclient.endpoint.find_endpoint(service_type, production=True, monitored=True, site=None)
+

Searching GOCDB for endpoints according to service types and status

+
+
Parameters:
+
    +
  • service_type

  • +
  • production

  • +
  • monitored

  • +
  • site – list of sites, None for searching all sites

  • +
+
+
Returns:
+

list of endpoints

+
+
+
+ +
+
+fedcloudclient.endpoint.format_project_as_dict(site_name, project)
+

Format project data as a dictionary

+
+ +
+
+fedcloudclient.endpoint.format_project_as_list(site_name, project)
+

Format project data as a list

+
+ +
+
+fedcloudclient.endpoint.get_keystone_url(os_auth_url, path)
+

Helper function for fixing Keystone URL

+
+ +
+
+fedcloudclient.endpoint.get_projects_from_single_site(os_auth_url, unscoped_token)
+

Get list of projects from unscoped token

+
+ +
+
+fedcloudclient.endpoint.get_projects_from_sites(access_token, site)
+

Get all projects from site(s) using access token, in the default output format (list)

+
+ +
+
+fedcloudclient.endpoint.get_projects_from_sites_as_dict(access_token, site)
+

Get all projects from site(s) using access token, as a dictionary

+
+ +
+
+fedcloudclient.endpoint.get_projects_from_sites_as_list(access_token, site)
+

Get all projects from site(s) using access token, as a list

+
+ +
+
+fedcloudclient.endpoint.get_projects_from_sites_with_format(access_token, site, output_format_function)
+

Get all projects from site(s) using access token

+
+ +
+
+fedcloudclient.endpoint.get_scoped_token(os_auth_url, access_token, project_id)
+

Get a scoped token, will try all protocols if needed

+
+ +
+
+fedcloudclient.endpoint.get_sites()
+

Get list of sites (using GOCDB instead of site configuration)

+
+
Returns:
+

list of site IDs

+
+
+
+ +
+
+fedcloudclient.endpoint.get_unscoped_token(os_auth_url, access_token)
+

Get an unscoped token, will try all protocols if needed

+
+ +
+
+fedcloudclient.endpoint.retrieve_unscoped_token(os_auth_url, access_token, protocol='openid')
+

Request an unscoped token

+
+ +
+
+

fedcloudclient.sites module

+

“fedcloud site” commands will read site configurations and manipulate with them. If +the local site configurations exist at ~/.config/fedcloud/site-config/, fedcloud will +read them from there, otherwise the commands will read from GitHub repository.

+

By default, fedcloud does not save anything on local disk, users have to save the +site configuration to local disk explicitly via “fedcloud site save-config” command. +The advantage of having local site configurations, beside faster loading, is to give +users ability to make customizations, e.g. add additional VOs, remove sites they +do not have access, and so on.

+
+
+fedcloudclient.sites.delete_site_config(config_dir)
+

Delete site configs to local directory specified in config_dir

+
+
Parameters:
+

config_dir – path to directory containing site configuration

+
+
Returns:
+

None

+
+
+
+ +
+
+fedcloudclient.sites.find_endpoint_and_project_id(site_name, vo)
+

Return Keystone endpoint and project ID from site name and VO according +to site configuration

+
+
Parameters:
+
    +
  • site_name – site ID in GOCDB

  • +
  • vo – VO name or None to find site endpoint only

  • +
+
+
Returns:
+

endpoint, project_id, protocol if the VO has access to the site, +otherwise None, None, None

+
+
+
+ +
+
+fedcloudclient.sites.find_site_data(site_name)
+

Return configuration of the correspondent site with site_name

+
+
Parameters:
+

site_name – site ID in GOCDB

+
+
Returns:
+

configuration of site if found, otherwise None

+
+
+
+ +
+
+fedcloudclient.sites.find_vo_from_project_id(site_name, project_id)
+

Return the VO name form the project ID and site_name according +to site configuration

+
+
Parameters:
+
    +
  • site_name – site ID in GOCDB

  • +
  • project_id – project_id configured to support the VO

  • +
+
+
Returns:
+

vo if the VO is configured, otherwise None

+
+
+
+ +
+
+fedcloudclient.sites.list_sites(vo=None)
+

List all sites IDs in site configurations +Optionally list all sites supporting a Virtual Organization

+
+
Returns:
+

list of site IDs

+
+
+
+ +
+
+fedcloudclient.sites.read_default_site_config()
+

Read default site configurations from GitHub +Storing site configurations in a global variable that will be used by other functions

+
+
Returns:
+

None

+
+
+
+ +
+
+fedcloudclient.sites.read_local_site_config(config_dir)
+

Read site configurations from local directory specified in config_dir +Storing site configurations in global variable, that will be used by other functions

+
+
Parameters:
+

config_dir – path to directory containing site configuration

+
+
Returns:
+

None

+
+
+
+ +
+
+fedcloudclient.sites.read_site_config()
+

Read site configurations from local config dir if exist, otherwise from default +GitHub location. Storing site configurations in global variable, that will be +used by other functions. Call read_local_site_config() +or read_default_site_config()

+
+
Returns:
+

None

+
+
+
+ +
+
+fedcloudclient.sites.read_site_schema()
+

Read schema.json for validating site configuration

+
+
Returns:
+

JSON object from schema.json

+
+
+
+ +
+
+fedcloudclient.sites.safe_read_yaml_from_url(url, max_length)
+

Safe reading from URL +Check URL and size before reading

+
+
Parameters:
+
    +
  • url

  • +
  • max_length

  • +
+
+
Returns:
+

data from URL

+
+
+
+ +
+
+fedcloudclient.sites.save_site_config(config_dir)
+

Save site configs to local directory specified in config_dir +Overwrite local configs if exist

+
+
Parameters:
+

config_dir – path to directory containing site configuration

+
+
Returns:
+

None

+
+
+
+ +
+
+

fedcloudclient.openstack module

+

Implementation of “fedcloud openstack” or “fedcloud openstack-int” for performing +OpenStack commands on sites

+
+
+fedcloudclient.openstack.check_openstack_client_installation()
+

Check if openstack command-line client is installed and available via $PATH

+
+
Returns:
+

True if available

+
+
+
+ +
+
+fedcloudclient.openstack.fedcloud_openstack(oidc_access_token, site, vo, openstack_command, json_output=True)
+

Simplified version of fedcloud_openstack_full() using +default EGI setting for identity provider and protocols +Calls OpenStack CLI with default options for EGI Check-in

+
+
Parameters:
+
    +
  • oidc_access_token – Checkin access token. +Passed to openstack client as –os-access-token

  • +
  • site – site ID in GOCDB

  • +
  • vo – VO name

  • +
  • openstack_command – OpenStack command in tuple, +e.g. (“image”, “list”, “–long”)

  • +
  • json_output – if result is JSON object or string. Default:True

  • +
+
+
Returns:
+

error code, result or error message

+
+
+
+ +
+
+fedcloudclient.openstack.fedcloud_openstack_full(oidc_access_token, os_protocol, os_auth_type, os_identity_provider, site, vo, openstack_command, json_output=True)
+

Calling openstack client with full options specified, including support +for other identity providers and protocols

+
+
Parameters:
+
    +
  • oidc_access_token – Checkin access token. Passed to openstack client +as –os-access-token

  • +
  • os_protocol – Checkin protocol (openi

  • +
+
+
+
+
, oidc). Passed to

openstack client as –os-protocol

+
+
+
+
Parameters:
+
    +
  • os_auth_type – Checkin authentication type (v3oidcaccesstoken). +Passed to openstack client as –os-auth-type

  • +
  • os_identity_provider – Checkin identity provider in mapping (egi.eu). +Passed to openstack client as –os-identity-provider

  • +
  • site – site ID in GOCDB

  • +
  • vo – VO name

  • +
  • openstack_command – OpenStack command in tuple, +e.g. (“image”, “list”, “–long”)

  • +
  • json_output – if result is JSON object or string. Default:True

  • +
+
+
Returns:
+

error code, result or error message

+
+
+
+ +
+
+fedcloudclient.openstack.print_result(site, vo, command, exc_msg, error_code, result, json_output, ignore_missing_vo, first)
+

Print output from an OpenStack command

+
+
Parameters:
+
    +
  • site

  • +
  • vo

  • +
  • command

  • +
  • exc_msg

  • +
  • error_code

  • +
  • result

  • +
  • json_output

  • +
  • ignore_missing_vo

  • +
  • first

  • +
+
+
Returns:
+

+
+
+
+ +
+
+

fedcloudclient.logger module

+

Logger configuration

+
+
+fedcloudclient.logger.init_logger()
+

Init logger +:return:

+
+ +
+
+fedcloudclient.logger.log_and_raise(error_msg: str, exception)
+

Log error and raise exception

+
+ +
+
+

fedcloudclient.locker_auth module

+

Class for managing Vault locker tokens

+
+
+class fedcloudclient.locker_auth.LockerToken(locker_token: str)
+

Bases: VaultToken

+

Managing Vault locker token

+
+
+get_user_id()
+

User ID is not available for locker token +:return:

+
+ +
+
+get_vault_auth_method()
+

Get Vault user ID from Vault token +:return:

+
+ +
+
+get_vault_user_id()
+

Get Vault user ID from Vault token +:return:

+
+ +
+
+vault_command(command: str, path: str, data: dict, vo: str | None = None)
+

Perform Vault command +:param command: +:param path: +:param data: +:param vo: +:return:

+
+ +
+ +
+
+

fedcloudclient.cli module

+

Main CLI module

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/genindex.html b/docs/_build/html/genindex.html new file mode 100644 index 0000000..01d78a3 --- /dev/null +++ b/docs/_build/html/genindex.html @@ -0,0 +1,421 @@ + + + + + + + + Index — fedcloudclient documentation + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Index

+ +
+ C + | D + | F + | G + | I + | L + | M + | O + | P + | R + | S + | T + | V + +
+

C

+ + + +
+ +

D

+ + + +
+ +

F

+ + + +
+ +

G

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + + +
+ +

O

+ + + +
+ +

P

+ + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

V

+ + +
+ + + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/index.html b/docs/_build/html/index.html new file mode 100644 index 0000000..cdaecb8 --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,174 @@ + + + + + + + + + <no title> — fedcloudclient documentation + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/install.html b/docs/_build/html/install.html new file mode 100644 index 0000000..7d1e040 --- /dev/null +++ b/docs/_build/html/install.html @@ -0,0 +1,197 @@ + + + + + + + + + Installation — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Installation

+
+

Installing FedCloud client with pip

+

Simply use the following pip3 command (should be done without root privileges).

+
$ pip3 install -U fedcloudclient
+
+
+
+

Installation Notes

+

Installing the latest version of the FedCloud client package using pip3 will also install all required dependencies (such as openstackclient). It will create the executable files ``fedcloud`` and ``openstack``, placing them in the appropriate directory based on your Python environment:

+
    +
  • $VIRTUAL_ENV/bin – when using pip3 inside a Python virtual environment,

  • +
  • ~/.local/bin – when installing with pip3 --user,

  • +
  • /usr/local/bin – when installing system-wide with pip3 as root.

  • +
+
+

Note

+

If you install the package with the --user option, make sure that ~/.local/bin is included in your $PATH.

+
+
+
+

Verifying the Installation

+

To check if the installation was successful, run the following command:

+
::
+
+
+
+

$ fedcloud –help

+
+

This will show output:

+
::
+
+
+
+

Usage: fedcloud [OPTIONS] COMMAND [ARGS]…

+

CLI main function. Intentionally empty

+

Options: +–version Show the version and exit. +–help Show this message and exit.

+

Commands: +ec3 EC3 cluster provisioning +endpoint Obtain endpoint details and scoped tokens +openstack Execute OpenStack commands on site and VO +openstack-int Interactive OpenStack client on site and VO +secret Commands for accessing secret objects +select Select resources according to specification +site Obtain site configurations +token Get details of access token

+
+
+
+
+

Installing EGI Core Trust Anchor certificates

+

Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message “SSL exception connecting to https:// …”, +follow instructions +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +README.md for more details.

+
$ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh
+$ bash install_certs.sh
+
+
+
+
+

Using FedCloud client via Docker container

+

You can use Docker container for testing FedCloud client without installation. EGI Core Trust Anchor certificates +and site configurations are preinstalled.

+
$ sudo docker pull tdviet/fedcloudclient
+$ sudo docker run -it  tdviet/fedcloudclient bash
+
+
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/intro.html b/docs/_build/html/intro.html new file mode 100644 index 0000000..39d9be3 --- /dev/null +++ b/docs/_build/html/intro.html @@ -0,0 +1,209 @@ + + + + + + + + + Introduction — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Introduction

+https://zenodo.org/badge/336671726.svg + +
+
+

FedCloud Client

+

The FedCloud Client (https://fedcloudclient.fedcloud.eu/) is a high-level Python package providing a unified command-line interface to the EGI Federated Cloud’s OpenStack services. It streamlines tasks such as managing access tokens, discovering services, and executing OpenStack commands across multiple sites.

+
+

Key Features

+
    +
  • Comprehensive Command Set +Access a broad range of operations, including:

    +
      +
    • Token management (retrieve, inspect, renew)

    • +
    • Service discovery and endpoint lookup

    • +
    • Listing and filtering sites, virtual organizations (VOs), and resources

    • +
    • Direct interaction with OpenStack services (compute, image, network, etc.)

    • +
    +
  • +
  • Minimal Syntax, Maximum Power +Execute any OpenStack action on any site with just three parameters:

    +
    $ fedcloud openstack <command> --vo <VO_NAME> --site <SITE_NAME>
    +
    +
    +

    For example, to list all available VM images for the VO vo.access.egi.eu on the IISAS-FedCloud site:

    +
    $ fedcloud openstack image list \
    +    --vo vo.access.egi.eu \
    +    --site IISAS-FedCloud
    +
    +
    +
  • +
  • Federation-Wide Scope +One client, all sites. Use the special ALL_SITES identifier to run a command across every configured site in the federation:

    +
    $ fedcloud openstack server list --vo vo.access.egi.eu --site ALL_SITES
    +
    +
    +
  • +
  • Scriptable & Extensible +Designed for automation and integration:

    +
      +
    • Shell scripting: embed fedcloud calls in CI/CD pipelines or custom scripts. See scripts.

    • +
    • Python library: import and invoke client functionality programmatically. See developer guide.

    • +
    +
  • +
+
+
+

Core Modules

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Module

Purpose

fedcloudclient.conf

Manage and validate client configuration (profiles, defaults).

fedcloudclient.checkin

Interact with EGI Check-in: fetch and renew OpenID Connect tokens.

fedcloudclient.endpoint

Discover service endpoints via GOCDB; obtain unscoped and scoped tokens from Keystone.

fedcloudclient.sites

Configure and query site metadata (regions, URLs, capabilities).

fedcloudclient.openstack

Execute OpenStack CLI commands (nova, glance, neutron) against specified sites.

fedcloudclient.secret

Retrieve and manage secrets from the EGI Vault service.

fedcloudclient.auth

Validate access tokens and credentials; utilities for checking token expiration and scopes.

+
+
+

Getting Started

+

A concise introductory walkthrough is available in this presentation.

+

For detailed installation instructions, full usage examples, and API references, see the official documentation:

+

https://fedcloudclient.fedcloud.eu/

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/modules.html b/docs/_build/html/modules.html new file mode 100644 index 0000000..24724dd --- /dev/null +++ b/docs/_build/html/modules.html @@ -0,0 +1,128 @@ + + + + + + + + + fedcloudclient API references — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

fedcloudclient API references

+ +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/objects.inv b/docs/_build/html/objects.inv new file mode 100644 index 0000000..45fec0e Binary files /dev/null and b/docs/_build/html/objects.inv differ diff --git a/docs/_build/html/py-modindex.html b/docs/_build/html/py-modindex.html new file mode 100644 index 0000000..3e1d00c --- /dev/null +++ b/docs/_build/html/py-modindex.html @@ -0,0 +1,176 @@ + + + + + + + + Python Module Index — fedcloudclient documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + +

Python Module Index

+ +
+ f +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ f
+ fedcloudclient +
    + fedcloudclient.auth +
    + fedcloudclient.checkin +
    + fedcloudclient.cli +
    + fedcloudclient.conf +
    + fedcloudclient.endpoint +
    + fedcloudclient.locker_auth +
    + fedcloudclient.logger +
    + fedcloudclient.openstack +
    + fedcloudclient.sites +
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/quickstart.html b/docs/_build/html/quickstart.html new file mode 100644 index 0000000..b59a390 --- /dev/null +++ b/docs/_build/html/quickstart.html @@ -0,0 +1,239 @@ + + + + + + + + + Quick start — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Quick start

+

The Tutorial +presentation is designed for new users of FedCloud client. It starts with the quick setup and basic usages, +then step by step to more advanced scenarios.

+
+

Setup

+
    +
  • Install FedCloud client via pip:

  • +
+
$ pip3 install fedcloudclient
+
+
+

or use Docker container:

+
$ docker run -it  tdviet/fedcloudclient bash
+
+
+ +
$ export FEDCLOUD_OIDC_ACCESS_TOKEN=<ACCESS_TOKEN>
+
+
+
+
+

Basic usages

+
    +
  • List your VO memberships according to the access token:

  • +
+
$ fedcloud token list-vos
+eosc-synergy.eu
+fedcloud.egi.eu
+training.egi.eu
+...
+
+
+
    +
  • List sites in the EGI Federated Cloud

  • +
+
$ fedcloud site list
+100IT
+BIFI
+CESGA
+...
+
+
+
    +
  • List sites supporting a Virtual Organization in the EGI Federated Cloud

  • +
+
$ fedcloud site list --vo vo.access.egi.eu
+BIFI
+CENI
+CESGA-CLOUD
+...
+
+
+
    +
  • Execute an OpenStack command, e.g. list images in eosc-synergy.eu VO on IISAS-FedCloud site +(or other combination of site and VO you have access):

  • +
+
$ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu
+Site: IISAS-FedCloud, VO: eosc-synergy.eu
++--------------------------------------+-------------------------------------------------+--------+
+| ID                                   | Name                                            | Status |
++--------------------------------------+-------------------------------------------------+--------+
+| 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox]    | active |
+...
+
+
+
    +
  • Execute an OpenStack command, e.g. list VMs in eosc-synergy.eu VO on all sites +and print output in JSON format for further machine processing:

  • +
+
$ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu --json-output
+[
+{
+  "Site": "IISAS-FedCloud",
+  "VO": "eosc-synergy.eu",
+  "command": "server list",
+  "Exception": null,
+  "Error code": 0,
+  "Result": [
+    {
+      ...
+    },
+    ...
+  ]
+},
+...
+]
+
+
+
    +
  • Get helps from the client

  • +
+
$ fedcloud --help
+Usage: fedcloud [OPTIONS] COMMAND [ARGS]...
+
+Options:
+  --help  Show this message and exit.
+
+Commands:
+  endpoint       Endpoint command group for interaction with GOCDB and endpoints
+  openstack      Executing OpenStack commands on site and VO
+  openstack-int  Interactive OpenStack client on site and VO
+  site           Site command group for manipulation with site configurations
+  token          Token command group for manipulation with tokens
+
+
+
    +
  • Read the Tutorial +presentation or next sections for more information.

  • +
+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/scripts.html b/docs/_build/html/scripts.html new file mode 100644 index 0000000..ab0ecba --- /dev/null +++ b/docs/_build/html/scripts.html @@ -0,0 +1,203 @@ + + + + + + + + + Using FedCloud client in scripts — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Using FedCloud client in scripts

+

FedCloud client can be used in scripts for simple automation, either for setting environment variables for other tools +or processing outputs from OpenStack commands.

+
+

Setting environment variables for external tools

+

Outputs from FedCloud client commands for setting environment variables are already in the forms “export VAR=VALUE”. +Simple eval command in scripts can be used for setting environment variables for external tools:

+
$ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu
+export OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/"
+export OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee"
+
+# This command will set environment variables
+$ eval $(fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu)
+
+# Check the value of the variable
+$ echo $OS_AUTH_URL
+https://cloud.ui.savba.sk:5000/v3/
+
+
+
+
+

Processing JSON outputs from OpenStack commands via jq

+

The outputs from Openstack command can be printed in JSON formats with –json-output parameter for further machine +processing. The JSON outputs can be processed in scripts by jq command. +For examples, if users want to select flavors with 2 CPUs:

+
$ fedcloud openstack flavor list  --site IISAS-FedCloud --vo eosc-synergy.eu --json-output
+[
+{
+  "Site": "IISAS-FedCloud",
+  "VO": "eosc-synergy.eu",
+  "command": "flavor list",
+  "Exception": null,
+  "Error code": 0,
+  "Result": [
+    {
+      "ID": "0",
+      "Name": "m1.nano",
+      "RAM": 64,
+      "Disk": 1,
+      "Ephemeral": 0,
+      "VCPUs": 1,
+      "Is Public": true
+    },
+    {
+      "ID": "2e562a51-8861-40d5-8fc9-2638bab4662c",
+      "Name": "m1.xlarge",
+      "RAM": 16384,
+      "Disk": 40,
+      "Ephemeral": 0,
+      "VCPUs": 8,
+      "Is Public": true
+    },
+    ...
+  ]
+}
+]
+
+# The following jq command selects flavors with VCPUs=2 and print their names
+$ fedcloud openstack flavor list  --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \
+    jq -r  '.[].Result[] | select(.VCPUs == 2) | .Name'
+m1.medium
+
+
+

The following example is more complex:

+
    +
  • List all flavors in the VO vo.access.egi.eu on all sites and print them in JSON format

  • +
  • Filter out sites with error code > 0

  • +
  • Select only GPU flavors

  • +
  • Filter out sites with empty list of GPU flavors

  • +
  • Print the result (list of all GPU flavors on all sites) in JSON format

  • +
+
$ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \
+    jq -r 'map(select(."Error code" ==  0)) |
+           map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) |
+           map(select(.Result | length >  0))'
+
+
+

Note that only OpenStack commands that have outputs can be used with –json-output. Using the parameter with +commands without outputs (e.g. setting properties) will generate errors of unsupported parameters.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_build/html/search.html b/docs/_build/html/search.html new file mode 100644 index 0000000..6c8c36a --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,131 @@ + + + + + + + + Search — fedcloudclient documentation + + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+
    +
  • + +
  • +
  • +
+
+
+
+
+ + + + +
+ +
+ +
+
+ +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/docs/_build/html/searchindex.js b/docs/_build/html/searchindex.js new file mode 100644 index 0000000..f4af777 --- /dev/null +++ b/docs/_build/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"alltitles": {"Additional Configurable Parameters": [[10, "additional-configurable-parameters"]], "Authentication Options": [[10, "authentication-options"]], "Basic usages": [[1, "basic-usages"], [8, "basic-usages"]], "Cheat sheet": [[1, null]], "Configuration": [[10, "configuration"]], "Consistent Parameter Naming": [[10, "consistent-parameter-naming"]], "Contents:": [[4, null]], "Core Modules": [[6, "core-modules"]], "FAQ and Troubleshooting": [[0, null]], "FedCloud Client": [[6, "fedcloud-client"]], "FedCloud client API references": [[3, null]], "Getting Started": [[6, "getting-started"]], "Installation": [[5, null]], "Installation Notes": [[5, "installation-notes"]], "Installing EGI Core Trust Anchor certificates": [[5, "installing-egi-core-trust-anchor-certificates"]], "Installing FedCloud client with pip": [[5, "installing-fedcloud-client-with-pip"]], "Introduction": [[6, null]], "Key Features": [[6, "key-features"]], "Local install via pip3": [[1, "local-install-via-pip3"]], "Mapping and filtering results from OpenStack commands": [[1, "mapping-and-filtering-results-from-openstack-commands"]], "More information": [[1, "more-information"]], "Processing JSON outputs from OpenStack commands via jq": [[9, "processing-json-outputs-from-openstack-commands-via-jq"]], "Quick start": [[8, null]], "Searching and selecting resources": [[1, "searching-and-selecting-resources"]], "Setting environment variables for external tools": [[9, "setting-environment-variables-for-external-tools"]], "Setup": [[8, "setup"]], "Shell completion": [[10, "shell-completion"]], "Usage": [[10, null]], "Useful commands": [[1, "useful-commands"]], "Using Docker container": [[1, "using-docker-container"]], "Using Environment Variables and Configuration Priorities": [[10, "using-environment-variables-and-configuration-priorities"]], "Using FedCloud client in Python": [[2, null]], "Using FedCloud client in scripts": [[9, null]], "Using FedCloud client via Docker container": [[5, "using-fedcloud-client-via-docker-container"]], "Using oidc-agent": [[1, "using-oidc-agent"]], "Verifying the Installation": [[5, "verifying-the-installation"]], "fedcloud config commands": [[10, "fedcloud-config-commands"]], "fedcloud endpoint commands": [[10, "fedcloud-endpoint-commands"]], "fedcloud openstack commands": [[10, "fedcloud-openstack-commands"]], "fedcloud secret commands": [[10, "fedcloud-secret-commands"]], "fedcloud select commands": [[10, "fedcloud-select-commands"]], "fedcloud site commands": [[10, "fedcloud-site-commands"]], "fedcloud token commands": [[10, "fedcloud-token-commands"]], "fedcloud \u2013help command": [[10, "fedcloud-help-command"]], "fedcloudclient API references": [[7, null]], "fedcloudclient.auth module": [[3, "module-fedcloudclient.auth"]], "fedcloudclient.checkin module": [[3, "module-fedcloudclient.checkin"]], "fedcloudclient.cli module": [[3, "module-fedcloudclient.cli"]], "fedcloudclient.conf module": [[3, "module-fedcloudclient.conf"]], "fedcloudclient.endpoint module": [[3, "module-fedcloudclient.endpoint"]], "fedcloudclient.locker_auth module": [[3, "module-fedcloudclient.locker_auth"]], "fedcloudclient.logger module": [[3, "module-fedcloudclient.logger"]], "fedcloudclient.openstack module": [[3, "module-fedcloudclient.openstack"]], "fedcloudclient.sites module": [[3, "module-fedcloudclient.sites"]]}, "docnames": ["FAQ", "cheat", "development", "fedcloudclient", "index", "install", "intro", "modules", "quickstart", "scripts", "usage"], "envversion": {"sphinx": 64, "sphinx.domains.c": 3, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 9, "sphinx.domains.index": 1, "sphinx.domains.javascript": 3, "sphinx.domains.math": 2, "sphinx.domains.python": 4, "sphinx.domains.rst": 2, "sphinx.domains.std": 2}, "filenames": ["FAQ.rst", "cheat.rst", "development.rst", "fedcloudclient.rst", "index.rst", "install.rst", "intro.rst", "modules.rst", "quickstart.rst", "scripts.rst", "usage.rst"], "indexentries": {"check_openstack_client_installation() (in module fedcloudclient.openstack)": [[3, "fedcloudclient.openstack.check_openstack_client_installation", false]], "check_token() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.check_token", false]], "decode_token() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.decode_token", false]], "delete_site_config() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.delete_site_config", false]], "fedcloud_openstack() (in module fedcloudclient.openstack)": [[3, "fedcloudclient.openstack.fedcloud_openstack", false]], "fedcloud_openstack_full() (in module fedcloudclient.openstack)": [[3, "fedcloudclient.openstack.fedcloud_openstack_full", false]], "fedcloudclient.auth": [[3, "module-fedcloudclient.auth", false]], "fedcloudclient.checkin": [[3, "module-fedcloudclient.checkin", false]], "fedcloudclient.cli": [[3, "module-fedcloudclient.cli", false]], "fedcloudclient.conf": [[3, "module-fedcloudclient.conf", false]], "fedcloudclient.endpoint": [[3, "module-fedcloudclient.endpoint", false]], "fedcloudclient.locker_auth": [[3, "module-fedcloudclient.locker_auth", false]], "fedcloudclient.logger": [[3, "module-fedcloudclient.logger", false]], "fedcloudclient.openstack": [[3, "module-fedcloudclient.openstack", false]], "fedcloudclient.sites": [[3, "module-fedcloudclient.sites", false]], "find_endpoint() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.find_endpoint", false]], "find_endpoint_and_project_id() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.find_endpoint_and_project_id", false]], "find_site_data() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.find_site_data", false]], "find_vo_from_project_id() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.find_vo_from_project_id", false]], "format_project_as_dict() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.format_project_as_dict", false]], "format_project_as_list() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.format_project_as_list", false]], "get_checkin_id() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.get_checkin_id", false]], "get_keystone_url() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_keystone_url", false]], "get_projects_from_single_site() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_projects_from_single_site", false]], "get_projects_from_sites() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_projects_from_sites", false]], "get_projects_from_sites_as_dict() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_projects_from_sites_as_dict", false]], "get_projects_from_sites_as_list() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_projects_from_sites_as_list", false]], "get_projects_from_sites_with_format() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_projects_from_sites_with_format", false]], "get_scoped_token() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_scoped_token", false]], "get_sites() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_sites", false]], "get_token_from_mytoken() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.get_token_from_mytoken", false]], "get_token_from_oidc_agent() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.get_token_from_oidc_agent", false]], "get_unscoped_token() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.get_unscoped_token", false]], "get_user_id() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.get_user_id", false]], "get_user_id() (fedcloudclient.locker_auth.lockertoken method)": [[3, "fedcloudclient.locker_auth.LockerToken.get_user_id", false]], "get_vault_auth_method() (fedcloudclient.locker_auth.lockertoken method)": [[3, "fedcloudclient.locker_auth.LockerToken.get_vault_auth_method", false]], "get_vault_user_id() (fedcloudclient.locker_auth.lockertoken method)": [[3, "fedcloudclient.locker_auth.LockerToken.get_vault_user_id", false]], "init_config() (in module fedcloudclient.conf)": [[3, "fedcloudclient.conf.init_config", false]], "init_default_config() (in module fedcloudclient.conf)": [[3, "fedcloudclient.conf.init_default_config", false]], "init_logger() (in module fedcloudclient.logger)": [[3, "fedcloudclient.logger.init_logger", false]], "list_sites() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.list_sites", false]], "load_config() (in module fedcloudclient.conf)": [[3, "fedcloudclient.conf.load_config", false]], "load_env() (in module fedcloudclient.conf)": [[3, "fedcloudclient.conf.load_env", false]], "lockertoken (class in fedcloudclient.locker_auth)": [[3, "fedcloudclient.locker_auth.LockerToken", false]], "log_and_raise() (in module fedcloudclient.logger)": [[3, "fedcloudclient.logger.log_and_raise", false]], "module": [[3, "module-fedcloudclient.auth", false], [3, "module-fedcloudclient.checkin", false], [3, "module-fedcloudclient.cli", false], [3, "module-fedcloudclient.conf", false], [3, "module-fedcloudclient.endpoint", false], [3, "module-fedcloudclient.locker_auth", false], [3, "module-fedcloudclient.logger", false], [3, "module-fedcloudclient.openstack", false], [3, "module-fedcloudclient.sites", false]], "multiple_token() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.multiple_token", false]], "oidc_discover() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.oidc_discover", false]], "oidctoken (class in fedcloudclient.auth)": [[3, "fedcloudclient.auth.OIDCToken", false]], "print_result() (in module fedcloudclient.openstack)": [[3, "fedcloudclient.openstack.print_result", false]], "read_default_site_config() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.read_default_site_config", false]], "read_local_site_config() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.read_local_site_config", false]], "read_site_config() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.read_site_config", false]], "read_site_schema() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.read_site_schema", false]], "retrieve_unscoped_token() (in module fedcloudclient.endpoint)": [[3, "fedcloudclient.endpoint.retrieve_unscoped_token", false]], "safe_read_yaml_from_url() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.safe_read_yaml_from_url", false]], "save_config() (in module fedcloudclient.conf)": [[3, "fedcloudclient.conf.save_config", false]], "save_site_config() (in module fedcloudclient.sites)": [[3, "fedcloudclient.sites.save_site_config", false]], "token (class in fedcloudclient.auth)": [[3, "fedcloudclient.auth.Token", false]], "token_list_vos() (fedcloudclient.auth.oidctoken method)": [[3, "fedcloudclient.auth.OIDCToken.token_list_vos", false]], "tokenexception": [[3, "fedcloudclient.endpoint.TokenException", false]], "vault_command() (fedcloudclient.locker_auth.lockertoken method)": [[3, "fedcloudclient.locker_auth.LockerToken.vault_command", false]]}, "objects": {"fedcloudclient": [[3, 0, 0, "-", "auth"], [3, 0, 0, "-", "checkin"], [3, 0, 0, "-", "cli"], [3, 0, 0, "-", "conf"], [3, 0, 0, "-", "endpoint"], [3, 0, 0, "-", "locker_auth"], [3, 0, 0, "-", "logger"], [3, 0, 0, "-", "openstack"], [3, 0, 0, "-", "sites"]], "fedcloudclient.auth": [[3, 1, 1, "", "OIDCToken"], [3, 1, 1, "", "Token"]], "fedcloudclient.auth.OIDCToken": [[3, 2, 1, "", "check_token"], [3, 2, 1, "", "decode_token"], [3, 2, 1, "", "get_checkin_id"], [3, 2, 1, "", "get_token_from_mytoken"], [3, 2, 1, "", "get_token_from_oidc_agent"], [3, 2, 1, "", "get_user_id"], [3, 2, 1, "", "multiple_token"], [3, 2, 1, "", "oidc_discover"], [3, 2, 1, "", "token_list_vos"]], "fedcloudclient.conf": [[3, 3, 1, "", "init_config"], [3, 3, 1, "", "init_default_config"], [3, 3, 1, "", "load_config"], [3, 3, 1, "", "load_env"], [3, 3, 1, "", "save_config"]], "fedcloudclient.endpoint": [[3, 4, 1, "", "TokenException"], [3, 3, 1, "", "find_endpoint"], [3, 3, 1, "", "format_project_as_dict"], [3, 3, 1, "", "format_project_as_list"], [3, 3, 1, "", "get_keystone_url"], [3, 3, 1, "", "get_projects_from_single_site"], [3, 3, 1, "", "get_projects_from_sites"], [3, 3, 1, "", "get_projects_from_sites_as_dict"], [3, 3, 1, "", "get_projects_from_sites_as_list"], [3, 3, 1, "", "get_projects_from_sites_with_format"], [3, 3, 1, "", "get_scoped_token"], [3, 3, 1, "", "get_sites"], [3, 3, 1, "", "get_unscoped_token"], [3, 3, 1, "", "retrieve_unscoped_token"]], "fedcloudclient.locker_auth": [[3, 1, 1, "", "LockerToken"]], "fedcloudclient.locker_auth.LockerToken": [[3, 2, 1, "", "get_user_id"], [3, 2, 1, "", "get_vault_auth_method"], [3, 2, 1, "", "get_vault_user_id"], [3, 2, 1, "", "vault_command"]], "fedcloudclient.logger": [[3, 3, 1, "", "init_logger"], [3, 3, 1, "", "log_and_raise"]], "fedcloudclient.openstack": [[3, 3, 1, "", "check_openstack_client_installation"], [3, 3, 1, "", "fedcloud_openstack"], [3, 3, 1, "", "fedcloud_openstack_full"], [3, 3, 1, "", "print_result"]], "fedcloudclient.sites": [[3, 3, 1, "", "delete_site_config"], [3, 3, 1, "", "find_endpoint_and_project_id"], [3, 3, 1, "", "find_site_data"], [3, 3, 1, "", "find_vo_from_project_id"], [3, 3, 1, "", "list_sites"], [3, 3, 1, "", "read_default_site_config"], [3, 3, 1, "", "read_local_site_config"], [3, 3, 1, "", "read_site_config"], [3, 3, 1, "", "read_site_schema"], [3, 3, 1, "", "safe_read_yaml_from_url"], [3, 3, 1, "", "save_site_config"]]}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "function", "Python function"], "4": ["py", "exception", "Python exception"]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:function", "4": "py:exception"}, "terms": {"": [2, 3, 6], "0": [1, 2, 8, 9, 10], "01": 10, "02": 10, "04": [1, 10], "1": [9, 10], "10": 10, "100it": [8, 10], "16384": 9, "169": 1, "18": [1, 10], "2": [1, 9, 10], "20": [1, 10], "2021": 10, "2048": [1, 10], "2096": 10, "233f045cb1ff46842a15ebb33af69460": 10, "25": 10, "254": 1, "2638bab4662c": 9, "2a7e2cd4b6dc4e609dd934964c1715c6": 10, "2e562a51": 9, "2xk20m": 10, "30": 10, "300": 0, "3571": 10, "39": 10, "3b9754ad8c6046b4aec43ec21abe7d8c": 10, "4": 2, "40": [9, 10], "4096": 10, "40d5": 9, "4227": [8, 10], "462d": 10, "4cab325ca8c2495bf2d4e8f230bcd51a": 10, "4e0b": 10, "5000": [9, 10], "51f736d36ce34b9ebdf196cfcabd24e": [9, 10], "5b533844996c": 10, "5bbdb5c1e0b2bcbac29904f4ac22dcaa": 10, "5bd8397c": 10, "64": 9, "6a11": [8, 10], "7": [8, 10], "7101022b9ae74ed9ac1a574497279499": 10, "8": 9, "805e": 10, "8200": 10, "8388": [8, 10], "862d4ede": [8, 10], "8861": 9, "8fc9": 9, "9d2b": 10, "A": [3, 6], "As": 10, "By": [3, 10], "For": [0, 5, 6, 9, 10], "If": [0, 2, 3, 5, 10], "In": 10, "It": [0, 5, 6, 8, 10], "Not": 10, "On": 0, "One": 6, "The": [0, 3, 6, 8, 9, 10], "These": 10, "To": [5, 10], "_fedcloud_complet": [1, 10], "_min_access_token_tim": [3, 10], "_vo_pattern": 3, "a22bbffb007745b2934bf308b0a4d186": 10, "aai": [1, 10], "abil": [3, 10], "about": 2, "abov": 10, "abstract": 3, "acc": 1, "acceler": [1, 9], "access": [1, 2, 3, 5, 6, 8, 9, 10], "access_token": [3, 8, 10], "accord": [1, 3, 5, 8, 10], "account": [1, 3, 10], "across": [6, 10], "action": 6, "activ": [1, 8, 10], "add": [0, 2, 3, 5, 10], "addit": 3, "advanc": 8, "advantag": [3, 10], "affect": 10, "after": [0, 10], "again": 10, "against": 6, "agent": [0, 3, 4, 8, 10], "all": [1, 3, 5, 6, 8, 9, 10], "all_sit": [1, 6, 8, 9, 10], "allow": 10, "alreadi": 9, "also": [0, 5, 10], "altern": 10, "an": [1, 2, 3, 8, 10], "anchor": [0, 4], "ani": [3, 6, 10], "anyth": [3, 10], "api": [2, 4, 6, 10], "appdb": 1, "applianc": 1, "appropri": 5, "ar": [0, 3, 5, 9, 10], "arg": [5, 8, 10], "argument": 10, "ask": 3, "auth": [1, 6, 10], "authent": [3, 4], "author": [0, 5], "autom": [6, 9], "avail": [1, 2, 3, 6, 10], "avoid": 10, "b97f": 10, "bari": 10, "base": [3, 5], "bash": [0, 1, 5, 8, 10], "bash_sourc": [1, 10], "bashrc": 10, "basic": 4, "been": 3, "befor": [3, 10], "begin": 10, "below": 10, "besid": [3, 10], "best": 10, "between": 10, "bifi": [8, 10], "bin": [1, 5], "bool": 3, "both": 10, "broad": 6, "built": 10, "bundl": [0, 1, 5], "c": 1, "c08r30": 10, "c16r60": 10, "c94141a5dac": [8, 10], "cach": [3, 10], "call": [2, 3, 6, 10], "can": [1, 2, 3, 5, 9, 10], "cannot": 3, "capabl": 6, "case": 3, "cat": [0, 1], "catania": 10, "cd": 6, "ceni": [8, 10], "cento": [0, 8, 10], "cert": [0, 1, 5, 10], "certif": [0, 1, 4], "cesga": [1, 8, 10], "cesnet": 1, "chang": 10, "cheat": 4, "check": [0, 1, 2, 3, 5, 6, 8, 9, 10], "check_in_url": 10, "check_openstack_client_instal": 3, "check_token": 3, "checkin": [6, 10], "choos": 10, "ci": 6, "claim": 3, "class": 3, "cli": [5, 6], "click": 10, "client": [0, 4, 7, 8, 10], "client_id": 10, "cloud": [1, 2, 6, 8, 9, 10], "cluster": 5, "code": [1, 2, 3, 8, 9, 10], "com": [0, 1, 5, 10], "combin": [8, 10], "command": [0, 2, 3, 4, 5, 6, 8], "comp": 1, "complementari": [3, 10], "complet": [1, 4], "complex": [1, 9], "comprehens": 6, "comput": 6, "concis": 6, "conf": [6, 10], "config": [1, 3, 4], "config_data": 3, "config_dir": 3, "configerror": 3, "configur": [3, 4, 5, 6, 8], "connect": [0, 3, 5, 6, 10], "construct": 1, "contact": 3, "contain": [2, 3, 4, 8], "conveni": 10, "convent": 10, "convert": 3, "copi": [1, 2, 10], "core": [0, 4], "correctli": 2, "correspond": [3, 10], "covid19": 10, "cpu": [1, 9, 10], "creat": [1, 5, 10], "credenti": [6, 10], "cubbyhol": 10, "curl": 1, "current": [3, 10], "custom": [3, 6, 10], "cyfronet": 2, "d340308880134d04294097524eace710": 10, "d34620ba0334": 10, "daemon": 0, "dashboard": 1, "data": [1, 3, 10], "databas": [3, 10], "dc": 1, "debug": 10, "decod": 3, "decode_token": 3, "default": [0, 3, 5, 6, 10], "default_set": [3, 10], "defin": [3, 10], "delet": 3, "delete_site_config": 3, "demo": [2, 10], "depend": 5, "describ": [0, 10], "design": [6, 8, 10], "detail": [0, 1, 2, 5, 6, 10], "determin": 10, "develop": [2, 6], "df25f80f": 10, "dict": 3, "dictionari": 3, "differ": 10, "dir": 3, "direct": [6, 10], "directli": [2, 3, 10], "directori": [3, 5, 10], "dirti": 1, "discov": [3, 6], "discoveri": [3, 6], "disk": [3, 9, 10], "displai": 10, "distribut": [0, 5], "do": [3, 10], "docker": [4, 8, 10], "document": [3, 6, 10], "doe": [3, 10], "done": [1, 5, 10], "download": 2, "dump": 2, "dure": 0, "e": [3, 8, 9, 10], "each": [2, 10], "earlier": [3, 10], "easi": 2, "eayeghjtjtj": 10, "ec3": 5, "echo": 9, "ed19": 10, "edit": 10, "edu": [1, 10], "eduperson_entitl": [1, 3], "effect": 2, "egi": [0, 1, 2, 3, 4, 6, 8, 9, 10], "egi_access": 10, "egwergwregrwegreg": 10, "either": 9, "els": 2, "email": 1, "emb": 6, "empti": [3, 5, 9], "enabl": 10, "endpoint": [1, 4, 5, 6, 8], "ensur": 10, "entri": 3, "entropi": 0, "entropy_avail": 0, "env": [1, 10], "env_config": 10, "environ": [0, 1, 3, 4, 5, 8], "eosc": [1, 8, 9, 10], "ephemer": [9, 10], "equival": [2, 10], "error": [0, 1, 2, 3, 5, 8, 9], "error_cod": [2, 3], "error_msg": 3, "etc": 6, "eu": [1, 2, 3, 6, 8, 9, 10], "eval": [1, 9, 10], "even": 10, "everi": 6, "exampl": [1, 2, 6, 9, 10], "exc_msg": 3, "except": [0, 3, 5, 8, 9], "exchang": 3, "execut": [0, 1, 2, 5, 6, 8, 10], "exist": [3, 10], "exit": [5, 8, 10], "expir": [1, 3, 6, 10], "explan": 10, "explicitli": [3, 10], "export": [1, 8, 9, 10], "expos": 3, "extens": 6, "extern": 4, "extract": 3, "f": 1, "fail": 3, "fals": 3, "faq": 4, "faster": [3, 10], "featur": 4, "fedcloud": [0, 1, 4, 7, 8], "fedcloud_": [3, 10], "fedcloud_access_token": 10, "fedcloud_bash_complet": [1, 10], "fedcloud_config_fil": [3, 10], "fedcloud_mytoken": 1, "fedcloud_oidc_access_token": [8, 10], "fedcloud_oidc_agent_account": 10, "fedcloud_openstack": [2, 3], "fedcloud_openstack_ful": 3, "fedcloud_os_access_token": 10, "fedcloud_os_auth_typ": 10, "fedcloud_os_auth_url": 10, "fedcloud_os_identity_provid": 10, "fedcloud_os_project_id": 10, "fedcloud_os_protocol": 10, "fedcloud_os_token": 10, "fedcloud_salt": 10, "fedcloud_sit": [1, 10], "fedcloud_vo": [1, 10], "fedcloudcli": [1, 2, 4, 5, 6, 8, 10], "feder": [1, 2, 6, 8, 10], "fetch": [3, 6], "file": [1, 3, 5, 10], "filenam": 3, "filter": [4, 6, 9], "find": 3, "find_endpoint": 3, "find_endpoint_and_project_id": 3, "find_site_data": 3, "find_vo_from_project_id": 3, "finish": 2, "first": [3, 10], "fix": [3, 10], "flavor": [1, 9, 10], "flexibl": 10, "follow": [0, 5, 9, 10], "form": [3, 9], "format": [1, 3, 8, 9, 10], "format_project_as_dict": 3, "format_project_as_list": 3, "found": 3, "fr": 10, "frequent": 10, "from": [2, 3, 4, 6, 8, 10], "frozen": 0, "full": [1, 3, 6, 10], "function": [2, 3, 5, 6], "further": [8, 9], "g": [3, 8, 9, 10], "g1": 10, "gen": 1, "gener": 9, "get": [1, 3, 4, 5, 8, 10], "get_checkin_id": 3, "get_keystone_url": 3, "get_projects_from_single_sit": 3, "get_projects_from_sit": 3, "get_projects_from_sites_as_dict": 3, "get_projects_from_sites_as_list": 3, "get_projects_from_sites_with_format": 3, "get_scoped_token": 3, "get_sit": 3, "get_token_from_mytoken": 3, "get_token_from_oidc_ag": 3, "get_unscoped_token": 3, "get_user_id": 3, "get_vault_auth_method": 3, "get_vault_user_id": 3, "github": [2, 3, 10], "githubusercont": [0, 1, 5, 10], "give": [0, 3, 10], "given": 10, "glanc": 6, "global": 3, "goc": [3, 10], "gocdb": [3, 6, 8, 10], "gocdb_public_url": 10, "gocdb_service_group": 10, "gocdbpi": 10, "gpu": [1, 9, 10], "grid": [3, 10], "group": [8, 10], "guid": 6, "ha": [3, 10], "handl": 10, "handler": 3, "hard": 3, "hardcod": 10, "hasn": 3, "have": [0, 3, 8, 9, 10], "haveg": 0, "help": [1, 4, 5, 8], "helper": 3, "hep": 10, "here": [0, 10], "high": 6, "higher": 10, "highest": 10, "home": 10, "horizon": 1, "how": 2, "http": [0, 1, 3, 5, 6, 9, 10], "httperror": 3, "huge": 10, "human": 3, "i": [0, 1, 2, 3, 5, 6, 8, 9, 10], "id": [1, 3, 8, 9, 10], "ident": 3, "identifi": [3, 6], "ifca": [1, 10], "ignore_missing_vo": 3, "igtf": 1, "iisa": [1, 6, 8, 9, 10], "imag": [1, 2, 3, 6, 8, 10], "implement": [3, 10], "import": [2, 6, 10], "in2p3": 10, "inaccess": 10, "includ": [0, 3, 5, 6, 10], "indent": 2, "indic": 3, "infn": 10, "inform": [3, 4, 8, 10], "init": 3, "init_config": 3, "init_default_config": 3, "init_logg": 3, "initi": [0, 3], "input": [2, 10], "insid": 5, "inspect": 6, "instal": [0, 3, 4, 6, 8], "install_cert": [0, 1, 5], "instanc": 10, "instead": [3, 10], "instruct": [0, 5, 6, 8, 10], "int": [3, 5, 8, 10], "integr": [6, 10], "intention": 5, "interact": [3, 5, 6, 8, 10], "interfac": 6, "introduct": 4, "introductori": 6, "invalid": 3, "invok": 6, "ir": 10, "iss": 3, "issu": [0, 1, 5, 10], "issuer": [1, 3], "its": [2, 3, 10], "join": 10, "jq": [1, 4], "json": [1, 2, 3, 4, 8, 10], "json_output": 3, "just": [0, 1, 2, 5, 6], "jwt": 3, "k20m": 10, "kei": [1, 3, 4], "kernel": 0, "keychain": 1, "keypair": 1, "keyston": [3, 6, 10], "kit": [1, 10], "known": [0, 3], "larg": 10, "later": 3, "latest": [1, 5], "lcg2": [1, 10], "length": [1, 9], "less": 10, "level": 6, "librari": [0, 2, 6], "libsodium": 0, "lifetim": 3, "like": 3, "lill": 10, "line": [2, 3, 6, 10], "list": [1, 2, 3, 6, 8, 9, 10], "list_sit": 3, "ll": 3, "load": [1, 3, 10], "load_config": 3, "load_env": 3, "local": [3, 4, 5, 10], "locat": [3, 10], "locker": 3, "locker_token": 3, "lockertoken": 3, "log": [1, 3, 10], "log_and_rais": 3, "log_config_fil": 10, "log_fil": 10, "log_level": 10, "long": [1, 2, 3, 9, 10], "longer": 1, "lookup": 6, "lower": [0, 3], "lowercas": 3, "lowest": 10, "m": 1, "m1": [9, 10], "machin": [0, 1, 8, 9], "mai": [1, 3, 10], "main": [0, 1, 3, 5, 10], "mainli": 0, "make": [3, 5, 10], "manag": [3, 6, 10], "manipul": [3, 8, 10], "manual": [0, 10], "map": [3, 4, 9, 10], "master": [1, 10], "match": 10, "max_length": 3, "maximum": 6, "mcc": 1, "md": [0, 5], "medium": [9, 10], "membership": [1, 3, 8, 10], "mention": 10, "merg": 3, "messag": [0, 2, 3, 5, 8, 10], "metadata": [3, 6], "method": [3, 10], "min_access_token_tim": 10, "minim": 6, "minimum": 3, "miss": 3, "mode": 10, "modul": 4, "monitor": 3, "more": [0, 2, 4, 5, 8, 9], "most": 2, "multipl": [3, 6, 10], "multiple_token": 3, "must": [2, 10], "my": 1, "mytoken": [1, 3, 10], "mytoken_serv": [3, 10], "name": [1, 3, 8, 9], "name_of_us": 10, "name_of_user_for_oidc_ag": 10, "nano": 9, "nation": [0, 5], "necessari": 10, "need": [3, 10], "network": [6, 10], "neutron": 6, "new": [8, 10], "newest": 10, "next": 8, "none": 3, "note": [9, 10], "notif": 10, "nova": [6, 10], "null": [8, 9], "o": [0, 3, 5, 10], "object": [1, 2, 3, 5, 10], "obtain": [3, 5, 6, 10], "occur": 3, "offici": 6, "oidc": [0, 3, 4, 8, 10], "oidc_access_token": 3, "oidc_agent_account": [1, 3, 10], "oidc_discov": 3, "oidc_url": 10, "oidctoken": 3, "ok": 2, "onc": [1, 10], "one": [3, 10], "ones": 3, "onli": [1, 3, 9, 10], "opaqu": 3, "openi": 3, "openid": [3, 6, 10], "openstack": [2, 4, 5, 6, 8], "openstack_command": [3, 10], "openstackcli": 5, "oper": [3, 6, 10], "option": [2, 3, 4, 5, 8], "order": [3, 10], "org": [1, 10], "organ": [1, 3, 6, 8, 10], "organis": 10, "os_access_token": 10, "os_auth_typ": [3, 10], "os_auth_url": [3, 9], "os_identity_provid": [3, 10], "os_project_id": 9, "os_protocol": [3, 10], "os_token": 1, "other": [1, 2, 3, 8, 9, 10], "otherwis": [3, 10], "out": [3, 9], "output": [1, 2, 3, 4, 5, 8, 10], "output_format": 10, "output_format_funct": 3, "overrid": [3, 10], "overridden": [3, 10], "overwrit": [3, 10], "own": [1, 10], "owner": 10, "packag": [5, 6], "padova": 10, "param": 3, "paramet": [2, 3, 6, 9], "pars": 3, "part": [3, 10], "pass": [1, 3, 10], "path": [3, 5, 10], "pattern": 3, "payload": 3, "pem": 10, "per": 10, "perform": [3, 10], "pip": [1, 4, 8], "pip3": [4, 5, 8], "pipelin": 6, "place": [5, 10], "portal": [8, 10], "possibl": 10, "power": 6, "preced": 3, "prefix": [3, 10], "preinstal": 5, "present": [6, 8], "press": 10, "previou": 1, "previous": 1, "print": [1, 2, 3, 8, 9, 10], "print_result": 3, "privat": 10, "privileg": 5, "probe": [3, 10], "problem": 0, "proc": 0, "process": [2, 3, 4, 8], "product": 3, "profil": 6, "programmat": 6, "project": [1, 3, 9, 10], "project_id": [3, 10], "properti": [1, 9], "protocol": [3, 10], "provid": [3, 6, 10], "provis": 5, "pub": 1, "public": [9, 10], "pull": [1, 5], "purpos": 6, "py": 2, "python": [0, 1, 4, 5, 6], "python3": 1, "queri": 6, "quick": [0, 1, 4, 5], "r": [1, 9], "rais": 3, "ram": [1, 9, 10], "random": 0, "rang": 6, "raw": [0, 1, 3, 5, 10], "read": [2, 3, 8, 10], "read_default_site_config": 3, "read_local_site_config": 3, "read_site_config": 3, "read_site_schema": 3, "readabl": 3, "readm": [0, 5], "realm": [1, 10], "reason": 10, "reboot": 0, "receiv": [0, 5], "recommend": 10, "refer": [2, 4, 6, 10], "refresh": 10, "regex": [1, 3], "region": 6, "regist": [3, 10], "relat": 3, "remaind": 3, "rememb": 10, "remov": [1, 3, 10], "renew": 6, "repeat": 10, "repositori": [3, 10], "repres": 10, "request": [0, 1, 3, 5, 10], "request_json": 3, "requests_cert_fil": 10, "requir": [3, 5, 10], "resourc": [4, 5, 6, 10], "respons": [3, 10], "restart": 1, "result": [0, 2, 3, 4, 8, 9], "retriev": [1, 3, 6, 10], "retrieve_unscoped_token": 3, "return": 3, "rng": 0, "root": [1, 5], "run": [1, 5, 6, 8], "safe": 3, "safe_read_yaml_from_url": 3, "same": [3, 10], "sampl": 10, "savba": [9, 10], "save": [3, 10], "save_config": 3, "save_site_config": 3, "saved_config": 10, "sbgcloud": 10, "scan": 3, "scenario": [8, 10], "schema": 3, "scope": [1, 3, 5, 6, 10], "script": [0, 1, 4, 5, 6, 10], "scriptabl": 6, "search": [3, 4], "second": 10, "secret": [4, 5, 6], "section": 8, "see": [0, 1, 2, 5, 6, 10], "select": [3, 4, 5, 9], "self": 3, "serial": 3, "server": [1, 3, 6, 8, 10], "servic": [1, 2, 3, 6, 10], "service_typ": 3, "set": [1, 2, 3, 4, 6, 8, 10], "setup": 4, "sh": [0, 1, 5, 10], "share": 10, "sheet": 4, "shell": [1, 4, 6], "short": 10, "shorter": 1, "should": 5, "show": [1, 5, 8, 9, 10], "shown": 10, "side": 2, "signatur": 3, "simpl": 9, "simpler": 1, "simpli": 5, "simplifi": [3, 10], "singl": 2, "site": [0, 1, 2, 4, 5, 6, 8, 9], "site_dir": 10, "site_list_url": 10, "site_nam": [3, 6], "size": 3, "sk": [9, 10], "small": 10, "so": [3, 10], "some": [0, 5, 10], "sort": [3, 10], "sourc": [1, 2, 3, 10], "spec": [1, 10], "special": 6, "specif": [5, 10], "specifi": [3, 6, 10], "ssl": [0, 5], "stack": 10, "start": [0, 1, 4], "statu": [3, 8, 10], "step": 8, "still": 3, "store": [2, 3, 10], "str": 3, "streamlin": 6, "string": [2, 3], "strip": 3, "sub": 3, "success": [5, 10], "sudo": [1, 5], "suitabl": 10, "summar": 10, "support": [1, 3, 8, 10], "sure": 5, "switch": 10, "sy": 0, "synergi": [1, 8, 9, 10], "syntax": 6, "system": 5, "systemat": 1, "t": 3, "tab": 10, "tabl": 10, "target": 10, "task": 6, "tdviet": [0, 1, 5, 8, 10], "termin": 1, "test": [0, 3, 5, 10], "testvm": 1, "than": 0, "thei": [3, 10], "them": [0, 3, 5, 9, 10], "thi": [0, 3, 5, 6, 8, 9, 10], "though": 10, "three": [3, 6], "time": [1, 3, 10], "timeout": 3, "titl": 1, "token": [1, 2, 3, 4, 5, 6, 8], "token_for_mytoken": 10, "token_list_vo": 3, "tokenerror": 3, "tokenexcept": 3, "tool": [0, 2, 4], "train": [1, 8, 10], "transient": 10, "trigger": 3, "troubleshoot": 4, "true": [3, 9, 10], "trust": [0, 4], "try": [3, 10], "tupl": [2, 3], "turn": 10, "tutori": [1, 8], "twice": 10, "txt": 1, "type": [1, 3, 9, 10], "typic": 10, "u": 5, "ubuntu": [1, 10], "ui": [9, 10], "unifi": 6, "univ": 10, "unrespons": 1, "unscop": [3, 6], "unscoped_token": 3, "unsupport": 9, "url": [3, 6, 10], "us": [0, 3, 4, 6, 8], "usag": [4, 5, 6], "user": [1, 3, 5, 8, 9, 10], "user_data": 1, "user_id": 3, "userinfo": 3, "usr": 5, "utc": 10, "util": 6, "v": 1, "v1": 10, "v3": [9, 10], "v3oidcaccesstoken": [3, 10], "valid": [3, 6, 10], "valu": [1, 2, 3, 9, 10], "var": 9, "variabl": [1, 3, 4, 8], "variou": [3, 10], "vault": [3, 6, 10], "vault_command": 3, "vault_endpoint": 10, "vault_locker_mount_point": 10, "vault_mount_point": 10, "vault_rol": 10, "vault_salt": 10, "vaulttoken": 3, "vcpu": [1, 9, 10], "venv": 1, "verbos": 3, "verifi": 3, "verify_signatur": 3, "version": [1, 3, 5], "via": [3, 4, 6, 8, 10], "viet": 10, "virtual": [0, 1, 3, 5, 6, 8, 10], "virtual_env": 5, "virtualbox": [1, 8, 10], "vm": [0, 1, 6, 8, 10], "vo": [1, 2, 3, 5, 6, 8, 9, 10], "vo_access_egi_eu": 10, "vo_nam": 6, "wa": 5, "wai": [1, 10], "walkthrough": 6, "want": 9, "we": 3, "well": 3, "wget": [0, 1, 5], "when": [0, 5, 10], "whether": 10, "which": [0, 10], "wide": [5, 6], "without": [2, 3, 5, 9, 10], "work": [1, 2, 10], "wrap": 3, "write": 3, "xlarg": [9, 10], "yaml": [3, 10], "yamlerror": 3, "yet": 3, "yield": 3, "you": [0, 1, 5, 8, 10], "your": [1, 2, 3, 5, 8], "your_access_token": 2}, "titles": ["FAQ and Troubleshooting", "Cheat sheet", "Using FedCloud client in Python", "FedCloud client API references", "<no title>", "Installation", "Introduction", "fedcloudclient API references", "Quick start", "Using FedCloud client in scripts", "Usage"], "titleterms": {"addit": 10, "agent": 1, "anchor": 5, "api": [3, 7], "auth": 3, "authent": 10, "basic": [1, 8], "certif": 5, "cheat": 1, "checkin": 3, "cli": 3, "client": [2, 3, 5, 6, 9], "command": [1, 9, 10], "complet": 10, "conf": 3, "config": 10, "configur": 10, "consist": 10, "contain": [1, 5], "content": 4, "core": [5, 6], "docker": [1, 5], "egi": 5, "endpoint": [3, 10], "environ": [9, 10], "extern": 9, "faq": 0, "featur": 6, "fedcloud": [2, 3, 5, 6, 9, 10], "fedcloudcli": [3, 7], "filter": 1, "from": [1, 9], "get": 6, "help": 10, "inform": 1, "instal": [1, 5], "introduct": 6, "jq": 9, "json": 9, "kei": 6, "local": 1, "locker_auth": 3, "logger": 3, "map": 1, "modul": [3, 6], "more": 1, "name": 10, "note": 5, "oidc": 1, "openstack": [1, 3, 9, 10], "option": 10, "output": 9, "paramet": 10, "pip": 5, "pip3": 1, "prioriti": 10, "process": 9, "python": 2, "quick": 8, "refer": [3, 7], "resourc": 1, "result": 1, "script": 9, "search": 1, "secret": 10, "select": [1, 10], "set": 9, "setup": 8, "sheet": 1, "shell": 10, "site": [3, 10], "start": [6, 8], "token": 10, "tool": 9, "troubleshoot": 0, "trust": 5, "us": [1, 2, 5, 9, 10], "usag": [1, 8, 10], "variabl": [9, 10], "verifi": 5, "via": [1, 5, 9]}}) \ No newline at end of file diff --git a/docs/_build/html/usage.html b/docs/_build/html/usage.html new file mode 100644 index 0000000..cf57370 --- /dev/null +++ b/docs/_build/html/usage.html @@ -0,0 +1,717 @@ + + + + + + + + + Usage — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Usage

+

FedCloud client has the following main groups of commands:

+
    +
  • “fedcloud config” for handling fedcloudclient configuration,

  • +
  • “fedcloud token” for interactions with EGI Check-in and access tokens,

  • +
  • “fedcloud endpoint” for interactions with GOCDB (and site endpoints according to GOCDB),

  • +
  • “fedcloud site” for manipulations with site configurations,

  • +
  • “fedcloud openstack” or “fedcloud openstack-int” for performing OpenStack commands on sites,

  • +
  • “fedcloud secret” for accessing secrets in +Secret management service,

  • +
  • “fedcloud auth” is used to check the validity of access tokens and authentication credential,

  • +
+
+

Authentication Options

+

FedCloud commands require access tokens for authentication. Users have multiple options for providing these tokens:

+
    +
  • Direct access token: Use the --oidc-access-token option to provide an access token directly. You can retrieve this token from the environment variable FEDCLOUD_OIDC_ACCESS_TOKEN, or pass it explicitly, e.g.:

    +

    fedcloud token check --oidc-access-token <ACCESS_TOKEN>

    +
  • +
  • OIDC agent: Use the --oidc-agent-account option to integrate with oidc-agent. For example, check token validity with:

    +

    fedcloud token check --oidc-agent-account <NAME_OF_USER_FOR_OIDC_AGENT>

    +

    To use this method, follow the instructions at oidc-agent for EGI to register a client, then pass the client (account) name to the FedCloud client.

    +
  • +
  • Mytoken: Use the --mytoken option to authenticate with a token from the Mytoken service. To check token validity:

    +

    fedcloud token check --mytoken <TOKEN_FOR_MYTOKEN>

    +

    When creating a Mytoken, ensure you select “Allows obtaining OpenID Connect Access Tokens”. You may also use the --mytoken-server option to authenticate with a specific Mytoken server.

    +
  • +
+

Alternatively, you can obtain tokens using the EGI Check-in Token Portal, which provides all necessary information for EGI Check-in users.

+

In addition to command-line options, environment variables can be used for passing tokens, as summarized in the table below (not shown here).

+

By default, the protocol used is openid. This can be changed using the --os-protocol option. Note that some sites may have a fixed protocol defined in their site configuration (e.g., oidc for INFN-CLOUD-BARI).

+
+
+

Configuration

+

Display the current configuration of fedcloud with:

+
$ fedcloud config show
+
+
+

This will show a list of configuration parameters:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

Parameter

Default value

site

IISAS-FedCloud

vo

vo.access.egi.eu

site_list_url

https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml

site_dir

${HOME}/.config/fedcloud/site-config

oidc_url

https://aai.egi.eu/auth/realms/egi

gocdb_public_url

https://goc.egi.eu/gocdbpi/public/

gocdb_service_group

org.openstack.nova

vault_endpoint

https://vault.services.fedcloud.eu:8200

vault_role

vault_mount_point

/secrets/

vault_locker_mount_point

/v1/cubbyhole/

vault_salt

fedcloud_salt

log_file

${HOME}/.config/fedcloud/logs/fedcloud.log

log_level

DEBUG

log_config_file

${HOME}/.config/fedcloud/logging.conf

requests_cert_file

${HOME}/.config/fedcloud/cert/certs.pem

oidc_agent_account

egi

min_access_token_time

30

mytoken_server

https://mytoken.data.kit.edu

os_protocol

openid

os_auth_type

v3oidcaccesstoken

os_identity_provider

egi.eu

_MIN_ACCESS_TOKEN_TIME

30

+

The FedCloud client supports multiple types of configuration:

+
    +
  • Default settings – accessible using ``DEFAULT_SETTINGS``, these are the built-in default values.

  • +
  • Local environment settings – custom configuration values defined in the environment and loaded via ``env_config``.

  • +
  • Saved configuration settings – user-defined settings stored in a JSON file, accessible via ``saved_config``.

  • +
+

For example, to print the environment configuration, use the following command:

+
$ fedcloud config show --source env_config
+
+
+

This command shows, for instance, the following output:

+ + + + + + + + + + + + + + +

Parameter

Default value

oidc_agent_account

<NAME_OF_USER>

+

The fedcloud configuration can be saved to a file using the following command

+
+
::

$ fedcloud config create

+
+
+

By default, the configuration file is saved to ${HOME}/.config/fedcloud/config.yaml, +but this location can be changed using the --config option. For example:

+
$ fedcloud config create --config-file /path/to/file.yaml
+
+
+
+

Using Environment Variables and Configuration Priorities

+

It is also possible to use the FEDCLOUD_CONFIG_FILE environment variable instead of the --config option in the command line. +This allows users to manage and switch between multiple configuration files—one per project—each with its own settings.

+

The fedcloud client supports configuration from multiple sources, in the following order of priority (highest to lowest):

+
    +
  1. Command-line options – override all other settings. +Example: --site IISAS-FedCloud

  2. +
  3. Environment variables – must begin with the prefix FEDCLOUD_. +Example: FEDCLOUD_SITE=IISAS-FedCloud

  4. +
  5. Configuration file – typically stored as config.yaml. +Example: the site setting in config.yaml

  6. +
  7. Default configuration – hardcoded defaults (lowest priority). +See the source code for details.

  8. +
+

The priority order is important: +default values are overridden by the configuration file, +which is overridden by environment variables, +which are in turn overridden by command-line options.

+

For example, the default configuration includes:

+
    +
  • site = IISAS-FedCloud

  • +
  • vo = vo.access.egi.eu

  • +
+

These values can be changed using any of the higher-priority methods. For example:

+
$ fedcloud openstack --vo training.egi.eu --site IFCA-LCG2 server list
+
+
+

or

+
$ export FEDCLOUD_VO=training.egi.eu
+$ export FEDCLOUD_SITE=IFCA-LCG2
+$ fedcloud openstack server list
+
+
+
+
+

Consistent Parameter Naming

+

Note the consistent naming convention for configuration parameters across different sources. For example, the same parameter is represented as:

+
    +
  • --access_token in the command-line

  • +
  • FEDCLOUD_ACCESS_TOKEN as an environment variable

  • +
+

All configuration parameters follow this consistent mapping across command-line options, environment variables, and configuration files.

+
+
+

Additional Configurable Parameters

+

In addition to oidc_agent_account, the following parameters can also be configured in the same way:

+
    +
  • site – the OpenStack site to target

  • +
  • vo – the Virtual Organisation (VO)

  • +
  • check_in_url – the EGI Check-in OIDC endpoint

  • +
  • client_id – the OIDC client ID

  • +
  • scopes – requested OIDC scopes

  • +
  • access_token – manually provided access token

  • +
  • output_format – format of output, e.g., table, json, or yaml

  • +
+

These parameters can be specified via:

+
    +
  • command-line options (e.g., --site, --vo),

  • +
  • environment variables (e.g., FEDCLOUD_SITE, FEDCLOUD_VO), or

  • +
  • configuration files (e.g., site: IISAS-FedCloud in config.yaml).

  • +
+

This design allows flexible and convenient configuration for various usage scenarios.

+ + + + + + + + + + + + + + + + + + + + + +

Environment variable

Command-line option

Configuration parameters

FEDCLOUD_OIDC_ACCESS_TOKEN

–oidc-access-token

Not implemented

Not implemented

–mytoken

mytoken

FEDCLOUD_OIDC_AGENT_ACCOUNT

–oidc-agent-account

oidc_agent_account

+

For convenience, it is recommended to set transient parameters—such as access tokens—via environment variables. +This simplifies the usage of fedcloud commands by avoiding the need to specify these parameters on the command line each time.

+
+
+
+

Shell completion

+

Shell completion for fedcloud command in bash can be activated by executing the following command:

+
$ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)"
+
+
+

The command above may affect responsiveness of the shell. For long work, it is recommended to copy the +fedcloud_bash_completion.sh script to a local file, and +source it from ~/.bashrc. Refer Click documentation for a long explanation.

+

After enabling shell completion, press <TAB> twice for shell completion:

+
$ fedcloud site <TAB><TAB>
+env              list             save-config      show             show-project-id
+
+
+
+
+

fedcloud –help command

+
    +
  • “fedcloud –help” command will print help message. When using it in combination with other +commands, e.g. “fedcloud token –help”, “fedcloud token check –hep”, it will print list of options for the +corresponding commands

  • +
+
$ fedcloud --help
+Usage: fedcloud [OPTIONS] COMMAND [ARGS]...
+
+Options:
+  --help  Show this message and exit.
+
+Commands:
+  config         Managing fedcloud configurations
+  endpoint       Obtain endpoint details and scoped tokens
+  openstack      Execute OpenStack commands on site and VO
+  openstack-int  Interactive OpenStack client on site and VO
+  secret         Commands for accessing secret objects
+  select         Select resources according to specification
+  site           Obtain site configurations
+  token          Get details of access token
+
+
+
+
+

fedcloud token commands

+
    +
  • ``fedcloud token check`` – Checks the expiration time of the configured access token, +allowing users to determine whether it needs to be refreshed.

  • +
+

As mentioned earlier, the access token can be provided via the environment variable FEDCLOUD_OIDC_ACCESS_TOKEN. +For this reason, the --oidc-access-token option is not shown in all examples below, even though it may be required if the token is not set via environment variables.

+
$ fedcloud token check
+
+
+

Output is shown as:

+
Access token is valid to 2021-01-02 01:25:39 UTC
+Access token expires in 3571 seconds
+
+
+
    +
  • “fedcloud token list-vos” : Print the list of VO memberships according to EGI Check-in

  • +
+
$ fedcloud token list-vos
+
+
+

Sample output:

+
eosc-synergy.eu
+fedcloud.egi.eu
+training.egi.eu
+
+
+
    +
  • “fedcloud token issue” : Print the access_token

  • +
+
$ fedcloud token issue
+
+
+

Sample output:

+
egwergwregrwegreg...
+
+
+
+
+

fedcloud endpoint commands

+

“fedcloud endpoint” commands are complementary part of the “fedcloud site” commands. Instead of using site +configurations defined in files saved in GitHub repository or local disk, the commands try to get site information +directly from GOCDB (Grid Operations Configuration Management Database) https://goc.egi.eu/ or make probe test on sites

+
    +
  • “fedcloud endpoint list” : List of endpoints of sites defined in GOCDB.

  • +
+
$ fedcloud endpoint list
+
+
+

Sample output:

+
+
::

Site type URL +—————— —————— ———————————————— +IFCA-LCG2 org.openstack.nova https://api.cloud.ifca.es:5000/v3/ +IN2P3-IRES org.openstack.nova https://sbgcloud.in2p3.fr:5000/v3 +…

+
+
+
    +
  • “fedcloud endpoint projects –site <SITE> –oidc-access-token <ACCESS_TOKEN>” : List of projects to which the owner +of the access token has access at the given site

  • +
+
$ fedcloud endpoint projects --site IFCA-LCG2
+id                                Name                        enabled    site
+--------------------------------  --------------------------  ---------  ---------
+2a7e2cd4b6dc4e609dd934964c1715c6  VO:demo.fedcloud.egi.eu     True       IFCA-LCG2
+3b9754ad8c6046b4aec43ec21abe7d8c  VO:eosc-synergy.eu          True       IFCA-LCG2
+...
+
+
+

If the site is set to ALL_SITES, or the argument -a is used, the command will show accessible projects from all sites of the EGI Federated Cloud.

+
    +
  • +
    “fedcloud endpoint vos –site <SITE> –oidc-access-token <ACCESS_TOKEN>”List of Virtual Organisations (VOs)

    to which the owner of the access token has access at the given site

    +
    +
    +
  • +
+
$ fedcloud endpoint vos --site IFCA-LCG2
+VO                id                                Project name         enabled    site
+----------------  --------------------------------  -------------------  ---------  ---------
+vo.access.egi.eu  233f045cb1ff46842a15ebb33af69460  VO:vo.access.egi.eu  True       IFCA-LCG2
+training.egi.eu   d340308880134d04294097524eace710  VO:training.egi.eu   True       IFCA-LCG2
+...
+
+
+

If the site is set to ALL_SITES, or the argument -a is used, the command will show accessible VOs from all sites of the EGI Federated Cloud.

+
$ fedcloud endpoint vos -a
+VO                   id                                Project name         enabled    site
+-------------------  --------------------------------  -------------------  ---------  -----------------
+vo.access.egi.eu     233f045cb1ff46842a15ebb33af69460  VO:vo.access.egi.eu  True       IFCA-LCG2
+training.egi.eu      d340308880134d04294097524eace710  VO:training.egi.eu   True       IFCA-LCG2
+vo.access.egi.eu     7101022b9ae74ed9ac1a574497279499  EGI_access           True       IN2P3-IRES
+vo.access.egi.eu     5bbdb5c1e0b2bcbac29904f4ac22dcaa  vo_access_egi_eu     True       UNIV-LILLE
+vo.access.egi.eu     4cab325ca8c2495bf2d4e8f230bcd51a  VO:vo.access.egi.eu  True       INFN-PADOVA-STACK
+...
+
+
+
    +
  • “fedcloud endpoint token –site <SITE> –project-id <PROJECT> –oidc-access-token <ACCESS_TOKEN>” : Get +OpenStack keystone scoped token on the site for the project ID.

  • +
+
$ fedcloud endpoint token --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c
+export FEDCLOUD_OS_TOKEN="eayeghjtjtj..."
+
+
+
    +
  • “fedcloud endpoint env –site <SITE> –project-id <PROJECT> –oidc-access-token <ACCESS_TOKEN>” : Print +environment variables for working with the project ID on the site.

  • +
+
$ fedcloud endpoint env --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c
+# environment for IFCA-LCG2
+export FEDCLOUD_OS_AUTH_URL="https://api.cloud.ifca.es:5000/v3/"
+export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken"
+export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu"
+export FEDCLOUD_OS_PROTOCOL="openid"
+export FEDCLOUD_OS_ACCESS_TOKEN="..."
+
+
+
+
+

fedcloud site commands

+

“fedcloud site” commands will read site configurations and manipulate with them. If the local site configurations +exist at ~/.config/fedcloud/site-config/, fedcloud will read them from there, otherwise the commands will read +from GitHub repository.

+

By default, fedcloud does not save anything on local disk, users have to save the site configuration to local disk +explicitly via “fedcloud site save-config” command. The advantage of having local +site configurations, beside faster loading, is to give users ability to make customizations, e.g. add additional VOs, +remove sites they do not have access, and so on.

+
    +
  • “fedcloud site save-config” : Read the default site configurations from GitHub +and save them to ~/.config/fedcloud/site-config/ local directory. The command will overwrite existing site configurations +in the local directory.

  • +
+
$ fedcloud site save-config
+Saving site configs to directory /home/viet/.config/fedcloud/site-config/
+
+
+

After saving site configurations, users can edit and customize them, e.g. remove inaccessible sites, add new +VOs and so on.

+
    +
  • “fedcloud site list” : List of existing sites in the site configurations

  • +
+
$ fedcloud site list
+100IT
+BIFI
+CESGA
+...
+
+
+
    +
  • “fedcloud site list –vo <VO-name>” : List all sites supporting a Virtual Organization

  • +
+
$ fedcloud site vo-list --vo vo.access.egi.eu
+BIFI
+CENI
+CESGA-CLOUD
+...
+
+
+
    +
  • “fedcloud site show –site <SITE>” : Show configuration of the corresponding site.

  • +
+
$ fedcloud site show --site IISAS-FedCloud
+endpoint: https://cloud.ui.savba.sk:5000/v3/
+gocdb: IISAS-FedCloud
+vos:
+- auth:
+    project_id: a22bbffb007745b2934bf308b0a4d186
+  name: covid19.eosc-synergy.eu
+- auth:
+    project_id: 51f736d36ce34b9ebdf196cfcabd24ee
+  name: eosc-synergy.eu
+
+
+
    +
  • “fedcloud site show-project-id –site <SITE> –vo <VO>”: show the project ID of the VO on the site.

  • +
+
$ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu
+export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/"
+export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee"
+
+
+
    +
  • “fedcloud site env –site <SITE> –vo <VO>”: set OpenStack environment variable for the VO on the site.

  • +
+
$ fedcloud site env --site IISAS-FedCloud --vo eosc-synergy.eu
+export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/"
+export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken"
+export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu"
+export FEDCLOUD_OS_PROTOCOL="openid"
+export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee"
+# Remember to set OS_ACCESS_TOKEN, e.g. :
+# export FEDCLOUD_OS_ACCESS_TOKEN=`oidc-token egi`
+
+
+

The main differences between “fedcloud endpoint env” and “fedcloud site env” commands are that the second command +needs VO name as input parameter instead of project ID. The command may set also environment variable OS_ACCESS_TOKEN, +if access token is provided, otherwise it will print notification.

+
+
+

fedcloud select commands

+
    +
  • “fedcloud select flavor –site <SITE> –vo <VO> –oidc-access-token <ACCESS_TOKEN> –flavor-specs <flavor-specs>” : +Select flavor according to the specification in flavor-specs. The specifications may be repeated, +e.g. –flavor-specs “VCPUs==2” –flavor-specs “RAM>=2048”, or may be joined, e.g. +–flavor-specs “VCPUs==2 & Disk>10”. For frequently used specs, short-option alternatives are available, e.g. +–vcpus 2 is equivalent to –flavor-specs “VCPUs==2”. The output is sorted, flavors using less resources +(in the order: GPUs, CPUs, RAM, Disk) are placed on the first places. Users can choose to print only the best-matched +flavor with –output-format first (suitable for scripting) or the full list of all matched flavors in list/YAML/JSON +format.

  • +
+
$ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --flavor-specs "RAM>=2096" --flavor-specs "Disk > 10" --output-format list
+m1.medium
+m1.large
+m1.xlarge
+m1.huge
+g1.c08r30-K20m
+g1.c16r60-2xK20m
+
+
+
    +
  • “fedcloud select image –site <SITE> –vo <VO> –oidc-access-token <ACCESS_TOKEN> –image-specs <image-specs>” : +Select image according to the specification in image-specs. The specifications may be repeated, +e.g. –image-specs “Name=~Ubuntu” –image-specs “Name=~’20.04’”. The output is sorted, newest images +are placed on the first places. Users can choose to print only the best-matched +image with –output-format first (suitable for scripting) or the full list of all matched images in list/YAML/JSON +format.

  • +
+
$ fedcloud select image --site INFN-CATANIA-STACK --vo training.egi.eu --image-specs "Name =~ Ubuntu" --output-format list
+TRAINING.EGI.EU Image for EGI Docker [Ubuntu/18.04/VirtualBox]
+TRAINING.EGI.EU Image for EGI Ubuntu 20.04 [Ubuntu/20.04/VirtualBox]
+
+
+
    +
  • “fedcloud select network –site <SITE> –vo <VO> –oidc-access-token <ACCESS_TOKEN> –network-specs <flavor-specs>” : +Select network according to the specification in network-specs. User can choose to select only public or private +network, or both (default). The output is sorted in the order: public, shared, +private. Users can choose to print only the best-matched network with –output-format first +(suitable for scripting) or the full list of all matched networks in list/YAML/JSON format.

  • +
+
$ fedcloud select network --site IISAS-FedCloud --vo training.egi.eu --network-specs default --output-format list
+public-network
+private-network
+
+
+
+
+

fedcloud openstack commands

+
    +
  • “fedcloud openstack –site <SITE> –vo <VO> –oidc-access-token <ACCESS_TOKEN> <OPENSTACK_COMMAND>” : Execute an +OpenStack command on the site and VO. Examples of OpenStack commands are “image list”, “server list” and can be used +with additional options for the commands, e.g. “image list –long”, “server list –format json”. The list of all +OpenStack commands, and their parameters/usages are available +here.

  • +
+
$ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu
+Site: IISAS-FedCloud, VO: eosc-synergy.eu
++--------------------------------------+-------------------------------------------------+--------+
+| ID                                   | Name                                            | Status |
++--------------------------------------+-------------------------------------------------+--------+
+| 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox]    | active |
+...
+
+
+

If the site is ALL_SITES, the OpenStack command will be executed on all sites in EGI Federated Cloud.

+
    +
  • “fedcloud openstack-int –site <SITE> –vo <VO> –oidc-access-token <ACCESS_TOKEN>” : Call OpenStack client without +command, so users can work with OpenStack site in interactive mode. This is useful when users need to perform multiple +commands successively. For example, users may need get list of images, list of flavors, list of networks before +creating a VM. OIDC authentication is done only once at the beginning, then the keystone token is cached and will +be used for successive commands without authentication via CheckIn again.

  • +
+
$ fedcloud openstack-int --site IISAS-FedCloud --vo eosc-synergy.eu
+(openstack) image list
++--------------------------------------+-------------------------------------------------+--------+
+| ID                                   | Name                                            | Status |
++--------------------------------------+-------------------------------------------------+--------+
+| 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox]    | active |
+...
+(openstack) flavor list
++--------------------------------------+-----------+-------+------+-----------+-------+-----------+
+| ID                                   | Name      |   RAM | Disk | Ephemeral | VCPUs | Is Public |
++--------------------------------------+-----------+-------+------+-----------+-------+-----------+
+| 5bd8397c-b97f-462d-9d2b-5b533844996c | m1.small  |  2048 |   10 |         0 |     1 | True      |
+| df25f80f-ed19-4e0b-805e-d34620ba0334 | m1.medium |  4096 |   40 |         0 |     2 | True      |
+...
+(openstack)
+
+
+
+
+

fedcloud config commands

+
    +
  • “fedcloud config –config-file create” : Create default configuration file in default location for configuration file

  • +
+
+
+

fedcloud secret commands

+

The “fedcloud secret” commands are described in details in the documentation of the +Secret management service.

+
+
+ + +
+
+ +
+
+
+
+ + + + \ No newline at end of file diff --git a/docs/_static/css/custom.css b/docs/_static/css/custom.css new file mode 100644 index 0000000..380f1f2 --- /dev/null +++ b/docs/_static/css/custom.css @@ -0,0 +1,23 @@ +/* 1) Classes stay blue */ +dt.sig.sig-class { + background-color: #e8f0fe !important; +} +dt.sig.sig-class .sig-name { + color: #000 !important; +} + +/* 2) “Pure” functions (module-level) go gray */ +dl.py.function dt.sig { + background-color: #f5f5f5 !important; +} +dl.py.function dt.sig .sig-name { + color: #000 !important; +} + +/* 3) Methods (inside classes) also go gray */ +dl.py.method dt.sig { + background-color: #f5f5f5 !important; +} +dl.py.method dt.sig .sig-name { + color: #000 !important; +} \ No newline at end of file diff --git a/docs/cheat.rst b/docs/cheat.rst index c62d2ec..155d40b 100644 --- a/docs/cheat.rst +++ b/docs/cheat.rst @@ -77,12 +77,18 @@ Basic usages $ fedcloud token list-vos -* List sites in EGI Federated Cloud: +* List sites in the EGI Federated Cloud: :: $ fedcloud site list +* List all sites supporting a Virtual Organization in the EGI Federated Cloud: + +:: + + $ fedcloud site list --vo vo.access.egi.eu + * Execute an OpenStack command: :: @@ -225,6 +231,27 @@ Useful commands $ source fedcloud_bash_completion.sh +* Pass a *mytoken* to Virtual Machines in the EGI Federated Cloud + +:: + + # Create the file "user.txt" with + $ cat user.txt + FEDCLOUD_MYTOKEN= # created on https://mytoken.data.kit.edu/ + + # Pass it to OpenStack + FEDCLOUD_SITE=IISAS-FedCloud + FEDCLOUD_VO=vo.access.egi.eu + fedcloud openstack server create --flavor --image --user-data user.txt --key-name testvm + + # Once you log into the VM you can retrieve the "mytoken" with + curl http://169.254.169.254/openstack/latest/user_data/ + + # and use it with + FEDCLOUD_MYTOKEN= # copied from the previous curl command + fedcloud token check + + More information **************** diff --git a/docs/conf.py b/docs/conf.py index 6beb2e9..789cc7d 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,73 +1,49 @@ # Configuration file for the Sphinx documentation builder. # -# This file only contains a selection of the most common options. For a full -# list see the documentation: +# For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html -# -- Path setup -------------------------------------------------------------- - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information import os import sys -from typing import List +# Adjust the path so that your top-level package folder is first +sys.path.insert(0, os.path.abspath("..")) -sys.path.insert(0, os.path.abspath("../")) -# -- Project information ----------------------------------------------------- - -project = "fedcloudclient" -copyright = "2022, Viet Tran" -author = "Viet Tran" - -# The full version, including alpha/beta/rc tags -# release = '0.0.2-dev15' +project = 'fedcloudclient' +copyright = '2025, jaro221' +author = 'jaro221' # -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ - "sphinx.ext.todo", - "sphinx.ext.viewcode", - "sphinx.ext.autodoc", + #'sphinx.ext.autodoc', # for automodule, autoclass, etc. + 'sphinx.ext.napoleon', # if you use Google/NumPy-style docstrings + #'sphinx.ext.viewcode', # optional: add links to highlighted source + # any other extensions you need… ] -smartquotes = False - -source_suffix = [".rst", ".md"] +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# Add any paths that contain templates here, relative to this directory. -templates_path = ["_templates"] - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This pattern also affects html_static_path and html_extra_path. -exclude_patterns: List[str] = [] +root_doc = 'index' # -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_context = { - "display_github": True, - "github_user": "tdviet", - "github_repo": "fedcloudclient", - "github_version": "master/docs/", -} - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = "sphinx_rtd_theme" +html_theme = 'sphinx_rtd_theme' +html_static_path = ['_static'] html_logo = "fedcloudclient-logo-non-transparent-small.png" + +# Add custom CSS (optional) +html_css_files = [ + "css/custom.css", +] +# Make sure sidebar and index content renders html_theme_options = { - "logo_only": True, - "display_version": False, + 'logo_only': False, # Show logo and project title + 'navigation_depth': 2, # Show deeper headings } -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ["_static"] diff --git a/docs/fedcloudclient.rst b/docs/fedcloudclient.rst index 0c9b798..4429eea 100644 --- a/docs/fedcloudclient.rst +++ b/docs/fedcloudclient.rst @@ -1,6 +1,7 @@ FedCloud client API references ============================== + fedcloudclient.checkin module ----------------------------- @@ -9,6 +10,22 @@ fedcloudclient.checkin module :undoc-members: :show-inheritance: +fedcloudclient.auth module +-------------------------- + +.. automodule:: fedcloudclient.auth + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.conf module +-------------------------- + +.. automodule:: fedcloudclient.conf + :members: + :undoc-members: + :show-inheritance: + fedcloudclient.endpoint module ------------------------------ @@ -33,6 +50,24 @@ fedcloudclient.openstack module :undoc-members: :show-inheritance: +fedcloudclient.logger module +---------------------------- + +.. automodule:: fedcloudclient.logger + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.locker_auth module +--------------------------------- + +.. automodule:: fedcloudclient.locker_auth + :members: + :undoc-members: + :show-inheritance: + + + fedcloudclient.cli module ------------------------- diff --git a/docs/index.rst b/docs/index.rst index 620e4ed..44d4418 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,23 +1,3 @@ -.. fedcloudclient documentation master file, created by - sphinx-quickstart on Sun Dec 27 22:25:01 2020. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. - -.. raw:: html - - - -Welcome to FedCloud client's documentation! -=========================================== - -.. image:: https://zenodo.org/badge/336671726.svg - :target: https://zenodo.org/badge/latestdoi/336671726 - .. toctree:: :maxdepth: 2 :caption: Contents: @@ -28,15 +8,6 @@ Welcome to FedCloud client's documentation! usage development scripts - fedcloudclient + modules FAQ - cheat - - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` + cheat diff --git a/docs/install.rst b/docs/install.rst index 8644e6d..55033d2 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -10,17 +10,51 @@ Simply use the following **pip3** command (should be done without root privilege $ pip3 install -U fedcloudclient -That will install latest version **FedCloud client** package together with its required packages -(like **openstackclient**). It will also create executable files **"fedcloud"** and **"openstack"** and add them to -corresponding directory according to your Python execution environment (*$VIRTUAL_ENV/bin* for executing *pip3* in -Python virtual environment, *~/.local/bin* for executing *pip3* as user (with *--user* option), and */usr/local/bin* -when executing *pip3* as root). Make sure to add *~/.local/bin* to $PATH if installing as user. +Installation Notes +------------------ -Check if the installation is correct by executing the client +Installing the latest version of the **FedCloud client** package using ``pip3`` will also install all required dependencies (such as **openstackclient**). It will create the executable files **``fedcloud``** and **``openstack``**, placing them in the appropriate directory based on your Python environment: + +* ``$VIRTUAL_ENV/bin`` – when using ``pip3`` inside a Python virtual environment, +* ``~/.local/bin`` – when installing with ``pip3 --user``, +* ``/usr/local/bin`` – when installing system-wide with ``pip3`` as root. + +.. note:: + + If you install the package with the ``--user`` option, make sure that ``~/.local/bin`` is included in your ``$PATH``. + +Verifying the Installation +-------------------------- + +To check if the installation was successful, run the following command:: + +:: + + $ fedcloud --help + + +This will show output:: :: + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + CLI main function. Intentionally empty + + Options: + --version Show the version and exit. + --help Show this message and exit. + + Commands: + ec3 EC3 cluster provisioning + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token + - $ fedcloud Installing EGI Core Trust Anchor certificates ********************************************* diff --git a/docs/intro.rst b/docs/intro.rst index 18bbe59..2b7301f 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -4,49 +4,78 @@ Introduction .. image:: https://zenodo.org/badge/336671726.svg :target: https://zenodo.org/badge/latestdoi/336671726 -The `FedCloud client `_ is a high-level Python package for a command-line client -designed for interaction with the OpenStack services in the EGI infrastructure. The client can access various EGI -services and can perform many tasks for users including managing access tokens, listing services, and mainly execute -commands on OpenStack sites in EGI infrastructure. +FedCloud Client +=============== -The most notable features of FedCloud client are following: +The **FedCloud Client** (`https://fedcloudclient.fedcloud.eu/`) is a high-level Python package providing a unified command-line interface to the EGI Federated Cloud’s OpenStack services. It streamlines tasks such as managing access tokens, discovering services, and executing OpenStack commands across multiple sites. -* **Rich functionalities:** have wide ranges of useful commands, including checking access token, searching for - services, listing sites and VOs, and interaction with OpenStack sites. +Key Features +------------ -* **Simple usages:** can perform any OpenStack command on any sites with only three parameters: the site, the VO - and the command. For example, to list virtual machines (VM) images available to members of ``vo.access.egi.eu`` VO - on ``IISAS-FedCloud`` site, run the following command: +- **Comprehensive Command Set** + Access a broad range of operations, including: + + - Token management (retrieve, inspect, renew) + - Service discovery and endpoint lookup + - Listing and filtering sites, virtual organizations (VOs), and resources + - Direct interaction with OpenStack services (compute, image, network, etc.) -:: +- **Minimal Syntax, Maximum Power** + Execute any OpenStack action on any site with just three parameters: + + .. code-block:: console - $ fedcloud openstack image list --vo vo.access.egi.eu --site IISAS-FedCloud + $ fedcloud openstack --vo --site -* **Federation-wide:** Single client for all OpenStack sites and related services of EGI Cloud infrastructure. - Single command may perform an action on all sites by specifying :code:`--site ALL_SITES`. + For example, to list all available VM images for the VO ``vo.access.egi.eu`` on the ``IISAS-FedCloud`` site: -* **Programmable:** the client is designed for using in - `scripts for automation `_ - or as a `Python library `_ - for programming FedCloud services. + .. code-block:: console -Six modules are included: + $ fedcloud openstack image list \ + --vo vo.access.egi.eu \ + --site IISAS-FedCloud -* **fedcloudclient.checkin** for operation with EGI Check-in like getting tokens, +- **Federation-Wide Scope** + One client, all sites. Use the special ``ALL_SITES`` identifier to run a command across every configured site in the federation: -* **fedcloudclient.endpoint** for searching endpoints via GOCDB, getting unscoped/scoped token from - OpenStack keystone, + .. code-block:: console -* **fedcloudclient.sites** for managing site configurations, + $ fedcloud openstack server list --vo vo.access.egi.eu --site ALL_SITES -* **fedcloudclient.openstack** for performing OpenStack operations on sites, +- **Scriptable & Extensible** + Designed for automation and integration: + + - **Shell scripting**: embed ``fedcloud`` calls in CI/CD pipelines or custom scripts. See :doc:`scripts `. + - **Python library**: import and invoke client functionality programmatically. See :doc:`developer guide `. -* **fedcloudclient.secret** for accessing secrets in - `Secret management service `_, +Core Modules +------------ -* **fedcloudclient.ec3** for deploying elastic computing clusters in Cloud. +.. list-table:: + :header-rows: 1 -A short tutorial of the fedcloudclient is available in `this -presentation `_. -The full documentation, including installation, usage and API description is available -at https://fedcloudclient.fedcloud.eu/. + * - Module + - Purpose + * - ``fedcloudclient.conf`` + - Manage and validate client configuration (profiles, defaults). + * - ``fedcloudclient.checkin`` + - Interact with EGI Check-in: fetch and renew OpenID Connect tokens. + * - ``fedcloudclient.endpoint`` + - Discover service endpoints via GOCDB; obtain unscoped and scoped tokens from Keystone. + * - ``fedcloudclient.sites`` + - Configure and query site metadata (regions, URLs, capabilities). + * - ``fedcloudclient.openstack`` + - Execute OpenStack CLI commands (``nova``, ``glance``, ``neutron``) against specified sites. + * - ``fedcloudclient.secret`` + - Retrieve and manage secrets from the EGI Vault service. + * - ``fedcloudclient.auth`` + - Validate access tokens and credentials; utilities for checking token expiration and scopes. + +Getting Started +--------------- + +A concise introductory walkthrough is available in this `presentation `_. + +For detailed installation instructions, full usage examples, and API references, see the official documentation: + +`https://fedcloudclient.fedcloud.eu/ `_ diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..32bb245 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules.rst b/docs/modules.rst index f7439b4..ab786e8 100644 --- a/docs/modules.rst +++ b/docs/modules.rst @@ -2,6 +2,6 @@ fedcloudclient API references ============================= .. toctree:: - :maxdepth: 4 + :maxdepth: 1 - fedcloudclient + fedcloudclient/ diff --git a/docs/quickstart.rst b/docs/quickstart.rst index ff71f84..2f08113 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -26,11 +26,9 @@ or use Docker container: or `oidc-agent `_ and set environment variable. - Note: Do not supply refresh tokens as access tokens. - :: - $ export OIDC_ACCESS_TOKEN= + $ export FEDCLOUD_OIDC_ACCESS_TOKEN= Basic usages ************ @@ -45,7 +43,7 @@ Basic usages training.egi.eu ... -* List sites in EGI Federated Cloud +* List sites in the EGI Federated Cloud :: @@ -55,6 +53,16 @@ Basic usages CESGA ... +* List sites supporting a Virtual Organization in the EGI Federated Cloud + +:: + + $ fedcloud site list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + * Execute an OpenStack command, e.g. list images in eosc-synergy.eu VO on IISAS-FedCloud site (or other combination of site and VO you have access): @@ -109,4 +117,4 @@ Basic usages token Token command group for manipulation with tokens * Read the `Tutorial `_ - presentation or next sections for more information. \ No newline at end of file + presentation or next sections for more information. diff --git a/docs/usage.rst b/docs/usage.rst index 62fe88b..ac422bd 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1,7 +1,9 @@ Usage ===== -**FedCloud client** has six main groups of commands: +**FedCloud client** has the following main groups of commands: + +* **"fedcloud config"** for handling fedcloudclient configuration, * **"fedcloud token"** for interactions with EGI Check-in and access tokens, @@ -11,71 +13,229 @@ Usage * **"fedcloud openstack"** or **"fedcloud openstack-int"** for performing OpenStack commands on sites, -* **fedcloud secret** for accessing secrets in +* **"fedcloud secret"** for accessing secrets in `Secret management service `_, -* **"fedcloud ec3"** as helper commands for deploying EC3. - - -Authentication -************** - -Many **fedcloud** commands need access tokens for authentication. Users can choose whether to provide access tokens -directly (via option *"--oidc-access-token"*), or via oidc-agent (via option *"--oidc-agent-account"*), use refresh -tokens (must be provided together with Check-in client ID and secret) to generate access tokens on the fly. Therefore, -in most cases, the option *"--oidc-access-token"* can be replaced by the option *"--oidc-agent-account"*, or the -combination of *"--oidc-refresh-token"*, *"--oidc-client-id"* and *"--oidc-client-secret"*. - -Users of EGI Check-in can get all information needed for obtaining refresh and access tokens from `EGI Check-in Token -Portal `_. For providing access token via *oidc-agent*, follow the instructions from -`oidc-agent `_ for registering a client, then -give the client name (account name in *oidc-agent*) to *FedCloud client* via option *"--oidc-agent-account"*. - -Refresh tokens have long lifetime (one year in EGI Check-in), so they must be properly protected. Exposing refresh -tokens via environment variables or command-line options is considered as insecure and will be disable in near -future in favor of using *oidc-agent*. If multiple methods of getting access tokens are given at the same time, -the client will try to get the tokens from the oidc-agent first, then from refresh tokens. - -The default OIDC identity provider is EGI Check-in (https://aai.egi.eu/auth/realms/egi). Users can set other OIDC identity -provider via option *"--oidc-url"*. Remember to set identity provider's name *"--openstack-auth-provider"* accordingly -for OpenStack commands. - -The default protocol is *"openid"*. Users can change default protocol via option *"--openstack-auth-protocol"*. However, -sites may have protocol fixedly defined in site configuration, e.g. *"oidc"* for INFN-CLOUD-BARI. - -Environment variables -********************* - -Most of fedcloud options, including options for tokens can be set via environment variables: - -+-----------------------------+---------------------------------+------------------------------------+ -| Environment variables | Command-line options | Default value | -+=============================+=================================+====================================+ -| OIDC_AGENT_ACCOUNT | --oidc-agent-account | | -+-----------------------------+---------------------------------+------------------------------------+ -| OIDC_ACCESS_TOKEN | --oidc-access-token | | -+-----------------------------+---------------------------------+------------------------------------+ -| OIDC_REFRESH_TOKEN | --oidc-refresh-token | | -+-----------------------------+---------------------------------+------------------------------------+ -| OIDC_CLIENT_ID | --oidc-client-id | | -+-----------------------------+---------------------------------+------------------------------------+ -| OIDC_CLIENT_SECRET | --oidc-client-secret | | -+-----------------------------+---------------------------------+------------------------------------+ -| OIDC_URL | --oidc-url | https://aai.egi.eu/auth/realms/egi | -+-----------------------------+---------------------------------+------------------------------------+ -| OPENSTACK_AUTH_PROTOCOL | --openstack-auth-protocol | openid | -+-----------------------------+---------------------------------+------------------------------------+ -| OPENSTACK_AUTH_PROVIDER | --openstack-auth-provider | egi.eu | -+-----------------------------+---------------------------------+------------------------------------+ -| OPENSTACK_AUTH_TYPE | --openstack-auth-type | v3oidcaccesstoken | -+-----------------------------+---------------------------------+------------------------------------+ -| EGI_SITE | --site | | -+-----------------------------+---------------------------------+------------------------------------+ -| EGI_VO | --vo | | -+-----------------------------+---------------------------------+------------------------------------+ - -For convenience, always set the frequently used options like tokens via environment variables, that can save a lot of -time. +* **"fedcloud auth"** is used to check the validity of access tokens and authentication credential, + + + +Authentication Options +********************** + +**FedCloud** commands require access tokens for authentication. Users have multiple options for providing these tokens: + +- **Direct access token**: Use the ``--oidc-access-token`` option to provide an access token directly. You can retrieve this token from the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``, or pass it explicitly, e.g.: + + ``fedcloud token check --oidc-access-token `` + +- **OIDC agent**: Use the ``--oidc-agent-account`` option to integrate with `oidc-agent `_. For example, check token validity with: + + ``fedcloud token check --oidc-agent-account `` + + To use this method, follow the instructions at `oidc-agent for EGI `_ to register a client, then pass the client (account) name to the FedCloud client. + +- **Mytoken**: Use the ``--mytoken`` option to authenticate with a token from the `Mytoken service `_. To check token validity: + + ``fedcloud token check --mytoken `` + + When creating a Mytoken, ensure you select **"Allows obtaining OpenID Connect Access Tokens"**. You may also use the ``--mytoken-server`` option to authenticate with a specific Mytoken server. + +Alternatively, you can obtain tokens using the `EGI Check-in Token Portal `_, which provides all necessary information for EGI Check-in users. + +In addition to command-line options, environment variables can be used for passing tokens, as summarized in the table below (not shown here). + +By default, the protocol used is ``openid``. This can be changed using the ``--os-protocol`` option. Note that some sites may have a fixed protocol defined in their site configuration (e.g., ``oidc`` for INFN-CLOUD-BARI). + + +Configuration +************* + +Display the current configuration of *fedcloud* with: + +:: + + $ fedcloud config show + +This will show a list of configuration parameters: + ++----------------------------+------------------------------------------------------------------------------------+ +| Parameter | Default value | ++============================+====================================================================================+ +| site | IISAS-FedCloud | ++----------------------------+------------------------------------------------------------------------------------+ +| vo | vo.access.egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ +| site_list_url | https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml | ++----------------------------+------------------------------------------------------------------------------------+ +| site_dir | ${HOME}/.config/fedcloud/site-config | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_url | https://aai.egi.eu/auth/realms/egi | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_public_url | https://goc.egi.eu/gocdbpi/public/ | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_service_group | org.openstack.nova | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_endpoint | https://vault.services.fedcloud.eu:8200 | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_role | | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_mount_point | /secrets/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_locker_mount_point | /v1/cubbyhole/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_salt | fedcloud_salt | ++----------------------------+------------------------------------------------------------------------------------+ +| log_file | ${HOME}/.config/fedcloud/logs/fedcloud.log | ++----------------------------+------------------------------------------------------------------------------------+ +| log_level | DEBUG | ++----------------------------+------------------------------------------------------------------------------------+ +| log_config_file | ${HOME}/.config/fedcloud/logging.conf | ++----------------------------+------------------------------------------------------------------------------------+ +| requests_cert_file | ${HOME}/.config/fedcloud/cert/certs.pem | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_agent_account | egi | ++----------------------------+------------------------------------------------------------------------------------+ +| min_access_token_time | 30 | ++----------------------------+------------------------------------------------------------------------------------+ +| mytoken_server | https://mytoken.data.kit.edu | ++----------------------------+------------------------------------------------------------------------------------+ +| os_protocol | openid | ++----------------------------+------------------------------------------------------------------------------------+ +| os_auth_type | v3oidcaccesstoken | ++----------------------------+------------------------------------------------------------------------------------+ +| os_identity_provider | egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ +| _MIN_ACCESS_TOKEN_TIME | 30 | ++----------------------------+------------------------------------------------------------------------------------+ + + +The **FedCloud client** supports multiple types of configuration: + +- **Default settings** – accessible using **``DEFAULT_SETTINGS``**, these are the built-in default values. +- **Local environment settings** – custom configuration values defined in the environment and loaded via **``env_config``**. +- **Saved configuration settings** – user-defined settings stored in a JSON file, accessible via **``saved_config``**. + +For example, to print the environment configuration, use the following command:: + + $ fedcloud config show --source env_config + + +This command shows, for instance, the following output: + ++-----------------------------+------------------------------------------------------------------+ +| Parameter | Default value | ++=============================+==================================================================+ +| oidc_agent_account | | ++-----------------------------+------------------------------------------------------------------+ +| ... | ... | ++-----------------------------+------------------------------------------------------------------+ + + + +The *fedcloud* configuration can be saved to a file using the following command + +:: + $ fedcloud config create + +By default, the configuration file is saved to **${HOME}/.config/fedcloud/config.yaml**, +but this location can be changed using the ``--config`` option. For example: + +:: + + $ fedcloud config create --config-file /path/to/file.yaml + + +Using Environment Variables and Configuration Priorities +-------------------------------------------------------- + +It is also possible to use the *FEDCLOUD_CONFIG_FILE* environment variable instead of the ``--config`` option in the command line. +This allows users to manage and switch between multiple configuration files—one per project—each with its own settings. + +The *fedcloud* client supports configuration from multiple sources, in the following order of priority (highest to lowest): + +#. **Command-line options** – override all other settings. + Example: ``--site IISAS-FedCloud`` + +#. **Environment variables** – must begin with the prefix ``FEDCLOUD_``. + Example: ``FEDCLOUD_SITE=IISAS-FedCloud`` + +#. **Configuration file** – typically stored as ``config.yaml``. + Example: the ``site`` setting in ``config.yaml`` + +#. **Default configuration** – hardcoded defaults (lowest priority). + See the `source code `_ for details. + +The priority order is important: +default values are overridden by the configuration file, +which is overridden by environment variables, +which are in turn overridden by command-line options. + +For example, the default configuration includes: + +- ``site = IISAS-FedCloud`` +- ``vo = vo.access.egi.eu`` + +These values can be changed using any of the higher-priority methods. For example: + +:: + + $ fedcloud openstack --vo training.egi.eu --site IFCA-LCG2 server list + +or + +:: + + $ export FEDCLOUD_VO=training.egi.eu + $ export FEDCLOUD_SITE=IFCA-LCG2 + $ fedcloud openstack server list + + +Consistent Parameter Naming +--------------------------- + +Note the consistent naming convention for configuration parameters across different sources. For example, the same parameter is represented as: + +* ``--access_token`` in the **command-line** +* ``FEDCLOUD_ACCESS_TOKEN`` as an **environment variable** + + +All configuration parameters follow this consistent mapping across command-line options, environment variables, and configuration files. + +Additional Configurable Parameters +---------------------------------- + +In addition to ``oidc_agent_account``, the following parameters can also be configured in the same way: + +* ``site`` – the OpenStack site to target +* ``vo`` – the Virtual Organisation (VO) +* ``check_in_url`` – the EGI Check-in OIDC endpoint +* ``client_id`` – the OIDC client ID +* ``scopes`` – requested OIDC scopes +* ``access_token`` – manually provided access token +* ``output_format`` – format of output, e.g., ``table``, ``json``, or ``yaml`` + +These parameters can be specified via: + +- command-line options (e.g., ``--site``, ``--vo``), +- environment variables (e.g., ``FEDCLOUD_SITE``, ``FEDCLOUD_VO``), or +- configuration files (e.g., ``site: IISAS-FedCloud`` in ``config.yaml``). + +This design allows flexible and convenient configuration for various usage scenarios. + ++-------------------------------+-------------------------+---------------------------+ +| Environment variable | Command-line option | Configuration parameters | ++===============================+=========================+===========================+ +| FEDCLOUD_OIDC_ACCESS_TOKEN | --oidc-access-token | Not implemented | ++-------------------------------+-------------------------+---------------------------+ +| Not implemented | --mytoken | mytoken | ++-------------------------------+-------------------------+---------------------------+ +| FEDCLOUD_OIDC_AGENT_ACCOUNT | --oidc-agent-account | oidc_agent_account | ++-------------------------------+-------------------------+---------------------------+ + +For convenience, it is recommended to set transient parameters—such as access tokens—via **environment variables**. +This simplifies the usage of *fedcloud* commands by avoiding the need to specify these parameters on the command line each time. + Shell completion **************** @@ -116,36 +276,57 @@ fedcloud --help command --help Show this message and exit. Commands: - endpoint Endpoint command group for interaction with GOCDB and endpoints - openstack Executing OpenStack commands on site and VO + config Managing fedcloud configurations + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO openstack-int Interactive OpenStack client on site and VO - site Site command group for manipulation with site configurations - token Token command group for manipulation with tokens + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token fedcloud token commands *********************** -* **"fedcloud token check --oidc-access-token "**: Check the expiration time of access token, so users can know whether - they need to refresh it. As mentioned before, access token may be given via environment variable *OIDC_ACCESS_TOKEN*, - so the option *--oidc-access-token* is not shown in all examples bellows, even if the option is required. +* **``fedcloud token check``** – Checks the expiration time of the configured access token, + allowing users to determine whether it needs to be refreshed. + +As mentioned earlier, the access token can be provided via the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``. +For this reason, the ``--oidc-access-token`` option is not shown in all examples below, even though it may be required if the token is not set via environment variables. + :: $ fedcloud token check + +Output is shown as: +:: Access token is valid to 2021-01-02 01:25:39 UTC Access token expires in 3571 seconds -* **"fedcloud token list-vos --oidc-access-token "** : Print the list of VO memberships according to EGI Check-in +* **"fedcloud token list-vos"** : Print the list of VO memberships according to EGI Check-in :: $ fedcloud token list-vos + +Sample output: +:: eosc-synergy.eu fedcloud.egi.eu training.egi.eu +* **"fedcloud token issue"** : Print the access_token + +:: + + $ fedcloud token issue + +Sample output: +:: + egwergwregrwegreg... fedcloud endpoint commands ************************** @@ -159,6 +340,10 @@ directly from GOCDB (Grid Operations Configuration Management Database) https:// :: $ fedcloud endpoint list + +Sample output: + +:: Site type URL ------------------ ------------------ ------------------------------------------------ IFCA-LCG2 org.openstack.nova https://api.cloud.ifca.es:5000/v3/ @@ -214,7 +399,7 @@ If the site is set to *ALL_SITES*, or the argument *-a* is used, the command wil :: $ fedcloud endpoint token --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c - export OS_TOKEN="gAAAAA..." + export FEDCLOUD_OS_TOKEN="eayeghjtjtj..." * **"fedcloud endpoint env --site --project-id --oidc-access-token "** : Print @@ -224,35 +409,11 @@ If the site is set to *ALL_SITES*, or the argument *-a* is used, the command wil $ fedcloud endpoint env --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c # environment for IFCA-LCG2 - export OS_AUTH_URL="https://api.cloud.ifca.es:5000/v3/" - export OS_AUTH_TYPE="v3oidcaccesstoken" - export OS_IDENTITY_PROVIDER="egi.eu" - export OS_PROTOCOL="openid" - export OS_ACCESS_TOKEN="..." - - -fedcloud ec3 commands -************************** - -**"fedcloud ec3"** commands are helper commands for deploying EC3 (Elastic Cloud Compute Cluster) in Cloud -via Infrastructure Manager. The commands will create necessary template and authorization files for EC3 client. - -* **"fedcloud ec3 init --site --vo --oidc-access-token --auth-file auth.dat --template-dir - ./templates"** : Generate authorization file (by default *auth.dat*) and template file (by default - *./templates/refresh.radl*) for EC3 client. - -:: - - $ fedcloud ec3 init --site CESGA --vo vo.access.egi.eu - - -* **"fedcloud ec3 refresh --site --vo --oidc-access-token --auth-file auth.dat"** : - Refresh the access token stored in authorization file (by default *auth.dat*). - -:: - - $ fedcloud ec3 init --site CESGA --vo vo.access.egi.eu - + export FEDCLOUD_OS_AUTH_URL="https://api.cloud.ifca.es:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_ACCESS_TOKEN="..." fedcloud site commands @@ -291,6 +452,17 @@ VOs and so on. ... +* **"fedcloud site list --vo "** : List all sites supporting a Virtual Organization + +:: + + $ fedcloud site vo-list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + + * **"fedcloud site show --site "** : Show configuration of the corresponding site. :: @@ -312,8 +484,8 @@ VOs and so on. :: $ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu - export OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" - export OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" * **"fedcloud site env --site --vo "**: set OpenStack environment variable for the VO on the site. @@ -321,13 +493,13 @@ VOs and so on. :: $ fedcloud site env --site IISAS-FedCloud --vo eosc-synergy.eu - export OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" - export OS_AUTH_TYPE="v3oidcaccesstoken" - export OS_IDENTITY_PROVIDER="egi.eu" - export OS_PROTOCOL="openid" - export OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" # Remember to set OS_ACCESS_TOKEN, e.g. : - # export OS_ACCESS_TOKEN=`oidc-token egi` + # export FEDCLOUD_OS_ACCESS_TOKEN=`oidc-token egi` The main differences between *"fedcloud endpoint env"* and *"fedcloud site env"* commands are that the second command @@ -431,8 +603,15 @@ If the site is *ALL_SITES*, the OpenStack command will be executed on all sites ... (openstack) + +fedcloud config commands +*************************** +* **"fedcloud config --config-file create"** : Create default configuration file in default location for configuration file + + + fedcloud secret commands *************************** The **"fedcloud secret"** commands are described in details in the documentation of the -`Secret management service `_. \ No newline at end of file +`Secret management service `_. diff --git a/docs_backup/FAQ.rst b/docs_backup/FAQ.rst new file mode 100644 index 0000000..5a5a7f9 --- /dev/null +++ b/docs_backup/FAQ.rst @@ -0,0 +1,29 @@ +FAQ and Troubleshooting +======================= + +1. *FedCloud client* gives error message *"SSL exception connecting to https:// ..."* when connecting to some sites + +Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message *"SSL exception connecting to https:// ..."*, +follow `instructions `_ +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +`README.md `_ for more details. + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + + +2. *FedCloud client* frozen during initialization (mainly on virtual machines at some sites) + +It is a known problem of *libsodium* which is used by *oidc-agent* Python library. The problem is described +`here `_. Check the entropy on the VMs by executing command +*"cat /proc/sys/kernel/random/entropy_avail"*, and if the result is lower than 300, install *haveged* or *rng-tools*. +On VMs with Centos, you also have to start the daemon manually after installation (or reboot the VMs) + + + + + diff --git a/docs_backup/cheat.rst b/docs_backup/cheat.rst new file mode 100644 index 0000000..155d40b --- /dev/null +++ b/docs_backup/cheat.rst @@ -0,0 +1,265 @@ +Cheat sheet +=========== + +See `Tutorial `_ +for more details of commands. + +Local install via pip3 +********************** + +* Create a Python virtual environment: + +:: + + $ python3 -m venv env + +* Activate the virtual environment + +:: + + $ source env/bin/activate + +* Install fedcloudclient via pip: + +:: + + $ pip3 install fedcloudclient + +* Install IGTF certificates: + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + +Using Docker container +********************** + +* Pull the latest version of fedcloudclient container + +:: + + $ sudo docker pull tdviet/fedcloudclient + +* Start fedcloudclient container with oidc-agent account: + +:: + + $ sudo docker run -it -v ~/.config/oidc-agent/egi:/root/.config/oidc-agent/egi --name fedcloud tdviet/fedcloudclient bash + +* Restart previously terminated container: + +:: + + $ sudo docker start -i fedcloud + +Using oidc-agent +**************** + +* Create an oidc-agent account (if not done): + +:: + + $ oidc-gen --pub --issuer https://aai.egi.eu/auth/realms/egi --scope "eduperson_entitlement email" egi + +* Load oidc-agent account and set environment for fedcloudclient: + +:: + + $ eval `oidc-keychain --accounts egi` && export OIDC_AGENT_ACCOUNT=egi + +Basic usages +************ + +* List your VO memberships according to the access token: + +:: + + $ fedcloud token list-vos + +* List sites in the EGI Federated Cloud: + +:: + + $ fedcloud site list + +* List all sites supporting a Virtual Organization in the EGI Federated Cloud: + +:: + + $ fedcloud site list --vo vo.access.egi.eu + +* Execute an OpenStack command: + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + +* Execute an OpenStack command on all sites: + +:: + + $ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu + + +* Print only selected values (for scripting): + +:: + + $ export OS_TOKEN=$(fedcloud openstack --site CESGA --vo vo.access.egi.eu token issue -c id -f value) + +* All-sites commands with full JSON output: + +:: + + $ fedcloud openstack image list --site ALL_SITES --vo eosc-synergy.eu --json-output + + +Searching and selecting resources +********************************* + +* Show all available projects: + +:: + + $ fedcloud endpoint projects --site ALL_SITES + +* Show all Horizon dashboards: + +:: + + $ fedcloud endpoint list --service-type org.openstack.horizon --site ALL_SITES + +* Search images with appliance title in AppDB: + +:: + + $ fedcloud openstack image list --property "dc:title"="Image for EGI Docker [Ubuntu/18.04/VirtualBox]" --site CESNET-MCC --vo eosc-synergy.eu + + +* Select flavors with 2 CPUs and RAM >= 2048 on a site/VO: + +:: + + $ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --vcpus 2 --flavor-specs "RAM>=2048" --output-format list + + +* Select EGI Ubuntu 20.04 images on a site/VO: + +:: + + # Simpler but longer way + $ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ Ubuntu" --image-specs "Name =~ '20.04'" --image-specs "Name =~ EGI" --output-format list + +:: + + # Shorter but more complex regex + $ fedcloud select image --site IFCA-LCG2 --vo training.egi.eu --image-specs "Name =~ 'EGI.*Ubuntu.*20.04'" --output-format list + + +Mapping and filtering results from OpenStack commands +***************************************************** + +* Select flavors with 2 CPUs: + +:: + + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \ + jq -r '.[].Result[] | select(.VCPUs == 2) | .Name' + +* Select GPU flavors and show their GPU properties on a site: + +:: + + $ fedcloud openstack flavor list --long --site IISAS-FedCloud --vo acc-comp.egi.eu --json-output | \ + jq -r '.[].Result | map(select(.Properties."Accelerator:Type" == "GPU")) | .' + +* Select GPU flavors and show their GPU properties on all sites: + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) | + map(select(.Result | length > 0))' + + +* Construct JSON objects just with site names and flavor names, remove all other properties: + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map({Site:.Site, Flavors:[.Result[].Name]})' + + +Useful commands +*************** + +* Check expiration time of access token (not work for oidc-agent-account): + +:: + + $ fedcloud token check + + +* Set OpenStack environment variables: + +:: + + $ eval $(fedcloud site env --site IISAS-FedCloud --vo vo.access.egi.eu) + + +* List all my own VMs: + +:: + + $ list-all-my-own-vms.sh --vo fedcloud.egi.eu + + +* Activate shell completion + +:: + + # Quick and dirty way (may be resulted in unresponsive shell) + $ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)" + +:: + + # More systematic way + $ wget https://raw.githubusercontent.com/tdviet/fedcloudclient/master/examples/fedcloud_bash_completion.sh + $ source fedcloud_bash_completion.sh + + +* Pass a *mytoken* to Virtual Machines in the EGI Federated Cloud + +:: + + # Create the file "user.txt" with + $ cat user.txt + FEDCLOUD_MYTOKEN= # created on https://mytoken.data.kit.edu/ + + # Pass it to OpenStack + FEDCLOUD_SITE=IISAS-FedCloud + FEDCLOUD_VO=vo.access.egi.eu + fedcloud openstack server create --flavor --image --user-data user.txt --key-name testvm + + # Once you log into the VM you can retrieve the "mytoken" with + curl http://169.254.169.254/openstack/latest/user_data/ + + # and use it with + FEDCLOUD_MYTOKEN= # copied from the previous curl command + fedcloud token check + + +More information +**************** + +* Get help: + +:: + + $ fedcloud --help + $ fedcloud site --help + +* Tutorial `Tutorial `_ diff --git a/docs_backup/development.rst b/docs_backup/development.rst new file mode 100644 index 0000000..e7c43bd --- /dev/null +++ b/docs_backup/development.rst @@ -0,0 +1,39 @@ +Using FedCloud client in Python +=============================== + +FedCloud client can be used as a library for developing other services and tools for EGI Federated Cloud. Most of +functionalities of FedCloud client can be called directly from other codes without side effects. An example of the code +using FedCloud client is available at `GitHub `_. +Just copy/download the code, add your access token and execute *"python demo.py"* to see how it works. + +:: + + # Import FedCloud client library + from fedcloudclient.openstack import fedcloud_openstack + import json + + # Setting values for input parameters: token, site, VO + token = "YOUR_ACCESS_TOKEN" + site = "CYFRONET-CLOUD" + vo = "fedcloud.egi.eu" + + # OpenStack command and options. Must be a tuple + command = ("image", "list", "--long") + + # Execute the OpenStack command on the site/VO with single line of code + # If command finishes correctly, the error_code is 0 and the result is stored + # in JSON object for easy processing + + error_code, result = fedcloud_openstack(token, site, vo, command) + + # Check error code and print the result if OK + if error_code == 0: + print(json.dumps(result, indent=4)) + else: + # If error, result is string containing error message + print("Error message is %s" % result) + + +Read the `FedCloud client API references `_ +for more details about each function in FedCloud client library. Check the output of the equivalent command of +FedCloud client and its source code to see how the function is used. \ No newline at end of file diff --git a/docs_backup/fedcloudclient-logo-non-transparent-small.png b/docs_backup/fedcloudclient-logo-non-transparent-small.png new file mode 100644 index 0000000..74e50c3 Binary files /dev/null and b/docs_backup/fedcloudclient-logo-non-transparent-small.png differ diff --git a/docs_backup/fedcloudclient.rst b/docs_backup/fedcloudclient.rst new file mode 100644 index 0000000..0c9b798 --- /dev/null +++ b/docs_backup/fedcloudclient.rst @@ -0,0 +1,43 @@ +FedCloud client API references +============================== + +fedcloudclient.checkin module +----------------------------- + +.. automodule:: fedcloudclient.checkin + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.endpoint module +------------------------------ + +.. automodule:: fedcloudclient.endpoint + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.sites module +--------------------------- + +.. automodule:: fedcloudclient.sites + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.openstack module +------------------------------- + +.. automodule:: fedcloudclient.openstack + :members: + :undoc-members: + :show-inheritance: + +fedcloudclient.cli module +------------------------- + +.. automodule:: fedcloudclient.cli + :members: + :undoc-members: + :show-inheritance: + diff --git a/docs_backup/index.rst b/docs_backup/index.rst new file mode 100644 index 0000000..61387dd --- /dev/null +++ b/docs_backup/index.rst @@ -0,0 +1,26 @@ +.. fedcloudclient documentation master file, created by + sphinx-quickstart on Wed May 14 12:59:25 2025. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +fedcloudclient documentation +============================ + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + intro + usage + FAQ + cheat + development + install + modules + quickstart + scripts \ No newline at end of file diff --git a/docs_backup/install.rst b/docs_backup/install.rst new file mode 100644 index 0000000..55033d2 --- /dev/null +++ b/docs_backup/install.rst @@ -0,0 +1,86 @@ +Installation +============ + +Installing FedCloud client with pip +*********************************** + +Simply use the following **pip3** command (should be done without root privileges). + +:: + + $ pip3 install -U fedcloudclient + +Installation Notes +------------------ + +Installing the latest version of the **FedCloud client** package using ``pip3`` will also install all required dependencies (such as **openstackclient**). It will create the executable files **``fedcloud``** and **``openstack``**, placing them in the appropriate directory based on your Python environment: + +* ``$VIRTUAL_ENV/bin`` – when using ``pip3`` inside a Python virtual environment, +* ``~/.local/bin`` – when installing with ``pip3 --user``, +* ``/usr/local/bin`` – when installing system-wide with ``pip3`` as root. + +.. note:: + + If you install the package with the ``--user`` option, make sure that ``~/.local/bin`` is included in your ``$PATH``. + +Verifying the Installation +-------------------------- + +To check if the installation was successful, run the following command:: + +:: + + $ fedcloud --help + + +This will show output:: + +:: + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + CLI main function. Intentionally empty + + Options: + --version Show the version and exit. + --help Show this message and exit. + + Commands: + ec3 EC3 cluster provisioning + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token + + + +Installing EGI Core Trust Anchor certificates +********************************************* + +Some sites use certificates issued by national certificate authorities that are not included in the default +OS distribution. If you receive error message *"SSL exception connecting to https:// ..."*, +follow `instructions `_ +for installing EGI Core Trust Anchor certificates and add them to the certificate bundle of Python requests. For quick +test in virtual environment, just execute the following commands. See this +`README.md `_ for more details. + +:: + + $ wget https://raw.githubusercontent.com/tdviet/python-requests-bundle-certs/main/scripts/install_certs.sh + $ bash install_certs.sh + +Using FedCloud client via Docker container +****************************************** + +You can use Docker container for testing **FedCloud client** without installation. EGI Core Trust Anchor certificates +and site configurations are preinstalled. + +:: + + $ sudo docker pull tdviet/fedcloudclient + $ sudo docker run -it tdviet/fedcloudclient bash + + + diff --git a/docs_backup/intro.rst b/docs_backup/intro.rst new file mode 100644 index 0000000..cd30666 --- /dev/null +++ b/docs_backup/intro.rst @@ -0,0 +1,54 @@ +Introduction +============ + +.. image:: https://zenodo.org/badge/336671726.svg + :target: https://zenodo.org/badge/latestdoi/336671726 + +The `FedCloud client `_ is a high-level Python package for a command-line client +designed for interaction with the OpenStack services in the EGI infrastructure. The client can access various EGI +services and can perform many tasks for users including managing access tokens, listing services, and mainly execute +commands on OpenStack sites in EGI infrastructure. + +The most notable features of FedCloud client are following: + +* **Rich functionalities:** have wide ranges of useful commands, including checking access token, searching for + services, listing sites and VOs, and interaction with OpenStack sites. + +* **Simple usages:** can perform any OpenStack command on any sites with only three parameters: the site, the VO + and the command. For example, to list virtual machines (VM) images available to members of ``vo.access.egi.eu`` VO + on ``IISAS-FedCloud`` site, run the following command: + +:: + + $ fedcloud openstack image list --vo vo.access.egi.eu --site IISAS-FedCloud + +* **Federation-wide:** Single client for all OpenStack sites and related services of EGI Cloud infrastructure. + Single command may perform an action on all sites by specifying :code:`--site ALL_SITES`. + +* **Programmable:** the client is designed for using in + `scripts for automation `_ + or as a `Python library `_ + for programming FedCloud services. + +The following modules are included: + +* **fedcloudclient.conf** for handling fedcloudclient configuration, + +* **fedcloudclient.checkin** for operation with EGI Check-in like getting tokens, + +* **fedcloudclient.endpoint** for searching endpoints via GOCDB, getting unscoped/scoped token from + OpenStack keystone, + +* **fedcloudclient.sites** for managing site configurations, + +* **fedcloudclient.openstack** for performing OpenStack operations on sites, + +* **fedcloudclient.secret** for accessing secrets in + `Secret management service `_, + +* **fedcloudclient.ec3** for deploying elastic computing clusters in Cloud. + +A short tutorial of the fedcloudclient is available in `this +presentation `_. +The full documentation, including installation, usage and API description is available +at https://fedcloudclient.fedcloud.eu/. diff --git a/docs_backup/modules.rst b/docs_backup/modules.rst new file mode 100644 index 0000000..f7439b4 --- /dev/null +++ b/docs_backup/modules.rst @@ -0,0 +1,7 @@ +fedcloudclient API references +============================= + +.. toctree:: + :maxdepth: 4 + + fedcloudclient diff --git a/docs_backup/quickstart.rst b/docs_backup/quickstart.rst new file mode 100644 index 0000000..2f08113 --- /dev/null +++ b/docs_backup/quickstart.rst @@ -0,0 +1,120 @@ +Quick start +=========== + +The `Tutorial `_ +presentation is designed for new users of **FedCloud client**. It starts with the quick setup and basic usages, +then step by step to more advanced scenarios. + +Setup +***** + +* Install FedCloud client via pip: + +:: + + $ pip3 install fedcloudclient + +or use Docker container: + +:: + + $ docker run -it tdviet/fedcloudclient bash + + +* Get a new access token from EGI Check-in according to instructions from + `EGI Check-in Token Portal `_ + or `oidc-agent `_ and set + environment variable. + +:: + + $ export FEDCLOUD_OIDC_ACCESS_TOKEN= + +Basic usages +************ + +* List your VO memberships according to the access token: + +:: + + $ fedcloud token list-vos + eosc-synergy.eu + fedcloud.egi.eu + training.egi.eu + ... + +* List sites in the EGI Federated Cloud + +:: + + $ fedcloud site list + 100IT + BIFI + CESGA + ... + +* List sites supporting a Virtual Organization in the EGI Federated Cloud + +:: + + $ fedcloud site list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + +* Execute an OpenStack command, e.g. list images in eosc-synergy.eu VO on IISAS-FedCloud site + (or other combination of site and VO you have access): + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + Site: IISAS-FedCloud, VO: eosc-synergy.eu + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + +* Execute an OpenStack command, e.g. list VMs in eosc-synergy.eu VO on all sites + and print output in JSON format for further machine processing: + +:: + + $ fedcloud openstack server list --site ALL_SITES --vo eosc-synergy.eu --json-output + [ + { + "Site": "IISAS-FedCloud", + "VO": "eosc-synergy.eu", + "command": "server list", + "Exception": null, + "Error code": 0, + "Result": [ + { + ... + }, + ... + ] + }, + ... + ] + +* Get helps from the client + +:: + + $ fedcloud --help + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + Options: + --help Show this message and exit. + + Commands: + endpoint Endpoint command group for interaction with GOCDB and endpoints + openstack Executing OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + site Site command group for manipulation with site configurations + token Token command group for manipulation with tokens + +* Read the `Tutorial `_ + presentation or next sections for more information. diff --git a/docs_backup/scripts.rst b/docs_backup/scripts.rst new file mode 100644 index 0000000..70cae88 --- /dev/null +++ b/docs_backup/scripts.rst @@ -0,0 +1,95 @@ +Using FedCloud client in scripts +================================ + +FedCloud client can be used in scripts for simple automation, either for setting environment variables for other tools +or processing outputs from OpenStack commands. + +Setting environment variables for external tools +************************************************ + +Outputs from FedCloud client commands for setting environment variables are already in the forms *"export VAR=VALUE"*. +Simple *eval* command in scripts can be used for setting environment variables for external tools: + +:: + + $ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu + export OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + + # This command will set environment variables + $ eval $(fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu) + + # Check the value of the variable + $ echo $OS_AUTH_URL + https://cloud.ui.savba.sk:5000/v3/ + + +Processing JSON outputs from OpenStack commands via jq +****************************************************** + +The outputs from Openstack command can be printed in JSON formats with *--json-output* parameter for further machine +processing. The JSON outputs can be processed in scripts by `jq `_ command. +For examples, if users want to select flavors with 2 CPUs: + +:: + + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output + [ + { + "Site": "IISAS-FedCloud", + "VO": "eosc-synergy.eu", + "command": "flavor list", + "Exception": null, + "Error code": 0, + "Result": [ + { + "ID": "0", + "Name": "m1.nano", + "RAM": 64, + "Disk": 1, + "Ephemeral": 0, + "VCPUs": 1, + "Is Public": true + }, + { + "ID": "2e562a51-8861-40d5-8fc9-2638bab4662c", + "Name": "m1.xlarge", + "RAM": 16384, + "Disk": 40, + "Ephemeral": 0, + "VCPUs": 8, + "Is Public": true + }, + ... + ] + } + ] + + # The following jq command selects flavors with VCPUs=2 and print their names + $ fedcloud openstack flavor list --site IISAS-FedCloud --vo eosc-synergy.eu --json-output | \ + jq -r '.[].Result[] | select(.VCPUs == 2) | .Name' + m1.medium + +The following example is more complex: + +* List all flavors in the VO vo.access.egi.eu on all sites and print them in JSON format + +* Filter out sites with error code > 0 + +* Select only GPU flavors + +* Filter out sites with empty list of GPU flavors + +* Print the result (list of all GPU flavors on all sites) in JSON format + +:: + + $ fedcloud openstack flavor list --long --site ALL_SITES --vo vo.access.egi.eu --json-output | \ + jq -r 'map(select(."Error code" == 0)) | + map(.Result = (.Result| map(select(.Properties."Accelerator:Type" == "GPU")))) | + map(select(.Result | length > 0))' + +Note that only OpenStack commands that have outputs can be used with *--json-output*. Using the parameter with +commands without outputs (e.g. setting properties) will generate errors of unsupported parameters. + + diff --git a/docs_backup/usage.rst b/docs_backup/usage.rst new file mode 100644 index 0000000..d29d846 --- /dev/null +++ b/docs_backup/usage.rst @@ -0,0 +1,616 @@ +Usage +===== + +**FedCloud client** has the following main groups of commands: + +* **"fedcloud config"** for handling fedcloudclient configuration, + +* **"fedcloud token"** for interactions with EGI Check-in and access tokens, + +* **"fedcloud endpoint"** for interactions with GOCDB (and site endpoints according to GOCDB), + +* **"fedcloud site"** for manipulations with site configurations, + +* **"fedcloud openstack"** or **"fedcloud openstack-int"** for performing OpenStack commands on sites, + +* **"fedcloud secret"** for accessing secrets in + `Secret management service `_, + +* **"fedcloud ec3"** as helper commands for deploying EC3. + + + +Authentication Options +====================== + +**FedCloud** commands require access tokens for authentication. Users have multiple options for providing these tokens: + +- **Direct access token**: Use the ``--oidc-access-token`` option to provide an access token directly. You can retrieve this token from the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``, or pass it explicitly, e.g.: + + ``fedcloud token check --oidc-access-token `` + +- **OIDC agent**: Use the ``--oidc-agent-account`` option to integrate with `oidc-agent `_. For example, check token validity with: + + ``fedcloud token check --oidc-agent-account `` + + To use this method, follow the instructions at `oidc-agent for EGI `_ to register a client, then pass the client (account) name to the FedCloud client. + +- **Mytoken**: Use the ``--mytoken`` option to authenticate with a token from the `Mytoken service `_. To check token validity: + + ``fedcloud token check --mytoken `` + + When creating a Mytoken, ensure you select **"Allows obtaining OpenID Connect Access Tokens"**. You may also use the ``--mytoken-server`` option to authenticate with a specific Mytoken server. + +Alternatively, you can obtain tokens using the `EGI Check-in Token Portal `_, which provides all necessary information for EGI Check-in users. + +In addition to command-line options, environment variables can be used for passing tokens, as summarized in the table below (not shown here). + +By default, the protocol used is ``openid``. This can be changed using the ``--os-protocol`` option. Note that some sites may have a fixed protocol defined in their site configuration (e.g., ``oidc`` for INFN-CLOUD-BARI). + + +Configuration +************* + +Display the current configuration of *fedcloud* with: + +:: + + $ fedcloud config show + +This will show a list of configuration parameters: + ++----------------------------+------------------------------------------------------------------------------------+ +| Parameter | Default value | ++============================+====================================================================================+ +| site | IISAS-FedCloud | ++----------------------------+------------------------------------------------------------------------------------+ +| vo | vo.access.egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ +| site_list_url | https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml | ++----------------------------+------------------------------------------------------------------------------------+ +| site_dir | ${HOME}/.config/fedcloud/site-config | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_url | https://aai.egi.eu/auth/realms/egi | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_public_url | https://goc.egi.eu/gocdbpi/public/ | ++----------------------------+------------------------------------------------------------------------------------+ +| gocdb_service_group | org.openstack.nova | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_endpoint | https://vault.services.fedcloud.eu:8200 | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_role | | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_mount_point | /secrets/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_locker_mount_point | /v1/cubbyhole/ | ++----------------------------+------------------------------------------------------------------------------------+ +| vault_salt | fedcloud_salt | ++----------------------------+------------------------------------------------------------------------------------+ +| log_file | ${HOME}/.config/fedcloud/logs/fedcloud.log | ++----------------------------+------------------------------------------------------------------------------------+ +| log_level | DEBUG | ++----------------------------+------------------------------------------------------------------------------------+ +| log_config_file | ${HOME}/.config/fedcloud/logging.conf | ++----------------------------+------------------------------------------------------------------------------------+ +| requests_cert_file | ${HOME}/.config/fedcloud/cert/certs.pem | ++----------------------------+------------------------------------------------------------------------------------+ +| oidc_agent_account | egi | ++----------------------------+------------------------------------------------------------------------------------+ +| min_access_token_time | 30 | ++----------------------------+------------------------------------------------------------------------------------+ +| mytoken_server | https://mytoken.data.kit.edu | ++----------------------------+------------------------------------------------------------------------------------+ +| os_protocol | openid | ++----------------------------+------------------------------------------------------------------------------------+ +| os_auth_type | v3oidcaccesstoken | ++----------------------------+------------------------------------------------------------------------------------+ +| os_identity_provider | egi.eu | ++----------------------------+------------------------------------------------------------------------------------+ + + +The **FedCloud client** supports multiple types of configuration: + +- **Default settings** – accessible using **``DEFAULT_SETTINGS``**, these are the built-in default values. +- **Local environment settings** – custom configuration values defined in the environment and loaded via **``env_config``**. +- **Saved configuration settings** – user-defined settings stored in a JSON file, accessible via **``saved_config``**. + +For example, to print the environment configuration, use the following command:: + + $ fedcloud config show --source env_config + + +**Testing new version.......** +This command shows, for instance, the following output: + ++----------------------------+-------------------------------------------------------------------+ +| Parameter | Default value | ++============================+===================================================================+ +| oidc_agent_account | | ++-----------------------------+------------------------------------------------------------------+ +| ... | ... | ++-----------------------------+------------------------------------------------------------------+ + + + +The *fedcloud* configuration can be saved to a file using the following command + +:: + $ fedcloud config create + +By default, the configuration file is saved to **${HOME}/.config/fedcloud/config.yaml**, +but this location can be changed using the ``--config`` option. For example: + +:: + + $ fedcloud config create --config-file /path/to/file.yaml + + +Using Environment Variables and Configuration Priorities +-------------------------------------------------------- + +It is also possible to use the *FEDCLOUD_CONFIG_FILE* environment variable instead of the ``--config`` option in the command line. +This allows users to manage and switch between multiple configuration files—one per project—each with its own settings. + +The *fedcloud* client supports configuration from multiple sources, in the following order of priority (highest to lowest): + +#. **Command-line options** – override all other settings. + Example: ``--site IISAS-FedCloud`` + +#. **Environment variables** – must begin with the prefix ``FEDCLOUD_``. + Example: ``FEDCLOUD_SITE=IISAS-FedCloud`` + +#. **Configuration file** – typically stored as ``config.yaml``. + Example: the ``site`` setting in ``config.yaml`` + +#. **Default configuration** – hardcoded defaults (lowest priority). + See the `source code `_ for details. + +The priority order is important: +default values are overridden by the configuration file, +which is overridden by environment variables, +which are in turn overridden by command-line options. + +For example, the default configuration includes: + +- ``site = IISAS-FedCloud`` +- ``vo = vo.access.egi.eu`` + +These values can be changed using any of the higher-priority methods. For example: + +:: + + $ fedcloud openstack --vo training.egi.eu --site IFCA-LCG2 server list + +or + +:: + + $ export FEDCLOUD_VO=training.egi.eu + $ export FEDCLOUD_SITE=IFCA-LCG2 + $ fedcloud openstack server list + + +Consistent Parameter Naming +--------------------------- + +Note the consistent naming convention for configuration parameters across different sources. For example, the same parameter is represented as: + +* ``--oidc-agent-account`` in the **command-line** +* ``FEDCLOUD_OIDC_AGENT_ACCOUNT`` as an **environment variable** +* ``oidc_agent_account`` in the **configuration file** + +All configuration parameters follow this consistent mapping across command-line options, environment variables, and configuration files. + +Additional Configurable Parameters +---------------------------------- + +In addition to ``oidc_agent_account``, the following parameters can also be configured in the same way: + +* ``site`` – the OpenStack site to target +* ``vo`` – the Virtual Organisation (VO) +* ``check_in_url`` – the EGI Check-in OIDC endpoint +* ``client_id`` – the OIDC client ID +* ``scopes`` – requested OIDC scopes +* ``access_token`` – manually provided access token +* ``output_format`` – format of output, e.g., ``table``, ``json``, or ``yaml`` + +These parameters can be specified via: + +- command-line options (e.g., ``--site``, ``--vo``), +- environment variables (e.g., ``FEDCLOUD_SITE``, ``FEDCLOUD_VO``), or +- configuration files (e.g., ``site: IISAS-FedCloud`` in ``config.yaml``). + +This design allows flexible and convenient configuration for various usage scenarios. + ++------------------------------+-------------------------+ +| Environment variable | Command-line option | ++==============================+=========================+ +| FEDCLOUD_OIDC_ACCESS_TOKEN | --oidc-access-token | ++------------------------------+-------------------------+ +| FEDCLOUD_MYTOKEN | --mytoken | ++------------------------------+-------------------------+ +| FEDCLOUD_OIDC_AGENT_ACCOUNT | --oidc-agent-account | ++------------------------------+-------------------------+ + +For convenience, it is recommended to set transient parameters—such as access tokens—via **environment variables**. +This simplifies the usage of *fedcloud* commands by avoiding the need to specify these parameters on the command line each time. + + +Shell completion +**************** + +Shell completion for *fedcloud* command in *bash* can be activated by executing the following command: + +:: + + $ eval "$(_FEDCLOUD_COMPLETE=bash_source fedcloud)" + +The command above may affect responsiveness of the shell. For long work, it is recommended to copy the +`fedcloud_bash_completion.sh script +`_ to a local file, and +source it from ~/.bashrc. Refer `Click documentation +`_ for a long explanation. + +After enabling shell completion, press twice for shell completion: + +:: + + $ fedcloud site + env list save-config show show-project-id + + +fedcloud --help command +*********************** + +* **"fedcloud --help"** command will print help message. When using it in combination with other + commands, e.g. **"fedcloud token --help"**, **"fedcloud token check --hep"**, it will print list of options for the + corresponding commands + +:: + + $ fedcloud --help + Usage: fedcloud [OPTIONS] COMMAND [ARGS]... + + Options: + --help Show this message and exit. + + Commands: + config Managing fedcloud configurations + endpoint Obtain endpoint details and scoped tokens + openstack Execute OpenStack commands on site and VO + openstack-int Interactive OpenStack client on site and VO + secret Commands for accessing secret objects + select Select resources according to specification + site Obtain site configurations + token Get details of access token + + +fedcloud token commands +*********************** + +* **``fedcloud token check``** – Checks the expiration time of the configured access token, + allowing users to determine whether it needs to be refreshed. + +As mentioned earlier, the access token can be provided via the environment variable ``FEDCLOUD_OIDC_ACCESS_TOKEN``. +For this reason, the ``--oidc-access-token`` option is not shown in all examples below, even though it may be required if the token is not set via environment variables. + + +:: + + $ fedcloud token check + +Output is shown as: +:: + Access token is valid to 2021-01-02 01:25:39 UTC + Access token expires in 3571 seconds + + +* **"fedcloud token list-vos"** : Print the list of VO memberships according to EGI Check-in + +:: + + $ fedcloud token list-vos + +Sample output: +:: + eosc-synergy.eu + fedcloud.egi.eu + training.egi.eu + +* **"fedcloud token issue"** : Print the access_token + +:: + + $ fedcloud token issue + +Sample output: +:: + egwergwregrwegreg... + +fedcloud endpoint commands +************************** + +**"fedcloud endpoint"** commands are complementary part of the **"fedcloud site"** commands. Instead of using site +configurations defined in files saved in GitHub repository or local disk, the commands try to get site information +directly from GOCDB (Grid Operations Configuration Management Database) https://goc.egi.eu/ or make probe test on sites + +* **"fedcloud endpoint list"** : List of endpoints of sites defined in GOCDB. + +:: + + $ fedcloud endpoint list + +Sample output: + +:: + Site type URL + ------------------ ------------------ ------------------------------------------------ + IFCA-LCG2 org.openstack.nova https://api.cloud.ifca.es:5000/v3/ + IN2P3-IRES org.openstack.nova https://sbgcloud.in2p3.fr:5000/v3 + ... + + +* **"fedcloud endpoint projects --site --oidc-access-token "** : List of projects to which the owner + of the access token has access at the given site + +:: + + $ fedcloud endpoint projects --site IFCA-LCG2 + id Name enabled site + -------------------------------- -------------------------- --------- --------- + 2a7e2cd4b6dc4e609dd934964c1715c6 VO:demo.fedcloud.egi.eu True IFCA-LCG2 + 3b9754ad8c6046b4aec43ec21abe7d8c VO:eosc-synergy.eu True IFCA-LCG2 + ... + +If the site is set to *ALL_SITES*, or the argument *-a* is used, the command will show accessible projects from all sites of the EGI Federated Cloud. + + +* **"fedcloud endpoint vos --site --oidc-access-token "** : List of Virtual Organisations (VOs) + to which the owner of the access token has access at the given site + +:: + + $ fedcloud endpoint vos --site IFCA-LCG2 + VO id Project name enabled site + ---------------- -------------------------------- ------------------- --------- --------- + vo.access.egi.eu 233f045cb1ff46842a15ebb33af69460 VO:vo.access.egi.eu True IFCA-LCG2 + training.egi.eu d340308880134d04294097524eace710 VO:training.egi.eu True IFCA-LCG2 + ... + +If the site is set to *ALL_SITES*, or the argument *-a* is used, the command will show accessible VOs from all sites of the EGI Federated Cloud. + +:: + + $ fedcloud endpoint vos -a + VO id Project name enabled site + ------------------- -------------------------------- ------------------- --------- ----------------- + vo.access.egi.eu 233f045cb1ff46842a15ebb33af69460 VO:vo.access.egi.eu True IFCA-LCG2 + training.egi.eu d340308880134d04294097524eace710 VO:training.egi.eu True IFCA-LCG2 + vo.access.egi.eu 7101022b9ae74ed9ac1a574497279499 EGI_access True IN2P3-IRES + vo.access.egi.eu 5bbdb5c1e0b2bcbac29904f4ac22dcaa vo_access_egi_eu True UNIV-LILLE + vo.access.egi.eu 4cab325ca8c2495bf2d4e8f230bcd51a VO:vo.access.egi.eu True INFN-PADOVA-STACK + ... + + +* **"fedcloud endpoint token --site --project-id --oidc-access-token "** : Get + OpenStack keystone scoped token on the site for the project ID. + +:: + + $ fedcloud endpoint token --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c + export FEDCLOUD_OS_TOKEN="eayeghjtjtj..." + + +* **"fedcloud endpoint env --site --project-id --oidc-access-token "** : Print + environment variables for working with the project ID on the site. + +:: + + $ fedcloud endpoint env --site IFCA-LCG2 --project-id 3b9754ad8c6046b4aec43ec21abe7d8c + # environment for IFCA-LCG2 + export FEDCLOUD_OS_AUTH_URL="https://api.cloud.ifca.es:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_ACCESS_TOKEN="..." + + +fedcloud site commands +********************** + +**"fedcloud site"** commands will read site configurations and manipulate with them. If the local site configurations +exist at *~/.config/fedcloud/site-config/*, **fedcloud** will read them from there, otherwise the commands will read +from `GitHub repository `_. + +By default, **fedcloud** does not save anything on local disk, users have to save the site configuration to local disk +explicitly via **"fedcloud site save-config"** command. The advantage of having local +site configurations, beside faster loading, is to give users ability to make customizations, e.g. add additional VOs, +remove sites they do not have access, and so on. + +* **"fedcloud site save-config"** : Read the default site configurations from GitHub + and save them to *~/.config/fedcloud/site-config/* local directory. The command will overwrite existing site configurations + in the local directory. + +:: + + $ fedcloud site save-config + Saving site configs to directory /home/viet/.config/fedcloud/site-config/ + + +After saving site configurations, users can edit and customize them, e.g. remove inaccessible sites, add new +VOs and so on. + +* **"fedcloud site list"** : List of existing sites in the site configurations + +:: + + $ fedcloud site list + 100IT + BIFI + CESGA + ... + + +* **"fedcloud site list --vo "** : List all sites supporting a Virtual Organization + +:: + + $ fedcloud site vo-list --vo vo.access.egi.eu + BIFI + CENI + CESGA-CLOUD + ... + + +* **"fedcloud site show --site "** : Show configuration of the corresponding site. + +:: + + $ fedcloud site show --site IISAS-FedCloud + endpoint: https://cloud.ui.savba.sk:5000/v3/ + gocdb: IISAS-FedCloud + vos: + - auth: + project_id: a22bbffb007745b2934bf308b0a4d186 + name: covid19.eosc-synergy.eu + - auth: + project_id: 51f736d36ce34b9ebdf196cfcabd24ee + name: eosc-synergy.eu + + +* **"fedcloud site show-project-id --site --vo "**: show the project ID of the VO on the site. + +:: + + $ fedcloud site show-project-id --site IISAS-FedCloud --vo eosc-synergy.eu + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + + +* **"fedcloud site env --site --vo "**: set OpenStack environment variable for the VO on the site. + +:: + + $ fedcloud site env --site IISAS-FedCloud --vo eosc-synergy.eu + export FEDCLOUD_OS_AUTH_URL="https://cloud.ui.savba.sk:5000/v3/" + export FEDCLOUD_OS_AUTH_TYPE="v3oidcaccesstoken" + export FEDCLOUD_OS_IDENTITY_PROVIDER="egi.eu" + export FEDCLOUD_OS_PROTOCOL="openid" + export FEDCLOUD_OS_PROJECT_ID="51f736d36ce34b9ebdf196cfcabd24ee" + # Remember to set OS_ACCESS_TOKEN, e.g. : + # export FEDCLOUD_OS_ACCESS_TOKEN=`oidc-token egi` + + +The main differences between *"fedcloud endpoint env"* and *"fedcloud site env"* commands are that the second command +needs VO name as input parameter instead of project ID. The command may set also environment variable OS_ACCESS_TOKEN, +if access token is provided, otherwise it will print notification. + + +fedcloud select commands +*************************** + +* **"fedcloud select flavor --site --vo --oidc-access-token --flavor-specs "** : + Select flavor according to the specification in *flavor-specs*. The specifications may be repeated, + e.g. *--flavor-specs "VCPUs==2" --flavor-specs "RAM>=2048"*, or may be joined, e.g. + *--flavor-specs "VCPUs==2 & Disk>10"*. For frequently used specs, short-option alternatives are available, e.g. + *--vcpus 2* is equivalent to *--flavor-specs "VCPUs==2"*. The output is sorted, flavors using less resources + (in the order: GPUs, CPUs, RAM, Disk) are placed on the first places. Users can choose to print only the best-matched + flavor with *--output-format first* (suitable for scripting) or the full list of all matched flavors in list/YAML/JSON + format. + +:: + + $ fedcloud select flavor --site IISAS-FedCloud --vo vo.access.egi.eu --flavor-specs "RAM>=2096" --flavor-specs "Disk > 10" --output-format list + m1.medium + m1.large + m1.xlarge + m1.huge + g1.c08r30-K20m + g1.c16r60-2xK20m + + +* **"fedcloud select image --site --vo --oidc-access-token --image-specs "** : + Select image according to the specification in *image-specs*. The specifications may be repeated, + e.g. *--image-specs "Name=~Ubuntu" --image-specs "Name=~'20.04'"*. The output is sorted, newest images + are placed on the first places. Users can choose to print only the best-matched + image with *--output-format first* (suitable for scripting) or the full list of all matched images in list/YAML/JSON + format. + +:: + + $ fedcloud select image --site INFN-CATANIA-STACK --vo training.egi.eu --image-specs "Name =~ Ubuntu" --output-format list + TRAINING.EGI.EU Image for EGI Docker [Ubuntu/18.04/VirtualBox] + TRAINING.EGI.EU Image for EGI Ubuntu 20.04 [Ubuntu/20.04/VirtualBox] + + +* **"fedcloud select network --site --vo --oidc-access-token --network-specs "** : + Select network according to the specification in *network-specs*. User can choose to select only public or private + network, or both (default). The output is sorted in the order: public, shared, + private. Users can choose to print only the best-matched network with *--output-format first* + (suitable for scripting) or the full list of all matched networks in list/YAML/JSON format. + +:: + + $ fedcloud select network --site IISAS-FedCloud --vo training.egi.eu --network-specs default --output-format list + public-network + private-network + + +fedcloud openstack commands +*************************** + +* **"fedcloud openstack --site --vo --oidc-access-token "** : Execute an + OpenStack command on the site and VO. Examples of OpenStack commands are *"image list"*, *"server list"* and can be used + with additional options for the commands, e.g. *"image list --long"*, *"server list --format json"*. The list of all + OpenStack commands, and their parameters/usages are available + `here `_. + +:: + + $ fedcloud openstack image list --site IISAS-FedCloud --vo eosc-synergy.eu + Site: IISAS-FedCloud, VO: eosc-synergy.eu + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + + +If the site is *ALL_SITES*, the OpenStack command will be executed on all sites in EGI Federated Cloud. + +* **"fedcloud openstack-int --site --vo --oidc-access-token "** : Call OpenStack client without + command, so users can work with OpenStack site in interactive mode. This is useful when users need to perform multiple + commands successively. For example, users may need get list of images, list of flavors, list of networks before + creating a VM. OIDC authentication is done only once at the beginning, then the keystone token is cached and will + be used for successive commands without authentication via CheckIn again. + +:: + + $ fedcloud openstack-int --site IISAS-FedCloud --vo eosc-synergy.eu + (openstack) image list + +--------------------------------------+-------------------------------------------------+--------+ + | ID | Name | Status | + +--------------------------------------+-------------------------------------------------+--------+ + | 862d4ede-6a11-4227-8388-c94141a5dace | Image for EGI CentOS 7 [CentOS/7/VirtualBox] | active | + ... + (openstack) flavor list + +--------------------------------------+-----------+-------+------+-----------+-------+-----------+ + | ID | Name | RAM | Disk | Ephemeral | VCPUs | Is Public | + +--------------------------------------+-----------+-------+------+-----------+-------+-----------+ + | 5bd8397c-b97f-462d-9d2b-5b533844996c | m1.small | 2048 | 10 | 0 | 1 | True | + | df25f80f-ed19-4e0b-805e-d34620ba0334 | m1.medium | 4096 | 40 | 0 | 2 | True | + ... + (openstack) + + +fedcloud config commands +*************************** +* **"fedcloud config --config-file create"** : Create default configuration file in default location for configuration file + + + +fedcloud secret commands +*************************** + +The **"fedcloud secret"** commands are described in details in the documentation of the +`Secret management service `_. diff --git a/fedcloudclient/auth.py b/fedcloudclient/auth.py new file mode 100644 index 0000000..694bfbd --- /dev/null +++ b/fedcloudclient/auth.py @@ -0,0 +1,378 @@ +""" +Class for managing tokens +""" + +import re +import time +from datetime import datetime +import jwt +import liboidcagent as agent +import requests + +from fedcloudclient.conf import CONF +from fedcloudclient.exception import TokenError +from fedcloudclient.logger import log_and_raise +from fedcloudclient.conf import save_config, DEFAULT_CONFIG_LOCATION + +# pylint: disable=too-few-public-methods +class Token: + """ + Abstract base class for token management. + """ + + def __init__(self): + pass + +# pylint: disable=too-few-public-methods +class OIDCToken(Token): + """ + OIDC token handler. + + This class manages access tokens from various sources (OIDC agent, + Mytoken server), decodes them, and exposes user and VO membership + information. + + :param access_token: + Optional initial access token to decode and discover metadata from. + :type access_token: str, optional + + :ivar str access_token: The raw access token string. + :ivar dict payload: The decoded JWT payload. + :ivar str oidc_agent_account: The account name used with oidc-agent. + :ivar str mytoken: The Mytoken identifier used to fetch a token. + :ivar str user_id: The `sub` claim extracted from the token payload. + :ivar str _vo_pattern: Regex pattern for extracting VO names. + :ivar dict request_json: OIDC discovery metadata. + :ivar int _min_access_token_time: Minimum required lifetime for a token. + :ivar dict conf: Reference to global CONF dict for defaults. + """ + + def __init__(self, access_token=None): + """ + Initialize the OIDC token handler. + + :param access_token: + A JWT or opaque token string. If provided, the token is immediately + decoded and OIDC discovery is performed. + :type access_token: str, optional + """ + super().__init__() + self.access_token = access_token + self.payload = None + self.oidc_agent_account = None + self.mytoken = None + self.user_id = None + self._vo_pattern = "urn:mace:egi.eu:group:(.+?):(.+:)*role=member#aai.egi.eu" + self.request_json=None + self._min_access_token_time=CONF["_MIN_ACCESS_TOKEN_TIME"] + self.conf=CONF + if access_token is not None: + self.decode_token() + self.oidc_discover() + + def decode_token(self) -> dict: + """ + Decode the stored access token into its JWT payload. + + This method will decode `self.access_token` without verifying the + signature (i.e. `verify_signature=False`), extract the `sub` claim + into `self.user_id`, and cache the payload in `self.payload`. + + :return: + The decoded JWT payload as a dictionary. + :rtype: dict + + :raises TokenError: + If `self.access_token` is missing or invalid. + """ + if not self.payload: + try: + self.payload = jwt.decode(self.access_token, options={"verify_signature": False}) + self.user_id = self.payload["sub"] + except jwt.exceptions.InvalidTokenError: + error_msg = "Invalid access token" + log_and_raise(error_msg, TokenError) + + return self.payload + + def get_checkin_id(self,access_token): + """ + Retrieve the user ID from the decoded token payload. + + If the payload hasn’t been decoded yet, this will trigger a decode + (via `self.decode_token()`) and may still return `None` if decoding fails. + + :return: + The `user_id` string extracted from the token payload, or `None` if + no payload is available. + :rtype: str or None + """ + self.access_token=access_token + payload = self.decode_token() + if payload is None: + return None + return payload["sub"] + + def get_user_id(self) -> str: + """ + Return use ID + + :return: user_id + + """ + + if not self.payload: + self.decode_token() + return None + return self.user_id + + def get_token_from_oidc_agent(self, oidc_agent_account: str) -> str: + """ + Obtain an access token from a local OIDC agent. + + :param oidc_agent_account: + The name of the account registered with your local `oidc-agent`. + :type oidc_agent_account: str + + :return: + The retrieved access token string. + :rtype: str + + :raises TokenError: + If `oidc_agent_account` is not provided or if the agent fails to return a token. + """ + + if oidc_agent_account: + try: + access_token = agent.get_access_token( + oidc_agent_account, + min_valid_period=CONF.get("_MIN_ACCESS_TOKEN_TIME"), + application_hint="fedcloudclient", + ) + self.access_token = access_token + self.oidc_agent_account = oidc_agent_account + self.conf["oidc_agent_account"]=str(oidc_agent_account) + save_config(DEFAULT_CONFIG_LOCATION,self.conf) + return access_token + + except agent.OidcAgentError as exception: + error_msg = f"Error getting access token from oidc-agent in -def get_token_from_oidc_agent()-: {exception}" + log_and_raise(error_msg, TokenError) + return None + else: + error_msg = f"Error getting access token from oidc-agent: {oidc_agent_account}" + log_and_raise(error_msg, TokenError) + return None + + def get_token_from_mytoken(self, mytoken: str, mytoken_server: str = None) -> str: + """ + Obtain an access token by exchanging a Mytoken identifier. + + :param mytoken: + The Mytoken identifier (a one-time code or token name) to exchange for an access token. + :type mytoken: str + + :param mytoken_server: + Optional base URL of the Mytoken server. + If not provided, defaults to `CONF["mytoken_server"]`. + :type mytoken_server: str, optional + + :return: + The access token string retrieved from the Mytoken server, or `None` if an error occurred. + :rtype: str or None + + :raises TokenError: + If no `mytoken` is provided, or if the server returns an HTTP error. + :raises requests.exceptions.Timeout: + If the HTTP request to the Mytoken server times out. + """ + + if not mytoken_server: + mytoken_server = CONF.get("mytoken_server") + + if mytoken: + try: + data = { + "grant_type": "mytoken", + "mytoken": mytoken, + } + try: + req = requests.post( + mytoken_server + "/api/v0/token/access", + json=data,) + + self.conf["mytoken"]=str(mytoken) + save_config(DEFAULT_CONFIG_LOCATION,self.conf) + + except requests.exceptions.Timeout as err: + error_msg = f"Timeout for requests in mytoken: {err}" + log_and_raise(error_msg, err) + return None + req.raise_for_status() + access_token = req.json().get("access_token") + self.access_token = access_token + self.mytoken = mytoken + return access_token + + except requests.exceptions.HTTPError as exception: + error_msg = f"Error getting access token from mytoken server: {exception}" + log_and_raise(error_msg, TokenError) + return None + else: + error_msg = f"Error getting access token from mytoken server: mytoken is {mytoken}" + log_and_raise(error_msg, TokenError) + return None + + def multiple_token(self, access_token: str, oidc_agent_account: str, mytoken: str, mytoken_server = None) -> str: + """ + Select a valid token from multiple sources. + + :param access_token: + An existing access token to try first. If it’s valid, it will be returned. + :type access_token: str + + :param oidc_agent_account: + The account name for your local OIDC agent. + If provided, we’ll ask the agent for a token. + :type oidc_agent_account: str + + :param mytoken: + A Mytoken identifier (e.g. a token name) to exchange for an access token. + :type mytoken: str + + :param mytoken_server: + Optional URL of the Mytoken server to contact (defaults to your configured server). + :type mytoken_server: str, optional + + :returns: + A valid access token string. + :rtype: str + + :raises TokenError: + If none of the methods yields a valid token. + """ + if mytoken: + try: + access_token=self.get_token_from_mytoken(mytoken) + return access_token + except TokenError: + pass + if oidc_agent_account: + try: + access_token=self.get_token_from_oidc_agent(oidc_agent_account) + return access_token + except TokenError: + pass + if mytoken_server: + pass + + if access_token: + self.access_token = access_token + return access_token + log_and_raise("Cannot get access token", TokenError) + return None + + def oidc_discover(self) -> dict: + """Perform OpenID Connect discovery. + + Reads the issuer URL from `self.payload["iss"]` and fetches the + provider metadata from `/.well-known/openid-configuration`. + + Returns: + dict: OIDC provider metadata as a JSON-like dict. + + Raises: + requests.HTTPError: If fetching the discovery document fails. + """ + oidc_url=self.payload["iss"] + request = requests.get(oidc_url + "/.well-known/openid-configuration") + request.raise_for_status() + self.request_json=request.json() + return self.request_json + + def check_token(self, access_token, verbose=False): + """ + Check validity of an access token. + + :param access_token: + The JWT or opaque token string to validate. + :type access_token: str + + :param verbose: + If `True`, print human‐readable expiration information. + Defaults to `False`. + :type verbose: bool + + :return: + The same `access_token` if it’s still valid, otherwise `None`. + :rtype: str or None + + :raises TokenError: + If the token is expired (or fails other validation checks). + """ + self.access_token=access_token + payload = self.decode_token() + if payload is None: + return None + + exp_timestamp = int(payload["exp"]) + current_timestamp = int(time.time()) + exp_time_in_sec = exp_timestamp - current_timestamp + + if exp_time_in_sec < self._min_access_token_time: + error_msg=f"Error: Expired access token in {exp_time_in_sec}" + log_and_raise(error_msg,TokenError) + return None + + if verbose: + exp_time_str = datetime.utcfromtimestamp(exp_timestamp).strftime( + "%Y-%m-%d %H:%M:%S" + ) + print(f"Token is valid until {exp_time_str} UTC") + if exp_time_in_sec < 24 * 3600: + print(f"Token expires in {exp_time_in_sec} seconds") + else: + exp_time_in_days = exp_time_in_sec // (24 * 3600) + print(f"Token expires in {exp_time_in_days} days") + + return access_token + + def token_list_vos(self,access_token): + """ + List VO memberships in EGI Check-in. + + :param access_token: + A valid access token (JWT or opaque string) to authenticate the request. + :type access_token: str + + :return: + A sorted list of VO names parsed from the `eduperson_entitlement` claims. + :rtype: list[str] + + :raises requests.exceptions.Timeout: + If the HTTP request to the userinfo endpoint times out. + :raises requests.HTTPError: + If the HTTP response status code indicates an error. + """ + self.access_token=access_token + oidc_ep = self.request_json + try: + request = requests.get( + oidc_ep["userinfo_endpoint"], + headers={"Authorization": f"Bearer {self.access_token}"}) + + except requests.exceptions.Timeout as err: + error_msg = f"Timeout for requests in list-vos: {err}" + log_and_raise(error_msg, err) + return None + + request.raise_for_status() + vos = set() + pattern = re.compile(self._vo_pattern) + for claim in request.json().get("eduperson_entitlement", []): + vo = pattern.match(claim) # pylint: disable=invalid-name + if vo: + vos.add(vo.groups()[0]) + request.raise_for_status() + + return sorted(vos) diff --git a/fedcloudclient/auth_test.py b/fedcloudclient/auth_test.py new file mode 100644 index 0000000..44db551 --- /dev/null +++ b/fedcloudclient/auth_test.py @@ -0,0 +1,102 @@ +""" +Testing unit for auth.py +""" +import os + +from fedcloudclient.auth import OIDCToken +from fedcloudclient.logger import log_and_raise +from fedcloudclient.exception import TokenError + +def verify_mytoken(mytoken: str) -> str: + """ + Get access token from mytoken, decode them, get user ID and verify + """ + token = OIDCToken() + try: + access_token_mytoken=token.get_token_from_mytoken(mytoken, None) + return access_token_mytoken + except TokenError: + err_msg="No MYTOKEN" + return log_and_raise(err_msg,TokenError) + + +def verify_oidc_agent(user_id: str) -> str: + """ Verify token access from oidc-agent""" + token = OIDCToken() + try: + access_token_oidc=token.get_token_from_oidc_agent(user_id) + return access_token_oidc + except TokenError: + err_msg="No FEDCLOUD_OIDC_AGENT_ACCOUNTT" + return log_and_raise(err_msg,TokenError) + + +def verify_access_token(access_token:str) -> str: + """ Verify access_token """ + token = OIDCToken() + try: + token.access_token=access_token + return token.access_token + except TokenError: + err_msg="Not valid ACCESS_TOKEN" + return log_and_raise(err_msg,TokenError) + +def verify_user_id(access_token:str) -> str: + """ Check user id from access_token """ + token = OIDCToken() + token.access_token=access_token + try: + user_id=token.get_user_id() + return user_id + except TokenError: + err_msg="No user ID from access_token" + return log_and_raise(err_msg,TokenError) + +def verify_pyload(access_token:str) -> dict: + """ Get payload, request_json, list_vos from access_token """ + token = OIDCToken() + token.access_token=access_token + try: + token.get_user_id() + payload=token.payload + request_json=token.oidc_discover() + list_vos=token.token_list_vos(access_token) + return payload,request_json,list_vos + except TokenError: + err_msg="Not valid ACCESS_TOKEN" + return log_and_raise(err_msg,TokenError) + + + +def printing_dict(var_dict: dict) -> None: + """ Printing dictionary """ + for item in var_dict: + print(f"{item}:\t {var_dict[item]}") + + +if __name__ == "__main__": + print("Start of verifying auth.py") + + access_token1= os.environ.get("FEDCLOUD_ACCESS_TOKEN","") + #access_token_check=verify_access_token(access_token1) + + #payload1,request_json1,list_vos1=verify_pyload(access_token_check) + + mytoken1=os.environ.get("FEDCLOUD_MYTOKEN","") + #access_token_mytok=verify_mytoken(mytoken1) + + oidc_agent_name=os.environ.get("FEDCLOUD_OIDC_AGENT_ACCOUNT","") + print(f"OIDC_AGENT name: {oidc_agent_name}") + access_token_oidc1=verify_oidc_agent(oidc_agent_name) + + user_id1=verify_user_id(access_token_oidc1) + payload1,request_json1,list_vos1=verify_pyload(access_token_oidc1) + + + print(f"{type(payload1)}") + printing_dict(payload1) + print("-------------------------------------------------") + printing_dict(request_json1) + print("-------------------------------------------------") + print(list_vos1) + print("Break") diff --git a/fedcloudclient/checkin.py b/fedcloudclient/checkin.py index cced568..9071643 100644 --- a/fedcloudclient/checkin.py +++ b/fedcloudclient/checkin.py @@ -3,327 +3,42 @@ access tokens """ -import re -import sys -import time -from datetime import datetime - import click -import jwt -import liboidcagent as agent -import requests - -from fedcloudclient.decorators import ( - DEFAULT_OIDC_URL, - oidc_access_token_params, - oidc_params, - oidc_params_with_url, - oidc_refresh_token_params, -) - -# Minimal lifetime of the access token is 30s and max 24h -_MIN_ACCESS_TOKEN_TIME = 30 -_MAX_ACCESS_TOKEN_TIME = 24 * 3600 - -VO_PATTERN = "urn:mace:egi.eu:group:(.+?):(.+:)*role=member#aai.egi.eu" - - -def print_error(message, quiet): - """ - Print error message to stderr if not quiet - """ - if not quiet: - print(message, file=sys.stderr) - - -def oidc_discover(oidc_url): - """ - Discover OIDC endpoints - - :param oidc_url: CheckIn URL - - :return: JSON object of OIDC configuration - """ - request = requests.get(oidc_url + "/.well-known/openid-configuration") - request.raise_for_status() - return request.json() - - -def token_refresh(oidc_client_id, oidc_client_secret, oidc_refresh_token, oidc_url): - """ - Helper function for retrieving JSON object with access token - - :param oidc_client_id: - :param oidc_client_secret: - :param oidc_refresh_token: - :param oidc_url: - - :return: JSON object with access token - """ +from fedcloudclient.auth import OIDCToken +from fedcloudclient.decorators import oidc_params - oidc_ep = oidc_discover(oidc_url) - refresh_data = { - "client_id": oidc_client_id, - "client_secret": oidc_client_secret, - "grant_type": "refresh_token", - "refresh_token": oidc_refresh_token, - "scope": "openid email profile eduperson_entitlement", - } - - request = requests.post( - oidc_ep["token_endpoint"], - auth=(oidc_client_id, oidc_client_secret), - data=refresh_data, - ) - request.raise_for_status() - return request.json() - - -def refresh_access_token( - oidc_client_id, - oidc_client_secret, - oidc_refresh_token, - oidc_url, - quiet=False, -): - """ - Retrieve access token in plain text (string) - - :param oidc_client_id: - :param oidc_client_secret: - :param oidc_refresh_token: - :param oidc_url: - :param quiet: If true, print no error message - - :return: access token or None on error - """ - if oidc_refresh_token: - if not (oidc_client_id and oidc_client_secret and oidc_url): - print_error( - "Error: Client ID and secret required together with refresh token", - quiet, - ) - return None - - print_error( - "Warning: Exposing refresh tokens is insecure and will be deprecated!", - quiet, - ) - try: - access_token = token_refresh( - oidc_client_id, oidc_client_secret, oidc_refresh_token, oidc_url - ) - return access_token["access_token"] - except requests.exceptions.RequestException as exception: - print_error( - "Error during getting access token from refresh token\n" - f"Error message: {exception}", - quiet, - ) - return None - - -def get_token_from_oidc_agent(oidc_agent_account, quiet=False): +@click.group() +def token(): """ - Get access token from oidc-agent - - :param quiet: If true, print no error message - :param oidc_agent_account: account name in oidc-agent - - :return: access token, or None on error + Get details of access token """ - if oidc_agent_account: - try: - access_token = agent.get_access_token( - oidc_agent_account, - min_valid_period=_MIN_ACCESS_TOKEN_TIME, - application_hint="fedcloudclient", - ) - return access_token - except agent.OidcAgentError as exception: - print_error( - "Error during getting access token from oidc-agent\n" - f"Error message: {exception}", - quiet, - ) - return None - - -def check_token(oidc_token, quiet=False, verbose=False, refresh_token=False): +@token.command() +@oidc_params +def check(access_token): """ Check validity of access token - - :param oidc_token: the token to check - :param refresh_token: the provided token is refresh token - :param verbose: If true, print additional info - :param quiet: If true, print no error message - - :return: - """ - if oidc_token: - # Check expiration time of access token - try: - payload = jwt.decode(oidc_token, options={"verify_signature": False}) - except jwt.exceptions.InvalidTokenError: - print_error("Error: Invalid access token.", quiet) - return None - - exp_timestamp = int(payload["exp"]) - current_timestamp = int(time.time()) - exp_time_in_sec = exp_timestamp - current_timestamp - - if exp_time_in_sec < _MIN_ACCESS_TOKEN_TIME: - print_error("Error: Expired access token.", quiet) - return None - - if exp_time_in_sec > _MAX_ACCESS_TOKEN_TIME and not refresh_token: - print_error( - "Warning: You probably use refresh tokens as access tokens.", - quiet, - ) - return None - - if verbose: - exp_time_str = datetime.utcfromtimestamp(exp_timestamp).strftime( - "%Y-%m-%d %H:%M:%S" - ) - print(f"Token is valid until {exp_time_str} UTC") - if exp_time_in_sec < 24 * 3600: - print(f"Token expires in {exp_time_in_sec} seconds") - else: - exp_time_in_days = exp_time_in_sec // (24 * 3600) - print(f"Token expires in {exp_time_in_days} days") - - return oidc_token - - -def get_checkin_id( - oidc_token, - quiet=False, -): - """ - Get EGI Check-in ID from access token - - :param oidc_token: the token - :param quiet: If true, print no error message - - :return: Check-in ID - """ - try: - payload = jwt.decode(oidc_token, options={"verify_signature": False}) - except jwt.exceptions.InvalidTokenError: - print_error("Error: Invalid access token.", quiet) - return None - - return payload["sub"] - - -def get_access_token( - oidc_access_token, - oidc_refresh_token, - oidc_client_id, - oidc_client_secret, - oidc_url, - oidc_agent_account, -): - """ - Get access token - Generates new access token from oidc-agent or - refresh token (if given), or use existing token - - Check expiration time of access token - Raise error if no valid token exists - - :param oidc_access_token: - :param oidc_refresh_token: - :param oidc_client_id: - :param oidc_client_secret: - :param oidc_url: - :param oidc_agent_account: - - :return: access token - """ - - # First, try to get access token from oidc-agent - access_token = get_token_from_oidc_agent(oidc_agent_account) - if access_token: - return access_token - - # Then try refresh token - access_token = refresh_access_token( - oidc_client_id, oidc_client_secret, oidc_refresh_token, oidc_url - ) - if access_token: - return access_token - - # Then finally access token - access_token = check_token(oidc_access_token) - if access_token: - return access_token - - # Nothing available - raise SystemExit( - "Error: An access token is needed for the operation. You can specify " - "access token directly via --oidc-access-token option or use oidc-agent " - "via --oidc-agent-account" - ) - - -def token_list_vos(oidc_access_token, oidc_url): - """ - List VO memberships in EGI Check-in - - :param oidc_access_token: - :param oidc_url: - - :return: list of VO names - """ - oidc_ep = oidc_discover(oidc_url) - request = requests.get( - oidc_ep["userinfo_endpoint"], - headers={"Authorization": f"Bearer {oidc_access_token}"}, - ) - - request.raise_for_status() - vos = set() - pattern = re.compile(VO_PATTERN) - for claim in request.json().get("eduperson_entitlement", []): - vo = pattern.match(claim) - if vo: - vos.add(vo.groups()[0]) - return sorted(vos) - - -@click.group() -def token(): - """ - Get details of access/refresh tokens """ + token_check=OIDCToken() + token_check.check_token(access_token, verbose=True) @token.command() -@oidc_refresh_token_params -@oidc_access_token_params -def check(oidc_refresh_token, oidc_access_token): +@oidc_params +def list_vos(access_token): """ - Check validity of access/refresh token + List VO membership(s) of access token """ - - if oidc_refresh_token: - check_token(oidc_refresh_token, verbose=True, refresh_token=True) - elif oidc_access_token: - check_token(oidc_access_token, verbose=True) - else: - raise SystemExit("OIDC access token or refresh token required") + token_vos=OIDCToken(access_token) + vos = token_vos.token_list_vos(access_token) + print("\n".join(vos)) @token.command() -@oidc_params_with_url -def list_vos(access_token, oidc_url): +@oidc_params +def issue(access_token): """ - List VO membership(s) of access token + print access token (from mytoken or oidc-agent) """ - vos = token_list_vos(access_token, oidc_url) - print("\n".join(vos)) + print(access_token) diff --git a/fedcloudclient/cli.py b/fedcloudclient/cli.py index 648e87e..3165572 100644 --- a/fedcloudclient/cli.py +++ b/fedcloudclient/cli.py @@ -6,7 +6,7 @@ from fedcloudclient.checkin import token from fedcloudclient.config import config -from fedcloudclient.ec3 import ec3 +from fedcloudclient.conf import config from fedcloudclient.endpoint import endpoint from fedcloudclient.openstack import openstack, openstack_int from fedcloudclient.secret import secret @@ -24,7 +24,6 @@ def cli(): cli.add_command(token) cli.add_command(endpoint) -cli.add_command(ec3) cli.add_command(site) cli.add_command(secret) cli.add_command(select) @@ -33,5 +32,6 @@ def cli(): cli.add_command(config) + if __name__ == "__main__": cli() diff --git a/fedcloudclient/conf.py b/fedcloudclient/conf.py new file mode 100644 index 0000000..33272e3 --- /dev/null +++ b/fedcloudclient/conf.py @@ -0,0 +1,250 @@ +""" +Read/write configuration files +""" +import json +import os +import sys +from pathlib import Path +import textwrap + +import click +import yaml +from tabulate import tabulate +from fedcloudclient.exception import ConfigError + + + +DEFAULT_CONFIG_LOCATION = Path.home() / ".config/fedcloud/config.yaml" +DEFAULT_SETTINGS = { + "site": "IISAS-FedCloud", + "vo": "vo.access.egi.eu", + "site_list_url": "https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml", + "site_dir": str(Path.home() / ".config/fedcloud/site-config/"), + "oidc_url": "https://aai.egi.eu/auth/realms/egi", + "gocdb_public_url": "https://goc.egi.eu/gocdbpi/public/", + "gocdb_service_group": "org.openstack.nova", + "vault_endpoint": "https://vault.services.fedcloud.eu:8200", + "vault_role": "", + "vault_mount_point": "/secrets/", + "vault_locker_mount_point": "/v1/cubbyhole/", + "vault_salt": "fedcloud_salt", + "log_file": str(Path.home() / ".config/fedcloud/logs/fedcloud.log"), + "log_level": "DEBUG", + "log_config_file": str(Path.home() / ".config/fedcloud/logging.conf"), + "requests_cert_file": str(Path.home() / ".config/fedcloud/cert/certs.pem"), + "oidc_agent_account": "egi", + "min_access_token_time": 30, + "mytoken_server": "https://mytoken.data.kit.edu", + "os_protocol": "openid", + "os_auth_type": "v3oidcaccesstoken", + "os_identity_provider": "egi.eu", + "_MIN_ACCESS_TOKEN_TIME": 30 +} + +def init_default_config(): + """ + Initialize the default configuration settings. + + :return: + A dictionary containing the default settings as defined by `DEFAULT_SETTINGS`. + :rtype: dict + """ + default_config_init=DEFAULT_SETTINGS + return default_config_init + +def save_config(filename: str, config_data: dict): + """ + Save a configuration dictionary to a YAML file. + + :param filename: + Path to the configuration file to write. + :type filename: str + + :param config_data: + A mapping of configuration keys to values to be serialized. + :type config_data: dict + + :raises ConfigError: + If writing the YAML fails (wraps `yaml.YAMLError`). + """ + config_file = Path(filename).resolve() + try: + with config_file.open(mode="w+", encoding="utf-8") as file: + yaml.dump(config_data, file) + except yaml.YAMLError as exception: + error_msg = f"Error during saving configuration to {filename}: {exception}" + raise ConfigError(error_msg) from exception + + +def load_config(filename: str) -> dict: + """ + Load configuration data from a YAML file. + + :param filename: + Path to the configuration file to read. + :type filename: str + + :return: + A dictionary of configuration values parsed from the file. + Returns an empty dict if the file does not exist. + :rtype: dict + + :raises ConfigError: + If the file exists but cannot be parsed as valid YAML. + """ + + config_file = Path(filename).resolve() + + if config_file.is_file(): + try: + with config_file.open(mode="r", encoding="utf-8") as file: + return yaml.safe_load(file) + except yaml.YAMLError as exception: + error_msg = f"Error during loading configuration from {filename}: {exception}" + raise ConfigError(error_msg) from exception + else: + return {} + + +def load_env() -> dict: + """ + Load FedCloud client configuration from environment variables. + + Scans the current process environment for variables prefixed with + `FEDCLOUD_`, strips that prefix, converts the remainder to lowercase, + and returns them as configuration entries. + + :return: + A dictionary mapping config keys (lowercased, prefix removed) to + their string values from the environment. + :rtype: dict[str, str] + """ + env_config = {} + for env in os.environ: + if env.startswith("FEDCLOUD_"): + config_key = env[9:].lower() + env_config[config_key] = os.environ[env] + return env_config + + +def init_config() -> dict: + """ + Initialize the FedCloud client configuration. + + This function merges three sources of configuration, in order of precedence: + + 1. `DEFAULT_SETTINGS` (hard-coded defaults). + 2. Environment variables prefixed with `FEDCLOUD_` (stripped of the prefix and lower-cased). + 3. Values loaded from a YAML config file (path can be overridden via `FEDCLOUD_CONFIG_FILE`). + + :return: + A dict containing the merged configuration settings. + Later sources override earlier ones. + :rtype: dict[str, Any] + """ + env_config = load_env() + config_file = env_config.get("config_file", DEFAULT_CONFIG_LOCATION) + + try: + saved_config = load_config(config_file) + except ConfigError: + saved_config = {} + + act_config = {**DEFAULT_SETTINGS, **env_config, **saved_config} + return act_config + +@click.group() +def config(): + """ + Managing fedcloud configurations + """ + +@config.command() +@click.option( + "--config-file", + type=click.Path(dir_okay=False), + default=DEFAULT_CONFIG_LOCATION, + help="configuration file", + envvar="FEDCLOUD_CONFIG_FILE", + show_default=True, +) +def create(config_file: str): + """Create default configuration file""" + save_config(config_file, CONF) + + +@config.command() +@click.option( + "--config-file", + type=click.Path(dir_okay=False), + default=DEFAULT_CONFIG_LOCATION, + help="configuration file", + envvar="FEDCLOUD_CONFIG_FILE", + show_default=True, +) + +@click.option( + "--output-format", + "-f", + required=False, + help="Output format", + type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False), +) + +@click.option( + "--source", "-s", + required=False, + help="Source of configuration data", + type=click.Choice(["DEFAULT_SETTINGS", "env_config", "saved_config"], case_sensitive=False), +) + +def show(config_file: str, output_format: str, source: str): + """ + Display the FedCloud client configuration. + + Merges three layers of configuration—defaults, environment, and on-disk— + then prints one of: + + - the defaults only + - the env-vars only + - the saved file only + - the full merged config + + :param config_file: + Path to the YAML config file on disk. + :type config_file: str + + :param output_format: + One of `"text"`, `"YAML"`, or `"JSON"` (case-insensitive) for the + desired output format. + :type output_format: str + + :param source: + If provided, limits display to one of `"DEFAULT_SETTINGS"`, + `"env_config"`, or `"saved_config"`. Otherwise shows the full merge. + :type source: Optional[str] + + :return: + None + :rtype: None + """ + saved_config = load_config(config_file) + env_config = load_env() + default_settings=init_default_config() + if source is not None: + act_config = vars()[source] + else: + act_config = {**default_settings, **env_config, **saved_config} + if output_format == "YAML": + yaml.dump(act_config, sys.stdout, sort_keys=False) + elif output_format == "JSON": + json.dump(act_config, sys.stdout, indent=4) + else: + wrapped_data = [ + ["\n".join(textwrap.wrap(cell, width=200)) if isinstance(cell, str) else cell for cell in row] + for row in act_config.items()] + + #print(tabulate(act_config.items(), headers=["parameter", "value"])) + print(tabulate(wrapped_data, headers=["parameter", "value"])) + +CONF = init_config() diff --git a/fedcloudclient/conf_test.py b/fedcloudclient/conf_test.py new file mode 100644 index 0000000..4b0c67f --- /dev/null +++ b/fedcloudclient/conf_test.py @@ -0,0 +1,57 @@ +""" +Testing unit for auth.py +""" + +import os + +from fedcloudclient.conf import CONF + + +def save_load_compare(): + """ + Save config to a temp file, load it and compare the result + """ + + config_data = { + "env1": "value1", + "env2": "value2", + "env3": "value3", + } + + config_file = "/tmp/test" + + CONF.save_config(config_file, config_data) + new_config = CONF.load_config(config_file) + + assert new_config == config_data + + +def load_env_merge_compare(): + """ + set OS env, load, and compare the result + """ + config_data = { + "env1": "value1", + "env2": "value2", + "env3": "value3", + } + + config_file = "/tmp/test" + + CONF.save_config(config_file, config_data) + saved_config = CONF.load_config(config_file) + + os.environ["FEDCLOUD_ENV1"] = "value10" + os.environ["FEDCLOUD_ENV4"] = "value4" + + env_config = CONF.load_env() + + act_config = {**config_data, **saved_config, **env_config} + + assert act_config["env1"] == "value10" + assert act_config["env4"] == "value4" + + +if __name__ == "__main__": + save_load_compare() + load_env_merge_compare() diff --git a/fedcloudclient/decorators.py b/fedcloudclient/decorators.py index 7fdaf34..2662381 100644 --- a/fedcloudclient/decorators.py +++ b/fedcloudclient/decorators.py @@ -1,19 +1,21 @@ """ Decorators for command-line parameters """ +import os from functools import wraps import click from click_option_group import ( RequiredAnyOptionGroup, - RequiredMutuallyExclusiveOptionGroup, - optgroup, + optgroup, MutuallyExclusiveOptionGroup, ) -DEFAULT_OIDC_URL = "https://aai.egi.eu/auth/realms/egi" -DEFAULT_PROTOCOL = "openid" -DEFAULT_AUTH_TYPE = "v3oidcaccesstoken" -DEFAULT_IDENTITY_PROVIDER = "egi.eu" +from fedcloudclient.conf import CONF +from fedcloudclient.exception import TokenError +from fedcloudclient.locker_auth import LockerToken +from fedcloudclient.vault_auth import VaultToken +from fedcloudclient.auth import OIDCToken +from fedcloudclient.logger import log_and_raise ALL_SITES_KEYWORDS = {"ALL_SITES", "ALL-SITES"} @@ -21,15 +23,7 @@ oidc_access_token_params = click.option( "--oidc-access-token", help="OIDC access token", - envvar="OIDC_ACCESS_TOKEN", - metavar="token", -) - -# Decorator for --oidc-refresh-token -oidc_refresh_token_params = click.option( - "--oidc-refresh-token", - help="OIDC refresh token", - envvar="OIDC_REFRESH_TOKEN", + default=CONF.get("oidc_access_token"), metavar="token", ) @@ -37,11 +31,19 @@ site_params = click.option( "--site", help="Name of the site", - required=True, - envvar="EGI_SITE", + default=CONF.get("site"), metavar="site-name", ) +# Output format for secret module +secret_output_params = click.option( + "--output-format", + "-f", + required=False, + help="Output format", + type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False), +) + def all_site_params(func): """ @@ -50,13 +52,13 @@ def all_site_params(func): @optgroup.group( "Site", - cls=RequiredMutuallyExclusiveOptionGroup, + cls=MutuallyExclusiveOptionGroup, help="Single Openstack site or all sites", ) @optgroup.option( "--site", help="Name of the site or ALL_SITES", - envvar="EGI_SITE", + default=CONF.get("site"), metavar="site-name", ) @optgroup.option( @@ -76,8 +78,7 @@ def wrapper(*args, **kwargs): project_id_params = click.option( "--project-id", help="Project ID", - required=True, - envvar="OS_PROJECT_ID", + default=CONF.get("project_id"), metavar="project-id", ) @@ -94,7 +95,15 @@ def wrapper(*args, **kwargs): vo_params = click.option( "--vo", help="Name of the VO", - required=True, + default=CONF.get("vo"), + metavar="vo-name", +) + +# Optional decorator for --vo +vo_params_optional = click.option( + "--vo", + help="Name of the VO", + required=False, envvar="EGI_VO", metavar="vo-name", ) @@ -124,121 +133,39 @@ def oidc_params(func): @optgroup.option( "--oidc-agent-account", help="Account name in oidc-agent", - envvar="OIDC_AGENT_ACCOUNT", + default=CONF.get("oidc_agent_account"), metavar="account", ) @optgroup.option( "--oidc-access-token", help="OIDC access token", - envvar="OIDC_ACCESS_TOKEN", + default=os.getenv("FEDCLOUD_OIDC_ACCESS_TOKEN"), metavar="token", ) @optgroup.option( - "--oidc-refresh-token", - help="OIDC refresh token. Require also client ID and secret", - envvar="OIDC_REFRESH_TOKEN", - metavar="token", - ) - @optgroup.option( - "--oidc-client-id", - help="OIDC client ID", - envvar="OIDC_CLIENT_ID", - metavar="id", - ) - @optgroup.option( - "--oidc-client-secret", - help="OIDC client secret", - envvar="OIDC_CLIENT_SECRET", - metavar="secret", + "--mytoken", + help="Mytoken string", + default=CONF.get("mytoken"), + metavar="mytoken", ) @optgroup.option( - "--oidc-url", - help="OIDC identity provider URL", - envvar="OIDC_URL", - default=DEFAULT_OIDC_URL, - show_default=True, - metavar="provider-url", + "--mytoken-server", + help="Mytoken sever", + default=CONF.get("mytoken_server"), + metavar="url", ) @wraps(func) def wrapper(*args, **kwargs): - from fedcloudclient.checkin import get_access_token - access_token = get_access_token( + token=OIDCToken() + access_token = token.multiple_token( kwargs.pop("oidc_access_token"), - kwargs.pop("oidc_refresh_token"), - kwargs.pop("oidc_client_id"), - kwargs.pop("oidc_client_secret"), - kwargs.pop("oidc_url"), kwargs.pop("oidc_agent_account"), - ) + kwargs.pop("mytoken"), + kwargs.pop("mytoken_server"), + ) # pylint: disable=assignment-from-none kwargs["access_token"] = access_token - return func(*args, **kwargs) - - return wrapper - - -def oidc_params_with_url(func): - """ - Decorator for OIDC parameters. - Get access token from oidc-* parameters and replace them in the wrapper function - Also adds the OIDC URL as part of the call to the inner function - """ - - @optgroup.group("OIDC token", help="Choose one of options for providing token") - @optgroup.option( - "--oidc-agent-account", - help="Account name in oidc-agent", - envvar="OIDC_AGENT_ACCOUNT", - metavar="account", - ) - @optgroup.option( - "--oidc-access-token", - help="OIDC access token", - envvar="OIDC_ACCESS_TOKEN", - metavar="token", - ) - @optgroup.option( - "--oidc-refresh-token", - help="OIDC refresh token. Require also client ID and secret", - envvar="OIDC_REFRESH_TOKEN", - metavar="token", - ) - @optgroup.option( - "--oidc-client-id", - help="OIDC client ID", - envvar="OIDC_CLIENT_ID", - metavar="id", - ) - @optgroup.option( - "--oidc-client-secret", - help="OIDC client secret", - envvar="OIDC_CLIENT_SECRET", - metavar="secret", - ) - @optgroup.option( - "--oidc-url", - help="OIDC identity provider URL", - envvar="OIDC_URL", - default=DEFAULT_OIDC_URL, - show_default=True, - metavar="provider-url", - ) - @wraps(func) - def wrapper(*args, **kwargs): - from fedcloudclient.checkin import get_access_token - - oidc_url = kwargs.pop("oidc_url") - access_token = get_access_token( - kwargs.pop("oidc_access_token"), - kwargs.pop("oidc_refresh_token"), - kwargs.pop("oidc_client_id"), - kwargs.pop("oidc_client_secret"), - oidc_url, - kwargs.pop("oidc_agent_account"), - ) - kwargs["access_token"] = access_token - kwargs["oidc_url"] = oidc_url return func(*args, **kwargs) return wrapper @@ -254,26 +181,23 @@ def openstack_params(func): help="Only change default values if necessary", ) @optgroup.option( - "--openstack-auth-protocol", + "--os-protocol", help="Authentication protocol", - envvar="OPENSTACK_AUTH_PROTOCOL", - default=DEFAULT_PROTOCOL, + default=CONF.get("os_protocol"), show_default=True, metavar="", ) @optgroup.option( - "--openstack-auth-provider", + "--os-identity-provider", help="Identity provider", - envvar="OPENSTACK_AUTH_PROVIDER", - default=DEFAULT_IDENTITY_PROVIDER, + default=CONF.get("os_identity_provider"), show_default=True, metavar="", ) @optgroup.option( - "--openstack-auth-type", + "--os-auth-type", help="Authentication type", - envvar="OPENSTACK_AUTH_TYPE", - default=DEFAULT_AUTH_TYPE, + default=CONF.get("os_auth_type"), show_default=True, metavar="", ) @@ -421,3 +345,70 @@ def wrapper(*args, **kwargs): return func(*args, **kwargs) return wrapper + + +def secret_token_params(func): + """ + Decorator for secret token. + If locker token is not defined, get access token from oidc-* parameters + and replace them in the wrapper function + """ + + @optgroup.group("Token options", help="Choose one of options for providing token") + @optgroup.option( + "--locker-token", + help="Locker token", + default=CONF.get("locker_token"), + metavar="locker_token", + ) + @optgroup.option( + "--vault-token", + help="Vault token", + default=CONF.get("vault_token"), + metavar="vault_token", + ) + @optgroup.option( + "--oidc-agent-account", + help="Account name in oidc-agent", + default=CONF.get("oidc_agent_account"), + metavar="account", + ) + @optgroup.option( + "--oidc-access-token", + help="OIDC access token", + default=CONF.get("oidc_access_token"), + metavar="token", + ) + @optgroup.option( + "--mytoken", + help="Mytoken string", + default=CONF.get("mytoken"), + metavar="mytoken", + ) + @wraps(func) + def wrapper(*args, **kwargs): + + # If locker token is given, ignore OIDC token options + locker_token = kwargs.pop("locker_token") + vault_token = kwargs.pop("vault_token") + access_token = kwargs.pop("oidc_access_token") + oidc_agent_account = kwargs.pop("oidc_agent_account") + mytoken = kwargs.pop("mytoken") + + if locker_token: + token = LockerToken(locker_token=locker_token) + elif vault_token: + token = VaultToken(vault_token=vault_token) + else: + token = VaultToken() + try: + token.multiple_token(access_token, oidc_agent_account, mytoken) + except TokenError: + error_msg=f"Can not access to the ACCESS_TOKEN: {TokenError}" + log_and_raise(error_msg, TokenError) + SystemExit(1) + + kwargs["token"] = token + return func(*args, **kwargs) + + return wrapper diff --git a/fedcloudclient/ec3.py b/fedcloudclient/ec3.py deleted file mode 100644 index 09fe21b..0000000 --- a/fedcloudclient/ec3.py +++ /dev/null @@ -1,198 +0,0 @@ -""" -Implementation of ec3 commands for deploying EC3 (Elastic Cloud Computing -Cluster) in Cloud via Infrastructure Manager -""" - -import os -import time - -import click -import jwt - -from fedcloudclient.decorators import ( - ALL_SITES_KEYWORDS, - auth_file_params, - oidc_params, -) -from fedcloudclient.sites import find_endpoint_and_project_id, site_vo_params - -__MIN_EXPIRATION_TIME = 300 - -EC3_REFRESHTOKEN_TEMPLATE = """ -description refreshtoken ( - kind = 'component' and - short = 'Tool to refresh LToS access token.' and - content = 'Tool to refresh LToS access token.' -) -configure front ( -@begin - - vars: - CLIENT_ID: %(client_id)s - CLIENT_SECRET: %(client_secret)s - REFRESH_TOKEN: %(refresh_token)s - tasks: - - name: Check if docker is available - command: which docker - changed_when: false - failed_when: docker_installed.rc not in [0,1] - register: docker_installed - - name: local install of fedcloudclient - block: - - name: Create dir /usr/local/ec3/ - file: path=/usr/local/ec3/ state=directory - - name: install git - package: - name: git - state: present - - name: install fedcloudclient - pip: - name: - - fedcloudclient - - cron: - name: "refresh token" - minute: "*/5" - job: "[ -f /usr/local/ec3/auth.dat ] - && /usr/local/bin/fedcloudclient endpoint ec3-refresh - --oidc-client-id {{ CLIENT_ID }} - --oidc-client-secret {{ CLIENT_SECRET }} - --oidc-refresh-token {{ REFRESH_TOKEN }} - --auth-file /usr/local/ec3/auth.dat &> /var/log/refresh.log" - user: root - cron_file: refresh_token - state: present - when: docker_installed.rc not in [ 0 ] - - name: local install of fedcloudclient - block: - - cron: - name: "refresh token" - minute: "*/5" - job: "[ -f /usr/local/ec3/auth.dat ] - && docker run -v /usr/local/ec3/auth.dat:/usr/local/ec3/auth.dat - tdviet/fedcloudclient fedcloudclient endpoint ec3-refresh - --oidc-client-id {{ CLIENT_ID }} - --oidc-client-secret {{ CLIENT_SECRET }} - --oidc-refresh-token {{ REFRESH_TOKEN }} - --auth-file /usr/local/ec3/auth.dat &> /var/log/refresh.log" - user: root - cron_file: refresh_token - state: present - when: docker_installed.rc not in [ 1 ] -@end -) -""" - - -@click.group() -def ec3(): - """ - EC3 cluster provisioning - """ - - -@ec3.command() -@oidc_params -@auth_file_params -def refresh( - access_token, - auth_file, -): - """ - Refresh token in EC3 authorization file - """ - # Get the right endpoint from GOCDB - auth_file_contents = [] - with open(auth_file, "r") as file: - for raw_line in file.readlines(): - line = raw_line.strip() - if "OpenStack" in line: - auth_tokens = [] - for token in line.split(";"): - if token.strip().startswith("password"): - current_access_token = token.split("=")[1].strip() - if current_access_token[0] in ["'", '"']: - current_access_token = current_access_token[1:-1] - # FIXME(enolfc): add verification - payload = jwt.decode( - current_access_token, options={"verify_signature": False} - ) - now = int(time.time()) - expires = int(payload["exp"]) - if expires - now < __MIN_EXPIRATION_TIME: - current_access_token = access_token - auth_tokens.append(f"password = {current_access_token}") - else: - auth_tokens.append(token.strip()) - auth_file_contents.append("; ".join(auth_tokens)) - elif line: - auth_file_contents.append(line) - - with open(auth_file, "w+") as file: - file.write("\n".join(auth_file_contents)) - - -@ec3.command() -@site_vo_params -@oidc_params -@auth_file_params -@click.option( - "--template-dir", - help="EC3 templates dir", - default="./templates", - show_default=True, -) -@click.option("--force", is_flag=True, help="Force rewrite of files") -def init( - access_token, - site, - vo, - auth_file, - template_dir, - force, -): - """ - Create EC3 authorization file and template - """ - if os.path.exists(auth_file) and not force: - print( - "Auth file already exists, not replacing unless --force option is included" - ) - raise click.Abort() - - if site in ALL_SITES_KEYWORDS: - print("EC3 commands cannot be used with ALL_SITES") - raise click.Abort() - - endpoint, project_id, protocol = find_endpoint_and_project_id(site, vo) - site_auth = [ - f"id = {site}", - "type = OpenStack", - "username = egi.eu", - f"tenant = {protocol}", - "auth_version = 3.x_oidc_access_token", - f"host = {endpoint}", - f"domain = {project_id}", - f"password = {access_token}", - ] - auth_file_contents = [";".join(site_auth)] - - if os.path.exists(auth_file): - with open(auth_file, "r") as file: - for line in file.readlines(): - if "OpenStack" in line: - continue - auth_file_contents.append(line) - - with open(auth_file, "w+") as file: - file.write("\n".join(auth_file_contents)) - - if not os.path.exists(template_dir): - os.mkdir(template_dir) - - # FIXME: this should not be used at all! - with open(os.path.join(template_dir, "refresh.radl"), "w+") as file: - token = dict( # nosec - client_id="ADD_CLIENT_ID_HERE", - client_secret="ADD_CLIENT_SECRET_HERE", - refresh_token="ADD_REFRESH_TOKEN_HERE", - ) - file.write(EC3_REFRESHTOKEN_TEMPLATE % token) diff --git a/fedcloudclient/endpoint.py b/fedcloudclient/endpoint.py index 7160ed9..53fe6a2 100644 --- a/fedcloudclient/endpoint.py +++ b/fedcloudclient/endpoint.py @@ -283,9 +283,9 @@ def vos( project_list, project_error_list = get_projects_from_sites(access_token, site) if len(project_list) > 0: - for p in project_list: - vo = find_vo_from_project_id(p[3], p[0]) - p.insert(0, vo) + for project in project_list: + vo = find_vo_from_project_id(project[3], project[0]) + project.insert(0, vo) print( tabulate( project_list, headers=["VO", "id", "Project name", "enabled", "site"] diff --git a/fedcloudclient/exception.py b/fedcloudclient/exception.py new file mode 100644 index 0000000..1bd3223 --- /dev/null +++ b/fedcloudclient/exception.py @@ -0,0 +1,27 @@ +""" +Define custom exceptions for fedcloudclient +""" + + +class FedcloudError(Exception): + """Master class for all custom exceptions in fedcloudclient.""" + def __init__(self, message="An unspecified Fedcloud error occurred"): + super().__init__(message) + + +class TokenError(FedcloudError): + """Authentication error, token not initialized or recognized""" + def __init__(self, message="Authentication token error"): + super().__init__(message) + + +class ServiceError(FedcloudError): + """Connection timeout, service not available and so on.""" + def __init__(self, message="Service communication error"): + super().__init__(message) + + +class ConfigError(FedcloudError): + """Configuration error, file does not exist and so on.""" + def __init__(self, message="Configuration error"): + super().__init__(message) diff --git a/fedcloudclient/locker_auth.py b/fedcloudclient/locker_auth.py new file mode 100644 index 0000000..d973682 --- /dev/null +++ b/fedcloudclient/locker_auth.py @@ -0,0 +1,87 @@ +""" +Class for managing Vault locker tokens +""" +import requests + +from fedcloudclient.conf import CONF +from fedcloudclient.exception import ConfigError, ServiceError, TokenError +from fedcloudclient.logger import log_and_raise +from fedcloudclient.vault_auth import VaultToken + + +class LockerToken(VaultToken): + """ + Managing Vault locker token + """ + + def __init__(self, locker_token: str): + """ + Init Locker token + :param locker_token: + """ + if locker_token: + super().__init__(vault_token=locker_token) + else: + log_and_raise("Locker token cannot be empty", TokenError) + + def get_user_id(self): + """ + User ID is not available for locker token + :return: + """ + log_and_raise("User ID is not available for locker token", TokenError) + + def get_vault_user_id(self): + """ + Get Vault user ID from Vault token + :return: + """ + log_and_raise("User ID is not available for locker token", TokenError) + + def get_vault_auth_method(self): + """ + Get Vault user ID from Vault token + :return: + """ + log_and_raise("Auth method is not available for locker token", TokenError) + + def vault_command(self, command: str, path: str, data: dict, vo: str = None): + """ + Perform Vault command + :param command: + :param path: + :param data: + :param vo: + :return: + """ + if vo: + log_and_raise("VO-shared is not supported by locker token", TokenError) + + try: + headers = {"X-Vault-Token": self.vault_token} + url = CONF.get("vault_endpoint") + CONF.get("vault_locker_mount_point") + path + if command == "list": + response = requests.get(url, headers=headers, params={"list": "true"}) + elif command == "get": + response = requests.get(url, headers=headers) + elif command == "delete": + response = requests.delete(url, headers=headers) + elif command == "put": + response = requests.post(url, headers=headers, data=data) + else: + msg_err=f"Invalid command {command}" + log_and_raise(msg_err, ConfigError) + response=None + + if response is not None: + response.raise_for_status() + + if command in ["list", "get"]: + response_json = response.json() + return dict(response_json) + + return None + except requests.exceptions.HTTPError as exception: + error_msg = f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(error_msg, ServiceError) + return None diff --git a/fedcloudclient/locker_auth_test.py b/fedcloudclient/locker_auth_test.py new file mode 100644 index 0000000..b4399e4 --- /dev/null +++ b/fedcloudclient/locker_auth_test.py @@ -0,0 +1,20 @@ +""" +Testing vault_auth.py +""" +import os + +import fedcloudclient.locker_auth as locker + + +def test_get_locker_secret(locker_token: str): + """ + Test getting VO-shared secrets + """ + token = locker.LockerToken(locker_token=locker_token) + response = token.vault_command(command="read_secret", path="test", data={}, vo=None) + assert response["data"]["test"] == "test" + + +if __name__ == "__main__": + locker_token_main = os.environ["FEDCLOUD_LOCKER_TOKEN"] + test_get_locker_secret(locker_token_main) diff --git a/fedcloudclient/logger.py b/fedcloudclient/logger.py new file mode 100644 index 0000000..b44067f --- /dev/null +++ b/fedcloudclient/logger.py @@ -0,0 +1,40 @@ +""" +Logger configuration +""" + +import logging +import logging.config +from pathlib import Path + +from fedcloudclient.conf import CONF + + +def init_logger(): + """ + Init logger + :return: + """ + log_file = CONF.get("log_file") + log_level = CONF.get("log_level") + Path(log_file).parent.mkdir(parents=True, exist_ok=True) + try: + logging.config.fileConfig( + fname=CONF.get("log_config_file"), + disable_existing_loggers=False, + defaults={"log_file": log_file, "log_level": log_level}, + ) + except (ValueError, TypeError, AttributeError, ImportError, KeyError, FileNotFoundError): + logging.basicConfig(filename=log_file, level=logging.getLevelName(log_level)) + + +def log_and_raise(error_msg: str, exception): + """ + Log error and raise exception + """ + LOG.error(error_msg) + raise exception(error_msg) + + +init_logger() + +LOG = logging.getLogger("fedcloudclient") diff --git a/fedcloudclient/openstack.py b/fedcloudclient/openstack.py index f76b3c1..e4462e0 100644 --- a/fedcloudclient/openstack.py +++ b/fedcloudclient/openstack.py @@ -9,14 +9,12 @@ import subprocess # nosec Subprocess is required for invoking openstack client import sys from distutils.spawn import find_executable - import click +from fedcloudclient.logger import log_and_raise +from fedcloudclient.conf import CONF from fedcloudclient.decorators import ( ALL_SITES_KEYWORDS, - DEFAULT_AUTH_TYPE, - DEFAULT_IDENTITY_PROVIDER, - DEFAULT_PROTOCOL, all_site_params, oidc_params, openstack_output_format_params, @@ -29,6 +27,10 @@ list_sites, ) +DEFAULT_AUTH_TYPE = CONF.get("os_auth_type") +DEFAULT_IDENTITY_PROVIDER = CONF.get("os_identity_provider") +DEFAULT_PROTOCOL = CONF.get("os_protocol") + __OPENSTACK_CLIENT = "openstack" __MAX_WORKER_THREAD = 30 __MISSING_VO_ERROR_CODE = 11 @@ -38,9 +40,9 @@ def fedcloud_openstack_full( oidc_access_token, - openstack_auth_protocol, - openstack_auth_type, - checkin_identity_provider, + os_protocol, + os_auth_type, + os_identity_provider, site, vo, openstack_command, @@ -52,11 +54,12 @@ def fedcloud_openstack_full( :param oidc_access_token: Checkin access token. Passed to openstack client as --os-access-token - :param openstack_auth_protocol: Checkin protocol (openid, oidc). Passed to + :param os_protocol: Checkin protocol (openi + , oidc). Passed to openstack client as --os-protocol - :param openstack_auth_type: Checkin authentication type (v3oidcaccesstoken). + :param os_auth_type: Checkin authentication type (v3oidcaccesstoken). Passed to openstack client as --os-auth-type - :param checkin_identity_provider: Checkin identity provider in mapping (egi.eu). + :param os_identity_provider: Checkin identity provider in mapping (egi.eu). Passed to openstack client as --os-identity-provider :param site: site ID in GOCDB :param vo: VO name @@ -65,6 +68,7 @@ def fedcloud_openstack_full( :param json_output: if result is JSON object or string. Default:True :return: error code, result or error message + """ endpoint, project_id, protocol = find_endpoint_and_project_id(site, vo) @@ -72,17 +76,17 @@ def fedcloud_openstack_full( return __MISSING_VO_ERROR_CODE, f"VO {vo} not found on site {site}\n" if protocol is None: - protocol = openstack_auth_protocol + protocol = os_protocol options = ( "--os-auth-url", endpoint, "--os-auth-type", - openstack_auth_type, + os_auth_type, "--os-protocol", protocol, "--os-identity-provider", - checkin_identity_provider, + os_identity_provider, "--os-access-token", oidc_access_token, ) @@ -105,7 +109,7 @@ def fedcloud_openstack_full( (__OPENSTACK_CLIENT,) + openstack_command + options, stdout=subprocess.PIPE, stderr=subprocess.PIPE, - env=my_env, + env=my_env, check=True ) error_code = completed.returncode @@ -146,6 +150,7 @@ def fedcloud_openstack( :param json_output: if result is JSON object or string. Default:True :return: error code, result or error message + """ return fedcloud_openstack_full( @@ -194,6 +199,7 @@ def print_result( :param ignore_missing_vo: :param first: :return: + """ command = " ".join(command) @@ -238,14 +244,15 @@ def openstack( all_sites, vo, openstack_command, - openstack_auth_protocol, - openstack_auth_type, - openstack_auth_provider, + os_protocol, + os_auth_type, + os_identity_provider, ignore_missing_vo, json_output, ): """ Execute OpenStack commands on site and VO + """ if not check_openstack_client_installation(): @@ -265,9 +272,9 @@ def openstack( executor.submit( fedcloud_openstack_full, access_token, - openstack_auth_protocol, - openstack_auth_type, - openstack_auth_provider, + os_protocol, + os_auth_type, + os_identity_provider, site, vo, openstack_command, @@ -279,14 +286,16 @@ def openstack( # Get results and print them first = True - # Get the result, first come first serve + # Get the result, first come, first served for future in concurrent.futures.as_completed(results): site = results[future] exc_msg = None try: error_code, result = future.result() - except Exception as exc: - exc_msg = exc + except Exception as exception: + msg_err=f"Can not get result in OpenStack: {exception}" + log_and_raise(msg_err, exception) + raise Exception(msg_err) from exception # Print result print_result( @@ -316,12 +325,15 @@ def openstack_int( access_token, site, vo, - openstack_auth_protocol, - openstack_auth_type, - openstack_auth_provider, -): + os_protocol, + os_auth_type, + os_identity_provider, + ): """ Interactive OpenStack client on site and VO + + :return: None + """ if not check_openstack_client_installation(): @@ -332,15 +344,16 @@ def openstack_int( raise SystemExit(f"Error: VO {vo} not found on site {site}") if protocol is None: - protocol = openstack_auth_protocol + protocol = os_protocol my_env = os.environ.copy() my_env["OS_AUTH_URL"] = endpoint - my_env["OS_AUTH_TYPE"] = openstack_auth_type + my_env["OS_AUTH_TYPE"] = os_auth_type my_env["OS_PROTOCOL"] = protocol - my_env["OS_IDENTITY_PROVIDER"] = openstack_auth_provider + my_env["OS_IDENTITY_PROVIDER"] = os_identity_provider my_env["OS_ACCESS_TOKEN"] = access_token my_env["OS_PROJECT_ID"] = project_id # Calling OpenStack client as subprocess # Ignore bandit warning - subprocess.run(__OPENSTACK_CLIENT, env=my_env) # nosec + subprocess.run(__OPENSTACK_CLIENT, env=my_env, check=True) # nosec + return None diff --git a/fedcloudclient/schema.json b/fedcloudclient/schema.json index 1439e28..b52ceef 100644 --- a/fedcloudclient/schema.json +++ b/fedcloudclient/schema.json @@ -5,6 +5,25 @@ "title": "site specs", "description": "site configuration schema", "definitions": { + "imagesdata": { + "type": "object", + "properties": { + "sync": { + "type": "boolean", + "description": "Do image synchronisation", + "default": false + }, + "formats": { + "type": "array", + "title": "formats", + "description": "Supported VM image formats at the site.", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + }, "vodata": { "type": "object", "properties": { @@ -22,17 +41,11 @@ } } }, - "required": [ - "auth", - "name" - ], + "required": ["auth", "name"], "additionalProperties": true } }, - "required": [ - "gocdb", - "endpoint" - ], + "required": ["gocdb", "endpoint"], "properties": { "gocdb": { "$id": "#/properties/gocdb", @@ -70,6 +83,13 @@ "items": { "$ref": "#/definitions/vodata" } + }, + "images": { + "$id": "#/properties/images", + "title": "Image Configuration", + "description": "Image configuration", + "type": "object", + "$ref": "#/definitions/imagesdata" } }, "additionalProperties": false diff --git a/fedcloudclient/secret.py b/fedcloudclient/secret.py index 6ff61ea..5d35f80 100644 --- a/fedcloudclient/secret.py +++ b/fedcloudclient/secret.py @@ -1,29 +1,26 @@ """ Implementation of "fedcloud secret" commands for accessing secret management service """ -import base64 -import json -import os import sys import click import hvac -import yaml - -from cryptography.fernet import Fernet, InvalidToken -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +import requests from hvac.exceptions import VaultError -from tabulate import tabulate -from yaml import YAMLError -from fedcloudclient.checkin import get_checkin_id -from fedcloudclient.decorators import oidc_params +from fedcloudclient.auth import OIDCToken +from fedcloudclient.conf import CONF +from fedcloudclient.decorators import oidc_params, secret_output_params, secret_token_params +from fedcloudclient.logger import LOG, log_and_raise +from fedcloudclient.secret_helper import decrypt_data, encrypt_data, print_secrets, print_value, secret_params_to_dict +from fedcloudclient.vault_auth import VaultToken +from fedcloudclient.exception import ServiceError -VAULT_ADDR = "https://vault.services.fedcloud.eu:8200" -VAULT_ROLE = "demo" -VAULT_MOUNT_POINT = "/secrets" -VAULT_SALT = "fedcloud_salt" +VAULT_ADDR = CONF.get("vault_endpoint") +VAULT_ROLE = CONF.get("vault_role") +VAULT_MOUNT_POINT = CONF.get("vault_mount_point") +VAULT_SALT = CONF.get("vault_salt") +VAULT_LOCKER_MOUNT_POINT = CONF.get("vault_locker_mount_point") def secret_client(access_token, command, path, data): @@ -39,8 +36,9 @@ def secret_client(access_token, command, path, data): try: client = hvac.Client(url=VAULT_ADDR) client.auth.jwt.jwt_login(role=VAULT_ROLE, jwt=access_token) - checkin_id = get_checkin_id(access_token) - full_path = checkin_id + "/" + path + token=OIDCToken() + checkin_id = token.get_checkin_id(access_token) + full_path = "users/" + checkin_id + "/" + path function_list = { "list_secrets": client.secrets.kv.v1.list_secrets, "read_secret": client.secrets.kv.v1.read_secret, @@ -58,181 +56,44 @@ def secret_client(access_token, command, path, data): mount_point=VAULT_MOUNT_POINT, ) return response - except VaultError as e: - raise SystemExit( - f"Error: Error when accessing secrets on server. Server response: {type(e).__name__}: {e}" - ) - - -def read_data_from_file(input_format, input_file): - """ - Read data from file. Format may be text, yaml, json or auto-detect according to file extension - :param input_format: - :param input_file: - :return: - """ - - if input_format is None or input_format == "auto-detect": - if input_file.endswith(".json"): - input_format = "json" - else: - # default format - input_format = "yaml" - - try: - - # read text/binary files to strings - if input_format == "binary": - with open(input_file, "rb") if input_file else sys.stdin.buffer as f: - return base64.b64encode(f.read()).decode() - if input_format == "text": - with open(input_file, "r") if input_file else sys.stdin as f: - return f.read() - - # reading YAML or JSON to dict - with open(input_file) if input_file else sys.stdin as f: - if input_format == "yaml": - data = yaml.safe_load(f) - elif input_format == "json": - data = json.load(f) - return dict(data) - - except (ValueError, FileNotFoundError, YAMLError) as e: - raise SystemExit( - f"Error: Error when reading file {input_file}. Error message: {type(e).__name__}: {e}" - ) - - -def secret_params_to_dict(params, binary_file=False): - """ - Convert secret params "key=value" to dict {"key":"value"} - :param binary_file: if reading files as binary - :param params: input string in format "key=value" - :return: dict {"key":"value"} - """ - - result = {} - - if len(params) == 0: - raise SystemExit( - "Error: Expecting 'key=value' arguments for secrets, None provided." - ) - - for param in params: - if param.startswith("@") or param == "-": - data = read_data_from_file(None, param[1:]) - result.update(data) - else: - try: - key, value = param.split("=", 1) - except ValueError: - raise SystemExit( - f"Error: Expecting 'key=value' arguments for secrets. '{param}' provided." - ) - if value.startswith("@") or value == "-": - if binary_file: - value = read_data_from_file("binary", value[1:]) - else: - value = read_data_from_file("text", value[1:]) - result[key] = value - - return result - - -def generate_derived_key(salt, passphrase): - """ - Generate derived encryption/decryption key from salted passphrase - :param salt: - :param passphrase: - :return: derived key - """ - - kdf = PBKDF2HMAC( - algorithm=hashes.SHA256(), - length=32, - salt=salt, - iterations=390000, - ) - return base64.b64encode(kdf.derive(passphrase.encode())) - - -def encrypt_data(encrypt_key, secrets): - """ - Encrypt values in secrets using key - :param encrypt_key: encryption key - :param secrets: dict containing secrets - :return: dict with encrypted values - """ - salt = os.urandom(16) - derived_key = generate_derived_key(salt, encrypt_key) - fernet = Fernet(derived_key) - for key in secrets: - secrets[key] = fernet.encrypt(secrets[key].encode()) - secrets[VAULT_SALT] = base64.b64encode(salt) - - -def decrypt_data(decrypt_key, secrets): - """ - Decrypt values in secrets using key - :param decrypt_key: decryption key - :param secrets: dict containing encrypted secrets - :return: dict with decrypted values - """ - try: - salt = base64.b64decode(secrets.pop(VAULT_SALT)) - derived_key = generate_derived_key(salt, decrypt_key) - fernet = Fernet(derived_key) - for key in secrets: - secrets[key] = fernet.decrypt(secrets[key].encode()).decode() - except InvalidToken as e: - raise SystemExit(f"Error: Error during decryption. {e}") - - -def print_secrets(output_file, output_format, secrets): - """ - Print secrets in different formats - :param output_file: - :param output_format: - :param secrets: - :return: - """ - - try: - with open(output_file, "wt") if output_file else sys.stdout as f: - if output_format == "JSON": - json.dump(secrets, f, indent=4) - elif output_format == "YAML": - yaml.dump(secrets, f, sort_keys=False) - else: - print(tabulate(secrets.items(), headers=["key", "value"]), file=f) - - except (ValueError, FileNotFoundError, YAMLError) as e: - raise SystemExit( - f"Error: Error when writing file {output_file}. Error message: {type(e).__name__}: {e}" - ) + except VaultError as err: + err_msg=f"Error: Error when accessing secrets on server. Server response: {type(err).__name__}: {err}" + log_and_raise(err_msg, err) + raise SystemExit(err_msg) from err -def print_value(output_file, binary_file, value): +def locker_client(locker_token, command, path, data): """ - Print secrets in different formats - :param output_file: - :param binary_file: - :param value: - :return: + Client function for accessing secrets + :param path: path to secret + :param command: the command to perform + :param data: input data + :param locker_token: locker token + :return: Output data from the service """ try: - if binary_file: - with open(output_file, "wb") if output_file else sys.stdout.buffer as f: - f.write(base64.b64decode(value.encode())) + headers = {"X-Vault-Token": locker_token} + url = VAULT_ADDR + VAULT_LOCKER_MOUNT_POINT + path + if command == "list_secrets": + response = requests.get(url, headers=headers, params={"list": "true"}) + elif command == "read_secret": + response = requests.get(url, headers=headers) + elif command == "delete_secret": + response = requests.delete(url, headers=headers) + elif command == "put": + response = requests.post(url, headers=headers, data=data) else: - with open(output_file, "wt") if output_file else sys.stdout as f: - f.write(value) - - except (ValueError, FileNotFoundError, TypeError) as e: - raise SystemExit( - f"Error: Error when writing file {output_file}. Error message: {type(e).__name__}: {e}" - ) + raise SystemExit(f"Invalid command {command}") + response.raise_for_status() + if command in ["list_secrets", "read_secret"]: + response_json = response.json() + return dict(response_json) + return None + except requests.exceptions.HTTPError as exception: + err_msg=f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(err_msg, exception) + raise SystemExit(err_msg) from exception @click.group() @@ -243,20 +104,14 @@ def secret(): @secret.command() -@oidc_params -@click.option( - "--output-format", - "-f", - required=False, - help="Output format", - type=click.Choice(["text", "YAML", "JSON"], case_sensitive=False), -) -@click.argument("short_path", metavar="[secret path]") +@secret_token_params +@secret_output_params +@click.argument("short_path", metavar="[secret_path]") @click.argument("key", metavar="[key]", required=False) @click.option( "--decrypt-key", "-d", - metavar="[key]", + metavar="passphrase", required=False, help="Decryption key or passphrase", ) @@ -270,74 +125,87 @@ def secret(): @click.option( "--output-file", "-o", - metavar="[filename]", + metavar="filename", required=False, help="Name of output file", ) def get( - access_token, - short_path, - key, - output_format, - decrypt_key, - binary_file, - output_file, + token: VaultToken, + short_path: str, + key: str, + output_format: str, + decrypt_key: str, + binary_file: bool, + output_file: str, ): """ Get the secret object in the path. If a key is given, print only the value of the key """ - response = secret_client(access_token, "read_secret", short_path, None) - if decrypt_key: - decrypt_data(decrypt_key, response["data"]) - if not key: - print_secrets(output_file, output_format, response["data"]) - else: - if key in response["data"]: - print_value(output_file, binary_file, response["data"][key]) + try: + response = token.vault_command(command="get", path=short_path, data={}) + if decrypt_key: + decrypt_data(decrypt_key, response["data"]) + if not key: + print_secrets(output_file, output_format, response["data"]) else: - raise SystemExit(f"Error: {key} not found in {short_path}") - + if key in response["data"]: + print_value(output_file, binary_file, response["data"][key]) + else: + raise SystemExit(f"Error: {key} not found in {short_path}") + except Exception as exception: + msg_err=f"An unexpected error occurred: {str(exception)}" #, file=sys.stderr + log_and_raise(msg_err, ServiceError(msg_err)) + raise ServiceError(msg_err) from exception @secret.command("list") -@oidc_params +@secret_token_params @click.argument("short_path", metavar="[secret path]", required=False, default="") def list_( - access_token, - short_path, + token: VaultToken, + short_path: str, ): """ List secret objects in the path """ + try: + response = token.vault_command(command="list", path=short_path, data={}) + print("\n".join(map(str, response["data"]["keys"]))) + except Exception as exception: + message = str(exception) + if "HTTPError: 404" in message: + file=sys.stderr + msg_err=f"No secrets found: {file}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise ServiceError(msg_err) from exception - response = secret_client(access_token, "list_secrets", short_path, None) - print("\n".join(map(str, response["data"]["keys"]))) + msg_err=f"An unexpected error occurred: {str(exception)}"#, file=sys.stderr + log_and_raise(msg_err, ServiceError(msg_err)) + raise ServiceError(msg_err) from exception @secret.command() -@oidc_params +@secret_token_params @click.argument("short_path", metavar="[secret path]") @click.argument("secrets", nargs=-1, metavar="[key=value...]") @click.option( "--encrypt-key", "-e", - metavar="[key]", - required=False, + metavar="passphrase", help="Encryption key or passphrase", ) @click.option( "--binary-file", "-b", - required=False, is_flag=True, help="True for reading secrets from binary files", ) def put( - access_token, - short_path, - secrets, - encrypt_key, - binary_file, + token: VaultToken, + short_path: str, + secrets: list, + encrypt_key: str, + binary_file: bool, ): """ Put a secret object to the path. Secrets are provided in form key=value @@ -346,18 +214,91 @@ def put( secret_dict = secret_params_to_dict(secrets, binary_file) if encrypt_key: encrypt_data(encrypt_key, secret_dict) - secret_client(access_token, "put", short_path, secret_dict) + token.vault_command(command="put", path=short_path, data=secret_dict) @secret.command() -@oidc_params +@secret_token_params @click.argument("short_path", metavar="[secret path]") def delete( - access_token, - short_path, + token: VaultToken, + short_path, ): """ Delete the secret object in the path """ + token.vault_command(command="delete", path=short_path, data={}) - secret_client(access_token, "delete_secret", short_path, None) + +@secret.group() +def locker(): + """ + Commands for creating and accessing locker objects + """ + + +@locker.command() +@oidc_params +@secret_output_params +@click.option("--ttl", default="24h", help="Locker's Time-to-live", show_default=True) +@click.option("--num-uses", default=10, help="Max number of uses", show_default=True) +@click.option("--verbose", is_flag=True, help="Print token details") +def create(access_token, ttl, num_uses, output_format, verbose): + """ + Create a locker and return the locker token + """ + LOG.debug("Creating a new locker") + try: + client = hvac.Client(url=VAULT_ADDR) + client.auth.jwt.jwt_login(role=VAULT_ROLE, jwt=access_token) + client.auth.token.renew_self(increment=ttl) + locker_token = client.auth.token.create( + policies=["default"], ttl=ttl, num_uses=num_uses, renewable=False + ) + if not verbose: + print(locker_token["auth"]["client_token"]) + else: + print_secrets("", output_format, locker_token["auth"]) + except VaultError as exception: + msg_err=f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + +@locker.command() +@secret_output_params +@click.argument( + "locker_token", metavar="[locker_token]", envvar="FEDCLOUD_LOCKER_TOKEN" +) +def check(locker_token, output_format): + """ + Check status of locker token + """ + + try: + client = hvac.Client(url=VAULT_ADDR) + client.token = locker_token + locker_info = client.auth.token.lookup_self() + print_secrets("", output_format, locker_info["data"]) + except VaultError as exception: + msg_err=f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + +@locker.command() +@click.argument( + "locker_token", metavar="[locker_token]", envvar="FEDCLOUD_LOCKER_TOKEN" +) +def revoke(locker_token): + """ + Revoke the locker token and delete all data in the locker + """ + try: + client = hvac.Client(url=VAULT_ADDR) + client.token = locker_token + client.auth.token.revoke_self() + except VaultError as exception: + msg_err=f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception diff --git a/fedcloudclient/secret_helper.py b/fedcloudclient/secret_helper.py new file mode 100644 index 0000000..3d00de4 --- /dev/null +++ b/fedcloudclient/secret_helper.py @@ -0,0 +1,196 @@ +""" +Secret helper functions +""" +import base64 +import json +import os +import sys + +import yaml +from cryptography.fernet import Fernet, InvalidToken +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC +from tabulate import tabulate + +from fedcloudclient.conf import CONF +from fedcloudclient.exception import ServiceError +from fedcloudclient.logger import log_and_raise + +VAULT_SALT = CONF.get("vault_salt") + + +def read_data_from_file(input_format: str, input_file: str): + """ + Read data from file. Format may be text, yaml, json or auto-detect according to file extension + :param input_format: + :param input_file: + :return: + """ + + if input_format is None or input_format == "auto-detect": + if input_file.endswith(".json"): + input_format = "JSON" + else: + # default format + input_format = "YAML" + + try: + + # read text/binary files to strings + if input_format == "binary": + with open(input_file, "rb") if input_file else sys.stdin.buffer as file: + return base64.b64encode(file.read()).decode() + if input_format == "text": + with open(input_file, "r", encoding="utf-8") if input_file else sys.stdin as file: + return file.read() + + # reading YAML or JSON to dict + with open(input_file, encoding="utf-8") if input_file else sys.stdin as file: + data = {} + if input_format == "YAML": + data = yaml.safe_load(file) + elif input_format == "JSON": + data = json.load(file) + return dict(data) + + except (ValueError, FileNotFoundError, yaml.YAMLError) as exception: + msg_err= f"Error: Error when reading file {input_file}. Error message: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + + +def secret_params_to_dict(params: list, binary_file: bool = False): + """ + Convert secret params "key=value" to dict {"key":"value"} + :param binary_file: if reading files as binary + :param params: input string in format "key=value" + :return: dict {"key":"value"} + """ + + result = {} + + if len(params) == 0: + raise SystemExit( + "Error: Expecting 'key=value' arguments for secrets, None provided." + ) + + for param in params: + if param.startswith("@") or param == "-": + data = read_data_from_file("auto-detect", param[1:]) + result.update(data) + else: + try: + key, value = param.split("=", 1) + except ValueError as exception: + msg_err=f"Error: Expecting 'key=value' arguments for secrets. '{param}' provided." + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + if value.startswith("@") or value == "-": + if binary_file: + value = read_data_from_file("binary", value[1:]) + else: + value = read_data_from_file("text", value[1:]) + result[key] = value + + return result + + +def generate_derived_key(salt: bytes, passphrase: str): + """ + Generate derived encryption/decryption key from salted passphrase + :param salt: + :param passphrase: + :return: derived key + """ + + kdf = PBKDF2HMAC( + algorithm=hashes.SHA256(), + length=32, + salt=salt, + iterations=390000, + ) + return base64.b64encode(kdf.derive(passphrase.encode())) + + +def encrypt_data(encrypt_key: str, secrets: dict): + """ + Encrypt values in secrets using key + :param encrypt_key: encryption key + :param secrets: dict containing secrets + :return: dict with encrypted values + """ + salt = os.urandom(16) + derived_key = generate_derived_key(salt, encrypt_key) + fernet = Fernet(derived_key) + for key in secrets: + secrets[key] = fernet.encrypt(secrets[key].encode()).decode() + secrets[VAULT_SALT] = base64.b64encode(salt) + + +def decrypt_data(decrypt_key: str, secrets: dict): + """ + Decrypt values in secrets using key + :param decrypt_key: decryption key + :param secrets: dict containing encrypted secrets + :return: dict with decrypted values + """ + try: + salt = base64.b64decode(secrets.pop(VAULT_SALT)) + derived_key = generate_derived_key(salt, decrypt_key) + fernet = Fernet(derived_key) + for key in secrets: + secrets[key] = fernet.decrypt(secrets[key].encode()).decode() + except InvalidToken as exception: + msg_err=f"Error: Error during decryption. {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + +def print_secrets(output_file: str, output_format: str, secrets: dict): + """ + Print secrets in different formats + :param output_file: + :param output_format: + :param secrets: + :return: + """ + + try: + with open(output_file, "wt", encoding="utf-8") if output_file else sys.stdout as file: + if output_format == "JSON": + json.dump(secrets, file, indent=4) + elif output_format == "YAML": + yaml.dump(secrets, file, sort_keys=False) + else: + print(tabulate(secrets.items(), headers=["key", "value"]), file=file) + + except (ValueError, FileNotFoundError, yaml.YAMLError) as exception: + msg_err= f"Error: Error when writing file {output_file}. Error message: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception + + + +def print_value(output_file: str, binary_file: bool, value: str): + """ + Print secrets in different formats + :param output_file: + :param binary_file: + :param value: + :return: + """ + + try: + if binary_file: + with open(output_file, "wb", encoding="utf-8") if output_file else sys.stdout.buffer as file: + file.write(base64.b64decode(value.encode())) + else: + with open(output_file, "wt", encoding="utf-8") if output_file else sys.stdout as file: + file.write(value) + + except (ValueError, FileNotFoundError, TypeError) as exception: + msg_err=f"Error: Error when writing file {output_file}. Error message: {type(exception).__name__}: {exception}" + log_and_raise(msg_err, ServiceError(msg_err)) + raise SystemExit(msg_err) from exception diff --git a/fedcloudclient/secret_helper_test.py b/fedcloudclient/secret_helper_test.py new file mode 100644 index 0000000..36937aa --- /dev/null +++ b/fedcloudclient/secret_helper_test.py @@ -0,0 +1,67 @@ +""" +Testing secret helper functions +""" +import base64 +import json +import os + +from fedcloudclient.secret_helper import decrypt_data, encrypt_data, print_secrets, print_value, read_data_from_file + +def save_read_binary_files(): + """ + Test save random binary data, read it again and compare + """ + print("Testing read/write data from binary files") + data = base64.b64encode(os.urandom(20)).decode() + temp_file = "/tmp/test" + print_value(output_file=temp_file, binary_file=True, value=data) + read_data = read_data_from_file(input_format="binary", input_file=temp_file) + + print(f" Original : {data} \n from file: {read_data}") + assert read_data == data + + +def save_read_dict(): + """ + Test saving dict in temp file and read it again + """ + print("Testing read/write data from YAML/JSON files") + + data = {"key1": "value1", "key2": base64.b64encode(os.urandom(8)).decode()} + temp_file = "/tmp/test" + print_secrets(output_file=temp_file, output_format="JSON", secrets=data) + save_data = read_data_from_file(input_format="JSON", input_file=temp_file) + + print(f" Original : {json.dumps(data)} \n from file: {json.dumps(save_data)}") + + assert data == save_data + print_secrets(output_file=temp_file, output_format="YAML", secrets=data) + save_data = read_data_from_file(input_format="YAML", input_file=temp_file) + + print(f" Original : {json.dumps(data)} \n from file: {json.dumps(save_data)}") + + assert data == save_data + + +def encrypt_decrypt(): + """ + Testing encrypt/decrypt + """ + print("Testing encrypt/decrypt") + secret = base64.b64encode(os.urandom(20)).decode() + passphrase = base64.b64encode(os.urandom(8)).decode() + data = {"key": secret} + encrypt_data(encrypt_key=passphrase, secrets=data) + print(f" Original : {secret} \n encrypted: {data['key']}") + decrypt_data(decrypt_key=passphrase, secrets=data) + print(f" Original : {secret} \n decrypted: {data['key']}") + assert secret == data["key"] + + +if __name__ == "__main__": + + print("Test help function") + + save_read_binary_files() + save_read_dict() + encrypt_decrypt() diff --git a/fedcloudclient/select.py b/fedcloudclient/select.py index 8fa8d5b..f057c8b 100644 --- a/fedcloudclient/select.py +++ b/fedcloudclient/select.py @@ -20,6 +20,7 @@ ) from fedcloudclient.openstack import fedcloud_openstack from fedcloudclient.sites import find_endpoint_and_project_id +from fedcloudclient.logger import log_and_raise FILTER_TEMPLATE = "$[?( {specs} )]" GET_FLAVOR_COMMAND = ("flavor", "list", "--long") @@ -109,11 +110,12 @@ def get_parser(filter_string): try: parser = parse(filter_string) except JSONPathError as exception: - raise SystemExit( - "Error during constructing filter\n" + msg_err="""Error during constructing filter\n" f"Filter string: {filter_string}\n" - f"{exception}" - ) + f"{exception}""" + log_and_raise(msg_err, JSONPathError) + raise SystemExit(msg_err) from exception + return parser @@ -129,11 +131,11 @@ def do_filter(parser, input_list): try: matched = [match.value for match in parser.find(input_list)] except TypeError as exception: - raise SystemExit( - "TypeError during filtering result\n" + msg_err="""TypeError during filtering result\n" "Probably string value for numeric property in filter\n" - f"{exception}" - ) + f"{exception}""" + log_and_raise(msg_err, TypeError) + raise SystemExit(msg_err) from exception return matched diff --git a/fedcloudclient/shell.py b/fedcloudclient/shell.py index 88b2413..e93bdc7 100644 --- a/fedcloudclient/shell.py +++ b/fedcloudclient/shell.py @@ -35,7 +35,7 @@ def get_shell_type(): return Shell.LINUX - +#Imported to the sites def print_set_env_command(name, value): """ Print command to set environment variable, diff --git a/fedcloudclient/sites.py b/fedcloudclient/sites.py index 9b2e34e..4d3bb7c 100644 --- a/fedcloudclient/sites.py +++ b/fedcloudclient/sites.py @@ -22,15 +22,18 @@ import yaml from jsonschema import validate +from fedcloudclient.conf import CONF from fedcloudclient.decorators import ( ALL_SITES_KEYWORDS, - DEFAULT_PROTOCOL, all_site_params, oidc_params, site_vo_params, + vo_params_optional, ) from fedcloudclient.shell import print_set_env_command +DEFAULT_PROTOCOL = CONF.get("os_protocol") + __REMOTE_CONFIG_FILE = ( "https://raw.githubusercontent.com/tdviet/fedcloudclient/master/config/sites.yaml" ) @@ -103,7 +106,7 @@ def safe_read_yaml_from_url(url, max_length): data = yaml.safe_load(yaml_file) except Exception as exception: print(f"Error during reading data from {url}") - raise SystemExit(f"Exception: {exception}") + raise SystemExit(f"Exception: {exception}") from exception return data @@ -131,7 +134,7 @@ def read_default_site_config(): validate(instance=site_info, schema=schema) except Exception as exception: print(f"Site config in file {filename} is in wrong format") - raise SystemExit(f"Exception: {exception}") + raise SystemExit(f"Exception: {exception}") from exception __site_config_data.append(site_info) @@ -155,7 +158,7 @@ def read_local_site_config(config_dir): __site_config_data.append(site_info) except Exception as exception: print(f"Error during reading site config from {file}") - raise SystemExit(f"Exception: {exception}") + raise SystemExit(f"Exception: {exception}") from exception def save_site_config(config_dir): @@ -184,16 +187,22 @@ def delete_site_config(config_dir): shutil.rmtree(config_dir, ignore_errors=True) -def list_sites(): +def list_sites(vo=None): """ - List of all sites IDs in site configurations + List all sites IDs in site configurations + Optionally list all sites supporting a Virtual Organization :return: list of site IDs """ read_site_config() result = [] for site_info in __site_config_data: - result.append(site_info["gocdb"]) + if vo is None: + result.append(site_info["gocdb"]) + else: + for vos in site_info["vos"]: + if vo == vos["name"]: + result.append(site_info["gocdb"]) return result @@ -269,40 +278,40 @@ def site(): @site.command() @all_site_params -def show(site, all_sites): +def show(site_local, all_sites): """ Print configuration of specified site(s) """ - if site in ALL_SITES_KEYWORDS or all_sites: + if site_local in ALL_SITES_KEYWORDS or all_sites: read_site_config() for site_info in __site_config_data: site_info_str = yaml.dump(site_info, sort_keys=True) print(site_info_str) else: - site_info = find_site_data(site) + site_info = find_site_data(site_local) if site_info: print(yaml.dump(site_info, sort_keys=True)) else: - raise SystemExit(f"Site {site} not found") + raise SystemExit(f"Site {site_local} not found") @site.command() @site_vo_params -def show_project_id(site, vo): +def show_project_id(site_local, vo): """ Print Keystone endpoint and project ID """ - if site in ALL_SITES_KEYWORDS: + if site_local in ALL_SITES_KEYWORDS: print("Cannot get project ID for ALL_SITES") raise click.Abort() - endpoint, project_id, _ = find_endpoint_and_project_id(site, vo) + endpoint, project_id, _ = find_endpoint_and_project_id(site_local, vo) if endpoint: print_set_env_command("OS_AUTH_URL", endpoint) print_set_env_command("OS_PROJECT_ID", project_id) else: - raise SystemExit(f"VO {vo} not found on site {site}") + raise SystemExit(f"VO {vo} not found on site {site_local}") @site.command() @@ -319,20 +328,21 @@ def save_config(): @site.command("list") -def list_(): +@vo_params_optional +def list_(vo=None): """ - List all sites + List all sites. If "--vo " is provided, list only sites + supporting a Virtual Organization. """ - read_site_config() - for site_info in __site_config_data: - print(site_info["gocdb"]) + for site_local in list_sites(vo): + print(site_local) @site.command() @site_vo_params @oidc_params def env( - site, + site_local, vo, access_token, ): @@ -341,11 +351,11 @@ def env( May set also environment variable OS_ACCESS_TOKEN, if access token is provided, otherwise print notification """ - if site in ALL_SITES_KEYWORDS: + if site_local in ALL_SITES_KEYWORDS: print("Cannot generate environment variables for ALL_SITES") raise click.Abort() - endpoint, project_id, protocol = find_endpoint_and_project_id(site, vo) + endpoint, project_id, protocol = find_endpoint_and_project_id(site_local, vo) if endpoint: if protocol is None: protocol = DEFAULT_PROTOCOL @@ -356,5 +366,5 @@ def env( print_set_env_command("OS_PROJECT_ID", project_id) print_set_env_command("OS_ACCESS_TOKEN", access_token) else: - print(f"VO {vo} not found to have access to site {site}") + print(f"VO {vo} not found to have access to site {site_local}") return 1 diff --git a/fedcloudclient/vault_auth.py b/fedcloudclient/vault_auth.py new file mode 100644 index 0000000..6e5af0c --- /dev/null +++ b/fedcloudclient/vault_auth.py @@ -0,0 +1,143 @@ +""" +Class for managing Vault tokens +""" +import hvac +from hvac.exceptions import VaultError + +from fedcloudclient.auth import OIDCToken +from fedcloudclient.conf import CONF +from fedcloudclient.exception import TokenError +from fedcloudclient.logger import LOG, log_and_raise + + +class VaultToken(OIDCToken): + """ + Managing tokens for Vault + """ + + def __init__(self, access_token: str = None, vault_token: str = None): + """ + Init Vault token + """ + super().__init__(access_token) + self.vault_token = vault_token + self.auth_method = None + self.vault_client = None + + def get_vault_client(self) -> hvac.Client: + """ + Init client if needed and return the client + """ + if self.vault_client: + # client is initialized, just return it + return self.vault_client + + # Create the client + client = hvac.Client(url=CONF.get("vault_endpoint")) + if self.vault_token: + client.token = self.vault_token + self.vault_client = client + return client + if self.access_token: + try: + client.auth.jwt.jwt_login(role=CONF.get("vault_role"), jwt=self.access_token) + self.vault_token = client.token + self.vault_client = client + return client + except VaultError as exception: + error_msg = f"Cannot login to Vault via access token: {exception}" + log_and_raise(error_msg, TokenError) + + error_msg = "Token is not initialized" + log_and_raise(error_msg, TokenError) + return None + + def get_vault_token(self) -> str: + """ + Return Vault token + """ + if self.vault_token: + return self.vault_token + if self.access_token: + self.get_vault_client() + return self.vault_token + + error_msg = "Vault token is not initialized" + log_and_raise(error_msg, TokenError) + return None + def get_user_id(self) -> str: + """ + Get user ID (from access token or vault token) + """ + if self.access_token: + return super().get_user_id() + return self.get_vault_user_id() + + def get_vault_user_id(self) -> str: + """ + Get Vault user ID from Vault token + """ + response = {} + try: + client = self.get_vault_client() + response = client.auth.token.lookup_self() + except VaultError as exception: + error_msg = f"Cannot get user information via Vault token: {exception}" + log_and_raise(error_msg, TokenError) + + display_name = response["data"]["display_name"] + msg_debug=f"Vault token display_name: {display_name}" + LOG.debug(msg_debug) + self.auth_method, self.user_id = display_name.split("-") + return self.user_id + + def get_vault_auth_method(self) -> str: + """ + Get authentication method creating the token + """ + if self.auth_method: + return self.auth_method + + self.get_vault_user_id() + return self.auth_method + + def vault_command(self, command: str, path: str, data: dict, vo: str = None) -> dict: + """ + Perform Vault kv command + """ + + client = self.get_vault_client() + + full_path = "" + if vo: + if self.get_vault_auth_method() == "oidc": + full_path = "vos/" + vo + "/" + path + else: + log_and_raise("VO-shared folders are accessible only for token created by OIDC method via GUI",TokenError) + else: + full_path = "users/" + self.get_user_id() + "/" + path + + function_list = { + "list": client.secrets.kv.v1.list_secrets, + "get": client.secrets.kv.v1.read_secret, + "delete": client.secrets.kv.v1.delete_secret, + } + mount_point = CONF.get("vault_mount_point") + try: + if command == "put": + response = client.secrets.kv.v1.create_or_update_secret( + path=full_path, + mount_point=mount_point, + secret=data, + ) + else: + response = function_list[command]( + path=full_path, + mount_point=mount_point, + ) + return response + + except VaultError as exception: + error_msg = f"Error: Error when accessing secrets on server. Server response: {type(exception).__name__}: {exception}" + log_and_raise(error_msg, TokenError) + return None diff --git a/fedcloudclient/vault_auth_test.py b/fedcloudclient/vault_auth_test.py new file mode 100644 index 0000000..7f52494 --- /dev/null +++ b/fedcloudclient/vault_auth_test.py @@ -0,0 +1,65 @@ +""" +Testing vault_auth.py +""" +import os + +import fedcloudclient.vault_auth as vault +from fedcloudclient.exception import TokenError + + +def test_vault_login(mytoken: str): + """ + test vault login with mytoken + """ + + token = vault.VaultToken() + token.get_token_from_mytoken(mytoken) + vault_client = token.get_vault_client() + + assert vault_client + + +def test_user_id_from_vault_token(vault_token: str, user_id: str): + """ + Test user id from OIDC vault token + """ + token = vault.VaultToken(vault_token=vault_token) + vault_id = None + try: + vault_id = token.get_user_id() + except TokenError: + print("Please check validity of your OIDC Vault token") + assert vault_id == user_id + + +def test_get_personal_secret(vault_token: str): + """ + Test getting personal secrets + """ + token = vault.VaultToken(vault_token=vault_token) + response = token.vault_command(command="get", path="test", data={}, vo=None) + assert response["data"]["test"] == "test" + + +def test_get_vo_secret(vault_token: str, vo_secret: str): + """ + Test getting VO-shared secrets + """ + token = vault.VaultToken(vault_token=vault_token) + response = token.vault_command(command="get", path="test", data={}, vo=vo_secret) + assert response["data"]["test"] == "test" + + +if __name__ == "__main__": + #Before testing, setup testing environment with + #export FEDCLOUD_MYTOKEN= + #export FEDCLOUD_ID= + #export FEDCLOUD_VAULT_TOKEN= + + os_mytoken = os.environ["FEDCLOUD_MYTOKEN"] + os_user_id = os.environ["FEDCLOUD_ID"] + oidc_vault_token = os.environ["FEDCLOUD_VAULT_TOKEN"] + test_vault_login(os_mytoken) + test_user_id_from_vault_token(oidc_vault_token, os_user_id) + test_get_personal_secret(oidc_vault_token) + test_get_vo_secret(oidc_vault_token, "vo.access.egi.eu") diff --git a/requirements.txt b/requirements.txt index c16eb06..f074593 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ -click~=8.1.3 -click_option_group>=0.5.3 -tabulate==0.8.10 -requests~=2.28.0 +click==8.1.7 +click_option_group==0.5.6 +tabulate==0.9.0 +requests==2.31.0 defusedxml~=0.7.1 -pyjwt~=2.4.0 -python-openstackclient==5.8.0 -liboidcagent~=0.4.0 -jsonpath-ng==1.5.3 -PyYAML~=6.0 -setuptools~=62.6.0 -jsonschema~=4.6.0 -psutil~=5.9.1 -hvac~=0.11.2 -cryptography==37.0.2 \ No newline at end of file +pyjwt==2.8.0 +python-openstackclient==6.3.0 +liboidcagent==0.6.0 +jsonpath-ng==1.6.0 +PyYAML==6.0.1 +setuptools==68.2.2 +jsonschema==4.19.2 +psutil==5.9.6 +hvac==2.0.0 +cryptography==42.0.4 \ No newline at end of file