Skip to content

Commit a4edfd5

Browse files
author
chalupaul
committed
Initial upload
0 parents  commit a4edfd5

File tree

5 files changed

+360
-0
lines changed

5 files changed

+360
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
TODO: make description
2+

ironic_importer/__init__.py

Whitespace-only changes.

ironic_importer/inventory.py

+247
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import gevent.monkey
2+
import gevent.pool
3+
import gevent.queue
4+
import os
5+
import sys
6+
import urllib3
7+
from keystoneauth1.identity import v3
8+
from keystoneauth1 import session as keystone_session
9+
from keystoneclient.v3 import client as keystone_client
10+
from ironicclient import client as ironic_client
11+
import ironic_inspector_client
12+
import pandas
13+
from novaclient import client as nova_client
14+
import glanceclient
15+
import argparse
16+
17+
gevent.monkey.patch_all()
18+
19+
# We don't need no security
20+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
21+
22+
flavor_body = {
23+
'name': 'general',
24+
'vcpus': 0,
25+
'ram': 0,
26+
'disk': 0,
27+
'description': 'A baremetal flavor'
28+
}
29+
flavor_schema = {
30+
'body': {
31+
'name': 'general',
32+
'vcpus': 1,
33+
'ram': 1,
34+
'disk': 1
35+
},
36+
'details': {
37+
'cpu_arch': 'x86_64',
38+
'capabilities:boot_option': 'local',
39+
'capabilities:disk_label': 'gpt',
40+
# Resouce limits on baremetal flavors is unimplemented in openstack
41+
'resources:VCPU': 0,
42+
'resources:MEMORY_MB': 0,
43+
'resources:DISK_GB': 0,
44+
}
45+
}
46+
47+
CLIENTS = {}
48+
IR_KERNEL_IMAGE = None
49+
IR_INITRD_IMAGE = None
50+
51+
52+
def load_args():
53+
parser = argparse.ArgumentParser()
54+
parser.add_argument("excel_file", help="Path to the excel file to read.")
55+
return parser.parse_args()
56+
57+
58+
def load_auth_clients():
59+
auth_fields = {
60+
'auth_url': os.environ['OS_AUTH_URL'],
61+
'username': os.environ['OS_USERNAME'],
62+
'password': os.environ['OS_PASSWORD'],
63+
'project_name': os.environ['OS_PROJECT_NAME'],
64+
'user_domain_name': os.environ['OS_USER_DOMAIN_NAME'],
65+
'project_domain_name': os.environ['OS_PROJECT_DOMAIN_NAME']
66+
}
67+
68+
v3_auth = v3.Password(**auth_fields)
69+
ks_sess = keystone_session.Session(auth=v3_auth, verify=False)
70+
ks_client = keystone_client.Client(session=ks_sess)
71+
CLIENTS['keystone'] = ks_client
72+
73+
gl_client = glanceclient.Client('2', session=ks_sess)
74+
CLIENTS['glance'] = gl_client
75+
76+
nv_client = nova_client.Client(2, session=ks_sess)
77+
CLIENTS['nova'] = nv_client
78+
79+
ir_client = ironic_client.get_client(1, insecure=True, **auth_fields)
80+
CLIENTS['ironic'] = ir_client
81+
82+
ins_client = ironic_inspector_client.ClientV1(session=ks_sess)
83+
CLIENTS['ironic-inspector'] = ins_client
84+
85+
86+
def read_excel_file(excel_file):
87+
ironic_hosts = pandas.read_excel(open(excel_file, 'rb'))
88+
89+
seen = set()
90+
nodes = []
91+
for i in ironic_hosts.index:
92+
node_name = ironic_hosts['hostname'][i]
93+
if node_name not in seen:
94+
seen.add(node_name)
95+
nodes.append({
96+
'hostname': ironic_hosts['hostname'][i],
97+
'ipv4': ironic_hosts['ipmi address'][i],
98+
'username': ironic_hosts['ipmi username'][i],
99+
'password': ironic_hosts['ipmi password'][i],
100+
'mac': ironic_hosts['provisioning mac address'][i],
101+
'drive': ironic_hosts['managed drive'][i],
102+
'flavor': ironic_hosts['server hardware type'][i]
103+
})
104+
else:
105+
error_msg = ("Duplicated hostname %s in excel file. ",
106+
"Names must be unique. Please remedy and rerun.")
107+
print(error_msg % (node_name))
108+
sys.exit(56)
109+
return nodes
110+
111+
112+
def get_safe_resource_name(flavor_name):
113+
# According to the rules, resource flavors must be a key that:
114+
# 1) begin with CUSTOM_
115+
# 2) have all punctuation replaced with an underline
116+
# 3) be all upper case
117+
# 4) has a value of 1 in the hash
118+
replace_chars = ('.', '-', '/', '`', '\'', '?', ',', '+')
119+
for i in replace_chars:
120+
flavor_name = flavor_name.replace(i, '_')
121+
return "CUSTOM_" + flavor_name.upper()
122+
123+
124+
def process_node(node):
125+
ironic = CLIENTS['ironic']
126+
ironic_vars = {
127+
'name': node['hostname'],
128+
'driver': 'ipmi',
129+
'driver_info': {
130+
'ipmi_address': node['ipv4'],
131+
'ipmi_password': node['password'],
132+
'ipmi_username': node['username'],
133+
# TODO: yank those uuids from something
134+
'deploy_kernel': IR_KERNEL_IMAGE,
135+
'deploy_ramdisk': IR_INITRD_IMAGE,
136+
'resource_class': node['flavor'],
137+
},
138+
'properties': {
139+
'capabilities': 'boot_option:local,disk_label:gpt',
140+
'cpu_arch': 'x86_64'
141+
}
142+
}
143+
ir_node = ironic.node.create(**ironic_vars)
144+
print(dir(ironic.node))
145+
print("Created node %s" % (node['hostname']))
146+
ir_port_vars = {
147+
"address": node['mac'],
148+
"node_uuid": ir_node.uuid
149+
}
150+
ir_port = ironic.port.create(**ir_port_vars)
151+
print("Created neutron port %s for node %s" % (ir_port.uuid, ir_node.name))
152+
153+
ironic.node.wait_for_provision_state(ir_node.uuid, 'available', 300)
154+
ironic.node.set_provision_state(ir_node.uuid, 'manage')
155+
ironic.node.wait_for_provision_state(ir_node.uuid, 'manageable', 300)
156+
print("Managed node %s" % (ir_node.name))
157+
ironic.node.set_provision_state(ir_node.uuid, 'inspect')
158+
print("Inspecting node %s" % (ir_node.name))
159+
ironic.node.wait_for_provision_state(ir_node.uuid, 'manageable', 3600)
160+
print("Inspection complete for node %s" % (ir_node.name))
161+
162+
inspector = CLIENTS['ironic-inspector']
163+
disks = inspector.get_data(ir_node.uuid)['inventory']['disks']
164+
device_name = '/dev/' + node['drive']
165+
target_drive = None
166+
for disk in disks:
167+
if disk['name'] == device_name:
168+
target_drive = disk['serial']
169+
break
170+
if target_drive is not None:
171+
props = [{
172+
'op': 'add',
173+
'path': '/properties/root_device',
174+
'value': target_drive
175+
}]
176+
ironic.node.update(ir_node.uuid, props)
177+
print("Setting boot device to %s on %s" % (target_drive, ir_node.name))
178+
179+
180+
def node_worker(queue, return_queue):
181+
try:
182+
node = queue.get(timeout=0)
183+
except gevent.queue.Empty:
184+
return
185+
try:
186+
process_node(node)
187+
except Exception as e:
188+
return_queue.put({'hostname': node['hostname'], 'message': e.message})
189+
190+
191+
def main():
192+
args = load_args()
193+
load_auth_clients()
194+
xl_nodes = read_excel_file(args.excel_file)
195+
196+
xl_flavors = set([node['flavor'] for node in xl_nodes])
197+
198+
images = CLIENTS['glance'].images.list()
199+
for image in images:
200+
if image.name == 'ironic-deploy.kernel':
201+
IR_KERNEL_IMAGE = image.id
202+
if image.name == 'ironic-deploy.initramfs':
203+
IR_INITRD_IMAGE = image.id
204+
205+
if None in [IR_KERNEL_IMAGE, IR_INITRD_IMAGE]:
206+
err_msg = ("An error has occured. Please ensure ironic introspection"
207+
" images are loaded into glance. Images must be named "
208+
"\"ironic-deploy.kernel\" and \"ironic-deploy.initramfs\".")
209+
print(err_msg)
210+
sys.exit(50)
211+
212+
nova = CLIENTS['nova']
213+
api_flavors = nova.flavors.list()
214+
api_flavor_names = [f.name for f in api_flavors]
215+
for flavor in xl_flavors:
216+
if flavor not in api_flavor_names:
217+
body = flavor_schema['body'].copy()
218+
details = flavor_schema['details'].copy()
219+
body['name'] = flavor
220+
safe_name = get_safe_resource_name(body['name'])
221+
details[safe_name] = 1
222+
fl = nova.flavors.create(**body)
223+
fl.set_keys(details)
224+
print("Created baremetal flavor %s" % (fl.name))
225+
226+
api_nodes = [node.name for node in CLIENTS['ironic'].node.list()]
227+
pool_limit = 2
228+
pool = gevent.pool.Pool(pool_limit)
229+
queue = gevent.queue.Queue()
230+
return_queue = gevent.queue.Queue()
231+
for node in xl_nodes:
232+
if node['hostname'] not in api_nodes:
233+
queue.put(node)
234+
235+
pool.spawn(node_worker, queue, return_queue)
236+
237+
while not queue.empty() and not pool.free_count == pool_limit:
238+
gevent.sleep(0.1)
239+
for x in range(0, min(queue.qsize(), pool.free_count())):
240+
pool.spawn(node_worker, queue, return_queue)
241+
242+
pool.join()
243+
244+
while not return_queue.empty():
245+
e = return_queue.get(timeout=0)
246+
print("Errors Detected!")
247+
print("%s: %s" % (e['hostname'], e['message']))

requirements.txt

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
appdirs==1.4.3
2+
asn1crypto==0.24.0
3+
attrs==19.1.0
4+
Babel==2.7.0
5+
certifi==2019.6.16
6+
cffi==1.12.3
7+
chardet==3.0.4
8+
cliff==2.15.0
9+
cmd2==0.9.14
10+
colorama==0.4.1
11+
cryptography==2.7
12+
debtcollector==1.21.0
13+
decorator==4.4.0
14+
dogpile.cache==0.7.1
15+
entrypoints==0.3
16+
gevent==1.4.0
17+
greenlet==0.4.15
18+
idna==2.8
19+
ironic-inventory==0.9.0
20+
iso8601==0.1.12
21+
jmespath==0.9.4
22+
jsonpatch==1.23
23+
jsonpointer==2.0
24+
jsonschema==3.0.1
25+
keystoneauth1==3.14.0
26+
mccabe==0.6.1
27+
msgpack==0.6.1
28+
munch==2.3.2
29+
netaddr==0.7.19
30+
netifaces==0.10.9
31+
numpy==1.17.0rc1
32+
openstacksdk==0.31.1
33+
os-service-types==1.7.0
34+
osc-lib==1.13.0
35+
oslo.config==6.11.0
36+
oslo.i18n==3.23.1
37+
oslo.serialization==2.29.1
38+
oslo.utils==3.41.0
39+
pandas==0.25.0rc0
40+
pbr==5.4.0
41+
pkg-resources==0.0.0
42+
prettytable==0.7.2
43+
pycparser==2.19
44+
pyOpenSSL==19.0.0
45+
pyparsing==2.4.0
46+
pyperclip==1.7.0
47+
pyrsistent==0.15.3
48+
python-cinderclient==4.2.0
49+
python-dateutil==2.8.0
50+
python-glanceclient==2.16.0
51+
python-ironic-inspector-client==3.6.1
52+
python-ironicclient==2.8.0
53+
python-keystoneclient==3.19.0
54+
python-novaclient==14.1.0
55+
python-openstackclient==3.19.0
56+
pytz==2019.1
57+
PyYAML==5.1.1
58+
requests==2.22.0
59+
requestsexceptions==1.4.0
60+
rfc3986==1.3.2
61+
simplejson==3.16.0
62+
six==1.12.0
63+
stevedore==1.30.1
64+
urllib3==1.25.3
65+
warlock==1.3.3
66+
wcwidth==0.1.7
67+
wrapt==1.11.2
68+
xlrd==1.2.0

setup.py

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
from setuptools import setup, find_packages
2+
from os import path
3+
4+
here = path.abspath(path.dirname(__file__))
5+
6+
with open(path.join(here, 'README.md'), encoding='utf8' ) as f:
7+
long_description = f.read()
8+
9+
setup(
10+
name='ironic_inventory',
11+
version='0.9.0',
12+
description='Register ironic nodes via excel spreadsheet',
13+
long_description=long_description,
14+
long_description_content_type='text/markdown',
15+
url='https://github.com/rcbops/ironic_inventory',
16+
author='paul sims',
17+
author_email='[email protected]',
18+
classifiers=[
19+
'Development Status :: 3 - Alpha',
20+
'Intended Audience :: Operators',
21+
'Topic :: Openstack :: Ironic',
22+
'License :: OSI Approved :: Apache2',
23+
'Programming Language :: Python :: 3',
24+
],
25+
keywords="ironic node registration",
26+
packages=find_packages(exclude=['contrib', 'docs', 'tests']),
27+
python_requires='>=3.5',
28+
install_requires=[
29+
'pandas',
30+
'xlrd',
31+
'gevent',
32+
'python-ironicclient',
33+
'python-ironic_inspector_client',
34+
'python-keystoneclient',
35+
'python-novaclient',
36+
'python-glanceclient',
37+
],
38+
entry_points={
39+
'console_scripts': [
40+
'ironic_inventory=ironic_importer.inventory:main',
41+
]
42+
}
43+
)

0 commit comments

Comments
 (0)