commit e79e652654895451c5fa47a9729ed11d54472875 Author: Gabriel Adrian Samfira Date: Tue Sep 1 15:11:38 2020 +0000 Initial commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b51c5b --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Ironic API juju charm diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..9c4c5ea --- /dev/null +++ b/config.yaml @@ -0,0 +1,80 @@ +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:- + cloud:-/updates + cloud:-/staging + cloud:-/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 Ironic database access + database: + default: ironic + type: string + description: Database name for Ironic + 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. + max-tftp-block-size: + default: 0 + type: int + description: | + Force TFTP server maximum block size. Setting this option to anything + other than 0, will force the block size sent over TFTP to the value + specified here. Valid range is 512-65535. By default, clients will + negotiate the block size. + + Use this option if you're running ironic in a network with lower + MTU. The value of this option should be 32 bits less than the MTU. + If your MTU is 1450, the value for this option should be 1418. + disable-secure-erase: + default: false + type: boolean + description: | + This will disable secure erase in Ironic, when releasing a node. An ATA + Secure Erase will be attempted. If it's not supported, the disks will be + shreaded by writting random data to them once, then overwriting that data + with zeros. + + Enabling this option will preserve the data on disk after release (not + recommended for production). + + diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..a918a2e --- /dev/null +++ b/icon.svg @@ -0,0 +1,1950 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..d83b5a7 --- /dev/null +++ b/layer.yaml @@ -0,0 +1,10 @@ +includes: + - layer:openstack-principle + - interface:mysql-shared + - interface:rabbitmq + - interface:keystone-credentials +repo: https://github.com/gabriel-samfira/charm-ironic-conductor +options: + basic: + use_venv: true + include_system_packages: true diff --git a/lib/__init__.py b/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/charm/__init__.py b/lib/charm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/charm/openstack/__init__.py b/lib/charm/openstack/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/charm/openstack/ironic/__init__.py b/lib/charm/openstack/ironic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/charm/openstack/ironic/controller_utils.py b/lib/charm/openstack/ironic/controller_utils.py new file mode 100644 index 0000000..f571d13 --- /dev/null +++ b/lib/charm/openstack/ironic/controller_utils.py @@ -0,0 +1,178 @@ +import os +import shutil + +from charmhelpers.core.templating import render +import charmhelpers.core.host as ch_host +import charmhelpers.fetch as fetch +import charmhelpers.core.hookenv as hookenv + + +_IRONIC_USER = "ironic" +_IRONIC_GROUP = "ironic" + + +class PXEBootBase(object): + + TFTP_ROOT = "/tftpboot" + HTTP_ROOT = "/httpboot" + HTTP_SERVER_CONF = "/etc/nginx/nginx.conf" + GRUB_DIR = os.path.join(TFTP_ROOT, "grub") + GRUB_CFG = os.path.join(GRUB_DIR, "grub.cfg") + 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 = ["tftpd-hpa"] + TFTPD_SERVICE = "tftpd-hpa" + PACKAGES = [ + 'syslinux-common', + 'pxelinux', + 'grub-efi-amd64-signed', + 'shim-signed', + 'ipxe', + ] + HTTPD_PACKAGES = ["nginx"] + HTTPD_SERVICE_NAME = "nginx" + IRONIC_USER = _IRONIC_USER + IRONIC_GROUP = _IRONIC_GROUP + + def __init__(self, charm_config): + self._config = charm_config + + def get_restart_map(self): + return { + self.TFTP_CONFIG: [self.TFTPD_SERVICE,], + self.MAP_FILE: [self.TFTPD_SERVICE,], + self.GRUB_CFG: [self.TFTPD_SERVICE,], + } + + def determine_packages(self): + default_packages = self.PACKAGES + self.TFTP_PACKAGES + use_ipxe = self._config.get('use-ipxe', False) + hookenv.log(">>>>>>>>> USE IPXE: %r --> %r" % (use_ipxe, self._config)) + if use_ipxe: + default_packages.extend(self.HTTPD_PACKAGES) + return default_packages + + 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) + ch_host.chownr( + self.TFTP_ROOT, _IRONIC_USER, _IRONIC_GROUP, chowntopdir=True) + + 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.GRUB_DIR) is False: + os.makedirs(self.GRUB_DIR) + + ch_host.chownr( + self.TFTP_ROOT, _IRONIC_USER, _IRONIC_GROUP, chowntopdir=True) + ch_host.chownr( + self.HTTP_ROOT, _IRONIC_USER, _IRONIC_GROUP, chowntopdir=True) + + # 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='tftpd-hpa', + # target=self.TFTP_CONFIG, + # owner="root", + # perms=0o644, + # context={ + # "tftpboot": self.TFTP_ROOT, + # "max_tftp_block_size": self._config.get( + # "max_tftp_block_size", 0) + # }) + + # def _enable_ipxe_webserver(self): + # if self._config.get("use_ipxe", False) is False: + # return + # fetch.apt_install(self.HTTPD_PACKAGES, fatal=True) + # render(source='nginx.conf', + # target=self.HTTP_SERVER_CONF, + # owner="root", + # perms=0o644, + # context={ + # "ironic_user": _IRONIC_USER, + # "ipxe_http_port": self._config.get( + # "ipxe_http_port", 8080), + # "httpboot": self.HTTP_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() + # self._create_file_map() + # fetch.apt_install(self.TFTP_PACKAGES, fatal=True) + # fetch.apt_install(self.PACKAGES, fatal=True) + # self._copy_resources() + # 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 = ch_host.get_distrib_codename() + if series == "bionic": + return PXEBootBionic(charm_config) + return PXEBootBase(charm_config) diff --git a/lib/charm/openstack/ironic/ironic.py b/lib/charm/openstack/ironic/ironic.py new file mode 100644 index 0000000..dd1bc5e --- /dev/null +++ b/lib/charm/openstack/ironic/ironic.py @@ -0,0 +1,167 @@ +from __future__ import absolute_import + +import collections +import os + +import charms_openstack.charm +import charms_openstack.adapters +import charms_openstack.ip as os_ip +from charms_openstack.adapters import ( + RabbitMQRelationAdapter, + DatabaseRelationAdapter, + OpenStackRelationAdapters, +) + +import charm.openstack.ironic.controller_utils as controller_utils + +PACKAGES = [ + '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 = os.path.join(IRONIC_DIR, "ironic.conf") +ROOTWRAP_CONF = os.path.join(IRONIC_DIR, "rootwrap.conf") +FILTERS_DIR = os.path.join(IRONIC_DIR, "rootwrap.d") +IRONIC_LIB_FILTERS = os.path.join( + FILTERS_DIR, "ironic-lib.filters") +IRONIC_UTILS_FILTERS = os.path.join( + FILTERS_DIR, "ironic-utils.filters") +TFTP_CONF = "/etc/default/tftpd-hpa" +HTTP_SERVER_CONF = "/etc/nginx/nginx.conf" + +OPENSTACK_RELEASE_KEY = 'ironic-charm.openstack-release-version' + + +# select the default release function +charms_openstack.charm.use_defaults('charm.default-select-release') + + +def restart_all(): + IronicConductorCharm.singleton.restart_all() + + +def assess_status(): + IronicConductorCharm.singleton.assess_status() + + +def request_endpoint_information(keystone): + charm = IronicConductorCharm.singleton + keystone.request_credentials( + charm.name, region=charm.region) + + +def request_amqp_access(amqp): + charm = IronicConductorCharm.singleton + user, vhost = charm.get_amqp_credentials() + amqp.request_access(username=user, vhost=vhost) + + +def setup_database(database): + charm = IronicConductorCharm.singleton + for db in charm.get_database_setup(): + database.configure(**db) + + +class IronicAdapters(OpenStackRelationAdapters): + + relation_adapters = { + 'amqp': RabbitMQRelationAdapter, + 'shared_db': DatabaseRelationAdapter, + } + + +class IronicConductorCharm(charms_openstack.charm.OpenStackCharm): + + adapters_class = IronicAdapters + + abstract_class = False + release = 'train' + name = 'ironic' + packages = PACKAGES + + service_type = 'ironic' + default_service = 'ironic-conductor' + services = ['ironic-conductor', 'tftpd-hpa'] + + required_relations = [ + 'shared-db', 'amqp', 'identity-credentials'] + + restart_map = { + IRONIC_CONF: ['ironic-conductor', ], + IRONIC_UTILS_FILTERS: ['ironic-conductor', ], + IRONIC_LIB_FILTERS: ['ironic-conductor', ], + ROOTWRAP_CONF: ['ironic-conductor', ], + } + + # Package for release version detection + release_pkg = 'ironic-common' + + # Package codename map for ironic-common + package_codenames = { + 'ironic-common': collections.OrderedDict([ + ('13', 'train'), + ('15', 'ussuri'), + ]), + } + + group = "ironic" + + def __init__(self, **kw): + super().__init__(**kw) + self.pxe_config = controller_utils.get_pxe_config_class( + self.config) + self.packages.extend(self.pxe_config.determine_packages()) + self._configure_ipxe_webserver() + self.config["tftpboot"] = self.pxe_config.TFTP_ROOT + self.config["httpboot"] = self.pxe_config.HTTP_ROOT + self.config["ironic_user"] = self.pxe_config.IRONIC_USER + self.config["ironic_group"] = self.pxe_config.IRONIC_GROUP + self.restart_map.update(self.pxe_config.get_restart_map()) + + def _configure_ipxe_webserver(self): + if self.config.get('use-ipxe', False) is False: + self.purge_packages.extend(self.pxe_config.HTTPD_PACKAGES) + return + httpd_svc_name = self.pxe_config.HTTPD_SERVICE_NAME + self.services.append(httpd_svc_name) + self.restart_map[self.pxe_config.HTTP_SERVER_CONF] = [httpd_svc_name,] + + def upgrade_charm(self): + self.install() + super().upgrade_charm() + self.assess_status() + + def config_changed(self): + self.install() + self.pxe_config._copy_resources() + super().config_changed() + self.assess_status() + + def install(self): + self.configure_source() + super().install() + self.assess_status() + + 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'], ) + ] \ No newline at end of file diff --git a/metadata.yaml b/metadata.yaml new file mode 100644 index 0000000..9e4c1f0 --- /dev/null +++ b/metadata.yaml @@ -0,0 +1,30 @@ +name: ironic-conductor +summary: Openstack bare metal conductor +maintainer: Gabriel Adrian Samfira +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: + cleaning: + internal: +subordinate: false +display-name: Ironic Conductor +requires: + shared-db: + interface: mysql-shared + amqp: + interface: rabbitmq + identity-credentials: + interface: keystone-credentials \ No newline at end of file diff --git a/reactive/__init__.py b/reactive/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/reactive/ironic_handlers.py b/reactive/ironic_handlers.py new file mode 100644 index 0000000..e6684d8 --- /dev/null +++ b/reactive/ironic_handlers.py @@ -0,0 +1,58 @@ +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', + 'upgrade-charm', + 'config.changed', + 'update-status') + + +@reactive.when('shared-db.available') +@reactive.when('identity-credentials.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-credentials.connected') +def setup_endpoint(keystone): + ironic.request_endpoint_information(keystone) + ironic.assess_status() + +@reactive.when('amqp.connected') +def request_amqp_access(amqp): + ironic.request_amqp_access(amqp) + ironic.assess_status() + + +@reactive.when('shared-db.connected') +def setup_database(database): + ironic.setup_database(database) + ironic.assess_status() + + +@adapters.config_property +def deployment_interface_ip(args): + return ch_ip.get_relation_ip("deployment") + + +@adapters.config_property +def internal_interface_ip(args): + return ch_ip.get_relation_ip("internal") diff --git a/templates/etc_default_tftpd-hpa b/templates/etc_default_tftpd-hpa new file mode 100644 index 0000000..8f214b9 --- /dev/null +++ b/templates/etc_default_tftpd-hpa @@ -0,0 +1,9 @@ +TFTP_USERNAME="root" +TFTP_DIRECTORY="{{ options.tftpboot }}" +TFTP_ADDRESS=":69" + +{% if options.max_tftp_block_size != 0 %} +TFTP_OPTIONS="-4 -v -v -v -v -v --map-file {{options.tftpboot}}/map-file --blocksize {{ options.max_tftp_block_size }}" +{% else %} +TFTP_OPTIONS="-4 -v -v -v -v -v --map-file {{options.tftpboot}}/map-file" +{% endif %} \ No newline at end of file diff --git a/templates/etc_ironic_rootwrap.d_ironic-lib.filters b/templates/etc_ironic_rootwrap.d_ironic-lib.filters new file mode 100644 index 0000000..15018b5 --- /dev/null +++ b/templates/etc_ironic_rootwrap.d_ironic-lib.filters @@ -0,0 +1,27 @@ +# An ironic-lib.filters to be used with rootwrap command. +# The following commands should be used in filters for disk manipulation. +# This file should be owned by (and only-writeable by) the root user. + +# NOTE: +# if you update this file, you will also need to adjust the +# ironic-lib.filters from the ironic module. + +[Filters] +# ironic_lib/disk_utils.py +blkid: CommandFilter, blkid, root +blockdev: CommandFilter, blockdev, root +hexdump: CommandFilter, hexdump, root +lsblk: CommandFilter, lsblk, root +qemu-img: CommandFilter, qemu-img, root +wipefs: CommandFilter, wipefs, root +sgdisk: CommandFilter, sgdisk, root +partprobe: CommandFilter, partprobe, root + +# ironic_lib/utils.py +mkswap: CommandFilter, mkswap, root +mkfs: CommandFilter, mkfs, root +dd: CommandFilter, dd, root + +# ironic_lib/disk_partitioner.py +fuser: CommandFilter, fuser, root +parted: CommandFilter, parted, root \ No newline at end of file diff --git a/templates/etc_ironic_rootwrap.d_ironic-utils.filters b/templates/etc_ironic_rootwrap.d_ironic-utils.filters new file mode 100644 index 0000000..48c1bfa --- /dev/null +++ b/templates/etc_ironic_rootwrap.d_ironic-utils.filters @@ -0,0 +1,10 @@ +# ironic-rootwrap command filters for disk manipulation +# This file should be owned by (and only-writable by) the root user + +[Filters] +# ironic/drivers/modules/deploy_utils.py +iscsiadm: CommandFilter, iscsiadm, root + +# ironic/common/utils.py +mount: CommandFilter, mount, root +umount: CommandFilter, umount, root \ No newline at end of file diff --git a/templates/etc_nginx_nginx.conf b/templates/etc_nginx_nginx.conf new file mode 100644 index 0000000..b13fee7 --- /dev/null +++ b/templates/etc_nginx_nginx.conf @@ -0,0 +1,40 @@ +user {{options.ironic_user}}; +worker_processes auto; +pid /run/nginx.pid; +include /etc/nginx/modules-enabled/*.conf; + +events { + worker_connections 768; + # multi_accept on; +} + +http { + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE + ssl_prefer_server_ciphers on; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + gzip off; + server { + listen {{options.ipxe_http_port}} default_server; + listen [::]:{{options.ipxe_http_port}} default_server; + + root {{options.httpboot}}; + index index.html; + server_name _; + location / { + try_files $uri $uri/ =404; + } + } + +} diff --git a/templates/parts/keystone-authtoken b/templates/parts/keystone-authtoken new file mode 100644 index 0000000..18d9f87 --- /dev/null +++ b/templates/parts/keystone-authtoken @@ -0,0 +1,36 @@ +{% if identity_credentials.auth_host -%} +{% if identity_credentials.api_version and identity_credentials.api_version == "3" %} +{% set auth_ver = "v3" %} +{% else %} +{% set auth_ver = "v2.0" %} +{% endif %} + +[keystone_authtoken] +auth_version = {{auth_ver}} +www_authenticate_uri = {{ identity_credentials.auth_protocol }}://{{ identity_credentials.auth_host }}:{{ identity_credentials.credentials_port }}/{{auth_ver}} +auth_url = {{ identity_credentials.auth_protocol }}://{{ identity_credentials.auth_host }}:{{ identity_credentials.credentials_port }} +auth_type = password + +{% if identity_credentials.credentials_project_domain_name -%} +project_domain_name = {{ identity_credentials.credentials_project_domain_name }} +user_domain_name = {{ identity_credentials.credentials_user_domain_name }} +{% else %} +project_domain_name = default +user_domain_name = default +{% endif -%} + +username = {{ identity_credentials.credentials_username }} +password = {{ identity_credentials.credentials_password }} +project_name = {{identity_credentials.credentials_project}} + +admin_user = {{ identity_credentials.credentials_username }} +admin_password = {{ identity_credentials.credentials_password }} +admin_tenant_name = {{identity_credentials.credentials_project}} + +{% if identity_credentials.signing_dir -%} +signing_dir = {{ identity_credentials.signing_dir }} +{% endif -%} +{% if options.use_memcache == true -%} +memcached_servers = {{ options.memcache_url }} +{% endif -%} +{% endif -%} diff --git a/templates/parts/section-deploy b/templates/parts/section-deploy new file mode 100644 index 0000000..0feaf26 --- /dev/null +++ b/templates/parts/section-deploy @@ -0,0 +1,14 @@ +[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 -%} + +{% if options.disable_secure_erase %} +enable_ata_secure_erase = false +shred_random_overwrite_iterations = 0 +shred_final_overwrite_with_zeros = false +{% endif %} \ No newline at end of file diff --git a/templates/parts/section-pxe b/templates/parts/section-pxe new file mode 100644 index 0000000..a0f9031 --- /dev/null +++ b/templates/parts/section-pxe @@ -0,0 +1,30 @@ +[pxe] +# Ironic compute node's tftp root path. (string value) +{% if not tftpboot -%} +{% set tftpboot = "/tftpboot" -%} +{% endif -%} +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 -%} diff --git a/templates/parts/service-auth b/templates/parts/service-auth new file mode 100644 index 0000000..7fe70dc --- /dev/null +++ b/templates/parts/service-auth @@ -0,0 +1,31 @@ +# Authentication type to load (string value) +auth_type = password + +# Authentication URL (string value) +auth_url = {{ identity_credentials.auth_protocol }}://{{ identity_credentials.auth_host }}:{{ identity_credentials.credentials_port }} + +# Username (string value) +username = {{ identity_credentials.credentials_username }} + +# User's password (string value) +password = {{ identity_credentials.credentials_password }} + +# Project name to scope to (string value) +project_name = {{identity_credentials.credentials_project}} + + +{% if identity_credentials.service_domain -%} +project_domain_name = {{ identity_credentials.credentials_project_domain_name }} +user_domain_name = {{ identity_credentials.credentials_user_domain_name }} +{% 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 }} \ No newline at end of file diff --git a/templates/tftpboot_grub_grub.cfg b/templates/tftpboot_grub_grub.cfg new file mode 100644 index 0000000..a9f7ad1 --- /dev/null +++ b/templates/tftpboot_grub_grub.cfg @@ -0,0 +1,7 @@ +set default=master +set timeout=5 +set hidden_timeout_quiet=false + +menuentry "master" { +configfile {{options.tftpboot}}/$net_default_ip.conf +} \ No newline at end of file diff --git a/templates/tftpboot_map-file b/templates/tftpboot_map-file new file mode 100644 index 0000000..e707a5b --- /dev/null +++ b/templates/tftpboot_map-file @@ -0,0 +1,4 @@ +re ^({{options.tftpboot}}/) {{options.tftpboot}}/\2 +re ^{{options.tftpboot}}/ {{options.tftpboot}}/ +re ^(^/) {{options.tftpboot}}/\1 +re ^([^/]) {{options.tftpboot}}/\1 \ No newline at end of file diff --git a/templates/train/ironic.conf b/templates/train/ironic.conf new file mode 100644 index 0000000..f062574 --- /dev/null +++ b/templates/train/ironic.conf @@ -0,0 +1,56 @@ +[DEFAULT] +debug = {{ options.debug }} +verbose = {{ options.verbose }} +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,noop +enabled_inspect_interfaces = idrac,ilo,irmc,redfish,no-inspect +enabled_network_interfaces = flat,neutron,noop +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 = flat + +transport_url = {{ amqp.transport_url }} + +{% include "parts/keystone-authtoken" %} + +[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" %} diff --git a/templates/train/rootwrap.conf b/templates/train/rootwrap.conf new file mode 100644 index 0000000..6e3e2f9 --- /dev/null +++ b/templates/train/rootwrap.conf @@ -0,0 +1,27 @@ +# Configuration for ironic-rootwrap +# This file should be owned by (and only writable by) the root user + +[DEFAULT] +# List of directories to load filter definitions from (separated by ','). +# These directories MUST all be only writable by root ! +filters_path=/etc/ironic/rootwrap.d + +# List of directories to search executables in, in case filters do not +# explicitly specify a full path (separated by ',') +# If not specified, defaults to system PATH environment variable. +# These directories MUST all be only writable by root ! +exec_dirs=/sbin,/usr/sbin,/bin,/usr/bin + +# Enable logging to syslog +# Default value is False +use_syslog=False + +# Which syslog facility to use. +# Valid values include auth, authpriv, syslog, user0, user1... +# Default value is 'syslog' +syslog_log_facility=syslog + +# Which messages to log. +# INFO means log all usage +# ERROR means only log unsuccessful attempts +syslog_log_level=ERROR \ No newline at end of file diff --git a/tests/00-setup b/tests/00-setup new file mode 100755 index 0000000..f0616a5 --- /dev/null +++ b/tests/00-setup @@ -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 diff --git a/tests/10-deploy b/tests/10-deploy new file mode 100755 index 0000000..c7cc7b5 --- /dev/null +++ b/tests/10-deploy @@ -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()