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..6f44de5 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..071863c 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..80d81ac 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..d3ac43e --- /dev/null +++ b/docs/_build/html/FAQ.html @@ -0,0 +1,139 @@ + + + + + + + + + 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..6e300f8 --- /dev/null +++ b/docs/_build/html/_sources/intro.rst.txt @@ -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.auth** module provides utilities to verify the validity of access tokens and authentication credentials used in the EGI Federated 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/_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..cc7fdb8 --- /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 | ++------------------------------+-------------------------+---------------------------+ +| Not implemented | --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..47eb527 --- /dev/null +++ b/docs/_build/html/cheat.html @@ -0,0 +1,376 @@ + + + + + + + + + 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..62dcc68 --- /dev/null +++ b/docs/_build/html/development.html @@ -0,0 +1,152 @@ + + + + + + + + + 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..b68a718 --- /dev/null +++ b/docs/_build/html/fedcloudclient.html @@ -0,0 +1,899 @@ + + + + + + + + + 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..ee7f2f7 --- /dev/null +++ b/docs/_build/html/genindex.html @@ -0,0 +1,420 @@ + + + + + + + + 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..829a047 --- /dev/null +++ b/docs/_build/html/index.html @@ -0,0 +1,167 @@ + + + + + + + + + <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..ec44276 --- /dev/null +++ b/docs/_build/html/install.html @@ -0,0 +1,196 @@ + + + + + + + + + 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..06cfc57 --- /dev/null +++ b/docs/_build/html/intro.html @@ -0,0 +1,160 @@ + + + + + + + + + Introduction — fedcloudclient documentation + + + + + + + + + + + + + + + + + + +
+ + +
+ +
+
+
+ +
+
+
+
+ +
+

Introduction

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

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 --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.auth module provides utilities to verify the validity of access tokens and authentication credentials used in the EGI Federated 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/.

+
+ + +
+
+ +
+
+
+
+ + + + \ 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..cfb3993 --- /dev/null +++ b/docs/_build/html/modules.html @@ -0,0 +1,127 @@ + + + + + + + + + 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..fd4d9ea --- /dev/null +++ b/docs/_build/html/py-modindex.html @@ -0,0 +1,175 @@ + + + + + + + + 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..08520be --- /dev/null +++ b/docs/_build/html/quickstart.html @@ -0,0 +1,238 @@ + + + + + + + + + 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..534498c --- /dev/null +++ b/docs/_build/html/scripts.html @@ -0,0 +1,202 @@ + + + + + + + + + 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..66fa94b --- /dev/null +++ b/docs/_build/html/search.html @@ -0,0 +1,130 @@ + + + + + + + + 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..180d6ea --- /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]], "FAQ and Troubleshooting": [[0, null]], "FedCloud client API references": [[3, null]], "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]], "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], "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, 8, 10], "Not": 10, "On": 0, "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": 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, "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, 6, 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, 6, 9, 10], "arg": [5, 8, 10], "argument": 10, "ask": 3, "auth": [1, 6, 10], "authent": [3, 4, 6], "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, "built": 10, "bundl": [0, 1, 5], "c": 1, "c08r30": 10, "c16r60": 10, "c94141a5dac": [8, 10], "cach": [3, 10], "call": [2, 3, 10], "can": [1, 2, 3, 5, 6, 9, 10], "cannot": 3, "case": 3, "cat": [0, 1], "catania": 10, "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, "claim": 3, "class": 3, "cli": 5, "click": 10, "client": [0, 4, 6, 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], "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, 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, 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, 10], "default_set": [3, 10], "defin": [3, 10], "delet": 3, "delete_site_config": 3, "demo": [2, 10], "depend": 5, "describ": [0, 10], "descript": 6, "design": [6, 8, 10], "detail": [0, 1, 2, 5, 10], "determin": 10, "develop": 2, "df25f80f": 10, "dict": 3, "dictionari": 3, "differ": 10, "dir": 3, "direct": 10, "directli": [2, 3, 10], "directori": [3, 5, 10], "dirti": 1, "discov": 3, "discoveri": 3, "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, "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, "eu": [1, 2, 3, 6, 8, 9, 10], "eval": [1, 9, 10], "even": 10, "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, 10], "explan": 10, "explicitli": [3, 10], "export": [1, 8, 9, 10], "expos": 3, "extern": 4, "extract": 3, "f": 1, "fail": 3, "fals": 3, "faq": 4, "faster": [3, 10], "featur": 6, "fedcloud": [0, 1, 4, 6, 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_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, "file": [1, 3, 5, 10], "filenam": 3, "filter": [4, 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, 6, 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, 5, 6, 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, "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], "ha": [3, 10], "handl": [6, 10], "handler": 3, "hard": 3, "hardcod": 10, "hasn": 3, "have": [0, 3, 6, 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, "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, 10], "in2p3": 10, "inaccess": 10, "includ": [0, 3, 5, 6, 10], "indent": 2, "indic": 3, "infn": 10, "inform": [3, 4, 8, 10], "infrastructur": 6, "init": 3, "init_config": 3, "init_default_config": 3, "init_logg": 3, "initi": [0, 3], "input": [2, 10], "insid": 5, "instal": [0, 3, 4, 6, 8], "install_cert": [0, 1, 5], "instanc": 10, "instead": [3, 10], "instruct": [0, 5, 8, 10], "int": [3, 5, 8, 10], "integr": 10, "intention": 5, "interact": [3, 5, 6, 8, 10], "introduct": 4, "invalid": 3, "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], "jwt": 3, "k20m": 10, "kei": [1, 3], "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, 6], "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, "lower": [0, 3], "lowercas": 3, "lowest": 10, "m": 1, "m1": [9, 10], "machin": [0, 1, 6, 8, 9], "mai": [1, 3, 6, 10], "main": [0, 1, 3, 5, 10], "mainli": [0, 6], "make": [3, 5, 10], "manag": [3, 6, 10], "mani": 6, "manipul": [3, 8, 10], "manual": [0, 10], "map": [3, 4, 9, 10], "master": [1, 10], "match": 10, "max_length": 3, "mcc": 1, "md": [0, 5], "medium": [9, 10], "member": 6, "membership": [1, 3, 8, 10], "mention": 10, "merg": 3, "messag": [0, 2, 3, 5, 8, 10], "metadata": 3, "method": [3, 10], "min_access_token_tim": 10, "minimum": 3, "miss": 3, "mode": 10, "modul": 6, "monitor": 3, "more": [0, 2, 4, 5, 8, 9], "most": [2, 6], "multipl": [3, 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": 10, "new": [8, 10], "newest": 10, "next": 8, "none": 3, "notabl": 6, "note": [9, 10], "notif": 10, "nova": 10, "null": [8, 9], "o": [0, 3, 5, 10], "object": [1, 2, 3, 5, 10], "obtain": [3, 5, 10], "occur": 3, "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, 6, 9, 10], "opaqu": 3, "openi": 3, "openid": [3, 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, 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, 6, 10], "pip": [1, 4, 8], "pip3": [4, 5, 8], "place": [5, 10], "portal": [8, 10], "possibl": 10, "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, "program": 6, "programm": 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], "py": 2, "python": [0, 1, 4, 5, 6], "python3": 1, "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, 10], "refresh": 10, "regex": [1, 3], "regist": [3, 10], "relat": [3, 6], "remaind": 3, "rememb": 10, "remov": [1, 3, 10], "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, 10], "respons": [3, 10], "restart": 1, "result": [0, 2, 3, 4, 8, 9], "retriev": [1, 3, 10], "retrieve_unscoped_token": 3, "return": 3, "rich": 6, "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], "search": [3, 4, 6], "second": 10, "secret": [4, 5, 6], "section": 8, "see": [0, 1, 2, 5, 10], "select": [3, 4, 5, 9], "self": 3, "serial": 3, "server": [1, 3, 8, 10], "servic": [1, 2, 3, 6, 10], "service_typ": 3, "set": [1, 2, 3, 4, 8, 10], "setup": 4, "sh": [0, 1, 5, 10], "share": 10, "sheet": 4, "shell": [1, 4], "short": [6, 10], "shorter": 1, "should": 5, "show": [1, 5, 8, 9, 10], "shown": 10, "side": 2, "signatur": 3, "simpl": [6, 9], "simpler": 1, "simpli": 5, "simplifi": [3, 10], "singl": [2, 6], "site": [0, 1, 2, 4, 5, 6, 8, 9], "site_dir": 10, "site_list_url": 10, "site_nam": 3, "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], "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, "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], "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, 6, 8], "twice": 10, "txt": 1, "type": [1, 3, 9, 10], "typic": 10, "u": 5, "ubuntu": [1, 10], "ui": [9, 10], "univ": 10, "unrespons": 1, "unscop": [3, 6], "unscoped_token": 3, "unsupport": 9, "url": [3, 10], "us": [0, 3, 4, 6, 8], "usag": [4, 5, 6], "user": [1, 3, 5, 6, 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, 6, 10], "vault": [3, 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, 6], "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, "wa": 5, "wai": [1, 10], "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, 9], "command": [1, 9, 10], "complet": 10, "conf": 3, "config": 10, "configur": 10, "consist": 10, "contain": [1, 5], "content": 4, "core": 5, "docker": [1, 5], "egi": 5, "endpoint": [3, 10], "environ": [9, 10], "extern": 9, "faq": 0, "fedcloud": [2, 3, 5, 9, 10], "fedcloudcli": [3, 7], "filter": 1, "from": [1, 9], "help": 10, "inform": 1, "instal": [1, 5], "introduct": 6, "jq": 9, "json": 9, "local": 1, "locker_auth": 3, "logger": 3, "map": 1, "modul": 3, "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": 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..07cae8f --- /dev/null +++ b/docs/_build/html/usage.html @@ -0,0 +1,716 @@ + + + + + + + + + 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

Not implemented

–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..6e300f8 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -30,7 +30,9 @@ The most notable features of FedCloud client are following: or as a `Python library `_ for programming FedCloud services. -Six modules are included: +The following modules are included: + +* **fedcloudclient.conf** for handling fedcloudclient configuration, * **fedcloudclient.checkin** for operation with EGI Check-in like getting tokens, @@ -44,7 +46,7 @@ Six modules are included: * **fedcloudclient.secret** for accessing secrets in `Secret management service `_, -* **fedcloudclient.ec3** for deploying elastic computing clusters in Cloud. +* **fedcloudclient.auth** module provides utilities to verify the validity of access tokens and authentication credentials used in the EGI Federated Cloud, A short tutorial of the fedcloudclient is available in `this presentation `_. 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