diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..a23c8bd4 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,4 @@ +cliff +jsonschema>=0.7 +requests +python-keystoneclient>=0.2.0 diff --git a/doc/source/conf.py b/doc/source/conf.py new file mode 100644 index 00000000..5004b8bf --- /dev/null +++ b/doc/source/conf.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# +# monikerclient documentation build configuration file, created by +# sphinx-quickstart on Wed Oct 31 18:58:17 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'monikerclient' +copyright = u'2012, Managed I.T.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +from monikerclient.version import version_info as monikerclient_version +version = monikerclient_version.canonical_version_string() +# The full version, including alpha/beta/rc tags. +release = monikerclient_version.version_string_with_vcs() + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# "<project> v<release> documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a <link> tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'monikerclientdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +latex_elements = { +# The paper size ('letterpaper' or 'a4paper'). +#'papersize': 'letterpaper', + +# The font size ('10pt', '11pt' or '12pt'). +#'pointsize': '10pt', + +# Additional stuff for the LaTeX preamble. +#'preamble': '', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'monikerclient.tex', u'Moniker Client Documentation', + u'Managed I.T.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'monikerclient', u'Moniker Client Documentation', + [u'Managed I.T.'], 1) +] + +# If true, show URL addresses after external links. +#man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------------ + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ('index', 'monikerclient', u'Moniker Client Documentation', + u'Managed I.T.', 'monikerclient', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +#texinfo_appendices = [] + +# If false, no module index is generated. +#texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +#texinfo_show_urls = 'footnote' diff --git a/doc/source/index.rst b/doc/source/index.rst new file mode 100644 index 00000000..1d3ef9e9 --- /dev/null +++ b/doc/source/index.rst @@ -0,0 +1,21 @@ +.. moniker documentation master file, created by + sphinx-quickstart on Wed Oct 31 18:58:17 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to monikerclients's documentation! +=================================== + +Contents: + +.. toctree:: + :maxdepth: 2 + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/monikerclient/auth.py b/monikerclient/auth.py new file mode 100644 index 00000000..36c7e3c5 --- /dev/null +++ b/monikerclient/auth.py @@ -0,0 +1,94 @@ +# 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. +from urlparse import urlparse +from requests.auth import AuthBase + +from keystoneclient.v2_0.client import Client + + +class KeystoneAuth(AuthBase): + def __init__(self, auth_url, username=None, password=None, tenant_id=None, + tenant_name=None, token=None, service_type=None, + endpoint_type=None): + self.auth_url = str(auth_url).rstrip('/') + self.username = username + self.password = password + self.tenant_id = tenant_id + self.tenant_name = tenant_name + self.token = token + + if (not username and not password) and not token: + raise ValueError('A username and password, or token is required') + + if not service_type or not endpoint_type: + raise ValueError("Need service_type and/or endpoint_type") + + self.service_type = service_type + self.endpoint_type = endpoint_type + + self.refresh_auth() + + def __call__(self, request): + if not self.token: + self.refresh_auth() + + request.headers['X-Auth-Token'] = self.token + + return request + + def get_ksclient(self): + insecure = urlparse(self.auth_url).scheme != 'https' + + return Client(username=self.username, + password=self.password, + tenant_id=self.tenant_id, + tenant_name=self.tenant_name, + auth_url=self.auth_url, + insecure=insecure) + + def get_endpoints(self, service_type=None, endpoint_type=None): + return self.service_catalog.get_endpoints( + service_type=service_type, + endpoint_type=endpoint_type) + + def get_url(self, service_type=None, endpoint_type=None): + service_type = service_type or self.service_type + endpoint_type = endpoint_type or self.endpoint_type + endpoints = self.get_endpoints(service_type, endpoint_type) + + return endpoints[service_type][0][endpoint_type].rstrip('/') + + def refresh_auth(self): + ks = self.get_ksclient() + self.token = ks.auth_token + self.service_catalog = ks.service_catalog + + def args_hook(self, args): + url = urlparse(args['url']) + + if str(url.scheme) == '': + if not self.token: + self.refresh_token() + + endpoints = self.get_endpoints() + + if url.netloc in endpoints.keys(): + + args['url'] = '%s/%s?%s' % ( + self.get_url(), + url.path.lstrip('/'), + url.query + ) diff --git a/monikerclient/cli/base.py b/monikerclient/cli/base.py new file mode 100644 index 00000000..a5d7993c --- /dev/null +++ b/monikerclient/cli/base.py @@ -0,0 +1,94 @@ +# 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 abc +from cliff.command import Command as CliffCommand +from cliff.lister import Lister +from cliff.show import ShowOne +from monikerclient.v1 import Client + + +class Command(CliffCommand): + __metaclass__ = abc.ABCMeta + + def run(self, parsed_args): + client_args = { + 'endpoint': self.app.options.os_endpoint, + 'auth_url': self.app.options.os_auth_url, + 'username': self.app.options.os_username, + 'password': self.app.options.os_password, + 'tenant_id': self.app.options.os_tenant_id, + 'tenant_name': self.app.options.os_tenant_name, + 'token': self.app.options.os_token, + 'region_name': self.app.options.os_region_name, + } + + self.client = Client(**client_args) + + return super(Command, self).run(parsed_args) + + @abc.abstractmethod + def execute(self, parsed_args): + """ + Execute something, this is since we overload self.take_action() + in order to format the data + + This method __NEEDS__ to be overloaded! + + :param parsed_args: The parsed args that are given by take_action() + """ + + def post_execute(self, data): + """ + Format the results locally if needed, by default we just return data + + :param data: Whatever is returned by self.execute() + """ + return data + + def take_action(self, parsed_args): + # TODO: Common Exception Handling Here + results = self.execute(parsed_args) + return self.post_execute(results) + + +class ListCommand(Command, Lister): + def post_execute(self, results): + if len(results) > 0: + column_names = results[0].keys() + data = [r.values() for r in results] + + return column_names, data + else: + return [], () + + +class GetCommand(Command, ShowOne): + def post_execute(self, results): + return results.keys(), results.values() + + +class CreateCommand(Command, ShowOne): + def post_execute(self, results): + return results.keys(), results.values() + + +class UpdateCommand(Command, ShowOne): + def post_execute(self, results): + return results.keys(), results.values() + + +class DeleteCommand(Command): + pass diff --git a/monikerclient/cli/domains.py b/monikerclient/cli/domains.py new file mode 100644 index 00000000..83df27c6 --- /dev/null +++ b/monikerclient/cli/domains.py @@ -0,0 +1,98 @@ +# 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.domains import Domain + +LOG = logging.getLogger(__name__) + + +class ListDomainsCommand(base.ListCommand): + """ List Domains """ + + def execute(self, parsed_args): + return self.client.domains.list() + + +class GetDomainCommand(base.GetCommand): + """ Get Domain """ + + def get_parser(self, prog_name): + parser = super(GetDomainCommand, self).get_parser(prog_name) + + parser.add_argument('--domain-id', help="Domain ID", required=True) + + return parser + + def execute(self, parsed_args): + return self.client.domains.get(parsed_args.domain_id) + + +class CreateDomainCommand(base.CreateCommand): + """ Create Domain """ + + 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) + + return parser + + def execute(self, parsed_args): + domain = Domain( + name=parsed_args.domain_name, + email=parsed_args.domain_email + ) + + return self.client.domains.create(domain) + + +class UpdateDomainCommand(base.UpdateCommand): + """ Update Domain """ + + 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") + + return parser + + def execute(self, parsed_args): + # TODO: API needs updating.. this get is silly + domain = self.client.domains.get(parsed_args.domain_id) + + # TODO: How do we tell if an arg was supplied or intentionally set to + # None? + + return self.client.domains.update(domain) + + +class DeleteDomainCommand(base.DeleteCommand): + """ Delete Domain """ + + def get_parser(self, prog_name): + parser = super(DeleteDomainCommand, self).get_parser(prog_name) + + parser.add_argument('--domain-id', help="Domain ID") + + return parser + + def execute(self, parsed_args): + return self.client.domains.delete(parsed_args.domain_id) diff --git a/monikerclient/exceptions.py b/monikerclient/exceptions.py index 09ebc14d..bf0ea252 100644 --- a/monikerclient/exceptions.py +++ b/monikerclient/exceptions.py @@ -21,3 +21,23 @@ class Base(Exception): class ResourceNotFound(Base): pass + + +class RemoteError(Base): + pass + + +class Unknown(RemoteError): + pass + + +class Forbidden(RemoteError): + pass + + +class Conflict(RemoteError): + pass + + +class NotFound(RemoteError): + pass diff --git a/monikerclient/shell.py b/monikerclient/shell.py index b8710cb9..c5ddaadf 100644 --- a/monikerclient/shell.py +++ b/monikerclient/shell.py @@ -13,7 +13,7 @@ # 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 os from cliff.app import App from cliff.commandmanager import CommandManager @@ -25,3 +25,41 @@ class MonikerShell(App): version='0.1', command_manager=CommandManager('moniker.cli'), ) + + def build_option_parser(self, description, version, argparse_kwargs=None): + parser = super(MonikerShell, self).build_option_parser( + description, version, argparse_kwargs) + + parser.add_argument('--os-endpoint', + default=os.environ.get('OS_SERVICE_ENDPOINT'), + help="Defaults to env[OS_SERVICE_ENDPOINT]") + + parser.add_argument('--os-auth-url', + default=os.environ.get('OS_AUTH_URL'), + help="Defaults to env[OS_AUTH_URL]") + + parser.add_argument('--os-username', + default=os.environ.get('OS_USERNAME'), + help="Defaults to env[OS_USERNAME]") + + parser.add_argument('--os-password', + default=os.environ.get('OS_PASSWORD'), + help="Defaults to env[OS_PASSWORD]") + + parser.add_argument('--os-tenant-id', + default=os.environ.get('OS_TENANT_ID'), + help="Defaults to env[OS_TENANT_ID]") + + parser.add_argument('--os-tenant-name', + default=os.environ.get('OS_TENANT_NAME'), + help="Defaults to env[OS_TENANT_NAME]") + + parser.add_argument('--os-token', + default=os.environ.get('OS_SERVICE_TOKEN'), + help="Defaults to env[OS_SERVICE_TOKEN]") + + parser.add_argument('--os-region-name', + default=os.environ.get('OS_REGION_NAME'), + help="Defaults to env[OS_REGION_NAME]") + + return parser diff --git a/monikerclient/tests/__init__.py b/monikerclient/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/monikerclient/v1/__init__.py b/monikerclient/v1/__init__.py index e69de29b..70a7617e 100644 --- a/monikerclient/v1/__init__.py +++ b/monikerclient/v1/__init__.py @@ -0,0 +1,98 @@ +# 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 requests +from urlparse import urlparse +from monikerclient import exceptions +from monikerclient.auth import KeystoneAuth +from monikerclient.v1 import domains +from monikerclient.v1 import records +from monikerclient.v1 import servers + + +class Client(object): + """ Client for the Moniker v1 API """ + + def __init__(self, endpoint=None, auth_url=None, username=None, + password=None, tenant_id=None, tenant_name=None, token=None, + region_name=None, endpoint_type='publicURL'): + """ + :param endpoint: Endpoint URL + :param auth_url: Keystone auth_url + :param username: The username to auth with + :param password: The password to auth with + :param tenant_id: The tenant ID + :param tenant_name: The tenant name + :param token: A token instead of username / password + :param region_name: The region name + :param endpoint_type: The endpoint type (publicURL for example) + """ + if auth_url: + auth = KeystoneAuth(auth_url, username, password, tenant_id, + tenant_name, token, 'dns', endpoint_type) + endpoint = auth.get_url() + elif endpoint: + auth = None + else: + raise ValueError('Either an auth_url or endpoint must be supplied') + + headers = {'Content-Type': 'application/json'} + + def _ensure_url_hook(args): + url_ = urlparse(args['url']) + if not url_.scheme: + args['url'] = endpoint + url_.path + + hooks = {'args': _ensure_url_hook} + + self.requests = requests.session( + auth=auth, + headers=headers, + hooks=hooks) + + self.domains = domains.DomainsController(client=self) + self.records = records.RecordsController(client=self) + self.servers = servers.ServersController(client=self) + + def wrap_api_call(self, func, *args, **kw): + """ + Wrap a self.<rest function> with exception handling + + :param func: The function to wrap + """ + response = func(*args, **kw) + + if response.status_code in (401, 403): + raise exceptions.Forbidden() + elif response.status_code == 404: + raise exceptions.NotFound() + elif response.status_code == 409: + raise exceptions.Conflict() + elif response.status_code == 500: + raise exceptions.Unknown() + else: + return response + + def get(self, path, **kw): + return self.wrap_api_call(self.requests.get, path, **kw) + + def post(self, path, **kw): + return self.wrap_api_call(self.requests.post, path, **kw) + + def put(self, path, **kw): + return self.wrap_api_call(self.requests.put, path, **kw) + + def delete(self, path, **kw): + return self.wrap_api_call(self.requests.delete, path, **kw) diff --git a/monikerclient/v1/base.py b/monikerclient/v1/base.py new file mode 100644 index 00000000..09d624bd --- /dev/null +++ b/monikerclient/v1/base.py @@ -0,0 +1,53 @@ +# 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 abc + + +class Controller(object): + __metaclass__ = abc.ABCMeta + + def __init__(self, client): + self.client = client + + def list(self, *args, **kw): + """ + List something + """ + raise NotImplementedError + + def get(self, *args, **kw): + """ + Get something + """ + raise NotImplementedError + + def create(self, *args, **kw): + """ + Create something + """ + raise NotImplementedError + + def update(self, *args, **kw): + """ + Update something + """ + raise NotImplementedError + + def delete(self, *args, **kw): + """ + Delete something + """ + raise NotImplementedError diff --git a/monikerclient/v1/client.py b/monikerclient/v1/client.py deleted file mode 100644 index e40f4a97..00000000 --- a/monikerclient/v1/client.py +++ /dev/null @@ -1,27 +0,0 @@ -# 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. -from monikerclient.v1 import domains -from monikerclient.v1 import records -from monikerclient.v1 import servers - - -class Client(object): - """ Client for the Moniker v1 API """ - - def __init__(self): - self.domains = domains.Controller() - self.records = records.Controller() - self.servers = servers.Controller() diff --git a/monikerclient/v1/domains.py b/monikerclient/v1/domains.py index c88de493..b5551056 100644 --- a/monikerclient/v1/domains.py +++ b/monikerclient/v1/domains.py @@ -13,20 +13,25 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json from monikerclient import warlock from monikerclient import utils +from monikerclient.v1.base import Controller Domain = warlock.model_factory(utils.load_schema('v1', 'domain')) -class Controller(object): +class DomainsController(Controller): def list(self): """ Retrieve a list of domains :returns: A list of :class:`Domain`s """ + response = self.client.get('/domains') + + return [Domain(i) for i in response.json['domains']] def get(self, domain_id): """ @@ -35,6 +40,9 @@ class Controller(object): :param domain_id: Domain Identifier :returns: :class:`Domain` """ + response = self.client.get('/domains/%s' % domain_id) + + return Domain(response.json) def create(self, domain): """ @@ -43,6 +51,9 @@ class Controller(object): :param domain: A :class:`Domain` to create :returns: :class:`Domain` """ + response = self.client.post('/domains', data=json.dumps(domain)) + + return Domain(response.json) def update(self, domain): """ @@ -51,6 +62,10 @@ class Controller(object): :param domain: A :class:`Domain` to update :returns: :class:`Domain` """ + response = self.client.put('/domains/%s' % domain.id, + data=json.dumps(domain.changes)) + + return Domain(response.json) def delete(self, domain): """ @@ -58,3 +73,7 @@ class Controller(object): :param domain: A :class:`Domain`, or Domain Identifier to delete """ + if isinstance(domain, Domain): + self.client.delete('/domains/%s' % domain.id) + else: + self.client.delete('/domains/%s' % domain) diff --git a/monikerclient/v1/records.py b/monikerclient/v1/records.py index d7b56a11..57a4cbba 100644 --- a/monikerclient/v1/records.py +++ b/monikerclient/v1/records.py @@ -13,20 +13,25 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json from monikerclient import warlock from monikerclient import utils +from monikerclient.v1.base import Controller Record = warlock.model_factory(utils.load_schema('v1', 'record')) -class Controller(object): +class RecordsController(Controller): def list(self): """ Retrieve a list of records :returns: A list of :class:`Record`s """ + response = self.client.get('/records') + + return [Record(i) for i in response.json['records']] def get(self, record_id): """ @@ -35,6 +40,9 @@ class Controller(object): :param record_id: Record Identifier :returns: :class:`Record` """ + response = self.client.get('/records/%s' % record_id) + + return Record(response.json) def create(self, record): """ @@ -43,6 +51,9 @@ class Controller(object): :param record: A :class:`Record` to create :returns: :class:`Record` """ + response = self.client.post('/records', data=json.dumps(record)) + + return record.update(response.json) def update(self, record): """ @@ -51,6 +62,10 @@ class Controller(object): :param record: A :class:`Record` to update :returns: :class:`Record` """ + response = self.client.put('/records/%s' % record.id, + data=json.dumps(record)) + + return record.update(response.json) def delete(self, record): """ @@ -58,3 +73,7 @@ class Controller(object): :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) diff --git a/monikerclient/v1/servers.py b/monikerclient/v1/servers.py index ec2923e5..3c98b121 100644 --- a/monikerclient/v1/servers.py +++ b/monikerclient/v1/servers.py @@ -13,20 +13,25 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. +import json from monikerclient import warlock from monikerclient import utils +from monikerclient.v1.base import Controller Server = warlock.model_factory(utils.load_schema('v1', 'server')) -class Controller(object): +class ServersController(Controller): def list(self): """ Retrieve a list of servers :returns: A list of :class:`Server`s """ + response = self.client.get('/servers') + + return [Server(i) for i in response.json['servers']] def get(self, server_id): """ @@ -35,6 +40,9 @@ class Controller(object): :param server_id: Server Identifier :returns: :class:`Server` """ + response = self.client.get('/servers/%s' % server_id) + + return Server(response.json) def create(self, server): """ @@ -43,6 +51,9 @@ class Controller(object): :param server: A :class:`Server` to create :returns: :class:`Server` """ + response = self.client.post('/servers', data=json.dumps(server)) + + return server.update(response.json) def update(self, server): """ @@ -51,6 +62,10 @@ class Controller(object): :param server: A :class:`Server` to update :returns: :class:`Server` """ + response = self.client.put('/servers/%s' % server.id, + data=json.dumps(server)) + + return server.update(response.json) def delete(self, server): """ @@ -58,3 +73,7 @@ class Controller(object): :param server: A :class:`Server`, or Server Identifier to delete """ + if isinstance(server, Server): + self.client.delete('/servers/%s' % server.id) + else: + self.client.delete('/servers/%s' % server) diff --git a/monikerclient/warlock.py b/monikerclient/warlock.py index e6db38ee..7fa0616e 100644 --- a/monikerclient/warlock.py +++ b/monikerclient/warlock.py @@ -113,6 +113,9 @@ def model_factory(schema): def itervalues(self): return copy.deepcopy(dict(self)).itervalues() + def keys(self): + return copy.deepcopy(dict(self)).keys() + def values(self): return copy.deepcopy(dict(self)).values() diff --git a/setup.cfg b/setup.cfg index f17eadfa..f0188f04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -6,3 +6,11 @@ cover-inclusive=true verbosity=2 detailed-errors=1 where=monikerclient/tests + +[build_sphinx] +source-dir = doc/source +build-dir = doc/build +all_files = 1 + +[upload_docs] +upload-dir = doc/build/html diff --git a/setup.py b/setup.py index bd6e49d4..1e76ba9d 100755 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ # 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 textwrap from setuptools import setup, find_packages from monikerclient.openstack.common import setup as common_setup from monikerclient.version import version_info as version @@ -46,6 +47,14 @@ setup( 'bin/moniker', ], cmdclass=common_setup.get_cmdclass(), + entry_points=textwrap.dedent(""" + [moniker.cli] + domain-list = monikerclient.cli.domains:ListDomainsCommand + domain-get = monikerclient.cli.domains:GetDomainCommand + domain-create = monikerclient.cli.domains:CreateDomainCommand + domain-update = monikerclient.cli.domains:UpdateDomainCommand + domain-delete = monikerclient.cli.domains:DeleteDomainCommand + """), classifiers=[ 'Development Status :: 3 - Alpha', 'Topic :: Internet :: Name Service (DNS)', diff --git a/tools/pip-requires b/tools/pip-requires index 877b6394..a23c8bd4 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -1,2 +1,4 @@ cliff jsonschema>=0.7 +requests +python-keystoneclient>=0.2.0 diff --git a/tools/test-requires b/tools/test-requires index 4828b5d9..daf9e637 100644 --- a/tools/test-requires +++ b/tools/test-requires @@ -1,3 +1,4 @@ nose mox openstack.nose_plugin +sphinx