├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG ├── CONTRIBUTORS ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── UPDATING ├── config.yaml.example ├── pushmanager ├── __about__.py ├── __init__.py ├── core │ ├── __init__.py │ ├── application.py │ ├── auth.py │ ├── db.py │ ├── git.py │ ├── mail.py │ ├── pid.py │ ├── rb.py │ ├── requesthandler.py │ ├── settings.py │ ├── util.py │ └── xmppclient.py ├── handlers.py ├── pushmanager_api.py ├── pushmanager_main.py ├── servlets │ ├── __init__.py │ ├── addrequest.py │ ├── api.py │ ├── blesspush.py │ ├── checklist.py │ ├── commentrequest.py │ ├── conflictcheck.py │ ├── delayrequest.py │ ├── deploypush.py │ ├── discardpush.py │ ├── discardrequest.py │ ├── editpush.py │ ├── livepush.py │ ├── msg.py │ ├── newpush.py │ ├── newrequest.py │ ├── pickmerequest.py │ ├── pingme.py │ ├── push.py │ ├── pushbyrequest.py │ ├── pushes.py │ ├── pushitems.py │ ├── removerequest.py │ ├── request.py │ ├── requests.py │ ├── smartdest.py │ ├── summaryforbranch.py │ ├── testtag.py │ ├── undelayrequest.py │ ├── userlist.py │ └── verifyrequest.py ├── static │ ├── css │ │ ├── base.css │ │ ├── flick │ │ │ ├── images │ │ │ │ ├── ui-bg_flat_0_aaaaaa_40x100.png │ │ │ │ ├── ui-bg_flat_0_eeeeee_40x100.png │ │ │ │ ├── ui-bg_flat_55_ffffff_40x100.png │ │ │ │ ├── ui-bg_flat_75_ffffff_40x100.png │ │ │ │ ├── ui-bg_glass_65_ffffff_1x400.png │ │ │ │ ├── ui-bg_highlight-soft_100_f6f6f6_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_25_0073ea_1x100.png │ │ │ │ ├── ui-bg_highlight-soft_50_dddddd_1x100.png │ │ │ │ ├── ui-icons_0073ea_256x240.png │ │ │ │ ├── ui-icons_454545_256x240.png │ │ │ │ ├── ui-icons_666666_256x240.png │ │ │ │ ├── ui-icons_ff0084_256x240.png │ │ │ │ └── ui-icons_ffffff_256x240.png │ │ │ └── jquery-ui-1.8.12.custom.css │ │ └── modules │ │ │ ├── newrequest.css │ │ │ └── request.css │ ├── favicon.ico │ ├── img │ │ ├── ajax-loader.gif │ │ ├── button_expand.gif │ │ ├── button_hide.gif │ │ ├── favicon.gif │ │ └── hamster.png │ └── js │ │ ├── ZeroClipboard.min.js │ │ ├── ZeroClipboard.min.map │ │ ├── ZeroClipboard.swf │ │ ├── jquery-1.4.2.min.js │ │ ├── jquery-ui-1.8.12.custom.min.js │ │ ├── modules │ │ ├── newrequest.js │ │ └── request.js │ │ └── push.js ├── templates │ ├── base.html │ ├── check_sites_bookmarklet.js │ ├── checklist.html │ ├── checklist │ │ └── category.html │ ├── confirm-conflict-check.html │ ├── create_request_bookmarklet.js │ ├── edit-push.html │ ├── home.html │ ├── login.html │ ├── modules │ │ ├── newrequest.html │ │ ├── request-buttons.html │ │ ├── request-info.html │ │ └── request.html │ ├── push-button-bar.html │ ├── push-dialogs.html │ ├── push-info.html │ ├── push-status.html │ ├── push.html │ ├── pushes.html │ ├── pushitems.html │ ├── request.html │ ├── requests.html │ └── userlist.html ├── testing │ ├── __init__.py │ ├── mocksettings.py │ ├── testdb.py │ ├── testdb.sql │ └── testservlet.py ├── tests │ ├── __init__.py │ ├── test_bookmarklet.py │ ├── test_core_auth.py │ ├── test_core_db.py │ ├── test_core_git.py │ ├── test_core_mail.py │ ├── test_core_pid.py │ ├── test_core_requesthandler.py │ ├── test_core_util.py │ ├── test_core_xmppclient.py │ ├── test_rename_checklist_type.py │ ├── test_rename_tag.py │ ├── test_servlet_addrequest.py │ ├── test_servlet_api.py │ ├── test_servlet_blesspush.py │ ├── test_servlet_checklist.py │ ├── test_servlet_delayrequest.py │ ├── test_servlet_deploypush.py │ ├── test_servlet_discardrequest.py │ ├── test_servlet_livepush.py │ ├── test_servlet_login.py │ ├── test_servlet_msg.py │ ├── test_servlet_newpush.py │ ├── test_servlet_newrequest.py │ ├── test_servlet_pickmerequests.py │ ├── test_servlet_push.py │ ├── test_servlet_pushes.py │ ├── test_servlet_pushitems.py │ ├── test_servlet_removerequest.py │ ├── test_servlet_summaryforbranch.py │ ├── test_servlet_testtag.py │ ├── test_template_newrequest.py │ ├── test_template_push.py │ ├── test_template_pushes.py │ ├── test_template_request.py │ ├── test_ui_methods.py │ └── test_ui_modules.py ├── ui_methods.py └── ui_modules.py ├── pushplans ├── add_conflicts.sql ├── add_stageenv.sql ├── add_watchers.sql └── rename_push_to_pushplan.sh ├── requirements-dev.txt ├── requirements.txt ├── scripts └── pushmanager ├── setup.py ├── tools ├── __init__.py ├── rename_checklist_type.py └── rename_tag.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | timid = True 4 | source = 5 | . 6 | omit = 7 | class_under_test.py 8 | *__init__.py 9 | testing/* 10 | tests/* 11 | */tests/* 12 | 13 | [report] 14 | exclude_lines = 15 | # Don't complain if tests don't hit defensive assertion code: 16 | raise AssertionError 17 | raise NotImplementedError 18 | 19 | # Don't complain if non-runnable code isn't run: 20 | if __name__ == .__main__.: 21 | ignore_errors = True 22 | 23 | [html] 24 | directory = coverage-html 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Sphinx documentation 38 | docs/_build/ 39 | 40 | # Editors 41 | [._]*.s[a-w][a-z] 42 | [._]s[a-w][a-z] 43 | *.un~ 44 | Session.vim 45 | .netrwhist 46 | *~ 47 | 48 | # Pushmanager 49 | app.*.log 50 | app.*.pid 51 | /config.yaml 52 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git@git.yelpcorp.com:mirrors/pre-commit/pre-commit-hooks 2 | sha: 9f107a03276857c668fe3e090752d3d22a4195e5 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: end-of-file-fixer 6 | - id: autopep8-wrapper 7 | args: ['-i', '--ignore=E265,E309,E501'] 8 | - id: check-yaml 9 | - id: debug-statements 10 | - id: flake8 11 | - id: requirements-txt-fixer 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | env: # These should match the tox env list 3 | - TOXENV=py26 4 | - TOXENV=py27 5 | 6 | before_install: 7 | - pip install --use-mirrors mysql-python==1.2.5 python-ldap==2.4.13 8 | 9 | install: pip install tox --use-mirrors 10 | script: tox 11 | -------------------------------------------------------------------------------- /CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | Amber Yust 2 | Baris Metin 3 | Evan Krall 4 | James Brown 5 | James Duncan 6 | Jon Madden 7 | Josh Snyder 8 | Sam Kimbrel 9 | Tyler Roscoe -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include config.yaml.example 2 | include pushmanager/testing/*sql 3 | include README.rst 4 | include requirements-dev.txt 5 | include requirements.txt 6 | include setup.py 7 | include tox.ini 8 | recursive-include pushmanager/static * 9 | recursive-include pushmanager/templates * 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: test flakes 2 | @echo "Default target is to test. You need to specify other targets explicitly." 3 | 4 | .PHONY: flakes 5 | flakes: 6 | tox 7 | 8 | .PHONY: test 9 | test: 10 | tox 11 | 12 | .PHONY: coverage 13 | coverage: 14 | tox -e cover 15 | coverage html 16 | coverage xml 17 | 18 | .PHONY: tests 19 | tests: test ; 20 | 21 | .PHONY: clean 22 | clean: 23 | rm -rf .coverage 24 | rm -rf .tox 25 | find . -name '*.pyc' -delete 26 | find . -name '__pycache__' -delete 27 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Pushmanager 2 | =========== 3 | 4 | Pushmanager is a tornado web application we use to manage deployments 5 | at Yelp. It helps pushmasters to conduct the deployment by bringing 6 | together push requests from engineers and information gathered from 7 | reviews, test builds and issue tracking system. 8 | 9 | 10 | Quick Start 11 | ----------- 12 | 13 | - ``python setup.py install`` 14 | - Create a config.yaml somewhere, e.g. as /etc/pushmanager/config.yaml. Use config.yaml.example as template. You need to change at least these settings: 15 | 16 | - main_app.servername 17 | - db_uri (use a local sqlite file, e.g. sqlite:////var/lib/pushmanager/sqlite.db) 18 | - username (effective user of service, either your own username or something like www-data) 19 | - log_path (this path must exist) 20 | - ssl_certfile and ssl_keyfile (see below) 21 | 22 | - You need a SSL certificate. If you don't have one lying around, you can create it: 23 | 24 | ``openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes`` 25 | 26 | - You also need to configure either SAML or LDAP for user authentication (see the example file). 27 | 28 | - Point to your configuration file: ``export SERVICE_ENV_CONFIG_PATH=/etc/pushmanager/config.yaml`` 29 | 30 | - Now start Pushmanager: ``pushmanager.sh start``. You should be able to point your webbrowser to 31 | ``https://main_app.servername:main_app.port`` and see a login screen. 32 | 33 | TODO: 34 | README update 35 | Changelog 36 | -------------------------------------------------------------------------------- /UPDATING: -------------------------------------------------------------------------------- 1 | 2015-01-30 2 | AFFECTS: Users with existing installs before 0.4.0 3 | AUTHOR: milki 4 | 5 | New config options must be defined: 6 | mail.notifyonly 7 | xmpp.notifyonly 8 | login_strategy 9 | saml_config_folder: "saml/" (if login_strategy is saml) 10 | 11 | SAML 2.0 Authentication requires additional packages: 12 | python-saml 13 | swig 14 | libxmlsec1-dev 15 | 16 | Additionally, a settings.json file must be placed in the saml_config_folder 17 | 18 | 2014-09-05 19 | AFFECTS: Users with existing installs before 0.3.7 20 | AUTHOR: milki 21 | 22 | Config option trac.servername has been removed in favour of 23 | ticket_tracker_url_format 24 | 25 | 2014-08-22 26 | AFFECTS: Users with existing installs before 0.3.6 27 | AUTHOR: milki 28 | 29 | 0.3.6 introduces a new config option git.conflict-threads that dictates how 30 | many addiotional worker threads are dedicated to conflict detection. 31 | 32 | 2014-08-14 33 | AFFECTS: Users with existing installs before 0.3.1 34 | AUTHOR: milki 35 | 36 | 0.3.1 introduces a new config option for debugging. Currently, 37 | it only enables git operation logging. 'config.yaml.sample' has 38 | been updated with the option disabled. 39 | 40 | 2014-08-12 41 | AFFECTS: Users with existing installs before 0.3.0 42 | AUTHOR: milki 43 | 44 | As of 0.3.0, a new column has been added to the db and 45 | several new config options have been added. 46 | 47 | The 'config.yaml.sample' has been updated to reflect the new config 48 | options and a convenient script, 'pushplans/add_conflicts.sql' has 49 | been included to facilitate the database update. 50 | 51 | 2013-10-39 52 | AFFECTS: Users with existing installs before commit ecb989b7 53 | AUTHOR: milki 54 | 55 | As of commit ecb989b7, a new column 'stageenv' has been added to the 56 | 'pull_pushes' table. Existing installs will 500 error until the 57 | database has been updated. 58 | 59 | A convenience script, 'pushplans/add_stageenv.sql' has been included 60 | to facilitate the database update. 61 | 62 | 2013-07-29 63 | AFFECTS: Users with existing installs before commit b3f8fa5 64 | AUTHOR: milki 65 | 66 | As of commit b3f8fa5, a new column 'watchers' has been added to the 67 | 'pull_requests' table. Existing installs will 500 error until the 68 | database has been updated. 69 | 70 | A convenience script, 'pushplans/add_watchers.sql' has been included 71 | to facilitate the database update. 72 | 73 | 2013-06-18: 74 | AFFECTS: Users with existing installs before commit 7a896c5 75 | AUTHOR: milki 76 | 77 | As of commit 7a896c5, the tag 'plans' was renamed to 'pushplans' in order 78 | to clarify the confusion between testplans and pushplans. 79 | 80 | In order to upgrade from an existing pushmanager setup, existing push 81 | requests and push checklists in the database with the tag 'plans' should be 82 | changed to use 'pushplans' instead. 83 | 84 | Two convenience scripts, tools/rename_tag.py and 85 | tools/rename_checklist_type.py have been included to facilitate these 86 | changes. Additionally, pushplans/rename_push_to_pushplans.sh has been 87 | provided for this particular change. 88 | 89 | To run: 90 | sh pushplans/rename_push_to_pushplan.sh 91 | 92 | from the production pushmanager root such that the production config.yaml is 93 | present and readable 94 | -------------------------------------------------------------------------------- /pushmanager/__about__.py: -------------------------------------------------------------------------------- 1 | __version_info__ = (0, 4, 2) 2 | __version__ = '%d.%d.%d' % __version_info__ 3 | -------------------------------------------------------------------------------- /pushmanager/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/__init__.py -------------------------------------------------------------------------------- /pushmanager/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/core/__init__.py -------------------------------------------------------------------------------- /pushmanager/core/application.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import pwd 4 | import signal 5 | import sys 6 | import time 7 | from abc import ABCMeta 8 | from abc import abstractmethod 9 | from optparse import OptionParser 10 | 11 | import daemon 12 | import tornado.ioloop 13 | from pushmanager.core import pid 14 | from pushmanager.core.settings import Settings 15 | 16 | 17 | logging.basicConfig( 18 | level=logging.WARNING, 19 | format="%(asctime)-15s [%(process)d|%(threadName)s] %(message)s", 20 | ) 21 | 22 | 23 | class Application: 24 | __metaclass__ = ABCMeta 25 | 26 | name = "NONE" 27 | 28 | def __init__(self): 29 | self.port = Settings['%s_app' % self.name]['port'] 30 | self.pid_file = os.path.join(Settings['log_path'], '%s.%d.pid' % (self.name, self.port)) 31 | self.log_file = os.path.join(Settings['log_path'], '%s.%d.log' % (self.name, self.port)) 32 | self.log = open(self.log_file, 'a+') 33 | self.command = self.parse_command() 34 | 35 | self.queue_worker_pids = [] 36 | self.clean_pids() 37 | 38 | if self.command == "stop": 39 | for worker_pid in self.queue_worker_pids: 40 | os.kill(worker_pid, signal.SIGKILL) 41 | sys.exit() 42 | 43 | def parse_command(self): 44 | usage = "Usage: %prog start|stop" 45 | parser = OptionParser(usage=usage) 46 | _, args = parser.parse_args() 47 | 48 | if len(args) != 1 and args[0] not in ('start', 'stop'): 49 | parser.print_help() 50 | sys.exit(1) 51 | 52 | return args[0] 53 | 54 | def clean_pids(self): 55 | if os.path.exists(self.pid_file): 56 | pid.check(self.pid_file) 57 | os.unlink(self.pid_file) 58 | time.sleep(1) 59 | 60 | @abstractmethod 61 | def start_services(self): 62 | pass 63 | 64 | def run(self): 65 | daemon_context = daemon.DaemonContext(stdout=self.log, stderr=self.log, working_directory=os.getcwd()) 66 | with daemon_context: 67 | pid.write(self.pid_file, append=True) 68 | try: 69 | self.start_services() 70 | pid.write(self.pid_file, append=True) 71 | 72 | # Drop privileges 73 | uid = pwd.getpwnam(Settings.get("username", "www-data"))[2] 74 | os.setuid(uid) 75 | 76 | tornado.ioloop.IOLoop.instance().start() 77 | finally: 78 | pid.remove(self.pid_file) 79 | -------------------------------------------------------------------------------- /pushmanager/core/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | 5 | import ldap 6 | 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | os.environ['LDAPTLS_REQCERT'] = 'demand' 11 | os.environ['LDAPTLS_CACERT'] = Settings['auth_ldap']['cert_file'] 12 | 13 | LDAP_URL = Settings['auth_ldap']['url'] 14 | 15 | 16 | def authenticate_ldap(username, password): 17 | """Attempts to bind a given username/password pair in LDAP and returns whether or not it succeeded.""" 18 | try: 19 | dn = "%s@%s" % (username, Settings['auth_ldap']['domain']) 20 | basedn = Settings['auth_ldap']['basedn'] 21 | 22 | con = ldap.initialize(LDAP_URL) 23 | 24 | con.set_option(ldap.OPT_NETWORK_TIMEOUT, 3) 25 | con.set_option(ldap.OPT_REFERRALS, 0) 26 | con.set_option(ldap.OPT_PROTOCOL_VERSION, ldap.VERSION3) 27 | 28 | con.start_tls_s() 29 | try: 30 | con.simple_bind_s(dn, password) 31 | con.search_s(basedn, ldap.SCOPE_ONELEVEL) 32 | except: 33 | return False 34 | con.unbind_s() 35 | return True 36 | except: 37 | # Tornado will log POST data in case of an uncaught 38 | # exception. In this case POST data will have username & 39 | # password and we do not want it. 40 | logging.exception("Authentication error") 41 | return False 42 | 43 | 44 | __all__ = ['authenticate_ldap'] 45 | -------------------------------------------------------------------------------- /pushmanager/core/mail.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import email.mime.text 3 | import logging 4 | import smtplib 5 | from Queue import Empty 6 | from multiprocessing import JoinableQueue 7 | from multiprocessing import Process 8 | 9 | from pushmanager.core.settings import Settings 10 | 11 | 12 | class MailQueue(object): 13 | 14 | message_queue = None 15 | worker_process = None 16 | smtp = None 17 | 18 | @classmethod 19 | def start_worker(cls): 20 | if cls.worker_process is not None: 21 | return [] 22 | cls.message_queue = JoinableQueue() 23 | cls.worker_process = Process(target=cls.process_queue, name='mail-queue') 24 | cls.worker_process.daemon = True 25 | cls.worker_process.start() 26 | return [cls.worker_process.pid] 27 | 28 | @classmethod 29 | def process_queue(cls): 30 | # We double-nest 'while True' blocks here so that we can 31 | # try to re-use the same SMTP server connection for batches 32 | # of emails, but not keep it open for long periods without 33 | # any emails to send. 34 | while True: 35 | # Blocks indefinitely 36 | send_email_args = cls.message_queue.get(True) 37 | cls.smtp = smtplib.SMTP('127.0.0.1', 25) 38 | while True: 39 | cls._send_email(*send_email_args) 40 | try: 41 | # Only blocks for 5 seconds max, raises Empty if still nothing 42 | send_email_args = cls.message_queue.get(True, 5) 43 | except Empty: 44 | # Done with this batch, use a blocking call to wait for the next 45 | break 46 | cls.smtp.quit() 47 | 48 | @classmethod 49 | def _send_email(cls, recipient, message, subject, from_email): 50 | msg = email.mime.text.MIMEText(message, 'html') 51 | msg['Subject'] = subject 52 | msg['From'] = from_email 53 | 54 | notifyonly = Settings['mail']['notifyonly'] 55 | if notifyonly: 56 | msg['To'] = ', '.join(notifyonly) 57 | msg.set_payload('Original recipients: %s\n\n%s' % (recipient, msg.get_payload())) 58 | cls.smtp.sendmail(from_email, notifyonly, msg.as_string()) 59 | cls.message_queue.task_done() 60 | return 61 | 62 | msg['To'] = recipient 63 | cls.smtp.sendmail(from_email, [recipient], msg.as_string()) 64 | other_recipients = set(Settings['mail']['notifyall']) - set([recipient]) 65 | if other_recipients: 66 | msg = email.mime.text.MIMEText(message, 'html') 67 | msg['Subject'] = '[all] %s' % subject 68 | msg['From'] = Settings['mail']['from'] 69 | msg['To'] = ', '.join(other_recipients) 70 | cls.smtp.sendmail(from_email, list(other_recipients), msg.as_string()) 71 | cls.message_queue.task_done() 72 | 73 | @classmethod 74 | def enqueue_email(cls, recipients, message, subject='', from_email=Settings['mail']['from']): 75 | if isinstance(recipients, (list, set, tuple)): 76 | # Flatten non-string iterables 77 | for recipient in recipients: 78 | cls.enqueue_email(recipient, message, subject, from_email) 79 | elif isinstance(recipients, (str, unicode)): 80 | if cls.message_queue is not None: 81 | cls.message_queue.put((recipients, message, subject, from_email)) 82 | else: 83 | logging.error("Failed to enqueue email: MailQueue not initialized") 84 | else: 85 | raise ValueError('Recipient(s) must be a string or iterable of strings') 86 | 87 | @classmethod 88 | def enqueue_user_email(cls, recipients, *args, **kwargs): 89 | """Transforms a list of 'user' to 'user@default_domain.com', then invokes enqueue_email.""" 90 | domain = Settings['mail']['default_domain'] 91 | recipients = ['%s@%s' % (recipient, domain) if '@' not in recipient else recipient for recipient in recipients] 92 | return cls.enqueue_email(recipients, *args, **kwargs) 93 | 94 | __all__ = ['MailQueue'] 95 | -------------------------------------------------------------------------------- /pushmanager/core/pid.py: -------------------------------------------------------------------------------- 1 | # pid.py - module to help manage PID files 2 | import errno 3 | import fcntl 4 | import logging 5 | import os 6 | 7 | 8 | def is_process_alive(pid): 9 | """Sends null signal to a process to check if it's alive""" 10 | try: 11 | # Sending the null signal (sig. 0) to the process will check 12 | # pid's validity. 13 | os.kill(pid, 0) 14 | except OSError, e: 15 | # Access denied, but process is alive 16 | return e.errno == errno.EPERM 17 | except: 18 | return False 19 | else: 20 | return True 21 | 22 | 23 | def kill_processes(pids): 24 | while pids: 25 | pid = pids.pop() 26 | if is_process_alive(pid): 27 | try: 28 | logging.info("Sending SIGKILL to PID: %d" % pid) 29 | os.kill(pid, 9) 30 | except OSError, e: 31 | if e.errno == errno.ESRCH: 32 | # process is dead already, no need to do anything 33 | pass 34 | else: 35 | raise 36 | else: 37 | # We'll check if the process is dead in a later iteration 38 | pids.insert(0, pid) 39 | 40 | 41 | def check(path): 42 | try: 43 | logging.info("Checking pidfile '%s'", path) 44 | pids = [int(pid) for pid in open(path).read().strip().split(' ')] 45 | kill_processes(pids) 46 | except IOError, (code, text): 47 | if code == errno.ENOENT: 48 | logging.warning("pidfile '%s' not found" % path) 49 | else: 50 | raise 51 | 52 | 53 | def write(path, append=False, pid=None): 54 | try: 55 | if pid is None: 56 | pid = os.getpid() 57 | if append: 58 | pidfile = open(path, 'a+b') 59 | else: 60 | pidfile = open(path, 'wb') 61 | # get a blocking exclusive lock, we may have multiple 62 | # processes updating this pid file. 63 | fcntl.flock(pidfile.fileno(), fcntl.LOCK_EX) 64 | if append: 65 | pidfile.write(" %d" % pid) 66 | else: 67 | # clear out the file 68 | pidfile.seek(0) 69 | pidfile.truncate(0) 70 | # write the pid 71 | pidfile.write(str(pid)) 72 | logging.info("Writing PID %s to '%s'", pid, path) 73 | except: 74 | raise 75 | finally: 76 | try: 77 | pidfile.close() 78 | except: 79 | pass 80 | 81 | 82 | def remove(path): 83 | try: 84 | # make sure we delete our pidfile 85 | logging.info("Removing pidfile '%s'", path) 86 | os.unlink(path) 87 | except: 88 | pass 89 | -------------------------------------------------------------------------------- /pushmanager/core/rb.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import httplib 3 | import json 4 | import logging 5 | import time 6 | from multiprocessing import JoinableQueue 7 | from multiprocessing import Process 8 | from urllib import urlencode 9 | 10 | from pushmanager.core.settings import Settings 11 | 12 | 13 | class RBQueue(object): 14 | 15 | review_queue = None 16 | worker_process = None 17 | 18 | @classmethod 19 | def start_worker(cls): 20 | if cls.worker_process is not None: 21 | return [] 22 | cls.review_queue = JoinableQueue() 23 | cls.worker_process = Process(target=cls.process_queue, name='rb-queue') 24 | cls.worker_process.daemon = True 25 | cls.worker_process.start() 26 | return [cls.worker_process.pid] 27 | 28 | @classmethod 29 | def process_queue(cls): 30 | while True: 31 | time.sleep(1) 32 | 33 | review_id = cls.review_queue.get() 34 | try: 35 | cls.mark_review_as_submitted(review_id) 36 | except Exception: 37 | logging.error( 38 | "ReviewBoard queue worker encountered an error (review_id: %r)", 39 | review_id, exc_info=True 40 | ) 41 | finally: 42 | cls.review_queue.task_done() 43 | 44 | @classmethod 45 | def mark_review_as_submitted(cls, review_id): 46 | credentials = base64.b64encode("%s:%s" % ( 47 | Settings['reviewboard']['username'], 48 | Settings['reviewboard']['password'], 49 | )) 50 | 51 | headers = { 52 | 'Accept': 'application/json', 53 | 'Authorization': 'Basic %s' % credentials, 54 | 'Content-Type': 'application/x-www-form-urlencoded', 55 | } 56 | 57 | data = urlencode({'status': 'submitted'}) 58 | 59 | conn = httplib.HTTPSConnection(Settings['reviewboard']['servername']) 60 | conn.request("PUT", "/api/review-requests/%d/" % review_id, data, headers) 61 | raw_result = conn.getresponse().read() 62 | try: 63 | result = json.loads(raw_result) 64 | except Exception: 65 | result = None 66 | conn.close() 67 | 68 | if not result or result.get('stat') != 'ok': 69 | logging.error( 70 | "Unable to mark review %r as submitted (%r)", 71 | review_id, 72 | raw_result, 73 | exc_info=True 74 | ) 75 | 76 | @classmethod 77 | def enqueue_review(cls, review_id): 78 | cls.review_queue.put(review_id) 79 | 80 | __all__ = ['RBQueue'] 81 | -------------------------------------------------------------------------------- /pushmanager/core/requesthandler.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | import json 3 | import urllib 4 | import urlparse 5 | 6 | import tornado.httpclient 7 | import tornado.stack_context 8 | import tornado.web 9 | 10 | from pushmanager.core.settings import JSSettings 11 | from pushmanager.core.settings import Settings 12 | 13 | 14 | @contextlib.contextmanager 15 | def async_api_call_error(): 16 | try: 17 | yield 18 | except Exception as e: 19 | if e[0] == "Stream is closed": 20 | # Client drops request before waiting for a response. You 21 | # can have this using Chrome pressing CTRL+r/Cmd+r 22 | # continuously. No need to log this as an error. 23 | pass 24 | else: 25 | raise 26 | 27 | 28 | def get_base_url(request): 29 | 30 | default_ports = {'https': ':443', 'http': ':80'} 31 | protocol = request.headers.get('X-Forwarded-Proto', request.protocol).lower() 32 | pushmanager_port = ':%s' % request.headers.get('X-Forwarded-Port', Settings['main_app']['port']) 33 | if default_ports[protocol] == pushmanager_port: 34 | pushmanager_port = '' 35 | 36 | pushmanager_base_url = '%(protocol)s://%(pushmanager_servername)s%(pushmanager_port)s' % { 37 | 'protocol': protocol, 38 | 'pushmanager_servername': Settings['main_app']['servername'], 39 | 'pushmanager_port': pushmanager_port 40 | } 41 | return pushmanager_base_url 42 | 43 | 44 | class RequestHandler(tornado.web.RequestHandler): 45 | 46 | def get_current_user(self): 47 | return self.get_secure_cookie("user") 48 | 49 | @staticmethod 50 | def get_api_page(method): 51 | host = "%s:%d" % ( 52 | Settings['api_app']['servername'], 53 | Settings['api_app']['port'], 54 | ) 55 | path = "api/%s" % method 56 | return urlparse.urlunsplit(( 57 | "http", 58 | host, 59 | path, 60 | '', 61 | '' 62 | )) 63 | 64 | def async_api_call(self, method, arguments, callback): 65 | self.http = tornado.httpclient.AsyncHTTPClient() 66 | with tornado.stack_context.StackContext(async_api_call_error): 67 | self.http.fetch( 68 | self.get_api_page(method), 69 | callback, 70 | method="POST", 71 | body=urllib.urlencode(arguments) 72 | ) 73 | 74 | def get_base_url(self): 75 | return get_base_url(self.request) 76 | 77 | def get_api_results(self, response): 78 | if response.error: 79 | return self.send_error() 80 | 81 | try: 82 | return json.loads(response.body) 83 | except ValueError: 84 | return self.send_error(500) 85 | 86 | def check_db_results(self, success, db_results): 87 | assert success, "Database error." 88 | 89 | def render(self, templ, **kwargs): 90 | # These are passed to templates and JSSettings should have 91 | # enough configuration information for templates too. Just 92 | # binding JSSettings as Settings and letting templates to use 93 | # it. JSSetting is just a subset of the Settings dictionary 94 | # and is safe to pass around. 95 | kwargs.setdefault('Settings', JSSettings) 96 | kwargs.setdefault('JSSettings_json', json.dumps(JSSettings, sort_keys=True)) 97 | super(RequestHandler, self).render(templ, **kwargs) 98 | 99 | 100 | __all__ = ['RequestHandler'] 101 | -------------------------------------------------------------------------------- /pushmanager/core/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import logging 3 | import os 4 | import sys 5 | 6 | import yaml 7 | from pushmanager.core.util import dict_copy_keys 8 | 9 | 10 | configuration_file = os.environ.get('SERVICE_ENV_CONFIG_PATH') 11 | 12 | Settings = {} 13 | 14 | try: 15 | with open(configuration_file) as settings_yaml: 16 | Settings = yaml.safe_load(settings_yaml) 17 | except: 18 | logging.error("Can not load configuration from '%s'." % configuration_file) 19 | sys.exit(1) 20 | 21 | # JS files in static/js need to know some of the configuration options 22 | # too, but we do not have to export everything, just what's 23 | # needed. This is what's needed. We're only setting up/defining keys 24 | # here and will copy values from Settings. 25 | JSSettings = { 26 | 'main_app': { 27 | 'servername': None, 28 | 'port': None, 29 | }, 30 | 'buildbot': { 31 | 'servername': None, 32 | }, 33 | 'reviewboard': { 34 | 'servername': None, 35 | }, 36 | 'ticket_tracker_url_format': None, 37 | 'git': { 38 | 'main_repository': None, 39 | }, 40 | 'check_sites_bookmarklet': None, 41 | 'tests_tag': { 42 | 'tag': None, 43 | 'push_test_label': None, 44 | 'push_url_tmpl': None, 45 | }, 46 | } 47 | 48 | dict_copy_keys(to_dict=JSSettings, from_dict=Settings) 49 | 50 | __all__ = ['Settings', 'JSSettings'] 51 | -------------------------------------------------------------------------------- /pushmanager/pushmanager_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import with_statement 3 | 4 | import os 5 | 6 | import pushmanager.core.db as db 7 | import tornado.httpserver 8 | import tornado.process 9 | import pushmanager.ui_modules as ui_modules 10 | from pushmanager.core.application import Application 11 | from pushmanager.core.settings import Settings 12 | from pushmanager.core.util import get_servlet_urlspec 13 | from pushmanager.servlets.api import APIServlet 14 | 15 | 16 | api_application = tornado.web.Application( 17 | # Servlet dispatch rules 18 | [ 19 | get_servlet_urlspec(APIServlet), 20 | ], 21 | # Server settings 22 | static_path=os.path.join(os.path.dirname(__file__), "static"), 23 | template_path=os.path.join(os.path.dirname(__file__), "templates"), 24 | login_url="/login", 25 | cookie_secret=Settings['cookie_secret'], 26 | ui_modules=ui_modules, 27 | autoescape=None, 28 | ) 29 | 30 | 31 | class PushManagerAPIApp(Application): 32 | name = "api" 33 | 34 | def start_services(self): 35 | # HTTP server (for api) 36 | sockets = tornado.netutil.bind_sockets(self.port, address=Settings['api_app']['servername']) 37 | tornado.process.fork_processes(Settings['tornado']['num_workers']) 38 | server = tornado.httpserver.HTTPServer(api_application) 39 | server.add_sockets(sockets) 40 | 41 | 42 | def main(): 43 | app = PushManagerAPIApp() 44 | db.init_db() 45 | app.run() 46 | 47 | 48 | if __name__ == '__main__': 49 | main() 50 | -------------------------------------------------------------------------------- /pushmanager/servlets/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/servlets/__init__.py -------------------------------------------------------------------------------- /pushmanager/servlets/addrequest.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.db as db 2 | import pushmanager.core.util 3 | from pushmanager.core.db import InsertIgnore 4 | from pushmanager.core.mail import MailQueue 5 | from pushmanager.core.requesthandler import RequestHandler 6 | from pushmanager.core.xmppclient import XMPPQueue 7 | 8 | 9 | class AddRequestServlet(RequestHandler): 10 | 11 | def post(self): 12 | if not self.current_user: 13 | return self.send_error(403) 14 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 15 | self.request_ids = self.request.arguments.get('request', []) 16 | 17 | insert_queries = [ 18 | InsertIgnore(db.push_pushcontents, ({'request': int(i), 'push': self.pushid})) 19 | for i in self.request_ids 20 | ] 21 | update_query = db.push_requests.update().where( 22 | db.push_requests.c.id.in_(self.request_ids)).values({'state': 'added'}) 23 | request_query = db.push_requests.select().where( 24 | db.push_requests.c.id.in_(self.request_ids)) 25 | 26 | db.execute_transaction_cb(insert_queries + [update_query, request_query], self.on_db_complete) 27 | 28 | # allow both GET and POST 29 | get = post 30 | 31 | def on_db_complete(self, success, db_results): 32 | self.check_db_results(success, db_results) 33 | 34 | for req in db_results[-1]: 35 | if req['watchers']: 36 | user_string = '%s (%s)' % (req['user'], req['watchers']) 37 | users = [req['user']] + req['watchers'].split(',') 38 | else: 39 | user_string = req['user'] 40 | users = [req['user']] 41 | msg = ( 42 | """ 43 |

44 | %(pushmaster)s has accepted request for %(user)s into a push: 45 |

46 |

47 | %(user)s - %(title)s
48 | %(repo)s/%(branch)s 49 |

50 |

51 | Regards,
52 | PushManager 53 |

""" 54 | ) % pushmanager.core.util.EscapedDict({ 55 | 'pushmaster': self.current_user, 56 | 'user': user_string, 57 | 'title': req['title'], 58 | 'repo': req['repo'], 59 | 'branch': req['branch'], 60 | }) 61 | subject = "[push] %s - %s" % (user_string, req['title']) 62 | MailQueue.enqueue_user_email(users, msg, subject) 63 | msg = '{0} has accepted request "{1}" for {2} into a push:\n{3}/push?id={4}'.format( 64 | self.current_user, 65 | req['title'], 66 | user_string, 67 | self.get_base_url(), 68 | self.pushid, 69 | ) 70 | XMPPQueue.enqueue_user_xmpp(users, msg) 71 | -------------------------------------------------------------------------------- /pushmanager/servlets/blesspush.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.mail import MailQueue 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.xmppclient import XMPPQueue 8 | 9 | 10 | class BlessPushServlet(RequestHandler): 11 | 12 | def _arg(self, key): 13 | return pushmanager.core.util.get_str_arg(self.request, key, '') 14 | 15 | def post(self): 16 | if not self.current_user: 17 | return self.send_error(403) 18 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 19 | request_query = db.push_requests.update().where(SA.and_( 20 | db.push_requests.c.state.in_(['staged', 'verified']), 21 | SA.exists( 22 | [1], 23 | SA.and_( 24 | db.push_pushcontents.c.push == self.pushid, 25 | db.push_pushcontents.c.request == db.push_requests.c.id, 26 | ) 27 | ))).values({ 28 | 'state': 'blessed', 29 | }) 30 | blessed_query = db.push_requests.select().where( 31 | SA.and_(db.push_requests.c.state == 'blessed', 32 | db.push_pushcontents.c.push == self.pushid, 33 | db.push_pushcontents.c.request == db.push_requests.c.id) 34 | ) 35 | push_query = db.push_pushes.select().where( 36 | db.push_pushes.c.id == self.pushid, 37 | ) 38 | db.execute_transaction_cb([request_query, blessed_query, push_query], self.on_db_complete) 39 | 40 | def on_db_complete(self, success, db_results): 41 | self.check_db_results(success, db_results) 42 | 43 | _, blessed_requests, push_results = db_results 44 | for req in blessed_requests: 45 | if req['watchers']: 46 | user_string = '%s (%s)' % (req['user'], req['watchers']) 47 | users = [req['user']] + req['watchers'].split(',') 48 | else: 49 | user_string = req['user'] 50 | users = [req['user']] 51 | msg = ( 52 | """ 53 |

54 | %(pushmaster)s has deployed request for %(user)s to production: 55 |

56 |

57 | %(user)s - %(title)s
58 | %(repo)s/%(branch)s 59 |

60 |

61 | Regards,
62 | PushManager 63 |

""" 64 | ) % pushmanager.core.util.EscapedDict({ 65 | 'pushmaster': self.current_user, 66 | 'user': user_string, 67 | 'title': req['title'], 68 | 'repo': req['repo'], 69 | 'branch': req['branch'], 70 | }) 71 | subject = "[push] %s - %s" % (user_string, req['title']) 72 | MailQueue.enqueue_user_email(users, msg, subject) 73 | msg = '%(pushmaster)s has deployed request "%(title)s" for %(user)s to production.' % { 74 | 'pushmaster': self.current_user, 75 | 'title': req['title'], 76 | 'user': user_string, 77 | } 78 | XMPPQueue.enqueue_user_xmpp(users, msg) 79 | 80 | push = push_results.fetchone() 81 | if push['extra_pings']: 82 | for user in push['extra_pings'].split(','): 83 | XMPPQueue.enqueue_user_xmpp([user], '%s has deployed a push to production.' % self.current_user) 84 | 85 | self.finish() 86 | -------------------------------------------------------------------------------- /pushmanager/servlets/commentrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.mail import MailQueue 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.xmppclient import XMPPQueue 8 | from tornado.escape import xhtml_escape 9 | 10 | 11 | class CommentRequestServlet(RequestHandler): 12 | 13 | def post(self): 14 | if not self.current_user: 15 | return self.send_error(403) 16 | 17 | requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 18 | comment = pushmanager.core.util.get_str_arg(self.request, 'comment') 19 | self.comment = comment 20 | if not comment: 21 | return self.send_error(500) 22 | 23 | update_query = db.push_requests.update().where( 24 | db.push_requests.c.id == requestid, 25 | ).values({ 26 | 'comments': SA.func.concat( 27 | db.push_requests.c.comments, 28 | '\n\n---\n\nComment from %s:\n\n' % self.current_user, 29 | comment, 30 | ), 31 | }) 32 | select_query = db.push_requests.select().where( 33 | db.push_requests.c.id == requestid, 34 | ) 35 | db.execute_transaction_cb([update_query, select_query], self.on_db_complete) 36 | 37 | get = post 38 | 39 | def on_db_complete(self, success, db_results): 40 | self.check_db_results(success, db_results) 41 | 42 | if db_results: 43 | req = db_results[1].first() 44 | msg = ( 45 | """ 46 |

47 | %(pushmaster)s has commented on your request: 48 |

49 |

50 | %(user)s - %(title)s
51 | %(repo)s/%(branch)s 52 |

53 |
54 | %(comment)s
55 |                 
56 |

57 | Regards,
58 | PushManager 59 |

""" 60 | ) % pushmanager.core.util.EscapedDict({ 61 | 'pushmaster': self.current_user, 62 | 'user': req['user'], 63 | 'title': req['title'], 64 | 'repo': req['repo'], 65 | 'branch': req['branch'], 66 | 'comment': self.comment, 67 | }) 68 | subject = "[push comment] %s - %s" % (req['user'], req['title']) 69 | MailQueue.enqueue_user_email([req['user']], msg, subject) 70 | msg = '%(pushmaster)s has commented on your request "%(title)s":\n%(comment)s' % { 71 | 'pushmaster': self.current_user, 72 | 'title': req['title'], 73 | 'comment': self.comment, 74 | } 75 | XMPPQueue.enqueue_user_xmpp([req['user']], msg) 76 | newcomments = req[db.push_requests.c.comments] 77 | self.write(xhtml_escape(newcomments)) 78 | -------------------------------------------------------------------------------- /pushmanager/servlets/conflictcheck.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | from pushmanager.core.git import GitQueue 3 | from pushmanager.core.git import GitTaskAction 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class ConflictCheckServlet(RequestHandler): 8 | 9 | def _arg(self, key): 10 | return pushmanager.core.util.get_str_arg(self.request, key, '') 11 | 12 | def post(self): 13 | if not self.current_user: 14 | return self.send_error(403) 15 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 16 | GitQueue.enqueue_request(GitTaskAction.TEST_ALL_PICKMES, self.pushid, pushmanager_url=self.get_base_url()) 17 | self.redirect("/push?id=%d" % self.pushid) 18 | -------------------------------------------------------------------------------- /pushmanager/servlets/delayrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.mail import MailQueue 6 | from pushmanager.core.requesthandler import RequestHandler 7 | 8 | 9 | class DelayRequestServlet(RequestHandler): 10 | 11 | def post(self): 12 | if not self.current_user: 13 | return self.send_error(403) 14 | 15 | self.requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 16 | update_query = db.push_requests.update().where(SA.and_( 17 | db.push_requests.c.id == self.requestid, 18 | db.push_requests.c.state.in_(('requested', 'pickme')), 19 | )).values({ 20 | 'state': 'delayed', 21 | }) 22 | delete_query = db.push_pushcontents.delete().where( 23 | SA.exists([1], SA.and_( 24 | db.push_pushcontents.c.request == db.push_requests.c.id, 25 | db.push_requests.c.state == 'delayed', 26 | ))) 27 | select_query = db.push_requests.select().where( 28 | db.push_requests.c.id == self.requestid, 29 | ) 30 | db.execute_transaction_cb([update_query, delete_query, select_query], self.on_db_complete) 31 | 32 | get = post 33 | 34 | def on_db_complete(self, success, db_results): 35 | self.check_db_results(success, db_results) 36 | 37 | _, _, req = db_results 38 | req = req.first() 39 | if req['state'] != 'delayed': 40 | # We didn't actually discard the record, for whatever reason 41 | return self.redirect("/requests?user=%s" % self.current_user) 42 | 43 | if req['watchers']: 44 | user_string = '%s (%s)' % (req['user'], req['watchers']) 45 | users = [req['user']] + req['watchers'].split(',') 46 | else: 47 | user_string = req['user'] 48 | users = [req['user']] 49 | msg = ( 50 | """ 51 |

52 | Request for %(user)s has been marked as delayed 53 | by %(pushmaster)s, and will not be accepted into 54 | pushes until you mark it as requested again: 55 |

56 |

57 | %(user)s - %(title)s
58 | %(repo)s/%(branch)s 59 |

60 |

61 | Regards,
62 | PushManager 63 |

""" 64 | ) % pushmanager.core.util.EscapedDict({ 65 | 'pushmaster': self.current_user, 66 | 'user': user_string, 67 | 'title': req['title'], 68 | 'repo': req['repo'], 69 | 'branch': req['branch'], 70 | }) 71 | subject = "[push] %s - %s" % (user_string, req['title']) 72 | MailQueue.enqueue_user_email(users, msg, subject) 73 | 74 | self.redirect("/requests?user=%s" % self.current_user) 75 | -------------------------------------------------------------------------------- /pushmanager/servlets/deploypush.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.mail import MailQueue 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.xmppclient import XMPPQueue 8 | 9 | 10 | class DeployPushServlet(RequestHandler): 11 | 12 | def _arg(self, key): 13 | return pushmanager.core.util.get_str_arg(self.request, key, '') 14 | 15 | def post(self): 16 | if not self.current_user: 17 | return self.send_error(403) 18 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 19 | request_query = db.push_requests.update().where( 20 | SA.and_( 21 | db.push_requests.c.state == 'added', 22 | SA.exists( 23 | [1], 24 | SA.and_( 25 | db.push_pushcontents.c.push == self.pushid, 26 | db.push_pushcontents.c.request == db.push_requests.c.id, 27 | ) 28 | ) 29 | )).values({ 30 | 'state': 'staged', 31 | }) 32 | staged_query = db.push_requests.select().where( 33 | SA.and_(db.push_requests.c.state == 'staged', 34 | db.push_pushcontents.c.push == self.pushid, 35 | db.push_pushcontents.c.request == db.push_requests.c.id) 36 | ) 37 | push_query = db.push_pushes.select().where( 38 | db.push_pushes.c.id == self.pushid, 39 | ) 40 | db.execute_transaction_cb([request_query, staged_query, push_query], self.on_db_complete) 41 | 42 | def on_db_complete(self, success, db_results): 43 | self.check_db_results(success, db_results) 44 | 45 | _, staged_requests, push_result = db_results 46 | push = push_result.fetchone() 47 | 48 | for req in staged_requests: 49 | if req['watchers']: 50 | user_string = '%s (%s)' % (req['user'], req['watchers']) 51 | users = [req['user']] + req['watchers'].split(',') 52 | else: 53 | user_string = req['user'] 54 | users = [req['user']] 55 | msg = ( 56 | """ 57 |

58 | %(pushmaster)s has deployed request for %(user)s to %(pushstage)s: 59 |

60 |

61 | %(user)s - %(title)s
62 | %(repo)s/%(branch)s 63 |

64 |

65 | Once you've checked that it works, mark it as verified here: 66 | 67 | %(pushmanager_base_url)s/push?id=%(pushid)s 68 | 69 |

70 |

71 | Regards,
72 | PushManager 73 |

""" 74 | ) % pushmanager.core.util.EscapedDict({ 75 | 'pushmaster': self.current_user, 76 | 'pushmanager_base_url': self.get_base_url(), 77 | 'user': user_string, 78 | 'title': req['title'], 79 | 'repo': req['repo'], 80 | 'branch': req['branch'], 81 | 'pushid': self.pushid, 82 | 'pushstage': push['stageenv'], 83 | }) 84 | subject = "[push] %s - %s" % (user_string, req['title']) 85 | MailQueue.enqueue_user_email(users, msg, subject) 86 | 87 | msg = '{0} has deployed request "{1}" for {2} to {3}.\nPlease verify it at {4}/push?id={5}'.format( 88 | self.current_user, 89 | req['title'], 90 | user_string, 91 | push['stageenv'], 92 | self.get_base_url(), 93 | self.pushid, 94 | ) 95 | XMPPQueue.enqueue_user_xmpp(users, msg) 96 | 97 | if push['extra_pings']: 98 | for user in push['extra_pings'].split(','): 99 | XMPPQueue.enqueue_user_xmpp([user], '%s has deployed a push to stage.' % self.current_user) 100 | -------------------------------------------------------------------------------- /pushmanager/servlets/discardpush.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import sqlalchemy as SA 4 | 5 | import pushmanager.core.db as db 6 | import pushmanager.core.util 7 | from pushmanager.core.requesthandler import RequestHandler 8 | 9 | 10 | class DiscardPushServlet(RequestHandler): 11 | 12 | def _arg(self, key): 13 | return pushmanager.core.util.get_str_arg(self.request, key, '') 14 | 15 | def post(self): 16 | if not self.current_user: 17 | return self.send_error(403) 18 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 19 | push_query = db.push_pushes.update().where(db.push_pushes.c.id == self.pushid).values({ 20 | 'state': 'discarded', 21 | 'modified': time.time(), 22 | }) 23 | request_query_pickme = db.push_requests.update().where( 24 | SA.exists( 25 | [1], 26 | SA.and_( 27 | db.push_pushcontents.c.push == self.pushid, 28 | db.push_pushcontents.c.request == db.push_requests.c.id, 29 | db.push_requests.c.state == 'pickme', 30 | ) 31 | )).values({ 32 | 'state': 'requested', 33 | }) 34 | delete_query = db.push_pushcontents.delete().where( 35 | SA.exists( 36 | [1], 37 | SA.and_( 38 | db.push_pushcontents.c.push == self.pushid, 39 | db.push_pushcontents.c.request == db.push_requests.c.id, 40 | db.push_requests.c.state == 'requested', 41 | ) 42 | )) 43 | request_query_all = db.push_requests.update().where( 44 | SA.exists( 45 | [1], 46 | SA.and_( 47 | db.push_pushcontents.c.push == self.pushid, 48 | db.push_pushcontents.c.request == db.push_requests.c.id, 49 | ) 50 | )).values({ 51 | 'state': 'requested', 52 | }) 53 | db.execute_transaction_cb( 54 | [push_query, request_query_pickme, delete_query, request_query_all], 55 | self.on_db_complete 56 | ) 57 | 58 | def on_db_complete(self, success, db_results): 59 | self.check_db_results(success, db_results) 60 | -------------------------------------------------------------------------------- /pushmanager/servlets/discardrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.mail import MailQueue 6 | from pushmanager.core.requesthandler import RequestHandler 7 | 8 | 9 | class DiscardRequestServlet(RequestHandler): 10 | 11 | def post(self): 12 | if not self.current_user: 13 | return self.send_error(403) 14 | self.requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 15 | update_query = db.push_requests.update().where(SA.and_( 16 | db.push_requests.c.id == self.requestid, 17 | db.push_requests.c.user == self.current_user, 18 | db.push_requests.c.state.in_(['requested', 'delayed']), 19 | )).values({ 20 | 'state': 'discarded', 21 | }) 22 | select_query = db.push_requests.select().where( 23 | db.push_requests.c.id == self.requestid, 24 | ) 25 | db.execute_transaction_cb([update_query, select_query], self.on_db_complete) 26 | # allow both GET and POST 27 | get = post 28 | 29 | def on_db_complete(self, success, db_results): 30 | self.check_db_results(success, db_results) 31 | 32 | _, req = db_results 33 | req = req.first() 34 | if req['state'] != 'discarded': 35 | # We didn't actually discard the record, for whatever reason 36 | return self.redirect("/requests?user=%s" % self.current_user) 37 | 38 | if req['watchers']: 39 | user_string = '%s (%s)' % (req['user'], req['watchers']) 40 | users = [req['user']] + req['watchers'].split(',') 41 | else: 42 | user_string = req['user'] 43 | users = [req['user']] 44 | msg = ( 45 | """ 46 |

47 | Request for %(user)s has been discarded: 48 |

49 |

50 | %(user)s - %(title)s
51 | %(repo)s/%(branch)s 52 |

53 |

54 | Regards,
55 | PushManager 56 |

""" 57 | ) % pushmanager.core.util.EscapedDict({ 58 | 'pushmaster': self.current_user, 59 | 'user': user_string, 60 | 'title': req['title'], 61 | 'repo': req['repo'], 62 | 'branch': req['branch'], 63 | }) 64 | subject = "[push] %s - %s" % (user_string, req['title']) 65 | MailQueue.enqueue_user_email(users, msg, subject) 66 | 67 | self.redirect("/requests?user=%s" % self.current_user) 68 | -------------------------------------------------------------------------------- /pushmanager/servlets/editpush.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class EditPushServlet(RequestHandler): 9 | 10 | def _arg(self, key): 11 | return pushmanager.core.util.get_str_arg(self.request, key, '') 12 | 13 | def post(self): 14 | if not self.current_user: 15 | return self.send_error(403) 16 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 17 | query = db.push_pushes.update().where(db.push_pushes.c.id == self.pushid).values(**{ 18 | 'title': self._arg('push-title'), 19 | 'user': self.current_user, 20 | 'branch': self._arg('push-branch'), 21 | 'stageenv': self._arg('push-stageenv'), 22 | 'revision': "0"*40, 23 | 'modified': time.time(), 24 | }) 25 | db.execute_cb( 26 | query, 27 | lambda _, __: self.redirect("/push?id=%d" % self.pushid) 28 | ) 29 | -------------------------------------------------------------------------------- /pushmanager/servlets/livepush.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import sqlalchemy as SA 4 | 5 | import pushmanager.core.db as db 6 | import pushmanager.core.util 7 | from pushmanager.core.mail import MailQueue 8 | from pushmanager.core.rb import RBQueue 9 | from pushmanager.core.requesthandler import RequestHandler 10 | 11 | 12 | class LivePushServlet(RequestHandler): 13 | 14 | def _arg(self, key): 15 | return pushmanager.core.util.get_str_arg(self.request, key, '') 16 | 17 | def post(self): 18 | if not self.current_user: 19 | return self.send_error(403) 20 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 21 | push_query = db.push_pushes.update().where(db.push_pushes.c.id == self.pushid).values({ 22 | 'state': 'live', 23 | 'modified': time.time(), 24 | }) 25 | request_query = db.push_requests.update().where(SA.and_( 26 | db.push_requests.c.state == 'blessed', 27 | SA.exists( 28 | [1], 29 | SA.and_( 30 | db.push_pushcontents.c.push == self.pushid, 31 | db.push_pushcontents.c.request == db.push_requests.c.id, 32 | ) 33 | ))).values({ 34 | 'state': 'live', 35 | 'modified': time.time(), 36 | }) 37 | reset_query = db.push_requests.update().where( 38 | SA.exists( 39 | [1], 40 | SA.and_( 41 | db.push_requests.c.state == 'pickme', 42 | db.push_pushcontents.c.push == self.pushid, 43 | db.push_pushcontents.c.request == db.push_requests.c.id, 44 | ) 45 | )).values({ 46 | 'state': 'requested', 47 | }) 48 | delete_query = db.push_pushcontents.delete().where( 49 | SA.exists([1], SA.and_( 50 | db.push_pushcontents.c.push == self.pushid, 51 | db.push_pushcontents.c.request == db.push_requests.c.id, 52 | db.push_requests.c.state == 'requested', 53 | ))) 54 | live_query = db.push_requests.select().where( 55 | SA.and_(db.push_requests.c.state == 'live', 56 | db.push_pushcontents.c.push == self.pushid, 57 | db.push_pushcontents.c.request == db.push_requests.c.id) 58 | ) 59 | db.execute_transaction_cb( 60 | [push_query, request_query, reset_query, delete_query, live_query], 61 | self.on_db_complete, 62 | ) 63 | 64 | def on_db_complete(self, success, db_results): 65 | self.check_db_results(success, db_results) 66 | 67 | _, _, _, _, live_requests = db_results 68 | for req in live_requests: 69 | if req['reviewid']: 70 | review_id = int(req['reviewid']) 71 | RBQueue.enqueue_review(review_id) 72 | 73 | if req['watchers']: 74 | user_string = '%s (%s)' % (req['user'], req['watchers']) 75 | users = [req['user']] + req['watchers'].split(',') 76 | else: 77 | user_string = req['user'] 78 | users = [req['user']] 79 | 80 | msg = ( 81 | """ 82 |

83 | %(pushmaster)s has certified request for %(user)s as stable in production: 84 |

85 |

86 | %(user)s - %(title)s
87 | %(repo)s/%(branch)s 88 |

89 |

90 | Regards,
91 | PushManager 92 |

""" 93 | ) % pushmanager.core.util.EscapedDict({ 94 | 'pushmaster': self.current_user, 95 | 'user': user_string, 96 | 'title': req['title'], 97 | 'repo': req['repo'], 98 | 'branch': req['branch'], 99 | }) 100 | subject = "[push] %s - %s" % (user_string, req['title']) 101 | MailQueue.enqueue_user_email(users, msg, subject) 102 | -------------------------------------------------------------------------------- /pushmanager/servlets/msg.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | from pushmanager.core.requesthandler import RequestHandler 4 | from pushmanager.core.settings import Settings 5 | from pushmanager.core.util import send_people_msg_in_groups 6 | from pushmanager.core import db 7 | import sqlalchemy as SA 8 | 9 | 10 | class MsgServlet(RequestHandler): 11 | people = [] 12 | 13 | def post(self): 14 | if not self.current_user: 15 | return self.send_error(403) 16 | 17 | push_id = self.request.arguments.get('id', [None])[0] 18 | if not push_id: 19 | return self.send_error(404) 20 | contents_query = self.generate_pushcontent_query(push_id) 21 | db.execute_cb(contents_query, self.get_push_request_users) 22 | people = self.people 23 | message = self.request.arguments.get('message', [None])[0] 24 | if not message: 25 | return self.send_error(500) 26 | 27 | irc_nick = Settings['irc']['nickname'].format( 28 | pushmaster=self.current_user 29 | ) 30 | 31 | if not people: 32 | irc_message = u'[[pushmaster {0}]] {1}'.format( 33 | self.current_user, 34 | message, 35 | ) 36 | 37 | subprocess.call([ 38 | '/nail/sys/bin/nodebot', 39 | '-i', 40 | irc_nick, 41 | Settings['irc']['channel'], 42 | irc_message 43 | ]) 44 | return 45 | 46 | send_people_msg_in_groups( 47 | people, 48 | message, 49 | irc_nick, 50 | Settings['irc']['channel'], 51 | person_per_group=5, 52 | prefix_msg='[[pushmaster %s]]' % self.current_user 53 | ) 54 | 55 | def generate_pushcontent_query(self, push_id): 56 | state = self.request.arguments.get('state', [None])[0] 57 | contents_query = '' 58 | if state == 'requested': 59 | contents_query = db.push_requests.select( 60 | db.push_pushcontents.c.push == push_id 61 | ) 62 | else: 63 | contents_query = db.push_requests.select( 64 | SA.and_( 65 | db.push_requests.c.id == db.push_pushcontents.c.request, 66 | db.push_pushcontents.c.push == push_id, 67 | ), 68 | order_by=(db.push_requests.c.user, db.push_requests.c.title) 69 | ) 70 | return contents_query 71 | 72 | def get_push_request_users(self, success, db_results): 73 | request_list = self.filter_request_by_state(success, db_results) 74 | people = [] 75 | for request in request_list: 76 | people.append((request['user'])) 77 | for watcher in request['watchers'].split(','): 78 | people.append((watcher)) 79 | people = list(set(people)) 80 | people = filter(None, people) 81 | self.people = people 82 | 83 | def filter_request_by_state(self, success, db_results): 84 | user_requests = [] 85 | state = self.request.arguments.get('state', [None])[0] 86 | for request in db_results: 87 | if (state == 'all' and request['state'] != 'pickme') or ( 88 | state == request['state']): 89 | user_requests.append(request) 90 | return user_requests 91 | -------------------------------------------------------------------------------- /pushmanager/servlets/newpush.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import time 3 | 4 | import pushmanager.core.db as db 5 | import pushmanager.core.util 6 | from pushmanager.core.mail import MailQueue 7 | from pushmanager.core.requesthandler import RequestHandler 8 | from pushmanager.core.settings import Settings 9 | from pushmanager.core.xmppclient import XMPPQueue 10 | from pushmanager.core.util import send_people_msg_in_groups 11 | 12 | 13 | def send_notifications(people, pushtype, pushmanager_url): 14 | pushmanager_servername = Settings['main_app']['servername'] 15 | 16 | if people: 17 | msg = '%s: %s push starting! %s' % (', '.join(people), pushtype, pushmanager_url) 18 | XMPPQueue.enqueue_user_xmpp(people, 'Push starting! %s' % pushmanager_url) 19 | elif pushtype == 'morning': 20 | msg = 'Morning push opened. %s' % pushmanager_servername 21 | else: 22 | msg = 'push starting. %s' % pushmanager_url 23 | 24 | if people: 25 | send_people_msg_in_groups( 26 | people, "%s push starting! %s" % (pushtype, pushmanager_url), 27 | Settings['irc']['nickname'], Settings['irc']['channel'], 28 | person_per_group=5, prefix_msg='' 29 | ) 30 | else: 31 | subprocess.call([ 32 | '/nail/sys/bin/nodebot', 33 | '-i', 34 | Settings['irc']['nickname'], 35 | Settings['irc']['channel'], 36 | msg 37 | ]) 38 | 39 | subject = "New push notification" 40 | MailQueue.enqueue_user_email(Settings['mail']['notifyall'], msg, subject) 41 | 42 | 43 | class NewPushServlet(RequestHandler): 44 | 45 | def _arg(self, key): 46 | return pushmanager.core.util.get_str_arg(self.request, key, '') 47 | 48 | def post(self): 49 | if not self.current_user: 50 | return self.send_error(403) 51 | self.pushtype = self._arg('push-type') 52 | insert_query = db.push_pushes.insert({ 53 | 'title': self._arg('push-title'), 54 | 'user': self.current_user, 55 | 'branch': self._arg('push-branch').strip(), 56 | 'revision': "0"*40, 57 | 'created': time.time(), 58 | 'modified': time.time(), 59 | 'state': 'accepting', 60 | 'pushtype': self.pushtype, 61 | }) 62 | select_query = db.push_requests.select().where( 63 | db.push_requests.c.state == 'requested', 64 | ) 65 | db.execute_transaction_cb([insert_query, select_query], self.on_db_complete) 66 | 67 | get = post 68 | 69 | def on_db_complete(self, success, db_results): 70 | self.check_db_results(success, db_results) 71 | 72 | insert_results, select_results = db_results 73 | pushurl = '/push?id=%s' % insert_results.lastrowid 74 | pushmanager_url = self.get_base_url() + pushurl 75 | 76 | def users_involved(request): 77 | if request['watchers']: 78 | return [request['user']] + request['watchers'].split(',') 79 | return [request['user']] 80 | 81 | if self.pushtype in ('private', 'morning'): 82 | people = None 83 | elif self.pushtype == 'urgent': 84 | people = set(user for x in select_results for user in users_involved(x) if 'urgent' in x['tags'].split(',')) 85 | else: 86 | people = set(user for x in select_results for user in users_involved(x)) 87 | 88 | send_notifications(people, self.pushtype, pushmanager_url) 89 | 90 | return self.redirect(pushurl) 91 | -------------------------------------------------------------------------------- /pushmanager/servlets/pickmerequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.git import GitQueue 6 | from pushmanager.core.git import GitTaskAction 7 | from pushmanager.core.requesthandler import RequestHandler 8 | 9 | 10 | class PickMeRequestServlet(RequestHandler): 11 | 12 | def post(self): 13 | if not self.current_user: 14 | return self.send_error(403) 15 | 16 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 17 | self.request_ids = self.request.arguments.get('request', []) 18 | 19 | db.execute_cb(db.push_pushes.select().where(db.push_pushes.c.id == self.pushid), self.on_push_select) 20 | 21 | def on_push_select(self, success, db_results): 22 | if not success or not db_results: 23 | return self.send_error(500) 24 | 25 | pushrow = db_results.fetchone() 26 | if not pushrow: 27 | return self.send_error(500) 28 | 29 | if pushrow[db.push_pushes.c.state] != 'accepting': 30 | return self.send_error(403) 31 | 32 | insert_queries = [ 33 | db.push_pushcontents.insert({ 34 | 'request': int(i), 35 | 'push': self.pushid 36 | }) for i in self.request_ids 37 | ] 38 | update_query = db.push_requests.update().where(SA.and_( 39 | db.push_requests.c.id.in_(self.request_ids), 40 | db.push_requests.c.state == 'requested', 41 | )).values({'state': 'pickme'}) 42 | request_query = db.push_requests.select().where( 43 | db.push_requests.c.id.in_(self.request_ids)) 44 | 45 | condition_query = SA.select( 46 | [db.push_pushes, db.push_pushcontents], 47 | SA.and_( 48 | db.push_pushcontents.c.request.in_(self.request_ids), 49 | db.push_pushes.c.id == db.push_pushcontents.c.push, 50 | db.push_pushes.c.state != 'discarded' 51 | ) 52 | ) 53 | 54 | def condition_fn(db_results): 55 | return db_results.fetchall() == [] 56 | 57 | db.execute_transaction_cb( 58 | insert_queries + [update_query, request_query], 59 | self.on_db_complete, 60 | condition=(condition_query, condition_fn) 61 | ) 62 | 63 | # allow both GET and POST 64 | get = post 65 | 66 | def on_db_complete(self, success, db_results): 67 | self.check_db_results(success, db_results) 68 | for request_id in self.request_ids: 69 | GitQueue.enqueue_request( 70 | GitTaskAction.TEST_PICKME_CONFLICT, 71 | request_id, 72 | pushmanager_url=self.get_base_url() 73 | ) 74 | 75 | 76 | class UnpickMeRequestServlet(RequestHandler): 77 | 78 | def post(self): 79 | if not self.current_user: 80 | return self.send_error(403) 81 | 82 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 83 | self.request_id = self.request.arguments.get('request', [None])[0] 84 | delete_query = db.push_pushcontents.delete().where( 85 | SA.exists([1], SA.and_( 86 | db.push_pushcontents.c.request == self.request_id, 87 | db.push_pushcontents.c.push == self.pushid, 88 | db.push_requests.c.id == db.push_pushcontents.c.request, 89 | db.push_requests.c.state == 'pickme', 90 | ))) 91 | update_query = db.push_requests.update().where(SA.and_( 92 | db.push_requests.c.id == self.request_id, 93 | db.push_requests.c.state == 'pickme', 94 | )).values({'state': 'requested'}) 95 | 96 | db.execute_transaction_cb([delete_query, update_query], self.on_db_complete) 97 | 98 | # allow both GET and POST 99 | get = post 100 | 101 | def on_db_complete(self, success, db_results): 102 | self.check_db_results(success, db_results) 103 | # Re-check pickmes that are marked as conflicting, in case this was the pickme 104 | # that they conflicted against. 105 | GitQueue.enqueue_request( 106 | GitTaskAction.TEST_CONFLICTING_PICKMES, 107 | self.pushid, 108 | pushmanager_url=self.get_base_url() 109 | ) 110 | -------------------------------------------------------------------------------- /pushmanager/servlets/pingme.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.db as db 2 | import pushmanager.core.util 3 | import tornado.gen 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class PingMeServlet(RequestHandler): 9 | 10 | @tornado.web.authenticated 11 | @tornado.web.asynchronous 12 | @tornado.gen.engine 13 | def get(self): 14 | pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 15 | ping_action = pushmanager.core.util.get_str_arg(self.request, 'action') 16 | response = yield tornado.gen.Task( 17 | self.async_api_call, 18 | "push", 19 | {"id": pushid} 20 | ) 21 | 22 | push = self.get_api_results(response) 23 | if not push: 24 | self.send_error() 25 | 26 | pings = set(x for x in (push['extra_pings'] or "").split(',') if x) 27 | if ping_action == 'set': 28 | pings.add(self.current_user) 29 | else: 30 | pings.discard(self.current_user) 31 | 32 | # This is not atomic, so we could theoretically 33 | # run into race conditions here, but since we're 34 | # working at machine speed on human input triggers 35 | # it should be okay for now. 36 | query = db.push_pushes.update().where( 37 | db.push_pushes.c.id == pushid, 38 | ).values({ 39 | 'extra_pings': ','.join(pings), 40 | }) 41 | db.execute_cb(query, self.on_update_complete) 42 | 43 | def on_update_complete(self, success, db_results): 44 | self.check_db_results(success, db_results) 45 | self.finish() 46 | -------------------------------------------------------------------------------- /pushmanager/servlets/push.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pushmanager.core.util 4 | import tornado.gen 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | def _repo(base): 11 | dev_repos_dir = Settings['git']['dev_repositories_dir'] 12 | main_repository = Settings['git']['main_repository'] 13 | return os.path.join(dev_repos_dir, base) if base != main_repository else base 14 | 15 | 16 | class PushServlet(RequestHandler): 17 | 18 | @tornado.web.asynchronous 19 | @tornado.web.authenticated 20 | @tornado.gen.engine 21 | def get(self): 22 | pushid = pushmanager.core.util.get_int_arg(self.request, 'id') 23 | override = pushmanager.core.util.get_int_arg(self.request, 'override') 24 | response = yield tornado.gen.Task( 25 | self.async_api_call, 26 | "pushdata", 27 | {"id": pushid} 28 | ) 29 | 30 | push_info, push_requests, available_requests = self.get_api_results(response) 31 | 32 | if not push_info['stageenv']: 33 | push_info['stageenv'] = '(to be determined)' 34 | 35 | push_survey_url = Settings.get('push_survey_url', None) 36 | 37 | self.render( 38 | "push.html", 39 | page_title=push_info['title'], 40 | pushid=pushid, 41 | push_info=push_info, 42 | push_contents=push_requests, 43 | push_survey_url=push_survey_url, 44 | available_requests=available_requests, 45 | fullrepo=_repo, 46 | override=override, 47 | pickme_orders=['urgent', 'no-verify', Settings['tests_tag']['tag']] 48 | ) 49 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushbyrequest.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushByRequestServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.gen.engine 11 | def get(self): 12 | requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 13 | response = yield tornado.gen.Task( 14 | self.async_api_call, 15 | "pushbyrequest", 16 | {"id": requestid} 17 | ) 18 | 19 | push = self.get_api_results(response) 20 | if push: 21 | self.redirect('/push?id=%s' % push['id']) 22 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushes.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushesServlet(RequestHandler): 8 | 9 | @tornado.web.authenticated 10 | @tornado.web.asynchronous 11 | @tornado.gen.engine 12 | def get(self): 13 | pushes_per_page = pushmanager.core.util.get_int_arg(self.request, 'rpp', 50) 14 | offset = pushmanager.core.util.get_int_arg(self.request, 'offset', 0) 15 | state = pushmanager.core.util.get_str_arg(self.request, 'state', '') 16 | push_user = pushmanager.core.util.get_str_arg(self.request, 'user', '') 17 | response = yield tornado.gen.Task( 18 | self.async_api_call, 19 | 'pushes', 20 | { 21 | 'rpp': pushes_per_page, 22 | 'offset': offset, 23 | 'state': state, 24 | 'user': push_user, 25 | } 26 | ) 27 | 28 | results = self.get_api_results(response) 29 | if not results: 30 | self.finish() 31 | 32 | pushes, pushes_count = results 33 | self.render( 34 | "pushes.html", 35 | page_title="Pushes", 36 | pushes=pushes, 37 | offset=offset, 38 | rpp=pushes_per_page, 39 | state=state, 40 | push_user=push_user, 41 | pushes_count=pushes_count, 42 | ) 43 | -------------------------------------------------------------------------------- /pushmanager/servlets/pushitems.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class PushItemsServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.gen.engine 11 | def get(self): 12 | pushid = pushmanager.core.util.get_int_arg(self.request, 'push', None) 13 | 14 | response = yield tornado.gen.Task( 15 | self.async_api_call, 16 | "pushitems", 17 | {"push_id": pushid} 18 | ) 19 | 20 | results = self.get_api_results(response) 21 | self.render("pushitems.html", requests=results) 22 | -------------------------------------------------------------------------------- /pushmanager/servlets/removerequest.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | import sqlalchemy as SA 4 | 5 | import pushmanager.core.db as db 6 | import pushmanager.core.util 7 | import tornado.web 8 | from pushmanager.core.mail import MailQueue 9 | from pushmanager.core.requesthandler import RequestHandler 10 | from pushmanager.core.xmppclient import XMPPQueue 11 | 12 | 13 | class RemoveRequestServlet(RequestHandler): 14 | 15 | @tornado.web.asynchronous 16 | def post(self): 17 | if not self.current_user: 18 | return self.send_error(403) 19 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 20 | self.requestid = self.request.arguments.get('request', []) 21 | select_query = db.push_requests.select().where( 22 | db.push_requests.c.id.in_(self.requestid) 23 | ) 24 | update_query = db.push_requests.update().where(SA.and_( 25 | db.push_requests.c.id.in_(self.requestid), 26 | SA.exists([1], SA.and_( 27 | db.push_pushcontents.c.push == self.pushid, 28 | db.push_pushcontents.c.request == db.push_requests.c.id, 29 | )), 30 | )).values({'state': 'requested'}) 31 | delete_query = db.push_pushcontents.delete(SA.and_( 32 | db.push_pushcontents.c.push == self.pushid, 33 | db.push_pushcontents.c.request.in_(self.requestid), 34 | )) 35 | db.execute_transaction_cb([select_query, update_query, delete_query], self.on_db_complete) 36 | 37 | # allow both GET and POST 38 | get = post 39 | 40 | def on_db_complete(self, success, db_results): 41 | self.check_db_results(success, db_results) 42 | 43 | reqs, _, _ = db_results 44 | removal_dicts = [] 45 | for req in reqs: 46 | if req['watchers']: 47 | user_string = '%s (%s)' % (req['user'], req['watchers']) 48 | users = [req['user']] + req['watchers'].split(',') 49 | else: 50 | user_string = req['user'] 51 | users = [req['user']] 52 | msg = ( 53 | """ 54 |

55 | %(pushmaster)s has removed request for %(user)s from a push: 56 |

57 |

58 | %(user)s - %(title)s
59 | %(repo)s/%(branch)s 60 |

61 |

62 | Regards,
63 | PushManager 64 |

""" 65 | ) % pushmanager.core.util.EscapedDict({ 66 | 'pushmaster': self.current_user, 67 | 'user': user_string, 68 | 'title': req['title'], 69 | 'repo': req['repo'], 70 | 'branch': req['branch'], 71 | }) 72 | subject = "[push] %s - %s" % (user_string, req['title']) 73 | MailQueue.enqueue_user_email(users, msg, subject) 74 | msg = '%(pushmaster)s has removed request "%(title)s" for %(user)s from a push' % { 75 | 'pushmaster': self.current_user, 76 | 'title': req['title'], 77 | 'pushid': self.pushid, 78 | 'user': user_string, 79 | } 80 | XMPPQueue.enqueue_user_xmpp(users, msg) 81 | removal_dicts.append({ 82 | 'request': req['id'], 83 | 'push': self.pushid, 84 | 'reason': 'removal after %s' % req['state'], 85 | 'pushmaster': self._current_user, 86 | 'timestamp': int(time.time()), 87 | }) 88 | 89 | removal_queries = [db.push_removals.insert(removal) for removal in removal_dicts] 90 | db.execute_transaction_cb(removal_queries, self.on_db_insert_complete) 91 | 92 | def on_db_insert_complete(self, success, db_results): 93 | if not success: 94 | self.send_error(500) 95 | self.finish() 96 | -------------------------------------------------------------------------------- /pushmanager/servlets/request.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class RequestServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.web.authenticated 11 | @tornado.gen.engine 12 | def get(self): 13 | request_id = pushmanager.core.util.get_int_arg(self.request, 'id') 14 | if not request_id: 15 | self.send_error(404) 16 | 17 | response = yield tornado.gen.Task( 18 | self.async_api_call, 19 | "request", 20 | {'id': request_id} 21 | ) 22 | 23 | req = self.get_api_results(response) 24 | if not req: 25 | self.send_error() 26 | 27 | self.render("request.html", page_title="Request #%d" % request_id, req=req) 28 | -------------------------------------------------------------------------------- /pushmanager/servlets/requests.py: -------------------------------------------------------------------------------- 1 | import pushmanager.core.util 2 | import tornado.gen 3 | import tornado.web 4 | from pushmanager.core.requesthandler import RequestHandler 5 | 6 | 7 | class RequestsServlet(RequestHandler): 8 | 9 | @tornado.web.asynchronous 10 | @tornado.web.authenticated 11 | @tornado.gen.engine 12 | def get(self): 13 | username = pushmanager.core.util.get_str_arg(self.request, 'user') 14 | limit_count = pushmanager.core.util.get_int_arg(self.request, 'max') 15 | arguments = {'limit': limit_count} 16 | 17 | if username: 18 | arguments['user'] = username 19 | page_title = 'Requests from %s' % username 20 | show_count = False 21 | else: 22 | arguments['limit'] = 0 23 | arguments['state'] = 'requested' 24 | page_title = 'Open Requests' 25 | show_count = True 26 | 27 | response = yield tornado.gen.Task( 28 | self.async_api_call, 29 | "requestsearch", 30 | arguments 31 | ) 32 | 33 | requests = self.get_api_results(response) 34 | self.render("requests.html", requests=requests, page_title=page_title, show_count=show_count) 35 | -------------------------------------------------------------------------------- /pushmanager/servlets/smartdest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class SmartDestServlet(RequestHandler): 9 | 10 | @tornado.web.authenticated 11 | def get(self): 12 | query = db.push_pushes.select(SA.and_( 13 | db.push_pushes.c.state == 'accepting', 14 | SA.exists( 15 | [1], 16 | SA.and_( 17 | db.push_pushcontents.c.push == db.push_pushes.c.id, 18 | db.push_pushcontents.c.request == db.push_requests.c.id, 19 | db.push_requests.c.user == self.current_user, 20 | ), 21 | ), 22 | ), 23 | order_by=db.push_pushes.c.created.asc(), 24 | ) 25 | db.execute_cb(query, self.on_db_response) 26 | 27 | def on_db_response(self, success, db_results): 28 | self.check_db_results(success, db_results) 29 | 30 | if db_results and db_results.rowcount > 0: 31 | push = db_results.first() 32 | if push: 33 | return self.redirect('/push?id=%s' % push['id']) 34 | 35 | return self.redirect('/pushes') 36 | -------------------------------------------------------------------------------- /pushmanager/servlets/summaryforbranch.py: -------------------------------------------------------------------------------- 1 | import operator 2 | 3 | import pushmanager.core.util 4 | import tornado.gen 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | from pushmanager.core.settings import Settings 8 | 9 | 10 | class SummaryForBranchServlet(RequestHandler): 11 | 12 | @tornado.web.asynchronous 13 | @tornado.gen.engine 14 | def get(self): 15 | userbranch = pushmanager.core.util.get_str_arg(self.request, 'userbranch') 16 | user, branch = userbranch.split('/', 1) 17 | response = yield tornado.gen.Task( 18 | self.async_api_call, 19 | "requestsearch", 20 | {'repo': user, 'branch': branch} 21 | ) 22 | 23 | requests = self.get_api_results(response) 24 | 25 | if requests: 26 | req = sorted(requests, key=operator.itemgetter("id"))[0] 27 | self.write(req['description'] or req['title']) 28 | if req['reviewid']: 29 | self.write("\n\nReview: https://%s/r/%s" % (Settings['reviewboard']['servername'], req['reviewid'])) 30 | self.finish() 31 | else: 32 | self.send_error(404) 33 | -------------------------------------------------------------------------------- /pushmanager/servlets/testtag.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import urllib2 4 | 5 | import tornado.web 6 | 7 | from tornado.escape import url_escape 8 | from tornado.escape import xhtml_escape 9 | 10 | import pushmanager.core.util 11 | from pushmanager.core.git import GitQueue 12 | from pushmanager.core.requesthandler import RequestHandler 13 | from pushmanager.core.settings import Settings 14 | 15 | 16 | class TestTagServlet(RequestHandler): 17 | 18 | def _arg(self, key): 19 | return pushmanager.core.util.get_str_arg(self.request, key, '') 20 | 21 | @tornado.web.asynchronous 22 | def get(self): 23 | request_id = pushmanager.core.util.get_int_arg(self.request, 'id') 24 | resp = {} 25 | if not request_id: 26 | self.set_status(404) 27 | else: 28 | request = GitQueue._get_request(request_id) 29 | resp = self._gen_test_tag_resp(request) 30 | self.finish(resp) 31 | 32 | @classmethod 33 | def _gen_test_tag_resp(cls, request): 34 | response = {} 35 | 36 | if 'tests_tag' in Settings and Settings['tests_tag']['tag'] in request['tags']: 37 | response['tag'] = Settings['tests_tag']['tag'] 38 | try: 39 | api_url = Settings['tests_tag']['tag_api_endpoint'].replace('%SHA%', request['revision']) 40 | api_body = Settings['tests_tag']['tag_api_body'].replace('%SHA%', request['revision']) 41 | api_resp = urllib2.urlopen(api_url, api_body) 42 | response['tag'] = xhtml_escape(json.loads(api_resp.read())['tag']) 43 | except Exception as e: 44 | response['tag'] += ": ERROR connecting to server" 45 | logging.error(e) 46 | 47 | response['url'] = '' 48 | if 'url_api_endpoint' in Settings['tests_tag']: 49 | try: 50 | result_api_url = Settings['tests_tag']['url_api_endpoint'].replace('%SHA%', request['revision']) 51 | result_api_url = result_api_url.replace('%BRANCH%', request['branch']) 52 | result_api_body = Settings['tests_tag']['url_api_body'].replace('%SHA%', request['revision']) 53 | result_api_body = result_api_body.replace('%BRANCH%', request['branch']) 54 | resp = urllib2.urlopen(result_api_url, result_api_body) 55 | result_id = url_escape(json.loads(resp.read())['id']) 56 | if result_id != '': 57 | response['url'] = Settings['tests_tag']['url_tmpl'].replace( 58 | '%ID%', 59 | result_id 60 | ).replace( 61 | '%SHA%', 62 | request['revision'] 63 | ) 64 | response['url'] = response['url'].replace('%BRANCH%', request['branch']) 65 | except Exception as e: 66 | logging.warning(e) 67 | logging.warning( 68 | "Couldn't load results for results test URL from %s with body %s" % ( 69 | Settings['tests_tag']['url_api_endpoint'].replace('%SHA%', request['revision']), 70 | Settings['tests_tag']['url_api_body'].replace('%SHA%', request['revision']) 71 | ) 72 | ) 73 | return response 74 | -------------------------------------------------------------------------------- /pushmanager/servlets/undelayrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | import tornado.web 6 | from pushmanager.core.requesthandler import RequestHandler 7 | 8 | 9 | class UndelayRequestServlet(RequestHandler): 10 | 11 | @tornado.web.asynchronous 12 | def post(self): 13 | if not self.current_user: 14 | return self.send_error(403) 15 | self.requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 16 | update_query = db.push_requests.update().where(SA.and_( 17 | db.push_requests.c.id == self.requestid, 18 | db.push_requests.c.user == self.current_user, 19 | db.push_requests.c.state == 'delayed', 20 | )).values({ 21 | 'state': 'requested', 22 | }) 23 | select_query = db.push_requests.select().where( 24 | db.push_requests.c.id == self.requestid, 25 | ) 26 | db.execute_transaction_cb([update_query, select_query], self.on_db_complete) 27 | 28 | # allow both GET and POST 29 | get = post 30 | 31 | def on_db_complete(self, success, db_results): 32 | self.check_db_results(success, db_results) 33 | 34 | self.redirect("/requests?user=%s" % self.current_user) 35 | -------------------------------------------------------------------------------- /pushmanager/servlets/userlist.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | import tornado.gen 4 | import tornado.web 5 | from pushmanager.core.requesthandler import RequestHandler 6 | 7 | 8 | class UserListServlet(RequestHandler): 9 | 10 | @tornado.web.asynchronous 11 | @tornado.web.authenticated 12 | @tornado.gen.engine 13 | def get(self): 14 | response = yield tornado.gen.Task( 15 | self.async_api_call, 16 | "userlist", 17 | {} 18 | ) 19 | 20 | users_by_alpha = defaultdict(list) 21 | map( 22 | lambda u: users_by_alpha[u[0]].append(u), 23 | self.get_api_results(response) 24 | ) 25 | 26 | self.render("userlist.html", page_title="Users", users_by_alpha=users_by_alpha) 27 | -------------------------------------------------------------------------------- /pushmanager/servlets/verifyrequest.py: -------------------------------------------------------------------------------- 1 | import sqlalchemy as SA 2 | 3 | import pushmanager.core.db as db 4 | import pushmanager.core.util 5 | from pushmanager.core.requesthandler import RequestHandler 6 | from pushmanager.core.xmppclient import XMPPQueue 7 | 8 | 9 | class VerifyRequestServlet(RequestHandler): 10 | 11 | def _arg(self, key): 12 | return pushmanager.core.util.get_str_arg(self.request, key, '') 13 | 14 | def post(self): 15 | if not self.current_user: 16 | return self.send_error(403) 17 | self.requestid = pushmanager.core.util.get_int_arg(self.request, 'id') 18 | self.pushid = pushmanager.core.util.get_int_arg(self.request, 'push') 19 | select_query = db.push_pushes.select().where( 20 | db.push_pushes.c.id == self.pushid, 21 | ) 22 | update_query = db.push_requests.update().where(SA.and_( 23 | db.push_requests.c.state == 'staged', 24 | db.push_requests.c.id == self.requestid, 25 | SA.exists( 26 | [1], 27 | SA.and_( 28 | db.push_pushcontents.c.push == self.pushid, 29 | db.push_pushcontents.c.request == self.requestid, 30 | ) 31 | ))).values({ 32 | 'state': 'verified', 33 | }) 34 | finished_query = db.push_requests.select().where(SA.and_( 35 | db.push_requests.c.state == 'staged', 36 | SA.exists( 37 | [1], 38 | SA.and_( 39 | db.push_pushcontents.c.push == self.pushid, 40 | db.push_pushcontents.c.request == db.push_requests.c.id, 41 | ) 42 | ))) 43 | db.execute_transaction_cb([select_query, update_query, finished_query], self.on_db_complete) 44 | 45 | def on_db_complete(self, success, db_results): 46 | self.check_db_results(success, db_results) 47 | 48 | push = db_results[0].first() 49 | unfinished_requests = db_results[2].first() 50 | pushmanager_base_url = self.get_base_url() 51 | if not unfinished_requests: 52 | msg = "All currently staged requests in %s/push?id=%s have been marked as verified." % \ 53 | (pushmanager_base_url, self.pushid) 54 | XMPPQueue.enqueue_user_xmpp([push['user']], msg) 55 | -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_0_aaaaaa_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_flat_0_aaaaaa_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_0_eeeeee_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_flat_0_eeeeee_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_55_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_flat_55_ffffff_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_flat_75_ffffff_40x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_flat_75_ffffff_40x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_glass_65_ffffff_1x400.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_glass_65_ffffff_1x400.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_highlight-soft_100_f6f6f6_1x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_highlight-soft_25_0073ea_1x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-bg_highlight-soft_50_dddddd_1x100.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_0073ea_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-icons_0073ea_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_666666_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-icons_666666_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_ff0084_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-icons_ff0084_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/flick/images/ui-icons_ffffff_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/css/flick/images/ui-icons_ffffff_256x240.png -------------------------------------------------------------------------------- /pushmanager/static/css/modules/newrequest.css: -------------------------------------------------------------------------------- 1 | #request-info-form { 2 | position: absolute; 3 | top: 20px; 4 | right: 20px; 5 | bottom: 20px; 6 | left: 20px; 7 | } 8 | 9 | #request-info-form > fieldset { 10 | position: absolute; 11 | top: 0; 12 | right: 0; 13 | bottom: 0; 14 | left: 0; 15 | } 16 | 17 | #request-info-form input, 18 | #request-info-form textarea { 19 | display: block; 20 | width: 99%; 21 | margin-bottom: 10px; 22 | } 23 | 24 | #request-info-form textarea { 25 | height: 100px; 26 | font-family: monospace; 27 | } 28 | 29 | #request-info-form #request-form-comments, 30 | #request-info-form #request-form-description { 31 | position: relative; 32 | } 33 | 34 | #request-info-form input[type=submit] { 35 | width: 150px; 36 | float: right; 37 | } 38 | 39 | #request-info-form label { 40 | display: block; 41 | margin-top: 10px; 42 | } 43 | 44 | #request-info-form #request-form-takeover { 45 | float: left; 46 | } 47 | -------------------------------------------------------------------------------- /pushmanager/static/css/modules/request.css: -------------------------------------------------------------------------------- 1 | div.request-module { 2 | display: inline; 3 | } 4 | 5 | .request-module img.request-item-expander { 6 | vertical-align: text-bottom; 7 | cursor: pointer; 8 | border: 0; 9 | } 10 | 11 | .request-module span.omitted { 12 | display: inline-block; 13 | background: #f00; 14 | color: #fff; 15 | font-weight: bold; 16 | padding: 3px 4px 3px 4px; 17 | -webkit-border-radius: 5px; 18 | -moz-border-radius: 5px; 19 | border-radius: 5px; 20 | } 21 | 22 | .request-module span.label { 23 | display: inline-block; 24 | background: #e8e8e8; 25 | color: #000; 26 | padding: 3px 4px 2px 4px; 27 | -webkit-border-radius: 5px 0 0 5px; 28 | -moz-border-radius: 5px 0 0 5px; 29 | border-radius: 5px 0 0 5px; 30 | } 31 | 32 | .request-module span.value { 33 | display: inline-block; 34 | background: #fff; 35 | color: #000; 36 | font-weight: bold; 37 | padding: 3px 4px 2px 4px; 38 | -webkit-border-radius: 0 5px 5px 0; 39 | -moz-border-radius: 0 5px 5px 0; 40 | border-radius: 0 5px 5px 0; 41 | } 42 | 43 | .request-module ul.request-info-inline { 44 | display: inline-block; 45 | } 46 | 47 | .request-module ul.request-info-inline li { 48 | display: inline-block; 49 | } 50 | 51 | .request-module .request-info-inline span.value { 52 | background: transparent; 53 | } 54 | 55 | .request-module .request-extended-fields { 56 | display: inline-block; 57 | } 58 | 59 | .request-module .request-extended-fields li { 60 | display: inline; 61 | margin: 3px 5px; 62 | } 63 | 64 | .request-module a { 65 | text-decoration: none; 66 | color: #00f; 67 | } 68 | 69 | .request-module ul.tags a { 70 | color: inherit; 71 | } 72 | 73 | .request-module .request-item-title { 74 | display: inline-block; 75 | padding: 3px 4px 2px; 76 | margin: 0; 77 | color: #33a; 78 | cursor: pointer; 79 | } 80 | 81 | .request-module .request-info-extended { 82 | display: none; 83 | border-left: 1px solid #333; 84 | background: #eef; 85 | color: #000; 86 | margin: 0 0 10px 40px; 87 | padding: 5px; 88 | -moz-border-radius: 0 5px 5px 0px; 89 | -webkit-border-radius: 0 5px 5px 0; 90 | border-radius: 0 5px 5px 0; 91 | } 92 | 93 | .request-module .request-info-extended .request-revision, 94 | .request-module .request-info-extended .request-comments { 95 | background: #fff; 96 | color: #000; 97 | font-family: monospace; 98 | border: 1px solid #eee; 99 | padding: 5px; 100 | margin-top: 5px; 101 | display: block; 102 | } 103 | 104 | .request-conflicts { 105 | background: #ee4048; 106 | color: #000; 107 | font-family: monospace; 108 | border: 1px solid #eee; 109 | padding: 5px; 110 | margin-top: 5px; 111 | display: block; 112 | } 113 | 114 | .request-module span.timeago { 115 | color: #888; 116 | } 117 | -------------------------------------------------------------------------------- /pushmanager/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/favicon.ico -------------------------------------------------------------------------------- /pushmanager/static/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/img/ajax-loader.gif -------------------------------------------------------------------------------- /pushmanager/static/img/button_expand.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/img/button_expand.gif -------------------------------------------------------------------------------- /pushmanager/static/img/button_hide.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/img/button_hide.gif -------------------------------------------------------------------------------- /pushmanager/static/img/favicon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/img/favicon.gif -------------------------------------------------------------------------------- /pushmanager/static/img/hamster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/img/hamster.png -------------------------------------------------------------------------------- /pushmanager/static/js/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Yelp/pushmanager/2fc410f99d06aa386b1633e72bb7c0cf8bfe72c8/pushmanager/static/js/ZeroClipboard.swf -------------------------------------------------------------------------------- /pushmanager/static/js/modules/newrequest.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | PushManager = window.PushManager || {}; 3 | PushManager.NewRequestDialog = PushManager.NewRequestDialog || {}; 4 | 5 | PushManager.NewRequestDialog.element = $('#create-new-request-dialog'); 6 | PushManager.NewRequestDialog.element.dialog({ 7 | autoOpen: false, 8 | title: 'Create/Edit Push Request', 9 | width: 650, 10 | minWidth: 650, 11 | height: 805, 12 | maxHeight: 805, 13 | minHeight: 805, 14 | closeOnEscape: false 15 | }); 16 | 17 | PushManager.NewRequestDialog.validate = function() { 18 | var d = PushManager.NewRequestDialog.element; 19 | 20 | // Validate ReviewBoard ID 21 | if(!/^\d*$/.test(d.find('#request-form-review').val())) { 22 | alert("Invalid review # - only integer ReviewBoard IDs are allowed."); 23 | return false; 24 | } 25 | 26 | return true; 27 | }; 28 | $('#request-info-form').submit(PushManager.NewRequestDialog.validate); 29 | 30 | PushManager.NewRequestDialog.open_new_request = function(title, branch, repo, review, comments, description, watchers, tags, requestid, requestuser, notyours) { 31 | var d = PushManager.NewRequestDialog.element; 32 | 33 | d.find('#request-form-title').val(title || ''); 34 | d.find('#request-form-repo').val(repo || PushManager.current_user); 35 | d.find('#request-form-branch').val(branch || ''); 36 | d.find('#request-form-review').val(review || ''); 37 | d.find('#request-form-tags').val(tags || ''); 38 | d.find('#request-form-comments').val(comments || ''); 39 | d.find('#request-form-description').val(description || ''); 40 | d.find('#request-form-watchers').val(watchers || ''); 41 | d.find('#request-form-user').val(requestuser || ''); 42 | d.find('#request-form-id').val(requestid || ''); 43 | d.find('#request-form-takeover-label').toggle(notyours || false); 44 | 45 | d.dialog('open'); 46 | }; 47 | 48 | $('#create-new-request').click(function() { 49 | PushManager.NewRequestDialog.open_new_request(); 50 | }); 51 | 52 | $('.edit-request').click(function() { 53 | var that = $(this).closest('.request-module'); 54 | var tags = ''; 55 | that.find('ul.tags > li').each(function(_, elem) { 56 | if(tags !== '') tags += ' '; 57 | tags += elem.classList[0].replace(/tag-/, ''); 58 | }); 59 | PushManager.NewRequestDialog.open_new_request( 60 | that.attr('request_title'), 61 | that.attr('branch'), 62 | that.attr('repo'), 63 | that.attr('reviewid'), 64 | that.find('.request-comments').text().replace(/\n{3,}/g, '\n\n'), 65 | that.find('.request-description').text(), 66 | that.attr('watchers'), 67 | tags, 68 | that.attr('request'), 69 | that.attr('user'), 70 | that.attr('user') != PushManager.current_user 71 | ); 72 | }); 73 | 74 | $('.tag-suggestion').click(function() { 75 | var that = $(this); 76 | var tagtext = $("#request-form-tags").val(); 77 | var newtag = that.text(); 78 | if(tagtext.indexOf(newtag) == -1) { 79 | if(tagtext.length > 0) { 80 | tagtext += " "; 81 | } 82 | tagtext += newtag; 83 | $("#request-form-tags").val(tagtext); 84 | } 85 | }); 86 | 87 | // Handle bookmarklet requests 88 | if(PushManager.urlParams['bookmarklet'] == '1') { 89 | PushManager.NewRequestDialog.open_new_request( 90 | PushManager.urlParams['title'], 91 | PushManager.urlParams['branch'], 92 | PushManager.urlParams['repo'], 93 | PushManager.urlParams['review'], 94 | PushManager.urlParams['comments'], 95 | PushManager.urlParams['description'] || '' // For those on old bookmarklet 96 | ); 97 | }; 98 | 99 | 100 | }); 101 | -------------------------------------------------------------------------------- /pushmanager/templates/base.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | {{ escape(page_title) }} - Push Management 6 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 40 |
{% block content %}Content content content.
Content content content.{% end %}
41 |
42 | 43 | 44 | 60 | 68 | {% block scripts %} 69 | {% end %} 70 | 71 | 72 | -------------------------------------------------------------------------------- /pushmanager/templates/check_sites_bookmarklet.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | Settings = {{ JSSettings_json }}; 3 | 4 | var domainName = Settings['check_sites_bookmarklet']['domain_name']; 5 | var urls = Settings['check_sites_bookmarklet']['urls']; 6 | // 'substitutions' defines strings that need to be replaced for 7 | // non-production environments. Example object for this: 8 | // {'prod_id': 'dev_id'} 9 | // The example above will replace 'prod_id' with 'dev_id' in the 10 | // given list of urls. 11 | var substitutions = Settings['check_sites_bookmarklet']['substitutions'] || {}; 12 | 13 | var prod = 'prod'; 14 | 15 | var env = window.prompt('Which environment would you like to test? e.g.: prod, stagea, stageb.', prod); 16 | 17 | // If env is false, the user hit 'cancel', and let's abort. 18 | if (!env) { 19 | return; 20 | } 21 | 22 | for (var i=0; i < urls.length; ++i) { 23 | var url = urls[i]; 24 | 25 | // We assume all URLs are encoded against a prod environment. 26 | // If not, we modify the URL with what we assume to be a testing sub-domain. e.g.: foo.com -> stage.foo.com 27 | if (env !== prod) { 28 | 29 | $.each(substitutions, function(prodString, devString) { 30 | if (url.match(prodString)) { 31 | url = url.replace(prodString, devString); 32 | } 33 | }); 34 | 35 | url = url.replace(domainName, env + '.' + domainName); 36 | } 37 | 38 | url = 'http://' + url; 39 | 40 | window.open(url, url, 'resizable=yes,menubar=yes,toolbar=yes,scrollbars=yes,status=yes,location=yes'); 41 | } 42 | })(); 43 | -------------------------------------------------------------------------------- /pushmanager/templates/checklist.html: -------------------------------------------------------------------------------- 1 | {% if not any(items_by_target.values()) %} 2 | (No checklist items for this push.) 3 | {% else %} 4 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Staging - Do In Stage', target='stage') %} 5 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Staging - Do In Stage', target='post-stage') %} 6 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Blessing - Do In Prod', target='prod') %} 7 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='After Blessing - Do In Prod', target='post-prod') %} 8 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Certifying - Do In Dev', target='verify') %} 9 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Certifying - Do In Prod', target='post-verify-prod') %} 10 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Certifying - Do In Dev', target='post-verify') %} 11 | {% module Template('checklist/category.html', pushmaster=pushmaster, items_by_target=items_by_target, reminders=checklist_reminders, title='Before Certifying - Do In Stage', target='post-verify-stage') %} 12 | {% end %} 13 | 14 | -------------------------------------------------------------------------------- /pushmanager/templates/checklist/category.html: -------------------------------------------------------------------------------- 1 | {% if items_by_target.get(target, []) %} 2 |

{{ title }}

3 |
    4 | {% for item in items_by_target.get(target, []) %} 5 |
  • 7 | 13 |
  • 14 | {% end %} 15 |
16 | {% end %} 17 | -------------------------------------------------------------------------------- /pushmanager/templates/confirm-conflict-check.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /pushmanager/templates/create_request_bookmarklet.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | Settings = {{ JSSettings_json }}; 3 | 4 | var ticketNumberToURL = function(bug) { 5 | return Settings['ticket_tracker_url_format'].replace("%TICKET%", bug); 6 | }; 7 | 8 | var summary = $('#field_summary').text(); 9 | var codeReview = location.href.split('#')[0]; 10 | var reviewid = codeReview.match(/\d+/)[0]; 11 | var tickets = $('#field_bugs_closed').text().split(',').filter(Boolean).map(ticketNumberToURL); 12 | var description = summary + '\n\n' + $('#field_description').text(); 13 | 14 | // Get a list of reviewers who have a 'Ship it!', filtering out dupes 15 | var reviewerSet = {}; 16 | var reviewers = $('div.shipit ~ div.reviewer > a').map(function() { 17 | var reviewer = $.trim(this.text); 18 | if (reviewer && !reviewerSet[reviewer]) { 19 | reviewerSet[reviewer] = true; 20 | return reviewer; 21 | } 22 | }).get(); 23 | 24 | var branch = $('#field_branch').text(); 25 | var repo = Settings['git']['main_repository']; 26 | if(branch.indexOf('/') != -1) { 27 | var branchparts = branch.split('/', 2); 28 | repo = branchparts[0]; 29 | branch = branchparts[1]; 30 | } 31 | 32 | var comments = []; 33 | if (reviewers.length > 0) { 34 | comments.push('SheepIt from ' + reviewers.join(', ')); 35 | } 36 | if (tickets.length > 0) { 37 | comments.push((tickets.length == 1 ? 'Ticket: ' : 'Tickets: ') + tickets.join(' ')); 38 | } 39 | comments = comments.join('\n\n'); 40 | 41 | main_app_port = Settings['main_app']['port'] == 443 ? ':' + Settings['main_app']['port'] : ''; 42 | 43 | location.href = 'https://' + Settings['main_app']['servername'] + main_app_port + '/requests?' + $.param({ 44 | 'bookmarklet': 1, 45 | 'title': summary, 46 | 'repo': repo, 47 | 'branch': branch, 48 | 'review': reviewid, 49 | 'comments': comments, 50 | 'description': description 51 | }); 52 | })(); 53 | -------------------------------------------------------------------------------- /pushmanager/templates/edit-push.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /pushmanager/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}Home{% end %} 4 | 5 | {% block content %} 6 | {% end %} 7 | -------------------------------------------------------------------------------- /pushmanager/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block page_name %}Login{% end %} 4 | 5 | {% block content %} 6 | {% if errors %} 7 |

{{ errors }}

8 | {% end %} 9 |
10 | {% if next_url %}{% end %} 11 | 12 | 13 | 14 | 15 | 16 |
17 | {% end %} 18 | 19 | {% block scripts %} 20 | 25 | {% end %} 26 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/newrequest.html: -------------------------------------------------------------------------------- 1 | 64 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/request-buttons.html: -------------------------------------------------------------------------------- 1 | {% if push_buttons %} 2 | 3 | 4 | {% if pushmaster %} 5 | 6 | 7 | 8 | 9 | 10 | {% end %} 11 | 12 | {% if authorized_to_manage_request(request, current_user, pushmaster) %} 13 | 14 | 15 | 16 | 17 | {% end %} 18 | 19 | {% elif edit_buttons %} 20 | 21 | 22 | {% if request['user'] == current_user %} 23 | 24 | {% if request['state'] == 'requested' %} 25 | 26 | {% elif request['state'] == 'delayed' %} 27 | 28 | {% end %} 29 | 30 | {% if request['state'] in ('requested', 'delayed') %} 31 | 32 | {% end %} 33 | 34 | {% end %} 35 | 36 | {% end %} 37 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/request-info.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 |
  • {{ escape(request['user']) }}{% if request['watchers'] %} ({{ escape(request['watchers']) }}){% end %}
  • 4 | 5 | {% if show_ago %} 6 |
  • 7 | {% if request['state'] in ('discarded', 'live') %} 8 | {{ pretty_date(int(request['modified'])) }} 9 | {% else %} 10 | {{ pretty_date(int(request['created'])) }} 11 | {% end %} 12 |
  • 13 | {% end %} 14 | 15 |
  • {{ escape(request['title']) }}
  • 16 | 17 | {% if tags %} 18 |
    • 19 | {% for (tag, tag_url) in tags %}
    • 20 | {% if tag_url %}{{ escape(tag) }}{% else %}{{ escape(tag) }}{% end %} 21 |
    • {% end %} 22 |
  • 23 | {% end %} 24 | 25 | {% if show_state_inline %} 26 |
  • 27 | {% if request['state'] in ('added','staged','verified', 'blessed', 'live') %} 28 | {{ escape(request['state']) }} 29 | {% else %} 30 | {{ escape(request['state']) }} 31 | {% end %} 32 |
  • 33 | {% end %} 34 | 35 |
36 | -------------------------------------------------------------------------------- /pushmanager/templates/modules/request.html: -------------------------------------------------------------------------------- 1 |
12 | 13 | 14 | 15 | {% include "request-buttons.html" %} 16 | 17 | {% include "request-info.html" %} 18 | 19 |
20 | 21 |
    22 | 23 | {% if review %} 24 |
  • Review # 25 | {{ escape(review['display']) }}
  • 26 | {% else %} 27 |
  • No Review #
  • 28 | {% end %} 29 | 30 |
  • Repo 31 | {{ escape(request['repo']) }}
  • 32 | 33 |
  • Branch 34 | {{ escape(request['branch']) }}
  • 35 | 36 |
  • Permalinkurl
  • 37 | 38 |
  • {{ web_hooks['service_name'] }}url
  • 39 | 40 |
  • Created{{ escape(create_time) }}
  • 41 | 42 | {% if not request['created'] == request['modified'] %} 43 |
  • Modified{{ escape(modify_time) }}
  • 44 | {% end %} 45 | 46 | {% if request['conflicts'] %} 47 |

    Conflicts:

    48 |
    {{ request['conflicts'].replace('\n', '
    ') }}
    49 | {% end %} 50 | 51 | {% if request['description'] %} 52 |

    Description:

    53 |
    {{ escape(request['description']) }}
    54 | {% end %} 55 | 56 | {% if request['revision'] %} 57 |

    Revision:

    58 |
    {{ escape(request['revision']) }}
    59 | {% end %} 60 | 61 | {% if request['comments'] %} 62 |

    Comments:

    63 |
    {{ escape(request['comments']) }}
    64 | {% end %} 65 | 66 |
67 |
68 | 69 |
70 | -------------------------------------------------------------------------------- /pushmanager/templates/push-button-bar.html: -------------------------------------------------------------------------------- 1 |
    2 |
  • 3 |
  • 4 | — 5 | {% if current_user in (push_info['extra_pings'] or []) %} 6 |
  • 7 | {% else %} 8 |
  • 9 | {% end %} 10 | {% if push_info['user'] == current_user or override %} 11 | — 12 |
  • 13 | {% if push_info['state'] == 'accepting' %} 14 |
  • 15 | — 16 |
  • 17 |
  • 18 |
  • 19 | — 20 |
  • 21 |
  • 22 |
  • 23 | — 24 |
  • 25 |
  • 26 |
  • 27 | {% end %} 28 | {% else %} 29 | {% if push_info['state'] == 'accepting' %} 30 | —
  • 31 | {% end %} 32 | {% end %} 33 |
  • {{ modules.NewRequestDialog() }}
  • 34 |
35 | -------------------------------------------------------------------------------- /pushmanager/templates/push-dialogs.html: -------------------------------------------------------------------------------- 1 |