summaryrefslogtreecommitdiff
path: root/ironic_python_agent/api/controllers
diff options
context:
space:
mode:
Diffstat (limited to 'ironic_python_agent/api/controllers')
-rw-r--r--ironic_python_agent/api/controllers/__init__.py15
-rw-r--r--ironic_python_agent/api/controllers/root.py96
-rw-r--r--ironic_python_agent/api/controllers/v1/__init__.py118
-rw-r--r--ironic_python_agent/api/controllers/v1/base.py73
-rw-r--r--ironic_python_agent/api/controllers/v1/command.py89
-rw-r--r--ironic_python_agent/api/controllers/v1/link.py43
-rw-r--r--ironic_python_agent/api/controllers/v1/status.py44
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)