bareon-ironic/bareon_ironic/modules/resources/actions.py

230 lines
8.1 KiB
Python

#
# Copyright 2016 Cray 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.
"""
Deploy driver actions.
"""
import datetime
import os
import tempfile
from oslo_concurrency import processutils
from oslo_config import cfg
from ironic.common import exception
from ironic.openstack.common import log
from bareon_ironic.modules import bareon_utils
from bareon_ironic.modules.resources import resources
from bareon_ironic.modules.resources import rsync
LOG = log.getLogger(__name__)
CONF = cfg.CONF
# This module allows to run a set of json-defined actions on the node.
#
# Actions json structure:
# {
# "actions": [
# {
# "cmd": "echo 'test 1 run success!'",
# "name": "test_script_1",
# "terminate_on_fail": true,
# "args": "",
# "sudo": false,
# "resources": [
# {
# "name": "swift prefetced",
# "mode": "push",
# "url": "swift:max_container/swift_pref1",
# "target": "/tmp/swift_prefetch_res"
# }
# ]
# },
# {
# another action ...
# }
# ]
# }
#
# Each action carries a list of associated resources. Resource, and thus
# action, can be in two states: not_fetched and fetched. See Resource
# documentation. You should always pass a json of not_fetched resources.
#
# The workflow can be one of the following:
# 1. When you need to fetch actions while you have a proper context,
# then serialize them, and run later, deserializing from a file.
# - Create controller from user json -> fetch_action_resources() -> to_dict()
# - Do whatever while actions are serialized, e.g. wait for node boot.
# - Create controller from the serialized json -> execute()
# 2. A workflow when you need to fetch and run actions immediately.
# - Create controller from user json -> execute()
class Action(resources.ResourceList):
def __init__(self, action, task):
req = ('name', 'cmd', 'args', 'sudo', 'resources',
'terminate_on_fail')
bareon_utils.validate_json(req, action)
super(Action, self).__init__(action, task)
LOG.debug("[%s] Action created from %s"
% (self._task.node.uuid, action))
def execute(self, ssh, sftp):
"""Execute action.
Fetch resources, upload them, and run command.
"""
cmd = ("%s %s" % (self.cmd, self.args))
if self.sudo:
cmd = "sudo %s" % cmd
self.fetch_resources()
self.upload_resources(sftp)
return processutils.ssh_execute(ssh, cmd)
@staticmethod
def from_dict(action, task):
return Action(action, task)
class ActionController(bareon_utils.RawToPropertyMixin):
def __init__(self, task, action_data):
self._raw = action_data
self._task = task
try:
req = ('actions',)
bareon_utils.validate_json(req, action_data)
self.actions = [Action.from_dict(a, self._task)
for a in action_data['actions']]
except Exception as ex:
self._save_exception_result(ex)
raise
def fetch_action_resources(self):
"""Fetch all resources of all actions.
Must be idempotent.
"""
for action in self.actions:
try:
action.fetch_resources()
except Exception as ex:
# Cleanup is already done in ResourceList.fetch_resources()
self._save_exception_result(ex)
raise
def cleanup_action_resources(self):
"""Cleanup all resources of all actions.
Must be idempotent.
Must return None if called when actions resources are not fetched.
"""
for action in self.actions:
action.cleanup_resources()
def _execute(self, ssh, sftp):
results = []
# Clean previous results at the beginning
self._save_results(results)
for action in self.actions:
try:
out, err = action.execute(ssh, sftp)
results.append({'name': action.name, 'passed': True})
LOG.info("[%s] Action '%s' finished with:"
"\n stdout: %s\n stderr: %s" %
(self._task.node.uuid, action.name, out, err))
except Exception as ex:
results.append({'name': action.name, 'passed': False,
'exception': str(ex)})
LOG.info("[%s] Action '%s' failed with error: %s" %
(self._task.node.uuid, action.name, str(ex)))
if action.terminate_on_fail:
raise
finally:
# Save results after each action. Result list will grow until
# all actions are done.
self._save_results(results)
def execute(self, ssh, sftp, **ssh_params):
"""Execute using already opened SSH connection to the node."""
try:
if CONF.rsync.rsync_secure_transfer:
ssh_user = ssh_params.get('username')
ssh_key_file = ssh_params.get('key_filename')
ssh_host = ssh_params.get('host')
ssh_port = ssh_params.get('port', 22)
with bareon_utils.ssh_tunnel(rsync.RSYNC_PORT, ssh_user,
ssh_key_file, ssh_host, ssh_port):
self._execute(ssh, sftp)
else:
self._execute(ssh, sftp)
finally:
self.cleanup_action_resources()
def ssh_and_execute(self, node_ip, ssh_user, ssh_key_url):
"""Open an SSH connection to the node and execute."""
# NOTE(lobur): Security flaw.
# A random-name tempfile with private key contents exists on Conductor
# during the time of execution of tenant-image actions when
# rsync_secure_transfer is True.
# Because we are using a bash command to start a tunnel we need to
# have the private key in a file.
# To fix this we need to start tunnel using Paramiko, which
# is impossible currently. Paramiko would accept raw key contents,
# thus we won't need a file.
with tempfile.NamedTemporaryFile(delete=True) as key_file:
os.chmod(key_file.name, 0o700)
try:
if not (ssh_user and ssh_key_url):
raise exception.MissingParameterValue(
"Need action_user and action_key params to "
"execute actions")
key_contents = resources.url_download_raw_secured(
self._task.context, self._task.node, ssh_key_url)
key_file.file.write(key_contents)
key_file.file.flush()
ssh = bareon_utils.get_ssh_connection(
self._task, username=ssh_user,
key_contents=key_contents, host=node_ip)
sftp = ssh.open_sftp()
except Exception as ex:
self._save_exception_result(ex)
raise
else:
self.execute(ssh, sftp,
username=ssh_user,
key_filename=key_file.name,
host=node_ip)
def _save_results(self, results):
bareon_utils.change_node_dict(
self._task.node, 'instance_info',
{'exec_actions': {
'results': results,
'finished_at': str(datetime.datetime.utcnow())}})
self._task.node.save()
def _save_exception_result(self, ex):
self._save_results({'exception': str(ex)})