From 38e58cfd30200aa4156856f7622fe7e3640a9f23 Mon Sep 17 00:00:00 2001 From: Bryan Strassner <bryan.strassner@gmail.com> Date: Fri, 25 Aug 2017 17:57:27 -0500 Subject: [PATCH] Add Action API This change introduces a large section of the API for the next major version of Shipyard - the action api. By interfacing with Airflow, Shipyard will invoke workflows and allow for controlling and querying status of those workflows. Foundationally, this patchset introduces a lot of framework code for other apis, including error handling to a common output format, database interaction for persistence of action information, and use of oslo_config for configuration support. Add GET all actions primary code - db connection not yet impl Update base classes to have more structure Add POST actions framework Add GET action by id Add GET of validations and steps Add control api Add unit tests of action api methods Re-Removed duplicate deps from test reqs Add routes for API Removed a lot of code better handled by falcon directly Cleaned up error flows- handlers and defaults Refactored existing airflow tests to match standard output format Updated json validation to be more specific Added basic start for alembic Added alembic upgrade at startup Added table creation definitions Added base revision for alembic upgrade Bug fixes - DB queries, airflow comm, logic issues, logging issues Bug fixes - date formats and alignment of keys between systems Exclusions to bandit / tox.ini Resolved merge conflicts with integration of auth Update to use oslo config and PBR Update the context middleware to check uuid in a less contentious way Removed routes and resources for regions endpoint - not used Add auth policies for action api Restructure execptions to be consistent class hierarchy and common handler Add generation of config and policy examples Update tests to init configs Update database configs to not use env. vars Removed examples directory, it was no longer accurate Addressed/removed several TODOs - left some behind as well Aligned input to DAGs with action: header Retrieved all sub-steps for dags Expanded step information Refactored auth handling for better logging rename create_actions policy to create_action removed some templated file comments in env.py generated by alembic updated inconsistent exception parameters updated to use ulid instead of uuid for action ids added action control audit code per review suggestion Fixed correlation date betwen dags/actions by more string parsing Change-Id: I2f9ea5250923f45456aa86826e344fc055bba762 --- .gitignore | 3 + AUTHORS | 13 + Dockerfile | 3 - alembic.ini | 69 +++ alembic/README | 1 + alembic/env.py | 81 ++++ alembic/script.py.mako | 24 + .../51b92375e5c4_initial_shipyard_base.py | 82 ++++ docs/API.md | 2 +- entrypoint.sh | 9 +- .../control => etc/shipyard}/api-paste.ini | 0 etc/shipyard/policy.yaml.sample | 27 ++ etc/shipyard/shipyard.conf.sample | 310 +++++++++++++ examples/manifests/README.md | 60 --- examples/manifests/hostprofile.yaml | 151 ------- examples/manifests/hwdefinition.yaml | 58 --- examples/manifests/manifest_hierarchy.png | Bin 112581 -> 0 bytes examples/manifests/network.yml | 230 ---------- examples/manifests/region_manifest.yml | 60 --- examples/manifests/servers.yaml | 420 ------------------ generator/config-generator.conf | 5 + generator/policy-generator.conf | 3 + requirements.txt | 24 +- setup.cfg | 28 ++ setup.py | 29 +- shipyard_airflow/airflow_client.py | 17 - shipyard_airflow/conf/__init__.py | 0 shipyard_airflow/conf/config.py | 250 +++++++++++ shipyard_airflow/conf/opts.py | 89 ++++ shipyard_airflow/config.py | 202 --------- shipyard_airflow/control/__init__.py | 13 - shipyard_airflow/control/action_helper.py | 63 +++ shipyard_airflow/control/actions_api.py | 330 ++++++++++++++ .../control/actions_control_api.py | 129 ++++++ shipyard_airflow/control/actions_id_api.py | 117 +++++ .../control/actions_steps_id_api.py | 84 ++++ .../control/actions_validations_id_api.py | 77 ++++ shipyard_airflow/control/api.py | 58 ++- shipyard_airflow/control/base.py | 179 ++++---- shipyard_airflow/control/health.py | 11 +- shipyard_airflow/control/json_schemas.py | 126 ++++++ shipyard_airflow/control/middleware.py | 37 +- .../control/shipyard.conf.example | 320 ------------- shipyard_airflow/db/__init__.py | 0 shipyard_airflow/db/airflow_db.py | 234 ++++++++++ shipyard_airflow/db/common_db.py | 121 +++++ .../db/db.py | 19 +- .../{control/regions.py => db/errors.py} | 23 +- shipyard_airflow/db/shipyard_db.py | 254 +++++++++++ shipyard_airflow/errors.py | 247 ++++++++-- shipyard_airflow/policy.py | 161 +++++-- shipyard_airflow/setup.py | 32 -- shipyard_airflow/shipyard.py | 45 +- tests/unit/control/__init__.py | 23 + tests/unit/control/test_actions_api.py | 239 ++++++++++ .../unit/control/test_actions_control_api.py | 164 +++++++ tests/unit/control/test_actions_id_api.py | 152 +++++++ .../unit/control/test_actions_steps_id_api.py | 116 +++++ .../test_actions_validations_id_api.py | 87 ++++ tox.ini | 16 +- 60 files changed, 3883 insertions(+), 1844 deletions(-) create mode 100644 AUTHORS create mode 100644 alembic.ini create mode 100644 alembic/README create mode 100644 alembic/env.py create mode 100644 alembic/script.py.mako create mode 100644 alembic/versions/51b92375e5c4_initial_shipyard_base.py rename {shipyard_airflow/control => etc/shipyard}/api-paste.ini (100%) create mode 100644 etc/shipyard/policy.yaml.sample create mode 100644 etc/shipyard/shipyard.conf.sample delete mode 100644 examples/manifests/README.md delete mode 100644 examples/manifests/hostprofile.yaml delete mode 100644 examples/manifests/hwdefinition.yaml delete mode 100644 examples/manifests/manifest_hierarchy.png delete mode 100644 examples/manifests/network.yml delete mode 100644 examples/manifests/region_manifest.yml delete mode 100644 examples/manifests/servers.yaml create mode 100644 generator/config-generator.conf create mode 100644 generator/policy-generator.conf create mode 100644 setup.cfg delete mode 100644 shipyard_airflow/airflow_client.py create mode 100644 shipyard_airflow/conf/__init__.py create mode 100644 shipyard_airflow/conf/config.py create mode 100644 shipyard_airflow/conf/opts.py delete mode 100644 shipyard_airflow/config.py create mode 100644 shipyard_airflow/control/action_helper.py create mode 100644 shipyard_airflow/control/actions_api.py create mode 100644 shipyard_airflow/control/actions_control_api.py create mode 100644 shipyard_airflow/control/actions_id_api.py create mode 100644 shipyard_airflow/control/actions_steps_id_api.py create mode 100644 shipyard_airflow/control/actions_validations_id_api.py create mode 100644 shipyard_airflow/control/json_schemas.py delete mode 100644 shipyard_airflow/control/shipyard.conf.example create mode 100644 shipyard_airflow/db/__init__.py create mode 100644 shipyard_airflow/db/airflow_db.py create mode 100644 shipyard_airflow/db/common_db.py rename examples/manifests/services.yaml => shipyard_airflow/db/db.py (59%) rename shipyard_airflow/{control/regions.py => db/errors.py} (58%) create mode 100644 shipyard_airflow/db/shipyard_db.py delete mode 100644 shipyard_airflow/setup.py create mode 100644 tests/unit/control/test_actions_api.py create mode 100644 tests/unit/control/test_actions_control_api.py create mode 100644 tests/unit/control/test_actions_id_api.py create mode 100644 tests/unit/control/test_actions_steps_id_api.py create mode 100644 tests/unit/control/test_actions_validations_id_api.py diff --git a/.gitignore b/.gitignore index 7bbc71c0..67c4c471 100644 --- a/.gitignore +++ b/.gitignore @@ -99,3 +99,6 @@ ENV/ # mypy .mypy_cache/ + +# Generated bogus docs +ChangeLog \ No newline at end of file diff --git a/AUTHORS b/AUTHORS new file mode 100644 index 00000000..963b8e3c --- /dev/null +++ b/AUTHORS @@ -0,0 +1,13 @@ +Alan Meadows <alan.meadows@gmail.com> +Anthony Lin <anthony.jclin@gmail.com> +Bryan Strassner <bryan.strassner@gmail.com> +Felipe Monteiro <felipe.monteiro@att.com> +Mark Burnett <mark.m.burnett@gmail.com> +One-Fine-Day <vd789v@att.com> +Pete Birley <pete@port.direct> +Rodolfo <rp2723@att.com> +Scott Hussey <sh8121@att.com> +Stacey Fletcher <staceylynnfletcher@gmail.com> +Tin Lam <tin@irrational.io> +Vamsi Krishna Surapureddi <vamsi.skrishna@gmail.com> +eanylin <anthony.jclin@gmail.com> diff --git a/Dockerfile b/Dockerfile index d13ce6e8..6b1d8d3b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -60,9 +60,6 @@ COPY ./ /home/shipyard/shipyard # Copy entrypoint.sh to /home/shipyard COPY entrypoint.sh /home/shipyard/entrypoint.sh -# Copy shipyard.conf to /home/shipyard -COPY ./shipyard_airflow/control/shipyard.conf /home/shipyard/shipyard.conf - # Change permissions RUN chown -R shipyard: /home/shipyard \ && chmod +x /home/shipyard/entrypoint.sh diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 00000000..a9e77cc9 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,69 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = alembic + +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# max length of characters to apply to the +# "slug" field +#truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; this defaults +# to alembic/versions. When using multiple version +# directories, initial revisions must be specified with --version-path +# version_locations = %(here)s/bar %(here)s/bat alembic/versions + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +#Uses the envrionment variable instead: DB_CONN_SHIPYARD +sqlalchemy.url = NOT_APPLICABLE + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/README b/alembic/README new file mode 100644 index 00000000..98e4f9c4 --- /dev/null +++ b/alembic/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 00000000..5c0bb68f --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,81 @@ +from __future__ import with_statement + +import os +from logging.config import fileConfig + +from alembic import context +from oslo_config import cfg +from sqlalchemy import create_engine, pool + +# this is the shipyard config object +CONF = cfg.CONF + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.attributes.get('configure_logger', True): + fileConfig(config.config_file_name) + +target_metadata = None + + +def get_url(): + """ + Returns the url to use instead of using the alembic configuration + file + """ + return CONF.base.postgresql_db + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = get_url() + # Default code: url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=target_metadata, literal_binds=True) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + connectable = create_engine(get_url()) + # Default/generated code: + # connectable = engine_from_config( + # config.get_section(config.config_ini_section), + # prefix='sqlalchemy.', + # poolclass=pool.NullPool) + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 00000000..2c015630 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/51b92375e5c4_initial_shipyard_base.py b/alembic/versions/51b92375e5c4_initial_shipyard_base.py new file mode 100644 index 00000000..d21a1a4a --- /dev/null +++ b/alembic/versions/51b92375e5c4_initial_shipyard_base.py @@ -0,0 +1,82 @@ +"""initial shipyard base + +Revision ID: 51b92375e5c4 +Revises: +Create Date: 2017-09-12 11:12:23.768269 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import (types, func) +from sqlalchemy.dialects import postgresql as pg + + +# revision identifiers, used by Alembic. +revision = '51b92375e5c4' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + """ + Create the initial tables needed by shipyard + 26 character IDs are ULIDs. See: https://github.com/mdipierro/ulid + """ + op.create_table( + 'actions', + # ULID key for the action + sa.Column('id', types.String(26), primary_key=True), + # The name of the action invoked + sa.Column('name', types.String(50), nullable=False), + # The parameters passed by the user to the action + sa.Column('parameters', pg.JSONB, nullable=True), + # The DAG/workflow name used in airflow, if applicable + sa.Column('dag_id', sa.Text, nullable=True), + # The DAG/workflow execution time string from airflow, if applicable + sa.Column('dag_execution_date', sa.Text, nullable=True), + # The invoking user + sa.Column('user', sa.Text, nullable=False), + # Timestamp of when an action was invoked + sa.Column('datetime', + types.TIMESTAMP(timezone=True), + server_default=func.now()), + # The user provided or shipayrd generated context marker + sa.Column('context_marker', types.String(36), nullable=False) + ) + + op.create_table( + 'preflight_validation_failures', + # ID (ULID) of the preflight validation failure + sa.Column('id', types.String(26), primary_key=True), + # The ID of action this failure is associated with + sa.Column('action_id', types.String(26), nullable=False), + # The common language name of the validation that failed + sa.Column('validation_name', sa.Text, nullable=True), + # The text indicating details of the failure + sa.Column('details', sa.Text, nullable=True), + ) + + op.create_table( + 'action_command_audit', + # ID (ULID) of the audit + sa.Column('id', types.String(26), primary_key=True), + # The ID of action this audit record + sa.Column('action_id', types.String(26), nullable=False), + # The text indicating command invoked + sa.Column('command', sa.Text, nullable=False), + # The user that invoked the command + sa.Column('user', sa.Text, nullable=False), + # Timestamp of when the command was invoked + sa.Column('datetime', + types.TIMESTAMP(timezone=True), + server_default=func.now()), + ) + +def downgrade(): + """ + Remove the database objects created by this revision + """ + op.drop_table('actions') + op.drop_table('preflight_validation_failures') + op.drop_table('action_command_audit') diff --git a/docs/API.md b/docs/API.md index 4f5f41f2..e238187a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -276,7 +276,7 @@ Returns the details for a step by id for the given action by Id. * 200 OK --- -### /v1.0/actions/{action_id}/{control_verb} +### /v1.0/actions/{action_id}/control/{control_verb} Allows for issuing DAG controls against an action. #### Payload Structure diff --git a/entrypoint.sh b/entrypoint.sh index 83b05ab6..4b59c5f1 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -14,7 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. - # Start shipyard application -exec uwsgi --http :9000 -w shipyard_airflow.shipyard --callable shipyard --enable-threads -L - +exec uwsgi \ + --http :9000 \ + --paste config:/etc/shipyard/api-paste.ini \ + --enable-threads \ + -L \ + --pyargv "--config-file /etc/shipyard/shipyard.conf" \ No newline at end of file diff --git a/shipyard_airflow/control/api-paste.ini b/etc/shipyard/api-paste.ini similarity index 100% rename from shipyard_airflow/control/api-paste.ini rename to etc/shipyard/api-paste.ini diff --git a/etc/shipyard/policy.yaml.sample b/etc/shipyard/policy.yaml.sample new file mode 100644 index 00000000..c78259c0 --- /dev/null +++ b/etc/shipyard/policy.yaml.sample @@ -0,0 +1,27 @@ +# Actions requiring admin authority +#"admin_required": "role:admin" + +# List workflow actions invoked by users +# GET /api/v1.0/actions +#"workflow_orchestrator:list_actions": "rule:admin_required" + +# Create a workflow action +# POST /api/v1.0/actions +#"workflow_orchestrator:create_actions": "rule:admin_required" + +# Retreive an action by its id +# GET /api/v1.0/actions/{action_id} +#"workflow_orchestrator:get_action": "rule:admin_required" + +# Retreive an action step by its id +# GET /api/v1.0/actions/{action_id}/steps/{step_id} +#"workflow_orchestrator:get_action_step": "rule:admin_required" + +# Retreive an action validation by its id +# GET /api/v1.0/actions/{action_id}/validations/{validation_id} +#"workflow_orchestrator:get_action_validation": "rule:admin_required" + +# Send a control to an action +# POST /api/v1.0/actions/{action_id}/control/{control_verb} +#"workflow_orchestrator:invoke_action_control": "rule:admin_required" + diff --git a/etc/shipyard/shipyard.conf.sample b/etc/shipyard/shipyard.conf.sample new file mode 100644 index 00000000..74b58ab5 --- /dev/null +++ b/etc/shipyard/shipyard.conf.sample @@ -0,0 +1,310 @@ +[DEFAULT] + + +[armada] + +# +# From shipyard_airflow +# + +# FQDN for the armada service (string value) +#host = armada-int.ucp + +# Port for the armada service (integer value) +#port = 8000 + + +[base] + +# +# From shipyard_airflow +# + +# The web server for Airflow (string value) +#web_server = http://localhost:32080 + +# The database for shipyard (string value) +#postgresql_db = postgresql+psycopg2://shipyard:changeme@postgresql.ucp:5432/shipyard + +# The database for airflow (string value) +#postgresql_airflow_db = postgresql+psycopg2://shipyard:changeme@postgresql.ucp:5432/airflow + +# The direcotry containing the alembic.ini file (string value) +#alembic_ini_path = /home/shipyard/shipyard + +# Upgrade the database on startup (boolean value) +#upgrade_db = true + + +[deckhand] + +# +# From shipyard_airflow +# + +# FQDN for the deckhand service (string value) +#host = deckhand-int.ucp + +# Port for the deckhand service (integer value) +#port = 80 + + +[drydock] + +# +# From shipyard_airflow +# + +# FQDN for the drydock service (string value) +#host = drydock-int.ucp + +# Port for the drydock service (integer value) +#port = 9000 + +# TEMPORARY: password for drydock (string value) +#token = bigboss + +# TEMPORARY: location of drydock yaml file (string value) +#site_yaml = /usr/local/airflow/plugins/drydock.yaml + +# TEMPORARY: location of promenade yaml file (string value) +#prom_yaml = /usr/local/airflow/plugins/promenade.yaml + + +[healthcheck] + +# +# From shipyard_airflow +# + +# Schema to perform health check with (string value) +#schema = http + +# Health check standard endpoint (string value) +#endpoint = /api/v1.0/health + + +[keystone] + +# +# From shipyard_airflow +# + +# The url for OpenStack Authentication (string value) +#OS_AUTH_URL = http://keystone-api.ucp:80/v3 + +# OpenStack project name (string value) +#OS_PROJECT_NAME = service + +# The OpenStack user domain name (string value) +#OS_USER_DOMAIN_NAME = Default + +# The OpenStack username (string value) +#OS_USERNAME = shipyard + +# THe OpenStack password for the shipyard svc acct (string value) +#OS_PASSWORD = password + +# The OpenStack user domain name (string value) +#OS_REGION_NAME = Regionone + +# The OpenStack identity api version (integer value) +#OS_IDENTITY_API_VERSION = 3 + + +[keystone_authtoken] + +# +# From keystonemiddleware.auth_token +# + +# Complete "public" Identity API endpoint. This endpoint should not be an +# "admin" endpoint, as it should be accessible by all end users. +# Unauthenticated clients are redirected to this endpoint to authenticate. +# Although this endpoint should ideally be unversioned, client support in the +# wild varies. If you're using a versioned v2 endpoint here, then this should +# *not* be the same endpoint the service user utilizes for validating tokens, +# because normal end users may not be able to reach that endpoint. (string +# value) +#auth_uri = <None> + +# API version of the admin Identity API endpoint. (string value) +#auth_version = <None> + +# Do not handle authorization requests within the middleware, but delegate the +# authorization decision to downstream WSGI components. (boolean value) +#delay_auth_decision = false + +# Request timeout value for communicating with Identity API server. (integer +# value) +#http_connect_timeout = <None> + +# How many times are we trying to reconnect when communicating with Identity +# API Server. (integer value) +#http_request_max_retries = 3 + +# Request environment key where the Swift cache object is stored. When +# auth_token middleware is deployed with a Swift cache, use this option to have +# the middleware share a caching backend with swift. Otherwise, use the +# ``memcached_servers`` option instead. (string value) +#cache = <None> + +# Required if identity server requires client certificate (string value) +#certfile = <None> + +# Required if identity server requires client certificate (string value) +#keyfile = <None> + +# A PEM encoded Certificate Authority to use when verifying HTTPs connections. +# Defaults to system CAs. (string value) +#cafile = <None> + +# Verify HTTPS connections. (boolean value) +#insecure = false + +# The region in which the identity server can be found. (string value) +#region_name = <None> + +# DEPRECATED: Directory used to cache files related to PKI tokens. This option +# has been deprecated in the Ocata release and will be removed in the P +# release. (string value) +# This option is deprecated for removal since Ocata. +# Its value may be silently ignored in the future. +# Reason: PKI token format is no longer supported. +#signing_dir = <None> + +# Optionally specify a list of memcached server(s) to use for caching. If left +# undefined, tokens will instead be cached in-process. (list value) +# Deprecated group/name - [keystone_authtoken]/memcache_servers +#memcached_servers = <None> + +# In order to prevent excessive effort spent validating tokens, the middleware +# caches previously-seen tokens for a configurable duration (in seconds). Set +# to -1 to disable caching completely. (integer value) +#token_cache_time = 300 + +# DEPRECATED: Determines the frequency at which the list of revoked tokens is +# retrieved from the Identity service (in seconds). A high number of revocation +# events combined with a low cache duration may significantly reduce +# performance. Only valid for PKI tokens. This option has been deprecated in +# the Ocata release and will be removed in the P release. (integer value) +# This option is deprecated for removal since Ocata. +# Its value may be silently ignored in the future. +# Reason: PKI token format is no longer supported. +#revocation_cache_time = 10 + +# (Optional) If defined, indicate whether token data should be authenticated or +# authenticated and encrypted. If MAC, token data is authenticated (with HMAC) +# in the cache. If ENCRYPT, token data is encrypted and authenticated in the +# cache. If the value is not one of these options or empty, auth_token will +# raise an exception on initialization. (string value) +# Allowed values: None, MAC, ENCRYPT +#memcache_security_strategy = None + +# (Optional, mandatory if memcache_security_strategy is defined) This string is +# used for key derivation. (string value) +#memcache_secret_key = <None> + +# (Optional) Number of seconds memcached server is considered dead before it is +# tried again. (integer value) +#memcache_pool_dead_retry = 300 + +# (Optional) Maximum total number of open connections to every memcached +# server. (integer value) +#memcache_pool_maxsize = 10 + +# (Optional) Socket timeout in seconds for communicating with a memcached +# server. (integer value) +#memcache_pool_socket_timeout = 3 + +# (Optional) Number of seconds a connection to memcached is held unused in the +# pool before it is closed. (integer value) +#memcache_pool_unused_timeout = 60 + +# (Optional) Number of seconds that an operation will wait to get a memcached +# client connection from the pool. (integer value) +#memcache_pool_conn_get_timeout = 10 + +# (Optional) Use the advanced (eventlet safe) memcached client pool. The +# advanced pool will only work under python 2.x. (boolean value) +#memcache_use_advanced_pool = false + +# (Optional) Indicate whether to set the X-Service-Catalog header. If False, +# middleware will not ask for service catalog on token validation and will not +# set the X-Service-Catalog header. (boolean value) +#include_service_catalog = true + +# Used to control the use and type of token binding. Can be set to: "disabled" +# to not check token binding. "permissive" (default) to validate binding +# information if the bind type is of a form known to the server and ignore it +# if not. "strict" like "permissive" but if the bind type is unknown the token +# will be rejected. "required" any form of token binding is needed to be +# allowed. Finally the name of a binding method that must be present in tokens. +# (string value) +#enforce_token_bind = permissive + +# DEPRECATED: If true, the revocation list will be checked for cached tokens. +# This requires that PKI tokens are configured on the identity server. (boolean +# value) +# This option is deprecated for removal since Ocata. +# Its value may be silently ignored in the future. +# Reason: PKI token format is no longer supported. +#check_revocations_for_cached = false + +# DEPRECATED: Hash algorithms to use for hashing PKI tokens. This may be a +# single algorithm or multiple. The algorithms are those supported by Python +# standard hashlib.new(). The hashes will be tried in the order given, so put +# the preferred one first for performance. The result of the first hash will be +# stored in the cache. This will typically be set to multiple values only while +# migrating from a less secure algorithm to a more secure one. Once all the old +# tokens are expired this option should be set to a single value for better +# performance. (list value) +# This option is deprecated for removal since Ocata. +# Its value may be silently ignored in the future. +# Reason: PKI token format is no longer supported. +#hash_algorithms = md5 + +# A choice of roles that must be present in a service token. Service tokens are +# allowed to request that an expired token can be used and so this check should +# tightly control that only actual services should be sending this token. Roles +# here are applied as an ANY check so any role in this list must be present. +# For backwards compatibility reasons this currently only affects the +# allow_expired check. (list value) +#service_token_roles = service + +# For backwards compatibility reasons we must let valid service tokens pass +# that don't pass the service_token_roles check as valid. Setting this true +# will become the default in a future release and should be enabled if +# possible. (boolean value) +#service_token_roles_required = false + +# Authentication type to load (string value) +# Deprecated group/name - [keystone_authtoken]/auth_plugin +#auth_type = <None> + +# Config Section from which to load plugin specific options (string value) +#auth_section = <None> + + +[logging] + +# +# From shipyard_airflow +# + +# The default logging level for the root logger. ERROR=40, WARNING=30, INFO=20, +# DEBUG=10 (integer value) +#log_level = 10 + + +[shipyard] + +# +# From shipyard_airflow +# + +# FQDN for the shipyard service (string value) +#host = shipyard-int.ucp + +# Port for the shipyard service (integer value) +#port = 9000 diff --git a/examples/manifests/README.md b/examples/manifests/README.md deleted file mode 100644 index e2746a0e..00000000 --- a/examples/manifests/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Shipyard Manifests - ----- - -Shipyard manifests contain the examination of the payloads that the shipyard api will receive. -A complete manifest will consist of multiple yaml file's assembled in some way. Each yaml file will follow -Kubernetes style artifact definition. - -The high level expectation of what the data on this manifests will define is pictured here : - -<img src="https://github.com/att-comdev/shipyard/examples/manifests/manifest_hierarchy.png" width="100"> - ----- - -## region_manifest.yaml - -Region is the largest resource shipyard can understand. -A region manifest will need to define : - -- Identity of the Region. Perhaps a name will suffice, but a UUID generated by shipyard might be applicable as well. -- Cloud : The type of cloud this region is running on. i.e. AIC, or AWS, or Google etc. -- deployOn : Whether the region UCP ( undercloud) is been deployed on VM's or Baremetal - ----- -## servers.yaml - ----- -## network.yaml - ----- -## hw_definition.yaml - ----- -## host_profile.yaml - ----- -## services.yaml - -Will define high level needs for all the services that need to run above the undercloud - -It relates to the files : - -## core_services.yaml -## clcp_services.yaml -## onap_services.yaml -## cdp_services.yaml - - ----- -## undercloud.yaml - -This file will incude the configuration aspects of the undercloud that are tunnables. -Such as : -i.e. --Security --RBAC definitions --Certificates --UCP Tunnables --Kernel Tunnables, etc --Agent Tunnables diff --git a/examples/manifests/hostprofile.yaml b/examples/manifests/hostprofile.yaml deleted file mode 100644 index 6ffb440d..00000000 --- a/examples/manifests/hostprofile.yaml +++ /dev/null @@ -1,151 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#################### -# -# bootstrap_seed.yaml - Site server design definition for physical layer -# -#################### -# version the schema in this file so consumers can rationally parse it - ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: default - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces - # No magic to this host_profile, it just provides a way to specify - # sitewide settings. If it is absent from a node's inheritance chain - # then these values will NOT be applied -spec: - # OOB (iLO, iDRAC, etc...) settings. Should prefer open standards such - # as IPMI over vender-specific when possible. - oob: - type: ipmi - # OOB networking should be preconfigured, but we can include a network - # definition for validation or enhancement (DNS registration) - network: oob - account: admin - credential: admin - # Specify storage layout of base OS. Ceph out of scope - storage: - # How storage should be carved up: lvm (logical volumes), flat - # (single partition) - layout: lvm - # Info specific to the boot and root disk/partitions - bootdisk: - # Device will specify an alias defined in hwdefinition.yaml - device: primary_boot - # For LVM, the size of the partition added to VG as a PV - # For flat, the size of the partition formatted as ext4 - root_size: 50g - # The /boot partition. If not specified, /boot will in root - boot_size: 2g - # Info for additional partitions. Need to balance between - # flexibility and complexity - partitions: - - name: logs - device: primary_boot - # Partition uuid if needed - part_uuid: 84db9664-f45e-11e6-823d-080027ef795a - size: 10g - # Optional, can carve up unformatted block devices - mountpoint: /var/log - fstype: ext4 - mount_options: defaults - # Filesystem UUID or label can be specified. UUID recommended - fs_uuid: cdb74f1c-9e50-4e51-be1d-068b0e9ff69e - fs_label: logs - # Platform (Operating System) settings - platform: - image: ubuntu_16.04_hwe - kernel_params: default - # Additional metadata to apply to a node - metadata: - # Base URL of the introspection service - may go in curtin data - introspection_url: http://172.16.1.10:9090 ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: k8-node - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - # host_profile inheritance allows for deduplication of common CIs - # Inheritance is additive for CIs that are lists of multiple items - # To remove an inherited list member, prefix the primary key value - # with '!'. - host_profile: defaults - # Hardware profile will map hardware specific details to the abstract - # names uses in the host profile as well as specify hardware specific - # configs. A viable model should be to build a host profile without a - # hardware_profile and then for each node inherit the host profile and - # specify a hardware_profile to map that node's hardware to the abstract - # settings of the host_profile - hardware_profile: HPGen9v3 - # Network interfaces. - interfaces: - # Keyed on device_name - # pxe is a special marker indicating which device should be used for pxe boot - - device_name: pxe - # The network link attached to this - network_link: pxe - # Slaves will specify aliases from hwdefinition.yaml - slaves: - - prim_nic01 - # Which networks will be configured on this interface - networks: - - name: pxe - - device_name: bond0 - network_link: gp - # If multiple slaves are specified, but no bonding config - # is applied to the link, design validation will fail - slaves: - - prim_nic01 - - prim_nic02 - # If multiple networks are specified, but no trunking - # config is applied to the link, design validation will fail - networks: - - name: mgmt - - name: private - metadata: - # Explicit tag assignment - tags: - - 'test' - # MaaS supports key/value pairs. Not sure of the use yet - owner_data: - foo: bar ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: k8-node-public - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - host_profile: k8-node - interfaces: - - device_name: bond0 - networks: - # This is additive, so adds a network to those defined in the host_profile - # inheritance chain - - name: public ---- \ No newline at end of file diff --git a/examples/manifests/hwdefinition.yaml b/examples/manifests/hwdefinition.yaml deleted file mode 100644 index d7daa741..00000000 --- a/examples/manifests/hwdefinition.yaml +++ /dev/null @@ -1,58 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -############################################################################# -# -# bootstrap_hwdefinition.yaml - Definitions of server hardware layout -# -############################################################################# -# version the schema in this file so consumers can rationally parse it ---- -apiVersion: 'v1.0' -kind: HardwareProfile -metadata: - name: HPGen8v3 - region: sitename - date: 17-FEB-2017 - description: Sample hardware definition - author: Scott Hussey -spec: - # Vendor of the server chassis - vendor: HP - # Generation of the chassis model - generation: '8' - # Version of the chassis model within its generation - not version of the hardware definition - hw_version: '3' - # The certified version of the chassis BIOS - bios_version: '2.2.3' - # Mode of the default boot of hardware - bios, uefi - boot_mode: bios - # Protocol of boot of the hardware - pxe, usb, hdd - bootstrap_protocol: pxe - # Which interface to use for network booting within the OOB manager, not OS device - pxe_interface: 0 - # Map hardware addresses to aliases/roles to allow a mix of hardware configs - # in a site to result in a consistent configuration - device_aliases: - pci: - - address: pci@0000:00:03.0 - alias: prim_nic01 - # type could identify expected hardware - used for hardware manifest validation - type: '82540EM Gigabit Ethernet Controller' - - address: pci@0000:00:04.0 - alias: prim_nic02 - type: '82540EM Gigabit Ethernet Controller' - scsi: - - address: scsi@2:0.0.0 - alias: primary_boot - type: 'VBOX HARDDISK' \ No newline at end of file diff --git a/examples/manifests/manifest_hierarchy.png b/examples/manifests/manifest_hierarchy.png deleted file mode 100644 index b571f21feab65475508c24c3b75693395d058778..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 112581 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-0L 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 E0Gyx2p8x;= diff --git a/examples/manifests/network.yml b/examples/manifests/network.yml deleted file mode 100644 index 26c9d052..00000000 --- a/examples/manifests/network.yml +++ /dev/null @@ -1,230 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#################### -# -# network.yaml - Network infor,ation design definition for physical layer -# -#################### -# version the schema in this file so consumers can rationally parse it - ---- ---- -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: oob - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. Primary key is 'name'. These settings will generally be things the switch and server have to agree on -spec: - bonding: - mode: none - mtu: 1500 - linkspeed: 100full - trunking: - mode: none - default_network: oob ---- -# pxe is a bit of 'magic' indicating the link config used when PXE booting -# a node. All other links indicate network configs applied when the node -# is deployed. -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: pxe - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. Primary key is 'name'. These settings will generally be things the switch and server have to agree on -spec: - bonding: - mode: none - mtu: 1500 - linkspeed: auto - # Is this link supporting multiple layer 2 networks? - # none is a port-based VLAN identified by default_network - # tagged is is using 802.1q VLAN tagging. Untagged packets will default to default_netwokr - trunking: - mode: none - # use name, will translate to VLAN ID - default_network: pxe ---- -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: gp - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. These CIs will generally be things the switch and server have to agree on - # pxe is a bit of 'magic' indicating the link config used when PXE booting - # a node. All other links indicate network configs applied when the node - # is deployed. -spec: - # If this link is a bond of physical links, how is it configured - # 802.3ad - # active-backup - # balance-rr - # Can add support for others down the road - bonding: - mode: 802.3ad - # For LACP (802.3ad) xmit hashing policy: layer2, layer2+3, layer3+4, encap3+4 - hash: layer3+4 - # 802.3ad specific options - peer_rate: slow - mon_rate: default - up_delay: default - down_delay: default - mtu: 9000 - linkspeed: auto - # Is this link supporting multiple layer 2 networks? - trunking: - mode: tagged - default_network: mgmt ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: oob - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - allocation: static - cidr: 172.16.100.0/24 - ranges: - - type: static - start: 172.16.100.15 - end: 172.16.100.254 - dns: - domain: ilo.sitename.att.com - servers: 172.16.100.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: pxe - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - # Layer 2 VLAN segment id, could support other segmentations. Optional - vlan_id: '99' - # How are addresses assigned? - allocation: dhcp - # MTU for this VLAN interface, if not specified it will be inherited from the link - mtu: 1500 - # Network address - cidr: 172.16.0.0/24 - # Desribe IP address ranges - ranges: - - type: dhcp - start: 172.16.0.5 - end: 172.16.0.254 - # DNS settings for this network - dns: - # Domain addresses on this network will be registered under - domain: admin.sitename.att.com - # DNS servers that a server using this network as its default gateway should use - servers: 172.16.0.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: mgmt - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '100' - # How are addresses assigned? - allocation: static - # Allow MTU to be inherited from link the network rides on - mtu: 1500 - # Network address - cidr: 172.16.1.0/24 - # Desribe IP address ranges - ranges: - - type: static - start: 172.16.1.15 - end: 172.16.1.254 - # Static routes to be added for this network - routes: - - subnet: 0.0.0.0/0 - # A blank gateway would leave to a static route specifying - # only the interface as a source - gateway: 172.16.1.1 - metric: 10 - # DNS settings for this network - dns: - # Domain addresses on this network will be registered under - domain: mgmt.sitename.example.com - # DNS servers that a server using this network as its default gateway should use - servers: 172.16.1.9,172.16.1.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: private - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '101' - allocation: static - mtu: 9000 - cidr: 172.16.2.0/24 - # Desribe IP address ranges - ranges: - # Type can be reserved (not used for baremetal), static (all explicit - # assignments should fall here), dhcp (will be used by a DHCP server on this network) - - type: static - start: 172.16.2.15 - end: 172.16.2.254 - dns: - domain: priv.sitename.example.com - servers: 172.16.2.9,172.16.2.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: public - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '102' - # How are addresses assigned? - allocation: static - # MTU size for the VLAN interface - mtu: 1500 - cidr: 172.16.3.0/24 - # Desribe IP address ranges - ranges: - - type: static - start: 172.16.3.15 - end: 172.16.3.254 - routes: - - subnet: 0.0.0.0/0 - gateway: 172.16.3.1 - metric: 9 - dns: - domain: sitename.example.com - servers: 8.8.8.8 \ No newline at end of file diff --git a/examples/manifests/region_manifest.yml b/examples/manifests/region_manifest.yml deleted file mode 100644 index db6ac399..00000000 --- a/examples/manifests/region_manifest.yml +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#################### -# -# region_manifest.yaml - Region Manifest File , encapsulates the multiple files -# -#################### -# version - ---- -# -# This describes the Global details of a Region -# -apiVersion: 'v1.0' -kind: Region -metadata: - name: sitename - date: 17-FEB-2017 - description: Sample site design - author: sh8121@att.com -spec: - - -------- -imports: - # Servers will include the list of Servers - # For Each Server it includes - # information such as : - # # OOB (iLO, iDRAC, etc...) settings. Should prefer open standards such - # as IPMI over vender-specific when possible. - # oob: - # type: ipmi - # OOB networking should be preconfigured, but we can include a network - # definition for validation or enhancement (DNS registration) - # Specify storage layout of base OS. Ceph out of scope - # storage: - # How storage should be carved up: lvm (logical volumes), flat - # (single partition) - # Platform (Operating System) settings - # platform: - # Additional metadata to apply to a node - @ metadata: - - 'servers.yaml' - - - 'network.yaml' - - 'hwdefinition.yaml' - - 'hostprofile.yaml' - - \ No newline at end of file diff --git a/examples/manifests/servers.yaml b/examples/manifests/servers.yaml deleted file mode 100644 index 360075f9..00000000 --- a/examples/manifests/servers.yaml +++ /dev/null @@ -1,420 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -#################### -# -# bootstrap_seed.yaml - Site server design definition for physical layer -# -#################### -# version the schema in this file so consumers can rationally parse it ---- -apiVersion: 'v1.0' -kind: Region -metadata: - name: sitename - date: 17-FEB-2017 - description: Sample site design - author: sh8121@att.com -spec: - # Not sure if we have site wide data that doesn't fall into another 'Kind' ---- -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: oob - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. Primary key is 'name'. These settings will generally be things the switch and server have to agree on -spec: - bonding: - mode: none - mtu: 1500 - linkspeed: 100full - trunking: - mode: none - default_network: oob ---- -# pxe is a bit of 'magic' indicating the link config used when PXE booting -# a node. All other links indicate network configs applied when the node -# is deployed. -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: pxe - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. Primary key is 'name'. These settings will generally be things the switch and server have to agree on -spec: - bonding: - mode: none - mtu: 1500 - linkspeed: auto - # Is this link supporting multiple layer 2 networks? - # none is a port-based VLAN identified by default_network - # tagged is is using 802.1q VLAN tagging. Untagged packets will default to default_netwokr - trunking: - mode: none - # use name, will translate to VLAN ID - default_network: pxe ---- -apiVersion: 'v1.0' -kind: NetworkLink -metadata: - name: gp - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 1 attributes. These CIs will generally be things the switch and server have to agree on - # pxe is a bit of 'magic' indicating the link config used when PXE booting - # a node. All other links indicate network configs applied when the node - # is deployed. -spec: - # If this link is a bond of physical links, how is it configured - # 802.3ad - # active-backup - # balance-rr - # Can add support for others down the road - bonding: - mode: 802.3ad - # For LACP (802.3ad) xmit hashing policy: layer2, layer2+3, layer3+4, encap3+4 - hash: layer3+4 - # 802.3ad specific options - peer_rate: slow - mon_rate: default - up_delay: default - down_delay: default - mtu: 9000 - linkspeed: auto - # Is this link supporting multiple layer 2 networks? - trunking: - mode: tagged - default_network: mgmt ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: oob - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - allocation: static - cidr: 172.16.100.0/24 - ranges: - - type: static - start: 172.16.100.15 - end: 172.16.100.254 - dns: - domain: ilo.sitename.att.com - servers: 172.16.100.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: pxe - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - # Layer 2 VLAN segment id, could support other segmentations. Optional - vlan_id: '99' - # How are addresses assigned? - allocation: dhcp - # MTU for this VLAN interface, if not specified it will be inherited from the link - mtu: 1500 - # Network address - cidr: 172.16.0.0/24 - # Desribe IP address ranges - ranges: - - type: dhcp - start: 172.16.0.5 - end: 172.16.0.254 - # DNS settings for this network - dns: - # Domain addresses on this network will be registered under - domain: admin.sitename.att.com - # DNS servers that a server using this network as its default gateway should use - servers: 172.16.0.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: mgmt - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '100' - # How are addresses assigned? - allocation: static - # Allow MTU to be inherited from link the network rides on - mtu: 1500 - # Network address - cidr: 172.16.1.0/24 - # Desribe IP address ranges - ranges: - - type: static - start: 172.16.1.15 - end: 172.16.1.254 - # Static routes to be added for this network - routes: - - subnet: 0.0.0.0/0 - # A blank gateway would leave to a static route specifying - # only the interface as a source - gateway: 172.16.1.1 - metric: 10 - # DNS settings for this network - dns: - # Domain addresses on this network will be registered under - domain: mgmt.sitename.example.com - # DNS servers that a server using this network as its default gateway should use - servers: 172.16.1.9,172.16.1.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: private - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '101' - allocation: static - mtu: 9000 - cidr: 172.16.2.0/24 - # Desribe IP address ranges - ranges: - # Type can be reserved (not used for baremetal), static (all explicit - # assignments should fall here), dhcp (will be used by a DHCP server on this network) - - type: static - start: 172.16.2.15 - end: 172.16.2.254 - dns: - domain: priv.sitename.example.com - servers: 172.16.2.9,172.16.2.10 ---- -apiVersion: 'v1.0' -kind: Network -metadata: - name: public - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - vlan_id: '102' - # How are addresses assigned? - allocation: static - # MTU size for the VLAN interface - mtu: 1500 - cidr: 172.16.3.0/24 - # Desribe IP address ranges - ranges: - - type: static - start: 172.16.3.15 - end: 172.16.3.254 - routes: - - subnet: 0.0.0.0/0 - gateway: 172.16.3.1 - metric: 9 - dns: - domain: sitename.example.com - servers: 8.8.8.8 ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: default - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces - # No magic to this host_profile, it just provides a way to specify - # sitewide settings. If it is absent from a node's inheritance chain - # then these values will NOT be applied -spec: - # OOB (iLO, iDRAC, etc...) settings. Should prefer open standards such - # as IPMI over vender-specific when possible. - oob: - type: ipmi - # OOB networking should be preconfigured, but we can include a network - # definition for validation or enhancement (DNS registration) - network: oob - account: admin - credential: admin - # Specify storage layout of base OS. Ceph out of scope - storage: - # How storage should be carved up: lvm (logical volumes), flat - # (single partition) - layout: lvm - # Info specific to the boot and root disk/partitions - bootdisk: - # Device will specify an alias defined in hwdefinition.yaml - device: primary_boot - # For LVM, the size of the partition added to VG as a PV - # For flat, the size of the partition formatted as ext4 - root_size: 50g - # The /boot partition. If not specified, /boot will in root - boot_size: 2g - # Info for additional partitions. Need to balance between - # flexibility and complexity - partitions: - - name: logs - device: primary_boot - # Partition uuid if needed - part_uuid: 84db9664-f45e-11e6-823d-080027ef795a - size: 10g - # Optional, can carve up unformatted block devices - mountpoint: /var/log - fstype: ext4 - mount_options: defaults - # Filesystem UUID or label can be specified. UUID recommended - fs_uuid: cdb74f1c-9e50-4e51-be1d-068b0e9ff69e - fs_label: logs - # Platform (Operating System) settings - platform: - image: ubuntu_16.04_hwe - kernel_params: default - # Additional metadata to apply to a node - metadata: - # Base URL of the introspection service - may go in curtin data - introspection_url: http://172.16.1.10:9090 ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: k8-node - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - # host_profile inheritance allows for deduplication of common CIs - # Inheritance is additive for CIs that are lists of multiple items - # To remove an inherited list member, prefix the primary key value - # with '!'. - host_profile: defaults - # Hardware profile will map hardware specific details to the abstract - # names uses in the host profile as well as specify hardware specific - # configs. A viable model should be to build a host profile without a - # hardware_profile and then for each node inherit the host profile and - # specify a hardware_profile to map that node's hardware to the abstract - # settings of the host_profile - hardware_profile: HPGen9v3 - # Network interfaces. - interfaces: - # Keyed on device_name - # pxe is a special marker indicating which device should be used for pxe boot - - device_name: pxe - # The network link attached to this - network_link: pxe - # Slaves will specify aliases from hwdefinition.yaml - slaves: - - prim_nic01 - # Which networks will be configured on this interface - networks: - - name: pxe - - device_name: bond0 - network_link: gp - # If multiple slaves are specified, but no bonding config - # is applied to the link, design validation will fail - slaves: - - prim_nic01 - - prim_nic02 - # If multiple networks are specified, but no trunking - # config is applied to the link, design validation will fail - networks: - - name: mgmt - - name: private - metadata: - # Explicit tag assignment - tags: - - 'test' - # MaaS supports key/value pairs. Not sure of the use yet - owner_data: - foo: bar ---- -apiVersion: 'v1.0' -kind: HostProfile -metadata: - name: k8-node-public - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - host_profile: k8-node - interfaces: - - device_name: bond0 - networks: - # This is additive, so adds a network to those defined in the host_profile - # inheritance chain - - name: public ---- -apiVersion: 'v1.0' -kind: BaremetalNode -metadata: - name: controller01 - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - host_profile: k8-node-public - # the hostname for a server, could be used in multiple DNS domains to - # represent different interfaces - interfaces: - - device_name: bond0 - networks: - # '!' prefix for the value of the primary key indicates a record should be removed - - name: '!private' - # Addresses assigned to network interfaces - addressing: - # Which network the address applies to. If a network appears in addressing - # that isn't assigned to an interface, design validation will fail - - network: pxe - # The address assigned. Either a explicit IPv4 or IPv6 address - # or dhcp or slaac - address: dhcp - - network: mgmt - address: 172.16.1.20 - - network: public - address: 172.16.3.20 - metadata: - tags: - - os_ctl - rack: rack01 ---- -apiVersion: 'v1.0' -kind: BaremetalNode -metadata: - name: compute01 - region: sitename - date: 17-FEB-2017 - author: sh8121@att.com - description: Describe layer 2/3 attributes. Primarily CIs used for configuring server interfaces -spec: - host_profile: k8-node - addressing: - - network: pxe - address: dhcp - - network: mgmt - address: 172.16.1.21 - - network: private - address: 172.16.2.21 diff --git a/generator/config-generator.conf b/generator/config-generator.conf new file mode 100644 index 00000000..690017db --- /dev/null +++ b/generator/config-generator.conf @@ -0,0 +1,5 @@ +[DEFAULT] +output_file = etc/shipyard/shipyard.conf.sample +wrap_width=79 +namespace = shipyard_airflow +namespace = keystonemiddleware.auth_token \ No newline at end of file diff --git a/generator/policy-generator.conf b/generator/policy-generator.conf new file mode 100644 index 00000000..9af53045 --- /dev/null +++ b/generator/policy-generator.conf @@ -0,0 +1,3 @@ +[DEFAULT] +output_file = etc/shipyard/policy.yaml.sample +namespace = shipyard_airflow \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index bc67a50a..4d4b670e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,16 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -PasteDeploy==1.5.2 -keystonemiddleware==4.17.0 -falcon==1.2.0 -python-dateutil==2.6.1 -requests==2.18.4 -uwsgi==2.0.15 +alembic==0.9.5 configparser==3.5.0 -python-openstackclient==3.11.0 -SQLAlchemy==1.1.13 -psycopg2==2.7.3.1 +falcon==1.2.0 +jsonschema==2.6.0 +keystoneauth1==2.13.0 +keystonemiddleware==4.17.0 oslo.config==4.11.0 oslo.policy==1.25.1 -keystoneauth1==2.13.0 +PasteDeploy==1.5.2 +pbr!=2.1.0,>=2.0.0 # Apache-2.0 +psycopg2==2.7.3.1 +python-dateutil==2.6.1 +python-openstackclient==3.11.0 +requests==2.18.4 +SQLAlchemy==1.1.13 +ulid==1.1 +uwsgi==2.0.15 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..96b41e17 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,28 @@ +[metadata] +name = shipyard +summary = Directed acyclic graph controller for Kubernetes and OpenStack control plane life cycle management +description-file = README.md + +author = undercloud team +home-page = https://github.com/att-comdev/shipyard +classifier = + Intended Audience :: Information Technology + Intended Audience :: System Administrators + License :: OSI Approved :: Apache Software License + Operating System :: POSIX :: Linux + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 + +[files] +packages = + shipyard_airflow + +[entry_points] +oslo.config.opts = + shipyard_airflow = shipyard_airflow.conf.opts:list_opts +oslo.policy.policies = + shipyard_airflow = shipyard_airflow.policy:list_policies + +[build_sphinx] +warning-is-error = True diff --git a/setup.py b/setup.py index f6d4d9b8..0cbf35b6 100644 --- a/setup.py +++ b/setup.py @@ -11,28 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import setuptools -from setuptools import setup - -setup( - name='shipyard_airflow', - version='0.1a1', - description='API for managing Airflow-based orchestration', - url='http://github.com/att-comdev/shipyard', - author='Anthony Lin - AT&T', - author_email='al498u@att.com', - license='Apache 2.0', - packages=['shipyard_airflow', 'shipyard_airflow.control'], - entry_points={ - "oslo.policy.policies": - ["shipyard = shipyard.common.policies:list_rules"], - "oslo.config.opts": ["shipyard = shipyard.conf.opts:list_opts"] - }, - install_requires=[ - 'falcon', - 'requests', - 'configparser', - 'uwsgi>1.4', - 'python-dateutil', - 'oslo.config', - ]) +setuptools.setup( + setup_requires=['pbr>=2.0.0'], + pbr=True +) diff --git a/shipyard_airflow/airflow_client.py b/shipyard_airflow/airflow_client.py deleted file mode 100644 index bb1feabf..00000000 --- a/shipyard_airflow/airflow_client.py +++ /dev/null @@ -1,17 +0,0 @@ -import requests - -from shipyard_airflow.errors import AirflowError - - -class AirflowClient(object): - def __init__(self, url): - self.url = url - - def get(self): - response = requests.get(self.url).json() - - # This gives us more freedom to handle the responses from airflow - if response["output"]["stderr"]: - raise AirflowError(response["output"]["stderr"]) - else: - return response["output"]["stdout"] diff --git a/shipyard_airflow/conf/__init__.py b/shipyard_airflow/conf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/shipyard_airflow/conf/config.py b/shipyard_airflow/conf/config.py new file mode 100644 index 00000000..5cd98c67 --- /dev/null +++ b/shipyard_airflow/conf/config.py @@ -0,0 +1,250 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +import logging + +import keystoneauth1.loading as ks_loading +from oslo_config import cfg + +from shipyard_airflow.conf.opts import ConfigSection + +CONF = cfg.CONF +SECTIONS = [ + ConfigSection( + name='base', + title='Base Configuration', + options=[ + cfg.StrOpt( + 'web_server', + default='http://localhost:32080', + help='The web server for Airflow' + ), + cfg.StrOpt( + 'postgresql_db', + default=( + 'postgresql+psycopg2://shipyard:changeme' + '@postgresql.ucp:5432/shipyard' + ), + help='The database for shipyard' + ), + cfg.StrOpt( + 'postgresql_airflow_db', + default=( + 'postgresql+psycopg2://shipyard:changeme' + '@postgresql.ucp:5432/airflow' + ), + help='The database for airflow' + ), + cfg.StrOpt( + 'alembic_ini_path', + default='/home/shipyard/shipyard', + help='The direcotry containing the alembic.ini file' + ), + cfg.BoolOpt( + 'upgrade_db', + default=True, + help='Upgrade the database on startup' + ) + ] + ), + ConfigSection( + name='logging', + title='Logging Options', + options=[ + cfg.IntOpt( + 'log_level', + default=logging.DEBUG, + help=('The default logging level for the root logger. ' + 'ERROR=40, WARNING=30, INFO=20, DEBUG=10') + ), + ] + ), + ConfigSection( + name='shipyard', + title='Shipyard connection info', + options=[ + cfg.StrOpt( + 'host', + default='shipyard-int.ucp', + help='FQDN for the shipyard service' + ), + cfg.IntOpt( + 'port', + default=9000, + help='Port for the shipyard service' + ), + ] + ), + ConfigSection( + name='deckhand', + title='Deckhand connection info', + options=[ + cfg.StrOpt( + 'host', + default='deckhand-int.ucp', + help='FQDN for the deckhand service' + ), + cfg.IntOpt( + 'port', + default=80, + help='Port for the deckhand service' + ), + ] + ), + ConfigSection( + name='armada', + title='Armada connection info', + options=[ + cfg.StrOpt( + 'host', + default='armada-int.ucp', + help='FQDN for the armada service' + ), + cfg.IntOpt( + 'port', + default=8000, + help='Port for the armada service' + ), + ] + ), + ConfigSection( + name='drydock', + title='Drydock connection info', + options=[ + cfg.StrOpt( + 'host', + default='drydock-int.ucp', + help='FQDN for the drydock service' + ), + cfg.IntOpt( + 'port', + default=9000, + help='Port for the drydock service' + ), + # TODO(Bryan Strassner) Remove this when integrated + cfg.StrOpt( + 'token', + default='bigboss', + help='TEMPORARY: password for drydock' + ), + # TODO(Bryan Strassner) Remove this when integrated + cfg.StrOpt( + 'site_yaml', + default='/usr/local/airflow/plugins/drydock.yaml', + help='TEMPORARY: location of drydock yaml file' + ), + # TODO(Bryan Strassner) Remove this when integrated + cfg.StrOpt( + 'prom_yaml', + default='/usr/local/airflow/plugins/promenade.yaml', + help='TEMPORARY: location of promenade yaml file' + ), + ] + ), + ConfigSection( + name='healthcheck', + title='Healthcheck connection info', + options=[ + cfg.StrOpt( + 'schema', + default='http', + help='Schema to perform health check with' + ), + cfg.StrOpt( + 'endpoint', + default='/api/v1.0/health', + help='Health check standard endpoint' + ), + ] + ), + # TODO (Bryan Strassner) This section is in use by the operators we send + # to the airflow pod(s). Needs to be refactored out + # when those operators are updated. + ConfigSection( + name='keystone', + title='Keystone connection and credential information', + options=[ + cfg.StrOpt( + 'OS_AUTH_URL', + default='http://keystone-api.ucp:80/v3', + help='The url for OpenStack Authentication' + ), + cfg.StrOpt( + 'OS_PROJECT_NAME', + default='service', + help='OpenStack project name' + ), + cfg.StrOpt( + 'OS_USER_DOMAIN_NAME', + default='Default', + help='The OpenStack user domain name' + ), + cfg.StrOpt( + 'OS_USERNAME', + default='shipyard', + help='The OpenStack username' + ), + cfg.StrOpt( + 'OS_PASSWORD', + default='password', + help='THe OpenStack password for the shipyard svc acct' + ), + cfg.StrOpt( + 'OS_REGION_NAME', + default='Regionone', + help='The OpenStack user domain name' + ), + cfg.IntOpt( + 'OS_IDENTITY_API_VERSION', + default=3, + help='The OpenStack identity api version' + ), + ] + ), +] + +def register_opts(conf): + """ + Registers all the sections in this module. + """ + for section in SECTIONS: + conf.register_group( + cfg.OptGroup(name=section.name, + title=section.title, + help=section.help)) + conf.register_opts(section.options, group=section.name) + + # TODO (Bryan Strassner) is there a better, more general way to do this, + # or is password enough? Probably need some guidance + # from someone with more experience in this space. + conf.register_opts( + ks_loading.get_auth_plugin_conf_options('password'), + group='keystone_authtoken' + ) + + +def list_opts(): + return { + section.name: section.options for section in SECTIONS + } + + +def parse_args(args=None, usage=None, default_config_files=None): + CONF(args=args, + project='shipyard', + usage=usage, + default_config_files=default_config_files) + + +register_opts(CONF) diff --git a/shipyard_airflow/conf/opts.py b/shipyard_airflow/conf/opts.py new file mode 100644 index 00000000..f02f8e71 --- /dev/null +++ b/shipyard_airflow/conf/opts.py @@ -0,0 +1,89 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections +import importlib +import os +import pkgutil + +LIST_OPTS_FUNC_NAME = "list_opts" +IGNORED_MODULES = ('opts', 'constants', 'utils') +CONFIG_PATH = 'shipyard_airflow.conf' + + +class ConfigSection(object): + """ + Defines a configuration section + """ + def __init__(self, name, title, options, help=None): + self.name = name + self.title = title + self.help = help + self.options = options + + +def _tupleize(dct): + """Take the dict of options and convert to the 2-tuple format.""" + return [(key, val) for key, val in dct.items()] + + +def list_opts(): + """Entry point used only in the context of sample file generation. + This is the single point of entry to generate the sample configuration + file. It collects all the necessary info from the other modules in this + package. It is assumed that: + * every other module in this package has a 'list_opts' function which + return a dict where + * the keys are strings which are the group names + * the value of each key is a list of config options for that group + * the {program}.conf package doesn't have further packages with config + options + """ + opts = collections.defaultdict(list) + module_names = _list_module_names() + imported_modules = _import_modules(module_names) + _append_config_options(imported_modules, opts) + return _tupleize(opts) + + +def _list_module_names(): + module_names = [] + package_path = os.path.dirname(os.path.abspath(__file__)) + for _, modname, ispkg in pkgutil.iter_modules(path=[package_path]): + if modname in IGNORED_MODULES or ispkg: + continue + else: + module_names.append(modname) + return module_names + + +def _import_modules(module_names): + imported_modules = [] + for modname in module_names: + mod = importlib.import_module(CONFIG_PATH + '.' + modname) + if not hasattr(mod, LIST_OPTS_FUNC_NAME): + msg = "The module '%s.%s' should have a '%s' "\ + "function which returns the config options." % \ + (CONFIG_PATH, modname, LIST_OPTS_FUNC_NAME) + raise Exception(msg) + else: + imported_modules.append(mod) + return imported_modules + + +def _append_config_options(imported_modules, config_options): + for mod in imported_modules: + configs = mod.list_opts() + for key, val in configs.items(): + config_options[key].extend(val) diff --git a/shipyard_airflow/config.py b/shipyard_airflow/config.py deleted file mode 100644 index 820027dc..00000000 --- a/shipyard_airflow/config.py +++ /dev/null @@ -1,202 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -"""Single point of entry to generate the sample configuration file. -This module collects all the necessary info from the other modules in this -package. It is assumed that: -* Every other module in this package has a 'list_opts' function which - returns a dict where: - * The keys are strings which are the group names. - * The value of each key is a list of config options for that group. -* The conf package doesn't have further packages with config options. -* This module is only used in the context of sample file generation. -""" -import importlib -import os -import pkgutil - -from oslo_config import cfg -import keystoneauth1.loading as loading - -IGNORED_MODULES = ('shipyard', 'config') - -if (os.path.exists('etc/shipyard/shipyard.conf')): - cfg.CONF(['--config-file', 'etc/shipyard/shipyard.conf']) - -class ShipyardConfig(object): - """ - Initialize all the core options - """ - # Default options - options = [ - cfg.IntOpt( - 'poll_interval', - default=10, - help=[ - '''Polling interval in seconds for checking subtask or - downstream status''' - ]), - ] - - # Logging options - logging_options = [ - cfg.StrOpt( - 'log_level', default='INFO', help='Global log level for Shipyard'), - cfg.StrOpt( - 'global_logger_name', - default='shipyard', - help='Logger name for the top-level logger'), - ] - - # Enabled plugins - plugin_options = [ - cfg.MultiStrOpt( - 'ingester', - default=['shipyard_airflow.ingester.plugins.yaml.YamlIngester'], - help='Module path string of a input ingester to enable'), - cfg.MultiStrOpt( - 'oob_driver', - default=[ - 'shipyard_airflow.drivers.oob.pyghmi_driver.PyghmiDriver' - ], - help='Module path string of a OOB driver to enable'), - cfg.StrOpt( - 'node_driver', - default=[ - '''shipyard_airflow.drivers.node.maasdriver.driver - .MaasNodeDriver''' - ], - help='Module path string of the Node driver to enable'), - # TODO Network driver not yet implemented - cfg.StrOpt( - 'network_driver', - default=None, - help='Module path string of the Network driver enable'), - ] - - # Timeouts for various tasks specified in minutes - timeout_options = [ - cfg.IntOpt( - 'shipyard_timeout', - default=5, - help='Fallback timeout when a specific one is not configured'), - cfg.IntOpt( - 'create_network_template', - default=2, - help='Timeout in minutes for creating site network templates'), - cfg.IntOpt( - 'configure_user_credentials', - default=2, - help='Timeout in minutes for creating user credentials'), - cfg.IntOpt( - 'identify_node', - default=10, - help='Timeout in minutes for initial node identification'), - cfg.IntOpt( - 'configure_hardware', - default=30, - help=[ - '''Timeout in minutes for node commissioning and - hardware configuration''' - ]), - cfg.IntOpt( - 'apply_node_networking', - default=5, - help='Timeout in minutes for configuring node networking'), - cfg.IntOpt( - 'apply_node_platform', - default=5, - help='Timeout in minutes for configuring node platform'), - cfg.IntOpt( - 'deploy_node', - default=45, - help='Timeout in minutes for deploying a node'), - ] - - def __init__(self): - self.conf = cfg.CONF - - def register_options(self): - self.conf.register_opts(ShipyardConfig.options) - self.conf.register_opts( - ShipyardConfig.logging_options, group='logging') - self.conf.register_opts(ShipyardConfig.plugin_options, group='plugins') - self.conf.register_opts( - ShipyardConfig.timeout_options, group='timeouts') - self.conf.register_opts( - loading.get_auth_plugin_conf_options('password'), - group='keystone_authtoken') - - -config_mgr = ShipyardConfig() - - -def list_opts(): - opts = { - 'DEFAULT': ShipyardConfig.options, - 'logging': ShipyardConfig.logging_options, - 'plugins': ShipyardConfig.plugin_options, - 'timeouts': ShipyardConfig.timeout_options - } - - package_path = os.path.dirname(os.path.abspath(__file__)) - parent_module = ".".join(__name__.split('.')[:-1]) - module_names = _list_module_names(package_path, parent_module) - imported_modules = _import_modules(module_names) - _append_config_options(imported_modules, opts) - # Assume we'll use the password plugin, - # so include those options in the configuration template - opts['keystone_authtoken'] = loading.get_auth_plugin_conf_options( - 'password') - return _tupleize(opts) - - -def _tupleize(d): - """Convert a dict of options to the 2-tuple format.""" - return [(key, value) for key, value in d.items()] - - -def _list_module_names(pkg_path, parent_module): - module_names = [] - for _, module_name, ispkg in pkgutil.iter_modules(path=[pkg_path]): - if module_name in IGNORED_MODULES: - # Skip this module. - continue - elif ispkg: - module_names.extend( - _list_module_names(pkg_path + "/" + module_name, - parent_module + "." + module_name)) - else: - module_names.append(parent_module + "." + module_name) - return module_names - - -def _import_modules(module_names): - imported_modules = [] - for module_name in module_names: - module = importlib.import_module(module_name) - if hasattr(module, 'list_opts'): - print("Pulling options from module %s" % module.__name__) - imported_modules.append(module) - return imported_modules - - -def _append_config_options(imported_modules, config_options): - for module in imported_modules: - configs = module.list_opts() - for key, val in configs.items(): - if key not in config_options: - config_options[key] = val - else: - config_options[key].extend(val) diff --git a/shipyard_airflow/control/__init__.py b/shipyard_airflow/control/__init__.py index f10bbbf6..e69de29b 100644 --- a/shipyard_airflow/control/__init__.py +++ b/shipyard_airflow/control/__init__.py @@ -1,13 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. diff --git a/shipyard_airflow/control/action_helper.py b/shipyard_airflow/control/action_helper.py new file mode 100644 index 00000000..09855d8a --- /dev/null +++ b/shipyard_airflow/control/action_helper.py @@ -0,0 +1,63 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Common methods for use by action api classes as necessary +""" + +DAG_STATE_MAPPING = { + 'QUEUED': 'Pending', + 'RUNNING': 'Processing', + 'SUCCESS': 'Complete', + 'SHUTDOWN': 'Failed', + 'FAILED': 'Failed', + 'UP_FOR_RETRY': 'Processing', + 'UPSTREAM_FAILED': 'Failed', + 'SKIPPED': 'Failed', + 'REMOVED': 'Failed', + 'SCHEDULED': 'Pending', + 'NONE': 'Pending', + 'PAUSED': 'Paused' +} + +def determine_lifecycle(dag_status=None): + """ + Convert a dag_status to an action_lifecycle value + """ + if dag_status is None: + dag_status = 'NONE' + return DAG_STATE_MAPPING.get(dag_status.upper()) + +def format_action_steps(action_id, steps): + """ + Converts a list of action step database records to desired format + """ + if not steps: + return [] + steps_response = [] + for idx, step in enumerate(steps): + steps_response.append(format_step(action_id=action_id, + step=step, + index=idx + 1)) + return steps_response + +def format_step(action_id, step, index): + """ + reformat a step (dictionary) into a common response format + """ + return { + 'url': '/actions/{}/steps/{}'.format(action_id, step.get('task_id')), + 'state': step.get('state'), + 'id': step.get('task_id'), + 'index': index + } diff --git a/shipyard_airflow/control/actions_api.py b/shipyard_airflow/control/actions_api.py new file mode 100644 index 00000000..5cbbdd63 --- /dev/null +++ b/shipyard_airflow/control/actions_api.py @@ -0,0 +1,330 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from datetime import datetime + +import falcon +import requests +from requests.exceptions import RequestException +from dateutil.parser import parse +from oslo_config import cfg +import ulid + +from shipyard_airflow import policy +from shipyard_airflow.control.action_helper import (determine_lifecycle, + format_action_steps) +from shipyard_airflow.control.base import BaseResource +from shipyard_airflow.control.json_schemas import ACTION +from shipyard_airflow.db.db import AIRFLOW_DB, SHIPYARD_DB +from shipyard_airflow.errors import ApiError + +CONF = cfg.CONF + +# Mappings of actions to dags +SUPPORTED_ACTION_MAPPINGS = { + # action : dag, validation + 'deploy_site': { + 'dag': 'deploy_site', + 'validator': None + }, + 'update_site': { + 'dag': 'update_site', + 'validator': None + }, + 'redeploy_server': { + 'dag': 'redeploy_sever', + # TODO (Bryan Strassner) This should have a validator method + # Needs to be revisited when defined + 'validator': None + } +} + + +# /api/v1.0/actions +class ActionsResource(BaseResource): + """ + The actions resource represent the asyncrhonous invocations of shipyard + """ + + @policy.ApiEnforcer('workflow_orchestrator:list_actions') + def on_get(self, req, resp, **kwargs): + """ + Return actions that have been invoked through shipyard. + :returns: a json array of action entities + """ + resp.body = self.to_json(self.get_all_actions()) + resp.status = falcon.HTTP_200 + self.info(req.context, 'response data is %s' % resp.body) + + @policy.ApiEnforcer('workflow_orchestrator:create_action') + def on_post(self, req, resp, **kwargs): + """ + Accept an action into shipyard + """ + input_action = self.req_json(req, validate_json_schema=ACTION) + action = self.create_action(action=input_action, context=req.context) + self.info(req.context, "Id %s generated for action %s " % + (action['id'], action['name'])) + # respond with the action and location for checking status + resp.status = falcon.HTTP_201 + resp.body = self.to_json(action) + # TODO (Bryan Strassner) figure out the right way to do this: + resp.location = '/api/v1.0/actions/{}'.format(action['id']) + + def create_action(self, action, context): + # use uuid assigned for this request as the id of the action. + action['id'] = ulid.ulid() + # the invoking user + action['user'] = context.user + # add current timestamp (UTC) to the action. + action['timestamp'] = str(datetime.utcnow()) + # validate that action is supported. + self.info(context, "Attempting action: %s" % action['name']) + if action['name'] not in SUPPORTED_ACTION_MAPPINGS: + raise ApiError( + title='Unable to start action', + description='Unsupported Action: {}'.format(action['name'])) + + dag = SUPPORTED_ACTION_MAPPINGS.get(action['name'])['dag'] + action['dag_id'] = dag + + # populate action parameters if they are not set + if 'parameters' not in action: + action['parameters'] = {} + + # validate if there is any validation to do + validator = SUPPORTED_ACTION_MAPPINGS.get(action['name'])['validator'] + if validator is not None: + # validators will raise ApiError if they are not validated. + validator(action) + + # invoke airflow, get the dag's date + dag_execution_date = self.invoke_airflow_dag( + dag_id=dag, action=action, context=context) + # set values on the action + action['dag_execution_date'] = dag_execution_date + action['dag_status'] = 'SCHEDULED' + + # context_marker is the uuid from the request context + action['context_marker'] = context.request_id + + # insert the action into the shipyard db + self.insert_action(action=action) + self.audit_control_command_db({ + 'id': ulid.ulid(), + 'action_id': action['id'], + 'command': 'invoke', + 'user': context.user + }) + + return action + + def get_all_actions(self): + """ + Interacts with airflow and the shipyard database to return the list of + actions invoked through shipyard. + """ + # fetch actions from the shipyard db + all_actions = self.get_action_map() + # fetch the associated dags, steps from the airflow db + all_dag_runs = self.get_dag_run_map() + all_tasks = self.get_all_tasks_db() + + # correlate the actions and dags into a list of action entites + actions = [] + + for action_id, action in all_actions.items(): + dag_key = action['dag_id'] + action['dag_execution_date'] + dag_key_id = action['dag_id'] + dag_key_date = action['dag_execution_date'] + # locate the dag run associated + dag_state = all_dag_runs.get(dag_key, {}).get('state', None) + # get the dag status from the dag run state + action['dag_status'] = dag_state + action['action_lifecycle'] = determine_lifecycle(dag_state) + # get the steps summary + action_tasks = [ + step for step in all_tasks + if step['dag_id'].startswith(dag_key_id) and + step['execution_date'].strftime( + '%Y-%m-%dT%H:%M:%S') == dag_key_date + ] + action['steps'] = format_action_steps(action_id, action_tasks) + actions.append(action) + + return actions + + def get_action_map(self): + """ + maps an array of dictionaries to a dictonary of the same results by id + :returns: a dictionary of dictionaries keyed by action id + """ + return {action['id']: action for action in self.get_all_actions_db()} + + def get_all_actions_db(self): + """ + Wrapper for call to the shipyard database to get all actions + :returns: a dictionary of dictionaries keyed by action id + """ + return SHIPYARD_DB.get_all_submitted_actions() + + def get_dag_run_map(self): + """ + Maps an array of dag runs to a keyed dictionary + :returns: a dictionary of dictionaries keyed by dag_id and + execution_date + """ + return { + run['dag_id'] + + run['execution_date'].strftime('%Y-%m-%dT%H:%M:%S'): run + for run in self.get_all_dag_runs_db() + } + + def get_all_dag_runs_db(self): + """ + Wrapper for call to the airflow db to get all dag runs + :returns: a dictionary of dictionaries keyed by dag_id and + execution_date + """ + return AIRFLOW_DB.get_all_dag_runs() + + def get_all_tasks_db(self): + """ + Wrapper for call to the airflow db to get all tasks + :returns: a list of task dictionaries + """ + return AIRFLOW_DB.get_all_tasks() + + def insert_action(self, action): + """ + Wrapper for call to the shipyard db to insert an action + """ + return SHIPYARD_DB.insert_action(action) + + def audit_control_command_db(self, action_audit): + """ + Wrapper for the shipyard db call to record an audit of the + action control taken + """ + return SHIPYARD_DB.insert_action_command_audit(action_audit) + + def invoke_airflow_dag(self, dag_id, action, context): + """ + Call airflow, and invoke a dag + :param dag_id: the name of the dag to invoke + :param action: the action structure to invoke the dag with + """ + # Retrieve URL + web_server_url = CONF.base.web_server + + if 'Error' in web_server_url: + raise ApiError( + title='Unable to invoke workflow', + description=('Airflow URL not found by Shipyard. ' + 'Shipyard configuration is missing web_server ' + 'value'), + status=falcon.HTTP_503, + retry=True, ) + + else: + conf_value = {'action': action} + # "conf" - JSON string that gets pickled into the DagRun's + # conf attribute + req_url = ('{}admin/rest_api/api?api=trigger_dag&dag_id={}' + '&conf={}'.format(web_server_url, + dag_id, self.to_json(conf_value))) + + try: + resp = requests.get(req_url, timeout=15) + self.info(context, + 'Response code from Airflow trigger_dag: %s' % + resp.status_code) + resp.raise_for_status() + response = resp.json() + self.info(context, + 'Response from Airflow trigger_dag: %s' % + response) + except (RequestException) as rex: + self.error(context, "Request to airflow failed: %s" % rex.args) + raise ApiError( + title='Unable to complete request to Airflow', + description=( + 'Airflow could not be contacted properly by Shipyard.' + ), + status=falcon.HTTP_503, + error_list=[{ + 'message': str(type(rex)) + }], + retry=True, ) + + # Returns error response if API call returns + # response code other than 200 + if response["http_response_code"] != 200: + raise ApiError( + title='Unable to invoke workflow', + description=( + 'Airflow URL not found by Shipyard.', + 'Shipyard configuration is missing web_server value'), + status=falcon.HTTP_503, + error_list=[{ + 'message': response['output'] + }], + retry=True, ) + else: + dag_time = self._exhume_date(dag_id, + response['output']['stdout']) + dag_execution_date = dag_time.strftime('%Y-%m-%dT%H:%M:%S') + return dag_execution_date + + def _exhume_date(self, dag_id, log_string): + # we are unable to use the response time because that + # does not match the time when the dag was recorded. + # We have to parse the stdout returned to find the + # Created <DagRun {dag_id} @ {timestamp} + # e.g. + # ...- Created <DagRun deploy_site @ 2017-09-22 22:16:14: man... + # split on "Created <DagRun deploy_site @ ", then ': " + # should get to the desired date string. + # + # returns the date found in a date object + log_split = log_string.split('Created <DagRun {} @ '.format(dag_id)) + if len(log_split) < 2: + raise ApiError( + title='Unable to determine if workflow has started', + description=( + 'Airflow has not responded with parseable output. ', + 'Shipyard is unable to determine run timestamp'), + status=falcon.HTTP_500, + error_list=[{ + 'message': log_string + }], + retry=True, + ) + else: + # everything before the ': ' should be a date/time + date_split = log_split[1].split(': ')[0] + try: + return parse(date_split, ignoretz=True) + except ValueError as valerr: + raise ApiError( + title='Unable to determine if workflow has started', + description=( + 'Airflow has not responded with parseable output. ', + 'Shipyard is unable to determine run timestamp'), + status=falcon.HTTP_500, + error_list=[{ + 'message': 'value {} has caused {}'.format(date_split, + valerr) + }], + retry=True, + ) diff --git a/shipyard_airflow/control/actions_control_api.py b/shipyard_airflow/control/actions_control_api.py new file mode 100644 index 00000000..203c0fd6 --- /dev/null +++ b/shipyard_airflow/control/actions_control_api.py @@ -0,0 +1,129 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import falcon +import ulid + +from shipyard_airflow import policy +from shipyard_airflow.control.base import BaseResource +from shipyard_airflow.db.db import AIRFLOW_DB, SHIPYARD_DB +from shipyard_airflow.db.errors import AirflowStateError +from shipyard_airflow.errors import ApiError + + +# /api/v1.0/actions/{action_id}/control/{control_verb} +class ActionsControlResource(BaseResource): + """ + The actions control resource allows for runtime control + """ + def __init__(self): + BaseResource.__init__(self) + self.controls = { + 'pause': self.pause_dag, + 'unpause': self.unpause_dag, + 'stop': self.stop_dag + } + + @policy.ApiEnforcer('workflow_orchestrator:invoke_action_control') + def on_post(self, req, resp, **kwargs): + """ + Returns that a control was recevied (202 response) + :returns: a no-body response + """ + self.handle_control(kwargs['action_id'], + kwargs['control_verb'], + req.context) + resp.status = falcon.HTTP_202 + + def handle_control(self, action_id, control_verb, context): + """ + Interacts with airflow to trigger a dag control + :returns: nothing + """ + action = self.get_action_db(action_id=action_id) + + if action is None: + raise ApiError( + title='Action not found', + description='Unknown action {}'.format(action_id), + status=falcon.HTTP_404) + + if control_verb in self.controls: + self.controls.get(control_verb)( + dag_id=action['dag_id'], + execution_date=action['dag_execution_date']) + self.audit_control_command_db({ + 'id': ulid.ulid(), + 'action_id': action_id, + 'command': control_verb, + 'user': context.user + }) + else: + raise ApiError( + title='Control not supported', + description='Unknown control {}'.format(control_verb), + status=falcon.HTTP_404) + + def get_action_db(self, action_id): + """ + Wrapper for call to the shipyard database to get an action + :returns: a dictionary of action details. + """ + return SHIPYARD_DB.get_action_by_id( + action_id=action_id) + + def audit_control_command_db(self, action_audit): + """ + Wrapper for the shipyard db call to record an audit of the + action control taken + """ + return SHIPYARD_DB.insert_action_command_audit(action_audit) + + def pause_dag(self, dag_id, execution_date): + """ + Sets the pause flag on this dag/execution + """ + try: + AIRFLOW_DB.pause_dag_run( + dag_id=dag_id, execution_date=execution_date) + except AirflowStateError as state_error: + raise ApiError( + title='Unable to pause action', + description=state_error.message, + status=falcon.HTTP_409) + + def unpause_dag(self, dag_id, execution_date): + """ + Clears the pause flag on this dag/execution + """ + try: + AIRFLOW_DB.unpause_dag_run( + dag_id=dag_id, execution_date=execution_date) + except AirflowStateError as state_error: + raise ApiError( + title='Unable to unpause action', + description=state_error.message, + status=falcon.HTTP_409) + + def stop_dag(self, dag_id, execution_date): + """ + Sets the stop flag on this dag/execution + """ + try: + AIRFLOW_DB.stop_dag_run( + dag_id=dag_id, execution_date=execution_date) + except AirflowStateError as state_error: + raise ApiError( + title='Unable to stop action', + description=state_error.message, + status=falcon.HTTP_409) diff --git a/shipyard_airflow/control/actions_id_api.py b/shipyard_airflow/control/actions_id_api.py new file mode 100644 index 00000000..be5193a5 --- /dev/null +++ b/shipyard_airflow/control/actions_id_api.py @@ -0,0 +1,117 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import falcon + +from shipyard_airflow import policy +from shipyard_airflow.control.action_helper import (determine_lifecycle, + format_action_steps) +from shipyard_airflow.control.base import BaseResource +from shipyard_airflow.db.db import AIRFLOW_DB, SHIPYARD_DB +from shipyard_airflow.errors import ApiError + + +# /api/v1.0/actions/{action_id} +class ActionsIdResource(BaseResource): + """ + The actions resource represent the asyncrhonous invocations of shipyard + """ + @policy.ApiEnforcer('workflow_orchestrator:get_action') + def on_get(self, req, resp, **kwargs): + """ + Return actions that have been invoked through shipyard. + :returns: a json array of action entities + """ + resp.body = self.to_json(self.get_action(kwargs['action_id'])) + resp.status = falcon.HTTP_200 + + def get_action(self, action_id): + """ + Interacts with airflow and the shipyard database to return the + requested action invoked through shipyard. + """ + # get the action from shipyard db + action = self.get_action_db(action_id=action_id) + if action is None: + raise ApiError( + title='Action not found', + description='Unknown Action: {}'.format(action_id), + status=falcon.HTTP_404) + + # lookup the dag and tasks based on the associated dag_id, + # execution_date + dag_id = action['dag_id'] + dag_execution_date = action['dag_execution_date'] + + dag = self.get_dag_run_by_id(dag_id, dag_execution_date) + steps = self.get_tasks_db(dag_id, dag_execution_date) + if dag is not None: + # put the values together into an "action" object + action['dag_status'] = dag['state'] + action['action_lifecycle'] = determine_lifecycle(dag['state']) + action['steps'] = format_action_steps(action_id, steps) + action['validations'] = self.get_validations_db(action_id) + action['command_audit'] = self.get_action_command_audit_db(action_id) + return action + + def get_dag_run_by_id(self, dag_id, execution_date): + """ + Wrapper for call to the airflow db to get a dag_run + :returns: a dag run dictionary + """ + dag_run_list = self.get_dag_run_db(dag_id, execution_date) + # should be only one result, return the first one + if dag_run_list: + return dag_run_list[0] + else: + return None + + def get_action_db(self, action_id): + """ + Wrapper for call to the shipyard database to get an action + :returns: a dictionary of action details. + """ + return SHIPYARD_DB.get_action_by_id( + action_id=action_id) + + def get_validations_db(self, action_id): + """ + Wrapper for call to the shipyard db to get validations associated with + an action + :returns: an array of dictionaries of validation details. + """ + return SHIPYARD_DB.get_validation_by_action_id( + action_id=action_id) + + def get_tasks_db(self, dag_id, execution_date): + """ + Wrapper for call to the airflow db to get all tasks + :returns: a list of task dictionaries + """ + return AIRFLOW_DB.get_tasks_by_id( + dag_id=dag_id, execution_date=execution_date) + + def get_dag_run_db(self, dag_id, execution_date): + """ + Wrapper for call to the airflow db to get a dag_run + :returns: a dag run dictionaries + """ + return AIRFLOW_DB.get_dag_runs_by_id( + dag_id=dag_id, execution_date=execution_date) + + def get_action_command_audit_db(self, action_id): + """ + Wrapper for call to the shipyard db to get the history of + action command audit records + """ + return SHIPYARD_DB.get_command_audit_by_action_id(action_id) diff --git a/shipyard_airflow/control/actions_steps_id_api.py b/shipyard_airflow/control/actions_steps_id_api.py new file mode 100644 index 00000000..a49d550a --- /dev/null +++ b/shipyard_airflow/control/actions_steps_id_api.py @@ -0,0 +1,84 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import falcon + +from shipyard_airflow import policy +from shipyard_airflow.control.base import BaseResource +from shipyard_airflow.db.db import AIRFLOW_DB, SHIPYARD_DB +from shipyard_airflow.errors import ApiError + + +# /api/v1.0/actions/{action_id}/steps/{step_id} +class ActionsStepsResource(BaseResource): + """ + The actions steps resource is the steps of an action + """ + @policy.ApiEnforcer('workflow_orchestrator:get_action_step') + def on_get(self, req, resp, **kwargs): + """ + Return step details for an action step + :returns: a json object describing a step + """ + resp.body = self.to_json( + self.get_action_step(kwargs['action_id'], kwargs['step_id'])) + resp.status = falcon.HTTP_200 + + def get_action_step(self, action_id, step_id): + """ + Interacts with airflow and the shipyard database to return the + requested step invoked through shipyard. + """ + action = self.get_action_db(action_id=action_id) + + if action is None: + raise ApiError( + title='Action not found', + description='Unknown action {}'.format(action_id), + status=falcon.HTTP_404) + + # resolve the ids for lookup of steps + dag_id = action['dag_id'] + dag_execution_date = action['dag_execution_date'] + + # get the action steps from shipyard db + steps = self.get_tasks_db(dag_id, dag_execution_date) + + for idx, step in enumerate(steps): + if step_id == step['task_id']: + # TODO (Bryan Strassner) more info about the step? + # like logs? Need requirements defined + step['index'] = idx + 1 + return step + + # if we didn't find it, 404 + raise ApiError( + title='Step not found', + description='Unknown step {}'.format(step_id), + status=falcon.HTTP_404) + + def get_action_db(self, action_id): + """ + Wrapper for call to the shipyard database to get an action + :returns: a dictionary of action details. + """ + return SHIPYARD_DB.get_action_by_id( + action_id=action_id) + + def get_tasks_db(self, dag_id, execution_date): + """ + Wrapper for call to the airflow db to get all tasks for a dag run + :returns: a list of task dictionaries + """ + return AIRFLOW_DB.get_tasks_by_id( + dag_id=dag_id, execution_date=execution_date) diff --git a/shipyard_airflow/control/actions_validations_id_api.py b/shipyard_airflow/control/actions_validations_id_api.py new file mode 100644 index 00000000..03798740 --- /dev/null +++ b/shipyard_airflow/control/actions_validations_id_api.py @@ -0,0 +1,77 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import falcon + +from shipyard_airflow import policy +from shipyard_airflow.control.base import BaseResource +from shipyard_airflow.db.db import SHIPYARD_DB +from shipyard_airflow.errors import ApiError + + +# /api/v1.0/actions/{action_id}/validations/{validation_id} +class ActionsValidationsResource(BaseResource): + """ + The actions validations resource is the validtions of an action + """ + + @policy.ApiEnforcer('workflow_orchestrator:get_action_validation') + def on_get(self, req, resp, **kwargs): + """ + Return validation details for an action validation + :returns: a json object describing a validation + """ + resp.body = self.to_json( + self.get_action_validation(kwargs['action_id'], + kwargs['validation_id'])) + resp.status = falcon.HTTP_200 + + def get_action_validation(self, action_id, validation_id): + """ + Interacts with the shipyard database to return the requested + validation information + :returns: the validation dicitonary object + """ + action = self.get_action_db(action_id=action_id) + + if action is None: + raise ApiError( + title='Action not found', + description='Unknown action {}'.format(action_id), + status=falcon.HTTP_404) + + validation = self.get_validation_db(validation_id=validation_id) + if validation is not None: + return validation + + # if we didn't find it, 404 + raise ApiError( + title='Validation not found', + description='Unknown validation {}'.format(validation_id), + status=falcon.HTTP_404) + + def get_action_db(self, action_id): + """ + Wrapper for call to the shipyard database to get an action + :returns: a dictionary of action details. + """ + return SHIPYARD_DB.get_action_by_id( + action_id=action_id) + + def get_validation_db(self, validation_id): + """ + Wrapper for call to the shipyard database to get an action + :returns: a dictionary of action details. + """ + return SHIPYARD_DB.get_validation_by_id( + validation_id=validation_id) diff --git a/shipyard_airflow/control/api.py b/shipyard_airflow/control/api.py index c115685a..25a37b5a 100644 --- a/shipyard_airflow/control/api.py +++ b/shipyard_airflow/control/api.py @@ -11,14 +11,26 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import json +import logging + import falcon -from shipyard_airflow.errors import AppError -from .regions import RegionsResource, RegionResource -from .base import ShipyardRequest, BaseResource -from .middleware import AuthMiddleware, ContextMiddleware, LoggingMiddleware -from .health import HealthResource +from shipyard_airflow.control.actions_api import ActionsResource +from shipyard_airflow.control.actions_control_api import ActionsControlResource +from shipyard_airflow.control.actions_id_api import ActionsIdResource +from shipyard_airflow.control.actions_steps_id_api import ActionsStepsResource +from shipyard_airflow.control.actions_validations_id_api import \ + ActionsValidationsResource +from shipyard_airflow.control.base import BaseResource, ShipyardRequest +from shipyard_airflow.control.health import HealthResource +from shipyard_airflow.control.middleware import (AuthMiddleware, + ContextMiddleware, + LoggingMiddleware) +from shipyard_airflow.errors import (AppError, default_error_serializer, + default_exception_handler) + +LOG = logging.getLogger(__name__) + def start_api(): middlewares = [ @@ -34,23 +46,43 @@ def start_api(): # v1.0 of Shipyard API v1_0_routes = [ # API for managing region data - ('/regions', RegionsResource()), - ('/regions/{region_id}', RegionResource()), ('/health', HealthResource()), + ('/actions', ActionsResource()), + ('/actions/{action_id}', ActionsIdResource()), + ('/actions/{action_id}/control/{control_verb}', + ActionsControlResource()), + ('/actions/{action_id}/steps/{step_id}', + ActionsStepsResource()), + ('/actions/{action_id}/validations/{validation_id}', + ActionsValidationsResource()), ] + # Set up the 1.0 routes + route_v1_0_prefix = '/api/v1.0' for path, res in v1_0_routes: - control_api.add_route('/api/v1.0' + path, res) + route = '{}{}'.format(route_v1_0_prefix, path) + LOG.info( + 'Adding route: %s Handled by %s', + route, + res.__class__.__name__ + ) + control_api.add_route(route, res) + # Error handlers (FILO handling) + control_api.add_error_handler(Exception, default_exception_handler) control_api.add_error_handler(AppError, AppError.handle) + + # built-in error serializer + control_api.set_error_serializer(default_error_serializer) + return control_api class VersionsResource(BaseResource): - - authorized_roles = ['anyone'] - + """ + Lists the versions supported by this API + """ def on_get(self, req, resp): - resp.body = json.dumps({ + resp.body = self.to_json({ 'v1.0': { 'path': '/api/v1.0', 'status': 'stable' diff --git a/shipyard_airflow/control/base.py b/shipyard_airflow/control/base.py index 3832af88..697f3511 100644 --- a/shipyard_airflow/control/base.py +++ b/shipyard_airflow/control/base.py @@ -11,95 +11,89 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import falcon -import uuid import json -import configparser -import os import logging +import uuid -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict +import falcon +import falcon.request as request +import falcon.routing as routing -from shipyard_airflow.errors import ( - AppError, - ERR_UNKNOWN, -) +from shipyard_airflow.control.json_schemas import validate_json +from shipyard_airflow.errors import InvalidFormatError class BaseResource(object): + def __init__(self): + self.logger = logging.getLogger('shipyard.control') - def on_options(self, req, resp): - self_attrs = dir(self) - methods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH'] - allowed_methods = [] - - for m in methods: - if 'on_' + m.lower() in self_attrs: - allowed_methods.append(m) - - resp.headers['Allow'] = ','.join(allowed_methods) + def on_options(self, req, resp, **kwargs): + """ + Handle options requests + """ + method_map = routing.create_http_method_map(self) + for method in method_map: + if method_map.get(method).__name__ != 'method_not_allowed': + resp.append_header('Allow', method) resp.status = falcon.HTTP_200 - def to_json(self, body_dict): - return json.dumps(body_dict) - - def on_success(self, res, message=None): - res.status = falcon.HTTP_200 - response_dict = OrderedDict() - response_dict['type'] = 'success' - response_dict['message'] = message - res.body = self.to_json(response_dict) - - # Error Handling - def return_error(self, resp, status_code, message="", retry=False): + def req_json(self, req, validate_json_schema=None): """ - Write a error message body and throw a Falcon exception to trigger - an HTTP status - - :param resp: Falcon response object to update - :param status_code: Falcon status_code constant - :param message: Optional error message to include in the body - :param retry: Optional flag whether client should retry the operation. - Can ignore if we rely solely on 4XX vs 5xx status codes + Reads and returns the input json message, optionally validates against + a provided jsonschema + :param req: the falcon request object + :param validate_json_schema: the optional jsonschema to use for + validation """ - resp.body = self.to_json( - {'type': 'error', 'message': message, 'retry': retry}) - resp.content_type = 'application/json' - resp.status = status_code - - # Get Config Data - def retrieve_config(self, section="", data=""): - - # Shipyard config will be located at /etc/shipyard/shipyard.conf - path = '/etc/shipyard/shipyard.conf' - - # Check that shipyard.conf exists - if os.path.isfile(path): - config = configparser.ConfigParser() - config.read(path) - - # Retrieve data from shipyard.conf - query_data = config.get(section, data) - - return query_data + has_input = False + if ((req.content_length is not None or req.content_length != 0) and + (req.content_type is not None and + req.content_type.lower() == 'application/json')): + raw_body = req.stream.read(req.content_length or 0) + if raw_body is not None: + has_input = True + self.info(req.context, 'Input message body: %s' % raw_body) + else: + self.info(req.context, 'No message body specified') + if has_input: + # read the json and validate if necessary + try: + raw_body = raw_body.decode('utf-8') + json_body = json.loads(raw_body) + if validate_json_schema: + # rasises an exception if it doesn't validate + validate_json(json_body, validate_json_schema) + return json_body + except json.JSONDecodeError as jex: + self.error(req.context, "Invalid JSON in request: \n%s" % + raw_body) + raise InvalidFormatError( + title='JSON could not be decoded', + description='%s: Invalid JSON in body: %s' % + (req.path, jex) + ) else: - raise AppError(ERR_UNKNOWN, "Missing Configuration File") + # No body passed as input. Fail validation if it was asekd for + if validate_json_schema is not None: + raise InvalidFormatError( + title='Json body is required', + description='%s: Bad input, no body provided' % + (req.path) + ) + else: + return None - def error(self, ctx, msg): - self.log_error(ctx, logging.ERROR, msg) + def to_json(self, body_dict): + """ + Thin wrapper around json.dumps, providing the default=str config + """ + return json.dumps(body_dict, default=str) - def info(self, ctx, msg): - self.log_error(ctx, logging.INFO, msg) - - def log_error(self, ctx, level, msg): - extra = { - 'user': 'N/A', - 'req_id': 'N/A', - 'external_ctx': 'N/A' - } + def log_message(self, ctx, level, msg): + """ + Logs a message with context, and extra populated. + """ + extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'} if ctx is not None: extra = { @@ -108,8 +102,36 @@ class BaseResource(object): 'external_ctx': ctx.external_marker, } -class ShipyardRequestContext(object): + self.logger.log(level, msg, extra=extra) + def debug(self, ctx, msg): + """ + Debug logger for resources, incorporating context. + """ + self.log_message(ctx, logging.DEBUG, msg) + + def info(self, ctx, msg): + """ + Info logger for resources, incorporating context. + """ + self.log_message(ctx, logging.INFO, msg) + + def warn(self, ctx, msg): + """ + Warn logger for resources, incorporating context. + """ + self.log_message(ctx, logging.WARN, msg) + + def error(self, ctx, msg): + """ + Error logger for resources, incorporating context. + """ + self.log_message(ctx, logging.ERROR, msg) + +class ShipyardRequestContext(object): + """ + Context object for shipyard resource requests + """ def __init__(self): self.log_level = 'error' self.user = None @@ -123,7 +145,6 @@ class ShipyardRequestContext(object): self.project_domain_id = None # Domain owning project self.is_admin_project = False self.authenticated = False - self.request_id = str(uuid.uuid4()) def set_log_level(self, level): if level in ['error', 'info', 'debug']: @@ -142,8 +163,7 @@ class ShipyardRequestContext(object): self.roles.extend(roles) def remove_role(self, role): - self.roles = [x for x in self.roles - if x != role] + self.roles = [x for x in self.roles if x != role] def set_external_marker(self, marker): self.external_marker = marker @@ -163,5 +183,6 @@ class ShipyardRequestContext(object): return policy_dict -class ShipyardRequest(falcon.request.Request): + +class ShipyardRequest(request.Request): context_type = ShipyardRequestContext diff --git a/shipyard_airflow/control/health.py b/shipyard_airflow/control/health.py index e9680cf3..260ccbaa 100644 --- a/shipyard_airflow/control/health.py +++ b/shipyard_airflow/control/health.py @@ -15,9 +15,14 @@ import falcon from shipyard_airflow.control.base import BaseResource -class HealthResource(BaseResource): - # Return empty response/body to show - # that shipyard is healthy +class HealthResource(BaseResource): + """ + Return empty response/body to show + that shipyard is healthy + """ def on_get(self, req, resp): + """ + It really does nothing right now. It may do more later + """ resp.status = falcon.HTTP_204 diff --git a/shipyard_airflow/control/json_schemas.py b/shipyard_airflow/control/json_schemas.py new file mode 100644 index 00000000..2f67e29e --- /dev/null +++ b/shipyard_airflow/control/json_schemas.py @@ -0,0 +1,126 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Contains the json schemas for the REST interface, and provides the functions +to validate against the schemas. +see: http://json-schema.org +see: https://pypi.python.org/pypi/jsonschema +""" +import json +import logging + +from jsonschema import validate +from jsonschema.exceptions import FormatError, SchemaError, ValidationError + +from shipyard_airflow.errors import AppError, InvalidFormatError + + +def validate_json(json_string, schema): + """ + invokes the validate function of jsonschema + """ + schema_dict = json.loads(schema) + schema_title = schema_dict['title'] + try: + validate(json_string, schema_dict) + except ValidationError as err: + title = 'JSON validation failed: {}'.format(err.message) + description = 'Failed validator: {} : {}'.format( + err.validator, + err.validator_value + ) + logging.error(title) + logging.error(description) + raise InvalidFormatError( + title=title, + description=description, + ) + except SchemaError as err: + title = 'SchemaError: Unable to validate JSON: {}'.format(err) + description = 'Invalid Schema: {}'.format(schema_title) + logging.error(title) + logging.error(description) + raise AppError( + title=title, + description=description + ) + except FormatError as err: + title = 'FormatError: Unable to validate JSON: {}'.format(err) + description = 'Invalid Format: {}'.format(schema_title) + logging.error(title) + logging.error(description) + raise AppError( + title=title, + description=description + ) + + +# The action resource structure +ACTION = ''' + { + "title": "Action schema", + "type" : "object", + "properties" : { + "id" : {"type" : "string"}, + "name" : {"type" : "string"}, + "parameters" : {"type" : "object"}, + "user" : {"type" : "string"}, + "time" : {"type" : "string"}, + "actionStatus" : { + "enum" : [ + "Pending", + "Validation Failed", + "Processing", + "Complete", + "Failed" + ] + }, + "dagStatus" : {"type" : "string"}, + "validations" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "id" : {"type" : "string"}, + "status" : { + "enum" : [ + "Passed", + "Failed", + "Pending" + ] + } + } + } + }, + "steps" : { + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "id" : {"type" : "string"}, + "status" : { + "enum" : [ + "Pending", + "Processing", + "Complete", + "Failed" + ] + } + } + } + } + }, + "required" : ["name"] + } +''' diff --git a/shipyard_airflow/control/middleware.py b/shipyard_airflow/control/middleware.py index 8c3066c4..70d5d07c 100644 --- a/shipyard_airflow/control/middleware.py +++ b/shipyard_airflow/control/middleware.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import logging -from uuid import UUID + from oslo_utils import uuidutils + from shipyard_airflow import policy @@ -73,28 +74,27 @@ class AuthMiddleware(object): ctx.is_admin_project = False self.logger.debug( - 'Request from authenticated user %s with roles %s' % - (ctx.user, ','.join(ctx.roles))) + 'Request from authenticated user %s with roles %s', + ctx.user, ','.join(ctx.roles) + ) else: ctx.authenticated = False class ContextMiddleware(object): - def __init__(self): - # Setup validation pattern for external marker - try: - uuid_value = uuidutils.generate_uuid(dashed=True) - UUID(uuid_value) - except: - self.logger.error('UUID generation fail') - + """ + Handle looking at the X-Context_Marker to see if it has value and that + value is a UUID (or close enough). If not, generate one. + """ def process_request(self, req, resp): ctx = req.context - ext_marker = req.get_header('X-Context-Marker') - - if ext_marker is not None and self.marker_re.fullmatch(ext_marker): + if ext_marker is not None and uuidutils.is_uuid_like(ext_marker): + # external passed in an ok context marker ctx.set_external_marker(ext_marker) + else: + # use the request id + ctx.set_external_marker(ctx.request_id) class LoggingMiddleware(object): @@ -111,4 +111,11 @@ class LoggingMiddleware(object): } resp.append_header('X-Shipyard-Req', ctx.request_id) - self.logger.info("%s - %s" % (req.uri, resp.status), extra=extra) + self.logger.info('%s %s - %s', + req.method, + req.uri, + resp.status, + extra=extra) + self.logger.debug('Response body:\n%s', + resp.body, + extra=extra) diff --git a/shipyard_airflow/control/shipyard.conf.example b/shipyard_airflow/control/shipyard.conf.example deleted file mode 100644 index 2b78f549..00000000 --- a/shipyard_airflow/control/shipyard.conf.example +++ /dev/null @@ -1,320 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -[base] -web_server=http://localhost:32080 -postgresql_db = postgresql+psycopg2://postgresql.ucp:5432/shipyard -postgresql_airflow_db = postgresql+psycopg2://postgresql.ucp:5432/airflow - -[shipyard] -host=shipyard-int.ucp -port=9000 - -[deckhand] -host=deckhand-api.ucp -port=80 - -[armada] -host=armada-api.ucp -port=8000 - -[drydock] -host=drydock-api.ucp -port=9000 -token=bigboss -site_yaml=/usr/local/airflow/plugins/drydock.yaml -prom_yaml=/usr/local/airflow/plugins/promenade.yaml - -[keystone] -OS_AUTH_URL=http://keystone-api.ucp:80/v3 -OS_PROJECT_NAME=service -OS_USER_DOMAIN_NAME=Default -OS_USERNAME=shipyard -OS_PASSWORD=password -OS_REGION_NAME=RegionOne -OS_IDENTITY_API_VERSION=3 - -[healthcheck] -schema=http -endpoint=/api/v1.0/health - -[keystone_authtoken] - -# -# From keystonemiddleware.auth_token -# - -# Complete "public" Identity API endpoint. This endpoint should not be an -# "admin" endpoint, as it should be accessible by all end users. Unauthenticated -# clients are redirected to this endpoint to authenticate. Although this -# endpoint should ideally be unversioned, client support in the wild varies. -# If you're using a versioned v2 endpoint here, then this should *not* be the -# same endpoint the service user utilizes for validating tokens, because normal -# end users may not be able to reach that endpoint. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.auth_uri -auth_uri = http://keystone-api.openstack:80/v3 - -# API version of the admin Identity API endpoint. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.auth_version -#auth_version = <None> - -# Do not handle authorization requests within the middleware, but delegate the -# authorization decision to downstream WSGI components. (boolean value) -# from .keystone_authtoken.keystonemiddleware.auth_token.delay_auth_decision -delay_auth_decision = true - -# Request timeout value for communicating with Identity API server. (integer -# value) -# from .keystone_authtoken.keystonemiddleware.auth_token.http_connect_timeout -#http_connect_timeout = <None> - -# How many times are we trying to reconnect when communicating with Identity API -# Server. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.http_request_max_retries -#http_request_max_retries = 3 - -# Request environment key where the Swift cache object is stored. When -# auth_token middleware is deployed with a Swift cache, use this option to have -# the middleware share a caching backend with swift. Otherwise, use the -# ``memcached_servers`` option instead. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.cache -#cache = <None> - -# Required if identity server requires client certificate (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.certfile -#certfile = <None> - -# Required if identity server requires client certificate (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.keyfile -#keyfile = <None> - -# A PEM encoded Certificate Authority to use when verifying HTTPs connections. -# Defaults to system CAs. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.cafile -#cafile = <None> - -# Verify HTTPS connections. (boolean value) -# from .keystone_authtoken.keystonemiddleware.auth_token.insecure -#insecure = false - -# The region in which the identity server can be found. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.region_name -#region_name = <None> - -# Directory used to cache files related to PKI tokens. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.signing_dir -#signing_dir = <None> - -# Optionally specify a list of memcached server(s) to use for caching. If left -# undefined, tokens will instead be cached in-process. (list value) -# Deprecated group/name - [keystone_authtoken]/memcache_servers -# from .keystone_authtoken.keystonemiddleware.auth_token.memcached_servers -#memcached_servers = <None> - -# In order to prevent excessive effort spent validating tokens, the middleware -# caches previously-seen tokens for a configurable duration (in seconds). Set to -# -1 to disable caching completely. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.token_cache_time -#token_cache_time = 300 - -# Determines the frequency at which the list of revoked tokens is retrieved from -# the Identity service (in seconds). A high number of revocation events combined -# with a low cache duration may significantly reduce performance. Only valid for -# PKI tokens. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.revocation_cache_time -#revocation_cache_time = 10 - -# (Optional) If defined, indicate whether token data should be authenticated or -# authenticated and encrypted. If MAC, token data is authenticated (with HMAC) -# in the cache. If ENCRYPT, token data is encrypted and authenticated in the -# cache. If the value is not one of these options or empty, auth_token will -# raise an exception on initialization. (string value) -# Allowed values: None, MAC, ENCRYPT -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_security_strategy -#memcache_security_strategy = None - -# (Optional, mandatory if memcache_security_strategy is defined) This string is -# used for key derivation. (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_secret_key -#memcache_secret_key = <None> - -# (Optional) Number of seconds memcached server is considered dead before it is -# tried again. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_pool_dead_retry -#memcache_pool_dead_retry = 300 - -# (Optional) Maximum total number of open connections to every memcached server. -# (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_pool_maxsize -#memcache_pool_maxsize = 10 - -# (Optional) Socket timeout in seconds for communicating with a memcached -# server. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_pool_socket_timeout -#memcache_pool_socket_timeout = 3 - -# (Optional) Number of seconds a connection to memcached is held unused in the -# pool before it is closed. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_pool_unused_timeout -#memcache_pool_unused_timeout = 60 - -# (Optional) Number of seconds that an operation will wait to get a memcached -# client connection from the pool. (integer value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_pool_conn_get_timeout -#memcache_pool_conn_get_timeout = 10 - -# (Optional) Use the advanced (eventlet safe) memcached client pool. The -# advanced pool will only work under python 2.x. (boolean value) -# from .keystone_authtoken.keystonemiddleware.auth_token.memcache_use_advanced_pool -#memcache_use_advanced_pool = false - -# (Optional) Indicate whether to set the X-Service-Catalog header. If False, -# middleware will not ask for service catalog on token validation and will not -# set the X-Service-Catalog header. (boolean value) -# from .keystone_authtoken.keystonemiddleware.auth_token.include_service_catalog -#include_service_catalog = true - -# Used to control the use and type of token binding. Can be set to: "disabled" -# to not check token binding. "permissive" (default) to validate binding -# information if the bind type is of a form known to the server and ignore it if -# not. "strict" like "permissive" but if the bind type is unknown the token will -# be rejected. "required" any form of token binding is needed to be allowed. -# Finally the name of a binding method that must be present in tokens. (string -# value) -# from .keystone_authtoken.keystonemiddleware.auth_token.enforce_token_bind -#enforce_token_bind = permissive - -# If true, the revocation list will be checked for cached tokens. This requires -# that PKI tokens are configured on the identity server. (boolean value) -# from .keystone_authtoken.keystonemiddleware.auth_token.check_revocations_for_cached -#check_revocations_for_cached = false - -# Hash algorithms to use for hashing PKI tokens. This may be a single algorithm -# or multiple. The algorithms are those supported by Python standard -# hashlib.new(). The hashes will be tried in the order given, so put the -# preferred one first for performance. The result of the first hash will be -# stored in the cache. This will typically be set to multiple values only while -# migrating from a less secure algorithm to a more secure one. Once all the old -# tokens are expired this option should be set to a single value for better -# performance. (list value) -# from .keystone_authtoken.keystonemiddleware.auth_token.hash_algorithms -#hash_algorithms = md5 - -# Authentication type to load (string value) -# Deprecated group/name - [keystone_authtoken]/auth_plugin -# from .keystone_authtoken.keystonemiddleware.auth_token.auth_type -auth_type = password - -# Config Section from which to load plugin specific options (string value) -# from .keystone_authtoken.keystonemiddleware.auth_token.auth_section -auth_section = keystone_authtoken - - - -# -# From shipyard_orchestrator -# - -# Authentication URL (string value) -# from .keystone_authtoken.shipyard_orchestrator.auth_url -auth_url = http://keystone-api.openstack:80/v3 - -# Domain ID to scope to (string value) -# from .keystone_authtoken.shipyard_orchestrator.domain_id -#domain_id = <None> - -# Domain name to scope to (string value) -# from .keystone_authtoken.shipyard_orchestrator.domain_name -#domain_name = <None> - -# Project ID to scope to (string value) -# Deprecated group/name - [keystone_authtoken]/tenant-id -# from .keystone_authtoken.shipyard_orchestrator.project_id -#project_id = <None> - -# Project name to scope to (string value) -# Deprecated group/name - [keystone_authtoken]/tenant-name -# from .keystone_authtoken.shipyard_orchestrator.project_name -project_name = service - -# Domain ID containing project (string value) -# from .keystone_authtoken.shipyard_orchestrator.project_domain_id -#project_domain_id = <None> - -# Domain name containing project (string value) -# from .keystone_authtoken.shipyard_orchestrator.project_domain_name -project_domain_name = default - -# Trust ID (string value) -# from .keystone_authtoken.shipyard_orchestrator.trust_id -#trust_id = <None> - -# Optional domain ID to use with v3 and v2 parameters. It will be used for both -# the user and project domain in v3 and ignored in v2 authentication. (string -# value) -# from .keystone_authtoken.shipyard_orchestrator.default_domain_id -#default_domain_id = <None> - -# Optional domain name to use with v3 API and v2 parameters. It will be used for -# both the user and project domain in v3 and ignored in v2 authentication. -# (string value) -# from .keystone_authtoken.shipyard_orchestrator.default_domain_name -#default_domain_name = <None> - -# User id (string value) -# from .keystone_authtoken.shipyard_orchestrator.user_id -#user_id = <None> - -# Username (string value) -# Deprecated group/name - [keystone_authtoken]/user-name -# from .keystone_authtoken.shipyard_orchestrator.username -username = shipyard - -# User's domain id (string value) -# from .keystone_authtoken.shipyard_orchestrator.user_domain_id -#user_domain_id = <None> - -# User's domain name (string value) -# from .keystone_authtoken.shipyard_orchestrator.user_domain_name -user_domain_name = default - -# User's password (string value) -# from .keystone_authtoken.shipyard_orchestrator.password -password = password - - -[oslo_policy] - -# -# From oslo.policy -# - -# The file that defines policies. (string value) -# Deprecated group/name - [DEFAULT]/policy_file -# from .oslo_policy.oslo.policy.policy_file -#policy_file = policy.json - -# Default rule. Enforced when a requested rule is not found. (string value) -# Deprecated group/name - [DEFAULT]/policy_default_rule -# from .oslo_policy.oslo.policy.policy_default_rule -#policy_default_rule = default - -# Directories where policy configuration files are stored. They can be relative -# to any directory in the search path defined by the config_dir option, or -# absolute paths. The file defined by policy_file must exist for these -# directories to be searched. Missing or empty directories are ignored. (multi -# valued) -# Deprecated group/name - [DEFAULT]/policy_dirs -# from .oslo_policy.oslo.policy.policy_dirs (multiopt) -#policy_dirs = policy.d diff --git a/shipyard_airflow/db/__init__.py b/shipyard_airflow/db/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/shipyard_airflow/db/airflow_db.py b/shipyard_airflow/db/airflow_db.py new file mode 100644 index 00000000..72c1d207 --- /dev/null +++ b/shipyard_airflow/db/airflow_db.py @@ -0,0 +1,234 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Airflow database access - see db.py for instances to use +""" +import sqlalchemy +from oslo_config import cfg + +from shipyard_airflow.db.common_db import DbAccess +from shipyard_airflow.db.errors import AirflowStateError + + +CONF = cfg.CONF + + +class AirflowDbAccess(DbAccess): + """ + Airflow database access + WARNING: This is a large set of assumptions based on the way airflow + arranges its database and are subject to change with airflow future + releases - i.e. we're leveraging undocumented/non-exposed interfaces + for airflow to work around lack of API and feature functionality. + """ + + SELECT_ALL_DAG_RUNS = sqlalchemy.sql.text(''' + SELECT + "dag_id", + "execution_date", + "state", + "run_id", + "external_trigger", + "start_date", + "end_date" + FROM + dag_run + ''') + + SELECT_DAG_RUNS_BY_ID = sqlalchemy.sql.text(''' + SELECT + "dag_id", + "execution_date", + "state", + "run_id", + "external_trigger", + "start_date", + "end_date" + FROM + dag_run + WHERE + dag_id = :dag_id + AND + execution_date = :execution_date + ''') + + SELECT_ALL_TASKS = sqlalchemy.sql.text(''' + SELECT + "task_id", + "dag_id", + "execution_date", + "start_date", + "end_date", + "duration", + "state", + "try_number", + "operator", + "queued_dttm" + FROM + task_instance + ORDER BY + priority_weight desc, + start_date + ''') + + SELECT_TASKS_BY_ID = sqlalchemy.sql.text(''' + SELECT + "task_id", + "dag_id", + "execution_date", + "start_date", + "end_date", + "duration", + "state", + "try_number", + "operator", + "queued_dttm" + FROM + task_instance + WHERE + dag_id LIKE :dag_id + AND + execution_date = :execution_date + ORDER BY + priority_weight desc, + start_date + ''') + + UPDATE_DAG_RUN_STATUS = sqlalchemy.sql.text(''' + UPDATE + dag_run + SET + state = :state + WHERE + dag_id = :dag_id + AND + execution_date = :execution_date + ''') + + def __init__(self): + DbAccess.__init__(self) + + def get_connection_string(self): + """ + Returns the connection string for this db connection + """ + return CONF.base.postgresql_airflow_db + + def get_all_dag_runs(self): + """ + Retrieves all dag runs. + """ + return self.get_as_dict_array(AirflowDbAccess.SELECT_ALL_DAG_RUNS) + + def get_dag_runs_by_id(self, dag_id, execution_date): + """ + Retrieves dag runs by dag id and execution date + """ + return self.get_as_dict_array( + AirflowDbAccess.SELECT_DAG_RUNS_BY_ID, + dag_id=dag_id, + execution_date=execution_date) + + def get_all_tasks(self): + """ + Retrieves all tasks. + """ + return self.get_as_dict_array(AirflowDbAccess.SELECT_ALL_TASKS) + + def get_tasks_by_id(self, dag_id, execution_date): + """ + Retrieves tasks by dag id and execution date + """ + return self.get_as_dict_array( + AirflowDbAccess.SELECT_TASKS_BY_ID, + dag_id=dag_id + '%', + execution_date=execution_date) + + def stop_dag_run(self, dag_id, execution_date): + """ + Triggers an update to set a dag_run to failed state + causing dag_run to be stopped + running -> failed + """ + self._control_dag_run( + dag_id=dag_id, + execution_date=execution_date, + expected_state='running', + desired_state='failed') + + def pause_dag_run(self, dag_id, execution_date): + """ + Triggers an update to set a dag_run to paused state + causing dag_run to be paused + running -> paused + """ + self._control_dag_run( + dag_id=dag_id, + execution_date=execution_date, + expected_state='running', + desired_state='paused') + + def unpause_dag_run(self, dag_id, execution_date): + """ + Triggers an update to set a dag_run to running state + causing dag_run to be unpaused + paused -> running + """ + self._control_dag_run( + dag_id=dag_id, + execution_date=execution_date, + expected_state='paused', + desired_state='running') + + def check_dag_run_state(self, dag_id, execution_date, expected_state): + """ + Examines a dag_run for state. Throws execption if it's not right + """ + dag_run_list = self.get_dag_runs_by_id( + dag_id=dag_id, execution_date=execution_date) + if dag_run_list: + dag_run = dag_run_list[0] + if dag_run['state'] != expected_state: + raise AirflowStateError( + message='dag_run state must be running, but is {}'.format( + dag_run['state'])) + else: + # not found + raise AirflowStateError(message='dag_run does not exist') + + def _control_dag_run(self, dag_id, execution_date, expected_state, + desired_state): + """ + checks a dag_run's state for expected state, and sets it to the + desired state + """ + self.check_dag_run_state( + dag_id=dag_id, + execution_date=execution_date, + expected_state=expected_state) + self._set_dag_run_state( + state=desired_state, dag_id=dag_id, execution_date=execution_date) + + def _set_dag_run_state(self, state, dag_id, execution_date): + """ + Sets a dag run to the specified state. + WARNING: this assumes that airflow works by reading state from the + dag_run table dynamically, is not caching results, and doesn't + start to use the states we're using in a new way. + """ + self.perform_insert( + AirflowDbAccess.UPDATE_DAG_RUN_STATUS, + state=state, + dag_id=dag_id, + execution_date=execution_date) diff --git a/shipyard_airflow/db/common_db.py b/shipyard_airflow/db/common_db.py new file mode 100644 index 00000000..8238120f --- /dev/null +++ b/shipyard_airflow/db/common_db.py @@ -0,0 +1,121 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import logging + +import sqlalchemy + +from shipyard_airflow.errors import DatabaseError + +LOG = logging.getLogger(__name__) + +class DbAccess: + """ + Base class for simple database access + """ + + def __init__(self): + self.engine = None + + def get_connection_string(self): + """ + Override to return the connection string. This allows for + lazy initialization + """ + raise NotImplementedError() + + def update_db(self): + """ + Unimplemented method for use in overriding to peform db updates + """ + LOG.info('No databse version updates specified for %s', + self.__class__.__name__) + + def get_engine(self): + """ + Returns the engine for airflow + """ + try: + connection_string = self.get_connection_string() + if connection_string is not None and self.engine is None: + self.engine = sqlalchemy.create_engine(connection_string) + if self.engine is None: + self._raise_invalid_db_config( + connection_string=connection_string + ) + LOG.info('Connected with <%s>, returning engine', + connection_string) + return self.engine + except sqlalchemy.exc.ArgumentError as exc: + self._raise_invalid_db_config( + exception=exc, + connection_string=connection_string + ) + + def _raise_invalid_db_config(self, + connection_string, + exception=None): + """ + Common handler for an invalid DB connection + """ + LOG.error('Connection string <%s> prevents database operation', + connection_string) + if exception is not None: + LOG.error("Associated exception: %s", exception) + raise DatabaseError( + title='No database connection', + description='Invalid database configuration' + ) + + def get_as_dict_array(self, query, **kwargs): + """ + executes the supplied query and returns the array of dictionaries of + the row results + """ + LOG.info('Query: %s', query) + result_dict_list = [] + if query is not None: + with self.get_engine().connect() as connection: + result_set = connection.execute(query, **kwargs) + result_dict_list = [dict(row) for row in result_set] + LOG.info('Result has %s rows', len(result_dict_list)) + for dict_row in result_dict_list: + LOG.info('Result: %s', dict_row) + return result_dict_list + + def perform_insert(self, query, **kwargs): + """ + Performs a parameterized insert + """ + self.perform_change_dml(query, **kwargs) + + def perform_update(self, query, **kwargs): + """ + Performs a parameterized update + """ + self.perform_change_dml(query, **kwargs) + + def perform_delete(self, query, **kwargs): + """ + Performs a parameterized delete + """ + self.perform_change_dml(query, **kwargs) + + def perform_change_dml(self, query, **kwargs): + """ + Performs an update/insert/delete + """ + LOG.debug('Query: %s', query) + if query is not None: + with self.get_engine().connect() as connection: + connection.execute(query, **kwargs) diff --git a/examples/manifests/services.yaml b/shipyard_airflow/db/db.py similarity index 59% rename from examples/manifests/services.yaml rename to shipyard_airflow/db/db.py index 358010c6..d4606bf7 100644 --- a/examples/manifests/services.yaml +++ b/shipyard_airflow/db/db.py @@ -11,17 +11,10 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -############################################################################# -# -# services.yaml - Definitions of server hardware layout -# -############################################################################# -# version the schema in this file so consumers can rationally parse it ---- -# -# Is this where I include a list of files per service ? -# -# -# Assuming something like this works for the insertion +""" +The Application scope instances of db access classes +""" +from shipyard_airflow.db import airflow_db, shipyard_db -imports: \ No newline at end of file +SHIPYARD_DB = shipyard_db.ShipyardDbAccess() +AIRFLOW_DB = airflow_db.AirflowDbAccess() diff --git a/shipyard_airflow/control/regions.py b/shipyard_airflow/db/errors.py similarity index 58% rename from shipyard_airflow/control/regions.py rename to shipyard_airflow/db/errors.py index 2209805c..ded955e6 100644 --- a/shipyard_airflow/control/regions.py +++ b/shipyard_airflow/db/errors.py @@ -11,19 +11,12 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -import falcon -from .base import BaseResource -from shipyard_airflow import policy - -class RegionsResource(BaseResource): - - @policy.ApiEnforcer('workflow_orchestrator:get_regions') - def on_get(self, req, resp): - resp.status = falcon.HTTP_200 - -class RegionResource(BaseResource): - - @policy.ApiEnforcer('workflow_orchestrator:get_regions') - def on_get(self, req, resp, region_id): - resp.status = falcon.HTTP_200 +class AirflowStateError(Exception): + def __init__(self, message=""): + """ + An error to convey that an attempt to modify airflow data cannot + be accomplished due to existing state. + :param message: Optional message for consumer + """ + self.message = message diff --git a/shipyard_airflow/db/shipyard_db.py b/shipyard_airflow/db/shipyard_db.py new file mode 100644 index 00000000..35524d43 --- /dev/null +++ b/shipyard_airflow/db/shipyard_db.py @@ -0,0 +1,254 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Shipyard database access - see db.py for instances to use +""" +import json +import logging +import os + +import sqlalchemy +from alembic import command as alembic_command +from alembic.config import Config +from oslo_config import cfg + +from shipyard_airflow.db.common_db import DbAccess + +LOG = logging.getLogger(__name__) +CONF = cfg.CONF + +class ShipyardDbAccess(DbAccess): + """ + Shipyard database access + """ + + SELECT_ALL_ACTIONS = sqlalchemy.sql.text(''' + SELECT + "id", + "name", + "parameters", + "dag_id", + "dag_execution_date", + "user", + "datetime", + "context_marker" + FROM + actions + ''') + + SELECT_ACTION_BY_ID = sqlalchemy.sql.text(''' + SELECT + "id", + "name", + "parameters", + "dag_id", + "dag_execution_date", + "user", + "datetime", + "context_marker" + FROM + actions + WHERE + id = :action_id + ''') + + INSERT_ACTION = sqlalchemy.sql.text(''' + INSERT INTO + actions ( + "id", + "name", + "parameters", + "dag_id", + "dag_execution_date", + "user", + "datetime", + "context_marker" + ) + VALUES ( + :id, + :name, + :parameters, + :dag_id, + :dag_execution_date, + :user, + :timestamp, + :context_marker ) + ''') + + SELECT_VALIDATIONS = sqlalchemy.sql.text(''' + SELECT + "id", + "action_id", + "validation_name" + FROM + preflight_validation_failures + ''') + + SELECT_VALIDATION_BY_ID = sqlalchemy.sql.text(''' + SELECT + "id", + "action_id", + "validation_name", + "details" + FROM + preflight_validation_failures + WHERE + id = :validation_id + ''') + + SELECT_VALIDATION_BY_ACTION_ID = sqlalchemy.sql.text(''' + SELECT + "id", + "action_id", + "validation_name", + "details" + FROM + preflight_validation_failures + WHERE + action_id = :action_id + ''') + + SELECT_CMD_AUDIT_BY_ACTION_ID = sqlalchemy.sql.text(''' + SELECT + "id", + "action_id", + "command", + "user", + "datetime" + FROM + action_command_audit + WHERE + action_id = :action_id + ''') + + INSERT_ACTION_COMMAND_AUDIT = sqlalchemy.sql.text(''' + INSERT INTO + action_command_audit ( + "id", + "action_id", + "command", + "user" + ) + VALUES ( + :id, + :action_id, + :command, + :user ) + ''') + + def __init__(self): + DbAccess.__init__(self) + + def get_connection_string(self): + """ + Returns the connection string for this db connection + """ + return CONF.base.postgresql_db + + def update_db(self): + """ + Trigger Alembic to upgrade to the latest version of the DB + """ + try: + LOG.info("Checking for shipyard database upgrade") + cwd = os.getcwd() + os.chdir(CONF.base.alembic_ini_path) + config = Config('alembic.ini', + attributes={'configure_logger': False}) + alembic_command.upgrade(config, 'head') + os.chdir(cwd) + except Exception as exception: + LOG.error('***\n' + 'Failed Alembic DB upgrade. Check the config: %s\n' + '***', + exception) + # don't let things continue... + raise exception + + def get_all_submitted_actions(self): + """ + Retrieves all actions. + """ + return self.get_as_dict_array(ShipyardDbAccess.SELECT_ALL_ACTIONS) + + def get_action_by_id(self, action_id): + """ + Get a single action + :param action_id: the id of the action to retrieve + """ + actions_array = self.get_as_dict_array( + ShipyardDbAccess.SELECT_ACTION_BY_ID, action_id=action_id) + if actions_array: + return actions_array[0] + else: + # Not found + return None + + def get_preflight_validation_fails(self): + """ + Retrieves the summary set of preflight validation failures + """ + return self.get_as_dict_array(ShipyardDbAccess.SELECT_VALIDATIONS) + + def get_validation_by_id(self, validation_id): + """ + Retreives a single validation for a given validation id + """ + validation_array = self.get_as_dict_array( + ShipyardDbAccess.SELECT_VALIDATION_BY_ID, + validation_id=validation_id) + if validation_array: + return validation_array[0] + else: + return None + + def get_validation_by_action_id(self, action_id): + """ + Retreives the validations for a given action id + """ + return self.get_as_dict_array( + ShipyardDbAccess.SELECT_VALIDATION_BY_ACTION_ID, + action_id=action_id) + + def insert_action(self, action): + """ + Inserts a single action row + """ + self.perform_insert(ShipyardDbAccess.INSERT_ACTION, + id=action['id'], + name=action['name'], + parameters=json.dumps(action['parameters']), + dag_id=action['dag_id'], + dag_execution_date=action['dag_execution_date'], + user=action['user'], + timestamp=action['timestamp'], + context_marker=action['context_marker']) + + def get_command_audit_by_action_id(self, action_id): + """ + Retreives the action audit records for a given action id + """ + return self.get_as_dict_array( + ShipyardDbAccess.SELECT_CMD_AUDIT_BY_ACTION_ID, + action_id=action_id) + + def insert_action_command_audit(self, ac_audit): + """ + Inserts a single action command audit + """ + self.perform_insert(ShipyardDbAccess.INSERT_ACTION_COMMAND_AUDIT, + id=ac_audit['id'], + action_id=ac_audit['action_id'], + command=ac_audit['command'], + user=ac_audit['user']) diff --git a/shipyard_airflow/errors.py b/shipyard_airflow/errors.py index deb7bacd..d44c62e2 100644 --- a/shipyard_airflow/errors.py +++ b/shipyard_airflow/errors.py @@ -1,49 +1,224 @@ -# -*- coding: utf-8 -*- - +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. import json +import logging +import traceback + import falcon -try: - from collections import OrderedDict -except ImportError: - OrderedDict = dict -ERR_UNKNOWN = {'status': falcon.HTTP_500, 'title': 'Internal Server Error'} +def get_version_from_request(req): + """ + Attempt to extract the api version string + """ + for part in req.path.split('/'): + if '.' in part and part.startswith('v'): + return part + return 'N/A' -ERR_AIRFLOW_RESPONSE = { - 'status': falcon.HTTP_400, - 'title': 'Error response from Airflow' -} + +# Standard error handler +def format_resp(req, + resp, + status_code, + message="", + reason="", + error_type="Unspecified Exception", + retry=False, + error_list=None): + """ + Write a error message body and throw a Falcon exception to trigger + an HTTP status + :param req: Falcon request object + :param resp: Falcon response object to update + :param status_code: Falcon status_code constant + :param message: Optional error message to include in the body + :param reason: Optional reason code to include in the body + :param retry: Optional flag whether client should retry the operation. + :param error_list: option list of errors + Can ignore if we rely solely on 4XX vs 5xx status codes + """ + if error_list is None: + error_list = [{'message': 'An error ocurred, but was not specified'}] + error_response = { + 'kind': 'status', + 'apiVersion': get_version_from_request(req), + 'metadata': {}, + 'status': 'Failure', + 'message': message, + 'reason': reason, + 'details': { + 'errorType': error_type, + 'errorCount': len(error_list), + 'errorList': error_list + }, + 'code': status_code + } + + resp.body = json.dumps(error_response, default=str) + resp.content_type = 'application/json' + resp.status = status_code + +def default_error_serializer(req, resp, exception): + """ + Writes the default error message body, when we don't handle it otherwise + """ + format_resp( + req, + resp, + status_code=exception.status, + message=exception.description, + reason=exception.title, + error_type=exception.__class__.__name__, + error_list=[{'message': exception.description}] + ) + +def default_exception_handler(ex, req, resp, params): + """ + Catch-all execption handler for standardized output. + If this is a standard falcon HTTPError, rethrow it for handling + """ + if isinstance(ex, falcon.HTTPError): + # allow the falcon http errors to bubble up and get handled + raise ex + else: + # take care of the uncaught stuff + exc_string = traceback.format_exc() + logging.error('Unhanded Exception being handled: \n%s', exc_string) + format_resp( + req, + resp, + falcon.HTTP_500, + error_type=ex.__class__.__name__, + message="Unhandled Exception raised: %s" % str(ex), + retry=True + ) class AppError(Exception): - def __init__(self, error=ERR_UNKNOWN, description=None): - self.error = error - self.error['description'] = description - - @property - def title(self): - return self.error['title'] - - @property - def status(self): - return self.error['status'] - - @property - def description(self): - return self.error['description'] + """ + Base error containing enough information to make a shipyard formatted error + """ + def __init__(self, + title='Internal Server Error', + description=None, + error_list=None, + status=falcon.HTTP_500, + retry=False): + """ + :param description: The internal error description + :param error_list: The list of errors + :param status: The desired falcon HTTP resposne code + :param title: The title of the error message + :param retry: Optional retry directive for the consumer + """ + self.title = title + self.description = description + self.error_list = massage_error_list(error_list, description) + self.status = status + self.retry = retry @staticmethod - def handle(exception, req, res, error=None): - res.status = exception.status - meta = OrderedDict() - meta['message'] = exception.title - if exception.description: - meta['description'] = exception.description - res.body = json.dumps(meta) + def handle(ex, req, resp, params): + format_resp( + req, + resp, + ex.status, + message=ex.title, + reason=ex.description, + error_list=ex.error_list, + error_type=ex.__class__.__name__, + retry=ex.retry) class AirflowError(AppError): - def __init__(self, description=None): - super().__init__(ERR_AIRFLOW_RESPONSE) - self.error['description'] = description + """ + An error to handle errors returned by the Airflow API + """ + def __init__(self, description=None, error_list=None): + super().__init__( + title='Error response from Airflow', + description=description, + error_list=error_list, + status=falcon.HTTP_400, + retry=False + ) + +class DatabaseError(AppError): + """ + An error to handle general api errors. + """ + def __init__(self, + description=None, + error_list=None, + status=falcon.HTTP_500, + title='Database Access Error', + retry=False): + super().__init__( + status=status, + title=title, + description=description, + error_list=error_list, + retry=retry + ) + + +class ApiError(AppError): + """ + An error to handle general api errors. + """ + def __init__(self, + description="", + error_list=None, + status=falcon.HTTP_400, + title="", + retry=False): + super().__init__( + status=status, + title=title, + description=description, + error_list=error_list, + retry=retry + ) + + +class InvalidFormatError(AppError): + """ + An exception to cover invalid input formatting + """ + def __init__(self, title, description="Not Specified", error_list=None): + + super().__init__( + title=title, + description='Validation has failed', + error_list=error_list, + status=falcon.HTTP_400, + retry=False + ) + + +def massage_error_list(error_list, placeholder_description): + """ + Returns a best-effort attempt to make a nice error list + """ + output_error_list = [] + if error_list: + for error in error_list: + if not error['message']: + output_error_list.append({'message': error}) + else: + output_error_list.append(error) + if not output_error_list: + output_error_list.append({'message': placeholder_description}) + return output_error_list diff --git a/shipyard_airflow/policy.py b/shipyard_airflow/policy.py index ab0d3f38..307d8d16 100644 --- a/shipyard_airflow/policy.py +++ b/shipyard_airflow/policy.py @@ -12,13 +12,17 @@ # See the License for the specific language governing permissions and # limitations under the License. # -import logging import functools -import falcon +import logging +import falcon from oslo_config import cfg from oslo_policy import policy +from shipyard_airflow.errors import ApiError, AppError + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) policy_engine = None @@ -26,6 +30,9 @@ class ShipyardPolicy(object): """ Initialize policy defaults """ + + RULE_ADMIN_REQUIRED = 'rule:admin_required' + # Base Policy base_rules = [ policy.RuleDefault( @@ -36,18 +43,61 @@ class ShipyardPolicy(object): # Orchestrator Policy task_rules = [ - policy.DocumentedRuleDefault('workflow_orchestrator:get_regions', - 'role:admin', 'Get region information', [{ - 'path': - '/api/v1.0/regions', - 'method': - 'GET' - }, { - 'path': - '/api/v1.0/regions/{region_id}', - 'method': - 'GET' - }]) + policy.DocumentedRuleDefault( + 'workflow_orchestrator:list_actions', + RULE_ADMIN_REQUIRED, + 'List workflow actions invoked by users', + [{ + 'path': '/api/v1.0/actions', + 'method': 'GET' + }] + ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:create_action', + RULE_ADMIN_REQUIRED, + 'Create a workflow action', + [{ + 'path': '/api/v1.0/actions', + 'method': 'POST' + }] + ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:get_action', + RULE_ADMIN_REQUIRED, + 'Retreive an action by its id', + [{ + 'path': '/api/v1.0/actions/{action_id}', + 'method': 'GET' + }] + ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:get_action_step', + RULE_ADMIN_REQUIRED, + 'Retreive an action step by its id', + [{ + 'path': '/api/v1.0/actions/{action_id}/steps/{step_id}', + 'method': 'GET' + }] + ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:get_action_validation', + RULE_ADMIN_REQUIRED, + 'Retreive an action validation by its id', + [{ + 'path': + '/api/v1.0/actions/{action_id}/validations/{validation_id}', + 'method': 'GET' + }] + ), + policy.DocumentedRuleDefault( + 'workflow_orchestrator:invoke_action_control', + RULE_ADMIN_REQUIRED, + 'Send a control to an action', + [{ + 'path': '/api/v1.0/actions/{action_id}/control/{control_verb}', + 'method': 'POST' + }] + ), ] # Regions Policy @@ -61,7 +111,6 @@ class ShipyardPolicy(object): def authorize(self, action, ctx): target = {'project_id': ctx.project_id, 'user_id': ctx.user_id} - self.enforcer.authorize(action, target, ctx.to_policy_view()) return self.enforcer.authorize(action, target, ctx.to_policy_view()) @@ -72,44 +121,68 @@ class ApiEnforcer(object): def __init__(self, action): self.action = action - self.logger = logging.getLogger('shipyard.policy') + self.logger = LOG def __call__(self, f): @functools.wraps(f) def secure_handler(slf, req, resp, *args, **kwargs): ctx = req.context - policy_engine = ctx.policy_engine - self.logger.debug("Enforcing policy %s on request %s" % - (self.action, ctx.request_id)) + policy_eng = ctx.policy_engine + slf.info(ctx, "Policy Engine: %s" % policy_eng.__class__.__name__) + # perform auth + slf.info(ctx, "Enforcing policy %s on request %s" % + (self.action, ctx.request_id)) + # policy engine must be configured + if policy_eng is None: + slf.error( + ctx, + "Error-Policy engine required-action: %s" % self.action) + raise AppError( + title="Auth is not being handled by any policy engine", + status=falcon.HTTP_500, + retry=False + ) + authorized = False try: - if policy_engine is not None and policy_engine.authorize( - self.action, ctx): - return f(slf, req, resp, *args, **kwargs) - else: - if ctx.authenticated: - slf.info(ctx, "Error - Forbidden access - action: %s" % - self.action) - slf.return_error( - resp, - falcon.HTTP_403, - message="Forbidden", - retry=False) - else: - slf.info(ctx, "Error - Unauthenticated access") - slf.return_error( - resp, - falcon.HTTP_401, - message="Unauthenticated", - retry=False) + if policy_eng.authorize(self.action, ctx): + # authorized + slf.info(ctx, "Request is authorized") + authorized = True except: - slf.info( + # couldn't service the auth request + slf.error( ctx, "Error - Expectation Failed - action: %s" % self.action) - slf.return_error( - resp, - falcon.HTTP_417, - message="Expectation Failed", - retry=False) + raise ApiError( + title="Expectation Failed", + status=falcon.HTTP_417, + retry=False + ) + if authorized: + return f(slf, req, resp, *args, **kwargs) + else: + slf.error(ctx, + "Auth check failed. Authenticated:%s" % + ctx.authenticated) + # raise the appropriate response exeception + if ctx.authenticated: + slf.error(ctx, + "Error: Forbidden access - action: %s" % + self.action) + raise ApiError( + title="Forbidden", + status=falcon.HTTP_403, + description="Credentials do not permit access", + retry=False + ) + else: + slf.error(ctx, "Error - Unauthenticated access") + raise ApiError( + title="Unauthenticated", + status=falcon.HTTP_401, + description="Credentials are not established", + retry=False + ) return secure_handler diff --git a/shipyard_airflow/setup.py b/shipyard_airflow/setup.py deleted file mode 100644 index 630bbde7..00000000 --- a/shipyard_airflow/setup.py +++ /dev/null @@ -1,32 +0,0 @@ -# Copyright 2017 AT&T Intellectual Property. All other rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from setuptools import setup - -setup(name='shipyard_airflow', - version='0.1a1', - description='API for managing Airflow-based orchestration', - url='http://github.com/att-comdev/shipyard', - author='Anthony Lin - AT&T', - author_email='al498u@att.com', - license='Apache 2.0', - packages=['shipyard_airflow', - 'shipyard_airflow.control'], - install_requires=[ - 'falcon', - 'requests', - 'configparser', - 'uwsgi>1.4', - 'python-dateutil' - ]) diff --git a/shipyard_airflow/shipyard.py b/shipyard_airflow/shipyard.py index 95f5df7c..70c4d1c1 100755 --- a/shipyard_airflow/shipyard.py +++ b/shipyard_airflow/shipyard.py @@ -15,28 +15,30 @@ import logging from oslo_config import cfg -from shipyard_airflow import policy import shipyard_airflow.control.api as api -# We need to import config so the initializing code can run for oslo config -import shipyard_airflow.config as config # noqa: F401 +from shipyard_airflow import policy +from shipyard_airflow.conf import config +from shipyard_airflow.db import db + +CONF = cfg.CONF def start_shipyard(): - - # Setup configuration parsing - cli_options = [ - cfg.BoolOpt( - 'debug', short='d', default=False, help='Enable debug logging'), - ] + # Trigger configuration resolution. + config.parse_args() # Setup root logger - logger = logging.getLogger('shipyard') + base_console_handler = logging.StreamHandler() - logger.setLevel('DEBUG') - ch = logging.StreamHandler() - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - ch.setFormatter(formatter) - logger.addHandler(ch) + logging.basicConfig(level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[base_console_handler]) + logging.getLogger().info("Setting logging level to: %s", + logging.getLevelName(CONF.logging.log_level)) + + logging.basicConfig(level=CONF.logging.log_level, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[base_console_handler]) # Specalized format for API logging logger = logging.getLogger('shipyard.control') @@ -45,14 +47,21 @@ def start_shipyard(): ('%(asctime)s - %(levelname)s - %(user)s - %(req_id)s - ' '%(external_ctx)s - %(message)s')) - ch = logging.StreamHandler() - ch.setFormatter(formatter) - logger.addHandler(ch) + console_handler = logging.StreamHandler() + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) # Setup the RBAC policy enforcer policy.policy_engine = policy.ShipyardPolicy() policy.policy_engine.register_policy() + # Upgrade database + if CONF.base.upgrade_db: + # this is a reasonable place to put any online alembic upgrades + # desired. Currently only shipyard db is under shipyard control. + db.SHIPYARD_DB.update_db() + + # Start the API return api.start_api() diff --git a/tests/unit/control/__init__.py b/tests/unit/control/__init__.py index e69de29b..1ff35516 100644 --- a/tests/unit/control/__init__.py +++ b/tests/unit/control/__init__.py @@ -0,0 +1,23 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest + +from shipyard_airflow.conf import config + +@pytest.fixture +def setup_config(): + """ + Initialize shipyard config - this is needed so that CONF resolves. + """ + config.parse_args() diff --git a/tests/unit/control/test_actions_api.py b/tests/unit/control/test_actions_api.py new file mode 100644 index 00000000..36144888 --- /dev/null +++ b/tests/unit/control/test_actions_api.py @@ -0,0 +1,239 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +import os +from datetime import datetime + +from shipyard_airflow.control.actions_api import ActionsResource +from shipyard_airflow.control.base import ShipyardRequestContext +from shipyard_airflow.errors import ApiError + +DATE_ONE = datetime(2017, 9, 13, 11, 13, 3, 57000) +DATE_TWO = datetime(2017, 9, 13, 11, 13, 5, 57000) +DATE_ONE_STR = DATE_ONE.strftime('%Y-%m-%dT%H:%M:%S') +DATE_TWO_STR = DATE_TWO.strftime('%Y-%m-%dT%H:%M:%S') + + +def actions_db(): + """ + replaces the actual db call + """ + return [ + { + 'id': 'aaaaaa', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'did1', + 'dag_execution_date': DATE_ONE_STR, + 'user': 'robot1', + 'timestamp': DATE_ONE, + 'context_marker': '8-4-4-4-12a' + }, + { + 'id': 'bbbbbb', + 'name': 'dag2', + 'parameters': { + 'p1': 'p1val' + }, + 'dag_id': 'did2', + 'dag_execution_date': DATE_ONE_STR, + 'user': 'robot2', + 'timestamp': DATE_ONE, + 'context_marker': '8-4-4-4-12b' + }, + ] + + +def dag_runs_db(): + """ + replaces the actual db call + """ + return [ + { + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_TWO + }, + { + 'dag_id': 'did1', + 'execution_date': DATE_ONE, + 'state': 'FAILED', + 'run_id': '99', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_ONE + }, + ] + + +def tasks_db(): + """ + replaces the actual db call + """ + return [ + { + 'task_id': '1a', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_TWO, + 'duration': '20mins', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + }, + { + 'task_id': '1b', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_TWO, + 'duration': '1minute', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + }, + { + 'task_id': '1c', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_TWO, + 'duration': '1day', + 'try_number': '3', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + }, + { + 'task_id': '2a', + 'dag_id': 'did1', + 'execution_date': DATE_ONE, + 'state': 'FAILED', + 'start_date': DATE_ONE, + 'end_date': DATE_ONE, + 'duration': '1second', + 'try_number': '2', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + }, + ] + +def airflow_stub(**kwargs): + """ + asserts that the airflow invocation method was called with the right + parameters + """ + assert kwargs['dag_id'] + assert kwargs['action'] + print(kwargs) + return '2017-09-06 14:10:08.528402' + +def insert_action_stub(**kwargs): + """ + asserts that the insert action was called with the right parameters + """ + assert kwargs['action'] + +def audit_control_command_db(action_audit): + """ + Stub for inserting the invoke record + """ + assert action_audit['command'] == 'invoke' + + +context = ShipyardRequestContext() + +def test_get_all_actions(): + """ + Tests the main response from get all actions + """ + action_resource = ActionsResource() + action_resource.get_all_actions_db = actions_db + action_resource.get_all_dag_runs_db = dag_runs_db + action_resource.get_all_tasks_db = tasks_db + os.environ['DB_CONN_AIRFLOW'] = 'nothing' + os.environ['DB_CONN_SHIPYARD'] = 'nothing' + result = action_resource.get_all_actions() + print(result) + assert len(result) == len(actions_db()) + for action in result: + if action['name'] == 'dag_it': + assert len(action['steps']) == 1 + assert action['dag_status'] == 'FAILED' + if action['name'] == 'dag2': + assert len(action['steps']) == 3 + assert action['dag_status'] == 'SUCCESS' + +def test_create_action(): + action_resource = ActionsResource() + action_resource.get_all_actions_db = actions_db + action_resource.get_all_dag_runs_db = dag_runs_db + action_resource.get_all_tasks_db = tasks_db + action_resource.invoke_airflow_dag = airflow_stub + action_resource.insert_action = insert_action_stub + action_resource.audit_control_command_db = audit_control_command_db + + # with invalid input. fail. + try: + action = action_resource.create_action( + action={'name': 'broken', 'parameters': {'a': 'aaa'}}, + context=context + ) + assert False, 'Should throw an ApiError' + except ApiError: + # expected + pass + + # with valid input and some parameters + try: + action = action_resource.create_action( + action={'name': 'deploy_site', 'parameters': {'a': 'aaa'}}, + context=context + ) + assert action['timestamp'] + assert action['id'] + assert len(action['id']) == 26 + assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402' + assert action['dag_status'] == 'SCHEDULED' + except ApiError: + assert False, 'Should not raise an ApiError' + print(json.dumps(action, default=str)) + + # with valid input and no parameters + try: + action = action_resource.create_action( + action={'name': 'deploy_site'}, + context=context + ) + assert action['timestamp'] + assert action['id'] + assert len(action['id']) == 26 + assert action['dag_execution_date'] == '2017-09-06 14:10:08.528402' + assert action['dag_status'] == 'SCHEDULED' + except ApiError: + assert False, 'Should not raise an ApiError' + print(json.dumps(action, default=str)) diff --git a/tests/unit/control/test_actions_control_api.py b/tests/unit/control/test_actions_control_api.py new file mode 100644 index 00000000..157a18c3 --- /dev/null +++ b/tests/unit/control/test_actions_control_api.py @@ -0,0 +1,164 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from shipyard_airflow.control.actions_control_api import ActionsControlResource +from shipyard_airflow.control.base import ShipyardRequestContext +from shipyard_airflow.db.errors import AirflowStateError +from shipyard_airflow.db.db import AIRFLOW_DB +from shipyard_airflow.errors import ApiError + + +def actions_db(action_id): + """ + replaces the actual db call + """ + if action_id == 'not found': + return None + elif action_id == 'state error': + return { + 'id': 'state error', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'state error', + 'dag_execution_date': '2017-09-06 14:10:08.528402', + 'user': 'robot1', + 'timestamp': '2017-09-06 14:10:08.528402', + 'context_marker': '8-4-4-4-12a' + } + else: + return { + 'id': '59bb330a-9e64-49be-a586-d253bb67d443', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'did2', + 'dag_execution_date': '2017-09-06 14:10:08.528402', + 'user': 'robot1', + 'timestamp': '2017-09-06 14:10:08.528402', + 'context_marker': '8-4-4-4-12a' + } + +def control_dag_run(dag_id, + execution_date, + expected_state, + desired_state): + if dag_id == 'state error': + raise AirflowStateError(message='test error') + else: + pass + +def audit_control_command_db(action_audit): + pass + +def test_get_action(): + """ + Tests the main response from get all actions + """ + saved_control_dag_run = AIRFLOW_DB._control_dag_run + try: + action_resource = ActionsControlResource() + # stubs for db + action_resource.get_action_db = actions_db + action_resource.audit_control_command_db = audit_control_command_db + + AIRFLOW_DB._control_dag_run = control_dag_run + + # bad action + try: + action_resource.handle_control( + action_id='not found', + control_verb='meep', + context=ShipyardRequestContext() + ) + assert False, "shouldn't find the action" + except ApiError as api_error: + assert api_error.title == 'Action not found' + assert api_error.status == '404 Not Found' + + # bad control + try: + action_resource.handle_control( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + control_verb='meep', + context=ShipyardRequestContext() + ) + assert False, 'meep is not a valid action' + except ApiError as api_error: + assert api_error.title == 'Control not supported' + assert api_error.status == '404 Not Found' + + # success on each action - pause, unpause, stop + try: + action_resource.handle_control( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + control_verb='pause', + context=ShipyardRequestContext() + ) + except ApiError as api_error: + assert False, 'Should not raise an ApiError' + + try: + action_resource.handle_control( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + control_verb='unpause', + context=ShipyardRequestContext() + ) + except ApiError as api_error: + assert False, 'Should not raise an ApiError' + + try: + action_resource.handle_control( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + control_verb='stop', + context=ShipyardRequestContext() + ) + except ApiError as api_error: + assert False, 'Should not raise an ApiError' + + # pause state conflict + try: + action_resource.handle_control( + action_id='state error', + control_verb='pause', + context=ShipyardRequestContext() + ) + assert False, 'should raise a conflicting state' + except ApiError as api_error: + assert api_error.title == 'Unable to pause action' + assert api_error.status == '409 Conflict' + + # Unpause state conflict + try: + action_resource.handle_control( + action_id='state error', + control_verb='unpause', + context=ShipyardRequestContext() + ) + assert False, 'should raise a conflicting state' + except ApiError as api_error: + assert api_error.title == 'Unable to unpause action' + assert api_error.status == '409 Conflict' + + # Stop state conflict + try: + action_resource.handle_control( + action_id='state error', + control_verb='stop', + context=ShipyardRequestContext() + ) + assert False, 'should raise a conflicting state' + except ApiError as api_error: + assert api_error.title == 'Unable to stop action' + assert api_error.status == '409 Conflict' + finally: + # modified class variable... replace it + AIRFLOW_DB._control_dag_run = saved_control_dag_run diff --git a/tests/unit/control/test_actions_id_api.py b/tests/unit/control/test_actions_id_api.py new file mode 100644 index 00000000..16175c7d --- /dev/null +++ b/tests/unit/control/test_actions_id_api.py @@ -0,0 +1,152 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from datetime import datetime +from shipyard_airflow.control.actions_id_api import (ActionsIdResource) + +DATE_ONE = datetime(2017, 9, 13, 11, 13, 3, 57000) +DATE_TWO = datetime(2017, 9, 13, 11, 13, 5, 57000) +DATE_ONE_STR = DATE_ONE.strftime('%Y-%m-%dT%H:%M:%S') +DATE_TWO_STR = DATE_TWO.strftime('%Y-%m-%dT%H:%M:%S') + +def actions_db(action_id): + """ + replaces the actual db call + """ + return { + 'id': '12345678901234567890123456', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'did2', + 'dag_execution_date': DATE_ONE_STR, + 'user': 'robot1', + 'timestamp': DATE_ONE, + 'context_marker': '8-4-4-4-12a' + } + +def dag_runs_db(dag_id, execution_date): + """ + replaces the actual db call + """ + return [{ + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'FAILED', + 'run_id': '99', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_ONE + }] + +def tasks_db(dag_id, execution_date): + """ + replaces the actual db call + """ + return [ + { + 'task_id': '1a', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_ONE, + 'duration': '20mins', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_ONE + }, + { + 'task_id': '1b', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_TWO, + 'end_date': DATE_TWO, + 'duration': '1minute', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_ONE + }, + { + 'task_id': '1c', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'FAILED', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_TWO, + 'end_date': DATE_TWO, + 'duration': '1day', + 'try_number': '3', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + } + ] + +def get_validations(action_id): + """ + Stub to return validations + """ + return [ + { + 'id': '43', + 'action_id': '12345678901234567890123456', + 'validation_name': 'It has shiny goodness', + 'details': 'This was not very shiny.' + } + ] + +def get_ac_audit(action_id): + """ + Stub to return command audit response + """ + return [ + { + 'id': 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'action_id': '12345678901234567890123456', + 'command': 'PAUSE', + 'user': 'Operator 99', + 'datetime': DATE_ONE + }, + { + 'id': 'ABCDEFGHIJKLMNOPQRSTUVWXYA', + 'action_id': '12345678901234567890123456', + 'command': 'UNPAUSE', + 'user': 'Operator 99', + 'datetime': DATE_TWO + } + ] + +def test_get_action(): + """ + Tests the main response from get all actions + """ + action_resource = ActionsIdResource() + # stubs for db + action_resource.get_action_db = actions_db + action_resource.get_dag_run_db = dag_runs_db + action_resource.get_tasks_db = tasks_db + action_resource.get_validations_db = get_validations + action_resource.get_action_command_audit_db = get_ac_audit + + action = action_resource.get_action('12345678901234567890123456') + print(json.dumps(action, default=str)) + if action['name'] == 'dag_it': + assert len(action['steps']) == 3 + assert action['dag_status'] == 'FAILED' + assert len(action['command_audit']) == 2 diff --git a/tests/unit/control/test_actions_steps_id_api.py b/tests/unit/control/test_actions_steps_id_api.py new file mode 100644 index 00000000..ae94d408 --- /dev/null +++ b/tests/unit/control/test_actions_steps_id_api.py @@ -0,0 +1,116 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from datetime import datetime + +from shipyard_airflow.errors import ApiError +from shipyard_airflow.control.actions_steps_id_api import ActionsStepsResource + +DATE_ONE = datetime(2017, 9, 13, 11, 13, 3, 57000) +DATE_TWO = datetime(2017, 9, 13, 11, 13, 5, 57000) +DATE_ONE_STR = DATE_ONE.strftime('%Y-%m-%dT%H:%M:%S') +DATE_TWO_STR = DATE_TWO.strftime('%Y-%m-%dT%H:%M:%S') + + +def actions_db(action_id): + """ + replaces the actual db call + """ + return { + 'id': '59bb330a-9e64-49be-a586-d253bb67d443', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'did2', + 'dag_execution_date': DATE_ONE_STR, + 'user': 'robot1', + 'timestamp': DATE_ONE_STR, + 'context_marker': '8-4-4-4-12a' + } + +def tasks_db(dag_id, execution_date): + """ + replaces the actual db call + """ + return [ + { + 'task_id': '1a', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_ONE, + 'end_date': DATE_ONE, + 'duration': '20mins', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_ONE + }, + { + 'task_id': '1b', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'SUCCESS', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_TWO, + 'end_date': DATE_TWO, + 'duration': '1minute', + 'try_number': '1', + 'operator': 'smooth', + 'queued_dttm': DATE_ONE + }, + { + 'task_id': '1c', + 'dag_id': 'did2', + 'execution_date': DATE_ONE, + 'state': 'FAILED', + 'run_id': '12345', + 'external_trigger': 'something', + 'start_date': DATE_TWO, + 'end_date': DATE_TWO, + 'duration': '1day', + 'try_number': '3', + 'operator': 'smooth', + 'queued_dttm': DATE_TWO + } + ] + +def test_get_action_steps(): + """ + Tests the main response from get all actions + """ + action_resource = ActionsStepsResource() + # stubs for db + action_resource.get_action_db = actions_db + action_resource.get_tasks_db = tasks_db + + step = action_resource.get_action_step( + '59bb330a-9e64-49be-a586-d253bb67d443', + '1c' + ) + assert step['index'] == 3 + assert step['try_number'] == '3' + assert step['operator'] == 'smooth' + print(json.dumps(step, default=str)) + + try: + step = action_resource.get_action_step( + '59bb330a-9e64-49be-a586-d253bb67d443', + 'cheese' + ) + assert False, 'should raise an ApiError' + except ApiError as api_error: + assert api_error.title == 'Step not found' + assert api_error.status == '404 Not Found' diff --git a/tests/unit/control/test_actions_validations_id_api.py b/tests/unit/control/test_actions_validations_id_api.py new file mode 100644 index 00000000..527ce7ad --- /dev/null +++ b/tests/unit/control/test_actions_validations_id_api.py @@ -0,0 +1,87 @@ +# Copyright 2017 AT&T Intellectual Property. All other rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import json +from shipyard_airflow.control.actions_validations_id_api import ( + ActionsValidationsResource +) +from shipyard_airflow.errors import ApiError + +def actions_db(action_id): + """ + replaces the actual db call + """ + if action_id == 'error_it': + return None + else: + return { + 'id': '59bb330a-9e64-49be-a586-d253bb67d443', + 'name': 'dag_it', + 'parameters': None, + 'dag_id': 'did2', + 'dag_execution_date': '2017-09-06 14:10:08.528402', + 'user': 'robot1', + 'timestamp': '2017-09-06 14:10:08.528402', + 'context_marker': '8-4-4-4-12a' + } + +def get_validations(validation_id): + """ + Stub to return validations + """ + if validation_id == '43': + return { + 'id': '43', + 'action_id': '59bb330a-9e64-49be-a586-d253bb67d443', + 'validation_name': 'It has shiny goodness', + 'details': 'This was not very shiny.' + } + else: + return None + +def test_get_action_validation(): + """ + Tests the main response from get all actions + """ + action_resource = ActionsValidationsResource() + # stubs for db + action_resource.get_action_db = actions_db + action_resource.get_validation_db = get_validations + + validation = action_resource.get_action_validation( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + validation_id='43' + ) + print(json.dumps(validation, default=str)) + assert validation['action_id'] == '59bb330a-9e64-49be-a586-d253bb67d443' + assert validation['validation_name'] == 'It has shiny goodness' + + try: + validation = action_resource.get_action_validation( + action_id='59bb330a-9e64-49be-a586-d253bb67d443', + validation_id='not a chance' + ) + assert False + except ApiError as api_error: + assert api_error.status == '404 Not Found' + assert api_error.title == 'Validation not found' + + try: + validation = action_resource.get_action_validation( + action_id='error_it', + validation_id='not a chance' + ) + assert False + except ApiError as api_error: + assert api_error.status == '404 Not Found' + assert api_error.title == 'Action not found' diff --git a/tox.ini b/tox.ini index db59bd4c..2327bad2 100644 --- a/tox.ini +++ b/tox.ini @@ -14,13 +14,21 @@ commands= commands = flake8 {posargs} [testenv:bandit] -commands = bandit -r shipyard_airflow -x tests -n 5 +# NOTE(Bryan Strassner) ignoring airflow plugin which uses a subexec +# tests are not under the shipyard_airflow directory, not exlcuding those +commands = bandit -r shipyard_airflow -x plugins/rest_api_plugin.py -n 5 + +[testenv:genconfig] +commands = oslo-config-generator --config-file=generator/config-generator.conf + +[testenv:genpolicy] +commands = oslopolicy-sample-generator --config-file=generator/policy-generator.conf [flake8] # NOTE(Bryan Strassner) ignoring F841 because of the airflow example pattern # of naming variables even if they aren't used for DAGs and Operators. # Doing so adds readability and context in this case. -ignore=E302,H306,D100,D101,D102,F841 -# NOTE(Bryan Strassner) excluding 3rd party code that is brought into the +ignore = E302,H306,D100,D101,D102,F841 +# NOTE(Bryan Strassner) excluding 3rd party and generated code that is brought into the # codebase. -exclude=*plugins/rest_api_plugin.py,*lib/python*,*egg,.git*,*.md,.tox* \ No newline at end of file +exclude = *plugins/rest_api_plugin.py,*lib/python*,*egg,.git*,*.md,.tox*,alembic/env.py,build/*