Merge branch 'iteration4'
This commit is contained in:
commit
243f51e673
20
conductor/.gitignore
vendored
Normal file
20
conductor/.gitignore
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
#IntelJ Idea
|
||||
.idea/
|
||||
|
||||
#virtualenv
|
||||
.venv/
|
||||
|
||||
#Build results
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
|
||||
#Python
|
||||
*.pyc
|
||||
|
||||
#Translation build
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
#SQLite Database files
|
||||
*.sqlite
|
1
conductor/babel.cfg
Normal file
1
conductor/babel.cfg
Normal file
@ -0,0 +1 @@
|
||||
[python: **.py]
|
@ -1,3 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
from conductor import app
|
@ -1,3 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
@ -15,22 +16,21 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
TEMPLATE_DEPLOYMENT_COMMAND = "Template"
|
||||
EXECUTION_PLAN_DEPLOYMENT_COMMAND = "EPlan"
|
||||
CHEF_COMMAND = "Chef"
|
||||
CHEF_OP_CREATE_ENV = "Env"
|
||||
CHEF_OP_CREATE_ROLE = "Role"
|
||||
CHEF_OP_ASSIGN_ROLE = "AssignRole"
|
||||
CHEF_OP_CREATE_NODE = "CRNode"
|
||||
|
||||
class Command(object):
|
||||
type = "Empty"
|
||||
context = None
|
||||
import sys
|
||||
|
||||
|
||||
def __init__(self, type="Empty", context=None, data=None):
|
||||
self.type = type
|
||||
self.context = context
|
||||
self.data = data
|
||||
|
||||
from conductor import config
|
||||
from conductor.openstack.common import log
|
||||
from conductor.openstack.common import service
|
||||
from conductor.app import ConductorWorkflowService
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
config.parse_args()
|
||||
log.setup('conductor')
|
||||
launcher = service.ServiceLauncher()
|
||||
launcher.launch_service(ConductorWorkflowService())
|
||||
launcher.wait()
|
||||
except RuntimeError, e:
|
||||
sys.stderr.write("ERROR: %s\n" % e)
|
||||
sys.exit(1)
|
@ -1,65 +1,90 @@
|
||||
import datetime
|
||||
import glob
|
||||
import json
|
||||
import time
|
||||
import sys
|
||||
import tornado.ioloop
|
||||
|
||||
import rabbitmq
|
||||
from workflow import Workflow
|
||||
import cloud_formation
|
||||
import windows_agent
|
||||
from commands.dispatcher import CommandDispatcher
|
||||
from config import Config
|
||||
import reporting
|
||||
|
||||
config = Config(sys.argv[1] if len(sys.argv) > 1 else None)
|
||||
|
||||
rmqclient = rabbitmq.RabbitMqClient(
|
||||
virtual_host=config.get_setting('rabbitmq', 'vhost', '/'),
|
||||
login=config.get_setting('rabbitmq', 'login', 'guest'),
|
||||
password=config.get_setting('rabbitmq', 'password', 'guest'),
|
||||
host=config.get_setting('rabbitmq', 'host', 'localhost'))
|
||||
|
||||
|
||||
def schedule(callback, *args, **kwargs):
|
||||
tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 0.1,
|
||||
lambda args=args, kwargs=kwargs: callback(*args, **kwargs))
|
||||
|
||||
|
||||
def task_received(task, message_id):
|
||||
print 'Starting at', datetime.datetime.now()
|
||||
reporter = reporting.Reporter(rmqclient, message_id, task['id'])
|
||||
|
||||
command_dispatcher = CommandDispatcher(task['name'], rmqclient)
|
||||
workflows = []
|
||||
for path in glob.glob("data/workflows/*.xml"):
|
||||
print "loading", path
|
||||
workflow = Workflow(path, task, command_dispatcher, config, reporter)
|
||||
workflows.append(workflow)
|
||||
|
||||
def loop(callback):
|
||||
for workflow in workflows:
|
||||
workflow.execute()
|
||||
func = lambda: schedule(loop, callback)
|
||||
if not command_dispatcher.execute_pending(func):
|
||||
callback()
|
||||
|
||||
def shutdown():
|
||||
command_dispatcher.close()
|
||||
rmqclient.send('task-results', json.dumps(task),
|
||||
message_id=message_id)
|
||||
print 'Finished at', datetime.datetime.now()
|
||||
|
||||
loop(shutdown)
|
||||
|
||||
|
||||
def message_received(body, message_id, **kwargs):
|
||||
task_received(json.loads(body), message_id)
|
||||
|
||||
|
||||
def start():
|
||||
rmqclient.subscribe("tasks", message_received)
|
||||
|
||||
rmqclient.start(start)
|
||||
tornado.ioloop.IOLoop.instance().start()
|
||||
import datetime
|
||||
import glob
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import anyjson
|
||||
from conductor.openstack.common import service
|
||||
from workflow import Workflow
|
||||
from commands.dispatcher import CommandDispatcher
|
||||
from openstack.common import log as logging
|
||||
from config import Config
|
||||
import reporting
|
||||
import rabbitmq
|
||||
|
||||
import windows_agent
|
||||
import cloud_formation
|
||||
|
||||
config = Config(sys.argv[1] if len(sys.argv) > 1 else None)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def task_received(task, message_id):
|
||||
with rabbitmq.RmqClient() as rmqclient:
|
||||
try:
|
||||
log.info('Starting processing task {0}: {1}'.format(
|
||||
message_id, anyjson.dumps(task)))
|
||||
reporter = reporting.Reporter(rmqclient, message_id, task['id'])
|
||||
|
||||
command_dispatcher = CommandDispatcher(
|
||||
task['name'], rmqclient, task['token'], task['tenant_id'])
|
||||
workflows = []
|
||||
for path in glob.glob("data/workflows/*.xml"):
|
||||
log.debug('Loading XML {0}'.format(path))
|
||||
workflow = Workflow(path, task, command_dispatcher, config,
|
||||
reporter)
|
||||
workflows.append(workflow)
|
||||
|
||||
while True:
|
||||
try:
|
||||
while True:
|
||||
result = False
|
||||
for workflow in workflows:
|
||||
if workflow.execute():
|
||||
result = True
|
||||
if not result:
|
||||
break
|
||||
if not command_dispatcher.execute_pending():
|
||||
break
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
break
|
||||
|
||||
command_dispatcher.close()
|
||||
finally:
|
||||
del task['token']
|
||||
result_msg = rabbitmq.Message()
|
||||
result_msg.body = task
|
||||
result_msg.id = message_id
|
||||
|
||||
rmqclient.send(message=result_msg, key='task-results')
|
||||
log.info('Finished processing task {0}. Result = {1}'.format(
|
||||
message_id, anyjson.dumps(task)))
|
||||
|
||||
|
||||
class ConductorWorkflowService(service.Service):
|
||||
def __init__(self):
|
||||
super(ConductorWorkflowService, self).__init__()
|
||||
|
||||
def start(self):
|
||||
super(ConductorWorkflowService, self).start()
|
||||
self.tg.add_thread(self._start_rabbitmq)
|
||||
|
||||
def stop(self):
|
||||
super(ConductorWorkflowService, self).stop()
|
||||
|
||||
def _start_rabbitmq(self):
|
||||
while True:
|
||||
try:
|
||||
with rabbitmq.RmqClient() as rmq:
|
||||
rmq.declare('tasks', 'tasks')
|
||||
rmq.declare('task-results')
|
||||
with rmq.open('tasks') as subscription:
|
||||
while True:
|
||||
msg = subscription.get_message()
|
||||
self.tg.add_thread(
|
||||
task_received, msg.body, msg.id)
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
|
||||
|
@ -1,38 +1,98 @@
|
||||
import base64
|
||||
|
||||
import xml_code_engine
|
||||
|
||||
|
||||
def update_cf_stack(engine, context, body, template,
|
||||
mappings, arguments, **kwargs):
|
||||
command_dispatcher = context['/commandDispatcher']
|
||||
print "update-cf", template
|
||||
|
||||
callback = lambda result: engine.evaluate_content(
|
||||
body.find('success'), context)
|
||||
|
||||
command_dispatcher.execute(
|
||||
name='cf', template=template, mappings=mappings,
|
||||
arguments=arguments, callback=callback)
|
||||
|
||||
|
||||
def prepare_user_data(context, template='Default', **kwargs):
|
||||
config = context['/config']
|
||||
with open('data/init.ps1') as init_script_file:
|
||||
with open('data/templates/agent-config/%s.template'
|
||||
% template) as template_file:
|
||||
init_script = init_script_file.read()
|
||||
template_data = template_file.read().replace(
|
||||
'%RABBITMQ_HOST%',
|
||||
config.get_setting('rabbitmq', 'host') or 'localhost')
|
||||
|
||||
return init_script.replace(
|
||||
'%WINDOWS_AGENT_CONFIG_BASE64%',
|
||||
base64.b64encode(template_data))
|
||||
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
update_cf_stack, "update-cf-stack")
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
prepare_user_data, "prepare_user_data")
|
||||
import base64
|
||||
|
||||
import xml_code_engine
|
||||
import config
|
||||
from random import choice
|
||||
import time
|
||||
import string
|
||||
|
||||
|
||||
def update_cf_stack(engine, context, body, template,
|
||||
mappings, arguments, **kwargs):
|
||||
command_dispatcher = context['/commandDispatcher']
|
||||
|
||||
callback = lambda result: engine.evaluate_content(
|
||||
body.find('success'), context)
|
||||
|
||||
command_dispatcher.execute(
|
||||
name='cf', command='CreateOrUpdate', template=template,
|
||||
mappings=mappings, arguments=arguments, callback=callback)
|
||||
|
||||
|
||||
def delete_cf_stack(engine, context, body, **kwargs):
|
||||
command_dispatcher = context['/commandDispatcher']
|
||||
|
||||
callback = lambda result: engine.evaluate_content(
|
||||
body.find('success'), context)
|
||||
|
||||
command_dispatcher.execute(
|
||||
name='cf', command='Delete', callback=callback)
|
||||
|
||||
|
||||
def prepare_user_data(context, hostname, service, unit, template='Default', **kwargs):
|
||||
settings = config.CONF.rabbitmq
|
||||
|
||||
with open('data/init.ps1') as init_script_file:
|
||||
with open('data/templates/agent-config/{0}.template'.format(
|
||||
template)) as template_file:
|
||||
init_script = init_script_file.read()
|
||||
template_data = template_file.read()
|
||||
template_data = template_data.replace(
|
||||
'%RABBITMQ_HOST%', settings.host)
|
||||
template_data = template_data.replace(
|
||||
'%RABBITMQ_INPUT_QUEUE%',
|
||||
'-'.join([str(context['/dataSource']['name']),
|
||||
str(service), str(unit)]).lower()
|
||||
)
|
||||
template_data = template_data.replace(
|
||||
'%RESULT_QUEUE%',
|
||||
'-execution-results-{0}'.format(
|
||||
str(context['/dataSource']['name'])).lower())
|
||||
|
||||
init_script = init_script.replace(
|
||||
'%WINDOWS_AGENT_CONFIG_BASE64%',
|
||||
base64.b64encode(template_data))
|
||||
|
||||
init_script = init_script.replace('%INTERNAL_HOSTNAME%', hostname)
|
||||
|
||||
return init_script
|
||||
|
||||
counter = 0
|
||||
|
||||
|
||||
def int2base(x, base):
|
||||
digs = string.digits + string.lowercase
|
||||
if x < 0: sign = -1
|
||||
elif x==0: return '0'
|
||||
else: sign = 1
|
||||
x *= sign
|
||||
digits = []
|
||||
while x:
|
||||
digits.append(digs[x % base])
|
||||
x /= base
|
||||
if sign < 0:
|
||||
digits.append('-')
|
||||
digits.reverse()
|
||||
return ''.join(digits)
|
||||
|
||||
|
||||
def generate_hostname(**kwargs):
|
||||
global counter
|
||||
prefix = ''.join(choice(string.lowercase) for _ in range(5))
|
||||
timestamp = int2base(int(time.time() * 1000), 36)[:8]
|
||||
suffix = int2base(counter, 36)
|
||||
counter = (counter + 1) % 1296
|
||||
return prefix + timestamp + suffix
|
||||
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
update_cf_stack, "update-cf-stack")
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
delete_cf_stack, "delete-cf-stack")
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
prepare_user_data, "prepare-user-data")
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(
|
||||
generate_hostname, "generate-hostname")
|
||||
|
@ -1,74 +1,169 @@
|
||||
import json
|
||||
import os
|
||||
import uuid
|
||||
|
||||
import conductor.helpers
|
||||
from command import CommandBase
|
||||
from subprocess import call
|
||||
|
||||
|
||||
class HeatExecutor(CommandBase):
|
||||
def __init__(self, stack):
|
||||
self._pending_list = []
|
||||
self._stack = stack
|
||||
|
||||
def execute(self, template, mappings, arguments, callback):
|
||||
with open('data/templates/cf/%s.template' % template) as template_file:
|
||||
template_data = template_file.read()
|
||||
|
||||
template_data = conductor.helpers.transform_json(
|
||||
json.loads(template_data), mappings)
|
||||
|
||||
self._pending_list.append({
|
||||
'template': template_data,
|
||||
'arguments': arguments,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def has_pending_commands(self):
|
||||
return len(self._pending_list) > 0
|
||||
|
||||
def execute_pending(self, callback):
|
||||
if not self.has_pending_commands():
|
||||
return False
|
||||
|
||||
template = {}
|
||||
arguments = {}
|
||||
for t in self._pending_list:
|
||||
template = conductor.helpers.merge_dicts(
|
||||
template, t['template'], max_levels=2)
|
||||
arguments = conductor.helpers.merge_dicts(
|
||||
arguments, t['arguments'], max_levels=1)
|
||||
|
||||
print 'Executing heat template', json.dumps(template), \
|
||||
'with arguments', arguments, 'on stack', self._stack
|
||||
|
||||
if not os.path.exists("tmp"):
|
||||
os.mkdir("tmp")
|
||||
file_name = "tmp/" + str(uuid.uuid4())
|
||||
print "Saving template to", file_name
|
||||
with open(file_name, "w") as f:
|
||||
f.write(json.dumps(template))
|
||||
|
||||
arguments_str = ';'.join(['%s=%s' % (key, value)
|
||||
for (key, value) in arguments.items()])
|
||||
call([
|
||||
"./heat_run", "stack-create",
|
||||
"-f" + file_name,
|
||||
"-P" + arguments_str,
|
||||
self._stack
|
||||
])
|
||||
|
||||
callbacks = []
|
||||
for t in self._pending_list:
|
||||
if t['callback']:
|
||||
callbacks.append(t['callback'])
|
||||
|
||||
self._pending_list = []
|
||||
|
||||
for cb in callbacks:
|
||||
cb(True)
|
||||
|
||||
callback()
|
||||
|
||||
return True
|
||||
import anyjson
|
||||
import eventlet
|
||||
|
||||
import jsonpath
|
||||
from conductor.openstack.common import log as logging
|
||||
import conductor.helpers
|
||||
from command import CommandBase
|
||||
import conductor.config
|
||||
from heatclient.client import Client
|
||||
import heatclient.exc
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
import types
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HeatExecutor(CommandBase):
|
||||
def __init__(self, stack, token, tenant_id):
|
||||
self._update_pending_list = []
|
||||
self._delete_pending_list = []
|
||||
self._stack = stack
|
||||
settings = conductor.config.CONF.heat
|
||||
|
||||
client = ksclient.Client(endpoint=settings.auth_url)
|
||||
auth_data = client.tokens.authenticate(
|
||||
tenant_id=tenant_id,
|
||||
token=token)
|
||||
|
||||
scoped_token = auth_data.id
|
||||
|
||||
heat_url = jsonpath.jsonpath(auth_data.serviceCatalog,
|
||||
"$[?(@.name == 'heat')].endpoints[0].publicURL")[0]
|
||||
|
||||
self._heat_client = Client('1', heat_url,
|
||||
token_only=True, token=scoped_token)
|
||||
|
||||
def execute(self, command, callback, **kwargs):
|
||||
log.debug('Got command {0} on stack {1}'.format(command, self._stack))
|
||||
|
||||
if command == 'CreateOrUpdate':
|
||||
return self._execute_create_update(
|
||||
kwargs['template'],
|
||||
kwargs['mappings'],
|
||||
kwargs['arguments'],
|
||||
callback)
|
||||
elif command == 'Delete':
|
||||
return self._execute_delete(callback)
|
||||
|
||||
def _execute_create_update(self, template, mappings, arguments, callback):
|
||||
with open('data/templates/cf/%s.template' % template) as template_file:
|
||||
template_data = template_file.read()
|
||||
|
||||
template_data = conductor.helpers.transform_json(
|
||||
anyjson.loads(template_data), mappings)
|
||||
|
||||
self._update_pending_list.append({
|
||||
'template': template_data,
|
||||
'arguments': arguments,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def _execute_delete(self, callback):
|
||||
self._delete_pending_list.append({
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def has_pending_commands(self):
|
||||
return len(self._update_pending_list) + \
|
||||
len(self._delete_pending_list) > 0
|
||||
|
||||
def execute_pending(self):
|
||||
r1 = self._execute_pending_updates()
|
||||
r2 = self._execute_pending_deletes()
|
||||
return r1 or r2
|
||||
|
||||
def _execute_pending_updates(self):
|
||||
if not len(self._update_pending_list):
|
||||
return False
|
||||
|
||||
template, arguments = self._get_current_template()
|
||||
stack_exists = (template != {})
|
||||
|
||||
for t in self._update_pending_list:
|
||||
template = conductor.helpers.merge_dicts(
|
||||
template, t['template'], max_levels=2)
|
||||
arguments = conductor.helpers.merge_dicts(
|
||||
arguments, t['arguments'], max_levels=1)
|
||||
|
||||
log.info(
|
||||
'Executing heat template {0} with arguments {1} on stack {2}'
|
||||
.format(anyjson.dumps(template), arguments, self._stack))
|
||||
|
||||
if stack_exists:
|
||||
self._heat_client.stacks.update(
|
||||
stack_id=self._stack,
|
||||
parameters=arguments,
|
||||
template=template)
|
||||
log.debug(
|
||||
'Waiting for the stack {0} to be update'.format(self._stack))
|
||||
self._wait_state('UPDATE_COMPLETE')
|
||||
log.info('Stack {0} updated'.format(self._stack))
|
||||
else:
|
||||
self._heat_client.stacks.create(
|
||||
stack_name=self._stack,
|
||||
parameters=arguments,
|
||||
template=template)
|
||||
log.debug('Waiting for the stack {0} to be create'.format(
|
||||
self._stack))
|
||||
self._wait_state('CREATE_COMPLETE')
|
||||
log.info('Stack {0} created'.format(self._stack))
|
||||
|
||||
pending_list = self._update_pending_list
|
||||
self._update_pending_list = []
|
||||
|
||||
for item in pending_list:
|
||||
item['callback'](True)
|
||||
|
||||
return True
|
||||
|
||||
def _execute_pending_deletes(self):
|
||||
if not len(self._delete_pending_list):
|
||||
return False
|
||||
|
||||
log.debug('Deleting stack {0}'.format(self._stack))
|
||||
try:
|
||||
self._heat_client.stacks.delete(
|
||||
stack_id=self._stack)
|
||||
log.debug(
|
||||
'Waiting for the stack {0} to be deleted'.format(self._stack))
|
||||
self._wait_state(['DELETE_COMPLETE', ''])
|
||||
log.info('Stack {0} deleted'.format(self._stack))
|
||||
except Exception as ex:
|
||||
log.exception(ex)
|
||||
|
||||
pending_list = self._delete_pending_list
|
||||
self._delete_pending_list = []
|
||||
|
||||
for item in pending_list:
|
||||
item['callback'](True)
|
||||
return True
|
||||
|
||||
def _get_current_template(self):
|
||||
try:
|
||||
stack_info = self._heat_client.stacks.get(stack_id=self._stack)
|
||||
template = self._heat_client.stacks.template(
|
||||
stack_id='{0}/{1}'.format(stack_info.stack_name, stack_info.id))
|
||||
return template, stack_info.parameters
|
||||
except heatclient.exc.HTTPNotFound:
|
||||
return {}, {}
|
||||
|
||||
def _wait_state(self, state):
|
||||
if isinstance(state, types.ListType):
|
||||
states = state
|
||||
else:
|
||||
states = [state]
|
||||
|
||||
while True:
|
||||
try:
|
||||
status = self._heat_client.stacks.get(
|
||||
stack_id=self._stack).stack_status
|
||||
except heatclient.exc.HTTPNotFound:
|
||||
status = ''
|
||||
|
||||
if 'IN_PROGRESS' in status:
|
||||
eventlet.sleep(1)
|
||||
continue
|
||||
if status not in states:
|
||||
raise EnvironmentError()
|
||||
return
|
||||
|
@ -2,7 +2,7 @@ class CommandBase(object):
|
||||
def execute(self, **kwargs):
|
||||
pass
|
||||
|
||||
def execute_pending(self, callback):
|
||||
def execute_pending(self):
|
||||
return False
|
||||
|
||||
def has_pending_commands(self):
|
||||
|
@ -1,44 +1,33 @@
|
||||
import command
|
||||
import cloud_formation
|
||||
import windows_agent
|
||||
|
||||
|
||||
class CommandDispatcher(command.CommandBase):
|
||||
def __init__(self, environment_name, rmqclient):
|
||||
self._command_map = {
|
||||
'cf': cloud_formation.HeatExecutor(environment_name),
|
||||
'agent': windows_agent.WindowsAgentExecutor(
|
||||
environment_name, rmqclient)
|
||||
}
|
||||
|
||||
def execute(self, name, **kwargs):
|
||||
self._command_map[name].execute(**kwargs)
|
||||
|
||||
def execute_pending(self, callback):
|
||||
result = 0
|
||||
count = [0]
|
||||
|
||||
def on_result():
|
||||
count[0] -= 1
|
||||
if not count[0]:
|
||||
callback()
|
||||
|
||||
for command in self._command_map.values():
|
||||
count[0] += 1
|
||||
result += 1
|
||||
if not command.execute_pending(on_result):
|
||||
count[0] -= 1
|
||||
result -= 1
|
||||
|
||||
return result > 0
|
||||
|
||||
def has_pending_commands(self):
|
||||
result = False
|
||||
for command in self._command_map.values():
|
||||
result |= command.has_pending_commands()
|
||||
|
||||
return result
|
||||
|
||||
def close(self):
|
||||
for t in self._command_map.values():
|
||||
t.close()
|
||||
import command
|
||||
import cloud_formation
|
||||
import windows_agent
|
||||
|
||||
|
||||
class CommandDispatcher(command.CommandBase):
|
||||
def __init__(self, environment_id, rmqclient, token, tenant_id):
|
||||
self._command_map = {
|
||||
'cf': cloud_formation.HeatExecutor(environment_id, token, tenant_id),
|
||||
'agent': windows_agent.WindowsAgentExecutor(
|
||||
environment_id, rmqclient)
|
||||
}
|
||||
|
||||
def execute(self, name, **kwargs):
|
||||
self._command_map[name].execute(**kwargs)
|
||||
|
||||
def execute_pending(self):
|
||||
result = False
|
||||
for command in self._command_map.values():
|
||||
result |= command.execute_pending()
|
||||
|
||||
return result
|
||||
|
||||
def has_pending_commands(self):
|
||||
result = False
|
||||
for command in self._command_map.values():
|
||||
result |= command.has_pending_commands()
|
||||
|
||||
return result
|
||||
|
||||
def close(self):
|
||||
for t in self._command_map.values():
|
||||
t.close()
|
||||
|
@ -1,65 +1,61 @@
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.rabbitmq import Message
|
||||
import conductor.helpers
|
||||
from command import CommandBase
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WindowsAgentExecutor(CommandBase):
|
||||
def __init__(self, stack, rmqclient):
|
||||
self._stack = stack
|
||||
self._rmqclient = rmqclient
|
||||
self._callback = None
|
||||
self._pending_list = []
|
||||
self._current_pending_list = []
|
||||
rmqclient.subscribe('-execution-results', self._on_message)
|
||||
self._results_queue = '-execution-results-%s' % str(stack).lower()
|
||||
rmqclient.declare(self._results_queue)
|
||||
|
||||
def execute(self, template, mappings, host, callback):
|
||||
with open('data/templates/agent/%s.template' %
|
||||
template) as template_file:
|
||||
template_data = template_file.read()
|
||||
def execute(self, template, mappings, host, service, callback):
|
||||
with open('data/templates/agent/%s.template' % template) as file:
|
||||
template_data = file.read()
|
||||
|
||||
template_data = json.dumps(conductor.helpers.transform_json(
|
||||
json.loads(template_data), mappings))
|
||||
template_data = conductor.helpers.transform_json(
|
||||
json.loads(template_data), mappings)
|
||||
|
||||
id = str(uuid.uuid4()).lower()
|
||||
host = ('%s-%s-%s' % (self._stack, service, host)).lower()
|
||||
self._pending_list.append({
|
||||
'id': str(uuid.uuid4()).lower(),
|
||||
'template': template_data,
|
||||
'host': ('%s-%s' % (self._stack, host)).lower().replace(' ', '-'),
|
||||
'id': id,
|
||||
'callback': callback
|
||||
})
|
||||
|
||||
def _on_message(self, body, message_id, **kwargs):
|
||||
msg_id = message_id.lower()
|
||||
item, index = conductor.helpers.find(lambda t: t['id'] == msg_id,
|
||||
self._current_pending_list)
|
||||
if item:
|
||||
self._current_pending_list.pop(index)
|
||||
item['callback'](json.loads(body))
|
||||
if self._callback and not self._current_pending_list:
|
||||
cb = self._callback
|
||||
self._callback = None
|
||||
cb()
|
||||
msg = Message()
|
||||
msg.body = template_data
|
||||
msg.id = id
|
||||
self._rmqclient.declare(host)
|
||||
self._rmqclient.send(message=msg, key=host)
|
||||
log.info('Sending RMQ message {0} to {1} with id {2}'.format(
|
||||
template_data, host, id))
|
||||
|
||||
def has_pending_commands(self):
|
||||
return len(self._pending_list) > 0
|
||||
|
||||
def execute_pending(self, callback):
|
||||
def execute_pending(self):
|
||||
if not self.has_pending_commands():
|
||||
return False
|
||||
|
||||
self._current_pending_list = self._pending_list
|
||||
self._pending_list = []
|
||||
|
||||
self._callback = callback
|
||||
|
||||
for rec in self._current_pending_list:
|
||||
self._rmqclient.send(
|
||||
queue=rec['host'], data=rec['template'], message_id=rec['id'])
|
||||
print 'Sending RMQ message %s to %s' % (
|
||||
rec['template'], rec['host'])
|
||||
with self._rmqclient.open(self._results_queue) as subscription:
|
||||
while self.has_pending_commands():
|
||||
msg = subscription.get_message()
|
||||
msg_id = msg.id.lower()
|
||||
item, index = conductor.helpers.find(
|
||||
lambda t: t['id'] == msg_id, self._pending_list)
|
||||
if item:
|
||||
self._pending_list.pop(index)
|
||||
item['callback'](msg.body)
|
||||
|
||||
return True
|
||||
|
||||
def close(self):
|
||||
self._rmqclient.unsubscribe('-execution-results')
|
||||
|
||||
|
@ -1,19 +1,213 @@
|
||||
from ConfigParser import SafeConfigParser
|
||||
|
||||
|
||||
class Config(object):
|
||||
CONFIG_PATH = './etc/app.config'
|
||||
|
||||
def __init__(self, filename=None):
|
||||
self.config = SafeConfigParser()
|
||||
self.config.read(filename or self.CONFIG_PATH)
|
||||
|
||||
def get_setting(self, section, name, default=None):
|
||||
if not self.config.has_option(section, name):
|
||||
return default
|
||||
return self.config.get(section, name)
|
||||
|
||||
def __getitem__(self, item):
|
||||
parts = item.rsplit('.', 1)
|
||||
return self.get_setting(
|
||||
parts[0] if len(parts) == 2 else 'DEFAULT', parts[-1])
|
||||
#!/usr/bin/env python
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Routines for configuring Glance
|
||||
"""
|
||||
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import sys
|
||||
|
||||
from oslo.config import cfg
|
||||
from paste import deploy
|
||||
|
||||
from conductor.version import version_info as version
|
||||
from ConfigParser import SafeConfigParser
|
||||
|
||||
paste_deploy_opts = [
|
||||
cfg.StrOpt('flavor'),
|
||||
cfg.StrOpt('config_file'),
|
||||
]
|
||||
|
||||
rabbit_opts = [
|
||||
cfg.StrOpt('host', default='localhost'),
|
||||
cfg.IntOpt('port', default=5672),
|
||||
cfg.StrOpt('login', default='guest'),
|
||||
cfg.StrOpt('password', default='guest'),
|
||||
cfg.StrOpt('virtual_host', default='/'),
|
||||
]
|
||||
|
||||
heat_opts = [
|
||||
cfg.StrOpt('auth_url'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(paste_deploy_opts, group='paste_deploy')
|
||||
CONF.register_opts(rabbit_opts, group='rabbitmq')
|
||||
CONF.register_opts(heat_opts, group='heat')
|
||||
|
||||
|
||||
CONF.import_opt('verbose', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('debug', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('log_dir', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('log_file', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('log_config', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('log_format', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('log_date_format', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('use_syslog', 'conductor.openstack.common.log')
|
||||
CONF.import_opt('syslog_log_facility', 'conductor.openstack.common.log')
|
||||
|
||||
|
||||
def parse_args(args=None, usage=None, default_config_files=None):
|
||||
CONF(args=args,
|
||||
project='conductor',
|
||||
version=version.cached_version_string(),
|
||||
usage=usage,
|
||||
default_config_files=default_config_files)
|
||||
|
||||
|
||||
def setup_logging():
|
||||
"""
|
||||
Sets up the logging options for a log with supplied name
|
||||
"""
|
||||
|
||||
if CONF.log_config:
|
||||
# Use a logging configuration file for all settings...
|
||||
if os.path.exists(CONF.log_config):
|
||||
logging.config.fileConfig(CONF.log_config)
|
||||
return
|
||||
else:
|
||||
raise RuntimeError("Unable to locate specified logging "
|
||||
"config file: %s" % CONF.log_config)
|
||||
|
||||
root_logger = logging.root
|
||||
if CONF.debug:
|
||||
root_logger.setLevel(logging.DEBUG)
|
||||
elif CONF.verbose:
|
||||
root_logger.setLevel(logging.INFO)
|
||||
else:
|
||||
root_logger.setLevel(logging.WARNING)
|
||||
|
||||
formatter = logging.Formatter(CONF.log_format, CONF.log_date_format)
|
||||
|
||||
if CONF.use_syslog:
|
||||
try:
|
||||
facility = getattr(logging.handlers.SysLogHandler,
|
||||
CONF.syslog_log_facility)
|
||||
except AttributeError:
|
||||
raise ValueError(_("Invalid syslog facility"))
|
||||
|
||||
handler = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=facility)
|
||||
elif CONF.log_file:
|
||||
logfile = CONF.log_file
|
||||
if CONF.log_dir:
|
||||
logfile = os.path.join(CONF.log_dir, logfile)
|
||||
handler = logging.handlers.WatchedFileHandler(logfile)
|
||||
else:
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
|
||||
handler.setFormatter(formatter)
|
||||
root_logger.addHandler(handler)
|
||||
|
||||
|
||||
def _get_deployment_flavor():
|
||||
"""
|
||||
Retrieve the paste_deploy.flavor config item, formatted appropriately
|
||||
for appending to the application name.
|
||||
"""
|
||||
flavor = CONF.paste_deploy.flavor
|
||||
return '' if not flavor else ('-' + flavor)
|
||||
|
||||
|
||||
def _get_paste_config_path():
|
||||
paste_suffix = '-paste.ini'
|
||||
conf_suffix = '.conf'
|
||||
if CONF.config_file:
|
||||
# Assume paste config is in a paste.ini file corresponding
|
||||
# to the last config file
|
||||
path = CONF.config_file[-1].replace(conf_suffix, paste_suffix)
|
||||
else:
|
||||
path = CONF.prog + '-paste.ini'
|
||||
return CONF.find_file(os.path.basename(path))
|
||||
|
||||
|
||||
def _get_deployment_config_file():
|
||||
"""
|
||||
Retrieve the deployment_config_file config item, formatted as an
|
||||
absolute pathname.
|
||||
"""
|
||||
path = CONF.paste_deploy.config_file
|
||||
if not path:
|
||||
path = _get_paste_config_path()
|
||||
if not path:
|
||||
msg = "Unable to locate paste config file for %s." % CONF.prog
|
||||
raise RuntimeError(msg)
|
||||
return os.path.abspath(path)
|
||||
|
||||
|
||||
def load_paste_app(app_name=None):
|
||||
"""
|
||||
Builds and returns a WSGI app from a paste config file.
|
||||
|
||||
We assume the last config file specified in the supplied ConfigOpts
|
||||
object is the paste config file.
|
||||
|
||||
:param app_name: name of the application to load
|
||||
|
||||
:raises RuntimeError when config file cannot be located or application
|
||||
cannot be loaded from config file
|
||||
"""
|
||||
if app_name is None:
|
||||
app_name = CONF.prog
|
||||
|
||||
# append the deployment flavor to the application name,
|
||||
# in order to identify the appropriate paste pipeline
|
||||
app_name += _get_deployment_flavor()
|
||||
|
||||
conf_file = _get_deployment_config_file()
|
||||
|
||||
try:
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.debug(_("Loading %(app_name)s from %(conf_file)s"),
|
||||
{'conf_file': conf_file, 'app_name': app_name})
|
||||
|
||||
app = deploy.loadapp("config:%s" % conf_file, name=app_name)
|
||||
|
||||
# Log the options used when starting if we're in debug mode...
|
||||
if CONF.debug:
|
||||
CONF.log_opt_values(logger, logging.DEBUG)
|
||||
|
||||
return app
|
||||
except (LookupError, ImportError), e:
|
||||
msg = _("Unable to load %(app_name)s from "
|
||||
"configuration file %(conf_file)s."
|
||||
"\nGot: %(e)r") % locals()
|
||||
logger.error(msg)
|
||||
raise RuntimeError(msg)
|
||||
|
||||
|
||||
class Config(object):
|
||||
CONFIG_PATH = './etc/app.config'
|
||||
|
||||
def __init__(self, filename=None):
|
||||
self.config = SafeConfigParser()
|
||||
self.config.read(filename or self.CONFIG_PATH)
|
||||
|
||||
def get_setting(self, section, name, default=None):
|
||||
if not self.config.has_option(section, name):
|
||||
return default
|
||||
return self.config.get(section, name)
|
||||
|
||||
def __getitem__(self, item):
|
||||
parts = item.rsplit('.', 1)
|
||||
return self.get_setting(
|
||||
parts[0] if len(parts) == 2 else 'DEFAULT', parts[-1])
|
||||
|
87
conductor/conductor/openstack/common/eventlet_backdoor.py
Normal file
87
conductor/conductor/openstack/common/eventlet_backdoor.py
Normal file
@ -0,0 +1,87 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright (c) 2012 OpenStack Foundation.
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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 gc
|
||||
import pprint
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import eventlet
|
||||
import eventlet.backdoor
|
||||
import greenlet
|
||||
from oslo.config import cfg
|
||||
|
||||
eventlet_backdoor_opts = [
|
||||
cfg.IntOpt('backdoor_port',
|
||||
default=None,
|
||||
help='port for eventlet backdoor to listen')
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(eventlet_backdoor_opts)
|
||||
|
||||
|
||||
def _dont_use_this():
|
||||
print "Don't use this, just disconnect instead"
|
||||
|
||||
|
||||
def _find_objects(t):
|
||||
return filter(lambda o: isinstance(o, t), gc.get_objects())
|
||||
|
||||
|
||||
def _print_greenthreads():
|
||||
for i, gt in enumerate(_find_objects(greenlet.greenlet)):
|
||||
print i, gt
|
||||
traceback.print_stack(gt.gr_frame)
|
||||
print
|
||||
|
||||
|
||||
def _print_nativethreads():
|
||||
for threadId, stack in sys._current_frames().items():
|
||||
print threadId
|
||||
traceback.print_stack(stack)
|
||||
print
|
||||
|
||||
|
||||
def initialize_if_enabled():
|
||||
backdoor_locals = {
|
||||
'exit': _dont_use_this, # So we don't exit the entire process
|
||||
'quit': _dont_use_this, # So we don't exit the entire process
|
||||
'fo': _find_objects,
|
||||
'pgt': _print_greenthreads,
|
||||
'pnt': _print_nativethreads,
|
||||
}
|
||||
|
||||
if CONF.backdoor_port is None:
|
||||
return None
|
||||
|
||||
# NOTE(johannes): The standard sys.displayhook will print the value of
|
||||
# the last expression and set it to __builtin__._, which overwrites
|
||||
# the __builtin__._ that gettext sets. Let's switch to using pprint
|
||||
# since it won't interact poorly with gettext, and it's easier to
|
||||
# read the output too.
|
||||
def displayhook(val):
|
||||
if val is not None:
|
||||
pprint.pprint(val)
|
||||
sys.displayhook = displayhook
|
||||
|
||||
sock = eventlet.listen(('localhost', CONF.backdoor_port))
|
||||
port = sock.getsockname()[1]
|
||||
eventlet.spawn_n(eventlet.backdoor.backdoor_server, sock,
|
||||
locals=backdoor_locals)
|
||||
return port
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -21,17 +21,9 @@ Exceptions common to OpenStack projects
|
||||
|
||||
import logging
|
||||
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
|
||||
class ProcessExecutionError(IOError):
|
||||
def __init__(self, stdout=None, stderr=None, exit_code=None, cmd=None,
|
||||
description=None):
|
||||
if description is None:
|
||||
description = "Unexpected error while running command."
|
||||
if exit_code is None:
|
||||
exit_code = '-'
|
||||
message = "%s\nCommand: %s\nExit code: %s\nStdout: %r\nStderr: %r" % (
|
||||
description, cmd, exit_code, stdout, stderr)
|
||||
IOError.__init__(self, message)
|
||||
_FATAL_EXCEPTION_FORMAT_ERRORS = False
|
||||
|
||||
|
||||
class Error(Exception):
|
||||
@ -109,7 +101,7 @@ def wrap_exception(f):
|
||||
except Exception, e:
|
||||
if not isinstance(e, Error):
|
||||
#exc_type, exc_value, exc_traceback = sys.exc_info()
|
||||
logging.exception('Uncaught exception')
|
||||
logging.exception(_('Uncaught exception'))
|
||||
#logging.error(traceback.extract_stack(exc_traceback))
|
||||
raise Error(str(e))
|
||||
raise
|
||||
@ -131,9 +123,12 @@ class OpenstackException(Exception):
|
||||
try:
|
||||
self._error_string = self.message % kwargs
|
||||
|
||||
except Exception:
|
||||
# at least get the core message out if something happened
|
||||
self._error_string = self.message
|
||||
except Exception as e:
|
||||
if _FATAL_EXCEPTION_FORMAT_ERRORS:
|
||||
raise e
|
||||
else:
|
||||
# at least get the core message out if something happened
|
||||
self._error_string = self.message
|
||||
|
||||
def __str__(self):
|
||||
return self._error_string
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -15,10 +15,19 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# This ensures the openstack namespace is defined
|
||||
try:
|
||||
import pkg_resources
|
||||
pkg_resources.declare_namespace(__name__)
|
||||
except ImportError:
|
||||
import pkgutil
|
||||
__path__ = pkgutil.extend_path(__path__, __name__)
|
||||
"""
|
||||
gettext for openstack-common modules.
|
||||
|
||||
Usual usage in an openstack.common module:
|
||||
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
"""
|
||||
|
||||
import gettext
|
||||
|
||||
|
||||
t = gettext.translation('conductor', 'locale', fallback=True)
|
||||
|
||||
|
||||
def _(msg):
|
||||
return t.ugettext(msg)
|
67
conductor/conductor/openstack/common/importutils.py
Normal file
67
conductor/conductor/openstack/common/importutils.py
Normal file
@ -0,0 +1,67 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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 related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class"""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
try:
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
except (ValueError, AttributeError):
|
||||
raise ImportError('Class %s cannot be found (%s)' %
|
||||
(class_str,
|
||||
traceback.format_exception(*sys.exc_info())))
|
||||
|
||||
|
||||
def import_object(import_str, *args, **kwargs):
|
||||
"""Import a class and return an instance of it."""
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_object_ns(name_space, import_str, *args, **kwargs):
|
||||
"""
|
||||
Import a class and return an instance of it, first by trying
|
||||
to find the class in a default namespace, then failing back to
|
||||
a full path if not found in the default namespace.
|
||||
"""
|
||||
import_value = "%s.%s" % (name_space, import_str)
|
||||
try:
|
||||
return import_class(import_value)(*args, **kwargs)
|
||||
except ImportError:
|
||||
return import_class(import_str)(*args, **kwargs)
|
||||
|
||||
|
||||
def import_module(import_str):
|
||||
"""Import a module."""
|
||||
__import__(import_str)
|
||||
return sys.modules[import_str]
|
||||
|
||||
|
||||
def try_import(import_str, default=None):
|
||||
"""Try to import a module and if it fails return default."""
|
||||
try:
|
||||
return import_module(import_str)
|
||||
except ImportError:
|
||||
return default
|
141
conductor/conductor/openstack/common/jsonutils.py
Normal file
141
conductor/conductor/openstack/common/jsonutils.py
Normal file
@ -0,0 +1,141 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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.
|
||||
|
||||
'''
|
||||
JSON related utilities.
|
||||
|
||||
This module provides a few things:
|
||||
|
||||
1) A handy function for getting an object down to something that can be
|
||||
JSON serialized. See to_primitive().
|
||||
|
||||
2) Wrappers around loads() and dumps(). The dumps() wrapper will
|
||||
automatically use to_primitive() for you if needed.
|
||||
|
||||
3) This sets up anyjson to use the loads() and dumps() wrappers if anyjson
|
||||
is available.
|
||||
'''
|
||||
|
||||
|
||||
import datetime
|
||||
import functools
|
||||
import inspect
|
||||
import itertools
|
||||
import json
|
||||
import xmlrpclib
|
||||
|
||||
from conductor.openstack.common import timeutils
|
||||
|
||||
|
||||
def to_primitive(value, convert_instances=False, convert_datetime=True,
|
||||
level=0, max_depth=3):
|
||||
"""Convert a complex object into primitives.
|
||||
|
||||
Handy for JSON serialization. We can optionally handle instances,
|
||||
but since this is a recursive function, we could have cyclical
|
||||
data structures.
|
||||
|
||||
To handle cyclical data structures we could track the actual objects
|
||||
visited in a set, but not all objects are hashable. Instead we just
|
||||
track the depth of the object inspections and don't go too deep.
|
||||
|
||||
Therefore, convert_instances=True is lossy ... be aware.
|
||||
|
||||
"""
|
||||
nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod,
|
||||
inspect.isfunction, inspect.isgeneratorfunction,
|
||||
inspect.isgenerator, inspect.istraceback, inspect.isframe,
|
||||
inspect.iscode, inspect.isbuiltin, inspect.isroutine,
|
||||
inspect.isabstract]
|
||||
for test in nasty:
|
||||
if test(value):
|
||||
return unicode(value)
|
||||
|
||||
# value of itertools.count doesn't get caught by inspects
|
||||
# above and results in infinite loop when list(value) is called.
|
||||
if type(value) == itertools.count:
|
||||
return unicode(value)
|
||||
|
||||
# FIXME(vish): Workaround for LP bug 852095. Without this workaround,
|
||||
# tests that raise an exception in a mocked method that
|
||||
# has a @wrap_exception with a notifier will fail. If
|
||||
# we up the dependency to 0.5.4 (when it is released) we
|
||||
# can remove this workaround.
|
||||
if getattr(value, '__module__', None) == 'mox':
|
||||
return 'mock'
|
||||
|
||||
if level > max_depth:
|
||||
return '?'
|
||||
|
||||
# The try block may not be necessary after the class check above,
|
||||
# but just in case ...
|
||||
try:
|
||||
recursive = functools.partial(to_primitive,
|
||||
convert_instances=convert_instances,
|
||||
convert_datetime=convert_datetime,
|
||||
level=level,
|
||||
max_depth=max_depth)
|
||||
# It's not clear why xmlrpclib created their own DateTime type, but
|
||||
# for our purposes, make it a datetime type which is explicitly
|
||||
# handled
|
||||
if isinstance(value, xmlrpclib.DateTime):
|
||||
value = datetime.datetime(*tuple(value.timetuple())[:6])
|
||||
|
||||
if isinstance(value, (list, tuple)):
|
||||
return [recursive(v) for v in value]
|
||||
elif isinstance(value, dict):
|
||||
return dict((k, recursive(v)) for k, v in value.iteritems())
|
||||
elif convert_datetime and isinstance(value, datetime.datetime):
|
||||
return timeutils.strtime(value)
|
||||
elif hasattr(value, 'iteritems'):
|
||||
return recursive(dict(value.iteritems()), level=level + 1)
|
||||
elif hasattr(value, '__iter__'):
|
||||
return recursive(list(value))
|
||||
elif convert_instances and hasattr(value, '__dict__'):
|
||||
# Likely an instance of something. Watch for cycles.
|
||||
# Ignore class member vars.
|
||||
return recursive(value.__dict__, level=level + 1)
|
||||
else:
|
||||
return value
|
||||
except TypeError:
|
||||
# Class objects are tricky since they may define something like
|
||||
# __iter__ defined but it isn't callable as list().
|
||||
return unicode(value)
|
||||
|
||||
|
||||
def dumps(value, default=to_primitive, **kwargs):
|
||||
return json.dumps(value, default=default, **kwargs)
|
||||
|
||||
|
||||
def loads(s):
|
||||
return json.loads(s)
|
||||
|
||||
|
||||
def load(s):
|
||||
return json.load(s)
|
||||
|
||||
|
||||
try:
|
||||
import anyjson
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
anyjson._modules.append((__name__, 'dumps', TypeError,
|
||||
'loads', ValueError, 'load'))
|
||||
anyjson.force_implementation(__name__)
|
48
conductor/conductor/openstack/common/local.py
Normal file
48
conductor/conductor/openstack/common/local.py
Normal file
@ -0,0 +1,48 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
"""Greenthread local storage of variables using weak references"""
|
||||
|
||||
import weakref
|
||||
|
||||
from eventlet import corolocal
|
||||
|
||||
|
||||
class WeakLocal(corolocal.local):
|
||||
def __getattribute__(self, attr):
|
||||
rval = corolocal.local.__getattribute__(self, attr)
|
||||
if rval:
|
||||
# NOTE(mikal): this bit is confusing. What is stored is a weak
|
||||
# reference, not the value itself. We therefore need to lookup
|
||||
# the weak reference and return the inner value here.
|
||||
rval = rval()
|
||||
return rval
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
value = weakref.ref(value)
|
||||
return corolocal.local.__setattr__(self, attr, value)
|
||||
|
||||
|
||||
# NOTE(mikal): the name "store" should be deprecated in the future
|
||||
store = WeakLocal()
|
||||
|
||||
# A "weak" store uses weak references and allows an object to fall out of scope
|
||||
# when it falls out of scope in the code that uses the thread local storage. A
|
||||
# "strong" store will hold a reference to the object so that it never falls out
|
||||
# of scope.
|
||||
weak_store = WeakLocal()
|
||||
strong_store = corolocal.local
|
543
conductor/conductor/openstack/common/log.py
Normal file
543
conductor/conductor/openstack/common/log.py
Normal file
@ -0,0 +1,543 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# 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.
|
||||
|
||||
"""Openstack logging handler.
|
||||
|
||||
This module adds to logging functionality by adding the option to specify
|
||||
a context object when calling the various log methods. If the context object
|
||||
is not specified, default formatting is used. Additionally, an instance uuid
|
||||
may be passed as part of the log message, which is intended to make it easier
|
||||
for admins to find messages related to a specific instance.
|
||||
|
||||
It also allows setting of formatting information through conf.
|
||||
|
||||
"""
|
||||
|
||||
import ConfigParser
|
||||
import cStringIO
|
||||
import inspect
|
||||
import itertools
|
||||
import logging
|
||||
import logging.config
|
||||
import logging.handlers
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import jsonutils
|
||||
from conductor.openstack.common import local
|
||||
from conductor.openstack.common import notifier
|
||||
|
||||
|
||||
_DEFAULT_LOG_FORMAT = "%(asctime)s %(levelname)8s [%(name)s] %(message)s"
|
||||
_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||||
|
||||
common_cli_opts = [
|
||||
cfg.BoolOpt('debug',
|
||||
short='d',
|
||||
default=False,
|
||||
help='Print debugging output (set logging level to '
|
||||
'DEBUG instead of default WARNING level).'),
|
||||
cfg.BoolOpt('verbose',
|
||||
short='v',
|
||||
default=False,
|
||||
help='Print more verbose output (set logging level to '
|
||||
'INFO instead of default WARNING level).'),
|
||||
]
|
||||
|
||||
logging_cli_opts = [
|
||||
cfg.StrOpt('log-config',
|
||||
metavar='PATH',
|
||||
help='If this option is specified, the logging configuration '
|
||||
'file specified is used and overrides any other logging '
|
||||
'options specified. Please see the Python logging module '
|
||||
'documentation for details on logging configuration '
|
||||
'files.'),
|
||||
cfg.StrOpt('log-format',
|
||||
default=_DEFAULT_LOG_FORMAT,
|
||||
metavar='FORMAT',
|
||||
help='A logging.Formatter log message format string which may '
|
||||
'use any of the available logging.LogRecord attributes. '
|
||||
'Default: %(default)s'),
|
||||
cfg.StrOpt('log-date-format',
|
||||
default=_DEFAULT_LOG_DATE_FORMAT,
|
||||
metavar='DATE_FORMAT',
|
||||
help='Format string for %%(asctime)s in log records. '
|
||||
'Default: %(default)s'),
|
||||
cfg.StrOpt('log-file',
|
||||
metavar='PATH',
|
||||
deprecated_name='logfile',
|
||||
help='(Optional) Name of log file to output to. '
|
||||
'If no default is set, logging will go to stdout.'),
|
||||
cfg.StrOpt('log-dir',
|
||||
deprecated_name='logdir',
|
||||
help='(Optional) The base directory used for relative '
|
||||
'--log-file paths'),
|
||||
cfg.BoolOpt('use-syslog',
|
||||
default=False,
|
||||
help='Use syslog for logging.'),
|
||||
cfg.StrOpt('syslog-log-facility',
|
||||
default='LOG_USER',
|
||||
help='syslog facility to receive log lines')
|
||||
]
|
||||
|
||||
generic_log_opts = [
|
||||
cfg.BoolOpt('use_stderr',
|
||||
default=True,
|
||||
help='Log output to standard error'),
|
||||
cfg.StrOpt('logfile_mode',
|
||||
default='0644',
|
||||
help='Default file mode used when creating log files'),
|
||||
]
|
||||
|
||||
log_opts = [
|
||||
cfg.StrOpt('logging_context_format_string',
|
||||
default='%(asctime)s.%(msecs)03d %(levelname)s %(name)s '
|
||||
'[%(request_id)s %(user)s %(tenant)s] %(instance)s'
|
||||
'%(message)s',
|
||||
help='format string to use for log messages with context'),
|
||||
cfg.StrOpt('logging_default_format_string',
|
||||
default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
|
||||
'%(name)s [-] %(instance)s%(message)s',
|
||||
help='format string to use for log messages without context'),
|
||||
cfg.StrOpt('logging_debug_format_suffix',
|
||||
default='%(funcName)s %(pathname)s:%(lineno)d',
|
||||
help='data to append to log format when level is DEBUG'),
|
||||
cfg.StrOpt('logging_exception_prefix',
|
||||
default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
|
||||
'%(instance)s',
|
||||
help='prefix each line of exception output with this format'),
|
||||
cfg.ListOpt('default_log_levels',
|
||||
default=[
|
||||
'amqplib=WARN',
|
||||
'sqlalchemy=WARN',
|
||||
'boto=WARN',
|
||||
'suds=INFO',
|
||||
'keystone=INFO',
|
||||
'eventlet.wsgi.server=WARN'
|
||||
],
|
||||
help='list of logger=LEVEL pairs'),
|
||||
cfg.BoolOpt('publish_errors',
|
||||
default=False,
|
||||
help='publish error events'),
|
||||
cfg.BoolOpt('fatal_deprecations',
|
||||
default=False,
|
||||
help='make deprecations fatal'),
|
||||
|
||||
# NOTE(mikal): there are two options here because sometimes we are handed
|
||||
# a full instance (and could include more information), and other times we
|
||||
# are just handed a UUID for the instance.
|
||||
cfg.StrOpt('instance_format',
|
||||
default='[instance: %(uuid)s] ',
|
||||
help='If an instance is passed with the log message, format '
|
||||
'it like this'),
|
||||
cfg.StrOpt('instance_uuid_format',
|
||||
default='[instance: %(uuid)s] ',
|
||||
help='If an instance UUID is passed with the log message, '
|
||||
'format it like this'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_cli_opts(common_cli_opts)
|
||||
CONF.register_cli_opts(logging_cli_opts)
|
||||
CONF.register_opts(generic_log_opts)
|
||||
CONF.register_opts(log_opts)
|
||||
|
||||
# our new audit level
|
||||
# NOTE(jkoelker) Since we synthesized an audit level, make the logging
|
||||
# module aware of it so it acts like other levels.
|
||||
logging.AUDIT = logging.INFO + 1
|
||||
logging.addLevelName(logging.AUDIT, 'AUDIT')
|
||||
|
||||
|
||||
try:
|
||||
NullHandler = logging.NullHandler
|
||||
except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7
|
||||
class NullHandler(logging.Handler):
|
||||
def handle(self, record):
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
def createLock(self):
|
||||
self.lock = None
|
||||
|
||||
|
||||
def _dictify_context(context):
|
||||
if context is None:
|
||||
return None
|
||||
if not isinstance(context, dict) and getattr(context, 'to_dict', None):
|
||||
context = context.to_dict()
|
||||
return context
|
||||
|
||||
|
||||
def _get_binary_name():
|
||||
return os.path.basename(inspect.stack()[-1][1])
|
||||
|
||||
|
||||
def _get_log_file_path(binary=None):
|
||||
logfile = CONF.log_file
|
||||
logdir = CONF.log_dir
|
||||
|
||||
if logfile and not logdir:
|
||||
return logfile
|
||||
|
||||
if logfile and logdir:
|
||||
return os.path.join(logdir, logfile)
|
||||
|
||||
if logdir:
|
||||
binary = binary or _get_binary_name()
|
||||
return '%s.log' % (os.path.join(logdir, binary),)
|
||||
|
||||
|
||||
class ContextAdapter(logging.LoggerAdapter):
|
||||
warn = logging.LoggerAdapter.warning
|
||||
|
||||
def __init__(self, logger, project_name, version_string):
|
||||
self.logger = logger
|
||||
self.project = project_name
|
||||
self.version = version_string
|
||||
|
||||
def audit(self, msg, *args, **kwargs):
|
||||
self.log(logging.AUDIT, msg, *args, **kwargs)
|
||||
|
||||
def deprecated(self, msg, *args, **kwargs):
|
||||
stdmsg = _("Deprecated: %s") % msg
|
||||
if CONF.fatal_deprecations:
|
||||
self.critical(stdmsg, *args, **kwargs)
|
||||
raise DeprecatedConfig(msg=stdmsg)
|
||||
else:
|
||||
self.warn(stdmsg, *args, **kwargs)
|
||||
|
||||
def process(self, msg, kwargs):
|
||||
if 'extra' not in kwargs:
|
||||
kwargs['extra'] = {}
|
||||
extra = kwargs['extra']
|
||||
|
||||
context = kwargs.pop('context', None)
|
||||
if not context:
|
||||
context = getattr(local.store, 'context', None)
|
||||
if context:
|
||||
extra.update(_dictify_context(context))
|
||||
|
||||
instance = kwargs.pop('instance', None)
|
||||
instance_extra = ''
|
||||
if instance:
|
||||
instance_extra = CONF.instance_format % instance
|
||||
else:
|
||||
instance_uuid = kwargs.pop('instance_uuid', None)
|
||||
if instance_uuid:
|
||||
instance_extra = (CONF.instance_uuid_format
|
||||
% {'uuid': instance_uuid})
|
||||
extra.update({'instance': instance_extra})
|
||||
|
||||
extra.update({"project": self.project})
|
||||
extra.update({"version": self.version})
|
||||
extra['extra'] = extra.copy()
|
||||
return msg, kwargs
|
||||
|
||||
|
||||
class JSONFormatter(logging.Formatter):
|
||||
def __init__(self, fmt=None, datefmt=None):
|
||||
# NOTE(jkoelker) we ignore the fmt argument, but its still there
|
||||
# since logging.config.fileConfig passes it.
|
||||
self.datefmt = datefmt
|
||||
|
||||
def formatException(self, ei, strip_newlines=True):
|
||||
lines = traceback.format_exception(*ei)
|
||||
if strip_newlines:
|
||||
lines = [itertools.ifilter(
|
||||
lambda x: x,
|
||||
line.rstrip().splitlines()) for line in lines]
|
||||
lines = list(itertools.chain(*lines))
|
||||
return lines
|
||||
|
||||
def format(self, record):
|
||||
message = {'message': record.getMessage(),
|
||||
'asctime': self.formatTime(record, self.datefmt),
|
||||
'name': record.name,
|
||||
'msg': record.msg,
|
||||
'args': record.args,
|
||||
'levelname': record.levelname,
|
||||
'levelno': record.levelno,
|
||||
'pathname': record.pathname,
|
||||
'filename': record.filename,
|
||||
'module': record.module,
|
||||
'lineno': record.lineno,
|
||||
'funcname': record.funcName,
|
||||
'created': record.created,
|
||||
'msecs': record.msecs,
|
||||
'relative_created': record.relativeCreated,
|
||||
'thread': record.thread,
|
||||
'thread_name': record.threadName,
|
||||
'process_name': record.processName,
|
||||
'process': record.process,
|
||||
'traceback': None}
|
||||
|
||||
if hasattr(record, 'extra'):
|
||||
message['extra'] = record.extra
|
||||
|
||||
if record.exc_info:
|
||||
message['traceback'] = self.formatException(record.exc_info)
|
||||
|
||||
return jsonutils.dumps(message)
|
||||
|
||||
|
||||
class PublishErrorsHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
if ('conductor.openstack.common.notifier.log_notifier' in
|
||||
CONF.notification_driver):
|
||||
return
|
||||
notifier.api.notify(None, 'error.publisher',
|
||||
'error_notification',
|
||||
notifier.api.ERROR,
|
||||
dict(error=record.msg))
|
||||
|
||||
|
||||
def _create_logging_excepthook(product_name):
|
||||
def logging_excepthook(type, value, tb):
|
||||
extra = {}
|
||||
if CONF.verbose:
|
||||
extra['exc_info'] = (type, value, tb)
|
||||
getLogger(product_name).critical(str(value), **extra)
|
||||
return logging_excepthook
|
||||
|
||||
|
||||
class LogConfigError(Exception):
|
||||
|
||||
message = _('Error loading logging config %(log_config)s: %(err_msg)s')
|
||||
|
||||
def __init__(self, log_config, err_msg):
|
||||
self.log_config = log_config
|
||||
self.err_msg = err_msg
|
||||
|
||||
def __str__(self):
|
||||
return self.message % dict(log_config=self.log_config,
|
||||
err_msg=self.err_msg)
|
||||
|
||||
|
||||
def _load_log_config(log_config):
|
||||
try:
|
||||
logging.config.fileConfig(log_config)
|
||||
except ConfigParser.Error, exc:
|
||||
raise LogConfigError(log_config, str(exc))
|
||||
|
||||
|
||||
def setup(product_name):
|
||||
"""Setup logging."""
|
||||
if CONF.log_config:
|
||||
_load_log_config(CONF.log_config)
|
||||
else:
|
||||
_setup_logging_from_conf()
|
||||
sys.excepthook = _create_logging_excepthook(product_name)
|
||||
|
||||
|
||||
def set_defaults(logging_context_format_string):
|
||||
cfg.set_defaults(log_opts,
|
||||
logging_context_format_string=
|
||||
logging_context_format_string)
|
||||
|
||||
|
||||
def _find_facility_from_conf():
|
||||
facility_names = logging.handlers.SysLogHandler.facility_names
|
||||
facility = getattr(logging.handlers.SysLogHandler,
|
||||
CONF.syslog_log_facility,
|
||||
None)
|
||||
|
||||
if facility is None and CONF.syslog_log_facility in facility_names:
|
||||
facility = facility_names.get(CONF.syslog_log_facility)
|
||||
|
||||
if facility is None:
|
||||
valid_facilities = facility_names.keys()
|
||||
consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
|
||||
'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
|
||||
'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
|
||||
'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
|
||||
'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
|
||||
valid_facilities.extend(consts)
|
||||
raise TypeError(_('syslog facility must be one of: %s') %
|
||||
', '.join("'%s'" % fac
|
||||
for fac in valid_facilities))
|
||||
|
||||
return facility
|
||||
|
||||
|
||||
def _setup_logging_from_conf():
|
||||
log_root = getLogger(None).logger
|
||||
for handler in log_root.handlers:
|
||||
log_root.removeHandler(handler)
|
||||
|
||||
if CONF.use_syslog:
|
||||
facility = _find_facility_from_conf()
|
||||
syslog = logging.handlers.SysLogHandler(address='/dev/log',
|
||||
facility=facility)
|
||||
log_root.addHandler(syslog)
|
||||
|
||||
logpath = _get_log_file_path()
|
||||
if logpath:
|
||||
filelog = logging.handlers.WatchedFileHandler(logpath)
|
||||
log_root.addHandler(filelog)
|
||||
|
||||
mode = int(CONF.logfile_mode, 8)
|
||||
st = os.stat(logpath)
|
||||
if st.st_mode != (stat.S_IFREG | mode):
|
||||
os.chmod(logpath, mode)
|
||||
|
||||
if CONF.use_stderr:
|
||||
streamlog = ColorHandler()
|
||||
log_root.addHandler(streamlog)
|
||||
|
||||
elif not CONF.log_file:
|
||||
# pass sys.stdout as a positional argument
|
||||
# python2.6 calls the argument strm, in 2.7 it's stream
|
||||
streamlog = logging.StreamHandler(sys.stdout)
|
||||
log_root.addHandler(streamlog)
|
||||
|
||||
if CONF.publish_errors:
|
||||
log_root.addHandler(PublishErrorsHandler(logging.ERROR))
|
||||
|
||||
for handler in log_root.handlers:
|
||||
datefmt = CONF.log_date_format
|
||||
if CONF.log_format:
|
||||
handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
|
||||
datefmt=datefmt))
|
||||
else:
|
||||
handler.setFormatter(LegacyFormatter(datefmt=datefmt))
|
||||
|
||||
if CONF.debug:
|
||||
log_root.setLevel(logging.DEBUG)
|
||||
elif CONF.verbose:
|
||||
log_root.setLevel(logging.INFO)
|
||||
else:
|
||||
log_root.setLevel(logging.WARNING)
|
||||
|
||||
level = logging.NOTSET
|
||||
for pair in CONF.default_log_levels:
|
||||
mod, _sep, level_name = pair.partition('=')
|
||||
level = logging.getLevelName(level_name)
|
||||
logger = logging.getLogger(mod)
|
||||
logger.setLevel(level)
|
||||
for handler in log_root.handlers:
|
||||
logger.addHandler(handler)
|
||||
|
||||
_loggers = {}
|
||||
|
||||
|
||||
def getLogger(name='unknown', version='unknown'):
|
||||
if name not in _loggers:
|
||||
_loggers[name] = ContextAdapter(logging.getLogger(name),
|
||||
name,
|
||||
version)
|
||||
return _loggers[name]
|
||||
|
||||
|
||||
class WritableLogger(object):
|
||||
"""A thin wrapper that responds to `write` and logs."""
|
||||
|
||||
def __init__(self, logger, level=logging.INFO):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
|
||||
def write(self, msg):
|
||||
self.logger.log(self.level, msg)
|
||||
|
||||
|
||||
class LegacyFormatter(logging.Formatter):
|
||||
"""A context.RequestContext aware formatter configured through flags.
|
||||
|
||||
The flags used to set format strings are: logging_context_format_string
|
||||
and logging_default_format_string. You can also specify
|
||||
logging_debug_format_suffix to append extra formatting if the log level is
|
||||
debug.
|
||||
|
||||
For information about what variables are available for the formatter see:
|
||||
http://docs.python.org/library/logging.html#formatter
|
||||
|
||||
"""
|
||||
|
||||
def format(self, record):
|
||||
"""Uses contextstring if request_id is set, otherwise default."""
|
||||
# NOTE(sdague): default the fancier formating params
|
||||
# to an empty string so we don't throw an exception if
|
||||
# they get used
|
||||
for key in ('instance', 'color'):
|
||||
if key not in record.__dict__:
|
||||
record.__dict__[key] = ''
|
||||
|
||||
if record.__dict__.get('request_id', None):
|
||||
self._fmt = CONF.logging_context_format_string
|
||||
else:
|
||||
self._fmt = CONF.logging_default_format_string
|
||||
|
||||
if (record.levelno == logging.DEBUG and
|
||||
CONF.logging_debug_format_suffix):
|
||||
self._fmt += " " + CONF.logging_debug_format_suffix
|
||||
|
||||
# Cache this on the record, Logger will respect our formated copy
|
||||
if record.exc_info:
|
||||
record.exc_text = self.formatException(record.exc_info, record)
|
||||
return logging.Formatter.format(self, record)
|
||||
|
||||
def formatException(self, exc_info, record=None):
|
||||
"""Format exception output with CONF.logging_exception_prefix."""
|
||||
if not record:
|
||||
return logging.Formatter.formatException(self, exc_info)
|
||||
|
||||
stringbuffer = cStringIO.StringIO()
|
||||
traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
|
||||
None, stringbuffer)
|
||||
lines = stringbuffer.getvalue().split('\n')
|
||||
stringbuffer.close()
|
||||
|
||||
if CONF.logging_exception_prefix.find('%(asctime)') != -1:
|
||||
record.asctime = self.formatTime(record, self.datefmt)
|
||||
|
||||
formatted_lines = []
|
||||
for line in lines:
|
||||
pl = CONF.logging_exception_prefix % record.__dict__
|
||||
fl = '%s%s' % (pl, line)
|
||||
formatted_lines.append(fl)
|
||||
return '\n'.join(formatted_lines)
|
||||
|
||||
|
||||
class ColorHandler(logging.StreamHandler):
|
||||
LEVEL_COLORS = {
|
||||
logging.DEBUG: '\033[00;32m', # GREEN
|
||||
logging.INFO: '\033[00;36m', # CYAN
|
||||
logging.AUDIT: '\033[01;36m', # BOLD CYAN
|
||||
logging.WARN: '\033[01;33m', # BOLD YELLOW
|
||||
logging.ERROR: '\033[01;31m', # BOLD RED
|
||||
logging.CRITICAL: '\033[01;31m', # BOLD RED
|
||||
}
|
||||
|
||||
def format(self, record):
|
||||
record.color = self.LEVEL_COLORS[record.levelno]
|
||||
return logging.StreamHandler.format(self, record)
|
||||
|
||||
|
||||
class DeprecatedConfig(Exception):
|
||||
message = _("Fatal call to deprecated config: %(msg)s")
|
||||
|
||||
def __init__(self, msg):
|
||||
super(Exception, self).__init__(self.message % dict(msg=msg))
|
95
conductor/conductor/openstack/common/loopingcall.py
Normal file
95
conductor/conductor/openstack/common/loopingcall.py
Normal file
@ -0,0 +1,95 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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 sys
|
||||
|
||||
from eventlet import event
|
||||
from eventlet import greenthread
|
||||
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import timeutils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LoopingCallDone(Exception):
|
||||
"""Exception to break out and stop a LoopingCall.
|
||||
|
||||
The poll-function passed to LoopingCall can raise this exception to
|
||||
break out of the loop normally. This is somewhat analogous to
|
||||
StopIteration.
|
||||
|
||||
An optional return-value can be included as the argument to the exception;
|
||||
this return-value will be returned by LoopingCall.wait()
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, retvalue=True):
|
||||
""":param retvalue: Value that LoopingCall.wait() should return."""
|
||||
self.retvalue = retvalue
|
||||
|
||||
|
||||
class LoopingCall(object):
|
||||
def __init__(self, f=None, *args, **kw):
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
self.f = f
|
||||
self._running = False
|
||||
|
||||
def start(self, interval, initial_delay=None):
|
||||
self._running = True
|
||||
done = event.Event()
|
||||
|
||||
def _inner():
|
||||
if initial_delay:
|
||||
greenthread.sleep(initial_delay)
|
||||
|
||||
try:
|
||||
while self._running:
|
||||
start = timeutils.utcnow()
|
||||
self.f(*self.args, **self.kw)
|
||||
end = timeutils.utcnow()
|
||||
if not self._running:
|
||||
break
|
||||
delay = interval - timeutils.delta_seconds(start, end)
|
||||
if delay <= 0:
|
||||
LOG.warn(_('task run outlasted interval by %s sec') %
|
||||
-delay)
|
||||
greenthread.sleep(delay if delay > 0 else 0)
|
||||
except LoopingCallDone, e:
|
||||
self.stop()
|
||||
done.send(e.retvalue)
|
||||
except Exception:
|
||||
LOG.exception(_('in looping call'))
|
||||
done.send_exception(*sys.exc_info())
|
||||
return
|
||||
else:
|
||||
done.send(True)
|
||||
|
||||
self.done = done
|
||||
|
||||
greenthread.spawn_n(_inner)
|
||||
return self.done
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
|
||||
def wait(self):
|
||||
return self.done.wait()
|
@ -1,6 +1,4 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010-2011 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
182
conductor/conductor/openstack/common/notifier/api.py
Normal file
182
conductor/conductor/openstack/common/notifier/api.py
Normal file
@ -0,0 +1,182 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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 uuid
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common import context
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import importutils
|
||||
from conductor.openstack.common import jsonutils
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import timeutils
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notifier_opts = [
|
||||
cfg.MultiStrOpt('notification_driver',
|
||||
default=[],
|
||||
help='Driver or drivers to handle sending notifications'),
|
||||
cfg.StrOpt('default_notification_level',
|
||||
default='INFO',
|
||||
help='Default notification level for outgoing notifications'),
|
||||
cfg.StrOpt('default_publisher_id',
|
||||
default='$host',
|
||||
help='Default publisher_id for outgoing notifications'),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(notifier_opts)
|
||||
|
||||
WARN = 'WARN'
|
||||
INFO = 'INFO'
|
||||
ERROR = 'ERROR'
|
||||
CRITICAL = 'CRITICAL'
|
||||
DEBUG = 'DEBUG'
|
||||
|
||||
log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
|
||||
|
||||
|
||||
class BadPriorityException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def notify_decorator(name, fn):
|
||||
""" decorator for notify which is used from utils.monkey_patch()
|
||||
|
||||
:param name: name of the function
|
||||
:param function: - object of the function
|
||||
:returns: function -- decorated function
|
||||
|
||||
"""
|
||||
def wrapped_func(*args, **kwarg):
|
||||
body = {}
|
||||
body['args'] = []
|
||||
body['kwarg'] = {}
|
||||
for arg in args:
|
||||
body['args'].append(arg)
|
||||
for key in kwarg:
|
||||
body['kwarg'][key] = kwarg[key]
|
||||
|
||||
ctxt = context.get_context_from_function_and_args(fn, args, kwarg)
|
||||
notify(ctxt,
|
||||
CONF.default_publisher_id,
|
||||
name,
|
||||
CONF.default_notification_level,
|
||||
body)
|
||||
return fn(*args, **kwarg)
|
||||
return wrapped_func
|
||||
|
||||
|
||||
def publisher_id(service, host=None):
|
||||
if not host:
|
||||
host = CONF.host
|
||||
return "%s.%s" % (service, host)
|
||||
|
||||
|
||||
def notify(context, publisher_id, event_type, priority, payload):
|
||||
"""Sends a notification using the specified driver
|
||||
|
||||
:param publisher_id: the source worker_type.host of the message
|
||||
:param event_type: the literal type of event (ex. Instance Creation)
|
||||
:param priority: patterned after the enumeration of Python logging
|
||||
levels in the set (DEBUG, WARN, INFO, ERROR, CRITICAL)
|
||||
:param payload: A python dictionary of attributes
|
||||
|
||||
Outgoing message format includes the above parameters, and appends the
|
||||
following:
|
||||
|
||||
message_id
|
||||
a UUID representing the id for this notification
|
||||
|
||||
timestamp
|
||||
the GMT timestamp the notification was sent at
|
||||
|
||||
The composite message will be constructed as a dictionary of the above
|
||||
attributes, which will then be sent via the transport mechanism defined
|
||||
by the driver.
|
||||
|
||||
Message example::
|
||||
|
||||
{'message_id': str(uuid.uuid4()),
|
||||
'publisher_id': 'compute.host1',
|
||||
'timestamp': timeutils.utcnow(),
|
||||
'priority': 'WARN',
|
||||
'event_type': 'compute.create_instance',
|
||||
'payload': {'instance_id': 12, ... }}
|
||||
|
||||
"""
|
||||
if priority not in log_levels:
|
||||
raise BadPriorityException(
|
||||
_('%s not in valid priorities') % priority)
|
||||
|
||||
# Ensure everything is JSON serializable.
|
||||
payload = jsonutils.to_primitive(payload, convert_instances=True)
|
||||
|
||||
msg = dict(message_id=str(uuid.uuid4()),
|
||||
publisher_id=publisher_id,
|
||||
event_type=event_type,
|
||||
priority=priority,
|
||||
payload=payload,
|
||||
timestamp=str(timeutils.utcnow()))
|
||||
|
||||
for driver in _get_drivers():
|
||||
try:
|
||||
driver.notify(context, msg)
|
||||
except Exception as e:
|
||||
LOG.exception(_("Problem '%(e)s' attempting to "
|
||||
"send to notification system. "
|
||||
"Payload=%(payload)s")
|
||||
% dict(e=e, payload=payload))
|
||||
|
||||
|
||||
_drivers = None
|
||||
|
||||
|
||||
def _get_drivers():
|
||||
"""Instantiate, cache, and return drivers based on the CONF."""
|
||||
global _drivers
|
||||
if _drivers is None:
|
||||
_drivers = {}
|
||||
for notification_driver in CONF.notification_driver:
|
||||
add_driver(notification_driver)
|
||||
|
||||
return _drivers.values()
|
||||
|
||||
|
||||
def add_driver(notification_driver):
|
||||
"""Add a notification driver at runtime."""
|
||||
# Make sure the driver list is initialized.
|
||||
_get_drivers()
|
||||
if isinstance(notification_driver, basestring):
|
||||
# Load and add
|
||||
try:
|
||||
driver = importutils.import_module(notification_driver)
|
||||
_drivers[notification_driver] = driver
|
||||
except ImportError:
|
||||
LOG.exception(_("Failed to load notifier %s. "
|
||||
"These notifications will not be sent.") %
|
||||
notification_driver)
|
||||
else:
|
||||
# Driver is already loaded; just add the object.
|
||||
_drivers[notification_driver] = notification_driver
|
||||
|
||||
|
||||
def _reset_drivers():
|
||||
"""Used by unit tests to reset the drivers."""
|
||||
global _drivers
|
||||
_drivers = None
|
@ -0,0 +1,35 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common import jsonutils
|
||||
from conductor.openstack.common import log as logging
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def notify(_context, message):
|
||||
"""Notifies the recipient of the desired event given the model.
|
||||
Log notifications using openstack's default logging system"""
|
||||
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
logger = logging.getLogger(
|
||||
'conductor.openstack.common.notification.%s' %
|
||||
message['event_type'])
|
||||
getattr(logger, priority)(jsonutils.dumps(message))
|
@ -1,6 +1,4 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -15,5 +13,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from heatclient import Client
|
||||
|
||||
def notify(_context, message):
|
||||
"""Notifies the recipient of the desired event given the model"""
|
||||
pass
|
@ -0,0 +1,46 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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 oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common import context as req_context
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import rpc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notification_topic_opt = cfg.ListOpt(
|
||||
'notification_topics', default=['notifications', ],
|
||||
help='AMQP topic used for openstack notifications')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opt(notification_topic_opt)
|
||||
|
||||
|
||||
def notify(context, message):
|
||||
"""Sends a notification via RPC"""
|
||||
if not context:
|
||||
context = req_context.get_admin_context()
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
for topic in CONF.notification_topics:
|
||||
topic = '%s.%s' % (topic, priority)
|
||||
try:
|
||||
rpc.notify(context, topic, message)
|
||||
except Exception:
|
||||
LOG.exception(_("Could not send notification to %(topic)s. "
|
||||
"Payload=%(message)s"), locals())
|
@ -0,0 +1,52 @@
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
'''messaging based notification driver, with message envelopes'''
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common import context as req_context
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import rpc
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
notification_topic_opt = cfg.ListOpt(
|
||||
'topics', default=['notifications', ],
|
||||
help='AMQP topic(s) used for openstack notifications')
|
||||
|
||||
opt_group = cfg.OptGroup(name='rpc_notifier2',
|
||||
title='Options for rpc_notifier2')
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_group(opt_group)
|
||||
CONF.register_opt(notification_topic_opt, opt_group)
|
||||
|
||||
|
||||
def notify(context, message):
|
||||
"""Sends a notification via RPC"""
|
||||
if not context:
|
||||
context = req_context.get_admin_context()
|
||||
priority = message.get('priority',
|
||||
CONF.default_notification_level)
|
||||
priority = priority.lower()
|
||||
for topic in CONF.rpc_notifier2.topics:
|
||||
topic = '%s.%s' % (topic, priority)
|
||||
try:
|
||||
rpc.notify(context, topic, message, envelope=True)
|
||||
except Exception:
|
||||
LOG.exception(_("Could not send notification to %(topic)s. "
|
||||
"Payload=%(message)s"), locals())
|
@ -1,6 +1,4 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -15,5 +13,10 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# TODO(jaypipes) Code in this module is intended to be ported to the eventual
|
||||
# openstack-common library
|
||||
|
||||
NOTIFICATIONS = []
|
||||
|
||||
|
||||
def notify(_context, message):
|
||||
"""Test notifier, stores notifications in memory for unittests."""
|
||||
NOTIFICATIONS.append(message)
|
332
conductor/conductor/openstack/common/service.py
Normal file
332
conductor/conductor/openstack/common/service.py
Normal file
@ -0,0 +1,332 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2010 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# Copyright 2011 Justin Santa Barbara
|
||||
# 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.
|
||||
|
||||
"""Generic Node base class for all workers that run on hosts."""
|
||||
|
||||
import errno
|
||||
import os
|
||||
import random
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
|
||||
import eventlet
|
||||
import logging as std_logging
|
||||
from oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common import eventlet_backdoor
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import importutils
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import threadgroup
|
||||
|
||||
|
||||
rpc = importutils.try_import('conductor.openstack.common.rpc')
|
||||
CONF = cfg.CONF
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Launcher(object):
|
||||
"""Launch one or more services and wait for them to complete."""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the service launcher.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._services = threadgroup.ThreadGroup()
|
||||
eventlet_backdoor.initialize_if_enabled()
|
||||
|
||||
@staticmethod
|
||||
def run_service(service):
|
||||
"""Start and wait for a service to finish.
|
||||
|
||||
:param service: service to run and wait for.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
service.start()
|
||||
service.wait()
|
||||
|
||||
def launch_service(self, service):
|
||||
"""Load and start the given service.
|
||||
|
||||
:param service: The service you would like to start.
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._services.add_thread(self.run_service, service)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all services which are currently running.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._services.stop()
|
||||
|
||||
def wait(self):
|
||||
"""Waits until all services have been stopped, and then returns.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
self._services.wait()
|
||||
|
||||
|
||||
class SignalExit(SystemExit):
|
||||
def __init__(self, signo, exccode=1):
|
||||
super(SignalExit, self).__init__(exccode)
|
||||
self.signo = signo
|
||||
|
||||
|
||||
class ServiceLauncher(Launcher):
|
||||
def _handle_signal(self, signo, frame):
|
||||
# Allow the process to be killed again and die from natural causes
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
raise SignalExit(signo)
|
||||
|
||||
def wait(self):
|
||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||
signal.signal(signal.SIGINT, self._handle_signal)
|
||||
|
||||
LOG.debug(_('Full set of CONF:'))
|
||||
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
|
||||
status = None
|
||||
try:
|
||||
super(ServiceLauncher, self).wait()
|
||||
except SignalExit as exc:
|
||||
signame = {signal.SIGTERM: 'SIGTERM',
|
||||
signal.SIGINT: 'SIGINT'}[exc.signo]
|
||||
LOG.info(_('Caught %s, exiting'), signame)
|
||||
status = exc.code
|
||||
except SystemExit as exc:
|
||||
status = exc.code
|
||||
finally:
|
||||
if rpc:
|
||||
rpc.cleanup()
|
||||
self.stop()
|
||||
return status
|
||||
|
||||
|
||||
class ServiceWrapper(object):
|
||||
def __init__(self, service, workers):
|
||||
self.service = service
|
||||
self.workers = workers
|
||||
self.children = set()
|
||||
self.forktimes = []
|
||||
|
||||
|
||||
class ProcessLauncher(object):
|
||||
def __init__(self):
|
||||
self.children = {}
|
||||
self.sigcaught = None
|
||||
self.running = True
|
||||
rfd, self.writepipe = os.pipe()
|
||||
self.readpipe = eventlet.greenio.GreenPipe(rfd, 'r')
|
||||
|
||||
signal.signal(signal.SIGTERM, self._handle_signal)
|
||||
signal.signal(signal.SIGINT, self._handle_signal)
|
||||
|
||||
def _handle_signal(self, signo, frame):
|
||||
self.sigcaught = signo
|
||||
self.running = False
|
||||
|
||||
# Allow the process to be killed again and die from natural causes
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
def _pipe_watcher(self):
|
||||
# This will block until the write end is closed when the parent
|
||||
# dies unexpectedly
|
||||
self.readpipe.read()
|
||||
|
||||
LOG.info(_('Parent process has died unexpectedly, exiting'))
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
def _child_process(self, service):
|
||||
# Setup child signal handlers differently
|
||||
def _sigterm(*args):
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
raise SignalExit(signal.SIGTERM)
|
||||
|
||||
signal.signal(signal.SIGTERM, _sigterm)
|
||||
# Block SIGINT and let the parent send us a SIGTERM
|
||||
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
||||
|
||||
# Reopen the eventlet hub to make sure we don't share an epoll
|
||||
# fd with parent and/or siblings, which would be bad
|
||||
eventlet.hubs.use_hub()
|
||||
|
||||
# Close write to ensure only parent has it open
|
||||
os.close(self.writepipe)
|
||||
# Create greenthread to watch for parent to close pipe
|
||||
eventlet.spawn_n(self._pipe_watcher)
|
||||
|
||||
# Reseed random number generator
|
||||
random.seed()
|
||||
|
||||
launcher = Launcher()
|
||||
launcher.run_service(service)
|
||||
|
||||
def _start_child(self, wrap):
|
||||
if len(wrap.forktimes) > wrap.workers:
|
||||
# Limit ourselves to one process a second (over the period of
|
||||
# number of workers * 1 second). This will allow workers to
|
||||
# start up quickly but ensure we don't fork off children that
|
||||
# die instantly too quickly.
|
||||
if time.time() - wrap.forktimes[0] < wrap.workers:
|
||||
LOG.info(_('Forking too fast, sleeping'))
|
||||
time.sleep(1)
|
||||
|
||||
wrap.forktimes.pop(0)
|
||||
|
||||
wrap.forktimes.append(time.time())
|
||||
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# NOTE(johannes): All exceptions are caught to ensure this
|
||||
# doesn't fallback into the loop spawning children. It would
|
||||
# be bad for a child to spawn more children.
|
||||
status = 0
|
||||
try:
|
||||
self._child_process(wrap.service)
|
||||
except SignalExit as exc:
|
||||
signame = {signal.SIGTERM: 'SIGTERM',
|
||||
signal.SIGINT: 'SIGINT'}[exc.signo]
|
||||
LOG.info(_('Caught %s, exiting'), signame)
|
||||
status = exc.code
|
||||
except SystemExit as exc:
|
||||
status = exc.code
|
||||
except BaseException:
|
||||
LOG.exception(_('Unhandled exception'))
|
||||
status = 2
|
||||
finally:
|
||||
wrap.service.stop()
|
||||
|
||||
os._exit(status)
|
||||
|
||||
LOG.info(_('Started child %d'), pid)
|
||||
|
||||
wrap.children.add(pid)
|
||||
self.children[pid] = wrap
|
||||
|
||||
return pid
|
||||
|
||||
def launch_service(self, service, workers=1):
|
||||
wrap = ServiceWrapper(service, workers)
|
||||
|
||||
LOG.info(_('Starting %d workers'), wrap.workers)
|
||||
while self.running and len(wrap.children) < wrap.workers:
|
||||
self._start_child(wrap)
|
||||
|
||||
def _wait_child(self):
|
||||
try:
|
||||
# Don't block if no child processes have exited
|
||||
pid, status = os.waitpid(0, os.WNOHANG)
|
||||
if not pid:
|
||||
return None
|
||||
except OSError as exc:
|
||||
if exc.errno not in (errno.EINTR, errno.ECHILD):
|
||||
raise
|
||||
return None
|
||||
|
||||
if os.WIFSIGNALED(status):
|
||||
sig = os.WTERMSIG(status)
|
||||
LOG.info(_('Child %(pid)d killed by signal %(sig)d'),
|
||||
dict(pid=pid, sig=sig))
|
||||
else:
|
||||
code = os.WEXITSTATUS(status)
|
||||
LOG.info(_('Child %(pid)s exited with status %(code)d'),
|
||||
dict(pid=pid, code=code))
|
||||
|
||||
if pid not in self.children:
|
||||
LOG.warning(_('pid %d not in child list'), pid)
|
||||
return None
|
||||
|
||||
wrap = self.children.pop(pid)
|
||||
wrap.children.remove(pid)
|
||||
return wrap
|
||||
|
||||
def wait(self):
|
||||
"""Loop waiting on children to die and respawning as necessary"""
|
||||
|
||||
LOG.debug(_('Full set of CONF:'))
|
||||
CONF.log_opt_values(LOG, std_logging.DEBUG)
|
||||
|
||||
while self.running:
|
||||
wrap = self._wait_child()
|
||||
if not wrap:
|
||||
# Yield to other threads if no children have exited
|
||||
# Sleep for a short time to avoid excessive CPU usage
|
||||
# (see bug #1095346)
|
||||
eventlet.greenthread.sleep(.01)
|
||||
continue
|
||||
|
||||
while self.running and len(wrap.children) < wrap.workers:
|
||||
self._start_child(wrap)
|
||||
|
||||
if self.sigcaught:
|
||||
signame = {signal.SIGTERM: 'SIGTERM',
|
||||
signal.SIGINT: 'SIGINT'}[self.sigcaught]
|
||||
LOG.info(_('Caught %s, stopping children'), signame)
|
||||
|
||||
for pid in self.children:
|
||||
try:
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
except OSError as exc:
|
||||
if exc.errno != errno.ESRCH:
|
||||
raise
|
||||
|
||||
# Wait for children to die
|
||||
if self.children:
|
||||
LOG.info(_('Waiting on %d children to exit'), len(self.children))
|
||||
while self.children:
|
||||
self._wait_child()
|
||||
|
||||
|
||||
class Service(object):
|
||||
"""Service object for binaries running on hosts."""
|
||||
|
||||
def __init__(self, threads=1000):
|
||||
self.tg = threadgroup.ThreadGroup(threads)
|
||||
|
||||
def start(self):
|
||||
pass
|
||||
|
||||
def stop(self):
|
||||
self.tg.stop()
|
||||
|
||||
def wait(self):
|
||||
self.tg.wait()
|
||||
|
||||
|
||||
def launch(service, workers=None):
|
||||
if workers:
|
||||
launcher = ProcessLauncher()
|
||||
launcher.launch_service(service, workers=workers)
|
||||
else:
|
||||
launcher = ServiceLauncher()
|
||||
launcher.launch_service(service)
|
||||
return launcher
|
367
conductor/conductor/openstack/common/setup.py
Normal file
367
conductor/conductor/openstack/common/setup.py
Normal file
@ -0,0 +1,367 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utilities with minimum-depends for use in setup.py
|
||||
"""
|
||||
|
||||
import email
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from setuptools.command import sdist
|
||||
|
||||
|
||||
def parse_mailmap(mailmap='.mailmap'):
|
||||
mapping = {}
|
||||
if os.path.exists(mailmap):
|
||||
with open(mailmap, 'r') as fp:
|
||||
for l in fp:
|
||||
try:
|
||||
canonical_email, alias = re.match(
|
||||
r'[^#]*?(<.+>).*(<.+>).*', l).groups()
|
||||
except AttributeError:
|
||||
continue
|
||||
mapping[alias] = canonical_email
|
||||
return mapping
|
||||
|
||||
|
||||
def _parse_git_mailmap(git_dir, mailmap='.mailmap'):
|
||||
mailmap = os.path.join(os.path.dirname(git_dir), mailmap)
|
||||
return parse_mailmap(mailmap)
|
||||
|
||||
|
||||
def canonicalize_emails(changelog, mapping):
|
||||
"""Takes in a string and an email alias mapping and replaces all
|
||||
instances of the aliases in the string with their real email.
|
||||
"""
|
||||
for alias, email_address in mapping.iteritems():
|
||||
changelog = changelog.replace(alias, email_address)
|
||||
return changelog
|
||||
|
||||
|
||||
# Get requirements from the first file that exists
|
||||
def get_reqs_from_files(requirements_files):
|
||||
for requirements_file in requirements_files:
|
||||
if os.path.exists(requirements_file):
|
||||
with open(requirements_file, 'r') as fil:
|
||||
return fil.read().split('\n')
|
||||
return []
|
||||
|
||||
|
||||
def parse_requirements(requirements_files=['requirements.txt',
|
||||
'tools/pip-requires']):
|
||||
requirements = []
|
||||
for line in get_reqs_from_files(requirements_files):
|
||||
# For the requirements list, we need to inject only the portion
|
||||
# after egg= so that distutils knows the package it's looking for
|
||||
# such as:
|
||||
# -e git://github.com/openstack/nova/master#egg=nova
|
||||
if re.match(r'\s*-e\s+', line):
|
||||
requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
|
||||
line))
|
||||
# such as:
|
||||
# http://github.com/openstack/nova/zipball/master#egg=nova
|
||||
elif re.match(r'\s*https?:', line):
|
||||
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
|
||||
line))
|
||||
# -f lines are for index locations, and don't get used here
|
||||
elif re.match(r'\s*-f\s+', line):
|
||||
pass
|
||||
# argparse is part of the standard library starting with 2.7
|
||||
# adding it to the requirements list screws distro installs
|
||||
elif line == 'argparse' and sys.version_info >= (2, 7):
|
||||
pass
|
||||
else:
|
||||
requirements.append(line)
|
||||
|
||||
return requirements
|
||||
|
||||
|
||||
def parse_dependency_links(requirements_files=['requirements.txt',
|
||||
'tools/pip-requires']):
|
||||
dependency_links = []
|
||||
# dependency_links inject alternate locations to find packages listed
|
||||
# in requirements
|
||||
for line in get_reqs_from_files(requirements_files):
|
||||
# skip comments and blank lines
|
||||
if re.match(r'(\s*#)|(\s*$)', line):
|
||||
continue
|
||||
# lines with -e or -f need the whole line, minus the flag
|
||||
if re.match(r'\s*-[ef]\s+', line):
|
||||
dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
|
||||
# lines that are only urls can go in unmolested
|
||||
elif re.match(r'\s*https?:', line):
|
||||
dependency_links.append(line)
|
||||
return dependency_links
|
||||
|
||||
|
||||
def _run_shell_command(cmd, throw_on_error=False):
|
||||
if os.name == 'nt':
|
||||
output = subprocess.Popen(["cmd.exe", "/C", cmd],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
else:
|
||||
output = subprocess.Popen(["/bin/sh", "-c", cmd],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
out = output.communicate()
|
||||
if output.returncode and throw_on_error:
|
||||
raise Exception("%s returned %d" % cmd, output.returncode)
|
||||
if len(out) == 0:
|
||||
return None
|
||||
if len(out[0].strip()) == 0:
|
||||
return None
|
||||
return out[0].strip()
|
||||
|
||||
|
||||
def _get_git_directory():
|
||||
parent_dir = os.path.dirname(__file__)
|
||||
while True:
|
||||
git_dir = os.path.join(parent_dir, '.git')
|
||||
if os.path.exists(git_dir):
|
||||
return git_dir
|
||||
parent_dir, child = os.path.split(parent_dir)
|
||||
if not child: # reached to root dir
|
||||
return None
|
||||
|
||||
|
||||
def write_git_changelog():
|
||||
"""Write a changelog based on the git changelog."""
|
||||
new_changelog = 'ChangeLog'
|
||||
git_dir = _get_git_directory()
|
||||
if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
|
||||
if git_dir:
|
||||
git_log_cmd = 'git --git-dir=%s log' % git_dir
|
||||
changelog = _run_shell_command(git_log_cmd)
|
||||
mailmap = _parse_git_mailmap(git_dir)
|
||||
with open(new_changelog, "w") as changelog_file:
|
||||
changelog_file.write(canonicalize_emails(changelog, mailmap))
|
||||
else:
|
||||
open(new_changelog, 'w').close()
|
||||
|
||||
|
||||
def generate_authors():
|
||||
"""Create AUTHORS file using git commits."""
|
||||
jenkins_email = 'jenkins@review.(openstack|stackforge).org'
|
||||
old_authors = 'AUTHORS.in'
|
||||
new_authors = 'AUTHORS'
|
||||
git_dir = _get_git_directory()
|
||||
if not os.getenv('SKIP_GENERATE_AUTHORS'):
|
||||
if git_dir:
|
||||
# don't include jenkins email address in AUTHORS file
|
||||
git_log_cmd = ("git --git-dir=" + git_dir +
|
||||
" log --format='%aN <%aE>' | sort -u | "
|
||||
"egrep -v '" + jenkins_email + "'")
|
||||
changelog = _run_shell_command(git_log_cmd)
|
||||
signed_cmd = ("git log --git-dir=" + git_dir +
|
||||
" | grep -i Co-authored-by: | sort -u")
|
||||
signed_entries = _run_shell_command(signed_cmd)
|
||||
if signed_entries:
|
||||
new_entries = "\n".join(
|
||||
[signed.split(":", 1)[1].strip()
|
||||
for signed in signed_entries.split("\n") if signed])
|
||||
changelog = "\n".join((changelog, new_entries))
|
||||
mailmap = _parse_git_mailmap(git_dir)
|
||||
with open(new_authors, 'w') as new_authors_fh:
|
||||
new_authors_fh.write(canonicalize_emails(changelog, mailmap))
|
||||
if os.path.exists(old_authors):
|
||||
with open(old_authors, "r") as old_authors_fh:
|
||||
new_authors_fh.write('\n' + old_authors_fh.read())
|
||||
else:
|
||||
open(new_authors, 'w').close()
|
||||
|
||||
|
||||
_rst_template = """%(heading)s
|
||||
%(underline)s
|
||||
|
||||
.. automodule:: %(module)s
|
||||
:members:
|
||||
:undoc-members:
|
||||
:show-inheritance:
|
||||
"""
|
||||
|
||||
|
||||
def get_cmdclass():
|
||||
"""Return dict of commands to run from setup.py."""
|
||||
|
||||
cmdclass = dict()
|
||||
|
||||
def _find_modules(arg, dirname, files):
|
||||
for filename in files:
|
||||
if filename.endswith('.py') and filename != '__init__.py':
|
||||
arg["%s.%s" % (dirname.replace('/', '.'),
|
||||
filename[:-3])] = True
|
||||
|
||||
class LocalSDist(sdist.sdist):
|
||||
"""Builds the ChangeLog and Authors files from VC first."""
|
||||
|
||||
def run(self):
|
||||
write_git_changelog()
|
||||
generate_authors()
|
||||
# sdist.sdist is an old style class, can't use super()
|
||||
sdist.sdist.run(self)
|
||||
|
||||
cmdclass['sdist'] = LocalSDist
|
||||
|
||||
# If Sphinx is installed on the box running setup.py,
|
||||
# enable setup.py to build the documentation, otherwise,
|
||||
# just ignore it
|
||||
try:
|
||||
from sphinx.setup_command import BuildDoc
|
||||
|
||||
class LocalBuildDoc(BuildDoc):
|
||||
|
||||
builders = ['html', 'man']
|
||||
|
||||
def generate_autoindex(self):
|
||||
print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
|
||||
modules = {}
|
||||
option_dict = self.distribution.get_option_dict('build_sphinx')
|
||||
source_dir = os.path.join(option_dict['source_dir'][1], 'api')
|
||||
if not os.path.exists(source_dir):
|
||||
os.makedirs(source_dir)
|
||||
for pkg in self.distribution.packages:
|
||||
if '.' not in pkg:
|
||||
os.path.walk(pkg, _find_modules, modules)
|
||||
module_list = modules.keys()
|
||||
module_list.sort()
|
||||
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
|
||||
with open(autoindex_filename, 'w') as autoindex:
|
||||
autoindex.write(""".. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
""")
|
||||
for module in module_list:
|
||||
output_filename = os.path.join(source_dir,
|
||||
"%s.rst" % module)
|
||||
heading = "The :mod:`%s` Module" % module
|
||||
underline = "=" * len(heading)
|
||||
values = dict(module=module, heading=heading,
|
||||
underline=underline)
|
||||
|
||||
print "Generating %s" % output_filename
|
||||
with open(output_filename, 'w') as output_file:
|
||||
output_file.write(_rst_template % values)
|
||||
autoindex.write(" %s.rst\n" % module)
|
||||
|
||||
def run(self):
|
||||
if not os.getenv('SPHINX_DEBUG'):
|
||||
self.generate_autoindex()
|
||||
|
||||
for builder in self.builders:
|
||||
self.builder = builder
|
||||
self.finalize_options()
|
||||
self.project = self.distribution.get_name()
|
||||
self.version = self.distribution.get_version()
|
||||
self.release = self.distribution.get_version()
|
||||
BuildDoc.run(self)
|
||||
|
||||
class LocalBuildLatex(LocalBuildDoc):
|
||||
builders = ['latex']
|
||||
|
||||
cmdclass['build_sphinx'] = LocalBuildDoc
|
||||
cmdclass['build_sphinx_latex'] = LocalBuildLatex
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return cmdclass
|
||||
|
||||
|
||||
def _get_revno(git_dir):
|
||||
"""Return the number of commits since the most recent tag.
|
||||
|
||||
We use git-describe to find this out, but if there are no
|
||||
tags then we fall back to counting commits since the beginning
|
||||
of time.
|
||||
"""
|
||||
describe = _run_shell_command(
|
||||
"git --git-dir=%s describe --always" % git_dir)
|
||||
if "-" in describe:
|
||||
return describe.rsplit("-", 2)[-2]
|
||||
|
||||
# no tags found
|
||||
revlist = _run_shell_command(
|
||||
"git --git-dir=%s rev-list --abbrev-commit HEAD" % git_dir)
|
||||
return len(revlist.splitlines())
|
||||
|
||||
|
||||
def _get_version_from_git(pre_version):
|
||||
"""Return a version which is equal to the tag that's on the current
|
||||
revision if there is one, or tag plus number of additional revisions
|
||||
if the current revision has no tag."""
|
||||
|
||||
git_dir = _get_git_directory()
|
||||
if git_dir:
|
||||
if pre_version:
|
||||
try:
|
||||
return _run_shell_command(
|
||||
"git --git-dir=" + git_dir + " describe --exact-match",
|
||||
throw_on_error=True).replace('-', '.')
|
||||
except Exception:
|
||||
sha = _run_shell_command(
|
||||
"git --git-dir=" + git_dir + " log -n1 --pretty=format:%h")
|
||||
return "%s.a%s.g%s" % (pre_version, _get_revno(git_dir), sha)
|
||||
else:
|
||||
return _run_shell_command(
|
||||
"git --git-dir=" + git_dir + " describe --always").replace(
|
||||
'-', '.')
|
||||
return None
|
||||
|
||||
|
||||
def _get_version_from_pkg_info(package_name):
|
||||
"""Get the version from PKG-INFO file if we can."""
|
||||
try:
|
||||
pkg_info_file = open('PKG-INFO', 'r')
|
||||
except (IOError, OSError):
|
||||
return None
|
||||
try:
|
||||
pkg_info = email.message_from_file(pkg_info_file)
|
||||
except email.MessageError:
|
||||
return None
|
||||
# Check to make sure we're in our own dir
|
||||
if pkg_info.get('Name', None) != package_name:
|
||||
return None
|
||||
return pkg_info.get('Version', None)
|
||||
|
||||
|
||||
def get_version(package_name, pre_version=None):
|
||||
"""Get the version of the project. First, try getting it from PKG-INFO, if
|
||||
it exists. If it does, that means we're in a distribution tarball or that
|
||||
install has happened. Otherwise, if there is no PKG-INFO file, pull the
|
||||
version from git.
|
||||
|
||||
We do not support setup.py version sanity in git archive tarballs, nor do
|
||||
we support packagers directly sucking our git repo into theirs. We expect
|
||||
that a source tarball be made from our git repo - or that if someone wants
|
||||
to make a source tarball from a fork of our repo with additional tags in it
|
||||
that they understand and desire the results of doing that.
|
||||
"""
|
||||
version = os.environ.get("OSLO_PACKAGE_VERSION", None)
|
||||
if version:
|
||||
return version
|
||||
version = _get_version_from_pkg_info(package_name)
|
||||
if version:
|
||||
return version
|
||||
version = _get_version_from_git(pre_version)
|
||||
if version:
|
||||
return version
|
||||
raise Exception("Versioning for this project requires either an sdist"
|
||||
" tarball, or access to an upstream git repository.")
|
80
conductor/conductor/openstack/common/sslutils.py
Normal file
80
conductor/conductor/openstack/common/sslutils.py
Normal file
@ -0,0 +1,80 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 IBM
|
||||
#
|
||||
# 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 os
|
||||
import ssl
|
||||
|
||||
from oslo.config import cfg
|
||||
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
|
||||
|
||||
ssl_opts = [
|
||||
cfg.StrOpt('ca_file',
|
||||
default=None,
|
||||
help="CA certificate file to use to verify "
|
||||
"connecting clients"),
|
||||
cfg.StrOpt('cert_file',
|
||||
default=None,
|
||||
help="Certificate file to use when starting "
|
||||
"the server securely"),
|
||||
cfg.StrOpt('key_file',
|
||||
default=None,
|
||||
help="Private key file to use when starting "
|
||||
"the server securely"),
|
||||
]
|
||||
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(ssl_opts, "ssl")
|
||||
|
||||
|
||||
def is_enabled():
|
||||
cert_file = CONF.ssl.cert_file
|
||||
key_file = CONF.ssl.key_file
|
||||
ca_file = CONF.ssl.ca_file
|
||||
use_ssl = cert_file or key_file
|
||||
|
||||
if cert_file and not os.path.exists(cert_file):
|
||||
raise RuntimeError(_("Unable to find cert_file : %s") % cert_file)
|
||||
|
||||
if ca_file and not os.path.exists(ca_file):
|
||||
raise RuntimeError(_("Unable to find ca_file : %s") % ca_file)
|
||||
|
||||
if key_file and not os.path.exists(key_file):
|
||||
raise RuntimeError(_("Unable to find key_file : %s") % key_file)
|
||||
|
||||
if use_ssl and (not cert_file or not key_file):
|
||||
raise RuntimeError(_("When running server in SSL mode, you must "
|
||||
"specify both a cert_file and key_file "
|
||||
"option value in your configuration file"))
|
||||
|
||||
return use_ssl
|
||||
|
||||
|
||||
def wrap(sock):
|
||||
ssl_kwargs = {
|
||||
'server_side': True,
|
||||
'certfile': CONF.ssl.cert_file,
|
||||
'keyfile': CONF.ssl.key_file,
|
||||
'cert_reqs': ssl.CERT_NONE,
|
||||
}
|
||||
|
||||
if CONF.ssl.ca_file:
|
||||
ssl_kwargs['ca_certs'] = CONF.ssl.ca_file
|
||||
ssl_kwargs['cert_reqs'] = ssl.CERT_REQUIRED
|
||||
|
||||
return ssl.wrap_socket(sock, **ssl_kwargs)
|
114
conductor/conductor/openstack/common/threadgroup.py
Normal file
114
conductor/conductor/openstack/common/threadgroup.py
Normal file
@ -0,0 +1,114 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 Red Hat, 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.
|
||||
|
||||
from eventlet import greenlet
|
||||
from eventlet import greenpool
|
||||
from eventlet import greenthread
|
||||
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import loopingcall
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _thread_done(gt, *args, **kwargs):
|
||||
""" Callback function to be passed to GreenThread.link() when we spawn()
|
||||
Calls the :class:`ThreadGroup` to notify if.
|
||||
|
||||
"""
|
||||
kwargs['group'].thread_done(kwargs['thread'])
|
||||
|
||||
|
||||
class Thread(object):
|
||||
""" Wrapper around a greenthread, that holds a reference to the
|
||||
:class:`ThreadGroup`. The Thread will notify the :class:`ThreadGroup` when
|
||||
it has done so it can be removed from the threads list.
|
||||
"""
|
||||
def __init__(self, thread, group):
|
||||
self.thread = thread
|
||||
self.thread.link(_thread_done, group=group, thread=self)
|
||||
|
||||
def stop(self):
|
||||
self.thread.kill()
|
||||
|
||||
def wait(self):
|
||||
return self.thread.wait()
|
||||
|
||||
|
||||
class ThreadGroup(object):
|
||||
""" The point of the ThreadGroup classis to:
|
||||
|
||||
* keep track of timers and greenthreads (making it easier to stop them
|
||||
when need be).
|
||||
* provide an easy API to add timers.
|
||||
"""
|
||||
def __init__(self, thread_pool_size=10):
|
||||
self.pool = greenpool.GreenPool(thread_pool_size)
|
||||
self.threads = []
|
||||
self.timers = []
|
||||
|
||||
def add_timer(self, interval, callback, initial_delay=None,
|
||||
*args, **kwargs):
|
||||
pulse = loopingcall.LoopingCall(callback, *args, **kwargs)
|
||||
pulse.start(interval=interval,
|
||||
initial_delay=initial_delay)
|
||||
self.timers.append(pulse)
|
||||
|
||||
def add_thread(self, callback, *args, **kwargs):
|
||||
gt = self.pool.spawn(callback, *args, **kwargs)
|
||||
th = Thread(gt, self)
|
||||
self.threads.append(th)
|
||||
|
||||
def thread_done(self, thread):
|
||||
self.threads.remove(thread)
|
||||
|
||||
def stop(self):
|
||||
current = greenthread.getcurrent()
|
||||
for x in self.threads:
|
||||
if x is current:
|
||||
# don't kill the current thread.
|
||||
continue
|
||||
try:
|
||||
x.stop()
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
|
||||
for x in self.timers:
|
||||
try:
|
||||
x.stop()
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
self.timers = []
|
||||
|
||||
def wait(self):
|
||||
for x in self.timers:
|
||||
try:
|
||||
x.wait()
|
||||
except greenlet.GreenletExit:
|
||||
pass
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
||||
current = greenthread.getcurrent()
|
||||
for x in self.threads:
|
||||
if x is current:
|
||||
continue
|
||||
try:
|
||||
x.wait()
|
||||
except greenlet.GreenletExit:
|
||||
pass
|
||||
except Exception as ex:
|
||||
LOG.exception(ex)
|
186
conductor/conductor/openstack/common/timeutils.py
Normal file
186
conductor/conductor/openstack/common/timeutils.py
Normal file
@ -0,0 +1,186 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Time related utilities and helper functions.
|
||||
"""
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
|
||||
import iso8601
|
||||
|
||||
|
||||
# ISO 8601 extended time format with microseconds
|
||||
_ISO8601_TIME_FORMAT_SUBSECOND = '%Y-%m-%dT%H:%M:%S.%f'
|
||||
_ISO8601_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'
|
||||
PERFECT_TIME_FORMAT = _ISO8601_TIME_FORMAT_SUBSECOND
|
||||
|
||||
|
||||
def isotime(at=None, subsecond=False):
|
||||
"""Stringify time in ISO 8601 format"""
|
||||
if not at:
|
||||
at = utcnow()
|
||||
st = at.strftime(_ISO8601_TIME_FORMAT
|
||||
if not subsecond
|
||||
else _ISO8601_TIME_FORMAT_SUBSECOND)
|
||||
tz = at.tzinfo.tzname(None) if at.tzinfo else 'UTC'
|
||||
st += ('Z' if tz == 'UTC' else tz)
|
||||
return st
|
||||
|
||||
|
||||
def parse_isotime(timestr):
|
||||
"""Parse time from ISO 8601 format"""
|
||||
try:
|
||||
return iso8601.parse_date(timestr)
|
||||
except iso8601.ParseError as e:
|
||||
raise ValueError(e.message)
|
||||
except TypeError as e:
|
||||
raise ValueError(e.message)
|
||||
|
||||
|
||||
def strtime(at=None, fmt=PERFECT_TIME_FORMAT):
|
||||
"""Returns formatted utcnow."""
|
||||
if not at:
|
||||
at = utcnow()
|
||||
return at.strftime(fmt)
|
||||
|
||||
|
||||
def parse_strtime(timestr, fmt=PERFECT_TIME_FORMAT):
|
||||
"""Turn a formatted time back into a datetime."""
|
||||
return datetime.datetime.strptime(timestr, fmt)
|
||||
|
||||
|
||||
def normalize_time(timestamp):
|
||||
"""Normalize time in arbitrary timezone to UTC naive object"""
|
||||
offset = timestamp.utcoffset()
|
||||
if offset is None:
|
||||
return timestamp
|
||||
return timestamp.replace(tzinfo=None) - offset
|
||||
|
||||
|
||||
def is_older_than(before, seconds):
|
||||
"""Return True if before is older than seconds."""
|
||||
if isinstance(before, basestring):
|
||||
before = parse_strtime(before).replace(tzinfo=None)
|
||||
return utcnow() - before > datetime.timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def is_newer_than(after, seconds):
|
||||
"""Return True if after is newer than seconds."""
|
||||
if isinstance(after, basestring):
|
||||
after = parse_strtime(after).replace(tzinfo=None)
|
||||
return after - utcnow() > datetime.timedelta(seconds=seconds)
|
||||
|
||||
|
||||
def utcnow_ts():
|
||||
"""Timestamp version of our utcnow function."""
|
||||
return calendar.timegm(utcnow().timetuple())
|
||||
|
||||
|
||||
def utcnow():
|
||||
"""Overridable version of utils.utcnow."""
|
||||
if utcnow.override_time:
|
||||
try:
|
||||
return utcnow.override_time.pop(0)
|
||||
except AttributeError:
|
||||
return utcnow.override_time
|
||||
return datetime.datetime.utcnow()
|
||||
|
||||
|
||||
def iso8601_from_timestamp(timestamp):
|
||||
"""Returns a iso8601 formated date from timestamp"""
|
||||
return isotime(datetime.datetime.utcfromtimestamp(timestamp))
|
||||
|
||||
|
||||
utcnow.override_time = None
|
||||
|
||||
|
||||
def set_time_override(override_time=datetime.datetime.utcnow()):
|
||||
"""
|
||||
Override utils.utcnow to return a constant time or a list thereof,
|
||||
one at a time.
|
||||
"""
|
||||
utcnow.override_time = override_time
|
||||
|
||||
|
||||
def advance_time_delta(timedelta):
|
||||
"""Advance overridden time using a datetime.timedelta."""
|
||||
assert(not utcnow.override_time is None)
|
||||
try:
|
||||
for dt in utcnow.override_time:
|
||||
dt += timedelta
|
||||
except TypeError:
|
||||
utcnow.override_time += timedelta
|
||||
|
||||
|
||||
def advance_time_seconds(seconds):
|
||||
"""Advance overridden time by seconds."""
|
||||
advance_time_delta(datetime.timedelta(0, seconds))
|
||||
|
||||
|
||||
def clear_time_override():
|
||||
"""Remove the overridden time."""
|
||||
utcnow.override_time = None
|
||||
|
||||
|
||||
def marshall_now(now=None):
|
||||
"""Make an rpc-safe datetime with microseconds.
|
||||
|
||||
Note: tzinfo is stripped, but not required for relative times."""
|
||||
if not now:
|
||||
now = utcnow()
|
||||
return dict(day=now.day, month=now.month, year=now.year, hour=now.hour,
|
||||
minute=now.minute, second=now.second,
|
||||
microsecond=now.microsecond)
|
||||
|
||||
|
||||
def unmarshall_time(tyme):
|
||||
"""Unmarshall a datetime dict."""
|
||||
return datetime.datetime(day=tyme['day'],
|
||||
month=tyme['month'],
|
||||
year=tyme['year'],
|
||||
hour=tyme['hour'],
|
||||
minute=tyme['minute'],
|
||||
second=tyme['second'],
|
||||
microsecond=tyme['microsecond'])
|
||||
|
||||
|
||||
def delta_seconds(before, after):
|
||||
"""
|
||||
Compute the difference in seconds between two date, time, or
|
||||
datetime objects (as a float, to microsecond resolution).
|
||||
"""
|
||||
delta = after - before
|
||||
try:
|
||||
return delta.total_seconds()
|
||||
except AttributeError:
|
||||
return ((delta.days * 24 * 3600) + delta.seconds +
|
||||
float(delta.microseconds) / (10 ** 6))
|
||||
|
||||
|
||||
def is_soon(dt, window):
|
||||
"""
|
||||
Determines if time is going to happen in the next window seconds.
|
||||
|
||||
:params dt: the time
|
||||
:params window: minimum seconds to remain to consider the time not soon
|
||||
|
||||
:return: True if expiration is within the given duration
|
||||
"""
|
||||
soon = (utcnow() + datetime.timedelta(seconds=window))
|
||||
return normalize_time(dt) <= soon
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright (c) 2012 Intel Corporation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -15,23 +15,25 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
class Builder:
|
||||
name = "Abstract Builder"
|
||||
type = "abstract"
|
||||
version = 0
|
||||
"""
|
||||
UUID related utilities and helper functions.
|
||||
"""
|
||||
|
||||
def __init__(self, conf):
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return self.name+' type: '+self.type+ ' version: ' + str(self.version)
|
||||
|
||||
def build(self, context, event, data):
|
||||
pass
|
||||
|
||||
def create_context():
|
||||
context = {}
|
||||
context['commands']=[]
|
||||
return context
|
||||
import uuid
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
def is_uuid_like(val):
|
||||
"""Returns validation of a value as a UUID.
|
||||
|
||||
For our purposes, a UUID is a canonical form string:
|
||||
aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
||||
|
||||
"""
|
||||
try:
|
||||
return str(uuid.UUID(val)) == val
|
||||
except (TypeError, ValueError, AttributeError):
|
||||
return False
|
94
conductor/conductor/openstack/common/version.py
Normal file
94
conductor/conductor/openstack/common/version.py
Normal file
@ -0,0 +1,94 @@
|
||||
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Utilities for consuming the version from pkg_resources.
|
||||
"""
|
||||
|
||||
import pkg_resources
|
||||
|
||||
|
||||
class VersionInfo(object):
|
||||
|
||||
def __init__(self, package):
|
||||
"""Object that understands versioning for a package
|
||||
:param package: name of the python package, such as glance, or
|
||||
python-glanceclient
|
||||
"""
|
||||
self.package = package
|
||||
self.release = None
|
||||
self.version = None
|
||||
self._cached_version = None
|
||||
|
||||
def __str__(self):
|
||||
"""Make the VersionInfo object behave like a string."""
|
||||
return self.version_string()
|
||||
|
||||
def __repr__(self):
|
||||
"""Include the name."""
|
||||
return "VersionInfo(%s:%s)" % (self.package, self.version_string())
|
||||
|
||||
def _get_version_from_pkg_resources(self):
|
||||
"""Get the version of the package from the pkg_resources record
|
||||
associated with the package."""
|
||||
try:
|
||||
requirement = pkg_resources.Requirement.parse(self.package)
|
||||
provider = pkg_resources.get_provider(requirement)
|
||||
return provider.version
|
||||
except pkg_resources.DistributionNotFound:
|
||||
# The most likely cause for this is running tests in a tree
|
||||
# produced from a tarball where the package itself has not been
|
||||
# installed into anything. Revert to setup-time logic.
|
||||
from conductor.openstack.common import setup
|
||||
return setup.get_version(self.package)
|
||||
|
||||
def release_string(self):
|
||||
"""Return the full version of the package including suffixes indicating
|
||||
VCS status.
|
||||
"""
|
||||
if self.release is None:
|
||||
self.release = self._get_version_from_pkg_resources()
|
||||
|
||||
return self.release
|
||||
|
||||
def version_string(self):
|
||||
"""Return the short version minus any alpha/beta tags."""
|
||||
if self.version is None:
|
||||
parts = []
|
||||
for part in self.release_string().split('.'):
|
||||
if part[0].isdigit():
|
||||
parts.append(part)
|
||||
else:
|
||||
break
|
||||
self.version = ".".join(parts)
|
||||
|
||||
return self.version
|
||||
|
||||
# Compatibility functions
|
||||
canonical_version_string = version_string
|
||||
version_string_with_vcs = release_string
|
||||
|
||||
def cached_version_string(self, prefix=""):
|
||||
"""Generate an object which will expand in a string context to
|
||||
the results of version_string(). We do this so that don't
|
||||
call into pkg_resources every time we start up a program when
|
||||
passing version information into the CONF constructor, but
|
||||
rather only do the calculation when and if a version is requested
|
||||
"""
|
||||
if not self._cached_version:
|
||||
self._cached_version = "%s%s" % (prefix,
|
||||
self.version_string())
|
||||
return self._cached_version
|
@ -1,6 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011 OpenStack Foundation.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -17,15 +17,17 @@
|
||||
|
||||
"""Utility methods for working with WSGI servers."""
|
||||
|
||||
import datetime
|
||||
import eventlet
|
||||
import eventlet.wsgi
|
||||
|
||||
eventlet.patcher.monkey_patch(all=False, socket=True)
|
||||
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import errno
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
import eventlet.wsgi
|
||||
from oslo.config import cfg
|
||||
import routes
|
||||
import routes.middleware
|
||||
import webob.dec
|
||||
@ -33,52 +35,130 @@ import webob.exc
|
||||
from xml.dom import minidom
|
||||
from xml.parsers import expat
|
||||
|
||||
from openstack.common import exception
|
||||
from conductor.openstack.common import exception
|
||||
from conductor.openstack.common.gettextutils import _
|
||||
from conductor.openstack.common import jsonutils
|
||||
from conductor.openstack.common import log as logging
|
||||
from conductor.openstack.common import service
|
||||
from conductor.openstack.common import sslutils
|
||||
from conductor.openstack.common import xmlutils
|
||||
|
||||
socket_opts = [
|
||||
cfg.IntOpt('backlog',
|
||||
default=4096,
|
||||
help="Number of backlog requests to configure the socket with"),
|
||||
cfg.IntOpt('tcp_keepidle',
|
||||
default=600,
|
||||
help="Sets the value of TCP_KEEPIDLE in seconds for each "
|
||||
"server socket. Not supported on OS X."),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(socket_opts)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
LOG = logging.getLogger('wsgi')
|
||||
|
||||
|
||||
class WritableLogger(object):
|
||||
"""A thin wrapper that responds to `write` and logs."""
|
||||
|
||||
def __init__(self, logger, level=logging.DEBUG):
|
||||
self.logger = logger
|
||||
self.level = level
|
||||
|
||||
def write(self, msg):
|
||||
self.logger.log(self.level, msg.strip("\n"))
|
||||
|
||||
|
||||
def run_server(application, port):
|
||||
def run_server(application, port, **kwargs):
|
||||
"""Run a WSGI server with the given application."""
|
||||
sock = eventlet.listen(('0.0.0.0', port))
|
||||
eventlet.wsgi.server(sock, application)
|
||||
eventlet.wsgi.server(sock, application, **kwargs)
|
||||
|
||||
|
||||
class Server(object):
|
||||
"""Server class to manage multiple WSGI sockets and applications."""
|
||||
class Service(service.Service):
|
||||
"""
|
||||
Provides a Service API for wsgi servers.
|
||||
|
||||
def __init__(self, threads=1000):
|
||||
self.pool = eventlet.GreenPool(threads)
|
||||
This gives us the ability to launch wsgi servers with the
|
||||
Launcher classes in service.py.
|
||||
"""
|
||||
|
||||
def start(self, application, port, host='0.0.0.0', backlog=128):
|
||||
"""Run a WSGI server with the given application."""
|
||||
socket = eventlet.listen((host, port), backlog=backlog)
|
||||
self.pool.spawn_n(self._run, application, socket)
|
||||
def __init__(self, application, port,
|
||||
host='0.0.0.0', backlog=4096, threads=1000):
|
||||
self.application = application
|
||||
self._port = port
|
||||
self._host = host
|
||||
self._backlog = backlog if backlog else CONF.backlog
|
||||
super(Service, self).__init__(threads)
|
||||
|
||||
def wait(self):
|
||||
"""Wait until all servers have completed running."""
|
||||
try:
|
||||
self.pool.waitall()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
def _get_socket(self, host, port, backlog):
|
||||
# TODO(dims): eventlet's green dns/socket module does not actually
|
||||
# support IPv6 in getaddrinfo(). We need to get around this in the
|
||||
# future or monitor upstream for a fix
|
||||
info = socket.getaddrinfo(host,
|
||||
port,
|
||||
socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM)[0]
|
||||
family = info[0]
|
||||
bind_addr = info[-1]
|
||||
|
||||
sock = None
|
||||
retry_until = time.time() + 30
|
||||
while not sock and time.time() < retry_until:
|
||||
try:
|
||||
sock = eventlet.listen(bind_addr,
|
||||
backlog=backlog,
|
||||
family=family)
|
||||
if sslutils.is_enabled():
|
||||
sock = sslutils.wrap(sock)
|
||||
|
||||
except socket.error, err:
|
||||
if err.args[0] != errno.EADDRINUSE:
|
||||
raise
|
||||
eventlet.sleep(0.1)
|
||||
if not sock:
|
||||
raise RuntimeError(_("Could not bind to %(host)s:%(port)s "
|
||||
"after trying for 30 seconds") %
|
||||
{'host': host, 'port': port})
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
# sockets can hang around forever without keepalive
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
|
||||
# This option isn't available in the OS X version of eventlet
|
||||
if hasattr(socket, 'TCP_KEEPIDLE'):
|
||||
sock.setsockopt(socket.IPPROTO_TCP,
|
||||
socket.TCP_KEEPIDLE,
|
||||
CONF.tcp_keepidle)
|
||||
|
||||
return sock
|
||||
|
||||
def start(self):
|
||||
"""Start serving this service using the provided server instance.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
super(Service, self).start()
|
||||
self._socket = self._get_socket(self._host, self._port, self._backlog)
|
||||
self.tg.add_thread(self._run, self.application, self._socket)
|
||||
|
||||
@property
|
||||
def backlog(self):
|
||||
return self._backlog
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
return self._socket.getsockname()[0] if self._socket else self._host
|
||||
|
||||
@property
|
||||
def port(self):
|
||||
return self._socket.getsockname()[1] if self._socket else self._port
|
||||
|
||||
def stop(self):
|
||||
"""Stop serving this API.
|
||||
|
||||
:returns: None
|
||||
|
||||
"""
|
||||
super(Service, self).stop()
|
||||
|
||||
def _run(self, application, socket):
|
||||
"""Start a WSGI server in a new green thread."""
|
||||
logger = logging.getLogger('eventlet.wsgi.server')
|
||||
eventlet.wsgi.server(socket, application, custom_pool=self.pool,
|
||||
log=WritableLogger(logger))
|
||||
logger = logging.getLogger('eventlet.wsgi')
|
||||
eventlet.wsgi.server(socket,
|
||||
application,
|
||||
custom_pool=self.tg.pool,
|
||||
log=logging.WritableLogger(logger))
|
||||
|
||||
|
||||
class Middleware(object):
|
||||
@ -241,7 +321,7 @@ class Request(webob.Request):
|
||||
Does not do any body introspection, only checks header
|
||||
|
||||
"""
|
||||
if not "Content-Type" in self.headers:
|
||||
if "Content-Type" not in self.headers:
|
||||
return None
|
||||
|
||||
content_type = self.content_type
|
||||
@ -371,8 +451,8 @@ class JSONDictSerializer(DictSerializer):
|
||||
if isinstance(obj, datetime.datetime):
|
||||
_dtime = obj - datetime.timedelta(microseconds=obj.microsecond)
|
||||
return _dtime.isoformat()
|
||||
return obj
|
||||
return json.dumps(data, default=sanitizer)
|
||||
return unicode(obj)
|
||||
return jsonutils.dumps(data, default=sanitizer)
|
||||
|
||||
|
||||
class XMLDictSerializer(DictSerializer):
|
||||
@ -495,8 +575,8 @@ class ResponseSerializer(object):
|
||||
}
|
||||
self.body_serializers.update(body_serializers or {})
|
||||
|
||||
self.headers_serializer = headers_serializer or \
|
||||
ResponseHeadersSerializer()
|
||||
self.headers_serializer = (headers_serializer or
|
||||
ResponseHeadersSerializer())
|
||||
|
||||
def serialize(self, response_data, content_type, action='default'):
|
||||
"""Serialize a dict into a string and wrap in a wsgi.Request object.
|
||||
@ -550,16 +630,16 @@ class RequestDeserializer(object):
|
||||
}
|
||||
self.body_deserializers.update(body_deserializers or {})
|
||||
|
||||
self.headers_deserializer = headers_deserializer or \
|
||||
RequestHeadersDeserializer()
|
||||
self.headers_deserializer = (headers_deserializer or
|
||||
RequestHeadersDeserializer())
|
||||
|
||||
def deserialize(self, request):
|
||||
"""Extract necessary pieces of the request.
|
||||
|
||||
:param request: Request object
|
||||
:returns tuple of expected controller action name, dictionary of
|
||||
keyword arguments to pass to the controller, the expected
|
||||
content type of the response
|
||||
:returns: tuple of (expected controller action name, dictionary of
|
||||
keyword arguments to pass to the controller, the expected
|
||||
content type of the response)
|
||||
|
||||
"""
|
||||
action_args = self.get_action_args(request.environ)
|
||||
@ -641,7 +721,7 @@ class JSONDeserializer(TextDeserializer):
|
||||
|
||||
def _from_json(self, datastring):
|
||||
try:
|
||||
return json.loads(datastring)
|
||||
return jsonutils.loads(datastring)
|
||||
except ValueError:
|
||||
msg = _("cannot understand JSON")
|
||||
raise exception.MalformedRequestBody(reason=msg)
|
||||
@ -664,7 +744,7 @@ class XMLDeserializer(TextDeserializer):
|
||||
plurals = set(self.metadata.get('plurals', {}))
|
||||
|
||||
try:
|
||||
node = minidom.parseString(datastring).childNodes[0]
|
||||
node = xmlutils.safe_minidom_parse_string(datastring).childNodes[0]
|
||||
return {node.nodeName: self._from_xml_node(node, plurals)}
|
||||
except expat.ExpatError:
|
||||
msg = _("cannot understand XML")
|
74
conductor/conductor/openstack/common/xmlutils.py
Normal file
74
conductor/conductor/openstack/common/xmlutils.py
Normal file
@ -0,0 +1,74 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 IBM
|
||||
#
|
||||
# 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 xml.dom import minidom
|
||||
from xml.parsers import expat
|
||||
from xml import sax
|
||||
from xml.sax import expatreader
|
||||
|
||||
|
||||
class ProtectedExpatParser(expatreader.ExpatParser):
|
||||
"""An expat parser which disables DTD's and entities by default."""
|
||||
|
||||
def __init__(self, forbid_dtd=True, forbid_entities=True,
|
||||
*args, **kwargs):
|
||||
# Python 2.x old style class
|
||||
expatreader.ExpatParser.__init__(self, *args, **kwargs)
|
||||
self.forbid_dtd = forbid_dtd
|
||||
self.forbid_entities = forbid_entities
|
||||
|
||||
def start_doctype_decl(self, name, sysid, pubid, has_internal_subset):
|
||||
raise ValueError("Inline DTD forbidden")
|
||||
|
||||
def entity_decl(self, entityName, is_parameter_entity, value, base,
|
||||
systemId, publicId, notationName):
|
||||
raise ValueError("<!ENTITY> entity declaration forbidden")
|
||||
|
||||
def unparsed_entity_decl(self, name, base, sysid, pubid, notation_name):
|
||||
# expat 1.2
|
||||
raise ValueError("<!ENTITY> unparsed entity forbidden")
|
||||
|
||||
def external_entity_ref(self, context, base, systemId, publicId):
|
||||
raise ValueError("<!ENTITY> external entity forbidden")
|
||||
|
||||
def notation_decl(self, name, base, sysid, pubid):
|
||||
raise ValueError("<!ENTITY> notation forbidden")
|
||||
|
||||
def reset(self):
|
||||
expatreader.ExpatParser.reset(self)
|
||||
if self.forbid_dtd:
|
||||
self._parser.StartDoctypeDeclHandler = self.start_doctype_decl
|
||||
self._parser.EndDoctypeDeclHandler = None
|
||||
if self.forbid_entities:
|
||||
self._parser.EntityDeclHandler = self.entity_decl
|
||||
self._parser.UnparsedEntityDeclHandler = self.unparsed_entity_decl
|
||||
self._parser.ExternalEntityRefHandler = self.external_entity_ref
|
||||
self._parser.NotationDeclHandler = self.notation_decl
|
||||
try:
|
||||
self._parser.SkippedEntityHandler = None
|
||||
except AttributeError:
|
||||
# some pyexpat versions do not support SkippedEntity
|
||||
pass
|
||||
|
||||
|
||||
def safe_minidom_parse_string(xml_string):
|
||||
"""Parse an XML string using minidom safely.
|
||||
|
||||
"""
|
||||
try:
|
||||
return minidom.parseString(xml_string, parser=ProtectedExpatParser())
|
||||
except sax.SAXParseException:
|
||||
raise expat.ExpatError()
|
@ -1,70 +1,127 @@
|
||||
import uuid
|
||||
import pika
|
||||
from pika.adapters import TornadoConnection
|
||||
import time
|
||||
|
||||
try:
|
||||
import tornado.ioloop
|
||||
|
||||
IOLoop = tornado.ioloop.IOLoop
|
||||
except ImportError:
|
||||
IOLoop = None
|
||||
|
||||
|
||||
class RabbitMqClient(object):
|
||||
def __init__(self, host='localhost', login='guest',
|
||||
password='guest', virtual_host='/'):
|
||||
credentials = pika.PlainCredentials(login, password)
|
||||
self._connection_parameters = pika.ConnectionParameters(
|
||||
credentials=credentials, host=host, virtual_host=virtual_host)
|
||||
self._subscriptions = {}
|
||||
|
||||
def _create_connection(self):
|
||||
self.connection = TornadoConnection(
|
||||
parameters=self._connection_parameters,
|
||||
on_open_callback=self._on_connected)
|
||||
|
||||
def _on_connected(self, connection):
|
||||
self._channel = connection.channel(self._on_channel_open)
|
||||
|
||||
def _on_channel_open(self, channel):
|
||||
self._channel = channel
|
||||
if self._started_callback:
|
||||
self._started_callback()
|
||||
|
||||
def _on_queue_declared(self, frame, queue, callback, ctag):
|
||||
def invoke_callback(ch, method_frame, header_frame, body):
|
||||
callback(body=body,
|
||||
message_id=header_frame.message_id or "")
|
||||
|
||||
self._channel.basic_consume(invoke_callback, queue=queue,
|
||||
no_ack=True, consumer_tag=ctag)
|
||||
|
||||
def subscribe(self, queue, callback):
|
||||
ctag = str(uuid.uuid4())
|
||||
self._subscriptions[queue] = ctag
|
||||
|
||||
self._channel.queue_declare(
|
||||
queue=queue, durable=True,
|
||||
callback=lambda frame, ctag=ctag: self._on_queue_declared(
|
||||
frame, queue, callback, ctag))
|
||||
|
||||
def unsubscribe(self, queue):
|
||||
self._channel.basic_cancel(consumer_tag=self._subscriptions[queue])
|
||||
del self._subscriptions[queue]
|
||||
|
||||
def start(self, callback=None):
|
||||
if IOLoop is None:
|
||||
raise ImportError("Tornado not installed")
|
||||
self._started_callback = callback
|
||||
ioloop = IOLoop.instance()
|
||||
self.timeout_id = ioloop.add_timeout(time.time() + 0.1,
|
||||
self._create_connection)
|
||||
|
||||
def send(self, queue, data, exchange="", message_id=""):
|
||||
properties = pika.BasicProperties(message_id=message_id)
|
||||
self._channel.queue_declare(
|
||||
queue=queue, durable=True,
|
||||
callback=lambda frame: self._channel.basic_publish(
|
||||
exchange=exchange, routing_key=queue,
|
||||
body=data, properties=properties))
|
||||
from eventlet import patcher
|
||||
puka = patcher.import_patched('puka')
|
||||
#import puka
|
||||
import anyjson
|
||||
import config
|
||||
|
||||
|
||||
class RmqClient(object):
|
||||
def __init__(self):
|
||||
settings = config.CONF.rabbitmq
|
||||
self._client = puka.Client('amqp://{0}:{1}@{2}:{3}/{4}'.format(
|
||||
settings.login,
|
||||
settings.password,
|
||||
settings.host,
|
||||
settings.port,
|
||||
settings.virtual_host
|
||||
))
|
||||
self._connected = False
|
||||
|
||||
def __enter__(self):
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
return False
|
||||
|
||||
def connect(self):
|
||||
if not self._connected:
|
||||
promise = self._client.connect()
|
||||
self._client.wait(promise, timeout=10000)
|
||||
self._connected = True
|
||||
|
||||
def close(self):
|
||||
if self._connected:
|
||||
self._client.close()
|
||||
self._connected = False
|
||||
|
||||
def declare(self, queue, exchange=None):
|
||||
promise = self._client.queue_declare(str(queue), durable=True)
|
||||
self._client.wait(promise)
|
||||
|
||||
if exchange:
|
||||
promise = self._client.exchange_declare(str(exchange), durable=True)
|
||||
self._client.wait(promise)
|
||||
promise = self._client.queue_bind(
|
||||
str(queue), str(exchange), routing_key=str(queue))
|
||||
self._client.wait(promise)
|
||||
|
||||
def send(self, message, key, exchange='', timeout=None):
|
||||
if not self._connected:
|
||||
raise RuntimeError('Not connected to RabbitMQ')
|
||||
|
||||
headers = { 'message_id': message.id }
|
||||
|
||||
promise = self._client.basic_publish(
|
||||
exchange=str(exchange),
|
||||
routing_key=str(key),
|
||||
body=anyjson.dumps(message.body),
|
||||
headers=headers)
|
||||
self._client.wait(promise, timeout=timeout)
|
||||
|
||||
def open(self, queue):
|
||||
if not self._connected:
|
||||
raise RuntimeError('Not connected to RabbitMQ')
|
||||
|
||||
return Subscription(self._client, queue)
|
||||
|
||||
|
||||
class Subscription(object):
|
||||
def __init__(self, client, queue):
|
||||
self._client = client
|
||||
self._queue = queue
|
||||
self._promise = None
|
||||
self._lastMessage = None
|
||||
|
||||
def __enter__(self):
|
||||
self._promise = self._client.basic_consume(
|
||||
queue=self._queue,
|
||||
prefetch_count=1)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self._ack_last()
|
||||
promise = self._client.basic_cancel(self._promise)
|
||||
self._client.wait(promise)
|
||||
return False
|
||||
|
||||
def _ack_last(self):
|
||||
if self._lastMessage:
|
||||
self._client.basic_ack(self._lastMessage)
|
||||
self._lastMessage = None
|
||||
|
||||
def get_message(self, timeout=None):
|
||||
if not self._promise:
|
||||
raise RuntimeError(
|
||||
"Subscription object must be used within 'with' block")
|
||||
self._ack_last()
|
||||
self._lastMessage = self._client.wait(self._promise, timeout=timeout)
|
||||
#print self._lastMessage
|
||||
msg = Message()
|
||||
msg.body = anyjson.loads(self._lastMessage['body'])
|
||||
msg.id = self._lastMessage['headers'].get('message_id')
|
||||
return msg
|
||||
|
||||
|
||||
class Message(object):
|
||||
def __init__(self):
|
||||
self._body = {}
|
||||
self._id = ''
|
||||
|
||||
@property
|
||||
def body(self):
|
||||
return self._body
|
||||
|
||||
@body.setter
|
||||
def body(self, value):
|
||||
self._body = value
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@id.setter
|
||||
def id(self, value):
|
||||
self._id = value or ''
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import xml_code_engine
|
||||
import json
|
||||
import rabbitmq
|
||||
|
||||
|
||||
class Reporter(object):
|
||||
@ -7,16 +7,23 @@ class Reporter(object):
|
||||
self._rmqclient = rmqclient
|
||||
self._task_id = task_id
|
||||
self._environment_id = environment_id
|
||||
rmqclient.declare('task-reports')
|
||||
|
||||
def _report_func(self, id, entity, text, **kwargs):
|
||||
msg = json.dumps({
|
||||
body = {
|
||||
'id': id,
|
||||
'entity': entity,
|
||||
'text': text,
|
||||
'environment_id': self._environment_id
|
||||
})
|
||||
}
|
||||
|
||||
msg = rabbitmq.Message()
|
||||
msg.body = body
|
||||
msg.id = self._task_id
|
||||
|
||||
self._rmqclient.send(
|
||||
queue='task-reports', data=msg, message_id=self._task_id)
|
||||
message=msg,
|
||||
key='task-reports')
|
||||
|
||||
|
||||
def _report_func(context, id, entity, text, **kwargs):
|
||||
|
@ -1,7 +1,6 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# All Rights Reserved.
|
||||
# Copyright 2012 OpenStack Foundation
|
||||
#
|
||||
# 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
|
||||
@ -15,5 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
# TODO(jaypipes) Code in this module is intended to be ported to the eventual
|
||||
# openstack-common library
|
||||
|
||||
from conductor.openstack.common import version as common_version
|
||||
|
||||
version_info = common_version.VersionInfo('conductor')
|
@ -1,27 +1,29 @@
|
||||
import xml_code_engine
|
||||
|
||||
|
||||
def send_command(engine, context, body, template, host, mappings=None,
|
||||
result=None, **kwargs):
|
||||
if not mappings:
|
||||
mappings = {}
|
||||
command_dispatcher = context['/commandDispatcher']
|
||||
|
||||
def callback(result_value):
|
||||
msg = "Received result for %s: %s. Body is %s"
|
||||
print msg % (template, result_value, body)
|
||||
if result is not None:
|
||||
context[result] = result_value['Result']
|
||||
|
||||
success_handler = body.find('success')
|
||||
if success_handler is not None:
|
||||
engine.evaluate_content(success_handler, context)
|
||||
|
||||
command_dispatcher.execute(name='agent',
|
||||
template=template,
|
||||
mappings=mappings,
|
||||
host=host,
|
||||
callback=callback)
|
||||
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(send_command, "send-command")
|
||||
import xml_code_engine
|
||||
|
||||
from openstack.common import log as logging
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def send_command(engine, context, body, template, service, host, mappings=None,
|
||||
result=None, **kwargs):
|
||||
if not mappings:
|
||||
mappings = {}
|
||||
command_dispatcher = context['/commandDispatcher']
|
||||
|
||||
def callback(result_value):
|
||||
log.info(
|
||||
'Received result from {2} for {0}: {1}'.format(
|
||||
template, result_value, host))
|
||||
if result is not None:
|
||||
context[result] = result_value['Result']
|
||||
|
||||
success_handler = body.find('success')
|
||||
if success_handler is not None:
|
||||
engine.evaluate_content(success_handler, context)
|
||||
|
||||
command_dispatcher.execute(
|
||||
name='agent', template=template, mappings=mappings,
|
||||
host=host, service=service, callback=callback)
|
||||
|
||||
|
||||
xml_code_engine.XmlCodeEngine.register_function(send_command, "send-command")
|
@ -17,21 +17,15 @@ class Workflow(object):
|
||||
self._reporter = reporter
|
||||
|
||||
def execute(self):
|
||||
while True:
|
||||
context = function_context.Context()
|
||||
context['/dataSource'] = self._data
|
||||
context['/commandDispatcher'] = self._command_dispatcher
|
||||
context['/config'] = self._config
|
||||
context['/reporter'] = self._reporter
|
||||
if not self._engine.execute(context):
|
||||
break
|
||||
context = function_context.Context()
|
||||
context['/dataSource'] = self._data
|
||||
context['/commandDispatcher'] = self._command_dispatcher
|
||||
context['/config'] = self._config
|
||||
context['/reporter'] = self._reporter
|
||||
return self._engine.execute(context)
|
||||
|
||||
@staticmethod
|
||||
def _get_path(obj, path, create_non_existing=False):
|
||||
# result = jsonpath.jsonpath(obj, '.'.join(path))
|
||||
# if not result or len(result) < 1:
|
||||
# return None
|
||||
# return result[0]
|
||||
current = obj
|
||||
for part in path:
|
||||
if isinstance(current, types.ListType):
|
||||
@ -118,6 +112,7 @@ class Workflow(object):
|
||||
if Workflow._get_path(data, position) != body_data:
|
||||
Workflow._set_path(data, position, body_data)
|
||||
context['/hasSideEffects'] = True
|
||||
|
||||
else:
|
||||
data = context['/dataSource']
|
||||
new_position = Workflow._correct_position(path, context)
|
||||
@ -129,8 +124,6 @@ class Workflow(object):
|
||||
def _rule_func(match, context, body, engine, limit=0, name=None, **kwargs):
|
||||
position = context['__dataSource_currentPosition'] or []
|
||||
|
||||
if name == 'marker':
|
||||
print "!"
|
||||
# data = context['__dataSource_currentObj']
|
||||
# if data is None:
|
||||
# data = context['/dataSource']
|
||||
@ -138,21 +131,29 @@ class Workflow(object):
|
||||
data = Workflow._get_path(context['/dataSource'], position)
|
||||
match = re.sub(r'@\.([\w.]+)',
|
||||
r"Workflow._get_path(@, '\1'.split('.'))", match)
|
||||
selected = jsonpath.jsonpath(data, match, 'IPATH') or []
|
||||
|
||||
match = match.replace('$.', '$[*].')
|
||||
selected = jsonpath.jsonpath([data], match, 'IPATH') or []
|
||||
index = 0
|
||||
for found_match in selected:
|
||||
if 0 < int(limit) <= index:
|
||||
break
|
||||
index += 1
|
||||
new_position = position + found_match
|
||||
new_position = position + found_match[1:]
|
||||
context['__dataSource_currentPosition'] = new_position
|
||||
context['__dataSource_currentObj'] = Workflow._get_path(
|
||||
context['/dataSource'], new_position)
|
||||
for element in body:
|
||||
if element.tag == 'empty':
|
||||
continue
|
||||
engine.evaluate(element, context)
|
||||
if element.tag == 'rule' and context['/hasSideEffects']:
|
||||
break
|
||||
if not index:
|
||||
empty_handler = body.find('empty')
|
||||
if empty_handler is not None:
|
||||
|
||||
engine.evaluate_content(empty_handler, context)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _workflow_func(context, body, engine, **kwargs):
|
||||
|
@ -9,22 +9,19 @@ $RestartRequired = $false
|
||||
|
||||
Import-Module CoreFunctions
|
||||
|
||||
if ( $WindowsAgentConfigBase64 -ne '%WINDOWS_AGENT_CONFIG_BASE64%' ) {
|
||||
Write-Log "Updating Keero Windows Agent."
|
||||
Stop-Service "Keero Agent"
|
||||
Backup-File $WindowsAgentConfigFile
|
||||
Remove-Item $WindowsAgentConfigFile -Force
|
||||
ConvertFrom-Base64String -Base64String $WindowsAgentConfigBase64 -Path $WindowsAgentConfigFile
|
||||
Exec sc.exe 'config','"Keero Agent"','start=','delayed-auto'
|
||||
Write-Log "Service has been updated."
|
||||
}
|
||||
Write-Log "Updating Keero Windows Agent."
|
||||
Stop-Service "Keero Agent"
|
||||
Backup-File $WindowsAgentConfigFile
|
||||
Remove-Item $WindowsAgentConfigFile -Force
|
||||
ConvertFrom-Base64String -Base64String $WindowsAgentConfigBase64 -Path $WindowsAgentConfigFile
|
||||
Exec sc.exe 'config','"Keero Agent"','start=','delayed-auto'
|
||||
Write-Log "Service has been updated."
|
||||
|
||||
Write-Log "Renaming computer ..."
|
||||
Rename-Computer -NewName $NewComputerName | Out-Null
|
||||
Write-Log "New name assigned, restart required."
|
||||
$RestartRequired = $true
|
||||
|
||||
if ( $NewComputerName -ne '%INTERNAL_HOSTNAME%' ) {
|
||||
Write-Log "Renaming computer ..."
|
||||
Rename-Computer -NewName $NewComputerName | Out-Null
|
||||
Write-Log "New name assigned, restart required."
|
||||
$RestartRequired = $true
|
||||
}
|
||||
|
||||
Write-Log 'All done!'
|
||||
if ( $RestartRequired ) {
|
||||
|
@ -22,8 +22,9 @@
|
||||
<add key="rabbitmq.user" value="keero"/>
|
||||
<add key="rabbitmq.password" value="keero"/>
|
||||
<add key="rabbitmq.vhost" value="keero"/>
|
||||
<add key="rabbitmq.inputQueue" value="%RABBITMQ_INPUT_QUEUE%"/>
|
||||
<add key="rabbitmq.resultExchange" value=""/>
|
||||
<add key="rabbitmq.resultRoutingKey" value="-execution-results"/>
|
||||
<add key="rabbitmq.resultRoutingKey" value="%RESULT_QUEUE%"/>
|
||||
<add key="rabbitmq.durableMessages" value="true"/>
|
||||
|
||||
</appSettings>
|
||||
|
@ -1,199 +1,218 @@
|
||||
<workflow>
|
||||
<rule match="$.services.activeDirectories[?(@.domain)].units[?(not @.isMaster)]">
|
||||
<set path="domain">
|
||||
<select path="::domain"/>
|
||||
</set>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.instanceName is None)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating instance <select path="name"/></parameter>
|
||||
</report>
|
||||
<update-cf-stack template="Windows">
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="instanceName">
|
||||
<select path="name"/>
|
||||
</mapping>
|
||||
<mapping name="userData">
|
||||
<prepare_user_data/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<parameter name="arguments">
|
||||
<map>
|
||||
<argument name="KeyName">keero-linux-keys</argument>
|
||||
<argument name="InstanceType">m1.medium</argument>
|
||||
<argument name="ImageName">ws-2012-full</argument>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.instanceName"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Instance <select path="name"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</update-cf-stack>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.instanceName and @.adminPassword and @.adminPassword != @.state.adminPassword)]">
|
||||
<send-command template="SetPassword">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="adminPassword">
|
||||
<select path="adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.adminPassword">
|
||||
<select path="adminPassword"/>
|
||||
</set>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.adminPassword and @.adminPassword != @.state.domainAdminPassword)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<send-command template="SetPassword">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="adminPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.domainAdminPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</set>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.state.primaryDc is None)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating Primary Domain Controller on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="CreatePrimaryDC">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="domain">
|
||||
<select path="::domain"/>
|
||||
</mapping>
|
||||
<mapping name="recoveryPassword">
|
||||
<select path="recoveryPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.primaryDc"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Primary Domain Controller created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.state.primaryDc and not @.state.primaryDcIp)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<send-command template="AskDnsIp" result="ip">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.primaryDcIp">
|
||||
<select source="ip" path="0.Result.0"/>
|
||||
</set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">DNS IP = <select source="ip" path="0.Result.0"/></parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$..units[?(@.state.instanceName and @.domain and @.domain != @.state.domain)]">
|
||||
<set path="#unit">
|
||||
<select/>
|
||||
</set>
|
||||
<rule>
|
||||
<parameter name="match">/$.services.activeDirectories[?(@.domain == '<select path="domain"/>' and @.state.primaryDcIp)]</parameter>
|
||||
|
||||
<send-command template="JoinDomain">
|
||||
<parameter name="host">
|
||||
<select path="name" source="unit"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="domain">
|
||||
<select path="domain"/>
|
||||
</mapping>
|
||||
<mapping name="domainPassword">
|
||||
<select path="adminPassword"/>
|
||||
</mapping>
|
||||
<mapping name="dnsIp">
|
||||
<select path="state.primaryDcIp"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.domain" target="unit">
|
||||
<select path="domain"/>
|
||||
</set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id" source="unit"/></parameter>
|
||||
<parameter name="text">Unit <select path="name" source="unit"/> has joined domain <select path="domain"/></parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
</rule>
|
||||
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.domain and not @.isMaster and not @.state.installed)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating Secondary Domain Controller on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="CreateSecondaryDC">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="recoveryPassword">
|
||||
<select path="recoveryPassword"/>
|
||||
</mapping>
|
||||
<mapping name="domainPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.installed"><true/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Secondary Domain Controller created</parameter>
|
||||
</report>
|
||||
<report entity="service">
|
||||
<parameter name="id"><select path="::id"/></parameter>
|
||||
<parameter name="text">Domain <select path="::domain"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
<workflow>
|
||||
<rule match="$.services.activeDirectories[?(@.domain)].units[?(not @.isMaster)]">
|
||||
<set path="domain">
|
||||
<select path="::domain"/>
|
||||
</set>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.hostname and not @.state.instanceName)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating instance <select path="name"/></parameter>
|
||||
</report>
|
||||
<update-cf-stack template="Windows">
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="instanceName"><select path="::name"/>-<select path="name"/></mapping>
|
||||
<mapping name="userData">
|
||||
<prepare-user-data>
|
||||
<parameter name="hostname"><select path="state.hostname"/></parameter>
|
||||
<parameter name="unit"><select path="name"/></parameter>
|
||||
<parameter name="service"><select path="::id"/></parameter>
|
||||
</prepare-user-data>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<parameter name="arguments">
|
||||
<map>
|
||||
<argument name="KeyName">keero-keys</argument>
|
||||
<argument name="InstanceType">m1.medium</argument>
|
||||
<argument name="ImageName">ws-2012-full</argument>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.instanceName"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Instance <select path="name"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</update-cf-stack>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.instanceName and @.adminPassword and @.adminPassword != @.state.adminPassword)]">
|
||||
<send-command template="SetPassword">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="adminPassword">
|
||||
<select path="adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.adminPassword">
|
||||
<select path="adminPassword"/>
|
||||
</set>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.adminPassword and @.adminPassword != @.state.domainAdminPassword)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<send-command template="SetPassword">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="adminPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.domainAdminPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</set>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.state.primaryDc is None)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating Primary Domain Controller on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="CreatePrimaryDC">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="domain">
|
||||
<select path="::domain"/>
|
||||
</mapping>
|
||||
<mapping name="recoveryPassword">
|
||||
<select path="recoveryPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.primaryDc"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Primary Domain Controller created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.activeDirectories[?(@.state.primaryDc and not @.state.primaryDcIp)].units[?(@.state.instanceName and @.isMaster)]">
|
||||
<send-command template="AskDnsIp" result="ip">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="::state.primaryDcIp">
|
||||
<select source="ip" path="0.Result.0"/>
|
||||
</set>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
|
||||
<rule match="$..units[?(@.state.instanceName and @.domain and @.domain != @.state.domain)]">
|
||||
<set path="#unit">
|
||||
<select/>
|
||||
</set>
|
||||
<set path="#service">
|
||||
<select path="::"/>
|
||||
</set>
|
||||
<rule>
|
||||
<parameter name="match">/$.services.activeDirectories[?(@.domain == '<select path="domain"/>' and @.state.primaryDcIp)]</parameter>
|
||||
|
||||
<send-command template="JoinDomain">
|
||||
<parameter name="host">
|
||||
<select path="name" source="unit"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="id" source="service"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="domain">
|
||||
<select path="domain"/>
|
||||
</mapping>
|
||||
<mapping name="domainPassword">
|
||||
<select path="adminPassword"/>
|
||||
</mapping>
|
||||
<mapping name="dnsIp">
|
||||
<select path="state.primaryDcIp"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.domain" target="unit">
|
||||
<select path="domain"/>
|
||||
</set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id" source="unit"/></parameter>
|
||||
<parameter name="text">Unit <select path="name" source="unit"/> has joined domain <select path="domain"/></parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
</rule>
|
||||
|
||||
|
||||
<rule match="$.services.activeDirectories[*].units[?(@.state.domain and not @.isMaster and not @.state.installed)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating Secondary Domain Controller on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="CreateSecondaryDC">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="recoveryPassword">
|
||||
<select path="recoveryPassword"/>
|
||||
</mapping>
|
||||
<mapping name="domainPassword">
|
||||
<select path="::adminPassword"/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.installed"><true/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Secondary Domain Controller created</parameter>
|
||||
</report>
|
||||
<report entity="service">
|
||||
<parameter name="id"><select path="::id"/></parameter>
|
||||
<parameter name="text">Domain <select path="::domain"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
</workflow>
|
19
conductor/data/workflows/Common.xml
Normal file
19
conductor/data/workflows/Common.xml
Normal file
@ -0,0 +1,19 @@
|
||||
<workflow>
|
||||
|
||||
<rule match="$.services[*][*].units[?(@.state.hostname is None)]">
|
||||
<set path="state.hostname"><generate-hostname/></set>
|
||||
</rule>
|
||||
|
||||
<rule match="$[?(not @.state.deleted)]">
|
||||
<rule match="$.services[*][*].units[*]">
|
||||
<empty>
|
||||
<delete-cf-stack>
|
||||
<success>
|
||||
<set path="/state.deleted"><true/></set>
|
||||
</success>
|
||||
</delete-cf-stack>
|
||||
</empty>
|
||||
</rule>
|
||||
</rule>
|
||||
|
||||
</workflow>
|
@ -1,60 +1,65 @@
|
||||
<workflow>
|
||||
<rule match="$.services.webServers[?(@.domain)].units[*]">
|
||||
<set path="domain">
|
||||
<select path="::domain"/>
|
||||
</set>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.webServers[*].units[?(@.state.instanceName is None)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating instance <select path="name"/></parameter>
|
||||
</report>
|
||||
<update-cf-stack template="Windows">
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="instanceName">
|
||||
<select path="name"/>
|
||||
</mapping>
|
||||
<mapping name="userData">
|
||||
<prepare_user_data/>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<parameter name="arguments">
|
||||
<map>
|
||||
<argument name="KeyName">keero-linux-keys</argument>
|
||||
<argument name="InstanceType">m1.medium</argument>
|
||||
<argument name="ImageName">ws-2012-full</argument>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.instanceName"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Instance <select path="name"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</update-cf-stack>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.webServers[*].units[?(@.state.instanceName and not @.state.iisInstalled)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating IIS Web Server on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="InstallIIS">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.iisInstalled"><true/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">IIS <select path="name"/> has started</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
<workflow>
|
||||
<rule match="$.services.webServers[?(@.domain)].units[*]">
|
||||
<set path="domain">
|
||||
<select path="::domain"/>
|
||||
</set>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.webServers[*].units[?(@.state.hostname and not @.state.instanceName)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating instance <select path="name"/></parameter>
|
||||
</report>
|
||||
<update-cf-stack template="Windows">
|
||||
<parameter name="mappings">
|
||||
<map>
|
||||
<mapping name="instanceName"><select path="::name"/>-<select path="name"/></mapping>
|
||||
<mapping name="userData">
|
||||
<prepare-user-data>
|
||||
<parameter name="hostname"><select path="state.hostname"/></parameter>
|
||||
<parameter name="unit"><select path="name"/></parameter>
|
||||
<parameter name="service"><select path="::id"/></parameter>
|
||||
</prepare-user-data>
|
||||
</mapping>
|
||||
</map>
|
||||
</parameter>
|
||||
<parameter name="arguments">
|
||||
<map>
|
||||
<argument name="KeyName">keero-keys</argument>
|
||||
<argument name="InstanceType">m1.medium</argument>
|
||||
<argument name="ImageName">ws-2012-full</argument>
|
||||
</map>
|
||||
</parameter>
|
||||
|
||||
<success>
|
||||
<set path="state.instanceName"><select path="name"/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Instance <select path="name"/> created</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</update-cf-stack>
|
||||
</rule>
|
||||
|
||||
<rule match="$.services.webServers[*].units[?(@.state.instanceName and not @.state.iisInstalled)]">
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">Creating IIS Web Server on unit <select path="name"/></parameter>
|
||||
</report>
|
||||
<send-command template="InstallIIS">
|
||||
<parameter name="host">
|
||||
<select path="name"/>
|
||||
</parameter>
|
||||
<parameter name="service">
|
||||
<select path="::id"/>
|
||||
</parameter>
|
||||
<success>
|
||||
<set path="state.iisInstalled"><true/></set>
|
||||
<report entity="unit">
|
||||
<parameter name="id"><select path="id"/></parameter>
|
||||
<parameter name="text">IIS <select path="name"/> has started</parameter>
|
||||
</report>
|
||||
</success>
|
||||
</send-command>
|
||||
</rule>
|
||||
</workflow>
|
BIN
conductor/doc/source/_static/header-line.gif
Normal file
BIN
conductor/doc/source/_static/header-line.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 B |
BIN
conductor/doc/source/_static/header_bg.jpg
Normal file
BIN
conductor/doc/source/_static/header_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
@ -1,5 +1,5 @@
|
||||
(function($) {
|
||||
|
||||
|
||||
$.fn.tweet = function(o){
|
||||
var s = {
|
||||
username: ["seaofclouds"], // [string] required, unless you want to display our tweets. :) it can be an array, just do ["username1","username2","etc"]
|
||||
@ -17,9 +17,9 @@
|
||||
loading_text: null, // [string] optional loading text, displayed while tweets load
|
||||
query: null // [string] optional search query
|
||||
};
|
||||
|
||||
|
||||
if(o) $.extend(s, o);
|
||||
|
||||
|
||||
$.fn.extend({
|
||||
linkUrl: function() {
|
||||
var returning = [];
|
245
conductor/doc/source/_static/nature.css
Normal file
245
conductor/doc/source/_static/nature.css
Normal file
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* nature.css_t
|
||||
* ~~~~~~~~~~~~
|
||||
*
|
||||
* Sphinx stylesheet -- nature theme.
|
||||
*
|
||||
* :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS.
|
||||
* :license: BSD, see LICENSE for details.
|
||||
*
|
||||
*/
|
||||
|
||||
@import url("basic.css");
|
||||
|
||||
/* -- page layout ----------------------------------------------------------- */
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
background-color: #111;
|
||||
color: #555;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
div.documentwrapper {
|
||||
float: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.bodywrapper {
|
||||
margin: 0 0 0 {{ theme_sidebarwidth|toint }}px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 1px solid #B1B4B6;
|
||||
}
|
||||
|
||||
div.document {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.body {
|
||||
background-color: #ffffff;
|
||||
color: #3E4349;
|
||||
padding: 0 30px 30px 30px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
div.footer {
|
||||
color: #555;
|
||||
width: 100%;
|
||||
padding: 13px 0;
|
||||
text-align: center;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
div.footer a {
|
||||
color: #444;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #6BA81E;
|
||||
line-height: 32px;
|
||||
color: #fff;
|
||||
text-shadow: 0px 1px 0 #444;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #E2F3CC;
|
||||
}
|
||||
|
||||
div.sphinxsidebar {
|
||||
font-size: 0.75em;
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper{
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3,
|
||||
div.sphinxsidebar h4 {
|
||||
font-family: Arial, sans-serif;
|
||||
color: #222;
|
||||
font-size: 1.2em;
|
||||
font-weight: normal;
|
||||
margin: 0;
|
||||
padding: 5px 10px;
|
||||
background-color: #ddd;
|
||||
text-shadow: 1px 1px 0 white
|
||||
}
|
||||
|
||||
div.sphinxsidebar h4{
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar h3 a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
|
||||
div.sphinxsidebar p {
|
||||
color: #888;
|
||||
padding: 5px 20px;
|
||||
}
|
||||
|
||||
div.sphinxsidebar p.topless {
|
||||
}
|
||||
|
||||
div.sphinxsidebar ul {
|
||||
margin: 10px 20px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
div.sphinxsidebar a {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input {
|
||||
border: 1px solid #ccc;
|
||||
font-family: sans-serif;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
div.sphinxsidebar input[type=text]{
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
/* -- body styles ----------------------------------------------------------- */
|
||||
|
||||
a {
|
||||
color: #005B81;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #E32E00;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
div.body h1,
|
||||
div.body h2,
|
||||
div.body h3,
|
||||
div.body h4,
|
||||
div.body h5,
|
||||
div.body h6 {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #BED4EB;
|
||||
font-weight: normal;
|
||||
color: #212224;
|
||||
margin: 30px 0px 10px 0px;
|
||||
padding: 5px 0 5px 10px;
|
||||
text-shadow: 0px 1px 0 white
|
||||
}
|
||||
|
||||
div.body h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
|
||||
div.body h2 { font-size: 150%; background-color: #C8D5E3; }
|
||||
div.body h3 { font-size: 120%; background-color: #D8DEE3; }
|
||||
div.body h4 { font-size: 110%; background-color: #D8DEE3; }
|
||||
div.body h5 { font-size: 100%; background-color: #D8DEE3; }
|
||||
div.body h6 { font-size: 100%; background-color: #D8DEE3; }
|
||||
|
||||
a.headerlink {
|
||||
color: #c60f0f;
|
||||
font-size: 0.8em;
|
||||
padding: 0 4px 0 4px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.headerlink:hover {
|
||||
background-color: #c60f0f;
|
||||
color: white;
|
||||
}
|
||||
|
||||
div.body p, div.body dd, div.body li {
|
||||
line-height: 1.5em;
|
||||
}
|
||||
|
||||
div.admonition p.admonition-title + p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.highlight{
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
div.note {
|
||||
background-color: #eee;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
div.seealso {
|
||||
background-color: #ffc;
|
||||
border: 1px solid #ff6;
|
||||
}
|
||||
|
||||
div.topic {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
div.warning {
|
||||
background-color: #ffe4e4;
|
||||
border: 1px solid #f66;
|
||||
}
|
||||
|
||||
p.admonition-title {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
p.admonition-title:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 10px;
|
||||
background-color: White;
|
||||
color: #222;
|
||||
line-height: 1.2em;
|
||||
border: 1px solid #C6C9CB;
|
||||
font-size: 1.1em;
|
||||
margin: 1.5em 0 1.5em 0;
|
||||
-webkit-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
-moz-box-shadow: 1px 1px 1px #d8d8d8;
|
||||
}
|
||||
|
||||
tt {
|
||||
background-color: #ecf0f3;
|
||||
color: #222;
|
||||
/* padding: 1px 2px; */
|
||||
font-size: 1.1em;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.viewcode-back {
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
div.viewcode-block:target {
|
||||
background-color: #f4debf;
|
||||
border-top: 1px solid #ac9;
|
||||
border-bottom: 1px solid #ac9;
|
||||
}
|
BIN
conductor/doc/source/_static/openstack_logo.png
Normal file
BIN
conductor/doc/source/_static/openstack_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
94
conductor/doc/source/_static/tweaks.css
Normal file
94
conductor/doc/source/_static/tweaks.css
Normal file
@ -0,0 +1,94 @@
|
||||
body {
|
||||
background: #fff url(../_static/header_bg.jpg) top left no-repeat;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 950px;
|
||||
margin: 0 auto;
|
||||
height: 102px;
|
||||
}
|
||||
|
||||
#header h1#logo {
|
||||
background: url(../_static/openstack_logo.png) top left no-repeat;
|
||||
display: block;
|
||||
float: left;
|
||||
text-indent: -9999px;
|
||||
width: 175px;
|
||||
height: 55px;
|
||||
}
|
||||
|
||||
#navigation {
|
||||
background: url(../_static/header-line.gif) repeat-x 0 bottom;
|
||||
display: block;
|
||||
float: left;
|
||||
margin: 27px 0 0 25px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#navigation li{
|
||||
float: left;
|
||||
display: block;
|
||||
margin-right: 25px;
|
||||
}
|
||||
|
||||
#navigation li a {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
text-decoration: none;
|
||||
background-position: 50% 0;
|
||||
padding: 20px 0 5px;
|
||||
color: #353535;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#navigation li a.current, #navigation li a.section {
|
||||
border-bottom: 3px solid #cf2f19;
|
||||
color: #cf2f19;
|
||||
}
|
||||
|
||||
div.related {
|
||||
background-color: #cde2f8;
|
||||
border: 1px solid #b0d3f8;
|
||||
}
|
||||
|
||||
div.related a {
|
||||
color: #4078ba;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
div.sphinxsidebarwrapper {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
div.documentwrapper h1, div.documentwrapper h2, div.documentwrapper h3, div.documentwrapper h4, div.documentwrapper h5, div.documentwrapper h6 {
|
||||
font-family: 'PT Sans', sans-serif !important;
|
||||
color: #264D69;
|
||||
border-bottom: 1px dotted #C5E2EA;
|
||||
padding: 0;
|
||||
background: none;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
div.documentwrapper h3 {
|
||||
color: #CF2F19;
|
||||
}
|
||||
|
||||
a.headerlink {
|
||||
color: #fff !important;
|
||||
margin-left: 5px;
|
||||
background: #CF2F19 !important;
|
||||
}
|
||||
|
||||
div.body {
|
||||
margin-top: -25px;
|
||||
margin-left: 230px;
|
||||
}
|
||||
|
||||
div.document {
|
||||
width: 960px;
|
||||
margin: 0 auto;
|
||||
}
|
@ -1,19 +1,6 @@
|
||||
{% extends "sphinxdoc/layout.html" %}
|
||||
{% extends "basic/layout.html" %}
|
||||
{% set css_files = css_files + ['_static/tweaks.css'] %}
|
||||
{% set script_files = script_files + ['_static/jquery.tweet.js'] %}
|
||||
{% block extrahead %}
|
||||
<script type='text/javascript'>
|
||||
$(document).ready(function(){
|
||||
$("#twitter_feed").tweet({
|
||||
username: "openstack",
|
||||
query: "from:openstack",
|
||||
avatar_size: 32,
|
||||
count: 10,
|
||||
loading_text: "loading tweets..."
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{%- macro sidebar() %}
|
||||
{%- if not embedded %}{% if not theme_nosidebar|tobool %}
|
||||
@ -72,15 +59,25 @@
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
|
||||
{%- if pagename == "index" %}
|
||||
<h3>{{ _('Twitter Feed') }}</h3>
|
||||
<div id="twitter_feed" class='twitter_feed'></div>
|
||||
{%- endif %}
|
||||
|
||||
|
||||
{%- endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% block relbar1 %}{% endblock relbar1 %}
|
||||
|
||||
{% block header %}
|
||||
<div id="header">
|
||||
<h1 id="logo"><a href="http://www.openstack.org/">OpenStack</a></h1>
|
||||
<ul id="navigation">
|
||||
<li><a href="http://www.openstack.org/" title="Go to the Home page" class="link">Home</a></li>
|
||||
<li><a href="http://www.openstack.org/projects/" title="Go to the OpenStack Projects page">Projects</a></li>
|
||||
<li><a href="http://www.openstack.org/user-stories/" title="Go to the User Stories page" class="link">User Stories</a></li>
|
||||
<li><a href="http://www.openstack.org/community/" title="Go to the Community page" class="link">Community</a></li>
|
||||
<li><a href="http://www.openstack.org/blog/" title="Go to the OpenStack Blog">Blog</a></li>
|
||||
<li><a href="http://wiki.openstack.org/" title="Go to the OpenStack Wiki">Wiki</a></li>
|
||||
<li><a href="http://docs.openstack.org/" title="Go to OpenStack Documentation" class="current">Documentation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
4
conductor/doc/source/_theme/theme.conf
Normal file
4
conductor/doc/source/_theme/theme.conf
Normal file
@ -0,0 +1,4 @@
|
||||
[theme]
|
||||
inherit = basic
|
||||
stylesheet = nature.css
|
||||
pygments_style = tango
|
@ -1,5 +1,6 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2011 OpenStack, LLC.
|
||||
# Copyright (c) 2010 OpenStack Foundation.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
@ -15,10 +16,10 @@
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# Skeleton documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue May 18 13:50:15 2010.
|
||||
# Conductor documentation build configuration file, created by
|
||||
# sphinx-quickstart on Tue February 28 13:50:15 2013.
|
||||
#
|
||||
# This file is execfile()'d with the current directory set to it's containing
|
||||
# This file is execfile()'d with the current directory set to its containing
|
||||
# dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
@ -33,24 +34,20 @@ import sys
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
sys.path.append([os.path.abspath('../skeleton'),
|
||||
os.path.abspath('..'),
|
||||
os.path.abspath('../bin')
|
||||
])
|
||||
sys.path = [os.path.abspath('../../conductor'),
|
||||
os.path.abspath('../..'),
|
||||
os.path.abspath('../../bin')
|
||||
] + sys.path
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = ['sphinx.ext.autodoc',
|
||||
'sphinx.ext.coverage',
|
||||
extensions = ['sphinx.ext.coverage',
|
||||
'sphinx.ext.ifconfig',
|
||||
'sphinx.ext.intersphinx',
|
||||
'sphinx.ext.pngmath',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.todo']
|
||||
|
||||
todo_include_todos = True
|
||||
'sphinx.ext.graphviz']
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = []
|
||||
@ -69,19 +66,19 @@ source_suffix = '.rst'
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'Skeleton'
|
||||
copyright = u'2011-present, OpenStack, LLC.'
|
||||
project = u'Conductor'
|
||||
copyright = u'2013, Mirantis, Inc.'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
from skeleton import version as skeleton_version
|
||||
from conductor.version import version_info as conductor_version
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = skeleton_version.version_string()
|
||||
release = conductor_version.version_string_with_vcs()
|
||||
# The short X.Y version.
|
||||
version = skeleton_version.canonical_version_string()
|
||||
version = conductor_version.canonical_version_string()
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@ -98,7 +95,7 @@ version = skeleton_version.canonical_version_string()
|
||||
|
||||
# List of directories, relative to source directory, that shouldn't be searched
|
||||
# for source files.
|
||||
exclude_trees = []
|
||||
exclude_trees = ['api']
|
||||
|
||||
# The reST default role (for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
@ -118,7 +115,7 @@ show_authors = True
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
modindex_common_prefix = ['skeleton.']
|
||||
modindex_common_prefix = ['portas.']
|
||||
|
||||
# -- Options for man page output --------------------------------------------
|
||||
|
||||
@ -126,13 +123,9 @@ modindex_common_prefix = ['skeleton.']
|
||||
# List of tuples 'sourcefile', 'target', u'title', u'Authors name', 'manual'
|
||||
|
||||
man_pages = [
|
||||
('man/skeletonapi', 'skeleton-api', u'Skeleton API Server',
|
||||
[u'OpenStack'], 1),
|
||||
('man/skeletonregistry', 'skeleton-registry', u'Skeleton Registry Server',
|
||||
[u'OpenStack'], 1),
|
||||
('man/skeletonmanage', 'skeleton-manage', u'Skeleton Management Utility',
|
||||
[u'OpenStack'], 1)
|
||||
]
|
||||
('man/conductor', 'conductor', u'Conductor Orchestrator',
|
||||
[u'Mirantis, Inc.'], 1)
|
||||
]
|
||||
|
||||
|
||||
# -- Options for HTML output -------------------------------------------------
|
||||
@ -174,6 +167,8 @@ html_static_path = ['_static']
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
git_cmd = "git log --pretty=format:'%ad, commit %h' --date=local -n1"
|
||||
html_last_updated_fmt = os.popen(git_cmd).read()
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
@ -187,10 +182,10 @@ html_static_path = ['_static']
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_use_modindex = True
|
||||
html_use_modindex = False
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
html_use_index = False
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
@ -207,7 +202,7 @@ html_static_path = ['_static']
|
||||
#html_file_suffix = ''
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'skeletondoc'
|
||||
htmlhelp_basename = 'conductordoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ------------------------------------------------
|
||||
@ -222,8 +217,8 @@ htmlhelp_basename = 'skeletondoc'
|
||||
# (source start file, target name, title, author,
|
||||
# documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'Skeleton.tex', u'Skeleton Documentation',
|
||||
u'Skeleton Team', 'manual'),
|
||||
('index', 'Conductor.tex', u'Conductor Documentation',
|
||||
u'Keero Team', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
@ -244,9 +239,4 @@ latex_documents = [
|
||||
#latex_use_modindex = True
|
||||
|
||||
# Example configuration for intersphinx: refer to the Python standard library.
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/', None),
|
||||
'dashboard': ('http://dashboard.openstack.org', None),
|
||||
'glance': ('http://glance.openstack.org', None),
|
||||
'keystone': ('http://keystone.openstack.org', None),
|
||||
'nova': ('http://nova.openstack.org', None),
|
||||
'swift': ('http://swift.openstack.org', None)}
|
||||
intersphinx_mapping = {'python': ('http://docs.python.org/', None)}
|
@ -1,5 +1,5 @@
|
||||
..
|
||||
Copyright 2011 OpenStack, LLC.
|
||||
Copyright 2013, Mirantis Inc.
|
||||
All Rights Reserved.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -14,40 +14,7 @@
|
||||
License for the specific language governing permissions and limitations
|
||||
under the License.
|
||||
|
||||
Welcome to Skeleton's documentation!
|
||||
===================================
|
||||
Welcome to Conductor's documentation!
|
||||
==================================
|
||||
|
||||
Description of Skeleton project
|
||||
|
||||
Concepts
|
||||
========
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Using Skeleton
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
gettingstarted
|
||||
installing
|
||||
|
||||
Developer Docs
|
||||
==============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
Outstanding Documentation Tasks
|
||||
===============================
|
||||
|
||||
.. todolist::
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
We rule the world!
|
@ -1,5 +0,0 @@
|
||||
[rabbitmq]
|
||||
host = localhost
|
||||
vhost = keero
|
||||
login = keero
|
||||
password = keero
|
14
conductor/etc/conductor.conf
Normal file
14
conductor/etc/conductor.conf
Normal file
@ -0,0 +1,14 @@
|
||||
[DEFAULT]
|
||||
log_file = logs/conductor.log
|
||||
debug=True
|
||||
verbose=True
|
||||
|
||||
[heat]
|
||||
auth_url = http://172.18.124.101:5000/v2.0
|
||||
|
||||
[rabbitmq]
|
||||
host = 172.18.124.101
|
||||
port = 5672
|
||||
virtual_host = keero
|
||||
login = keero
|
||||
password = keero
|
4
conductor/logs/.gitignore
vendored
Normal file
4
conductor/logs/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
7
conductor/openstack-common.conf
Normal file
7
conductor/openstack-common.conf
Normal file
@ -0,0 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from openstack-common
|
||||
modules=setup,wsgi,config,exception,gettextutils,importutils,jsonutils,log,xmlutils,sslutils,service,notifier,local,install_venv_common,version,timeutils,eventlet_backdoor,threadgroup,loopingcall,uuidutils
|
||||
|
||||
# The base module to hold the copy of openstack.common
|
||||
base=conductor
|
49
conductor/run_tests.sh
Executable file
49
conductor/run_tests.sh
Executable file
@ -0,0 +1,49 @@
|
||||
#!/bin/bash
|
||||
|
||||
function usage {
|
||||
echo "Usage: $0 [OPTION]..."
|
||||
echo "Run python-portasclient's test suite(s)"
|
||||
echo ""
|
||||
echo " -p, --pep8 Just run pep8"
|
||||
echo " -h, --help Print this usage message"
|
||||
echo ""
|
||||
echo "This script is deprecated and currently retained for compatibility."
|
||||
echo 'You can run the full test suite for multiple environments by running "tox".'
|
||||
echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only'
|
||||
echo 'the pep8 tests with "tox -e pep8".'
|
||||
exit
|
||||
}
|
||||
|
||||
command -v tox > /dev/null 2>&1
|
||||
if [ $? -ne 0 ]; then
|
||||
echo 'This script requires "tox" to run.'
|
||||
echo 'You can install it with "pip install tox".'
|
||||
exit 1;
|
||||
fi
|
||||
|
||||
just_pep8=0
|
||||
|
||||
function process_option {
|
||||
case "$1" in
|
||||
-h|--help) usage;;
|
||||
-p|--pep8) let just_pep8=1;;
|
||||
esac
|
||||
}
|
||||
|
||||
for arg in "$@"; do
|
||||
process_option $arg
|
||||
done
|
||||
|
||||
if [ $just_pep8 -eq 1 ]; then
|
||||
tox -e pep8
|
||||
exit
|
||||
fi
|
||||
|
||||
tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit
|
||||
if [ ${PIPESTATUS[0]} -ne 0 ]; then
|
||||
exit ${PIPESTATUS[0]}
|
||||
fi
|
||||
|
||||
if [ -z "$toxargs" ]; then
|
||||
tox -e pep8
|
||||
fi
|
33
conductor/setup.cfg
Normal file
33
conductor/setup.cfg
Normal file
@ -0,0 +1,33 @@
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = doc/build
|
||||
source-dir = doc/source
|
||||
|
||||
[egg_info]
|
||||
tag_build =
|
||||
tag_date = 0
|
||||
tag_svn_revision = 0
|
||||
|
||||
[compile_catalog]
|
||||
directory = conductor/locale
|
||||
domain = conductor
|
||||
|
||||
[update_catalog]
|
||||
domain = conductor
|
||||
output_dir = conductor/locale
|
||||
input_file = conductor/locale/conductor.pot
|
||||
|
||||
[extract_messages]
|
||||
keywords = _ gettext ngettext l_ lazy_gettext
|
||||
mapping_file = babel.cfg
|
||||
output_file = conductor/locale/conductor.pot
|
||||
|
||||
[nosetests]
|
||||
# NOTE(jkoelker) To run the test suite under nose install the following
|
||||
# coverage http://pypi.python.org/pypi/coverage
|
||||
# tissue http://pypi.python.org/pypi/tissue (pep8 checker)
|
||||
# openstack-nose https://github.com/jkoelker/openstack-nose
|
||||
verbosity=2
|
||||
cover-package = conductor
|
||||
cover-html = true
|
||||
cover-erase = true
|
49
conductor/setup.py
Normal file
49
conductor/setup.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright (c) 2010 OpenStack, LLC.
|
||||
#
|
||||
# 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 setuptools
|
||||
|
||||
from conductor.openstack.common import setup
|
||||
|
||||
requires = setup.parse_requirements()
|
||||
depend_links = setup.parse_dependency_links()
|
||||
project = 'conductor'
|
||||
|
||||
setuptools.setup(
|
||||
name=project,
|
||||
version=setup.get_version(project, '2013.1'),
|
||||
description='The Conductor is orchestration engine server',
|
||||
license='Apache License (2.0)',
|
||||
author='Mirantis, Inc.',
|
||||
author_email='openstack@lists.launchpad.net',
|
||||
url='http://conductor.openstack.org/',
|
||||
packages=setuptools.find_packages(exclude=['bin']),
|
||||
test_suite='nose.collector',
|
||||
cmdclass=setup.get_cmdclass(),
|
||||
include_package_data=True,
|
||||
install_requires=requires,
|
||||
dependency_links=depend_links,
|
||||
classifiers=[
|
||||
'Development Status :: 4 - Beta',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Environment :: No Input/Output (Daemon)',
|
||||
'Environment :: OpenStack',
|
||||
],
|
||||
scripts=['bin/conductor'],
|
||||
py_modules=[]
|
||||
)
|
@ -1,6 +1,7 @@
|
||||
{
|
||||
"name": "MyDataCenter",
|
||||
"name": "MyDataCenterx",
|
||||
"id": "adc6d143f9584d10808c7ef4d07e4802",
|
||||
"token": "MIINIQYJKoZIhvcNAQcCoIINEjCCDQ4CAQExCTAHBgUrDgMCGjCCC-oGCSqGSIb3DQEHAaCCC+sEggvneyJhY2Nlc3MiOiB7InRva2VuIjogeyJpc3N1ZWRfYXQiOiAiMjAxMy0wMy0yNlQwNjo0NTozNy4zOTI0MDAiLCAiZXhwaXJlcyI6ICIyMDEzLTAzLTI3VDA2OjQ1OjM3WiIsICJpZCI6ICJwbGFjZWhvbGRlciIsICJ0ZW5hbnQiOiB7ImRlc2NyaXB0aW9uIjogbnVsbCwgImVuYWJsZWQiOiB0cnVlLCAiaWQiOiAiMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAiLCAibmFtZSI6ICJhZG1pbiJ9fSwgInNlcnZpY2VDYXRhbG9nIjogW3siZW5kcG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzQvdjIvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAiLCAicmVnaW9uIjogIlJlZ2lvbk9uZSIsICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODc3NC92Mi8xNmViNzhjYmI2ODg0NTljODMwOGQ4OTY3OGJjZWY1MCIsICJpZCI6ICIwNGFlNjM2ZTdhYzc0NmJjYjExM2EwYzI5NDYzMzgzMCIsICJwdWJsaWNVUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzQvdjIvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiY29tcHV0ZSIsICJuYW1lIjogIm5vdmEifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6MzMzMyIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVybmFsVVJMIjogImh0dHA6Ly8xNzIuMTguMTI0LjEwMTozMzMzIiwgImlkIjogIjA5MmJkMjMyMGU5ZDRlYWY4ZDBlZjEzNDhjOGU3NTJjIiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6MzMzMyJ9XSwgImVuZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJzMyIsICJuYW1lIjogInMzIn0sIHsiZW5kcG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjkyOTIiLCAicmVnaW9uIjogIlJlZ2lvbk9uZSIsICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6OTI5MiIsICJpZCI6ICI1ZWUzNjdjYzRhNjY0YmQzYTYyNmI2MjBkMzFhYzcwYyIsICJwdWJsaWNVUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjkyOTIifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaW1hZ2UiLCAibmFtZSI6ICJnbGFuY2UifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODAwMC92MSIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVybmFsVVJMIjogImh0dHA6Ly8xNzIuMTguMTI0LjEwMTo4MDAwL3YxIiwgImlkIjogIjM3MzMzYmQwNDkxOTQzY2FiNWEyZGM5N2I5YWQzYjE2IiwgInB1YmxpY1VSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODAwMC92MSJ9XSwgImVuZHBvaW50c19saW5rcyI6IFtdLCAidHlwZSI6ICJjbG91ZGZvcm1hdGlvbiIsICJuYW1lIjogImhlYXQtY2ZuIn0sIHsiZW5kcG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzYvdjEvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAiLCAicmVnaW9uIjogIlJlZ2lvbk9uZSIsICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODc3Ni92MS8xNmViNzhjYmI2ODg0NTljODMwOGQ4OTY3OGJjZWY1MCIsICJpZCI6ICI4NTgwYjMzOTAxZWU0YTUyOWI0OGMyMzU0ZjFiMWNhZSIsICJwdWJsaWNVUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzYvdjEvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAidm9sdW1lIiwgIm5hbWUiOiAiY2luZGVyIn0sIHsiZW5kcG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzMvc2VydmljZXMvQWRtaW4iLCAicmVnaW9uIjogIlJlZ2lvbk9uZSIsICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODc3My9zZXJ2aWNlcy9DbG91ZCIsICJpZCI6ICIwYTViOTIyNTNiZjg0NTAwYTA4OWY1N2VkMmYzZDY3NSIsICJwdWJsaWNVUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjg3NzMvc2VydmljZXMvQ2xvdWQifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiZWMyIiwgIm5hbWUiOiAiZWMyIn0sIHsiZW5kcG9pbnRzIjogW3siYWRtaW5VUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjgwMDQvdjEvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAiLCAicmVnaW9uIjogIlJlZ2lvbk9uZSIsICJpbnRlcm5hbFVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6ODAwNC92MS8xNmViNzhjYmI2ODg0NTljODMwOGQ4OTY3OGJjZWY1MCIsICJpZCI6ICJhMjRjMGY1ZmUzMmQ0ZDU5YWEwMTk1Mzg3OGFlMDQwNyIsICJwdWJsaWNVUkwiOiAiaHR0cDovLzE3Mi4xOC4xMjQuMTAxOjgwMDQvdjEvMTZlYjc4Y2JiNjg4NDU5YzgzMDhkODk2NzhiY2VmNTAifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAib3JjaGVzdHJhdGlvbiIsICJuYW1lIjogImhlYXQifSwgeyJlbmRwb2ludHMiOiBbeyJhZG1pblVSTCI6ICJodHRwOi8vMTcyLjE4LjEyNC4xMDE6MzUzNTcvdjIuMCIsICJyZWdpb24iOiAiUmVnaW9uT25lIiwgImludGVybmFsVVJMIjogImh0dHA6Ly8xNzIuMTguMTI0LjEwMTo1MDAwL3YyLjAiLCAiaWQiOiAiNGM4M2VlYjk3MDA5NDg3M2FiNjg3NjUzNWJlZjgxZWEiLCAicHVibGljVVJMIjogImh0dHA6Ly8xNzIuMTguMTI0LjEwMTo1MDAwL3YyLjAifV0sICJlbmRwb2ludHNfbGlua3MiOiBbXSwgInR5cGUiOiAiaWRlbnRpdHkiLCAibmFtZSI6ICJrZXlzdG9uZSJ9XSwgInVzZXIiOiB7InVzZXJuYW1lIjogImFkbWluIiwgInJvbGVzX2xpbmtzIjogW10sICJpZCI6ICJmMmNkZWM4NTQ2MmQ0N2UzODQ5ZTZmMzE3NGRhMTk4NSIsICJyb2xlcyI6IFt7Im5hbWUiOiAiYWRtaW4ifV0sICJuYW1lIjogImFkbWluIn0sICJtZXRhZGF0YSI6IHsiaXNfYWRtaW4iOiAwLCAicm9sZXMiOiBbIjc4N2JlODdjMGFkMjQ3ODJiNTQ4NWU5NjNhZjllNzllIl19fX0xgf8wgfwCAQEwXDBXMQswCQYDVQQGEwJVUzEOMAwGA1UECBMFVW5zZXQxDjAMBgNVBAcTBVVuc2V0MQ4wDAYDVQQKEwVVbnNldDEYMBYGA1UEAxMPd3d3LmV4YW1wbGUuY29tAgEBMAcGBSsOAwIaMA0GCSqGSIb3DQEBAQUABIGAURfgqd8iZ-UWZTta2pyKzXBXm9nmdzlOY-TN8526LWH4jrU1uuimAZKSjZUCwmnaSvoXHLlP6CSGvNUJWDDu6YFNmDfmatVqFrTij4EFGruExmtUxmhbQOnAyhKqIxHFg2t3VKEB2tVhLGSzoSH1dM2+j0-I0JgOLWIStVFEF5A=",
|
||||
"services": {
|
||||
"activeDirectories": [
|
||||
{
|
||||
|
13
conductor/tests/conductor/test_methods.py
Normal file
13
conductor/tests/conductor/test_methods.py
Normal file
@ -0,0 +1,13 @@
|
||||
import unittest
|
||||
from conductor.app import ConductorWorkflowService
|
||||
import conductor.rabbitmq as rabbitmq
|
||||
from conductor.workflow import Workflow
|
||||
import conductor.xml_code_engine as engine
|
||||
|
||||
class TestMethodsAndClasses(unittest.TestCase):
|
||||
|
||||
def test_init_service_class(self):
|
||||
con = ConductorWorkflowService()
|
||||
|
||||
con.start()
|
||||
con.stop()
|
11
conductor/tests/conductor/test_with_fake_service.py
Normal file
11
conductor/tests/conductor/test_with_fake_service.py
Normal file
@ -0,0 +1,11 @@
|
||||
import unittest
|
||||
from conductor.app import ConductorWorkflowService
|
||||
from conductor.openstack.common import service
|
||||
|
||||
class TestMethodsAndClasses(unittest.TestCase):
|
||||
|
||||
def test_init_service_class(self):
|
||||
launcher = service.ServiceLauncher()
|
||||
con = ConductorWorkflowService()
|
||||
launcher.launch_service(con)
|
||||
|
220
conductor/tools/install_venv_common.py
Normal file
220
conductor/tools/install_venv_common.py
Normal file
@ -0,0 +1,220 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2013 OpenStack Foundation
|
||||
# Copyright 2013 IBM Corp.
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Provides methods needed by installation script for OpenStack development
|
||||
virtual environments.
|
||||
|
||||
Synced in from openstack-common
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
class InstallVenv(object):
|
||||
|
||||
def __init__(self, root, venv, pip_requires, test_requires, py_version,
|
||||
project):
|
||||
self.root = root
|
||||
self.venv = venv
|
||||
self.pip_requires = pip_requires
|
||||
self.test_requires = test_requires
|
||||
self.py_version = py_version
|
||||
self.project = project
|
||||
|
||||
def die(self, message, *args):
|
||||
print >> sys.stderr, message % args
|
||||
sys.exit(1)
|
||||
|
||||
def check_python_version(self):
|
||||
if sys.version_info < (2, 6):
|
||||
self.die("Need Python Version >= 2.6")
|
||||
|
||||
def run_command_with_code(self, cmd, redirect_output=True,
|
||||
check_exit_code=True):
|
||||
"""Runs a command in an out-of-process shell.
|
||||
|
||||
Returns the output of that command. Working directory is self.root.
|
||||
"""
|
||||
if redirect_output:
|
||||
stdout = subprocess.PIPE
|
||||
else:
|
||||
stdout = None
|
||||
|
||||
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
|
||||
output = proc.communicate()[0]
|
||||
if check_exit_code and proc.returncode != 0:
|
||||
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
|
||||
return (output, proc.returncode)
|
||||
|
||||
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
|
||||
return self.run_command_with_code(cmd, redirect_output,
|
||||
check_exit_code)[0]
|
||||
|
||||
def get_distro(self):
|
||||
if (os.path.exists('/etc/fedora-release') or
|
||||
os.path.exists('/etc/redhat-release')):
|
||||
return Fedora(self.root, self.venv, self.pip_requires,
|
||||
self.test_requires, self.py_version, self.project)
|
||||
else:
|
||||
return Distro(self.root, self.venv, self.pip_requires,
|
||||
self.test_requires, self.py_version, self.project)
|
||||
|
||||
def check_dependencies(self):
|
||||
self.get_distro().install_virtualenv()
|
||||
|
||||
def create_virtualenv(self, no_site_packages=True):
|
||||
"""Creates the virtual environment and installs PIP.
|
||||
|
||||
Creates the virtual environment and installs PIP only into the
|
||||
virtual environment.
|
||||
"""
|
||||
if not os.path.isdir(self.venv):
|
||||
print 'Creating venv...',
|
||||
if no_site_packages:
|
||||
self.run_command(['virtualenv', '-q', '--no-site-packages',
|
||||
self.venv])
|
||||
else:
|
||||
self.run_command(['virtualenv', '-q', self.venv])
|
||||
print 'done.'
|
||||
print 'Installing pip in venv...',
|
||||
if not self.run_command(['tools/with_venv.sh', 'easy_install',
|
||||
'pip>1.0']).strip():
|
||||
self.die("Failed to install pip.")
|
||||
print 'done.'
|
||||
else:
|
||||
print "venv already exists..."
|
||||
pass
|
||||
|
||||
def pip_install(self, *args):
|
||||
self.run_command(['tools/with_venv.sh',
|
||||
'pip', 'install', '--upgrade'] + list(args),
|
||||
redirect_output=False)
|
||||
|
||||
def install_dependencies(self):
|
||||
print 'Installing dependencies with pip (this can take a while)...'
|
||||
|
||||
# First things first, make sure our venv has the latest pip and
|
||||
# distribute.
|
||||
# NOTE: we keep pip at version 1.1 since the most recent version causes
|
||||
# the .venv creation to fail. See:
|
||||
# https://bugs.launchpad.net/nova/+bug/1047120
|
||||
self.pip_install('pip==1.1')
|
||||
self.pip_install('distribute')
|
||||
|
||||
# Install greenlet by hand - just listing it in the requires file does
|
||||
# not
|
||||
# get it installed in the right order
|
||||
self.pip_install('greenlet')
|
||||
|
||||
self.pip_install('-r', self.pip_requires)
|
||||
self.pip_install('-r', self.test_requires)
|
||||
|
||||
def post_process(self):
|
||||
self.get_distro().post_process()
|
||||
|
||||
def parse_args(self, argv):
|
||||
"""Parses command-line arguments."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-n', '--no-site-packages',
|
||||
action='store_true',
|
||||
help="Do not inherit packages from global Python "
|
||||
"install")
|
||||
return parser.parse_args(argv[1:])
|
||||
|
||||
|
||||
class Distro(InstallVenv):
|
||||
|
||||
def check_cmd(self, cmd):
|
||||
return bool(self.run_command(['which', cmd],
|
||||
check_exit_code=False).strip())
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if self.check_cmd('easy_install'):
|
||||
print 'Installing virtualenv via easy_install...',
|
||||
if self.run_command(['easy_install', 'virtualenv']):
|
||||
print 'Succeeded'
|
||||
return
|
||||
else:
|
||||
print 'Failed'
|
||||
|
||||
self.die('ERROR: virtualenv not found.\n\n%s development'
|
||||
' requires virtualenv, please install it using your'
|
||||
' favorite package management tool' % self.project)
|
||||
|
||||
def post_process(self):
|
||||
"""Any distribution-specific post-processing gets done here.
|
||||
|
||||
In particular, this is useful for applying patches to code inside
|
||||
the venv.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class Fedora(Distro):
|
||||
"""This covers all Fedora-based distributions.
|
||||
|
||||
Includes: Fedora, RHEL, CentOS, Scientific Linux
|
||||
"""
|
||||
|
||||
def check_pkg(self, pkg):
|
||||
return self.run_command_with_code(['rpm', '-q', pkg],
|
||||
check_exit_code=False)[1] == 0
|
||||
|
||||
def yum_install(self, pkg, **kwargs):
|
||||
print "Attempting to install '%s' via yum" % pkg
|
||||
self.run_command(['sudo', 'yum', 'install', '-y', pkg], **kwargs)
|
||||
|
||||
def apply_patch(self, originalfile, patchfile):
|
||||
self.run_command(['patch', '-N', originalfile, patchfile],
|
||||
check_exit_code=False)
|
||||
|
||||
def install_virtualenv(self):
|
||||
if self.check_cmd('virtualenv'):
|
||||
return
|
||||
|
||||
if not self.check_pkg('python-virtualenv'):
|
||||
self.yum_install('python-virtualenv', check_exit_code=False)
|
||||
|
||||
super(Fedora, self).install_virtualenv()
|
||||
|
||||
def post_process(self):
|
||||
"""Workaround for a bug in eventlet.
|
||||
|
||||
This currently affects RHEL6.1, but the fix can safely be
|
||||
applied to all RHEL and Fedora distributions.
|
||||
|
||||
This can be removed when the fix is applied upstream.
|
||||
|
||||
Nova: https://bugs.launchpad.net/nova/+bug/884915
|
||||
Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
|
||||
"""
|
||||
|
||||
# Install "patch" program if it's not there
|
||||
if not self.check_pkg('patch'):
|
||||
self.yum_install('patch')
|
||||
|
||||
# Apply the eventlet patch
|
||||
self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
|
||||
'site-packages',
|
||||
'eventlet/green/subprocess.py'),
|
||||
'contrib/redhat-eventlet.patch')
|
@ -1,3 +1,10 @@
|
||||
pika
|
||||
tornado
|
||||
jsonpath
|
||||
anyjson
|
||||
eventlet>=0.9.12
|
||||
jsonpath
|
||||
puka
|
||||
Paste
|
||||
PasteDeploy
|
||||
iso8601>=0.1.4
|
||||
python-heatclient
|
||||
|
||||
http://tarballs.openstack.org/oslo-config/oslo-config-2013.1b4.tar.gz#egg=oslo-config
|
||||
|
@ -1,31 +0,0 @@
|
||||
# TO DO:
|
||||
# 1. Add new functional for services and data centers
|
||||
# 2. Fix issue with list of services: services table shoudl show services for
|
||||
# specific data center
|
||||
|
||||
This file is described how to install new tab on horizon dashboard.
|
||||
We should do the following:
|
||||
1. Copy directory 'windc' to directory '/opt/stack/horizon/openstack_dashboard/dashboards/project'
|
||||
2. Copy api/windc.py to directory '/opt/stack/horizon/openstack_dashboard/api'
|
||||
3. Copy directory 'windcclient' to directory '/opt/stack/horizon/'
|
||||
4. Edit file '/opt/stack/horizon/openstack_dashboard/dashboards/project/dashboard.py'
|
||||
Add line with windc project:
|
||||
|
||||
...
|
||||
class BasePanels(horizon.PanelGroup):
|
||||
slug = "compute"
|
||||
name = _("Manage Compute")
|
||||
panels = ('overview',
|
||||
'instances',
|
||||
'volumes',
|
||||
'images_and_snapshots',
|
||||
'access_and_security',
|
||||
'networks',
|
||||
'routers',
|
||||
'windc')
|
||||
|
||||
...
|
||||
|
||||
5. Run the test Django server:
|
||||
cd /opt/stack/horizon
|
||||
python manage.py runserver 67.207.197.36:8080
|
@ -1,126 +0,0 @@
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
# Copyright 2012 United States Government as represented by the
|
||||
# Administrator of the National Aeronautics and Space Administration.
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Copyright 2012 Nebula, 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 logging
|
||||
import urlparse
|
||||
|
||||
from django.utils.decorators import available_attrs
|
||||
from portasclient.v1.client import Client as windc_client
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def windcclient(request):
|
||||
url = "http://127.0.0.1:8082"
|
||||
LOG.debug('windcclient connection created using token "%s" and url "%s"'
|
||||
% (request.user.token, url))
|
||||
return windc_client(endpoint=url, token=request.user.token.token['id'])
|
||||
|
||||
|
||||
def datacenters_create(request, parameters):
|
||||
name = parameters.get('name', '')
|
||||
return windcclient(request).environments.create(name)
|
||||
|
||||
|
||||
def datacenters_delete(request, datacenter_id):
|
||||
return windcclient(request).environments.delete(datacenter_id)
|
||||
|
||||
|
||||
def datacenters_get(request, datacenter_id):
|
||||
return windcclient(request).environments.get(datacenter_id)
|
||||
|
||||
|
||||
def datacenters_list(request):
|
||||
return windcclient(request).environments.list()
|
||||
|
||||
|
||||
def datacenters_deploy(request, datacenter_id):
|
||||
sessions = windcclient(request).sessions.list(datacenter_id)
|
||||
for session in sessions:
|
||||
if session.state == 'open':
|
||||
session_id = session.id
|
||||
if not session_id:
|
||||
return "Sorry, nothing to deploy."
|
||||
return windcclient(request).sessions.deploy(datacenter_id, session_id)
|
||||
|
||||
|
||||
def services_create(request, datacenter, parameters):
|
||||
session_id = windcclient(request).sessions.list(datacenter)[0].id
|
||||
if parameters['service_type'] == 'Active Directory':
|
||||
res = windcclient(request).activeDirectories.create(datacenter,
|
||||
session_id,
|
||||
parameters)
|
||||
else:
|
||||
res = windcclient(request).webServers.create(datacenter,
|
||||
session_id,
|
||||
parameters)
|
||||
|
||||
return res
|
||||
|
||||
|
||||
def services_list(request, datacenter_id):
|
||||
session_id = None
|
||||
sessions = windcclient(request).sessions.list(datacenter_id)
|
||||
LOG.critical('DC ID: ' + str(datacenter_id))
|
||||
|
||||
for s in sessions:
|
||||
if s.state in ['open', 'deploying']:
|
||||
session_id = s.id
|
||||
|
||||
if session_id is None:
|
||||
session_id = windcclient(request).sessions.configure(datacenter_id).id
|
||||
|
||||
services = windcclient(request).activeDirectories.list(datacenter_id,
|
||||
session_id)
|
||||
services += windcclient(request).webServers.list(datacenter_id, session_id)
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def services_get(request, datacenter_id, service_id):
|
||||
services = services_list(request, datacenter_id)
|
||||
|
||||
for service in services:
|
||||
if service.id is service_id:
|
||||
return service
|
||||
|
||||
|
||||
def services_delete(request, datacenter_id, service_id):
|
||||
services = services_list(request, datacenter_id)
|
||||
|
||||
session_id = None
|
||||
sessions = windcclient(request).sessions.list(datacenter_id)
|
||||
for session in sessions:
|
||||
if session.state == 'open':
|
||||
session_id = session.id
|
||||
|
||||
if session_id is None:
|
||||
raise Exception("Sorry, you can not delete this service now.")
|
||||
|
||||
for service in services:
|
||||
if service.id is service_id:
|
||||
if service.type is 'Active Directory':
|
||||
windcclient(request).activeDirectories.delete(datacenter_id,
|
||||
session_id,
|
||||
service_id)
|
||||
elif service.type is 'IIS':
|
||||
windcclient(request).webServers.delete(datacenter_id,
|
||||
session_id,
|
||||
service_id)
|
@ -1,3 +0,0 @@
|
||||
{% load i18n sizeformat %}
|
||||
|
||||
<h3>{% trans "Services" %}</h3>
|
@ -1,137 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
"""
|
||||
Base utilities to build API operation managers and objects on top of.
|
||||
"""
|
||||
|
||||
|
||||
def getid(obj):
|
||||
"""
|
||||
Abstracts the common pattern of allowing both an object or an object's ID
|
||||
(UUID) as a parameter when dealing with relationships.
|
||||
"""
|
||||
try:
|
||||
return obj.id
|
||||
except AttributeError:
|
||||
return obj
|
||||
|
||||
|
||||
class Manager(object):
|
||||
"""
|
||||
Managers interact with a particular type of API and provide CRUD
|
||||
operations for them.
|
||||
"""
|
||||
resource_class = None
|
||||
|
||||
def __init__(self, api):
|
||||
self.api = api
|
||||
|
||||
def _list(self, url, response_key, obj_class=None, body=None):
|
||||
resp, body = self.api.client.json_request('GET', url, body=body)
|
||||
|
||||
if obj_class is None:
|
||||
obj_class = self.resource_class
|
||||
|
||||
data = body[response_key]
|
||||
return [obj_class(self, res, loaded=True) for res in data if res]
|
||||
|
||||
def _delete(self, url):
|
||||
self.api.client.raw_request('DELETE', url)
|
||||
|
||||
def _update(self, url, body, response_key=None):
|
||||
resp, body = self.api.client.json_request('PUT', url, body=body)
|
||||
# PUT requests may not return a body
|
||||
if body:
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _create(self, url, body, response_key, return_raw=False):
|
||||
resp, body = self.api.client.json_request('POST', url, body=body)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
def _get(self, url, response_key, return_raw=False):
|
||||
resp, body = self.api.client.json_request('GET', url)
|
||||
if return_raw:
|
||||
return body[response_key]
|
||||
return self.resource_class(self, body[response_key])
|
||||
|
||||
|
||||
class Resource(object):
|
||||
"""
|
||||
A resource represents a particular instance of an object (tenant, user,
|
||||
etc). This is pretty much just a bag for attributes.
|
||||
|
||||
:param manager: Manager object
|
||||
:param info: dictionary representing resource attributes
|
||||
:param loaded: prevent lazy-loading if set to True
|
||||
"""
|
||||
def __init__(self, manager, info, loaded=False):
|
||||
self.manager = manager
|
||||
self._info = info
|
||||
self._add_details(info)
|
||||
self._loaded = loaded
|
||||
|
||||
def _add_details(self, info):
|
||||
for (k, v) in info.iteritems():
|
||||
setattr(self, k, v)
|
||||
|
||||
def __getattr__(self, k):
|
||||
if k not in self.__dict__:
|
||||
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
return self.__getattr__(k)
|
||||
|
||||
raise AttributeError(k)
|
||||
else:
|
||||
return self.__dict__[k]
|
||||
|
||||
def __repr__(self):
|
||||
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||
k != 'manager')
|
||||
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||
return "<%s %s>" % (self.__class__.__name__, info)
|
||||
|
||||
def get_info(self):
|
||||
if not self.is_loaded():
|
||||
self.get()
|
||||
if self._info:
|
||||
return self._info.copy()
|
||||
return {}
|
||||
|
||||
def get(self):
|
||||
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||
self.set_loaded(True)
|
||||
if not hasattr(self.manager, 'get'):
|
||||
return
|
||||
|
||||
new = self.manager.get(self.id)
|
||||
if new:
|
||||
self._info = new._info
|
||||
self._add_details(new._info)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, self.__class__):
|
||||
return False
|
||||
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||
return self.id == other.id
|
||||
return self._info == other._info
|
||||
|
||||
def is_loaded(self):
|
||||
return self._loaded
|
||||
|
||||
def set_loaded(self, val):
|
||||
self._loaded = val
|
@ -1,151 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
"""
|
||||
OpenStack Client interface. Handles the REST calls and responses.
|
||||
"""
|
||||
|
||||
import httplib2
|
||||
import copy
|
||||
import logging
|
||||
import json
|
||||
|
||||
from . import exceptions
|
||||
from . import utils
|
||||
from .service_catalog import ServiceCatalog
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HTTPClient(httplib2.Http):
|
||||
|
||||
USER_AGENT = 'python-windcclient'
|
||||
|
||||
def __init__(self, endpoint=None, token=None, username=None,
|
||||
password=None, tenant_name=None, tenant_id=None,
|
||||
region_name=None, auth_url=None, auth_tenant_id=None,
|
||||
timeout=600, insecure=False):
|
||||
super(HTTPClient, self).__init__(timeout=timeout)
|
||||
self.endpoint = endpoint
|
||||
self.auth_token = token
|
||||
self.auth_url = auth_url
|
||||
self.auth_tenant_id = auth_tenant_id
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.tenant_name = tenant_name
|
||||
self.tenant_id = tenant_id
|
||||
self.region_name = region_name
|
||||
self.force_exception_to_status_code = True
|
||||
self.disable_ssl_certificate_validation = insecure
|
||||
if self.endpoint is None:
|
||||
self.authenticate()
|
||||
|
||||
def _http_request(self, url, method, **kwargs):
|
||||
""" Send an http request with the specified characteristics.
|
||||
"""
|
||||
|
||||
kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {}))
|
||||
kwargs['headers'].setdefault('User-Agent', self.USER_AGENT)
|
||||
if self.auth_token:
|
||||
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||
|
||||
resp, body = super(HTTPClient, self).request(url, method, **kwargs)
|
||||
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
utils.http_log(logger, (url, method,), kwargs, resp, body)
|
||||
|
||||
if resp.status in (301, 302, 305):
|
||||
return self._http_request(resp['location'], method, **kwargs)
|
||||
|
||||
return resp, body
|
||||
|
||||
def _json_request(self, method, url, **kwargs):
|
||||
""" Wrapper around _http_request to handle setting headers,
|
||||
JSON enconding/decoding and error handling.
|
||||
"""
|
||||
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type', 'application/json')
|
||||
|
||||
if 'body' in kwargs and kwargs['body'] is not None:
|
||||
kwargs['body'] = json.dumps(kwargs['body'])
|
||||
|
||||
resp, body = self._http_request(url, method, **kwargs)
|
||||
|
||||
if body:
|
||||
try:
|
||||
body = json.loads(body)
|
||||
except ValueError:
|
||||
logger.debug("Could not decode JSON from body: %s" % body)
|
||||
else:
|
||||
logger.debug("No body was returned.")
|
||||
body = None
|
||||
|
||||
if 400 <= resp.status < 600:
|
||||
# DELETE THIS STRING
|
||||
logger.exception(url)
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def raw_request(self, method, url, **kwargs):
|
||||
url = self.endpoint + url
|
||||
|
||||
kwargs.setdefault('headers', {})
|
||||
kwargs['headers'].setdefault('Content-Type',
|
||||
'application/octet-stream')
|
||||
|
||||
resp, body = self._http_request(url, method, **kwargs)
|
||||
|
||||
if 400 <= resp.status < 600:
|
||||
logger.exception(url)
|
||||
raise exceptions.from_response(resp, body)
|
||||
|
||||
return resp, body
|
||||
|
||||
def json_request(self, method, url, **kwargs):
|
||||
url = self.endpoint + url
|
||||
resp, body = self._json_request(method, url, **kwargs)
|
||||
return resp, body
|
||||
|
||||
def authenticate(self):
|
||||
token_url = self.auth_url + "/tokens"
|
||||
body = {'auth': {'passwordCredentials': {'username': self.username,
|
||||
'password': self.password}}}
|
||||
if self.tenant_id:
|
||||
body['auth']['tenantId'] = self.tenant_id
|
||||
elif self.tenant_name:
|
||||
body['auth']['tenantName'] = self.tenant_name
|
||||
|
||||
tmp_follow_all_redirects = self.follow_all_redirects
|
||||
self.follow_all_redirects = True
|
||||
try:
|
||||
resp, body = self._json_request('POST', token_url, body=body)
|
||||
finally:
|
||||
self.follow_all_redirects = tmp_follow_all_redirects
|
||||
|
||||
try:
|
||||
self.service_catalog = ServiceCatalog(body['access'])
|
||||
token = self.service_catalog.get_token()
|
||||
self.auth_token = token['id']
|
||||
self.auth_tenant_id = token['tenant_id']
|
||||
except KeyError:
|
||||
logger.exception("Parse service catalog failed.")
|
||||
raise exceptions.AuthorizationFailure()
|
||||
|
||||
self.endpoint = self.service_catalog.url_for(attr='region',
|
||||
filter_value=self.region_name)
|
@ -1,140 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
"""
|
||||
Exception definitions.
|
||||
"""
|
||||
|
||||
|
||||
class UnsupportedVersion(Exception):
|
||||
"""Indicates that the user is trying to use an unsupported
|
||||
version of the API"""
|
||||
pass
|
||||
|
||||
|
||||
class CommandError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthorizationFailure(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoUniqueMatch(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoTokenLookupException(Exception):
|
||||
"""This form of authentication does not support looking up
|
||||
endpoints from an existing token."""
|
||||
pass
|
||||
|
||||
|
||||
class EndpointNotFound(Exception):
|
||||
"""Could not find Service or Region in Service Catalog."""
|
||||
pass
|
||||
|
||||
|
||||
class AmbiguousEndpoints(Exception):
|
||||
"""Found more than one matching endpoint in Service Catalog."""
|
||||
def __init__(self, endpoints=None):
|
||||
self.endpoints = endpoints
|
||||
|
||||
def __str__(self):
|
||||
return "AmbiguousEndpoints: %s" % repr(self.endpoints)
|
||||
|
||||
|
||||
class ClientException(Exception):
|
||||
"""
|
||||
The base exception class for all exceptions this library raises.
|
||||
"""
|
||||
def __init__(self, code, message=None, details=None):
|
||||
self.code = code
|
||||
self.message = message or self.__class__.message
|
||||
self.details = details
|
||||
|
||||
def __str__(self):
|
||||
return "%s (HTTP %s)" % (self.message, self.code)
|
||||
|
||||
|
||||
class BadRequest(ClientException):
|
||||
"""
|
||||
HTTP 400 - Bad request: you sent some malformed data.
|
||||
"""
|
||||
http_status = 400
|
||||
message = "Bad request"
|
||||
|
||||
|
||||
class Unauthorized(ClientException):
|
||||
"""
|
||||
HTTP 401 - Unauthorized: bad credentials.
|
||||
"""
|
||||
http_status = 401
|
||||
message = "Unauthorized"
|
||||
|
||||
|
||||
class Forbidden(ClientException):
|
||||
"""
|
||||
HTTP 403 - Forbidden: your credentials don't give you access to this
|
||||
resource.
|
||||
"""
|
||||
http_status = 403
|
||||
message = "Forbidden"
|
||||
|
||||
|
||||
class NotFound(ClientException):
|
||||
"""
|
||||
HTTP 404 - Not found
|
||||
"""
|
||||
http_status = 404
|
||||
message = "Not found"
|
||||
|
||||
|
||||
class OverLimit(ClientException):
|
||||
"""
|
||||
HTTP 413 - Over limit: you're over the API limits for this time period.
|
||||
"""
|
||||
http_status = 413
|
||||
message = "Over limit"
|
||||
|
||||
|
||||
# NotImplemented is a python keyword.
|
||||
class HTTPNotImplemented(ClientException):
|
||||
"""
|
||||
HTTP 501 - Not Implemented: the server does not support this operation.
|
||||
"""
|
||||
http_status = 501
|
||||
message = "Not Implemented"
|
||||
|
||||
|
||||
# In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__()
|
||||
# so we can do this:
|
||||
# _code_map = dict((c.http_status, c)
|
||||
# for c in ClientException.__subclasses__())
|
||||
#
|
||||
# Instead, we have to hardcode it:
|
||||
_code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized,
|
||||
Forbidden, NotFound, OverLimit, HTTPNotImplemented])
|
||||
|
||||
|
||||
def from_response(response, body):
|
||||
"""
|
||||
Return an instance of an ClientException or subclass
|
||||
based on an httplib2 response.
|
||||
|
||||
Usage::
|
||||
|
||||
resp, body = http.request(...)
|
||||
if resp.status != 200:
|
||||
raise exception_from_response(resp, body)
|
||||
"""
|
||||
cls = _code_map.get(response.status, ClientException)
|
||||
if body:
|
||||
if hasattr(body, 'keys'):
|
||||
error = body[body.keys()[0]]
|
||||
message = error.get('message', None)
|
||||
details = error.get('details', None)
|
||||
else:
|
||||
message = 'n/a'
|
||||
details = body
|
||||
return cls(code=response.status, message=message, details=details)
|
||||
else:
|
||||
return cls(code=response.status)
|
@ -1,62 +0,0 @@
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# Copyright 2011, Piston Cloud Computing, Inc.
|
||||
# Copyright 2011 Nebula, 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 . import exceptions
|
||||
|
||||
|
||||
class ServiceCatalog(object):
|
||||
"""Helper methods for dealing with a Keystone Service Catalog."""
|
||||
|
||||
def __init__(self, resource_dict):
|
||||
self.catalog = resource_dict
|
||||
|
||||
def get_token(self):
|
||||
"""Fetch token details fron service catalog"""
|
||||
token = {'id': self.catalog['token']['id'],
|
||||
'expires': self.catalog['token']['expires']}
|
||||
try:
|
||||
token['user_id'] = self.catalog['user']['id']
|
||||
token['tenant_id'] = self.catalog['token']['tenant']['id']
|
||||
except:
|
||||
# just leave the tenant and user out if it doesn't exist
|
||||
pass
|
||||
return token
|
||||
|
||||
def url_for(self, attr=None, filter_value=None,
|
||||
service_type='loadbalancer', endpoint_type='publicURL'):
|
||||
"""Fetch an endpoint from the service catalog.
|
||||
|
||||
Fetch the specified endpoint from the service catalog for
|
||||
a particular endpoint attribute. If no attribute is given, return
|
||||
the first endpoint of the specified type.
|
||||
|
||||
See tests for a sample service catalog.
|
||||
"""
|
||||
catalog = self.catalog.get('serviceCatalog', [])
|
||||
|
||||
for service in catalog:
|
||||
if service['type'] != service_type:
|
||||
continue
|
||||
|
||||
endpoints = service['endpoints']
|
||||
for endpoint in endpoints:
|
||||
if not filter_value or endpoint.get(attr) == filter_value:
|
||||
return endpoint[endpoint_type]
|
||||
|
||||
raise exceptions.EndpointNotFound('Endpoint not found.')
|
@ -1,291 +0,0 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import logging
|
||||
import prettytable
|
||||
|
||||
from . import exceptions
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args."""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*vars, **kwargs):
|
||||
"""
|
||||
returns the first environment variable set
|
||||
if none are non-empty, defaults to '' or keyword arg default
|
||||
"""
|
||||
for v in vars:
|
||||
value = os.environ.get(v, None)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def add_arg(f, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(f, 'arguments'):
|
||||
f.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in f.arguments:
|
||||
# Because of the sematics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
f.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def add_resource_manager_extra_kwargs_hook(f, hook):
|
||||
"""Adds hook to bind CLI arguments to ResourceManager calls.
|
||||
|
||||
The `do_foo` calls in shell.py will receive CLI args and then in turn pass
|
||||
them through to the ResourceManager. Before passing through the args, the
|
||||
hooks registered here will be called, giving us a chance to add extra
|
||||
kwargs (taken from the command-line) to what's passed to the
|
||||
ResourceManager.
|
||||
"""
|
||||
if not hasattr(f, 'resource_manager_kwargs_hooks'):
|
||||
f.resource_manager_kwargs_hooks = []
|
||||
|
||||
names = [h.__name__ for h in f.resource_manager_kwargs_hooks]
|
||||
if hook.__name__ not in names:
|
||||
f.resource_manager_kwargs_hooks.append(hook)
|
||||
|
||||
|
||||
def get_resource_manager_extra_kwargs(f, args, allow_conflicts=False):
|
||||
"""Return extra_kwargs by calling resource manager kwargs hooks."""
|
||||
hooks = getattr(f, "resource_manager_kwargs_hooks", [])
|
||||
extra_kwargs = {}
|
||||
for hook in hooks:
|
||||
hook_name = hook.__name__
|
||||
hook_kwargs = hook(args)
|
||||
|
||||
conflicting_keys = set(hook_kwargs.keys()) & set(extra_kwargs.keys())
|
||||
if conflicting_keys and not allow_conflicts:
|
||||
raise Exception("Hook '%(hook_name)s' is attempting to redefine"
|
||||
" attributes '%(conflicting_keys)s'" % locals())
|
||||
|
||||
extra_kwargs.update(hook_kwargs)
|
||||
|
||||
return extra_kwargs
|
||||
|
||||
|
||||
def unauthenticated(f):
|
||||
"""
|
||||
Adds 'unauthenticated' attribute to decorated function.
|
||||
Usage:
|
||||
@unauthenticated
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
f.unauthenticated = True
|
||||
return f
|
||||
|
||||
|
||||
def isunauthenticated(f):
|
||||
"""
|
||||
Checks to see if the function is marked as not requiring authentication
|
||||
with the @unauthenticated decorator. Returns True if decorator is
|
||||
set to True, False otherwise.
|
||||
"""
|
||||
return getattr(f, 'unauthenticated', False)
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""
|
||||
Adds 'service_type' attribute to decorated function.
|
||||
Usage:
|
||||
@service_type('volume')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""
|
||||
Retrieves service type from function
|
||||
"""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters={}, sortby_index=0):
|
||||
if sortby_index == None:
|
||||
sortby = None
|
||||
else:
|
||||
sortby = fields[sortby_index]
|
||||
|
||||
pt = prettytable.PrettyTable([f for f in fields], caching=False)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
print pt.get_string(sortby=sortby)
|
||||
|
||||
|
||||
def print_flat_list(lst, field):
|
||||
pt = prettytable.PrettyTable(field)
|
||||
for el in lst:
|
||||
pt.add_row([el])
|
||||
print pt.get_string()
|
||||
|
||||
|
||||
def print_dict(d, property="Property"):
|
||||
pt = prettytable.PrettyTable([property, 'Value'], caching=False)
|
||||
pt.align = 'l'
|
||||
[pt.add_row(list(r)) for r in d.iteritems()]
|
||||
print pt.get_string(sortby=property)
|
||||
|
||||
|
||||
def find_resource(manager, name_or_id):
|
||||
"""Helper for the _find_* methods."""
|
||||
# first try to get entity as integer id
|
||||
try:
|
||||
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||
return manager.get(int(name_or_id))
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# now try to get entity as uuid
|
||||
try:
|
||||
uuid.UUID(str(name_or_id))
|
||||
return manager.get(name_or_id)
|
||||
except (ValueError, exceptions.NotFound):
|
||||
pass
|
||||
|
||||
try:
|
||||
try:
|
||||
return manager.find(human_id=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
pass
|
||||
|
||||
# finally try to find entity by name
|
||||
try:
|
||||
return manager.find(name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
try:
|
||||
# Volumes does not have name, but display_name
|
||||
return manager.find(display_name=name_or_id)
|
||||
except exceptions.NotFound:
|
||||
msg = "No %s with a name or ID of '%s' exists." % \
|
||||
(manager.resource_class.__name__.lower(), name_or_id)
|
||||
raise exceptions.CommandError(msg)
|
||||
except exceptions.NoUniqueMatch:
|
||||
msg = ("Multiple %s matches found for '%s', use an ID to be more"
|
||||
" specific." % (manager.resource_class.__name__.lower(),
|
||||
name_or_id))
|
||||
raise exceptions.CommandError(msg)
|
||||
|
||||
|
||||
def _format_servers_list_networks(server):
|
||||
output = []
|
||||
for (network, addresses) in server.networks.items():
|
||||
if len(addresses) == 0:
|
||||
continue
|
||||
addresses_csv = ', '.join(addresses)
|
||||
group = "%s=%s" % (network, addresses_csv)
|
||||
output.append(group)
|
||||
|
||||
return '; '.join(output)
|
||||
|
||||
|
||||
class HookableMixin(object):
|
||||
"""Mixin so classes can register and run hooks."""
|
||||
_hooks_map = {}
|
||||
|
||||
@classmethod
|
||||
def add_hook(cls, hook_type, hook_func):
|
||||
if hook_type not in cls._hooks_map:
|
||||
cls._hooks_map[hook_type] = []
|
||||
|
||||
cls._hooks_map[hook_type].append(hook_func)
|
||||
|
||||
@classmethod
|
||||
def run_hooks(cls, hook_type, *args, **kwargs):
|
||||
hook_funcs = cls._hooks_map.get(hook_type) or []
|
||||
for hook_func in hook_funcs:
|
||||
hook_func(*args, **kwargs)
|
||||
|
||||
|
||||
def safe_issubclass(*args):
|
||||
"""Like issubclass, but will just return False if not a class."""
|
||||
|
||||
try:
|
||||
if issubclass(*args):
|
||||
return True
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def import_class(import_str):
|
||||
"""Returns a class from a string including module and class."""
|
||||
mod_str, _sep, class_str = import_str.rpartition('.')
|
||||
__import__(mod_str)
|
||||
return getattr(sys.modules[mod_str], class_str)
|
||||
|
||||
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
||||
_slugify_hyphenate_re = re.compile(r'[-\s]+')
|
||||
|
||||
|
||||
# http://code.activestate.com/recipes/
|
||||
# 577257-slugify-make-a-string-usable-in-a-url-or-filename/
|
||||
def slugify(value):
|
||||
"""
|
||||
Normalizes string, converts to lowercase, removes non-alpha characters,
|
||||
and converts spaces to hyphens.
|
||||
|
||||
From Django's "django/template/defaultfilters.py".
|
||||
"""
|
||||
import unicodedata
|
||||
if not isinstance(value, unicode):
|
||||
value = unicode(value)
|
||||
value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore')
|
||||
value = unicode(_slugify_strip_re.sub('', value).strip().lower())
|
||||
return _slugify_hyphenate_re.sub('-', value)
|
||||
|
||||
|
||||
def http_log(logger, args, kwargs, resp, body):
|
||||
# if not logger.isEnabledFor(logging.DEBUG):
|
||||
# return
|
||||
|
||||
string_parts = ['curl -i']
|
||||
for element in args:
|
||||
if element in ('GET', 'POST'):
|
||||
string_parts.append(' -X %s' % element)
|
||||
else:
|
||||
string_parts.append(' %s' % element)
|
||||
|
||||
for element in kwargs['headers']:
|
||||
header = ' -H "%s: %s"' % (element, kwargs['headers'][element])
|
||||
string_parts.append(header)
|
||||
|
||||
logger.debug("REQ: %s\n" % "".join(string_parts))
|
||||
if 'body' in kwargs and kwargs['body']:
|
||||
logger.debug("REQ BODY: %s\n" % (kwargs['body']))
|
||||
logger.debug("RESP:%s\n", resp)
|
||||
logger.debug("RESP BODY:%s\n", body)
|
@ -1,285 +0,0 @@
|
||||
# Copyright 2010 Jacob Kaplan-Moss
|
||||
# Copyright 2011 OpenStack LLC.
|
||||
# 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.
|
||||
|
||||
"""
|
||||
Command-line interface to the OpenStack LBaaS API.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import httplib2
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
|
||||
from balancerclient.common import exceptions as exc
|
||||
from balancerclient.common import utils
|
||||
from balancerclient.v1 import shell as shell_v1
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OpenStackBalancerShell(object):
|
||||
|
||||
def get_base_parser(self):
|
||||
parser = argparse.ArgumentParser(
|
||||
prog='balancer',
|
||||
description=__doc__.strip(),
|
||||
epilog='See "balancer help COMMAND" '
|
||||
'for help on a specific command.',
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter,
|
||||
)
|
||||
|
||||
# Global arguments
|
||||
parser.add_argument('-h',
|
||||
'--help',
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--debug',
|
||||
default=False,
|
||||
action='store_true',
|
||||
help=argparse.SUPPRESS)
|
||||
|
||||
parser.add_argument('--os_username',
|
||||
metavar='<auth-user-name>',
|
||||
default=utils.env('OS_USERNAME'),
|
||||
help='Defaults to env[OS_USERNAME]')
|
||||
|
||||
parser.add_argument('--os_password',
|
||||
metavar='<auth-password>',
|
||||
default=utils.env('OS_PASSWORD'),
|
||||
help='Defaults to env[OS_PASSWORD]')
|
||||
|
||||
parser.add_argument('--os_tenant_name',
|
||||
metavar='<auth-tenant-name>',
|
||||
default=utils.env('OS_TENANT_NAME'),
|
||||
help='Defaults to env[OS_TENANT_NAME]')
|
||||
|
||||
parser.add_argument('--os_tenant_id',
|
||||
metavar='<tenant-id>',
|
||||
default=utils.env('OS_TENANT_ID'),
|
||||
help='Defaults to env[OS_TENANT_ID]')
|
||||
|
||||
parser.add_argument('--os_auth_url',
|
||||
metavar='<auth-url>',
|
||||
default=utils.env('OS_AUTH_URL'),
|
||||
help='Defaults to env[OS_AUTH_URL]')
|
||||
|
||||
parser.add_argument('--os_region_name',
|
||||
metavar='<region-name>',
|
||||
default=utils.env('OS_REGION_NAME'),
|
||||
help='Defaults to env[OS_REGION_NAME]')
|
||||
|
||||
parser.add_argument('--os_balancer_api_version',
|
||||
metavar='<balancer-api-version>',
|
||||
default=utils.env('OS_BALANCER_API_VERSION',
|
||||
'KEYSTONE_VERSION'),
|
||||
help='Defaults to env[OS_BALANCER_API_VERSION]'
|
||||
' or 2.0')
|
||||
|
||||
parser.add_argument('--token',
|
||||
metavar='<service-token>',
|
||||
default=utils.env('SERVICE_TOKEN'),
|
||||
help='Defaults to env[SERVICE_TOKEN]')
|
||||
|
||||
parser.add_argument('--endpoint',
|
||||
metavar='<service-endpoint>',
|
||||
default=utils.env('SERVICE_ENDPOINT'),
|
||||
help='Defaults to env[SERVICE_ENDPOINT]')
|
||||
|
||||
return parser
|
||||
|
||||
def get_subcommand_parser(self, version):
|
||||
parser = self.get_base_parser()
|
||||
|
||||
self.subcommands = {}
|
||||
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||
|
||||
try:
|
||||
actions_module = {
|
||||
'1': shell_v1,
|
||||
}[version]
|
||||
except KeyError:
|
||||
actions_module = shell_v1
|
||||
|
||||
self._find_actions(subparsers, actions_module)
|
||||
self._find_actions(subparsers, self)
|
||||
|
||||
return parser
|
||||
|
||||
def _find_actions(self, subparsers, actions_module):
|
||||
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||
# I prefer to be hypen-separated instead of underscores.
|
||||
command = attr[3:].replace('_', '-')
|
||||
callback = getattr(actions_module, attr)
|
||||
desc = callback.__doc__ or ''
|
||||
help = desc.strip().split('\n')[0]
|
||||
arguments = getattr(callback, 'arguments', [])
|
||||
|
||||
subparser = subparsers.add_parser(
|
||||
command,
|
||||
help=help,
|
||||
description=desc,
|
||||
add_help=False,
|
||||
formatter_class=OpenStackHelpFormatter)
|
||||
subparser.add_argument('-h', '--help', action='help',
|
||||
help=argparse.SUPPRESS)
|
||||
self.subcommands[command] = subparser
|
||||
for (args, kwargs) in arguments:
|
||||
subparser.add_argument(*args, **kwargs)
|
||||
subparser.set_defaults(func=callback)
|
||||
|
||||
def main(self, argv):
|
||||
# Parse args once to find version
|
||||
parser = self.get_base_parser()
|
||||
(options, args) = parser.parse_known_args(argv)
|
||||
|
||||
# build available subcommands based on version
|
||||
api_version = options.os_balancer_api_version
|
||||
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||
self.parser = subcommand_parser
|
||||
|
||||
# Handle top-level --help/-h before attempting to parse
|
||||
# a command off the command line
|
||||
if not argv or options.help:
|
||||
self.do_help(options)
|
||||
return 0
|
||||
|
||||
# Parse args again and call whatever callback was selected
|
||||
args = subcommand_parser.parse_args(argv)
|
||||
|
||||
# Deal with global arguments
|
||||
if args.debug:
|
||||
httplib2.debuglevel = 1
|
||||
|
||||
# Short-circuit and deal with help command right away.
|
||||
if args.func == self.do_help:
|
||||
self.do_help(args)
|
||||
return 0
|
||||
|
||||
#FIXME(usrleon): Here should be restrict for project id same as
|
||||
# for username or apikey but for compatibility it is not.
|
||||
|
||||
if not utils.isunauthenticated(args.func):
|
||||
# if the user hasn't provided any auth data
|
||||
if not (args.token or args.endpoint or args.os_username or
|
||||
args.os_password or args.os_auth_url):
|
||||
raise exc.CommandError('Expecting authentication method via \n'
|
||||
' either a service token, '
|
||||
'--token or env[SERVICE_TOKEN], \n'
|
||||
' or credentials, '
|
||||
'--os_username or env[OS_USERNAME].')
|
||||
|
||||
# if it looks like the user wants to provide a service token
|
||||
# but is missing something
|
||||
if args.token or args.endpoint and not (
|
||||
args.token and args.endpoint):
|
||||
if not args.token:
|
||||
raise exc.CommandError(
|
||||
'Expecting a token provided via either --token or '
|
||||
'env[SERVICE_TOKEN]')
|
||||
|
||||
if not args.endpoint:
|
||||
raise exc.CommandError(
|
||||
'Expecting an endpoint provided via either --endpoint '
|
||||
'or env[SERVICE_ENDPOINT]')
|
||||
|
||||
# if it looks like the user wants to provide a credentials
|
||||
# but is missing something
|
||||
if ((args.os_username or args.os_password or args.os_auth_url)
|
||||
and not (args.os_username and args.os_password and
|
||||
args.os_auth_url)):
|
||||
if not args.os_username:
|
||||
raise exc.CommandError(
|
||||
'Expecting a username provided via either '
|
||||
'--os_username or env[OS_USERNAME]')
|
||||
|
||||
if not args.os_password:
|
||||
raise exc.CommandError(
|
||||
'Expecting a password provided via either '
|
||||
'--os_password or env[OS_PASSWORD]')
|
||||
|
||||
if not args.os_auth_url:
|
||||
raise exc.CommandError(
|
||||
'Expecting an auth URL via either --os_auth_url or '
|
||||
'env[OS_AUTH_URL]')
|
||||
|
||||
if utils.isunauthenticated(args.func):
|
||||
self.cs = shell_generic.CLIENT_CLASS(endpoint=args.os_auth_url)
|
||||
else:
|
||||
token = None
|
||||
endpoint = None
|
||||
if args.token and args.endpoint:
|
||||
token = args.token
|
||||
endpoint = args.endpoint
|
||||
api_version = options.os_balancer_api_version
|
||||
self.cs = self.get_api_class(api_version)(
|
||||
username=args.os_username,
|
||||
tenant_name=args.os_tenant_name,
|
||||
tenant_id=args.os_tenant_id,
|
||||
token=token,
|
||||
endpoint=endpoint,
|
||||
password=args.os_password,
|
||||
auth_url=args.os_auth_url,
|
||||
region_name=args.os_region_name)
|
||||
|
||||
try:
|
||||
args.func(self.cs, args)
|
||||
except exc.Unauthorized:
|
||||
raise exc.CommandError("Invalid OpenStack LBaaS credentials.")
|
||||
except exc.AuthorizationFailure:
|
||||
raise exc.CommandError("Unable to authorize user")
|
||||
|
||||
def get_api_class(self, version):
|
||||
try:
|
||||
return {
|
||||
"1": shell_v1.CLIENT_CLASS,
|
||||
}[version]
|
||||
except KeyError:
|
||||
return shell_v1.CLIENT_CLASS
|
||||
|
||||
@utils.arg('command', metavar='<subcommand>', nargs='?',
|
||||
help='Display help for <subcommand>')
|
||||
def do_help(self, args):
|
||||
"""
|
||||
Display help about this program or one of its subcommands.
|
||||
"""
|
||||
if getattr(args, 'command', None):
|
||||
if args.command in self.subcommands:
|
||||
self.subcommands[args.command].print_help()
|
||||
else:
|
||||
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||
args.command)
|
||||
else:
|
||||
self.parser.print_help()
|
||||
|
||||
|
||||
# I'm picky about my shell help.
|
||||
class OpenStackHelpFormatter(argparse.HelpFormatter):
|
||||
def start_section(self, heading):
|
||||
# Title-case the headings
|
||||
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||
super(OpenStackHelpFormatter, self).start_section(heading)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
return OpenStackBalancerShell().main(sys.argv[1:])
|
||||
except Exception, err:
|
||||
LOG.exception("The operation executed with an error %r." % err)
|
||||
raise
|
@ -1,29 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
from windcclient.common import client
|
||||
from . import datacenters
|
||||
from . import services
|
||||
|
||||
|
||||
class Client(object):
|
||||
"""Client for the WinDC v1 API."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.client = client.HTTPClient(**kwargs)
|
||||
self.datacenters = datacenters.DCManager(self)
|
||||
self.services = services.DCServiceManager(self)
|
@ -1,43 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
from windcclient.common import base
|
||||
|
||||
|
||||
class DC(base.Resource):
|
||||
|
||||
def __repr__(self):
|
||||
return "<DC(%s)>" % self._info
|
||||
|
||||
|
||||
class DCManager(base.Manager):
|
||||
resource_class = DC
|
||||
|
||||
def list(self):
|
||||
return self._list('/datacenters', 'datacenters')
|
||||
|
||||
def create(self, name, **extra):
|
||||
body = {'name': name, 'services': {}}
|
||||
body.update(extra)
|
||||
return self._create('/datacenters', body, 'datacenter')
|
||||
|
||||
def delete(self, datacenter_id):
|
||||
return self._delete("/datacenters/%s" % datacenter_id)
|
||||
|
||||
def get(self, datacenter_id):
|
||||
return self._get("/datacenters/%s" % datacenter_id,
|
||||
'datacenter')
|
@ -1,48 +0,0 @@
|
||||
# Copyright 2012 OpenStack LLC.
|
||||
# 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.
|
||||
#
|
||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||
|
||||
from windcclient.common import base
|
||||
|
||||
|
||||
class DCService(base.Resource):
|
||||
|
||||
def __repr__(self):
|
||||
return "<Service(%s)>" % self._info
|
||||
|
||||
|
||||
class DCServiceManager(base.Manager):
|
||||
resource_class = DCService
|
||||
|
||||
def list(self, datacenter):
|
||||
return self._list("/datacenters/%s/services" % base.getid(datacenter),
|
||||
'services')
|
||||
|
||||
def create(self, datacenter, parameters):
|
||||
body = {'dc_count': 1,}
|
||||
body.update(parameters)
|
||||
return self._create("/datacenters/%s/services" % base.getid(datacenter),
|
||||
body, 'service')
|
||||
|
||||
def delete(self, datacenter_id, service_id):
|
||||
return self._delete("/datacenters/%s/services/%s" % \
|
||||
(datacenter_id, service_id))
|
||||
|
||||
def get(self, datacenter, service):
|
||||
return self._get("/datacenters/%s/services/%s" % \
|
||||
(base.getid(datacenter),
|
||||
base.getid(service)),
|
||||
'service')
|
@ -27,7 +27,6 @@ reports_queue = task-reports
|
||||
[rabbitmq]
|
||||
host = localhost
|
||||
port = 5672
|
||||
use_ssl = false
|
||||
userid = keero
|
||||
password = keero
|
||||
virtual_host = keero
|
||||
login = keero
|
||||
password = keero
|
||||
|
@ -72,7 +72,8 @@ class ContextMiddleware(BaseContextMiddleware):
|
||||
'user': req.headers.get('X-User-Id'),
|
||||
'tenant': req.headers.get('X-Tenant-Id'),
|
||||
'roles': roles,
|
||||
'auth_tok': req.headers.get('X-Auth-Token', deprecated_token),
|
||||
'auth_token': req.headers.get('X-Auth-Token',
|
||||
deprecated_token),
|
||||
'service_catalog': service_catalog,
|
||||
'session': req.headers.get('X-Configuration-Session')
|
||||
}
|
||||
|
@ -28,10 +28,10 @@ def get_env_status(environment_id, session_id):
|
||||
|
||||
if not session_id:
|
||||
variants = ['open', 'deploying']
|
||||
session = unit.query(Session).filter(Session.environment_id ==
|
||||
environment_id and
|
||||
Session.state.in_(variants)
|
||||
).first()
|
||||
session = unit.query(Session).filter(
|
||||
Session.environment_id == environment_id and
|
||||
Session.state.in_(variants)
|
||||
).first()
|
||||
if session:
|
||||
session_id = session.id
|
||||
else:
|
||||
@ -79,10 +79,10 @@ def get_service_status(environment_id, session_id, service):
|
||||
|
||||
entities = [u['id'] for u in service['units']]
|
||||
reports_count = unit.query(Status).filter(
|
||||
Status.environment_id == environment_id
|
||||
and Status.session_id == session_id
|
||||
and Status.entity_id.in_(entities)
|
||||
).count()
|
||||
Status.environment_id == environment_id
|
||||
and Status.session_id == session_id
|
||||
and Status.entity_id.in_(entities)
|
||||
).count()
|
||||
|
||||
if session_state == 'deployed':
|
||||
status = 'finished'
|
||||
|
@ -101,7 +101,7 @@ class Controller(object):
|
||||
connection = amqp.Connection('{0}:{1}'.
|
||||
format(rabbitmq.host, rabbitmq.port),
|
||||
virtual_host=rabbitmq.virtual_host,
|
||||
userid=rabbitmq.userid,
|
||||
userid=rabbitmq.login,
|
||||
password=rabbitmq.password,
|
||||
ssl=rabbitmq.use_ssl, insist=True)
|
||||
channel = connection.channel()
|
||||
|
@ -31,17 +31,16 @@ class Controller(object):
|
||||
log.debug(_('Session:Configure <EnvId: {0}>'.format(environment_id)))
|
||||
|
||||
params = {'environment_id': environment_id,
|
||||
'user_id': request.context.user,
|
||||
'state': 'open'}
|
||||
'user_id': request.context.user, 'state': 'open'}
|
||||
|
||||
session = Session()
|
||||
session.update(params)
|
||||
|
||||
unit = get_session()
|
||||
if unit.query(Session).filter(Session.environment_id == environment_id
|
||||
and
|
||||
Session.state.in_(['open', 'deploing'])
|
||||
).first():
|
||||
if unit.query(Session).filter(
|
||||
Session.environment_id == environment_id and
|
||||
Session.state.in_(['open', 'deploying'])
|
||||
).first():
|
||||
log.info('There is already open session for this environment')
|
||||
raise exc.HTTPConflict
|
||||
|
||||
@ -55,8 +54,8 @@ class Controller(object):
|
||||
return session.to_dict()
|
||||
|
||||
def show(self, request, environment_id, session_id):
|
||||
log.debug(_('Session:Show <EnvId: {0}, SessionId: {1}>'.
|
||||
format(environment_id, session_id)))
|
||||
log.debug(_('Session:Show <EnvId: {0}, '
|
||||
'SessionId: {1}>'.format(environment_id, session_id)))
|
||||
|
||||
unit = get_session()
|
||||
session = unit.query(Session).get(session_id)
|
||||
@ -68,8 +67,8 @@ class Controller(object):
|
||||
return session.to_dict()
|
||||
|
||||
def delete(self, request, environment_id, session_id):
|
||||
log.debug(_('Session:Delete <EnvId: {0}, SessionId: {1}>'.
|
||||
format(environment_id, session_id)))
|
||||
log.debug(_('Session:Delete <EnvId: {0}, '
|
||||
'SessionId: {1}>'.format(environment_id, session_id)))
|
||||
|
||||
unit = get_session()
|
||||
session = unit.query(Session).get(session_id)
|
||||
@ -85,17 +84,42 @@ class Controller(object):
|
||||
return None
|
||||
|
||||
def reports(self, request, environment_id, session_id):
|
||||
log.debug(_('Session:Reports <EnvId: {0}, SessionId: {1}>'.
|
||||
format(environment_id, session_id)))
|
||||
log.debug(_('Session:Reports <EnvId: {0}, '
|
||||
'SessionId: {1}>'.format(environment_id, session_id)))
|
||||
|
||||
unit = get_session()
|
||||
statuses = unit.query(Status).filter_by(session_id=session_id)
|
||||
statuses = unit.query(Status).filter_by(session_id=session_id).all()
|
||||
result = statuses
|
||||
|
||||
return {'reports': [status.to_dict() for status in statuses]}
|
||||
if 'service_id' in request.GET:
|
||||
service_id = request.GET['service_id']
|
||||
|
||||
environment = unit.query(Session).get(session_id).description
|
||||
services = []
|
||||
if 'services' in environment and 'activeDirectories' in\
|
||||
environment['services']:
|
||||
services += environment['services']['activeDirectories']
|
||||
|
||||
if 'services' in environment and 'webServers' in\
|
||||
environment['services']:
|
||||
services += environment['services']['webServers']
|
||||
|
||||
service = [service for service in services
|
||||
if service['id'] == service_id][0]
|
||||
|
||||
if service:
|
||||
entities = [u['id'] for u in service['units']]
|
||||
entities.append(service_id)
|
||||
result = []
|
||||
for status in statuses:
|
||||
if status.entity_id in entities:
|
||||
result.append(status)
|
||||
|
||||
return {'reports': [status.to_dict() for status in result]}
|
||||
|
||||
def deploy(self, request, environment_id, session_id):
|
||||
log.debug(_('Session:Deploy <EnvId: {0}, SessionId: {1}>'.
|
||||
format(environment_id, session_id)))
|
||||
log.debug(_('Session:Deploy <EnvId: {0}, '
|
||||
'SessionId: {1}>'.format(environment_id, session_id)))
|
||||
|
||||
unit = get_session()
|
||||
session = unit.query(Session).get(session_id)
|
||||
@ -115,7 +139,7 @@ class Controller(object):
|
||||
connection = amqp.Connection('{0}:{1}'.
|
||||
format(rabbitmq.host, rabbitmq.port),
|
||||
virtual_host=rabbitmq.virtual_host,
|
||||
userid=rabbitmq.userid,
|
||||
userid=rabbitmq.login,
|
||||
password=rabbitmq.password,
|
||||
ssl=rabbitmq.use_ssl, insist=True)
|
||||
channel = connection.channel()
|
||||
|
@ -36,7 +36,7 @@ class Controller(object):
|
||||
for unit in webServer['units']:
|
||||
unit_count += 1
|
||||
unit['id'] = uuidutils.generate_uuid()
|
||||
unit['name'] = 'iis{0}'.format(unit_count)
|
||||
unit['name'] = webServer['name'] + '_instance_' + str(unit_count)
|
||||
|
||||
draft = prepare_draft(draft)
|
||||
draft['services']['webServers'].append(webServer)
|
||||
|
@ -52,7 +52,7 @@ rabbit_opts = [
|
||||
cfg.StrOpt('host', default='localhost'),
|
||||
cfg.IntOpt('port', default=5672),
|
||||
cfg.BoolOpt('use_ssl', default=False),
|
||||
cfg.StrOpt('userid', default='guest'),
|
||||
cfg.StrOpt('login', default='guest'),
|
||||
cfg.StrOpt('password', default='guest'),
|
||||
cfg.StrOpt('virtual_host', default='/'),
|
||||
]
|
||||
|
@ -30,7 +30,7 @@ class TaskResultHandlerService(service.Service):
|
||||
connection = amqp.Connection('{0}:{1}'.
|
||||
format(rabbitmq.host, rabbitmq.port),
|
||||
virtual_host=rabbitmq.virtual_host,
|
||||
userid=rabbitmq.userid,
|
||||
userid=rabbitmq.login,
|
||||
password=rabbitmq.password,
|
||||
ssl=rabbitmq.use_ssl, insist=True)
|
||||
ch = connection.channel()
|
||||
@ -80,7 +80,7 @@ def handle_result(msg):
|
||||
'orchestration engine:\n{0}'.format(msg.body)))
|
||||
|
||||
environment_result = anyjson.deserialize(msg.body)
|
||||
if environment_result['deleted']:
|
||||
if 'deleted' in environment_result:
|
||||
log.debug(_('Result for environment {0} is dropped. '
|
||||
'Environment is deleted'.format(environment_result['id'])))
|
||||
|
||||
|
@ -24,10 +24,10 @@ class RequestContext(object):
|
||||
accesses the system, as well as additional request information.
|
||||
"""
|
||||
|
||||
def __init__(self, auth_tok=None, user=None, tenant=None,
|
||||
def __init__(self, auth_token=None, user=None, tenant=None,
|
||||
roles=None, service_catalog=None, session=None):
|
||||
|
||||
self.auth_tok = auth_tok
|
||||
self.auth_token = auth_token
|
||||
self.user = user
|
||||
self.tenant = tenant
|
||||
self.roles = roles or []
|
||||
@ -51,7 +51,7 @@ class RequestContext(object):
|
||||
'project_id': self.tenant,
|
||||
|
||||
'roles': self.roles,
|
||||
'auth_token': self.auth_tok,
|
||||
'auth_token': self.auth_token,
|
||||
'session': self.session
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import unittest
|
||||
|
||||
import portas.api.v1 as api
|
||||
|
||||
class Test(unittest.TestCase):
|
||||
def test(self):
|
||||
|
@ -12,6 +12,7 @@
|
||||
# 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 mercurial import patch
|
||||
|
||||
from portasclient.common import base
|
||||
|
||||
@ -49,10 +50,9 @@ class ActiveDirectoryManager(base.Manager):
|
||||
def delete(self, environment_id, session_id, service_id):
|
||||
headers = {'X-Configuration-Session': session_id}
|
||||
path = 'environments/{id}/activeDirectories/{active_directory_id}'
|
||||
path = path.format(id=environment_id, active_directory_id=service_id)
|
||||
|
||||
return self._delete(path.format(id=environment_id,
|
||||
active_directory_id=service_id),
|
||||
headers=headers)
|
||||
return self._delete(path, headers=headers)
|
||||
|
||||
|
||||
class WebServer(base.Resource):
|
||||
|
@ -54,11 +54,13 @@ class SessionManager(base.Manager):
|
||||
path.format(id=environment_id,
|
||||
session_id=session_id))
|
||||
|
||||
def reports(self, environment_id, session_id):
|
||||
def reports(self, environment_id, session_id, service_id=None):
|
||||
path = 'environments/{id}/sessions/{session_id}/reports'
|
||||
resp, body = self.api.json_request('GET',
|
||||
path.format(id=environment_id,
|
||||
session_id=session_id))
|
||||
path = path.format(id=environment_id, session_id=session_id)
|
||||
if service_id:
|
||||
path += '?service_id={0}'.format(service_id)
|
||||
|
||||
resp, body = self.api.json_request('GET', path)
|
||||
|
||||
data = body.get('reports', [])
|
||||
return [Status(self, res, loaded=True) for res in data if res]
|
||||
|
17
python-portasclient/tests/portasclient/.project
Normal file
17
python-portasclient/tests/portasclient/.project
Normal file
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>portasclient</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.python.pydev.PyDevBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.python.pydev.pythonNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user