Initial commit of MoltenIron

MoltenIron is a tool to manage a pool of baremetal nodes that
are to be used as test targets in a baremetal CI environment,
instead of VM guests.

MoltenIron allows you to add, allocate, and release nodes from
it's pool using the following methods:

add - Add a node to the pool

allocate - checkout a baremetal node from the pool, returning
the required info in json format to the requester. It then
marks the node as in use so that no other VM will check it out.

release - return the baremetal node to the pool, allowing another
VM to eventually allocate it.

Change-Id: I8d276d677d9b09bc34032f46c825320d5d83e756
This commit is contained in:
Mike 2016-04-12 09:05:59 -04:00 committed by Kurt Taylor
commit c3b10dc8d4
5 changed files with 1333 additions and 0 deletions

35
README Normal file
View File

@ -0,0 +1,35 @@
MoltenIron maintains a pool of bare metal nodes.
==============================================================================
Before starting the server for the first time, the createDB.py
script must be run.
To start or restart the server, run moltenIronD.py.
==============================================================================
Use the molteniron client (molteniron.py) to communicate with the server. For
usage information type './molteniron.py -h'. For usage of a specific command
use ./molteniron.py [command] -h (ie - molteniron.py add -h)
==============================================================================
Configuration of MoltenIron is specified in the file conf.yaml.
"B)" means that this configuration option is required for both the client and
the server. "C)" means that it is required only for the client. "S)" means
it is only required for the server.
B) mi_port: - the port that the server uses to respond to
commands.
C) serverIP: - The IP address of the server. This is only used by
clients.
S) maxTime: - The maximum amount of time, in seconds, that a node
is allowed to be allocated to a particular BM node.
S) logdir: - The path to the directory where the logs should be
stored.
S) maxLogDays: - The amount of time, in days, to keep old logs.
S) sqlUser: - The username to use for the MI server. This user
will automatically be generated when createDB.py is run.
S) sqlPass: - The password of sqlUser

9
conf.yaml Normal file
View File

@ -0,0 +1,9 @@
mi_port: 5656
serverIP: 127.0.0.1
timeout: 5
retry: 5
maxTime: 6000
logdir: "./logs"
maxLogDays: 15
sqlUser: "root"
sqlPass: "password"

41
createDB.py Executable file
View File

@ -0,0 +1,41 @@
#!/usr/bin/env python
# Copyright (c) 2016 IBM Corporation.
#
# 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 sys
import yaml
def SQL(query):
print(os.popen("mysql -u root -p --execute=\"" + query + "\"").read())
def main():
path = sys.argv[0]
dirs = path.split("/")
newPath = "/".join(dirs[:-1]) + "/"
fobj = open(newPath + "conf.yaml", "r")
conf = yaml.load(fobj)
# Create the SQL User
SQL("CREATE USER '"+conf["sqlUser"]+"'@'localhost' "
"IDENTIFIED BY '"+conf["sqlPass"]+"';")
SQL("GRANT ALL ON MoltenIron.* TO '"+conf["sqlUser"]+"'@'localhost';")
return 0
if __name__ == "__main__":
main()

186
molteniron.py Executable file
View File

@ -0,0 +1,186 @@
#! /usr/bin/env python
# Copyright (c) 2016 IBM Corporation.
#
# 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 argparse
import httplib
import json
import sys
import yaml
DEBUG = False
class MoltenIron(object):
def __init__(self):
self.conf = self.read_conf()
# Parse the arguments and generate a request
parser = argparse.ArgumentParser()
parser.add_argument('command', help='Subcommand to run')
args = parser.parse_args(sys.argv[1:2])
request = getattr(self, args.command)()
# Send the request and print the response
self.response_str = self.send(request)
self.response_json = json.loads(self.response_str)
def send(self, request):
"""Send the generated request """
connection = httplib.HTTPConnection(str(self.conf['serverIP']),
int(self.conf['mi_port']))
connection.request('POST', '/', json.dumps(request))
response = connection.getresponse()
return response.read()
def get_response(self):
"""Returns the response from the server """
return self.response_str
def get_response_map(self):
"""Returns the response from the server """
return self.response_json
def add(self):
"""Generate a request to add a node to the MoltenIron database """
parser = argparse.ArgumentParser(
description='Add a node to the micli')
parser.add_argument('name', help="Name of the baremetal node")
parser.add_argument('ipmi_ip', help="IP for issuing IPMI commands to"
" this node")
parser.add_argument('ipmi_user', help="IPMI username used when issuing"
" IPMI commands to this node")
parser.add_argument('ipmi_password', help="IPMI password used when"
" issuing IPMI commands to this node")
parser.add_argument('allocation_pool', help="Comma separated list of"
" IPs to be used in deployment")
parser.add_argument('port_hwaddr', help="MAC address of port on"
" machine to use during deployment")
parser.add_argument('cpu_arch', help="Architecture of the node")
parser.add_argument('cpus', type=int, help="Number of CPUs on the"
" node")
parser.add_argument('ram_mb', type=int, help="Amount of RAM (in MiB)"
" that the node has")
parser.add_argument('disk_gb', type=int, help="Amount of disk (in GiB)"
" that the node has")
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'add'
return request
def allocate(self):
"""Generate request to checkout a node from the MoltenIron database """
parser = argparse.ArgumentParser(
description="Checkout a node in molteniron. Returns the node's"
" info")
parser.add_argument('owner_name', help="Name of the requester")
parser.add_argument('number_of_nodes', type=int, help="How many nodes"
" to reserve")
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'allocate'
return request
def release(self):
"""Generate a request to release an allocated node from the MoltenIron
database
"""
parser = argparse.ArgumentParser(
description="Given an owner name, release allocated node,"
" returning it to the available state")
parser.add_argument('owner_name', help="Name of the owner who"
" currently owns the nodes to be released")
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'release'
return request
def get_field(self):
"""Generate a request to return a field of data from an owned node from
the MoltenIron database
"""
parser = argparse.ArgumentParser(
description="Given an owner name and the name of a field, get the"
" value of the field")
parser.add_argument('owner_name', help="Name of the owner who"
" currently owns the nodes to get the field from")
parser.add_argument('field_name', help="Name of the field to retrieve"
" the value from")
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'get_field'
return request
def set_field(self):
"""Generate request to set a field of data from an id in the MoltenIron
database
"""
parser = argparse.ArgumentParser(
description='Given an id, set a field with a value')
parser.add_argument('id', help='Id of the entry')
parser.add_argument('key', help='Field name to set')
parser.add_argument('value', help='Field value to set')
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'set_field'
return request
def status(self):
"""Return status """
parser = argparse.ArgumentParser(
description="Return a list of current MoltenIron Node database"
" entries")
args = parser.parse_args(sys.argv[2:])
request = vars(args)
request['method'] = 'status'
return request
def read_conf(self):
"""Read ./conf.yaml in """
path = sys.argv[0]
dirs = path.split("/")
newPath = "/".join(dirs[:-1]) + "/"
fobj = open(newPath + "conf.yaml", "r")
conf = yaml.load(fobj)
return conf
if __name__ == "__main__":
mi = MoltenIron()
print(mi.get_response())
try:
rc = mi.get_response_map()['status']
except KeyError:
print("Error: Server returned: %s" % (mi.get_response_map(),))
rc = 444
if rc == 200:
exit(0)
else:
exit(1)

1062
moltenirond.py Executable file

File diff suppressed because it is too large Load Diff