├── route53 ├── views │ ├── __init__.py │ ├── main.py │ ├── slicehost.py │ ├── zones.py │ └── records.py ├── static │ ├── img │ │ └── grid.png │ └── css │ │ ├── print.css │ │ ├── ie.css │ │ └── screen.css ├── templates │ ├── index.html │ ├── slicehost │ │ ├── base.html │ │ ├── zones.html │ │ ├── index.html │ │ ├── import_zone.html │ │ └── records.html │ ├── _formhelpers.html │ ├── zones │ │ ├── detail.html │ │ ├── clone.html │ │ ├── delete.html │ │ ├── new.html │ │ ├── list.html │ │ └── records.html │ ├── xml │ │ ├── change_batch.xml │ │ └── _macros.xml │ ├── records │ │ ├── delete.html │ │ ├── update.html │ │ └── new.html │ └── base.html ├── connection.py ├── application.cfg.example ├── xmltools.py ├── __init__.py ├── models.py └── forms.py ├── .gitignore ├── runserver.py ├── create_db.py ├── shell.py ├── requirements.txt ├── sass ├── src │ ├── print.sass │ ├── partials │ │ └── _base.sass │ ├── ie.sass │ └── screen.sass └── config.rb ├── auth.py ├── LICENSE ├── README.rst └── authdigest.py /route53/views/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | application.cfg 3 | sass/.sass-cache/* 4 | dev.db 5 | -------------------------------------------------------------------------------- /runserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from route53 import app 3 | app.run() 4 | -------------------------------------------------------------------------------- /create_db.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from route53.models import db 3 | db.drop_all() 4 | db.create_all() 5 | -------------------------------------------------------------------------------- /route53/static/img/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrew-kurin/route53manager/HEAD/route53/static/img/grid.png -------------------------------------------------------------------------------- /shell.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ipython 2 | from route53 import app 3 | 4 | ctx = app.test_request_context() 5 | ctx.push() 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==0.10 2 | Flask-SQLAlchemy==0.9.1 3 | Flask-WTF==0.6 4 | simplejson 5 | -e git://github.com/boto/boto.git#egg=boto 6 | pyactiveresource==1.0.1 7 | -------------------------------------------------------------------------------- /route53/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block body %} 4 |

Edit zones 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /route53/templates/slicehost/base.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block toolbar %} 4 |

  • New API key
  • 5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /route53/views/main.py: -------------------------------------------------------------------------------- 1 | from flask import redirect, Module, url_for, request 2 | 3 | main = Module(__name__) 4 | 5 | 6 | @main.route('/') 7 | def index(): 8 | return redirect(url_for('zones.zones_list')) 9 | -------------------------------------------------------------------------------- /route53/connection.py: -------------------------------------------------------------------------------- 1 | from boto.route53 import Route53Connection 2 | 3 | 4 | def get_connection(): 5 | from route53 import app 6 | return Route53Connection(aws_access_key_id=app.config['AWS_ACCESS_KEY_ID'], 7 | aws_secret_access_key=app.config['AWS_SECRET_ACCESS_KEY']) 8 | -------------------------------------------------------------------------------- /sass/src/print.sass: -------------------------------------------------------------------------------- 1 | @import blueprint 2 | 3 | // To generate css equivalent to the blueprint css but with your configuration applied, uncomment: 4 | // @include blueprint-print 5 | 6 | //Recommended Blueprint configuration with scoping and semantic layout: 7 | body.bp 8 | +blueprint-print(true) -------------------------------------------------------------------------------- /route53/application.cfg.example: -------------------------------------------------------------------------------- 1 | SECRET_KEY = '\xb3\x8b\xdc\xc4;\xa2I.\x16\x93\xbb"\xc3\xf8]\xaa^a\xed\xad\xb2\xd9wj' 2 | SQLALCHEMY_DATABASE_URI = 'sqlite:////tmp/test.db' 3 | SQLALCHEMY_ECHO = False 4 | DEBUG = True 5 | AWS_ACCESS_KEY_ID="" 6 | AWS_SECRET_ACCESS_KEY="" 7 | AUTH_USERS = [ 8 | ('admin', 'admin'), 9 | ] 10 | -------------------------------------------------------------------------------- /route53/templates/_formhelpers.html: -------------------------------------------------------------------------------- 1 | {% macro render_field(field) %} 2 |
    {{ field.label }}
    3 |
    {{ field(**kwargs)|safe }} 4 | {% if field.errors %} 5 | 8 | {% endif %} 9 |
    10 | {% endmacro %} 11 | -------------------------------------------------------------------------------- /route53/templates/slicehost/zones.html: -------------------------------------------------------------------------------- 1 | {% extends "slicehost/base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Home
  • 5 |
  • Slicehost Zones
  • 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

    Slicehost Zones

    10 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /route53/templates/zones/detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • {{ zone['Name'] }}
  • 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

    Hosted Zone "{{ zone['Name'] }}"

    10 |
    11 |
    Id:
    12 |
    {{ zone['Id']|shortid }}
    13 |
    Nameservers:
    14 | {% for ns in nameservers %} 15 |
    {{ ns }}
    16 | {% endfor %} 17 |
    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /sass/config.rb: -------------------------------------------------------------------------------- 1 | # Require any additional compass plugins here. 2 | # Set this to the root of your project when deployed: 3 | http_path = "/" 4 | http_images_path = "/static/img/" 5 | css_dir = "../route53/static/css" 6 | sass_dir = "src" 7 | images_dir = "../route53/static/img" 8 | javascripts_dir = "javascripts" 9 | output_style = :compact 10 | line_comments = false 11 | # To enable relative paths to assets via compass helper functions. Uncomment: 12 | # relative_assets = true 13 | preferred_syntax = :sass 14 | -------------------------------------------------------------------------------- /route53/templates/xml/change_batch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% if comment %} 5 | {{ comment }} 6 | {% endif %} 7 | 8 | {% for change in changes %} 9 | {% from "xml/_macros.xml" import change_item %} 10 | {{ change_item(change) }} 11 | {% endfor %} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /route53/templates/zones/clone.html: -------------------------------------------------------------------------------- 1 | {% extends "zones/new.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Clone zone "{{ original_zone['Name'] }}"
  • 6 | {% endblock %} 7 | 8 | {% block page_title %}Clone zone "{{ original_zone['Name'] }}"{% endblock %} 9 | 10 | {% block afterform %} 11 | {% if errors %} 12 |

    Zone was cloned with following errors

    13 | 18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /route53/templates/slicehost/index.html: -------------------------------------------------------------------------------- 1 | {% extends "slicehost/base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Home
  • 5 |
  • Slicehost
  • 6 | {% endblock %} 7 | 8 | {% block toolbar %}{% endblock %} 9 | 10 | {% block body %} 11 |

    Enter Slicehost API key

    12 |
    13 | {{ form.csrf_token }} 14 | {% from "_formhelpers.html" import render_field %} 15 |
    16 | {{ render_field(form.key) }} 17 |
    18 | 19 |
    20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /sass/src/partials/_base.sass: -------------------------------------------------------------------------------- 1 | // Here is where you can define your constants for your application and to configure the blueprint framework. 2 | // Feel free to delete these if you want keep the defaults: 3 | 4 | $blueprint-grid-columns : 24 5 | $blueprint-container-size : 950px 6 | $blueprint-grid-margin : 10px 7 | 8 | // Use this to calculate the width based on the total width. 9 | // Or you can set $blueprint-grid-width to a fixed value and unset $blueprint-container-size -- it will be calculated for you. 10 | $blueprint-grid-width: ($blueprint-container-size + $blueprint-grid-margin) / $blueprint-grid-columns - $blueprint-grid-margin 11 | -------------------------------------------------------------------------------- /sass/src/ie.sass: -------------------------------------------------------------------------------- 1 | @import blueprint 2 | 3 | // To generate css equivalent to the blueprint css but with your configuration applied, uncomment: 4 | // @include blueprint-ie 5 | 6 | //Recommended Blueprint configuration with scoping and semantic layout: 7 | body.bp 8 | +blueprint-ie(true) 9 | // Note: Blueprint centers text to fix IE6 container centering. 10 | // This means all your texts will be centered under all version of IE by default. 11 | // If your container does not have the .container class, don't forget to restore 12 | // the correct behavior to your main container (but not the body tag!) 13 | // Example: 14 | // .my-container 15 | // text-align: left 16 | -------------------------------------------------------------------------------- /route53/templates/zones/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Delete {{ zone['Name'] }}
  • 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

    Delete zone "{{ zone['Name'] }}"

    10 |

    Are you sure?

    11 | {% if error %} 12 |

    {{ error.error_message }}

    13 | {% endif %} 14 |
    15 | 16 |

    17 | 18 | No 19 |

    20 |
    21 | {% endblock %} 22 | -------------------------------------------------------------------------------- /route53/templates/zones/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Add new zone
  • 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

    {% block page_title %}Add new zone{% endblock %}

    10 |
    11 | {{ form.csrf_token }} 12 | {% from "_formhelpers.html" import render_field %} 13 |
    14 | {{ render_field(form.name) }} 15 | {{ render_field(form.comment) }} 16 |
    17 |

    18 | 19 | Cancel 20 |

    21 |
    22 | {% block afterform %}{% endblock %} 23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /route53/templates/slicehost/import_zone.html: -------------------------------------------------------------------------------- 1 | {% extends "slicehost/base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Home
  • 5 |
  • Slicehost Zones
  • 6 |
  • Slicehost Records for {{ zone['origin'] }}
  • 7 |
  • Errors in import for {{ zone['origin'] }}
  • 8 | {% endblock %} 9 | 10 | {% block body %} 11 |

    Errors in import for {{ zone['origin'] }}

    12 | 17 |

    Go to Route 53 zone list

    18 | {% endblock %} 19 | -------------------------------------------------------------------------------- /route53/templates/zones/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 | {% endblock %} 6 | 7 | {% block body %} 8 |

    Zones

    9 |

    Add new zone

    10 | 20 | {% endblock %} 21 | -------------------------------------------------------------------------------- /route53/templates/xml/_macros.xml: -------------------------------------------------------------------------------- 1 | {% macro change_item(change_object) %} 2 | 3 | {{ change_object.action }} 4 | 5 | {{ change_object.name }} 6 | {{ change_object.type }} 7 | {{ change_object.ttl }} 8 | {% if change_object.values['values'] %} 9 | 10 | {% for value in change_object.values['values'] %} 11 | 12 | {{ value }} 13 | 14 | {% endfor %} 15 | 16 | {% else %} 17 | 18 | {{ change_object.values['alias_hosted_zone_id'] }} 19 | {{ change_object.values['alias_dns_name'] }} 20 | 21 | {% endif %} 22 | 23 | 24 | {% endmacro %} 25 | -------------------------------------------------------------------------------- /auth.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | import authdigest 3 | import flask 4 | 5 | 6 | class FlaskRealmDigestDB(authdigest.RealmDigestDB): 7 | def requires_auth(self, f): 8 | @wraps(f) 9 | def decorated(*args, **kwargs): 10 | request = flask.request 11 | if not self.isAuthenticated(request): 12 | return self.challenge() 13 | 14 | return f(*args, **kwargs) 15 | 16 | return decorated 17 | 18 | 19 | class AuthMiddleware(object): 20 | 21 | def __init__(self, app, authDB): 22 | self.app = app 23 | self.authDB = authDB 24 | 25 | def __call__(self, environ, start_response): 26 | req = flask.Request(environ) 27 | if not self.authDB.isAuthenticated(req): 28 | response = self.authDB.challenge() 29 | return response(environ, start_response) 30 | return self.app(environ, start_response) 31 | -------------------------------------------------------------------------------- /route53/templates/slicehost/records.html: -------------------------------------------------------------------------------- 1 | {% extends "slicehost/base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Home
  • 5 |
  • Slicehost Zones
  • 6 |
  • Slicehost Records for {{ zone['origin'] }}
  • 7 | {% endblock %} 8 | 9 | {% block body %} 10 |

    Slicehost Records for {{ zone['origin'] }}

    11 | 22 |
    23 | 24 |
    25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /route53/xmltools.py: -------------------------------------------------------------------------------- 1 | try: 2 | import lxml.etree as etree 3 | except ImportError: 4 | try: 5 | import cElementTree as etree 6 | print "Using cElementTree" 7 | except ImportError: 8 | try: 9 | import elementtree.ElementTree as etree 10 | except ImportError: 11 | from xml.etree import ElementTree as etree 12 | 13 | 14 | NAMESPACE = "{https://route53.amazonaws.com/doc/2010-10-01/}" 15 | RECORDSET_TAG = NAMESPACE + 'ResourceRecordSet' 16 | NAME_TAG = NAMESPACE + 'Name' 17 | TYPE_TAG = NAMESPACE + 'Type' 18 | TTL_TAG = NAMESPACE + 'TTL' 19 | RECORD_TAG = NAMESPACE + 'ResourceRecord' 20 | VALUE_TAG = NAMESPACE + 'Value' 21 | RECORDS_TAG = NAMESPACE + 'ResourceRecords' 22 | 23 | 24 | def render_change_batch(context): 25 | from route53 import app 26 | template = app.jinja_env.get_template('xml/change_batch.xml') 27 | rendered_xml = template.render(context) 28 | return rendered_xml 29 | -------------------------------------------------------------------------------- /route53/static/css/print.css: -------------------------------------------------------------------------------- 1 | body.bp { line-height: 1.5; font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; color: black; background: none; font-size: 10pt; } 2 | body.bp .container { background: none; } 3 | body.bp hr { background: #cccccc; color: #cccccc; width: 100%; height: 2px; margin: 2em 0; padding: 0; border: none; } 4 | body.bp hr.space { background: white; color: white; } 5 | body.bp h1, body.bp h2, body.bp h3, body.bp h4, body.bp h5, body.bp h6 { font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; } 6 | body.bp code { font-size: 0.9em; font-family: "andale mono", "lucida console", monospace; } 7 | body.bp a img { border: none; } 8 | body.bp a:link, body.bp a:visited { background: transparent; font-weight: 700; text-decoration: underline; } 9 | body.bp p img.top { margin-top: 0; } 10 | body.bp blockquote { margin: 1.5em; padding: 1em; font-style: italic; font-size: 0.9em; } 11 | body.bp .small { font-size: 0.9em; } 12 | body.bp .large { font-size: 1.1em; } 13 | body.bp .quiet { color: #999999; } 14 | body.bp .hide { display: none; } 15 | -------------------------------------------------------------------------------- /route53/static/css/ie.css: -------------------------------------------------------------------------------- 1 | body.bp { text-align: center; } 2 | * html body.bp legend { margin: 0px -8px 16px 0; padding: 0; } 3 | html > body.bp p code { *white-space: normal; } 4 | body.bp .container { text-align: left; } 5 | body.bp sup { vertical-align: text-top; } 6 | body.bp sub { vertical-align: text-bottom; } 7 | body.bp hr { margin: -8px auto 11px; } 8 | body.bp img { -ms-interpolation-mode: bicubic; } 9 | body.bp fieldset { padding-top: 0; } 10 | body.bp textarea { overflow: auto; } 11 | body.bp input.text { margin: 0.5em 0; background-color: white; border: 1px solid #bbbbbb; } 12 | body.bp input.text:focus { border: 1px solid #666666; } 13 | body.bp input.title { margin: 0.5em 0; background-color: white; border: 1px solid #bbbbbb; } 14 | body.bp input.title:focus { border: 1px solid #666666; } 15 | body.bp input.checkbox { position: relative; top: 0.25em; } 16 | body.bp input.radio { position: relative; top: 0.25em; } 17 | body.bp input.button { position: relative; top: 0.25em; } 18 | body.bp textarea { margin: 0.5em 0; } 19 | body.bp select { margin: 0.5em 0; } 20 | body.bp button { position: relative; top: 0.25em; } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Andrii Kurinnyi 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /route53/__init__.py: -------------------------------------------------------------------------------- 1 | import urllib 2 | 3 | from flask import Flask 4 | 5 | from route53.views.zones import zones 6 | from route53.views.main import main 7 | from route53.views.records import records 8 | from route53.views.slicehost import slicehost 9 | 10 | from auth import FlaskRealmDigestDB, AuthMiddleware 11 | 12 | app = Flask(__name__) 13 | app.register_module(main) 14 | app.register_module(zones, url_prefix='/zones') 15 | app.register_module(records, url_prefix='/records') 16 | app.register_module(slicehost, url_prefix='/slicehost') 17 | 18 | # load configuration 19 | app.config.from_pyfile('application.cfg') 20 | 21 | 22 | @app.template_filter('shortid') 23 | def shortid(s): 24 | return s.replace('/hostedzone/', '') 25 | 26 | 27 | @app.template_filter('urlencode') 28 | def urlencode(s): 29 | return urllib.quote(s, '/') 30 | 31 | #authentication 32 | 33 | auth_users = app.config.get('AUTH_USERS', None) 34 | if auth_users: 35 | authDB = FlaskRealmDigestDB('Route53Realm') 36 | 37 | for user,password in auth_users: 38 | authDB.add_user(user, password) 39 | 40 | app.wsgi_app = AuthMiddleware(app.wsgi_app, authDB) 41 | 42 | import route53.models 43 | -------------------------------------------------------------------------------- /route53/templates/records/delete.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Records for {{ zone['Name'] }}
  • 6 |
  • Delete record {{ val_dict['type'] }} {{ val_dict['name'] }}
  • 7 | {% endblock %} 8 | 9 | {% block body %} 10 |

    Delete record

    11 | {% if error %} 12 |

    {{ error.error_message }}

    13 | {% endif %} 14 |
    15 |

    Are you sure?

    16 | 17 | 18 | 19 | 20 | 21 | {% for value in values %} 22 | 23 | {% endfor %} 24 |

    25 | 26 | No 27 |

    28 |
    29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /route53/models.py: -------------------------------------------------------------------------------- 1 | import simplejson 2 | 3 | from route53 import app 4 | 5 | from flaskext.sqlalchemy import SQLAlchemy 6 | 7 | # initialize db 8 | db = SQLAlchemy(app) 9 | 10 | # Models 11 | 12 | 13 | class ChangeBatch(db.Model): 14 | __tablename__ = "change_batches" 15 | 16 | id = db.Column(db.Integer, primary_key=True) 17 | change_id = db.Column(db.String(255)) 18 | status = db.Column(db.String(255)) 19 | comment = db.Column(db.String(255)) 20 | 21 | changes = db.relation("Change", backref="change_batch") 22 | 23 | def process_response(self, resp): 24 | change_info = resp['ChangeResourceRecordSetsResponse']['ChangeInfo'] 25 | self.change_id = change_info['Id'][8:] 26 | self.status = change_info['Status'] 27 | 28 | 29 | class Change(db.Model): 30 | __tablename__ = "changes" 31 | 32 | id = db.Column(db.Integer, primary_key=True) 33 | action = db.Column(db.String(255)) 34 | name = db.Column(db.String(255)) 35 | type = db.Column(db.String(255)) 36 | ttl = db.Column(db.String(255)) 37 | value = db.Column(db.String(255)) 38 | 39 | change_batch_id = db.Column(db.Integer, db.ForeignKey("change_batches.id")) 40 | 41 | @property 42 | def values(self): 43 | return simplejson.loads(self.value) 44 | 45 | @values.setter 46 | def values(self, values): 47 | self.value = simplejson.dumps(values) 48 | -------------------------------------------------------------------------------- /route53/templates/records/update.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Records for {{ zone['Name'] }}
  • 6 |
  • Update record {{ val_dict['type'] }} {{ val_dict['name'] }}
  • 7 | {% endblock %} 8 | 9 | {% block body %} 10 |

    Update record

    11 | {% if error %} 12 |

    {{ error.error_message }}

    13 | {% endif %} 14 |
    15 | {{ form.csrf_token }} 16 | {% from "_formhelpers.html" import render_field %} 17 |
    18 | {{ render_field(form.type) }} 19 | {{ render_field(form.name) }} 20 | {{ render_field(form.value) }} 21 | {{ render_field(form.ttl) }} 22 | {{ render_field(form.comment) }} 23 |
    24 | 25 | 26 | 27 | {% for value in values %} 28 | 29 | {% endfor %} 30 |

    31 | 32 | No 33 |

    34 |
    35 | {% endblock %} 36 | -------------------------------------------------------------------------------- /route53/templates/records/new.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Records for {{ zone['Name'] }}
  • 6 |
  • Add new record
  • 7 | {% endblock %} 8 | 9 | {% block body %} 10 |

    Add new record

    11 |

    You can use semicolon ";" to separate values in the "Value" field. It allows you to create recordset with several records.

    12 |

    Examples

    13 |

    Assign multiple IPs to one A record for round-robin DNS:
    192.168.1.1;192.168.1.2

    14 |

    Configure MX records for Google Apps:
    1 ASPMX.L.GOOGLE.COM;5 ALT1.ASPMX.L.GOOGLE.COM;5 ALT2.ASPMX.L.GOOGLE.COM;10 ASPMX2.GOOGLEMAIL.COM;10 ASPMX3.GOOGLEMAIL.COM

    15 | {% if error %} 16 |

    {{ error.error_message }}

    17 | {% endif %} 18 |
    19 | {{ form.csrf_token }} 20 | {% from "_formhelpers.html" import render_field %} 21 |
    22 | {{ render_field(form.type) }} 23 | {{ render_field(form.name) }} 24 | {{ render_field(form.value) }} 25 | {{ render_field(form.ttl) }} 26 | {{ render_field(form.comment) }} 27 |
    28 |

    29 | 30 | Cancel 31 |

    32 |
    33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /route53/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Route 53 Manager 5 | 6 | 7 | 10 | 11 | 12 |
    13 |
    14 |

    Route 53 Manager

    15 |
    16 |
      17 | {% if request.authorization.username %} 18 |
    • Welcome, {{ request.authorization.username }}
    • 19 | {% endif %} 20 | {% block toolbar %} 21 |
    • Import from Slicehost
    • 22 | {% endblock %} 23 |
    24 |
    25 |
    26 | 32 |
    33 | {% with messages = get_flashed_messages() %} 34 | {% if messages %} 35 | 40 | {% endif %} 41 | {% endwith %} 42 |
    43 | 44 |
    45 | {% block body %} 46 | {% endblock %} 47 |
    48 |
    49 | 50 | 51 | -------------------------------------------------------------------------------- /route53/templates/zones/records.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block breadcrumbs %} 4 |
  • Zones
  • 5 |
  • Records for {{ zone['Name'] }}
  • 6 | {% endblock %} 7 | 8 | {% block body %} 9 |

    Hosted Zone "{{ zone['Name'] }}"

    10 |

    Add new record

    11 | {% if groups %} 12 | {% for groupname, recordsets in groups %} 13 |

    {{ groupname }}

    14 | 34 | {% endfor %} 35 | {% endif %} 36 | {% endblock %} 37 | -------------------------------------------------------------------------------- /route53/forms.py: -------------------------------------------------------------------------------- 1 | from flaskext import wtf 2 | from flaskext.wtf import validators 3 | 4 | 5 | RECORD_CHOICES = [ 6 | ('A', 'A'), 7 | ('AAAA', 'AAAA'), 8 | ('CNAME', 'CNAME'), 9 | ('MX', 'MX'), 10 | ('NS', 'NS'), 11 | ('PTR', 'PTR'), 12 | ('SOA', 'SOA'), 13 | ('SPF', 'SPF'), 14 | ('SRV', 'SRV'), 15 | ('TXT', 'TXT'), 16 | ] 17 | 18 | 19 | class ZoneForm(wtf.Form): 20 | name = wtf.TextField('Domain Name', validators=[validators.Required()]) 21 | comment = wtf.TextAreaField('Comment') 22 | 23 | 24 | class RecordForm(wtf.Form): 25 | type = wtf.SelectField("Type", choices=RECORD_CHOICES) 26 | name = wtf.TextField("Name", validators=[validators.Required()]) 27 | value = wtf.TextField("Value", validators=[validators.Required()]) 28 | ttl = wtf.IntegerField("TTL", default="86400", 29 | validators=[validators.Required()]) 30 | comment = wtf.TextAreaField("Comment") 31 | 32 | @property 33 | def values(self): 34 | if self.type.data != 'TXT': 35 | return filter(lambda x: x, 36 | map(lambda x: x.strip(), 37 | self.value.data.strip().split(';'))) 38 | else: 39 | return [self.value.data.strip()] 40 | 41 | class RecordAliasForm(wtf.Form): 42 | type = wtf.SelectField("Type", choices=RECORD_CHOICES) 43 | name = wtf.TextField("Name", validators=[validators.Required()]) 44 | alias_hosted_zone_id = wtf.TextField("Alias hosted zone ID", validators=[validators.Required()]) 45 | alias_dns_name = wtf.TextField("Alias DNS name", validators=[validators.Required()]) 46 | ttl = wtf.IntegerField("TTL", default="86400", 47 | validators=[validators.Required()]) 48 | comment = wtf.TextAreaField("Comment") 49 | 50 | class APIKeyForm(wtf.Form): 51 | key = wtf.TextField('API Key', validators=[validators.Required()]) 52 | -------------------------------------------------------------------------------- /sass/src/screen.sass: -------------------------------------------------------------------------------- 1 | // This import applies a global reset to any page that imports this stylesheet. 2 | @import blueprint/reset 3 | // To configure blueprint, edit the partials/base.sass file. 4 | @import partials/base 5 | // Import all the default blueprint modules so that we can access their mixins. 6 | @import blueprint 7 | // Import the non-default scaffolding module. 8 | @import blueprint/scaffolding 9 | @import "blueprint/buttons" 10 | 11 | @import "compass/utilities/lists" 12 | @import "compass/utilities/links" 13 | @import "compass/css3" 14 | 15 | // To generate css equivalent to the blueprint css but with your 16 | // configuration applied, uncomment: 17 | // @include blueprint 18 | 19 | // But Compass recommends that you scope your blueprint styles 20 | // So that you can better control what pages use blueprint 21 | // when stylesheets are concatenated together. 22 | 23 | +blueprint-scaffolding("body.bp") 24 | body.bp 25 | +blueprint-typography(true) 26 | +blueprint-utilities 27 | +blueprint-debug 28 | +blueprint-interaction 29 | // Remove the scaffolding when you're ready to start doing visual design. 30 | // Or leave it in if you're happy with how blueprint looks out-of-the-box 31 | .container 32 | +container 33 | +box-shadow 34 | 35 | .heading 36 | $heading-columns: $blueprint-grid-columns - 1 37 | +column($heading-columns, true) 38 | padding: 20px 20px 0 39 | min-height: 60px 40 | border-bottom: 1px solid #222 41 | h1 42 | $sidebar-columns: floor($blueprint-grid-columns / 3) 43 | +column($sidebar-columns) 44 | a 45 | +hover-link 46 | color: $font-color 47 | .toolbar 48 | $content-columns: ceil(2 * $blueprint-grid-columns / 3) - 1 49 | +column($content-columns, true) 50 | ul 51 | +float-right 52 | +horizontal-list 53 | 54 | .breadcrumbs 55 | clear: both 56 | border-bottom: 1px solid #555 57 | ul 58 | overflow: hidden 59 | +horizontal-list(10px) 60 | margin-left: 20px 61 | padding: 0.5em 0 62 | a 63 | +hover-link 64 | li 65 | border-right: 1px solid #222 66 | &:last-child, &.last 67 | border-right: none 68 | 69 | .messages 70 | clear: both 71 | +column($blueprint-grid-columns) 72 | .flashes 73 | padding-left: 20px 74 | margin-bottom: 0 75 | margin-top: 1.5em 76 | li 77 | list-style-type: none 78 | +notice 79 | margin-bottom: 0 80 | 81 | .body 82 | clear: both 83 | padding: 2em 20px 84 | $body-columns: $blueprint-grid-columns - 1 85 | +column($body-columns) 86 | 87 | a.button 88 | +anchor-button 89 | 90 | button 91 | +button-button 92 | 93 | form.bp 94 | +blueprint-form 95 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Route 53 Manager 2 | ---------------- 3 | 4 | Simple GUI for Route 53 Amazon Cloud DNS service written using Flask and 5 | boto. 6 | 7 | Features 8 | ======== 9 | 10 | * Create/delete hosted zones 11 | * Create/update/delete records 12 | * Manipulate recordsets 13 | * Stores change log in the SQL database 14 | * Optional Digest Authentication 15 | * Import DNS records from Slicehost one zone at a time 16 | * Clone DNS zone with all rules, but new domain 17 | 18 | Route 53 Manager is meant to be running locally, on user's machine, or local 19 | network behind the firewall. It allows you to manage DNS zones and records 20 | for AWS credentials specified in application.cfg 21 | (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) 22 | 23 | Since you are running the app, you don't need to pass your AWS credentials to 24 | any third party website. 25 | 26 | How to install 27 | ============== 28 | 29 | Clone the project from GitHub 30 | 31 | :: 32 | 33 | git clone git://github.com/zen4ever/route53manager.git 34 | cd route53manager 35 | 36 | Install dependencies 37 | 38 | :: 39 | 40 | pip install -r requirements.txt 41 | 42 | Create config file 43 | 44 | :: 45 | 46 | cp route53/application.cfg.example route53/application.cfg 47 | 48 | Add your AWS credentials to newly created "application.cfg" 49 | 50 | Create empty database and run development server 51 | 52 | :: 53 | 54 | ./create_db.py 55 | ./runserver.py 56 | 57 | Visit http://127.0.0.1:5000/ in your favorite browser 58 | 59 | Authentication 60 | ============== 61 | 62 | If you want to run Route53Manager on intranet open only to certain users, 63 | you can enable digest authentication. Define AUTH_USERS variable in your 64 | route53/application.cfg like this: 65 | 66 | :: 67 | 68 | AUTH_USERS = [ 69 | ('admin', 'admin_password'), 70 | ('test', 'secret_password'), 71 | ] 72 | 73 | FAQ 74 | === 75 | 76 | 1. Which Python version is supported by route53manager? 77 | 78 | Flask `documentation `_ says that Python2.5+ is required. Some of the plugins might use Python 2.6 specific features, so, recommended version is Python 2.6+. 79 | 80 | 2. How do I make Route53Manager accessible to requests from other machines on 81 | my local network? 82 | 83 | By default runserver.py spins a local development server which listens to 84 | 127.0.0.1 IP address, which is not accessible from other machines on your 85 | network. You can use your machine's IP address (e.g. 192.168.1.15) or 86 | 0.0.0.0 in runserver.py 87 | 88 | :: 89 | 90 | app.run(host="0.0.0.0") 91 | 92 | so your dev server will listen to external requests. 93 | 94 | You can also use some other WSGI server like Gunicorn. 95 | 96 | :: 97 | 98 | pip install gunicorn 99 | gunicorn route53:app -b 0.0.0.0:8000 100 | -------------------------------------------------------------------------------- /route53/views/slicehost.py: -------------------------------------------------------------------------------- 1 | from boto.route53.exception import DNSServerError 2 | from functools import wraps 3 | from itertools import groupby 4 | 5 | from flask import Module, session, redirect, url_for, render_template, request 6 | 7 | from pyactiveresource.activeresource import ActiveResource 8 | 9 | from route53.connection import get_connection 10 | from route53.forms import APIKeyForm 11 | from route53.xmltools import render_change_batch 12 | 13 | slicehost = Module(__name__) 14 | 15 | API_KEY = 'slicehost_api_key' 16 | API_URL = 'https://%s@api.slicehost.com/' 17 | 18 | 19 | def get_zone_class(): 20 | class Zone(ActiveResource): 21 | _site = API_URL % session[API_KEY] 22 | return Zone 23 | 24 | 25 | def get_record_class(): 26 | class Record(ActiveResource): 27 | _site = API_URL % session[API_KEY] 28 | return Record 29 | 30 | 31 | def requires_key(f): 32 | @wraps(f) 33 | def decorated(*args, **kwargs): 34 | if not API_KEY in session: 35 | return redirect(url_for('slicehost.index')) 36 | return f(*args, **kwargs) 37 | return decorated 38 | 39 | 40 | @slicehost.route('/', methods=['GET', 'POST']) 41 | def index(): 42 | if 'clean' in request.args: 43 | del session[API_KEY] 44 | if API_KEY in session: 45 | return redirect(url_for('slicehost.zones')) 46 | form = APIKeyForm() 47 | if form.validate_on_submit(): 48 | session[API_KEY] = form.key.data 49 | return redirect(url_for('slicehost.zones')) 50 | return render_template('slicehost/index.html', form=form) 51 | 52 | 53 | @slicehost.route('/zones') 54 | @requires_key 55 | def zones(): 56 | Zone = get_zone_class() 57 | zones = Zone.find() 58 | return render_template('slicehost/zones.html', zones=zones) 59 | 60 | 61 | @slicehost.route('/zones/') 62 | @requires_key 63 | def records(zone_id): 64 | Zone = get_zone_class() 65 | zone = Zone.find(zone_id) 66 | Record = get_record_class() 67 | records = Record.find(zone_id=zone_id) 68 | records = sorted(records, key=lambda x: x.record_type) 69 | results = [] 70 | for k, g in groupby(records, key=lambda x: (x.record_type, x.name)): 71 | record_type, name = k 72 | results.append((record_type, name, list(g))) 73 | return render_template('slicehost/records.html', zone=zone, records=results) 74 | 75 | 76 | @slicehost.route('/zones//import', methods=['GET', 'POST']) 77 | @requires_key 78 | def import_zone(zone_id): 79 | from route53.models import ChangeBatch, Change, db 80 | 81 | Zone = get_zone_class() 82 | zone = Zone.find(zone_id) 83 | Record = get_record_class() 84 | 85 | # filter out NS records 86 | records = filter(lambda x: x.record_type != 'NS', Record.find(zone_id=zone_id)) 87 | 88 | records = sorted(records, key=lambda x: x.record_type) 89 | 90 | # order records by record_type and name into recordsets 91 | 92 | conn = get_connection() 93 | response = conn.create_hosted_zone(zone.origin) 94 | info = response['CreateHostedZoneResponse'] 95 | new_zone_id = info['HostedZone']['Id'] 96 | 97 | errors = [] 98 | 99 | for k, g in groupby(records, key=lambda x: (x.record_type, x.name)): 100 | change_batch = ChangeBatch(change_id='', 101 | status='created', 102 | comment='') 103 | 104 | db.session.add(change_batch) 105 | record_type, name = k 106 | rcds = list(g) 107 | record_name = zone.origin in name and name or name + "." + zone.origin 108 | 109 | if record_type not in ('MX', 'SRV'): 110 | values = map(lambda x: x.data, rcds) 111 | else: 112 | values = map(lambda x: "%s %s" % (x.aux, x.data), rcds) 113 | change = Change(action="CREATE", 114 | name=record_name, 115 | type=record_type, 116 | ttl=rcds[0].ttl, 117 | values={'values':values}, 118 | change_batch_id=change_batch.id) 119 | db.session.add(change) 120 | changes = [change] 121 | 122 | rendered_xml = render_change_batch({'changes': changes, 'comment': ''}) 123 | 124 | try: 125 | from route53 import shortid 126 | resp = conn.change_rrsets(shortid(new_zone_id), rendered_xml) 127 | change_batch.process_response(resp) 128 | db.session.commit() 129 | except DNSServerError as error: 130 | errors.append((record_type, name, error)) 131 | db.session.rollback() 132 | 133 | if errors: 134 | return render_template('slicehost/import_zone.html', 135 | errors=errors, 136 | zone=zone) 137 | 138 | return redirect(url_for('main.index')) 139 | -------------------------------------------------------------------------------- /route53/views/zones.py: -------------------------------------------------------------------------------- 1 | from boto.route53.exception import DNSServerError 2 | from itertools import groupby 3 | from flask import Module 4 | 5 | from flask import url_for, render_template, \ 6 | redirect, flash, request 7 | 8 | from route53.forms import ZoneForm 9 | from route53.connection import get_connection 10 | 11 | from route53.xmltools import render_change_batch 12 | 13 | zones = Module(__name__) 14 | 15 | 16 | @zones.route('/') 17 | def zones_list(): 18 | conn = get_connection() 19 | response = conn.get_all_hosted_zones() 20 | zones = response['ListHostedZonesResponse']['HostedZones'] 21 | return render_template('zones/list.html', zones=zones) 22 | 23 | 24 | @zones.route('/new', methods=['GET', 'POST']) 25 | def zones_new(): 26 | conn = get_connection() 27 | 28 | form = ZoneForm() 29 | if form.validate_on_submit(): 30 | response = conn.create_hosted_zone( 31 | form.name.data, 32 | comment=form.comment.data) 33 | 34 | info = response['CreateHostedZoneResponse'] 35 | 36 | nameservers = ', '.join(info['DelegationSet']['NameServers']) 37 | zone_id = info['HostedZone']['Id'] 38 | 39 | flash(u"A zone with id %s has been created. " 40 | u"Use following nameservers %s" 41 | % (zone_id, nameservers)) 42 | 43 | return redirect(url_for('zones_list')) 44 | return render_template('zones/new.html', form=form) 45 | 46 | 47 | @zones.route('//delete', methods=['GET', 'POST']) 48 | def zones_delete(zone_id): 49 | conn = get_connection() 50 | zone = conn.get_hosted_zone(zone_id)['GetHostedZoneResponse']['HostedZone'] 51 | 52 | error = None 53 | 54 | if request.method == 'POST' and 'delete' in request.form: 55 | try: 56 | conn.delete_hosted_zone(zone_id) 57 | 58 | flash(u"A zone with id %s has been deleted" % zone_id) 59 | 60 | return redirect(url_for('zones_list')) 61 | except DNSServerError as error: 62 | error = error 63 | return render_template('zones/delete.html', 64 | zone_id=zone_id, 65 | zone=zone, 66 | error=error) 67 | 68 | 69 | @zones.route('/') 70 | def zones_detail(zone_id): 71 | conn = get_connection() 72 | resp = conn.get_hosted_zone(zone_id) 73 | zone = resp['GetHostedZoneResponse']['HostedZone'] 74 | nameservers = resp['GetHostedZoneResponse']['DelegationSet']['NameServers'] 75 | 76 | return render_template('zones/detail.html', 77 | zone_id=zone_id, 78 | zone=zone, 79 | nameservers=nameservers) 80 | 81 | 82 | @zones.route('//records') 83 | def zones_records(zone_id): 84 | conn = get_connection() 85 | resp = conn.get_hosted_zone(zone_id) 86 | zone = resp['GetHostedZoneResponse']['HostedZone'] 87 | 88 | record_resp = sorted(conn.get_all_rrsets(zone_id), key=lambda x: x.type) 89 | 90 | groups = groupby(record_resp, key=lambda x: x.type) 91 | 92 | groups = [(k, list(v)) for k, v in groups] 93 | 94 | return render_template('zones/records.html', 95 | zone_id=zone_id, 96 | zone=zone, 97 | groups=groups) 98 | 99 | 100 | @zones.route('/clone/', methods=['GET', 'POST']) 101 | def zones_clone(zone_id): 102 | conn = get_connection() 103 | 104 | zone_response = conn.get_hosted_zone(zone_id) 105 | original_zone = zone_response['GetHostedZoneResponse']['HostedZone'] 106 | 107 | form = ZoneForm() 108 | errors = [] 109 | 110 | if form.validate_on_submit(): 111 | response = conn.create_hosted_zone( 112 | form.name.data, 113 | comment=form.comment.data) 114 | 115 | info = response['CreateHostedZoneResponse'] 116 | 117 | nameservers = ', '.join(info['DelegationSet']['NameServers']) 118 | 119 | new_zone_id = info['HostedZone']['Id'] 120 | 121 | original_records = conn.get_all_rrsets(zone_id) 122 | 123 | from route53.models import ChangeBatch, Change, db 124 | 125 | for recordset in original_records: 126 | if not recordset.type in ["SOA", "NS"]: 127 | 128 | change_batch = ChangeBatch(change_id='', 129 | status='created', 130 | comment='') 131 | db.session.add(change_batch) 132 | change = Change(action="CREATE", 133 | name=recordset.name.replace(original_zone['Name'], 134 | form.name.data), 135 | type=recordset.type, 136 | ttl=recordset.ttl, 137 | values = recordset.resource_records, 138 | change_batch_id=change_batch.id) 139 | 140 | db.session.add(change) 141 | changes = [change] 142 | 143 | rendered_xml = render_change_batch({'changes': changes, 'comment': ''}) 144 | 145 | try: 146 | from route53 import shortid 147 | resp = conn.change_rrsets(shortid(new_zone_id), rendered_xml) 148 | change_batch.process_response(resp) 149 | db.session.commit() 150 | except DNSServerError as error: 151 | errors.append((recordset.type, recordset.name, error)) 152 | db.session.rollback() 153 | 154 | if not errors: 155 | flash(u"A zone with id %s has been created. " 156 | u"Use following nameservers %s" 157 | % (new_zone_id, nameservers)) 158 | return redirect(url_for('zones_list')) 159 | 160 | return render_template('zones/clone.html', 161 | form=form, errors=errors, original_zone=original_zone) 162 | -------------------------------------------------------------------------------- /route53/views/records.py: -------------------------------------------------------------------------------- 1 | from boto.route53.exception import DNSServerError 2 | from flask import Module, redirect, url_for, render_template, request, abort 3 | 4 | from route53.forms import RecordForm 5 | from route53.connection import get_connection 6 | from route53.xmltools import render_change_batch 7 | 8 | 9 | records = Module(__name__) 10 | 11 | 12 | @records.route('//new', methods=['GET', 'POST']) 13 | def records_new(zone_id): 14 | from route53.models import ChangeBatch, Change, db 15 | conn = get_connection() 16 | zone = conn.get_hosted_zone(zone_id)['GetHostedZoneResponse']['HostedZone'] 17 | form = RecordForm() 18 | error = None 19 | if form.validate_on_submit(): 20 | change_batch = ChangeBatch(change_id='', 21 | status='created', 22 | comment=form.comment.data) 23 | db.session.add(change_batch) 24 | change = Change(action="CREATE", 25 | name=form.name.data, 26 | type=form.type.data, 27 | ttl=form.ttl.data, 28 | values={'values': form.values}, 29 | change_batch_id=change_batch.id) 30 | db.session.add(change) 31 | rendered_xml = render_change_batch({'changes': [change], 32 | 'comment': change_batch.comment}) 33 | try: 34 | resp = conn.change_rrsets(zone_id, rendered_xml) 35 | change_batch.process_response(resp) 36 | db.session.commit() 37 | return redirect(url_for('zones.zones_records', zone_id=zone_id)) 38 | except DNSServerError as error: 39 | error = error 40 | db.session.rollback() 41 | return render_template('records/new.html', 42 | form=form, 43 | zone=zone, 44 | zone_id=zone_id, 45 | error=error) 46 | 47 | 48 | def get_record_fields(): 49 | fields = [ 50 | 'name', 51 | 'type', 52 | 'ttl', 53 | ] 54 | val_dict = {} 55 | for field in fields: 56 | if request.method == "GET": 57 | result = request.args.get(field, None) 58 | elif request.method == "POST": 59 | result = request.form.get("data_"+field, None) 60 | if result is None: 61 | abort(404) 62 | val_dict[field] = result 63 | return val_dict 64 | 65 | 66 | @records.route('//delete', methods=['GET', 'POST']) 67 | def records_delete(zone_id): 68 | from route53.models import ChangeBatch, Change, db 69 | conn = get_connection() 70 | zone = conn.get_hosted_zone(zone_id)['GetHostedZoneResponse']['HostedZone'] 71 | val_dict = get_record_fields() 72 | 73 | if request.method == "GET": 74 | values = request.args.getlist('value') 75 | alias_hosted_zone_id = request.args.get('alias_hosted_zone_id', None) 76 | alias_dns_name = request.args.get('alias_dns_name', None) 77 | if not values and not alias_hosted_zone_id and not alias_dns_name: 78 | abort(404) 79 | 80 | error = None 81 | if request.method == "POST": 82 | change_batch = ChangeBatch(change_id='', status='created', comment='') 83 | db.session.add(change_batch) 84 | values = request.form.getlist('data_value') 85 | alias_hosted_zone_id = request.form.get('data_alias_hosted_zone_id', None) 86 | alias_dns_name = request.form.get('data_alias_dns_name', None) 87 | change = Change(action="DELETE", 88 | change_batch_id=change_batch.id, 89 | values={ 90 | 'values': values, 91 | 'alias_hosted_zone_id': alias_hosted_zone_id, 92 | 'alias_dns_name': alias_dns_name, 93 | }, 94 | **val_dict) 95 | db.session.add(change) 96 | rendered_xml = render_change_batch({'changes': [change], 97 | 'comment': change_batch.comment}) 98 | try: 99 | resp = conn.change_rrsets(zone_id, rendered_xml) 100 | change_batch.process_response(resp) 101 | db.session.commit() 102 | return redirect(url_for('zones.zones_records', zone_id=zone_id)) 103 | except DNSServerError as error: 104 | error = error 105 | return render_template('records/delete.html', 106 | val_dict=val_dict, 107 | values=values, 108 | alias_hosted_zone_id=alias_hosted_zone_id, 109 | alias_dns_name=alias_dns_name, 110 | zone=zone, 111 | zone_id=zone_id, 112 | error=error) 113 | 114 | 115 | @records.route('//update', methods=['GET', 'POST']) 116 | def records_update(zone_id): 117 | from route53.models import ChangeBatch, Change, db 118 | conn = get_connection() 119 | zone = conn.get_hosted_zone(zone_id)['GetHostedZoneResponse']['HostedZone'] 120 | val_dict = get_record_fields() 121 | 122 | if request.method == "GET": 123 | values = request.args.getlist('value') 124 | if not values: 125 | abort(404) 126 | initial_data = dict(val_dict) 127 | initial_data['value'] = ';'.join(values) 128 | form = RecordForm(**initial_data) 129 | 130 | error = None 131 | if request.method == "POST": 132 | form = RecordForm() 133 | change_batch = ChangeBatch(change_id='', status='created', comment=form.comment.data) 134 | db.session.add(change_batch) 135 | values = request.form.getlist('data_value') 136 | delete_change = Change(action="DELETE", 137 | change_batch_id=change_batch.id, 138 | values={'values': values}, 139 | **val_dict) 140 | create_change = Change(action="CREATE", 141 | change_batch_id=change_batch.id, 142 | values={'values': form.values}, 143 | type=form.type.data, 144 | ttl=form.ttl.data, 145 | name=form.name.data) 146 | db.session.add(delete_change) 147 | db.session.add(create_change) 148 | rendered_xml = render_change_batch({'changes': [delete_change, create_change], 149 | 'comment': change_batch.comment}) 150 | try: 151 | resp = conn.change_rrsets(zone_id, rendered_xml) 152 | change_batch.process_response(resp) 153 | db.session.commit() 154 | return redirect(url_for('zones.zones_records', zone_id=zone_id)) 155 | except DNSServerError as error: 156 | error = error 157 | return render_template('records/update.html', 158 | val_dict=val_dict, 159 | values=values, 160 | form=form, 161 | zone=zone, 162 | zone_id=zone_id, 163 | error=error) 164 | -------------------------------------------------------------------------------- /authdigest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | werkzeug.contrib.authdigest 4 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 5 | 6 | The authdigest module contains classes to support 7 | digest authentication compliant with RFC 2617. 8 | 9 | 10 | Usage 11 | ===== 12 | 13 | :: 14 | 15 | from werkzeug.contrib.authdigest import RealmDigestDB 16 | 17 | authDB = RealmDigestDB('test-realm') 18 | authDB.add_user('admin', 'test') 19 | 20 | def protectedResource(environ, start_reponse): 21 | request = Request(environ) 22 | if not authDB.isAuthenticated(request): 23 | return authDB.challenge() 24 | 25 | return get_protected_response(request) 26 | 27 | :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details. 28 | :license: BSD, see LICENSE for more details. 29 | """ 30 | 31 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 32 | #~ Imports 33 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 34 | 35 | import os 36 | import weakref 37 | import hashlib 38 | import werkzeug 39 | 40 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 41 | #~ Realm Digest Credentials Database 42 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 43 | 44 | class RealmDigestDB(object): 45 | """Database mapping user to hashed password. 46 | 47 | Passwords are hashed using realm key, and specified 48 | digest algorithm. 49 | 50 | :param realm: string identifing the hashing realm 51 | :param algorthm: string identifying hash algorithm to use, 52 | default is 'md5' 53 | """ 54 | 55 | def __init__(self, realm, algorithm='md5'): 56 | self.realm = realm 57 | self.alg = self.newAlgorithm(algorithm) 58 | self.db = self.newDB() 59 | 60 | @property 61 | def algorithm(self): 62 | return self.alg.algorithm 63 | 64 | def toDict(self): 65 | r = {'cfg':{ 'algorithm': self.alg.algorithm, 66 | 'realm': self.realm}, 67 | 'db': self.db, } 68 | return r 69 | def toJson(self, **kw): 70 | import json 71 | kw.setdefault('sort_keys', True) 72 | kw.setdefault('indent', 2) 73 | return json.dumps(self.toDict(), **kw) 74 | 75 | def add_user(self, user, password): 76 | r = self.alg.hashPassword(user, self.realm, password) 77 | self.db[user] = r 78 | return r 79 | 80 | def __contains__(self, user): 81 | return user in self.db 82 | def get(self, user, default=None): 83 | return self.db.get(user, default) 84 | def __getitem__(self, user): 85 | return self.db.get(user) 86 | def __setitem__(self, user, password): 87 | return self.add_user(user, password) 88 | def __delitem__(self, user): 89 | return self.db.pop(user, None) 90 | 91 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 92 | 93 | def newDB(self): 94 | return dict() 95 | def newAlgorithm(self, algorithm): 96 | return DigestAuthentication(algorithm) 97 | 98 | def isAuthenticated(self, request, **kw): 99 | authResult = AuthenticationResult(self) 100 | request.authentication = authResult 101 | 102 | authorization = request.authorization 103 | if authorization is None: 104 | return authResult.deny('initial', None) 105 | authorization.result = authResult 106 | 107 | hashPass = self[authorization.username] 108 | if hashPass is None: 109 | return authResult.deny('unknown_user') 110 | elif not self.alg.verify(authorization, hashPass, method=request.method, **kw): 111 | return authResult.deny('invalid_password') 112 | else: 113 | return authResult.approve('success') 114 | 115 | challenge_class = werkzeug.Response 116 | def challenge(self, response=None, status=401): 117 | try: 118 | authReq = response.www_authenticate 119 | except AttributeError: 120 | response = self.challenge_class(response, status) 121 | authReq = response.www_authenticate 122 | else: 123 | if isinstance(status, (int, long)): 124 | response.status_code = status 125 | else: response.status = status 126 | 127 | authReq.set_digest(self.realm, os.urandom(8).encode('hex')) 128 | return response 129 | 130 | 131 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 132 | #~ Authentication Result 133 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 134 | 135 | class AuthenticationResult(object): 136 | """Authentication Result object 137 | 138 | Created by RealmDigestDB.isAuthenticated to operate as a boolean result, 139 | and storage of authentication information.""" 140 | 141 | authenticated = None 142 | reason = None 143 | status = 500 144 | 145 | def __init__(self, authDB): 146 | self.authDB = weakref.ref(authDB) 147 | 148 | def __repr__(self): 149 | return '' % ( 150 | self.authenticated, self.reason) 151 | def __nonzero__(self): 152 | return bool(self.authenticated) 153 | 154 | def deny(self, reason, authenticated=False): 155 | if bool(authenticated): 156 | raise ValueError("Denied authenticated parameter must evaluate as False") 157 | self.authenticated = authenticated 158 | self.reason = reason 159 | self.status = 401 160 | return self 161 | 162 | def approve(self, reason, authenticated=True): 163 | if not bool(authenticated): 164 | raise ValueError("Approved authenticated parameter must evaluate as True") 165 | self.authenticated = authenticated 166 | self.reason = reason 167 | self.status = 200 168 | return self 169 | 170 | def challenge(self, response=None, force=False): 171 | if force or not self: 172 | return self.authDB().challenge(response, self.status) 173 | 174 | 175 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 176 | #~ Digest Authentication Algorithm 177 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 178 | 179 | class DigestAuthentication(object): 180 | """Digest Authentication implementation. 181 | 182 | references: 183 | "HTTP Authentication: Basic and Digest Access Authentication". RFC 2617. 184 | http://tools.ietf.org/html/rfc2617 185 | 186 | "Digest access authentication" 187 | http://en.wikipedia.org/wiki/Digest_access_authentication 188 | """ 189 | 190 | def __init__(self, algorithm='md5'): 191 | self.algorithm = algorithm.lower() 192 | self.H = self.hashAlgorithms[self.algorithm] 193 | 194 | def verify(self, authorization, hashPass=None, **kw): 195 | reqResponse = self.digest(authorization, hashPass, **kw) 196 | if reqResponse: 197 | return (authorization.response.lower() == reqResponse.lower()) 198 | 199 | def digest(self, authorization, hashPass=None, **kw): 200 | if authorization is None: 201 | return None 202 | 203 | if hashPass is None: 204 | hA1 = self._compute_hA1(authorization, kw['password']) 205 | else: hA1 = hashPass 206 | 207 | hA2 = self._compute_hA2(authorization, kw.pop('method', 'GET')) 208 | 209 | if 'auth' in authorization.qop: 210 | res = self._compute_qop_auth(authorization, hA1, hA2) 211 | elif not authorization.qop: 212 | res = self._compute_qop_empty(authorization, hA1, hA2) 213 | else: 214 | raise ValueError("Unsupported qop: %r" % (authorization.qop,)) 215 | return res 216 | 217 | def hashPassword(self, username, realm, password): 218 | return self.H(username, realm, password) 219 | 220 | def _compute_hA1(self, auth, password=None): 221 | return self.hashPassword(auth.username, auth.realm, password or auth.password) 222 | def _compute_hA2(self, auth, method): 223 | return self.H(method, auth.uri) 224 | def _compute_qop_auth(self, auth, hA1, hA2): 225 | return self.H(hA1, auth.nonce, auth.nc, auth.cnonce, auth.qop, hA2) 226 | def _compute_qop_empty(self, auth, hA1, hA2): 227 | return self.H(hA1, auth.nonce, hA2) 228 | 229 | #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 230 | 231 | hashAlgorithms = {} 232 | 233 | @classmethod 234 | def addDigestHashAlg(klass, key, hashObj): 235 | key = key.lower() 236 | def H(*args): 237 | x = ':'.join(map(str, args)) 238 | return hashObj(x).hexdigest() 239 | 240 | H.__name__ = "H_"+key 241 | klass.hashAlgorithms[key] = H 242 | return H 243 | 244 | DigestAuthentication.addDigestHashAlg('md5', hashlib.md5) 245 | DigestAuthentication.addDigestHashAlg('sha', hashlib.sha1) 246 | -------------------------------------------------------------------------------- /route53/static/css/screen.css: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | html, body { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } 3 | 4 | html { font-size: 100.01%; } 5 | 6 | div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, pre, a, abbr, acronym, address, code, del, dfn, em, img, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, caption, tbody, tfoot, thead, tr { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } 7 | 8 | blockquote, q { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; quotes: "" ""; } 9 | blockquote:before, blockquote:after, q:before, q:after { content: ""; } 10 | 11 | th, td, caption { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; text-align: left; font-weight: normal; vertical-align: middle; } 12 | 13 | table { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; border-collapse: separate; border-spacing: 0; vertical-align: middle; } 14 | 15 | a img { border: none; } 16 | 17 | body.bp .box { padding: 1.5em; margin-bottom: 1.5em; background: #e5ecf9; } 18 | body.bp div.border { padding-right: 4px; margin-right: 5px; border-right: 1px solid #eeeeee; } 19 | body.bp div.colborder { padding-right: 24px; margin-right: 25px; border-right: 1px solid #eeeeee; } 20 | body.bp hr { background: #dddddd; color: #dddddd; clear: both; float: none; width: 100%; height: 0.1em; margin: 0 0 1.45em; border: none; } 21 | body.bp hr.space { background: #dddddd; color: #dddddd; clear: both; float: none; width: 100%; height: 0.1em; margin: 0 0 1.45em; border: none; background: white; color: white; visibility: hidden; } 22 | body.bp form.inline { line-height: 3; } 23 | body.bp form.inline p { margin-bottom: 0; } 24 | 25 | body.bp { line-height: 1.5; font-family: "Helvetica Neue", Arial, Helvetica, sans-serif; color: #333333; font-size: 75%; } 26 | body.bp h1, body.bp h2, body.bp h3, body.bp h4, body.bp h5, body.bp h6 { font-weight: normal; color: #222222; } 27 | body.bp h1 img, body.bp h2 img, body.bp h3 img, body.bp h4 img, body.bp h5 img, body.bp h6 img { margin: 0; } 28 | body.bp h1 { font-size: 3em; line-height: 1; margin-bottom: 0.50em; } 29 | body.bp h2 { font-size: 2em; margin-bottom: 0.75em; } 30 | body.bp h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1.00em; } 31 | body.bp h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; } 32 | body.bp h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.50em; } 33 | body.bp h6 { font-size: 1em; font-weight: bold; } 34 | body.bp p { margin: 0 0 1.5em; } 35 | body.bp p img.left { display: inline; float: left; margin: 1.5em 1.5em 1.5em 0; padding: 0; } 36 | body.bp p img.right { display: inline; float: right; margin: 1.5em 0 1.5em 1.5em; padding: 0; } 37 | body.bp a { text-decoration: underline; color: #000099; } 38 | body.bp a:visited { color: #000066; } 39 | body.bp a:focus { color: black; } 40 | body.bp a:hover { color: black; } 41 | body.bp a:active { color: #cc0099; } 42 | body.bp blockquote { margin: 1.5em; color: #666666; font-style: italic; } 43 | body.bp strong { font-weight: bold; } 44 | body.bp em { font-style: italic; } 45 | body.bp dfn { font-style: italic; font-weight: bold; } 46 | body.bp sup, body.bp sub { line-height: 0; } 47 | body.bp abbr, body.bp acronym { border-bottom: 1px dotted #666666; } 48 | body.bp address { margin: 0 0 1.5em; font-style: italic; } 49 | body.bp del { color: #666666; } 50 | body.bp pre { margin: 1.5em 0; white-space: pre; } 51 | body.bp pre, body.bp code, body.bp tt { font: 1em "andale mono", "lucida console", monospace; line-height: 1.5; } 52 | body.bp li ul, body.bp li ol { margin: 0; } 53 | body.bp ul, body.bp ol { margin: 0 1.5em 1.5em 0; padding-left: 3.333em; } 54 | body.bp ul { list-style-type: disc; } 55 | body.bp ol { list-style-type: decimal; } 56 | body.bp dl { margin: 0 0 1.5em 0; } 57 | body.bp dl dt { font-weight: bold; } 58 | body.bp dd { margin-left: 1.5em; } 59 | body.bp table { margin-bottom: 1.4em; width: 100%; } 60 | body.bp th { font-weight: bold; } 61 | body.bp thead th { background: #c3d9ff; } 62 | body.bp th, body.bp td, body.bp caption { padding: 4px 10px 4px 5px; } 63 | body.bp tr.even td { background: #e5ecf9; } 64 | body.bp tfoot { font-style: italic; } 65 | body.bp caption { background: #eeeeee; } 66 | body.bp .quiet { color: #666666; } 67 | body.bp .loud { color: #111111; } 68 | body.bp .clear { clear: both; } 69 | body.bp .nowrap { white-space: nowrap; } 70 | body.bp .clearfix { overflow: hidden; *zoom: 1; } 71 | body.bp .small { font-size: 0.8em; margin-bottom: 1.875em; line-height: 1.875em; } 72 | body.bp .large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; } 73 | body.bp .first { margin-left: 0; padding-left: 0; } 74 | body.bp .last { margin-right: 0; padding-right: 0; } 75 | body.bp .top { margin-top: 0; padding-top: 0; } 76 | body.bp .bottom { margin-bottom: 0; padding-bottom: 0; } 77 | body.bp .showgrid { background: url('/static/img/grid.png?1293959049'); } 78 | body.bp .error { padding: 0.8em; margin-bottom: 1em; border: 2px solid #dddddd; background: #fbe3e4; color: #8a1f11; border-color: #fbc2c4; } 79 | body.bp .error a { color: #8a1f11; } 80 | body.bp .notice { padding: 0.8em; margin-bottom: 1em; border: 2px solid #dddddd; background: #fff6bf; color: #514721; border-color: #ffd324; } 81 | body.bp .notice a { color: #514721; } 82 | body.bp .success { padding: 0.8em; margin-bottom: 1em; border: 2px solid #dddddd; background: #e6efc2; color: #264409; border-color: #c6d880; } 83 | body.bp .success a { color: #264409; } 84 | body.bp .hide { display: none; } 85 | body.bp .highlight { background: yellow; } 86 | body.bp .added { background: #006600; color: white; } 87 | body.bp .removed { background: #990000; color: white; } 88 | body.bp .container { width: 950px; margin: 0 auto; overflow: hidden; *zoom: 1; -moz-box-shadow: #333333 1px 1px 5px 0; -webkit-box-shadow: #333333 1px 1px 5px 0; -o-box-shadow: #333333 1px 1px 5px 0; box-shadow: #333333 1px 1px 5px 0; } 89 | body.bp .heading { display: inline; float: left; margin-right: 0; width: 910px; padding: 20px 20px 0; min-height: 60px; border-bottom: 1px solid #222222; } 90 | * html body.bp .heading { overflow-x: hidden; } 91 | body.bp .heading h1 { display: inline; float: left; margin-right: 10px; width: 310px; } 92 | * html body.bp .heading h1 { overflow-x: hidden; } 93 | body.bp .heading h1 a { text-decoration: none; color: #333333; } 94 | body.bp .heading h1 a:hover { text-decoration: underline; } 95 | body.bp .heading .toolbar { display: inline; float: left; margin-right: 0; width: 590px; } 96 | * html body.bp .heading .toolbar { overflow-x: hidden; } 97 | body.bp .heading .toolbar ul { display: inline; float: right; margin: 0; padding: 0; border: 0; outline: 0; overflow: hidden; *zoom: 1; } 98 | body.bp .heading .toolbar ul li { list-style-image: none; list-style-type: none; margin-left: 0px; white-space: nowrap; display: inline; float: left; padding-left: 4px; padding-right: 4px; } 99 | body.bp .heading .toolbar ul li:first-child, body.bp .heading .toolbar ul li.first { padding-left: 0; } 100 | body.bp .heading .toolbar ul li:last-child, body.bp .heading .toolbar ul li.last { padding-right: 0; } 101 | body.bp .breadcrumbs { clear: both; border-bottom: 1px solid #555555; } 102 | body.bp .breadcrumbs ul { overflow: hidden; margin: 0; padding: 0; border: 0; outline: 0; overflow: hidden; *zoom: 1; margin-left: 20px; padding: 0.5em 0; } 103 | body.bp .breadcrumbs ul li { list-style-image: none; list-style-type: none; margin-left: 0px; white-space: nowrap; display: inline; float: left; padding-left: 10px; padding-right: 10px; } 104 | body.bp .breadcrumbs ul li:first-child, body.bp .breadcrumbs ul li.first { padding-left: 0; } 105 | body.bp .breadcrumbs ul li:last-child, body.bp .breadcrumbs ul li.last { padding-right: 0; } 106 | body.bp .breadcrumbs ul a { text-decoration: none; } 107 | body.bp .breadcrumbs ul a:hover { text-decoration: underline; } 108 | body.bp .breadcrumbs ul li { border-right: 1px solid #222222; } 109 | body.bp .breadcrumbs ul li:last-child, body.bp .breadcrumbs ul li.last { border-right: none; } 110 | body.bp .messages { clear: both; display: inline; float: left; margin-right: 10px; width: 950px; } 111 | * html body.bp .messages { overflow-x: hidden; } 112 | body.bp .messages .flashes { padding-left: 20px; margin-bottom: 0; margin-top: 1.5em; } 113 | body.bp .messages .flashes li { list-style-type: none; padding: 0.8em; margin-bottom: 1em; border: 2px solid #dddddd; background: #fff6bf; color: #514721; border-color: #ffd324; margin-bottom: 0; } 114 | body.bp .messages .flashes li a { color: #514721; } 115 | body.bp .body { clear: both; padding: 2em 20px; display: inline; float: left; margin-right: 10px; width: 910px; } 116 | * html body.bp .body { overflow-x: hidden; } 117 | body.bp .body a.button { display: -moz-inline-box; -moz-box-orient: vertical; display: inline-block; vertical-align: middle; *vertical-align: auto; margin: 0.7em 0.5em 0.7em 0; border-width: 1px; border-style: solid; font-family: "Lucida Grande", Tahoma, Arial, Verdana, sans-serif; font-size: 100%; line-height: 130%; font-weight: bold; text-decoration: none; cursor: pointer; padding: 5px 10px 5px 7px; } 118 | body.bp .body a.button { *display: inline; } 119 | body.bp .body a.button img { margin: 0 3px -3px 0 !important; padding: 0; border: none; width: 16px; height: 16px; float: none; } 120 | body.bp .body button { display: -moz-inline-box; -moz-box-orient: vertical; display: inline-block; vertical-align: middle; *vertical-align: auto; margin: 0.7em 0.5em 0.7em 0; border-width: 1px; border-style: solid; font-family: "Lucida Grande", Tahoma, Arial, Verdana, sans-serif; font-size: 100%; line-height: 130%; font-weight: bold; text-decoration: none; cursor: pointer; width: auto; overflow: visible; padding: 4px 10px 3px 7px; } 121 | body.bp .body button { *display: inline; } 122 | body.bp .body button img { margin: 0 3px -3px 0 !important; padding: 0; border: none; width: 16px; height: 16px; float: none; } 123 | body.bp .body button[type] { padding: 4px 10px 4px 7px; line-height: 17px; } 124 | *:first-child + html body.bp .body button[type] { padding: 4px 10px 3px 7px; } 125 | 126 | form.bp label { font-weight: bold; } 127 | form.bp fieldset { padding: 1.4em; margin: 0 0 1.5em 0; } 128 | form.bp legend { font-weight: bold; font-size: 1.2em; } 129 | form.bp input.text, form.bp input.title, form.bp input[type=email], form.bp input[type=text], form.bp input[type=password] { margin: 0.5em 0; background-color: white; padding: 5px; } 130 | form.bp input.title { font-size: 1.5em; } 131 | form.bp input[type=checkbox], form.bp input.checkbox, form.bp input[type=radio], form.bp input.radio { position: relative; top: 0.25em; } 132 | form.bp textarea { margin: 0.5em 0; padding: 5px; } 133 | form.bp select { margin: 0.5em 0; } 134 | form.bp fieldset { border: 1px solid #cccccc; } 135 | form.bp input.text, form.bp input.title, form.bp input[type=email], form.bp input[type=text], form.bp input[type=password], form.bp textarea, form.bp select { border: 1px solid #bbbbbb; } 136 | form.bp input.text:focus, form.bp input.title:focus, form.bp input[type=email]:focus, form.bp input[type=text]:focus, form.bp input[type=password]:focus, form.bp textarea:focus, form.bp select:focus { border: 1px solid #666666; } 137 | form.bp input.text, form.bp input.title, form.bp input[type=email], form.bp input[type=text], form.bp input[type=password] { width: 300px; } 138 | form.bp textarea { width: 390px; height: 250px; } 139 | --------------------------------------------------------------------------------