diff options
| author | Josh Gachnang <josh@pcsforeducation.com> | 2014-03-19 17:29:53 -0700 |
|---|---|---|
| committer | Josh Gachnang <josh@pcsforeducation.com> | 2014-03-19 17:29:53 -0700 |
| commit | 0e6776f6c0e88ccec5d4875d6ff196bc06ec558f (patch) | |
| tree | 6fd24ed526c914ccaab77e2ccbad22a00ce0b75a /ironic_python_agent/api/controllers/v1 | |
| parent | 6e366520bbcdb1e4540d550a2017d5440b16938b (diff) | |
| parent | fc043dd1d345034dc9f63738ec09d2c88e3bb286 (diff) | |
| download | ironic-python-agent-0e6776f6c0e88ccec5d4875d6ff196bc06ec558f.tar.gz | |
Merge pull request #72 from rackerlabs/JoshNang/ipa
Rename to Ironic Python Agent
Diffstat (limited to 'ironic_python_agent/api/controllers/v1')
| -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 |
5 files changed, 367 insertions, 0 deletions
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..726a0bf4 --- /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 ironic_python_agent.api.controllers.v1 import base +from ironic_python_agent.api.controllers.v1 import command +from ironic_python_agent.api.controllers.v1 import link +from ironic_python_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..5815c4fa --- /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 ironic_python_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..7fa7f982 --- /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 ironic_python_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..7916f09c --- /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 ironic_python_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) |
