diff --git a/requirements.txt b/requirements.txt index 148f0bcf8..19e997103 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,3 +6,4 @@ wsgiref>=0.1.2 pecan>=0.4.5 oslo.config>=1.2.0 WSME>=0.6 +six>=1.5.2 diff --git a/teeth_agent/agent.py b/teeth_agent/agent.py index 3db736b61..9083c989e 100644 --- a/teeth_agent/agent.py +++ b/teeth_agent/agent.py @@ -112,7 +112,7 @@ class TeethAgent(object): self.listen_address = listen_address self.mode_implementation = None self.version = pkg_resources.get_distribution('teeth-agent').version - self.api = app.VersionSelectorApplication() + self.api = app.VersionSelectorApplication(self) self.command_results = collections.OrderedDict() self.heartbeater = TeethAgentHeartbeater(self) self.hardware = hardware.get_manager() @@ -187,7 +187,10 @@ class TeethAgent(object): except Exception as e: # Other errors are considered command execution errors, and are # recorded as an - result = base.SyncCommandResult(command_name, kwargs, False, e) + result = base.SyncCommandResult(command_name, + kwargs, + False, + unicode(e)) self.command_results[result.id] = result return result diff --git a/teeth_agent/api/app.py b/teeth_agent/api/app.py index 3c25a6850..a850b6187 100644 --- a/teeth_agent/api/app.py +++ b/teeth_agent/api/app.py @@ -16,19 +16,29 @@ limitations under the License. from oslo.config import cfg import pecan +from pecan import hooks from teeth_agent.api import config CONF = cfg.CONF +class AgentHook(hooks.PecanHook): + def __init__(self, agent, *args, **kwargs): + super(AgentHook, self).__init__(*args, **kwargs) + self.agent = agent + + def before(self, state): + state.request.agent = self.agent + + def get_pecan_config(): # Set up the pecan configuration filename = config.__file__.replace('.pyc', '.py') return pecan.configuration.conf_from_file(filename) -def setup_app(pecan_config=None, extra_hooks=None): +def setup_app(agent, pecan_config=None, extra_hooks=None): #policy.init() #app_hooks = [hooks.ConfigHook(), @@ -39,6 +49,8 @@ def setup_app(pecan_config=None, extra_hooks=None): #if extra_hooks: #app_hooks.extend(extra_hooks) + app_hooks = [AgentHook(agent)] + if not pecan_config: pecan_config = get_pecan_config() @@ -50,7 +62,7 @@ def setup_app(pecan_config=None, extra_hooks=None): debug=True, #debug=CONF.debug, force_canonical=getattr(pecan_config.app, 'force_canonical', True), - #hooks=app_hooks, + hooks=app_hooks, #wrap_app=middleware.ParsableErrorMiddleware, ) @@ -58,9 +70,9 @@ def setup_app(pecan_config=None, extra_hooks=None): class VersionSelectorApplication(object): - def __init__(self): + def __init__(self, agent): pc = get_pecan_config() - self.v1 = setup_app(pecan_config=pc) + self.v1 = setup_app(agent, pecan_config=pc) def __call__(self, environ, start_response): return self.v1(environ, start_response) diff --git a/teeth_agent/api/controllers/v1/__init__.py b/teeth_agent/api/controllers/v1/__init__.py index a02b02e1b..8a120c118 100644 --- a/teeth_agent/api/controllers/v1/__init__.py +++ b/teeth_agent/api/controllers/v1/__init__.py @@ -13,13 +13,7 @@ # under the License. """ -Version 1 of the Ironic API - -NOTE: IN PROGRESS AND NOT FULLY IMPLEMENTED. - -Should maintain feature parity with Nova Baremetal Extension. - -Specification can be found at ironic/doc/api/v1.rst +Version 1 of the Ironic Python Agent API """ import pecan @@ -29,11 +23,8 @@ from wsme import types as wtypes import wsmeext.pecan as wsme_pecan from teeth_agent.api.controllers.v1 import base -#from ironic.api.controllers.v1 import chassis -#from ironic.api.controllers.v1 import driver +from teeth_agent.api.controllers.v1 import command from teeth_agent.api.controllers.v1 import link -#from ironic.api.controllers.v1 import node -#from ironic.api.controllers.v1 import port class MediaType(base.APIBase): @@ -59,17 +50,8 @@ class V1(base.APIBase): links = [link.Link] "Links that point to a specific URL for this version and documentation" - #chassis = [link.Link] - #"Links to the chassis resource" - - #nodes = [link.Link] - #"Links to the nodes resource" - - #ports = [link.Link] - #"Links to the ports resource" - - #drivers = [link.Link] - #"Links to the drivers resource" + commands = [link.Link] + "Links to the command resource" @classmethod def convert(self): @@ -84,46 +66,22 @@ class V1(base.APIBase): 'api-spec-v1.html', bookmark=True, type='text/html') ] + v1.command = [link.Link.make_link('self', pecan.request.host_url, + 'commands', ''), + link.Link.make_link('bookmark', + pecan.request.host_url, + 'commands', '', + bookmark=True) + ] v1.media_types = [MediaType('application/json', 'application/vnd.openstack.ironic.v1+json')] - #v1.chassis = [link.Link.make_link('self', pecan.request.host_url, - #'chassis', ''), - #link.Link.make_link('bookmark', - #pecan.request.host_url, - #'chassis', '', - #bookmark=True) - #] - #v1.nodes = [link.Link.make_link('self', pecan.request.host_url, - #'nodes', ''), - #link.Link.make_link('bookmark', - #pecan.request.host_url, - #'nodes', '', - #bookmark=True) - #] - #v1.ports = [link.Link.make_link('self', pecan.request.host_url, - #'ports', ''), - #link.Link.make_link('bookmark', - #pecan.request.host_url, - #'ports', '', - #bookmark=True) - #] - #v1.drivers = [link.Link.make_link('self', pecan.request.host_url, - #'drivers', ''), - #link.Link.make_link('bookmark', - #pecan.request.host_url, - #'drivers', '', - #bookmark=True) - #] return v1 class Controller(rest.RestController): """Version 1 API controller root.""" - #nodes = node.NodesController() - #ports = port.PortsController() - #chassis = chassis.ChassisController() - #drivers = driver.DriversController() + commands = command.CommandController() @wsme_pecan.wsexpose(V1) def get(self): diff --git a/teeth_agent/api/controllers/v1/base.py b/teeth_agent/api/controllers/v1/base.py index 8a10ffbfc..15f3aec82 100644 --- a/teeth_agent/api/controllers/v1/base.py +++ b/teeth_agent/api/controllers/v1/base.py @@ -12,36 +12,44 @@ # License for the specific language governing permissions and limitations # under the License. -import datetime - +import six import wsme from wsme import types as wtypes +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): - - created_at = wsme.wsattr(datetime.datetime, readonly=True) - "The time in UTC at which the object is created" - - updated_at = wsme.wsattr(datetime.datetime, readonly=True) - "The time in UTC at which the object is updated" - def as_dict(self): """Render this object as a dict of its fields.""" return dict((k, getattr(self, k)) for k in self.fields if hasattr(self, k) and getattr(self, k) != wsme.Unset) - - def unset_fields_except(self, except_list=None): - """Unset fields so they don't appear in the message body. - - :param except_list: A list of fields that won't be touched. - - """ - if except_list is None: - except_list = [] - - for k in self.as_dict(): - if k not in except_list: - setattr(self, k, wsme.Unset) diff --git a/teeth_agent/api/controllers/v1/command.py b/teeth_agent/api/controllers/v1/command.py new file mode 100644 index 000000000..89aa77406 --- /dev/null +++ b/teeth_agent/api/controllers/v1/command.py @@ -0,0 +1,88 @@ +# Copyright 2013 Red Hat, 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 = base.json_type + command_status = types.text + command_error = types.text + command_result = types.text + + @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.text + params = base.json_type + + @classmethod + def deserialize(cls, obj): + instance = cls() + instance.name = obj['name'] + instance.params = obj['params'] + return instance + + +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, command): + agent = pecan.request.agent + result = agent.execute_command(command.name, **command.params) + return result diff --git a/teeth_agent/base.py b/teeth_agent/base.py index 3170678a6..f666a232f 100644 --- a/teeth_agent/base.py +++ b/teeth_agent/base.py @@ -112,7 +112,7 @@ class AsyncCommandResult(BaseCommandResult): e = errors.CommandExecutionError(str(e)) with self.command_state_lock: - self.command_error = e + self.command_error = '{}: {}'.format(e.message, e.details) self.command_status = AgentCommandStatus.FAILED