2014-01-15 15:22:12 -08:00
|
|
|
"""
|
|
|
|
Copyright 2013 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 random
|
|
|
|
import threading
|
|
|
|
import time
|
|
|
|
|
2014-03-19 17:06:34 -07:00
|
|
|
import pkg_resources
|
2014-03-25 18:00:10 +04:00
|
|
|
from stevedore import extension
|
2014-03-19 17:06:34 -07:00
|
|
|
from wsgiref import simple_server
|
|
|
|
|
2014-03-19 16:19:52 -07:00
|
|
|
from ironic_python_agent.api import app
|
|
|
|
from ironic_python_agent import base
|
|
|
|
from ironic_python_agent import encoding
|
|
|
|
from ironic_python_agent import errors
|
|
|
|
from ironic_python_agent import hardware
|
|
|
|
from ironic_python_agent import ironic_api_client
|
|
|
|
from ironic_python_agent.openstack.common import log
|
|
|
|
from ironic_python_agent import utils
|
|
|
|
|
2014-01-15 15:22:12 -08:00
|
|
|
|
2014-03-17 15:17:27 -07:00
|
|
|
def _time():
|
|
|
|
"""Wraps time.time() for simpler testing."""
|
|
|
|
return time.time()
|
|
|
|
|
|
|
|
|
2014-03-19 16:19:52 -07:00
|
|
|
class IronicPythonAgentStatus(encoding.Serializable):
|
2014-03-25 18:00:10 +04:00
|
|
|
def __init__(self, started_at, version):
|
2014-01-15 15:22:12 -08:00
|
|
|
self.started_at = started_at
|
|
|
|
self.version = version
|
|
|
|
|
2014-03-17 10:58:39 -07:00
|
|
|
def serialize(self):
|
2014-01-15 15:22:12 -08:00
|
|
|
"""Turn the status into a dict."""
|
2014-03-11 13:31:19 -07:00
|
|
|
return utils.get_ordereddict([
|
2014-01-15 15:22:12 -08:00
|
|
|
('started_at', self.started_at),
|
|
|
|
('version', self.version),
|
|
|
|
])
|
|
|
|
|
|
|
|
|
2014-03-19 16:19:52 -07:00
|
|
|
class IronicPythonAgentHeartbeater(threading.Thread):
|
2014-01-15 15:22:12 -08:00
|
|
|
# If we could wait at most N seconds between heartbeats (or in case of an
|
|
|
|
# error) we will instead wait r x N seconds, where r is a random value
|
|
|
|
# between these multipliers.
|
|
|
|
min_jitter_multiplier = 0.3
|
|
|
|
max_jitter_multiplier = 0.6
|
|
|
|
|
|
|
|
# Exponential backoff values used in case of an error. In reality we will
|
|
|
|
# only wait a portion of either of these delays based on the jitter
|
|
|
|
# multipliers.
|
|
|
|
initial_delay = 1.0
|
|
|
|
max_delay = 300.0
|
|
|
|
backoff_factor = 2.7
|
|
|
|
|
|
|
|
def __init__(self, agent):
|
2014-03-19 16:19:52 -07:00
|
|
|
super(IronicPythonAgentHeartbeater, self).__init__()
|
2014-01-15 15:22:12 -08:00
|
|
|
self.agent = agent
|
2014-01-28 11:25:46 -08:00
|
|
|
self.hardware = hardware.get_manager()
|
2014-03-19 16:19:52 -07:00
|
|
|
self.api = ironic_api_client.APIClient(agent.api_url)
|
2014-03-17 15:17:27 -07:00
|
|
|
self.log = log.getLogger(__name__)
|
2014-01-15 15:22:12 -08:00
|
|
|
self.stop_event = threading.Event()
|
|
|
|
self.error_delay = self.initial_delay
|
|
|
|
|
|
|
|
def run(self):
|
|
|
|
# The first heartbeat happens now
|
|
|
|
self.log.info('starting heartbeater')
|
|
|
|
interval = 0
|
|
|
|
|
|
|
|
while not self.stop_event.wait(interval):
|
2014-03-20 15:18:48 -07:00
|
|
|
self.do_heartbeat()
|
2014-01-15 15:22:12 -08:00
|
|
|
interval_multiplier = random.uniform(self.min_jitter_multiplier,
|
|
|
|
self.max_jitter_multiplier)
|
2014-03-20 15:18:48 -07:00
|
|
|
interval = self.agent.heartbeat_timeout * interval_multiplier
|
2014-03-17 15:17:27 -07:00
|
|
|
log_msg = 'sleeping before next heartbeat, interval: {0}'
|
|
|
|
self.log.info(log_msg.format(interval))
|
2014-01-15 15:22:12 -08:00
|
|
|
|
|
|
|
def do_heartbeat(self):
|
|
|
|
try:
|
2014-03-20 15:18:48 -07:00
|
|
|
self.api.heartbeat(
|
2014-03-19 13:55:56 -07:00
|
|
|
uuid=self.agent.get_node_uuid(),
|
|
|
|
advertise_address=self.agent.advertise_address
|
|
|
|
)
|
2014-01-15 15:22:12 -08:00
|
|
|
self.error_delay = self.initial_delay
|
|
|
|
self.log.info('heartbeat successful')
|
2014-03-17 15:25:01 -07:00
|
|
|
except Exception:
|
|
|
|
self.log.exception('error sending heartbeat')
|
2014-01-15 15:22:12 -08:00
|
|
|
self.error_delay = min(self.error_delay * self.backoff_factor,
|
|
|
|
self.max_delay)
|
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
self.log.info('stopping heartbeater')
|
|
|
|
self.stop_event.set()
|
|
|
|
return self.join()
|
|
|
|
|
|
|
|
|
2014-03-19 16:19:52 -07:00
|
|
|
class IronicPythonAgent(object):
|
2014-03-26 10:57:25 -07:00
|
|
|
def __init__(self, api_url, advertise_address, listen_address,
|
|
|
|
lookup_timeout, lookup_interval):
|
2014-01-15 15:22:12 -08:00
|
|
|
self.api_url = api_url
|
2014-03-19 16:19:52 -07:00
|
|
|
self.api_client = ironic_api_client.APIClient(self.api_url)
|
2014-01-15 15:22:12 -08:00
|
|
|
self.listen_address = listen_address
|
2014-03-19 13:55:56 -07:00
|
|
|
self.advertise_address = advertise_address
|
2014-03-19 16:19:52 -07:00
|
|
|
self.version = pkg_resources.get_distribution('ironic-python-agent')\
|
|
|
|
.version
|
2014-03-11 14:13:13 -07:00
|
|
|
self.api = app.VersionSelectorApplication(self)
|
2014-03-11 13:31:19 -07:00
|
|
|
self.command_results = utils.get_ordereddict()
|
2014-03-19 16:19:52 -07:00
|
|
|
self.heartbeater = IronicPythonAgentHeartbeater(self)
|
2014-03-20 15:18:48 -07:00
|
|
|
self.heartbeat_timeout = None
|
2014-01-22 17:28:24 -08:00
|
|
|
self.hardware = hardware.get_manager()
|
2014-01-15 15:22:12 -08:00
|
|
|
self.command_lock = threading.Lock()
|
2014-03-17 15:17:27 -07:00
|
|
|
self.log = log.getLogger(__name__)
|
2014-01-15 15:22:12 -08:00
|
|
|
self.started_at = None
|
2014-03-18 12:45:36 -07:00
|
|
|
self.node = None
|
2014-03-25 18:00:10 +04:00
|
|
|
self.ext_mgr = extension.ExtensionManager(
|
|
|
|
namespace='ironic_python_agent.extensions',
|
|
|
|
invoke_on_load=True,
|
|
|
|
propagate_map_exceptions=True,
|
|
|
|
)
|
2014-03-26 10:57:25 -07:00
|
|
|
# lookup timeout in seconds
|
|
|
|
self.lookup_timeout = lookup_timeout
|
|
|
|
self.lookup_interval = lookup_interval
|
2014-01-23 12:36:29 -08:00
|
|
|
|
2014-01-15 15:22:12 -08:00
|
|
|
def get_status(self):
|
|
|
|
"""Retrieve a serializable status."""
|
2014-03-19 16:19:52 -07:00
|
|
|
return IronicPythonAgentStatus(
|
2014-01-15 15:22:12 -08:00
|
|
|
started_at=self.started_at,
|
|
|
|
version=self.version
|
|
|
|
)
|
|
|
|
|
|
|
|
def get_agent_mac_addr(self):
|
|
|
|
return self.hardware.get_primary_mac_address()
|
|
|
|
|
2014-03-13 16:52:45 -07:00
|
|
|
def get_node_uuid(self):
|
2014-03-19 13:55:56 -07:00
|
|
|
if 'uuid' not in self.node:
|
|
|
|
errors.HeartbeatError('Tried to heartbeat without node UUID.')
|
2014-03-18 12:45:36 -07:00
|
|
|
return self.node['uuid']
|
2014-03-13 16:52:45 -07:00
|
|
|
|
2014-01-15 15:22:12 -08:00
|
|
|
def list_command_results(self):
|
|
|
|
return self.command_results.values()
|
|
|
|
|
|
|
|
def get_command_result(self, result_id):
|
|
|
|
try:
|
|
|
|
return self.command_results[result_id]
|
|
|
|
except KeyError:
|
|
|
|
raise errors.RequestedObjectNotFoundError('Command Result',
|
|
|
|
result_id)
|
|
|
|
|
2014-01-23 12:36:29 -08:00
|
|
|
def _split_command(self, command_name):
|
|
|
|
command_parts = command_name.split('.', 1)
|
|
|
|
if len(command_parts) != 2:
|
|
|
|
raise errors.InvalidCommandError(
|
2014-03-25 18:00:10 +04:00
|
|
|
'Command name must be of the form <extension>.<name>')
|
2014-01-23 12:36:29 -08:00
|
|
|
|
|
|
|
return (command_parts[0], command_parts[1])
|
|
|
|
|
2014-01-15 15:22:12 -08:00
|
|
|
def execute_command(self, command_name, **kwargs):
|
|
|
|
"""Execute an agent command."""
|
|
|
|
with self.command_lock:
|
2014-03-25 18:00:10 +04:00
|
|
|
extension_part, command_part = self._split_command(command_name)
|
2014-01-23 12:36:29 -08:00
|
|
|
|
2014-01-15 15:22:12 -08:00
|
|
|
if len(self.command_results) > 0:
|
|
|
|
last_command = self.command_results.values()[-1]
|
|
|
|
if not last_command.is_done():
|
|
|
|
raise errors.CommandExecutionError('agent is busy')
|
|
|
|
|
|
|
|
try:
|
2014-03-25 18:00:10 +04:00
|
|
|
ext = self.ext_mgr[extension_part].obj
|
|
|
|
result = ext.execute(command_part, **kwargs)
|
|
|
|
except KeyError:
|
|
|
|
# Extension Not found
|
|
|
|
raise errors.RequestedObjectNotFoundError('Extension',
|
|
|
|
extension_part)
|
2014-03-17 10:58:39 -07:00
|
|
|
except errors.InvalidContentError as e:
|
2014-01-15 15:22:12 -08:00
|
|
|
# Any command may raise a InvalidContentError which will be
|
|
|
|
# returned to the caller directly.
|
|
|
|
raise e
|
|
|
|
except Exception as e:
|
|
|
|
# Other errors are considered command execution errors, and are
|
|
|
|
# recorded as an
|
2014-03-11 14:13:13 -07:00
|
|
|
result = base.SyncCommandResult(command_name,
|
|
|
|
kwargs,
|
|
|
|
False,
|
|
|
|
unicode(e))
|
2014-01-15 15:22:12 -08:00
|
|
|
|
|
|
|
self.command_results[result.id] = result
|
|
|
|
return result
|
|
|
|
|
|
|
|
def run(self):
|
2014-03-19 16:19:52 -07:00
|
|
|
"""Run the Ironic Python Agent."""
|
2014-03-26 10:57:25 -07:00
|
|
|
# Get the UUID so we can heartbeat to Ironic. Raises LookupNodeError
|
|
|
|
# if there is an issue (uncaught, restart agent)
|
2014-03-17 15:17:27 -07:00
|
|
|
self.started_at = _time()
|
2014-03-20 15:18:48 -07:00
|
|
|
content = self.api_client.lookup_node(
|
2014-03-26 10:57:25 -07:00
|
|
|
hardware_info=self.hardware.list_hardware_info(),
|
|
|
|
timeout=self.lookup_timeout,
|
|
|
|
starting_interval=self.lookup_interval)
|
|
|
|
|
2014-03-20 15:18:48 -07:00
|
|
|
self.node = content['node']
|
|
|
|
self.heartbeat_timeout = content['heartbeat_timeout']
|
2014-01-15 15:22:12 -08:00
|
|
|
self.heartbeater.start()
|
2014-03-07 15:36:22 -08:00
|
|
|
wsgi = simple_server.make_server(
|
|
|
|
self.listen_address[0],
|
|
|
|
self.listen_address[1],
|
|
|
|
self.api,
|
|
|
|
server_class=simple_server.WSGIServer)
|
2014-01-15 15:22:12 -08:00
|
|
|
|
|
|
|
try:
|
2014-03-07 15:36:22 -08:00
|
|
|
wsgi.serve_forever()
|
2014-03-17 15:25:01 -07:00
|
|
|
except BaseException:
|
|
|
|
self.log.exception('shutting down')
|
2014-01-15 15:22:12 -08:00
|
|
|
|
|
|
|
self.heartbeater.stop()
|
|
|
|
|
|
|
|
|
2014-03-19 13:55:56 -07:00
|
|
|
def build_agent(api_url,
|
|
|
|
advertise_host,
|
|
|
|
advertise_port,
|
|
|
|
listen_host,
|
2014-03-26 10:57:25 -07:00
|
|
|
listen_port,
|
|
|
|
lookup_timeout,
|
|
|
|
lookup_interval):
|
2014-03-19 13:55:56 -07:00
|
|
|
|
2014-03-19 16:19:52 -07:00
|
|
|
return IronicPythonAgent(api_url,
|
|
|
|
(advertise_host, advertise_port),
|
2014-03-26 10:57:25 -07:00
|
|
|
(listen_host, listen_port),
|
|
|
|
lookup_timeout,
|
|
|
|
lookup_interval)
|