diff --git a/monikerclient/cli/base.py b/monikerclient/cli/base.py index cf0fab7f..8ab1a457 100644 --- a/monikerclient/cli/base.py +++ b/monikerclient/cli/base.py @@ -76,11 +76,6 @@ class ListCommand(Command, Lister): class GetCommand(Command, ShowOne): - def get_parser(self, prog_name): - parser = super(GetCommand, self).get_parser(prog_name) - parser.add_argument('id', help='The ID or Name to get') - return parser - def post_execute(self, results): return results.keys(), results.values() diff --git a/monikerclient/cli/domains.py b/monikerclient/cli/domains.py index 83df27c6..39ff9280 100644 --- a/monikerclient/cli/domains.py +++ b/monikerclient/cli/domains.py @@ -33,12 +33,12 @@ class GetDomainCommand(base.GetCommand): def get_parser(self, prog_name): parser = super(GetDomainCommand, self).get_parser(prog_name) - parser.add_argument('--domain-id', help="Domain ID", required=True) + parser.add_argument('id', help="Domain ID", required=True) return parser def execute(self, parsed_args): - return self.client.domains.get(parsed_args.domain_id) + return self.client.domains.get(parsed_args.id) class CreateDomainCommand(base.CreateCommand): @@ -47,16 +47,15 @@ class CreateDomainCommand(base.CreateCommand): def get_parser(self, prog_name): parser = super(CreateDomainCommand, self).get_parser(prog_name) - parser.add_argument('--domain-name', help="Domain Name", required=True) - parser.add_argument('--domain-email', help="Domain Email", - required=True) + parser.add_argument('--name', help="Domain Name", required=True) + parser.add_argument('--email', help="Domain Email", required=True) return parser def execute(self, parsed_args): domain = Domain( - name=parsed_args.domain_name, - email=parsed_args.domain_email + name=parsed_args.name, + email=parsed_args.email, ) return self.client.domains.create(domain) @@ -68,18 +67,22 @@ class UpdateDomainCommand(base.UpdateCommand): def get_parser(self, prog_name): parser = super(UpdateDomainCommand, self).get_parser(prog_name) - parser.add_argument('--domain-id', help="Domain ID", required=True) - parser.add_argument('--domain-name', help="Domain Name") - parser.add_argument('--domain-email', help="Domain Email") + parser.add_argument('id', help="Domain ID", required=True) + parser.add_argument('--name', help="Domain Name") + parser.add_argument('--email', help="Domain Email") return parser def execute(self, parsed_args): # TODO: API needs updating.. this get is silly - domain = self.client.domains.get(parsed_args.domain_id) + domain = self.client.domains.get(parsed_args.id) # TODO: How do we tell if an arg was supplied or intentionally set to # None? + domain.update({ + 'name': parsed_args.name, + 'email': parsed_args.email, + }) return self.client.domains.update(domain) @@ -90,9 +93,9 @@ class DeleteDomainCommand(base.DeleteCommand): def get_parser(self, prog_name): parser = super(DeleteDomainCommand, self).get_parser(prog_name) - parser.add_argument('--domain-id', help="Domain ID") + parser.add_argument('id', help="Domain ID", required=True) return parser def execute(self, parsed_args): - return self.client.domains.delete(parsed_args.domain_id) + return self.client.domains.delete(parsed_args.id) diff --git a/monikerclient/cli/records.py b/monikerclient/cli/records.py new file mode 100644 index 00000000..d3bc41e4 --- /dev/null +++ b/monikerclient/cli/records.py @@ -0,0 +1,121 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import logging +from monikerclient.cli import base +from monikerclient.v1.records import Record + +LOG = logging.getLogger(__name__) + + +class ListRecordsCommand(base.ListCommand): + """ List Records """ + + def get_parser(self, prog_name): + parser = super(ListRecordsCommand, self).get_parser(prog_name) + + parser.add_argument('domain_id', help="Domain ID") + + return parser + + def execute(self, parsed_args): + return self.client.records.list(parsed_args.domain_id) + + +class GetRecordCommand(base.GetCommand): + """ Get Record """ + + def get_parser(self, prog_name): + parser = super(GetRecordCommand, self).get_parser(prog_name) + + parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('id', help="Record ID") + + return parser + + def execute(self, parsed_args): + return self.client.records.get(parsed_args.domain_id, parsed_args.id) + + +class CreateRecordCommand(base.CreateCommand): + """ Create Record """ + + def get_parser(self, prog_name): + parser = super(CreateRecordCommand, self).get_parser(prog_name) + + parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('--name', help="Record Name", required=True) + parser.add_argument('--type', help="Record Type", required=True) + parser.add_argument('--data', help="Record Data", required=True) + parser.add_argument('--ttl', type=int, help="Record TTL") + + return parser + + def execute(self, parsed_args): + record = Record( + name=parsed_args.name, + type=parsed_args.type, + data=parsed_args.data, + ttl=parsed_args.ttl, + ) + + return self.client.records.create(parsed_args.domain_id, record) + + +class UpdateRecordCommand(base.UpdateCommand): + """ Update Record """ + + def get_parser(self, prog_name): + parser = super(UpdateRecordCommand, self).get_parser(prog_name) + + parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('id', help="Record ID") + parser.add_argument('--name', help="Record Name") + parser.add_argument('--type', help="Record Type") + parser.add_argument('--data', help="Record Data") + parser.add_argument('--ttl', type=int, help="Record TTL") + + return parser + + def execute(self, parsed_args): + # TODO: API needs updating.. this get is silly + record = self.client.records.get(parsed_args.domain_id, parsed_args.id) + + # TODO: How do we tell if an arg was supplied or intentionally set to + # None? + record.update({ + 'name': parsed_args.name, + 'type': parsed_args.type, + 'data': parsed_args.data, + 'ttl': parsed_args.ttl, + }) + + return self.client.records.update(parsed_args.domain_id, record) + + +class DeleteRecordCommand(base.DeleteCommand): + """ Delete Record """ + + def get_parser(self, prog_name): + parser = super(DeleteRecordCommand, self).get_parser(prog_name) + + parser.add_argument('domain_id', help="Domain ID") + parser.add_argument('id', help="Record ID") + + return parser + + def execute(self, parsed_args): + return self.client.records.delete(parsed_args.domain_id, + parsed_args.id) diff --git a/monikerclient/cli/servers.py b/monikerclient/cli/servers.py new file mode 100644 index 00000000..a399abe1 --- /dev/null +++ b/monikerclient/cli/servers.py @@ -0,0 +1,105 @@ +# Copyright 2012 Managed I.T. +# +# Author: Kiall Mac Innes <kiall@managedit.ie> +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +import logging +from monikerclient.cli import base +from monikerclient.v1.servers import Server + +LOG = logging.getLogger(__name__) + + +class ListServersCommand(base.ListCommand): + """ List Servers """ + + def execute(self, parsed_args): + return self.client.servers.list() + + +class GetServerCommand(base.GetCommand): + """ Get Server """ + + def get_parser(self, prog_name): + parser = super(GetServerCommand, self).get_parser(prog_name) + + parser.add_argument('id', help="Server ID", required=True) + + return parser + + def execute(self, parsed_args): + return self.client.servers.get(parsed_args.id) + + +class CreateServerCommand(base.CreateCommand): + """ Create Server """ + + def get_parser(self, prog_name): + parser = super(CreateServerCommand, self).get_parser(prog_name) + + parser.add_argument('--name', help="Server Name", required=True) + parser.add_argument('--ipv4', help="Server IPv4 Address") + parser.add_argument('--ipv6', help="Server IPv6 Address") + + return parser + + def execute(self, parsed_args): + server = Server( + name=parsed_args.name, + ipv4=parsed_args.ipv4, + ipv6=parsed_args.ipv6, + ) + + return self.client.servers.create(server) + + +class UpdateServerCommand(base.UpdateCommand): + """ Update Server """ + + def get_parser(self, prog_name): + parser = super(UpdateServerCommand, self).get_parser(prog_name) + + parser.add_argument('id', help="Server ID", required=True) + parser.add_argument('--name', help="Server Name") + parser.add_argument('--ipv4', help="Server IPv4 Address") + parser.add_argument('--ipv6', help="Server IPv6 Address") + + return parser + + def execute(self, parsed_args): + # TODO: API needs updating.. this get is silly + server = self.client.servers.get(parsed_args.id) + + # TODO: How do we tell if an arg was supplied or intentionally set to + # None? + server.update({ + 'name': parsed_args.name, + 'ipv4': parsed_args.ipv4, + 'ipv6': parsed_args.ipv6, + }) + + return self.client.servers.update(server) + + +class DeleteServerCommand(base.DeleteCommand): + """ Delete Server """ + + def get_parser(self, prog_name): + parser = super(DeleteServerCommand, self).get_parser(prog_name) + + parser.add_argument('id', help="Server ID", required=True) + + return parser + + def execute(self, parsed_args): + return self.client.servers.delete(parsed_args.id) diff --git a/monikerclient/resources/schemas/v1/domain.json b/monikerclient/resources/schemas/v1/domain.json index d41646f9..0eecc9b7 100644 --- a/monikerclient/resources/schemas/v1/domain.json +++ b/monikerclient/resources/schemas/v1/domain.json @@ -16,8 +16,8 @@ "name": { "type": "string", "description": "Domain name", + "format": "host-name", "maxLength": 255, - "pattern": "^.+[^\\.]$", "required": true }, "email": { @@ -43,10 +43,20 @@ "readonly": true }, "updated_at": { - "type": "string", + "type": ["string", "null"], "description": "Date and time of image registration", "format": "date-time", "readonly": true } - } + }, + "links": [{ + "rel": "self", + "href": "/domains/{id}" + }, { + "rel": "records", + "href": "/domains/{id}/records" + }, { + "rel": "collection", + "href": "/domains" + }] } diff --git a/monikerclient/resources/schemas/v1/record.json b/monikerclient/resources/schemas/v1/record.json index a6b3391e..80a16dc7 100644 --- a/monikerclient/resources/schemas/v1/record.json +++ b/monikerclient/resources/schemas/v1/record.json @@ -13,17 +13,24 @@ "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "readonly": true }, + "domain_id": { + "type": "string", + "description": "Domain Identifier", + "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", + "readonly": true + }, "name": { "type": "string", "description": "DNS Record Name", + "format": "host-name", "maxLength": 255, - "pattern": "^.+[^\\.]$", "required": true }, "type": { "type": "string", "description": "DNS Record Type", - "enum": ["A", "AAAA", "CNAME", "MX", "SRV", "TXT", "SPF", "NS"] + "enum": ["A", "AAAA", "CNAME", "MX", "SRV", "TXT", "SPF", "NS"], + "required": true }, "data": { "type": "string", @@ -31,8 +38,14 @@ "maxLength": 255, "required": true }, + "priority": { + "type": ["integer", "null"], + "description": "DNS Record Priority", + "min": 0, + "max": 99999 + }, "ttl": { - "type": "integer", + "type": ["integer", "null"], "description": "Time to live", "min": 60 }, @@ -43,10 +56,131 @@ "readonly": true }, "updated_at": { - "type": "string", + "type": ["string", "null"], "description": "Date and time of image registration", "format": "date-time", "readonly": true } - } + }, + "oneOf": [{ + "description": "An A Record", + "properties": { + "type": { + "type": "string", + "enum": ["A"] + }, + "data": { + "format": "ip-address", + "required": true + }, + "priority": { + "type": "null" + } + } + }, { + "description": "An AAAA Record", + "properties": { + "type": { + "type": "string", + "enum": ["AAAA"] + }, + "data": { + "format": "ipv6", + "required": true + }, + "priority": { + "type": "null" + } + } + }, { + "description": "A CNAME Record", + "properties": { + "type": { + "type": "string", + "enum": ["CNAME"] + }, + "data": { + "format": "host-name", + "required": true + }, + "priority": { + "type": "null" + } + } + }, { + "description": "A MX Record", + "properties": { + "type": { + "type": "string", + "enum": ["MX"] + }, + "data": { + "format": "host-name", + "required": true + }, + "priority": { + "type": "integer", + "required": true + } + } + }, { + "description": "A SRV Record", + "properties": { + "type": { + "type": "string", + "enum": ["SRV"] + }, + "priority": { + "type": "integer", + "required": true + } + } + }, { + "description": "A TXT Record", + "properties": { + "type": { + "type": "string", + "enum": ["TXT"] + }, + "priority": { + "type": "null" + } + } + }, { + "description": "A SPF Record", + "properties": { + "type": { + "type": "string", + "enum": ["SPF"] + }, + "priority": { + "type": "null" + } + } + }, { + "description": "A NS Record", + "properties": { + "type": { + "type": "string", + "enum": ["NS"] + }, + "data": { + "format": "host-name", + "required": true + }, + "priority": { + "type": "null" + } + } + }], + "links": [{ + "rel": "self", + "href": "/domains/{domain_id}/records/{id}" + }, { + "rel": "domain", + "href": "/domains/{domain_id}" + }, { + "rel": "collection", + "href": "/domains/{domain_id}/records" + }] } diff --git a/monikerclient/resources/schemas/v1/server.json b/monikerclient/resources/schemas/v1/server.json index 92470fb9..2af9c781 100644 --- a/monikerclient/resources/schemas/v1/server.json +++ b/monikerclient/resources/schemas/v1/server.json @@ -16,15 +16,14 @@ "name": { "type": "string", "description": "Server DNS name", + "format": "host-name", "maxLength": 255, - "pattern": "^.+[^\\.]$", "required": true }, "ipv4": { "type": "string", "description": "IPv4 address of server", - "format": "ip-address", - "required": true + "format": "ip-address" }, "ipv6": { "type": "string", @@ -38,10 +37,32 @@ "readonly": true }, "updated_at": { - "type": "string", + "type": ["string", "null"], "description": "Date and time of last server update", "format": "date-time", "readonly": true } - } + }, + "anyOf": [{ + "description": "IPv4", + "properties": { + "ipv4": { + "required": true + } + } + }, { + "description": "IPv6", + "properties": { + "ipv6": { + "required": true + } + } + }], + "links": [{ + "rel": "self", + "href": "/servers/{id}" + }, { + "rel": "collection", + "href": "/servers" + }] } diff --git a/monikerclient/v1/records.py b/monikerclient/v1/records.py index 57a4cbba..e42ce898 100644 --- a/monikerclient/v1/records.py +++ b/monikerclient/v1/records.py @@ -17,63 +17,97 @@ import json from monikerclient import warlock from monikerclient import utils from monikerclient.v1.base import Controller +from monikerclient.v1.domains import Domain Record = warlock.model_factory(utils.load_schema('v1', 'record')) class RecordsController(Controller): - def list(self): + def list(self, domain): """ Retrieve a list of records + :param domain: :class:`Domain` or Domain Identifier :returns: A list of :class:`Record`s """ - response = self.client.get('/records') + domain_id = domain.id if isinstance(domain, Domain) else domain + + response = self.client.get('/domains/%(domain_id)s/records' % { + 'domain_id': domain_id + }) return [Record(i) for i in response.json['records']] - def get(self, record_id): + def get(self, domain, record_id): """ Retrieve a record + :param domain: :class:`Domain` or Domain Identifier :param record_id: Record Identifier :returns: :class:`Record` """ - response = self.client.get('/records/%s' % record_id) + domain_id = domain.id if isinstance(domain, Domain) else domain + + uri = '/domains/%(domain_id)s/records/%(record_id)s' % { + 'domain_id': domain_id, + 'record_id': record_id + } + + response = self.client.get(uri) return Record(response.json) - def create(self, record): + def create(self, domain, record): """ Create a record + :param domain: :class:`Domain` or Domain Identifier :param record: A :class:`Record` to create :returns: :class:`Record` """ - response = self.client.post('/records', data=json.dumps(record)) + domain_id = domain.id if isinstance(domain, Domain) else domain - return record.update(response.json) + uri = '/domains/%(domain_id)s/records' % { + 'domain_id': domain_id + } - def update(self, record): + response = self.client.post(uri, data=json.dumps(record)) + + return Record(response.json) + + def update(self, domain, record): """ Update a record + :param domain: :class:`Domain` or Domain Identifier :param record: A :class:`Record` to update :returns: :class:`Record` """ - response = self.client.put('/records/%s' % record.id, - data=json.dumps(record)) + domain_id = domain.id if isinstance(domain, Domain) else domain + + uri = '/domains/%(domain_id)s/records/%(record_id)s' % { + 'domain_id': domain_id, + 'record_id': record.id + } + + response = self.client.put(uri, data=json.dumps(record)) return record.update(response.json) - def delete(self, record): + def delete(self, domain, record): """ Delete a record + :param domain: :class:`Domain` or Domain Identifier :param record: A :class:`Record`, or Record Identifier to delete """ - if isinstance(record, Record): - self.client.delete('/records/%s' % record.id) - else: - self.client.delete('/records/%s' % record) + domain_id = domain.id if isinstance(domain, Domain) else domain + record_id = record.id if isinstance(record, Record) else record + + uri = '/domains/%(domain_id)s/records/%(record_id)s' % { + 'domain_id': domain_id, + 'record_id': record_id + } + + self.client.delete(uri) diff --git a/monikerclient/v1/servers.py b/monikerclient/v1/servers.py index 3c98b121..d5b69463 100644 --- a/monikerclient/v1/servers.py +++ b/monikerclient/v1/servers.py @@ -53,7 +53,7 @@ class ServersController(Controller): """ response = self.client.post('/servers', data=json.dumps(server)) - return server.update(response.json) + return Server(response.json) def update(self, server): """ diff --git a/monikerclient/warlock.py b/monikerclient/warlock.py index 7fa0616e..ad2bfadf 100644 --- a/monikerclient/warlock.py +++ b/monikerclient/warlock.py @@ -16,8 +16,11 @@ # Hopefully we can upstream the changes ASAP. # import copy +import logging import jsonschema +LOG = logging.getLogger(__name__) + class InvalidOperation(RuntimeError): pass @@ -38,8 +41,8 @@ def model_factory(schema): """Apply a JSON schema to an object""" try: jsonschema.validate(obj, schema) - except jsonschema.ValidationError: - raise ValidationError() + except jsonschema.ValidationError, e: + raise ValidationError(str(e)) class Model(dict): """Self-validating model for arbitrary objects""" @@ -51,8 +54,8 @@ def model_factory(schema): self.__dict__['validator'] = validator try: self.validator(d) - except ValidationError: - raise ValueError() + except ValidationError, e: + raise ValueError('Validation Error: %s' % str(e)) else: dict.__init__(self, d) diff --git a/setup.py b/setup.py index 1e76ba9d..608cba00 100755 --- a/setup.py +++ b/setup.py @@ -54,6 +54,18 @@ setup( domain-create = monikerclient.cli.domains:CreateDomainCommand domain-update = monikerclient.cli.domains:UpdateDomainCommand domain-delete = monikerclient.cli.domains:DeleteDomainCommand + + record-list = monikerclient.cli.records:ListRecordsCommand + record-get = monikerclient.cli.records:GetRecordCommand + record-create = monikerclient.cli.records:CreateRecordCommand + record-update = monikerclient.cli.records:UpdateRecordCommand + record-delete = monikerclient.cli.records:DeleteRecordCommand + + server-list = monikerclient.cli.servers:ListServersCommand + server-get = monikerclient.cli.servers:GetServerCommand + server-create = monikerclient.cli.servers:CreateServerCommand + server-update = monikerclient.cli.servers:UpdateServerCommand + server-delete = monikerclient.cli.servers:DeleteServerCommand """), classifiers=[ 'Development Status :: 3 - Alpha',