Commit 2d5377a6 authored by Swann Perarnau's avatar Swann Perarnau

[feature] Add appliance management script

Will allow appliance management more easily, in a script separated from
the lease stuff.

Uses ansible under the hood to configure the appliance.
parent 8d3ae2c2
......@@ -9,6 +9,7 @@ python-openstackclient = "*"
shade = "*"
python-blazarclient = "*"
os-client-config = "*"
pyyaml = "*"
[dev-packages]
ipython = "*"
......
{
"_meta": {
"hash": {
"sha256": "30e8d85593ea74573ac91e3912ab154f29072176b853b2e2b8274c8fb77e8fd4"
"sha256": "7ad19733cc7ac3aff37e297340f4c154c491c3114d0e24c73b6d8a9485c04b6c"
},
"pipfile-spec": 6,
"requires": {
......@@ -115,7 +115,6 @@
"sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f",
"sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb"
],
"markers": "platform_python_implementation != 'pypy'",
"version": "==1.11.5"
},
"chardet": {
......@@ -240,10 +239,10 @@
},
"keystoneauth1": {
"hashes": [
"sha256:0561cd860decbdea9b4388e9b863d0f4791d16bf127ef42a4f490bda7a023b5e",
"sha256:ed1bcdaec0738076fe730d37654488556146b2bff7f32e78baa25c9779cf3814"
"sha256:4504b15bedbdcd5a3b3981351d6c90a47b0069c2b9aaf46da1885abdba168b3f",
"sha256:dc30869174dbc22d516999634ad91988150c3cac65d15be335abae2279e910ae"
],
"version": "==3.6.0"
"version": "==3.6.1"
},
"markupsafe": {
"hashes": [
......@@ -346,10 +345,10 @@
},
"oslo.config": {
"hashes": [
"sha256:135ae788d7a1bb57327364da9a6fd9225f053d33b9da0bbece9a146c8cc55802",
"sha256:dfd5ac210072c2277478f2df81cddd5fdea7e4f41c029057485bc39bbb7f3a74"
"sha256:5c3925b19139782847aedadc4877687be5282b402eeed427c99fb7cf9b0427b9",
"sha256:c989f7441e5eea658482276d2f34d3c9d77089f4f723076efc442211c3256743"
],
"version": "==6.2.0"
"version": "==6.2.1"
},
"oslo.context": {
"hashes": [
......@@ -381,10 +380,10 @@
},
"oslo.utils": {
"hashes": [
"sha256:672c48f89431480ce4a6b4a58d33601c1692932ac10defe5532ed07fa2f95cc3",
"sha256:baaffb9d1528bdb5677f8c67828c457d5c015249674a33c62e6a0dbddd9f0e58"
"sha256:9900be2bc8bf14c187731393dea672ea9579312d6f31b862e527999fde63f2c6",
"sha256:d7153bf63d4c5bec5491c08f6036de0d1a388f3e4aedd1fb483d8c04615cf883"
],
"version": "==3.36.1"
"version": "==3.36.2"
},
"packaging": {
"hashes": [
......@@ -402,10 +401,10 @@
},
"pbr": {
"hashes": [
"sha256:4e8a0ed6a8705a26768f4c3da26026013b157821fe5f95881599556ea9d91c19",
"sha256:dae4aaa78eafcad10ce2581fc34d694faa616727837fd8e55c1a00951ad6744f"
"sha256:680bf5ba9b28dd56e08eb7c267991a37c7a5f90a92c2e07108829931a50ff80a",
"sha256:6874feb22334a1e9a515193cba797664e940b763440c88115009ec323a7f2df5"
],
"version": "==4.0.2"
"version": "==4.0.3"
},
"prettytable": {
"hashes": [
......@@ -475,10 +474,10 @@
},
"pyopenssl": {
"hashes": [
"sha256:07a2de1a54de07448732a81e38a55df7da109b2f47f599f8bb35b0cbec69d4bd",
"sha256:2c10cfba46a52c0b0950118981d61e72c1e5b1aac451ca1bc77de1a679456773"
"sha256:26ff56a6b5ecaf3a2a59f132681e2a80afcc76b4f902f612f518f92c2a1bf854",
"sha256:6488f1423b00f73b7ad5167885312bb0ce410d3312eb212393795b53c8caa580"
],
"version": "==17.5.0"
"version": "==18.0.0"
},
"pyparsing": {
"hashes": [
......@@ -573,6 +572,7 @@
"sha256:ca233c64c6e40eaa6c66ef97058cdc80e8d0157a443655baa1b2966e812807ca",
"sha256:e863072cdf4c72eebf179342c94e6989c67185842d9997960b3e69290b2fa269"
],
"index": "pypi",
"version": "==3.12"
},
"requests": {
......@@ -606,29 +606,29 @@
},
"simplejson": {
"hashes": [
"sha256:0ba465c81fdca6e0d7eae9ecf92ca93b15695817b44c67ebe8d854a752c8beba",
"sha256:0f48968bb3e3162adcc50282b7e70225f307b935207bccafeaf565d5129b60eb",
"sha256:114f12e2a631563ec78a43d118faeda11ec92eeba2d431808fc228c2b9052411",
"sha256:19d4f96ebb3882055837f04f5da158e44d699124b4402dd10ac2294d6356f4ae",
"sha256:1d9df576bf7e06bb443bdd00164184e71320c6351b9b60a5d8aa650e143fdf90",
"sha256:1ebbd84c2d7512f7ba65df0b9cc3cbc1bbd6ef9eab39fc9389dfe7e3681f7bd2",
"sha256:22ce1ea88e925ea5bd8a8b76fe2228d5fbb0f63e52530c8d102f7130a7dc0677",
"sha256:2611177b8f0d68e5bafdc0728912ccbb53822191bce9c4161165cdd497e01815",
"sha256:3cc94bd19c7cc01e95e40bfb128cbac61a631b0b6f944832461b099994716251",
"sha256:73d0e1ae19503a6721fb9000e074376e956a52a53c6e9f2b10df2e103ff8fc7e",
"sha256:766f705deedbbe133d77f2ba200144d6faad38fb20794e9a7a72d10f6e11ae63",
"sha256:8ba60c4486973ff6fc39b8c16b2e0e891fea81ece8e555d07a6ff17d3eec1034",
"sha256:8f3d9d2ac328afa6a057a8a698436cc2d7418336fe3c645a4d64aa8d0d44b9a6",
"sha256:a79b11548d8c295902d32cf4a4a029082bc08bae6b95787d4028aac4da9e4af2",
"sha256:b86c50438ce05cd83ffe5dd151a30001e955b4731646713f1ac903a228ee7455",
"sha256:d1edcd214f4cd5a640cbef32d3310a9eb6ea1d5e323f76a3a7fa02b9dc92d9d7",
"sha256:da52ab7a61f56be2f5dc7f8a209ce07b0ae578a0362ce5ea4609722fcaa1ba46",
"sha256:e13adaf52103a42e57d1ff160b3753b30b622c3b534c0b358a6103006fdb21e5",
"sha256:e407845b70057ebd546d6c943129edd3f2af39846120a8e9e441448e7ca4869d",
"sha256:eee2a7f25e32b1307477bc3d6df416e76fc37727899e27d80191ec611a322565",
"sha256:fb81d95c504273259e52c7d7e78f7e0e4765571127058b2aef03446da3408e80"
],
"version": "==3.14.0"
"sha256:1bdd7c7c8c3ece26a251c835e73627a5f825b6ac1d16a68f190c8c29a3a4d4fe",
"sha256:1e651e49b91024e615267fe800ad094c1174800ff06a3b29652f4a656dc51228",
"sha256:2fa4eafab7cb4f900ce7739129cef0da1f25acfa89089540ce6241a15a61df78",
"sha256:346ef48e38d202634ef5f8402d3e043984ef9f504f00b2275807cc8a01ae7d31",
"sha256:446cc58ef7d8a4c5cb336d6893fd6aca1c22800207c18aa72f1496798e2aa6e8",
"sha256:523d3df8dd6a366f8911ffdd9778d2e52d174f1b2ae867c543f9f200ab06595d",
"sha256:60427da697809f2ec0a49b1c9146bf858722bd04fabef77b2b4fca0e883595ef",
"sha256:7418a069e046df6afc9024076a154d35c4df432ad081f2a1cd57661c9dc95b51",
"sha256:839ce3128375c4b6a08b4d20e67befe8fc0d6cf7ed1a1ee6117f035225368de8",
"sha256:8b7644d71f8fb11088660775da0ab09151583939edeb840a53bdf2c5acf3b725",
"sha256:95d19832c666c5942c57e67132138a9332aa84519919a97bd9bbcff1fce7cefd",
"sha256:97502022f2fe5cf78580d5b0889030a62f46080508d32a0992ee7d7d107790b8",
"sha256:a0bdc46d207edaa1db128be6f5b84e415aa033a854ed02d40256fc12d72ce0d7",
"sha256:ad332f65d9551ceffc132d0a683f4ffd12e4bc7538681100190d577ced3473fb",
"sha256:b5263de68cd0891ff4fe8ceb14e6382635ffb1be2ee0c8f7e664681679cd8163",
"sha256:bb68f637d12dccf7ddeece29b4a5f27f15b768dff7f02c198d3d7640f9b8d2dc",
"sha256:d6b38d952a3e90022287998928b12a77471b054455bcded9731dfb8371c47df8",
"sha256:d7e1191ddccabcdc5b300d4469e0fbe69abda6d5e1fba37dad66f8a9913a9994",
"sha256:e22f4bda9177893eb89a6a3cfc5335ae393690407b9b3407e86fe47c2a4adb1d",
"sha256:f00fb192452506454ce7dba6de5a0d5386631e1d6cbc8dcb7e7d4b220bb13c06",
"sha256:fae2430550b625ab2284110f4802ed1a1cae45f96871afbb014ee10f30a37fa3"
],
"version": "==3.15.0"
},
"six": {
"hashes": [
......
......@@ -41,17 +41,19 @@ required*
# Appliance Management
Once a lease is active, you can manage an appliance on it using ansible. First,
output the required extra variables for ansible.
> ./chi-lease.py ansible-extra-vars <lease_name> <stack_name> <template_file> <chameleon_key> <image> > myextravars
Then call the ansible playbooks.
> ansible-playbook -e @myextravars --connection=local stack-create.yaml
Use can then ssh onto the front node and run your experiments.
To destroy the stack, before deleting the lease:
> ansible-playbook -e @myextravars --connection=local stack-delete.yaml
Once a lease is active, you can manage an appliance on it.
> ./chi-appliance list
> ./chi-appliance create
> ./chi-appliance show
> ./chi-appliance configure
> ./chi-appliance delete
In particular, `configure` will use ansible to reconfigure the appliance nodes.
The playbook used for this configuration is available in the `ansible`
directory.
You can add tasks and make this playbook as complex as you need. Over time, the
common bits used by everyone in the project should be available here. Launching
`configure` again will reconfigure the nodes entirely, you don't have to delete
an appliance to reconfigure it.
---
- hosts: all
tasks:
- name: update etc/hosts
lineinfile:
dest: /etc/hosts
line: "{{ hostvars[item].ansible_host }} {{ hostvars[item].inventory_hostname }} {{ hostvars[item].inventory_hostname_short }}"
state: present
with_items: "{{ groups.all }}"
become: yes
# Base Template for an Argo cluster:
# - use the chameleon default image
# - install argo dependencies
# - does not install argo software itself
# If used to deploy multiple nodes, will take care of allocating a floating IP
# for a frontend node
# OpenStack Liberty release version
heat_template_version: 2015-10-15
description: Bare-metal cluster with Argo dependencies
parameters:
key_name:
type: string
label: Key name
description: Name of a key pair to enable SSH access to the instance
default: default
constraints:
- custom_constraint: nova.keypair
reservation_id:
type: string
description: ID of the Blazar reservation to use for launching instances
constraints:
- custom_constraint: blazar.reservation
node_count:
type: number
label: Node count
description: Number of physical nodes
default: 1
constraints:
- range: { min: 1 }
description: There must be at least one physical node.
image:
type: string
label: Image
description: Image to use for all instances
default: CC-CentOS7
constraints:
- custom_constraint: glance.image
resources:
# floating IP for frontend
instance_floating_ip:
type: OS::Nova::FloatingIP
properties:
pool: public
deletion_policy: Delete
instance_association:
type: OS::Nova::FloatingIPAssociation
properties:
floating_ip: { get_resource: instance_floating_ip }
server_id: { get_attr: [argo_cluster, resource.0] }
argo_cluster:
type: OS::Heat::ResourceGroup
properties:
count: { get_param: node_count }
resource_def:
type: OS::Nova::Server
properties:
name: argo-base-%index%
image: { get_param: image }
flavor: baremetal
key_name: { get_param: key_name }
networks:
- network: sharednet1
scheduler_hints: { reservation: { get_param: reservation_id } }
outputs:
first_instance_ip:
description: The public IP address of the first instance. Login with the command 'ssh cc@first_instance_ip'.
value: { get_attr: [instance_floating_ip, ip] }
#!/usr/bin/env python
import argparse
import json
import logging
import os
import subprocess
from tempfile import NamedTemporaryFile
import yaml
import os_client_config
from blazarclient import client as blazar_client
import shade
def get_cloud_config(cloud=None):
"""Retrieve the config in clouds.yaml."""
config = os_client_config.OpenStackConfig()
return config.get_one(cloud)
def get_shade_client(cloud=None):
cloud = get_cloud_config(cloud)
return shade.OpenStackCloud(cloud_config=cloud)
def get_blazar_client(cloud=None):
"""Retrieve a client to blazar based on clouds.yaml config."""
cloud_config = get_cloud_config(cloud)
session = cloud_config.get_session()
# blazar acces
return blazar_client.Client(1, session=session, service_type='reservation')
def do_create(argv):
"""Create an appliance inside a lease, based on template."""
shade_client = get_shade_client(argv.cloud)
blazar_client = get_blazar_client(argv.cloud)
# build common parameters
leases = blazar_client.lease.list()
leases = [l for l in leases if l['name'] == argv.lease]
if not leases:
print("ERROR: lease", argv.lease, "does not exists.")
return
reservation = leases[0]['reservations'][0]
extra_args = dict()
extra_args.update(argv.extra)
extra_args['reservation_id'] = reservation['id']
extra_args['node_count'] = reservation['max']
template = os.path.abspath(argv.template)
try:
ret = shade_client.create_stack(argv.name, template_file=template,
wait=True, **extra_args)
print(json.dumps(ret, indent=4))
except shade.exc.OpenStackCloudHTTPError as e:
print(e)
def do_delete(argv):
"""Delete an appliance with <name>."""
shade_client = get_shade_client(argv.cloud)
ret = shade_client.delete_stack(argv.name)
if ret:
print("Appliance successfully deleted.")
else:
print("Appliance not found.")
def do_show(argv):
"""Show appliance with <name>."""
shade_client = get_shade_client(argv.cloud)
app = shade_client.get_stack(argv.name)
if app:
print(json.dumps(app, indent=4))
else:
print("No appliance with this name:", argv.name)
def do_list(argv):
"""List all appliances."""
shade_client = get_shade_client(argv.cloud)
app_list = shade_client.list_stacks()
if app_list:
print(json.dumps(app_list, indent=4))
else:
print("No appliances.")
def do_configure(argv):
"""Copy the ansible configuration to the frontend node and launch it.
We use ansible-playbook to perform this. This function basically generates
2 inventories and a playbook:
- one inventory with how to connect to the stack frontend.
- one inventory with stack information from inside the stack
- one playbook to copy the ansible config over to the frontend and launch
ansible inside.
"""
shade_client = get_shade_client(argv.cloud)
blazar_client = get_blazar_client(argv.cloud)
# basic info
appliance = shade_client.get_stack(argv.name)
if not appliance:
print("ERROR: missing appliance.")
return
lease_list = blazar_client.lease.list()
for l in lease_list:
reservation = l['reservations'][0]
if reservation['id'] == appliance['parameters']['reservation_id']:
lease = l
break
else:
print("ERROR: Could not find lease for appliance.")
return
# local inventory creation. Only need to be able to connect to frontend
local_inventory = {'all': {'hosts': {'frontend': {'ansible_ssh_host': "",
'ansible_ssh_user': "cc"}
}}}
for out in appliance['outputs']:
if out['output_key'] == 'first_instance_ip':
public_ip = out['output_value']
break
else:
print("ERROR: missing first_instance_ip from appliance output")
return
local_inventory['all']['hosts']['frontend']['ansible_ssh_host'] = public_ip
# appliance inventory, need to grab info about hostnames and private_ip of
# all nodes
remote_inventory = {'all': {'hosts': {}}}
server_list = shade_client.list_servers({'az': 'blazar_'+lease['id']})
for s in server_list:
name = s['name']
for i in s['addresses']['sharednet1']:
if i['OS-EXT-IPS:type'] == 'fixed':
ip = i['addr']
break
else:
print("ERROR: server", name, "does not have a private IP")
return
remote_inventory['all']['hosts'][name] = {'name': name,
'ansible_host': ip}
# local playbook
mypath = os.path.abspath(os.path.dirname(__file__))
confpath = os.path.join(mypath, 'ansible')
playbook = ('---\n'
'- hosts: all\n'
' tasks:\n'
' - name: Ensure dependencies are installed\n'
' package:\n'
' name: "{{ item }}"\n'
' state: present\n'
' with_items:\n'
' - ansible\n'
' - rsync\n'
' become: yes\n'
' - name: Copy ansible configuration to the frontend\n'
' synchronize:\n'
' src: '+confpath+'\n'
' dest: ~/\n'
' - name: Generate ssh-key for appliance\n'
' user:\n'
' name: cc\n'
' state: present\n'
' generate_ssh_key: yes\n'
' - name: Execute ansible on frontend\n'
' command: ansible-playbook -i inventory main.yaml\n'
' args:\n'
' chdir: ~/ansible\n'
' register: config\n'
' - debug: var=config.stdout_lines')
# generate files
# BAD ERROR HANDLING HERE
remote_inv_path = "./ansible/inventory.yaml"
local_temp = NamedTemporaryFile(mode='w+', encoding='utf8', delete=False)
play_temp = NamedTemporaryFile(mode='w+', encoding='utf8', delete=False)
with open(remote_inv_path, "w+", encoding='utf8') as remote_inv:
yaml.dump(remote_inventory, stream=remote_inv)
with local_temp, play_temp:
yaml.dump(local_inventory, stream=local_temp)
play_temp.write(playbook)
try:
# call ansible
aargs = ["ansible-playbook", "-i", local_temp.name, play_temp.name]
proc = subprocess.Popen(aargs, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True)
while True:
err = proc.poll()
if not err:
print(proc.stdout.readline(), end='', flush=True)
else:
if err == 0:
print("Configuration successful.")
else:
for l in proc.stderr.readlines():
print(l, end='', flush=True)
print("Configuration failed.")
break
finally:
os.unlink(local_temp.name)
os.unlink(play_temp.name)
def main():
parser = argparse.ArgumentParser(description='Chameleon Appliance Helper')
parser.add_argument('--cloud', default=os.environ.get('OS_CLOUD'),
help='Cloud name')
parser.add_argument('--debug', help="Print debugging output",
action='store_true')
subparsers = parser.add_subparsers(title='Commands', dest='command')
subparsers.required = True
# create a lease
parser_create = subparsers.add_parser("create", help="Create an appliance")
parser_create.add_argument("name", help="Name of the appliance")
parser_create.add_argument("lease", help="Lease for the appliance")
parser_create.add_argument("template", help="Appliance template")
parser_create.add_argument("extra",
help="JSON dict of extra template parameters",
default=dict(), type=json.loads)
parser_create.set_defaults(func=do_create)
parser_delete = subparsers.add_parser("delete", help="Delete an appliance")
parser_delete.add_argument("name", help="Name of the appliance")
parser_delete.set_defaults(func=do_delete)
parser_show = subparsers.add_parser("show", help="Show an appliance")
parser_show.add_argument("name", help="Name of the appliance")
parser_show.set_defaults(func=do_show)
parser_list = subparsers.add_parser("list", help="List all appliances")
parser_list.set_defaults(func=do_list)
parser_config = subparsers.add_parser("configure",
help="Configure an appliance")
parser_config.add_argument("name", help="Name of the appliance")
parser_config.set_defaults(func=do_configure)
args = parser.parse_args()
if args.debug:
logger = logging.getLogger('keystoneauth')
logger.addHandler(logging.StreamHandler())
logger.setLevel(logging.DEBUG)
args.func(args)
if __name__ == '__main__':
main()
......@@ -9,7 +9,6 @@ import os
import os_client_config
from blazarclient import client as blazar_client
import keystoneauth1
import shade
def get_cloud_config(cloud=None):
......@@ -101,27 +100,6 @@ def do_list(argv):
print("No leases.")
def do_ansible_extra_vars(argv):
"""Output a JSON dict with parameters for ansible config."""
cloud = get_cloud_config(argv.cloud)
client = get_client(argv.cloud)
lease_list = client.lease.list()
leases = [l for l in lease_list if l['name'] == argv.lease]
lease = leases.pop()
if leases:
print("WARNING: more than one lease with that name, using the first.")
reservation = lease['reservations'][0]
extra_vars = {'cloud_name': cloud.name,
'stack_name': argv.stack,
'template_file': os.path.abspath(argv.template),
'key_name': argv.key,
'reservation_id': reservation['id'],
'node_count': reservation['min'],
'image': argv.image
}
print(json.dumps(extra_vars, indent=4))
def main():
parser = argparse.ArgumentParser(description='Chameleon Lease Helper')
parser.add_argument('--cloud', default=os.environ.get('OS_CLOUD'),
......@@ -154,15 +132,6 @@ def main():
parser_list = subparsers.add_parser("list", help="List all leases")
parser_list.set_defaults(func=do_list)
parser_extra = subparsers.add_parser("ansible-extra-vars",
help="generate extra-vars file")
parser_extra.add_argument("lease", help="Lease name")
parser_extra.add_argument("stack", help="Stack name")
parser_extra.add_argument("template", help="appliance template")
parser_extra.add_argument("key", help="User key name")
parser_extra.add_argument("image", help="Image name")
parser_extra.set_defaults(func=do_ansible_extra_vars)
args = parser.parse_args()
if args.debug:
logger = logging.getLogger('keystoneauth')
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment