Add initial charm code

This commit is contained in:
Gabriel Adrian Samfira 2020-08-29 00:25:57 +00:00
parent 6e0c703cde
commit 82102ccf3e
22 changed files with 2618 additions and 0 deletions

62
config.yaml Normal file
View File

@ -0,0 +1,62 @@
options:
openstack-origin:
default: distro
type: string
description: |
Repository from which to install. May be one of the following:
distro (default), ppa:somecustom/ppa, a deb url sources entry,
or a supported Ubuntu Cloud Archive e.g.
.
cloud:<series>-<openstack-release>
cloud:<series>-<openstack-release>/updates
cloud:<series>-<openstack-release>/staging
cloud:<series>-<openstack-release>/proposed
.
See https://wiki.ubuntu.com/OpenStack/CloudArchive for info on which
cloud archives are available and supported.
rabbit-user:
default: ironic
type: string
description: Username used to access rabbitmq queue
rabbit-vhost:
default: openstack
type: string
description: Rabbitmq vhost
database-user:
default: ironic
type: string
description: Username for Magnum database access
database:
default: ironic
type: string
description: Database name for Magnum
debug:
default: False
type: boolean
description: Enable debug logging
verbose:
default: False
type: boolean
description: Enable verbose logging
region:
default: RegionOne
type: string
description: OpenStack Region
use-ipxe:
default: false
type: boolean
description: |
Use iPXE instead of PXE. This option will install an aditional
HTTP server with a root in /httpboot.
ipxe-http-port:
default: "8080"
type: string
description: |
The port used for the HTTP server used to serve iPXE resources.
api-port:
default: "6385"
type: string
description: |
The API port ironic-api will listen on.

1950
icon.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 81 KiB

11
layer.yaml Normal file
View File

@ -0,0 +1,11 @@
includes:
- layer:openstack
- layer:openstack-api
- interface:mysql-shared
- interface:rabbitmq
- interface:keystone
repo: https://github.com/gabriel-samfira/charm-ironic-api
options:
basic:
use_venv: true
include_system_packages: true

0
lib/__init__.py Normal file
View File

0
lib/charm/__init__.py Normal file
View File

View File

View File

View File

@ -0,0 +1,148 @@
import os
import shutil
from charmhelpers.core.templating import render
from charmhelpers.core.host import get_distrib_codename
import charmhelpers.fetch as fetch
_IRONIC_USER = "ironic"
_IRONIC_GROUP = "ironic"
class PXEBootBase(object):
TFTP_ROOT = "/tftproot"
HTTP_ROOT = "/httproot"
IPXE_BOOT = os.path.join(HTTP_ROOT, "boot.ipxe")
GRUB_DIR = os.path.join(TFTP_ROOT, "grub")
MAP_FILE = os.path.join(TFTP_ROOT, "map-file")
TFTP_CONFIG = "/etc/default/tftpd-hpa"
# This is a file map of source to destination. The destination is
# relative to self.TFTP_ROOT
FILE_MAP = {
"/usr/lib/PXELINUX/pxelinux.0": "pxelinux.0",
"/usr/lib/syslinux/modules/bios/chain.c32": "chain.c32",
"/usr/lib/syslinux/modules/bios/ldlinux.c32": "ldlinux.c32",
"/usr/lib/grub/x86_64-efi-signed/grubnetx64.efi.signed": "grubx64.efi",
"/usr/lib/shim/shim.efi.signed": "bootx64.efi",
"/usr/lib/ipxe/undionly.kpxe": "undionly.kpxe",
"/usr/lib/ipxe/ipxe.efi": "ipxe.efi",
}
TFTP_PACKAGES = ["tftp-hpa"]
PACKAGES = [
'syslinux-common',
'pxelinux',
'grub-efi-amd64-signed',
'shim-signed',
'ipxe',
]
def __init__(self, charm_config):
self._config = charm_config
def _copy_resources(self):
self._ensure_folders()
for f in self.FILE_MAP:
if os.path.isfile(f) is False:
raise ValueError(
"Missing required file %s. Package not installes?" % f)
shutil.copy(
f, os.path.join(self.TFTP_ROOT, self.FILE_MAP[f]),
follow_symlinks=True)
self._recursive_chown(
self.TFTP_ROOT, user=_IRONIC_USER, group=_IRONIC_GROUP)
def _recursive_chown(self, path, user=None, group=None):
for root, _, files in os.walk(path):
shutil.chown(root, user=user, group=group)
for f in files:
shutil.chown(
os.path.join(root, f), user=user, group=group)
def _ensure_folders(self):
if os.path.isdir(self.TFTP_ROOT) is False:
os.makedirs(self.TFTP_ROOT)
if os.path.isdir(self.HTTP_ROOT) is False:
os.makedirs(self.HTTP_ROOT)
if os.path.isdir(self.IPXE_BOOT) is False:
os.makedirs(self.IPXE_BOOT)
if os.path.isdir(self.GRUB_DIR) is False:
os.makedirs(self.GRUB_DIR)
self._recursive_chown(
self.TFTP_ROOT, user=_IRONIC_USER, group=_IRONIC_GROUP)
self._recursive_chown(
self.HTTP_ROOT, user=_IRONIC_USER, group=_IRONIC_GROUP)
def _create_file_map(self):
self._ensure_folders()
render(source='tftp-file-map',
target=self.MAP_FILE,
owner=_IRONIC_USER,
perms=0o664,
context={})
def _create_grub_cfg(self):
self._ensure_folders()
render(source='grub-efi',
target=os.path.join(self.GRUB_DIR, "grub.cfg"),
owner="root",
perms=0o644,
context={
"tftpboot": self.TFTP_ROOT,
})
def _create_tftp_config(self):
cfg_dir = os.path.dirname(self.TFTP_CONFIG)
if os.path.isdir(cfg_dir) is False:
raise Exception("Could not find %s" % cfg_dir)
render(source='tftp-hpa',
target=self.TFTP_CONFIG,
owner="root",
perms=0o644,
context={
"tftpboot": self.TFTP_ROOT,
})
def configure_resources(self):
# On Ubuntu 20.04, if IPv6 is not available on the system,
# the tftp-hpa package fails to install properly. We create the
# config beforehand, forcing IPv4.
self._create_tftp_config()
fetch.apt_install(self.TFTP_PACKAGES, fatal=True)
fetch.apt_install(self.PACKAGES, fatal=True)
self._copy_resources()
self._create_file_map()
self._create_grub_cfg()
class PXEBootBionic(PXEBootBase):
# This is a file map of source to destination. The destination is
# relative to self.TFTP_ROOT
FILE_MAP = {
"/usr/lib/PXELINUX/pxelinux.0": "pxelinux.0",
"/usr/lib/syslinux/modules/bios/chain.c32": "chain.c32",
"/usr/lib/syslinux/modules/bios/ldlinux.c32": "ldlinux.c32",
"/usr/lib/grub/x86_64-efi-signed/grubnetx64.efi.signed": "grubx64.efi",
"/usr/lib/shim/shimx64.efi.signed": "bootx64.efi",
"/usr/lib/ipxe/undionly.kpxe": "undionly.kpxe",
"/usr/lib/ipxe/ipxe.efi": "ipxe.efi",
}
def get_pxe_config_class(charm_config):
# In the future, we may need to make slight adjustments to package
# names and/or configuration files, based on the version of Ubuntu
# we are installing on. This function serves as a factory which will
# return an instance of the proper class to the charm. For now we only
# have one class.
series = get_distrib_codename()
if series == "bionic":
return PXEBootBionic(charm_config)
return PXEBootBase(charm_config)

View File

@ -0,0 +1,137 @@
# Copyright 2020 Cloudbase Solutions
from __future__ import absolute_import
import collections
import charms_openstack.charm
import charms_openstack.adapters
import charms_openstack.ip as os_ip
import charm.openstack.ironic.controller_utils as controller_utils
PACKAGES = [
'ironic-api',
'ironic-conductor',
'python-mysqldb',
'python3-dracclient',
'python3-sushy',
'python3-ironicclient',
'python3-scciclient',
'shellinabox',
'openssl',
'socat',
'open-iscsi',
'qemu-utils',
'ipmitool']
IRONIC_DIR = "/etc/ironic/"
IRONIC_CONF = IRONIC_DIR + "ironic.conf"
TFTP_CONF = "/etc/default/tftpd-hpa"
OPENSTACK_RELEASE_KEY = 'ironic-charm.openstack-release-version'
# select the default release function
charms_openstack.charm.use_defaults('charm.default-select-release')
def db_sync_done():
return IronicAPICharm.singleton.db_sync_done()
def restart_all():
IronicAPICharm.singleton.restart_all()
def db_sync():
IronicAPICharm.singleton.db_sync()
def configure_ha_resources(hacluster):
IronicAPICharm.singleton.configure_ha_resources(hacluster)
def assess_status():
IronicAPICharm.singleton.assess_status()
def setup_endpoint(keystone):
charm = IronicAPICharm.singleton
public_ep = '{}/v1'.format(charm.public_url)
internal_ep = '{}/v1'.format(charm.internal_url)
admin_ep = '{}/v1'.format(charm.admin_url)
keystone.register_endpoints(charm.service_type,
charm.region,
public_ep,
internal_ep,
admin_ep)
class IronicAPICharm(charms_openstack.charm.HAOpenStackCharm):
abstract_class = False
release = 'train'
name = 'ironic'
packages = PACKAGES
api_ports = {
'ironic-api': {
os_ip.PUBLIC: 6385,
os_ip.ADMIN: 6385,
os_ip.INTERNAL: 6385,
}
}
service_type = 'ironic'
default_service = 'ironic-api'
services = ['ironic-api', 'ironic-conductor']
sync_cmd = ['ironic-dbsync', 'upgrade']
required_relations = [
'shared-db', 'amqp', 'identity-service']
restart_map = {
IRONIC_CONF: services,
TFTP_CONF: ["tftp-hpa"]
}
ha_resources = ['vips', 'haproxy']
# Package for release version detection
release_pkg = 'ironic-common'
# Package codename map for ironic-common
package_codenames = {
'ironic-common': collections.OrderedDict([
('14', 'train'),
('15', 'ussuri'),
]),
}
group = "ironic"
def __init__(self, **kw):
super().__init__(**kw)
self.pxe_config = controller_utils.get_pxe_config_class(
self.config)
def get_amqp_credentials(self):
"""Provide the default amqp username and vhost as a tuple.
:returns (username, host): two strings to send to the amqp provider.
"""
return (self.config['rabbit-user'], self.config['rabbit-vhost'])
def get_database_setup(self):
return [
dict(
database=self.config['database'],
username=self.config['database-user'], )
]
def install(self):
super().install()
self.pxe_config.configure_resources()
# def configue_tls(self, certificates_instance=None):
# # TODO(gsamfira): add tls support
# pass

27
metadata.yaml Normal file
View File

@ -0,0 +1,27 @@
name: ironic-api
summary: Openstack bare metal component
maintainer: Gabriel Adrian Samfira <gsamfira@cloudbasesolutions.com>
description: |
OpenStack bare metal provisioning a.k.a Ironic is an integrated OpenStack
program which aims to provision bare metal machines instead of virtual
machines, forked from the Nova baremetal driver. It is best thought of
as a bare metal hypervisor API and a set of plugins which interact with
the bare metal hypervisors. By default, it will use PXE and IPMI in order
to provision and turn on/off machines, but Ironic also supports
vendor-specific plugins which may implement additional functionality.
tags:
- openstack
- baremetal
series:
- bionic
- focal
extra-bindings:
deployment:
subordinate: false
requires:
shared-db:
interface: mysql-shared
amqp:
interface: rabbitmq
identity-service:
interface: keystone

0
reactive/__init__.py Normal file
View File

View File

@ -0,0 +1,64 @@
# Copyright 2018 Cloudbase Solutions
from __future__ import absolute_import
import charms.reactive as reactive
import charmhelpers.core.hookenv as hookenv
import charms_openstack.charm as charm
import charm.openstack.ironic.ironic as ironic # noqa
from charmhelpers.core.templating import render
import charmhelpers.contrib.network.ip as ch_ip
import charms_openstack.adapters as adapters
# Use the charms.openstack defaults for common states and hooks
charm.use_defaults(
'charm.installed',
'amqp.connected',
'shared-db.connected',
'identity-service.available', # enables SSL support
'config.changed',
'update-status')
@reactive.when('shared-db.available')
@reactive.when('identity-service.available')
@reactive.when('amqp.available')
def render_stuff(*args):
hookenv.log("about to call the render_configs with {}".format(args))
with charm.provide_charm_instance() as ironic_charm:
ironic_charm.render_with_interfaces(
charm.optional_interfaces(args))
ironic_charm.assess_status()
reactive.set_state('config.complete')
@reactive.when('identity-service.connected')
def setup_endpoint(keystone):
ironic.setup_endpoint(keystone)
ironic.assess_status()
@reactive.when('config.complete')
@reactive.when_not('db.synced')
def run_db_migration():
ironic.db_sync()
ironic.restart_all()
reactive.set_state('db.synced')
ironic.assess_status()
@reactive.when('ha.connected')
def cluster_connected(hacluster):
ironic.configure_ha_resources(hacluster)
@adapters.config_property
def deployment_interface_ip(self):
return ch_ip.get_relation_ip("deployment")
@adapters.config_property
def internal_interface_ip(self):
return ch_ip.get_relation_ip("internal")

View File

@ -0,0 +1,7 @@
{% if not tftpboot -%}
{% set tftpboot = "/tftpboot" -%}
{% endif -%}
TFTP_USERNAME="root"
TFTP_DIRECTORY="{{ tftpboot }}"
TFTP_ADDRESS=":69"
TFTP_OPTIONS="--secure -4 -v -v -v -v -v --map-file {{tftpboot}}/map-file"

View File

@ -0,0 +1,35 @@
{% if identity_service.auth_host -%}
{% if identity_service.api_version and identity_service.api_version == "3" %}
{% set auth_ver = "v3" %}
{% else %}
{% set auth_ver = "v2.0" %}
{% endif %}
[keystone_authtoken]
auth_version = {{auth_ver}}
www_authenticate_uri = {{ identity_service.service_protocol }}://{{ identity_service.service_host }}:{{ identity_service.service_port }}/{{auth_ver}}
auth_url = {{ identity_service.auth_protocol }}://{{ identity_service.auth_host }}:{{ identity_service.auth_port }}
auth_type = password
{% if identity_service.service_domain -%}
project_domain_name = {{ identity_service.service_domain }}
user_domain_name = {{ identity_service.service_domain }}
{% else %}
project_domain_name = default
user_domain_name = default
{% endif -%}
username = {{ identity_service.service_username }}
password = {{ identity_service.service_password }}
project_name = {{identity_service.service_tenant}}
admin_user = {{ identity_service.service_username }}
admin_password = {{ identity_service.service_password }}
admin_tenant_name = {{identity_service.service_tenant}}
{% if identity_service.signing_dir -%}
signing_dir = {{ identity_service.signing_dir }}
{% endif -%}
{% if options.use_memcache == true -%}
memcached_servers = {{ options.memcache_url }}
{% endif -%}
{% endif -%}

View File

@ -0,0 +1,8 @@
[deploy]
{% if options.use_ipxe -%}
# Ironic compute node's http root path. (string value)
http_root=/httpboot
# Ironic compute node's HTTP server URL (string value)
http_url=http://{{ options.deployment_interface_ip }}:{{ options.ipxe_http_port}}
{% endif -%}

View File

@ -0,0 +1,27 @@
[pxe]
# Ironic compute node's tftp root path. (string value)
tftp_root=/tftpboot
# IP address of Ironic compute node's tftp server. (string
# value)
tftp_server = {{ options.deployment_interface_ip }}
pxe_append_params = nofb nomodeset vga=normal console=tty0 console=ttyS0,115200n8
{% if options.use_ipxe -%}
# Enable iPXE boot. (boolean value)
ipxe_enabled=True
# Neutron bootfile DHCP parameter. (string value)
pxe_bootfile_name=undionly.kpxe
# Bootfile DHCP parameter for UEFI boot mode. (string value)
uefi_pxe_bootfile_name=ipxe.efi
# Template file for PXE configuration. (string value)
pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
# Template file for PXE configuration for UEFI boot loader.
# (string value)
uefi_pxe_config_template=$pybasedir/drivers/modules/ipxe_config.template
{% endif -%}

View File

@ -0,0 +1,31 @@
# Authentication type to load (string value)
auth_type = password
# Authentication URL (string value)
auth_url = {{ identity_service.auth_protocol }}://{{ identity_service.auth_host }}:{{ identity_service.auth_port }}
# Username (string value)
username = {{ identity_service.service_username }}
# User's password (string value)
password = {{ identity_service.service_password }}
# Project name to scope to (string value)
project_name = {{identity_service.service_tenant}}
{% if identity_service.service_domain -%}
project_domain_name = {{ identity_service.service_domain }}
user_domain_name = {{ identity_service.service_domain }}
{% else -%}
project_domain_name = default
user_domain_name = default
{% endif -%}
{% if options.ca_cert_path -%}
# PEM encoded Certificate Authority to use when verifying
# HTTPs connections. (string value)
cafile = {{ options.ca_cert_path }}
{% endif -%}
region_name = {{ options.region }}

View File

@ -0,0 +1,10 @@
{% if not tftpboot -%}
{% set tftpboot = "/tftpboot" -%}
{% endif -%}
set default=master
set timeout=5
set hidden_timeout_quiet=false
menuentry "master" {
configfile {{tftpboot}}/$net_default_ip.conf
}

View File

@ -0,0 +1,4 @@
re ^(/tftpboot/) /tftpboot/\2
re ^/tftpboot/ /tftpboot/
re ^(^/) /tftpboot/\1
re ^([^/]) /tftpboot/\1

View File

@ -0,0 +1,57 @@
[DEFAULT]
auth_strategy=keystone
my_ip = {{ options.internal_interface_ip }}
enabled_deploy_interfaces = iscsi
enabled_hardware_types = ipmi,ilo,idrac,redfish,irmc
{% if options.use_ipxe -%}
enabled_boot_interfaces = pxe,ipxe,ilo-pxe,ilo-ipxe,irmc-pxe
{% else -%}
enabled_boot_interfaces = pxe,ilo-pxe,irmc-pxe
{% endif -%}
enabled_management_interfaces = ipmitool,redfish,ilo,irmc,idrac
enabled_inspect_interfaces = idrac,ilo,irmc,redfish,no-inspect
enabled_network_interfaces = flat,neutron
enabled_power_interfaces = ipmitool,redfish,ilo,irmc,idrac
enabled_storage_interfaces = cinder,noop
enabled_console_interfaces = ipmitool-socat,ipmitool-shellinabox,no-console
enabled_raid_interfaces = agent,idrac,irmc,no-raid
enabled_vendor_interfaces = ipmitool,idrac,ilo,no-vendor
default_deploy_interface = iscsi
default_network_interface = neutron
transport_url = {{ amqp.transport_url }}
{% include "parts/keystone-authtoken" %}
[api]
port = {{ options.service_listen_info.ironic_api.port }}
[database]
{% include "parts/database" %}
[neutron]
{% include "parts/service-auth" %}
# {% if options.cleaning_network %}
# cleaning_network = {{ options.cleaning_network }}
# {% endif %}
# {% if options.provisioning_network %}
# provisioning_network = {{ options.provisioning_network }}
# {% endif %}
cleaning_network = 512147a6-37ed-4ab4-ac4c-c55bb845de8e
provisioning_network = 512147a6-37ed-4ab4-ac4c-c55bb845de8e
[glance]
{% include "parts/service-auth" %}
[cinder]
{% include "parts/service-auth" %}
[service_catalog]
{% include "parts/service-auth" %}
{% include "parts/section-pxe" %}
{% include "parts/section-deploy" %}

5
tests/00-setup Executable file
View File

@ -0,0 +1,5 @@
#!/bin/bash
sudo add-apt-repository ppa:juju/stable -y
sudo apt-get update
sudo apt-get install amulet python-requests -y

35
tests/10-deploy Executable file
View File

@ -0,0 +1,35 @@
#!/usr/bin/python3
import amulet
import requests
import unittest
class TestCharm(unittest.TestCase):
def setUp(self):
self.d = amulet.Deployment()
self.d.add('charm-ironic')
self.d.expose('charm-ironic')
self.d.setup(timeout=900)
self.d.sentry.wait()
self.unit = self.d.sentry['charm-ironic'][0]
def test_service(self):
# test we can access over http
page = requests.get('http://{}'.format(self.unit.info['public-address']))
self.assertEqual(page.status_code, 200)
# Now you can use self.d.sentry[SERVICE][UNIT] to address each of the units and perform
# more in-depth steps. Each self.d.sentry[SERVICE][UNIT] has the following methods:
# - .info - An array of the information of that unit from Juju
# - .file(PATH) - Get the details of a file on that unit
# - .file_contents(PATH) - Get plain text output of PATH file from that unit
# - .directory(PATH) - Get details of directory
# - .directory_contents(PATH) - List files and folders in PATH on that unit
# - .relation(relation, service:rel) - Get relation data from return service
if __name__ == '__main__':
unittest.main()