├── .gitignore ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.md ├── alembic.ini ├── alembic ├── README ├── env.py ├── script.py.mako └── versions │ ├── 159b60132535_initial_migrations.py │ ├── 1868e29e8306_add_compose_details_and_compose_job_.py │ └── 55932e5d6b3f_added_family_arch_fields_in_job_table.py ├── ansible ├── README.rst ├── autocloud.yml ├── group_vars │ └── all ├── hosts └── roles │ ├── backend │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── autocloud.j2 │ ├── commons │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── hosts.j2 │ ├── database │ ├── handlers │ │ └── main.yml │ └── tasks │ │ └── main.yml │ └── web │ └── tasks │ └── main.yml ├── apache ├── autocloud.conf └── autocloud.wsgi ├── autocloud.service ├── autocloud.spec ├── autocloud.txt ├── autocloud ├── __init__.py ├── constants.py ├── consumer.py ├── models.py ├── producer.py ├── utils │ └── __init__.py └── web │ ├── __init__.py │ ├── app.py │ ├── pagination.py │ ├── static │ ├── bootstrap │ ├── bootstrap-3.3.4-fedora │ │ ├── css │ │ │ ├── bootstrap-theme.css │ │ │ ├── bootstrap-theme.css.map │ │ │ ├── bootstrap-theme.min.css │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ └── bootstrap.min.css │ │ ├── fonts │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ └── js │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.min.js │ │ │ └── npm.js │ ├── css │ │ ├── avatars.css │ │ ├── fonts │ │ │ ├── Cantarell-Bold-webfont.eot │ │ │ ├── Cantarell-Bold-webfont.svg │ │ │ ├── Cantarell-Bold-webfont.ttf │ │ │ ├── Cantarell-Bold-webfont.woff │ │ │ ├── Cantarell-BoldOblique-webfont.eot │ │ │ ├── Cantarell-BoldOblique-webfont.svg │ │ │ ├── Cantarell-BoldOblique-webfont.ttf │ │ │ ├── Cantarell-BoldOblique-webfont.woff │ │ │ ├── Cantarell-Oblique-webfont.eot │ │ │ ├── Cantarell-Oblique-webfont.svg │ │ │ ├── Cantarell-Oblique-webfont.ttf │ │ │ ├── Cantarell-Oblique-webfont.woff │ │ │ ├── Cantarell-Regular-webfont.eot │ │ │ ├── Cantarell-Regular-webfont.svg │ │ │ ├── Cantarell-Regular-webfont.ttf │ │ │ ├── Cantarell-Regular-webfont.woff │ │ │ ├── Comfortaa_Bold-webfont.eot │ │ │ ├── Comfortaa_Bold-webfont.svg │ │ │ ├── Comfortaa_Bold-webfont.ttf │ │ │ ├── Comfortaa_Bold-webfont.woff │ │ │ ├── Comfortaa_Regular-webfont.eot │ │ │ ├── Comfortaa_Regular-webfont.svg │ │ │ ├── Comfortaa_Regular-webfont.ttf │ │ │ ├── Comfortaa_Regular-webfont.woff │ │ │ ├── Comfortaa_Thin-webfont.eot │ │ │ ├── Comfortaa_Thin-webfont.svg │ │ │ ├── Comfortaa_Thin-webfont.ttf │ │ │ └── Comfortaa_Thin-webfont.woff │ │ ├── footer.css │ │ ├── navbar.css │ │ ├── style.css │ │ └── text.css │ ├── ico │ │ └── favicon.ico │ ├── img │ │ └── spinner.gif │ └── js │ │ ├── jquery-1.10.2.min.js │ │ └── site.js │ ├── templates │ ├── 404.html │ ├── __init__.py │ ├── compose_details.html │ ├── index.html │ ├── job_details.html │ ├── job_output.html │ ├── login.html │ └── master.html │ └── utils.py ├── autocloud_job ├── autocloud_job.py ├── config └── autocloud.cfg ├── createdb.py ├── docs ├── Makefile ├── building.rst ├── conf.py ├── deployment.png ├── design.png ├── design.rst ├── design.svg ├── index.rst └── setup.rst ├── fedmsg.d ├── autocloud.py ├── base.py ├── endpoints-autocloud.py ├── logging.py └── ssl.py ├── kill_vagrant ├── publish ├── fixtures.json ├── generate_fixtures.py ├── publish_messages.py └── seed.py ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg* 3 | dist 4 | build 5 | *.swp 6 | *~ 7 | autocloud.db 8 | -------------------------------------------------------------------------------- /CHANGELOG.rst: -------------------------------------------------------------------------------- 1 | 2 | 0.8.0 3 | ----- 4 | 5 | Pull Requests 6 | 7 | - (@vivekanand1101) #60, Fix config file 8 | https://github.com/kushaldas/autocloud/pull/60 9 | - (@sayanchowdhury) #62, autocloud: Fix the autocloud consumer for the F28 messages 10 | https://github.com/kushaldas/autocloud/pull/62 11 | 12 | Commits 13 | 14 | - 1ead83200 config: Fix typo, cofig => config 15 | https://github.com/kushaldas/autocloud/commit/1ead83200 16 | - 2a9b88437 config: Fix loading of default config values 17 | https://github.com/kushaldas/autocloud/commit/2a9b88437 18 | - 537134eb7 autocloud: Fix the autocloud consumer for the F28 messages 19 | https://github.com/kushaldas/autocloud/commit/537134eb7 20 | 21 | 0.7.4 22 | ----- 23 | 24 | Pull Requests 25 | 26 | - (@sayanchowdhury) #58, Fix the queryset to ignore globally instead of just compose id 27 | https://github.com/kushaldas/autocloud/pull/58 28 | 29 | Commits 30 | 31 | - 615b2bebc web: Fix the queryset to ignore globally instead of just compose id 32 | https://github.com/kushaldas/autocloud/commit/615b2bebc 33 | 34 | 0.7.3 35 | ----- 36 | 37 | Pull Requests 38 | 39 | - (@sayanchowdhury) #57, Only show the supported archs in the webapp 40 | https://github.com/kushaldas/autocloud/pull/57 41 | 42 | Commits 43 | 44 | - 5e7dbbcc4 web: Exclude the archs that are not supported in the webapp too 45 | https://github.com/kushaldas/autocloud/commit/5e7dbbcc4 46 | 47 | 0.7.2 48 | ----- 49 | 50 | Pull Requests 51 | 52 | - (@sayanchowdhury) #56, Ignore if the arch is not supported 53 | https://github.com/kushaldas/autocloud/pull/56 54 | 55 | Commits 56 | 57 | - 84281ac05 Don't process arch which are not supported 58 | https://github.com/kushaldas/autocloud/commit/84281ac05 59 | 60 | 0.7.1 61 | ----- 62 | 63 | Pull Requests 64 | 65 | - (@sayanchowdhury) #51, Sync 0.6 release 66 | https://github.com/kushaldas/autocloud/pull/51 67 | - (@sayanchowdhury) #52, Add new entries to the MANIFEST file 68 | https://github.com/kushaldas/autocloud/pull/52 69 | 70 | Commits 71 | 72 | - 3d48bfc67 Removes sha-bang line 73 | https://github.com/kushaldas/autocloud/commit/3d48bfc67 74 | - 622b99d9f If no result file, just save that info to db 75 | https://github.com/kushaldas/autocloud/commit/622b99d9f 76 | - f0e1cac3e Add new entries to the MANIFEST file 77 | https://github.com/kushaldas/autocloud/commit/f0e1cac3e 78 | 79 | 0.7.0 80 | ----- 81 | 82 | Pull Requests 83 | 84 | - (@yahzaa) #49, Refactor AutoCloudConsumer 85 | https://github.com/kushaldas/autocloud/pull/49 86 | - (@puiterwijk) #50, Add artifact information to compose.complete message 87 | https://github.com/kushaldas/autocloud/pull/50 88 | - (@sayanchowdhury) #46, Explicitly close the DB connections 89 | https://github.com/kushaldas/autocloud/pull/46 90 | 91 | Commits 92 | 93 | - 144aa8eb8 Reduce nesting in AutoCloudConsumer.consume 94 | https://github.com/kushaldas/autocloud/commit/144aa8eb8 95 | - 874ee5ffb Add artifact information to compose.complete message 96 | https://github.com/kushaldas/autocloud/commit/874ee5ffb 97 | - 53cf57544 pep8 98 | https://github.com/kushaldas/autocloud/commit/53cf57544 99 | - 060c9014e Explicitly close the DB connections 100 | https://github.com/kushaldas/autocloud/commit/060c9014e 101 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include fedmsg.d/autocloud.py 3 | include fedmsg.d/endpoints-autocloud.py 4 | include config/autocloud.cfg 5 | include kill_vagrant 6 | recursive-include apache * 7 | recursive-include alembic * 8 | include alembic.ini 9 | include LICENSE 10 | include autocloud_job 11 | include autocloud_job.py 12 | include createdb.py 13 | include autocloud.service 14 | recursive-include autocloud/web/static * 15 | recursive-include autocloud/web/templates * 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autocloud 2 | 3 | ## Setup 4 | 5 | ``` 6 | sudo dnf install fedfind 7 | 8 | pip install -r requirements.txt 9 | python setup.py develop 10 | sudo mkdir -p /etc/autocloud; sudo cp config/autocloud.cfg /etc/autocloud/autocloud.cfg 11 | ``` 12 | 13 | ## Development 14 | 15 | ### Create the database 16 | 17 | ``` 18 | python createdb.py 19 | ``` 20 | 21 | ### Run the fedmsg-hub 22 | 23 | ``` 24 | fedmsg-hub 25 | ``` 26 | 27 | ### Publish messages for testing 28 | 29 | Run this command in a seperate terminal 30 | 31 | ``` 32 | python publish/publish_messages.py 33 | ``` 34 | 35 | ### Applying the migrations 36 | 37 | ``` 38 | alembic upgrade head 39 | ``` 40 | -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = alembic 6 | 7 | # template used to generate migration files 8 | # file_template = %%(rev)s_%%(slug)s 9 | 10 | # max length of characters to apply to the 11 | # "slug" field 12 | #truncate_slug_length = 40 13 | 14 | # set to 'true' to run the environment during 15 | # the 'revision' command, regardless of autogenerate 16 | # revision_environment = false 17 | 18 | # set to 'true' to allow .pyc and .pyo files without 19 | # a source .py file to be detected as revisions in the 20 | # versions/ directory 21 | # sourceless = false 22 | 23 | # version location specification; this defaults 24 | # to alembic/versions. When using multiple version 25 | # directories, initial revisions must be specified with --version-path 26 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions 27 | 28 | # the output encoding used when revision files 29 | # are written from script.py.mako 30 | # output_encoding = utf-8 31 | 32 | sqlalchemy.url = driver://user:pass@localhost/dbname 33 | 34 | 35 | # Logging configuration 36 | [loggers] 37 | keys = root,sqlalchemy,alembic 38 | 39 | [handlers] 40 | keys = console 41 | 42 | [formatters] 43 | keys = generic 44 | 45 | [logger_root] 46 | level = WARN 47 | handlers = console 48 | qualname = 49 | 50 | [logger_sqlalchemy] 51 | level = WARN 52 | handlers = 53 | qualname = sqlalchemy.engine 54 | 55 | [logger_alembic] 56 | level = INFO 57 | handlers = 58 | qualname = alembic 59 | 60 | [handler_console] 61 | class = StreamHandler 62 | args = (sys.stderr,) 63 | level = NOTSET 64 | formatter = generic 65 | 66 | [formatter_generic] 67 | format = %(levelname)-5.5s [%(name)s] %(message)s 68 | datefmt = %H:%M:%S 69 | -------------------------------------------------------------------------------- /alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import engine_from_config, pool 4 | from logging.config import fileConfig 5 | 6 | # this is the Alembic Config object, which provides 7 | # access to the values within the .ini file in use. 8 | config = context.config 9 | 10 | # Interpret the config file for Python logging. 11 | # This line sets up loggers basically. 12 | fileConfig(config.config_file_name) 13 | 14 | # add your model's MetaData object here 15 | # for 'autogenerate' support 16 | # from myapp import mymodel 17 | # target_metadata = mymodel.Base.metadata 18 | from autocloud.models import Base 19 | target_metadata = Base.metadata 20 | 21 | # other values from the config, defined by the needs of env.py, 22 | # can be acquired: 23 | # my_important_option = config.get_main_option("my_important_option") 24 | # ... etc. 25 | 26 | 27 | def run_migrations_offline(): 28 | """Run migrations in 'offline' mode. 29 | 30 | This configures the context with just a URL 31 | and not an Engine, though an Engine is acceptable 32 | here as well. By skipping the Engine creation 33 | we don't even need a DBAPI to be available. 34 | 35 | Calls to context.execute() here emit the given string to the 36 | script output. 37 | 38 | """ 39 | from autoclould import SQLALCHEMY_URI 40 | url = SQLALCHEMY_URI 41 | context.configure( 42 | url=url, target_metadata=target_metadata, literal_binds=True) 43 | 44 | with context.begin_transaction(): 45 | context.run_migrations() 46 | 47 | 48 | def run_migrations_online(): 49 | """Run migrations in 'online' mode. 50 | 51 | In this scenario we need to create an Engine 52 | and associate a connection with the context. 53 | 54 | """ 55 | alembic_config = config.get_section(config.config_ini_section) 56 | from autocloud import SQLALCHEMY_URI 57 | alembic_config['sqlalchemy.url'] = SQLALCHEMY_URI 58 | alembic_config['include_schemas'] = True 59 | connectable = engine_from_config( 60 | alembic_config, 61 | prefix='sqlalchemy.', 62 | poolclass=pool.NullPool) 63 | 64 | with connectable.connect() as connection: 65 | context.configure( 66 | connection=connection, 67 | target_metadata=target_metadata 68 | ) 69 | 70 | with context.begin_transaction(): 71 | context.run_migrations() 72 | 73 | if context.is_offline_mode(): 74 | run_migrations_offline() 75 | else: 76 | run_migrations_online() 77 | -------------------------------------------------------------------------------- /alembic/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = ${repr(up_revision)} 11 | down_revision = ${repr(down_revision)} 12 | branch_labels = ${repr(branch_labels)} 13 | depends_on = ${repr(depends_on)} 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | ${imports if imports else ""} 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /alembic/versions/159b60132535_initial_migrations.py: -------------------------------------------------------------------------------- 1 | """Initial migrations. 2 | 3 | Revision ID: 159b60132535 4 | Revises: 5 | Create Date: 2015-12-04 21:53:17.278656 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '159b60132535' 11 | down_revision = None 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | 18 | 19 | def upgrade(): 20 | ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('job_details', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('taskid', sa.String(length=255), nullable=False), 24 | sa.Column('status', sa.String(length=255), nullable=True), 25 | sa.Column('output', sa.Text(), nullable=False), 26 | sa.Column('created_on', sa.DateTime(), nullable=True), 27 | sa.Column('last_updated', sa.DateTime(), nullable=True), 28 | sa.Column('user', sa.String(length=255), nullable=False), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | ### end Alembic commands ### 32 | 33 | 34 | def downgrade(): 35 | ### commands auto generated by Alembic - please adjust! ### 36 | op.drop_table('job_details') 37 | ### end Alembic commands ### 38 | -------------------------------------------------------------------------------- /alembic/versions/1868e29e8306_add_compose_details_and_compose_job_.py: -------------------------------------------------------------------------------- 1 | """add compose details and compose job details table 2 | 3 | Revision ID: 1868e29e8306 4 | Revises: 55932e5d6b3f 5 | Create Date: 2016-04-26 18:28:54.865917 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '1868e29e8306' 11 | down_revision = '55932e5d6b3f' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | 18 | 19 | def upgrade(): 20 | op.create_table( 21 | 'compose_details', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('date', sa.DateTime(), nullable=False), 24 | sa.Column('compose_id', sa.String(length=255), nullable=False, 25 | unique=True), 26 | sa.Column('respin', sa.String(length=255), nullable=False), 27 | sa.Column('type', sa.String(length=255), nullable=False), 28 | sa.Column('passed', sa.Integer(), nullable=True, default=0), 29 | sa.Column('failed', sa.Integer(), nullable=True, default=0), 30 | sa.Column('status', sa.String(length=255), nullable=True), 31 | sa.Column('created_on', sa.DateTime(), nullable=False), 32 | sa.Column('last_updated', sa.DateTime(), nullable=False), 33 | sa.Column('location', sa.String(length=255), nullable=False), 34 | sa.PrimaryKeyConstraint('id') 35 | ) 36 | op.create_table( 37 | 'compose_job_details', 38 | sa.Column('id', sa.Integer(), nullable=False), 39 | sa.Column('arch', sa.String(length=255), nullable=True), 40 | sa.Column('compose_id', sa.String(length=255), nullable=False), 41 | sa.Column('created_on', sa.DateTime(), nullable=False), 42 | sa.Column('family', sa.String(length=255), nullable=True), 43 | sa.Column('image_url', sa.String(length=255), nullable=False), 44 | sa.Column('last_updated', sa.DateTime(), nullable=True), 45 | sa.Column('output', sa.Text(), nullable=False), 46 | sa.Column('release', sa.String(length=255), nullable=True), 47 | sa.Column('status', sa.String(length=255), nullable=False), 48 | sa.Column('subvariant', sa.String(length=255), nullable=False), 49 | sa.Column('user', sa.String(length=255), nullable=False), 50 | sa.Column('image_format', sa.String(length=255), nullable=False), 51 | sa.Column('image_type', sa.String(length=255), nullable=False), 52 | sa.Column('image_name', sa.String(length=255), nullable=False), 53 | sa.PrimaryKeyConstraint('id') 54 | ) 55 | pass 56 | 57 | 58 | def downgrade(): 59 | op.drop_table('compose_details') 60 | op.drop_table('compose_job_details') 61 | pass 62 | -------------------------------------------------------------------------------- /alembic/versions/55932e5d6b3f_added_family_arch_fields_in_job_table.py: -------------------------------------------------------------------------------- 1 | """Added family, arch fields in Job table. 2 | 3 | Revision ID: 55932e5d6b3f 4 | Revises: 159b60132535 5 | Create Date: 2015-12-05 12:44:33.525292 6 | 7 | """ 8 | 9 | # revision identifiers, used by Alembic. 10 | revision = '55932e5d6b3f' 11 | down_revision = '159b60132535' 12 | branch_labels = None 13 | depends_on = None 14 | 15 | from alembic import op 16 | import sqlalchemy as sa 17 | 18 | 19 | def upgrade(): 20 | ### commands auto generated by Alembic - please adjust! ### 21 | op.add_column('job_details', sa.Column('arch', sa.String(length=255), nullable=True)) 22 | op.add_column('job_details', sa.Column('family', sa.String(length=255), nullable=True)) 23 | op.add_column('job_details', sa.Column('release', sa.String(length=255), nullable=True)) 24 | ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | ### commands auto generated by Alembic - please adjust! ### 29 | op.drop_column('job_details', 'family') 30 | op.drop_column('job_details', 'arch') 31 | op.drop_column('job_details', 'release') 32 | ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /ansible/README.rst: -------------------------------------------------------------------------------- 1 | Configuration 2 | ============= 3 | 4 | * Update host detail to hosts file and make sure each node have 5 | password-less ssh connection with a specific host user. 6 | 7 | How To Run 8 | ========== 9 | 10 | :: 11 | 12 | ansible-playbook -i hosts autocloud.yml 13 | -------------------------------------------------------------------------------- /ansible/autocloud.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | user: fedora 4 | sudo: yes 5 | roles: 6 | - { role: commons, tags: ['config'] } 7 | 8 | - hosts: backend 9 | user: fedora 10 | sudo: yes 11 | roles: 12 | - { role: backend, tags: ['backend'] } 13 | 14 | - hosts: database 15 | user: fedora 16 | sudo: yes 17 | roles: 18 | - { role: database, tags: ['database'] } 19 | 20 | - hosts: web 21 | user: fedora 22 | sudo: yes 23 | roles: 24 | - { role: web, tags: ['web'] } 25 | -------------------------------------------------------------------------------- /ansible/group_vars/all: -------------------------------------------------------------------------------- 1 | # Interface Settings 2 | iface: '{{ ansible_default_ipv4.interface }}' 3 | 4 | # Database URL for connection 5 | db_uri: sqlite:///autocloud.db 6 | 7 | # Database Variable 8 | dbname: test 9 | dbuser: test_user 10 | dbpassword: mysupersecretpassword 11 | 12 | # Virtualbox repo url 13 | virtbox_repo_url: http://download.virtualbox.org/virtualbox/rpm/fedora/virtualbox.repo 14 | 15 | # Fedora cloud configuration file 16 | fedora_txt: https://raw.githubusercontent.com/kushaldas/tunirtests/master/fedora.txt 17 | 18 | # Dependencies 19 | common_dependencies: 20 | - https://kojipkgs.fedoraproject.org//work/tasks/8432/11198432/autocloud-common-0.1-4.fc23.noarch.rpm 21 | 22 | backend_dependencies: 23 | - redis 24 | - vagrant 25 | - https://kojipkgs.fedoraproject.org//work/tasks/8432/11198432/autocloud-backend-0.1-4.fc23.noarch.rpm 26 | 27 | libbox_dependencies: 28 | - vagrant-libvirt 29 | 30 | virtbox_dependencies: 31 | - VirtualBox-4.3 32 | - kernel-devel 33 | 34 | web_dependencies: 35 | - http 36 | - https://kojipkgs.fedoraproject.org//work/tasks/8432/11198432/autocloud-web-0.1-4.fc23.noarch.rpm 37 | 38 | database_dependencies: 39 | - postgresql 40 | - postgresql-server 41 | - postgresql-contrib 42 | - postgresql-devel 43 | - python-psycopg2 44 | -------------------------------------------------------------------------------- /ansible/hosts: -------------------------------------------------------------------------------- 1 | [backend] 2 | 10.65.213.168 3 | 10.65.213.164 virtbox=true 4 | 5 | [database] 6 | 10.65.213.168 7 | 8 | [web] 9 | 10.65.213.168 10 | 10.65.213.164 11 | -------------------------------------------------------------------------------- /ansible/roles/backend/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Add required repositories 2 | get_url: url={{ item }} dest=/etc/yum.repos.d/ 3 | with_items: 4 | - "{{ virtbox_repo_url }}" 5 | when: virtbox is defined 6 | 7 | - name: Install common dependencies 8 | dnf: name={{ item }} state=present 9 | with_items: 10 | - "{{ backend_dependencies }}" 11 | 12 | - name: Install required packages to virtbox backend 13 | dnf: name={{ item }} state=present 14 | with_items: 15 | - "{{ virtbox_dependencies }}" 16 | when: virtbox is defined 17 | 18 | - name: Install required packages to libbox backend 19 | dnf: name={{ item }} state=present 20 | with_items: 21 | - "{{ libbox_dependencies }}" 22 | when: virtbox is not defined 23 | 24 | - name: Configure tunir job details 25 | get_url: url={{ fedora_txt }} dest=/etc/autocloud/ 26 | 27 | - name: setup virtbox 28 | command: /etc/init.d/vboxdrv setup 29 | when: virtbox is defined 30 | 31 | - name: Start redis service 32 | service: name=redis state=started 33 | 34 | - name: Enable tunir ports 35 | command: python /usr/share/tunir/createports.py 36 | 37 | - name: Update autocloud.cfg file with updated sqlalchemy value 38 | template: src=autocloud.j2 dest=/etc/autocloud/autocloud.cfg 39 | 40 | - name: Start fedmsg-hub service 41 | service: name=fedmsg-hub state=started 42 | 43 | - name: start autocloud service 44 | service: name=autocloud state=started 45 | -------------------------------------------------------------------------------- /ansible/roles/backend/templates/autocloud.j2: -------------------------------------------------------------------------------- 1 | [autocloud] 2 | koji_server_url = http://koji.fedoraproject.org/kojihub/ 3 | base_koji_task_url = https://kojipkgs.fedoraproject.org//work/ 4 | virtualbox = false 5 | host = 0.0.0.0 6 | port = 5000 7 | debug = false 8 | 9 | [sqlalchemy] 10 | uri = {{ db_uri }} 11 | -------------------------------------------------------------------------------- /ansible/roles/commons/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Create host file for each machines 3 | template: src=hosts.j2 dest=/etc/hosts 4 | 5 | - name: Install common package to each node 6 | dnf: name={{ item }} state=present 7 | with_items: 8 | - "{{ common_dependencies }}" 9 | -------------------------------------------------------------------------------- /ansible/roles/commons/templates/hosts.j2: -------------------------------------------------------------------------------- 1 | 127.0.0.1 localhost 2 | {% for host in groups['all'] %} 3 | {{ hostvars[host]['ansible_' + iface].ipv4.address }} {{ hostvars[host].ansible_fqdn }} 4 | {% endfor %} 5 | -------------------------------------------------------------------------------- /ansible/roles/database/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: restart postgresql 2 | service: name=postgresql state=restarted 3 | -------------------------------------------------------------------------------- /ansible/roles/database/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Install database package 2 | dnf: name={{ item }} state=present 3 | with_items: 4 | - "{{ database_dependencies }}" 5 | register: result 6 | 7 | - name: Initiate postgresql server 8 | command: postgresql-setup initdb 9 | when: result.changed 10 | 11 | - name: Start postgresql service 12 | service: name=postgresql state=started enabled=yes 13 | 14 | - name: Ensure PostgreSQL is listening on all localhost 15 | lineinfile: dest=/var/lib/pgsql/data/postgresql.conf 16 | regexp='^#?listen_addresses\s*=' 17 | line="listen_addresses = '127.0.0.1'" 18 | state=present 19 | notify: restart postgresql 20 | 21 | - lineinfile: dest=/var/lib/pgsql/data/pg_hba.conf 22 | regexp='host\s+all\s+all\s+127.0.0.1/32\s+md5' 23 | line='host all all 127.0.0.1/32 md5' 24 | insertbefore=BOF 25 | notify: restart postgresql 26 | 27 | - name: Ensure user does exist 28 | sudo_user: postgres 29 | sudo: yes 30 | postgresql_user: name={{dbuser}} 31 | 32 | - name: ensure database is created 33 | sudo_user: postgres 34 | sudo: yes 35 | postgresql_db: name={{dbname}} owner={{dbuser}} state=present 36 | 37 | - name: ensure user has access to database 38 | sudo_user: postgres 39 | sudo: yes 40 | postgresql_user: db={{dbname}} name={{dbuser}} password={{dbpassword}} priv=ALL 41 | -------------------------------------------------------------------------------- /ansible/roles/web/tasks/main.yml: -------------------------------------------------------------------------------- 1 | - name: Install web package 2 | dnf: name={{ item }} state=present 3 | with_items: 4 | - "{{ web_dependencies }}" 5 | 6 | - name: Start Apache service 7 | service: name=httpd status=started 8 | -------------------------------------------------------------------------------- /apache/autocloud.conf: -------------------------------------------------------------------------------- 1 | LoadModule wsgi_module modules/mod_wsgi.so 2 | 3 | WSGIPythonEggs /var/cache/autocloud/.python-eggs 4 | WSGIDaemonProcess autocloud user=apache group=apache maximum-requests=50000 display-name=autocloud processes=3 threads=4 inactivity-timeout=300 5 | WSGISocketPrefix run/wsgi 6 | WSGIRestrictStdout Off 7 | WSGIRestrictSignal Off 8 | WSGIPythonOptimize 1 9 | 10 | WSGIScriptAlias / /usr/share/autocloud/autocloud.wsgi 11 | 12 | 13 | Order deny,allow 14 | Allow from all 15 | Require all granted 16 | 17 | -------------------------------------------------------------------------------- /apache/autocloud.wsgi: -------------------------------------------------------------------------------- 1 | import __main__ 2 | __main__.__requires__ = ['SQLAlchemy >= 0.7', 'jinja2 >= 2.4'] 3 | import pkg_resources 4 | 5 | # http://stackoverflow.com/questions/8007176/500-error-without-anything-in-the-apache-logs 6 | import logging 7 | import sys 8 | logging.basicConfig(stream=sys.stderr) 9 | 10 | from autocloud.web.app import app as application 11 | #application.debug = True # Nope. Be careful! 12 | 13 | # vim: set ft=python: 14 | -------------------------------------------------------------------------------- /autocloud.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Generic Autocloud service 3 | After=network.target redis.service fedmsg-hub.service 4 | 5 | [Service] 6 | Environment="HOME=/root" 7 | ExecStart=/usr/sbin/autocloud_job 8 | Type=simple 9 | Restart=on-failure 10 | 11 | [Install] 12 | WantedBy=multi-user.target 13 | -------------------------------------------------------------------------------- /autocloud.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib()")} 2 | %{!?pyver: %global pyver %(%{__python} -c "import sys ; print sys.version[:3]")} 3 | 4 | %global modname autocloud 5 | %global commit0 415c74f8df1cdebbf25034bd0218d34faf466d24 6 | %global gittag0 GIT-TAG 7 | %global shortcommit0 %(c=%{commit0}; echo ${c:0:7}) 8 | 9 | 10 | Name: autocloud 11 | Version: 0.1 12 | Release: 4%{?dist} 13 | Summary: A test framework to test Fedora cloud images 14 | Group: Applications/Internet 15 | License: GPLv3 16 | URL: http://github.com/kushaldas/autocloud 17 | Source0: https://github.com/kushaldas/%{name}/archive/%{commit0}.tar.gz#/%{name}-%{shortcommit0}.tar.gz 18 | 19 | BuildArch: noarch 20 | 21 | BuildRequires: python2-devel 22 | BuildRequires: python-setuptools 23 | BuildRequires: fedmsg 24 | BuildRequires: python-sqlalchemy 25 | BuildRequires: python-redis 26 | BuildRequires: python-retask 27 | BuildRequires: python-sqlalchemy-utils 28 | BuildRequires: python-flask 29 | BuildRequires: systemd 30 | 31 | %description 32 | A test framework which automatically downloads and tests Fedora cloud image 33 | builds from koji. 34 | 35 | %package common 36 | Summary: autocloud common library 37 | 38 | Requires: python-psycopg2 39 | 40 | %description common 41 | This package contains the common libraries required by other autocloud 42 | components. 43 | 44 | %package web 45 | Summary: Autocloud web interface 46 | 47 | Requires: %{name}-common = %{version}-%{release} 48 | Requires: python-flask 49 | Requires: python-flask-restless 50 | Requires: httpd 51 | Requires: mod_wsgi 52 | 53 | %description web 54 | This package is for web interface for autocloud. 55 | 56 | %package backend 57 | Summary: autocloud backend 58 | Requires: %{name}-common = %{version}-%{release} 59 | Requires: fedmsg-hub 60 | Requires: tunir 61 | Requires: python-sqlalchemy 62 | Requires: python-redis 63 | Requires: python-retask 64 | Requires: python-sqlalchemy-utils 65 | Requires: redis 66 | Requires: fedmsg 67 | Requires: koji 68 | 69 | 70 | %description backend 71 | This runs a daemon which keeps listening to fedmsg and dispatches messages 72 | to autocloud service for process images. 73 | 74 | 75 | %prep 76 | %setup -q -n %{name}-%{commit0} 77 | 78 | %build 79 | %{__python} setup.py build 80 | 81 | %install 82 | %{__python} setup.py install -O1 --skip-build \ 83 | --install-data=%{_datadir} --root %{buildroot} 84 | 85 | %{__mkdir_p} %{buildroot}%{_sysconfdir}/fedmsg.d/ 86 | %{__cp} fedmsg.d/autocloud.py %{buildroot}%{_sysconfdir}/fedmsg.d/. 87 | %{__cp} fedmsg.d/endpoints-autocloud.py %{buildroot}%{_sysconfdir}/fedmsg.d/. 88 | 89 | %{__mkdir_p} %{buildroot}%{_datadir}/%{modname}/ 90 | %{__mkdir_p} %{buildroot}/%{_sysconfdir}/httpd/conf.d 91 | install -m 644 apache/%{modname}.wsgi %{buildroot}%{_datadir}/%{modname}/%{modname}.wsgi 92 | install -m 644 apache/%{modname}.conf %{buildroot}%{_sysconfdir}/httpd/conf.d/%{modname}.conf 93 | install -m 644 autocloud_job.py %{buildroot}%{_datadir}/%{modname}/autocloud_job.py 94 | install -m 644 createdb.py %{buildroot}%{_datadir}/%{modname}/createdb.py 95 | mv autocloud/web/static/ %{buildroot}%{_datadir}/%{modname} 96 | 97 | %{__mkdir_p} %{buildroot}%{_sbindir} 98 | install -m 755 autocloud_job %{buildroot}%{_sbindir}/autocloud_job 99 | 100 | mkdir -p %{buildroot}%{_sysconfdir}/%{modname}/ 101 | install -m 644 config/%{modname}.cfg %{buildroot}%{_sysconfdir}/%{modname}/%{modname}.cfg 102 | 103 | %{__mkdir_p} %{buildroot}%{_unitdir} 104 | %{__install} -pm644 autocloud.service \ 105 | %{buildroot}%{_unitdir}/autocloud.service 106 | 107 | rm -rf %{buildroot}%{_datadir}/%{modname}/static/bootstrap 108 | 109 | %files common 110 | %doc README.md 111 | %license LICENSE 112 | %dir %{python_sitelib}/%{modname}/ 113 | %dir %{_sysconfdir}/%{modname}/ 114 | %dir %{_datadir}/%{modname} 115 | %{python_sitelib}/%{modname}/__init__.py* 116 | %{python_sitelib}/%{modname}/models.py* 117 | %{python_sitelib}/%{modname}/utils/* 118 | %{python_sitelib}/%{modname}-%{version}-py%{pyver}.egg-info/ 119 | %config(noreplace) %{_sysconfdir}/fedmsg.d/autocloud.py* 120 | %config(noreplace) %{_sysconfdir}/fedmsg.d/endpoints-autocloud.py* 121 | %config(noreplace) %{_sysconfdir}/%{modname}/%{modname}.cfg 122 | %{_datadir}/%{modname}/createdb.py* 123 | 124 | %files web 125 | %{python_sitelib}/%{modname}/web/* 126 | %{_datadir}/%{modname}/%{modname}.wsgi 127 | %{_datadir}/%{modname}/static/* 128 | %config(noreplace) %{_sysconfdir}/httpd/conf.d/%{modname}.conf 129 | 130 | %files backend 131 | %{python_sitelib}/%{modname}/consumer.py* 132 | %{python_sitelib}/%{modname}/producer.py* 133 | %{_datadir}/%{modname}/autocloud_job.py* 134 | %{_sbindir}/autocloud_job 135 | %{_unitdir}/autocloud.service 136 | 137 | 138 | %changelog 139 | * Wed Sep 23 2015 Praveen Kumar 0.1-4 140 | - Add createdb to proper location 141 | - Remove db creation during rpmbuild 142 | 143 | * Wed Sep 23 2015 Kushal Das - 0.1-3 144 | - Fixes dependencies 145 | 146 | * Tue Sep 22 2015 Ratnadeep Debnath - 0.1-2 147 | - Updating SPEC based on suggestions from review request. 148 | 149 | * Mon Aug 31 2015 Ratnadeep Debnath - 0.1-1 150 | - Initial packaging. 151 | -------------------------------------------------------------------------------- /autocloud.txt: -------------------------------------------------------------------------------- 1 | .. title: Autocloud SOP 2 | .. slug: infra-autocloud 3 | .. date: 2016-02-01 4 | .. taxonomy: Contributors/Infrastructure 5 | 6 | ============= 7 | Autocloud SOP 8 | ============= 9 | 10 | To setup a https://autocloud.fedoraproject.org based on Autocloud project 11 | which will automatically test Fedora Cloud and Atomic and Vagrant images. 12 | 13 | This page describes how to set up the server. 14 | 15 | Contents 16 | ======== 17 | 18 | 1. Contact Information 19 | #. Install Fedora on the servers 20 | #. Setting up the servers 21 | 22 | 23 | Contact Information 24 | =================== 25 | 26 | Owner: 27 | Fedora Infrastructure Team 28 | Contact: 29 | #fedora-admin 30 | Persons: 31 | kushal 32 | Sponsor: 33 | nirik 34 | Location: 35 | phx2 36 | Servers: 37 | autocloud-web01 , autcloud-web02, autocloud-backend-libvirt, autocloud-backend-vbox 38 | Purpose: 39 | To host Autocloud 40 | 41 | Install Fedora on the servers 42 | ============================= 43 | 44 | We are running Fedora on the servers as we have to use Vagrant, and Virtualbox. 45 | 46 | Setting up the servers 47 | ========================= 48 | 49 | Install the autocloud-common package in all systems 50 | ---------------------------------------------------- 51 | 52 | :: 53 | 54 | $ sudo dnf install autolcoud-common 55 | 56 | The above command will install the latest package from the repo. You may want 57 | to install vagrant-libvirt if you will execute libvirt based tests on the 58 | system. 59 | 60 | 61 | Install autocloud-backend package on both the autocloud-back0* systems 62 | ----------------------------------------------------------------------- 63 | 64 | :: 65 | 66 | $ sudo dnf install autocloud-backend 67 | 68 | 69 | Start the redis server in both autocloud-back0* systems 70 | ------------------------------------------------------- 71 | 72 | :: 73 | 74 | $ sudo systemctl start redis 75 | 76 | 77 | Enable ports for tunir in both autocloud-back0* systems 78 | -------------------------------------------------------- 79 | 80 | Autocloud uses tunir to execute the tests on a given image. We will have to do 81 | the follow setup for tunir to execute in a proper way. 82 | 83 | :: 84 | 85 | $ python /usr/share/tunir/createports.py 86 | 87 | Enable kill_vagrant command in cron job 88 | ---------------------------------------- 89 | 90 | Enable a cron job which will run */usr/sbin/kill_vagrant* in every 10 minutes 91 | (or an hour). This is required as many vagrant images do not work, and 92 | boot_timeout never works with vagrant-libvirt. 93 | 94 | .. note:: This is a workaround which is required for now (2015-09-29). But may get removed in future. 95 | 96 | 97 | Configure the database URI in all systems 98 | ------------------------------------------ 99 | 100 | In */etc/autocloud/autocloud.cfg* file please configure the sqlalchemy uri 101 | value. For our work, we are using postgres as database. 102 | 103 | Create the tables in the database 104 | ---------------------------------- 105 | 106 | 107 | .. note:: This has to be done only once from autocloud-back01 system 108 | 109 | 110 | :: 111 | 112 | $ python /usr/share/autocloud/createdb.py 113 | 114 | 115 | Install vagrant-libvirt on autocloud-back01 116 | -------------------------------------------- 117 | 118 | This is the system to handle all libvirt tasks, so we will have to install 119 | vagrant-libvirt on this system. 120 | 121 | :: 122 | 123 | $ sudo dnf install vagrant-libvirt 124 | 125 | 126 | Configure for the vagrant-virtualbox jobs in autocloud-back02 127 | --------------------------------------------------------------- 128 | 129 | In */etc/autocloud/autocloud.cfg* file set *virtualbox* value to True. If you 130 | want to know how to setup virtualbox on the system, please refer to `this guide 131 | `_. 132 | 133 | 134 | Configure the correct tunir job deatils 135 | ---------------------------------------- 136 | 137 | We need the exact commands/job details for tunir. This is a configuration file 138 | so that we can update it whenever required. 139 | 140 | :: 141 | 142 | $ sudo wget https://infrastructure.fedoraproject.org/cgit/ansible.git/tree/roles/autocloud/backend/files/fedora.txt -O /etc/autocloud/fedora.txt 143 | 144 | Start fedmsg-hub service in autocloud-back0* systems 145 | ----------------------------------------------------- 146 | 147 | This service listens for new koji builds, and creates the database entry and 148 | corresponding task in the queue. 149 | 150 | :: 151 | 152 | $ sudo systemctl start fedmsg-hub 153 | 154 | Start autocloud service in autocloud-back0* systems 155 | ---------------------------------------------------- 156 | 157 | This service will listen for new task in the queue, and execute the tasks. 158 | 159 | :: 160 | 161 | $ sudo systemctl start autocloud 162 | 163 | Starting the web dashboard in autocloud-web0* systems 164 | ------------------------------------------------------- 165 | 166 | This is the web dashboard for the Autocloud, we use httpd for the this. 167 | 168 | :: 169 | 170 | $ sudo systemctl start httpd 171 | -------------------------------------------------------------------------------- /autocloud/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import ConfigParser 4 | import os 5 | 6 | PROJECT_ROOT = os.path.abspath(os.path.dirname(__name__)) 7 | 8 | name = '/etc/autocloud/autocloud.cfg' 9 | if not os.path.exists(name): 10 | raise Exception('Please add a proper config file under /etc/autocloud/') 11 | 12 | default = {'host': '127.0.0.1', 'port': 5000} 13 | config = ConfigParser.RawConfigParser(default) 14 | config.read(name) 15 | 16 | KOJI_SERVER_URL = config.get('autocloud', 'koji_server_url') 17 | BASE_KOJI_TASK_URL = config.get('autocloud', 'base_koji_task_url') 18 | 19 | 20 | HOST = config.get('autocloud', 'host') 21 | PORT = config.getint('autocloud', 'port') 22 | DEBUG = config.getboolean('autocloud', 'debug') 23 | 24 | SQLALCHEMY_URI = config.get('sqlalchemy', 'uri') 25 | 26 | VIRTUALBOX = config.getboolean('autocloud', 'virtualbox') 27 | -------------------------------------------------------------------------------- /autocloud/constants.py: -------------------------------------------------------------------------------- 1 | SUCCESS = 'success' 2 | FAILED = 'failed' 3 | ABORTED = 'aborted' 4 | RUNNING = 'running' 5 | -------------------------------------------------------------------------------- /autocloud/consumer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from datetime import datetime 3 | 4 | import requests 5 | import fedmsg.consumers 6 | import fedfind.release 7 | 8 | from sqlalchemy import exc 9 | 10 | import autocloud 11 | 12 | from autocloud.models import init_model, ComposeDetails, ComposeJobDetails 13 | from autocloud.producer import publish_to_fedmsg 14 | from autocloud.utils import is_valid_image, produce_jobs 15 | 16 | import logging 17 | log = logging.getLogger("fedmsg") 18 | 19 | DEBUG = autocloud.DEBUG 20 | 21 | 22 | class AutoCloudConsumer(fedmsg.consumers.FedmsgConsumer): 23 | """ 24 | Fedmsg consumer for Autocloud 25 | """ 26 | 27 | if DEBUG: 28 | topic = [ 29 | 'org.fedoraproject.dev.__main__.pungi.compose.status.change' 30 | ] 31 | 32 | else: 33 | topic = [ 34 | 'org.fedoraproject.prod.pungi.compose.status.change' 35 | ] 36 | 37 | config_key = 'autocloud.consumer.enabled' 38 | 39 | def __init__(self, *args, **kwargs): 40 | self.supported_archs = [arch for arch, _ in ComposeJobDetails.ARCH_TYPES] 41 | 42 | log.info("Autocloud Consumer is ready for action.") 43 | super(AutoCloudConsumer, self).__init__(*args, **kwargs) 44 | 45 | def consume(self, msg): 46 | """ This is called when we receive a message matching the topic. """ 47 | 48 | log.info('Received %r %r' % (msg['topic'], msg['body']['msg_id'])) 49 | 50 | STATUS_F = ('FINISHED_INCOMPLETE', 'FINISHED',) 51 | 52 | images = [] 53 | compose_db_update = False 54 | msg_body = msg['body'] 55 | status = msg_body['msg']['status'] 56 | compose_images_json = None 57 | 58 | # Till F27, both cloud-base and atomic images were available 59 | # under variant CloudImages. With F28 and onward releases, 60 | # cloud-base image compose moved to cloud variant and atomic images 61 | # moved under atomic variant. 62 | prev_rel = ['26', '27'] 63 | if msg_body['msg']['release_version'] in prev_rel: 64 | VARIANTS_F = ('CloudImages',) 65 | else: 66 | VARIANTS_F = ('AtomicHost', 'Cloud') 67 | 68 | if status in STATUS_F: 69 | location = msg_body['msg']['location'] 70 | json_metadata = '{}/metadata/images.json'.format(location) 71 | resp = requests.get(json_metadata) 72 | compose_images_json = getattr(resp, 'json', False) 73 | 74 | if compose_images_json is not None: 75 | compose_images_json = compose_images_json() 76 | compose_images = compose_images_json['payload']['images'] 77 | compose_details = compose_images_json['payload']['compose'] 78 | compose_images = dict((variant, compose_images[variant]) 79 | for variant in VARIANTS_F 80 | if variant in compose_images) 81 | compose_id = compose_details['id'] 82 | rel = fedfind.release.get_release(cid=compose_id) 83 | release = rel.release 84 | compose_details.update({'release': release}) 85 | 86 | compose_images_variants = [variant for variant in VARIANTS_F 87 | if variant in compose_images] 88 | 89 | for variant in compose_images_variants: 90 | compose_image = compose_images[variant] 91 | for arch, payload in compose_image.iteritems(): 92 | 93 | if arch not in self.supported_archs: 94 | continue 95 | 96 | for item in payload: 97 | relative_path = item['path'] 98 | if not is_valid_image(relative_path): 99 | continue 100 | absolute_path = '{}/{}'.format(location, relative_path) 101 | item.update({ 102 | 'compose': compose_details, 103 | 'absolute_path': absolute_path, 104 | }) 105 | images.append(item) 106 | compose_db_update = True 107 | 108 | if compose_db_update: 109 | session = init_model() 110 | compose_date = datetime.strptime(compose_details['date'], '%Y%m%d') 111 | try: 112 | cd = ComposeDetails( 113 | date=compose_date, 114 | compose_id=compose_details['id'], 115 | respin=compose_details['respin'], 116 | type=compose_details['type'], 117 | status=u'q', 118 | location=location, 119 | ) 120 | 121 | session.add(cd) 122 | session.commit() 123 | 124 | compose_details.update({ 125 | 'status': 'queued', 126 | 'compose_job_id': cd.id, 127 | }) 128 | publish_to_fedmsg(topic='compose.queued', 129 | **compose_details) 130 | except exc.IntegrityError: 131 | session.rollback() 132 | cd = session.query(ComposeDetails).filter_by( 133 | compose_id=compose_details['id']).first() 134 | log.info('Compose already exists %s: %s' % ( 135 | compose_details['id'], 136 | cd.id 137 | )) 138 | session.close() 139 | 140 | num_images = len(images) 141 | for pos, image in enumerate(images): 142 | image.update({'pos': (pos+1, num_images)}) 143 | 144 | produce_jobs(images) 145 | -------------------------------------------------------------------------------- /autocloud/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | 5 | from sqlalchemy import Column, Integer, String, DateTime, Text 6 | from sqlalchemy.ext.declarative import declarative_base 7 | from sqlalchemy import create_engine 8 | from sqlalchemy.orm import sessionmaker 9 | from sqlalchemy.orm import scoped_session 10 | from sqlalchemy_utils import ChoiceType 11 | 12 | import autocloud 13 | 14 | Base = declarative_base() 15 | 16 | 17 | class JobDetails(Base): 18 | __tablename__ = 'job_details' 19 | 20 | STATUS_TYPES = ( 21 | ('s', 'Success'), 22 | ('f', 'Failed'), 23 | ('a', 'Aborted'), 24 | ('r', 'Running'), 25 | ('q', 'Queued') 26 | ) 27 | 28 | IMAGE_FAMILY_TYPES = ( 29 | ('b', 'Base'), 30 | ('a', 'Atomic') 31 | ) 32 | 33 | ARCH_TYPES = ( 34 | ('i386', 'i386'), 35 | ('x86_64', 'x86_64') 36 | ) 37 | 38 | id = Column(Integer, primary_key=True) 39 | taskid = Column(String(255), nullable=False) 40 | status = Column(ChoiceType(STATUS_TYPES)) 41 | family = Column(ChoiceType(IMAGE_FAMILY_TYPES)) 42 | arch = Column(ChoiceType(ARCH_TYPES)) 43 | release = Column(String(255)) 44 | output = Column(Text, nullable=False, default='') 45 | created_on = Column(DateTime, default=datetime.datetime.utcnow) 46 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 47 | user = Column(String(255), nullable=False) 48 | 49 | 50 | class ComposeDetails(Base): 51 | __tablename__ = 'compose_details' 52 | 53 | STATUS_TYPES = ( 54 | ('c', 'Complete'), 55 | ('q', 'Queued'), 56 | ('r', 'Running'), 57 | ) 58 | id = Column(Integer, primary_key=True) 59 | date = Column(DateTime, nullable=False) 60 | compose_id = Column(String(255), nullable=False, unique=True) 61 | respin = Column(Integer, nullable=False) 62 | type = Column(String(255), nullable=False) 63 | passed = Column(Integer, nullable=True, default=0) 64 | failed = Column(Integer, nullable=True, default=0) 65 | status = Column(ChoiceType(STATUS_TYPES)) 66 | created_on = Column(DateTime, default=datetime.datetime.utcnow) 67 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 68 | location = Column(String(255), nullable=False) 69 | 70 | 71 | class ComposeJobDetails(Base): 72 | __tablename__ = 'compose_job_details' 73 | 74 | STATUS_TYPES = ( 75 | ('s', 'Success'), 76 | ('f', 'Failed'), 77 | ('a', 'Aborted'), 78 | ('r', 'Running'), 79 | ('q', 'Queued') 80 | ) 81 | 82 | IMAGE_FAMILY_TYPES = ( 83 | ('b', u'Base'), 84 | ('a', u'Atomic') 85 | ) 86 | 87 | ARCH_TYPES = ( 88 | ('i386', 'i386'), 89 | ('x86_64', 'x86_64') 90 | ) 91 | 92 | id = Column(Integer, primary_key=True) 93 | arch = Column(ChoiceType(ARCH_TYPES)) 94 | compose_id = Column(String(255), nullable=False) 95 | created_on = Column(DateTime, default=datetime.datetime.utcnow) 96 | family = Column(ChoiceType(IMAGE_FAMILY_TYPES)) 97 | image_url = Column(String(255), nullable=False) 98 | last_updated = Column(DateTime, default=datetime.datetime.utcnow) 99 | output = Column(Text, nullable=False, default='') 100 | release = Column(String(255)) 101 | status = Column(ChoiceType(STATUS_TYPES)) 102 | subvariant = Column(String(255), nullable=False) 103 | user = Column(String(255), nullable=False) 104 | image_format = Column(String(255), nullable=False) 105 | image_type = Column(String(255), nullable=False) 106 | image_name = Column(String(255), nullable=False) 107 | 108 | 109 | def create_tables(): 110 | # Create an engine that stores data in the local directory 111 | engine = create_engine(autocloud.SQLALCHEMY_URI) 112 | 113 | # Create all tables in the engine. This is equivalent to "Create Table" 114 | # statements in raw SQL. 115 | Base.metadata.create_all(engine) 116 | 117 | 118 | def init_model(): 119 | engine = create_engine(autocloud.SQLALCHEMY_URI) 120 | scopedsession = scoped_session(sessionmaker(bind=engine)) 121 | return scopedsession 122 | -------------------------------------------------------------------------------- /autocloud/producer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import fedmsg 4 | import logging 5 | logging.basicConfig(level=logging.DEBUG) 6 | log = logging.getLogger(__name__) 7 | 8 | 9 | def publish_to_fedmsg(topic, *args, **params): 10 | """ Publish the message to fedmsg with image_url, image_name, status and 11 | build_id 12 | """ 13 | try: 14 | fedmsg.publish(topic=topic, modname="autocloud", msg=params) 15 | except Exception as err: 16 | log.error(err) 17 | -------------------------------------------------------------------------------- /autocloud/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from retask.task import Task 3 | from retask.queue import Queue 4 | 5 | import autocloud 6 | from autocloud.models import init_model, ComposeJobDetails 7 | from autocloud.producer import publish_to_fedmsg 8 | 9 | import datetime 10 | 11 | import logging 12 | log = logging.getLogger("fedmsg") 13 | 14 | 15 | def produce_jobs(infox): 16 | """ Queue the jobs into jobqueue 17 | :args infox: list of dictionaries contains the image url and the buildid 18 | """ 19 | jobqueue = Queue('jobqueue') 20 | jobqueue.connect() 21 | 22 | family_mapping = { 23 | 'Cloud_Base': 'b', 24 | 'Atomic': 'a', 25 | 'AtomicHost': 'a', 26 | } 27 | 28 | session = init_model() 29 | timestamp = datetime.datetime.now() 30 | for info in infox: 31 | image_name = info['path'].split('/')[-1].split(info['arch'])[0] 32 | jd = ComposeJobDetails( 33 | arch=info['arch'], 34 | compose_id=info['compose']['id'], 35 | created_on=timestamp, 36 | family=family_mapping[info['subvariant']], 37 | image_url=info['absolute_path'], 38 | last_updated=timestamp, 39 | release=info['compose']['release'], 40 | status='q', 41 | subvariant=info['subvariant'], 42 | user='admin', 43 | image_format=info['format'], 44 | image_type=info['type'], 45 | image_name=image_name, 46 | ) 47 | session.add(jd) 48 | session.commit() 49 | 50 | job_details_id = jd.id 51 | log.info('Save {jd_id} to database'.format(jd_id=job_details_id)) 52 | 53 | info.update({'job_id': jd.id}) 54 | task = Task(info) 55 | jobqueue.enqueue(task) 56 | log.info('Enqueue {jd_id} to redis'.format(jd_id=job_details_id)) 57 | 58 | publish_to_fedmsg(topic='image.queued', 59 | compose_url=info['absolute_path'], 60 | compose_id=info['compose']['id'], 61 | image_name=image_name, 62 | status='queued', 63 | job_id=info['job_id'], 64 | release=info['compose']['release'], 65 | family=jd.family.value, 66 | type=info['type']) 67 | 68 | session.close() 69 | 70 | 71 | def is_valid_image(image_url): 72 | if autocloud.VIRTUALBOX: 73 | supported_image_ext = ('.vagrant-virtualbox.box',) 74 | else: 75 | supported_image_ext = ('.qcow2', '.vagrant-libvirt.box') 76 | 77 | if image_url.endswith(supported_image_ext): 78 | return True 79 | 80 | return False 81 | 82 | 83 | def get_image_name(image_name): 84 | if 'vagrant' in image_name.lower(): 85 | if autocloud.VIRTUALBOX: 86 | image_name = '{image_name}-Virtualbox'.format( 87 | image_name=image_name) 88 | else: 89 | image_name = '{image_name}-Libvirt'.format(image_name=image_name) 90 | return image_name 91 | -------------------------------------------------------------------------------- /autocloud/web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/__init__.py -------------------------------------------------------------------------------- /autocloud/web/app.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import 4 | 5 | import flask 6 | import flask.ext.restless 7 | 8 | from flask import request, url_for, render_template 9 | from sqlalchemy import desc 10 | from werkzeug.exceptions import abort 11 | 12 | import autocloud 13 | 14 | from autocloud.models import init_model 15 | from autocloud.models import JobDetails, ComposeJobDetails, ComposeDetails 16 | from autocloud.web.pagination import RangeBasedPagination 17 | from autocloud.web.utils import get_object_or_404 18 | 19 | app = flask.Flask(__name__) 20 | session = init_model() 21 | 22 | 23 | class JobDetailsPagination(RangeBasedPagination): 24 | 25 | def get_page_link(self, page_key, limit): 26 | get_params = dict(request.args) 27 | get_params.update({ 28 | 'from': page_key, 'limit': limit}) 29 | return url_for('job_details', **dict( 30 | [(key, value) for key, value in get_params.items()]) 31 | ) 32 | 33 | def order_queryset(self): 34 | if self.direction == 'next': 35 | self.queryset = self.queryset.order_by(desc( 36 | ComposeJobDetails.id)) 37 | else: 38 | self.queryset = self.queryset.order_by(ComposeJobDetails.id) 39 | 40 | def filter_queryset(self): 41 | if self.page_key is None: 42 | return 43 | from_jobdetails = session.query(ComposeJobDetails).get(self.page_key) 44 | if from_jobdetails: 45 | if self.direction == 'prev': 46 | self.queryset = self.queryset.filter( 47 | ComposeJobDetails.id > from_jobdetails.id) 48 | else: 49 | self.queryset = self.queryset.filter( 50 | ComposeJobDetails.id < from_jobdetails.id) 51 | 52 | 53 | class ComposeDetailsPagination(RangeBasedPagination): 54 | def get_page_link(self, page_key, limit): 55 | get_params = dict(request.args) 56 | get_params.update({ 57 | 'from': page_key, 'limit': limit}) 58 | return url_for('compose_details', **dict( 59 | [(key, value) for key, value in get_params.items()]) 60 | ) 61 | 62 | def order_queryset(self): 63 | if self.direction == 'next': 64 | self.queryset = self.queryset.order_by(desc( 65 | ComposeDetails.id)) 66 | else: 67 | self.queryset = self.queryset.order_by(ComposeDetails.id) 68 | 69 | def filter_queryset(self): 70 | if self.page_key is None: 71 | return 72 | from_jobdetails = session.query(ComposeDetails).get(self.page_key) 73 | if from_jobdetails: 74 | if self.direction == 'prev': 75 | self.queryset = self.queryset.filter( 76 | ComposeDetails.id > from_jobdetails.id) 77 | else: 78 | self.queryset = self.queryset.filter( 79 | ComposeDetails.id < from_jobdetails.id) 80 | 81 | 82 | @app.route('/') 83 | def index(): 84 | return flask.render_template('index.html', navbar_fixed=True) 85 | 86 | 87 | @app.route('/compose/') 88 | @app.route('/compose') 89 | def compose_details(): 90 | queryset = session.query(ComposeDetails) 91 | 92 | limit = int(request.args.get('limit', 5)) 93 | compose_details, prev_link, next_link = ComposeDetailsPagination( 94 | queryset, request.args.get('from'), limit, request.path, 95 | request.referrer, dict(request.args)).paginate() 96 | 97 | compose_ids = [item.compose_id for item in compose_details] 98 | compose_locations = dict(session.query( 99 | ComposeDetails.compose_id, 100 | ComposeDetails.location).filter( 101 | ComposeDetails.compose_id.in_(compose_ids)).all()) 102 | 103 | return flask.render_template( 104 | 'compose_details.html', compose_details=compose_details, 105 | prev_link=prev_link, next_link=next_link, 106 | compose_locations=compose_locations, 107 | navbar_fixed=True 108 | ) 109 | 110 | 111 | @app.route('/jobs/') 112 | @app.route('/jobs') 113 | @app.route('/jobs//') 114 | @app.route('/jobs/') 115 | def job_details(compose_pk=None): 116 | queryset = session.query(ComposeJobDetails) 117 | 118 | # Filter the non-supported archs 119 | supported_archs = [arch for arch, _ in ComposeJobDetails.ARCH_TYPES] 120 | queryset = queryset.filter(ComposeJobDetails.arch.in_(supported_archs)) 121 | 122 | if compose_pk is not None: 123 | compose_obj = session.query(ComposeDetails).get(compose_pk) 124 | if compose_obj is None: 125 | abort(404) 126 | 127 | compose_id = compose_obj.compose_id 128 | queryset = queryset.filter_by(compose_id=compose_id) 129 | 130 | # Apply filters 131 | filters = ('family', 'arch', 'status', 'image_type') 132 | selected_filters = {} 133 | for filter in filters: 134 | if request.args.get(filter): 135 | queryset = queryset.filter( 136 | getattr(ComposeJobDetails, filter) == request.args[filter]) 137 | selected_filters[filter] = request.args[filter] 138 | 139 | limit = int(request.args.get('limit', 50)) 140 | job_details, prev_link, next_link = JobDetailsPagination( 141 | queryset, request.args.get('from'), limit, 142 | request.path, 143 | request.referrer, dict(request.args)).paginate() 144 | filter_fields = ( 145 | {'label': 'Family', 'name': 'family', 146 | 'options': ComposeJobDetails.IMAGE_FAMILY_TYPES}, 147 | {'label': 'Architecture', 'name': 'arch', 148 | 'options': ComposeJobDetails.ARCH_TYPES}, 149 | {'label': 'Type', 'name': 'image_type', 150 | 'options': [(value[0], value[0]) 151 | for value in session.query( 152 | ComposeJobDetails.image_type).distinct()]}, 153 | {'label': 'Status', 'name': 'status', 154 | 'options': ComposeJobDetails.STATUS_TYPES} 155 | ) 156 | 157 | compose_ids = [item.compose_id for item in job_details] 158 | compose_locations = dict(session.query( 159 | ComposeDetails.compose_id, 160 | ComposeDetails.location).filter( 161 | ComposeDetails.compose_id.in_(compose_ids)).all()) 162 | 163 | return flask.render_template( 164 | 'job_details.html', job_details=job_details, prev_link=prev_link, 165 | next_link=next_link, filter_fields=filter_fields, 166 | selected_filters=selected_filters, compose_locations=compose_locations, 167 | navbar_fixed=True 168 | ) 169 | 170 | 171 | @app.route('/jobs//output') 172 | def job_output(jobid): 173 | job_detail = get_object_or_404(session, 174 | ComposeJobDetails, 175 | ComposeJobDetails.id == jobid) 176 | 177 | _id = session.query(ComposeDetails.id).filter_by( 178 | compose_id=job_detail.compose_id).all()[0][0] 179 | 180 | compose_locations = dict(session.query( 181 | ComposeDetails.compose_id, 182 | ComposeDetails.location).filter( 183 | ComposeDetails.compose_id.in_(job_detail.compose_id)).all()) 184 | 185 | job_output_lines = [] 186 | if job_detail.output: 187 | job_output_lines = job_detail.output.split('\n') 188 | 189 | return flask.render_template( 190 | 'job_output.html', job_detail=job_detail, 191 | compose_locations=compose_locations, _id=_id, 192 | job_output_lines=job_output_lines, navbar_fixed=False) 193 | 194 | 195 | # Custom Error pages 196 | @app.errorhandler(404) 197 | def page_not_found(e): 198 | return render_template('404.html'), 404 199 | 200 | # API stuff 201 | apimanager = flask.ext.restless.APIManager(app, session=session) 202 | apimanager.create_api(JobDetails, methods=['GET']) 203 | 204 | if __name__ == '__main__': 205 | app.run(host=autocloud.HOST, port=autocloud.PORT, debug=autocloud.DEBUG) 206 | -------------------------------------------------------------------------------- /autocloud/web/pagination.py: -------------------------------------------------------------------------------- 1 | class RangeBasedPagination(object): 2 | """ 3 | Generic range based pagination. 4 | """ 5 | def __init__(self, queryset, page_key, limit, base_url, referrer_url=None, 6 | get_params={}): 7 | """ 8 | Instantiate RangeBasedPagination. 9 | 10 | Args: 11 | queryset: A SQLAlchemy queryset. 12 | page_key: A unique key (string/number) to identify a point 13 | of reference to paginate from. 14 | limit: An integer specifying number of items to render 15 | in a page. A negative value indicates that page 16 | items are to be fetched before the point of reference, 17 | and a positive value means that page items are to be 18 | fetched after the point of reference. 19 | base_url: A string for the base url of the paginated view. 20 | referrer_url: A string for url from which the current page 21 | has been referred. 22 | get_params: A dictionary for request's GET parameters. 23 | """ 24 | self.queryset = queryset 25 | self.page_key = page_key 26 | self.limit = abs(limit) 27 | self.direction = 'next' if limit >= 0 else 'prev' 28 | self.base_url = base_url 29 | self.referrer_url = referrer_url or '' 30 | self.get_params = get_params or {} 31 | 32 | def paginate(self): 33 | """ 34 | Generate data for rendering the current page. 35 | 36 | Returns: 37 | A tuple: (page_items, prev_link, next_link) 38 | where 39 | page_items: A list of items to be rendered in the 40 | current page. 41 | prev_link: A string for the link to previous page, if any, 42 | else, None. 43 | next_link: A string for the link to the next page, if any, 44 | else, None. 45 | """ 46 | self.filter_queryset() 47 | self.order_queryset() 48 | self.limit_queryset() 49 | page_items = self.get_ordered_page_items() 50 | prev_link, next_link = self.get_pagination_links(page_items) 51 | return page_items, prev_link, next_link 52 | 53 | def order_queryset(self): 54 | """ 55 | Order queryset for pagination. 56 | """ 57 | pass 58 | 59 | def filter_queryset(self): 60 | """ 61 | Filter queryset for pagination. 62 | """ 63 | pass 64 | 65 | def limit_queryset(self): 66 | """ 67 | Limit queryset for pagination. 68 | """ 69 | self.queryset = self.queryset.limit(self.limit) 70 | 71 | def get_ordered_page_items(self): 72 | """ 73 | Fetch all items from the resulting queryset, and order 74 | the list of items in the page, if needed. 75 | """ 76 | items = self.queryset.all() 77 | if self.direction == 'prev': 78 | items.reverse() 79 | return items 80 | 81 | def get_pagination_links(self, page_items): 82 | """ 83 | Get pagination links for the current page. 84 | 85 | Args: 86 | page_items: List of items in the current page. 87 | 88 | Returns: 89 | A tuple: (prev_link, next_link) 90 | where: 91 | prev_link: A string for the link to the previous 92 | page, if any, else None. 93 | next_link: A string for the link to the next 94 | page, if any, else None. 95 | """ 96 | page_items_count = len(page_items) 97 | prev_link = next_link = None 98 | if page_items_count == 0 and self.referrer_url.find( 99 | self.base_url) >= 0: 100 | if self.direction == 'next': 101 | prev_link = self.referrer_url 102 | elif self.direction == 'prev': 103 | next_link = self.referrer_url 104 | elif page_items_count <= self.limit: 105 | if self.page_key and (self.direction == 'next' or ( 106 | self.direction == 'prev' and 107 | page_items_count == self.limit)): 108 | 109 | prev_link = self.get_page_link( 110 | page_key=self.get_page_key_from_page_item(page_items[0]), 111 | limit=-1 * self.limit) 112 | if self.direction == 'prev' or ( 113 | self.direction == 'next' and 114 | page_items_count == self.limit): 115 | next_link = self.get_page_link( 116 | page_key=self.get_page_key_from_page_item(page_items[-1]), 117 | limit=self.limit) 118 | return prev_link, next_link 119 | 120 | def get_page_key_from_page_item(self, page_item): 121 | """ 122 | Get page key for an item in a page. 123 | 124 | Args: 125 | page_item: An item from the items list to be 126 | rendered in the current page. 127 | 128 | Returns: 129 | A key (string/number) to be used as a key to 130 | identify a page. 131 | """ 132 | return page_item.id 133 | 134 | def get_page_link(self, page_key, limit): 135 | """ 136 | Get paginated link for a page. 137 | 138 | Args: 139 | page_key: A unique key (string/number) to identify a point 140 | of reference to paginate from. 141 | limit: An integer specifying number of items to render 142 | in a page. A negative value indicates that page 143 | items are to be fetched before the point of reference, 144 | and a positive value means that page items are to be 145 | fetched after the point of reference. 146 | 147 | Returns: 148 | A string for the paginated link. 149 | """ 150 | return '{url}?from={page_key}&limit={limit}'.format( 151 | url=self.base_url, page_key=page_key, limit=limit) 152 | -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap: -------------------------------------------------------------------------------- 1 | bootstrap-3.3.4-fedora -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/css/bootstrap-theme.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.3.4 (http://getbootstrap.com) 3 | * Copyright 2011-2015 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */.btn-danger,.btn-default,.btn-info,.btn-primary,.btn-success,.btn-warning{text-shadow:0 -1px 0 rgba(0,0,0,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 1px rgba(0,0,0,.075)}.btn-danger.active,.btn-danger:active,.btn-default.active,.btn-default:active,.btn-info.active,.btn-info:active,.btn-primary.active,.btn-primary:active,.btn-success.active,.btn-success:active,.btn-warning.active,.btn-warning:active{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,.125);box-shadow:inset 0 3px 5px rgba(0,0,0,.125)}.btn-danger.disabled,.btn-danger[disabled],.btn-default.disabled,.btn-default[disabled],.btn-info.disabled,.btn-info[disabled],.btn-primary.disabled,.btn-primary[disabled],.btn-success.disabled,.btn-success[disabled],.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-danger,fieldset[disabled] .btn-default,fieldset[disabled] .btn-info,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-success,fieldset[disabled] .btn-warning{-webkit-box-shadow:none;box-shadow:none}.btn-danger .badge,.btn-default .badge,.btn-info .badge,.btn-primary .badge,.btn-success .badge,.btn-warning .badge{text-shadow:none}.btn.active,.btn:active{background-image:none}.btn-default{text-shadow:0 1px 0 #fff;background-image:-webkit-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-o-linear-gradient(top,#fff 0,#e0e0e0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#e0e0e0));background-image:linear-gradient(to bottom,#fff 0,#e0e0e0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#dbdbdb;border-color:#ccc}.btn-default:focus,.btn-default:hover{background-color:#e0e0e0;background-position:0 -15px}.btn-default.active,.btn-default:active{background-color:#e0e0e0;border-color:#dbdbdb}.btn-default.disabled,.btn-default.disabled.active,.btn-default.disabled.focus,.btn-default.disabled:active,.btn-default.disabled:focus,.btn-default.disabled:hover,.btn-default[disabled],.btn-default[disabled].active,.btn-default[disabled].focus,.btn-default[disabled]:active,.btn-default[disabled]:focus,.btn-default[disabled]:hover,fieldset[disabled] .btn-default,fieldset[disabled] .btn-default.active,fieldset[disabled] .btn-default.focus,fieldset[disabled] .btn-default:active,fieldset[disabled] .btn-default:focus,fieldset[disabled] .btn-default:hover{background-color:#e0e0e0;background-image:none}.btn-primary{background-image:-webkit-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-o-linear-gradient(top,#428bca 0,#2d6ca2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#2d6ca2));background-image:linear-gradient(to bottom,#428bca 0,#2d6ca2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff2d6ca2', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#2b669a}.btn-primary:focus,.btn-primary:hover{background-color:#2d6ca2;background-position:0 -15px}.btn-primary.active,.btn-primary:active{background-color:#2d6ca2;border-color:#2b669a}.btn-primary.disabled,.btn-primary.disabled.active,.btn-primary.disabled.focus,.btn-primary.disabled:active,.btn-primary.disabled:focus,.btn-primary.disabled:hover,.btn-primary[disabled],.btn-primary[disabled].active,.btn-primary[disabled].focus,.btn-primary[disabled]:active,.btn-primary[disabled]:focus,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary,fieldset[disabled] .btn-primary.active,fieldset[disabled] .btn-primary.focus,fieldset[disabled] .btn-primary:active,fieldset[disabled] .btn-primary:focus,fieldset[disabled] .btn-primary:hover{background-color:#2d6ca2;background-image:none}.btn-success{background-image:-webkit-linear-gradient(top,#79db32 0,#5cb11f 100%);background-image:-o-linear-gradient(top,#79db32 0,#5cb11f 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#79db32),to(#5cb11f));background-image:linear-gradient(to bottom,#79db32 0,#5cb11f 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff79db32', endColorstr='#ff5cb11f', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#58a81e}.btn-success:focus,.btn-success:hover{background-color:#5cb11f;background-position:0 -15px}.btn-success.active,.btn-success:active{background-color:#5cb11f;border-color:#58a81e}.btn-success.disabled,.btn-success.disabled.active,.btn-success.disabled.focus,.btn-success.disabled:active,.btn-success.disabled:focus,.btn-success.disabled:hover,.btn-success[disabled],.btn-success[disabled].active,.btn-success[disabled].focus,.btn-success[disabled]:active,.btn-success[disabled]:focus,.btn-success[disabled]:hover,fieldset[disabled] .btn-success,fieldset[disabled] .btn-success.active,fieldset[disabled] .btn-success.focus,fieldset[disabled] .btn-success:active,fieldset[disabled] .btn-success:focus,fieldset[disabled] .btn-success:hover{background-color:#5cb11f;background-image:none}.btn-info{background-image:-webkit-linear-gradient(top,#a07cbc 0,#8255a6 100%);background-image:-o-linear-gradient(top,#a07cbc 0,#8255a6 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#a07cbc),to(#8255a6));background-image:linear-gradient(to bottom,#a07cbc 0,#8255a6 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa07cbc', endColorstr='#ff8255a6', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#7d519f}.btn-info:focus,.btn-info:hover{background-color:#8255a6;background-position:0 -15px}.btn-info.active,.btn-info:active{background-color:#8255a6;border-color:#7d519f}.btn-info.disabled,.btn-info.disabled.active,.btn-info.disabled.focus,.btn-info.disabled:active,.btn-info.disabled:focus,.btn-info.disabled:hover,.btn-info[disabled],.btn-info[disabled].active,.btn-info[disabled].focus,.btn-info[disabled]:active,.btn-info[disabled]:focus,.btn-info[disabled]:hover,fieldset[disabled] .btn-info,fieldset[disabled] .btn-info.active,fieldset[disabled] .btn-info.focus,fieldset[disabled] .btn-info:active,fieldset[disabled] .btn-info:focus,fieldset[disabled] .btn-info:hover{background-color:#8255a6;background-image:none}.btn-warning{background-image:-webkit-linear-gradient(top,#e59728 0,#b97616 100%);background-image:-o-linear-gradient(top,#e59728 0,#b97616 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e59728),to(#b97616));background-image:linear-gradient(to bottom,#e59728 0,#b97616 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe59728', endColorstr='#ffb97616', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#b07015}.btn-warning:focus,.btn-warning:hover{background-color:#b97616;background-position:0 -15px}.btn-warning.active,.btn-warning:active{background-color:#b97616;border-color:#b07015}.btn-warning.disabled,.btn-warning.disabled.active,.btn-warning.disabled.focus,.btn-warning.disabled:active,.btn-warning.disabled:focus,.btn-warning.disabled:hover,.btn-warning[disabled],.btn-warning[disabled].active,.btn-warning[disabled].focus,.btn-warning[disabled]:active,.btn-warning[disabled]:focus,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning,fieldset[disabled] .btn-warning.active,fieldset[disabled] .btn-warning.focus,fieldset[disabled] .btn-warning:active,fieldset[disabled] .btn-warning:focus,fieldset[disabled] .btn-warning:hover{background-color:#b97616;background-image:none}.btn-danger{background-image:-webkit-linear-gradient(top,#db3279 0,#b11f5c 100%);background-image:-o-linear-gradient(top,#db3279 0,#b11f5c 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#db3279),to(#b11f5c));background-image:linear-gradient(to bottom,#db3279 0,#b11f5c 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdb3279', endColorstr='#ffb11f5c', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-color:#a81e58}.btn-danger:focus,.btn-danger:hover{background-color:#b11f5c;background-position:0 -15px}.btn-danger.active,.btn-danger:active{background-color:#b11f5c;border-color:#a81e58}.btn-danger.disabled,.btn-danger.disabled.active,.btn-danger.disabled.focus,.btn-danger.disabled:active,.btn-danger.disabled:focus,.btn-danger.disabled:hover,.btn-danger[disabled],.btn-danger[disabled].active,.btn-danger[disabled].focus,.btn-danger[disabled]:active,.btn-danger[disabled]:focus,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger,fieldset[disabled] .btn-danger.active,fieldset[disabled] .btn-danger.focus,fieldset[disabled] .btn-danger:active,fieldset[disabled] .btn-danger:focus,fieldset[disabled] .btn-danger:hover{background-color:#b11f5c;background-image:none}.img-thumbnail,.thumbnail{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.dropdown-menu>li>a:focus,.dropdown-menu>li>a:hover{background-color:#e8e8e8;background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.dropdown-menu>.active>a,.dropdown-menu>.active>a:focus,.dropdown-menu>.active>a:hover{background-color:#357ebd;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.navbar-default{background-image:-webkit-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-o-linear-gradient(top,#fff 0,#f8f8f8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#fff),to(#f8f8f8));background-image:linear-gradient(to bottom,#fff 0,#f8f8f8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075);box-shadow:inset 0 1px 0 rgba(255,255,255,.15),0 1px 5px rgba(0,0,0,.075)}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-o-linear-gradient(top,#dbdbdb 0,#e2e2e2 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dbdbdb),to(#e2e2e2));background-image:linear-gradient(to bottom,#dbdbdb 0,#e2e2e2 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.075);box-shadow:inset 0 3px 9px rgba(0,0,0,.075)}.navbar-brand,.navbar-nav>li>a{text-shadow:0 1px 0 rgba(255,255,255,.25)}.navbar-inverse{background-image:-webkit-linear-gradient(top,#365698 0,#294172 100%);background-image:-o-linear-gradient(top,#365698 0,#294172 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#365698),to(#294172));background-image:linear-gradient(to bottom,#365698 0,#294172 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff365698', endColorstr='#ff294172', GradientType=0);filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);background-repeat:repeat-x}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.open>a{background-image:-webkit-linear-gradient(top,#1c2c4c 0,#1f3156 100%);background-image:-o-linear-gradient(top,#1c2c4c 0,#1f3156 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#1c2c4c),to(#1f3156));background-image:linear-gradient(to bottom,#1c2c4c 0,#1f3156 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff1c2c4c', endColorstr='#ff1f3156', GradientType=0);background-repeat:repeat-x;-webkit-box-shadow:inset 0 3px 9px rgba(0,0,0,.25);box-shadow:inset 0 3px 9px rgba(0,0,0,.25)}.navbar-inverse .navbar-brand,.navbar-inverse .navbar-nav>li>a{text-shadow:0 -1px 0 rgba(0,0,0,.25)}.navbar-fixed-bottom,.navbar-fixed-top,.navbar-static-top{border-radius:0}@media (max-width:767px){.navbar .navbar-nav .open .dropdown-menu>.active>a,.navbar .navbar-nav .open .dropdown-menu>.active>a:focus,.navbar .navbar-nav .open .dropdown-menu>.active>a:hover{color:#fff;background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}}.alert{text-shadow:0 1px 0 rgba(255,255,255,.2);-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05);box-shadow:inset 0 1px 0 rgba(255,255,255,.25),0 1px 2px rgba(0,0,0,.05)}.alert-success{background-image:-webkit-linear-gradient(top,#dcf6ca 0,#c7f0a9 100%);background-image:-o-linear-gradient(top,#dcf6ca 0,#c7f0a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dcf6ca),to(#c7f0a9));background-image:linear-gradient(to bottom,#dcf6ca 0,#c7f0a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdcf6ca', endColorstr='#ffc7f0a9', GradientType=0);background-repeat:repeat-x;border-color:#b2ea89}.alert-info{background-image:-webkit-linear-gradient(top,#f6f2f8 0,#e3d9eb 100%);background-image:-o-linear-gradient(top,#f6f2f8 0,#e3d9eb 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f2f8),to(#e3d9eb));background-image:linear-gradient(to bottom,#f6f2f8 0,#e3d9eb 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f2f8', endColorstr='#ffe3d9eb', GradientType=0);background-repeat:repeat-x;border-color:#d1bfdf}.alert-warning{background-image:-webkit-linear-gradient(top,#f8e4c7 0,#f4d4a5 100%);background-image:-o-linear-gradient(top,#f8e4c7 0,#f4d4a5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f8e4c7),to(#f4d4a5));background-image:linear-gradient(to bottom,#f8e4c7 0,#f4d4a5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8e4c7', endColorstr='#fff4d4a5', GradientType=0);background-repeat:repeat-x;border-color:#f0c383}.alert-danger{background-image:-webkit-linear-gradient(top,#f6cadc 0,#f0a9c7 100%);background-image:-o-linear-gradient(top,#f6cadc 0,#f0a9c7 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6cadc),to(#f0a9c7));background-image:linear-gradient(to bottom,#f6cadc 0,#f0a9c7 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6cadc', endColorstr='#fff0a9c7', GradientType=0);background-repeat:repeat-x;border-color:#ea89b2}.progress{background-image:-webkit-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#ebebeb 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#ebebeb),to(#f5f5f5));background-image:linear-gradient(to bottom,#ebebeb 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x}.progress-bar{background-image:-webkit-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-o-linear-gradient(top,#428bca 0,#3071a9 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3071a9));background-image:linear-gradient(to bottom,#428bca 0,#3071a9 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3071a9', GradientType=0);background-repeat:repeat-x}.progress-bar-success{background-image:-webkit-linear-gradient(top,#79db32 0,#61b921 100%);background-image:-o-linear-gradient(top,#79db32 0,#61b921 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#79db32),to(#61b921));background-image:linear-gradient(to bottom,#79db32 0,#61b921 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff79db32', endColorstr='#ff61b921', GradientType=0);background-repeat:repeat-x}.progress-bar-info{background-image:-webkit-linear-gradient(top,#a07cbc 0,#885aab 100%);background-image:-o-linear-gradient(top,#a07cbc 0,#885aab 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#a07cbc),to(#885aab));background-image:linear-gradient(to bottom,#a07cbc 0,#885aab 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffa07cbc', endColorstr='#ff885aab', GradientType=0);background-repeat:repeat-x}.progress-bar-warning{background-image:-webkit-linear-gradient(top,#e59728 0,#c27c18 100%);background-image:-o-linear-gradient(top,#e59728 0,#c27c18 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e59728),to(#c27c18));background-image:linear-gradient(to bottom,#e59728 0,#c27c18 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe59728', endColorstr='#ffc27c18', GradientType=0);background-repeat:repeat-x}.progress-bar-danger{background-image:-webkit-linear-gradient(top,#db3279 0,#b92161 100%);background-image:-o-linear-gradient(top,#db3279 0,#b92161 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#db3279),to(#b92161));background-image:linear-gradient(to bottom,#db3279 0,#b92161 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdb3279', endColorstr='#ffb92161', GradientType=0);background-repeat:repeat-x}.progress-bar-striped{background-image:-webkit-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:-o-linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent)}.list-group{border-radius:4px;-webkit-box-shadow:0 1px 2px rgba(0,0,0,.075);box-shadow:0 1px 2px rgba(0,0,0,.075)}.list-group-item.active,.list-group-item.active:focus,.list-group-item.active:hover{text-shadow:0 -1px 0 #3071a9;background-image:-webkit-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-o-linear-gradient(top,#428bca 0,#3278b3 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#3278b3));background-image:linear-gradient(to bottom,#428bca 0,#3278b3 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff3278b3', GradientType=0);background-repeat:repeat-x;border-color:#3278b3}.list-group-item.active .badge,.list-group-item.active:focus .badge,.list-group-item.active:hover .badge{text-shadow:none}.panel{-webkit-box-shadow:0 1px 2px rgba(0,0,0,.05);box-shadow:0 1px 2px rgba(0,0,0,.05)}.panel-default>.panel-heading{background-image:-webkit-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-o-linear-gradient(top,#f5f5f5 0,#e8e8e8 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f5f5f5),to(#e8e8e8));background-image:linear-gradient(to bottom,#f5f5f5 0,#e8e8e8 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);background-repeat:repeat-x}.panel-primary>.panel-heading{background-image:-webkit-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-o-linear-gradient(top,#428bca 0,#357ebd 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#428bca),to(#357ebd));background-image:linear-gradient(to bottom,#428bca 0,#357ebd 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff428bca', endColorstr='#ff357ebd', GradientType=0);background-repeat:repeat-x}.panel-success>.panel-heading{background-image:-webkit-linear-gradient(top,#dcf6ca 0,#cef2b4 100%);background-image:-o-linear-gradient(top,#dcf6ca 0,#cef2b4 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#dcf6ca),to(#cef2b4));background-image:linear-gradient(to bottom,#dcf6ca 0,#cef2b4 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdcf6ca', endColorstr='#ffcef2b4', GradientType=0);background-repeat:repeat-x}.panel-info>.panel-heading{background-image:-webkit-linear-gradient(top,#f6f2f8 0,#e9e1f0 100%);background-image:-o-linear-gradient(top,#f6f2f8 0,#e9e1f0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6f2f8),to(#e9e1f0));background-image:linear-gradient(to bottom,#f6f2f8 0,#e9e1f0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6f2f8', endColorstr='#ffe9e1f0', GradientType=0);background-repeat:repeat-x}.panel-warning>.panel-heading{background-image:-webkit-linear-gradient(top,#f8e4c7 0,#f6d9b0 100%);background-image:-o-linear-gradient(top,#f8e4c7 0,#f6d9b0 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f8e4c7),to(#f6d9b0));background-image:linear-gradient(to bottom,#f8e4c7 0,#f6d9b0 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff8e4c7', endColorstr='#fff6d9b0', GradientType=0);background-repeat:repeat-x}.panel-danger>.panel-heading{background-image:-webkit-linear-gradient(top,#f6cadc 0,#f2b4ce 100%);background-image:-o-linear-gradient(top,#f6cadc 0,#f2b4ce 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#f6cadc),to(#f2b4ce));background-image:linear-gradient(to bottom,#f6cadc 0,#f2b4ce 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff6cadc', endColorstr='#fff2b4ce', GradientType=0);background-repeat:repeat-x}.well{background-image:-webkit-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-o-linear-gradient(top,#e8e8e8 0,#f5f5f5 100%);background-image:-webkit-gradient(linear,left top,left bottom,from(#e8e8e8),to(#f5f5f5));background-image:linear-gradient(to bottom,#e8e8e8 0,#f5f5f5 100%);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);background-repeat:repeat-x;border-color:#dcdcdc;-webkit-box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1);box-shadow:inset 0 1px 3px rgba(0,0,0,.05),0 1px 0 rgba(255,255,255,.1)} -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/bootstrap-3.3.4-fedora/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /autocloud/web/static/bootstrap-3.3.4-fedora/js/npm.js: -------------------------------------------------------------------------------- 1 | // This file is autogenerated via the `commonjs` Grunt task. You can require() this file in a CommonJS environment. 2 | require('../../js/transition.js') 3 | require('../../js/alert.js') 4 | require('../../js/button.js') 5 | require('../../js/carousel.js') 6 | require('../../js/collapse.js') 7 | require('../../js/dropdown.js') 8 | require('../../js/modal.js') 9 | require('../../js/tooltip.js') 10 | require('../../js/popover.js') 11 | require('../../js/scrollspy.js') 12 | require('../../js/tab.js') 13 | require('../../js/affix.js') -------------------------------------------------------------------------------- /autocloud/web/static/css/avatars.css: -------------------------------------------------------------------------------- 1 | .centered { 2 | display: block; 3 | margin: 0 auto; 4 | } 5 | -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Bold-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Bold-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Bold-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-BoldOblique-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Oblique-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Regular-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Regular-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | This is a custom SVG webfont generated by Font Squirrel. 6 | Designer : Dave Crossland 7 | Foundry URL : http://abattis.org 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Regular-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Cantarell-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Cantarell-Regular-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Bold-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Regular-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.eot -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.ttf -------------------------------------------------------------------------------- /autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/css/fonts/Comfortaa_Thin-webfont.woff -------------------------------------------------------------------------------- /autocloud/web/static/css/footer.css: -------------------------------------------------------------------------------- 1 | /* Sticky footer styles */ 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | .max-width { 9 | min-width: 100%; 10 | } 11 | 12 | #wrap { 13 | min-height: 100%; 14 | height: auto; 15 | margin: 0 auto -60px; 16 | padding: 0 0 60px; 17 | } 18 | 19 | #footer { 20 | height: 80px; 21 | background-color: #f5f5f5; 22 | border-top: 1px dashed black; 23 | text-align: center; 24 | } 25 | 26 | #footer { 27 | padding: 10px; 28 | } 29 | -------------------------------------------------------------------------------- /autocloud/web/static/css/navbar.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 35px; 3 | padding-bottom: 20px; 4 | } 5 | 6 | .navbar { 7 | margin-bottom: 20px; 8 | } 9 | 10 | .carousel { 11 | width: 100%; 12 | background: #4c4c4c; 13 | } 14 | 15 | .carousel-inner { 16 | text-align: center; 17 | } 18 | 19 | .carousel .item > img { 20 | display: inline-block; 21 | } 22 | 23 | .carousel-caption { 24 | background-color: rgba(0,0,0,0.8); 25 | -moz-border-radius: 15px; 26 | border-radius: 15px; 27 | padding-left: 30px; 28 | padding-right: 30px; 29 | margin: auto; 30 | max-width: 600px; 31 | } 32 | 33 | .carousel-caption > h3 { 34 | margin-top: -5px; 35 | } 36 | 37 | .masthead { 38 | margin-top: 140px; 39 | font-size: 120%; 40 | } 41 | .masthead h1 { 42 | font-size: 64pt; 43 | } 44 | .masthead .lead { 45 | font-size: 28pt; 46 | } 47 | .example-message strong { 48 | font-size: 9pt; 49 | } 50 | .example-message img { 51 | width: 24px; 52 | height: 24px; 53 | } 54 | -------------------------------------------------------------------------------- /autocloud/web/static/css/style.css: -------------------------------------------------------------------------------- 1 | .i-b { 2 | display: inline-block !important; 3 | } 4 | 5 | .m-t-5 { 6 | margin-top: 5px; 7 | } 8 | 9 | .m-t-10 { 10 | margin-top: 10px; 11 | } 12 | 13 | .m-l-10 { 14 | margin-left: 10px; 15 | } 16 | 17 | pre { 18 | animation : none; 19 | animation-delay : 0; 20 | animation-direction : normal; 21 | animation-duration : 0; 22 | animation-fill-mode : none; 23 | animation-iteration-count : 1; 24 | animation-name : none; 25 | animation-play-state : running; 26 | animation-timing-function : ease; 27 | backface-visibility : visible; 28 | background : 0; 29 | background-attachment : scroll; 30 | background-clip : border-box; 31 | background-color : transparent; 32 | background-image : none; 33 | background-origin : padding-box; 34 | background-position : 0 0; 35 | background-position-x : 0; 36 | background-position-y : 0; 37 | background-repeat : repeat; 38 | background-size : auto auto; 39 | border : 0; 40 | border-style : none; 41 | border-width : medium; 42 | border-color : inherit; 43 | border-bottom : 0; 44 | border-bottom-color : inherit; 45 | border-bottom-left-radius : 0; 46 | border-bottom-right-radius : 0; 47 | border-bottom-style : none; 48 | border-bottom-width : medium; 49 | border-collapse : separate; 50 | border-image : none; 51 | border-left : 0; 52 | border-left-color : inherit; 53 | border-left-style : none; 54 | border-left-width : medium; 55 | border-radius : 0; 56 | border-right : 0; 57 | border-right-color : inherit; 58 | border-right-style : none; 59 | border-right-width : medium; 60 | border-spacing : 0; 61 | border-top : 0; 62 | border-top-color : inherit; 63 | border-top-left-radius : 0; 64 | border-top-right-radius : 0; 65 | border-top-style : none; 66 | border-top-width : medium; 67 | bottom : auto; 68 | box-shadow : none; 69 | box-sizing : content-box; 70 | caption-side : top; 71 | clear : none; 72 | clip : auto; 73 | color : inherit; 74 | columns : auto; 75 | column-count : auto; 76 | column-fill : balance; 77 | column-gap : normal; 78 | column-rule : medium none currentColor; 79 | column-rule-color : currentColor; 80 | column-rule-style : none; 81 | column-rule-width : none; 82 | column-span : 1; 83 | column-width : auto; 84 | content : normal; 85 | counter-increment : none; 86 | counter-reset : none; 87 | cursor : auto; 88 | direction : ltr; 89 | display : inline; 90 | empty-cells : show; 91 | float : none; 92 | font : normal; 93 | font-family : inherit; 94 | font-size : medium; 95 | font-style : normal; 96 | font-variant : normal; 97 | font-weight : normal; 98 | height : auto; 99 | hyphens : none; 100 | left : auto; 101 | letter-spacing : normal; 102 | line-height : normal; 103 | list-style : none; 104 | list-style-image : none; 105 | list-style-position : outside; 106 | list-style-type : disc; 107 | margin : 0; 108 | margin-bottom : 0; 109 | margin-left : 0; 110 | margin-right : 0; 111 | margin-top : 0; 112 | max-height : none; 113 | max-width : none; 114 | min-height : 0; 115 | min-width : 0; 116 | opacity : 1; 117 | orphans : 0; 118 | outline : 0; 119 | outline-color : invert; 120 | outline-style : none; 121 | outline-width : medium; 122 | overflow : visible; 123 | overflow-x : visible; 124 | overflow-y : visible; 125 | padding : 0; 126 | padding-bottom : 0; 127 | padding-left : 0; 128 | padding-right : 0; 129 | padding-top : 0; 130 | page-break-after : auto; 131 | page-break-before : auto; 132 | page-break-inside : auto; 133 | perspective : none; 134 | perspective-origin : 50% 50%; 135 | position : static; 136 | /* May need to alter quotes for different locales (e.g fr) */ 137 | quotes : '\201C' '\201D' '\2018' '\2019'; 138 | right : auto; 139 | tab-size : 8; 140 | table-layout : auto; 141 | text-align : inherit; 142 | text-align-last : auto; 143 | text-decoration : none; 144 | text-decoration-color : inherit; 145 | text-decoration-line : none; 146 | text-decoration-style : solid; 147 | text-indent : 0; 148 | text-shadow : none; 149 | text-transform : none; 150 | top : auto; 151 | transform : none; 152 | transform-style : flat; 153 | transition : none; 154 | transition-delay : 0s; 155 | transition-duration : 0s; 156 | transition-property : none; 157 | transition-timing-function : ease; 158 | unicode-bidi : normal; 159 | vertical-align : baseline; 160 | visibility : visible; 161 | white-space : normal; 162 | widows : 0; 163 | width : auto; 164 | word-spacing : normal; 165 | z-index : auto; 166 | } 167 | 168 | .output_table tr .lineno a::before { 169 | content: attr(data-lineno); 170 | color: #603e7a !important; 171 | padding-left: 3px; 172 | padding-right: 6px; 173 | cursor: pointer; 174 | } 175 | 176 | .highlight { 177 | background: #F8E4C7; 178 | width: inherit; 179 | overflow-x: scroll; 180 | border-radius:3px; 181 | } 182 | 183 | .output_table td.lineno { 184 | background-color: #FFCF89; 185 | } 186 | 187 | .output_table td.lineout { 188 | padding-left: 3px; 189 | } 190 | 191 | .navbar-fixed-top-abs { 192 | position: absolute!important; 193 | } 194 | -------------------------------------------------------------------------------- /autocloud/web/static/css/text.css: -------------------------------------------------------------------------------- 1 | /* new bulletproof @font-face syntax from http://www.fontspring.com/blog/the-new-bulletproof-font-face-syntax */ 2 | @font-face { 3 | font-family: 'ComfortaaThin'; 4 | src: url('fonts/Comfortaa_Thin-webfont.eot?#iefix') format('embedded-opentype'), 5 | url('fonts/Comfortaa_Thin-webfont.ttf') format('truetype'), 6 | url('fonts/Comfortaa_Thin-webfont.svg#webfontReo2lGxG') format('svg'); 7 | font-weight: normal; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: 'ComfortaaRegular'; 13 | src: url('fonts/Comfortaa_Regular-webfont.eot?#iefix') format('embedded-opentype'), 14 | url('fonts/Comfortaa_Regular-webfont.ttf') format('truetype'), 15 | url('fonts/Comfortaa_Regular-webfont.svg#webfontxbL3cos8') format('svg'); 16 | font-weight: normal; 17 | font-style: normal; 18 | } 19 | 20 | @font-face { 21 | font-family: 'ComfortaaBold'; 22 | src: url('fonts/Comfortaa_Bold-webfont.eot?#iefix') format('embedded-opentype'), 23 | url('fonts/Comfortaa_Bold-webfont.ttf') format('truetype'), 24 | url('fonts/Comfortaa_Bold-webfont.svg#webfontjkcnhWWT') format('svg'); 25 | font-weight: normal; 26 | font-style: normal; 27 | } 28 | 29 | @font-face { 30 | font-family: 'CantarellRegular'; 31 | src: url('fonts/Cantarell-Regular-webfont.eot?#iefix') format('embedded-opentype'), 32 | url('fonts/Cantarell-Regular-webfont.ttf') format('truetype'), 33 | url('fonts/Cantarell-Regular-webfont.svg#webfontPQ4tPnyo') format('svg'); 34 | font-weight: normal; 35 | font-style: normal; 36 | } 37 | 38 | @font-face { 39 | font-family: 'CantarellBold'; 40 | src: url('fonts/Cantarell-Bold-webfont.eot?#iefix') format('embedded-opentype'), 41 | url('fonts/Cantarell-Bold-webfont.ttf') format('truetype'), 42 | url('fonts/Cantarell-Bold-webfont.svg#webfont3gCLDhwY') format('svg'); 43 | font-weight: normal; 44 | font-style: normal; 45 | } 46 | 47 | @font-face { 48 | font-family: 'CantarellBoldOblique'; 49 | src: url('fonts/Cantarell-BoldOblique-webfont.eot?#iefix') format('embedded-opentype'), 50 | url('fonts/Cantarell-BoldOblique-webfont.ttf') format('truetype'), 51 | url('fonts/Cantarell-BoldOblique-webfont.svg#webfont3gCLDhwY') format('svg'); 52 | font-weight: normal; 53 | font-style: normal; 54 | } 55 | 56 | @font-face { 57 | font-family: 'CantarellOblique'; 58 | src: url('fonts/Cantarell-Oblique-webfont.eot?#iefix') format('embedded-opentype'), 59 | url('fonts/Cantarell-Oblique-webfont.ttf') format('truetype'), 60 | url('fonts/Cantarell-Oblique-webfont.svg#webfont3gCLDhwY') format('svg'); 61 | font-weight: normal; 62 | font-style: normal; 63 | } 64 | -------------------------------------------------------------------------------- /autocloud/web/static/ico/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/ico/favicon.ico -------------------------------------------------------------------------------- /autocloud/web/static/img/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/static/img/spinner.gif -------------------------------------------------------------------------------- /autocloud/web/static/js/site.js: -------------------------------------------------------------------------------- 1 | var examples_loading_message = "Searching for example messages that match this filter "; 2 | 3 | var load_examples = function(page, endtime) { 4 | // First, destroy the more button if there is one. 5 | $('#more-button').remove(); 6 | 7 | // Then, get the next page of data from the API (relative url).. 8 | $.ajax("ex/" + page + "/" + endtime, { 9 | success: examples_success, 10 | error: examples_error, 11 | }); 12 | } 13 | 14 | var examples_success = function(data, status, jqXHR) { 15 | var stopping = false; 16 | if (data.results.length == 0) { 17 | load_examples(data.next_page, data.endtime); 18 | } else { 19 | $('#examples-container .lead').html( 20 | "The following messages would have matched this filter"); 21 | stopping = true; 22 | } 23 | 24 | // Put our results on the page. 25 | $.each(data.results, function(i, meta) { 26 | var content = "
  • "; 27 | 28 | if (meta.icon2 != "" && meta.icon2 != null) { 29 | content = content + ""; 30 | } 31 | if (meta.icon != "" && meta.icon != null) { 32 | content = content + ""; 33 | } 34 | 35 | if (meta.link != "" && meta.link != null) { 36 | content = content + 37 | " " + meta.time + " "; 38 | } else { 39 | content = content + " " + meta.time + " "; 40 | } 41 | 42 | content = content + "" + meta.subtitle + " "; 43 | 44 | content = content + '
  • ' 45 | 46 | $('#examples-container .list-group').append(content) 47 | $('#examples-container .list-group li:last-child').hide(); 48 | $('#examples-container .list-group li:last-child').slideDown('slow'); 49 | }); 50 | 51 | // Tack a MOAR button on the end 52 | if (stopping) { 53 | var button = '
    ' + 54 | '' + 59 | '
    '; 60 | $('#examples-container .list-group').append(button) 61 | $('#examples-container .list-group div:last-child').hide(); 62 | $('#examples-container .list-group div:last-child').slideDown('slow'); 63 | } 64 | } 65 | 66 | var examples_error = function(jqXHR, status, errorThrown) { 67 | data = jqXHR.responseJSON; 68 | if (data === undefined) { 69 | $('#examples-container .lead').html("Unknown error getting examples."); 70 | } else { 71 | $('#examples-container .lead').html(data.reason); 72 | } 73 | if (data.furthermore != undefined) { 74 | $('#examples-container').append('

    ' + data.furthermore + '

    '); 75 | } 76 | $('#examples-container p').addClass('text-danger'); 77 | } 78 | 79 | $(document).ready(function() { 80 | // Kick it off, but only if we're on the right page and there are rules. 81 | var rules = $("#rules"); 82 | var container = $('#examples-container'); 83 | if (container.length > 0 && rules.children().length > 0) { 84 | $('#examples-container .lead').html(examples_loading_message); 85 | var now = Math.floor(new Date().getTime() / 1000.0); 86 | load_examples(1, now); 87 | } 88 | 89 | function autocloud_message() { 90 | var message = " _ _ _ \n\ 91 | __ _ _ _| |_ ___ ___| | ___ _ _ __| |\n\ 92 | / _` | | | | __/ _ \\ / __| |/ _ \\| | | |/ _` |\n\ 93 | | (_| | |_| | || (_) | (__| | (_) | |_| | (_| |\n\ 94 | \\__,_|\\__,_|\\__\\___/ \\___|_|\\___/ \\__,_|\\__,_|\n\ 95 | \n\ 96 | =======================================================\n\ 97 | Looking forward to contribute to the project?\n\ 98 | Head over to https://github.com/kushaldas/autocloud \n\ 99 | =======================================================\n\ 100 | "; 101 | console.log(message); 102 | } 103 | setTimeout(autocloud_message, 1000); 104 | }); 105 | -------------------------------------------------------------------------------- /autocloud/web/templates/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Autocloud 12 | 13 | 14 | 15 | 16 |
    17 |                                                                                      _  _    ___  _  _   
    18 |                                                                                     | || |  / _ \| || |  
    19 |                                                                                     | || |_| | | | || |_ 
    20 |                                                                                     |__  __| |_| |__   _|
    21 |                                                                                       |_|   \___/   |_|  
    22 |                                                                                               
    23 | 
    24 | 
    25 | 
    26 |                                                                                              _\|/_
    27 |                                                                                              (o o)
    28 |                              +------------------------------------------------------------oOO-{_}-OOo---------------------------------------------------------+
    29 |                              | _   _                 _                                 _                                               _     _                |
    30 |                              || | | | ___  _   _ ___| |_ ___  _ __     __      _____  | |__   __ ___   _____    __ _   _ __  _ __ ___ | |__ | | ___ _ __ ___  |
    31 |                              || |_| |/ _ \| | | / __| __/ _ \| '_ \    \ \ /\ / / _ \ | '_ \ / _` \ \ / / _ \  / _` | | '_ \| '__/ _ \| '_ \| |/ _ \ '_ ` _ \ |
    32 |                              ||  _  | (_) | |_| \__ \ || (_) | | | |_   \ V  V /  __/ | | | | (_| |\ V /  __/ | (_| | | |_) | | | (_) | |_) | |  __/ | | | | ||
    33 |                              ||_| |_|\___/ \__,_|___/\__\___/|_| |_( )   \_/\_/ \___| |_| |_|\__,_| \_/ \___|  \__,_| | .__/|_|  \___/|_.__/|_|\___|_| |_| |_||
    34 |                              |                                     |/                                                 |_|                                     |
    35 |                              |                                                                                                                                |
    36 |                              +-------------------------------------------------------------------------------------------------------------------------------*/
    37 |         
    38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /autocloud/web/templates/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/autocloud/web/templates/__init__.py -------------------------------------------------------------------------------- /autocloud/web/templates/compose_details.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block body %} 4 |
    5 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for compose_detail in compose_details %} 33 | 34 | 37 | 50 | 53 | 56 | 59 | 63 | 64 | 65 | {% else %} 66 | 67 | {% endfor %} 68 | 69 |
    Compose IDStatusDateTypeRespinOverview
    35 | {{ compose_detail.compose_id }} 36 | 38 | {% if compose_detail.status == 'c' %} 39 | 40 | {% elif compose_detail.status == 'r' %} 41 | 42 | {% elif compose_detail.status == 'q' %} 43 | 44 | {% else %} 45 | 46 | {% endif %} 47 | {{ compose_detail.status }} 48 | 49 | 51 | {{ compose_detail.date.strftime('%Y-%m-%d') }} 52 | 54 | {{ compose_detail.type }} 55 | 57 | {{ compose_detail.respin }} 58 | 60 | {{ compose_detail.passed }} / 61 | {{ compose_detail.failed }} 62 | Results
    No results to display!
    70 |
    71 | {% endblock %} 72 | -------------------------------------------------------------------------------- /autocloud/web/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block body %} 4 |
    5 |

    Autocloud

    6 | Go to compose details 7 |
    8 | {% endblock %} 9 | -------------------------------------------------------------------------------- /autocloud/web/templates/job_details.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block body %} 4 |
    5 | 19 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for job_detail in job_details %} 57 | 58 | 61 | 64 | 67 | 70 | 73 | 88 | 89 | 90 | 91 | 92 | {% else %} 93 | 94 | {% endfor %} 95 | 96 |
    Compose IDFamilyArchitectureReleaseTypeStatusCreatedLast updatedOutput
    59 | {{ job_detail.compose_id }} 60 | 62 | {{ job_detail.family }} 63 | 65 | {{ job_detail.arch }} 66 | 68 | {{ job_detail.release }} 69 | 71 | {{ job_detail.image_type }} 72 | 74 | {% if job_detail.status == 's' %} 75 | 76 | {% elif job_detail.status in ('f', 'a') %} 77 | 78 | {% elif job_detail.status == 'r' %} 79 | 80 | {% elif job_detail.status == 'q' %} 81 | 82 | {% else %} 83 | 84 | {% endif %} 85 | {{ job_detail.status }} 86 | 87 | {{ job_detail.created_on.strftime('%Y-%m-%d %H:%M:%S') }}{{ job_detail.last_updated.strftime('%Y-%m-%d %H:%M:%S') }} Output
    No results to display!
    97 |
    98 | {% endblock %} 99 | -------------------------------------------------------------------------------- /autocloud/web/templates/job_output.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block body %} 4 |
    5 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 34 | 37 | 40 | 43 | 58 | 59 | 60 | 61 | 62 | 63 |
    Compose IDFamilyArchitectureReleaseTypeStatusCreatedLast updatedLink
    29 | {{ job_detail.compose_id }} 30 | 32 | {{ job_detail.family }} 33 | 35 | {{ job_detail.arch }} 36 | 38 | {{ job_detail.release }} 39 | 41 | {{ job_detail.image_type }} 42 | 44 | {% if job_detail.status == 's' %} 45 | 46 | {% elif job_detail.status in ('f', 'a') %} 47 | 48 | {% elif job_detail.status == 'r' %} 49 | 50 | {% elif job_detail.status == 'q' %} 51 | 52 | {% else %} 53 | 54 | {% endif %} 55 | {{ job_detail.status }} 56 | 57 | {{ job_detail.created_on.strftime('%Y-%m-%d %H:%M:%S') }}{{ job_detail.last_updated.strftime('%Y-%m-%d %H:%M:%S') }} Download Image
    64 |
    65 | 66 | 67 | {% for line in job_output_lines %} 68 | 69 | 72 | 75 | 76 | {% endfor %} 77 | 78 |
    70 | 71 | 73 |
    {{ line }}
    74 |
    79 |
    80 |
    81 | {% endblock %} 82 | -------------------------------------------------------------------------------- /autocloud/web/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "master.html" %} 2 | 3 | {% block body %} 4 |
    5 |
    6 | 7 |
    8 | {% if config['FMN_ALLOW_GENERIC_OPENID'] %} 9 | 10 |

    OpenId Login

    11 |
    12 | 13 | 14 | 17 | 18 |
    19 |
    20 |
    21 | {% endif %} 22 |
    23 |
    24 |
    25 | 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /autocloud/web/templates/master.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Autocloud 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
    25 | 26 | 27 | 41 | 42 | {% block body %}{% endblock %} 43 | 44 |
    45 | 46 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 79 | 80 | {% if config['FEDMENU_URL'] %} 81 | 82 | 83 | 93 | {% endif %} 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /autocloud/web/utils.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import exc 2 | from werkzeug.exceptions import abort 3 | 4 | 5 | def get_object_or_404(session, model, *criterion): 6 | try: 7 | return session.query(model).filter(*criterion).one() 8 | except exc.NoResultFound, exc.MultipleResultsFound: 9 | abort(404) 10 | -------------------------------------------------------------------------------- /autocloud_job: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | python -tt /usr/share/autocloud/autocloud_job.py "$@" 4 | -------------------------------------------------------------------------------- /autocloud_job.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import copy 3 | import datetime 4 | import json 5 | import os 6 | import subprocess 7 | 8 | import fedfind.release 9 | 10 | from retask.queue import Queue 11 | 12 | from autocloud.constants import SUCCESS, FAILED, RUNNING 13 | from autocloud.models import init_model, ComposeJobDetails, ComposeDetails 14 | from autocloud.producer import publish_to_fedmsg 15 | 16 | import logging 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | def handle_err(session, data, out, err): 23 | """ 24 | Prints the details and exits. 25 | :param out: 26 | :param err: 27 | :return: None 28 | """ 29 | # Update DB first. 30 | data.status = u'f' 31 | data.output = "%s: %s" % (out, err) 32 | timestamp = datetime.datetime.now() 33 | data.last_updated = timestamp 34 | session.commit() 35 | log.debug("%s: %s", out, err) 36 | 37 | 38 | def system(cmd): 39 | """ 40 | Runs a shell command, and returns the output, err, returncode 41 | 42 | :param cmd: The command to run. 43 | :return: Tuple with (output, err, returncode). 44 | """ 45 | ret = subprocess.Popen(cmd, shell=True, stdin=subprocess.PIPE, 46 | stdout=subprocess.PIPE, 47 | stderr=subprocess.PIPE, close_fds=True) 48 | out, err = ret.communicate() 49 | returncode = ret.returncode 50 | return out, err, returncode 51 | 52 | 53 | def refresh_storage_pool(): 54 | '''Refreshes libvirt storage pool. 55 | 56 | http://kushaldas.in/posts/storage-volume-error-in-libvirt-with-vagrant.html 57 | ''' 58 | out, err, retcode = system('virsh pool-list') 59 | lines = out.split('\n') 60 | if len(lines) > 2: 61 | for line in lines[2:]: 62 | words = line.split() 63 | if len(words) == 3: 64 | if words[1] == 'active': 65 | system('virsh pool-refresh {0}'.format(words[0])) 66 | 67 | 68 | def image_cleanup(image_path): 69 | """ 70 | Delete the image if it is processed or if there is any exception occur 71 | 72 | :param basename: Absoulte path for image 73 | """ 74 | if os.path.exists(image_path): 75 | try: 76 | os.remove(image_path) 77 | except OSError as e: 78 | log.error('Error: %s - %s.', e.filename, e.strerror) 79 | 80 | 81 | def create_dirs(): 82 | """ 83 | Creates the runtime dirs 84 | """ 85 | system('mkdir -p /var/run/tunir') 86 | system('mkdir -p /var/run/autocloud') 87 | 88 | 89 | def create_result_text(out): 90 | """ 91 | :param out: Output text from the command. 92 | """ 93 | result_filename = None 94 | lines = out.splitlines() 95 | for line in lines: 96 | if line.startswith('Result file at:'): 97 | result_filename = line.split(' ')[1] 98 | 99 | if not result_filename: # In case no result file. 100 | return "NO RESULT FILE." 101 | result_filename = result_filename.strip() 102 | if os.path.exists(result_filename): 103 | new_content = '' 104 | with open(result_filename) as fobj: 105 | new_content = fobj.read() 106 | job_status_index = out.find('Job status:') 107 | if job_status_index == -1: 108 | return out # No job status in the output. 109 | new_line_index = out[job_status_index:].find('\n') 110 | out = out[:job_status_index + new_line_index] 111 | out = out + '\n\n' + new_content 112 | system('rm -f {0}'.format(result_filename)) 113 | return out 114 | return out 115 | 116 | 117 | def auto_job(task_data): 118 | """ 119 | This fuction queues the job, and then executes the tests, 120 | updates the db as required. 121 | 122 | :param taskid: Koji taskid. 123 | :param image_url: URL to download the fedora image. 124 | :return: 125 | """ 126 | # TODO: 127 | # We will have to update the job information on DB, rather 128 | # than creating it. But we will do it afterwards. 129 | 130 | compose_image_url = task_data['absolute_path'] 131 | compose_id = task_data['compose']['id'] 132 | release = task_data['compose']['release'] 133 | job_id = task_data['job_id'] 134 | image_type = task_data['type'] 135 | 136 | job_type = 'vm' 137 | 138 | # Just to make sure that we have runtime dirs 139 | create_dirs() 140 | 141 | session = init_model() 142 | timestamp = datetime.datetime.now() 143 | data = None 144 | try: 145 | data = session.query(ComposeJobDetails).get(str(job_id)) 146 | data.status = u'r' 147 | data.last_updated = timestamp 148 | except Exception as err: 149 | log.error("%s" % err) 150 | log.error("%s: %s", compose_id, compose_image_url) 151 | session.commit() 152 | 153 | params = { 154 | 'compose_url': compose_image_url, 155 | 'compose_id': compose_id, 156 | 'status': RUNNING, 157 | 'job_id': job_id, 158 | 'release': release, 159 | 'family': data.family.value, 160 | 'type': image_type, 161 | 'image_name': data.image_name, 162 | } 163 | publish_to_fedmsg(topic='image.running', **params) 164 | 165 | # Now we have job queued, let us start the job. 166 | # Step 1: Download the image 167 | image_url = compose_image_url 168 | basename = os.path.basename(image_url) 169 | image_path = '/var/run/autocloud/%s' % basename 170 | log.debug("Going to download {0}".format(image_url)) 171 | out, err, ret_code = system('wget %s -O %s' % (image_url, image_path)) 172 | if ret_code: 173 | image_cleanup(image_path) 174 | handle_err(session, data, out, err) 175 | log.debug("Return code: %d" % ret_code) 176 | 177 | params.update({'status': FAILED}) 178 | publish_to_fedmsg(topic='image.failed', **params) 179 | check_status_of_compose_image(compose_id) 180 | return FAILED 181 | 182 | # Step 2: Create the conf file with correct image path. 183 | if basename.find('vagrant') == -1: 184 | conf = {"image": "/var/run/autocloud/%s" % basename, 185 | "name": "fedora", 186 | "password": "passw0rd", 187 | "ram": 2048, 188 | "type": "vm", 189 | "user": "fedora"} 190 | 191 | else: # We now have a Vagrant job. 192 | conf = { 193 | "name": "fedora", 194 | "type": "vagrant", 195 | "image": "/var/run/autocloud/%s" % basename, 196 | "ram": 2048, 197 | "user": "vagrant", 198 | "port": "22" 199 | } 200 | if basename.find('virtualbox') != -1: 201 | conf['provider'] = 'virtualbox' 202 | job_type = 'vagrant' 203 | 204 | # Now let us refresh the storage pool 205 | refresh_storage_pool() 206 | 207 | with open('/var/run/autocloud/fedora.json', 'w') as fobj: 208 | fobj.write(json.dumps(conf)) 209 | 210 | system('/usr/bin/cp -f /etc/autocloud/fedora.txt ' 211 | '/var/run/autocloud/fedora.txt') 212 | 213 | cmd = 'tunir --job fedora --config-dir /var/run/autocloud/' 214 | # Now run tunir 215 | out, err, ret_code = system(cmd) 216 | if ret_code: 217 | image_cleanup(image_path) 218 | handle_err(session, data, create_result_text(out), err) 219 | log.debug("Return code: %d" % ret_code) 220 | params.update({'status': FAILED}) 221 | publish_to_fedmsg(topic='image.failed', **params) 222 | check_status_of_compose_image(compose_id) 223 | return FAILED 224 | else: 225 | image_cleanup(image_path) 226 | 227 | # Enabling direct stdout as output of the command 228 | out = create_result_text(out) 229 | if job_type == 'vm': 230 | com_text = out[out.find('/usr/bin/qemu-kvm'):] 231 | else: 232 | com_text = out 233 | 234 | data.status = u's' 235 | timestamp = datetime.datetime.now() 236 | data.last_updated = timestamp 237 | data.output = com_text 238 | session.commit() 239 | session.close() 240 | 241 | params.update({'status': SUCCESS}) 242 | publish_to_fedmsg(topic='image.success', **params) 243 | check_status_of_compose_image(compose_id) 244 | return SUCCESS 245 | 246 | 247 | def check_status_of_compose_image(compose_id): 248 | session = init_model() 249 | compose_job_objs = session.query(ComposeJobDetails).filter_by( 250 | compose_id=compose_id).all() 251 | compose_obj = session.query(ComposeDetails).filter_by( 252 | compose_id=compose_id).first() 253 | 254 | results = { 255 | SUCCESS: 0, 256 | FAILED: 0, 257 | 'artifacts': {} 258 | } 259 | 260 | for compose_job_obj in compose_job_objs: 261 | status = compose_job_obj.status.code 262 | if status in ('r', 'q'): 263 | # Abort, since there's still jobs not finished 264 | return False 265 | 266 | elif status in ('s',): 267 | results[SUCCESS] = results[SUCCESS] + 1 268 | 269 | elif status in ('f', 'a'): 270 | results[FAILED] = results[FAILED] + 1 271 | 272 | artifact = { 273 | 'architecture': compose_job_obj.arch.value, 274 | 'family': compose_job_obj.family.value, 275 | 'image_url': compose_job_obj.image_url, 276 | 'release': compose_job_obj.release, 277 | 'subvariant': compose_job_obj.subvariant, 278 | 'format': compose_job_obj.image_format, 279 | 'type': compose_job_obj.image_type, 280 | 'name': compose_job_obj.image_name, 281 | 'status': compose_job_obj.status.value 282 | } 283 | results['artifacts'][str(compose_job_obj.id)] = artifact 284 | 285 | compose_obj.passed = results[SUCCESS] 286 | compose_obj.failed = results[FAILED] 287 | compose_obj.status = u'c' 288 | 289 | session.commit() 290 | 291 | compose_id = compose_obj.compose_id 292 | rel = fedfind.release.get_release(cid=compose_id) 293 | release = rel.release 294 | 295 | params = { 296 | 'id': compose_obj.compose_id, 297 | 'respin': compose_obj.respin, 298 | 'type': compose_obj.type, 299 | 'date': datetime.datetime.strftime(compose_obj.date, '%Y%m%d'), 300 | 'results': results, 301 | 'release': release, 302 | 'status': 'completed', 303 | 'compose_job_id': compose_obj.id 304 | } 305 | 306 | publish_to_fedmsg(topic='compose.complete', **params) 307 | 308 | session.close() 309 | 310 | return True 311 | 312 | 313 | def main(): 314 | jobqueue = Queue('jobqueue') 315 | jobqueue.connect() 316 | 317 | while True: 318 | task = jobqueue.wait() 319 | 320 | task_data = task.data 321 | pos, num_images = task_data['pos'] 322 | 323 | compose_details = task_data['compose'] 324 | 325 | if pos == 1: 326 | session = init_model() 327 | 328 | compose_id = compose_details['id'] 329 | compose_obj = session.query(ComposeDetails).filter_by( 330 | compose_id=compose_id).first() 331 | 332 | compose_status = compose_obj.status.code 333 | 334 | # Here the check if the compose_status has completed 'c' is for 335 | # failsafe. This condition is never to be hit. This is to avoid 336 | # sending message to fedmsg. 337 | if compose_status in ('r', 'c'): 338 | log.info("Compose %s already running. Skipping sending to \ 339 | fedmsg" % compose_id) 340 | else: 341 | compose_obj.status = u'r' 342 | session.commit() 343 | 344 | params = copy.deepcopy(compose_details) 345 | params.update({'status': 'running'}) 346 | publish_to_fedmsg(topic='compose.running', **params) 347 | 348 | session.close() 349 | 350 | result = auto_job(task_data) 351 | 352 | 353 | if __name__ == '__main__': 354 | main() 355 | -------------------------------------------------------------------------------- /config/autocloud.cfg: -------------------------------------------------------------------------------- 1 | [autocloud] 2 | koji_server_url = http://koji.fedoraproject.org/kojihub/ 3 | base_koji_task_url = https://kojipkgs.fedoraproject.org//work/ 4 | virtualbox = false 5 | host = 0.0.0.0 6 | port = 5000 7 | debug = false 8 | 9 | [sqlalchemy] 10 | uri = sqlite:///autocloud.db 11 | -------------------------------------------------------------------------------- /createdb.py: -------------------------------------------------------------------------------- 1 | from autocloud import models 2 | 3 | models.create_tables() 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | 49 | clean: 50 | rm -rf $(BUILDDIR)/* 51 | 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | dirhtml: 58 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 59 | @echo 60 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 61 | 62 | singlehtml: 63 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 64 | @echo 65 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 66 | 67 | pickle: 68 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 69 | @echo 70 | @echo "Build finished; now you can process the pickle files." 71 | 72 | json: 73 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 74 | @echo 75 | @echo "Build finished; now you can process the JSON files." 76 | 77 | htmlhelp: 78 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 79 | @echo 80 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 81 | ".hhp project file in $(BUILDDIR)/htmlhelp." 82 | 83 | qthelp: 84 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 85 | @echo 86 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 87 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 88 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Autocloud.qhcp" 89 | @echo "To view the help file:" 90 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Autocloud.qhc" 91 | 92 | devhelp: 93 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 94 | @echo 95 | @echo "Build finished." 96 | @echo "To view the help file:" 97 | @echo "# mkdir -p $$HOME/.local/share/devhelp/Autocloud" 98 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Autocloud" 99 | @echo "# devhelp" 100 | 101 | epub: 102 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 103 | @echo 104 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 105 | 106 | latex: 107 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 108 | @echo 109 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 110 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 111 | "(use \`make latexpdf' here to do that automatically)." 112 | 113 | latexpdf: 114 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 115 | @echo "Running LaTeX files through pdflatex..." 116 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 117 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 118 | 119 | latexpdfja: 120 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 121 | @echo "Running LaTeX files through platex and dvipdfmx..." 122 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 123 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 124 | 125 | text: 126 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 127 | @echo 128 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 129 | 130 | man: 131 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 132 | @echo 133 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 134 | 135 | texinfo: 136 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 137 | @echo 138 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 139 | @echo "Run \`make' in that directory to run these through makeinfo" \ 140 | "(use \`make info' here to do that automatically)." 141 | 142 | info: 143 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 144 | @echo "Running Texinfo files through makeinfo..." 145 | make -C $(BUILDDIR)/texinfo info 146 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 147 | 148 | gettext: 149 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 150 | @echo 151 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 152 | 153 | changes: 154 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 155 | @echo 156 | @echo "The overview file is in $(BUILDDIR)/changes." 157 | 158 | linkcheck: 159 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 160 | @echo 161 | @echo "Link check complete; look for any errors in the above output " \ 162 | "or in $(BUILDDIR)/linkcheck/output.txt." 163 | 164 | doctest: 165 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 166 | @echo "Testing of doctests in the sources finished, look at the " \ 167 | "results in $(BUILDDIR)/doctest/output.txt." 168 | 169 | xml: 170 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 171 | @echo 172 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 173 | 174 | pseudoxml: 175 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 176 | @echo 177 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 178 | -------------------------------------------------------------------------------- /docs/building.rst: -------------------------------------------------------------------------------- 1 | How to build Autocloud for development? 2 | ======================================== 3 | 4 | Autocloud is written in Python2. We will port it to Python3 in future, but for now it is on Python2 only. 5 | First you can git clone the repo from github. 6 | :: 7 | 8 | $ git clone https://github.com/kushaldas/autocloud.git 9 | 10 | You should also install vagrant-libvirt from Fedora repo. 11 | :: 12 | 13 | $ sudo dnf install vagrant-libvirt -y 14 | 15 | 16 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Autocloud documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Sep 9 01:48:39 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ------------------------------------------------ 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be 29 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 30 | # ones. 31 | extensions = [] 32 | 33 | # Add any paths that contain templates here, relative to this directory. 34 | templates_path = ['_templates'] 35 | 36 | # The suffix of source filenames. 37 | source_suffix = '.rst' 38 | 39 | # The encoding of source files. 40 | #source_encoding = 'utf-8-sig' 41 | 42 | # The master toctree document. 43 | master_doc = 'index' 44 | 45 | # General information about the project. 46 | project = u'Autocloud' 47 | copyright = u'2015, Kushal Das' 48 | 49 | # The version info for the project you're documenting, acts as replacement for 50 | # |version| and |release|, also used in various other places throughout the 51 | # built documents. 52 | # 53 | # The short X.Y version. 54 | version = '0.1' 55 | # The full version, including alpha/beta/rc tags. 56 | release = '0.1' 57 | 58 | # The language for content autogenerated by Sphinx. Refer to documentation 59 | # for a list of supported languages. 60 | #language = None 61 | 62 | # There are two options for replacing |today|: either, you set today to some 63 | # non-false value, then it is used: 64 | #today = '' 65 | # Else, today_fmt is used as the format for a strftime call. 66 | #today_fmt = '%B %d, %Y' 67 | 68 | # List of patterns, relative to source directory, that match files and 69 | # directories to ignore when looking for source files. 70 | exclude_patterns = ['_build'] 71 | 72 | # The reST default role (used for this markup: `text`) to use for all 73 | # documents. 74 | #default_role = None 75 | 76 | # If true, '()' will be appended to :func: etc. cross-reference text. 77 | #add_function_parentheses = True 78 | 79 | # If true, the current module name will be prepended to all description 80 | # unit titles (such as .. function::). 81 | #add_module_names = True 82 | 83 | # If true, sectionauthor and moduleauthor directives will be shown in the 84 | # output. They are ignored by default. 85 | #show_authors = False 86 | 87 | # The name of the Pygments (syntax highlighting) style to use. 88 | pygments_style = 'sphinx' 89 | 90 | # A list of ignored prefixes for module index sorting. 91 | #modindex_common_prefix = [] 92 | 93 | # If true, keep warnings as "system message" paragraphs in the built documents. 94 | #keep_warnings = False 95 | 96 | 97 | # -- Options for HTML output ---------------------------------------------- 98 | 99 | # The theme to use for HTML and HTML Help pages. See the documentation for 100 | # a list of builtin themes. 101 | html_theme = 'default' 102 | 103 | # Theme options are theme-specific and customize the look and feel of a theme 104 | # further. For a list of options available for each theme, see the 105 | # documentation. 106 | #html_theme_options = {} 107 | 108 | # Add any paths that contain custom themes here, relative to this directory. 109 | #html_theme_path = [] 110 | 111 | # The name for this set of Sphinx documents. If None, it defaults to 112 | # " v documentation". 113 | #html_title = None 114 | 115 | # A shorter title for the navigation bar. Default is the same as html_title. 116 | #html_short_title = None 117 | 118 | # The name of an image file (relative to this directory) to place at the top 119 | # of the sidebar. 120 | #html_logo = None 121 | 122 | # The name of an image file (within the static path) to use as favicon of the 123 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 124 | # pixels large. 125 | #html_favicon = None 126 | 127 | # Add any paths that contain custom static files (such as style sheets) here, 128 | # relative to this directory. They are copied after the builtin static files, 129 | # so a file named "default.css" will overwrite the builtin "default.css". 130 | html_static_path = ['_static'] 131 | 132 | # Add any extra paths that contain custom files (such as robots.txt or 133 | # .htaccess) here, relative to this directory. These files are copied 134 | # directly to the root of the documentation. 135 | #html_extra_path = [] 136 | 137 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 138 | # using the given strftime format. 139 | #html_last_updated_fmt = '%b %d, %Y' 140 | 141 | # If true, SmartyPants will be used to convert quotes and dashes to 142 | # typographically correct entities. 143 | #html_use_smartypants = True 144 | 145 | # Custom sidebar templates, maps document names to template names. 146 | #html_sidebars = {} 147 | 148 | # Additional templates that should be rendered to pages, maps page names to 149 | # template names. 150 | #html_additional_pages = {} 151 | 152 | # If false, no module index is generated. 153 | #html_domain_indices = True 154 | 155 | # If false, no index is generated. 156 | #html_use_index = True 157 | 158 | # If true, the index is split into individual pages for each letter. 159 | #html_split_index = False 160 | 161 | # If true, links to the reST sources are added to the pages. 162 | #html_show_sourcelink = True 163 | 164 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 165 | #html_show_sphinx = True 166 | 167 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 168 | #html_show_copyright = True 169 | 170 | # If true, an OpenSearch description file will be output, and all pages will 171 | # contain a tag referring to it. The value of this option must be the 172 | # base URL from which the finished HTML is served. 173 | #html_use_opensearch = '' 174 | 175 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 176 | #html_file_suffix = None 177 | 178 | # Output file base name for HTML help builder. 179 | htmlhelp_basename = 'Autoclouddoc' 180 | 181 | 182 | # -- Options for LaTeX output --------------------------------------------- 183 | 184 | latex_elements = { 185 | # The paper size ('letterpaper' or 'a4paper'). 186 | #'papersize': 'letterpaper', 187 | 188 | # The font size ('10pt', '11pt' or '12pt'). 189 | #'pointsize': '10pt', 190 | 191 | # Additional stuff for the LaTeX preamble. 192 | #'preamble': '', 193 | } 194 | 195 | # Grouping the document tree into LaTeX files. List of tuples 196 | # (source start file, target name, title, 197 | # author, documentclass [howto, manual, or own class]). 198 | latex_documents = [ 199 | ('index', 'Autocloud.tex', u'Autocloud Documentation', 200 | u'Kushal Das', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output --------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'autocloud', u'Autocloud Documentation', 230 | [u'Kushal Das'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------- 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'Autocloud', u'Autocloud Documentation', 244 | u'Kushal Das', 'Autocloud', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | 257 | # If true, do not generate a @detailmenu in the "Top" node's menu. 258 | #texinfo_no_detailmenu = False 259 | -------------------------------------------------------------------------------- /docs/deployment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/docs/deployment.png -------------------------------------------------------------------------------- /docs/design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kushaldas/autocloud/651272df9c24e8b8e2f11970cdfca61554a3e323/docs/design.png -------------------------------------------------------------------------------- /docs/design.rst: -------------------------------------------------------------------------------- 1 | Design of the system 2 | ====================== 3 | 4 | Autocloud helps to automated testing of the Fedora Cloud images, and vagrant images produced 5 | in the koji. It uses fedmsg to keep listening to new builds (createImage) task, and when a 6 | new image is available it enqueues it to it's own job queue, and then finally execute the 7 | latest tests on the image. It can find out if the image is qcow2 based cloud image, or a 8 | vagrant image, and calls tunir to do the real testing. The following image gives a basic 9 | idea about the flow of the steps in autocloud. 10 | 11 | 12 | .. image:: design.png 13 | 14 | Second system to test virtualbox based vagrant images 15 | ------------------------------------------------------ 16 | 17 | We need a second system which only tests the vagrant-virtualbox images. This has to be done 18 | in a separate system as Virtualbox can not co-exists with kvm. So, the primary system does all 19 | the work related to any libvirt based image (may be a qcow2, or a box file). The secondary 20 | system only listens for vagrant-virtualbox based images, and tests those vagrant images. 21 | -------------------------------------------------------------------------------- /docs/design.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 39 | 41 | 42 | 44 | image/svg+xml 45 | 47 | 48 | 49 | 50 | 51 | 55 | 60 | 63 | 71 | 72 | 80 | RETASK QUEUE 91 | 92 | 97 | 105 | 108 | 116 | FEDMSG CONSUMER 127 | 128 | 136 | Listens to Koji createImagetask completion 151 | 152 | 157 | 165 | 167 | 175 | AutoCloud service 186 | 187 | 188 | 192 | 200 | DATABASE 211 | 212 | 217 | 220 | 221 | 226 | 229 | 230 | 235 | 238 | 239 | 243 | 251 | Flask Application based web service running on Apache 262 | 263 | 268 | 271 | 272 | 273 | 274 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Autocloud documentation master file, created by 2 | sphinx-quickstart on Wed Sep 9 01:48:39 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Autocloud's documentation! 7 | ===================================== 8 | 9 | Autocloud is part of Fedora Project where we are using it to test Fedora Cloud image 10 | build on Koji. It listens to fedmsg, and pushes new messages on the status of the tests. 11 | 12 | Contents: 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | building 18 | setup 19 | design 20 | 21 | 22 | 23 | Indices and tables 24 | ================== 25 | 26 | * :ref:`genindex` 27 | * :ref:`modindex` 28 | * :ref:`search` 29 | 30 | -------------------------------------------------------------------------------- /docs/setup.rst: -------------------------------------------------------------------------------- 1 | Setup instruction on Fedora 2 | ============================ 3 | 4 | The following image explains the deployment plan of autocloud. 5 | 6 | .. image:: deployment.png 7 | 8 | We have two bare metal server autocloud-back01, and autocloud-back02, the later one is only 9 | used for vagrant-virtualbox based images. We also have two load balanced vms running the web 10 | frontend. 11 | 12 | Install the autocloud-common package in all systems 13 | ---------------------------------------------------- 14 | 15 | :: 16 | 17 | $ sudo dnf install autolcoud-common 18 | 19 | The above command will install the latest package from the repo. You may want to install 20 | vagrant-libvirt if you will execute libvirt based tests on the system. 21 | 22 | 23 | Install autocloud-backend package on both the autocloud-back0* systems 24 | ----------------------------------------------------------------------- 25 | 26 | :: 27 | 28 | $ sudo dnf install autocloud-backend 29 | 30 | 31 | Start the redis server in both autocloud-back0* systems 32 | ------------------------------------------------------- 33 | 34 | :: 35 | 36 | $ sudo systemctl start redis 37 | 38 | 39 | Enable ports for tunir in both autocloud-back0* systems 40 | -------------------------------------------------------- 41 | 42 | Autocloud uses tunir to execute the tests on a given image. We will have to do the follow setup for tunir 43 | to execute in a proper way. 44 | 45 | :: 46 | 47 | $ python /usr/share/tunir/createports.py 48 | 49 | Enable kill_vagrant command in cron job 50 | ---------------------------------------- 51 | 52 | Enable a cron job which will run */usr/sbin/kill_vagrant* in every 10 minutes (or an hour). This is required 53 | as many vagrant images do not work, and boot_timeout never works with vagrant-libvirt. 54 | 55 | .. note:: This is a workaround which is required for now (2015-09-29). But may get removed in future. 56 | 57 | 58 | Configure the database URI in all systems 59 | ------------------------------------------ 60 | 61 | In */etc/autocloud/autocloud.cfg* file please configure the sqlalchemy uri value. For our work, we are using 62 | postgres as database. 63 | 64 | Create the tables in the database 65 | ---------------------------------- 66 | 67 | 68 | .. note:: This has to be done only once from autocloud-back01 system 69 | 70 | 71 | :: 72 | 73 | $ python /usr/share/autocloud/createdb.py 74 | 75 | 76 | Install vagrant-libvirt on autocloud-back01 77 | -------------------------------------------- 78 | 79 | This is the system to handle all libvirt tasks, so we will have to install vagrant-libvirt on this system. 80 | 81 | :: 82 | 83 | $ sudo dnf install vagrant-libvirt 84 | 85 | 86 | Configure for the vagrant-virtualbox jobs in autocloud-back02 87 | --------------------------------------------------------------- 88 | 89 | In */etc/autocloud/autocloud.cfg* file set *virtualbox* value to True. If you want to know how to setup virtualbox on the system, please refer to `this guide `_. 90 | 91 | 92 | Configure the correct tunir job deatils 93 | ---------------------------------------- 94 | 95 | We need the exact commands/job details for tunir. This is a configuration file so that we can update it 96 | whenever required. 97 | 98 | :: 99 | 100 | $ sudo wget https://raw.githubusercontent.com/kushaldas/tunirtests/master/fedora.txt -O /etc/autocloud/fedora.txt 101 | 102 | Start fedmsg-hub service in autocloud-back0* systems 103 | ----------------------------------------------------- 104 | 105 | This service listens for new koji builds, and creates the database entry and corresponding task in the queue. 106 | 107 | :: 108 | 109 | $ sudo systemctl start fedmsg-hub 110 | 111 | Start autocloud service in autocloud-back0* systems 112 | ---------------------------------------------------- 113 | 114 | This service will listen for new task in the queue, and execute the tasks. 115 | 116 | :: 117 | 118 | $ sudo systemctl start autocloud 119 | 120 | Starting the web dashboard in autocloud-web0* systems 121 | ------------------------------------------------------- 122 | 123 | This is the web dashboard for the Autocloud, we use httpd for the this. 124 | 125 | :: 126 | 127 | $ sudo systemctl start httpd 128 | -------------------------------------------------------------------------------- /fedmsg.d/autocloud.py: -------------------------------------------------------------------------------- 1 | import socket 2 | hostname = socket.gethostname().split('.')[0] 3 | 4 | 5 | config = { 6 | # Consumer stuff 7 | "autocloud.consumer.enabled": True, 8 | "autocloud.sqlalchemy.uri": "sqlite:////var/tmp/autocloud-dev-db.sqlite", 9 | 10 | # Turn on logging for autocloud 11 | "logging": dict( 12 | loggers=dict( 13 | autocloud={ 14 | "level": "DEBUG", 15 | "propagate": False, 16 | "handlers": ["console"], 17 | }, 18 | ), 19 | ), 20 | } 21 | -------------------------------------------------------------------------------- /fedmsg.d/base.py: -------------------------------------------------------------------------------- 1 | # This file is part of fedmsg. 2 | # Copyright (C) 2012 Red Hat, Inc. 3 | # 4 | # fedmsg is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # fedmsg is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with fedmsg; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Authors: Ralph Bean 19 | # 20 | config = dict( 21 | # Set this to dev if you're hacking on fedmsg or an app. 22 | # Set to stg or prod if running in the Fedora Infrastructure 23 | environment="dev", 24 | 25 | # Default is 0 26 | high_water_mark=0, 27 | io_threads=1, 28 | 29 | ## For the fedmsg-hub and fedmsg-relay. ## 30 | 31 | # We almost always want the fedmsg-hub to be sending messages with zmq as 32 | # opposed to amqp or stomp. 33 | zmq_enabled=True, 34 | 35 | # When subscribing to messages, we want to allow splats ('*') so we tell 36 | # the hub to not be strict when comparing messages topics to subscription 37 | # topics. 38 | zmq_strict=False, 39 | 40 | # Number of seconds to sleep after initializing waiting for sockets to sync 41 | post_init_sleep=0.5, 42 | 43 | # Wait a whole second to kill all the last io threads for messages to 44 | # exit our outgoing queue (if we have any). This is in milliseconds. 45 | zmq_linger=1000, 46 | 47 | # See the following 48 | # - http://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html 49 | # - http://api.zeromq.org/3-2:zmq-setsockopt 50 | zmq_tcp_keepalive=1, 51 | zmq_tcp_keepalive_cnt=3, 52 | zmq_tcp_keepalive_idle=60, 53 | zmq_tcp_keepalive_intvl=5, 54 | ) 55 | -------------------------------------------------------------------------------- /fedmsg.d/endpoints-autocloud.py: -------------------------------------------------------------------------------- 1 | # This file is part of fedmsg. 2 | # Copyright (C) 2012 Red Hat, Inc. 3 | # 4 | # fedmsg is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # fedmsg is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with fedmsg; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Authors: Ralph Bean 19 | # 20 | import socket 21 | hostname = socket.gethostname().split('.', 1)[0] 22 | 23 | config = dict( 24 | # This is a dict of possible addresses from which fedmsg can send 25 | # messages. fedmsg.init(...) requires that a 'name' argument be passed 26 | # to it which corresponds with one of the keys in this dict. 27 | endpoints={ 28 | "autocloud.%s" % hostname: [ 29 | "tcp://127.0.0.1:4322", 30 | "tcp://127.0.0.1:4323", 31 | ], 32 | "__main__.%s" % hostname: [ 33 | "tcp://127.0.0.1:4324", 34 | ], 35 | }, 36 | ) 37 | -------------------------------------------------------------------------------- /fedmsg.d/logging.py: -------------------------------------------------------------------------------- 1 | # Setup fedmsg logging. 2 | # See the following for constraints on this format http://bit.ly/Xn1WDn 3 | config = dict( 4 | logging=dict( 5 | version=1, 6 | formatters=dict( 7 | bare={ 8 | "datefmt": "%Y-%m-%d %H:%M:%S", 9 | "format": "[%(asctime)s][%(name)10s %(levelname)7s] %(message)s" 10 | }, 11 | ), 12 | handlers=dict( 13 | console={ 14 | "class": "logging.StreamHandler", 15 | "formatter": "bare", 16 | "level": "DEBUG", 17 | "stream": "ext://sys.stdout", 18 | } 19 | ), 20 | loggers=dict( 21 | fedmsg={ 22 | "level": "DEBUG", 23 | "propagate": False, 24 | "handlers": ["console"], 25 | }, 26 | moksha={ 27 | "level": "DEBUG", 28 | "propagate": False, 29 | "handlers": ["console"], 30 | }, 31 | ), 32 | ), 33 | ) 34 | -------------------------------------------------------------------------------- /fedmsg.d/ssl.py: -------------------------------------------------------------------------------- 1 | # This file is part of fedmsg. 2 | # Copyright (C) 2012 Red Hat, Inc. 3 | # 4 | # fedmsg is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU Lesser General Public 6 | # License as published by the Free Software Foundation; either 7 | # version 2.1 of the License, or (at your option) any later version. 8 | # 9 | # fedmsg is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 12 | # Lesser General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU Lesser General Public 15 | # License along with fedmsg; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Authors: Ralph Bean 19 | # 20 | import os 21 | import socket 22 | 23 | SEP = os.path.sep 24 | here = os.getcwd() 25 | 26 | config = dict( 27 | sign_messages=False, 28 | validate_signatures=False, 29 | ssldir="/etc/pki/fedmsg", 30 | 31 | crl_location="https://fedoraproject.org/fedmsg/crl.pem", 32 | crl_cache="/var/run/fedmsg/crl.pem", 33 | crl_cache_expiry=10, 34 | 35 | ca_cert_location="https://fedoraproject.org/fedmsg/ca.crt", 36 | ca_cert_cache="/var/run/fedmsg/ca.crt", 37 | ca_cert_cache_expiry=0, # Never expires 38 | 39 | certnames={ 40 | # In prod/stg, map hostname to the name of the cert in ssldir. 41 | # Unfortunately, we can't use socket.getfqdn() 42 | #"app01.stg": "app01.stg.phx2.fedoraproject.org", 43 | }, 44 | 45 | # A mapping of fully qualified topics to a list of cert names for which 46 | # a valid signature is to be considered authorized. Messages on topics not 47 | # listed here are considered automatically authorized. 48 | routing_policy={ 49 | # Only allow announcements from production if they're signed by a 50 | # certain certificate. 51 | "org.fedoraproject.prod.announce.announcement": [ 52 | "announce-lockbox.phx2.fedoraproject.org", 53 | ], 54 | }, 55 | 56 | # Set this to True if you want messages to be dropped that aren't 57 | # explicitly whitelisted in the routing_policy. 58 | # When this is False, only messages that have a topic in the routing_policy 59 | # but whose cert names aren't in the associated list are dropped; messages 60 | # whose topics do not appear in the routing_policy are not dropped. 61 | routing_nitpicky=False, 62 | ) 63 | -------------------------------------------------------------------------------- /kill_vagrant: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | VPID=`ps aux | grep "/usr/share/vagrant/bin/vagrant up" | grep -v grep | awk '{print $2}'` 3 | ET=`ps -p $VPID -o etimes= | awk '{print $1}'` 4 | if [ ! -z "$VPID" ] 5 | then 6 | if [ "$ET" -gt "300" ] 7 | then 8 | kill -2 $VPID 9 | fi 10 | fi 11 | -------------------------------------------------------------------------------- /publish/generate_fixtures.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | import json 5 | import logging 6 | import requests 7 | import fedmsg 8 | 9 | logging.basicConfig(level=logging.DEBUG) 10 | log = logging.getLogger(__name__) 11 | 12 | url = 'https://apps.fedoraproject.org/datagrepper/raw' 13 | params = { 14 | 'topic': 'org.fedoraproject.prod.pungi.compose.status.change', 15 | 'rows_per_page': 100, 16 | } 17 | 18 | fixtures = [] 19 | 20 | for page in range(3, 20): 21 | params.update({'page': page}) 22 | resp = requests.get(url, params=params) 23 | print page 24 | 25 | results = resp.json() 26 | fixtures = [] 27 | 28 | for result in results['raw_messages']: 29 | if result['msg'] in ['FINISHED_INCOMPLETE', 'FINISHED']: 30 | fixtures.append(result) 31 | 32 | with open('fixtures.json', 'r') as f: 33 | f.write(json.dumps(fixtures)) 34 | -------------------------------------------------------------------------------- /publish/publish_messages.py: -------------------------------------------------------------------------------- 1 | import json 2 | import fedmsg 3 | import time 4 | 5 | with open('publish/fixtures.json', 'r') as infile: 6 | raw_messages = json.load(infile) 7 | for raw_message in raw_messages: 8 | time.sleep(1) 9 | fedmsg.publish(msg=raw_message['msg'], 10 | topic='pungi.compose.status.change') 11 | 12 | -------------------------------------------------------------------------------- /publish/seed.py: -------------------------------------------------------------------------------- 1 | from autocloud.models import init_model, ComposeJobDetails 2 | import datetime 3 | import hashlib 4 | import random 5 | 6 | def r(data): 7 | return random.choice(data) 8 | 9 | if __name__ == '__main__': 10 | session = init_model() 11 | timestamp = datetime.datetime.now() 12 | 13 | for i in range(100): 14 | arch = r(ComposeJobDetails.ARCH_TYPES) 15 | n = r([0,1,2,3]) 16 | timestamp += datetime.timedelta(seconds=random.randint(1, 500)) 17 | 18 | jd = JobDetails( 19 | taskid=hashlib.md5(str(timestamp)).hexdigest()[:8], 20 | status=random.choice('sfar'), 21 | created_on=timestamp, 22 | user='admin', 23 | last_updated=timestamp) 24 | session.add(jd) 25 | session.commit() 26 | 27 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | SQLAlchemy>=0.8 2 | fedmsg 3 | redis 4 | retask 5 | sqlalchemy-utils 6 | alembic 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | requires = [ 4 | 'redis', 5 | 'retask', 6 | 'fedmsg', 7 | ] 8 | 9 | setup( 10 | name='autocloud', 11 | version='0.8.0', 12 | description='', 13 | author='Kushal Das', 14 | author_email='kushaldas@gmail.com', 15 | url='https://github.com/kushaldas/autocloud', 16 | install_requires=requires, 17 | packages=find_packages(), 18 | include_package_data=True, 19 | zip_safe=False, 20 | classifiers=[ 21 | 'Environment :: Web Environment', 22 | 'Topic :: Software Development :: Libraries :: Python Modules', 23 | 'Intended Audience :: Developers', 24 | 'Programming Language :: Python', 25 | ], 26 | entry_points={ 27 | 'moksha.consumer': [ 28 | "autocloud_consumer = autocloud.consumer:AutoCloudConsumer", 29 | ], 30 | }, 31 | ) 32 | --------------------------------------------------------------------------------