diff options
Diffstat (limited to 'ironic_python_agent/api/controllers')
| -rw-r--r-- | ironic_python_agent/api/controllers/__init__.py | 15 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/root.py | 96 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/v1/__init__.py | 118 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/v1/base.py | 73 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/v1/command.py | 89 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/v1/link.py | 43 | ||||
| -rw-r--r-- | ironic_python_agent/api/controllers/v1/status.py | 44 |
7 files changed, 478 insertions, 0 deletions
diff --git a/ironic_python_agent/api/controllers/__init__.py b/ironic_python_agent/api/controllers/__init__.py new file mode 100644 index 00000000..2a30de06 --- /dev/null +++ b/ironic_python_agent/api/controllers/__init__.py @@ -0,0 +1,15 @@ +""" +Copyright 2014 Rackspace, Inc. + +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. +""" diff --git a/ironic_python_agent/api/controllers/root.py b/ironic_python_agent/api/controllers/root.py new file mode 100644 index 00000000..4552e731 --- /dev/null +++ b/ironic_python_agent/api/controllers/root.py @@ -0,0 +1,96 @@ +# Copyright 2014 Rackspace, Inc. +# +# 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 pecan +from pecan import rest + +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from teeth_agent.api.controllers import v1 +from teeth_agent.api.controllers.v1 import base +from teeth_agent.api.controllers.v1 import link + + +class Version(base.APIBase): + """An API version representation.""" + + id = wtypes.text + "The ID of the version, also acts as the release number" + + links = [link.Link] + "A Link that point to a specific version of the API" + + @classmethod + def convert(self, id): + version = Version() + version.id = id + version.links = [link.Link.make_link('self', pecan.request.host_url, + id, '', bookmark=True)] + return version + + +class Root(base.APIBase): + + name = wtypes.text + "The name of the API" + + description = wtypes.text + "Some information about this API" + + versions = [Version] + "Links to all the versions available in this API" + + default_version = Version + "A link to the default version of the API" + + @classmethod + def convert(self): + root = Root() + root.name = 'OpenStack Ironic Python Agent API' + root.description = ('Ironic Python Agent is a provisioning agent for ' + 'OpenStack Ironic') + root.versions = [Version.convert('v1')] + root.default_version = Version.convert('v1') + return root + + +class RootController(rest.RestController): + + _versions = ['v1'] + "All supported API versions" + + _default_version = 'v1' + "The default API version" + + v1 = v1.Controller() + + @wsme_pecan.wsexpose(Root) + def get(self): + # NOTE: The reason why convert() it's being called for every + # request is because we need to get the host url from + # the request object to make the links. + return Root.convert() + + @pecan.expose() + def _route(self, args): + """Overrides the default routing behavior. + + It redirects the request to the default version of the ironic API + if the version number is not specified in the url. + """ + + if args[0] and args[0] not in self._versions: + args = [self._default_version] + args + return super(RootController, self)._route(args) diff --git a/ironic_python_agent/api/controllers/v1/__init__.py b/ironic_python_agent/api/controllers/v1/__init__.py new file mode 100644 index 00000000..a2bc16c7 --- /dev/null +++ b/ironic_python_agent/api/controllers/v1/__init__.py @@ -0,0 +1,118 @@ +# All Rights Reserved. +# +# 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. + +""" +Version 1 of the Ironic Python Agent API +""" + +import pecan +from pecan import rest + +from wsme import types as wtypes +import wsmeext.pecan as wsme_pecan + +from teeth_agent.api.controllers.v1 import base +from teeth_agent.api.controllers.v1 import command +from teeth_agent.api.controllers.v1 import link +from teeth_agent.api.controllers.v1 import status + + +class MediaType(base.APIBase): + """A media type representation.""" + + base = wtypes.text + type = wtypes.text + + def __init__(self, base, type): + self.base = base + self.type = type + + +class V1(base.APIBase): + """The representation of the version 1 of the API.""" + + id = wtypes.text + "The ID of the version, also acts as the release number" + + media_types = [MediaType] + "An array of supported media types for this version" + + links = [link.Link] + "Links that point to a specific URL for this version and documentation" + + commands = [link.Link] + "Links to the command resource" + + status = [link.Link] + "Links to the status resource" + + @classmethod + def convert(self): + v1 = V1() + v1.id = "v1" + v1.links = [ + link.Link.make_link('self', + pecan.request.host_url, + 'v1', + '', + bookmark=True), + link.Link.make_link('describedby', + 'https://github.com', + 'rackerlabs', + 'teeth-agent', + bookmark=True, + type='text/html') + ] + v1.commands = [ + link.Link.make_link('self', + pecan.request.host_url, + 'commands', + ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'commands', + '', + bookmark=True) + ] + v1.status = [ + link.Link.make_link('self', + pecan.request.host_url, + 'status', + ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'status', + '', + bookmark=True) + ] + v1.media_types = [MediaType('application/json', + ('application/vnd.openstack.' + 'ironic-python-agent.v1+json'))] + return v1 + + +class Controller(rest.RestController): + """Version 1 API controller root.""" + + commands = command.CommandController() + status = status.StatusController() + + @wsme_pecan.wsexpose(V1) + def get(self): + # NOTE: The reason why convert() it's being called for every + # request is because we need to get the host url from + # the request object to make the links. + return V1.convert() + +__all__ = (Controller) diff --git a/ironic_python_agent/api/controllers/v1/base.py b/ironic_python_agent/api/controllers/v1/base.py new file mode 100644 index 00000000..20af8964 --- /dev/null +++ b/ironic_python_agent/api/controllers/v1/base.py @@ -0,0 +1,73 @@ +# All Rights Reserved. +# +# 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 six +from wsme import types as wtypes + + +class ExceptionType(wtypes.UserType): + basetype = wtypes.DictType + name = 'exception' + + def validate(self, value): + if not isinstance(value, BaseException): + raise ValueError('Value is not an exception') + return value + + def tobasetype(self, value): + """Turn a RESTError into a dict.""" + return { + 'type': value.__class__.__name__, + 'code': value.status_code, + 'message': value.message, + 'details': value.details, + } + + frombasetype = tobasetype + + +exception_type = ExceptionType() + + +class MultiType(wtypes.UserType): + """A complex type that represents one or more types. + + Used for validating that a value is an instance of one of the types. + + :param *types: Variable-length list of types. + + """ + def __init__(self, *types): + self.types = types + + def __str__(self): + return ' | '.join(map(str, self.types)) + + def validate(self, value): + for t in self.types: + if t is wtypes.text and isinstance(value, wtypes.bytes): + value = value.decode() + if isinstance(value, t): + return value + else: + raise ValueError( + "Wrong type. Expected '{type}', got '{value}'".format( + type=self.types, value=type(value))) + + +json_type = MultiType(list, dict, six.integer_types, wtypes.text) + + +class APIBase(wtypes.Base): + pass diff --git a/ironic_python_agent/api/controllers/v1/command.py b/ironic_python_agent/api/controllers/v1/command.py new file mode 100644 index 00000000..81100000 --- /dev/null +++ b/ironic_python_agent/api/controllers/v1/command.py @@ -0,0 +1,89 @@ +# Copyright 2014 Rackspace, Inc. +# All Rights Reserved. +# +# 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 pecan +from pecan import rest +from wsme import types +from wsmeext import pecan as wsme_pecan + +from teeth_agent.api.controllers.v1 import base + + +class CommandResult(base.APIBase): + id = types.text + command_name = types.text + command_params = types.DictType(types.text, base.json_type) + command_status = types.text + command_error = base.exception_type + command_result = types.DictType(types.text, base.json_type) + + @classmethod + def from_result(cls, result): + instance = cls() + for field in ('id', 'command_name', 'command_params', 'command_status', + 'command_error', 'command_result'): + setattr(instance, field, getattr(result, field)) + return instance + + +class CommandResultList(base.APIBase): + commands = [CommandResult] + + @classmethod + def from_results(cls, results): + instance = cls() + instance.commands = [CommandResult.from_result(result) + for result in results] + return instance + + +class Command(base.APIBase): + """A command representation.""" + name = types.wsattr(types.text, mandatory=True) + params = types.wsattr(base.MultiType(dict), mandatory=True) + + +class CommandController(rest.RestController): + """Controller for issuing commands and polling for command status.""" + + @wsme_pecan.wsexpose(CommandResultList) + def get_all(self): + agent = pecan.request.agent + results = agent.list_command_results() + return CommandResultList.from_results(results) + + @wsme_pecan.wsexpose(CommandResult, types.text, types.text) + def get_one(self, result_id, wait=False): + agent = pecan.request.agent + result = agent.get_command_result(result_id) + + if wait and wait.lower() == 'true': + result.join() + + return CommandResult.from_result(result) + + @wsme_pecan.wsexpose(CommandResult, body=Command) + def post(self, wait=False, command=None): + # the POST body is always the last arg, + # so command must be a kwarg here + if command is None: + command = Command() + agent = pecan.request.agent + result = agent.execute_command(command.name, **command.params) + + if wait and wait.lower() == 'true': + result.join() + + return result diff --git a/ironic_python_agent/api/controllers/v1/link.py b/ironic_python_agent/api/controllers/v1/link.py new file mode 100644 index 00000000..987eb386 --- /dev/null +++ b/ironic_python_agent/api/controllers/v1/link.py @@ -0,0 +1,43 @@ +# Copyright 2014 Rackspace, Inc. +# All Rights Reserved. +# +# 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 wsme import types as wtypes + +from teeth_agent.api.controllers.v1 import base + + +class Link(base.APIBase): + """A link representation.""" + + href = wtypes.text + "The url of a link." + + rel = wtypes.text + "The name of a link." + + type = wtypes.text + "Indicates the type of document/link." + + @classmethod + def make_link(cls, rel_name, url, resource, resource_args, + bookmark=False, type=wtypes.Unset): + template = '%s/%s' if bookmark else '%s/v1/%s' + # FIXME(lucasagomes): I'm getting a 404 when doing a GET on + # a nested resource that the URL ends with a '/'. + # https://groups.google.com/forum/#!topic/pecan-dev/QfSeviLg5qs + template += '%s' if resource_args.startswith('?') else '/%s' + + return Link(href=(template) % (url, resource, resource_args), + rel=rel_name, type=type) diff --git a/ironic_python_agent/api/controllers/v1/status.py b/ironic_python_agent/api/controllers/v1/status.py new file mode 100644 index 00000000..c631ee59 --- /dev/null +++ b/ironic_python_agent/api/controllers/v1/status.py @@ -0,0 +1,44 @@ +# Copyright 2014 Rackspace, Inc. +# All Rights Reserved. +# +# 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 pecan +from pecan import rest +from wsme import types +from wsmeext import pecan as wsme_pecan + +from teeth_agent.api.controllers.v1 import base + + +class AgentStatus(base.APIBase): + mode = types.text + started_at = base.MultiType(float) + version = types.text + + @classmethod + def from_agent_status(cls, status): + instance = cls() + for field in ('mode', 'started_at', 'version'): + setattr(instance, field, getattr(status, field)) + return instance + + +class StatusController(rest.RestController): + """Controller for getting agent status.""" + + @wsme_pecan.wsexpose(AgentStatus) + def get_all(self): + agent = pecan.request.agent + status = agent.get_status() + return AgentStatus.from_agent_status(status) |
